From b68dd645b6008c02f111cb560d5c442170e208d9 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 29 Apr 2025 11:45:42 -0700 Subject: [PATCH 01/12] Expand `test_halfedge` function and add new test --- test/topologies.jl | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/test/topologies.jl b/test/topologies.jl index 3e51a0b7e..356b16b63 100644 --- a/test/topologies.jl +++ b/test/topologies.jl @@ -372,8 +372,17 @@ end for e in 1:nelements(topology) he = half4elem(topology, e) inds = indices(elems[e]) - @test he.elem == e - @test he.head ∈ inds + i = 1 + while i ≤ length(inds) + @test he.elem == e + @test he.head ∈ inds + @test he.next.elem == e + @test he.prev.elem == e + @test he.next.prev == he + @test he.prev.next == he + he = he.next + i += 1 + end end end @@ -483,6 +492,7 @@ end # correct construction from inconsistent orientation e = connect.([(1, 2, 3), (3, 4, 2), (4, 3, 5), (6, 3, 1)]) t = HalfEdgeTopology(e) + test_halfedge(e, t) n = collect(elements(t)) @test n[1] == e[1] @test n[2] != e[2] @@ -492,9 +502,14 @@ end # more challenging case with inconsistent orientation e = connect.([(4, 1, 5), (2, 6, 4), (3, 5, 6), (4, 5, 6)]) t = HalfEdgeTopology(e) + test_halfedge(e, t) n = collect(elements(t)) @test n == connect.([(5, 4, 1), (6, 2, 4), (6, 5, 3), (4, 5, 6)]) + e = connect.([(1, 2, 3), (1, 3, 4), (2, 5, 3), (5, 4, 6), (3, 5, 4)], Triangle) + t = HalfEdgeTopology(e) + test_halfedge(e, t) + # indexable api g = GridTopology(10, 10) t = convert(HalfEdgeTopology, g) From 6e6023872031c4711ec6b194c49c2621ada687d3 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 18 Apr 2025 17:18:32 -0700 Subject: [PATCH 02/12] Refactor `HalfEdgeTopology(::Vector{Connectivity}) --- src/topologies/halfedge.jl | 158 ++++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 55 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index ade856472..ec65fd8de 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -139,6 +139,29 @@ function HalfEdgeTopology(halves::AbstractVector{Tuple{HalfEdge,HalfEdge}}; nele HalfEdgeTopology(halfedges, half4elem, half4vert, edge4pair) end +function any_edges_exist(inds, half4pair) + n = length(inds) + for i in eachindex(inds) + uv = (inds[i], inds[mod1(i + 1, n)]) + if haskey(half4pair, uv) + return true + end + end + return false +end + +const NULL_EDGE = HalfEdge(0, nothing) +function any_claimed_edges_exist(inds, half4pair) + n = length(inds) + for i in eachindex(inds) + uv = (inds[i], inds[mod1(i + 1, n)]) + if !isnothing(get(half4pair, uv, NULL_EDGE).elem) + return true + end + end + return false +end + function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) assertion(all(e -> paramdim(e) == 2, elems), "invalid element for half-edge topology") @@ -153,75 +176,100 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # initialize with first element half4pair = Dict{Tuple{Int,Int},HalfEdge}() - elem = first(adjelems) - inds = collect(indices(elem)) - v = CircularVector(inds) - n = length(v) - for i in 1:n - half4pair[(v[i], v[i + 1])] = HalfEdge(v[i], eleminds[1]) + inds = first(adjelems) + for i in eachindex(inds) + u = inds[i] + u1 = inds[mod1(i + 1, length(inds))] + ei = eleminds[1] + he = get!(() -> HalfEdge(u, ei), half4pair, (u, u1)) + # reserve half-edge to enable recognizing orientation mismatches + half = get!(() -> HalfEdge(u1, nothing), half4pair, (u1, u)) + he.half = half + half.half = he end # insert all other elements - for e in 2:length(adjelems) - elem = adjelems[e] - inds = collect(indices(elem)) - v = CircularVector(inds) - n = length(v) - for i in 1:n - # if pair of vertices is already in the - # dictionary this means that the current - # polygon has inconsistent orientation - if haskey(half4pair, (v[i], v[i + 1])) - # delete inserted pairs so far - CCW[e] = false - for j in 1:(i - 1) - delete!(half4pair, (v[j], v[j + 1])) - end - break + remaining = collect(2:length(adjelems)) + added = false + disconnected = false + while !isempty(remaining) + iter = 1 + while iter ≤ length(remaining) + e = remaining[iter] + inds = adjelems[e] + n = length(inds) + if any_edges_exist(inds, half4pair) || disconnected + # at least one edge has been reserved, so we can assess the orientation w.r.t. + # previously added elements/edges + deleteat!(remaining, iter) + added = true + disconnected = false else - # insert pair in consistent orientation - half4pair[(v[i], v[i + 1])] = HalfEdge(v[i], eleminds[e]) + iter += 1 + continue end - end - if !CCW[e] - # reinsert pairs in CCW orientation - for i in 1:n - half4pair[(v[i + 1], v[i])] = HalfEdge(v[i + 1], eleminds[e]) + if any_claimed_edges_exist(inds, half4pair) + CCW[e] = false end - end - end - # add missing pointers - for (e, elem) in Iterators.enumerate(adjelems) - inds = CCW[e] ? indices(elem) : reverse(indices(elem)) - v = CircularVector(collect(inds)) - n = length(v) - for i in 1:n - # update pointers prev and next - he = half4pair[(v[i], v[i + 1])] - he.prev = half4pair[(v[i - 1], v[i])] - he.next = half4pair[(v[i + 1], v[i + 2])] - - # if not a border element, update half - if haskey(half4pair, (v[i + 1], v[i])) - he.half = half4pair[(v[i + 1], v[i])] - else # create half-edge for border - be = HalfEdge(v[i + 1], nothing) - be.half = he - he.half = be + ei = eleminds[e] + if !CCW[e] + # reinsert pairs in CCW orientation + for i in eachindex(inds) + u = inds[i] + u1 = inds[mod1(i + 1, n)] + he = get!(() -> HalfEdge(u1, ei), half4pair, (u1, u)) + if !isnothing(he.elem) + @assert he.elem === ei lazy"inconsistent duplicate edge $he for $(ei) and $(he.elem)" + else + he.elem = ei + end + half = get!(() -> HalfEdge(u, nothing), half4pair, (u, u1)) + he.half = half + half.half = he + end + else + for i in eachindex(inds) + u = inds[i] + u1 = inds[mod1(i + 1, n)] + he = get!(() -> HalfEdge(u, ei), half4pair, (u, u1)) + he.elem = ei # this may be a pre-existing/reserved edge with a nothing `elem` field + half = get!(() -> HalfEdge(u1, nothing), half4pair, (u1, u)) + he.half = half + half.half = he + end end end + + if added + added = false + elseif !isempty(remaining) + disconnected = true + added = false + end end - # save halfedges in a vector of pairs + # add missing pointers and save halfedges in a vector of pairs halves = Vector{Tuple{HalfEdge,HalfEdge}}() visited = Set{Tuple{Int,Int}}() - for ((u, v), he) in half4pair - uv = minmax(u, v) - if uv ∉ visited - push!(halves, (he, he.half)) - push!(visited, uv) + for (e, inds) in enumerate(adjelems) + inds = CCW[e] ? inds : reverse(inds) + n = length(inds) + for i in eachindex(inds) + vi = inds[i] + vi1 = inds[mod1(i + 1, n)] + vi2 = inds[mod1(i + 2, n)] + # update pointers prev and next + he = half4pair[(vi, vi1)] + he.next = half4pair[(vi1, vi2)] + he.next.prev = he + + uv = minmax(vi, vi1) + if uv ∉ visited + push!(halves, (he, he.half)) + push!(visited, uv) + end end end From 879a95b0ef724f52c8a1e1366661f45d243f1613 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 12 May 2025 18:31:12 -0700 Subject: [PATCH 03/12] 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) --- src/topologies/halfedge.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index ec65fd8de..7d05e068a 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -171,8 +171,8 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) eleminds = sort ? indexin(adjelems, elems) : 1:length(elems) # start assuming that all elements are - # oriented consistently as CCW - CCW = trues(length(adjelems)) + # oriented consistently + REV_DIR = falses(length(adjelems)) # initialize with first element half4pair = Dict{Tuple{Int,Int},HalfEdge}() @@ -210,12 +210,12 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) end if any_claimed_edges_exist(inds, half4pair) - CCW[e] = false + REV_DIR[e] = true end ei = eleminds[e] - if !CCW[e] - # reinsert pairs in CCW orientation + if REV_DIR[e] + # insert pairs in consistent orientation for i in eachindex(inds) u = inds[i] u1 = inds[mod1(i + 1, n)] @@ -254,7 +254,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) halves = Vector{Tuple{HalfEdge,HalfEdge}}() visited = Set{Tuple{Int,Int}}() for (e, inds) in enumerate(adjelems) - inds = CCW[e] ? inds : reverse(inds) + inds = REV_DIR[e] ? reverse(inds) : inds n = length(inds) for i in eachindex(inds) vi = inds[i] From 836a0dbc7ff12137c4f31b7a51cc992c99aa3df1 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 13 May 2025 10:12:10 -0700 Subject: [PATCH 04/12] collect all indices at the beginning --- src/topologies/halfedge.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 7d05e068a..dcf7a792b 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -167,8 +167,9 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # sort elements to make sure that they # are traversed in adjacent-first order - adjelems = sort ? adjsort(elems) : elems - eleminds = sort ? indexin(adjelems, elems) : 1:length(elems) + _elems = sort ? adjsort(elems) : elems + eleminds = sort ? indexin(_elems, elems) : 1:length(elems) + adjelems::Vector{Vector{Int}} = map(collect ∘ indices, _elems) # start assuming that all elements are # oriented consistently From 16aa7c4ba4a385dad60894bdd20b762306a80881 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 13 May 2025 12:13:15 -0700 Subject: [PATCH 05/12] Keep first index the same when reversing orientation --- src/topologies/halfedge.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index dcf7a792b..29f86cb2c 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -255,7 +255,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) halves = Vector{Tuple{HalfEdge,HalfEdge}}() visited = Set{Tuple{Int,Int}}() for (e, inds) in enumerate(adjelems) - inds = REV_DIR[e] ? reverse(inds) : inds + inds = REV_DIR[e] ? circshift!(reverse!(inds), 1) : inds n = length(inds) for i in eachindex(inds) vi = inds[i] From 27deeb855680de15fb6d323ddf54d3ae9e8a9db9 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Wed, 14 May 2025 09:13:36 -0700 Subject: [PATCH 06/12] Update test/topologies.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Júlio Hoffimann --- test/topologies.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/topologies.jl b/test/topologies.jl index 356b16b63..2a442d7db 100644 --- a/test/topologies.jl +++ b/test/topologies.jl @@ -372,8 +372,7 @@ end for e in 1:nelements(topology) he = half4elem(topology, e) inds = indices(elems[e]) - i = 1 - while i ≤ length(inds) + for _ in inds @test he.elem == e @test he.head ∈ inds @test he.next.elem == e @@ -381,7 +380,6 @@ end @test he.next.prev == he @test he.prev.next == he he = he.next - i += 1 end end end From 5a0c047a20e5f5953ee38b7799f8cca9ce57b6a1 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Wed, 14 May 2025 09:13:45 -0700 Subject: [PATCH 07/12] Update test/topologies.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Júlio Hoffimann --- test/topologies.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/topologies.jl b/test/topologies.jl index 2a442d7db..995193408 100644 --- a/test/topologies.jl +++ b/test/topologies.jl @@ -504,7 +504,7 @@ end n = collect(elements(t)) @test n == connect.([(5, 4, 1), (6, 2, 4), (6, 5, 3), (4, 5, 6)]) - e = connect.([(1, 2, 3), (1, 3, 4), (2, 5, 3), (5, 4, 6), (3, 5, 4)], Triangle) + e = connect.([(1, 2, 3), (1, 3, 4), (2, 5, 3), (5, 4, 6), (3, 5, 4)]) t = HalfEdgeTopology(e) test_halfedge(e, t) From f9f741827e2b0e57cdbbda270c02a12e243cf067 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Wed, 14 May 2025 09:17:26 -0700 Subject: [PATCH 08/12] Update src/topologies/halfedge.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Júlio Hoffimann --- src/topologies/halfedge.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 29f86cb2c..895f7bdaf 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -167,9 +167,9 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # sort elements to make sure that they # are traversed in adjacent-first order - _elems = sort ? adjsort(elems) : elems - eleminds = sort ? indexin(_elems, elems) : 1:length(elems) - adjelems::Vector{Vector{Int}} = map(collect ∘ indices, _elems) + elemsort = sort ? adjsort(elems) : elems + eleminds = sort ? indexin(elemsort, elems) : 1:length(elems) + adjelems::Vector{Vector{Int}} = map(collect ∘ indices, elemsort) # start assuming that all elements are # oriented consistently From f84346f4119d2919cf24bd079f8ae956ed78772b Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Wed, 14 May 2025 11:02:31 -0700 Subject: [PATCH 09/12] Update tests --- test/topologies.jl | 6 +-- test/toporelations.jl | 88 +++++++++++++++++++++---------------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/test/topologies.jl b/test/topologies.jl index 995193408..7158330a7 100644 --- a/test/topologies.jl +++ b/test/topologies.jl @@ -502,7 +502,7 @@ end t = HalfEdgeTopology(e) test_halfedge(e, t) n = collect(elements(t)) - @test n == connect.([(5, 4, 1), (6, 2, 4), (6, 5, 3), (4, 5, 6)]) + @test n == connect.([(4, 1, 5), (4, 6, 2), (6, 5, 3), (4, 5, 6)]) e = connect.([(1, 2, 3), (1, 3, 4), (2, 5, 3), (5, 4, 6), (3, 5, 4)]) t = HalfEdgeTopology(e) @@ -511,8 +511,8 @@ end # indexable api g = GridTopology(10, 10) t = convert(HalfEdgeTopology, g) - @test t[begin] == connect((13, 12, 1, 2), Quadrangle) - @test t[end] == connect((110, 121, 120, 109), Quadrangle) + @test t[begin] == g[begin] + @test t[end] == connect((120, 109, 110, 121), Quadrangle) @test t[10] == connect((22, 21, 10, 11), Quadrangle) @test length(t) == 100 @test eltype(t) == Connectivity{Quadrangle,4} diff --git a/test/toporelations.jl b/test/toporelations.jl index 4b158ade1..1afbe48e9 100644 --- a/test/toporelations.jl +++ b/test/toporelations.jl @@ -445,30 +445,30 @@ end elems = connect.([(1, 2, 3), (4, 3, 2)]) t = HalfEdgeTopology(elems) ∂ = Boundary{2,0}(t) - @test ∂(1) == (2, 3, 1) + @test ∂(1) == (1, 2, 3) @test ∂(2) == (3, 2, 4) ∂ = Boundary{2,1}(t) - @test ∂(1) == (1, 3, 2) - @test ∂(2) == (1, 4, 5) + @test ∂(1) == (1, 2, 3) + @test ∂(2) == (2, 5, 4) ∂ = Boundary{1,0}(t) - @test ∂(1) == (3, 2) - @test ∂(2) == (1, 2) + @test ∂(1) == (1, 2) + @test ∂(2) == (2, 3) @test ∂(3) == (3, 1) - @test ∂(4) == (2, 4) - @test ∂(5) == (4, 3) + @test ∂(4) == (4, 3) + @test ∂(5) == (2, 4) 𝒞 = Coboundary{0,1}(t) - @test 𝒞(1) == (2, 3) - @test 𝒞(2) == (4, 1, 2) - @test 𝒞(3) == (3, 1, 5) - @test 𝒞(4) == (5, 4) + @test 𝒞(1) == (1, 3) + @test 𝒞(2) == (5, 2, 1) + @test 𝒞(3) == (3, 2, 4) + @test 𝒞(4) == (4, 5) 𝒞 = Coboundary{0,2}(t) @test 𝒞(1) == (1,) @test 𝒞(2) == (2, 1) @test 𝒞(3) == (1, 2) @test 𝒞(4) == (2,) 𝒞 = Coboundary{1,2}(t) - @test 𝒞(1) == (2, 1) - @test 𝒞(2) == (1,) + @test 𝒞(1) == (1,) + @test 𝒞(2) == (1, 2) @test 𝒞(3) == (1,) @test 𝒞(4) == (2,) @test 𝒞(5) == (2,) @@ -484,30 +484,30 @@ end ∂ = Boundary{2,0}(t) @test ∂(1) == (1, 2, 6, 5) @test ∂(2) == (6, 2, 4) - @test ∂(3) == (6, 4, 3, 5) - @test ∂(4) == (3, 1, 5) + @test ∂(3) == (5, 6, 4, 3) + @test ∂(4) == (1, 5, 3) ∂ = Boundary{2,1}(t) - @test ∂(1) == (1, 3, 5, 6) - @test ∂(2) == (3, 9, 4) - @test ∂(3) == (4, 7, 8, 5) - @test ∂(4) == (2, 6, 8) + @test ∂(1) == (1, 2, 3, 4) + @test ∂(2) == (2, 7, 8) + @test ∂(3) == (3, 8, 9, 5) + @test ∂(4) == (4, 5, 6) ∂ = Boundary{1,0}(t) @test ∂(1) == (1, 2) - @test ∂(2) == (3, 1) - @test ∂(3) == (6, 2) - @test ∂(4) == (4, 6) - @test ∂(5) == (5, 6) - @test ∂(6) == (1, 5) - @test ∂(7) == (4, 3) - @test ∂(8) == (3, 5) - @test ∂(9) == (2, 4) + @test ∂(2) == (2, 6) + @test ∂(3) == (6, 5) + @test ∂(4) == (5, 1) + @test ∂(5) == (5, 3) + @test ∂(6) == (3, 1) + @test ∂(7) == (2, 4) + @test ∂(8) == (4, 6) + @test ∂(9) == (4, 3) 𝒞 = Coboundary{0,1}(t) - @test 𝒞(1) == (1, 6, 2) - @test 𝒞(2) == (9, 3, 1) - @test 𝒞(3) == (2, 8, 7) - @test 𝒞(4) == (7, 4, 9) - @test 𝒞(5) == (5, 8, 6) - @test 𝒞(6) == (3, 4, 5) + @test 𝒞(1) == (1, 4, 6) + @test 𝒞(2) == (7, 2, 1) + @test 𝒞(3) == (6, 5, 9) + @test 𝒞(4) == (9, 8, 7) + @test 𝒞(5) == (3, 5, 4) + @test 𝒞(6) == (2, 8, 3) 𝒞 = Coboundary{0,2}(t) @test 𝒞(1) == (1, 4) @test 𝒞(2) == (2, 1) @@ -517,14 +517,14 @@ end @test 𝒞(6) == (2, 3, 1) 𝒞 = Coboundary{1,2}(t) @test 𝒞(1) == (1,) - @test 𝒞(2) == (4,) - @test 𝒞(3) == (2, 1) - @test 𝒞(4) == (2, 3) - @test 𝒞(5) == (3, 1) - @test 𝒞(6) == (4, 1) - @test 𝒞(7) == (3,) - @test 𝒞(8) == (3, 4) - @test 𝒞(9) == (2,) + @test 𝒞(2) == (1, 2) + @test 𝒞(3) == (1, 3) + @test 𝒞(4) == (1, 4) + @test 𝒞(5) == (4, 3) + @test 𝒞(6) == (4,) + @test 𝒞(7) == (2,) + @test 𝒞(8) == (2, 3) + @test 𝒞(9) == (3,) 𝒜 = Adjacency{0}(t) @test 𝒜(1) == (2, 5, 3) @test 𝒜(2) == (4, 6, 1) @@ -539,17 +539,17 @@ end 𝒜 = Adjacency{2}(t) @test 𝒜(1) == (2, 3, 4) @test 𝒜(2) == (1, 3) - @test 𝒜(3) == (2, 4, 1) + @test 𝒜(3) == (1, 2, 4) @test 𝒜(4) == (1, 3) # 4 quadrangles in a grid elems = connect.([(1, 2, 5, 4), (2, 3, 6, 5), (4, 5, 8, 7), (5, 6, 9, 8)]) t = HalfEdgeTopology(elems) 𝒜 = Adjacency{2}(t) - @test 𝒜(1) == (3, 2) + @test 𝒜(1) == (2, 3) @test 𝒜(2) == (1, 4) @test 𝒜(3) == (1, 4) - @test 𝒜(4) == (3, 2) + @test 𝒜(4) == (2, 3) # invalid relations elems = connect.([(1, 2, 3), (4, 3, 2)]) From d6bc0a0bce1fe8ad9cc1b0fb5cfd4f67d374086d Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 16 May 2025 14:22:15 -0700 Subject: [PATCH 10/12] Rename REV_DIR => isreversed --- src/topologies/halfedge.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 895f7bdaf..fcd29a416 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -173,7 +173,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # start assuming that all elements are # oriented consistently - REV_DIR = falses(length(adjelems)) + isreversed = falses(length(adjelems)) # initialize with first element half4pair = Dict{Tuple{Int,Int},HalfEdge}() @@ -211,11 +211,11 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) end if any_claimed_edges_exist(inds, half4pair) - REV_DIR[e] = true + isreversed[e] = true end ei = eleminds[e] - if REV_DIR[e] + if isreversed[e] # insert pairs in consistent orientation for i in eachindex(inds) u = inds[i] @@ -255,7 +255,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) halves = Vector{Tuple{HalfEdge,HalfEdge}}() visited = Set{Tuple{Int,Int}}() for (e, inds) in enumerate(adjelems) - inds = REV_DIR[e] ? circshift!(reverse!(inds), 1) : inds + inds = isreversed[e] ? circshift!(reverse!(inds), 1) : inds n = length(inds) for i in eachindex(inds) vi = inds[i] From 608d7eb7ca0903e5f702126b53d32cfb4123d9d8 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 16 May 2025 14:24:07 -0700 Subject: [PATCH 11/12] Remove _ in NULL_EDGE name --- src/topologies/halfedge.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index fcd29a416..2161aeb45 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -150,12 +150,12 @@ function any_edges_exist(inds, half4pair) return false end -const NULL_EDGE = HalfEdge(0, nothing) +const NULLEDGE = HalfEdge(0, nothing) function any_claimed_edges_exist(inds, half4pair) n = length(inds) for i in eachindex(inds) uv = (inds[i], inds[mod1(i + 1, n)]) - if !isnothing(get(half4pair, uv, NULL_EDGE).elem) + if !isnothing(get(half4pair, uv, NULLEDGE).elem) return true end end From dab47eb6ccd5ffd506b7ebff826e10081596ff0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Hoffimann?= Date: Mon, 19 May 2025 21:01:25 -0300 Subject: [PATCH 12/12] Refactor for improved readability --- src/topologies/halfedge.jl | 200 +++++++++++++++++++------------------ 1 file changed, 103 insertions(+), 97 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 2161aeb45..5f73f7797 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -139,29 +139,6 @@ function HalfEdgeTopology(halves::AbstractVector{Tuple{HalfEdge,HalfEdge}}; nele HalfEdgeTopology(halfedges, half4elem, half4vert, edge4pair) end -function any_edges_exist(inds, half4pair) - n = length(inds) - for i in eachindex(inds) - uv = (inds[i], inds[mod1(i + 1, n)]) - if haskey(half4pair, uv) - return true - end - end - return false -end - -const NULLEDGE = HalfEdge(0, nothing) -function any_claimed_edges_exist(inds, half4pair) - n = length(inds) - for i in eachindex(inds) - uv = (inds[i], inds[mod1(i + 1, n)]) - if !isnothing(get(half4pair, uv, NULLEDGE).elem) - return true - end - end - return false -end - function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) assertion(all(e -> paramdim(e) == 2, elems), "invalid element for half-edge topology") @@ -172,71 +149,72 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) adjelems::Vector{Vector{Int}} = map(collect ∘ indices, elemsort) # start assuming that all elements are - # oriented consistently + # oriented consistently (e.g. CCW) isreversed = falses(length(adjelems)) - # initialize with first element + # initialize half-edges using first element half4pair = Dict{Tuple{Int,Int},HalfEdge}() + elem = first(eleminds) inds = first(adjelems) + n = length(inds) for i in eachindex(inds) u = inds[i] - u1 = inds[mod1(i + 1, length(inds))] - ei = eleminds[1] - he = get!(() -> HalfEdge(u, ei), half4pair, (u, u1)) - # reserve half-edge to enable recognizing orientation mismatches - half = get!(() -> HalfEdge(u1, nothing), half4pair, (u1, u)) + v = inds[mod1(i + 1, n)] + # insert halfedges u -> v and v -> u + he = get!(() -> HalfEdge(u, elem), half4pair, (u, v)) + half = get!(() -> HalfEdge(v, nothing), half4pair, (v, u)) he.half = half half.half = he end - # insert all other elements - remaining = collect(2:length(adjelems)) + # update half-edges using all other elements added = false disconnected = false + remaining = collect(2:length(adjelems)) while !isempty(remaining) iter = 1 while iter ≤ length(remaining) - e = remaining[iter] - inds = adjelems[e] + other = remaining[iter] + elem = eleminds[other] + inds = adjelems[other] n = length(inds) - if any_edges_exist(inds, half4pair) || disconnected - # at least one edge has been reserved, so we can assess the orientation w.r.t. - # previously added elements/edges - deleteat!(remaining, iter) + if anyhalf(inds, half4pair) || disconnected + # at least one half-edge has been found, so we can assess + # the orientation w.r.t. previously added elements/edges added = true disconnected = false + deleteat!(remaining, iter) else iter += 1 continue end - if any_claimed_edges_exist(inds, half4pair) - isreversed[e] = true + if anyhalfclaimed(inds, half4pair) + isreversed[other] = true end - ei = eleminds[e] - if isreversed[e] - # insert pairs in consistent orientation + # insert half-edges in consistent orientation + if isreversed[other] for i in eachindex(inds) u = inds[i] - u1 = inds[mod1(i + 1, n)] - he = get!(() -> HalfEdge(u1, ei), half4pair, (u1, u)) - if !isnothing(he.elem) - @assert he.elem === ei lazy"inconsistent duplicate edge $he for $(ei) and $(he.elem)" + v = inds[mod1(i + 1, n)] + he = get!(() -> HalfEdge(v, elem), half4pair, (v, u)) + if isnothing(he.elem) + he.elem = elem else - he.elem = ei + assertion(he.elem === elem, lazy"inconsistent duplicate edge $he for $(elem) and $(he.elem)") end - half = get!(() -> HalfEdge(u, nothing), half4pair, (u, u1)) + half = get!(() -> HalfEdge(u, nothing), half4pair, (u, v)) he.half = half half.half = he end else for i in eachindex(inds) u = inds[i] - u1 = inds[mod1(i + 1, n)] - he = get!(() -> HalfEdge(u, ei), half4pair, (u, u1)) - he.elem = ei # this may be a pre-existing/reserved edge with a nothing `elem` field - half = get!(() -> HalfEdge(u1, nothing), half4pair, (u1, u)) + v = inds[mod1(i + 1, n)] + he = get!(() -> HalfEdge(u, elem), half4pair, (u, v)) + he.elem = elem # this may be a pre-allocated half-edge with a nothing `elem` + half = get!(() -> HalfEdge(v, nothing), half4pair, (v, u)) he.half = half half.half = he end @@ -258,15 +236,16 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) inds = isreversed[e] ? circshift!(reverse!(inds), 1) : inds n = length(inds) for i in eachindex(inds) - vi = inds[i] - vi1 = inds[mod1(i + 1, n)] - vi2 = inds[mod1(i + 2, n)] + u = inds[i] + v = inds[mod1(i + 1, n)] + w = inds[mod1(i + 2, n)] + # update pointers prev and next - he = half4pair[(vi, vi1)] - he.next = half4pair[(vi1, vi2)] + he = half4pair[(u, v)] + he.next = half4pair[(v, w)] he.next.prev = he - uv = minmax(vi, vi1) + uv = minmax(u, v) if uv ∉ visited push!(halves, (he, he.half)) push!(visited, uv) @@ -277,44 +256,6 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) HalfEdgeTopology(halves; nelems=length(elems)) end -function adjsort(elems::AbstractVector{<:Connectivity}) - # initialize list of adjacent elements - # with first element from original list - list = indices.(elems) - adjs = Tuple[popfirst!(list)] - - # the loop will terminate if the mesh - # is manifold, and that is always true - # with half-edge topology - while !isempty(list) - # lookup all elements that share at least - # one vertex with the last adjacent element - found = false - vinds = last(adjs) - for i in vinds - einds = findall(e -> i ∈ e, list) - if !isempty(einds) - # lookup all elements that share at - # least two vertices (i.e. edge) - for j in sort(einds, rev=true) - if length(vinds ∩ list[j]) > 1 - found = true - push!(adjs, popat!(list, j)) - end - end - end - end - - if !found && !isempty(list) - # we are done with this connected component - # pop a new element from the original list - push!(adjs, popfirst!(list)) - end - end - - connect.(adjs) -end - paramdim(::HalfEdgeTopology) = 2 """ @@ -387,3 +328,68 @@ nfacets(t::HalfEdgeTopology) = length(t.halfedges) ÷ 2 # ------------ Base.convert(::Type{HalfEdgeTopology}, t::Topology) = HalfEdgeTopology(collect(elements(t))) + +# ----------------- +# HELPER FUNCTIONS +# ----------------- + +function adjsort(elems::AbstractVector{<:Connectivity}) + # initialize list of adjacent elements + # with first element from original list + list = indices.(elems) + adjs = Tuple[popfirst!(list)] + + # the loop will terminate if the mesh + # is manifold, and that is always true + # with half-edge topology + while !isempty(list) + # lookup all elements that share at least + # one vertex with the last adjacent element + found = false + vinds = last(adjs) + for i in vinds + einds = findall(e -> i ∈ e, list) + if !isempty(einds) + # lookup all elements that share at + # least two vertices (i.e. edge) + for j in sort(einds, rev=true) + if length(vinds ∩ list[j]) > 1 + found = true + push!(adjs, popat!(list, j)) + end + end + end + end + + if !found && !isempty(list) + # we are done with this connected component + # pop a new element from the original list + push!(adjs, popfirst!(list)) + end + end + + connect.(adjs) +end + +function anyhalf(inds, half4pair) + n = length(inds) + for i in eachindex(inds) + uv = (inds[i], inds[mod1(i + 1, n)]) + if haskey(half4pair, uv) + return true + end + end + return false +end + +function anyhalfclaimed(inds, half4pair) + ∅ = HalfEdge(0, nothing) + n = length(inds) + for i in eachindex(inds) + uv = (inds[i], inds[mod1(i + 1, n)]) + if !isnothing(get(half4pair, uv, ∅).elem) + return true + end + end + return false +end