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") } }