package main import ( "fmt" "math" "strings" ) // GenerateStructuralSCAD produces an OpenSCAD source string that extrudes // the SVG outline filled with a structural pattern. func GenerateStructuralSCADString(session *StructuralSession) (string, error) { if session == nil || session.SVGDoc == nil { return "", fmt.Errorf("no structural session") } outline := extractOutlinePolygon(session.SVGDoc) if len(outline) < 3 { return "", fmt.Errorf("SVG has no usable outline polygon (need at least 3 points)") } var b strings.Builder b.WriteString("// Structural Fill — Generated by Former\n") b.WriteString(fmt.Sprintf("// Pattern: %s, Cell: %.1fmm, Wall: %.1fmm, Height: %.1fmm\n\n", session.Pattern, session.CellSize, session.WallThick, session.Height)) // Write outline polygon module writePolygonModule(&b, "outline_shape", outline) // Write pattern module bbox := polygonBBox(outline) switch session.Pattern { case "hexagon": writeHexPattern(&b, bbox, session.CellSize, session.WallThick) case "triangle": writeTrianglePattern(&b, bbox, session.CellSize, session.WallThick) case "diamond": writeDiamondPattern(&b, bbox, session.CellSize, session.WallThick) case "grid": writeGridPattern(&b, bbox, session.CellSize, session.WallThick) case "gyroid": writeGyroidPattern(&b, bbox, session.CellSize, session.WallThick) default: writeHexPattern(&b, bbox, session.CellSize, session.WallThick) } // Assembly: shell + infill, extruded shellT := session.ShellThick if shellT <= 0 { shellT = session.WallThick } b.WriteString(fmt.Sprintf("\n// Assembly\nlinear_extrude(height = %.2f, convexity = 10) {\n", session.Height)) b.WriteString(fmt.Sprintf(" // Outer shell\n difference() {\n outline_shape();\n offset(r = -%.2f) outline_shape();\n }\n", shellT)) b.WriteString(" // Internal pattern clipped to outline\n intersection() {\n outline_shape();\n fill_pattern();\n }\n") b.WriteString("}\n") return b.String(), nil } // extractOutlinePolygon pulls the largest closed polygon from the SVG document. // It flattens all elements' segments into point sequences and picks the one // with the most points that forms a closed path. func extractOutlinePolygon(doc *SVGDocument) [][2]float64 { var best [][2]float64 for _, el := range doc.Elements { pts := segmentsToPoints(el.Segments, el.Transform) if len(pts) > len(best) { best = pts } } return best } // segmentsToPoints flattens path segments into a coordinate list, // applying the element transform. func segmentsToPoints(segs []PathSegment, xf [6]float64) [][2]float64 { var pts [][2]float64 transform := func(x, y float64) (float64, float64) { return xf[0]*x + xf[2]*y + xf[4], xf[1]*x + xf[3]*y + xf[5] } for _, seg := range segs { switch seg.Command { case 'M', 'L': if len(seg.Args) >= 2 { tx, ty := transform(seg.Args[0], seg.Args[1]) pts = append(pts, [2]float64{tx, ty}) } case 'C': if len(seg.Args) >= 6 { // Sample cubic bezier at endpoints + midpoint tx, ty := transform(seg.Args[4], seg.Args[5]) pts = append(pts, [2]float64{tx, ty}) } case 'Q', 'S': if len(seg.Args) >= 4 { tx, ty := transform(seg.Args[2], seg.Args[3]) pts = append(pts, [2]float64{tx, ty}) } case 'A': if len(seg.Args) >= 7 { tx, ty := transform(seg.Args[5], seg.Args[6]) pts = append(pts, [2]float64{tx, ty}) } case 'T': if len(seg.Args) >= 2 { tx, ty := transform(seg.Args[0], seg.Args[1]) pts = append(pts, [2]float64{tx, ty}) } case 'Z': // Close path — no new point needed } } return pts } func polygonBBox(poly [][2]float64) [4]float64 { if len(poly) == 0 { return [4]float64{} } minX, minY := poly[0][0], poly[0][1] maxX, maxY := minX, minY for _, p := range poly[1:] { if p[0] < minX { minX = p[0] } if p[0] > maxX { maxX = p[0] } if p[1] < minY { minY = p[1] } if p[1] > maxY { maxY = p[1] } } return [4]float64{minX, minY, maxX, maxY} } func writePolygonModule(b *strings.Builder, name string, poly [][2]float64) { b.WriteString(fmt.Sprintf("module %s() {\n polygon(points=[\n", name)) for i, p := range poly { comma := "," if i == len(poly)-1 { comma = "" } b.WriteString(fmt.Sprintf(" [%.4f, %.4f]%s\n", p[0], p[1], comma)) } b.WriteString(" ]);\n}\n\n") } // Hexagonal honeycomb pattern func writeHexPattern(b *strings.Builder, bbox [4]float64, cellSize, wallThick float64) { r := cellSize / 2.0 ri := r - wallThick/2.0 if ri < 0.1 { ri = 0.1 } dx := cellSize * 1.5 dy := cellSize * math.Sqrt(3) / 2.0 b.WriteString("module fill_pattern() {\n") b.WriteString(fmt.Sprintf(" r = %.4f;\n", r)) b.WriteString(fmt.Sprintf(" ri = %.4f;\n", ri)) b.WriteString(fmt.Sprintf(" wall = %.4f;\n", wallThick)) b.WriteString(" difference() {\n") b.WriteString(" union() {\n") startX := bbox[0] - cellSize startY := bbox[1] - cellSize endX := bbox[2] + cellSize endY := bbox[3] + cellSize row := 0 for y := startY; y <= endY; y += dy { offsetX := 0.0 if row%2 == 1 { offsetX = cellSize * 0.75 } for x := startX + offsetX; x <= endX; x += dx { b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) circle(r=r, $fn=6);\n", x, y)) } row++ } b.WriteString(" }\n") b.WriteString(" // Subtract smaller hexagons to create walls\n") b.WriteString(" union() {\n") row = 0 for y := startY; y <= endY; y += dy { offsetX := 0.0 if row%2 == 1 { offsetX = cellSize * 0.75 } for x := startX + offsetX; x <= endX; x += dx { b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) circle(r=ri, $fn=6);\n", x, y)) } row++ } b.WriteString(" }\n") b.WriteString(" }\n") b.WriteString("}\n\n") } // Triangle grid pattern func writeTrianglePattern(b *strings.Builder, bbox [4]float64, cellSize, wallThick float64) { b.WriteString("module fill_pattern() {\n") b.WriteString(fmt.Sprintf(" cell = %.4f;\n", cellSize)) b.WriteString(fmt.Sprintf(" wall = %.4f;\n", wallThick)) // Horizontal lines startY := bbox[1] - cellSize endY := bbox[3] + cellSize h := cellSize * math.Sqrt(3) / 2.0 b.WriteString(" union() {\n") for y := startY; y <= endY; y += h { b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) square([%.4f, wall]);\n", bbox[0]-cellSize, y-wallThick/2, bbox[2]-bbox[0]+2*cellSize)) } // Diagonal lines (60 degrees) for x := bbox[0] - (bbox[3]-bbox[1])*2; x <= bbox[2]+(bbox[3]-bbox[1])*2; x += cellSize { b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) rotate(60) square([wall, %.4f]);\n", x, bbox[1]-cellSize, (bbox[3]-bbox[1]+2*cellSize)*2)) b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) rotate(-60) square([wall, %.4f]);\n", x, bbox[1]-cellSize, (bbox[3]-bbox[1]+2*cellSize)*2)) } b.WriteString(" }\n") b.WriteString("}\n\n") } // Diamond/rhombus lattice func writeDiamondPattern(b *strings.Builder, bbox [4]float64, cellSize, wallThick float64) { b.WriteString("module fill_pattern() {\n") b.WriteString(fmt.Sprintf(" cell = %.4f;\n", cellSize)) b.WriteString(fmt.Sprintf(" wall = %.4f;\n", wallThick)) extentX := bbox[2] - bbox[0] + 2*cellSize extentY := bbox[3] - bbox[1] + 2*cellSize diag := math.Sqrt(extentX*extentX + extentY*extentY) b.WriteString(" union() {\n") // 45-degree lines in both directions for d := -diag; d <= diag; d += cellSize { cx := (bbox[0] + bbox[2]) / 2.0 cy := (bbox[1] + bbox[3]) / 2.0 b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) rotate(45) translate([0, %.4f]) square([wall, %.4f], center=true);\n", cx, cy, d, diag*2)) b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) rotate(-45) translate([0, %.4f]) square([wall, %.4f], center=true);\n", cx, cy, d, diag*2)) } b.WriteString(" }\n") b.WriteString("}\n\n") } // Simple rectangular grid func writeGridPattern(b *strings.Builder, bbox [4]float64, cellSize, wallThick float64) { b.WriteString("module fill_pattern() {\n") b.WriteString(fmt.Sprintf(" cell = %.4f;\n", cellSize)) b.WriteString(fmt.Sprintf(" wall = %.4f;\n", wallThick)) b.WriteString(" union() {\n") for x := bbox[0] - cellSize; x <= bbox[2]+cellSize; x += cellSize { b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) square([wall, %.4f]);\n", x-wallThick/2, bbox[1]-cellSize, bbox[3]-bbox[1]+2*cellSize)) } for y := bbox[1] - cellSize; y <= bbox[3]+cellSize; y += cellSize { b.WriteString(fmt.Sprintf(" translate([%.4f, %.4f]) square([%.4f, wall]);\n", bbox[0]-cellSize, y-wallThick/2, bbox[2]-bbox[0]+2*cellSize)) } b.WriteString(" }\n") b.WriteString("}\n\n") } // Gyroid approximation (sinusoidal cross-section) func writeGyroidPattern(b *strings.Builder, bbox [4]float64, cellSize, wallThick float64) { b.WriteString("module fill_pattern() {\n") b.WriteString(fmt.Sprintf(" wall = %.4f;\n", wallThick)) // Approximate gyroid cross-section with a dense polygon path of sine waves // Two perpendicular sets of sinusoidal walls period := cellSize amplitude := cellSize / 2.0 steps := 40 b.WriteString(" union() {\n") // Horizontal sine waves for baseY := bbox[1] - cellSize; baseY <= bbox[3]+cellSize; baseY += period { b.WriteString(" polygon(points=[") // Forward path (top edge) for i := 0; i <= steps; i++ { t := float64(i) / float64(steps) x := bbox[0] - cellSize + t*(bbox[2]-bbox[0]+2*cellSize) y := baseY + amplitude*math.Sin(2*math.Pi*t*(bbox[2]-bbox[0]+2*cellSize)/period) + wallThick/2 b.WriteString(fmt.Sprintf("[%.4f,%.4f],", x, y)) } // Reverse path (bottom edge) for i := steps; i >= 0; i-- { t := float64(i) / float64(steps) x := bbox[0] - cellSize + t*(bbox[2]-bbox[0]+2*cellSize) y := baseY + amplitude*math.Sin(2*math.Pi*t*(bbox[2]-bbox[0]+2*cellSize)/period) - wallThick/2 b.WriteString(fmt.Sprintf("[%.4f,%.4f],", x, y)) } b.WriteString("]);\n") } // Vertical sine waves for baseX := bbox[0] - cellSize; baseX <= bbox[2]+cellSize; baseX += period { b.WriteString(" polygon(points=[") for i := 0; i <= steps; i++ { t := float64(i) / float64(steps) y := bbox[1] - cellSize + t*(bbox[3]-bbox[1]+2*cellSize) x := baseX + amplitude*math.Sin(2*math.Pi*t*(bbox[3]-bbox[1]+2*cellSize)/period) + wallThick/2 b.WriteString(fmt.Sprintf("[%.4f,%.4f],", x, y)) } for i := steps; i >= 0; i-- { t := float64(i) / float64(steps) y := bbox[1] - cellSize + t*(bbox[3]-bbox[1]+2*cellSize) x := baseX + amplitude*math.Sin(2*math.Pi*t*(bbox[3]-bbox[1]+2*cellSize)/period) - wallThick/2 b.WriteString(fmt.Sprintf("[%.4f,%.4f],", x, y)) } b.WriteString("]);\n") } b.WriteString(" }\n") b.WriteString("}\n\n") }