Skip to content

Commit 6f1cd35

Browse files
Improve HalfEdgeTopology(::Vector{<:Connectivity}) correctness (#1194)
* Expand `test_halfedge` function and add new test * Refactor `HalfEdgeTopology(::Vector{Connectivity}) * Rename variable CCW => DIR to avoid confusion The code/algorithm doesn't actually check for counter-clockwise-ness, it only knows whether something has an inconsistent orientation with previously added elements (ultimately going back to whichever element was added first--whose orientation was never checked) * collect all indices at the beginning * Keep first index the same when reversing orientation * Update test/topologies.jl Co-authored-by: Júlio Hoffimann <[email protected]> * Update test/topologies.jl Co-authored-by: Júlio Hoffimann <[email protected]> * Update src/topologies/halfedge.jl Co-authored-by: Júlio Hoffimann <[email protected]> * Update tests * Rename REV_DIR => isreversed * Remove _ in NULL_EDGE name * Refactor for improved readability * Avoid JuliaLang/julia#58470 * Directly set `isreversed[other]` * Add more details and improve phrasing for inconsistent edge error * Fix formatting * Add additional comment * Update test/topologies.jl * Unify reversed consistency branches * Rename utility functions for improved readability --------- Co-authored-by: Júlio Hoffimann <[email protected]>
1 parent 3a3d0e3 commit 6f1cd35

File tree

3 files changed

+216
-147
lines changed

3 files changed

+216
-147
lines changed

src/topologies/halfedge.jl

Lines changed: 154 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -144,126 +144,115 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true)
144144

145145
# sort elements to make sure that they
146146
# are traversed in adjacent-first order
147-
adjelems = sort ? adjsort(elems) : elems
148-
eleminds = sort ? indexin(adjelems, elems) : 1:length(elems)
147+
elemsort = sort ? adjsort(elems) : elems
148+
eleminds = sort ? indexin(elemsort, elems) : 1:length(elems)
149+
adjelems::Vector{Vector{Int}} = map(collect indices, elemsort)
149150

150151
# start assuming that all elements are
151-
# oriented consistently as CCW
152-
CCW = trues(length(adjelems))
152+
# oriented consistently (e.g. CCW)
153+
isreversed = falses(length(adjelems))
153154

154-
# initialize with first element
155+
# initialize half-edges using first element
155156
half4pair = Dict{Tuple{Int,Int},HalfEdge}()
156-
elem = first(adjelems)
157-
inds = collect(indices(elem))
158-
v = CircularVector(inds)
159-
n = length(v)
160-
for i in 1:n
161-
half4pair[(v[i], v[i + 1])] = HalfEdge(v[i], eleminds[1])
157+
elem = first(eleminds)
158+
inds = first(adjelems)
159+
n = length(inds)
160+
for i in eachindex(inds)
161+
u = inds[i]
162+
v = inds[mod1(i + 1, n)]
163+
# insert halfedges u -> v and v -> u
164+
he = get!(() -> HalfEdge(u, elem), half4pair, (u, v))
165+
half = get!(() -> HalfEdge(v, nothing), half4pair, (v, u))
166+
he.half = half
167+
half.half = he
162168
end
163169

164-
# insert all other elements
165-
for e in 2:length(adjelems)
166-
elem = adjelems[e]
167-
inds = collect(indices(elem))
168-
v = CircularVector(inds)
169-
n = length(v)
170-
for i in 1:n
171-
# if pair of vertices is already in the
172-
# dictionary this means that the current
173-
# polygon has inconsistent orientation
174-
if haskey(half4pair, (v[i], v[i + 1]))
175-
# delete inserted pairs so far
176-
CCW[e] = false
177-
for j in 1:(i - 1)
178-
delete!(half4pair, (v[j], v[j + 1]))
179-
end
180-
break
170+
# update half-edges using all other elements
171+
added = false
172+
disconnected = false
173+
remaining = collect(2:length(adjelems))
174+
while !isempty(remaining)
175+
iter = 1
176+
while iter length(remaining)
177+
other = remaining[iter]
178+
elem = eleminds[other]
179+
inds = adjelems[other]
180+
n = length(inds)
181+
if anyhalf(inds, half4pair) || disconnected
182+
# at least one half-edge has been found, so we can assess
183+
# the orientation w.r.t. previously added elements/edges
184+
added = true
185+
disconnected = false
186+
deleteat!(remaining, iter)
181187
else
182-
# insert pair in consistent orientation
183-
half4pair[(v[i], v[i + 1])] = HalfEdge(v[i], eleminds[e])
188+
iter += 1
189+
continue
184190
end
185-
end
186191

187-
if !CCW[e]
188-
# reinsert pairs in CCW orientation
189-
for i in 1:n
190-
half4pair[(v[i + 1], v[i])] = HalfEdge(v[i + 1], eleminds[e])
192+
# if the other element is already claimed by any half-edge
193+
# then the element must be reversed before further updates
194+
isreversed[other] = anyhalfclaimed(inds, half4pair)
195+
196+
# insert half-edges in consistent orientation
197+
if isreversed[other]
198+
step₁ = add1
199+
step₂ = add0
200+
else
201+
step₁ = add0
202+
step₂ = add1
191203
end
192-
end
193-
end
194204

195-
# add missing pointers
196-
for (e, elem) in Iterators.enumerate(adjelems)
197-
inds = CCW[e] ? indices(elem) : reverse(indices(elem))
198-
v = CircularVector(collect(inds))
199-
n = length(v)
200-
for i in 1:n
201-
# update pointers prev and next
202-
he = half4pair[(v[i], v[i + 1])]
203-
he.prev = half4pair[(v[i - 1], v[i])]
204-
he.next = half4pair[(v[i + 1], v[i + 2])]
205-
206-
# if not a border element, update half
207-
if haskey(half4pair, (v[i + 1], v[i]))
208-
he.half = half4pair[(v[i + 1], v[i])]
209-
else # create half-edge for border
210-
be = HalfEdge(v[i + 1], nothing)
211-
be.half = he
212-
he.half = be
205+
for i in eachindex(inds)
206+
u = inds[step₁(i, n)]
207+
v = inds[step₂(i, n)]
208+
he = get!(() -> HalfEdge(u, elem), half4pair, (u, v))
209+
if isnothing(he.elem)
210+
he.elem = elem
211+
else
212+
assertion(
213+
he.elem === elem,
214+
lazy"duplicate edge $((u, v)) for element $(elem) is inconsistent with previous edge $he"
215+
)
216+
end
217+
half = get!(() -> HalfEdge(v, nothing), half4pair, (v, u))
218+
he.half = half
219+
half.half = he
213220
end
214221
end
215-
end
216222

217-
# save halfedges in a vector of pairs
218-
halves = Vector{Tuple{HalfEdge,HalfEdge}}()
219-
visited = Set{Tuple{Int,Int}}()
220-
for ((u, v), he) in half4pair
221-
uv = minmax(u, v)
222-
if uv visited
223-
push!(halves, (he, he.half))
224-
push!(visited, uv)
223+
if added
224+
added = false
225+
elseif !isempty(remaining)
226+
disconnected = true
227+
added = false
225228
end
226229
end
227230

228-
HalfEdgeTopology(halves; nelems=length(elems))
229-
end
230-
231-
function adjsort(elems::AbstractVector{<:Connectivity})
232-
# initialize list of adjacent elements
233-
# with first element from original list
234-
list = indices.(elems)
235-
adjs = Tuple[popfirst!(list)]
231+
# add missing pointers and save halfedges in a vector of pairs
232+
halves = Vector{Tuple{HalfEdge,HalfEdge}}()
233+
visited = Set{Tuple{Int,Int}}()
234+
for (e, inds) in enumerate(adjelems)
235+
inds = isreversed[e] ? circshift!(reverse!(inds), 1) : inds
236+
n = length(inds)
237+
for i in eachindex(inds)
238+
u = inds[i]
239+
v = inds[mod1(i + 1, n)]
240+
w = inds[mod1(i + 2, n)]
236241

237-
# the loop will terminate if the mesh
238-
# is manifold, and that is always true
239-
# with half-edge topology
240-
while !isempty(list)
241-
# lookup all elements that share at least
242-
# one vertex with the last adjacent element
243-
found = false
244-
vinds = last(adjs)
245-
for i in vinds
246-
einds = findall(e -> i e, list)
247-
if !isempty(einds)
248-
# lookup all elements that share at
249-
# least two vertices (i.e. edge)
250-
for j in sort(einds, rev=true)
251-
if length(vinds list[j]) > 1
252-
found = true
253-
push!(adjs, popat!(list, j))
254-
end
255-
end
242+
# update pointers prev and next
243+
he = half4pair[(u, v)]
244+
he.next = half4pair[(v, w)]
245+
he.next.prev = he
246+
247+
uv = minmax(u, v)
248+
if uv visited
249+
push!(halves, (he, he.half))
250+
push!(visited, uv)
256251
end
257252
end
258-
259-
if !found && !isempty(list)
260-
# we are done with this connected component
261-
# pop a new element from the original list
262-
push!(adjs, popfirst!(list))
263-
end
264253
end
265254

266-
connect.(adjs)
255+
HalfEdgeTopology(halves; nelems=length(elems))
267256
end
268257

269258
paramdim(::HalfEdgeTopology) = 2
@@ -338,3 +327,70 @@ nfacets(t::HalfEdgeTopology) = length(t.halfedges) ÷ 2
338327
# ------------
339328

340329
Base.convert(::Type{HalfEdgeTopology}, t::Topology) = HalfEdgeTopology(collect(elements(t)))
330+
331+
# -----------------
332+
# HELPER FUNCTIONS
333+
# -----------------
334+
335+
function adjsort(elems::AbstractVector{<:Connectivity})
336+
# initialize list of adjacent elements
337+
# with first element from original list
338+
list = indices.(elems)
339+
adjs = Tuple[popfirst!(list)]
340+
341+
# the loop will terminate if the mesh
342+
# is manifold, and that is always true
343+
# with half-edge topology
344+
while !isempty(list)
345+
# lookup all elements that share at least
346+
# one vertex with the last adjacent element
347+
found = false
348+
vinds = last(adjs)
349+
for i in vinds
350+
einds = findall(e -> i e, list)
351+
if !isempty(einds)
352+
# lookup all elements that share at
353+
# least two vertices (i.e. edge)
354+
for j in sort(einds, rev=true)
355+
if length(vinds list[j]) > 1
356+
found = true
357+
push!(adjs, popat!(list, j))
358+
end
359+
end
360+
end
361+
end
362+
363+
if !found && !isempty(list)
364+
# we are done with this connected component
365+
# pop a new element from the original list
366+
push!(adjs, popfirst!(list))
367+
end
368+
end
369+
370+
connect.(adjs)
371+
end
372+
373+
function anyhalf(inds, half4pair)
374+
n = length(inds)
375+
for i in eachindex(inds)
376+
uv = (inds[i], inds[mod1(i + 1, n)])
377+
if haskey(half4pair, uv)
378+
return true
379+
end
380+
end
381+
return false
382+
end
383+
384+
function anyhalfclaimed(inds, half4pair)
385+
n = length(inds)
386+
for i in eachindex(inds)
387+
uv = (inds[i], inds[mod1(i + 1, n)])
388+
if !isnothing(get(() -> HalfEdge(0, nothing), half4pair, uv).elem)
389+
return true
390+
end
391+
end
392+
return false
393+
end
394+
395+
add0(i, n) = i
396+
add1(i, n) = mod1(i + 1, n)

test/topologies.jl

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,15 @@ end
372372
for e in 1:nelements(topology)
373373
he = half4elem(topology, e)
374374
inds = indices(elems[e])
375-
@test he.elem == e
376-
@test he.head inds
375+
for _ in inds
376+
@test he.elem == e
377+
@test he.head inds
378+
@test he.next.elem == e
379+
@test he.prev.elem == e
380+
@test he.next.prev == he
381+
@test he.prev.next == he
382+
he = he.next
383+
end
377384
end
378385
end
379386

@@ -483,6 +490,7 @@ end
483490
# correct construction from inconsistent orientation
484491
e = connect.([(1, 2, 3), (3, 4, 2), (4, 3, 5), (6, 3, 1)])
485492
t = HalfEdgeTopology(e)
493+
test_halfedge(e, t)
486494
n = collect(elements(t))
487495
@test n[1] == e[1]
488496
@test n[2] != e[2]
@@ -492,14 +500,19 @@ end
492500
# more challenging case with inconsistent orientation
493501
e = connect.([(4, 1, 5), (2, 6, 4), (3, 5, 6), (4, 5, 6)])
494502
t = HalfEdgeTopology(e)
503+
test_halfedge(e, t)
495504
n = collect(elements(t))
496-
@test n == connect.([(5, 4, 1), (6, 2, 4), (6, 5, 3), (4, 5, 6)])
505+
@test n == connect.([(4, 1, 5), (4, 6, 2), (6, 5, 3), (4, 5, 6)])
506+
507+
e = connect.([(1, 2, 3), (1, 3, 4), (2, 5, 3), (5, 4, 6), (3, 5, 4)])
508+
t = HalfEdgeTopology(e)
509+
test_halfedge(e, t)
497510

498511
# indexable api
499512
g = GridTopology(10, 10)
500513
t = convert(HalfEdgeTopology, g)
501-
@test t[begin] == connect((13, 12, 1, 2), Quadrangle)
502-
@test t[end] == connect((110, 121, 120, 109), Quadrangle)
514+
@test t[begin] == connect((1, 2, 13, 12), Quadrangle)
515+
@test t[end] == connect((120, 109, 110, 121), Quadrangle)
503516
@test t[10] == connect((22, 21, 10, 11), Quadrangle)
504517
@test length(t) == 100
505518
@test eltype(t) == Connectivity{Quadrangle,4}

0 commit comments

Comments
 (0)