Skip to content

Commit 14f3ce4

Browse files
authored
Improve test coverage (#238)
* automatic formatting * add tests for Rect constructors and refactor constructors * test the rest of Rect * more tests for basic_types.jl * add triangulation tests * treat float precision error, add isapprox for Rects * test split_mesh * improve meshes.jl coverage * refactor boundingboxes, add tests * test and clean up line intersection code * fix docs * improve Sphere test coverage * fix Rect dim truncation * a few more tests for OffsetIntegers & FixedArrays * add basic docs for bounding boxes * add deprecation warning for developers * nvm, doesn't work * fix docs * explicitly test Rect getters/utility functions * add poly promotion for MultiPolygon * fix type targeting in connect * fix test failure * fix 32Bit, 1.6 * add more connect tests * make line intersection changes not breaking * fix and test single-face mesh constructor * fix tests * test Rect union, update
1 parent 429889a commit 14f3ce4

16 files changed

+848
-269
lines changed

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ makedocs(format=Documenter.HTML(prettyurls=get(ENV, "CI", "false") == "true"),
1313
"polygons.md",
1414
"meshes.md",
1515
"decomposition.md",
16+
"boundingboxes.md",
1617
"static_array_types.md",
17-
"api.md"
18+
"api.md",
1819
],
1920
modules=[GeometryBasics])
2021

docs/src/boundingboxes.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Bounding Boxes
2+
3+
You can generate an axis aligned bounding box for any `AbstractGeometry` by calling `Rect(geom)`.
4+
Depending on the object this will either rely on `coordinates(geom)` or a specialized method.
5+
You can also create a bounding box of set dimension or type by adding the related parameters.
6+
7+
8+
```@repl
9+
using GeometryBasics
10+
11+
s = Circle(Point2f(0), 1f0)
12+
Rect(s) # specialized, exact bounding box
13+
Rect3(s)
14+
Rect3d(s)
15+
RectT{Float64}(s)
16+
Rect(GeometryBasics.mesh(s)) # using generated coordinates in mesh
17+
```
18+
19+
## Extending
20+
21+
If you want to add a specialized bounding box method you should implement `Rect{N, T}(geom) = ...`.
22+
All other methods funnel into that one, defaulting to the same `N, T` that the given `AbstractGeometry{N, T}` has.
23+
GeometryBasics allows the user given dimension `N` to be smaller or equal to that of the geometry.
24+
This is checked with `GeometryBasics.bbox_dim_check(user_dim, geom_dim)` which you may reuse.
25+
26+
```julia
27+
function Rect{N, T}(a::HyperSphere{N2}) where {N, N2, T}
28+
GeometryBasics.bbox_dim_check(N, N2)
29+
return Rect{N, T}(minimum(a), widths(a))
30+
end
31+
```

src/GeometryBasics.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export AbstractFace, TriangleFace, QuadFace, GLTriangleFace
4141
export OffsetInteger, ZeroIndex, OneIndex, GLIndex
4242
export decompose, coordinates, faces, normals, decompose_uv, decompose_normals,
4343
texturecoordinates, vertex_attributes
44-
export expand_faceviews
44+
export expand_faceviews, split_mesh, remove_duplicates
4545
export face_normals
4646
export Tessellation, Normal, UV, UVW
4747
export AbstractMesh, Mesh, MetaMesh, FaceView
@@ -58,7 +58,7 @@ export uv_mesh, normal_mesh, uv_normal_mesh
5858
export height, origin, radius, width, widths
5959
export HyperSphere, Circle, Sphere
6060
export Cylinder, Pyramid, extremity
61-
export HyperRectangle, Rect, Rect2, Rect3, Recti, Rect2i, Rect3i, Rectf, Rect2f, Rect3f, Rectd, Rect2d, Rect3d
61+
export HyperRectangle, Rect, Rect2, Rect3, Recti, Rect2i, Rect3i, Rectf, Rect2f, Rect3f, Rectd, Rect2d, Rect3d, RectT
6262
export before, during, meets, overlaps, intersects, finishes
6363
export centered, direction, area, volume, update
6464
export max_dist_dim, max_euclidean, max_euclideansq, min_dist_dim, min_euclidean

