Former/mesh_unwrap.go

449 lines
11 KiB
Go

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(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %.2f %.2f" width="%.2fmm" height="%.2fmm">`, w, h, w, h))
b.WriteString("\n<style>\n")
b.WriteString(" .panel { fill: none; stroke: #333; stroke-width: 0.3; }\n")
b.WriteString(" .fold-line { stroke: #aaa; stroke-width: 0.15; stroke-dasharray: 2,1; }\n")
b.WriteString(" .cut-line { stroke: #333; stroke-width: 0.4; }\n")
b.WriteString("</style>\n")
// White background for printing
b.WriteString(fmt.Sprintf(`<rect width="%.2f" height="%.2f" fill="white"/>`, w, h))
b.WriteString("\n")
// Triangles
for _, t := range result.Triangles {
b.WriteString(fmt.Sprintf(`<polygon class="panel" points="%.4f,%.4f %.4f,%.4f %.4f,%.4f"/>`,
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(`<line class="fold-line" x1="%.4f" y1="%.4f" x2="%.4f" y2="%.4f"/>`,
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(`<line class="cut-line" x1="%.4f" y1="%.4f" x2="%.4f" y2="%.4f"/>`,
e[0].X, e[0].Y, e[1].X, e[1].Y))
b.WriteString("\n")
}
b.WriteString("</svg>\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()
}