Skip to content

Commit e55d51c

Browse files
authored
Fix float stability in orthogonal_vector (#252)
* fix float precision issue? * make fast path more reachable * add sanity check for larger polygon face * test stability * fix stability * add back old Syntax for Makie compat * add comments
1 parent 21f8f6e commit e55d51c

File tree

2 files changed

+54
-19
lines changed

2 files changed

+54
-19
lines changed

src/geometry_primitives.jl

+20-18
Original file line numberDiff line numberDiff line change
@@ -148,30 +148,32 @@ expand to 3D space.
148148
149149
Note that this vector is not normalized.
150150
"""
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
151+
orthogonal_vector(::Type{VT}, vertices) where {VT <: VecTypes{3}} = _orthogonal_vector(VT, coordinates(vertices))
152+
orthogonal_vector(::Type{VT}, vertices::Tuple) where {VT <: VecTypes{3}} = _orthogonal_vector(VT, vertices)
153+
154+
function _orthogonal_vector(::Type{VT}, vertices) where {VT <: VecTypes{3}}
155+
# Using shoelace approach (with N+1 = 1) (see #245)
156+
# \sum_i p_i × p_{i+1}
157+
# with distance vectors to avoid float precision issues
158+
# \sum_i (p_i - p_1) × (p_{i+1} - p_1)
159+
# These terms are equal when expanded (extra terms cancel over full sum)
160+
c = zero(VT)
161+
p0 = first(vertices)
162+
prev = zero(VT)
163+
for i in eachindex(vertices)
164+
# i = 1 and N don't contribute as then produce (q_1 - q_1) terms
165+
# we need i = 1 to set up prev though
166+
i == lastindex(vertices) && break
167+
v = to_ndim(VT, vertices[i+1] - p0, 0)
168+
c += cross(prev, v)
168169
prev = v
169170
end
170171
return c
171172
end
172173

173174
# 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+
orthogonal_vector(p1::VT, p2::VT, p3::VT) where {VT <: VecTypes{3}} = _orthogonal_vector(Vec3f, to_ndim.(Vec3f, (p1, p2, p3), 0))
176+
function orthogonal_vector(::Type{VT}, triangle::Union{Triangle, NTuple{3, <:VecTypes}, StaticVector{3, <:VecTypes}}) where {VT <: VecTypes{3}}
175177
a, b, c = triangle
176178
return cross(to_ndim(VT, b-a, 0), to_ndim(VT, c-a, 0))
177179
end

test/geometrytypes.jl

+34-1
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,14 @@ end
352352

353353
t = (Point3f(0), Point3f(1,0,1), Point3f(0,1,0))
354354
@test GeometryBasics.orthogonal_vector(t) == Vec3f(-1,0,1)
355-
@test GeometryBasics.orthogonal_vector(Vec3i, t) == Vec3i(-1,0,1)#
355+
@test GeometryBasics.orthogonal_vector(Vec3i, t) == Vec3i(-1,0,1)
356+
357+
t = Point2f[(0,0), (1,0), (2,1), (1,2), (0,2), (-1, 1)]
358+
@test GeometryBasics.orthogonal_vector(Vec3f, t) == Vec3f(0,0,8)
359+
360+
# Makie uses this syntax
361+
@test GeometryBasics.orthogonal_vector(Point3f(0), Point3f(1,0,1), Point3f(0,1,0)) == Vec3f(-1,0,1)
362+
@test GeometryBasics.orthogonal_vector((1,2,3), (2,3,1), (3,1,2)) == Vec3f(-3.0)
356363

357364
# Maybe the ::Any fallback is too generic...?
358365
struct TestType
@@ -361,6 +368,32 @@ end
361368
GeometryBasics.coordinates(x::TestType) = x.data
362369
x = TestType([Point3f(1,1,1), Point3f(0,0,0), Point3f(0.5,0,0)])
363370
@test GeometryBasics.orthogonal_vector(x) == Vec3f(0, -0.5, 0.5)
371+
372+
@testset "Float Precision" begin
373+
# Collection of risky triangles
374+
tris = [
375+
# with 0s that remove terms from cross(a-b, c-b)
376+
(Point3f(-10.180012, 9.928894, 63.42825), Point3f(-10.180012, 9.92804, 63.424137), Point3f(-10.181978, 9.92804, 63.42825)),
377+
(Point3f(-11.75692, 6.245064, 86.05223), Point3f(-11.758429, 6.245064, 86.048584), Point3f(-11.758429, 6.2428894, 86.05223)),
378+
(Point3f(52.430557, 13.611008, 7.657501), Point3f(52.43055, 13.611008, 7.6574783), Point3f(52.43055, 13.611008, 7.657501)),
379+
(Point3f(52.430557, 13.611008, 7.657501), Point3f(52.43055, 13.611008, 7.657501), Point3f(52.45581, 13.61525, 8.18364)),
380+
# tiny shift
381+
(Point3f(1, 1 + 1e-4, 1), Point3f(1 + 1e-4, 1 + 1e-4, 1 - 1e-4), Point3f(1 - 1e-4, 1, 1 + 1e-4))
382+
]
383+
384+
for tri in tris
385+
a, b, c = tri
386+
n64 = Vec3f(cross(Point3d(b) - Point3d(a), Point3d(c) - Point3d(a)))
387+
# fast paths
388+
n_fast = GeometryBasics.orthogonal_vector(Vec3f, Triangle(a, b, c))
389+
@test n64 n_fast
390+
@test n_fast == GeometryBasics.orthogonal_vector(Vec3f, (a, b, c))
391+
@test n_fast == GeometryBasics.orthogonal_vector(Vec3f, GeometryBasics.SVector(a, b, c)) # hit by points[face]
392+
n_slow = GeometryBasics.orthogonal_vector(Vec3f, TestType([a, b, c]))
393+
@test n_slow n64
394+
@test n_slow == GeometryBasics.orthogonal_vector(Vec3f, [a, b, c])
395+
end
396+
end
364397
end
365398

366399
@testset "Normals" begin

0 commit comments

Comments
 (0)