Recently, I found that the Arc Overhangs algorithm I had been working with was amazing, but also buggy. After some work trying to write an algorithm to automatically parse in arc overhangs to an existing GCODE file, I got frustrated with Python and decided this was not the optimal way to proceed. I had remembered HackaDay had previously featured a Go library for programmatic modeling similar to OpenSCAD called SDFX. I thought that would be a good starting point for a full-blown slicer, which, it probably was.

Theory:

The theory of a slicer on paper is fairly simple:

  • A 3D model is imported.
  • This model is chopped up along the Z axis by a plane.
  • That creates a 2D shape which is used to generate a toolpath.
  • This is repeated for every layer until a full GCODE file is created.

There is of course a little more to it then that, but I would be very happy if that’s all that I got.

First steps:

The fist part is, of course, importing the model, luckily SDFX has an STL importer, so to get a file into the Go program, all that is needed is the following:

import (
    "log"
    "os"

    "github.com/deadsy/sdfx/obj"
)
func main() {
    // A random file to open, in my case, a LEGO compatible 2x4 brick.
    file, err := os.OpenFile("/home/fedx/Downloads/2x4.stl", os.O_RDONLY, 0400)
    if err != nil {
        log.Fatalf("ERROR", err)
        return
    }

    // create the SDF from the mesh
    object, err := obj.ImportSTL(file, 20, 3, 5)
}

That’s simple enough, now all we need is to extract the vertex data, right? Wrong!

What the heck is an SDF?

When I first started using this library, I did not read all any of the documentation, and did not realize this SDFX is not encoding objects as a point-cloud as I am used to. Instead it encodes objects as a signed distance function, a 4D graph intersected by a 3D bounding box. This comes with a lot of advantages, but in my case, required modifying the library to fit my needs. In the future, I will need to re-write this so it is less of a hack, but for now, I am just modifying the source in the vendor directory like so:

vendor/pkg/mod/github.com/deadsy/sdfx@v0.0.0-20221027125250-c456ed660b0c/render/march2.go

func (r *MarchingSquaresUniform) ToArray(s sdf.SDF2) []*Line{
	bbSize := s.BoundingBox().Size()
	resolution := bbSize.MaxComponent() / float64(r.meshCells)
	return(marchingSquares(s, resolution))
}

This is simply using the Marching Squares algorithm to find where the lines exist in an 2 dimensional SDF (An intersection between a plane and a 3D plot) and returning an array of those lines start and end points. In many ways, this will be more useful then actually using a vertex array and interpolating the lines as the lines are already interpolated.

A visualization function:

At this point, I wanted to visualize the slicing process to ensure the program was (mostly) working, so I made the following addition to the program:

import (
    "bytes"
    "image/color"
    "github.com/deadsy/sdfx/render"
)
var ASCIISTR = "MND8OZ$7I?+=~:,.."
//-----------------------------------------------------------------------------
func Convert2Ascii(layer sdf.SDF2, w, h int) string {
	b := layer.BoundingBox().ScaleAboutCenter(1.0)

	pixels := v2i.Vec{w, h}
	d, err := render.NewPNG("./tmp.png", b, pixels)
	if err != nil {
		log.Fatalf("error: %s", err)
	}
	d.RenderSDF2(layer)
	img := d.Image()
	table := []byte(ASCIISTR)
    buf := new(bytes.Buffer)
    for i := 0; i < h; i++ {
        for j := 0; j < w; j++ {
            g := color.GrayModel.Convert(img.At(j, i))
            y := reflect.ValueOf(g).FieldByName("Y").Uint()
            pos := int(y * 16 / 255)
            _ = buf.WriteByte(table[pos])
        }
        _ = buf.WriteByte('\n')
    }
    return string(buf.Bytes())
}
//------------------------------------------------------------------------------------

There is probably a better way of doing this, but this was fairly easy to implement. All it’s doing is rendering a 2D SDF to a PNG, then using code from this program, I convert the PNG to ASCII art.

Now to slice the model!

Finally, I can start experimenting with chopping the model into it’s layers. This is also where the visualizer comes in handy as I can quickly get a visual of how the program is working. This brings us up to the current working codebase:

package main

import (
	"log"
	"os"
	"os/exec"
	"fmt"
	"reflect"

	"bytes"
    "image/color"

	"github.com/deadsy/sdfx/obj"
	v3 "github.com/deadsy/sdfx/vec/v3"
	"github.com/deadsy/sdfx/sdf"

	"github.com/deadsy/sdfx/render"
	"github.com/deadsy/sdfx/vec/v2i"
	spinners "github.com/tom-draper/go-spinners"
)
//-----------------------------------------------------------------------------

var ASCIISTR = "MND8OZ$7I?+=~:,.."
//-----------------------------------------------------------------------------
func Convert2Ascii(layer sdf.SDF2, w, h int) string {

	b := layer.BoundingBox().ScaleAboutCenter(1.0)

	pixels := v2i.Vec{w, h}
	d, err := render.NewPNG("./tmp.png", b, pixels)
	if err != nil {
		log.Fatalf("error: %s", err)
	}
	d.RenderSDF2(layer)
	img := d.Image()
	table := []byte(ASCIISTR)
    buf := new(bytes.Buffer)
    for i := 0; i < h; i++ {
        for j := 0; j < w; j++ {
            g := color.GrayModel.Convert(img.At(j, i))
            y := reflect.ValueOf(g).FieldByName("Y").Uint()
            pos := int(y * 16 / 255)
            _ = buf.WriteByte(table[pos])
        }
        _ = buf.WriteByte('\n')
    }
    return string(buf.Bytes())
}
//-----------------------------------------------------------------------------
func main() {
	file, err := os.OpenFile("/home/fedx/Downloads/2x4.stl", os.O_RDONLY, 0400)
	if err != nil {
		log.Fatalf("ERROR", err)
		return
	}

	// create the SDF from the mesh
	object, err := obj.ImportSTL(file, 20, 3, 5)
	if err != nil {
		log.Fatalf("ERROR", err)
		return
	}
	object = sdf.Transform3D(object, sdf.RotateZ(sdf.DtoR(90)))
	gcode := "print_start\n"
	gcode += ""
	var layer_height = 0.3
	z_height := 300.0
	s := spinners.Spinner("lines")
	s.Start()
	layernum := 0
	for i := layer_height; i < z_height; i+= layer_height{
		layernum ++
		layer := sdf.Slice2D(object,v3.Vec{0, 0, i}, v3.Vec{0, 0, 100})
		art := Convert2Ascii(layer, 150, 35)
		cmd := exec.Command("clear") // This creates the effect of a true TUI without the work.
        cmd.Stdout = os.Stdout
        cmd.Run()
		fmt.Println(art)
		fmt.Println("On layer", layernum)
	}
	s.Stop()

}

//-----------------------------------------------------------------------------

At the moment, all this does is chop up an SLT and render ASCII art of the model for each layer, but it has enabled me to ensure the theory of the slicer will work, and mostly it looks super cool. If I ever get around to writing a part 2, I will attempt to generate the perimeters of the object and start on actually producing GCODE.