382 lines
9.8 KiB
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")
|
|
}
|
|
}
|