Skip to content

Commit d547aa9

Browse files
Use shoelace approach to compute normals (#245)
* Changed orthogonal_vector to use shoelace formula * Added testing for ngons and non-planar faces * Further attempt to fix use of new orthogonal_vector * add to_ndim from Makie * bring back fast path for triangles * try to derive eltypes * move coordinates into orthogonal_vector and always return a vector * add some tests for to_ndim * add basic tests for orthogonal_vector paths * fix 1.6 * add tests for missed functions --------- Co-authored-by: ffreyer <[email protected]>
1 parent e60d315 commit d547aa9

File tree

6 files changed

+129
-45
lines changed

6 files changed

+129
-45
lines changed

src/fixed_arrays.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ Base.isnan(p::Union{AbstractPoint,Vec}) = any(isnan, p)
145145
Base.isinf(p::Union{AbstractPoint,Vec}) = any(isinf, p)
146146
Base.isfinite(p::Union{AbstractPoint,Vec}) = all(isfinite, p)
147147

148+
function to_ndim(T::Type{<: VecTypes{N, ET}}, vec::VecTypes{N2}, fillval) where {N,ET,N2}
149+
T(ntuple(Val(N)) do i
150+
i > N2 && return ET(fillval)
151+
@inbounds return vec[i]
152+
end)
153+
end
154+
148155
## Generate aliases
149156
## As a text file instead of eval/macro, to not confuse code linter
150157

src/geometry_primitives.jl

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,22 +140,61 @@ function collect_with_eltype!(result::AbstractVector{T}, iter) where {T}
140140
end
141141

142142
"""
143-
orthogonal_vector(p1, p2, p3)
143+
orthogonal_vector([target_type = Vec3f], points)
144144
145-
Calculates an orthogonal vector `cross(p2 - p1, p3 - p1)` to a plane described
146-
by 3 points p1, p2, p3.
145+
Calculates an orthogonal vector to a polygon defined by a vector of ordered
146+
`points`. Note that the orthogonal vector to a collection of 2D points needs to
147+
expand to 3D space.
148+
149+
Note that this vector is not normalized.
147150
"""
148-
orthogonal_vector(p1, p2, p3) = cross(p2 - p1, p3 - p1)
149-
orthogonal_vector(::Type{VT}, p1, p2, p3) where {VT} = orthogonal_vector(VT(p1), VT(p2), VT(p3))
151+
function orthogonal_vector(::Type{VT}, vertices) where {VT <: VecTypes{3}}
152+
c = zeros(VT) # Inherit vector type from input
153+
prev = to_ndim(VT, last(coordinates(vertices)), 0)
154+
@inbounds for p in coordinates(vertices) # Use shoelace approach
155+
v = to_ndim(VT, p, 0)
156+
c += cross(prev, v) # Add each edge contribution
157+
prev = v
158+
end
159+
return c
160+
end
161+
162+
function orthogonal_vector(::Type{VT}, vertices::Tuple) where {VT <: VecTypes{3}}
163+
c = zeros(VT) # Inherit vector type from input
164+
prev = to_ndim(VT, last(vertices), 0)
165+
@inbounds for p in vertices # Use shoelace approach
166+
v = to_ndim(VT, p, 0)
167+
c += cross(prev, v) # Add each edge contribution
168+
prev = v
169+
end
170+
return c
171+
end
172+
173+
# Not sure how useful this fast path is, but it's simple to keep
174+
function orthogonal_vector(::Type{VT}, triangle::Triangle) where {VT <: VecTypes{3}}
175+
a, b, c = triangle
176+
return cross(to_ndim(VT, b-a, 0), to_ndim(VT, c-a, 0))
177+
end
178+
179+
# derive target type
180+
orthogonal_vector(vertices::Ngon{D, T}) where {D, T} = orthogonal_vector(Vec3{T}, vertices)
181+
function orthogonal_vector(vertices::NTuple{N, VT}) where {N, D, T, VT <: VecTypes{D, T}}
182+
return orthogonal_vector(Vec3{T}, vertices)
183+
end
184+
function orthogonal_vector(vertices::AbstractArray{VT}) where {D, T, VT <: VecTypes{D, T}}
185+
return orthogonal_vector(Vec3{T}, vertices)
186+
end
187+
# fallback to Vec3f if vertices is something else
188+
orthogonal_vector(vertices) = orthogonal_vector(Vec3f, vertices)
150189

151190
"""
152191
normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[; normaltype = Vec3{T}])
153192
154193
Compute vertex normals from the given `positions` and `faces`.
155194
156195
This runs through all faces, computing a face normal each and adding it to every
157-
involved vertex. The direction of the face normal is based on winding direction
158-
and assumed counter-clockwise faces. At the end the summed face normals are
196+
involved vertex. The direction of the face normal is based on winding direction
197+
and assumed counter-clockwise faces. At the end the summed face normals are
159198
normalized again to produce a vertex normal.
160199
"""
161200
function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F};
@@ -165,12 +204,12 @@ end
165204