src/basic_types.jl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,17 @@ function coordinates(polygon::Polygon{N,T}) where {N,T}
321321
end
322322
end
323323

324+
function Base.promote_rule(::Type{Polygon{N, T1}}, ::Type{Polygon{N, T2}}) where {N, T1, T2}
325+
return Polygon{N, promote_type(T1, T2)}
326+
end
327+
328+
function Base.convert(::Type{Polygon{N, T}}, poly::Polygon{N}) where {N, T}
329+
return Polygon(
330+
convert(Vector{Point{N, T}}, poly.exterior),
331+
convert(Vector{Vector{Point{N, T}}}, poly.interiors),
332+
)
333+
end
334+
324335
"""
325336
MultiPolygon(polygons::AbstractPolygon)
326337
@@ -337,6 +348,7 @@ end
337348
Base.getindex(mp::MultiPolygon, i) = mp.polygons[i]
338349
Base.size(mp::MultiPolygon) = size(mp.polygons)
339350
Base.length(mp::MultiPolygon) = length(mp.polygons)
351+
Base.:(==)(a::MultiPolygon, b::MultiPolygon) = a.polygons == b.polygons
340352

341353
"""
342354
LineString(points::AbstractVector{<:Point})
@@ -361,6 +373,7 @@ end
361373
Base.getindex(ms::MultiLineString, i) = ms.linestrings[i]
362374
Base.size(ms::MultiLineString) = size(ms.linestrings)
363375
Base.length(mpt::MultiLineString) = length(mpt.linestrings)
376+
Base.:(==)(a::MultiLineString, b::MultiLineString) = a.linestrings == b.linestrings
364377

