From 3b958971db1f5d36e8218647fd004ae7a0fee1cf Mon Sep 17 00:00:00 2001 From: aligator Date: Sun, 23 Aug 2020 15:41:38 +0200 Subject: [PATCH] Skirt & Brim (#25) --- README.md | 1 + clip/clip.go | 127 +++++++++++++++++++++++++++----- data/layer.go | 19 +++++ data/option.go | 76 ++++++++++++------- gcode/generator.go | 8 +- gcode/generator_test.go | 4 +- gcode/renderer/brim.go | 35 +++++++++ gcode/renderer/infill.go | 6 +- gcode/renderer/layer.go | 13 ++-- gcode/renderer/perimeter.go | 8 +- gcode/renderer/skirt.go | 73 ++++++++++++++++++ go.mod | 1 + go.sum | 2 + modifier/brim.go | 142 ++++++++++++++++++++++++++++++++++++ modifier/perimeter.go | 4 +- modifier/support.go | 33 ++++++++- slicer.go | 3 + slicer_test.go | 1 + 18 files changed, 490 insertions(+), 66 deletions(-) create mode 100644 gcode/renderer/brim.go create mode 100644 gcode/renderer/skirt.go create mode 100644 modifier/brim.go diff --git a/README.md b/README.md index a3d577b..c55a6c1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Supported features: * simple retraction on crossing perimeters * several options to customize slicing output * simple support generation +* brim and skirt Example: sliced Gopher logo diff --git a/clip/clip.go b/clip/clip.go index 06c83c5..f8e427d 100644 --- a/clip/clip.go +++ b/clip/clip.go @@ -6,6 +6,7 @@ import ( "GoSlice/data" clipper "github.com/aligator/go.clipper" + go_convex_hull_2d "github.com/furstenheim/go-convex-hull-2d" ) // Pattern is an interface for all infill types which can be used to fill layer parts. @@ -15,13 +16,35 @@ type Pattern interface { Fill(layerNr int, part data.LayerPart) data.Paths } -// OffsetResult is built the following way: [part][insetNr][insetParts]data.LayerPart +// OffsetResult is built the following way: [partNr][insetNr][insetPartsNr]data.LayerPart // -// * Part is the part number from the input-layer. -// * Wall is the wall of the part. The first wall is the outer perimeter. The others are for example the holes. -// * InsetNum is the number of the inset (starting by the outer walls with 0) -// and all following are from holes inside of the polygon. -// The array for a part may be empty. +// * partNr is the part number from the input-layer. +// * insetNr is the number of the inset (so if the inset count is 5 this contains 5 insets with 0 the ounter one) +// * insetPartsNr: If the insetting of one line results in several polygons and not only one this is filled with them. +// For example if this polygon is inset (by one inset): +// ---------------------| |-----------| +// | | | | +// | |----| | +// | | +// | | +// --------------------------------------- +// It results in this: (the line is partNr 1, insetNr 1 and insetPartsNr 1) +// ---------------------| |-----------| +// |11111111111111111111| |11111111111| +// |1 1|----|1 1| +// |1 11111111 1| +// |1111111111111111111111111111111111111| +// --------------------------------------- +// If it is inset by another line the new resulting 2 lines are +// partNr 1, insetNr 2 and insetPartsNr 1 (left circle of 2ers) +// and +// partNr 1, insetNr 2 and insetPartsNr 2 (right circle of 2ers) +// ---------------------| |-----------| +// |11111111111111111111| |11111111111| +// |12222222222222222221|----|12222222221| +// |1222222222222222222111111112222222221| +// |1111111111111111111111111111111111111| +// --------------------------------------- type OffsetResult [][][]data.LayerPart func (or OffsetResult) ToOneDimension() []data.LayerPart { @@ -36,6 +59,20 @@ func (or OffsetResult) ToOneDimension() []data.LayerPart { return result } +// ForEach just runs through everything and calls the callback cb for each element. +// If the callback returns true, the whole looping just stops and ForEach returns immediately. +func (or OffsetResult) ForEach(cb func(part data.LayerPart, partNr, insetNr, insetPartsNr int) (doBreak bool)) { + for partNr, part := range or { + for insetNr, inset := range part { + for insetPartsNr, insetParts := range inset { + if cb(insetParts, partNr, insetNr, insetPartsNr) { + return + } + } + } + } +} + // Clipper is an interface that provides methods needed by GoSlice to clip and alter polygons. type Clipper interface { // GenerateLayerParts partitions the whole layer into several partition parts. @@ -44,18 +81,16 @@ type Clipper interface { // InsetLayer returns all new paths generated by insetting all parts of the layer. // If you need to ex-set a part, just provide a negative offset. - InsetLayer(layer []data.LayerPart, offset data.Micrometer, insetCount int) OffsetResult + // The initialOffset is used for the first inset, so that the first inset can be a bit more or less offset. + InsetLayer(layer []data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) OffsetResult // Inset insets the given layer part. // The result is built the following way: [insetNr][insetParts]data.LayerPart - // - // * Wall is the wall of the part. The first wall is the outer perimeter - // * InsetNum is the number of the inset (starting by the outer walls with 0) - // and all following are from holes inside of the polygon. - // The array for a part may be empty. + // See also OffsetResult for a more specific description. // // If you need to ex-set a part, just provide a negative offset. - Inset(part data.LayerPart, offset data.Micrometer, insetCount int) [][]data.LayerPart + // The initialOffset is used for the first inset, so that the first inset can be a bit more or less offset. + Inset(part data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) [][]data.LayerPart // Difference calculates the difference between the parts and the toRemove parts. // It returns the result as a new slice of layer parts. @@ -71,6 +106,25 @@ type Clipper interface { // IsCrossingPerimeter checks if the given line crosses any perimeter of the given parts. If yes, the result is true. IsCrossingPerimeter(parts []data.LayerPart, line data.Path) (result, ok bool) + + // Hull generates an outline around all LayerParts. + Hull(parts []data.LayerPart) (hull data.Path, ok bool) + + // TopLevelPolygons only returns polygons which are at the top level. + // this means if there are 3 (1, 2, 3) polygons and one of them (3) is inside of another (2): + // ######### ############## + // # 1 # # 2 # + // # # # +++++ # + // # # # + 3 + # + // # # # +++++ # + // # # # # + // ######### ############## + // + // Then the top level polygons are only 1 and 2. + // This is also true if the polygons are stacked even deeper. + // + // Only the outlines are checked and returned, the holes of the LayerParts are ignored! + TopLevelPolygons(parts []data.LayerPart) (topLevel data.Paths, ok bool) } // clipperClipper implements Clipper using the external clipper library. @@ -202,20 +256,22 @@ func polyTreeToLayerParts(tree *clipper.PolyTree) []data.LayerPart { return layerParts } -func (c clipperClipper) InsetLayer(layer []data.LayerPart, offset data.Micrometer, insetCount int) OffsetResult { +func (c clipperClipper) InsetLayer(layer []data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) OffsetResult { var result OffsetResult for _, part := range layer { - result = append(result, c.Inset(part, offset, insetCount)) + result = append(result, c.Inset(part, offset, insetCount, initialOffset)) } return result } -func (c clipperClipper) Inset(part data.LayerPart, offset data.Micrometer, insetCount int) [][]data.LayerPart { +func (c clipperClipper) Inset(part data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) [][]data.LayerPart { var insets [][]data.LayerPart co := clipper.NewClipperOffset() + currentOffset := float64(initialOffset) + for insetNr := 0; insetNr < insetCount; insetNr++ { // insets for the outline co.Clear() @@ -223,8 +279,10 @@ func (c clipperClipper) Inset(part data.LayerPart, offset data.Micrometer, inset co.AddPaths(clipperPaths(part.Holes()), clipper.JtSquare, clipper.EtClosedPolygon) co.MiterLimit = 2 - allNewInsets := co.Execute2(float64(-int(offset)*insetNr) - float64(offset/2)) + allNewInsets := co.Execute2(currentOffset) insets = append(insets, polyTreeToLayerParts(allNewInsets)) + + currentOffset += float64(-int(offset)) } return insets @@ -287,3 +345,38 @@ func (c clipperClipper) IsCrossingPerimeter(parts []data.LayerPart, line data.Pa return tree.Total() > 0, true } + +func (c clipperClipper) Hull(parts []data.LayerPart) (hull data.Path, ok bool) { + var allPoints data.Path + for _, part := range parts { + allPoints = append(allPoints, part.Outline()...) + } + + convexHull := go_convex_hull_2d.New(allPoints) + + hullPath, ok := convexHull.(data.Path) + if !ok { + return nil, ok + } + return hullPath, true +} + +func (c clipperClipper) TopLevelPolygons(parts []data.LayerPart) (topLevel data.Paths, ok bool) { + cl := clipper.NewClipper(clipper.IoNone) + + for _, part := range parts { + cl.AddPath(clipperPath(part.Outline()), clipper.PtSubject, true) + } + + // this is just a dummy-call to Execute2 as I found no other way to get a tree from clipper... + tree, ok := cl.Execute2(clipper.CtUnion, clipper.PftEvenOdd, clipper.PftEvenOdd) + if !ok { + return nil, false + } + + for _, child := range tree.Childs() { + topLevel = append(topLevel, microPath(child.Contour(), false)) + } + + return topLevel, true +} diff --git a/data/layer.go b/data/layer.go index 07dd54b..9d6f8e4 100644 --- a/data/layer.go +++ b/data/layer.go @@ -2,6 +2,8 @@ package data +import go_convex_hull_2d "github.com/furstenheim/go-convex-hull-2d" + // Path is a simple list of points. // It can be used to represent polygons (if they are closed) or just lines. type Path []MicroPoint @@ -179,6 +181,23 @@ func (p Path) Rotate(degree float64) { } } +func (p Path) Take(i int) (x, y float64) { + point := p[i] + return float64(point.X()), float64(point.Y()) +} + +func (p Path) Len() int { + return len(p) +} + +func (p Path) Swap(i, j int) { + p[j], p[i] = p[i], p[j] +} + +func (p Path) Slice(i, j int) go_convex_hull_2d.Interface { + return p[i:j] +} + // Paths represents a group of Paths. type Paths []Path diff --git a/data/option.go b/data/option.go index f42757e..bfb6098 100644 --- a/data/option.go +++ b/data/option.go @@ -77,32 +77,6 @@ func (v microVec3) Type() string { return "Micrometer" } -// SupportOptions contains all Support specific GoSlice options. -type SupportOptions struct { - // Enabled enables the generation of support structures. - Enabled bool - - // ThresholdAngle is the angle up to which no support is generated. - ThresholdAngle int - - // TopGapLayers is the amount of layers without support. - TopGapLayers int - - // InterfaceLayers is the amount of layers which are filled differently as interface to the object. - InterfaceLayers int - - // PatternSpacing is the spacing used to create the support pattern. - PatternSpacing Millimeter - - // Gap is the gap between the model and the support. - Gap Millimeter -} - -// FanSpeedOptions used to control fan speed at given layers. -type FanSpeedOptions struct { - LayerToSpeedLUT map[int]int -} - // NewDefaultFanSpeedOptions Creates instance FanSpeedOptions // and sets a of full fan (255) at layer 3. func NewDefaultFanSpeedOptions() FanSpeedOptions { @@ -195,6 +169,8 @@ type PrintOptions struct { NumberTopLayers int Support SupportOptions + + BrimSkirt BrimSkirtOptions } // FilamentOptions contains all Filament specific GoSlice options. @@ -228,6 +204,44 @@ type FilamentOptions struct { FanSpeed FanSpeedOptions } +// SupportOptions contains all Support specific GoSlice options. +type SupportOptions struct { + // Enabled enables the generation of support structures. + Enabled bool + + // ThresholdAngle is the angle up to which no support is generated. + ThresholdAngle int + + // TopGapLayers is the amount of layers without support. + TopGapLayers int + + // InterfaceLayers is the amount of layers which are filled differently as interface to the object. + InterfaceLayers int + + // PatternSpacing is the spacing used to create the support pattern. + PatternSpacing Millimeter + + // Gap is the gap between the model and the support. + Gap Millimeter +} + +// BrimSkirtOptions contains all options for the brim and skirt generation. +type BrimSkirtOptions struct { + // SkirtCount is the amount of skirt lines around the initial layer. + SkirtCount int + + // SkirtDistance is the distance between the model (or the most outer brim lines) and the most inner skirt line. + SkirtDistance Millimeter + + // BrimCount specifies the amount of brim lines around the parts of the initial layer. + BrimCount int +} + +// FanSpeedOptions used to control fan speed at given layers. +type FanSpeedOptions struct { + LayerToSpeedLUT map[int]int +} + // PrinterOptions contains all Printer specific GoSlice options. type PrinterOptions struct { // ExtrusionWidth is the diameter of your nozzle. @@ -292,6 +306,11 @@ func DefaultOptions() Options { PatternSpacing: Millimeter(2.5), Gap: Millimeter(0.6), }, + BrimSkirt: BrimSkirtOptions{ + SkirtCount: 2, + SkirtDistance: Millimeter(5), + BrimCount: 0, + }, }, Filament: FilamentOptions{ FilamentDiameter: Millimeter(1.75).ToMicrometer(), @@ -355,6 +374,11 @@ func ParseFlags() Options { flag.Var(&options.Print.Support.PatternSpacing, "support-pattern-spacing", "The spacing used to create the support pattern.") flag.Var(&options.Print.Support.Gap, "support-gap", "The gap between the model and the support.") + // brim & skirt options + flag.IntVar(&options.Print.BrimSkirt.SkirtCount, "skirt-count", options.Print.BrimSkirt.SkirtCount, "The amount of skirt lines around the initial layer.") + flag.Var(&options.Print.BrimSkirt.SkirtDistance, "skirt-distance", "The distance between the model (or the most outer brim lines) and the most inner skirt line.") + flag.IntVar(&options.Print.BrimSkirt.BrimCount, "brim-count", options.Print.BrimSkirt.BrimCount, "The amount of brim lines around the parts of the initial layer.") + // filament options flag.Var(&options.Filament.FilamentDiameter, "filament-diameter", "The filament diameter used by the printer.") flag.IntVar(&options.Filament.InitialBedTemperature, "initial-bed-temperature", options.Filament.InitialBedTemperature, "The temperature for the heated bed for the first layers.") diff --git a/gcode/generator.go b/gcode/generator.go index 0a085d7..46d22f0 100644 --- a/gcode/generator.go +++ b/gcode/generator.go @@ -10,11 +10,11 @@ import ( // Several renderers can be provided to the generator. type Renderer interface { // Init is called once at the beginning and can be used to set up the renderer. - // For example the infill patterns can be instanciated int this method. + // For example the infill patterns can be instantiated int this method. Init(model data.OptimizedModel) // Render is called for each layer and the provided Builder can be used to add gcode. - Render(b *Builder, layerNr int, layers []data.PartitionedLayer, z data.Micrometer, options *data.Options) error + Render(b *Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error } type generator struct { @@ -62,10 +62,12 @@ func (g *generator) init() { func (g *generator) Generate(layers []data.PartitionedLayer) (string, error) { g.init() + maxLayer := len(layers) - 1 + for layerNr := range layers { for _, renderer := range g.renderers { z := g.options.Print.InitialLayerThickness + data.Micrometer(layerNr)*g.options.Print.LayerThickness - err := renderer.Render(g.builder, layerNr, layers, z, g.options) + err := renderer.Render(g.builder, layerNr, maxLayer, layers[layerNr], z, g.options) if err != nil { return "", err } diff --git a/gcode/generator_test.go b/gcode/generator_test.go index d681741..7e0f7bf 100644 --- a/gcode/generator_test.go +++ b/gcode/generator_test.go @@ -26,9 +26,9 @@ func (f *fakeRenderer) Init(model data.OptimizedModel) { f.c.c["init"]++ } -func (f *fakeRenderer) Render(b *gcode.Builder, layerNr int, layers []data.PartitionedLayer, z data.Micrometer, options *data.Options) error { +func (f *fakeRenderer) Render(b *gcode.Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error { f.c.c["render"]++ - test.Assert(f.t, len(layers) > layerNr, "the number of layers should be more than the current layer number") + test.Assert(f.t, maxLayer >= layerNr, "the number of layers should be more or equal than the current layer number") b.AddCommand("number %v", layerNr) return nil } diff --git a/gcode/renderer/brim.go b/gcode/renderer/brim.go new file mode 100644 index 0000000..a89e89d --- /dev/null +++ b/gcode/renderer/brim.go @@ -0,0 +1,35 @@ +// This file provides a renderer for the brim lines generated by the brim modifier. +package renderer + +import ( + "GoSlice/data" + "GoSlice/gcode" + "GoSlice/modifier" +) + +// Brim just draws the brim lines generated by the brim modifier. +type Brim struct{} + +func (Brim) Init(model data.OptimizedModel) {} + +func (Brim) Render(b *gcode.Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error { + // Get the brim data. + brim, err := modifier.Brim(layer) + if err != nil { + return err + } + if brim == nil { + return nil + } + + // Use type SKIRT as Cura also does it the same. This is for support of the gcode viewer in Cura. + b.AddComment("TYPE:SKIRT") + + err = nil + brim.ForEach(func(part data.LayerPart, _, _, _ int) bool { + err = b.AddPolygon(nil, part.Outline(), z, false) + return err != nil + }) + + return err +} diff --git a/gcode/renderer/infill.go b/gcode/renderer/infill.go index 67aee89..0652572 100644 --- a/gcode/renderer/infill.go +++ b/gcode/renderer/infill.go @@ -29,12 +29,12 @@ func (i *Infill) Init(model data.OptimizedModel) { i.pattern = i.PatternSetup(model.Min().PointXY(), model.Max().PointXY()) } -func (i *Infill) Render(b *gcode.Builder, layerNr int, layers []data.PartitionedLayer, z data.Micrometer, options *data.Options) error { +func (i *Infill) Render(b *gcode.Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error { if i.pattern == nil { return nil } - infillParts, err := modifier.PartsAttribute(layers[layerNr], i.AttrName) + infillParts, err := modifier.PartsAttribute(layer, i.AttrName) if err != nil { return err } @@ -48,7 +48,7 @@ func (i *Infill) Render(b *gcode.Builder, layerNr int, layers []data.Partitioned } for _, path := range i.pattern.Fill(layerNr, part) { - err := b.AddPolygon(layers[layerNr], path, z, true) + err := b.AddPolygon(layer, path, z, true) if err != nil { return err } diff --git a/gcode/renderer/layer.go b/gcode/renderer/layer.go index 136ed7c..d834f5f 100644 --- a/gcode/renderer/layer.go +++ b/gcode/renderer/layer.go @@ -12,7 +12,7 @@ type PreLayer struct{} func (PreLayer) Init(model data.OptimizedModel) {} -func (PreLayer) Render(b *gcode.Builder, layerNr int, layers []data.PartitionedLayer, z data.Micrometer, options *data.Options) error { +func (PreLayer) Render(b *gcode.Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error { b.AddComment("LAYER:%v", layerNr) if layerNr == 0 { b.AddComment("Generated with GoSlice") @@ -28,9 +28,6 @@ func (PreLayer) Render(b *gcode.Builder, layerNr int, layers []data.PartitionedL // starting gcode b.AddComment("START_GCODE") - b.AddCommand("G1 X0 Y20 Z0.2 F3000 ; get ready to prime") - b.AddCommand("G92 E0 ; reset extrusion distance") - b.AddCommand("G1 X200 E20 F600 ; prime nozzle") b.AddCommand("G1 Z5 F5000 ; lift nozzle") b.AddCommand("G92 E0 ; reset extrusion distance") @@ -75,9 +72,9 @@ type PostLayer struct{} func (PostLayer) Init(model data.OptimizedModel) {} -func (PostLayer) Render(b *gcode.Builder, layerNr int, layers []data.PartitionedLayer, z data.Micrometer, options *data.Options) error { +func (PostLayer) Render(b *gcode.Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error { // ending gcode - if layerNr == len(layers)-1 { + if layerNr == maxLayer { b.AddComment("END_GCODE") b.SetExtrusion(options.Print.LayerThickness, options.Printer.ExtrusionWidth, options.Filament.FilamentDiameter) b.AddCommand("M107 ; disable fan") @@ -85,6 +82,10 @@ func (PostLayer) Render(b *gcode.Builder, layerNr int, layers []data.Partitioned // disable heaters b.AddCommand("M104 S0 ; Set Hot-end to 0C (off)") b.AddCommand("M140 S0 ; Set bed to 0C (off)") + + b.AddCommand("G28 X0 ; home X axis to get head out of the way") + b.AddCommand("M84 ;steppers off") + } return nil diff --git a/gcode/renderer/perimeter.go b/gcode/renderer/perimeter.go index 12a5714..a349cd6 100644 --- a/gcode/renderer/perimeter.go +++ b/gcode/renderer/perimeter.go @@ -13,8 +13,8 @@ type Perimeter struct{} func (p Perimeter) Init(model data.OptimizedModel) {} -func (p Perimeter) Render(b *gcode.Builder, layerNr int, layers []data.PartitionedLayer, z data.Micrometer, options *data.Options) error { - perimeters, err := modifier.Perimeters(layers[layerNr]) +func (p Perimeter) Render(b *gcode.Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error { + perimeters, err := modifier.Perimeters(layer) if err != nil { return err } @@ -41,13 +41,13 @@ func (p Perimeter) Render(b *gcode.Builder, layerNr int, layers []data.Partition } for _, hole := range insetParts.Holes() { - err := b.AddPolygon(layers[layerNr], hole, z, false) + err := b.AddPolygon(layer, hole, z, false) if err != nil { return err } } - err := b.AddPolygon(layers[layerNr], insetParts.Outline(), z, false) + err := b.AddPolygon(layer, insetParts.Outline(), z, false) if err != nil { return err } diff --git a/gcode/renderer/skirt.go b/gcode/renderer/skirt.go new file mode 100644 index 0000000..98dfc6a --- /dev/null +++ b/gcode/renderer/skirt.go @@ -0,0 +1,73 @@ +// This file provides a renderer for the skirt. + +package renderer + +import ( + "GoSlice/clip" + "GoSlice/data" + "GoSlice/gcode" + "GoSlice/modifier" + "errors" +) + +// Skirt generates the skirt lines. +// Skirt is basically a surrounding of the object in some dinstance. +// It is used for priming the nozzle. +// The skirt is generated by creating a hull around everything (including support and brim) +// and then exsetting it by the configured distance. +// A 2d hull is basically one line surrounding everything. +// (htps://spolearninglab.com/curriculum/lessonPlans/hacking/resources/software/3d/openscad/openscad_hull.html) +type Skirt struct{} + +func (Skirt) Init(model data.OptimizedModel) {} + +func (Skirt) Render(b *gcode.Builder, layerNr int, maxLayer int, layer data.PartitionedLayer, z data.Micrometer, options *data.Options) error { + if options.Print.BrimSkirt.SkirtCount == 0 { + return nil + } + + if layerNr == 0 { + // Get the perimeters and support to base the hull (line around everything) on them. + perimeters, err := modifier.Perimeters(layer) + if err != nil { + return err + } + + support, err := modifier.FullSupport(layer) + if err != nil { + return err + } + if support == nil && perimeters == nil { + return nil + } + + // Skirt distance + (1/2 extrusion with of the model side + 1/2 extrusion width of the most inner brim line) + the brim width + // is the distance between the perimeter (or brim) and skirt. + distance := options.Print.BrimSkirt.SkirtDistance.ToMicrometer() + (options.Printer.ExtrusionWidth * data.Micrometer(options.Print.BrimSkirt.BrimCount)) + options.Printer.ExtrusionWidth + + // Draw the skirt. + c := clip.NewClipper() + // Generate the hull around everything. + hull, ok := c.Hull(append(support, perimeters.ToOneDimension()...)) + if !ok { + return errors.New("could not generate hull around all perimeters to create the skirt") + } + + // Generate all skirt lines by exsetting the hull. + skirt := c.Inset(data.NewBasicLayerPart(hull, nil), -options.Printer.ExtrusionWidth, options.Print.BrimSkirt.SkirtCount, distance) + + b.AddComment("TYPE:SKIRT") + + for _, wall := range skirt { + for _, loopPart := range wall { + // As we use the hull around the whole object there shouldn't be any collision with the model -> currentLayer is nil + err := b.AddPolygon(nil, loopPart.Outline(), z, false) + if err != nil { + return err + } + } + } + } + + return nil +} diff --git a/go.mod b/go.mod index e87aad3..8ebd1e9 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/aligator/go.clipper v0.0.0-20200424185851-fc8a51077d44 + github.com/furstenheim/go-convex-hull-2d v0.0.0-20181121204724-08788ab09726 github.com/google/go-cmp v0.5.1 github.com/hschendel/stl v1.0.4 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index f261263..fc43b9c 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/furstenheim/go-convex-hull-2d v0.0.0-20181121204724-08788ab09726 h1:mEp/tvyrw9KwfZwq/RmFdd27parkvVDgS4P0cF/I4Ck= +github.com/furstenheim/go-convex-hull-2d v0.0.0-20181121204724-08788ab09726/go.mod h1:fRxBKGOAhWlwrKnguS+EmgbZDYdbdALrLXaZvX86L/E= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/modifier/brim.go b/modifier/brim.go new file mode 100644 index 0000000..75268d2 --- /dev/null +++ b/modifier/brim.go @@ -0,0 +1,142 @@ +package modifier + +import ( + "GoSlice/clip" + "GoSlice/data" + "GoSlice/handler" + "fmt" +) + +type brimModifier struct { + options *data.Options +} + +func (m brimModifier) Init(model data.OptimizedModel) {} + +// NewBrimModifier generates the brim lines. +// The brim is basically a surrounding of the objects on the first layer +// by several lines which directly contact the object +// perimeters. +// This can improve the adhesion to the build plate. +// It is generated by exsetting the most outer perimeter and the result is set +// as the attribute "brim" to the layer. +// Additionally the attribute "outerBrim" is generated to make it more easy +// for other modifiers and renderers to clip with the brim to avoid overlapping. +// "outerBrim" just contains the outline of the brim (taking into account the line width also). +func NewBrimModifier(options *data.Options) handler.LayerModifier { + return &brimModifier{ + options: options, + } +} + +// Brim extracts the attribute "brim" from the layer. +// If it has the wrong type, a error is returned. +// If it doesn't exist, (nil, nil) is returned. +// If it exists, the infill is returned. +func Brim(layer data.PartitionedLayer) (clip.OffsetResult, error) { + if attr, ok := layer.Attributes()["brim"]; ok { + parts, ok := attr.(clip.OffsetResult) + if !ok { + return nil, fmt.Errorf("the attribute 'brim' has the wrong datatype") + } + + return parts, nil + } + + return nil, nil +} + +// BrimOuterDimension extracts the attribute "outerBrim" from the layer. +// This attribute describes the exact outer dimension of the brim. +// Can be clipped from other parts to avoid overlapping with the brim. +// +// If it has the wrong type, a error is returned. +// If it doesn't exist, (nil, nil) is returned. +// If it exists, the infill is returned. +func BrimOuterDimension(layer data.PartitionedLayer) ([]data.LayerPart, error) { + if attr, ok := layer.Attributes()["outerBrim"]; ok { + parts, ok := attr.([]data.LayerPart) + if !ok { + return nil, fmt.Errorf("the attribute 'outerbrim' has the wrong datatype") + } + + return parts, nil + } + + return nil, nil +} + +func (m brimModifier) Modify(layers []data.PartitionedLayer) error { + if m.options.Print.BrimSkirt.BrimCount == 0 { + return nil + } + + layer := layers[0] + + // Get the perimeters to base the brim on them. + perimeters, err := Perimeters(layer) + if err != nil { + return err + } + if perimeters == nil { + return nil + } + + // Extract the outer perimeters of all perimeters. + var allOuterPerimeters []data.LayerPart + + for _, part := range perimeters { + for _, wall := range part { + if len(wall) > 0 { + // wall[0] is the outer perimeter + allOuterPerimeters = append(allOuterPerimeters, wall[0]) + } + } + } + + cl := clip.NewClipper() + + // Get the top level polys e.g. the polygons which are not inside another. + topLevelPerimeters, _ := cl.TopLevelPolygons(allOuterPerimeters) + allOuterPerimeters = nil + for _, p := range topLevelPerimeters { + allOuterPerimeters = append(allOuterPerimeters, data.NewBasicLayerPart(p, nil)) + } + + if allOuterPerimeters == nil { + // No need to go further and prevent fail of union. + return nil + } + + // Generate the brim. + brim := cl.InsetLayer(allOuterPerimeters, -m.options.Printer.ExtrusionWidth, m.options.Print.BrimSkirt.BrimCount, m.options.Printer.ExtrusionWidth) + + // Now we need to generate the outer bounds of the brim (e.g. outer brim line + half line width) + // That is needed for the support, to remove the support at the places where the brim is. + var outerBrimLines []data.LayerPart + // For this we first get only the most outer brim lines. + for _, part := range brim { + if len(part) == 0 { + continue + } + for _, insetPart := range part[len(part)-1] { + outerBrimLines = append(outerBrimLines, insetPart) + } + } + + // Then the outer brim lines are exset so that the result matches the exact dimension taking into account the extrusion width. + outerBrim := cl.InsetLayer(outerBrimLines, -m.options.Printer.ExtrusionWidth, 1, m.options.Printer.ExtrusionWidth/2).ToOneDimension() + + newLayer := newExtendedLayer(layers[0]) + if len(brim) > 0 { + newLayer.attributes["brim"] = append(brim) + } + + if len(outerBrim) > 0 { + newLayer.attributes["outerBrim"] = outerBrim + } + + layers[0] = newLayer + + return nil +} diff --git a/modifier/perimeter.go b/modifier/perimeter.go index 49a6057..61aef17 100644 --- a/modifier/perimeter.go +++ b/modifier/perimeter.go @@ -60,7 +60,7 @@ func (m perimeterModifier) Modify(layers []data.PartitionedLayer) error { for layerNr := range layers { // Generate the perimeters. c := clip.NewClipper() - insetParts := c.InsetLayer(layers[layerNr].LayerParts(), m.options.Printer.ExtrusionWidth, m.options.Print.InsetCount) + insetParts := c.InsetLayer(layers[layerNr].LayerParts(), m.options.Printer.ExtrusionWidth, m.options.Print.InsetCount, -m.options.Printer.ExtrusionWidth/2) // Also generate the overlapping perimeter, which helps with calculating the infill. // This is derived from the most inner perimeters and offset by the options.Print.InfillOverlapPercent option. @@ -99,7 +99,7 @@ func calculateOverlapPerimeter(part data.LayerPart, overlapPercent int, extrusio if perimeterOverlap != 0 { c := clip.NewClipper() // As we use only one inset, just return index 0. - return c.Inset(part, perimeterOverlap, 1)[0], nil + return c.Inset(part, perimeterOverlap, 1, -perimeterOverlap/2)[0], nil } else { // If no overlap needed, just return the input part. return []data.LayerPart{part}, nil diff --git a/modifier/support.go b/modifier/support.go index f8af384..af673b3 100644 --- a/modifier/support.go +++ b/modifier/support.go @@ -13,6 +13,23 @@ import ( "math" ) +// FullSupport extracts the attribute "fullSupport" from the layer. +// If it has the wrong type, a error is returned. +// If it doesn't exist, (nil, nil) is returned. +// If it exists, the support areas are returned. +func FullSupport(layer data.PartitionedLayer) ([]data.LayerPart, error) { + if attr, ok := layer.Attributes()["fullSupport"]; ok { + fullSupport, ok := attr.([]data.LayerPart) + if !ok { + return nil, errors.New("the attribute fullSupport has the wrong datatype") + } + + return fullSupport, nil + } + + return nil, nil +} + type supportDetectorModifier struct { options *data.Options } @@ -68,7 +85,7 @@ func (m supportDetectorModifier) Modify(layers []data.PartitionedLayer) error { // offset layer by d cl := clip.NewClipper() - offsetLayer := cl.InsetLayer(layers[layerNr].LayerParts(), data.Micrometer(-math.Round(distance)), 1).ToOneDimension() + offsetLayer := cl.InsetLayer(layers[layerNr].LayerParts(), data.Micrometer(-math.Round(distance)), 1, -data.Micrometer(-math.Round(distance))/2).ToOneDimension() // subtract result from the next layer support, ok := cl.Difference(layers[layerNr+1].LayerParts(), offsetLayer) @@ -77,7 +94,7 @@ func (m supportDetectorModifier) Modify(layers []data.PartitionedLayer) error { } // make the support a little bit bigger to provide at least two lines on most places - support = cl.InsetLayer(support, -m.options.Print.Support.PatternSpacing.ToMicrometer()*3, 1).ToOneDimension() + support = cl.InsetLayer(support, -m.options.Print.Support.PatternSpacing.ToMicrometer()*3, 1, m.options.Print.Support.PatternSpacing.ToMicrometer()*3/2).ToOneDimension() // Save the result at the current layer minus TopGapLayers to skip the amount of TopGapLayers newLayer := newExtendedLayer(layers[layerNr-m.options.Print.Support.TopGapLayers]) @@ -144,7 +161,7 @@ func (m supportGeneratorModifier) Modify(layers []data.PartitionedLayer) error { } // make the layer a bit bigger to create a gap between the support and the model - biggerLayer := cl.InsetLayer(layers[layerNr-1].LayerParts(), -m.options.Print.Support.Gap.ToMicrometer(), 1).ToOneDimension() + biggerLayer := cl.InsetLayer(layers[layerNr-1].LayerParts(), -m.options.Print.Support.Gap.ToMicrometer(), 1, m.options.Print.Support.Gap.ToMicrometer()/2).ToOneDimension() // subtract the model from the result actualSupport, ok := cl.Difference(result, biggerLayer) @@ -180,6 +197,16 @@ func (m supportGeneratorModifier) Modify(layers []data.PartitionedLayer) error { if !ok { return errors.New("error while calculating the actual support without the interface parts") } + + // If there is any brim in this layer, remove it from the support to avoid overlapping. + brimArea, err := BrimOuterDimension(layers[layerNr-1]) + if err != nil { + return err + } + if brimArea != nil { + interfaceParts, ok = c.Difference(interfaceParts, brimArea) + actualWithoutInterfaceParts, ok = c.Difference(actualWithoutInterfaceParts, brimArea) + } } lastSupport = actualSupport diff --git a/slicer.go b/slicer.go index 4f305c6..1b46cdd 100644 --- a/slicer.go +++ b/slicer.go @@ -45,6 +45,7 @@ func NewGoSlice(options data.Options) *GoSlice { modifier.NewPerimeterModifier(&options), modifier.NewInfillModifier(&options), modifier.NewInternalInfillModifier(&options), + modifier.NewBrimModifier(&options), modifier.NewSupportDetectorModifier(&options), modifier.NewSupportGeneratorModifier(&options), } @@ -54,6 +55,8 @@ func NewGoSlice(options data.Options) *GoSlice { 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. diff --git a/slicer_test.go b/slicer_test.go index 20d54cd..9fc596e 100644 --- a/slicer_test.go +++ b/slicer_test.go @@ -28,6 +28,7 @@ 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 {