166205
function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{<: NgonFace},
167206
::Type{NormalType}) where {NormalType}
168-
207+
169208
normals_result = zeros(NormalType, length(vertices))
170209
for face in faces
171210
v = vertices[face]
172211
# we can get away with two edges since faces are planar.
173-
n = orthogonal_vector(NormalType, v[1], v[2], v[3])
212+
n = orthogonal_vector(NormalType, v)
174213
for i in 1:length(face)
175214
fi = face[i]
176215
normals_result[fi] = normals_result[fi] .+ n
@@ -188,15 +227,15 @@ Compute face normals from the given `positions` and `faces` and returns an
188227
appropriate `FaceView`.
189228
"""
190229
function face_normals(
191-
positions::AbstractVector{<:Point3{T}}, fs::AbstractVector{<: AbstractFace};
230+
positions::AbstractVector{<:Point3{T}}, fs::AbstractVector{<: AbstractFace};
192231
normaltype = Vec3{T}) where {T}
193232
return face_normals(positions, fs, normaltype)
194233
end
195-
234+
196235
@generated function face_normals(positions::AbstractVector{<:Point3}, fs::AbstractVector{F},
197236
::Type{NormalType}) where {F<:NgonFace,NormalType}
198-
199-
# If the facetype is not concrete it likely varies and we need to query it
237+
238+
# If the facetype is not concrete it likely varies and we need to query it
200239
# doing the iteration
201240
FT = ifelse(isconcretetype(F), :($F), :(typeof(f)))
202241

@@ -206,7 +245,7 @@ end
206245

207246
for (i, f) in enumerate(fs)
208247
ps = positions[f]
209-
n = orthogonal_vector(NormalType, ps[1], ps[2], ps[3])
248+
n = orthogonal_vector(NormalType, ps)
210249
normals[i] = normalize(n)
211250
faces[i] = $(FT)(i)
212251
end

src/meshes.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,9 @@ end
213213
Calculate the signed volume of one tetrahedron. Be sure the orientation of your
214214
surface is right.
215215
"""
216-
function volume(triangle::Triangle)
216+
function volume(triangle::Triangle{3, T}) where {T}
217217
v1, v2, v3 = triangle
218-
sig = sign(orthogonal_vector(v1, v2, v3) v1)
218+
sig = sign(orthogonal_vector(Vec3{T}, triangle) v1)
219219
return sig * abs(v1 (v2 × v3)) / 6
220220
end
221221