365378
"""
366379
MultiPoint(points::AbstractVector{AbstractPoint})
@@ -639,6 +652,65 @@ function Base.:(==)(a::Mesh, b::Mesh)
639652
(faces(a) == faces(b)) && (a.views == b.views)
640653
end
641654

655+
"""
656+
strictly_equal_face_vertices(a::Mesh, b::Mesh)
657+
658+
Checks whether mesh a and b are equal in terms of vertices used in their faces.
659+
This allows for vertex data and indices to be synchronously permuted. For
660+
example, this will recognize
661+
```
662+
a = Mesh([a, b, c], [GLTriangleFace(1,2,3)])
663+
b = Mesh([a, c, b], [GLTriangleFace(1,3,2)])
664+
```
665+
as equal, because while the positions and faces have different orders the vertices
666+
in the face are the same:
667+
```
668+
[a, c, b][[1, 3, 2]] == [a, b, c] == [a, b, c][[1,2,3]]
669+
```
670+
671+
This still returns false if the order of faces is permuted, e.g.
672+
`Mesh(ps, [f1, f2]) != Mesh(ps, [f2, f1])`. It also returns false if vertices are
673+
cyclically permuted within a face, i.e. `ps[[1,2,3]] != ps[[2,3,1]]`.
674+
"""
675+
function strictly_equal_face_vertices(a::Mesh, b::Mesh)
676+
# Quick checks
677+
if propertynames(a) != propertynames(b) || length(faces(a)) != length(faces(b))
678+
return false
679+
end
680+
681+
N = length(faces(a))
682+
# for views we want to ignore empty ranges (they don't represent geometry)
683+
# and treat 1:N as no range (as that is used interchangeably)
684+
views1 = filter(view -> length(view) > 0 && (minimum(view) > 1 || maximum(view) < N), a.views)
685+
views2 = filter(view -> length(view) > 0 && (minimum(view) > 1 || maximum(view) < N), b.views)
686+
views1 != views2 && return false
687+
688+
# TODO: Allow different face orders & cyclic permutation within faces.
689+
# E.g. use hash.(data[face]), cyclically permute min to front, hash result
690+
# and add them to heaps (or sets?) so we can compare them at the end
691+
# That should probably be another function as it's probably a significant
692+
# step up in overhead?
693+
for (attrib1, attrib2) in zip(vertex_attributes(a), vertex_attributes(b))
694+
if attrib1 isa FaceView
695+
if !(attrib2 isa FaceView) || length(faces(attrib1)) != length(faces(attrib2))
696+
return false
697+
end
698+
for (f1, f2) in zip(faces(attrib1), faces(attrib2))
699+
values(attrib1)[f1] == values(attrib2)[f2] || return false
700+
end
701+
else
702+
if attrib2 isa FaceView
703+
return false
704+
end
705+
for (f1, f2) in zip(faces(a), faces(b))
706+
attrib1[f1] == attrib2[f2] || return false
707+
end
708+
end
709+
end
710+
711+
return true
712+
end
713+
642714
function Base.iterate(mesh::Mesh, i=1)
643715
return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing
644716
end
@@ -686,6 +758,12 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer},
686758
return Mesh(points, connect(faces, facetype, skip))
687759
end
688760

761+
# the method above allows Mesh(..., Face(...), ...) to work, but produces bad results
762+
# explicitly error here
763+
function Mesh(points::AbstractVector{<:Point}, faces::AbstractFace, args...; kwargs...)
764+
throw(MethodError(Mesh, (points, faces, args...)))
765+
end
766+
689767
function Mesh(; kwargs...)
690768
fs = faces(kwargs[:position]::FaceView)
691769
va = NamedTuple{keys(kwargs)}(map(keys(kwargs)) do k

src/boundingboxes.jl

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
1-
function Rect(geometry::AbstractArray{<:Point{N,T}}) where {N,T}
2-
return Rect{N,T}(geometry)
1+
# Boundingbox-like Rect constructors
2+
3+
Rect(p::AbstractGeometry{N, T}) where {N, T} = Rect{N, T}(p)
4+
RectT{T}(p::AbstractGeometry{N}) where {N, T} = Rect{N, T}(p)
5+
Rect{N}(p::AbstractGeometry{_N, T}) where {N, _N, T} = Rect{N, T}(p)
6+
7+
Rect(p::AbstractArray{<: VecTypes{N, T}}) where {N, T} = Rect{N, T}(p)
8+
RectT{T}(p::AbstractArray{<: VecTypes{N}}) where {N, T} = Rect{N, T}(p)
9+
Rect{N}(p::AbstractArray{<: VecTypes{_N, T}}) where {N, _N, T} = Rect{N, T}(p)
10+
11+
# Implementations
12+
# Specialize fully typed Rect constructors
13+
Rect{N, T}(p::AbstractGeometry) where {N, T} = Rect{N, T}(coordinates(p))
14+
15+
function bbox_dim_check(trg, src::Integer)
16+
@assert trg isa Integer "Rect{$trg, $T1} is invalid. This may have happened due to calling Rect{$N1}(obj) to get a bounding box."
17+
if trg < src
18+
throw(ArgumentError("Cannot construct a $trg dimensional bounding box from $src dimensional Points. ($trg must be ≥ $src)"))
19+
end
320
end
421

522
"""
6-
Rect(points::AbstractArray{<: Point})
23+
Rect(points::AbstractArray{<: VecTypes})
724
825
Construct a bounding box containing all the given points.
926
"""
10-
function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point}
11-
N2, T2 = length(PT), eltype(PT)
12-
@assert N1 >= N2
27+
function Rect{N1, T1}(points::AbstractArray{<: VecTypes{N2, T2}}) where {N1, T1, N2, T2}
28+
bbox_dim_check(N1, N2)
1329
vmin = Point{N2,T2}(typemax(T2))
1430
vmax = Point{N2,T2}(typemin(T2))
15-
for p in geometry
31+
for p in points
1632
vmin, vmax = _minmax(p, vmin, vmax)
1733
end
1834
o = vmin
@@ -25,29 +41,25 @@ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point}
2541
end
2642
end
2743

44+
2845
"""
2946
Rect(primitive::GeometryPrimitive)
3047
3148
Construct a bounding box for the given primitive.
3249
"""
33-
function Rect(primitive::GeometryPrimitive{N,T}) where {N,T}
34-
return Rect{N,T}(primitive)
35-
end
36-
37-
function Rect{T}(primitive::GeometryPrimitive{N,T}) where {N,T}
38-
return Rect{N,T}(primitive)
39-
end
40-
41-
function Rect{T}(a::Pyramid) where {T}
42-
w, h = a.width / T(2), a.length
50+
function Rect{N, T}(a::Pyramid) where {N, T}
51+
bbox_dim_check(N, 3)
52+
w, h = a.width, a.length
4353
m = Vec{3,T}(a.middle)
44-
return Rect{T}(m .- Vec{3,T}(w, w, 0), m .+ Vec{3,T}(w, w, h))
54+
return Rect{N, T}(m .- Vec{3,T}(w / T(2), w / T(2), 0), Vec{3,T}(w, w, h))
4555
end
4656

47-
function Rect{T}(a::Sphere) where {T}
48-
mini, maxi = extrema(a)
49-
return Rect{T}(mini, maxi .- mini)
57+
function Rect{N, T}(a::HyperSphere{N2}) where {N, N2, T}
58+
bbox_dim_check(N, N2)
59+
return Rect{N, T}(minimum(a), widths(a))
5060
end
5161

52-
Rect{T}(a) where {T} = Rect{T}(coordinates(a))
53-
Rect{N,T}(a) where {N,T} = Rect{N,T}(coordinates(a))
62+
# TODO: exact implementation that doesn't rely on coordinates
63+
# function Rect{N, T}(a::Cylinder) where {N, T}
64+
# return Rect{N, T}(...)
65+
# end

src/fixed_arrays.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ macro fixed_vector(name_parent)
5757
function $(name){S}(x::T) where {S,T <: Tuple}
5858
return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x)
5959
end
60-
$(name){S,T}(x::StaticVector) where {S,T} = $(name){S,T}(Tuple(x))
60+
$(name){S,T}(x::StaticVector{S}) where {S,T} = $(name){S,T}(Tuple(x))
61+
$(name){S,T}(x::StaticVector) where {S,T} = $(name){S,T}(ntuple(i -> x[i], S))
6162

6263
@generated function (::Type{$(name){S,T}})(x::$(name)) where {S,T}
6364
idx = [:(x[$i]) for i in 1:S]
@@ -139,7 +140,7 @@ const VecTypes{N,T} = Union{StaticVector{N,T},NTuple{N,T}}
139140
const Vecf{N} = Vec{N,Float32}
140141
const PointT{T} = Point{N,T} where N
141142
const Pointf{N} = Point{N,Float32}
142-
143+
143144
Base.isnan(p::Union{AbstractPoint,Vec}) = any(isnan, p)
144145
Base.isinf(p::Union{AbstractPoint,Vec}) = any(isinf, p)
145146
Base.isfinite(p::Union{AbstractPoint,Vec}) = all(isfinite, p)
@@ -177,9 +178,9 @@ export Vecf, Pointf
177178
Vec{N, T}(args...)
178179
Vec{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector})
179180
180-
Constructs a Vec of length `N` from the given arguments.
181+
Constructs a Vec of length `N` from the given arguments.
181182
182-
Note that Point and Vec don't follow strict mathematical definitions. Instead
183+
Note that Point and Vec don't follow strict mathematical definitions. Instead
183184
we allow them to be used interchangeably.
184185
185186
## Aliases
@@ -197,9 +198,9 @@ Vec
197198
Point{N, T}(args...)
198199
Point{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector})
199200
200-
Constructs a Point of length `N` from the given arguments.
201+
Constructs a Point of length `N` from the given arguments.
201202
202-
Note that Point and Vec don't follow strict mathematical definitions. Instead
203+
Note that Point and Vec don't follow strict mathematical definitions. Instead
203204
we allow them to be used interchangeably.
204205
205206
## Aliases

src/lines.jl

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Returns `(intersection_found::Bool, intersection_point::Point)`
77
"""
88
# 2D Line-segment intersection algorithm by Paul Bourke and many others.
99
# http://paulbourke.net/geometry/pointlineplane/
10-
function intersects(a::Line{2,T1}, b::Line{2,T2}) where {T1,T2}
10+
function intersects(a::Line{2,T1}, b::Line{2,T2}; eps = 0) where {T1,T2}
1111
T = promote_type(T1, T2)
1212
p0 = zero(Point2{T})
1313

