package main import ( "fmt" "math" "strings" ) type meshVertex struct{ X, Y, Z float64 } type meshTri struct{ V [3]int } type meshEdge struct{ V0, V1 int } // canonical: V0 < V1 type flatVert struct{ X, Y float64 } type flatTri struct { V [3]flatVert TriIdx int } type meshUnwrapResult struct { Triangles []flatTri FoldEdges [][2]flatVert CutEdges [][2]flatVert BoundsW float64 BoundsH float64 } func canonEdge(a, b int) meshEdge { if a > b { a, b = b, a } return meshEdge{a, b} } // indexMesh merges coincident vertices and returns an indexed representation. func indexMesh(triangles [][3]Point) ([]meshVertex, []meshTri) { const eps = 1e-6 const gridSize = 1e-5 type gridKey struct{ ix, iy, iz int64 } buckets := make(map[gridKey][]int) var verts []meshVertex quantize := func(v float64) int64 { return int64(math.Floor(v / gridSize)) } findOrAdd := func(x, y, z float64) int { gk := gridKey{quantize(x), quantize(y), quantize(z)} // Check 27 neighboring cells for dx := int64(-1); dx <= 1; dx++ { for dy := int64(-1); dy <= 1; dy++ { for dz := int64(-1); dz <= 1; dz++ { nk := gridKey{gk.ix + dx, gk.iy + dy, gk.iz + dz} for _, idx := range buckets[nk] { v := verts[idx] d := (v.X-x)*(v.X-x) + (v.Y-y)*(v.Y-y) + (v.Z-z)*(v.Z-z) if d < eps*eps { return idx } } } } } idx := len(verts) verts = append(verts, meshVertex{x, y, z}) buckets[gk] = append(buckets[gk], idx) return idx } rawVerts := len(triangles) * 3 tris := make([]meshTri, len(triangles)) for i, t := range triangles { tris[i].V[0] = findOrAdd(t[0].X, t[0].Y, t[0].Z) tris[i].V[1] = findOrAdd(t[1].X, t[1].Y, t[1].Z) tris[i].V[2] = findOrAdd(t[2].X, t[2].Y, t[2].Z) } debugLog(" indexMesh: %d raw vertices -> %d merged, %d triangles", rawVerts, len(verts), len(tris)) return verts, tris } // buildAdjacency maps each edge to the triangle indices that share it. func buildAdjacency(tris []meshTri) map[meshEdge][]int { adj := make(map[meshEdge][]int) for i, t := range tris { for e := 0; e < 3; e++ { edge := canonEdge(t.V[e], t.V[(e+1)%3]) adj[edge] = append(adj[edge], i) } } manifold, boundary := 0, 0 for _, v := range adj { if len(v) == 2 { manifold++ } else if len(v) == 1 { boundary++ } } debugLog(" buildAdjacency: %d edges, %d manifold, %d boundary", len(adj), manifold, boundary) return adj } func dist3D(verts []meshVertex, a, b int) float64 { dx := verts[b].X - verts[a].X dy := verts[b].Y - verts[a].Y dz := verts[b].Z - verts[a].Z return math.Sqrt(dx*dx + dy*dy + dz*dz) } func triArea2D(a, b, c flatVert) float64 { return 0.5 * ((b.X-a.X)*(c.Y-a.Y) - (c.X-a.X)*(b.Y-a.Y)) } // placeThirdVertex positions a point given two known 2D points and // the 3D distances from each to the unknown point. // side: +1 or -1 to select which side of the edge. func placeThirdVertex(p0, p1 flatVert, d0, d1 float64, side float64) flatVert { dx := p1.X - p0.X dy := p1.Y - p0.Y d := math.Sqrt(dx*dx + dy*dy) if d < 1e-12 { return p0 } a := (d0*d0 - d1*d1 + d*d) / (2 * d) hSq := d0*d0 - a*a if hSq < 0 { hSq = 0 } h := math.Sqrt(hSq) mx := p0.X + a*dx/d my := p0.Y + a*dy/d return flatVert{ X: mx + side*h*dy/d, Y: my - side*h*dx/d, } } // unfoldMesh performs BFS tree-based greedy unfolding of a triangle mesh. func unfoldMesh(verts []meshVertex, tris []meshTri, adj map[meshEdge][]int) *meshUnwrapResult { n := len(tris) if n == 0 { return &meshUnwrapResult{} } debugLog(" unfoldMesh: %d triangles, %d vertices", n, len(verts)) placed := make([]bool, n) flat := make([]flatTri, n) // Track which edges are in the spanning tree treeEdges := make(map[meshEdge]bool) type bfsItem struct { triIdx int parentIdx int sharedE meshEdge } var result meshUnwrapResult islandOffsetX := 0.0 islandCount := 0 for seed := 0; seed < n; seed++ { if placed[seed] { continue } islandCount++ // Place the seed triangle t := tris[seed] d01 := dist3D(verts, t.V[0], t.V[1]) d02 := dist3D(verts, t.V[0], t.V[2]) d12 := dist3D(verts, t.V[1], t.V[2]) // Skip degenerate triangles if d01 < 1e-12 || d02 < 1e-12 || d12 < 1e-12 { debugLog(" degenerate seed triangle %d, skipping", seed) placed[seed] = true continue } flat[seed].TriIdx = seed flat[seed].V[0] = flatVert{islandOffsetX, 0} flat[seed].V[1] = flatVert{islandOffsetX + d01, 0} flat[seed].V[2] = placeThirdVertex(flat[seed].V[0], flat[seed].V[1], d02, d12, 1) placed[seed] = true queue := []bfsItem{} // Enqueue neighbors of seed for e := 0; e < 3; e++ { edge := canonEdge(t.V[e], t.V[(e+1)%3]) for _, nb := range adj[edge] { if nb != seed && !placed[nb] { queue = append(queue, bfsItem{nb, seed, edge}) } } } for len(queue) > 0 { item := queue[0] queue = queue[1:] if placed[item.triIdx] { continue } ct := tris[item.triIdx] // Find the shared edge vertex indices in the child triangle sharedA, sharedB := item.sharedE.V0, item.sharedE.V1 childThirdVert := -1 for _, vi := range ct.V { if vi != sharedA && vi != sharedB { childThirdVert = vi break } } if childThirdVert < 0 { continue } // Find 2D positions of shared edge from parent pt := tris[item.parentIdx] pf := flat[item.parentIdx] var p2dA, p2dB flatVert var parentThird flatVert for vi := 0; vi < 3; vi++ { if pt.V[vi] == sharedA { p2dA = pf.V[vi] } if pt.V[vi] == sharedB { p2dB = pf.V[vi] } if pt.V[vi] != sharedA && pt.V[vi] != sharedB { parentThird = pf.V[vi] } } // Determine which side of the edge the parent's third vertex is on cross := (p2dB.X-p2dA.X)*(parentThird.Y-p2dA.Y) - (p2dB.Y-p2dA.Y)*(parentThird.X-p2dA.X) side := -1.0 if cross < 0 { side = 1.0 } dCA := dist3D(verts, childThirdVert, sharedA) dCB := dist3D(verts, childThirdVert, sharedB) if dCA < 1e-12 || dCB < 1e-12 { placed[item.triIdx] = true continue } newVert := placeThirdVertex(p2dA, p2dB, dCA, dCB, side) // Assign 2D positions to child triangle vertices flat[item.triIdx].TriIdx = item.triIdx for vi := 0; vi < 3; vi++ { if ct.V[vi] == sharedA { flat[item.triIdx].V[vi] = p2dA } else if ct.V[vi] == sharedB { flat[item.triIdx].V[vi] = p2dB } else { flat[item.triIdx].V[vi] = newVert } } placed[item.triIdx] = true treeEdges[item.sharedE] = true // Enqueue neighbors for e := 0; e < 3; e++ { edge := canonEdge(ct.V[e], ct.V[(e+1)%3]) for _, nb := range adj[edge] { if !placed[nb] { queue = append(queue, bfsItem{nb, item.triIdx, edge}) } } } } // Compute island bounds for offset minX, maxX := math.Inf(1), math.Inf(-1) for i := 0; i < n; i++ { if !placed[i] { continue } for _, v := range flat[i].V { if v.X < minX { minX = v.X } if v.X > maxX { maxX = v.X } } } if maxX > islandOffsetX { islandOffsetX = maxX + 10.0 } } debugLog(" unfoldMesh: %d islands", islandCount) // Collect placed triangles and classify edges minX, minY := math.Inf(1), math.Inf(1) maxX, maxY := math.Inf(-1), math.Inf(-1) for i := 0; i < n; i++ { if flat[i].V[0] == (flatVert{}) && flat[i].V[1] == (flatVert{}) && flat[i].V[2] == (flatVert{}) { // Unplaced degenerate triangle continue } result.Triangles = append(result.Triangles, flat[i]) for _, v := range flat[i].V { if v.X < minX { minX = v.X } if v.X > maxX { maxX = v.X } if v.Y < minY { minY = v.Y } if v.Y > maxY { maxY = v.Y } } } // Classify edges for edge, triList := range adj { if len(triList) < 1 { continue } // Find 2D positions for this edge from first triangle that has it placed var eA, eB flatVert found := false for _, ti := range triList { t := tris[ti] f := flat[ti] for vi := 0; vi < 3; vi++ { ni := (vi + 1) % 3 if canonEdge(t.V[vi], t.V[ni]) == edge { eA = f.V[vi] eB = f.V[ni] found = true break } } if found { break } } if !found { continue } if len(triList) == 1 || !treeEdges[edge] { result.CutEdges = append(result.CutEdges, [2]flatVert{eA, eB}) } else if treeEdges[edge] { result.FoldEdges = append(result.FoldEdges, [2]flatVert{eA, eB}) } } // Translate to positive quadrant with margin margin := 5.0 offsetX := margin - minX offsetY := margin - minY for i := range result.Triangles { for vi := range result.Triangles[i].V { result.Triangles[i].V[vi].X += offsetX result.Triangles[i].V[vi].Y += offsetY } } for i := range result.FoldEdges { for vi := range result.FoldEdges[i] { result.FoldEdges[i][vi].X += offsetX result.FoldEdges[i][vi].Y += offsetY } } for i := range result.CutEdges { for vi := range result.CutEdges[i] { result.CutEdges[i][vi].X += offsetX result.CutEdges[i][vi].Y += offsetY } } result.BoundsW = (maxX - minX) + 2*margin result.BoundsH = (maxY - minY) + 2*margin debugLog(" unfoldMesh: %d placed triangles, %d fold edges, %d cut edges, bounds %.1f x %.1f mm", len(result.Triangles), len(result.FoldEdges), len(result.CutEdges), result.BoundsW, result.BoundsH) return &result } // GenerateMeshUnwrapSVG produces an SVG string from the unfolded mesh. func GenerateMeshUnwrapSVG(result *meshUnwrapResult) string { var b strings.Builder w := result.BoundsW h := result.BoundsH if w < 1 { w = 100 } if h < 1 { h = 100 } b.WriteString(fmt.Sprintf(``, w, h, w, h)) b.WriteString("\n\n") // White background for printing b.WriteString(fmt.Sprintf(``, w, h)) b.WriteString("\n") // Triangles for _, t := range result.Triangles { b.WriteString(fmt.Sprintf(``, t.V[0].X, t.V[0].Y, t.V[1].X, t.V[1].Y, t.V[2].X, t.V[2].Y)) b.WriteString("\n") } // Fold edges for _, e := range result.FoldEdges { b.WriteString(fmt.Sprintf(``, e[0].X, e[0].Y, e[1].X, e[1].Y)) b.WriteString("\n") } // Cut edges for _, e := range result.CutEdges { b.WriteString(fmt.Sprintf(``, e[0].X, e[0].Y, e[1].X, e[1].Y)) b.WriteString("\n") } b.WriteString("\n") debugLog("GenerateMeshUnwrapSVG: %d triangles, %d fold, %d cut edges, %.0f x %.0f mm", len(result.Triangles), len(result.FoldEdges), len(result.CutEdges), w, h) return b.String() }