Skip to content

Commit 666aedc

Browse files
authored
Fix planar hull on corner case (#320)
* Fix planar hull on corner case * Fix first and last of semi hulls
1 parent 9cdfda5 commit 666aedc

File tree

6 files changed

+78
-38
lines changed

6 files changed

+78
-38
lines changed

src/planar.jl

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
function getsemihull(ps::Vector{PT}, sign_sense, counterclockwise, yray::Nothing = nothing) where PT
1+
function _semi_hull(ps::Vector{PT}, sign_sense, counterclockwise, sweep_norm, yray::Nothing = nothing) where PT
22
hull = PT[]
3-
if length(ps) == 0
3+
if isempty(ps)
44
return hull
55
end
66
prev = sign_sense == 1 ? first(ps) : last(ps)
@@ -17,12 +17,22 @@ function getsemihull(ps::Vector{PT}, sign_sense, counterclockwise, yray::Nothing
1717
psj_vec = ps[j] - prev
1818
cc = counterclockwise(cur_vec, psj_vec)
1919
if isapproxzero(cc)
20-
# `cur` and `ps[j]` are on the same ray starting from `prev`.
21-
# The one that is closer to `prev` is redundant.
22-
# If `j` is the last index and redundant (it may happen if this
23-
# ray is perpendicular to the direction of sorting) then we should
24-
# also avoid adding `ps[j]` to `hull` so we set `skip` to `true`.
25-
if norm(cur_vec, 1) > norm(psj_vec, 1)
20+
# `prev`, `cur` and `ps[j]` are on the same line
21+
# Two cases here:
22+
# 1) `prev`, `cur`, `ps[j]` are on a line perpendicular to `sweep_norm`
23+
# The one that is not clockwise is redundant.
24+
# 2) `cur`, `ps[j]` and on the same ray starting from `prev` of direction
25+
# `sweep_norm * sign_sense`
26+
# The one that is closer to `prev` is redundant.
27+
dcur = dot(cur_vec, sweep_norm)
28+
dpsj = dot(psj_vec, sweep_norm)
29+
if isapproxzero(dcur) && isapproxzero(dpsj)
30+
# Case 1
31+
if sign_sense * counterclockwise(cur_vec, sweep_norm) < sign_sense * counterclockwise(psj_vec, sweep_norm)
32+
skip = true
33+
break
34+
end
35+
elseif sign_sense * dcur > sign_sense * dpsj
2636
skip = true
2737
break
2838
end
@@ -46,14 +56,16 @@ function getsemihull(ps::Vector{PT}, sign_sense, counterclockwise, yray::Nothing
4656
return hull
4757
end
4858

49-
function getsemihull(ps::Vector{PT}, sign_sense, counterclockwise, yray) where PT
50-
hull = getsemihull(ps, sign_sense, counterclockwise)
59+
function _semi_hull(ps::Vector{PT}, sign_sense, counterclockwise, sweep_norm, yray) where PT
60+
hull = _semi_hull(ps, sign_sense, counterclockwise, sweep_norm)
5161
while length(hull) >= 2 && counterclockwise(hull[end] - hull[end - 1], yray) >= 0
5262
pop!(hull)
5363
end
5464
return hull
5565
end
5666

67+
_colinear(counterclockwise, a, b, c) = isapproxzero(counterclockwise(b - a, c - a))
68+
5769
function _planar_hull(d::FullDim, points, lines, rays, counterclockwise, rotate)
5870
line = nothing
5971
lineleft = false
@@ -124,32 +136,31 @@ function _planar_hull(d::FullDim, points, lines, rays, counterclockwise, rotate)
124136
else
125137
sweep_norm = rotate(coord(line))
126138
end
127-
sort!(points, by = x -> dot(x, sweep_norm))
128-
129-
if !isempty(points)
130-
# `getsemihull` fails if `points` starts or end with several points on the same sweep line that are not ordered clockwise
131-
start_line = dot(points[1], sweep_norm)
132-
# `isapprox` won't work well with `atol=0` (which is the default) if `start_line` is zero, so we set a nonzero `atol`.
133-
# TODO We should also multiply it with a scaling.
134-
end_start = something(findfirst(x -> !isapprox(dot(x, sweep_norm), start_line, atol=Base.rtoldefault(typeof(start_line))), points), length(points) + 1) - 1
135-
if end_start > 1
136-
sort!(view(points, 1:end_start), by = x -> counterclockwise(x, sweep_norm), rev=true)
137-
end
138-
end_line = dot(points[end], sweep_norm)
139-
start_end = something(findlast(x -> !isapprox(dot(x, sweep_norm), end_line, atol=Base.rtoldefault(typeof(start_line))), points), 0) + 1
140-
if start_end < length(points)
141-
sort!(view(points, start_end:length(points)), by = x -> counterclockwise(x, sweep_norm))
142-
end
143-
end
139+
sort!(points, by = Base.Fix2(dot, sweep_norm))
144140

145141
_points = eltype(points)[]
146142
_lines = eltype(lines)[]
147143
_rays = eltype(rays)[]
148144
if line === nothing
149-
append!(_points, getsemihull(points, 1, counterclockwise, yray))
145+
half_points = _semi_hull(points, 1, counterclockwise, sweep_norm, yray)
150146
if yray === nothing
151-
append!(_points, getsemihull(points, -1, counterclockwise, yray)[2:end-1])
147+
other_half = _semi_hull(points, -1, counterclockwise, sweep_norm, yray)[2:end-1]
148+
if !isempty(other_half) && length(half_points) > 1 &&
149+
_colinear(counterclockwise, half_points[1], half_points[2], other_half[end])
150+
start = 2
151+
else
152+
start = 1
153+
end
154+
if !isempty(other_half) && length(half_points) > 1 &&
155+
_colinear(counterclockwise, half_points[end], half_points[end - 1], other_half[1])
156+
stop = length(half_points) - 1
157+
else
158+
stop = length(half_points)
159+
end
160+
append!(_points, half_points[start:stop])
161+
append!(_points, other_half)
152162
else
163+
append!(_points, half_points)
153164
push!(_rays, Ray(xray))
154165
if !(Ray(xray) Ray(yray))
155166
push!(_rays, Ray(yray))

src/recipe.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ function planar_contour(p::Polyhedron)
1818
end
1919
sort!(ps, by = x -> x[1])
2020
counterclockwise(p1, p2) = dot(cross([p1; 0], [p2; 0]), [0, 0, 1])
21-
top = getsemihull(ps, 1, counterclockwise)
22-
bot = getsemihull(ps, -1, counterclockwise)
21+
sweep_norm = basis(eltype(ps), fulldim(p), 1)
22+
top = _semi_hull(ps, 1, counterclockwise, sweep_norm)
23+
bot = _semi_hull(ps, -1, counterclockwise, sweep_norm)
2324
if !isempty(top) && !isempty(bot)
2425
@assert top[end] == bot[1]
2526
pop!(top)

test/issue311.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
function issue311test(lib::Polyhedra.Library)
2+
W = [
3+
[4, 8],
4+
[3, 9],
5+
[9, 3],
6+
[8, 4],
7+
[9, 8],
8+
[8, 9],
9+
[4, 8],
10+
[8, 4],
11+
[8, 8],
12+
[3, 8],
13+
[8, 3],
14+
[8, 8],
15+
]
16+
V = [
17+
[3, 9],
18+
[3, 8],
19+
[9, 8],
20+
[8, 9],
21+
[8, 3],
22+
[9, 3],
23+
]
24+
p = polyhedron(vrep(W), lib)
25+
generator_fulltest(p, V)
26+
end

test/misc.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ include("docexample.jl")
66
include("issue48.jl")
77
include("issue224.jl")
88
include("issue301.jl")
9+
include("issue311.jl")
910
include("empty.jl")
1011
include("sparse.jl")
1112
include("sparserect.jl")
@@ -25,6 +26,7 @@ const misctests = Dict(
2526
"issue48" => issue48test,
2627
"issue224" => issue224test,
2728
"issue301" => issue301test,
29+
"issue311" => issue311test,
2830
"empty" => emptytest,
2931
"sparse" => sparsetest,
3032
"sparserect" => sparserecttest,

test/planar.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function test_planar_square(VT, d)
88
no = -ei[1] - ei[2]
99
ne = ei[1] - ei[2]
1010
se = ei[1] + ei[2]
11-
expected = [ne, no, so, se]
11+
expected = [no, so, se, ne]
1212
for ps in [
1313
[no, so, ne, se],
1414
[no, se, so, ne],
@@ -61,7 +61,7 @@ end
6161

6262
# Square + redundant points + [+-1.5, -+0.25]
6363
function test_planar_square_with_more()
64-
expected = [[1.0, -1.0], [-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0]]
64+
expected = [[-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [1.0, -1.0]]
6565
v0 = convexhull([-1.0, -1.0], [1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [0.0, 1.0])
6666
v1 = Polyhedra.planar_hull(v0)
6767
@test collect(points(v1)) == expected
@@ -72,10 +72,10 @@ function test_planar_square_with_more()
7272
v1 = Polyhedra.planar_hull(v0)
7373
@test collect(points(v1)) == expected
7474
expected = [[1.0, -1.0], [-1.0, -1.0], [-1.0, 1.0], [0.0, 1.0 + 1e-4], [1.0, 1.0]]
75-
v0 = convexhull([-1.0, -1.0], [1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [0.0, 1.0 + 1e-4])
75+
v0 = convexhull([1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [0.0, 1.0 + 1e-4], [-1.0, -1.0])
7676
v1 = Polyhedra.planar_hull(v0)
7777
@test collect(points(v1)) == expected
78-
expected = [[1.0, -1.0], [-1.0, -1.0], [-1.5, 0.25], [-1.0, 1.0], [1.0, 1.0], [1.5, -0.25]]
78+
expected = [[-1.0, -1.0], [-1.5, 0.25], [-1.0, 1.0], [1.0, 1.0], [1.5, -0.25], [1.0, -1.0]]
7979
v0 = convexhull([-1.0, -1.0], [1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [0.0, 1.0], [-1.5, 0.25], [1.5, -0.25])
8080
v1 = Polyhedra.planar_hull(v0)
8181
@test collect(points(v1)) == expected
@@ -91,11 +91,11 @@ function test_planar_square_with_more()
9191
end
9292

9393
function test_issue_271()
94-
expected = [[1.0, 0.0], [-0.5, 0.0], [0.5, 0.5]]
94+
expected = [[-0.5, 0.0], [0.5, 0.5], [1.0, 0.0]]
9595
v = convexhull([0.0, 0.0], [1, 0], [0.5, 0.0], [-0.5, 0.0], [0.5, 0.5])
9696
p = Polyhedra.planar_hull(v)
9797
@test collect(points(p)) == expected
98-
v = convexhull([1, 0], [0.0, 0.0], [0.5, 0.0], [-0.5, 0.0], [0.5, 0.5])
98+
v = convexhull([0.0, 0.0], [0.5, 0.0], [-0.5, 0.0], [0.5, 0.5], [1, 0])
9999
p = Polyhedra.planar_hull(v)
100100
@test collect(points(p)) == expected
101101

test/utils.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Test
1+
using LinearAlgebra, Test
22
using Polyhedra
33

44
_isapprox(x::Real, y::Real) = _isapprox(promote(x, y)...)

0 commit comments

Comments
 (0)