@@ -34,7 +34,7 @@ function intersects(a::Line{2,T1}, b::Line{2,T2}) where {T1,T2}
3434

3535
# Values between [0, 1] mean the intersection point of the lines rests on
3636
# both of the line segments.
37-
if 0 <= unknown_a <= 1 && 0 <= unknown_b <= 1
37+
if eps <= unknown_a <= 1-eps && eps <= unknown_b <= 1-eps
3838
# Substituting an unknown back lets us find the intersection point.
3939
x = x1 + (unknown_a * (x2 - x1))
4040
y = y1 + (unknown_a * (y2 - y1))
@@ -62,27 +62,39 @@ end
6262
"""
6363
self_intersections(points::AbstractVector{<:Point})
6464
65-
Finds all self intersections of polygon `points`
65+
Finds all self intersections of in a continuous line described by `points`.
66+
Returns a Vector of indices where each pair `v[2i], v[2i+1]` refers two
67+
intersecting line segments by their first point, and a Vector of intersection
68+
points.
69+
70+
Note that if two points are the same, they will generate a self intersection
71+
unless they are consecutive segments. (The first and last point are assumed to
72+
be shared between the first and last segment.)
6673
"""
67-
function self_intersections(points::AbstractVector{<:Point})
74+
function self_intersections(points::AbstractVector{<:VecTypes{D, T}}) where {D, T}
75+
ti, sections = _self_intersections(points)
76+
# convert array of tuples to flat array
77+
return [x for t in ti for x in t], sections
78+
end
79+
80+
function _self_intersections(points::AbstractVector{<:VecTypes{D, T}}) where {D, T}
6881
sections = similar(points, 0)
69-
intersections = Int[]
70-
71-
wraparound(i) = mod1(i, length(points) - 1)
72-
73-
for (i, (a, b)) in enumerate(consecutive_pairs(points))
74-
for (j, (a2, b2)) in enumerate(consecutive_pairs(points))
75-
is1, is2 = wraparound(i + 1), wraparound(i - 1)
76-
if i != j &&
77-
is1 != j &&
78-
is2 != j &&
79-
!(i in intersections) &&
80-
!(j in intersections)
81-
intersected, p = intersects(Line(a, b), Line(a2, b2))
82-
if intersected
83-
push!(intersections, i, j)
84-
push!(sections, p)
85-
end
82+
intersections = Tuple{Int, Int}[]
83+
84+
N = length(points)
85+
86+
for i in 1:N-3
87+
a = points[i]; b = points[i+1]
88+
# i+1 == j describes consecutive segments which are always "intersecting"
89+
# at point i+1/j. Skip those (start at i+2)
90+
# Special case: We assume points[1] == points[end] so 1 -> 2 and N-1 -> N
91+
# always "intersect" at 1/N. Skip this too (end at N-2 in this case)
92+
for j in i+2 : N-1 - (i == 1)
93+
a2 = points[j]; b2 = points[j+1]
94+
intersected, p = intersects(Line(a, b), Line(a2, b2))
95+
if intersected
96+
push!(intersections, (i, j))
97+
push!(sections, p)
8698
end
8799
end
88100
end
@@ -95,15 +107,14 @@ end
95107
Splits polygon `points` into it's self intersecting parts. Only 1 intersection
96108
is handled right now.
97109
"""
98-
function split_intersections(points::AbstractVector{<:Point})
99-
intersections, sections = self_intersections(points)
110+
function split_intersections(points::AbstractVector{<:VecTypes{N, T}}) where {N, T}
111+
intersections, sections = _self_intersections(points)
100112
return if isempty(intersections)
101113
return [points]
102-
elseif length(intersections) == 2 && length(sections) == 1
103-
a, b = intersections
114+
elseif length(intersections) == 1 && length(sections) == 1
115+
a, b = intersections[1]
104116
p = sections[1]
105-
a, b = min(a, b), max(a, b)
106-
poly1 = simple_concat(points, (a + 1):(b - 1), p)
117+
poly1 = simple_concat(points, (a + 1):b, p)
107118
poly2 = simple_concat(points, (b + 1):(length(points) + a), p)
108119
return [poly1, poly2]
109120
else

0 commit comments

Comments
 (0)