diff --git a/README.md b/README.md index 65b1f56..76701ab 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ # GoSlice -This is a very experimental slicer for 3d printing. It is currently in a very early stage but it can already slice models: +This is a very experimental slicer for 3d printing. It is currently in a very early stage, but it can already slice models: -Supported features: +__Supported features:__ * perimeters * simple linear infill * rotated infill @@ -18,11 +18,18 @@ Supported features: * simple support generation * brim and skirt +__For users - Use CLI:__ +Provides a basic command line interface. Just run with `--help` and see the description bellow. + +__For developers - Use as Go library:__ +You can use GoSlice as slicing lib, with support to inject custom slicing logic at any stage. +See __"Use as lib"__ bellow. + Example: sliced Gopher logo ## Try it out - for users -Download latest release matching your platform from here: +Download the latest release matching your platform from here: https://github.com/aligator/GoSlice/releases Unpack the executable and run it in the commandline. @@ -41,6 +48,9 @@ If you need the usage of all possible flags, run it with the `--help` flag: ./goslice --help ``` +Note that some flags exist as --initial-... also which applies to the first layer only. +The non-initial apply to all other layers, but not the first one. + ## Try it out - for developers Just running GoSlice: ``` @@ -65,6 +75,54 @@ go build -ldflags "-X=main.Version=$(git describe --tags) -X=main.Build=$(git re ## How does it work [see here](docs/README.md) +## Use as lib +You want to +* Create a slicer but do not want to do everything of it? +* Extend GoSlice functionality? (Please consider Pull Requests if you created a nice addition :-) +* Create a new, user-friendly frontend? + +-> Then you can do it with GoSlice! + +To do this you can copy the `goslice/slicer.go/NewGoSlice` function and just pass to GoSlice what you want. +You can add new logic by implementing one of the various handler interfaces used by it. +If you need even more control, you can even copy and modify the whole `goslice/slicer.go` file which allows you to +control how the steps are called after each other. + +### Handler Interfaces +Here some brief explanation of the interfaces. For more detailed information just look into the code... +(And take a look at [the docs](docs/README.md) where I explained some aspects a bit deeper.) +* Reader handler.ModelReader + Is used to read a mesh file. GoSlice provides an implementation for stl files. + +* Optimizer handler.ModelOptimizer + Is responsible for + 1. checking the model + 2. optimizing it by e.g. removing doubles + 3. calculating some additional information, + like the touching vertices etc. which is needed for the next step. + The implementation of GoSlice is very currently basic and may have problems with some models. + +* Slicer handler.ModelSlicer + Creates the slices (e.g. layers) out of the model. + It then tries to combine all lines to several polygons per each layer. + The implementation of GoSlice is again very basic, but it works. + +* Modifiers []handler.LayerModifier + This is the most interesting part: Modifiers are called after each other and + Calculate things like perimeters, infill, support, ... + They add this information as "Attributes" which is basically just a map of interface{}. + GoSlice already provides several basic modifiers. + +* Generator handler.GCodeGenerator + The generator then generates the final gcode based on the data the modifiers added. + The implementation of GoSlice is basically a collection of `Renderer` which often just match one modifier. + You can provide your own, additional Renderers or even replace existing ones. + +* Writer handler.GCodeWriter + This is the last part, and it basically just writes the gcode to somewhere. + You could for example provide a writer which directly sends the gcode to OctoPrint. + The default implementation just writes it to a gcode file. + ## Contribution You are welcome to help. [Just look for open issues](https://github.com/aligator/GoSlice/issues) and pick one, create new issues or create new pull requests. diff --git a/cmd/goslice/main.go b/cmd/goslice/main.go index 21f38de..2025baf 100644 --- a/cmd/goslice/main.go +++ b/cmd/goslice/main.go @@ -2,6 +2,7 @@ package main import ( "GoSlice/data" + "GoSlice/goslice" "fmt" "io" "os" @@ -25,7 +26,7 @@ func main() { os.Exit(1) } - p := NewGoSlice(o) + p := goslice.NewGoSlice(o) err := p.Process() if err != nil { diff --git a/cmd/goslice/slicer.go b/cmd/goslice/slicer.go deleted file mode 100644 index 1a26778..0000000 --- a/cmd/goslice/slicer.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "GoSlice/clip" - "GoSlice/data" - "GoSlice/gcode" - "GoSlice/gcode/renderer" - "GoSlice/handler" - "GoSlice/modifier" - "GoSlice/optimizer" - "GoSlice/reader" - "GoSlice/slicer" - "GoSlice/writer" - "fmt" - "time" -) - -// GoSlice combines all logic needed to slice -// a model and generate a GCode file. -type GoSlice struct { - options *data.Options - reader handler.ModelReader - optimizer handler.ModelOptimizer - slicer handler.ModelSlicer - modifiers []handler.LayerModifier - generator handler.GCodeGenerator - writer handler.GCodeWriter -} - -// NewGoSlice provides a GoSlice with all built in implementations. -func NewGoSlice(options data.Options) *GoSlice { - s := &GoSlice{ - options: &options, - } - - // create handlers - topBottomPatternFactory := func(min data.MicroPoint, max data.MicroPoint) clip.Pattern { - return clip.NewLinearPattern(options.Printer.ExtrusionWidth, options.Printer.ExtrusionWidth, min, max, options.Print.InfillRotationDegree, true, false) - } - - s.reader = reader.Reader(&options) - s.optimizer = optimizer.NewOptimizer(&options) - s.slicer = slicer.NewSlicer(&options) - s.modifiers = []handler.LayerModifier{ - modifier.NewPerimeterModifier(&options), - modifier.NewInfillModifier(&options), - modifier.NewInternalInfillModifier(&options), - modifier.NewBrimModifier(&options), - modifier.NewSupportDetectorModifier(&options), - modifier.NewSupportGeneratorModifier(&options), - } - - patternSpacing := options.Print.Support.PatternSpacing.ToMicrometer() - - s.generator = gcode.NewGenerator( - &options, - gcode.WithRenderer(renderer.PreLayer{}), - gcode.WithRenderer(renderer.Skirt{}), - gcode.WithRenderer(renderer.Brim{}), - gcode.WithRenderer(renderer.Perimeter{}), - - // Add infill for support generation. - gcode.WithRenderer(&renderer.Infill{ - PatternSetup: func(min data.MicroPoint, max data.MicroPoint) clip.Pattern { - // make bounding box bigger to allow generation of support which has always at least two lines - min.SetX(min.X() - patternSpacing) - min.SetY(min.Y() - patternSpacing) - max.SetX(max.X() + patternSpacing) - max.SetY(max.Y() + patternSpacing) - return clip.NewLinearPattern(options.Printer.ExtrusionWidth, patternSpacing, min, max, 90, false, true) - }, - AttrName: "support", - Comments: []string{"TYPE:SUPPORT"}, - }), - // Interface pattern for support generation is generated by rotating 90° to the support and no spaces between the lines. - gcode.WithRenderer(&renderer.Infill{ - PatternSetup: func(min data.MicroPoint, max data.MicroPoint) clip.Pattern { - // make bounding box bigger to allow generation of support which has always at least two lines - min.SetX(min.X() - patternSpacing) - min.SetY(min.Y() - patternSpacing) - max.SetX(max.X() + patternSpacing) - max.SetY(max.Y() + patternSpacing) - return clip.NewLinearPattern(options.Printer.ExtrusionWidth, options.Printer.ExtrusionWidth, min, max, 0, false, true) - }, - AttrName: "supportInterface", - Comments: []string{"TYPE:SUPPORT"}, - }), - - gcode.WithRenderer(&renderer.Infill{ - PatternSetup: topBottomPatternFactory, - AttrName: "bottom", - Comments: []string{"TYPE:FILL", "BOTTOM-FILL"}, - }), - gcode.WithRenderer(&renderer.Infill{ - PatternSetup: topBottomPatternFactory, - AttrName: "top", - Comments: []string{"TYPE:FILL", "TOP-FILL"}, - }), - gcode.WithRenderer(&renderer.Infill{ - PatternSetup: func(min data.MicroPoint, max data.MicroPoint) clip.Pattern { - // TODO: the calculation of the percentage is currently very basic and may not be correct. - - if options.Print.InfillPercent != 0 { - mm10 := data.Millimeter(10).ToMicrometer() - linesPer10mmFor100Percent := mm10 / options.Printer.ExtrusionWidth - linesPer10mmForInfillPercent := float64(linesPer10mmFor100Percent) * float64(options.Print.InfillPercent) / 100.0 - - lineWidth := data.Micrometer(float64(mm10) / linesPer10mmForInfillPercent) - - return clip.NewLinearPattern(options.Printer.ExtrusionWidth, lineWidth, min, max, options.Print.InfillRotationDegree, true, options.Print.InfillZigZag) - } - - return nil - }, - AttrName: "infill", - Comments: []string{"TYPE:FILL", "INTERNAL-FILL"}, - }), - gcode.WithRenderer(renderer.PostLayer{}), - ) - s.writer = writer.Writer() - - return s -} - -func (s *GoSlice) Process() error { - startTime := time.Now() - - // 1. Load model - models, err := s.reader.Read(s.options.GoSlice.InputFilePath) - if err != nil { - return err - } - - // 2. Optimize model - var optimizedModel data.OptimizedModel - - optimizedModel, err = s.optimizer.Optimize(models) - if err != nil { - return err - } - - //err = optimizedModel.SaveDebugSTL("test.stl") - //if err != nil { - // return err - //} - - // 3. Slice model into layers - layers, err := s.slicer.Slice(optimizedModel) - if err != nil { - return err - } - - // 4. Modify the layers - // e.g. generate perimeter paths, - // generate the parts which should be filled in, ... - for _, m := range s.modifiers { - m.Init(optimizedModel) - err = m.Modify(layers) - if err != nil { - return err - } - } - - // 5. generate gcode from the layers - s.generator.Init(optimizedModel) - finalGcode, err := s.generator.Generate(layers) - if err != nil { - return err - } - - outputPath := s.options.GoSlice.OutputFilePath - if outputPath == "" { - outputPath = s.options.GoSlice.InputFilePath + ".gcode" - } - - err = s.writer.Write(finalGcode, outputPath) - fmt.Println("full processing time:", time.Now().Sub(startTime)) - - return err -} diff --git a/cmd/goslice/slicer_test.go b/cmd/goslice/slicer_test.go deleted file mode 100644 index 0ff5348..0000000 --- a/cmd/goslice/slicer_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "GoSlice/data" - "GoSlice/util/test" - "testing" -) - -const ( - folder = "../../test_stl/" - - // The models are copied to the project just to avoid downloading them for each test. - - // 3DBenchy is the unmodified model from here: - // https://www.thingiverse.com/thing:763622 - // using the following license - // https://creativecommons.org/licenses/by-nd/4.0/ - benchy = "3DBenchy.stl" - - // Go Gopher mascot is the unmodified model from here: - // https://www.thingiverse.com/thing:3413597 - // using the following license - // https://creativecommons.org/licenses/by/4.0/ - gopher = "gopher_union.stl" -) - -func TestWholeSlicer(t *testing.T) { - o := data.DefaultOptions() - // enable support so that it is tested also - o.Print.Support.Enabled = true - o.Print.BrimSkirt.BrimCount = 3 - s := NewGoSlice(o) - - var tests = []struct { - path string - }{ - { - path: benchy, - }, - { - path: gopher, - }, - } - - for _, testCase := range tests { - t.Log("slice " + testCase.path) - s.options.GoSlice.InputFilePath = folder + testCase.path - err := s.Process() - test.Ok(t, err) - } -} diff --git a/data/option.go b/data/option.go index e68c546..1d48bf2 100644 --- a/data/option.go +++ b/data/option.go @@ -260,6 +260,15 @@ type GoSliceOptions struct { // PrintVersion indicates if the GoSlice version should be printed. PrintVersion bool + // InputFilePath specifies the path to the input stl file. + InputFilePath string + + // OutputFilePath specifies the path to the output gcode file. + OutputFilePath string +} + +// SlicingOptions contains all options related to slice a model. +type SlicingOptions struct { // MeldDistance is the distance which two points have to be // within to count them as one point. MeldDistance Micrometer @@ -272,16 +281,11 @@ type GoSliceOptions struct { // FinishPolygonSnapDistance is the max distance between start end endpoint of // a polygon used to check if a open polygon can be closed. FinishPolygonSnapDistance Micrometer - - // InputFilePath specifies the path to the input stl file. - InputFilePath string - - // OutputFilePath specifies the path to the output gcode file. - OutputFilePath string } // Options contains all GoSlice options. type Options struct { + Slicing SlicingOptions Printer PrinterOptions Filament FilamentOptions Print PrintOptions @@ -290,6 +294,11 @@ type Options struct { func DefaultOptions() Options { return Options{ + Slicing: SlicingOptions{ + MeldDistance: 30, + JoinPolygonSnapDistance: 160, + FinishPolygonSnapDistance: 1000, + }, Print: PrintOptions{ IntialLayerSpeed: 30, LayerSpeed: 60, @@ -340,12 +349,9 @@ func DefaultOptions() Options { ), }, GoSlice: GoSliceOptions{ - PrintVersion: false, - MeldDistance: 30, - JoinPolygonSnapDistance: 160, - FinishPolygonSnapDistance: 1000, - InputFilePath: "", - OutputFilePath: "", + PrintVersion: false, + InputFilePath: "", + OutputFilePath: "", }, } } @@ -362,11 +368,13 @@ func ParseFlags() Options { // GoSlice options flag.BoolVarP(&options.GoSlice.PrintVersion, "version", "v", false, "Print the GoSlice version.") - flag.Var(&options.GoSlice.MeldDistance, "meld-distance", "The distance which two points have to be within to count them as one point.") - flag.Var(&options.GoSlice.JoinPolygonSnapDistance, "join-polygon-snap-distance", "The distance used to check if two open polygons can be snapped together to one bigger polygon. Checked by the start and endpoints of the polygons.") - flag.Var(&options.GoSlice.FinishPolygonSnapDistance, "finish-polygon-snap-distance", "The max distance between start end endpoint of a polygon used to check if a open polygon can be closed.") flag.StringVarP(&options.GoSlice.OutputFilePath, "output", "o", options.GoSlice.OutputFilePath, "File path for the output gcode file. Default is the inout file path with .gcode as file ending.") + // Slicing options + flag.Var(&options.Slicing.MeldDistance, "meld-distance", "The distance which two points have to be within to count them as one point.") + flag.Var(&options.Slicing.JoinPolygonSnapDistance, "join-polygon-snap-distance", "The distance used to check if two open polygons can be snapped together to one bigger polygon. Checked by the start and endpoints of the polygons.") + flag.Var(&options.Slicing.FinishPolygonSnapDistance, "finish-polygon-snap-distance", "The max distance between start end endpoint of a polygon used to check if a open polygon can be closed.") + // print options flag.Var(&options.Print.IntialLayerSpeed, "initial-layer-speed", "The speed only for the first layer in mm per second.") flag.Var(&options.Print.LayerSpeed, "layer-speed", "The speed for all but the first layer in mm per second.") diff --git a/go.sum b/go.sum index 3ee5ff4..8539435 100644 --- a/go.sum +++ b/go.sum @@ -36,7 +36,6 @@ github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb h1:61ndUreYSlWFeCY44JxD github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY= github.com/paulmach/orb v0.1.6/go.mod h1:pPwxxs3zoAyosNSbNKn1jiXV2+oovRDObDKfTvRegDI= github.com/paulmach/osm v0.1.1/go.mod h1:/UEV7XqKKTG3/46W+MtSmIl81yjV7cGoLkpol3S094I= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -46,7 +45,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -59,7 +57,6 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/handler/handler.go b/handler/handler.go index e6cf42f..4164920 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -33,7 +33,7 @@ type GCodeGenerator interface { Generate(layer []data.PartitionedLayer) (string, error) } -// GCodeWriter writes the given GCode into the given file. +// GCodeWriter writes the given GCode into the given destination. type GCodeWriter interface { - Write(gcode string, filename string) error + Write(gcode string, destination string) error } diff --git a/optimizer/optimizer.go b/optimizer/optimizer.go index 45d09a4..150466b 100644 --- a/optimizer/optimizer.go +++ b/optimizer/optimizer.go @@ -60,7 +60,7 @@ FacesLoop: currentPoint := face.Points()[j] // create hash for the pos // points which are within the meldDistance fall into the same category of the indices map - meldDistanceHash := pointHash(o.options.GoSlice.MeldDistance) + meldDistanceHash := pointHash(o.options.Slicing.MeldDistance) hash := ((pointHash(currentPoint.X()) + meldDistanceHash/2) / meldDistanceHash) ^ (((pointHash(currentPoint.Y()) + meldDistanceHash/2) / meldDistanceHash) << 10) ^ (((pointHash(currentPoint.Z()) + meldDistanceHash/2) / meldDistanceHash) << 20) @@ -72,7 +72,7 @@ FacesLoop: // is smaller (or same) than the currently tested pos for _, index := range indices[hash] { differenceVec := om.points[index].pos.Sub(currentPoint) - if differenceVec.ShorterThanOrEqual(o.options.GoSlice.MeldDistance) { + if differenceVec.ShorterThanOrEqual(o.options.Slicing.MeldDistance) { // if true for any of the points with the same hash, // do not add the current pos to the indices map // but save the indices of the already existing duplicate diff --git a/slicer/layer.go b/slicer/layer.go index f67c65a..167bab5 100644 --- a/slicer/layer.go +++ b/slicer/layer.go @@ -77,7 +77,7 @@ func (l *layer) makePolygons(om data.OptimizedModel, joinPolygonSnapDistance, fi p1 := l.segments[touchingSegmentIndex].start diff := p0.Sub(p1) - if diff.ShorterThanOrEqual(l.options.GoSlice.MeldDistance) { + if diff.ShorterThanOrEqual(l.options.Slicing.MeldDistance) { if touchingSegmentIndex == startSegmentIndex { canClose = true } diff --git a/slicer/slicer.go b/slicer/slicer.go index 4599e85..43597fe 100644 --- a/slicer/slicer.go +++ b/slicer/slicer.go @@ -112,7 +112,7 @@ func (s slicer) Slice(m data.OptimizedModel) ([]data.PartitionedLayer, error) { c := clip.NewClipper() for i, layer := range layers { - layer.makePolygons(m, s.options.GoSlice.JoinPolygonSnapDistance, s.options.GoSlice.FinishPolygonSnapDistance) + layer.makePolygons(m, s.options.Slicing.JoinPolygonSnapDistance, s.options.Slicing.FinishPolygonSnapDistance) lp, ok := c.GenerateLayerParts(layer) if !ok {