src/triangulation.jl

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,54 +13,42 @@ The above copyright notice and this permission notice shall be included in all c
1313
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1414
=#
1515
"""
16-
area(vertices::AbstractVector{Point{3}}, face::TriangleFace)
16+
area(vertices::AbstractVector{Point{3}}, face::NgonFace)
1717
18-
Calculate the area of one triangle.
18+
Calculate the area of one face.
1919
"""
20-
function area(vertices::AbstractVector{<:Point{3,VT}},
21-
face::TriangleFace{FT}) where {VT,FT}
22-
v1, v2, v3 = vertices[face]
23-
return 0.5 * norm(orthogonal_vector(v1, v2, v3))
20+
function area(vertices::AbstractVector{<:Point}, face::NgonFace)
21+
return 0.5 * norm(orthogonal_vector(vertices[face]))
2422
end
2523

2624
"""
27-
area(vertices::AbstractVector{Point{3}}, faces::AbstractVector{TriangleFace})
25+
area(vertices::AbstractVector{Point}, faces::AbstractVector{NgonFace})
2826
29-
Calculate the area of all triangles.
27+
Calculate the area of all faces.
3028
"""
31-
function area(vertices::AbstractVector{Point{3,VT}},
32-
faces::AbstractVector{TriangleFace{FT}}) where {VT,FT}
29+
function area(vertices::AbstractVector{<:Point}, faces::AbstractVector{<:NgonFace})
3330
return sum(x -> area(vertices, x), faces)
3431
end
3532

3633
"""
37-
area(contour::AbstractVector{Point}})
34+
area(vertices::AbstractVector{Point}})
3835
3936
Calculate the area of a polygon.
4037
4138
For 2D points, the oriented area is returned (negative when the points are
4239
oriented clockwise).
4340
"""
44-
function area(contour::AbstractVector{Point{2,T}}) where {T}
45-
length(contour) < 3 && return zero(T)
46-
A = zero(T)
47-
p = lastindex(contour)
48-
for q in eachindex(contour)
49-
A += cross(contour[p], contour[q])
50-
p = q
51-
end
52-
return A * T(0.5)
41+
function area(vertices::AbstractVector{Point{2,T}}) where {T}
42+
length(vertices) < 3 && return zero(T)
43+
return T(0.5) * orthogonal_vector(vertices)[3]
5344
end
5445

55-
function area(contour::AbstractVector{Point{3,T}}) where {T}
56-
A = zero(eltype(contour))
57-
o = first(contour)
58-
for i in (firstindex(contour) + 1):(lastindex(contour) - 1)
59-
A += cross(contour[i] - o, contour[i + 1] - o)
60-
end
61-
return norm(A) * T(0.5)
46+
function area(vertices::AbstractVector{Point{3,T}}) where {T}
47+
length(vertices) < 3 && return zero(T)
48+
return T(0.5) * norm(orthogonal_vector(vertices))
6249
end
6350

