334 lines
10 KiB
Go
334 lines
10 KiB
Go
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")
|
|
}
|