From 3018e54b47761f6849de2e4bc75ad50aaff27731 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 12 Feb 2025 01:20:15 +0100 Subject: [PATCH 1/3] don't consider faces equal under cyclic permutation by default --- src/basic_types.jl | 13 ++++++------- src/meshes.jl | 6 +++--- test/geometrytypes.jl | 12 ++++++------ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index f60e05da..58b5f546 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -82,7 +82,7 @@ function Base.show(io::IO, x::NgonFace{N, T}) where {N, T} end # two faces are the same if they match or they just cycle indices -function Base.:(==)(f1::FT, f2::FT) where {N, FT <: AbstractFace{N}} +function cyclic_equal(f1::FT, f2::FT) where {N, FT <: AbstractFace{N}} _, min_i1 = findmin(f1.data) _, min_i2 = findmin(f2.data) @inbounds for i in 1:N @@ -92,7 +92,7 @@ function Base.:(==)(f1::FT, f2::FT) where {N, FT <: AbstractFace{N}} end return true end -function Base.hash(f::AbstractFace{N}, h::UInt) where {N} +function cyclic_hash(f::AbstractFace{N}, h::UInt = hash(0)) where {N} _, min_i = findmin(f.data) @inbounds for i in min_i:N h = hash(f[i], h) @@ -102,17 +102,16 @@ function Base.hash(f::AbstractFace{N}, h::UInt) where {N} end return h end -Base.isequal(f1::AbstractFace, f2::AbstractFace) = ==(f1, f2) # Fastpaths -Base.:(==)(f1::FT, f2::FT) where {FT <: AbstractFace{2}} = minmax(f1.data...) == minmax(f2.data...) -Base.hash(f::AbstractFace{2}, h::UInt) = hash(minmax(f.data...), h) +cyclic_equal(f1::FT, f2::FT) where {FT <: AbstractFace{2}} = minmax(f1.data...) == minmax(f2.data...) +cyclic_hash(f::AbstractFace{2}, h::UInt = hash(0)) = hash(minmax(f.data...), h) -function Base.:(==)(f1::FT, f2::FT) where {FT <: AbstractFace{3}} +function cyclic_equal(f1::FT, f2::FT) where {FT <: AbstractFace{3}} return (f1.data == f2.data) || (f1.data == (f2[2], f2[3], f2[1])) || (f1.data == (f2[3], f2[1], f2[2])) end -function Base.hash(f::AbstractFace{3}, h::UInt) +function cyclic_hash(f::AbstractFace{3}, h::UInt = hash(0)) if f[1] < f[2] if f[1] < f[3] return hash(f.data, h) diff --git a/src/meshes.jl b/src/meshes.jl index 6e2c1174..11e5e508 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -468,9 +468,9 @@ end Uses a Dict to remove duplicates from the given `faces`. """ function remove_duplicates(fs::AbstractVector{FT}) where {FT <: AbstractFace} - hashmap = Dict{FT, Nothing}() - foreach(k -> setindex!(hashmap, nothing, k), fs) - return collect(keys(hashmap)) + hashmap = Dict{UInt64, FT}() + foreach(f -> hashmap[cyclic_hash(f)] = f, fs) + return collect(values(hashmap)) end diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index f7bfb44e..c5800a84 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -290,13 +290,13 @@ end f = QuadFace(3, 4, 7, 8) @test data[f] == ("3", "4", "7", "8") - @test hash(f) != hash(QuadFace(1,2,3,4)) - @test hash(f) == hash(QuadFace(3,4,7,8)) + @test GeometryBasics.cyclic_hash(f) != GeometryBasics.cyclic_hash(QuadFace(1,2,3,4)) + @test GeometryBasics.cyclic_hash(f) == GeometryBasics.cyclic_hash(QuadFace(3,4,7,8)) # cyclic permutation does not change the face - @test hash(f) == hash(QuadFace(7,8,3,4)) - @test hash(GLTriangleFace(1,2,3)) == hash(GLTriangleFace(1,2,3)) - @test hash(GLTriangleFace(1,2,3)) == hash(GLTriangleFace(2,3,1)) - @test hash(GLTriangleFace(1,2,3)) == hash(GLTriangleFace(3,1,2)) + @test GeometryBasics.cyclic_hash(f) == GeometryBasics.cyclic_hash(QuadFace(7,8,3,4)) + @test GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) == GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) + @test GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) == GeometryBasics.cyclic_hash(GLTriangleFace(2,3,1)) + @test GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) == GeometryBasics.cyclic_hash(GLTriangleFace(3,1,2)) end @testset "FaceView" begin From b6eb992a61e51fb230bcc41dcdb583807d609dcd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Feb 2025 14:44:02 +0100 Subject: [PATCH 2/3] move definitions and add docstrings --- src/basic_types.jl | 46 -------------------------------- src/meshes.jl | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 58b5f546..996b0800 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -81,52 +81,6 @@ function Base.show(io::IO, x::NgonFace{N, T}) where {N, T} return print(io, name, "(", join(value.(x), ", "), ")") end -# two faces are the same if they match or they just cycle indices -function cyclic_equal(f1::FT, f2::FT) where {N, FT <: AbstractFace{N}} - _, min_i1 = findmin(f1.data) - _, min_i2 = findmin(f2.data) - @inbounds for i in 1:N - if f1[mod1(min_i1 + i, end)] !== f2[mod1(min_i2 + i, end)] - return false - end - end - return true -end -function cyclic_hash(f::AbstractFace{N}, h::UInt = hash(0)) where {N} - _, min_i = findmin(f.data) - @inbounds for i in min_i:N - h = hash(f[i], h) - end - @inbounds for i in 1:min_i-1 - h = hash(f[i], h) - end - return h -end - -# Fastpaths -cyclic_equal(f1::FT, f2::FT) where {FT <: AbstractFace{2}} = minmax(f1.data...) == minmax(f2.data...) -cyclic_hash(f::AbstractFace{2}, h::UInt = hash(0)) = hash(minmax(f.data...), h) - -function cyclic_equal(f1::FT, f2::FT) where {FT <: AbstractFace{3}} - return (f1.data == f2.data) || (f1.data == (f2[2], f2[3], f2[1])) || - (f1.data == (f2[3], f2[1], f2[2])) -end -function cyclic_hash(f::AbstractFace{3}, h::UInt = hash(0)) - if f[1] < f[2] - if f[1] < f[3] - return hash(f.data, h) - else - return hash((f[3], f[1], f[2]), h) - end - else - if f[2] < f[3] - return hash((f[2], f[3], f[1]), h) - else - return hash((f[3], f[1], f[2]), h) - end - end -end - Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F diff --git a/src/meshes.jl b/src/meshes.jl index 11e5e508..28a8d005 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -462,6 +462,71 @@ function split_mesh(mesh::Mesh, views::Vector{<: UnitRange{<: Integer}} = mesh.v end end + + +# two faces are the same if they match or they just cycle indices +""" + cyclic_equal(face1, face2) + +Returns true if two faces are equal up to a cyclic permutation of their indices. +E.g. considers `GLTriangleFace(2,3,1)` equal to `GLTriangleFace(1,2,3)` but not +`GLTriangleFace(2,1,3)`. +""" +function cyclic_equal(f1::FT, f2::FT) where {N, FT <: AbstractFace{N}} + _, min_i1 = findmin(f1.data) + _, min_i2 = findmin(f2.data) + @inbounds for i in 1:N + if f1[mod1(min_i1 + i, end)] !== f2[mod1(min_i2 + i, end)] + return false + end + end + return true +end + +""" + cyclic_hash(face[, h::UInt = hash(0)]) + +Creates a hash for the given face that is equal under cyclic permutation of the +faces indices. +For example `GLTriangleFace(1,2,3)` will have the same hash as `(2,3,1)` and +`(3,1,2)`, but be different from `(1,3,2)` and its cyclic permutations. +""" +function cyclic_hash(f::AbstractFace{N}, h::UInt = hash(0)) where {N} + _, min_i = findmin(f.data) + @inbounds for i in min_i:N + h = hash(f[i], h) + end + @inbounds for i in 1:min_i-1 + h = hash(f[i], h) + end + return h +end + +# Fastpaths +cyclic_equal(f1::FT, f2::FT) where {FT <: AbstractFace{2}} = minmax(f1.data...) == minmax(f2.data...) +cyclic_hash(f::AbstractFace{2}, h::UInt = hash(0)) = hash(minmax(f.data...), h) + +function cyclic_equal(f1::FT, f2::FT) where {FT <: AbstractFace{3}} + return (f1.data == f2.data) || (f1.data == (f2[2], f2[3], f2[1])) || + (f1.data == (f2[3], f2[1], f2[2])) +end +function cyclic_hash(f::AbstractFace{3}, h::UInt = hash(0)) + if f[1] < f[2] + if f[1] < f[3] + return hash(f.data, h) + else + return hash((f[3], f[1], f[2]), h) + end + else + if f[2] < f[3] + return hash((f[2], f[3], f[1]), h) + else + return hash((f[3], f[1], f[2]), h) + end + end +end + + """ remove_duplicates(faces) From fd8556677e6872540828c3801f8c8dfcdb093665 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Feb 2025 14:47:31 +0100 Subject: [PATCH 3/3] add tests --- test/geometrytypes.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index c5800a84..f4d73097 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -297,6 +297,14 @@ end @test GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) == GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) @test GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) == GeometryBasics.cyclic_hash(GLTriangleFace(2,3,1)) @test GeometryBasics.cyclic_hash(GLTriangleFace(1,2,3)) == GeometryBasics.cyclic_hash(GLTriangleFace(3,1,2)) + + # repeat with cyclic_equal + @test !GeometryBasics.cyclic_equal(f, QuadFace(1,2,3,4)) + @test GeometryBasics.cyclic_equal(f, QuadFace(3,4,7,8)) + @test GeometryBasics.cyclic_equal(f, QuadFace(7,8,3,4)) + @test GeometryBasics.cyclic_equal(GLTriangleFace(1,2,3), GLTriangleFace(1,2,3)) + @test GeometryBasics.cyclic_equal(GLTriangleFace(1,2,3), GLTriangleFace(2,3,1)) + @test GeometryBasics.cyclic_equal(GLTriangleFace(1,2,3), GLTriangleFace(3,1,2)) end @testset "FaceView" begin