Skip to content

Commit

Permalink
Skirt & Brim (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
aligator authored Aug 23, 2020
1 parent 3f831f2 commit 3b95897
Show file tree
Hide file tree
Showing 18 changed files with 490 additions and 66 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Supported features:
* simple retraction on crossing perimeters
* several options to customize slicing output
* simple support generation
* brim and skirt

Example:
<img width="200" alt="sliced Gopher logo" src="https://raw.githubusercontent.com/aligator/GoSlice/master/docs/GoSlice-print.png">
Expand Down
127 changes: 110 additions & 17 deletions clip/clip.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -202,29 +256,33 @@ 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()
co.AddPaths(clipperPaths(data.Paths{part.Outline()}), clipper.JtSquare, clipper.EtClosedPolygon)
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
Expand Down Expand Up @@ -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
}
19 changes: 19 additions & 0 deletions data/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
76 changes: 50 additions & 26 deletions data/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -195,6 +169,8 @@ type PrintOptions struct {
NumberTopLayers int

Support SupportOptions

BrimSkirt BrimSkirtOptions
}

// FilamentOptions contains all Filament specific GoSlice options.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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.")
Expand Down
8 changes: 5 additions & 3 deletions gcode/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions gcode/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 3b95897

Please sign in to comment.