@@ -2,88 +2,158 @@ import PlantGeomPrimitives as G
22using Test
33
44let
5+ # Default Rectangle() lies in the x=0 plane:
6+ # y ∈ {-0.5, 0.5}, z ∈ {0, 1}
7+ # Vertex list (two triangles, winding 1→2→3 and 1→3→4):
8+ # v1=(0, 0.5,1), v2=(0,-0.5,1), v3=(0,-0.5,0), v1, v3, v4=(0, 0.5,0)
9+ # Normal for both faces: (1, 0, 0) (face points in +x direction)
10+ # Area: width 1 × length 1 = 1
511
612 m = G. Rectangle ()
7- m_area = G. area (m)
13+ verts0 = copy (G. vertices (m))
14+ norms0 = copy (G. normals (m))
15+ area0 = G. area (m)
816
9- # Scaling
17+ # Helpers for per-axis coordinate extraction
18+ xs (m) = getindex .(G. vertices (m), 1 )
19+ ys (m) = getindex .(G. vertices (m), 2 )
20+ zs (m) = getindex .(G. vertices (m), 3 )
21+
22+ atol = sqrt (eps (Float64))
23+
24+ # ── Baseline: verify the default geometry ────────────────────────────────
25+ @test all (verts0 .≈ [G. Vec (0.0 , 0.5 , 1.0 ),
26+ G. Vec (0.0 , - 0.5 , 1.0 ),
27+ G. Vec (0.0 , - 0.5 , 0.0 ),
28+ G. Vec (0.0 , 0.5 , 1.0 ),
29+ G. Vec (0.0 , - 0.5 , 0.0 ),
30+ G. Vec (0.0 , 0.5 , 0.0 )])
31+ @test all (n ≈ G. Vec (1.0 , 0.0 , 0.0 ) for n in norms0)
32+ @test area0 ≈ 1.0
33+
34+ # ── rotatex!(90°) Rx(90°): (x,y,z) → (x, -z, y) ───────────────────────
35+ # x is the rotation axis → x-coordinates are invariant.
36+ # Normal (1,0,0) lies on the rotation axis → must stay (1,0,0).
37+ m1 = deepcopy (m)
38+ G. rotatex! (m1, 90.0 )
39+ @test all (isapprox .(xs (m1), getindex .(verts0, 1 ), atol = atol)) # x unchanged
40+ @test all (isapprox .(ys (m1), - getindex .(verts0, 3 ), atol = atol)) # y ← −z_orig
41+ @test all (isapprox .(zs (m1), getindex .(verts0, 2 ), atol = atol)) # z ← y_orig
42+ @test all (n ≈ G. Vec (1.0 , 0.0 , 0.0 ) for n in G. normals (m1))
43+ @test G. area (m1) ≈ area0 # rotation preserves area
44+
45+ # ── rotatey!(90°) Ry(90°): (x,y,z) → (z, y, -x) ───────────────────────
46+ # y is the rotation axis → y-coordinates are invariant.
47+ # Normal (1,0,0) → (0,0,-1) under Ry(90°).
1048 m2 = deepcopy (m)
11- G. scale! (m2, G. Vec (1.0 , 1.0 , 2.0 ))
12- G. area (m2) == 2 * m_area
49+ G. rotatey! (m2, 90.0 )
50+ @test all (isapprox .(xs (m2), getindex .(verts0, 3 ), atol = atol)) # x ← z_orig
51+ @test all (isapprox .(ys (m2), getindex .(verts0, 2 ), atol = atol)) # y unchanged
52+ @test all (isapprox .(zs (m2), - getindex .(verts0, 1 ), atol = atol)) # z ← −x_orig (all 0 here)
53+ @test all (n ≈ G. Vec (0.0 , 0.0 , - 1.0 ) for n in G. normals (m2))
54+ @test G. area (m2) ≈ area0
1355
14- # Rotating around x axis
15- m3 = deepcopy (m)
16- G. rotatex! (m3, 45.0 )
17- @test all (getindex .(G. vertices (m3), 1 ) .≈ getindex .(G. vertices (m), 1 ))
18- @test all (getindex .(G. vertices (m3), 2 ) .!= = getindex .(G. vertices (m), 2 ))
19- @test all (getindex .(G. vertices (m3), 3 ) .!= = getindex .(G. vertices (m), 3 ))
20- @test all (G. normals (m3) .≈ G. normals (m))
21- G. rotatex! (m3, - 45.0 )
22- @test all (G. vertices (m3) .≈ G. vertices (m))
23-
24- # Rotating around y axis
56+ # ── rotatez!(90°) Rz(90°): (x,y,z) → (-y, x, z) ───────────────────────
57+ # z is the rotation axis → z-coordinates are invariant.
58+ # Normal (1,0,0) → (0,1,0) under Rz(90°).
2559 m3 = deepcopy (m)
26- G. rotatey! (m3, 45.0 )
27- @test all (
28- (getindex .(G. vertices (m3), 1 ) .!= = getindex .(G. vertices (m), 1 )) .==
29- [true , true , false , true , false , false ],
30- )
31- @test all (getindex .(G. vertices (m3), 2 ) .≈ getindex .(G. vertices (m), 2 ))
32- @test all (
33- (getindex .(G. vertices (m3), 3 ) .!= = getindex .(G. vertices (m), 3 )) .==
34- [true , true , false , true , false , false ],
35- )
36- @test all (G. normals (m3) .!= = G. normals (m))
37- G. rotatey! (m3, - 45.0 )
38- @test all (G. vertices (m3) .≈ G. vertices (m))
39-
40- # Rotating around z axis
41- m3 = deepcopy (m)
42- G. rotatez! (m3, 45.0 )
43- @test all ((getindex .(G. vertices (m3), 1 ) .!= = getindex .(G. vertices (m), 1 )))
44- @test all (getindex .(G. vertices (m3), 2 ) .!= = getindex .(G. vertices (m), 2 ))
45- @test all (getindex .(G. vertices (m3), 3 ) .≈ getindex .(G. vertices (m), 3 ))
46- @test all (G. normals (m3) .!= = G. normals (m))
47- G. rotatez! (m3, - 45.0 )
48- @test all (G. vertices (m3) .≈ G. vertices (m))
49-
50- # Rotate along all axis simulatenously
51- m4 = deepcopy (m)
52- G. rotate! (m4, x = G. X (), y = G. Y (), z = .- G. Z ())
53- @test all (getindex .(G. vertices (m4), 1 ) .== getindex .(G. vertices (m), 1 ))
54- @test all (getindex .(G. vertices (m4), 2 ) .== getindex .(G. vertices (m), 2 ))
55- @test all ((getindex .(G. vertices (m4), 3 ) .== getindex .(G. vertices (m), 3 )) .==
56- [false , false , true , false , true , true ])
57- @test all (G. normals (m4) .== G. normals (m))
58-
59- # Translating along the x axis
60- m4 = deepcopy (m)
61- G. translate! (m4, G. Vec (2.0 , 0.0 , 0.0 ))
62- @test all ((getindex .(G. vertices (m4), 1 ) .!= = getindex .(G. vertices (m), 1 )))
63- @test all (getindex .(G. vertices (m4), 2 ) .≈ getindex .(G. vertices (m), 2 ))
64- @test all (getindex .(G. vertices (m4), 3 ) .≈ getindex .(G. vertices (m), 3 ))
65- @test all (G. normals (m4) .≈ G. normals (m))
66- G. translate! (m4, G. Vec (- 2.0 , 0.0 , 0.0 ))
67- @test all (G. vertices (m4) .≈ G. vertices (m))
68-
69- # Translating along the y axis
70- m4 = deepcopy (m)
71- G. translate! (m4, G. Vec (0.0 , 2.0 , 0.0 ))
72- @test all ((getindex .(G. vertices (m4), 1 ) .≈ getindex .(G. vertices (m), 1 )))
73- @test all (getindex .(G. vertices (m4), 2 ) .!= = getindex .(G. vertices (m), 2 ))
74- @test all (getindex .(G. vertices (m4), 3 ) .≈ getindex .(G. vertices (m), 3 ))
75- @test all (G. normals (m4) .≈ G. normals (m))
76- G. translate! (m4, G. Vec (0.0 , - 2.0 , 0.0 ))
77- @test all (G. vertices (m4) .≈ G. vertices (m))
78-
79- # Translating along the z axis
60+ G. rotatez! (m3, 90.0 )
61+ @test all (isapprox .(xs (m3), - getindex .(verts0, 2 ), atol = atol)) # x ← −y_orig
62+ @test all (isapprox .(ys (m3), getindex .(verts0, 1 ), atol = atol)) # y ← x_orig (all 0 here)
63+ @test all (isapprox .(zs (m3), getindex .(verts0, 3 ), atol = atol)) # z unchanged
64+ @test all (n ≈ G. Vec (0.0 , 1.0 , 0.0 ) for n in G. normals (m3))
65+ @test G. area (m3) ≈ area0
66+
67+ # ── rotatex!(180°) Rx(180°): (x,y,z) → (x, -y, -z) ────────────────────
68+ # (1,0,0) is still on the rotation axis → normal unchanged.
8069 m4 = deepcopy (m)
81- G. translate! (m4, G. Vec (0.0 , 0.0 , 2.0 ))
82- @test all ((getindex .(G. vertices (m4), 1 ) .≈ getindex .(G. vertices (m), 1 )))
83- @test all (getindex .(G. vertices (m4), 2 ) .≈ getindex .(G. vertices (m), 2 ))
84- @test all (getindex .(G. vertices (m4), 3 ) .!= = getindex .(G. vertices (m), 3 ))
85- @test all (G. normals (m4) .≈ G. normals (m))
86- G. translate! (m4, G. Vec (0.0 , 0.0 , - 2.0 ))
87- @test all (G. vertices (m4) .≈ G. vertices (m))
70+ G. rotatex! (m4, 180.0 )
71+ @test all (isapprox .(xs (m4), getindex .(verts0, 1 ), atol = atol))
72+ @test all (isapprox .(ys (m4), - getindex .(verts0, 2 ), atol = atol))
73+ @test all (isapprox .(zs (m4), - getindex .(verts0, 3 ), atol = atol))
74+ @test all (n ≈ G. Vec (1.0 , 0.0 , 0.0 ) for n in G. normals (m4))
75+
76+ # ── rotatez!(180°) Rz(180°): (x,y,z) → (-x, -y, z) ────────────────────
77+ # Normal (1,0,0) → (-1,0,0): the face now points in the -x direction.
78+ m5 = deepcopy (m)
79+ G. rotatez! (m5, 180.0 )
80+ @test all (isapprox .(xs (m5), - getindex .(verts0, 1 ), atol = atol)) # all 0 here, but sign matters
81+ @test all (isapprox .(ys (m5), - getindex .(verts0, 2 ), atol = atol))
82+ @test all (isapprox .(zs (m5), getindex .(verts0, 3 ), atol = atol))
83+ @test all (n ≈ G. Vec (- 1.0 , 0.0 , 0.0 ) for n in G. normals (m5))
84+
85+ # ── Composition: Ry(90°) then Rz(90°) ───────────────────────────────────
86+ # (0,y,z) →[Ry]→ (z, y, 0) →[Rz]→ (-y, z, 0)
87+ # Normal: (1,0,0) →[Ry]→ (0,0,-1) →[Rz]→ (0, 0, -1) (Rz maps (-y,x,z): (0,0,-1) stays)
88+ m6 = deepcopy (m)
89+ G. rotatey! (m6, 90.0 )
90+ G. rotatez! (m6, 90.0 )
91+ @test all (isapprox .(xs (m6), - getindex .(verts0, 2 ), atol = atol)) # x ← −y_orig
92+ @test all (isapprox .(ys (m6), getindex .(verts0, 3 ), atol = atol)) # y ← z_orig
93+ @test all (isapprox .(zs (m6), - getindex .(verts0, 1 ), atol = atol)) # z ← −x_orig (all 0)
94+ @test all (n ≈ G. Vec (0.0 , 0.0 , - 1.0 ) for n in G. normals (m6))
95+
96+ # ── Invertibility (arbitrary angles) ─────────────────────────────────────
97+ # Rotating by θ then −θ must restore the original mesh exactly.
98+ for (fn, θ) in [(G. rotatex!, 37.5 ), (G. rotatey!, - 53.2 ), (G. rotatez!, 123.4 )]
99+ m_inv = deepcopy (m)
100+ fn (m_inv, θ)
101+ fn (m_inv, - θ)
102+ @test all (G. vertices (m_inv) .≈ verts0)
103+ @test all (G. normals (m_inv) .≈ norms0)
104+ end
105+
106+ # ── rotate! with custom axes ─────────────────────────────────────────────
107+ # Cyclic permutation x→ŷ, y→ẑ, z→x̂ builds the column-major matrix
108+ # col1=(0,1,0), col2=(0,0,1), col3=(1,0,0)
109+ # which maps (x,y,z) → (z, x, y).
110+ # For (0,y,z): new coords are (z, 0, y).
111+ # Normal (1,0,0): mat*(1,0,0) = first column of mat = (0,1,0).
112+ m7 = deepcopy (m)
113+ G. rotate! (m7, x = G. Vec (0.0 , 1.0 , 0.0 ), y = G. Vec (0.0 , 0.0 , 1.0 ), z = G. Vec (1.0 , 0.0 , 0.0 ))
114+ @test all (isapprox .(xs (m7), getindex .(verts0, 3 ), atol = atol)) # new x = orig z
115+ @test all (isapprox .(ys (m7), getindex .(verts0, 1 ), atol = atol)) # new y = orig x (all 0)
116+ @test all (isapprox .(zs (m7), getindex .(verts0, 2 ), atol = atol)) # new z = orig y
117+ @test all (n ≈ G. Vec (0.0 , 1.0 , 0.0 ) for n in G. normals (m7))
118+
119+ # Identity rotation (x→x̂, y→ŷ, z→ẑ) must leave the mesh unchanged.
120+ m7b = deepcopy (m)
121+ G. rotate! (m7b, x = G. X (), y = G. Y (), z = G. Z ())
122+ @test all (G. vertices (m7b) .≈ verts0)
123+ @test all (G. normals (m7b) .≈ norms0)
124+
125+ # ── translate! ────────────────────────────────────────────────────────────
126+ # Each vertex must shift by exactly d; normals are unaffected.
127+ m8 = deepcopy (m)
128+ d = G. Vec (1.0 , 2.0 , 3.0 )
129+ G. translate! (m8, d)
130+ @test all (G. vertices (m8) .≈ verts0 .+ Ref (d))
131+ @test all (G. normals (m8) .≈ norms0)
132+ G. translate! (m8, - d)
133+ @test all (G. vertices (m8) .≈ verts0) # reversible
134+
135+ # ── scale! ────────────────────────────────────────────────────────────────
136+ # scale!(m, Vec(sx,sy,sz)) multiplies vertex coords component-wise.
137+ m9 = deepcopy (m)
138+ G. scale! (m9, G. Vec (2.0 , 3.0 , 0.5 ))
139+ @test all (isapprox .(xs (m9), 2.0 .* getindex .(verts0, 1 ), atol = atol))
140+ @test all (isapprox .(ys (m9), 3.0 .* getindex .(verts0, 2 ), atol = atol))
141+ @test all (isapprox .(zs (m9), 0.5 .* getindex .(verts0, 3 ), atol = atol))
142+
143+ # Area of the rectangle (in the x=0 plane) scales with sy*sz.
144+ m10 = deepcopy (m)
145+ G. scale! (m10, G. Vec (1.0 , 2.0 , 3.0 ))
146+ @test G. area (m10) ≈ 6.0 * area0
147+
148+ # Scaling along x (perpendicular to the face) does not change the area.
149+ m11 = deepcopy (m)
150+ G. scale! (m11, G. Vec (5.0 , 1.0 , 1.0 ))
151+ @test G. area (m11) ≈ area0
152+ # But it does move vertices' x-coordinates.
153+ @test all (isapprox .(xs (m11), 5.0 .* getindex .(verts0, 1 ), atol = atol))
154+
155+ # Scaling along x: normal direction (1,0,0) under (S⁻¹)ᵀ = diag(1/sx,1,1)
156+ # → (1/sx, 0, 0) which normalises back to (1,0,0).
157+ @test all (n ≈ G. Vec (1.0 , 0.0 , 0.0 ) for n in G. normals (m11))
88158
89159end
0 commit comments