449 lines
11 KiB
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()
|
|
}
|