Former/pattern.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")
}