From 0e96daa4bb493ee96152a33ee489354c364b64af Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 29 Apr 2025 11:45:42 -0700 Subject: [PATCH 01/13] 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..c9b8c3218 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 d6f936c60dd9ec1ea3b2fed47262305e2f57647d Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 7 Apr 2025 11:55:11 -0700 Subject: [PATCH 02/13] Improve type stability/specifity in half-edge construction functions --- src/topologies/halfedge.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index b9827d97a..70c67ff0f 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -141,7 +141,7 @@ 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 + adjelems = sort ? adjsort(elems)::typeof(elems) : elems eleminds = sort ? indexin(adjelems, elems) : 1:length(elems) # start assuming that all elements are @@ -151,7 +151,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # initialize with first element half4pair = Dict{Tuple{Int,Int},HalfEdge}() elem = first(adjelems) - inds = collect(indices(elem)) + inds::Vector{Int} = collect(indices(elem)) v = CircularVector(inds) n = length(v) for i in 1:n @@ -191,8 +191,8 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # add missing pointers for (e, elem) in Iterators.enumerate(adjelems) - inds = CCW[e] ? indices(elem) : reverse(indices(elem)) - v = CircularVector(collect(inds)) + inds = CCW[e] ? collect(indices(elem)) : reverse(collect(indices(elem))) + v = CircularVector(inds) n = length(v) for i in 1:n # update pointers prev and next @@ -228,8 +228,9 @@ end function adjsort(elems::AbstractVector{<:Connectivity}) # initialize list of adjacent elements # with first element from original list - list = indices.(elems) - adjs = Tuple[popfirst!(list)] + list::Vector{Tuple{Vararg{Int}}} = map(indices, elems) + adjs = similar(list, 0) + push!(adjs, popfirst!(list)) # the loop will terminate if the mesh # is manifold, and that is always true @@ -260,7 +261,7 @@ function adjsort(elems::AbstractVector{<:Connectivity}) end end - connect.(adjs) + map(connect, adjs) end paramdim(::HalfEdgeTopology) = 2 From 687517cd362b3f191b650ca8ab4450f005741af6 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 7 Apr 2025 11:55:41 -0700 Subject: [PATCH 03/13] Use more looping to avoid intermediate allocs --- src/topologies/halfedge.jl | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 70c67ff0f..770753944 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -241,16 +241,15 @@ function adjsort(elems::AbstractVector{<:Connectivity}) 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 + not_i = filter(!=(i), vinds) + for j in reverse(eachindex(list)) + # equivalent to `length(vinds ∩ list[j]) > 1` but more efficient (no allocs(?)) + any(==(i), list[j]) || continue + isdisjoint(not_i, list[j]) && continue + + # implicitly `list[j]` contains `i` and at least one other vertex + found = true + push!(adjs, popat!(list, j)) end end From a074c0fc6d0a6ef829799d243a00da96d5674d84 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 14 Apr 2025 12:11:52 -0700 Subject: [PATCH 04/13] Refactor `adjsort` to work with indices directly and return a sort-perm --- src/topologies/halfedge.jl | 68 ++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 770753944..cc8f3ab5d 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -141,8 +141,12 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # sort elements to make sure that they # are traversed in adjacent-first order - adjelems = sort ? adjsort(elems)::typeof(elems) : elems - eleminds = sort ? indexin(adjelems, elems) : 1:length(elems) + if sort + eleminds = adjsortperm(elems) + adjelems = map(collect ∘ indices, elems[eleminds])::Vector{Vector{Int}} + else + adjelems, eleminds = map(collect ∘ indices, elems)::Vector{Vector{Int}}, eachindex(elems) + end # start assuming that all elements are # oriented consistently as CCW @@ -150,8 +154,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # initialize with first element half4pair = Dict{Tuple{Int,Int},HalfEdge}() - elem = first(adjelems) - inds::Vector{Int} = collect(indices(elem)) + inds = first(adjelems) v = CircularVector(inds) n = length(v) for i in 1:n @@ -160,8 +163,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # insert all other elements for e in 2:length(adjelems) - elem = adjelems[e] - inds = collect(indices(elem)) + inds = adjelems[e] v = CircularVector(inds) n = length(v) for i in 1:n @@ -190,8 +192,8 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) end # add missing pointers - for (e, elem) in Iterators.enumerate(adjelems) - inds = CCW[e] ? collect(indices(elem)) : reverse(collect(indices(elem))) + for (e, inds) in Iterators.enumerate(adjelems) + inds = CCW[e] ? inds : reverse(inds) v = CircularVector(inds) n = length(v) for i in 1:n @@ -225,42 +227,50 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) HalfEdgeTopology(halves, length(elems)) end -function adjsort(elems::AbstractVector{<:Connectivity}) +function adjsortperm(elems::AbstractVector{<:Connectivity}) + listi = collect(eachindex(elems)[2:end]) # initialize list of adjacent elements # with first element from original list - list::Vector{Tuple{Vararg{Int}}} = map(indices, elems) - adjs = similar(list, 0) - push!(adjs, popfirst!(list)) - - # the loop will terminate if the mesh - # is manifold, and that is always true - # with half-edge topology - while !isempty(list) + adjs = Int[1] + sizehint!(adjs, length(elems)) + + # found minimizes adjacency discontinuities. if `found == true` for the last edge in an + # element, then we continue from that new element adjacent to that edge + found = false + while !isempty(listi) # lookup all elements that share at least # one vertex with the last adjacent element - found = false - vinds = last(adjs) + adj = last(adjs) + vinds::Tuple{Vararg{Int}} = indices(elems[adj]) for i in vinds not_i = filter(!=(i), vinds) - for j in reverse(eachindex(list)) + j = 1 + while j ≤ length(listi) # equivalent to `length(vinds ∩ list[j]) > 1` but more efficient (no allocs(?)) - any(==(i), list[j]) || continue - isdisjoint(not_i, list[j]) && continue - - # implicitly `list[j]` contains `i` and at least one other vertex - found = true - push!(adjs, popat!(list, j)) + elem = indices(elems[listi[j]]) + if any(==(i), elem) && !isdisjoint(not_i, elem)::Bool + # `list[j]` contains `i` and at least one other vertex + push!(adjs, popat!(listi, j)) + found = true + # don't increment j here because `popat!` just put the j+1 element at j + # (avoids the need to reverse the array) + else + j += 1 + found = false + iter += 1 + end end end - if !found && !isempty(list) + if !found && !isempty(listi) # we are done with this connected component # pop a new element from the original list - push!(adjs, popfirst!(list)) + push!(adjs, popfirst!(listi)) + found = false end end - map(connect, adjs) + adjs end paramdim(::HalfEdgeTopology) = 2 From bda92db3fc1b8c7c36f46e20e06cb436dcb84778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Hoffimann?= Date: Tue, 15 Apr 2025 10:46:06 -0300 Subject: [PATCH 05/13] Refactor adjsortperm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Author: Júlio Hoffimann --- src/topologies/halfedge.jl | 58 ++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index cc8f3ab5d..182ca0fc1 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -141,12 +141,8 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # sort elements to make sure that they # are traversed in adjacent-first order - if sort - eleminds = adjsortperm(elems) - adjelems = map(collect ∘ indices, elems[eleminds])::Vector{Vector{Int}} - else - adjelems, eleminds = map(collect ∘ indices, elems)::Vector{Vector{Int}}, eachindex(elems) - end + eleminds = sort ? adjsortperm(elems) : eachindex(elems) + adjelems = map(collect ∘ indices, elems[eleminds]) # start assuming that all elements are # oriented consistently as CCW @@ -228,49 +224,49 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) end function adjsortperm(elems::AbstractVector{<:Connectivity}) - listi = collect(eachindex(elems)[2:end]) # initialize list of adjacent elements # with first element from original list - adjs = Int[1] - sizehint!(adjs, length(elems)) + einds = Int[1] + sizehint!(einds, length(elems)) + + # remaining list of elements to process + oinds = collect(2:length(elems)) - # found minimizes adjacency discontinuities. if `found == true` for the last edge in an - # element, then we continue from that new element adjacent to that edge + # lookup all elements that share at least + # two vertices (i.e., edge) with the last + # adjacent element found = false - while !isempty(listi) - # lookup all elements that share at least - # one vertex with the last adjacent element - adj = last(adjs) - vinds::Tuple{Vararg{Int}} = indices(elems[adj]) - for i in vinds - not_i = filter(!=(i), vinds) - j = 1 - while j ≤ length(listi) - # equivalent to `length(vinds ∩ list[j]) > 1` but more efficient (no allocs(?)) - elem = indices(elems[listi[j]]) - if any(==(i), elem) && !isdisjoint(not_i, elem)::Bool - # `list[j]` contains `i` and at least one other vertex - push!(adjs, popat!(listi, j)) + while !isempty(oinds) + lelem = elems[last(einds)] + vinds = indices(lelem) + for v in vinds + # vertices that are not `v` + v! = filter(!=(v), vinds) + + # iteratively test other elements + iter = 1 + while iter ≤ length(oinds) + oelem = elems[oinds[iter]] + vinds′ = indices(oelem) + if any(==(v), vinds′) && !isdisjoint(v!, vinds′) found = true - # don't increment j here because `popat!` just put the j+1 element at j - # (avoids the need to reverse the array) + push!(einds, popat!(oinds, iter)) else - j += 1 found = false iter += 1 end end end - if !found && !isempty(listi) + if !found && !isempty(oinds) # we are done with this connected component # pop a new element from the original list - push!(adjs, popfirst!(listi)) + push!(einds, popfirst!(oinds)) found = false end end - adjs + einds end paramdim(::HalfEdgeTopology) = 2 From 882a74a8b1b96436bf9f8d8f809f26c8dbc53d5c Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 15 Apr 2025 09:23:00 -0700 Subject: [PATCH 06/13] Tweak refactor --- src/topologies/halfedge.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 182ca0fc1..df59787d5 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -142,7 +142,7 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # sort elements to make sure that they # are traversed in adjacent-first order eleminds = sort ? adjsortperm(elems) : eachindex(elems) - adjelems = map(collect ∘ indices, elems[eleminds]) + adjelems = map(collect ∘ indices, elems[eleminds])::Vector{Vector{Int}} # start assuming that all elements are # oriented consistently as CCW @@ -232,10 +232,13 @@ function adjsortperm(elems::AbstractVector{<:Connectivity}) # remaining list of elements to process oinds = collect(2:length(elems)) + # `found` minimizes adjacency discontinuities. if `found == true` for the last edge in an + # element, then we continue from that new element adjacent to that edge + found = false + # lookup all elements that share at least # two vertices (i.e., edge) with the last # adjacent element - found = false while !isempty(oinds) lelem = elems[last(einds)] vinds = indices(lelem) @@ -251,6 +254,8 @@ function adjsortperm(elems::AbstractVector{<:Connectivity}) if any(==(v), vinds′) && !isdisjoint(v!, vinds′) found = true push!(einds, popat!(oinds, iter)) + # don't increment j here because `popat!` just put the j+1 element at j + # (avoids the need to reverse the array) else found = false iter += 1 From 23e5b9d7357e1d192cb8ffe94c2552cf26c45394 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 15 Apr 2025 15:54:10 -0700 Subject: [PATCH 07/13] Improve adjacency (substantially reduce number of adjacency discontinuities) --- src/topologies/halfedge.jl | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index df59787d5..7619e4c47 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -224,23 +224,24 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) end function adjsortperm(elems::AbstractVector{<:Connectivity}) + # remaining list of elements to process + oinds = collect(eachindex(elems)[2:end]) + # initialize list of adjacent elements # with first element from original list - einds = Int[1] + einds = Int[] sizehint!(einds, length(elems)) - # remaining list of elements to process - oinds = collect(2:length(elems)) - # `found` minimizes adjacency discontinuities. if `found == true` for the last edge in an # element, then we continue from that new element adjacent to that edge found = false + lastfound = firstindex(elems) # lookup all elements that share at least # two vertices (i.e., edge) with the last # adjacent element while !isempty(oinds) - lelem = elems[last(einds)] + lelem = elems[lastfound] vinds = indices(lelem) for v in vinds # vertices that are not `v` @@ -253,23 +254,27 @@ function adjsortperm(elems::AbstractVector{<:Connectivity}) vinds′ = indices(oelem) if any(==(v), vinds′) && !isdisjoint(v!, vinds′) found = true - push!(einds, popat!(oinds, iter)) + push!(einds, lastfound) + lastfound = popat!(oinds, iter) # don't increment j here because `popat!` just put the j+1 element at j # (avoids the need to reverse the array) else - found = false iter += 1 end end end - if !found && !isempty(oinds) + if found + found = false + elseif !isempty(oinds) # we are done with this connected component # pop a new element from the original list - push!(einds, popfirst!(oinds)) + push!(einds, lastfound) + lastfound = popfirst!(oinds) found = false end end + push!(einds, lastfound) einds end From 44793fe4fa2ff6ae5ccfbc8ad36cf72852c49205 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Wed, 16 Apr 2025 11:57:02 -0700 Subject: [PATCH 08/13] Final(?) refactor of `adjsortperm` --- src/topologies/halfedge.jl | 69 +++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 7619e4c47..15ba095e2 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -224,57 +224,64 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) end function adjsortperm(elems::AbstractVector{<:Connectivity}) + reduce(vcat, connected_components(elems)) +end + +function connected_components(elems::AbstractVector{<:Connectivity}) # remaining list of elements to process oinds = collect(eachindex(elems)[2:end]) # initialize list of adjacent elements # with first element from original list - einds = Int[] - sizehint!(einds, length(elems)) + einds = Vector{Vector{Int}}(undef, 0) + push!(einds, Int[firstindex(elems)]) + + seen = Set{Int}() - # `found` minimizes adjacency discontinuities. if `found == true` for the last edge in an - # element, then we continue from that new element adjacent to that edge found = false - lastfound = firstindex(elems) + for v in indices(elems[firstindex(elems)]) + push!(seen, v) + end - # lookup all elements that share at least - # two vertices (i.e., edge) with the last - # adjacent element while !isempty(oinds) - lelem = elems[lastfound] - vinds = indices(lelem) - for v in vinds - # vertices that are not `v` - v! = filter(!=(v), vinds) - - # iteratively test other elements - iter = 1 - while iter ≤ length(oinds) - oelem = elems[oinds[iter]] - vinds′ = indices(oelem) - if any(==(v), vinds′) && !isdisjoint(v!, vinds′) - found = true - push!(einds, lastfound) - lastfound = popat!(oinds, iter) - # don't increment j here because `popat!` just put the j+1 element at j - # (avoids the need to reverse the array) - else - iter += 1 - end + # iteratively test other elements + iter = 1 + while iter ≤ length(oinds) + lelem = elems[oinds[iter]] + vinds = indices(lelem) + cnt = count(∈(seen), vinds) + # add elements that share at least two vertices (i.e., edge) with the last + # adjacent element + if cnt > 1 + push!.((seen,), vinds) + found = true + push!(last(einds), popat!(oinds, iter)) + # don't increment j here because `popat!` just put the j+1 element at j + # (avoids the need to reverse the array) + else + iter += 1 end end if found + # new vertices were "seen" while iterating `oinds`, so we need to iterate + # again because there may be elements which are now adjacent with the newly + # "seen" vertices found = false elseif !isempty(oinds) # we are done with this connected component # pop a new element from the original list - push!(einds, lastfound) - lastfound = popfirst!(oinds) + push!(einds, Int[]) + push!(last(einds), popfirst!(oinds)) + # a disconnected component means that ≥N-1 vertices in the newest element + # haven't been "seen" (but its possible the new component is connected + # by a single vertex) + for v in indices(elems[last(last(einds))]) + push!(seen, v) + end found = false end end - push!(einds, lastfound) einds end From 05856fabfa474c496d5e798e4b7792a53f87b7e9 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 18 Apr 2025 10:20:49 -0700 Subject: [PATCH 09/13] Restart iteration of remaining elements after "seeing" new vertices --- src/topologies/halfedge.jl | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 15ba095e2..d871a3964 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -238,14 +238,14 @@ function connected_components(elems::AbstractVector{<:Connectivity}) seen = Set{Int}() - found = false for v in indices(elems[firstindex(elems)]) push!(seen, v) end while !isempty(oinds) - # iteratively test other elements iter = 1 + # this loop only exits when oinds is empty, or if we have iterated through all elements + # and none are adjacent to >1 "seen" elements while iter ≤ length(oinds) lelem = elems[oinds[iter]] vinds = indices(lelem) @@ -254,32 +254,29 @@ function connected_components(elems::AbstractVector{<:Connectivity}) # adjacent element if cnt > 1 push!.((seen,), vinds) - found = true push!(last(einds), popat!(oinds, iter)) - # don't increment j here because `popat!` just put the j+1 element at j - # (avoids the need to reverse the array) + # we may have "seen" a new vertex which makes element(s) in `oinds[1:iter]` adjacent + # now. reset `j` so that we can check earlier elements for adjacency before adding + # later elements + iter = 1 else iter += 1 end end - if found - # new vertices were "seen" while iterating `oinds`, so we need to iterate - # again because there may be elements which are now adjacent with the newly - # "seen" vertices - found = false - elseif !isempty(oinds) - # we are done with this connected component - # pop a new element from the original list + if !isempty(oinds) + # there are more elements, but none are adjacent (>1 shared vertices) to previously + # seen elements + # pop a new element from the original list to start a new connected component push!(einds, Int[]) push!(last(einds), popfirst!(oinds)) + # a disconnected component means that ≥N-1 vertices in the newest element # haven't been "seen" (but its possible the new component is connected # by a single vertex) for v in indices(elems[last(last(einds))]) push!(seen, v) end - found = false end end From 2984b53e67bc9f7cc43330180760df2ab08d1c44 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 18 Apr 2025 11:05:13 -0700 Subject: [PATCH 10/13] Only create the predicate once (saves some allocs) --- src/topologies/halfedge.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index d871a3964..c92013711 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -237,6 +237,7 @@ function connected_components(elems::AbstractVector{<:Connectivity}) push!(einds, Int[firstindex(elems)]) seen = Set{Int}() + in_seen = ∈(seen) for v in indices(elems[firstindex(elems)]) push!(seen, v) From 5bda39af33827739575c9db8359524db5d064371 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 18 Apr 2025 11:05:51 -0700 Subject: [PATCH 11/13] Split actual adjacency check into separate function and union-split for common polygons (tris and quads) --- src/topologies/halfedge.jl | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index c92013711..3dde70f29 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -227,6 +227,17 @@ function adjsortperm(elems::AbstractVector{<:Connectivity}) reduce(vcat, connected_components(elems)) end +function _update_adjacency!(seen, inds, in_seen=∈(seen)) + # add elements that have at least two previously seen vertices + if count(in_seen, inds) > 1 + for v in inds + push!(seen, v) + end + return true + end + return false +end + function connected_components(elems::AbstractVector{<:Connectivity}) # remaining list of elements to process oinds = collect(eachindex(elems)[2:end]) @@ -249,12 +260,17 @@ function connected_components(elems::AbstractVector{<:Connectivity}) # and none are adjacent to >1 "seen" elements while iter ≤ length(oinds) lelem = elems[oinds[iter]] - vinds = indices(lelem) - cnt = count(∈(seen), vinds) - # add elements that share at least two vertices (i.e., edge) with the last - # adjacent element - if cnt > 1 - push!.((seen,), vinds) + + # manually union-split two most common connectivities for max type stability and speed + adjacent = if lelem isa Connectivity{Triangle, 3} + _update_adjacency!(seen, indices(lelem), in_seen) + elseif lelem isa Connectivity{Quadrangle, 4} + _update_adjacency!(seen, indices(lelem), in_seen) + else + _update_adjacency!(seen, indices(lelem), in_seen) + end + + if adjacent push!(last(einds), popat!(oinds, iter)) # we may have "seen" a new vertex which makes element(s) in `oinds[1:iter]` adjacent # now. reset `j` so that we can check earlier elements for adjacency before adding From 7f8770a62a0ac7b97c8e11caca543abf813a9d75 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 18 Apr 2025 13:26:29 -0700 Subject: [PATCH 12/13] Switch iteration order back for better adjacency ordering --- src/topologies/halfedge.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index 3dde70f29..bf38a9243 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -254,6 +254,7 @@ function connected_components(elems::AbstractVector{<:Connectivity}) push!(seen, v) end + found = false while !isempty(oinds) iter = 1 # this loop only exits when oinds is empty, or if we have iterated through all elements @@ -275,13 +276,15 @@ function connected_components(elems::AbstractVector{<:Connectivity}) # we may have "seen" a new vertex which makes element(s) in `oinds[1:iter]` adjacent # now. reset `j` so that we can check earlier elements for adjacency before adding # later elements - iter = 1 + found = true else iter += 1 end end - if !isempty(oinds) + if found + found = false + elseif !isempty(oinds) # there are more elements, but none are adjacent (>1 shared vertices) to previously # seen elements # pop a new element from the original list to start a new connected component From 7a1918969c2bfd244771f39d69c7c8250298b4ff Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 18 Apr 2025 17:18:32 -0700 Subject: [PATCH 13/13] Refactor `HalfEdgeTopology(::Vector{Connectivity}) --- src/topologies/halfedge.jl | 136 ++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 40 deletions(-) diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl index bf38a9243..941e1619a 100644 --- a/src/topologies/halfedge.jl +++ b/src/topologies/halfedge.jl @@ -136,6 +136,28 @@ 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) + ordered_uv = minmax(inds[i], inds[mod1(i + 1, n)]) + if haskey(half4pair, ordered_uv) + return true + end + end + return false +end + +function any_claimed_edges_exist(inds, half4pair) + n = length(inds) + for i in eachindex(inds) + uv = (inds[i], inds[mod1(i + 1, n)]) + if haskey(half4pair, uv) && !isnothing(half4pair[uv].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") @@ -151,60 +173,94 @@ function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true) # initialize with first element half4pair = Dict{Tuple{Int,Int},HalfEdge}() inds = first(adjelems) - v = CircularVector(inds) - n = length(v) - for i in 1:n - half4pair[(v[i], v[i + 1])] = HalfEdge(v[i], eleminds[1]) + 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) - inds = adjelems[e] - 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])) + 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 + iter += 1 + continue + end + + ei = eleminds[e] + if any_claimed_edges_exist(inds, half4pair) + CCW[e] = false + end + + 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)" + end + he.elem = ei + half = get!(() -> HalfEdge(u, nothing), half4pair, (u, u1)) + he.half = half + half.half = he end - break else - # insert pair in consistent orientation - half4pair[(v[i], v[i + 1])] = HalfEdge(v[i], eleminds[e]) + 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 + half = get!(() -> HalfEdge(u1, nothing), half4pair, (u1, u)) + he.half = half + half.half = he + end 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]) - end + if added + added = false + elseif !isempty(remaining) + disconnected = true + added = false end end - # add missing pointers + # add missing pointers and save halfedges in a vector of pairs + halves = Vector{Tuple{HalfEdge,HalfEdge}}() + visited = Set{Tuple{Int,Int}}() for (e, inds) in Iterators.enumerate(adjelems) inds = CCW[e] ? inds : reverse(inds) - v = CircularVector(inds) - n = length(v) - for i in 1:n + 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[(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 + he = half4pair[(vi, vi1)] + he.next = half4pair[(vi1, vi2)] + he.next.prev = he + if !in!(minmax(vi, vi1), visited) + push!(halves, (he, he.half)) end end end