Former/face_reconstruct_test.go

382 lines
9.8 KiB
Go

package main
import (
"math"
"os"
"path/filepath"
"strings"
"testing"
)
func TestSub3(t *testing.T) {
r := sub3([3]float64{3, 5, 7}, [3]float64{1, 2, 3})
if r != [3]float64{2, 3, 4} {
t.Errorf("sub3 wrong: %v", r)
}
}
func TestCross3(t *testing.T) {
x := [3]float64{1, 0, 0}
y := [3]float64{0, 1, 0}
z := cross3(x, y)
if z != [3]float64{0, 0, 1} {
t.Errorf("cross3(x,y) should be z, got %v", z)
}
}
func TestNorm3(t *testing.T) {
n := norm3([3]float64{3, 4, 0})
if math.Abs(n-5) > 1e-9 {
t.Errorf("expected 5, got %f", n)
}
}
func TestScale3(t *testing.T) {
r := scale3([3]float64{2, 3, 4}, 0.5)
if r != [3]float64{1, 1.5, 2} {
t.Errorf("scale3 wrong: %v", r)
}
}
func TestNormalize3(t *testing.T) {
n := normalize3([3]float64{0, 0, 5})
if math.Abs(n[2]-1) > 1e-9 || math.Abs(n[0]) > 1e-9 || math.Abs(n[1]) > 1e-9 {
t.Errorf("normalize3 wrong: %v", n)
}
}
func TestNormalize3Zero(t *testing.T) {
n := normalize3([3]float64{0, 0, 0})
if n != [3]float64{0, 0, 1} {
t.Errorf("normalize3 zero vector should return {0,0,1}, got %v", n)
}
}
func TestComputeFaceNormal(t *testing.T) {
verts := [][3]float64{{0, 0, 0}, {1, 0, 0}, {0, 1, 0}}
n := computeFaceNormal(verts)
if math.Abs(n[2]-1) > 1e-9 {
t.Errorf("XY triangle normal should be +Z, got %v", n)
}
}
func TestComputeFaceNormalDegenerate(t *testing.T) {
n := computeFaceNormal([][3]float64{{0, 0, 0}, {1, 0, 0}})
if n != [3]float64{0, 0, 1} {
t.Errorf("degenerate face normal should default to {0,0,1}, got %v", n)
}
}
func TestReconstructSingleFace(t *testing.T) {
faces := []TracedFace{{
FaceNum: 1,
Outline: [][2]float64{{0, 0}, {50, 0}, {50, 30}, {0, 30}},
}}
obj, err := ReconstructFromFaces(faces, nil)
if err != nil {
t.Fatal(err)
}
if len(obj.Vertices3D) != 4 {
t.Errorf("expected 4 vertices, got %d", len(obj.Vertices3D))
}
if len(obj.FaceIndices) != 1 {
t.Errorf("expected 1 face index list, got %d", len(obj.FaceIndices))
}
if obj.SCAD == "" {
t.Error("SCAD should not be empty")
}
}
func TestReconstructEmptyFaces(t *testing.T) {
_, err := ReconstructFromFaces(nil, nil)
if err == nil {
t.Error("expected error for empty faces")
}
}
func TestBuildFlatSpacing(t *testing.T) {
faces := []TracedFace{
{FaceNum: 1, Outline: [][2]float64{{0, 0}, {20, 0}, {20, 10}, {0, 10}}},
{FaceNum: 2, Outline: [][2]float64{{0, 0}, {30, 0}, {30, 15}, {0, 15}}},
}
obj, err := ReconstructFromFaces(faces, nil)
if err != nil {
t.Fatal(err)
}
if len(obj.Vertices3D) != 8 {
t.Errorf("expected 8 vertices, got %d", len(obj.Vertices3D))
}
// All z coordinates should be 0 (flat)
for i, v := range obj.Vertices3D {
if math.Abs(v[2]) > 1e-9 {
t.Errorf("vertex %d z should be 0, got %f", i, v[2])
}
}
// Second face should be offset in X
face2Start := obj.Vertices3D[4]
if face2Start[0] < 20 {
t.Errorf("second face X offset should be >= 20, got %f", face2Start[0])
}
}
func TestFoldTwoFaces(t *testing.T) {
faces := []TracedFace{
{FaceNum: 1, Outline: [][2]float64{{0, 0}, {40, 0}, {40, 20}, {0, 20}}},
{FaceNum: 2, Outline: [][2]float64{{0, 0}, {40, 0}, {40, 25}, {0, 25}}},
}
adj := FaceAdjacency{
1: []EdgeLink{{Neighbor: 2, EdgeIdx: 2, NeighborEdge: 0}},
2: []EdgeLink{{Neighbor: 1, EdgeIdx: 0, NeighborEdge: 2}},
}
obj, err := ReconstructFromFaces(faces, adj)
if err != nil {
t.Fatal(err)
}
if len(obj.FaceIndices) != 2 {
t.Errorf("expected 2 face index lists, got %d", len(obj.FaceIndices))
}
// Face 1 should be flat on z=0
for _, idx := range obj.FaceIndices[0] {
if math.Abs(obj.Vertices3D[idx][2]) > 1e-6 {
t.Errorf("face 1 vertex should be at z=0, got z=%f", obj.Vertices3D[idx][2])
}
}
// Face 2 should have some vertices NOT at z=0 (folded)
allZero := true
for _, idx := range obj.FaceIndices[1] {
if math.Abs(obj.Vertices3D[idx][2]) > 1e-6 {
allZero = false
break
}
}
if allZero {
t.Error("face 2 should be folded (not all z=0)")
}
}
func TestFoldBoxSixFaces(t *testing.T) {
w, h, d := 40.0, 30.0, 20.0
faces := []TracedFace{
{FaceNum: 1, Outline: [][2]float64{{0, 0}, {w, 0}, {w, h}, {0, h}}}, // bottom
{FaceNum: 2, Outline: [][2]float64{{0, 0}, {w, 0}, {w, d}, {0, d}}}, // front
{FaceNum: 3, Outline: [][2]float64{{0, 0}, {w, 0}, {w, d}, {0, d}}}, // back
{FaceNum: 4, Outline: [][2]float64{{0, 0}, {h, 0}, {h, d}, {0, d}}}, // left
{FaceNum: 5, Outline: [][2]float64{{0, 0}, {h, 0}, {h, d}, {0, d}}}, // right
{FaceNum: 6, Outline: [][2]float64{{0, 0}, {w, 0}, {w, h}, {0, h}}}, // top
}
// Bottom edges: 0=bottom(0→1), 1=right(1→2), 2=top(2→3), 3=left(3→0)
adj := FaceAdjacency{
1: []EdgeLink{
{Neighbor: 2, EdgeIdx: 0, NeighborEdge: 0},
{Neighbor: 3, EdgeIdx: 2, NeighborEdge: 0},
{Neighbor: 5, EdgeIdx: 1, NeighborEdge: 0},
{Neighbor: 4, EdgeIdx: 3, NeighborEdge: 0},
},
}
obj, err := ReconstructFromFaces(faces, adj)
if err != nil {
t.Fatal(err)
}
if len(obj.FaceIndices) != 6 {
t.Errorf("expected 6 face index lists, got %d", len(obj.FaceIndices))
}
if obj.SCAD == "" {
t.Error("SCAD should not be empty")
}
}
func TestGenerateSCADContents(t *testing.T) {
faces := []TracedFace{
{FaceNum: 1, Outline: [][2]float64{{0, 0}, {10, 0}, {10, 10}, {0, 10}}},
}
obj, err := ReconstructFromFaces(faces, nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(obj.SCAD, "module face_1()") {
t.Error("SCAD should contain face_1 module")
}
if !strings.Contains(obj.SCAD, "polygon(") {
t.Error("SCAD should contain polygon")
}
if !strings.Contains(obj.SCAD, "linear_extrude(") {
t.Error("SCAD without adjacency should use linear_extrude")
}
}
func TestGenerateReconstructedSCADFile(t *testing.T) {
faces := []TracedFace{
{FaceNum: 1, Outline: [][2]float64{{0, 0}, {10, 0}, {10, 10}, {0, 10}}},
}
obj, err := ReconstructFromFaces(faces, nil)
if err != nil {
t.Fatal(err)
}
tmpDir := t.TempDir()
path, err := GenerateReconstructedSCAD(obj, tmpDir)
if err != nil {
t.Fatal(err)
}
if filepath.Base(path) != "reconstructed.scad" {
t.Errorf("expected reconstructed.scad, got %s", filepath.Base(path))
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if len(data) == 0 {
t.Error("output file should not be empty")
}
if !strings.Contains(string(data), "face_1") {
t.Error("output should reference face_1")
}
}
func TestValidate3N6Cube(t *testing.T) {
faces := make([]TracedFace, 6)
for i := range faces {
faces[i] = TracedFace{
FaceNum: i + 1,
Outline: [][2]float64{{0, 0}, {10, 0}, {10, 10}, {0, 10}},
}
}
// Cube adjacency: 12 edges
adj := FaceAdjacency{
1: []EdgeLink{{Neighbor: 2, EdgeIdx: 0, NeighborEdge: 0}},
2: []EdgeLink{{Neighbor: 1, EdgeIdx: 0, NeighborEdge: 0}, {Neighbor: 3, EdgeIdx: 1, NeighborEdge: 0}},
3: []EdgeLink{{Neighbor: 2, EdgeIdx: 0, NeighborEdge: 1}, {Neighbor: 4, EdgeIdx: 1, NeighborEdge: 0}},
4: []EdgeLink{{Neighbor: 3, EdgeIdx: 0, NeighborEdge: 1}, {Neighbor: 5, EdgeIdx: 1, NeighborEdge: 0}},
5: []EdgeLink{{Neighbor: 4, EdgeIdx: 0, NeighborEdge: 1}, {Neighbor: 6, EdgeIdx: 1, NeighborEdge: 0}},
6: []EdgeLink{{Neighbor: 5, EdgeIdx: 0, NeighborEdge: 1}},
}
ok, msg := Validate3N6(faces, adj)
if !ok {
t.Errorf("6-face cube should be sufficiently constrained: %s", msg)
}
}
func TestValidate3N6TooFewFaces(t *testing.T) {
faces := []TracedFace{
{FaceNum: 1, Outline: [][2]float64{{0, 0}, {10, 0}, {10, 10}}},
}
ok, _ := Validate3N6(faces, nil)
if ok {
t.Error("1 face should not be valid")
}
}
func TestValidate3N6NoAdjacency(t *testing.T) {
faces := make([]TracedFace, 6)
for i := range faces {
faces[i] = TracedFace{
FaceNum: i + 1,
Outline: [][2]float64{{0, 0}, {10, 0}, {10, 10}, {0, 10}},
}
}
ok, msg := Validate3N6(faces, nil)
t.Logf("no-adj validation: ok=%v msg=%s", ok, msg)
}
func TestFoldFaceOntoEdgeBasic(t *testing.T) {
nb := [][2]float64{{0, 0}, {10, 0}, {10, 5}, {0, 5}}
p0 := [3]float64{0, 0, 0}
p1 := [3]float64{10, 0, 0}
curVerts := [][3]float64{{0, 0, 0}, {10, 0, 0}, {10, 10, 0}, {0, 10, 0}}
curOutline := [][2]float64{{0, 0}, {10, 0}, {10, 10}, {0, 10}}
result := foldFaceOntoEdge(nb, 0, 1, nb[0], nb[1], p0, p1, curVerts, 0, 1, curOutline)
if len(result) != 4 {
t.Fatalf("expected 4 vertices, got %d", len(result))
}
// The shared edge points (ne0=0, ne1=1) should map to p0 and p1
if dist3(result[0], p0) > 0.01 {
t.Errorf("vertex 0 should be at p0 %v, got %v", p0, result[0])
}
if dist3(result[1], p1) > 0.01 {
t.Errorf("vertex 1 should be at p1 %v, got %v", p1, result[1])
}
}
func dist3(a, b [3]float64) float64 {
return norm3(sub3(a, b))
}
func TestReconstructPrism(t *testing.T) {
face := TracedFace{
FaceNum: 1,
Outline: [][2]float64{{0, 0}, {30, 0}, {30, 20}, {0, 20}},
}
obj, err := ReconstructPrism(face, 15.0)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(obj.SCAD, "linear_extrude(height=15.0000)") {
t.Error("SCAD should contain linear_extrude with correct depth")
}
if !strings.Contains(obj.SCAD, "polygon(points=") {
t.Error("SCAD should contain polygon")
}
if strings.Contains(obj.SCAD, "polyhedron") {
t.Error("prism SCAD should NOT contain polyhedron")
}
}
func TestReconstructPrismTooFewVerts(t *testing.T) {
face := TracedFace{FaceNum: 1, Outline: [][2]float64{{0, 0}, {10, 0}}}
_, err := ReconstructPrism(face, 10)
if err == nil {
t.Error("expected error for face with fewer than 3 vertices")
}
}
func TestReconstructPrismFile(t *testing.T) {
face := TracedFace{
FaceNum: 1,
Outline: [][2]float64{{0, 0}, {25, 0}, {25, 15}, {0, 15}},
}
obj, err := ReconstructPrism(face, 50.0)
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
path, err := GenerateReconstructedSCAD(obj, dir)
if err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(path)
scad := string(data)
if !strings.Contains(scad, "linear_extrude") {
t.Error("output file should contain linear_extrude")
}
if !strings.Contains(scad, "50.0000") {
t.Error("output file should contain the depth value")
}
}