From 46ea52ad3e94da6b03dfd3f5e8b32133e3a51315 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 3 Feb 2025 15:22:00 +0100 Subject: [PATCH 01/28] automatic formatting --- src/primitives/rectangles.jl | 2 +- test/geometrytypes.jl | 37 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index fa6d0aa5..ab3702ad 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -589,7 +589,7 @@ end function faces(::Rect3) return QuadFace{Int}[ - (1, 2, 4, 3), (7, 8, 6, 5), (5, 6, 2, 1), + (1, 2, 4, 3), (7, 8, 6, 5), (5, 6, 2, 1), (3, 4, 8, 7), (1, 3, 7, 5), (6, 8, 4, 2) ] end diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 65554449..e8e7ea72 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -28,16 +28,16 @@ using Test, GeometryBasics (7.535533905932738, 1.4644660940672627, 6.0), (6.041241452319315, 7.041241452319315, 1.9175170953613705), (0.46446609406726314, 8.535533905932738, 6.0), - (1.9587585476806848, 2.9587585476806857, 10.08248290463863), - (1, 2, 3), + (1.9587585476806848, 2.9587585476806857, 10.08248290463863), + (1, 2, 3), (4, 5, 6) ] @test decompose(Point3{Float64}, Tessellation(s, 8)) ≈ positions _faces = TriangleFace[ - (9, 2, 1), (9, 3, 2), (9, 4, 3), (9, 1, 4), (1, 2, 6), (1, 6, 5), - (2, 3, 7), (2, 7, 6), (3, 4, 8), (3, 8, 7), (4, 1, 5), (4, 5, 8), + (9, 2, 1), (9, 3, 2), (9, 4, 3), (9, 1, 4), (1, 2, 6), (1, 6, 5), + (2, 3, 7), (2, 7, 6), (3, 4, 8), (3, 8, 7), (4, 1, 5), (4, 5, 8), (10, 5, 6), (10, 6, 7), (10, 7, 8), (10, 8, 5)] @test _faces == decompose(TriangleFace{Int}, Tessellation(s, 8)) @@ -46,7 +46,7 @@ using Test, GeometryBasics @test m === triangle_mesh(m) @test GeometryBasics.faces(m) == decompose(GLTriangleFace, _faces) @test GeometryBasics.coordinates(m) ≈ positions - + m = normal_mesh(s) # just test that it works without explicit resolution parameter @test hasproperty(m, :position) @test hasproperty(m, :normal) @@ -56,15 +56,15 @@ using Test, GeometryBasics ns = GeometryBasics.FaceView( Vec{3, Float32}[ - [0.70710677, -0.70710677, 0.0], [0.4082483, 0.4082483, -0.8164966], - [-0.70710677, 0.70710677, -9.9991995f-17], [-0.4082483, -0.4082483, 0.8164966], + [0.70710677, -0.70710677, 0.0], [0.4082483, 0.4082483, -0.8164966], + [-0.70710677, 0.70710677, -9.9991995f-17], [-0.4082483, -0.4082483, 0.8164966], [-0.57735026, -0.57735026, -0.57735026], [0.57735026, 0.57735026, 0.57735026] ], [ - GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), - GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), - QuadFace{Int64}(1, 2, 2, 1), QuadFace{Int64}(2, 3, 3, 2), - QuadFace{Int64}(3, 4, 4, 3), QuadFace{Int64}(4, 1, 1, 4), - GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6), + GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), + GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), + QuadFace{Int64}(1, 2, 2, 1), QuadFace{Int64}(2, 3, 3, 2), + QuadFace{Int64}(3, 4, 4, 3), QuadFace{Int64}(4, 1, 1, 4), + GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6) ] ) @@ -84,21 +84,20 @@ end @test decompose(Point2f, mesh) == pt_expa b = Rect(Vec(1, 1, 1), Vec(1, 1, 1)) - pt_expb = Point{3,Int64}[[1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 2, 2], - [2, 1, 1], [2, 1, 2], [2, 2, 1], [2, 2, 2]] + pt_expb = Point{3,Int64}[[1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 2, 2], + [2, 1, 1], [2, 1, 2], [2, 2, 1], [2, 2, 2]] @test decompose(Point{3,Int}, b) == pt_expb - mesh = normal_mesh(b) @test faces(mesh) == GLTriangleFace[ - (1, 2, 4), (1, 4, 3), (7, 8, 6), (7, 6, 5), (5, 6, 2), (5, 2, 1), + (1, 2, 4), (1, 4, 3), (7, 8, 6), (7, 6, 5), (5, 6, 2), (5, 2, 1), (3, 4, 8), (3, 8, 7), (1, 3, 7), (1, 7, 5), (6, 8, 4), (6, 4, 2)] @test normals(mesh) == GeometryBasics.FaceView( - Vec{3, Float32}[[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]], + Vec{3, Float32}[[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]], GLTriangleFace[(1, 1, 1), (1, 1, 1), (2, 2, 2), (2, 2, 2), (3, 3, 3), (3, 3, 3), (4, 4, 4), (4, 4, 4), (5, 5, 5), (5, 5, 5), (6, 6, 6), (6, 6, 6)] ) @test coordinates(mesh) == Point{3, Float32}[ - [1.0, 1.0, 1.0], [1.0, 1.0, 2.0], [1.0, 2.0, 1.0], [1.0, 2.0, 2.0], + [1.0, 1.0, 1.0], [1.0, 1.0, 2.0], [1.0, 2.0, 1.0], [1.0, 2.0, 2.0], [2.0, 1.0, 1.0], [2.0, 1.0, 2.0], [2.0, 2.0, 1.0], [2.0, 2.0, 2.0]] @test isempty(Rect{3,Float32}()) @@ -248,7 +247,7 @@ end h1 = Rect(0.0, 0.0, 1.0, 1.0) h2 = Rect(1.0, 1.0, 2.0, 2.0) @test union(h1, h2) isa GeometryBasics.HyperRectangle{2,Float64} - # @test GeometryBasics.diff(h1, h2) == h1 + # @test GeometryBasics.diff(h1, h2) == h1 @test GeometryBasics.intersect(h1, h2) isa GeometryBasics.HyperRectangle{2,Float64} b = Rect(0.0, 0.0, 1.0, 1.0) From 9c97cf9ffd94be13e2a9fcab50e660b6e04e9fbf Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 3 Feb 2025 16:56:05 +0100 Subject: [PATCH 02/28] add tests for Rect constructors and refactor constructors --- src/GeometryBasics.jl | 2 +- src/primitives/rectangles.jl | 135 ++++++++++++++--------------------- test/geometrytypes.jl | 82 +++++++++++++++++++++ 3 files changed, 136 insertions(+), 83 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index c05416ce..815aea3a 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -58,7 +58,7 @@ export uv_mesh, normal_mesh, uv_normal_mesh export height, origin, radius, width, widths export HyperSphere, Circle, Sphere export Cylinder, Pyramid, extremity -export HyperRectangle, Rect, Rect2, Rect3, Recti, Rect2i, Rect3i, Rectf, Rect2f, Rect3f, Rectd, Rect2d, Rect3d +export HyperRectangle, Rect, Rect2, Rect3, Recti, Rect2i, Rect3i, Rectf, Rect2f, Rect3f, Rectd, Rect2d, Rect3d, RectT export before, during, meets, overlaps, intersects, finishes export centered, direction, area, volume, update export max_dist_dim, max_euclidean, max_euclideansq, min_dist_dim, min_euclidean diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index ab3702ad..e2e3b8b6 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -48,6 +48,10 @@ const Recti{N} = Rect{N,Int} const Rect2i = Rect2{Int} const Rect3i = Rect3{Int} + +# Constructors + + Rect() = Rect{2,Float32}() RectT{T}() where {T} = Rect{2,T}() Rect{N}() where {N} = Rect{N,Float32}() @@ -57,116 +61,83 @@ function Rect{N,T}() where {T,N} return Rect{N,T}(Vec{N,T}(typemax(T)), Vec{N,T}(typemin(T))) end -# conversion from other Rects -function Rect{N,T1}(a::Rect{N,T2}) where {N,T1,T2} - return Rect(Vec{N,T1}(minimum(a)), Vec{N,T1}(widths(a))) -end - -function Rect(v1::VecTypes{N,T1}, v2::VecTypes{N,T2}) where {N,T1,T2} - T = promote_type(T1, T2) - return Rect{N,T}(Vec{N,T}(v1), Vec{N,T}(v2)) -end - -function RectT{T}(v1::VecTypes{N}, v2::VecTypes{N}) where {N,T} - return if T <: Integer - Rect{N,T}(round.(T, v1), round.(T, v2)) - else - return Rect{N,T}(Vec{N,T}(v1), Vec{N,T}(v2)) - end -end - -function Rect{N}(v1::VecTypes{N}, v2::VecTypes{N}) where {N} - T = promote_type(eltype(v1), eltype(v2)) - return Rect{N,T}(Vec{N,T}(v1), Vec{N,T}(v2)) -end +# Rect(numbers...) +Rect(args::Vararg{Number, N}) where {N} = Rect{div(N, 2), promote_type(typeof.(args)...)}(args...) +RectT{T}(args::Vararg{Number, N}) where {N, T} = Rect{div(N, 2), T}(args...) +Rect{N}(args::Vararg{Number}) where {N} = Rect{N, promote_type(typeof.(args)...)}(args...) """ Rect(vals::Number...) -``` -Rect(vals::Number...) -``` Rect constructor for individually specified intervals. e.g. Rect(0,0,1,2) has origin == Vec(0,0) and width == Vec(1,2) """ -function Rect(vals::Vararg{Number, N}) where {N} - M, r = fldmod(N, 2) - (r == 0) || throw(ArgumentError("number of arguments must be even")) - origin, widths = ntuple(i -> vals[i], M), ntuple(i -> vals[i+M], M) - return Rect(Vec(origin), Vec(widths)) +function Rect{N, T}(vals::Vararg{Number, M}) where {N, M, T} + n, r = fldmod(M, 2) + if r != 0 || n != N + throw(ArgumentError("Number of arguments must be compatible with given or derived Rect size. Got $M arguments for a Rect{$N} expecting $(2 * N).")) + end + origin, widths = ntuple(i -> vals[i], N), ntuple(i -> vals[i+N], N) + return Rect{N, T}(Vec(origin), Vec(widths)) end -Rect3(a::Vararg{Number,6}) = Rect3(Vec{3}(a[1], a[2], a[3]), Vec{3}(a[4], a[5], a[6])) -Rect3(args::Vararg{Number,4}) = Rect3(Rect{2}(args...)) -#= -From different args -=# -function (Rect)(args::Vararg{Number,4}) - args_prom = promote(args...) - return Rect2{typeof(args_prom[1])}(args_prom...) -end +# VecTypes -function (Rect2)(args::Vararg{Number,4}) - args_prom = promote(args...) - return Rect2{typeof(args_prom[1])}(args_prom...) -end + Rect(o::VecTypes{N, T1}, w::VecTypes{N, T2}) where {N, T1, T2} = Rect{N, promote_type(T1, T2)}(o, w) +RectT{ T}(o::VecTypes{N}, w::VecTypes{N}) where {N, T} = Rect{N, T}(o, w) +Rect{N }(o::VecTypes{N, T1}, w::VecTypes{N, T2}) where {N, T1, T2} = Rect{N, promote_type(T1, T2)}(o, w) -function (Rect{2,T})(args::Vararg{Number,4}) where {T} - x, y, w, h = T <: Integer ? round.(T, args) : args - return Rect2{T}(Vec{2,T}(x, y), Vec{2,T}(w, h)) -end +# mixed number - vectype -function RectT{T}(args::Vararg{Number,4}) where {T} - x, y, w, h = T <: Integer ? round.(T, args) : args - return Rect2{T}(Vec{2,T}(x, y), Vec{2,T}(w, h)) -end + Rect(o::VecTypes{N, <:Number}, args::Vararg{Number, N}) where {N} = Rect{N }(o, promote(args...)) +RectT{ T}(o::VecTypes{N, <:Number}, args::Vararg{Number, N}) where {N, T} = Rect{N, T}(o, promote(args...)) +Rect{N }(o::VecTypes{N, <:Number}, args::Vararg{Number, N}) where {N} = Rect{N }(o, promote(args...)) +Rect{N, T}(o::VecTypes{N, <:Number}, args::Vararg{Number, N}) where {N, T} = Rect{N, T}(o, promote(args...)) -function Rect3f(x::Rect2{T}) where {T} - return Rect{3,T}(Vec{3,T}(minimum(x)..., 0), Vec{3,T}(widths(x)..., 0.0)) -end + Rect(x::Number, y::Number, w::VecTypes{2, <:Number}) = Rect{2 }(Vec(x, y), w) +RectT{ T}(x::Number, y::Number, w::VecTypes{2, <:Number}) where {T} = Rect{2, T}(Vec(x, y), w) +Rect{2 }(x::Number, y::Number, w::VecTypes{2, <:Number}) = Rect{2 }(Vec(x, y), w) +Rect{2, T}(x::Number, y::Number, w::VecTypes{2, <:Number}) where {T} = Rect{2, T}(Vec(x, y), w) -function Rect2{T}(a::Rect2) where {T} - return Rect2{T}(minimum(a), widths(a)) -end + Rect(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) = Rect{3 }(Vec(x, y, z), w) +RectT{ T}(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) where {T} = Rect{3, T}(Vec(x, y, z), w) +Rect{3 }(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) = Rect{3 }(Vec(x, y, z), w) +Rect{3, T}(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) where {T} = Rect{3, T}(Vec(x, y, z), w) -function RectT{T}(a::Rect2) where {T} - return Rect2{T}(minimum(a), widths(a)) -end +# copy constructors -function Rect{N,T}(a::GeometryPrimitive) where {N,T} - return Rect{N,T}(Vec{N,T}(minimum(a)), Vec{N,T}(widths(a))) -end + Rect(r::Rect{N, T}) where {N, T} = Rect{N, T}(origin(r), widths(r)) +RectT{ T}(r::Rect{N}) where {N, T} = Rect{N, T}(origin(r), widths(r)) +Rect{N }(r::Rect{N, T}) where {N, T} = Rect{N, T}(origin(r), widths(r)) +Rect{N, T}(r::Rect{N}) where {N, T} = Rect{N, T}(origin(r), widths(r)) -function Rect2(xy::VecTypes{2}, w::Number, h::Number) - return Rect2(xy..., w, h) -end +# dimensional promotion -function Rect2(x::Number, y::Number, wh::VecTypes{2}) - return Rect2(x, y, wh...) -end +Rect{3, T}(o::VecTypes{2}, w::VecTypes{3}) where {T} = Rect{3, T}(Vec(o..., 0), w) +Rect{3, T}(o::VecTypes{3}, w::VecTypes{2}) where {T} = Rect{3, T}(o, Vec(w..., 0)) +Rect{3, T}(o::VecTypes{2}, w::VecTypes{2}) where {T} = Rect{3, T}(Vec(o..., 0), Vec(w..., 0)) -function RectT{T}(xy::VecTypes{2}, w::Number, h::Number) where {T} - return Rect2{T}(xy..., w, h) -end +# Boundingbox-like constructors -function RectT{T}(x::Number, y::Number, wh::VecTypes{2}) where {T} - return Rect2{T}(x, y, wh...) -end +# Rect(r::GeometryPrimitive{N, T}) where {N, T} = Rect{N, T}(minimum(r), widths(r)) # in boundingboxes.jl +RectT{T}(r::GeometryPrimitive{N}) where {N, T} = Rect{N, T}(minimum(r), widths(r)) +Rect{N}(r::GeometryPrimitive{_N, T}) where {N, _N, T} = Rect{N, T}(minimum(r), widths(r)) +Rect{N, T}(r::GeometryPrimitive) where {N, T} = Rect{N, T}(minimum(r), widths(r)) # TODO These are kinda silly function Rect2(xy::NamedTuple{(:x, :y)}, wh::NamedTuple{(:width, :height)}) return Rect2(xy.x, xy.y, wh.width, wh.height) end -function Rect3f(x::Tuple{Tuple{<:Number,<:Number},Tuple{<:Number,<:Number}}) - return Rect3f(Vec3f(x[1]..., 0), Vec3f(x[2]..., 0)) -end +Rect(ow::Tuple) = Rect(ow...) +RectT{T}(ow::Tuple) where {T} = RectT{T}(ow...) +Rect{N}(ow::Tuple) where {N} = Rect{N}(ow...) +Rect{N, T}(ow::Tuple) where {N, T} = Rect{N, T}(ow...) + + +# Utilities -function Rect3f(x::Tuple{Tuple{<:Number,<:Number,<:Number}, - Tuple{<:Number,<:Number,<:Number}}) - return Rect3f(Vec3f(x[1]...), Vec3f(x[2]...)) -end # allow auto-conversion between different eltypes Base.convert(::Type{Rect{N, T}}, r::Rect{N}) where {N, T} = Rect{N, T}(r) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index e8e7ea72..02f85363 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -77,6 +77,88 @@ using Test, GeometryBasics end @testset "HyperRectangles" begin + @testset "Constructors" begin + # TODO: Do these actually make sense? + # Should they not be Rect(NaN..., 0...)? + @testset "Empty Constructors" begin + for constructor in [Rect, Rect{2}, Rect2, RectT, Rect2f] + @test constructor() == Rect{2, Float32}(Inf, Inf, -Inf, -Inf) + end + for constructor in [Rect{3}, Rect3, Rect3f] + @test constructor() == Rect{3, Float32}((Inf, Inf, Inf), (-Inf, -Inf, -Inf)) + end + + for T in [UInt32, Int16, Float64] + a = typemax(T) + b = typemin(T) + for constructor in [Rect{2, T}, Rect2{T}, RectT{T, 2}] + @test constructor() == Rect{2, T}(a, a, b, b) + end + for constructor in [Rect{3, T}, Rect3{T}, RectT{T, 3}] + @test constructor() == Rect{3, T}(Point(a, a, a), Vec(b, b, b)) + end + end + end + + @testset "Constructor arg conversions" begin + function expected_rect(::Type{<: Rect}, arg1, arg2) + return Rect{min(length(arg1), length(arg2)), promote_type(eltype(arg1), eltype(arg2))}(arg1, arg2) + end + function expected_rect(::Type{<: Rect{N}}, arg1, arg2) where {N} + return Rect{N, promote_type(eltype(arg1), eltype(arg2))}(arg1, arg2) + end + function expected_rect(::Type{<: Rect{N, T}}, arg1, arg2) where {N, T} + return Rect{N, T}(arg1, arg2) + end + + @testset "2D args -> 2D Rect" begin + for constructor in [Rect, RectT, Rect2, Rect{2}, RectT{Int32}, + Rect2f, Rect{2, Float16}, Rect2{UInt32}, RectT{Float64, 2}] + @testset "$constructor" begin + @test constructor(1,2,3,4) == expected_rect(constructor, Point(1,2), Vec(3,4)) + @test constructor(1.0,2,3,4) == expected_rect(constructor, Point(1.0,2), Vec(3,4)) + @test constructor(Point2f(1,2),3,4) == expected_rect(constructor, Point2f(1,2), Vec(3,4)) + @test constructor(Vec2(1,2),3,4.0) == expected_rect(constructor, Point(1,2), Vec(3,4.0)) + @test constructor((1,2),Point2(3,4)) == expected_rect(constructor, Point(1,2), Vec(3,4)) + @test constructor(1.0,2,Vec2(3,4)) == expected_rect(constructor, Point(1,2), Vec(3,4)) + end + end + end + + @testset "3D args -> 3D Rect" begin + for constructor in [Rect, RectT, Rect3, Rect{3}, RectT{Float64}, + Rect3d, Rect{3, Int16}, Rect3{UInt8}, RectT{Float32, 3}] + @testset "$constructor" begin + @test constructor(1,2,3,4,5,6) == expected_rect(constructor, Point(1,2,3), Vec(4,5,6)) + @test constructor(1,2,3,4,5,6.0) == expected_rect(constructor, Point(1,2,3), Vec(4,5,6.0)) + @test constructor(1,2.0,3,Vec3f(4,5,6)) == expected_rect(constructor, Point(1,2,3), Vec3f(4,5,6)) + @test constructor(Vec3(1,2,3),4,5,6) == expected_rect(constructor, Point3(1,2,3), Vec(4,5,6)) + @test constructor((1,2,3),Point3(4,5,6)) == expected_rect(constructor, Point(1,2,3), Vec(4,5,6)) + @test constructor(Vec3(1,2,3),4,5,6) == expected_rect(constructor, Point(1,2,3), Vec(4,5,6)) + end + end + end + end + + @testset "Copy Constructors" begin + r = Rect2i(0,0,1,1) + for constructor in [Rect, Rect2f, Rect3f, RectT{Float64}] + @test constructor(r) == constructor(Point2(0), Vec2(1)) + end + end + + @testset "Special Constructors" begin + @test Rect3f((1, 2, 3, Vec(1,2,3))) == Rect3f(1,2,3, Vec(1,2,3)) + @test Rect2(((1, 2), 3, 4)) == Rect2f((1,2), 3, 4) + @test Rect((1, 2, 3, 4)) == Rect2f(1, 2, 3, 4) + @test Rect2((x = 1, y = 2), (width = 3, height = 4)) == Rect2f(1, 2, 3, 4) + end + + # TODO: test/check for Rect(::GeometryPrimitive) for all primitives + end + + # TODO: origin, minimum, maximum, width, height, widths, area, volume with empty constructed Rects + a = Rect(Vec(0, 0), Vec(1, 1)) pt_expa = Point{2,Int}[(0, 0), (1, 0), (1, 1), (0, 1)] @test decompose(Point{2,Int}, a) == pt_expa From 2b6257321504d5a2f6606403165e7a168bdfc2da Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 3 Feb 2025 18:50:43 +0100 Subject: [PATCH 03/28] test the rest of Rect --- src/primitives/rectangles.jl | 33 +++++++------ test/geometrytypes.jl | 91 +++++++++++++++++++++++++++++------- 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index e2e3b8b6..e589c735 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -125,6 +125,13 @@ RectT{T}(r::GeometryPrimitive{N}) where {N, T} = Rect{N, T}(minimum(r), widths(r Rect{N}(r::GeometryPrimitive{_N, T}) where {N, _N, T} = Rect{N, T}(minimum(r), widths(r)) Rect{N, T}(r::GeometryPrimitive) where {N, T} = Rect{N, T}(minimum(r), widths(r)) +# centered Rects + +centered(R::Type{Rect{N,T}}) where {N,T} = R(Vec{N,T}(-0.5), Vec{N,T}(1)) +centered(R::Type{RectT{T}}) where {T} = R(Vec{2,T}(-0.5), Vec{2,T}(1)) +centered(R::Type{Rect{N}}) where {N} = R(Vec{N,Float32}(-0.5), Vec{N,Float32}(1)) +centered(R::Type{Rect}) = R(Vec{2,Float32}(-0.5), Vec{2,Float32}(1)) + # TODO These are kinda silly function Rect2(xy::NamedTuple{(:x, :y)}, wh::NamedTuple{(:width, :height)}) return Rect2(xy.x, xy.y, wh.width, wh.height) @@ -337,11 +344,11 @@ function Base.intersect(h1::Rect{N}, h2::Rect{N}) where {N} return Rect{N}(m, mm - m) end -function update(b::Rect{N,T}, v::Vec{N,T2}) where {N,T,T2} +function update(b::Rect{N,T}, v::VecTypes{N,T2}) where {N,T,T2} return update(b, Vec{N,T}(v)) end -function update(b::Rect{N,T}, v::Vec{N,T}) where {N,T} +function update(b::Rect{N,T}, v::VecTypes{N,T}) where {N,T} m = min.(minimum(b), v) maxi = maximum(b) mm = if any(isnan, maxi) @@ -353,11 +360,11 @@ function update(b::Rect{N,T}, v::Vec{N,T}) where {N,T} end # Min maximum distance functions between hrectangle and point for a given dimension -function min_dist_dim(rect::Rect{N,T}, p::Vec{N,T}, dim::Int) where {N,T} +function min_dist_dim(rect::Rect{N,T}, p::VecTypes{N,T}, dim::Int) where {N,T} return max(zero(T), max(minimum(rect)[dim] - p[dim], p[dim] - maximum(rect)[dim])) end -function max_dist_dim(rect::Rect{N,T}, p::Vec{N,T}, dim::Int) where {N,T} +function max_dist_dim(rect::Rect{N,T}, p::VecTypes{N,T}, dim::Int) where {N,T} return max(maximum(rect)[dim] - p[dim], p[dim] - minimum(rect)[dim]) end @@ -373,7 +380,7 @@ function max_dist_dim(rect1::Rect{N,T}, rect2::Rect{N,T}, dim::Int) where {N,T} end # Total minimum maximum distance functions -function min_euclideansq(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N,T} +function min_euclideansq(rect::Rect{N,T}, p::Union{VecTypes{N,T},Rect{N,T}}) where {N,T} minimum_dist = T(0.0) for dim in 1:length(p) d = min_dist_dim(rect, p, dim) @@ -382,7 +389,7 @@ function min_euclideansq(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N return minimum_dist end -function max_euclideansq(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N,T} +function max_euclideansq(rect::Rect{N,T}, p::Union{VecTypes{N,T},Rect{N,T}}) where {N,T} maximum_dist = T(0.0) for dim in 1:length(p) d = max_dist_dim(rect, p, dim) @@ -391,29 +398,29 @@ function max_euclideansq(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N return maximum_dist end -function min_euclidean(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N,T} +function min_euclidean(rect::Rect{N,T}, p::Union{VecTypes{N,T},Rect{N,T}}) where {N,T} return sqrt(min_euclideansq(rect, p)) end -function max_euclidean(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N,T} +function max_euclidean(rect::Rect{N,T}, p::Union{VecTypes{N,T},Rect{N,T}}) where {N,T} return sqrt(max_euclideansq(rect, p)) end # Functions that return both minimum and maximum for convenience -function minmax_dist_dim(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}, +function minmax_dist_dim(rect::Rect{N,T}, p::Union{VecTypes{N,T},Rect{N,T}}, dim::Int) where {N,T} minimum_d = min_dist_dim(rect, p, dim) maximum_d = max_dist_dim(rect, p, dim) return minimum_d, maximum_d end -function minmax_euclideansq(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N,T} +function minmax_euclideansq(rect::Rect{N,T}, p::Union{VecTypes{N,T},Rect{N,T}}) where {N,T} minimum_dist = min_euclideansq(rect, p) maximum_dist = max_euclideansq(rect, p) return minimum_dist, maximum_dist end -function minmax_euclidean(rect::Rect{N,T}, p::Union{Vec{N,T},Rect{N,T}}) where {N,T} +function minmax_euclidean(rect::Rect{N,T}, p::Union{VecTypes{N,T},Rect{N,T}}) where {N,T} minimumsq, maximumsq = minmax_euclideansq(rect, p) return sqrt(minimumsq), sqrt(maximumsq) end @@ -502,10 +509,6 @@ Base.:(==)(b1::Rect, b2::Rect) = minimum(b1) == minimum(b2) && widths(b1) == wid Base.isequal(b1::Rect, b2::Rect) = b1 == b2 -centered(R::Type{Rect{N,T}}) where {N,T} = R(Vec{N,T}(-0.5), Vec{N,T}(1)) -centered(R::Type{Rect{N}}) where {N} = R(Vec{N,Float32}(-0.5), Vec{N,Float32}(1)) -centered(R::Type{Rect}) = R(Vec{2,Float32}(-0.5), Vec{2,Float32}(1)) - ## # Rect2 decomposition diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 02f85363..6a01e936 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -121,6 +121,7 @@ end @test constructor(Vec2(1,2),3,4.0) == expected_rect(constructor, Point(1,2), Vec(3,4.0)) @test constructor((1,2),Point2(3,4)) == expected_rect(constructor, Point(1,2), Vec(3,4)) @test constructor(1.0,2,Vec2(3,4)) == expected_rect(constructor, Point(1,2), Vec(3,4)) + @test_throws ArgumentError constructor(1,2,3) end end end @@ -135,6 +136,7 @@ end @test constructor(Vec3(1,2,3),4,5,6) == expected_rect(constructor, Point3(1,2,3), Vec(4,5,6)) @test constructor((1,2,3),Point3(4,5,6)) == expected_rect(constructor, Point(1,2,3), Vec(4,5,6)) @test constructor(Vec3(1,2,3),4,5,6) == expected_rect(constructor, Point(1,2,3), Vec(4,5,6)) + @test_throws ArgumentError constructor(1,2,3) end end end @@ -152,6 +154,14 @@ end @test Rect2(((1, 2), 3, 4)) == Rect2f((1,2), 3, 4) @test Rect((1, 2, 3, 4)) == Rect2f(1, 2, 3, 4) @test Rect2((x = 1, y = 2), (width = 3, height = 4)) == Rect2f(1, 2, 3, 4) + + for constructor in [Rect, RectT, Rect2, Rect{2}, RectT{Float32}, + Rect2f, Rect{2, Float16}, Rect2{Float16}, RectT{Float64, 2}] + @test centered(constructor) == constructor(Point2d(-0.5), Vec2d(1)) + end + for constructor in [Rect3, Rect{3}, Rect3d, Rect{3, Float16}, Rect3{Float64}, RectT{Float32, 3}] + @test centered(constructor) == constructor(Point3d(-0.5), Vec3d(1)) + end end # TODO: test/check for Rect(::GeometryPrimitive) for all primitives @@ -306,8 +316,17 @@ end @test widths(split1) == widths(split2) @test origin(split1) == Vec(0, 0) @test origin(split2) == Vec(0, 1) - @test in(split1, rect1) - @test !in(rect1, split1) + @test in(split1, rect1) && in(split2, rect1) + @test !(in(rect1, split1) || in(rect1, split2)) + + rect1 = Rect(Vec(0.0, 0.0, -1.0), Vec(1.0, 2.0, 1.0)) + split1, split2 = GeometryBasics.split(rect1, 1, 0.75) + @test widths(split1) == Vec(0.75, 2, 1) + @test widths(split2) == Vec(0.25, 2, 1) + @test origin(split1) == Vec(0, 0, -1) + @test origin(split2) == Vec(0.75, 0, -1) + @test in(split1, rect1) && in(split2, rect1) + @test !(in(rect1, split1) || in(rect1, split2)) prim = Rect(0.0, 0.0, 1.0, 1.0) @test length(prim) == 2 @@ -338,19 +357,33 @@ end v = Vec(1.0, 2.0) @test update(b, v) isa GeometryBasics.HyperRectangle{2,Float64} - p = Vec(5.0, 4.0) - rect = Rect(0.0, 0.0, 1.0, 1.0) - @test min_dist_dim(rect, p, 1) == 4.0 - @test min_dist_dim(rect, p, 2) == 3.0 - @test max_dist_dim(rect, p, 1) == 5.0 - @test max_dist_dim(rect, p, 2) == 4.0 - - rect1 = Rect(0.0, 0.0, 1.0, 1.0) - rect2 = Rect(3.0, 1.0, 4.0, 2.0) - @test min_dist_dim(rect1, rect2, 1) == 2.0 - @test min_dist_dim(rect1, rect2, 2) == 0.0 - @test max_dist_dim(rect1, rect2, 1) == 7.0 - @test max_dist_dim(rect1, rect2, 2) == 3.0 + @testset "euclidean distances" begin + p = Vec(5.0, 4.0) + rect = Rect(0.0, 0.0, 1.0, 1.0) + @test min_dist_dim(rect, p, 1) == 4.0 + @test min_dist_dim(rect, p, 2) == 3.0 + @test max_dist_dim(rect, p, 1) == 5.0 + @test max_dist_dim(rect, p, 2) == 4.0 + @test minmax_dist_dim(rect, p, 1) == (4.0, 5.0) + + rect1 = Rect(0.0, 0.0, 1.0, 1.0) + rect2 = Rect(3.0, 1.0, 4.0, 2.0) + @test min_dist_dim(rect1, rect2, 1) == 2.0 + @test min_dist_dim(rect1, rect2, 2) == 0.0 + @test max_dist_dim(rect1, rect2, 1) == 7.0 + @test max_dist_dim(rect1, rect2, 2) == 3.0 + @test minmax_dist_dim(rect1, rect2, 1) == (2.0, 7.0) + + r = Rect2f(-1, -1, 2, 3) + p = Point2f(1, 2) + Point2f(3, 4) + @test min_euclidean(r, p) == 5f0 + @test max_euclidean(r, p) ≈ sqrt(5*5 + 7*7) + + r2 = Rect2f(0, 0, 2, 3) + @test min_euclidean(r, r2) == 0f0 + @test max_euclidean(r, r2) == 5f0 + @test minmax_euclidean(r, r2) == (0f0, 5f0) + end @test !before(rect1, rect2) rect1 = Rect(0.0, 0.0, 1.0, 1.0) @@ -397,5 +430,29 @@ end rect2 = Rect(Vec(1, 2, 3, 4), Vec(5, 6, 7, 8)) @test rect1 == rect2 - @test_throws ArgumentError Rect(1, 2, 3) -end + @testset "Matrix Multiplications" begin + r = Rect2f(-1, -2, 4, 3) + + # TODO: this seems quite dangerous: We pad points with ones which makes + # sense for translations if we go to D+1, but is nonsense if we + # go higher dimensions than that. + M = rand(Mat4f) + ps = Point2f[M * Point(p..., 1, 1) for p in coordinates(r)] + @test Rect2f(ps) == M * r + + M = Mat2f(0.5, -0.3, 0.7, 1.5) + ps = Point2f[M * p for p in coordinates(r)] + @test Rect2f(ps) == M * r + + r = Rect3f(-1, -2, -3, 2, 4, 1) + M = rand(Mat4f) + ps = Point3f[M * Point(p..., 1) for p in coordinates(r)] + @test Rect3f(ps) == M * r + end + + # TODO: this is effectively 0-indexed... should it be? + M = reshape(collect(11:100), 10, 9)[1:9, :] + r = Rect2i(2, 4, 2, 4) + @test M[r] == [53 63 73 83; 54 64 74 84] + +end \ No newline at end of file From 2140dde4313cf618444aedb2e2f7cf5067ca83ac Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Feb 2025 03:26:11 +0100 Subject: [PATCH 04/28] more tests for basic_types.jl --- src/basic_types.jl | 2 ++ test/geometrytypes.jl | 71 ++++++++++++++++++++++++++++++++++++++----- test/meshes.jl | 30 +++++++++++++++++- test/polygons.jl | 23 +++++++++++++- test/runtests.jl | 52 ++++++++++++++++++------------- 5 files changed, 148 insertions(+), 30 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 857cf8ab..b353816f 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -337,6 +337,7 @@ end Base.getindex(mp::MultiPolygon, i) = mp.polygons[i] Base.size(mp::MultiPolygon) = size(mp.polygons) Base.length(mp::MultiPolygon) = length(mp.polygons) +Base.:(==)(a::MultiPolygon, b::MultiPolygon) = a.polygons == b.polygons """ LineString(points::AbstractVector{<:Point}) @@ -361,6 +362,7 @@ end Base.getindex(ms::MultiLineString, i) = ms.linestrings[i] Base.size(ms::MultiLineString) = size(ms.linestrings) Base.length(mpt::MultiLineString) = length(mpt.linestrings) +Base.:(==)(a::MultiLineString, b::MultiLineString) = a.linestrings == b.linestrings """ MultiPoint(points::AbstractVector{AbstractPoint}) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 6a01e936..27bdb1a7 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -204,16 +204,19 @@ end @test values(ns) ≈ Vec3f[[0.9805807, 0.0, 0.19611615], [0.0, 0.9805807, 0.19611615], [-0.9805807, 0.0, 0.19611615], [0.0, -0.9805807, 0.19611615], [0.0, 0.0, -1.0]] end -NFace = NgonFace +@testset "Type Utils" begin + @test GeometryBasics.Face(TriangleFace, Int32) == TriangleFace{Int32} + @test GeometryBasics.Face(GLTriangleFace, Int32) == TriangleFace{GLIndex} +end @testset "Faces" begin @test convert_simplex(GLTriangleFace, QuadFace{Int}(1, 2, 3, 4)) == (GLTriangleFace(1, 2, 3), GLTriangleFace(1, 3, 4)) - @test convert_simplex(NFace{3,ZeroIndex{Int}}, QuadFace{ZeroIndex{Int}}(1, 2, 3, 4)) == - (NFace{3,ZeroIndex{Int}}(1, 2, 3), NFace{3,ZeroIndex{Int}}(1, 3, 4)) - @test convert_simplex(NFace{3,OffsetInteger{3,Int}}, - NFace{4,OffsetInteger{2,Int}}(1, 2, 3, 4)) == - (NFace{3,OffsetInteger{3,Int}}(1, 2, 3), NFace{3,OffsetInteger{3,Int}}(1, 3, 4)) + @test convert_simplex(NgonFace{3,ZeroIndex{Int}}, QuadFace{ZeroIndex{Int}}(1, 2, 3, 4)) == + (NgonFace{3,ZeroIndex{Int}}(1, 2, 3), NgonFace{3,ZeroIndex{Int}}(1, 3, 4)) + @test convert_simplex(NgonFace{3,OffsetInteger{3,Int}}, + NgonFace{4,OffsetInteger{2,Int}}(1, 2, 3, 4)) == + (NgonFace{3,OffsetInteger{3,Int}}(1, 2, 3), NgonFace{3,OffsetInteger{3,Int}}(1, 3, 4)) @test convert_simplex(LineFace{Int}, QuadFace{Int}(1, 2, 3, 4)) == (LineFace{Int}(1, 2), LineFace{Int}(2, 3), LineFace{Int}(3, 4), LineFace{Int}(4, 1)) @@ -226,6 +229,33 @@ NFace = NgonFace @test convert_simplex(NgonFace{1,UInt32}, face) === (NgonFace{1,UInt32}((1,)),) @test convert_simplex(typeof(face), face) === (face,) end + + ps = rand(Point2f, 10) + f = GLTriangleFace(1, 2, 3) + @test ps[f] == Triangle(ps[[1,2,3]]...) + data = [string(i) for i in 1:10] + 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)) + # 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)) +end + +@testset "FaceView" begin + ps = rand(Point2f, 5) + fs = GLTriangleFace[(1,2,3), (2,3,4), (5,5,5)] + fv = FaceView(ps, fs) + @test faces(fv) == fs + @test values(fv) == ps + @test fv[fs[1]] == ps[fs[1]] + @test !isempty(fv) + @test fv == FaceView(ps, fs) + @test length(fv) == 5 end @testset "Normals" begin @@ -253,7 +283,6 @@ end v_ns = normals(coordinates(c), filter!(f -> f isa QuadFace, faces(c)))[1:end-2] @test values(ns)[1:15] ≈ v_ns[1:15] @test values(ns)[1:15] ≈ v_ns[16:30] # repeated via FaceView in ns - end @testset "HyperSphere" begin @@ -455,4 +484,32 @@ end r = Rect2i(2, 4, 2, 4) @test M[r] == [53 63 73 83; 54 64 74 84] +end + +@testset "LineStrings" begin + ps1 = rand(Point2f, 10) + ls1 = LineString(ps1) + _ls1 = LineString(ps1) + @test coordinates(ls1) == ps1 + @test length(ls1) == 10 + @test ls1 == _ls1 + + ls2 = LineString(rand(Point2f, 6)) + ls3 = LineString(rand(Point2f, 4)) + mls = MultiLineString([ls1, ls2, ls3]) + @test mls.linestrings == [ls1, ls2, ls3] + @test mls[1] == ls1 + @test mls[2] == ls2 + @test mls[3] == ls3 + @test size(mls) == (3, ) # TODO: Does this make sense? + @test length(mls) == 3 + @test MultiLineString(OffsetArray([ls1, ls2, ls3], 0)) == mls +end + +@testset "MultiPoint" begin + ps1 = rand(Point2f, 10) + mp = MultiPoint(ps1) + @test all(getindex.(Ref(mp), 1:10) .== ps1) + @test size(mp) == (10, ) # TODO: Does this make sense? + @test length(mp) == 10 end \ No newline at end of file diff --git a/test/meshes.jl b/test/meshes.jl index f15ad37b..03ca71eb 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -173,15 +173,27 @@ end @test hasproperty(m, :uv) @test hasproperty(m, :faces) + @test GeometryBasics.meta(m) == NamedTuple() + @test Mesh(m) === m + mm = MetaMesh(m, name = "test") @test Mesh(mm) == m @test haskey(mm, :name) @test get(mm, :name, nothing) == "test" + @test get(() -> nothing, mm, :name) == "test" + @test get(mm, :foo, nothing) === nothing + @test get(() -> nothing, mm, :foo) === nothing @test mm[:name] == "test" @test !haskey(mm, :foo) - @test get!(mm, :foo, "bar") == "bar" + @test get!(mm, :foo, "foo") == "foo" @test haskey(mm, :foo) + @test get!(() -> "bar", mm, :bar) == "bar" + @test haskey(mm, :bar) + mm[:foo] = "bar" + @test mm[:foo] == "bar" + delete!(mm, :bar) + @test !haskey(mm, :bar) @test keys(mm) == keys(getfield(mm, :meta)) @test vertex_attributes(mm) == getfield(m, :vertex_attributes) @@ -189,6 +201,22 @@ end @test normals(mm) == vertex_attributes(m)[:normal] @test texturecoordinates(mm) == vertex_attributes(m)[:uv] @test faces(mm) == getfield(m, :faces) + + @test hasproperty(mm, :vertex_attributes) + @test hasproperty(mm, :position) + @test hasproperty(mm, :normal) + @test hasproperty(mm, :uv) + @test hasproperty(mm, :faces) + @test propertynames(mm) == (:mesh, :meta, propertynames(m)...) + + @test mm.vertex_attributes == getfield(m, :vertex_attributes) + @test mm.position == vertex_attributes(m)[:position] + @test mm.normal == vertex_attributes(m)[:normal] + @test mm.uv == vertex_attributes(m)[:uv] + @test mm.faces == getfield(m, :faces) + + @test GeometryBasics.meta(mm) == mm.meta + @test Mesh(mm) === mm.mesh end @testset "mesh() constructors" begin diff --git a/test/polygons.jl b/test/polygons.jl index de15805d..6afacc5e 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -1,8 +1,29 @@ @testset "Polygon" begin - @testset "from points" begin + @testset "Constructors" begin points = connect([1, 2, 3, 4, 5, 6], Point2f) polygon = Polygon(points) @test polygon == Polygon(points) + @test polygon == copy(polygon) + @test coordinates(polygon) == points + @test Polygon(OffsetArray(points)) == polygon + + interiors = [rand(Point2f, 4), rand(Point2f, 4)] + exterior = rand(Point2f, 5) + p1 = Polygon(exterior, interiors) + @test p1.interiors == interiors + @test p1.exterior == exterior + p2 = Polygon(OffsetArray(exterior, 0), interiors) + @test p2 == p1 + + # TODO: promote polygon type automatically + polygon = Polygon(Point2f.(points)) + mp = MultiPolygon([polygon, p1, p2]) + @test mp.polygons == [polygon, p1, p2] + @test mp[1] == polygon + @test mp[2] == p1 + @test size(mp) == (3,) # TODO: What does size even mean here? + @test length(mp) == 3 + @test MultiPolygon(OffsetArray([polygon, p1, p2], 0)) == mp end rect = Rect2f(0, 0, 1, 1) diff --git a/test/runtests.jl b/test/runtests.jl index 19d19b15..ee297109 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -92,6 +92,9 @@ end tetrahedra = connect(x, NSimplex{4}) @test tetrahedra == [Tetrahedron(x[1], x[2], x[3], x[4])] + @test length(tetrahedra[1]) == 4 + @test all(coordinates(tetrahedra[1]) .== x) + @testset "matrix non-copy point views" begin # point in row points = [1 2; 1 4; 66 77] @@ -237,7 +240,7 @@ end @test GeometryBasics.faces(m) isa Vector{GLTriangleFace} end -@testset "lines intersects" begin +@testset "Lines" begin a = Line(Point(0.0, 0.0), Point(4.0, 1.0)) b = Line(Point(0.0, 0.25), Point(3.0, 0.25)) c = Line(Point(0.0, 0.25), Point(0.5, 0.25)) @@ -245,28 +248,35 @@ end e = Line(Point(1.0, 0.0), Point(0.0, 4.0)) f = Line(Point(5.0, 0.0), Point(6.0, 0.0)) - @test intersects(a, b) === (true, Point(1.0, 0.25)) - @test intersects(a, c) === (false, Point(0.0, 0.0)) - @test intersects(d, d) === (false, Point(0.0, 0.0)) - found, point = intersects(d, e) - @test found && point ≈ Point(0.0, 4.0) - @test intersects(a, f) === (false, Point(0.0, 0.0)) - - # issue #168 - # If these tests fail then you can increase the tolerance on the checks so - # long as you know what you're doing :) - line_helper(a, b, c, d) = Line(Point(a, b), Point(c, d)) - b, loc = intersects(line_helper(-3.1, 15.588457268119894, 3.1, 15.588457268119894), - line_helper(2.0866025403784354, 17.37050807568877, -4.0866025403784505, 13.806406460551015)) - @test b - @test loc ≈ Point(-1.0000000000000058, 15.588457268119894) - - b, loc = intersects(line_helper(5743.933982822018, 150.0, 5885.355339059327, -50.0), - line_helper(5760.0, 100.0, 5760.0, 140.0)) - @test b - @test loc ≈ Point(5760.0, 127.27922061357884) + multi_line = [a,b,c,d,e,f] + @test coordinates(multi_line) == vcat([[x.points...] for x in multi_line]...) + + @testset "intersects" begin + @test intersects(a, b) === (true, Point(1.0, 0.25)) + @test intersects(a, c) === (false, Point(0.0, 0.0)) + @test intersects(d, d) === (false, Point(0.0, 0.0)) + found, point = intersects(d, e) + @test found && point ≈ Point(0.0, 4.0) + @test intersects(a, f) === (false, Point(0.0, 0.0)) + + # issue #168 + # If these tests fail then you can increase the tolerance on the checks so + # long as you know what you're doing :) + line_helper(a, b, c, d) = Line(Point(a, b), Point(c, d)) + b, loc = intersects(line_helper(-3.1, 15.588457268119894, 3.1, 15.588457268119894), + line_helper(2.0866025403784354, 17.37050807568877, -4.0866025403784505, 13.806406460551015)) + @test b + @test loc ≈ Point(-1.0000000000000058, 15.588457268119894) + + b, loc = intersects(line_helper(5743.933982822018, 150.0, 5885.355339059327, -50.0), + line_helper(5760.0, 100.0, 5760.0, 140.0)) + @test b + @test loc ≈ Point(5760.0, 127.27922061357884) + end end + + @testset "Offsetintegers" begin x = 1 @test GeometryBasics.raw(x) isa Int From 558b3646e32956daf768e6fa5ce70c63e7a90539 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Feb 2025 14:12:54 +0100 Subject: [PATCH 05/28] add triangulation tests --- test/polygons.jl | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/polygons.jl b/test/polygons.jl index 6afacc5e..e53ced00 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -55,3 +55,47 @@ @test Polygon(p) == Polygon(Array(p)) end end + +@testset "triangulation" begin + tri = Triangle(Point2f(-0.5, -0.5), Point2f(0.8, 0), Point2f(0, 0.8)) + for phi in rand(0:2pi, 10) + @test !in(Point2f(cos(phi), sin(phi)), tri) + end + for phi in rand(0:2pi, 10) + @test in(0.2 * Point2f(cos(phi), sin(phi)), tri) + end + @test Point2f(0) in tri # sanity check + + # corner cases + @test Point2f(-0.5) in tri + @test Point2f(0, 0.8) in tri + @test Point2f(0.8, 0) in tri + + # TODO: test snip directly? + ps = Point2f[(0,0), (1,0), (1,1), (0,1)] + @test decompose(GLTriangleFace, ps) == GLTriangleFace[(4,1,2), (2,3,4)] + + ps = [Point2f(cos(phi), sin(phi)) for phi in range(0, 2pi, length=8)[1:end-1]] + fs = decompose(GLTriangleFace, ps) + @test fs == GLTriangleFace[(7,1,2), (2,3,4), (4,5,6), (6,7,2), (2,4,6)] + lfs = decompose(LineFace{Int32}, fs) + for i in 1:7 + @test (LineFace(i, mod1(i+1, 7)) in lfs) || (LineFace(mod1(i+1, 7), i) in lfs) + end + + @testset "earcut" begin + ps = Point2i[(-1,-1), (1,-1), (1,1), (-1,1)] + @test GeometryBasics.earcut_triangulate([Point{2, Int32}.(ps)]) == GLTriangleFace[(3,4,1), (1,2,3)] + @test GeometryBasics.earcut_triangulate([Point{2, Int64}.(ps)]) == GLTriangleFace[(3,4,1), (1,2,3)] + @test GeometryBasics.earcut_triangulate([Point{2, Float32}.(ps)]) == GLTriangleFace[(3,4,1), (1,2,3)] + @test GeometryBasics.earcut_triangulate([Point{2, Float64}.(ps)]) == GLTriangleFace[(3,4,1), (1,2,3)] + + ps2 = Point2i[(0,-1), (1,0), (0,1), (-1,0)] + @test faces(Polygon(Point2{Int32}.(ps), [Point2{Int32}.(ps2)])) == GLTriangleFace[(4,8,7), (5,8,1), (6,5,2), (3,7,6)] + @test faces(Polygon(Point2{Int64}.(ps), [Point2{Int64}.(ps2)])) == GLTriangleFace[(4,8,7), (5,8,1), (6,5,2), (3,7,6)] + @test faces(Polygon(Point2{Int8}.(ps), [Point2{Int8}.(ps2)])) == GLTriangleFace[(4,8,7), (5,8,1), (6,5,2), (3,7,6)] + @test faces(Polygon(Point2{Float32}.(ps), [Point2{Float32}.(ps2)])) == GLTriangleFace[(4,8,7), (5,8,1), (6,5,2), (3,7,6)] + @test faces(Polygon(Point2{Float64}.(ps), [Point2{Float64}.(ps2)])) == GLTriangleFace[(4,8,7), (5,8,1), (6,5,2), (3,7,6)] + @test faces(Polygon(Point2{Float16}.(ps), [Point2{Float16}.(ps2)])) == GLTriangleFace[(4,8,7), (5,8,1), (6,5,2), (3,7,6)] + end +end \ No newline at end of file From 778299268379bf6142bd48dc6eaa262933601d05 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Feb 2025 14:23:22 +0100 Subject: [PATCH 06/28] treat float precision error, add isapprox for Rects --- src/primitives/rectangles.jl | 6 ++++-- test/geometrytypes.jl | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index d64bc53b..f9582139 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -505,9 +505,11 @@ end # # Equality # -Base.:(==)(b1::Rect, b2::Rect) = minimum(b1) == minimum(b2) && widths(b1) == widths(b2) +Base.:(==)(b1::Rect, b2::Rect) = origin(b1) == origin(b2) && widths(b1) == widths(b2) -Base.isequal(b1::Rect, b2::Rect) = b1 == b2 +function Base.isapprox(r1::Rect, r2::Rect; kwargs...) + return isapprox(origin(r1), origin(r2); kwargs...) && isapprox(widths(r1), widths(r2); kwargs...) +end ## # Rect2 decomposition diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 27bdb1a7..a6675227 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -467,16 +467,16 @@ end # go higher dimensions than that. M = rand(Mat4f) ps = Point2f[M * Point(p..., 1, 1) for p in coordinates(r)] - @test Rect2f(ps) == M * r + @test Rect2f(ps) ≈ M * r M = Mat2f(0.5, -0.3, 0.7, 1.5) ps = Point2f[M * p for p in coordinates(r)] - @test Rect2f(ps) == M * r + @test Rect2f(ps) ≈ M * r r = Rect3f(-1, -2, -3, 2, 4, 1) M = rand(Mat4f) ps = Point3f[M * Point(p..., 1) for p in coordinates(r)] - @test Rect3f(ps) == M * r + @test Rect3f(ps) ≈ M * r end # TODO: this is effectively 0-indexed... should it be? From 73aedef0e641050bdcff67fc8cf6372983dbf850 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Feb 2025 15:33:39 +0100 Subject: [PATCH 07/28] test split_mesh --- src/basic_types.jl | 59 ++++++++++++++++++++++++++++++++++++++++++++++ test/meshes.jl | 8 +++++++ 2 files changed, 67 insertions(+) diff --git a/src/basic_types.jl b/src/basic_types.jl index b353816f..6b60ef68 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -641,6 +641,65 @@ function Base.:(==)(a::Mesh, b::Mesh) (faces(a) == faces(b)) && (a.views == b.views) end +""" + strictly_equal_face_vertices(a::Mesh, b::Mesh) + +Checks whether mesh a and b are equal in terms of vertices used in their faces. +This allows for vertex data and indices to be synchronously permuted. For +example, this will recognize +``` +a = Mesh([a, b, c], [GLTriangleFace(1,2,3)]) +b = Mesh([a, c, b], [GLTriangleFace(1,3,2)]) +``` +as equal, because while the positions and faces have different orders the vertices +in the face are the same: +``` +[a, c, b][[1, 3, 2]] == [a, b, c] == [a, b, c][[1,2,3]] +``` + +This still returns false if the order of faces is permuted, e.g. +`Mesh(ps, [f1, f2]) != Mesh(ps, [f2, f1])`. It also returns false if vertices are +cyclically permuted within a face, i.e. `ps[[1,2,3]] != ps[[2,3,1]]`. +""" +function strictly_equal_face_vertices(a::Mesh, b::Mesh) + # Quick checks + if propertynames(a) != propertynames(b) || length(faces(a)) != length(faces(b)) + return false + end + + N = length(faces(a)) + # for views we want to ignore empty ranges (they don't represent geometry) + # and treat 1:N as no range (as that is used interchangeably) + views1 = filter(view -> length(view) > 0 && (minimum(view) > 1 || maximum(view) < N), a.views) + views2 = filter(view -> length(view) > 0 && (minimum(view) > 1 || maximum(view) < N), b.views) + views1 != views2 && return false + + # TODO: Allow different face orders & cyclic permutation within faces. + # E.g. use hash.(data[face]), cyclically permute min to front, hash result + # and add them to heaps (or sets?) so we can compare them at the end + # That should probably be another function as it's probably a significant + # step up in overhead? + for (attrib1, attrib2) in zip(vertex_attributes(a), vertex_attributes(b)) + if attrib1 isa FaceView + if !(attrib2 isa FaceView) || length(faces(attrib1)) != length(faces(attrib2)) + return false + end + for (f1, f2) in zip(faces(attrib1), faces(attrib2)) + values(attrib1)[f1] == values(attrib2)[f2] || return false + end + else + if attrib2 isa FaceView + return false + end + for (f1, f2) in zip(faces(a), faces(b)) + attrib1[f1] == attrib2[f2] || return false + end + end + end + + return true +end + function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end diff --git a/test/meshes.jl b/test/meshes.jl index 03ca71eb..93ccabd8 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -70,6 +70,9 @@ end @test !allunique([idx for f in faces(dm) for idx in f]) @test !allunique([idx for f in faces(dm.normal) for idx in f]) + split_meshes = split_mesh(dm) + @test all(GeometryBasics.strictly_equal_face_vertices.(split_meshes, direct_meshes)) + indirect_meshes = map(rects) do r m = GeometryBasics.mesh(coordinates(r), faces(r), normal = normals(r), facetype = QuadFace{Int64}) # Also testing merge of meshes with views @@ -81,6 +84,9 @@ end @test im == dm @test GeometryBasics.facetype(im) == QuadFace{Int64} + split_meshes = split_mesh(im) + @test all(GeometryBasics.strictly_equal_face_vertices.(split_meshes, indirect_meshes)) + converted_meshes = map(rects) do r m = GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) GeometryBasics.expand_faceviews(m) @@ -95,6 +101,8 @@ end @test coordinates(cm) isa Vector @test allunique([idx for f in faces(cm) for idx in f]) + split_meshes = split_mesh(cm) + @test all(GeometryBasics.strictly_equal_face_vertices.(split_meshes, converted_meshes)) mixed_meshes = map(direct_meshes, indirect_meshes, converted_meshes) do dm, im, cm rand((dm, im, cm)) # (with FaceView, with mesh.views & FaceView, w/o FaceView) From 744c51b6bcb8840e03e2a3128ab1d610d17e1cee Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Feb 2025 15:47:37 +0100 Subject: [PATCH 08/28] improve meshes.jl coverage --- src/GeometryBasics.jl | 2 +- src/meshes.jl | 15 ++------------- test/meshes.jl | 32 +++++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 815aea3a..89ccd370 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -41,7 +41,7 @@ export AbstractFace, TriangleFace, QuadFace, GLTriangleFace export OffsetInteger, ZeroIndex, OneIndex, GLIndex export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, texturecoordinates, vertex_attributes -export expand_faceviews +export expand_faceviews, split_mesh, remove_duplicates export face_normals export Tessellation, Normal, UV, UVW export AbstractMesh, Mesh, MetaMesh, FaceView diff --git a/src/meshes.jl b/src/meshes.jl index ab5a38e1..6e2c1174 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -390,17 +390,6 @@ function expand_faceviews(mesh::Mesh) end end -function merge_vertex_indices( - faces::AbstractVector{FT}, args... - ) where {N, T, FT <: AbstractFace{N, T}} - if args[end] isa Integer - fs = tuple(faces, args[1:end-1]...) - return merge_vertex_indices(fs, args[end]) - else - return merge_vertex_indices(tuple(faces, args...)) - end -end - function merge_vertex_indices( faces::NTuple{N_Attrib, <: AbstractVector{FT}}, vertex_index_counter::Integer = T(1) @@ -486,13 +475,13 @@ end function map_coordinates(f, mesh::Mesh) - result = copy(mesh) + result = deepcopy(mesh) map_coordinates!(f, result) return result end function map_coordinates(f, mesh::MetaMesh) - result = copy(Mesh(mesh)) + result = deepcopy(Mesh(mesh)) map_coordinates!(f, result) return MetaMesh(result, meta(mesh)) end diff --git a/test/meshes.jl b/test/meshes.jl index 93ccabd8..58911928 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -54,7 +54,18 @@ end @test isempty(m2.views) end -@testset "complex merge" begin +@testset "Duplicate face removal" begin + fs = GLTriangleFace[(1,2,3), (2,3,4), (3,4,5), (1,2,3), (1,4,5)] + fs = [fs; fs] + new_fs = remove_duplicates(fs) + @test all(f -> f in GLTriangleFace[(1,2,3), (2,3,4), (3,4,5), (1,4,5)], new_fs) + + fs = rand(QuadFace{Int32}, 4) + new_fs = remove_duplicates([fs; fs]) + @test all(in(fs), new_fs) +end + +@testset "complex merge + split" begin rects = [Rect3f(Point3f(x, y, z), Vec3f(0.5)) for x in -1:1 for y in -1:1 for z in -1:1] direct_meshes = map(rects) do r GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) @@ -264,6 +275,8 @@ end @test faces(m) isa Vector{GLTriangleFace} @test length(faces(m)) == 12 @test GeometryBasics.facetype(m) == GLTriangleFace + + @test normal_mesh(coordinates(m), faces(m)) == m end @testset "normal_uv_mesh()" begin @@ -372,4 +385,21 @@ end @test length(faces(m2)) == 12 end +end + +@testset "map_coordinates" begin + m = GeometryBasics.mesh(Rect3f(0,0,0,1,1,1)) + m2 = GeometryBasics.map_coordinates(p -> 2 * p, m) + @test m !== m2 + @test 2 * coordinates(m) == coordinates(m2) + + m3 = GeometryBasics.map_coordinates!(p -> 0.5 * p, m2) + @test m3 === m2 + @test coordinates(m) == coordinates(m3) + + m = MetaMesh(GeometryBasics.mesh(Rect3f(0,0,0,1,1,1)), meta = "test") + m2 = GeometryBasics.map_coordinates(p -> 2 * p, m) + @test m !== m2 + @test 2 * coordinates(m) == coordinates(m2) + @test GeometryBasics.meta(m) == GeometryBasics.meta(m2) end \ No newline at end of file From 89b992125f32f06d2c055fb7477c25bac50357fd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Feb 2025 18:59:11 +0100 Subject: [PATCH 09/28] refactor boundingboxes, add tests --- src/boundingboxes.jl | 62 +++++++++++++++++++++++------------- src/primitives/rectangles.jl | 7 ---- test/geometrytypes.jl | 16 ++++++++++ 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index 212886dc..0a4831f8 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -1,18 +1,40 @@ -function Rect(geometry::AbstractArray{<:Point{N,T}}) where {N,T} - return Rect{N,T}(geometry) +# Boundingbox-like Rect constructors + +Rect(p::AbstractGeometry{N, T}) where {N, T} = Rect{N, T}(p) +RectT{T}(p::AbstractGeometry{N}) where {N, T} = Rect{N, T}(p) +Rect{N}(p::AbstractGeometry{_N, T}) where {N, _N, T} = Rect{N, T}(p) + +Rect(p::AbstractArray{<: VecTypes{N, T}}) where {N, T} = Rect{N, T}(p) +RectT{T}(p::AbstractArray{<: VecTypes{N}}) where {N, T} = Rect{N, T}(p) +Rect{N}(p::AbstractArray{<: VecTypes{_N, T}}) where {N, _N, T} = Rect{N, T}(p) + +# compat: bounding boxes also defined Rect{T} constructors +# This is not really compatible with Rect{N}... +# How do you even deprecate this? +# @deprecate Rect{T}(x::AbstractGeometry) where {T <: Number} RectT{T}(x) where {T} +# @deprecate Rect{T}(x::AbstractArray) where {T <: Number} RectT{T}(x) where {T} + +# Implementations +# Specialize fully typed Rect constructors +Rect{N, T}(p::AbstractGeometry) where {N, T} = Rect{N, T}(coordinates(p)) + +function bbox_dim_check(trg, src::Integer) + @assert trg isa Integer "Rect{$trg, $T1} is invalid. This may have happened due to calling Rect{$N1}(obj) to get a bounding box." + if trg < src + throw(ArgumentError("Cannot construct a $trg dimensional bounding box from $src dimensional Points. ($trg must be ≥ $src)")) + end end """ - Rect(points::AbstractArray{<: Point}) + Rect(points::AbstractArray{<: VecTypes}) Construct a bounding box containing all the given points. """ -function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} - N2, T2 = length(PT), eltype(PT) - @assert N1 >= N2 +function Rect{N1, T1}(points::AbstractArray{<: VecTypes{N2, T2}}) where {N1, T1, N2, T2} + bbox_dim_check(N1, N2) vmin = Point{N2,T2}(typemax(T2)) vmax = Point{N2,T2}(typemin(T2)) - for p in geometry + for p in points vmin, vmax = _minmax(p, vmin, vmax) end o = vmin @@ -25,29 +47,25 @@ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} end end + """ Rect(primitive::GeometryPrimitive) Construct a bounding box for the given primitive. """ -function Rect(primitive::GeometryPrimitive{N,T}) where {N,T} - return Rect{N,T}(primitive) -end - -function Rect{T}(primitive::GeometryPrimitive{N,T}) where {N,T} - return Rect{N,T}(primitive) -end - -function Rect{T}(a::Pyramid) where {T} - w, h = a.width / T(2), a.length +function Rect{N, T}(a::Pyramid) where {N, T} + bbox_dim_check(N, 3) + w, h = a.width, a.length m = Vec{3,T}(a.middle) - return Rect{T}(m .- Vec{3,T}(w, w, 0), m .+ Vec{3,T}(w, w, h)) + return Rect{N, T}(m .- Vec{3,T}(w / T(2), w / T(2), 0), Vec{3,T}(w, w, h)) end -function Rect{T}(a::Sphere) where {T} +function Rect{N, T}(a::HyperSphere) where {N, T} mini, maxi = extrema(a) - return Rect{T}(mini, maxi .- mini) + return Rect{N, T}(mini, maxi .- mini) end -Rect{T}(a) where {T} = Rect{T}(coordinates(a)) -Rect{N,T}(a) where {N,T} = Rect{N,T}(coordinates(a)) +# TODO: exact implementation that doesn't rely on coordinates +# function Rect{N, T}(a::Cylinder) where {N, T} +# return Rect{N, T}(...) +# end diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index f9582139..e44dc1ec 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -118,13 +118,6 @@ Rect{3, T}(o::VecTypes{2}, w::VecTypes{3}) where {T} = Rect{3, T}(Vec(o..., 0), Rect{3, T}(o::VecTypes{3}, w::VecTypes{2}) where {T} = Rect{3, T}(o, Vec(w..., 0)) Rect{3, T}(o::VecTypes{2}, w::VecTypes{2}) where {T} = Rect{3, T}(Vec(o..., 0), Vec(w..., 0)) -# Boundingbox-like constructors - -# Rect(r::GeometryPrimitive{N, T}) where {N, T} = Rect{N, T}(minimum(r), widths(r)) # in boundingboxes.jl -RectT{T}(r::GeometryPrimitive{N}) where {N, T} = Rect{N, T}(minimum(r), widths(r)) -Rect{N}(r::GeometryPrimitive{_N, T}) where {N, _N, T} = Rect{N, T}(minimum(r), widths(r)) -Rect{N, T}(r::GeometryPrimitive) where {N, T} = Rect{N, T}(minimum(r), widths(r)) - # centered Rects centered(R::Type{Rect{N,T}}) where {N,T} = R(Vec{N,T}(-0.5), Vec{N,T}(1)) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index a6675227..74a465f3 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -165,6 +165,22 @@ end end # TODO: test/check for Rect(::GeometryPrimitive) for all primitives + @testset "Boundingbox-like" begin + for constructor in [Rect, Rect{2}, Rect{2, Float32}, Rect3f] + @test constructor(Circle(Point2f(0), 1f0)) == constructor(Point2f(-1, -1), Vec2f(2, 2)) + @test constructor(Rect2f(0, 0, 1, 1)) == constructor(Point2f( 0, 0), Vec2f(1, 1)) + m = GeometryBasics.mesh(Tessellation(Circle(Point2f(0), 1f0), 5)) + @test constructor(m) ≈ constructor(Point2f(-1, -1), Vec2f(2, 2)) + end + for constructor in [Rect, Rect{3}, Rect{3, Float32}] + @test constructor(Sphere(Point3f(0), 1f0)) == Rect3f(-1, -1, -1, 2, 2, 2) + @test constructor(Rect3f(0, 0, 0, 1, 1, 1)) == Rect3f(0, 0, 0, 1, 1, 1) + @test constructor(Cylinder(Point3f(0, 0, -1), Point3f(0,0,1), 1f0)) ≈ Rect3f(-1, -1, -1, 2, 2, 2) atol = 0.05 + @test constructor(Pyramid(Point3f(0, 0, -1), 2f0, 2f0)) == Rect3f(-1, -1, -1, 2, 2, 2) + m = GeometryBasics.mesh(Tessellation(Sphere(Point3f(0), 1f0), 5)) + @test constructor(m) ≈ Rect3f(-1, -1, -1, 2, 2, 2) + end + end end # TODO: origin, minimum, maximum, width, height, widths, area, volume with empty constructed Rects From ca807f2b5326e081e5f1b594e1fe4e3294bcb67f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 14:16:03 +0100 Subject: [PATCH 10/28] test and clean up line intersection code --- src/lines.jl | 53 ++++++++++++++++++++++++------------------------ test/runtests.jl | 24 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/lines.jl b/src/lines.jl index 9ead98e4..ff9fd295 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -7,7 +7,7 @@ Returns `(intersection_found::Bool, intersection_point::Point)` """ # 2D Line-segment intersection algorithm by Paul Bourke and many others. # http://paulbourke.net/geometry/pointlineplane/ -function intersects(a::Line{2,T1}, b::Line{2,T2}) where {T1,T2} +function intersects(a::Line{2,T1}, b::Line{2,T2}; eps = 0) where {T1,T2} T = promote_type(T1, T2) p0 = zero(Point2{T}) @@ -34,7 +34,7 @@ function intersects(a::Line{2,T1}, b::Line{2,T2}) where {T1,T2} # Values between [0, 1] mean the intersection point of the lines rests on # both of the line segments. - if 0 <= unknown_a <= 1 && 0 <= unknown_b <= 1 + if eps <= unknown_a <= 1-eps && eps <= unknown_b <= 1-eps # Substituting an unknown back lets us find the intersection point. x = x1 + (unknown_a * (x2 - x1)) y = y1 + (unknown_a * (y2 - y1)) @@ -62,27 +62,29 @@ end """ self_intersections(points::AbstractVector{<:Point}) -Finds all self intersections of polygon `points` +Finds all self intersections of in a continuous line described by `points`. + +Note that if two points are the same, they will generate a self intersection +unless they are the first and last point or part of consecutive segments. """ -function self_intersections(points::AbstractVector{<:Point}) +function self_intersections(points::AbstractVector{<:VecTypes{D, T}}) where {D, T} sections = similar(points, 0) - intersections = Int[] - - wraparound(i) = mod1(i, length(points) - 1) - - for (i, (a, b)) in enumerate(consecutive_pairs(points)) - for (j, (a2, b2)) in enumerate(consecutive_pairs(points)) - is1, is2 = wraparound(i + 1), wraparound(i - 1) - if i != j && - is1 != j && - is2 != j && - !(i in intersections) && - !(j in intersections) - intersected, p = intersects(Line(a, b), Line(a2, b2)) - if intersected - push!(intersections, i, j) - push!(sections, p) - end + intersections = Tuple{Int, Int}[] + + N = length(points) + + for i in 1:N-3 + a = points[i]; b = points[i+1] + # i+1 == j describes consecutive segments which are always "intersecting" + # at point i+1/j. Skip those (start at i+2) + # Special case: We assume points[1] == points[end] so 1 -> 2 and N-1 -> N + # always "intersect" at 1/N. Skip this too (end at N-2 in this case) + for j in i+2 : N-1 - (i == 1) + a2 = points[j]; b2 = points[j+1] + intersected, p = intersects(Line(a, b), Line(a2, b2)) + if intersected + push!(intersections, (i, j)) + push!(sections, p) end end end @@ -95,15 +97,14 @@ end Splits polygon `points` into it's self intersecting parts. Only 1 intersection is handled right now. """ -function split_intersections(points::AbstractVector{<:Point}) +function split_intersections(points::AbstractVector{<:VecTypes{N, T}}) where {N, T} intersections, sections = self_intersections(points) return if isempty(intersections) return [points] - elseif length(intersections) == 2 && length(sections) == 1 - a, b = intersections + elseif length(intersections) == 1 && length(sections) == 1 + a, b = intersections[1] p = sections[1] - a, b = min(a, b), max(a, b) - poly1 = simple_concat(points, (a + 1):(b - 1), p) + poly1 = simple_concat(points, (a + 1):b, p) poly2 = simple_concat(points, (b + 1):(length(points) + a), p) return [poly1, poly2] else diff --git a/test/runtests.jl b/test/runtests.jl index ee297109..63a521bd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -258,6 +258,8 @@ end found, point = intersects(d, e) @test found && point ≈ Point(0.0, 4.0) @test intersects(a, f) === (false, Point(0.0, 0.0)) + @test intersects(a, d) === (true, Point(0.0, 0.0)) + @test intersects(a, d, eps = 1e-6) === (false, Point(0.0, 0.0)) # issue #168 # If these tests fail then you can increase the tolerance on the checks so @@ -273,6 +275,28 @@ end @test b @test loc ≈ Point(5760.0, 127.27922061357884) end + + ps = [Point2f(1), Point2f(2)] + @test GeometryBasics.simple_concat(ps, 2:2, Point2f(3)) == [Point2f(2), Point2f(3)] + ps = [Point2f(i) for i in 1:4] + @test collect(GeometryBasics.consecutive_pairs(ps)) == collect(zip(ps[1:end-1], ps[2:end])) + + ps = Point2f[(0,0), (1,0), (0,1), (1,2), (0,2), (1,1), (0,0)] + idxs, ips = self_intersections(ps) + @test idxs == [(2, 6), (3, 5)] + @test ips == [Point2f(0.5), Point2f(0.5, 1.5)] + + ps = [Point2f(cos(x), sin(x)) for x in 0:4pi/5:4pi+0.1] + idxs, ips = self_intersections(ps) + @test idxs == [(1, 3), (1, 4), (2, 4), (2, 5), (3, 5)] + @test all(ips .≈ Point2f[(0.30901694, 0.2245140), (-0.118034005, 0.36327127), (-0.38196602, 0), (-0.118033946, -0.3632713), (0.309017, -0.22451389)]) + + @test_throws ErrorException split_intersections(ps) + ps = Point2f[(0,0), (1,0), (0,1), (1,1), (0, 0)] + idxs, ips = self_intersections(ps) + sps = split_intersections(ps) + @test sps[1] == [ps[3], ps[4], ips[1]] + @test sps[2] == [ps[5], ps[1], ps[2], ips[1]] end From fb756da5774fa03341442bc1c9f97708af19692d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 14:34:39 +0100 Subject: [PATCH 11/28] fix docs --- src/primitives/rectangles.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index e44dc1ec..f22b5afb 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -105,12 +105,12 @@ RectT{ T}(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) where {T} Rect{3 }(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) = Rect{3 }(Vec(x, y, z), w) Rect{3, T}(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) where {T} = Rect{3, T}(Vec(x, y, z), w) -# copy constructors +# copy constructors (allow explicit truncation) - Rect(r::Rect{N, T}) where {N, T} = Rect{N, T}(origin(r), widths(r)) -RectT{ T}(r::Rect{N}) where {N, T} = Rect{N, T}(origin(r), widths(r)) -Rect{N }(r::Rect{N, T}) where {N, T} = Rect{N, T}(origin(r), widths(r)) -Rect{N, T}(r::Rect{N}) where {N, T} = Rect{N, T}(origin(r), widths(r)) + Rect(r::Rect{N, T}) where {N, T} = Rect{N, T}(origin(r), widths(r)) +RectT{ T}(r::Rect{N}) where {N, T} = Rect{N, T}(origin(r), widths(r)) +Rect{N }(r::Rect{_N, T}) where {N, _N, T} = Rect{N, T}(Vec{N, T}(origin(r)), Vec{N, T}(widths(r))) +Rect{N, T}(r::Rect) where {N, T} = Rect{N, T}(Vec{N, T}(origin(r)), Vec{N, T}(widths(r))) # dimensional promotion From 95ea7153d3f585b1c23178d8ac037779f66b9e4a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 15:08:17 +0100 Subject: [PATCH 12/28] improve Sphere test coverage --- src/primitives/spheres.jl | 9 ++++----- test/geometrytypes.jl | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index 76602326..c29c6d4e 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -36,10 +36,9 @@ function Base.in(x::Point, c::HyperSphere) return norm(origin(c) - x) ≤ radius(c) end -centered(S::Type{HyperSphere{N,T}}) where {N,T} = S(Vec{N,T}(0), T(0.5)) -function centered(::Type{T}) where {T<:HyperSphere} - return centered(HyperSphere{ndims_or(T, 3),eltype_or(T, Float32)}) -end +centered(S::Type{HyperSphere{N,T}}) where {N,T} = S(Point{N,T}(0), T(0.5)) +centered(S::Type{<: HyperSphere{N}}) where {N} = S(Point{N,Float32}(0), 0.5f0) +centered(S::Type{<: HyperSphere}) = S(Point3f(0), 0.5f0) function coordinates(s::Circle, nvertices=64) r = radius(s); o = origin(s) @@ -52,7 +51,7 @@ function texturecoordinates(::Circle, nvertices=64) return coordinates(Circle(Point2f(0.5), 0.5f0), nvertices) end -# TODO: Consider generating meshes for circles with a point in the center so +# TODO: Consider generating meshes for circles with a point in the center so # that the triangles are more regular # function faces(::Circle, nvertices=64) # return [GLTriangleFace(nvertices+1, i, mod1(i+1, nvertices)) for i in 1:nvertices] diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 74a465f3..1431e352 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -317,12 +317,39 @@ end face_target = TriangleFace{Int}[[1, 2, 5], [1, 5, 4], [2, 3, 6], [2, 6, 5], [4, 5, 8], [4, 8, 7], [5, 6, 9], [5, 9, 8]] @test f == face_target + + uv = decompose_uv(Tesselation(sphere, 3)) + uv_target = Vec{2, Float32}[[0.0, 1.0], [0.0, 0.5], [0.0, 0.0], [0.5, 1.0], [0.5, 0.5], + [0.5, 0.0], [1.0, 1.0], [1.0, 0.5], [1.0, 0.0]] + @test uv == uv_target + + @test minimum(sphere) == Point3f(-1) + @test maximum(sphere) == Point3f(1) + @test origin(sphere) == Point3f(0) + @test widths(sphere) == Vec3f(2) + @test radius(sphere) == 1f0 + @test !(Point3f(1) in sphere) + @test Point3f(0.5) in sphere + @test centered(HyperSphere) == Sphere(Point3f(0), 0.5f0) + @test centered(Sphere) == Sphere(Point3f(0), 0.5f0) + @test centered(Sphere{Float64}) == Sphere(Point3(0.0), 0.5) + circle = Circle(Point2f(0), 1.0f0) points = decompose(Point2f, Tessellation(circle, 20)) @test length(points) == 20 tess_circle = Tessellation(circle, 32) mesh = triangle_mesh(tess_circle) @test decompose(Point2f, mesh) ≈ decompose(Point2f, tess_circle) + + @test minimum(circle) == Point2f(-1) + @test maximum(circle) == Point2f(1) + @test origin(circle) == Point2f(0) + @test widths(circle) == Vec2f(2) + @test radius(circle) == 1f0 + @test !(Point2f(-1) in circle) + @test Point2f(-0.5) in circle + @test centered(Circle) == Circle(Point2f(0), 0.5f0) + @test centered(Circle{Float64}) == Circle(Point2(0.0), 0.5) end @testset "Rectangles" begin From 23b2346bfa2af6f474b98c886a70f01d6818d254 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 15:08:40 +0100 Subject: [PATCH 13/28] fix Rect dim truncation --- src/primitives/rectangles.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index f22b5afb..0aa73f36 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -109,8 +109,8 @@ Rect{3, T}(x::Number, y::Number, z::Number, w::VecTypes{3, <:Number}) where {T} Rect(r::Rect{N, T}) where {N, T} = Rect{N, T}(origin(r), widths(r)) RectT{ T}(r::Rect{N}) where {N, T} = Rect{N, T}(origin(r), widths(r)) -Rect{N }(r::Rect{_N, T}) where {N, _N, T} = Rect{N, T}(Vec{N, T}(origin(r)), Vec{N, T}(widths(r))) -Rect{N, T}(r::Rect) where {N, T} = Rect{N, T}(Vec{N, T}(origin(r)), Vec{N, T}(widths(r))) +Rect{N }(r::Rect{N2, T}) where {N, N2, T} = Rect{N, T}(Vec{min(N, N2), T}(origin(r)), Vec{min(N, N2), T}(widths(r))) +Rect{N, T}(r::Rect{N2}) where {N, N2, T} = Rect{N, T}(Vec{min(N, N2), T}(origin(r)), Vec{min(N, N2), T}(widths(r))) # dimensional promotion From b9515352c8968df770a2bf453e3e3f4b3548ac53 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 15:45:29 +0100 Subject: [PATCH 14/28] a few more tests for OffsetIntegers & FixedArrays --- src/fixed_arrays.jl | 13 +++++++------ test/fixed_arrays.jl | 26 +++++++++++++++++++++++--- test/runtests.jl | 32 +++++++++++++++++--------------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 341b8ec2..9e32e77c 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -57,7 +57,8 @@ macro fixed_vector(name_parent) function $(name){S}(x::T) where {S,T <: Tuple} return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x) end - $(name){S,T}(x::StaticVector) where {S,T} = $(name){S,T}(Tuple(x)) + $(name){S,T}(x::StaticVector{S}) where {S,T} = $(name){S,T}(Tuple(x)) + $(name){S,T}(x::StaticVector) where {S,T} = $(name){S,T}(ntuple(i -> x[i], S)) @generated function (::Type{$(name){S,T}})(x::$(name)) where {S,T} idx = [:(x[$i]) for i in 1:S] @@ -139,7 +140,7 @@ const VecTypes{N,T} = Union{StaticVector{N,T},NTuple{N,T}} const Vecf{N} = Vec{N,Float32} const PointT{T} = Point{N,T} where N const Pointf{N} = Point{N,Float32} - + Base.isnan(p::Union{AbstractPoint,Vec}) = any(isnan, p) Base.isinf(p::Union{AbstractPoint,Vec}) = any(isinf, p) Base.isfinite(p::Union{AbstractPoint,Vec}) = all(isfinite, p) @@ -177,9 +178,9 @@ export Vecf, Pointf Vec{N, T}(args...) Vec{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector}) -Constructs a Vec of length `N` from the given arguments. +Constructs a Vec of length `N` from the given arguments. -Note that Point and Vec don't follow strict mathematical definitions. Instead +Note that Point and Vec don't follow strict mathematical definitions. Instead we allow them to be used interchangeably. ## Aliases @@ -197,9 +198,9 @@ Vec Point{N, T}(args...) Point{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector}) -Constructs a Point of length `N` from the given arguments. +Constructs a Point of length `N` from the given arguments. -Note that Point and Vec don't follow strict mathematical definitions. Instead +Note that Point and Vec don't follow strict mathematical definitions. Instead we allow them to be used interchangeably. ## Aliases diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index 256d5f31..694811b1 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -1,6 +1,22 @@ using Test -@testset "conversion" begin +@testset "Construction and Conversion" begin + for VT in [Point, Vec] + for T in [Int32, Float32, Float64, UInt16, BigFloat] + p = VT{3, T}(1,2,3) + @test p[1] == T(1) + @test p[2] == T(2) + @test p[3] == T(3) + end + + for VT2 in [Point, Vec] + @test VT{2, Float32}(VT2{3, Float32}(1,2,3)) == VT{2, Float32}(1,2) + @test VT{2, Float32}(VT2{3, Float64}(1,2,3)) == VT{2, Float32}(1,2) + end + @test VT{2, Float32}(Float32[1,2,3]) == VT{2, Float32}(1,2) + @test VT{2, Float32}([1,2,3]) == VT{2, Float32}(1,2) + end + @test convert(Point, (2, 3)) === Point(2, 3) @test convert(Point, (2.0, 3)) === Point(2.0, 3.0) end @@ -17,6 +33,10 @@ end T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) @test T(2, 2, 4) == T1(1,2,3) .+ T2(1, 0, 1) @test T(foo.((1,2,3), (1, 0, 1))) == foo.(T1(1,2,3), T2(1, 0, 1)) + @test T1(1,2,3) .* T2(1,2,3) == T(1, 4, 9) + # TODO: repair this: + # @test foo.(T1(1,2,3), [T2(1,1,1), T2(2,2,2)]) == [T(1,2,3), T(2,4,6)] + # @test foo.([T2(1,1,1), T2(2,2,2)], T1(1,2,3)) == [T(0, -3, -8), T(3, 0, -5)] end end @@ -51,7 +71,7 @@ end for T2 in (Vec, Point, tuple) T1 == tuple && T2 == tuple && continue T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) - @test T(foo2.((1,2,3), (1, 0, 1), (3, 2, 1), (2,2,0))) == + @test T(foo2.((1,2,3), (1, 0, 1), (3, 2, 1), (2,2,0))) == foo2.(T1(1,2,3), T2(1, 0, 1), T2(3, 2, 1), T2(2,2,0)) end end @@ -80,7 +100,7 @@ end @testset "Mat" begin M3 = Mat3(1,2,3, 4,5,6, 7,8,9) @test M3 isa Mat{3,3,Int,9} - + @testset "indexing" begin for i in 1:9 @test getindex(M3, i) == i diff --git a/test/runtests.jl b/test/runtests.jl index 63a521bd..cabfb82a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -312,21 +312,23 @@ end x = OffsetInteger{0}(1) @test typeof(x) == OffsetInteger{0,Int} - x1 = OffsetInteger{0}(2) - x2 = 1 - @test Base.to_index(x1) == 2 - @test -(x1) == OffsetInteger{0,Int}(-2) - @test abs(x1) == OffsetInteger{0,Int}(2) - @test +(x, x1) == OffsetInteger{0,Int}(3) - @test *(x, x1) == OffsetInteger{0,Int}(2) - @test -(x, x1) == OffsetInteger{0,Int}(-1) - #test for / - @test div(x, x1) == OffsetInteger{0,Int}(0) - @test !==(x, x1) - @test !>=(x, x1) - @test <=(x, x1) - @test !>(x, x1) - @test <(x, x1) + for x1 in [OffsetInteger{0}(2), 2, 0x02] + @test Base.to_index(x1) == 2 + @test -(x1) == OffsetInteger{0,Int}(-2) + @test abs(x1) == OffsetInteger{0,Int}(2) + for x in [OffsetInteger{0}(1), 1, 0x01] + @test +(x, x1) == OffsetInteger{0,Int}(3) + @test *(x, x1) == OffsetInteger{0,Int}(2) + @test -(x, x1) == OffsetInteger{0,Int}(-1) + #test for / + @test div(x, x1) == OffsetInteger{0,Int}(0) + @test !==(x, x1) + @test !>=(x, x1) + @test <=(x, x1) + @test !>(x, x1) + @test <(x, x1) + end + end end @testset "Tests from GeometryTypes" begin From 34795e33bb3f60454911e2e2af0016253f66499f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 17:16:19 +0100 Subject: [PATCH 15/28] add basic docs for bounding boxes --- docs/make.jl | 3 ++- docs/src/boundingboxes.md | 31 +++++++++++++++++++++++++++++++ src/boundingboxes.jl | 6 +++--- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 docs/src/boundingboxes.md diff --git a/docs/make.jl b/docs/make.jl index 9d41c5e7..7b7f8f97 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,8 +13,9 @@ makedocs(format=Documenter.HTML(prettyurls=get(ENV, "CI", "false") == "true"), "polygons.md", "meshes.md", "decomposition.md", + "boundingboxes.md", "static_array_types.md", - "api.md" + "api.md", ], modules=[GeometryBasics]) diff --git a/docs/src/boundingboxes.md b/docs/src/boundingboxes.md new file mode 100644 index 00000000..2616b03d --- /dev/null +++ b/docs/src/boundingboxes.md @@ -0,0 +1,31 @@ +# Bounding Boxes + +You can generate an axis aligned bounding box for any `AbstractGeometry` by calling `Rect(geom)`. +Depending on the object this will either rely on `coordinates(geom)` or a specialized method. +You can also create a bounding box of set dimension or type by adding the related parameters. + + +```@repl +using GeometryBasics + +s = Circle(Point2f(0), 1f0) +Rect(s) # specialized, exact bounding box +Rect3(s) +Rect3d(s) +RectT{Float64}(s) +Rect(GeometryBasics.mesh(s)) # using generated coordinates in mesh +``` + +## Extending + +If you want to add a specialized bounding box method you should implement `Rect{N, T}(geom) = ...`. +All other methods funnel into that one, defaulting to the same `N, T` that the given `AbstractGeometry{N, T}` has. +GeometryBasics allows the user given dimension `N` to be smaller or equal to that of the geometry. +This is checked with `GeometryBasics.bbox_dim_check(user_dim, geom_dim)` which you may reuse. + +```@example +function Rect{N, T}(a::HyperSphere{N2}) where {N, N2, T} + bbox_dim_check(N, N2) + return Rect{N, T}(minimum(a), widths(a)) +end +``` \ No newline at end of file diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index 0a4831f8..f26311e8 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -60,9 +60,9 @@ function Rect{N, T}(a::Pyramid) where {N, T} return Rect{N, T}(m .- Vec{3,T}(w / T(2), w / T(2), 0), Vec{3,T}(w, w, h)) end -function Rect{N, T}(a::HyperSphere) where {N, T} - mini, maxi = extrema(a) - return Rect{N, T}(mini, maxi .- mini) +function Rect{N, T}(a::HyperSphere{N2}) where {N, N2, T} + bbox_dim_check(N, N2) + return Rect{N, T}(minimum(a), widths(a)) end # TODO: exact implementation that doesn't rely on coordinates From bae73b38b9b07e07581421c1b0e530bd4802bebc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 18:21:11 +0100 Subject: [PATCH 16/28] add deprecation warning for developers --- src/boundingboxes.jl | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index f26311e8..da469bdb 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -2,21 +2,22 @@ Rect(p::AbstractGeometry{N, T}) where {N, T} = Rect{N, T}(p) RectT{T}(p::AbstractGeometry{N}) where {N, T} = Rect{N, T}(p) -Rect{N}(p::AbstractGeometry{_N, T}) where {N, _N, T} = Rect{N, T}(p) +Rect{N}(p::AbstractGeometry{_N, T}) where {N <: Val, _N, T} = Rect{N, T}(p) Rect(p::AbstractArray{<: VecTypes{N, T}}) where {N, T} = Rect{N, T}(p) RectT{T}(p::AbstractArray{<: VecTypes{N}}) where {N, T} = Rect{N, T}(p) Rect{N}(p::AbstractArray{<: VecTypes{_N, T}}) where {N, _N, T} = Rect{N, T}(p) -# compat: bounding boxes also defined Rect{T} constructors -# This is not really compatible with Rect{N}... -# How do you even deprecate this? -# @deprecate Rect{T}(x::AbstractGeometry) where {T <: Number} RectT{T}(x) where {T} -# @deprecate Rect{T}(x::AbstractArray) where {T <: Number} RectT{T}(x) where {T} - # Implementations # Specialize fully typed Rect constructors -Rect{N, T}(p::AbstractGeometry) where {N, T} = Rect{N, T}(coordinates(p)) +function Rect{N, T}(geom::AbstractGeometry) where {N, T <: Number} + if applicable(Rect{T}, geom) + @warn "`Rect{T}(geom)` is deprecated as the final boundingbox method. Define `Rect{N, T}(geom)` instead." + return Rect{T}(geom) + else + return Rect{N, T}(coordinates(geom)) + end +end function bbox_dim_check(trg, src::Integer) @assert trg isa Integer "Rect{$trg, $T1} is invalid. This may have happened due to calling Rect{$N1}(obj) to get a bounding box." @@ -68,4 +69,4 @@ end # TODO: exact implementation that doesn't rely on coordinates # function Rect{N, T}(a::Cylinder) where {N, T} # return Rect{N, T}(...) -# end +# end \ No newline at end of file From dc17a6370cd92ec2e31f40892a6c6c272545dd5a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 18:37:26 +0100 Subject: [PATCH 17/28] nvm, doesn't work --- src/boundingboxes.jl | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index da469bdb..ccf5695a 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -2,7 +2,7 @@ Rect(p::AbstractGeometry{N, T}) where {N, T} = Rect{N, T}(p) RectT{T}(p::AbstractGeometry{N}) where {N, T} = Rect{N, T}(p) -Rect{N}(p::AbstractGeometry{_N, T}) where {N <: Val, _N, T} = Rect{N, T}(p) +Rect{N}(p::AbstractGeometry{_N, T}) where {N, _N, T} = Rect{N, T}(p) Rect(p::AbstractArray{<: VecTypes{N, T}}) where {N, T} = Rect{N, T}(p) RectT{T}(p::AbstractArray{<: VecTypes{N}}) where {N, T} = Rect{N, T}(p) @@ -10,14 +10,7 @@ Rect{N}(p::AbstractArray{<: VecTypes{_N, T}}) where {N, _N, T} = Rect{N, T}(p) # Implementations # Specialize fully typed Rect constructors -function Rect{N, T}(geom::AbstractGeometry) where {N, T <: Number} - if applicable(Rect{T}, geom) - @warn "`Rect{T}(geom)` is deprecated as the final boundingbox method. Define `Rect{N, T}(geom)` instead." - return Rect{T}(geom) - else - return Rect{N, T}(coordinates(geom)) - end -end +Rect{N, T}(p::AbstractGeometry) where {N, T} = Rect{N, T}(coordinates(p)) function bbox_dim_check(trg, src::Integer) @assert trg isa Integer "Rect{$trg, $T1} is invalid. This may have happened due to calling Rect{$N1}(obj) to get a bounding box." @@ -69,4 +62,4 @@ end # TODO: exact implementation that doesn't rely on coordinates # function Rect{N, T}(a::Cylinder) where {N, T} # return Rect{N, T}(...) -# end \ No newline at end of file +# end From e33ff0cc52bce2c47e6483b01cff76eb85ebb464 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 5 Feb 2025 18:50:12 +0100 Subject: [PATCH 18/28] fix docs --- docs/src/boundingboxes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/boundingboxes.md b/docs/src/boundingboxes.md index 2616b03d..1444e9ad 100644 --- a/docs/src/boundingboxes.md +++ b/docs/src/boundingboxes.md @@ -23,9 +23,9 @@ All other methods funnel into that one, defaulting to the same `N, T` that the g GeometryBasics allows the user given dimension `N` to be smaller or equal to that of the geometry. This is checked with `GeometryBasics.bbox_dim_check(user_dim, geom_dim)` which you may reuse. -```@example +```julia function Rect{N, T}(a::HyperSphere{N2}) where {N, N2, T} - bbox_dim_check(N, N2) + GeometryBasics.bbox_dim_check(N, N2) return Rect{N, T}(minimum(a), widths(a)) end ``` \ No newline at end of file From 358d4d1964a573e7d7dc95c61c722536c1dc9ebc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 6 Feb 2025 18:08:28 +0100 Subject: [PATCH 19/28] explicitly test Rect getters/utility functions --- test/geometrytypes.jl | 42 ++++++++++++++++++++++++++++++++++-------- test/polygons.jl | 2 +- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 1431e352..708b26ed 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -164,7 +164,6 @@ end end end - # TODO: test/check for Rect(::GeometryPrimitive) for all primitives @testset "Boundingbox-like" begin for constructor in [Rect, Rect{2}, Rect{2, Float32}, Rect3f] @test constructor(Circle(Point2f(0), 1f0)) == constructor(Point2f(-1, -1), Vec2f(2, 2)) @@ -183,19 +182,46 @@ end end end - # TODO: origin, minimum, maximum, width, height, widths, area, volume with empty constructed Rects - - a = Rect(Vec(0, 0), Vec(1, 1)) - pt_expa = Point{2,Int}[(0, 0), (1, 0), (1, 1), (0, 1)] + # TODO: These don't really make sense... + r = Rect2f() + @test origin(r) == Vec(Inf, Inf) + @test minimum(r) == Vec(Inf, Inf) + @test isnan(maximum(r)) + @test width(r) == -Inf + @test height(r) == -Inf + @test widths(r) == Vec(-Inf, -Inf) + @test area(r) == Inf + @test volume(r) == Inf + + a = Rect(Vec(0, 1), Vec(2, 3)) + pt_expa = Point{2,Int}[(0, 1), (2, 1), (2, 4), (0, 4)] @test decompose(Point{2,Int}, a) == pt_expa mesh = normal_mesh(a) @test decompose(Point2f, mesh) == pt_expa - b = Rect(Vec(1, 1, 1), Vec(1, 1, 1)) - pt_expb = Point{3,Int64}[[1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 2, 2], - [2, 1, 1], [2, 1, 2], [2, 2, 1], [2, 2, 2]] + @test origin(a) == Vec(0,1) + @test minimum(a) == Vec(0,1) + @test maximum(a) == Vec(2,4) + @test width(a) == 2 + @test height(a) == 3 + @test widths(a) == Vec(2,3) + @test area(a) == 2*3 + @test volume(a) == 2*3 + + b = Rect(Vec(1, 2, 3), Vec(4, 5, 6)) + pt_expb = Point{3, Int64}[[1, 2, 3], [1, 2, 9], [1, 7, 3], [1, 7, 9], + [5, 2, 3], [5, 2, 9], [5, 7, 3], [5, 7, 9]] @test decompose(Point{3,Int}, b) == pt_expb + @test origin(b) == Vec(1,2,3) + @test minimum(b) == Vec(1,2,3) + @test maximum(b) == Vec(5,7,9) + @test width(b) == 4 + @test height(b) == 5 + @test widths(b) == Vec(4,5,6) + @test_throws MethodError area(b) + @test volume(b) == 4*5*6 + mesh = normal_mesh(b) @test faces(mesh) == GLTriangleFace[ (1, 2, 4), (1, 4, 3), (7, 8, 6), (7, 6, 5), (5, 6, 2), (5, 2, 1), diff --git a/test/polygons.jl b/test/polygons.jl index e53ced00..8755e3d8 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -15,7 +15,7 @@ p2 = Polygon(OffsetArray(exterior, 0), interiors) @test p2 == p1 - # TODO: promote polygon type automatically + # TODO: promote polygon type automatically when creating MultiPolygon polygon = Polygon(Point2f.(points)) mp = MultiPolygon([polygon, p1, p2]) @test mp.polygons == [polygon, p1, p2] From b1ae5f5f60d002ff44fb6aab0e9f76b3f3e38871 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 7 Feb 2025 14:30:30 +0100 Subject: [PATCH 20/28] add poly promotion for MultiPolygon --- src/basic_types.jl | 15 +++++++++++++++ test/polygons.jl | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 6b60ef68..77afd86a 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -321,6 +321,17 @@ function coordinates(polygon::Polygon{N,T}) where {N,T} end end +function Base.promote_rule(::Type{Polygon{N, T1}}, ::Type{Polygon{N, T2}}) where {N, T1, T2} + return Polygon{N, promote_rule(T1, T2)} +end + +function Base.convert(::Type{Polygon{N, T}}, poly::Polygon{N}) where {N, T} + return Polygon( + convert(Vector{Point{N, T}}, poly.exterior), + convert(Vector{Vector{Point{N, T}}}, poly.interiors), + ) +end + """ MultiPolygon(polygons::AbstractPolygon) @@ -333,6 +344,10 @@ end function MultiPolygon(polygons::AbstractVector{<:AbstractPolygon{Dim,T}}) where {Dim,T} return MultiPolygon(convert(Vector{eltype(polygons)}, polygons)) end +function MultiPolygon(polygons::AbstractVector{<:AbstractPolygon{Dim}}) where {Dim} + T = reduce(promote_type, typeof.(polygons), init = eltype(polygons)) + return MultiPolygon(convert(Vector{T}, polygons)) +end Base.getindex(mp::MultiPolygon, i) = mp.polygons[i] Base.size(mp::MultiPolygon) = size(mp.polygons) diff --git a/test/polygons.jl b/test/polygons.jl index 8755e3d8..39d7c4db 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -15,8 +15,7 @@ p2 = Polygon(OffsetArray(exterior, 0), interiors) @test p2 == p1 - # TODO: promote polygon type automatically when creating MultiPolygon - polygon = Polygon(Point2f.(points)) + polygon = Polygon(points) mp = MultiPolygon([polygon, p1, p2]) @test mp.polygons == [polygon, p1, p2] @test mp[1] == polygon @@ -24,6 +23,8 @@ @test size(mp) == (3,) # TODO: What does size even mean here? @test length(mp) == 3 @test MultiPolygon(OffsetArray([polygon, p1, p2], 0)) == mp + mp = MultiPolygon([Polygon(Point2f.()), p1, p2]) + end rect = Rect2f(0, 0, 1, 1) From 1915d313cf831bfed0e2f12ef51365122de6ade8 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 7 Feb 2025 14:37:44 +0100 Subject: [PATCH 21/28] fix type targeting in connect --- src/viewtypes.jl | 12 +++++++++++- test/polygons.jl | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/viewtypes.jl b/src/viewtypes.jl index 0f531036..0b760836 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -73,15 +73,25 @@ function connect(points::AbstractVector{Point}, end function connect(points::AbstractVector{T}, ::Type{<:Point{N}}, skip::Int=N) where {T <: Real,N} + return connect(points, Point{N, T}, skip) +end +function connect(points::AbstractVector{_T}, ::Type{<:Point{N, T}}, skip::Int=N) where {T <: Real, N, _T <: Real} return map(Point{N,T}, TupleView{N,skip}(points)) end function connect(indices::AbstractVector{T}, P::Type{<:AbstractFace{N}}, skip::Int=N) where {T <: Integer, N} + return connect(indices, Face(P, T), skip) +end +function connect(indices::AbstractVector{_T}, P::Type{<:AbstractFace{N, T}}, + skip::Int=N) where {T <: Integer, N, _T <: Integer} return collect(reinterpret(Face(P, T), TupleView{N, skip}(indices))) end -function connect(points::AbstractMatrix{T}, P::Type{<:Point{N}}) where {T <: Real, N} +function connect(points::AbstractMatrix{T}, ::Type{<:Point{N}}) where {T <: Real, N} + return connect(points, Point{N, T}) +end +function connect(points::AbstractMatrix{_T}, P::Type{Point{N, T}}) where {T <: Real, N, _T <: Real} return if size(points, 1) === N return reinterpret(Point{N,T}, points) elseif size(points, 2) === N diff --git a/test/polygons.jl b/test/polygons.jl index 39d7c4db..d8e65878 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -1,6 +1,9 @@ @testset "Polygon" begin @testset "Constructors" begin points = connect([1, 2, 3, 4, 5, 6], Point2f) + @test points isa Vector{Point2f} + points = connect([1, 2, 3, 4, 5, 6], Point2) + @test points isa Vector{Point2i} polygon = Polygon(points) @test polygon == Polygon(points) @test polygon == copy(polygon) @@ -23,8 +26,7 @@ @test size(mp) == (3,) # TODO: What does size even mean here? @test length(mp) == 3 @test MultiPolygon(OffsetArray([polygon, p1, p2], 0)) == mp - mp = MultiPolygon([Polygon(Point2f.()), p1, p2]) - + @test MultiPolygon([Polygon(Point2f.(points)), p1, p2]) == mp end rect = Rect2f(0, 0, 1, 1) From f179ff00e00595aa505437544883b42ec2f32e87 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 7 Feb 2025 14:38:14 +0100 Subject: [PATCH 22/28] fix test failure --- test/geometrytypes.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 708b26ed..14101d61 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -231,8 +231,8 @@ end GLTriangleFace[(1, 1, 1), (1, 1, 1), (2, 2, 2), (2, 2, 2), (3, 3, 3), (3, 3, 3), (4, 4, 4), (4, 4, 4), (5, 5, 5), (5, 5, 5), (6, 6, 6), (6, 6, 6)] ) @test coordinates(mesh) == Point{3, Float32}[ - [1.0, 1.0, 1.0], [1.0, 1.0, 2.0], [1.0, 2.0, 1.0], [1.0, 2.0, 2.0], - [2.0, 1.0, 1.0], [2.0, 1.0, 2.0], [2.0, 2.0, 1.0], [2.0, 2.0, 2.0]] + [1, 2, 3], [1, 2, 9], [1, 7, 3], [1, 7, 9], + [5, 2, 3], [5, 2, 9], [5, 7, 3], [5, 7, 9]] @test isempty(Rect{3,Float32}()) end From e330decd5726289bbc16e03256fc664156bc0b63 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 7 Feb 2025 15:03:36 +0100 Subject: [PATCH 23/28] fix 32Bit, 1.6 --- src/basic_types.jl | 6 +----- test/polygons.jl | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 77afd86a..5a82d6dd 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -322,7 +322,7 @@ function coordinates(polygon::Polygon{N,T}) where {N,T} end function Base.promote_rule(::Type{Polygon{N, T1}}, ::Type{Polygon{N, T2}}) where {N, T1, T2} - return Polygon{N, promote_rule(T1, T2)} + return Polygon{N, promote_type(T1, T2)} end function Base.convert(::Type{Polygon{N, T}}, poly::Polygon{N}) where {N, T} @@ -344,10 +344,6 @@ end function MultiPolygon(polygons::AbstractVector{<:AbstractPolygon{Dim,T}}) where {Dim,T} return MultiPolygon(convert(Vector{eltype(polygons)}, polygons)) end -function MultiPolygon(polygons::AbstractVector{<:AbstractPolygon{Dim}}) where {Dim} - T = reduce(promote_type, typeof.(polygons), init = eltype(polygons)) - return MultiPolygon(convert(Vector{T}, polygons)) -end Base.getindex(mp::MultiPolygon, i) = mp.polygons[i] Base.size(mp::MultiPolygon) = size(mp.polygons) diff --git a/test/polygons.jl b/test/polygons.jl index d8e65878..2bf53288 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -3,7 +3,7 @@ points = connect([1, 2, 3, 4, 5, 6], Point2f) @test points isa Vector{Point2f} points = connect([1, 2, 3, 4, 5, 6], Point2) - @test points isa Vector{Point2i} + @test points isa Vector{Point2{Int}} polygon = Polygon(points) @test polygon == Polygon(points) @test polygon == copy(polygon) From 50c3abcb973618246295d8e3fa1aa7c0b1a8c92f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 7 Feb 2025 15:03:54 +0100 Subject: [PATCH 24/28] add more connect tests --- test/runtests.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index cabfb82a..e522916b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -80,11 +80,14 @@ end @testset "connected views" begin numbers = [1, 2, 3, 4, 5, 6] x = connect(numbers, Point{2}) - - @test x == Point[(1, 2), (3, 4), (5, 6)] + @test x == Point{2, Int}[(1, 2), (3, 4), (5, 6)] + xf = connect(numbers, Point2f) + @test xf == Point2f[(1, 2), (3, 4), (5, 6)] line = connect(x, Line, 1) - @test line == [Line(Point(1, 2), Point(3, 4)), Line(Point(3, 4), Point(5, 6))] + @test line == [Line(Point{2, Int}(1, 2), Point{2, Int}(3, 4)), Line(Point{2, Int}(3, 4), Point{2, Int}(5, 6))] + linef = connect(xf, Line, 1) + @test linef == [Line(Point2f(1, 2), Point2f(3, 4)), Line(Point2f(3, 4), Point2f(5, 6))] triangles = connect(x, Triangle) @test triangles == [Triangle(Point(1, 2), Point(3, 4), Point(5, 6))] From 99a3e201f2ee072fb5935c2724dedd57ffbd89b3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 11 Feb 2025 16:04:12 +0100 Subject: [PATCH 25/28] make line intersection changes not breaking --- src/lines.jl | 14 ++++++++++++-- test/runtests.jl | 12 +++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/lines.jl b/src/lines.jl index ff9fd295..da06be43 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -63,11 +63,21 @@ end self_intersections(points::AbstractVector{<:Point}) Finds all self intersections of in a continuous line described by `points`. +Returns a Vector of indices where each pair `v[2i], v[2i+1]` refers two +intersecting line segments by their first point, and a Vector of intersection +points. Note that if two points are the same, they will generate a self intersection -unless they are the first and last point or part of consecutive segments. +unless they are consecutive segments. (The first and last point are assumed to +be shared between the first and last segment.) """ function self_intersections(points::AbstractVector{<:VecTypes{D, T}}) where {D, T} + ti, sections = _self_intersections(points) + # convert array of tuples to flat array + return [x for t in ti for x in t], sections +end + +function _self_intersections(points::AbstractVector{<:VecTypes{D, T}}) where {D, T} sections = similar(points, 0) intersections = Tuple{Int, Int}[] @@ -98,7 +108,7 @@ Splits polygon `points` into it's self intersecting parts. Only 1 intersection is handled right now. """ function split_intersections(points::AbstractVector{<:VecTypes{N, T}}) where {N, T} - intersections, sections = self_intersections(points) + intersections, sections = _self_intersections(points) return if isempty(intersections) return [points] elseif length(intersections) == 1 && length(sections) == 1 diff --git a/test/runtests.jl b/test/runtests.jl index e522916b..f1cfc16e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -285,18 +285,24 @@ end @test collect(GeometryBasics.consecutive_pairs(ps)) == collect(zip(ps[1:end-1], ps[2:end])) ps = Point2f[(0,0), (1,0), (0,1), (1,2), (0,2), (1,1), (0,0)] - idxs, ips = self_intersections(ps) + idxs, ips = _self_intersections(ps) @test idxs == [(2, 6), (3, 5)] @test ips == [Point2f(0.5), Point2f(0.5, 1.5)] + idxs2, ips2 = self_intersections(ps) + @test ips2 == ips + @test idxs2 == [2, 6, 3, 5] ps = [Point2f(cos(x), sin(x)) for x in 0:4pi/5:4pi+0.1] - idxs, ips = self_intersections(ps) + idxs, ips = _self_intersections(ps) @test idxs == [(1, 3), (1, 4), (2, 4), (2, 5), (3, 5)] @test all(ips .≈ Point2f[(0.30901694, 0.2245140), (-0.118034005, 0.36327127), (-0.38196602, 0), (-0.118033946, -0.3632713), (0.309017, -0.22451389)]) + idxs2, ips2 = self_intersections(ps) + @test ips2 == ips + @test idxs2 == [1, 3, 1, 4, 2, 4, 2, 5, 3, 5] @test_throws ErrorException split_intersections(ps) ps = Point2f[(0,0), (1,0), (0,1), (1,1), (0, 0)] - idxs, ips = self_intersections(ps) + idxs, ips = _self_intersections(ps) sps = split_intersections(ps) @test sps[1] == [ps[3], ps[4], ips[1]] @test sps[2] == [ps[5], ps[1], ps[2], ips[1]] From 5c248f02765c3129791693a15370f316d8f88c16 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 11 Feb 2025 16:10:29 +0100 Subject: [PATCH 26/28] fix and test single-face mesh constructor --- src/basic_types.jl | 6 ++++++ test/meshes.jl | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/basic_types.jl b/src/basic_types.jl index 5a82d6dd..f60e05da 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -758,6 +758,12 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, return Mesh(points, connect(faces, facetype, skip)) end +# the method above allows Mesh(..., Face(...), ...) to work, but produces bad results +# explicitly error here +function Mesh(points::AbstractVector{<:Point}, faces::AbstractFace, args...; kwargs...) + throw(MethodError(Mesh, (points, faces, args...))) +end + function Mesh(; kwargs...) fs = faces(kwargs[:position]::FaceView) va = NamedTuple{keys(kwargs)}(map(keys(kwargs)) do k diff --git a/test/meshes.jl b/test/meshes.jl index 58911928..e6c0b0e8 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -31,6 +31,8 @@ end @testset "Merge empty vector of meshes" begin # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/136 merge(Mesh[]) == Mesh(Point3f[], GLTriangleFace[]) + merge([Mesh(Point3f[], GLTriangleFace[])]) == Mesh(Point3f[], GLTriangleFace[]) + merge([Mesh(Point3f[], GLTriangleFace[]), Mesh(Point3f[], GLTriangleFace[])]) == Mesh(Point3f[], GLTriangleFace[]) end @testset "Vertex Index Remapping" begin @@ -138,6 +140,8 @@ end @test faces(m) == fs end + @test_throws MethodError Mesh(Point2f[], GLTriangleFace(1,2,3)) + @testset "Verification" begin # enough vertices present @test_throws ErrorException Mesh(rand(Point2f, 7), fs) From e544dd02141ab80e85b87fde769f61bd0a3d72a6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 11 Feb 2025 16:17:23 +0100 Subject: [PATCH 27/28] fix tests --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index f1cfc16e..5fb98ded 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -285,7 +285,7 @@ end @test collect(GeometryBasics.consecutive_pairs(ps)) == collect(zip(ps[1:end-1], ps[2:end])) ps = Point2f[(0,0), (1,0), (0,1), (1,2), (0,2), (1,1), (0,0)] - idxs, ips = _self_intersections(ps) + idxs, ips = GeometryBasics._self_intersections(ps) @test idxs == [(2, 6), (3, 5)] @test ips == [Point2f(0.5), Point2f(0.5, 1.5)] idxs2, ips2 = self_intersections(ps) @@ -293,7 +293,7 @@ end @test idxs2 == [2, 6, 3, 5] ps = [Point2f(cos(x), sin(x)) for x in 0:4pi/5:4pi+0.1] - idxs, ips = _self_intersections(ps) + idxs, ips = GeometryBasics._self_intersections(ps) @test idxs == [(1, 3), (1, 4), (2, 4), (2, 5), (3, 5)] @test all(ips .≈ Point2f[(0.30901694, 0.2245140), (-0.118034005, 0.36327127), (-0.38196602, 0), (-0.118033946, -0.3632713), (0.309017, -0.22451389)]) idxs2, ips2 = self_intersections(ps) @@ -302,7 +302,7 @@ end @test_throws ErrorException split_intersections(ps) ps = Point2f[(0,0), (1,0), (0,1), (1,1), (0, 0)] - idxs, ips = _self_intersections(ps) + idxs, ips = GeometryBasics._self_intersections(ps) sps = split_intersections(ps) @test sps[1] == [ps[3], ps[4], ips[1]] @test sps[2] == [ps[5], ps[1], ps[2], ips[1]] From 1be5f723e84c10c5e7667630900392e46df14051 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 11 Feb 2025 17:33:32 +0100 Subject: [PATCH 28/28] test Rect union, update --- test/geometrytypes.jl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 14101d61..f7bfb44e 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -192,6 +192,10 @@ end @test widths(r) == Vec(-Inf, -Inf) @test area(r) == Inf @test volume(r) == Inf + # TODO: broken? returns NaN widths + # @test union(r, Rect2f(1,1,2,2)) == Rect2f(1,1,2,2) + # @test union(Rect2f(1,1,2,2), r) == Rect2f(1,1,2,2) + @test update(r, Vec2f(1,1)) == Rect2f(1,1,0,0) a = Rect(Vec(0, 1), Vec(2, 3)) pt_expa = Point{2,Int}[(0, 1), (2, 1), (2, 4), (0, 4)] @@ -207,6 +211,9 @@ end @test widths(a) == Vec(2,3) @test area(a) == 2*3 @test volume(a) == 2*3 + @test union(a, Rect2f(1,1,2,2)) == Rect2f(0,1,3,3) + @test union(Rect2f(1,1,2,2), a) == Rect2f(0,1,3,3) + @test update(a, Vec2f(0,0)) == Rect2f(0,0,2,4) b = Rect(Vec(1, 2, 3), Vec(4, 5, 6)) pt_expb = Point{3, Int64}[[1, 2, 3], [1, 2, 9], [1, 7, 3], [1, 7, 9], @@ -221,6 +228,10 @@ end @test widths(b) == Vec(4,5,6) @test_throws MethodError area(b) @test volume(b) == 4*5*6 + @test union(b, Rect3f(1,1,1,2,2,2)) == Rect3f(1,1,1, 4,6,8) + @test union(Rect3f(1,1,1,2,2,2), b) == Rect3f(1,1,1, 4,6,8) + @test update(b, Vec3f(0)) == Rect3f(0,0,0,5,7,9) + mesh = normal_mesh(b) @test faces(mesh) == GLTriangleFace[