51+
6452
"""
6553
in(point, triangle)
6654

test/fixed_arrays.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Test
2+
using GeometryBasics: to_ndim
23

34
@testset "Construction and Conversion" begin
45
for VT in [Point, Vec]
@@ -19,6 +20,10 @@ using Test
1920

2021
@test convert(Point, (2, 3)) === Point(2, 3)
2122
@test convert(Point, (2.0, 3)) === Point(2.0, 3.0)
23+
24+
@test to_ndim(Point3f, Vec2i(1,2), 0) == Point3f(1,2,0)
25+
@test to_ndim(Vec4i, (1f0, 2), 0) == Vec4i(1,2,0,0)
26+
@test to_ndim(NTuple{2, Float64}, Point3f(1,2,3), 0) == (1.0, 2.0)
2227
end
2328

2429
@testset "broadcast" begin

test/geometrytypes.jl

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,32 @@ end
332332
@test length(fv) == 5
333333
end
334334

335+
@testset "orthogonal_vector" begin
336+
tri = Triangle(Point3d(0,0,0), Point3d(1,0,0), Point3d(0,1,0))
337+
@test GeometryBasics.orthogonal_vector(tri) === Vec3d(0,0,1)
338+
@test GeometryBasics.orthogonal_vector(collect(coordinates(tri))) === Vec3d(0,0,1)
339+
@test GeometryBasics.orthogonal_vector(Vec3f, tri) === Vec3f(0,0,1)
340+
@test GeometryBasics.orthogonal_vector(Vec3f, collect(coordinates(tri))) === Vec3f(0,0,1)
341+
342+
quad = GeometryBasics.Quadrilateral(Point2i(0,0), Point2i(1,0), Point2i(1,1), Point2i(0,1))
343+
@test GeometryBasics.orthogonal_vector(quad) === Vec3i(0,0,2)
344+
@test GeometryBasics.orthogonal_vector(collect(coordinates(quad))) === Vec3i(0,0,2)
345+
@test GeometryBasics.orthogonal_vector(Vec3d, quad) === Vec3d(0,0,2)
346+
@test GeometryBasics.orthogonal_vector(Vec3d, collect(coordinates(quad))) === Vec3d(0,0,2)
347+
348+
t = (Point3f(0), Point3f(1,0,1), Point3f(0,1,0))
349+
@test GeometryBasics.orthogonal_vector(t) == Vec3f(-1,0,1)
350+
@test GeometryBasics.orthogonal_vector(Vec3i, t) == Vec3i(-1,0,1)#
351+
352+
# Maybe the ::Any fallback is too generic...?
353+
struct TestType
354+
data::Vector{Vec3f}
355+
end
356+
GeometryBasics.coordinates(x::TestType) = x.data
357+
x = TestType([Point3f(1,1,1), Point3f(0,0,0), Point3f(0.5,0,0)])
358+
@test GeometryBasics.orthogonal_vector(x) == Vec3f(0, -0.5, 0.5)
359+
end
360+
335361
@testset "Normals" begin
336362
# per face normals
337363
r = Rect3f(Point3f(0), Vec3f(1))
@@ -351,12 +377,31 @@ end
351377
ns = normals(c)
352378
# caps without mantle
353379
f_ns = face_normals(coordinates(c), filter!(f -> f isa TriangleFace, faces(c)))
354-
@test all(n -> n == values(ns)[end-1], values(f_ns)[1:15])
355-
@test all(n -> n == values(ns)[end], values(f_ns)[16:end])
380+
@test all(n -> n values(ns)[end-1], values(f_ns)[1:15])
381+
@test all(n -> n values(ns)[end], values(f_ns)[16:end])
356382
# Mantle without caps
357383
v_ns = normals(coordinates(c), filter!(f -> f isa QuadFace, faces(c)))[1:end-2]
358384
@test values(ns)[1:15] v_ns[1:15]
359385
@test values(ns)[1:15] v_ns[16:30] # repeated via FaceView in ns
386+
387+
# Planar QuadFace with colinear edge
388+
v = [Point3d(0,0,0),Point3d(1,0,0),Point3d(2,0,0),Point3d(2,1,0)]
389+
f = [QuadFace{Int}(1,2,3,4)]
390+
n = normals(v,f)
391+
@test all(n_i -> n_i [0.0,0.0,1.0], n)
392+
393+
# Planar NgonFace (5-sided) with colinear edge
394+
v = [Point3d(0,0,0),Point3d(1,0,0),Point3d(2,0,0),Point3d(2,1,0),Point3d(2,0.5,0)]
395+
f = [NgonFace{5,Int}(1,2,3,4,5)]
396+
n = normals(v,f)
397+
@test all(n_i -> n_i [0.0,0.0,1.0], n)
398+
399+
# Non-planar NgonFace (6 sided), features equal up and down variations resulting in z-dir average face_normal
400+
t = range(0.0, 2*pi-(2*pi)/6, length = 6)
401+
v = [Point{3,Float64}(cos(t[i]),sin(t[i]),iseven(i)) for i in eachindex(t)]
402+
f = [NgonFace{6,Int}(1,2,3,4,5,6)]
403+
n = normals(v,f)
404+
@test all(n_i -> n_i [0.0,0.0,1.0], n)
360405
end
361406

362407
@testset "HyperSphere" begin

0 commit comments

Comments
 (0)