Skip to content

Commit f6df43a

Browse files
authored
Merge pull request #14 from VirtualPlantLab/degrees
Fix bug: The angles in rotations were still assumed to be radians
2 parents 624ef2c + 0fc9303 commit f6df43a

3 files changed

Lines changed: 156 additions & 86 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "PlantGeomPrimitives"
22
uuid = "7eef3cc5-4580-4ff0-8f6f-933507db6664"
33
authors = ["Alejandro Morales Sierra <alejandro.moralessierra@wur.nl> and contributors"]
4-
version = "1.0.0"
4+
version = "1.0.1"
55

66
[deps]
77
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"

src/Mesh/Transformations.jl

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,19 @@ Rotate a mesh `m` around the x axis by angle `θ`.
7070
7171
# Arguments
7272
- `m`: The mesh to be scaled.
73-
- `θ`: Angle of rotation in radians.
73+
- `θ`: Angle of rotation in degrees.
7474
7575
# Examples
7676
```jldoctest
7777
julia> m = Rectangle();
7878
79-
julia> θ = pi/2;
79+
julia> θ = 90.0;
8080
8181
julia> rotatex!(m, θ)
8282
```
8383
"""
8484
function rotatex!(m::Mesh, θ)
85-
trans = CT.LinearMap(Rotations.RotX(θ))
85+
trans = CT.LinearMap(Rotations.RotX*pi/180))
8686
transform!(m, trans)
8787
end
8888

@@ -93,19 +93,19 @@ Rotate a mesh `m` around the y axis by angle `θ`.
9393
9494
# Arguments
9595
- `m`: The mesh to be scaled.
96-
- `θ`: Angle of rotation in radians.
96+
- `θ`: Angle of rotation in degrees.
9797
9898
# Examples
9999
```jldoctest
100100
julia> m = Rectangle();
101101
102-
julia> θ = pi/2;
102+
julia> θ = 90.0;
103103
104104
julia> rotatey!(m, θ);
105105
```
106106
"""
107107
function rotatey!(m::Mesh, θ)
108-
trans = CT.LinearMap(Rotations.RotY(θ))
108+
trans = CT.LinearMap(Rotations.RotY*pi/180))
109109
transform!(m, trans)
110110
end
111111

@@ -116,19 +116,19 @@ Rotate a mesh `m` around the z axis by angle `θ`.
116116
117117
# Arguments
118118
- `m`: The mesh to be scaled.
119-
- `θ`: Angle of rotation in radians.
119+
- `θ`: Angle of rotation in degrees.
120120
121121
# Examples
122122
```jldoctest
123123
julia> m = Rectangle();
124124
125-
julia> θ = pi/2;
125+
julia> θ = 90.0;
126126
127127
julia> rotatez!(m, θ);
128128
```
129129
"""
130130
function rotatez!(m::Mesh, θ)
131-
trans = CT.LinearMap(Rotations.RotZ(θ))
131+
trans = CT.LinearMap(Rotations.RotZ*pi/180))
132132
transform!(m, trans)
133133
end
134134

test/test_transformations.jl

Lines changed: 146 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,88 +2,158 @@ import PlantGeomPrimitives as G
22
using Test
33

44
let
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

89159
end

0 commit comments

Comments
 (0)