diff --git a/src/lines.jl b/src/lines.jl index da06be43..d49a0e3c 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -63,21 +63,14 @@ 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. +Returns a Vector of index tuples corresponding to the 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 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}[] @@ -108,7 +101,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/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 0aa73f36..c962e6c6 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -9,6 +9,10 @@ Formally it is the Cartesian product of intervals, which is represented by the struct HyperRectangle{N,T} <: GeometryPrimitive{N,T} origin::Vec{N,T} widths::Vec{N,T} + + function HyperRectangle{N, T}(origin::VecTypes, widths::VecTypes) where {N, T} + return new{N, T}(Vec{N, T}(min.(origin, origin .+ widths)), Vec{N, T}(abs.(widths))) + end end ## @@ -56,10 +60,9 @@ Rect() = Rect{2,Float32}() RectT{T}() where {T} = Rect{2,T}() Rect{N}() where {N} = Rect{N,Float32}() -function Rect{N,T}() where {T,N} - # empty constructor such that update will always include the first point - return Rect{N,T}(Vec{N,T}(typemax(T)), Vec{N,T}(typemin(T))) -end +Rect{N,T}() where {T <: AbstractFloat,N} = Rect{N,T}(Vec{N,T}(NaN), Vec{N,T}(0)) +Rect{N,T}() where {T, N} = throw(MethodError(Rect{N,T}, tuple())) +# TODO: what about integers and other types? No reasonable default? # Rect(numbers...) Rect(args::Vararg{Number, N}) where {N} = Rect{div(N, 2), promote_type(typeof.(args)...)}(args...) @@ -289,12 +292,7 @@ end # return vmin, vmax # end -function positive_widths(rect::Rect{N,T}) where {N,T} - mini, maxi = minimum(rect), maximum(rect) - realmin = min.(mini, maxi) - realmax = max.(mini, maxi) - return Rect{N,T}(realmin, realmax .- realmin) -end +positive_widths(rect::Rect{N,T}) where {N,T} = rect ### # set operations @@ -302,9 +300,10 @@ end """ isempty(h::Rect) -Return `true` if any of the widths of `h` are negative. +Return `true` if any of the widths of `h` are zero or negative. """ -Base.isempty(h::Rect{N,T}) where {N,T} = any(<(zero(T)), h.widths) +Base.isempty(h::Rect{N,T}) where {N,T} = any(<=(zero(T)), h.widths) +Base.isnan(r::Rect) = isnan(origin(r)) || isnan(widths(r)) """ union(r1::Rect{N}, r2::Rect{N}) @@ -312,6 +311,8 @@ Base.isempty(h::Rect{N,T}) where {N,T} = any(<(zero(T)), h.widths) Returns a new `Rect{N}` which contains both r1 and r2. """ function Base.union(h1::Rect{N}, h2::Rect{N}) where {N} + isnan(h1) && return h2 + isnan(h2) && return h1 m = min.(minimum(h1), minimum(h2)) mm = max.(maximum(h1), maximum(h2)) return Rect{N}(m, mm - m) @@ -332,6 +333,7 @@ end Perform a intersection between two Rects. """ function Base.intersect(h1::Rect{N}, h2::Rect{N}) where {N} + isnan(h1) || isnan(h2) && return Rect{N}() m = max.(minimum(h1), minimum(h2)) mm = min.(maximum(h1), maximum(h2)) return Rect{N}(m, mm - m) @@ -342,6 +344,8 @@ function update(b::Rect{N,T}, v::VecTypes{N,T2}) where {N,T,T2} end function update(b::Rect{N,T}, v::VecTypes{N,T}) where {N,T} + isnan(b) && return Rect{N, T}(v, Vec{N, T}(0)) + m = min.(minimum(b), v) maxi = maximum(b) mm = if any(isnan, maxi) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index f7bfb44e..a197a4d3 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -78,26 +78,36 @@ end @testset "HyperRectangles" begin @testset "Constructors" begin - # TODO: Do these actually make sense? - # Should they not be Rect(NaN..., 0...)? @testset "Empty Constructors" begin + function nan_equal(r1::Rect, r2::Rect) + o1 = origin(r1); o2 = origin(r2) + return ((isnan(o1) && isnan(o2)) || (o1 == o2)) && (widths(r1) == widths(r2)) + end + for constructor in [Rect, Rect{2}, Rect2, RectT, Rect2f] - @test constructor() == Rect{2, Float32}(Inf, Inf, -Inf, -Inf) + @test nan_equal(constructor(), Rect{2, Float32}(NaN, NaN, 0, 0)) end for constructor in [Rect{3}, Rect3, Rect3f] - @test constructor() == Rect{3, Float32}((Inf, Inf, Inf), (-Inf, -Inf, -Inf)) + @test nan_equal(constructor(), Rect{3, Float32}((NaN, NaN, NaN), (0, 0, 0))) end - for T in [UInt32, Int16, Float64] + for T in [UInt32, Int16] 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) + @test_throws MethodError constructor() 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)) + @test_throws MethodError constructor() end end + + for constructor in [Rect{2, Float64}, Rect2{Float64}, RectT{Float64, 2}] + @test nan_equal(constructor(), Rect{2, Float64}(NaN, NaN, 0, 0)) + end + for constructor in [Rect{3, Float64}, Rect3{Float64}, RectT{Float64, 3}] + @test nan_equal(constructor(), Rect{3, Float64}(Point3(NaN), Vec3(0))) + end end @testset "Constructor arg conversions" begin @@ -180,21 +190,24 @@ end @test constructor(m) ≈ Rect3f(-1, -1, -1, 2, 2, 2) end end + + r = Rect2f(10, 10, -5, -5) + @test origin(r) == Point2f(5) + @test widths(r) == Vec2f(5) + @test maximum(r) == Point2f(10) end - # TODO: These don't really make sense... r = Rect2f() - @test origin(r) == Vec(Inf, Inf) - @test minimum(r) == Vec(Inf, Inf) + @test isnan(origin(r)) + @test isnan(minimum(r)) @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 - # 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 width(r) == 0 + @test height(r) == 0 + @test widths(r) == Vec2(0) + @test area(r) == 0 + @test volume(r) == 0 + @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)) diff --git a/test/runtests.jl b/test/runtests.jl index 5fb98ded..e522916b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -285,24 +285,18 @@ 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 = GeometryBasics._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 = GeometryBasics._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 = GeometryBasics._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]]