diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 27e782248..39a024047 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -16,13 +16,13 @@ env:
JULIA_NUM_THREADS: 2
jobs:
test:
- name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
+ name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ matrix.float }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
version:
- - '1.9'
+ - 'lts'
- '1'
os:
- ubuntu-latest
@@ -30,9 +30,12 @@ jobs:
- windows-latest
arch:
- x64
+ float:
+ - Float32
+ - Float64
steps:
- uses: actions/checkout@v4
- - uses: julia-actions/setup-julia@v1
+ - uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
@@ -48,8 +51,10 @@ jobs:
${{ runner.os }}-
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
+ env:
+ FLOAT_TYPE: ${{ matrix.float }}
- uses: julia-actions/julia-processcoverage@v1
- - uses: codecov/codecov-action@v4
+ - uses: codecov/codecov-action@v5
with:
file: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
@@ -58,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: julia-actions/setup-julia@v1
+ - uses: julia-actions/setup-julia@v2
with:
version: '1'
- run: |
diff --git a/.github/workflows/FormatPR.yml b/.github/workflows/FormatPR.yml
index 3717c0414..8ce8a2d44 100644
--- a/.github/workflows/FormatPR.yml
+++ b/.github/workflows/FormatPR.yml
@@ -1,28 +1,11 @@
-name: FormatPR
+name: Format suggestions
on:
- push:
- branches:
- - master
+ pull_request
+
jobs:
- build:
+ code-style:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - name: Install JuliaFormatter and format
- run: |
- julia -e 'import Pkg; Pkg.add("JuliaFormatter")'
- julia -e 'using JuliaFormatter; format(".")'
- - name: Create Pull Request
- id: cpr
- uses: peter-evans/create-pull-request@v6
+ - uses: julia-actions/julia-format@v3
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- commit-message: ":robot: Format .jl files"
- title: '[AUTO] JuliaFormatter.jl run'
- branch: auto-juliaformatter-pr
- delete-branch: true
- labels: formatting, automated pr, no changelog
- - name: Check outputs
- run: |
- echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
- echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
+ version: '1' # Set `version` to '1.0.54' if you need to use JuliaFormatter.jl v1.0.54 (default: '1')
diff --git a/.gitignore b/.gitignore
index 2de6290f9..a14a49f25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,3 @@ Manifest.toml
docs/build
/.benchmarkci
/benchmark/*.json
-docs/src/assets/themes/
diff --git a/Project.toml b/Project.toml
index 18616e8bd..0f05c7249 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,42 +1,50 @@
name = "Meshes"
uuid = "eacbb407-ea5a-433e-ab97-5258b1ca43fa"
authors = ["Júlio Hoffimann and contributors"]
-version = "0.40.12"
+version = "0.52.6"
[deps]
Bessels = "0e736298-9ec6-45e8-9647-e4fc86a2fe38"
CircularArrays = "7a955b69-7140-5f4e-a0ed-f168c5e2e749"
+Colorfy = "03fe91ce-8ec6-4610-8e8d-e7491ccca690"
+CoordRefSystems = "b46f11dc-f210-4604-bfba-323c1ec968cb"
+DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df"
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc"
+ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
-Transducers = "28d57a85-8fef-5791-bfe6-a80928e7c999"
+TiledIteration = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
TransformsBase = "28dd2a49-a57a-4bfb-84ca-1a49db9b96b8"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
-[weakdeps]
-Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
-
-[extensions]
-MeshesMakieExt = "Makie"
-
[compat]
Bessels = "0.2"
CircularArrays = "1.3"
+Colorfy = "1.0"
+CoordRefSystems = "0.15"
+DelaunayTriangulation = "1.0"
Distances = "0.10"
LinearAlgebra = "1.9"
-Makie = "0.20"
+Makie = "0.21"
NearestNeighbors = "0.4"
Random = "1.9"
Rotations = "1.5.1"
+ScopedValues = "1.2"
SparseArrays = "1.9"
StaticArrays = "1.0"
StatsBase = "0.33, 0.34"
-Transducers = "0.4"
-TransformsBase = "1.4.1"
+TiledIteration = "0.5"
+TransformsBase = "1.6"
Unitful = "1.17"
julia = "1.9"
+
+[extensions]
+MeshesMakieExt = "Makie"
+
+[weakdeps]
+Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
diff --git a/docs/Project.toml b/docs/Project.toml
index ae8c85bbf..626819324 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -1,10 +1,9 @@
[deps]
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
+CoordRefSystems = "b46f11dc-f210-4604-bfba-323c1ec968cb"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
-DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8"
Meshes = "eacbb407-ea5a-433e-ab97-5258b1ca43fa"
PlyIO = "42171d58-473b-503a-8d5f-782019eb09ec"
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc"
-
-[compat]
-Documenter = "0.27"
+Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
diff --git a/docs/make.jl b/docs/make.jl
index 2fea7d09e..d5aa54458 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -1,39 +1,8 @@
using Documenter, Meshes
-using DocumenterTools: Themes
-
-istravis = "TRAVIS" ∈ keys(ENV)
-
-Themes.compile(
- joinpath(@__DIR__, "src/assets/light.scss"),
- joinpath(@__DIR__, "src/assets/themes/documenter-light.css")
-)
-Themes.compile(joinpath(@__DIR__, "src/assets/dark.scss"), joinpath(@__DIR__, "src/assets/themes/documenter-dark.css"))
makedocs(
- format=Documenter.HTML(
- assets=[
- "assets/favicon.ico",
- asset("https://fonts.googleapis.com/css?family=Montserrat|Source+Code+Pro&display=swap", class=:css)
- ],
- prettyurls=istravis,
- mathengine=KaTeX(
- Dict(
- :macros => Dict(
- "\\x" => "\\boldsymbol{x}",
- "\\z" => "\\boldsymbol{z}",
- "\\l" => "\\boldsymbol{\\lambda}",
- "\\c" => "\\boldsymbol{c}",
- "\\C" => "\\boldsymbol{C}",
- "\\g" => "\\boldsymbol{g}",
- "\\G" => "\\boldsymbol{G}",
- "\\f" => "\\boldsymbol{f}",
- "\\F" => "\\boldsymbol{F}",
- "\\R" => "\\mathbb{R}",
- "\\1" => "\\mathbb{1}"
- )
- )
- )
- ),
+ warnonly=[:missing_docs, :cross_references],
+ format=Documenter.HTML(prettyurls=get(ENV, "CI", nothing) == "true"),
sitename="Meshes.jl",
authors="Júlio Hoffimann and contributors",
pages=[
@@ -47,7 +16,9 @@ makedocs(
"algorithms/sampling.md",
"algorithms/partitioning.md",
"algorithms/discretization.md",
+ "algorithms/tesselation.md",
"algorithms/refinement.md",
+ "algorithms/coarsening.md",
"algorithms/simplification.md",
"algorithms/intersection.md",
"algorithms/clipping.md",
@@ -61,8 +32,10 @@ makedocs(
"algorithms/hulls.md"
],
"Transforms" => "transforms.md",
+ "Random" => "rand.md",
"Visualization" => "visualization.md",
- "Input/Output" => "io.md"
+ "Input/Output" => "io.md",
+ "Tolerances" => "tolerances.md"
],
"Contributing" => ["contributing/guidelines.md"],
"About" => ["License" => "about/license.md"],
diff --git a/docs/src/algorithms/boundingbox.md b/docs/src/algorithms/boundingbox.md
index 21d6e4fa6..08c960d79 100644
--- a/docs/src/algorithms/boundingbox.md
+++ b/docs/src/algorithms/boundingbox.md
@@ -2,6 +2,7 @@
```@example boundingbox
using Meshes # hide
+using CoordRefSystems # hide
import CairoMakie as Mke # hide
```
@@ -10,7 +11,7 @@ boundingbox
```
```@example boundingbox
-pset = PointSet(rand(Point2, 100))
+pset = PointSet(rand(Point, 100, crs=Cartesian2D))
bbox = boundingbox(pset)
fig = Mke.Figure(size = (800, 400))
diff --git a/docs/src/algorithms/clamping.md b/docs/src/algorithms/clamping.md
index 97f52e53e..86a17e0f2 100644
--- a/docs/src/algorithms/clamping.md
+++ b/docs/src/algorithms/clamping.md
@@ -3,16 +3,17 @@
Meshes adds methods to Julia's built-in `clamp` function. The additional methods clamp points to the edges of a box in any number of dimensions. The target points and boxes must have the same number of dimensions and the same numeric type.
```@docs
-clamp(point::Point{Dim,T}, box::Box{Dim,T}) where {Dim,T}
-clamp(points::PointSet{Dim,T}, box::Box{Dim,T}) where {Dim,T}
+clamp(::Point, ::Box)
+clamp(::PointSet, ::Box)
```
```@example clamping
using Meshes # hide
+using CoordRefSystems # hide
import CairoMakie as Mke # hide
# set of 2D points to clamp
-points = PointSet(rand(2, 100))
+points = PointSet(rand(Point, 100, crs=Cartesian2D))
# 2D box defining the clamping boundaries
box = Box((0.25, 0.25), (0.75, 0.75))
@@ -28,4 +29,4 @@ ax = Mke.Axis(fig[1,2], title="clamped", aspect=1, limits=(0,1,0,1))
viz!(ax, box)
viz!(ax, clamped, color=:black, pointsize=6)
fig
-```
\ No newline at end of file
+```
diff --git a/docs/src/algorithms/clipping.md b/docs/src/algorithms/clipping.md
index 434ef591b..c2655598f 100644
--- a/docs/src/algorithms/clipping.md
+++ b/docs/src/algorithms/clipping.md
@@ -10,10 +10,10 @@ clip
ClippingMethod
```
-## SutherlandHodgman
+## Sutherland-Hodgman
```@docs
-SutherlandHodgman
+SutherlandHodgmanClipping
```
```@example clipping
@@ -26,7 +26,7 @@ poly = PolyArea([outer, inner])
other = Box((0,1), (3,7))
# clipped polygon
-clipped = clip(poly, other, SutherlandHodgman())
+clipped = clip(poly, other, SutherlandHodgmanClipping())
viz(poly)
viz!(other, color = :black, alpha = 0.2)
diff --git a/docs/src/algorithms/coarsening.md b/docs/src/algorithms/coarsening.md
new file mode 100644
index 000000000..3f3ab1e32
--- /dev/null
+++ b/docs/src/algorithms/coarsening.md
@@ -0,0 +1,33 @@
+# Coarsening
+
+```@example coarsening
+using Meshes # hide
+import CairoMakie as Mke # hide
+```
+
+```@docs
+coarsen
+CoarseningMethod
+```
+
+## RegularCoarsening
+
+```@docs
+RegularCoarsening
+```
+
+```@example coarsening
+grid = CartesianGrid(100, 100)
+
+# refine three times
+cor1 = coarsen(grid, RegularCoarsening(2, 2))
+cor2 = coarsen(cor1, RegularCoarsening(3, 2))
+cor3 = coarsen(cor2, RegularCoarsening(2, 3))
+
+fig = Mke.Figure(size = (800, 800))
+viz(fig[1,1], grid, showsegments = true)
+viz(fig[1,2], cor1, showsegments = true)
+viz(fig[2,1], cor2, showsegments = true)
+viz(fig[2,2], cor3, showsegments = true)
+fig
+```
diff --git a/docs/src/algorithms/discretization.md b/docs/src/algorithms/discretization.md
index 31c538cf3..ffacec181 100644
--- a/docs/src/algorithms/discretization.md
+++ b/docs/src/algorithms/discretization.md
@@ -10,7 +10,6 @@ discretize
discretizewithin
simplexify
DiscretizationMethod
-BoundaryDiscretizationMethod
```
## FanTriangulation
@@ -27,30 +26,14 @@ mesh = discretize(hexagon, FanTriangulation())
fig = Mke.Figure(size = (800, 400))
viz(fig[1,1], hexagon)
-viz(fig[1,2], mesh, showfacets = true)
+viz(fig[1,2], mesh, showsegments = true)
fig
```
-## RegularDiscretization
-
-```@docs
-RegularDiscretization
-```
-
-```@example discretization
-sphere = Sphere((0.,0.,0.), 1.)
-
-mesh = discretize(sphere, RegularDiscretization(10,10))
-
-fig = Mke.Figure(size = (400, 400))
-viz(fig[1,1], mesh, showfacets = true)
-fig
-```
-
-## Dehn1899
+## DehnTriangulation
```@docs
-Dehn1899
+DehnTriangulation
```
```@example discretization
@@ -96,26 +79,41 @@ polyarea = PolyArea([(0.22926679, 0.47329807), (0.23094065, 0.44913536), (0.2569
(0.37951034, 0.31436795), (0.37547874, 0.30905423), (0.36070493, 0.3204269),
(0.33518887, 0.348486), (0.29893062, 0.3932315), (0.25193012, 0.45466346)])
-mesh = discretize(polyarea, Dehn1899())
+mesh = discretize(polyarea, DehnTriangulation())
fig = Mke.Figure(size = (800, 400))
viz(fig[1,1], polyarea)
-viz(fig[1,2], mesh, showfacets = true)
+viz(fig[1,2], mesh, showsegments = true)
fig
```
-## FIST
+## HeldTriangulation
```@docs
-FIST
+HeldTriangulation
```
```@example discretization
-mesh = discretize(polyarea, FIST())
+mesh = discretize(polyarea, HeldTriangulation())
fig = Mke.Figure(size = (800, 400))
viz(fig[1,1], polyarea)
-viz(fig[1,2], mesh, showfacets = true)
+viz(fig[1,2], mesh, showsegments = true)
+fig
+```
+
+## DelaunayTriangulation
+
+```@docs
+DelaunayTriangulation
+```
+
+```@example discretization
+mesh = discretize(polyarea, DelaunayTriangulation())
+
+fig = Mke.Figure(size = (800, 400))
+viz(fig[1,1], polyarea)
+viz(fig[1,2], mesh, showsegments = true)
fig
```
@@ -137,10 +135,38 @@ inners = [[(0.87789994, 0.32551613), (0.5614043, 0.540334), (0.9494598, 0.396227
polyarea = PolyArea([outer, inners...])
-mesh = discretize(polyarea, FIST())
+mesh = discretize(polyarea, DelaunayTriangulation())
fig = Mke.Figure(size = (800, 400))
viz(fig[1,1], polyarea)
-viz(fig[1,2], mesh, showfacets = true)
+viz(fig[1,2], mesh, showsegments = true)
fig
```
+
+## RegularDiscretization
+
+```@docs
+RegularDiscretization
+```
+
+```@example discretization
+sphere = Sphere((0.,0.,0.), 1.)
+
+mesh = discretize(sphere, RegularDiscretization(10,10))
+
+viz(mesh, showsegments = true)
+```
+
+## ManualSimplexification
+
+```@docs
+ManualSimplexification
+```
+
+```@example discretization
+box = Box((0., 0., 0.), (1., 1., 1.))
+
+mesh = discretize(box, ManualSimplexification())
+
+viz(mesh, colors = 1:nelements(mesh))
+```
diff --git a/docs/src/algorithms/hulls.md b/docs/src/algorithms/hulls.md
index 5fb9351f7..f56df606a 100644
--- a/docs/src/algorithms/hulls.md
+++ b/docs/src/algorithms/hulls.md
@@ -2,6 +2,7 @@
```@example hull
using Meshes # hide
+using CoordRefSystems # hide
import CairoMakie as Mke # hide
```
@@ -14,7 +15,7 @@ JarvisMarch
```
```@example hull
-pset = PointSet(rand(Point2, 100))
+pset = PointSet(rand(Point, 100, crs=Cartesian2D))
chul = convexhull(pset)
fig = Mke.Figure(size = (800, 400))
diff --git a/docs/src/algorithms/merging.md b/docs/src/algorithms/merging.md
index 524c6597b..e7d3d259c 100644
--- a/docs/src/algorithms/merging.md
+++ b/docs/src/algorithms/merging.md
@@ -1,5 +1,20 @@
# Merging
+Geometries and meshes can be [`merge`](@ref)d into a single
+geometric object as illustrated in the following example.
+The resulting type depends on the combination of input types,
+and can be a [`Mesh`](@ref) or [`Multi`](@ref) geometry.
+
```@docs
merge(::Mesh, ::Mesh)
-```
\ No newline at end of file
+```
+
+```@example merge
+using Meshes # hide
+import CairoMakie as Mke # hide
+
+g = CartesianGrid(2, 2)
+t = Triangle((3, 0), (4, 0), (3, 1))
+
+m = merge(g, t)
+```
diff --git a/docs/src/algorithms/neighborsearch.md b/docs/src/algorithms/neighborsearch.md
index 0ed36cd69..c77c0eecd 100644
--- a/docs/src/algorithms/neighborsearch.md
+++ b/docs/src/algorithms/neighborsearch.md
@@ -10,6 +10,7 @@ point of reference. This can be performed with search methods:
```@docs
NeighborSearchMethod
+BoundedNeighborSearchMethod
search
search!
searchdists
diff --git a/docs/src/algorithms/orientation.md b/docs/src/algorithms/orientation.md
index 818ee33f6..e854e6a0a 100644
--- a/docs/src/algorithms/orientation.md
+++ b/docs/src/algorithms/orientation.md
@@ -1,9 +1,42 @@
# Orientation
+```@example orientation
+using Meshes # hide
+import CairoMakie as Mke # hide
+```
+
+Many geometric processing algorithms for 2D geometries
+rely on the concept of [`orientation`](@ref), which is
+illustrated below.
+
```@docs
OrientationType
orientation
-OrientationMethod
-WindingOrientation
-TriangleOrientation
-```
\ No newline at end of file
+```
+
+For polygons without holes, the function returns the
+orientation of the boundary, which is a [`Ring`](@ref):
+
+```@example orientation
+tri = Triangle((0, 0), (1, 0), (0, 1))
+
+orientation(tri)
+```
+
+```@example orientation
+tri = Triangle((0, 0), (0, 1), (1, 0))
+
+orientation(tri)
+```
+
+For polygons with holes, the function returns a
+vector with the orientation of all constituent rings:
+
+```@example orientation
+outer = [(0, 0), (1, 0), (1, 1), (0, 1)]
+hole1 = [(0.2, 0.2), (0.2, 0.4), (0.4, 0.4), (0.4, 0.2)]
+hole2 = [(0.6, 0.2), (0.6, 0.4), (0.8, 0.4), (0.8, 0.2)]
+poly = PolyArea([outer, hole1, hole2])
+
+orientation(poly)
+```
diff --git a/docs/src/algorithms/refinement.md b/docs/src/algorithms/refinement.md
index 616754fee..ce6d43d92 100644
--- a/docs/src/algorithms/refinement.md
+++ b/docs/src/algorithms/refinement.md
@@ -25,10 +25,10 @@ ref2 = refine(ref1, TriRefinement())
ref3 = refine(ref2, TriRefinement())
fig = Mke.Figure(size = (800, 800))
-viz(fig[1,1], grid, showfacets = true)
-viz(fig[1,2], ref1, showfacets = true)
-viz(fig[2,1], ref2, showfacets = true)
-viz(fig[2,2], ref3, showfacets = true)
+viz(fig[1,1], grid, showsegments = true)
+viz(fig[1,2], ref1, showsegments = true)
+viz(fig[2,1], ref2, showsegments = true)
+viz(fig[2,2], ref3, showsegments = true)
fig
```
@@ -47,35 +47,57 @@ ref2 = refine(ref1, QuadRefinement())
ref3 = refine(ref2, QuadRefinement())
fig = Mke.Figure(size = (800, 800))
-viz(fig[1,1], grid, showfacets = true)
-viz(fig[1,2], ref1, showfacets = true)
-viz(fig[2,1], ref2, showfacets = true)
-viz(fig[2,2], ref3, showfacets = true)
+viz(fig[1,1], grid, showsegments = true)
+viz(fig[1,2], ref1, showsegments = true)
+viz(fig[2,1], ref2, showsegments = true)
+viz(fig[2,2], ref3, showsegments = true)
+fig
+```
+
+## RegularRefinement
+
+```@docs
+RegularRefinement
+```
+
+```@example refinement
+grid = CartesianGrid(10, 10)
+
+# refine three times
+ref1 = refine(grid, RegularRefinement(2, 2))
+ref2 = refine(ref1, RegularRefinement(3, 2))
+ref3 = refine(ref2, RegularRefinement(2, 3))
+
+fig = Mke.Figure(size = (800, 800))
+viz(fig[1,1], grid, showsegments = true)
+viz(fig[1,2], ref1, showsegments = true)
+viz(fig[2,1], ref2, showsegments = true)
+viz(fig[2,2], ref3, showsegments = true)
fig
```
## Catmull-Clark
```@docs
-CatmullClark
+CatmullClarkRefinement
```
```@example refinement
# define a cube in R^3
-points = Point3[(0,0,0),(1,0,0),(1,1,0),(0,1,0),(0,0,1),(1,0,1),(1,1,1),(0,1,1)]
+points = [(0,0,0),(1,0,0),(1,1,0),(0,1,0),(0,0,1),(1,0,1),(1,1,1),(0,1,1)]
connec = connect.([(1,4,3,2),(5,6,7,8),(1,2,6,5),(3,4,8,7),(1,5,8,4),(2,3,7,6)])
mesh = SimpleMesh(points, connec)
# refine three times
-ref1 = refine(mesh, CatmullClark())
-ref2 = refine(ref1, CatmullClark())
-ref3 = refine(ref2, CatmullClark())
+ref1 = refine(mesh, CatmullClarkRefinement())
+ref2 = refine(ref1, CatmullClarkRefinement())
+ref3 = refine(ref2, CatmullClarkRefinement())
fig = Mke.Figure(size = (800, 800))
-viz(fig[1,1], mesh, showfacets = true)
-viz(fig[1,2], ref1, showfacets = true)
-viz(fig[2,1], ref2, showfacets = true)
-viz(fig[2,2], ref3, showfacets = true)
+viz(fig[1,1], mesh, showsegments = true)
+viz(fig[1,2], ref1, showsegments = true)
+viz(fig[2,1], ref2, showsegments = true)
+viz(fig[2,2], ref3, showsegments = true)
fig
```
@@ -94,9 +116,9 @@ ref2 = refine(ref1, TriSubdivision())
ref3 = refine(ref2, TriSubdivision())
fig = Mke.Figure(size = (800, 800))
-viz(fig[1,1], grid, showfacets = true)
-viz(fig[1,2], ref1, showfacets = true)
-viz(fig[2,1], ref2, showfacets = true)
-viz(fig[2,2], ref3, showfacets = true)
+viz(fig[1,1], grid, showsegments = true)
+viz(fig[1,2], ref1, showsegments = true)
+viz(fig[2,1], ref2, showsegments = true)
+viz(fig[2,2], ref3, showsegments = true)
fig
```
diff --git a/docs/src/algorithms/sampling.md b/docs/src/algorithms/sampling.md
index 4a36ba0e7..c533eaeaf 100644
--- a/docs/src/algorithms/sampling.md
+++ b/docs/src/algorithms/sampling.md
@@ -6,7 +6,7 @@ import CairoMakie as Mke # hide
```
```@docs
-sample
+sample(::Any, ::SamplingMethod)
SamplingMethod
DiscreteSamplingMethod
ContinuousSamplingMethod
@@ -113,4 +113,19 @@ sampler = MinDistanceSampling(3.0)
points = sample(grid, sampler) |> collect
viz(points)
-```
\ No newline at end of file
+```
+
+### FibonacciSampling
+```@docs
+FibonacciSampling
+```
+
+```@example sampling
+sphere = Sphere((0.,0.,0.), 1.)
+
+# sample points using the Fibonacci lattice method
+sampler = FibonacciSampling(100)
+points = sample(sphere, sampler) |> collect
+
+viz(points)
+```
diff --git a/docs/src/algorithms/sideof.md b/docs/src/algorithms/sideof.md
index 7979dd62e..5f844b657 100644
--- a/docs/src/algorithms/sideof.md
+++ b/docs/src/algorithms/sideof.md
@@ -1,6 +1,24 @@
# Sideof
+The [`sideof`](@ref) function can be used to efficiently query the side
+of multiple points with respect to a given geometry or mesh.
+
```@docs
SideType
-sideof
-```
\ No newline at end of file
+sideof(::Point, ::Line)
+sideof(::Point, ::Ring)
+sideof(::Point, ::Mesh)
+```
+
+```@example sideof
+using Meshes # hide
+
+sideof(Point(0, 0), Line((1, 0), (1, 1)))
+```
+
+```@example sideof
+points = [Point(0, 0), Point(0.2, 0.2), Point(2, 1)]
+polygon = Triangle((0, 0), (1, 0), (0, 1))
+
+sideof(points, boundary(polygon))
+```
diff --git a/docs/src/algorithms/simplification.md b/docs/src/algorithms/simplification.md
index 8a9640bdb..49448d195 100644
--- a/docs/src/algorithms/simplification.md
+++ b/docs/src/algorithms/simplification.md
@@ -7,86 +7,103 @@ import CairoMakie as Mke # hide
```@docs
simplify
-decimate
SimplificationMethod
```
-## Douglas-Peucker
+## SelingerSimplification
```@docs
-DouglasPeucker
+SelingerSimplification
```
```@example simplification
-# polygonal area
-polyarea = PolyArea([(0.22926679, 0.47329807), (0.23094065, 0.44913536), (0.2569517, 0.38217533),
- (0.3072999, 0.272418), (0.34814754, 0.18421611), (0.37949452, 0.11756973),
- (0.4013409, 0.07247882), (0.41368666, 0.048943404), (0.42597583, 0.031655528),
- (0.4382084, 0.0206152), (0.45038435, 0.015822414), (0.4625037, 0.017277176),
- (0.47175184, 0.02439156), (0.47812873, 0.03716557), (0.4816344, 0.055599205),
- (0.48226887, 0.07969247), (0.48172843, 0.10446181), (0.4800131, 0.12990724),
- (0.47712287, 0.15602873), (0.47305775, 0.18282633), (0.47093934, 0.20558843),
- (0.47076762, 0.22431506), (0.47254258, 0.23900622), (0.47626427, 0.24966191),
- (0.47768936, 0.25845313), (0.47681788, 0.26537988), (0.4736498, 0.27044216),
- (0.46818516, 0.27363995), (0.4613889, 0.27232954), (0.45326096, 0.2665109),
- (0.44380143, 0.256184), (0.43301025, 0.24134888), (0.4246466, 0.22978415),
- (0.41871038, 0.22148979), (0.4152017, 0.21646582), (0.4141205, 0.21471222),
- (0.41227448, 0.21589448), (0.40966362, 0.22001258), (0.40628797, 0.22706655),
- (0.40214747, 0.23705636), (0.40200475, 0.24653101), (0.40585983, 0.25549048),
- (0.41371268, 0.2639348), (0.4255633, 0.2718639), (0.4378565, 0.28495985),
- (0.4505922, 0.30322257), (0.46377045, 0.32665208), (0.47739124, 0.35524836),
- (0.5046394, 0.36442512), (0.5455148, 0.35418236), (0.60001767, 0.32452005),
- (0.66814786, 0.27543822), (0.7186763, 0.24664374), (0.75160307, 0.23813659),
- (0.76692814, 0.2499168), (0.7646515, 0.28198436), (0.7769703, 0.29925033),
- (0.8038847, 0.3017147), (0.84539455, 0.28937748), (0.9015, 0.26223865),
- (0.94408435, 0.24899776), (0.9731477, 0.24965483), (0.98869, 0.26420987),
- (0.9907113, 0.29266283), (0.9849871, 0.31338844), (0.97151726, 0.32638666),
- (0.950302, 0.3316575), (0.9213412, 0.32920095), (0.8798396, 0.34078467),
- (0.8257972, 0.36640862), (0.7592141, 0.40607283), (0.6800901, 0.4597773),
- (0.6450007, 0.49104902), (0.6539457, 0.49988794), (0.7069251, 0.48629412),
- (0.803939, 0.45026752), (0.877913, 0.4226481), (0.9288472, 0.40343583),
- (0.9567415, 0.39263073), (0.961596, 0.39023277), (0.9419039, 0.40523484),
- (0.89766514, 0.43763688), (0.8288798, 0.48743892), (0.7355478, 0.55464095),
- (0.6655121, 0.60063523), (0.6187727, 0.6254217), (0.5953296, 0.62900037),
- (0.5951828, 0.6113712), (0.57516366, 0.60261106), (0.53527224, 0.6027198),
- (0.4755085, 0.6116975), (0.3958725, 0.6295441), (0.33913234, 0.6398651),
- (0.30528808, 0.6426605), (0.2943397, 0.6379303), (0.30628717, 0.6256744),
- (0.32149008, 0.6093727), (0.33994842, 0.5890249), (0.36166218, 0.5646312),
- (0.38663134, 0.5361916), (0.3919681, 0.520893), (0.3776725, 0.5187355),
- (0.34374446, 0.52971905), (0.29018405, 0.5538437), (0.25439468, 0.5678829),
- (0.2363764, 0.5718367), (0.23612918, 0.56570506), (0.25365302, 0.549488),
- (0.2733971, 0.5246488), (0.29536137, 0.49118724), (0.3195459, 0.4491035),
- (0.34595063, 0.39839754), (0.3647463, 0.3590396), (0.37593287, 0.33102974),
- (0.37951034, 0.31436795), (0.37547874, 0.30905423), (0.36070493, 0.3204269),
- (0.33518887, 0.348486), (0.29893062, 0.3932315), (0.25193012, 0.45466346)])
+poly = PolyArea([(0.22926679, 0.47329807), (0.23094065, 0.44913536), (0.2569517, 0.38217533),
+ (0.3072999, 0.272418), (0.34814754, 0.18421611), (0.37949452, 0.11756973),
+ (0.4013409, 0.07247882), (0.41368666, 0.048943404), (0.42597583, 0.031655528),
+ (0.4382084, 0.0206152), (0.45038435, 0.015822414), (0.4625037, 0.017277176),
+ (0.47175184, 0.02439156), (0.47812873, 0.03716557), (0.4816344, 0.055599205),
+ (0.48226887, 0.07969247), (0.48172843, 0.10446181), (0.4800131, 0.12990724),
+ (0.47712287, 0.15602873), (0.47305775, 0.18282633), (0.47093934, 0.20558843),
+ (0.47076762, 0.22431506), (0.47254258, 0.23900622), (0.47626427, 0.24966191),
+ (0.47768936, 0.25845313), (0.47681788, 0.26537988), (0.4736498, 0.27044216),
+ (0.46818516, 0.27363995), (0.4613889, 0.27232954), (0.45326096, 0.2665109),
+ (0.44380143, 0.256184), (0.43301025, 0.24134888), (0.4246466, 0.22978415),
+ (0.41871038, 0.22148979), (0.4152017, 0.21646582), (0.4141205, 0.21471222),
+ (0.41227448, 0.21589448), (0.40966362, 0.22001258), (0.40628797, 0.22706655),
+ (0.40214747, 0.23705636), (0.40200475, 0.24653101), (0.40585983, 0.25549048),
+ (0.41371268, 0.2639348), (0.4255633, 0.2718639), (0.4378565, 0.28495985),
+ (0.4505922, 0.30322257), (0.46377045, 0.32665208), (0.47739124, 0.35524836),
+ (0.5046394, 0.36442512), (0.5455148, 0.35418236), (0.60001767, 0.32452005),
+ (0.66814786, 0.27543822), (0.7186763, 0.24664374), (0.75160307, 0.23813659),
+ (0.76692814, 0.2499168), (0.7646515, 0.28198436), (0.7769703, 0.29925033),
+ (0.8038847, 0.3017147), (0.84539455, 0.28937748), (0.9015, 0.26223865),
+ (0.94408435, 0.24899776), (0.9731477, 0.24965483), (0.98869, 0.26420987),
+ (0.9907113, 0.29266283), (0.9849871, 0.31338844), (0.97151726, 0.32638666),
+ (0.950302, 0.3316575), (0.9213412, 0.32920095), (0.8798396, 0.34078467),
+ (0.8257972, 0.36640862), (0.7592141, 0.40607283), (0.6800901, 0.4597773),
+ (0.6450007, 0.49104902), (0.6539457, 0.49988794), (0.7069251, 0.48629412),
+ (0.803939, 0.45026752), (0.877913, 0.4226481), (0.9288472, 0.40343583),
+ (0.9567415, 0.39263073), (0.961596, 0.39023277), (0.9419039, 0.40523484),
+ (0.89766514, 0.43763688), (0.8288798, 0.48743892), (0.7355478, 0.55464095),
+ (0.6655121, 0.60063523), (0.6187727, 0.6254217), (0.5953296, 0.62900037),
+ (0.5951828, 0.6113712), (0.57516366, 0.60261106), (0.53527224, 0.6027198),
+ (0.4755085, 0.6116975), (0.3958725, 0.6295441), (0.33913234, 0.6398651),
+ (0.30528808, 0.6426605), (0.2943397, 0.6379303), (0.30628717, 0.6256744),
+ (0.32149008, 0.6093727), (0.33994842, 0.5890249), (0.36166218, 0.5646312),
+ (0.38663134, 0.5361916), (0.3919681, 0.520893), (0.3776725, 0.5187355),
+ (0.34374446, 0.52971905), (0.29018405, 0.5538437), (0.25439468, 0.5678829),
+ (0.2363764, 0.5718367), (0.23612918, 0.56570506), (0.25365302, 0.549488),
+ (0.2733971, 0.5246488), (0.29536137, 0.49118724), (0.3195459, 0.4491035),
+ (0.34595063, 0.39839754), (0.3647463, 0.3590396), (0.37593287, 0.33102974),
+ (0.37951034, 0.31436795), (0.37547874, 0.30905423), (0.36070493, 0.3204269),
+ (0.33518887, 0.348486), (0.29893062, 0.3932315), (0.25193012, 0.45466346)])
-simp1 = simplify(polyarea, DouglasPeucker(0.01))
-simp2 = simplify(polyarea, DouglasPeucker(0.05))
-simp3 = simplify(polyarea, DouglasPeucker(0.10))
+simp1 = simplify(poly, SelingerSimplification(0.01))
+simp2 = simplify(poly, SelingerSimplification(0.05))
+simp3 = simplify(poly, SelingerSimplification(0.10))
fig = Mke.Figure(size = (800, 800))
-viz(fig[1,1], polyarea)
+viz(fig[1,1], poly)
viz(fig[1,2], simp1)
viz(fig[2,1], simp2)
viz(fig[2,2], simp3)
fig
```
-## Selinger
+## DouglasPeuckerSimplification
```@docs
-Selinger
+DouglasPeuckerSimplification
```
```@example simplification
-simp1 = simplify(polyarea, Selinger(0.01))
-simp2 = simplify(polyarea, Selinger(0.05))
-simp3 = simplify(polyarea, Selinger(0.10))
+simp1 = simplify(poly, DouglasPeuckerSimplification(0.01))
+simp2 = simplify(poly, DouglasPeuckerSimplification(0.05))
+simp3 = simplify(poly, DouglasPeuckerSimplification(0.10))
fig = Mke.Figure(size = (800, 800))
-viz(fig[1,1], polyarea)
+viz(fig[1,1], poly)
viz(fig[1,2], simp1)
viz(fig[2,1], simp2)
viz(fig[2,2], simp3)
fig
-```
\ No newline at end of file
+```
+
+## MinMaxSimplification
+
+```@docs
+MinMaxSimplification
+```
+
+```@example simplification
+simp1 = simplify(poly, MinMaxSimplification(DouglasPeuckerSimplification, max=20))
+simp2 = simplify(poly, MinMaxSimplification(DouglasPeuckerSimplification, max=10))
+simp3 = simplify(poly, MinMaxSimplification(DouglasPeuckerSimplification, max=5))
+
+fig = Mke.Figure(size = (800, 800))
+viz(fig[1,1], poly)
+viz(fig[1,2], simp1)
+viz(fig[2,1], simp2)
+viz(fig[2,2], simp3)
+fig
+```
diff --git a/docs/src/algorithms/tesselation.md b/docs/src/algorithms/tesselation.md
new file mode 100644
index 000000000..383cb8598
--- /dev/null
+++ b/docs/src/algorithms/tesselation.md
@@ -0,0 +1,44 @@
+# Tesselation
+
+```@example tesselation
+using Meshes # hide
+using CoordRefSystems # hide
+import CairoMakie as Mke # hide
+```
+
+```@docs
+tesselate
+TesselationMethod
+```
+
+## DelaunayTesselation
+
+```@docs
+DelaunayTesselation
+```
+
+```@example tesselation
+points = rand(Point, 100, crs=Cartesian2D)
+
+mesh = tesselate(points, DelaunayTesselation())
+
+viz(mesh, showsegments = true)
+viz!(points, color = :red)
+Mke.current_figure()
+```
+
+## VoronoiTesselation
+
+```@docs
+VoronoiTesselation
+```
+
+```@example tesselation
+points = rand(Point, 100, crs=Cartesian2D)
+
+mesh = tesselate(points, VoronoiTesselation())
+
+viz(mesh, showsegments = true)
+viz!(points, color = :red)
+Mke.current_figure()
+```
diff --git a/docs/src/algorithms/winding.md b/docs/src/algorithms/winding.md
index 6c27587d6..a8d9c95ee 100644
--- a/docs/src/algorithms/winding.md
+++ b/docs/src/algorithms/winding.md
@@ -1,5 +1,17 @@
# Winding
+The [`winding`](@ref) number is intimately connected to the
+[`sideof`](@ref) function, which is used more often in applications.
+
```@docs
winding
-```
\ No newline at end of file
+```
+
+```@example winding
+using Meshes # hide
+
+points = [Point(0, 0), Point(0.2, 0.2), Point(2, 1)]
+polygon = Triangle((0, 0), (1, 0), (0, 1))
+
+winding(points, boundary(polygon))
+```
diff --git a/docs/src/assets/dark.scss b/docs/src/assets/dark.scss
deleted file mode 100644
index 37236360c..000000000
--- a/docs/src/assets/dark.scss
+++ /dev/null
@@ -1,155 +0,0 @@
-@charset "UTF-8";
-// The customizable variables can be found here:
-// https://github.com/JuliaDocs/Documenter.jl/tree/master/assets/html/scss
-// under documenter/_variables or documenter/_overrides. But some stuff are Bulma defaults
-// as well, so you may need to look them up too: https://bulma.io/documentation/customize/variables/
-
-
-////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////
-// These define the template:
-$maincolor: rgb(78, 134, 151); // main color of the org theme
-$secondcolor: rgb(197, 96, 255); // secondary color that serves as accents
- // it is used as-is for links in light theme
-$mainwhite: #fff; // color representing white
-$mainblack: #202020; // color representing black
-$darkbg: #1e1e20; // dark theme main page background
-
-// These commands set up the fonts for the main text and code blocks.
-// the fonts must be included into the assets of the `makdocs` command, with e.g.
-// format = Documenter.HTML(
-// prettyurls = CI,
-// assets = [
-// "assets/logo.ico",
-// asset("https://fonts.googleapis.com/css?family=Montserrat|Source+Code+Pro&display=swap", class=:css),
-// ],
-// ),
-$family-sans-serif: 'Montserrat', sans-serif;
-$family-monospace: 'Source Code Pro', monospace;
-////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////
-
-// variables controlling the siderbar's shadow
-$shadow-color: #bbb !default;
-$shadow-size: 0.2rem !default;
-$shadow-blur: 0.4rem !default;
-
-// Two cool helper functions:
-$lightness-unit: 8% !default;
-// Uses adjust-color to create a darker version of $color
-@function darken-color($color, $factor) {
- @return adjust-color($color, $lightness: -$factor*$lightness-unit);
-}
-// Uses adjust-color to create a lighter version of $color
-@function lighten-color($color, $factor) {
- @return adjust-color($color, $lightness: $factor*$lightness-unit);
-}
-// This template file overrides some of the Documenter theme variables to customize the theme:
-$themename: "documenter-dark"; // CSS file must be called `$(themename).css`
-// Instruct documenter/*.scss files that this is a dark theme
-$documenter-is-dark-theme: true;
-
-$boldcolor: lighten-color($maincolor, 4.5);
-
-$body-background-color: $darkbg; // main page background
-
-// this is the color the links get, and also when they are hovered
-$link: lighten-color($secondcolor, 2);
-$link-hover: lighten-color($link, 3);
-
-// Main text color:
-$text: darken-color($mainwhite, 0.2);
-// Bold text color, also affects headers
-$text-strong: $boldcolor;
-
-// Code text color:
-$code: #fff;
-//$code-background: rgba(0.5,0,0, 0.05);
-$codebg: darken-color($maincolor, 3);
-$code-background: $codebg; // for inline code
-$pre-background: $codebg; // for code blocks
-$documenter-docstring-header-background: lighten-color($body-background-color, 0.5);
-
-// Sidebar
-$documenter-sidebar-background: darken-color($maincolor, 1.2); //background color for sidebar
-$documenter-sidebar-color: $text; //font color for sidebar
-$documenter-sidebar-menu-hover-color: $documenter-sidebar-color;
-$documenter-sidebar-menu-hover-background: darken-color($documenter-sidebar-background, 1.2);
-
-$documenter-sidebar-menu-active-background: $darkbg;
-$documenter-sidebar-menu-active-color: $mainwhite;
-$documenter-sidebar-menu-active-hover-background: darken-color($documenter-sidebar-background, 1);
-$documenter-sidebar-menu-active-hover-color: $documenter-sidebar-menu-active-color;
-// these two change what happens with input boxes (the search box):
-$input-hover-border-color: $secondcolor;
-$input-focus-border-color: $mainwhite;
-
-$documenter-docstring-shadow: 3px 3px 4px invert($shadow-color);
-
-// Admonition stuff
-$admbg: lighten-color($body-background-color, 0.5);
-$admonition-background: (
- 'default': $admbg, 'info': $admbg, 'success': $admbg, 'warning': $admbg,
- 'danger': $admbg, 'compat': $admbg
-);
-$admonition-header-background: (
- 'default': #ba3f1f, 'warning': #a88b17, 'danger': #c7524c,
- 'success': #42ac68, 'info': #28c);
-
-// All secondary themes have to be nested in a theme--$(themename) class. When Documenter
-// switches themes, it applies this class to and then disables the primary
-// stylesheet.
-@import "documenter/utilities";
-@import "documenter/variables";
-@import "bulma/utilities/all";
-@import "bulma/base/minireset.sass";
-@import "bulma/base/helpers.sass";
-
-html.theme--#{$themename} {
- @import "bulma/base/generic.sass";
-
- @import "documenter/overrides";
-
- @import "bulma/elements/all";
- @import "bulma/form/all";
- @import "bulma/components/all";
- @import "bulma/grid/all";
- @import "bulma/layout/all";
-
- // Additional overrides, if need be
-
- @import "documenter/elements";
- @import "documenter/components/all";
- @import "documenter/patches";
- @import "documenter/layout/all";
-
- @import "documenter/theme_overrides";
-
- // $shadow-color: #202224;
-
- #documenter .docs-sidebar { // This makes sidebar have shadow at all displays
- border-right: none;
- box-shadow: 1.2*$shadow-size 0rem 1*$shadow-blur invert($shadow-color);
-
- form.docs-search > input { // these controls are for the searchbar
- color: $mainwhite;
- background-color: darken-color($documenter-sidebar-background, 1);
- border-color: darken-color($documenter-sidebar-background, 2);
- margin-top: 1.0rem;
- margin-bottom: 1.0rem; // adjust the margins between search and other elements
- &::placeholder {
- color: $mainwhite; // placeholder text color ("Search here...")
- }
- }
- }
- // FIXME: Hack to get a proper theme for highlight.js in the dark theme
- @import "highlightjs/a11y-dark";
- // Also, a11y-dark does not highlight string interpolation properly.
- .hljs-subst {
- color: #f8f8f2;
- }
-}
-
-#documenter .admonition-header { // Color of notes
- background-color: $maincolor;
-}
diff --git a/docs/src/assets/light.scss b/docs/src/assets/light.scss
deleted file mode 100644
index 1efed7db3..000000000
--- a/docs/src/assets/light.scss
+++ /dev/null
@@ -1,121 +0,0 @@
-@charset "UTF-8";
-// The customizable variables can be found here:
-// https://github.com/JuliaDocs/Documenter.jl/tree/master/assets/html/scss
-// under documenter/_variables or documenter/_overrides. But some stuff are Bulma defaults
-// as well, so you may need to look them up too: https://bulma.io/documentation/customize/variables/
-
-
-////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////
-// These define the template:
-$maincolor: rgb(78, 134, 151); // main color of the org theme
-$secondcolor: rgb(48, 23, 140); // secondary color that serves as accents
- // it is used as-is for links in light theme
-$mainwhite: #fff; // color representing white
-$mainblack: #202020; // color representing black
-$darkbg: #1e1e20; // dark theme main page background
-
-// These commands set up the fonts for the main text and code blocks.
-// the fonts must be included into the assets of the `makdocs` command, with e.g.
-// format = Documenter.HTML(
-// prettyurls = CI,
-// assets = [
-// "assets/logo.ico",
-// asset("https://fonts.googleapis.com/css?family=Montserrat|Source+Code+Pro&display=swap", class=:css),
-// ],
-// ),
-$family-sans-serif: 'Montserrat', sans-serif;
-$family-monospace: 'Source Code Pro', monospace;
-////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////
-
-// variables controlling the siderbar's shadow
-$shadow-color: #bbb !default;
-$shadow-size: 0.2rem !default;
-$shadow-blur: 0.4rem !default;
-
-// Two cool helper functions:
-$lightness-unit: 8% !default;
-// Uses adjust-color to create a darker version of $color
-@function darken-color($color, $factor) {
- @return adjust-color($color, $lightness: -$factor*$lightness-unit);
-}
-// Uses adjust-color to create a lighter version of $color
-@function lighten-color($color, $factor) {
- @return adjust-color($color, $lightness: $factor*$lightness-unit);
-}
-// This template file overrides some of the Documenter theme variables to customize the theme:
-$themename: "documenter-light"; // CSS file must be called `$(themename).css`
-
-$boldcolor: $maincolor; // darken-color($maincolor, 1);
-
-$body-background-color: $mainwhite; // main page background
-
-// Sidebar
-$documenter-sidebar-background: $maincolor; //background color for sidebar
-$documenter-sidebar-color: $mainwhite; //font color all sidebar
-$documenter-sidebar-menu-hover-color: $documenter-sidebar-color; // text color
-$documenter-sidebar-menu-hover-background: darken-color($documenter-sidebar-background, 1.5);
-
-$documenter-sidebar-menu-active-background: $mainwhite;
-$documenter-sidebar-menu-active-color: $mainblack;
-$documenter-sidebar-menu-active-hover-background: lighten-color($documenter-sidebar-background, 5.2);
-$documenter-sidebar-menu-active-hover-color: $documenter-sidebar-menu-active-color;
-
-// this is the color the links get, and also when they are hovered
-$link: $secondcolor;
-$link-hover: darken-color($link, 1);
-
-// Main text color:
-$text: $mainblack;
-// Bold text color, also affects headers
-$text-strong: $boldcolor;
-
-// Section headers (markdown ###) text color:
-// TODO
-
-// Code text color:
-$code: #000000;
-//$code-background: rgba(0.5,0,0, 0.05);
-$codebg: lighten-color($maincolor, 6.2);
-// $codebg: lighten-color($secondcolor, 8);
-$code-background: $codebg; // for inline code
-$pre-background: $codebg; // for code blocks
-$documenter-docstring-header-background: darken-color($body-background-color, 0.4);
-
-// main text font size
-$body-font-size: 1.0em; // the default is 1.0
-// Sidebar text font size
-$documenter-sidebar-size: 1.0em; // the default is 1.0 as well
-
-// these two change what happens with input boxes (the search box):
-$input-hover-border-color: $secondcolor;
-$input-focus-border-color: $mainwhite;
-
-// Include the original theme which will now use the updated variables.
-// This should be the last thing in the SCSS file.
-@import "documenter-light";
-
-#documenter .docs-sidebar { // This makes sidebar have shadow at all displays
- border-right: none;
- box-shadow: 1.2*$shadow-size 0rem 1*$shadow-blur $shadow-color;
-
- form.docs-search > input { // these controls are for the searchbar
- color: $mainwhite;
- background-color: darken-color($documenter-sidebar-background, 1);
- border-color: darken-color($documenter-sidebar-background, 2);
- margin-top: 1.0rem;
- margin-bottom: 1.0rem; // adjust the margins between search and other elements
- &::placeholder {
- color: $mainwhite; // placeholder text color ("Search here...")
- }
- }
-}
-
-#documenter .content p { // Justify text in paragraphs
- text-align: justify;
-}
-
-#documenter .admonition-header { // Color of notes
- background-color: $maincolor;
-}
diff --git a/docs/src/domains/meshes.md b/docs/src/domains/meshes.md
index b1c4a2053..b58606fc3 100644
--- a/docs/src/domains/meshes.md
+++ b/docs/src/domains/meshes.md
@@ -2,6 +2,7 @@
```@example meshes
using Meshes # hide
+using CoordRefSystems # hide
import CairoMakie as Mke # hide
```
@@ -15,6 +16,17 @@ Mesh
Grid
```
+```@docs
+RegularGrid
+```
+
+```@example meshes
+# 2D regular grid
+grid = RegularGrid((8, 8), Point(Polar(0, 0)), (1, π/4))
+
+viz(grid, showsegments = true)
+```
+
```@docs
CartesianGrid
```
@@ -23,7 +35,7 @@ CartesianGrid
# 3D Cartesian grid
grid = CartesianGrid(10, 10, 10)
-viz(grid, showfacets = true)
+viz(grid, showsegments = true)
```
```@docs
@@ -36,7 +48,7 @@ x = 0.0:0.2:1.0
y = [0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
grid = RectilinearGrid(x, y)
-viz(grid, showfacets = true)
+viz(grid, showsegments = true)
```
```@docs
@@ -49,7 +61,7 @@ X = [i/20 * cos(3π/2 * (j-1) / (30-1)) for i in 1:20, j in 1:30]
Y = [i/20 * sin(3π/2 * (j-1) / (30-1)) for i in 1:20, j in 1:30]
grid = StructuredGrid(X, Y)
-viz(grid, showfacets = true)
+viz(grid, showsegments = true)
```
```@docs
@@ -58,7 +70,7 @@ SimpleMesh
```@example meshes
# global vector of 2D points
-points = Point2[(0,0),(1,0),(0,1),(1,1),(0.25,0.5),(0.75,0.5)]
+points = [(0,0),(1,0),(0,1),(1,1),(0.25,0.5),(0.75,0.5)]
# connect the points into N-gon
connec = connect.([(1,2,6,5),(2,4,6),(4,3,5,6),(3,1,5)], Ngon)
@@ -66,7 +78,7 @@ connec = connect.([(1,2,6,5),(2,4,6),(4,3,5,6),(3,1,5)], Ngon)
# 2D mesh made of N-gon elements
mesh = SimpleMesh(points, connec)
-viz(mesh, showfacets = true)
+viz(mesh, showsegments = true)
```
## Connectivities
@@ -95,11 +107,12 @@ Coboundary
Adjacency
```
-### Examples
+Consider the following examples with the [`Boundary`](@ref) and
+[`Coboundary`](@ref) relations defined for the [`HalfEdgeTopology`](@ref):
```@example meshes
# global vector of 2D points
-points = Point2[(0,0),(1,0),(0,1),(1,1),(0.25,0.5),(0.75,0.5)]
+points = [(0,0),(1,0),(0,1),(1,1),(0.25,0.5),(0.75,0.5)]
# connect the points into N-gon
connec = connect.([(1,2,6,5),(2,4,6),(4,3,5,6),(3,1,5)], Ngon)
@@ -126,3 +139,65 @@ topo = convert(HalfEdgeTopology, topology(mesh))
# show n-gons that share edge 3
𝒞₁₂(3)
```
+
+## Matrices
+
+Based on topological relations, we can extract matrices that
+are widely used in applications such as [`laplacematrix`](@ref),
+and [`adjacencymatrix`](@ref).
+
+### Laplace
+
+```@docs
+laplacematrix
+```
+
+```@example meshes
+grid = CartesianGrid(10, 10)
+
+laplacematrix(grid, kind = :uniform)
+```
+
+```@example meshes
+points = [(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
+connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)])
+mesh = SimpleMesh(points, connec)
+
+laplacematrix(mesh, kind = :cotangent)
+```
+
+### Measure
+
+```@docs
+measurematrix
+```
+
+```@example meshes
+grid = CartesianGrid(10, 10)
+
+measurematrix(grid)
+```
+
+```@example meshes
+points = [(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
+connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)])
+mesh = SimpleMesh(points, connec)
+
+measurematrix(mesh)
+```
+
+### Adjacency
+
+```@docs
+adjacencymatrix
+```
+
+```@example meshes
+grid = CartesianGrid(10, 10)
+
+adjacencymatrix(grid)
+```
+
+```@example meshes
+adjacencymatrix(grid, rank = 0)
+```
diff --git a/docs/src/domains/sets.md b/docs/src/domains/sets.md
index ded5521fe..27e83fd68 100644
--- a/docs/src/domains/sets.md
+++ b/docs/src/domains/sets.md
@@ -2,6 +2,7 @@
```@example sets
using Meshes # hide
+using CoordRefSystems # hide
import CairoMakie as Mke # hide
```
@@ -13,7 +14,7 @@ GeometrySet
```
```@example sets
-GeometrySet(rand(Ball{3,Float64}, 3)) |> viz
+GeometrySet(rand(Ball, 3)) |> viz
```
```@docs
@@ -21,5 +22,5 @@ PointSet
```
```@example sets
-PointSet(rand(Point2, 100)) |> viz
+PointSet(rand(Point, 100, crs=Cartesian2D)) |> viz
```
\ No newline at end of file
diff --git a/docs/src/geometries/polytopes.md b/docs/src/geometries/polytopes.md
index c921e28a2..398a816d9 100644
--- a/docs/src/geometries/polytopes.md
+++ b/docs/src/geometries/polytopes.md
@@ -63,9 +63,9 @@ PolyArea
```
```@example polytopes
-outer = [(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)]
-hole1 = [(0.2,0.2),(0.4,0.2),(0.4,0.4),(0.2,0.4)]
-hole2 = [(0.6,0.2),(0.8,0.2),(0.8,0.4),(0.6,0.4)]
+outer = [(0, 0), (1, 0), (1, 1), (0, 1)]
+hole1 = [(0.2, 0.2), (0.2, 0.4), (0.4, 0.4), (0.4, 0.2)]
+hole2 = [(0.6, 0.2), (0.6, 0.4), (0.8, 0.4), (0.8, 0.2)]
poly = PolyArea([outer, hole1, hole2]) |> viz
```
diff --git a/docs/src/geometries/primitives.md b/docs/src/geometries/primitives.md
index 8f6420542..194948872 100644
--- a/docs/src/geometries/primitives.md
+++ b/docs/src/geometries/primitives.md
@@ -20,11 +20,11 @@ Point
```
```@example primitives
-rand(Point3, 100) |> viz
+rand(Point, 100) |> viz
```
```@docs
-coordinates(::Point)
+to(::Point)
-(::Point, ::Point)
+(::Point, ::Vec)
-(::Point, ::Vec)
@@ -52,6 +52,16 @@ BezierCurve
BezierCurve((0.,0.), (1.,0.), (1.,1.)) |> viz
```
+### ParametrizedCurve
+
+```@docs
+ParametrizedCurve
+```
+
+```@example primitives
+ParametrizedCurve(t -> Point(cos(t), sin(t), 0.2t), (0, 4π)) |> viz
+```
+
### Plane
```@docs
@@ -68,80 +78,68 @@ Box
Box((0.,0.,0.), (1.,1.,1.)) |> viz
```
-### Ball
+### Ball/Sphere
```@docs
Ball
+Sphere
```
```@example primitives
Ball((0.,0.,0.), 1.) |> viz
```
-### Sphere
+### Ellipsoid
```@docs
-Sphere
+Ellipsoid
```
```@example primitives
-Sphere((0.,0.,0.), 1.) |> viz
+Ellipsoid((3., 2., 1.)) |> viz
```
-### Disk
+### Disk/Circle
```@docs
Disk
-```
-
-### Circle
-
-```@docs
Circle
```
-### Cylinder
+### Cylinder/CylinderSurface
```@docs
Cylinder
-```
-
-```@example primitives
-Cylinder(1.0) |> viz
-```
-
-### CylinderSurface
-
-```@docs
CylinderSurface
```
```@example primitives
-CylinderSurface(1.0) |> viz
+Cylinder(1.0) |> viz
```
-### Cone
+### Cone/ConeSurface
```@docs
Cone
+ConeSurface
```
-### ConeSurface
-
-```@docs
-ConeSurface
+```@example primitives
+Cone(Disk(Plane((0,0,0), (0,0,1)), 1), (0,0,1)) |> viz
```
-### Frustum
+### Frustum/FrustumSurface
```@docs
Frustum
+FrustumSurface
```
-### FrustumSurface
-
-```@docs
-FrustumSurface
+```@example primitives
+Frustum(
+ Disk(Plane((0,0,0), (0,0,1)), 2),
+ Disk(Plane((0,0,10), (0,0,1)), 1)
+) |> viz
```
### Torus
@@ -161,10 +159,5 @@ ParaboloidSurface
```
```@example primitives
-a = Point3(5, 2, 4)
-r = 1.0
-f = 0.25
-par = ParaboloidSurface(a, r, f)
-disk = Disk(Plane(a, Vec(0, 0, 1)), r)
-viz([par, disk], color = [:green, :gray])
+ParaboloidSurface((5., 2., 4.), 1.0, 0.25) |> viz
```
diff --git a/docs/src/index.md b/docs/src/index.md
index 1189e265b..fb8a3e27f 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -13,8 +13,8 @@
[Meshes.jl](https://github.com/JuliaGeometry/Meshes.jl) provides efficient
implementations of concepts from computational geometry. It promotes rigorous
mathematical definitions of spatial discretizations (a.k.a. meshes) that are
-adequate for describing general manifolds embedded in $\R^n$, including surfaces
-described with spherical coordinates, and geometries described with multiple
+adequate for describing general manifolds embedded in $\mathbb{R}^3$, including
+surfaces described with spherical coordinates, and geometries described with multiple
coordinate reference systems.
Unlike other existing efforts in the Julia ecosystem, this project is being carefully
@@ -32,13 +32,9 @@ for finite element analysis (e.g. [JuAFEM.jl](https://kristofferc.github.io/JuAF
experience with mesh representations that are adequate for finite finite element analysis,
advanced geospatial modeling *and* visualization, not just one domain.
-For advanced data science with geospatial data (i.e., tables over meshes), consider the
-[GeoStats.jl](https://github.com/JuliaEarth/GeoStats.jl) framework. It provides sophisticated
-methods for estimating (interpolating), simulating and learning geospatial functions over
-[Meshes.jl](https://github.com/JuliaGeometry/Meshes.jl) meshes. Please check the
-[Geospatial Data Science with Julia](https://juliaearth.github.io/geospatial-data-science-with-julia)
-book for more information:
-
+The [Geospatial Data Science with Julia](https://juliaearth.github.io/geospatial-data-science-with-julia)
+book is a great resource to learn more about [Meshes.jl](https://github.com/JuliaGeometry/Meshes.jl)
+and geospatial data (i.e. tables over meshes):
```@raw html
@@ -48,9 +44,8 @@ book for more information:
```
-If you have questions or would like to brainstorm ideas in general, don't hesitate to start
-a thread in our [zulip channel](https://julialang.zulipchat.com/#narrow/stream/275558-meshes.2Ejl).
-We are happy to improve the ecosystem to meet user's needs.
+If you have questions or would like to brainstorm ideas, don't hesitate to start a thread in our
+[zulip channel](https://julialang.zulipchat.com/#narrow/stream/275558-meshes.2Ejl).
## Installation
@@ -77,59 +72,61 @@ import CairoMakie as Mke
### Points and vectors
-A [`Point`](@ref) is defined by its coordinates in a global reference system. The type of the
-coordinates is determined automatically based on the specified literals, or is forced
-to a specific type using helper constructors (e.g. `Point2`, `Point3`, `Point2f`, `Point3f`).
+A [`Point`](@ref) is defined by its coordinates in a coordinate reference system
+from [CoordRefSystems.jl](https://github.com/JuliaEarth/CoordRefSystems.jl). By
+default, a `Cartesian` coordinates with `NoDatum` are used.
+
`Integer` coordinates are converted to `Float64` to fulfill the requirements of most
-geometric processing algorithms, which would be undefined in a discrete scale.
+geometric processing algorithms, which would be undefined in a discrete scale:
-A vector [`Vec`](@ref) follows the same pattern. It can be constructed with the generic `Vec`
-constructor or with the variants `Vec2` and `Vec3` for double precision and `Vec2f`
-and `Vec3f` for single precision.
+```@example overview
+Point(0.0, 1.0) # double precision as expected
+```
```@example overview
-# 2D points
-A = Point(0.0, 1.0) # double precision as expected
-B = Point(0f0, 1f0) # single precision as expected
-C = Point(0, 0) # Integer is converted to Float64 by design
-D = Point2(0, 1) # explicitly ask for double precision
-E = Point2f(0, 1) # explicitly ask for single precision
+Point(0f0, 1f0) # single precision as expected
+```
-# 3D points
-F = Point(1.0, 2.0, 3.0) # double precision as expected
-G = Point(1f0, 2f0, 3f0) # single precision as expected
-H = Point(1, 2, 3) # Integer is converted to Float64 by design
-I = Point3(1, 2, 3) # explicitly ask for double precision
-J = Point3f(1, 2, 3) # explicitly ask for single precision
+```@example overview
+Point(0, 0) # Integer is converted to Float64 by design
+```
-for P in (A,B,C,D,E,F,G,H,I,J)
- println("Coordinate type: ", coordtype(P))
- println("Embedding dimension: ", embeddim(P))
-end
+```@example overview
+Point(1.0, 2.0, 3.0) # double precision as expected
+```
+
+```@example overview
+Point(1f0, 2f0, 3f0) # single precision as expected
+```
+
+```@example overview
+Point(1, 2, 3) # Integer is converted to Float64 by design
```
Points can be subtracted to produce a vector:
```@example overview
+A = Point(1.0, 1.0)
+B = Point(3.0, 3.0)
B - A
```
-They can't be added, but their coordinates can:
+They can't be added, but the vectors from the origin to the points can:
```@example overview
-coordinates(F) + coordinates(H)
+to(A) + to(B)
```
We can add a point to a vector though, and get a new point:
```@example overview
-F + Vec(1, 1, 1)
+A + Vec(1, 1)
```
-And finally, we can create points at random with:
+Every point and vector has well-defined coordinates:
```@example overview
-ps = rand(Point2, 10)
+coords(A)
```
### Primitives
@@ -195,7 +192,11 @@ viz(t)
Some of these geometries have additional functionality like the measure (or area):
```@example overview
-measure(t) == area(t) == 1/2
+measure(t)
+```
+
+```@example overview
+measure(t) == area(t)
```
Or the ability to know whether or not a point is inside:
@@ -261,19 +262,19 @@ We can also compute angles of any given chain, no matter if it
is open or closed:
```@example overview
-angles(r) * 180 / pi
+angles(r)
```
The sign of these angles is a function of the orientation:
```@example overview
-angles(reverse(r)) * 180 / pi
+angles(reverse(r))
```
In case of rings (i.e. closed chains), we can compute inner angles as well:
```@example overview
-innerangles(r) * 180 / pi
+innerangles(r)
```
And there is a lot more functionality available like for instance
@@ -294,9 +295,9 @@ Domain
```
```@example overview
-g = CartesianGrid(100, 100)
+grid = CartesianGrid(100, 100)
-viz(g, showfacets = true)
+viz(grid, showsegments = true)
```
No memory is allocated:
@@ -308,7 +309,7 @@ No memory is allocated:
but we can still loop over the elements, which are quadrangles in 2D:
```@example overview
-collect(elements(g))
+collect(grid)
```
We can construct a general unstructured mesh with a global vector of points
@@ -316,19 +317,19 @@ and a collection of [`Connectivity`](@ref) that store the indices to the
global vector of points:
```@example overview
-points = Point2[(0,0), (1,0), (0,1), (1,1), (0.25,0.5), (0.75,0.5)]
+points = [(0,0), (1,0), (0,1), (1,1), (0.25,0.5), (0.75,0.5)]
tris = connect.([(1,5,3), (4,6,2)], Triangle)
quads = connect.([(1,2,6,5), (4,3,5,6)], Quadrangle)
mesh = SimpleMesh(points, [tris; quads])
```
```@example overview
-viz(mesh, showfacets = true)
+viz(mesh, showsegments = true)
```
The actual geometries of the elements are materialized in a lazy fashion
like with the Cartesian grid:
```@example overview
-collect(elements(mesh))
+collect(mesh)
```
\ No newline at end of file
diff --git a/docs/src/predicates.md b/docs/src/predicates.md
index 4a1a682a1..7761fd03d 100644
--- a/docs/src/predicates.md
+++ b/docs/src/predicates.md
@@ -12,11 +12,12 @@ One important note to make is that these predicates are not necessarily exact.
For example, rather than checking if a point `p` is exactly in a sphere of radius
`r` centered at `c`, we check if `norm(p-c) ≈ r` with an absolute tolerance depending
on the point type, so `p` might be slightly outside the sphere but still be considered
-as being inside.
+as being inside. This absolute tolerance can be adjusted in specific scopes as discussed
+in the [Tolerances](tolerances.md) section.
-Exact arithmetic is expensive to apply and approximations are typically sufficient;
-exact predicates are available in [ExactPredicates.jl](https://github.com/lairez/ExactPredicates.jl)
-if you need them.
+Robust predicates are often expensive to apply and approximations typically suffice.
+If needed, consider [ExactPredicates.jl](https://github.com/lairez/ExactPredicates.jl) or
+[AdaptivePredicates.jl](https://github.com/JuliaGeometry/AdaptivePredicates.jl).
## isparametrized
@@ -61,6 +62,24 @@ issimple
hasholes
```
+## point₁ ≤ point₂
+
+```@docs
+Base.:<(::Point, ::Point)
+Base.:>(::Point, ::Point)
+Base.:≤(::Point, ::Point)
+Base.:≥(::Point, ::Point)
+```
+
+## point₁ ⪯ point₂
+
+```@docs
+≺(::Point, ::Point)
+≻(::Point, ::Point)
+⪯(::Point, ::Point)
+⪰(::Point, ::Point)
+```
+
## point ∈ geometry
```@docs
@@ -81,9 +100,9 @@ supportfun
```
```@example intersects
-outer = Point2[(0,0),(1,0),(1,1),(0,1)]
-hole1 = Point2[(0.2,0.2),(0.4,0.2),(0.4,0.4),(0.2,0.4)]
-hole2 = Point2[(0.6,0.2),(0.8,0.2),(0.8,0.4),(0.6,0.4)]
+outer = [(0,0),(1,0),(1,1),(0,1)]
+hole1 = [(0.2,0.2),(0.4,0.2),(0.4,0.4),(0.2,0.4)]
+hole2 = [(0.6,0.2),(0.8,0.2),(0.8,0.4),(0.6,0.4)]
poly = PolyArea([outer, hole1, hole2])
ball1 = Ball((0.5,0.5), 0.05)
ball2 = Ball((0.3,0.3), 0.05)
diff --git a/docs/src/rand.md b/docs/src/rand.md
new file mode 100644
index 000000000..8f524200e
--- /dev/null
+++ b/docs/src/rand.md
@@ -0,0 +1,42 @@
+# Random
+
+```@example rand
+using Meshes # hide
+using Random # hide
+using CoordRefSystems # hide
+```
+
+```@docs
+rand(::Type{<:Geometry})
+rand(::Type{<:Geometry}, ::Int)
+```
+
+Random geometries can be generated using the `rand` function:
+
+```@example rand
+rand(Point)
+```
+
+By default, the `rand` function uses the `Cartesian3D` CRS (Coordinate Reference System).
+It's possible to change the CRS using the `crs` keyword argument:
+
+```@example rand
+rand(Point, crs=Cartesian2D)
+```
+
+A vector of geometries can be generated by passing the number of elements as the second argument:
+
+```@example rand
+rand(Segment, 5, crs=LatLon)
+```
+
+For reproducibility purposes, a random number generator can be passed as the first argument in both methods:
+
+```@example rand
+rng = MersenneTwister(123)
+rand(rng, Triangle)
+```
+
+```@example rand
+rand(rng, Triangle, 5)
+```
diff --git a/docs/src/tolerances.md b/docs/src/tolerances.md
new file mode 100644
index 000000000..c11b8b701
--- /dev/null
+++ b/docs/src/tolerances.md
@@ -0,0 +1,15 @@
+# Tolerances
+
+The absolute tolerance used for floating point comparisons is hard-coded in
+the project to `1e-10` for `Float64` and to `1f-5` for `Float32`. You can use
+[ScopedValues.jl](https://github.com/vchuravy/ScopedValues.jl) to customize
+these tolerance values in specific computations:
+
+```julia
+using Meshes
+using ScopedValues
+
+with(Meshes.ATOL64 => 1e-9, Meshes.ATOL32 => 1f-4) do
+ # do your computations with custom tolerances
+end
+```
diff --git a/docs/src/transforms.md b/docs/src/transforms.md
index 1ed93a4bc..8bec4a279 100644
--- a/docs/src/transforms.md
+++ b/docs/src/transforms.md
@@ -2,6 +2,7 @@
```@example transforms
using Meshes # hide
+using CoordRefSystems # hide
import CairoMakie as Mke # hide
```
@@ -14,6 +15,14 @@ GeometricTransform
CoordinateTransform
```
+Some transforms have an inverse that can be created with the [`inverse`](@ref) function.
+The function [`isinvertible`](@ref) can be used to check if a transform is invertible.
+
+```@docs
+inverse
+isinvertible
+```
+
## Rotate
```@docs
@@ -21,11 +30,9 @@ Rotate
```
```@example transforms
-using Rotations: Angle2d
-
grid = CartesianGrid(10, 10)
-mesh = grid |> Rotate(Angle2d(π/4))
+mesh = grid |> Rotate(π/4)
fig = Mke.Figure(size = (800, 400))
viz(fig[1,1], grid)
@@ -50,16 +57,16 @@ viz(fig[1,2], mesh)
fig
```
-## Affine
+## Scale
```@docs
-Affine
+Scale
```
```@example transforms
grid = CartesianGrid(10, 10)
-mesh = grid |> Affine(Angle2d(π/4), [10., 20.])
+mesh = grid |> Scale(2., 3.)
fig = Mke.Figure(size = (800, 400))
viz(fig[1,1], grid)
@@ -67,16 +74,18 @@ viz(fig[1,2], mesh)
fig
```
-## Scale
+## Affine
```@docs
-Scale
+Affine
```
```@example transforms
+using Rotations: Angle2d
+
grid = CartesianGrid(10, 10)
-mesh = grid |> Scale(2., 3.)
+mesh = grid |> Affine(Angle2d(π/4), [10., 20.])
fig = Mke.Figure(size = (800, 400))
viz(fig[1,1], grid)
@@ -120,6 +129,83 @@ viz(fig[1,2], mesh)
fig
```
+## Proj
+
+```@docs
+Proj
+```
+
+```@example transforms
+# load coordinate reference system
+using CoordRefSystems: Polar
+
+# triangle with Cartesian coordinates
+triangle = Triangle((0, 0), (1, 0), (1, 1))
+
+# reproject to polar coordinates
+triangle |> Proj(Polar)
+```
+
+## Morphological
+
+```@docs
+Morphological
+```
+
+```@example transforms
+# triangle with Cartesian coordinates
+triangle = Triangle((0, 0), (1, 0), (1, 1))
+
+# transform triangle coordinates
+triangle |> Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+```
+
+## LengthUnit
+
+```@docs
+LengthUnit
+```
+
+```@example transforms
+using Unitful: m, cm
+
+# convert meters to centimeters
+Point(1m, 2m, 3m) |> LengthUnit(cm)
+```
+
+## Shadow
+
+```@docs
+Shadow
+```
+
+```@example transforms
+ball = Ball((0, 0, 0), 1)
+disk = ball |> Shadow("xy")
+
+
+fig = Mke.Figure(size = (800, 400))
+viz(fig[1,1], ball)
+viz(fig[1,2], disk)
+fig
+```
+
+## Slice
+
+```@docs
+Slice
+```
+
+```@example transforms
+grid = CartesianGrid(10, 10)
+subgrid = grid |> Slice(x=(1.5, 6.5), y=(3.5, 8.5))
+
+fig = Mke.Figure(size = (800, 400))
+viz(fig[1,1], grid)
+viz(fig[1,2], subgrid)
+fig
+```
+
## Repair
```@docs
@@ -128,11 +214,11 @@ Repair
```@example transforms
# mesh with unreferenced point
-points = Point3[(0, 0, 0), (0, 0, 1), (5, 5, 5), (0, 1, 0), (1, 0, 0)]
+points = [(0, 0, 0), (0, 0, 1), (5, 5, 5), (0, 1, 0), (1, 0, 0)]
connec = connect.([(1, 2, 4), (1, 2, 5), (1, 4, 5), (2, 4, 5)])
mesh = SimpleMesh(points, connec)
-rmesh = mesh |> Repair{1}()
+rmesh = mesh |> Repair(1)
```
## Bridge
@@ -144,8 +230,8 @@ Bridge
```@example transforms
# polygon with two holes
outer = [(0, 0), (1, 0), (1, 1), (0, 1)]
-hole1 = [(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
-hole2 = [(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
+hole1 = [(0.2, 0.2), (0.2, 0.4), (0.4, 0.4), (0.4, 0.2)]
+hole2 = [(0.6, 0.2), (0.6, 0.4), (0.8, 0.4), (0.8, 0.2)]
poly = PolyArea([outer, hole1, hole2])
# polygon with single outer ring
@@ -174,7 +260,7 @@ function readply(fname)
x = ply["vertex"]["x"]
y = ply["vertex"]["y"]
z = ply["vertex"]["z"]
- points = Point3.(x, y, z)
+ points = Point.(x, y, z)
connec = [connect(Tuple(c.+1)) for c in ply["face"]["vertex_indices"]]
SimpleMesh(points, connec)
end
diff --git a/docs/src/vectors.md b/docs/src/vectors.md
index a7451875b..ea092f3c4 100644
--- a/docs/src/vectors.md
+++ b/docs/src/vectors.md
@@ -8,7 +8,3 @@ import CairoMakie as Mke # hide
```@docs
Vec
```
-
-```@example vectors
-rand(Vec3, 100)
-```
\ No newline at end of file
diff --git a/docs/src/visualization.md b/docs/src/visualization.md
index a47788817..eee154e39 100644
--- a/docs/src/visualization.md
+++ b/docs/src/visualization.md
@@ -2,6 +2,7 @@
```@example viz
using Meshes # hide
+using CoordRefSystems # hide
import CairoMakie as Mke # hide
```
@@ -14,19 +15,12 @@ viz
viz!
```
-Vectors of custom objects can be visualized with the `color`
-argument provided that they implement the `ascolors` trait:
-
-```@docs
-Meshes.ascolors
-```
-
## Geometries
We can visualize a single geometry or multiple geometries in a vector:
```@example viz
-triangles = rand(Triangle{2,Float64}, 10)
+triangles = rand(Triangle, 10, crs=Cartesian2D)
viz(triangles, color = 1:10)
```
@@ -39,5 +33,5 @@ such as [`Mesh`](@ref) and show facets efficiently:
```@example viz
grid = CartesianGrid(10, 10, 10)
-viz(grid, showfacets = true, facetcolor = :teal)
+viz(grid, showsegments = true, segmentcolor = :teal)
```
\ No newline at end of file
diff --git a/ext/MeshesMakieExt.jl b/ext/MeshesMakieExt.jl
index 907ad65d4..2a282739b 100644
--- a/ext/MeshesMakieExt.jl
+++ b/ext/MeshesMakieExt.jl
@@ -5,35 +5,41 @@
module MeshesMakieExt
using Meshes
+using Unitful
using Rotations
+using StaticArrays
using LinearAlgebra
+using CoordRefSystems
+using Colorfy
-using Makie: cgrad
-using Makie: coloralpha
-using Makie.Colors: Colorant
+using Unitful: numtype
+using Meshes: lentype
import TransformsBase as TB
+import Makie.GeometryBasics as GB
import Meshes: viz, viz!
-import Meshes: ascolors
-import Meshes: defaultscheme
import Makie
Makie.@recipe(Viz, object) do scene
Makie.Attributes(
color=:slategray3,
alpha=nothing,
- colorscheme=nothing,
- pointsize=4,
+ colormap=nothing,
+ colorrange=nothing,
+ showsegments=false,
+ segmentcolor=:gray30,
segmentsize=1.5,
- showfacets=false,
- facetcolor=:gray30
+ showpoints=false,
+ pointmarker=:circle,
+ pointcolor=:gray30,
+ pointsize=4
)
end
# choose between 2D and 3D axis
-Makie.args_preferred_axis(::Geometry{Dim}) where {Dim} = Dim === 3 ? Makie.LScene : Makie.Axis
-Makie.args_preferred_axis(::Domain{Dim}) where {Dim} = Dim === 3 ? Makie.LScene : Makie.Axis
+Makie.args_preferred_axis(g::Geometry) = embeddim(g) === 3 ? Makie.LScene : Makie.Axis
+Makie.args_preferred_axis(d::Domain) = embeddim(d) === 3 ? Makie.LScene : Makie.Axis
Makie.args_preferred_axis(::AbstractVector{<:Vec{Dim}}) where {Dim} = Dim === 3 ? Makie.LScene : Makie.Axis
# color handling
@@ -43,10 +49,10 @@ include("colors.jl")
include("utils.jl")
# viz recipes
+include("mesh.jl")
include("grid.jl")
-include("simplemesh.jl")
-include("subcartesiangrid.jl")
include("geometryset.jl")
+include("subdomain.jl")
include("vector.jl")
include("fallbacks.jl")
diff --git a/ext/colors.jl b/ext/colors.jl
index e67714fa3..1d01c0221 100644
--- a/ext/colors.jl
+++ b/ext/colors.jl
@@ -2,52 +2,12 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-# type alias to reduce typing
-const V{T} = AbstractVector{<:T}
-
-# convert value to colorant, optionally using color scheme object
-ascolors(values::V{Symbol}, scheme) = ascolors(string.(values), scheme)
-ascolors(values::V{AbstractString}, scheme) = parse.(Ref(Colorant), values)
-ascolors(values::V{Number}, scheme) = get(scheme, values, :extrema)
-ascolors(values::V{Colorant}, scheme) = values
-
-# convert color scheme name to color scheme object
-ascolorscheme(name::Symbol) = cgrad(name)
-ascolorscheme(name::AbstractString) = ascolorscheme(Symbol(name))
-ascolorscheme(scheme) = scheme
-
-# default colorscheme for vector of values
-defaultscheme(values) = cgrad(:viridis)
-
-# add transparency to colors
-setalpha(colors, alphas) = coloralpha.(colors, alphas)
-setalpha(colors, ::Nothing) = colors
-
-# --------------------------------
-# PROCESS COLORS PROVIDED BY USER
-# --------------------------------
-
-# convert user input to colors
-function process(values::V, scheme, alphas)
- # find invalid and valid indices
- isinvalid(v) = ismissing(v) || (v isa Number && isnan(v))
- iinds = findall(isinvalid, values)
- vinds = setdiff(1:length(values), iinds)
-
- # invalid values are assigned full transparency
- icolors = parse(Colorant, "rgba(0,0,0,0)")
-
- # valid values are assigned colors from scheme
- vals = coalesce.(values[vinds])
- vscheme = isnothing(scheme) ? defaultscheme(vals) : ascolorscheme(scheme)
- vcolors = setalpha(ascolors(vals, vscheme), alphas)
-
- # build final vector of colors
- colors = Vector{Colorant}(undef, length(values))
- colors[iinds] .= icolors
- colors[vinds] .= vcolors
-
- colors
+# preprocess colors provided by user
+function process(values::AbstractVector, colorscheme, colorrange, alphas)
+ valphas = isnothing(alphas) ? Colorfy.defaultalphas(values) : alphas
+ vcolorscheme = isnothing(colorscheme) ? Colorfy.defaultcolorscheme(values) : colorscheme
+ vcolorrange = isnothing(colorrange) ? Colorfy.defaultcolorrange(values) : colorrange
+ colorfy(values, alphas=valphas, colorscheme=vcolorscheme, colorrange=vcolorrange)
end
-process(value, scheme, alphas) = process([value], scheme, alphas) |> first
+process(value, colorscheme, colorrange, alphas) = process([value], colorscheme, colorrange, alphas) |> first
diff --git a/ext/fallbacks.jl b/ext/fallbacks.jl
index 334defcb3..f266ca395 100644
--- a/ext/fallbacks.jl
+++ b/ext/fallbacks.jl
@@ -9,14 +9,12 @@ Makie.plottype(::AbstractVector{<:Vec}) = Viz{<:Tuple{AbstractVector{<:Vec}}}
Makie.convert_arguments(::Type{<:Viz}, geom::Geometry) = (GeometrySet([geom]),)
Makie.convert_arguments(::Type{<:Viz}, domain::Domain) = (GeometrySet(collect(domain)),)
-Makie.convert_arguments(::Type{<:Viz}, mesh::Mesh) = (convert(SimpleMesh, mesh),)
Makie.convert_arguments(::Type{<:Viz}, vec::Vec) = ([vec],)
# skip conversion for these types
+Makie.convert_arguments(::Type{<:Viz}, mesh::Mesh) = (mesh,)
Makie.convert_arguments(::Type{<:Viz}, gset::GeometrySet) = (gset,)
-Makie.convert_arguments(::Type{<:Viz}, mesh::SimpleMesh) = (mesh,)
-Makie.convert_arguments(::Type{<:Viz}, grid::Grid) = (grid,)
-Makie.convert_arguments(::Type{<:Viz}, grid::SubCartesianGrid) = (grid,)
+Makie.convert_arguments(::Type{<:Viz}, subdom::SubDomain) = (subdom,)
Makie.convert_arguments(::Type{<:Viz}, vecs::AbstractVector{<:Vec}) = (vecs,)
# vector of geometries for convenience
diff --git a/ext/geometryset.jl b/ext/geometryset.jl
index 37764a441..ff7933373 100644
--- a/ext/geometryset.jl
+++ b/ext/geometryset.jl
@@ -2,157 +2,153 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-function Makie.plot!(plot::Viz{<:Tuple{GeometrySet}})
- gset = plot[:object]
- color = plot[:color]
- alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
-
- # process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
+Makie.plot!(plot::Viz{<:Tuple{GeometrySet}}) = vizgeoms!(plot)
- # get geometries
- geoms = Makie.@lift parent($gset)
+const ObservableVector{T} = Makie.Observable{<:AbstractVector{T}}
- # get geometry types
- types = Makie.@lift unique(typeof.($geoms))
+function vizgset!(plot, ::Type{<:🌐}, pdim::Val, edim::Val, geoms, colorant)
+ vizgset!(plot, 𝔼, pdim, edim, geoms, colorant)
+end
- for G in types[]
- inds = Makie.@lift findall(g -> g isa G, $geoms)
- gvec = Makie.@lift collect(G, $geoms[$inds])
- colors = Makie.@lift $colorant isa AbstractVector ? $colorant[$inds] : $colorant
- rank = Makie.@lift paramdim(first($gvec))
- if rank[] == 0
- vizgset0D!(plot, gvec, colors)
- elseif rank[] == 1
- vizgset1D!(plot, gvec, colors)
- elseif rank[] == 2
- vizgset2D!(plot, gvec, colors)
- elseif rank[] == 3
- vizgset3D!(plot, gvec, colors)
- end
- end
+function vizgset!(plot, ::Type{<:𝔼}, ::Val{0}, ::Val, geoms, colorant)
+ points = Makie.@lift pointify.($geoms)
+ vizmany!(plot, points, colorant)
end
-function Makie.plot!(plot::Viz{<:Tuple{PointSet}})
- pset = plot[:object]
- color = plot[:color]
- alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
+function vizgset!(plot, ::Type{<:𝔼}, ::Val{0}, ::Val, geoms::ObservableVector{<:Point}, colorant)
+ pointmarker = plot[:pointmarker]
pointsize = plot[:pointsize]
- # process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
-
- # get geometries and coordinates
- geoms = Makie.@lift parent($pset)
- coords = Makie.@lift coordinates.($geoms)
+ # get raw Cartesian coordinates of points
+ coords = Makie.@lift map(p -> ustrip.(to(p)), $geoms)
- # visualize point set
- Makie.scatter!(plot, coords, color=colorant, markersize=pointsize, overdraw=true)
+ # visualize points with given marker and size
+ Makie.scatter!(plot, coords, color=colorant, marker=pointmarker, markersize=pointsize, overdraw=true)
end
-const ObservableVector{T} = Makie.Observable{<:AbstractVector{T}}
+function vizgset!(plot, ::Type{<:𝔼}, ::Val{1}, ::Val, geoms, colorant)
+ showpoints = plot[:showpoints]
-function vizgset0D!(plot, geoms, colorant)
- points = Makie.@lift pointify.($geoms)
- vizmany!(plot, points, colorant)
-end
-
-function vizgset1D!(plot, geoms, colorant)
- meshes = Makie.@lift discretize.($geoms)
+ meshes = Makie.@lift _discretize.($geoms)
vizmany!(plot, meshes, colorant)
- showfactes1D!(plot, geoms)
+
+ if showpoints[]
+ vizfacets!(plot, geoms)
+ end
end
-function vizgset1D!(plot, geoms::ObservableVector{<:Ray}, colorant)
+function vizgset!(plot, ::Type{<:𝔼}, ::Val{1}, ::Val, geoms::ObservableVector{<:Ray}, colorant)
rset = plot[:object]
+ segmentsize = plot[:segmentsize]
+ showpoints = plot[:showpoints]
- if embeddim(rset[]) ∉ (2, 3)
- error("not implemented")
- end
+ Dim = embeddim(rset[])
+
+ Dim ∈ (2, 3) || error("not implemented")
# visualize as built-in arrows
- origins = Makie.@lift [asmakie(ray(0)) for ray in $geoms]
- directions = Makie.@lift [asmakie(ray(1) - ray(0)) for ray in $geoms]
- Makie.arrows!(plot, origins, directions, color=colorant)
+ orig = Makie.@lift [asmakie(ray(0)) for ray in $geoms]
+ dirs = Makie.@lift [asmakie(ray(1) - ray(0)) for ray in $geoms]
+ size = Makie.@lift 0.1 * $segmentsize
+ Makie.arrows!(plot, orig, dirs, color=colorant, arrowsize=size)
- showfactes1D!(plot, geoms)
+ if showpoints[]
+ vizfacets!(plot, geoms)
+ end
end
-function vizgset2D!(plot, geoms, colorant)
- meshes = Makie.@lift discretize.($geoms)
+function vizgset!(plot, ::Type{<:𝔼}, ::Val{2}, ::Val, geoms, colorant)
+ showsegments = plot[:showsegments]
+
+ meshes = Makie.@lift _discretize.($geoms)
vizmany!(plot, meshes, colorant)
- showfactes2D!(plot, geoms)
+
+ if showsegments[]
+ vizfacets!(plot, geoms)
+ end
end
-const PolygonLike{Dim,T} = Union{Polygon{Dim,T},MultiPolygon{Dim,T}}
+const PolygonLike = Union{Polygon,MultiPolygon}
-function vizgset2D!(plot, geoms::ObservableVector{<:PolygonLike{2}}, colorant)
+function vizgset!(plot, ::Type{<:𝔼}, ::Val{2}, ::Val{2}, geoms::ObservableVector{<:PolygonLike}, colorant)
+ showsegments = plot[:showsegments]
+ segmentcolor = plot[:segmentcolor]
segmentsize = plot[:segmentsize]
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
# repeat colors if necessary
colors = Makie.@lift mayberepeat($colorant, $geoms)
# visualize as built-in poly
polys = Makie.@lift asmakie($geoms)
- if showfacets[]
- Makie.poly!(plot, polys, color=colors, strokecolor=facetcolor, strokewidth=segmentsize)
+ if showsegments[]
+ Makie.poly!(plot, polys, color=colors, strokecolor=segmentcolor, strokewidth=segmentsize)
else
Makie.poly!(plot, polys, color=colors)
end
-
- showfactes2D!(plot, geoms)
end
-function vizgset3D!(plot, geoms, colorant)
- meshes = Makie.@lift discretize.(boundary.($geoms))
+function vizgset!(plot, ::Type{<:𝔼}, ::Val{3}, ::Val, geoms, colorant)
+ meshes = Makie.@lift _discretize.(boundary.($geoms))
vizmany!(plot, meshes, colorant)
end
-function showfactes1D!(plot, geoms)
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
+vizfacets!(plot::Viz{<:Tuple{GeometrySet}}) = vizgeoms!(plot, facets=false)
+
+function vizfacets!(plot::Viz{<:Tuple{GeometrySet}}, geoms)
+ M = Makie.@lift manifold(first($geoms))
+ pdim = Makie.@lift paramdim(first($geoms))
+ edim = Makie.@lift embeddim(first($geoms))
+ vizgsetfacets!(plot, M[], Val(pdim[]), Val(edim[]), geoms)
+end
+
+function vizgsetfacets!(plot, ::Type, ::Val{1}, ::Val, geoms)
+ pointmarker = plot[:pointmarker]
+ pointcolor = plot[:pointcolor]
pointsize = plot[:pointsize]
- if showfacets[]
- # all boundaries are points or multipoints
- bounds = Makie.@lift filter(!isnothing, boundary.($geoms))
- bset = Makie.@lift GeometrySet($bounds)
- viz!(plot, bset, color=facetcolor, pointsize=pointsize)
- end
+ # all boundaries are points or multipoints
+ points = Makie.@lift filter(!isnothing, boundary.($geoms))
+ pset = Makie.@lift GeometrySet($points)
+ viz!(plot, pset, color=pointcolor, pointmarker=pointmarker, pointsize=pointsize)
end
-function showfactes2D!(plot, geoms)
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
+function vizgsetfacets!(plot, ::Type, ::Val{2}, ::Val, geoms)
+ segmentcolor = plot[:segmentcolor]
segmentsize = plot[:segmentsize]
- if showfacets[]
- # all boundaries are geometries
- bounds = Makie.@lift filter(!isnothing, boundary.($geoms))
- bset = Makie.@lift GeometrySet($bounds)
- viz!(plot, bset, color=facetcolor, segmentsize=segmentsize)
- end
+ # all boundaries are 1D geometries
+ bounds = Makie.@lift filter(!isnothing, boundary.($geoms))
+ bset = Makie.@lift GeometrySet($bounds)
+ viz!(plot, bset, color=segmentcolor, segmentsize=segmentsize)
end
-asmakie(geoms::AbstractVector{<:Geometry}) = asmakie.(geoms)
+function vizgeoms!(plot; facets=false)
+ gset = plot[:object]
+ color = plot[:color]
+ alpha = plot[:alpha]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
-asmakie(multis::AbstractVector{<:Multi}) = mapreduce(m -> asmakie.(parent(m)), vcat, multis)
+ # process color spec into colorant
+ colorant = facets ? nothing : Makie.@lift(process($color, $colormap, $colorrange, $alpha))
-function asmakie(poly::Polygon)
- rs = rings(poly)
- outer = [asmakie(p) for p in vertices(first(rs))]
- if hasholes(poly)
- inners = map(i -> [asmakie(p) for p in vertices(rs[i])], 2:length(rs))
- Makie.Polygon(outer, inners)
- else
- Makie.Polygon(outer)
- end
-end
+ # get geometries
+ geoms = Makie.@lift parent($gset)
-asmakie(p::Point{Dim,T}) where {Dim,T} = Makie.Point{Dim,T}(Tuple(coordinates(p)))
+ # get geometry types
+ types = Makie.@lift unique(typeof.($geoms))
-asmakie(v::Vec{Dim,T}) where {Dim,T} = Makie.Vec{Dim,T}(Tuple(v))
+ for G in types[]
+ inds = Makie.@lift findall(g -> g isa G, $geoms)
+ gvec = Makie.@lift collect(G, $geoms[$inds])
+ M = Makie.@lift manifold(first($gvec))
+ pdim = Makie.@lift paramdim(first($gvec))
+ edim = Makie.@lift embeddim(first($gvec))
+ if facets
+ vizgsetfacets!(plot, M[], Val(pdim[]), Val(edim[]), gvec)
+ else
+ cvec = Makie.@lift $colorant isa AbstractVector ? $colorant[$inds] : $colorant
+ vizgset!(plot, M[], Val(pdim[]), Val(edim[]), gvec, cvec)
+ end
+ end
+end
diff --git a/ext/grid.jl b/ext/grid.jl
index 7ca6fee13..32c72de49 100644
--- a/ext/grid.jl
+++ b/ext/grid.jl
@@ -3,45 +3,28 @@
# ------------------------------------------------------------------
function Makie.plot!(plot::Viz{<:Tuple{Grid}})
- grid = plot[:object][]
- Dim = embeddim(grid)
- if Dim == 2
- vizgrid2D!(plot)
- elseif Dim == 3
- vizgrid3D!(plot)
- end
-end
-
-vizgrid2D!(plot) = vizmesh2D!(plot)
-
-function vizgrid3D!(plot)
grid = plot[:object]
- color = plot[:color]
-
- # number of vertices and colors
- nv = Makie.@lift nvertices($grid)
- nc = Makie.@lift $color isa AbstractVector ? length($color) : 1
+ M = Makie.@lift manifold($grid)
+ pdim = Makie.@lift paramdim($grid)
+ edim = Makie.@lift embeddim($grid)
+ vizgrid!(plot, M[], Val(pdim[]), Val(edim[]))
+end
- if nv[] == nc[]
- error("not implemented")
- else
- vizmesh3D!(plot)
- end
+function vizgrid!(plot, ::Type{<:🌐}, pdim::Val, edim::Val)
+ vizgrid!(plot, 𝔼, pdim, edim)
end
-# defining a Makie.data_limits method is necessary because
-# Makie.scale!, Makie.translate! and Makie.rotate!
-# don't adjust axis limits automatically
-function Makie.data_limits(plot::Viz{<:Tuple{Grid}})
- grid = plot[:object][]
- bbox = boundingbox(grid)
- pmin = aspoint3f(minimum(bbox))
- pmax = aspoint3f(maximum(bbox))
- Makie.limits_from_transformed_points([pmin, pmax])
+vizgrid!(plot, M::Type{<:𝔼}, pdim::Val, edim::Val) = vizgridfallback!(plot, M, pdim, edim)
+
+function vizfacets!(plot::Viz{<:Tuple{Grid}})
+ grid = plot[:object]
+ M = Makie.@lift manifold($grid)
+ pdim = Makie.@lift paramdim($grid)
+ edim = Makie.@lift embeddim($grid)
+ vizgridfacets!(plot, M[], Val(pdim[]), Val(edim[]))
end
-aspoint3f(p::Point{2}) = Makie.Point3f(coordinates(p)..., 0)
-aspoint3f(p::Point{3}) = Makie.Point3f(coordinates(p)...)
+vizgridfacets!(plot, M::Type, pdim::Val, edim::Val) = vizmeshfacets!(plot, M, pdim, edim)
# ----------------
# SPECIALIZATIONS
@@ -49,8 +32,69 @@ aspoint3f(p::Point{3}) = Makie.Point3f(coordinates(p)...)
include("grid/cartesian.jl")
include("grid/rectilinear.jl")
-include("grid/transformed.jl")
include("grid/structured.jl")
+include("grid/transformed.jl")
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function vizgridfallback!(plot, M, pdim, edim)
+ grid = plot[:object]
+ color = plot[:color]
+ alpha = plot[:alpha]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
+
+ # process color spec into colorant
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
+
+ # number of vertices, elements and colors
+ nverts = Makie.@lift nvertices($grid)
+ nelems = Makie.@lift nelements($grid)
+ ncolor = Makie.@lift $colorant isa AbstractVector ? length($colorant) : 1
+
+ # visualize quadrangle mesh with texture using uv coords
+ # plots with uv coords are always interpolated,
+ # so it is only used in the case ncolor == nverts
+ # or when there is a large number of elements
+ if pdim == Val(2) && (ncolor[] == 1 || ncolor[] == nverts[] || nelems[] ≥ 1000)
+ # decide whether or not to reverse connectivity list
+ rfunc = Makie.@lift _reverse($grid)
+
+ verts = Makie.@lift map(asmakie, eachvertex($grid))
+ quads = Makie.@lift [GB.QuadFace($rfunc(indices(e))) for e in elements(topology($grid))]
+
+ dims = Makie.@lift size($grid)
+ vdims = Makie.@lift Meshes.vsize($grid)
+ texture = if ncolor[] == 1
+ Makie.@lift fill($colorant, $dims)
+ elseif ncolor[] == nelems[]
+ Makie.@lift reshape($colorant, $dims)
+ elseif ncolor[] == nverts[]
+ Makie.@lift reshape($colorant, $vdims)
+ else
+ throw(ArgumentError("invalid number of colors"))
+ end
+
+ uv = Makie.@lift [Makie.Vec2f(v, 1 - u) for v in range(0, 1, $vdims[2]) for u in range(0, 1, $vdims[1])]
+
+ mesh = Makie.@lift GB.Mesh(Makie.meta($verts, uv=$uv), $quads)
+
+ shading = edim == Val(3) ? Makie.FastShading : Makie.NoShading
+
+ Makie.mesh!(plot, mesh, color=texture, shading=shading)
+
+ if showsegments[]
+ vizfacets!(plot)
+ end
+ else # fallback to triangle mesh visualization
+ vizmesh!(plot, M, pdim, edim)
+ end
+end
+
+_reverse(grid) = crs(grid) <: LatLon && orientation(first(grid)) == CW ? reverse : identity
# helper functions to create a minimum number
# of line segments within Cartesian/Rectilinear grid
diff --git a/ext/grid/cartesian.jl b/ext/grid/cartesian.jl
index c8c00ee9d..270f634a4 100644
--- a/ext/grid/cartesian.jl
+++ b/ext/grid/cartesian.jl
@@ -2,25 +2,24 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-function vizgrid2D!(plot::Viz{<:Tuple{CartesianGrid}})
+function vizgrid!(plot::Viz{<:Tuple{CartesianGrid}}, ::Type{<:𝔼}, ::Val{2}, ::Val{2})
grid = plot[:object]
color = plot[:color]
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
- segmentsize = plot[:segmentsize]
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
# process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
# number of vertices and colors
nv = Makie.@lift nvertices($grid)
nc = Makie.@lift $colorant isa AbstractVector ? length($colorant) : 1
# origin, spacing and size of grid
- or = Makie.@lift coordinates(minimum($grid))
- sp = Makie.@lift spacing($grid)
+ or = Makie.@lift ustrip.(to(minimum($grid)))
+ sp = Makie.@lift ustrip.(spacing($grid))
sz = Makie.@lift size($grid)
if nc[] == 1
@@ -29,10 +28,8 @@ function vizgrid2D!(plot::Viz{<:Tuple{CartesianGrid}})
bbox = Makie.@lift boundingbox($grid)
viz!(plot, bbox, color=colorant)
- if showfacets[]
- tup = Makie.@lift xysegments(Meshes.xyz($grid)...)
- x, y = Makie.@lift($tup[1]), Makie.@lift($tup[2])
- Makie.lines!(plot, x, y, color=facetcolor, linewidth=segmentsize)
+ if showsegments[]
+ vizfacets!(plot)
end
else
if nc[] == nv[]
@@ -45,10 +42,8 @@ function vizgrid2D!(plot::Viz{<:Tuple{CartesianGrid}})
Makie.image!(plot, C, interpolate=false)
end
- if showfacets[]
- tup = Makie.@lift xysegments(0:$sz[1], 0:$sz[2])
- x, y = Makie.@lift($tup[1]), Makie.@lift($tup[2])
- Makie.lines!(plot, x, y, color=facetcolor, linewidth=segmentsize)
+ if showsegments[]
+ vizfacets!(plot)
end
# adjust spacing and origin
@@ -59,26 +54,25 @@ function vizgrid2D!(plot::Viz{<:Tuple{CartesianGrid}})
end
end
-function vizgrid3D!(plot::Viz{<:Tuple{CartesianGrid}})
+function vizgrid!(plot::Viz{<:Tuple{CartesianGrid}}, ::Type{<:𝔼}, ::Val{3}, ::Val{3})
# retrieve parameters
grid = plot[:object]
color = plot[:color]
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
- segmentsize = plot[:segmentsize]
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
# process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
# number of vertices and colors
nv = Makie.@lift nvertices($grid)
nc = Makie.@lift $colorant isa AbstractVector ? length($colorant) : 1
# spacing and coordinates
- sp = Makie.@lift spacing($grid)
- xyz = Makie.@lift Meshes.xyz($grid)
+ sp = Makie.@lift ustrip.(spacing($grid))
+ xyz = Makie.@lift map(x -> ustrip.(x), Meshes.xyz($grid))
if nc[] == 1
# visualize bounding box with a single
@@ -99,9 +93,29 @@ function vizgrid3D!(plot::Viz{<:Tuple{CartesianGrid}})
end
end
- if showfacets[]
- tup = Makie.@lift xyzsegments($xyz...)
- x, y, z = Makie.@lift($tup[1]), Makie.@lift($tup[2]), Makie.@lift($tup[3])
- Makie.lines!(plot, x, y, z, color=facetcolor, linewidth=segmentsize)
+ if showsegments[]
+ vizfacets!(plot)
end
end
+
+function vizgridfacets!(plot::Viz{<:Tuple{CartesianGrid}}, ::Type{<:𝔼}, ::Val{2}, ::Val{2})
+ grid = plot[:object]
+ segmentcolor = plot[:segmentcolor]
+ segmentsize = plot[:segmentsize]
+
+ xyz = Makie.@lift map(x -> ustrip.(x), Meshes.xyz($grid))
+ tup = Makie.@lift xysegments($xyz...)
+ x, y = Makie.@lift($tup[1]), Makie.@lift($tup[2])
+ Makie.lines!(plot, x, y, color=segmentcolor, linewidth=segmentsize)
+end
+
+function vizgridfacets!(plot::Viz{<:Tuple{CartesianGrid}}, ::Type{<:𝔼}, ::Val{3}, ::Val{3})
+ grid = plot[:object]
+ segmentcolor = plot[:segmentcolor]
+ segmentsize = plot[:segmentsize]
+
+ xyz = Makie.@lift map(x -> ustrip.(x), Meshes.xyz($grid))
+ tup = Makie.@lift xyzsegments($xyz...)
+ x, y, z = Makie.@lift($tup[1]), Makie.@lift($tup[2]), Makie.@lift($tup[3])
+ Makie.lines!(plot, x, y, z, color=segmentcolor, linewidth=segmentsize)
+end
diff --git a/ext/grid/rectilinear.jl b/ext/grid/rectilinear.jl
index 8ed163014..7178b952e 100644
--- a/ext/grid/rectilinear.jl
+++ b/ext/grid/rectilinear.jl
@@ -2,48 +2,60 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-function vizgrid2D!(plot::Viz{<:Tuple{RectilinearGrid}})
+function vizgrid!(plot::Viz{<:Tuple{RectilinearGrid}}, M::Type{<:𝔼}, pdim::Val{2}, edim::Val{2})
grid = plot[:object]
color = plot[:color]
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
- segmentsize = plot[:segmentsize]
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
-
- # process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
-
- # number of vertices and colors
- nv = Makie.@lift nvertices($grid)
- nc = Makie.@lift $colorant isa AbstractVector ? length($colorant) : 1
-
- # grid coordinates
- xyz = Makie.@lift Meshes.xyz($grid)
- xs = Makie.@lift $xyz[1]
- ys = Makie.@lift $xyz[2]
-
- if nc[] == 1
- # visualize bounding box with a single
- # color for maximum performance
- bbox = Makie.@lift boundingbox($grid)
- viz!(plot, bbox, color=colorant)
- else
- if nc[] == nv[]
- # visualize as a simple mesh so that
- # colors can be specified at vertices
- vizmesh2D!(plot)
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
+
+ if crs(grid[]) <: Cartesian
+ # process color spec into colorant
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
+
+ # number of vertices and colors
+ nv = Makie.@lift nvertices($grid)
+ nc = Makie.@lift $colorant isa AbstractVector ? length($colorant) : 1
+
+ # grid coordinates
+ xyz = Makie.@lift map(x -> ustrip.(x), Meshes.xyz($grid))
+ xs = Makie.@lift $xyz[1]
+ ys = Makie.@lift $xyz[2]
+
+ if nc[] == 1
+ # visualize bounding box with a single
+ # color for maximum performance
+ bbox = Makie.@lift boundingbox($grid)
+ viz!(plot, bbox, color=colorant)
else
- # visualize as built-in heatmap
- sz = Makie.@lift size($grid)
- C = Makie.@lift reshape($colorant, $sz)
- Makie.heatmap!(plot, xs, ys, C)
+ if nc[] == nv[]
+ # visualize as a simple mesh so that
+ # colors can be specified at vertices
+ vizmesh!(plot, M, pdim, edim)
+ else
+ # visualize as built-in heatmap
+ sz = Makie.@lift size($grid)
+ C = Makie.@lift reshape($colorant, $sz)
+ Makie.heatmap!(plot, xs, ys, C)
+ end
end
- end
- if showfacets[]
- tup = Makie.@lift xysegments($xs, $ys)
- x, y = Makie.@lift($tup[1]), Makie.@lift($tup[2])
- Makie.lines!(plot, x, y, color=facetcolor, linewidth=segmentsize)
+ if showsegments[]
+ vizfacets!(plot)
+ end
+ else
+ vizgridfallback!(plot, M, pdim, edim)
end
end
+
+function vizgridfacets!(plot::Viz{<:Tuple{RectilinearGrid}}, ::Type{<:𝔼}, ::Val{2}, ::Val{2})
+ grid = plot[:object]
+ segmentcolor = plot[:segmentcolor]
+ segmentsize = plot[:segmentsize]
+
+ xyz = Makie.@lift map(x -> ustrip.(x), Meshes.xyz($grid))
+ tup = Makie.@lift xysegments($xyz...)
+ x, y = Makie.@lift($tup[1]), Makie.@lift($tup[2])
+ Makie.lines!(plot, x, y, color=segmentcolor, linewidth=segmentsize)
+end
diff --git a/ext/grid/structured.jl b/ext/grid/structured.jl
index f456a8513..b8d928e50 100644
--- a/ext/grid/structured.jl
+++ b/ext/grid/structured.jl
@@ -2,43 +2,54 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-function vizgrid2D!(plot::Viz{<:Tuple{StructuredGrid}})
+function vizgrid!(plot::Viz{<:Tuple{StructuredGrid}}, M::Type{<:𝔼}, pdim::Val{2}, edim::Val{2})
grid = plot[:object]
color = plot[:color]
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
- segmentsize = plot[:segmentsize]
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
- # process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
+ if crs(grid[]) <: Cartesian
+ # process color spec into colorant
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
- # number of vertices and colors
- nv = Makie.@lift nvertices($grid)
- nc = Makie.@lift $colorant isa AbstractVector ? length($colorant) : 1
+ # number of vertices and colors
+ nv = Makie.@lift nvertices($grid)
+ nc = Makie.@lift $colorant isa AbstractVector ? length($colorant) : 1
- if nc[] == nv[]
- # size and coordinates
- sz = Makie.@lift size($grid) .+ 1
- XYZ = Makie.@lift Meshes.XYZ($grid)
- X = Makie.@lift $XYZ[1]
- Y = Makie.@lift $XYZ[2]
+ if nc[] == nv[]
+ # size and coordinates
+ sz = Makie.@lift size($grid) .+ 1
+ XYZ = Makie.@lift map(X -> ustrip.(X), Meshes.XYZ($grid))
+ X = Makie.@lift $XYZ[1]
+ Y = Makie.@lift $XYZ[2]
- # visualize as built-in surface
- C = Makie.@lift reshape($colorant, $sz)
- Makie.surface!(plot, X, Y, color=C)
+ # visualize as built-in surface
+ C = Makie.@lift reshape($colorant, $sz)
+ Makie.surface!(plot, X, Y, color=C)
- if showfacets[]
- tup = Makie.@lift structuredsegments($grid)
- x, y = Makie.@lift($tup[1]), Makie.@lift($tup[2])
- Makie.lines!(plot, x, y, color=facetcolor, linewidth=segmentsize)
+ if showsegments[]
+ vizfacets!(plot)
+ end
+ else
+ vizmesh!(plot, M, pdim, edim)
end
else
- vizmesh2D!(plot)
+ vizgridfallback!(plot, M, pdim, edim)
end
end
+function vizgridfacets!(plot::Viz{<:Tuple{StructuredGrid}}, ::Type{<:𝔼}, ::Val{2}, ::Val{2})
+ grid = plot[:object]
+ segmentcolor = plot[:segmentcolor]
+ segmentsize = plot[:segmentsize]
+
+ tup = Makie.@lift structuredsegments($grid)
+ x, y = Makie.@lift($tup[1]), Makie.@lift($tup[2])
+ Makie.lines!(plot, x, y, color=segmentcolor, linewidth=segmentsize)
+end
+
function structuredsegments(grid)
cinds = CartesianIndices(size(grid) .+ 1)
coords = []
@@ -46,7 +57,7 @@ function structuredsegments(grid)
for j in axes(cinds, 2)
for i in axes(cinds, 1)
p = vertex(grid, cinds[i, j])
- c = Tuple(coordinates(p))
+ c = ustrip.(Tuple(to(p)))
push!(coords, c)
end
push!(coords, (NaN, NaN))
@@ -55,7 +66,7 @@ function structuredsegments(grid)
for i in axes(cinds, 1)
for j in axes(cinds, 2)
p = vertex(grid, cinds[i, j])
- c = Tuple(coordinates(p))
+ c = ustrip.(Tuple(to(p)))
push!(coords, c)
end
push!(coords, (NaN, NaN))
diff --git a/ext/grid/transformed.jl b/ext/grid/transformed.jl
index 992e1c9c7..c45175329 100644
--- a/ext/grid/transformed.jl
+++ b/ext/grid/transformed.jl
@@ -2,10 +2,33 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-isoptimized(::TB.Identity) = true
-isoptimized(t::TB.SequentialTransform) = all(isoptimized, t)
+function vizgrid!(plot::Viz{<:Tuple{TransformedGrid}}, M::Type{<:𝔼}, pdim::Val, edim::Val)
+ tgrid = plot[:object]
+ color = plot[:color]
+ alpha = plot[:alpha]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
+ segmentcolor = plot[:segmentcolor]
+ segmentsize = plot[:segmentsize]
+
+ # retrieve transformation
+ trans = Makie.@lift Meshes.transform($tgrid)
+
+ if isoptimized(trans[]) # visualize parent grid and transform visualization
+ grid = Makie.@lift parent($tgrid)
+ viz!(plot, grid; color, alpha, colormap, colorrange, showsegments, segmentcolor, segmentsize)
+ makietransform!(plot, trans)
+ else # fallback to full grid visualization
+ vizgridfallback!(plot, M, pdim, edim)
+ end
+end
+
+# --------------
+# OPTIMIZATIONS
+# --------------
-isoptimized(::GeometricTransform) = false
+isoptimized(t) = false
isoptimized(::Rotate{<:Angle2d}) = true
isoptimized(::Translate) = true
isoptimized(::Scale) = true
@@ -13,51 +36,27 @@ function isoptimized(t::Affine{2})
A, _ = TB.parameters(t)
isdiag(A) || isrotation(A)
end
+isoptimized(::TB.Identity) = true
+isoptimized(t::TB.SequentialTransform) = all(isoptimized, t)
-vizgrid2D!(plot::Viz{<:Tuple{TransformedGrid}}) = transformedgrid!(plot, vizmesh2D!)
-
-vizgrid3D!(plot::Viz{<:Tuple{TransformedGrid}}) = transformedgrid!(plot, vizmesh3D!)
-
-function transformedgrid!(plot, fallback)
- tgrid = plot[:object]
- grid = Makie.@lift parent($tgrid)
- trans = Makie.@lift Meshes.transform($tgrid)
- if isoptimized(trans[])
- color = plot[:color]
- alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
- segmentsize = plot[:segmentsize]
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
- viz!(plot, grid; color, alpha, colorscheme, segmentsize, showfacets, facetcolor)
- makietransform!(plot, trans[])
- else
- fallback(tgrid)
- end
-end
-
-makietransform!(plot, trans::TB.Identity) = nothing
-
-makietransform!(plot, trans::TB.SequentialTransform) = foreach(t -> makietransform!(plot, t), trans)
-
-function makietransform!(plot, trans::Rotate{<:Angle2d})
- rot = first(TB.parameters(trans))
+function makietransform!(plot, trans::Makie.Observable{<:Rotate{<:Angle2d}})
+ rot = first(TB.parameters(trans[]))
θ = first(Rotations.params(rot))
Makie.rotate!(plot, θ)
end
-function makietransform!(plot, trans::Translate)
- offsets = first(TB.parameters(trans))
- Makie.translate!(plot, offsets...)
+function makietransform!(plot, trans::Makie.Observable{<:Translate})
+ offsets = first(TB.parameters(trans[]))
+ Makie.translate!(plot, ustrip.(offsets)...)
end
-function makietransform!(plot, trans::Scale)
- factors = first(TB.parameters(trans))
+function makietransform!(plot, trans::Makie.Observable{<:Scale})
+ factors = first(TB.parameters(trans[]))
Makie.scale!(plot, factors...)
end
-function makietransform!(plot, trans::Affine{2})
- A, b = TB.parameters(trans)
+function makietransform!(plot, trans::Makie.Observable{<:Affine{2}})
+ A, b = TB.parameters(trans[])
if isdiag(A)
s₁, s₂ = A[1, 1], A[2, 2]
Makie.scale!(plot, s₁, s₂)
@@ -66,5 +65,10 @@ function makietransform!(plot, trans::Affine{2})
θ = first(Rotations.params(rot))
Makie.rotate!(plot, θ)
end
- Makie.translate!(plot, b...)
+ Makie.translate!(plot, ustrip.(b)...)
end
+
+makietransform!(plot, trans::Makie.Observable{<:TB.Identity}) = nothing
+
+makietransform!(plot, trans::Makie.Observable{<:TB.SequentialTransform}) =
+ foreach(t -> makietransform!(plot, Makie.Observable(t)), trans[])
diff --git a/ext/simplemesh.jl b/ext/mesh.jl
similarity index 51%
rename from ext/simplemesh.jl
rename to ext/mesh.jl
index f059779a3..0cdc8bdd6 100644
--- a/ext/simplemesh.jl
+++ b/ext/mesh.jl
@@ -2,29 +2,29 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-function Makie.plot!(plot::Viz{<:Tuple{SimpleMesh}})
- # retrieve mesh and rank
- mesh = plot[:object][]
- rank = paramdim(mesh)
-
- if rank == 1
- vizmesh1D!(plot)
- elseif rank == 2
- vizmesh2D!(plot)
- elseif rank == 3
- vizmesh3D!(plot)
- end
+function Makie.plot!(plot::Viz{<:Tuple{Mesh}})
+ # retrieve mesh and dimensions
+ mesh = plot[:object]
+ M = Makie.@lift manifold($mesh)
+ pdim = Makie.@lift paramdim($mesh)
+ edim = Makie.@lift embeddim($mesh)
+ vizmesh!(plot, M[], Val(pdim[]), Val(edim[]))
end
-function vizmesh1D!(plot)
+function vizmesh!(plot, ::Type{<:🌐}, pdim::Val, edim::Val)
+ vizmesh!(plot, 𝔼, pdim, edim)
+end
+
+function vizmesh!(plot, ::Type{<:𝔼}, ::Val{1}, ::Val)
mesh = plot[:object]
color = plot[:color]
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
segmentsize = plot[:segmentsize]
# process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
# retrieve coordinates of segments
coords = Makie.@lift let
@@ -47,17 +47,16 @@ function vizmesh1D!(plot)
Makie.lines!(plot, coords, color=colors, linewidth=segmentsize)
end
-function vizmesh2D!(plot)
+function vizmesh!(plot, ::Type{<:𝔼}, ::Val{2}, ::Val)
mesh = plot[:object]
color = plot[:color]
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
- segmentsize = plot[:segmentsize]
- showfacets = plot[:showfacets]
- facetcolor = plot[:facetcolor]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
# process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
# retrieve triangle mesh parameters
tparams = Makie.@lift let
@@ -65,22 +64,25 @@ function vizmesh2D!(plot)
dim = embeddim($mesh)
nvert = nvertices($mesh)
nelem = nelements($mesh)
- verts = vertices($mesh)
+ verts = eachvertex($mesh)
topo = topology($mesh)
elems = elements(topo)
# coordinates of vertices
- coords = coordinates.(verts)
+ coords = map(asmakie, verts)
# fan triangulation (assume convexity)
- tris4elem = map(elems) do elem
+ ntris = sum(e -> nvertices(pltype(e)) - 2, elems)
+ tris = Vector{GB.TriangleFace{Int}}(undef, ntris)
+ tind = 0
+ for elem in elems
I = indices(elem)
- [[I[1], I[i], I[i + 1]] for i in 2:(length(I) - 1)]
+ for i in 2:(length(I) - 1)
+ tind += 1
+ tris[tind] = GB.TriangleFace(I[1], I[i], I[i + 1])
+ end
end
- # flatten vector of triangles
- tris = [tri for tris in tris4elem for tri in tris]
-
# element vs. vertex coloring
if $colorant isa AbstractVector
ncolor = length($colorant)
@@ -88,18 +90,18 @@ function vizmesh2D!(plot)
# duplicate vertices and adjust
# connectivities to avoid linear
# interpolation of colors
- nt = 0
+ tind = 0
elem4tri = Dict{Int,Int}()
- for e in 1:nelem
- Δs = tris4elem[e]
- for _ in 1:length(Δs)
- nt += 1
- elem4tri[nt] = e
+ sizehint!(elem4tri, ntris)
+ for (eind, e) in enumerate(elems)
+ for _ in 1:(nvertices(pltype(e)) - 2)
+ tind += 1
+ elem4tri[tind] = eind
end
end
- nv = 3nt
+ nv = 3ntris
tcoords = [coords[i] for tri in tris for i in tri]
- tconnec = [collect(I) for I in Iterators.partition(1:nv, 3)]
+ tconnec = [GB.TriangleFace(i, i + 1, i + 2) for i in range(start=1, step=3, length=ntris)]
tcolors = map(1:nv) do i
t = ceil(Int, i / 3)
e = elem4tri[t]
@@ -125,82 +127,94 @@ function vizmesh2D!(plot)
tcolors = $colorant
end
- # convert connectivities to matrix format
- tmatrix = reduce(hcat, tconnec) |> transpose
-
# enable shading in 3D
tshading = dim == 3 ? Makie.FastShading : Makie.NoShading
- tcoords, tmatrix, tcolors, tshading
+ tcoords, tconnec, tcolors, tshading
end
# unpack observable of parameters
tcoords = Makie.@lift $tparams[1]
- tmatrix = Makie.@lift $tparams[2]
+ tconnec = Makie.@lift $tparams[2]
tcolors = Makie.@lift $tparams[3]
tshading = Makie.@lift $tparams[4]
- Makie.mesh!(plot, tcoords, tmatrix, color=tcolors, shading=tshading)
-
- if showfacets[]
- # retrieve coordinates parameters
- xparams = Makie.@lift let
- # relevant settings
- dim = embeddim($mesh)
- topo = topology($mesh)
- nvert = nvertices($mesh)
- verts = vertices($mesh)
- coords = coordinates.(verts)
-
- # use a sophisticated data structure
- # to extract the edges from the n-gons
- t = convert(HalfEdgeTopology, topo)
- ∂ = Boundary{1,0}(t)
-
- # append indices of incident vertices
- # interleaved with a sentinel index
- inds = Int[]
- for i in 1:nfacets(t)
- append!(inds, ∂(i))
- push!(inds, nvert + 1)
- end
-
- # fill sentinel index with NaN coordinates
- push!(coords, Vec(ntuple(i -> NaN, dim)))
+ # Makie's triangle mesh
+ mkemesh = Makie.@lift GB.Mesh($tcoords, $tconnec)
- # extract incident vertices
- coords = coords[inds]
+ # main visualization
+ Makie.mesh!(plot, mkemesh, color=tcolors, shading=tshading)
- # split coordinates to match signature
- [getindex.(coords, j) for j in 1:dim]
- end
-
- # unpack observable of paramaters
- xyz = map(1:embeddim(mesh[])) do i
- Makie.@lift $xparams[i]
- end
-
- Makie.lines!(plot, xyz..., color=facetcolor, linewidth=segmentsize)
+ if showsegments[]
+ vizfacets!(plot)
end
end
-function vizmesh3D!(plot)
+function vizmesh!(plot, ::Type{<:𝔼}, ::Val{3}, ::Val)
mesh = plot[:object]
color = plot[:color]
meshes = Makie.@lift let
geoms = elements($mesh)
bounds = boundary.(geoms)
- discretize.(bounds)
+ _discretize.(bounds)
end
vizmany!(plot, meshes, color)
end
+function vizfacets!(plot::Viz{<:Tuple{Mesh}})
+ mesh = plot[:object]
+ M = Makie.@lift manifold($mesh)
+ pdim = Makie.@lift paramdim($mesh)
+ edim = Makie.@lift embeddim($mesh)
+ vizmeshfacets!(plot, M[], Val(pdim[]), Val(edim[]))
+end
+
+function vizmeshfacets!(plot, ::Type, ::Val{2}, ::Val)
+ mesh = plot[:object]
+ segmentcolor = plot[:segmentcolor]
+ segmentsize = plot[:segmentsize]
+
+ # retrieve coordinates parameters
+ coords = Makie.@lift let
+ # relevant settings
+ T = Unitful.numtype(Meshes.lentype($mesh))
+ dim = embeddim($mesh)
+ topo = topology($mesh)
+ nvert = nvertices($mesh)
+ verts = vertices($mesh)
+ coords = map(p -> ustrip.(to(p)), verts)
+
+ # use a sophisticated data structure
+ # to extract the edges from the n-gons
+ t = convert(HalfEdgeTopology, topo)
+ ∂ = Boundary{1,0}(t)
+
+ # append indices of incident vertices
+ # interleaved with a sentinel index
+ inds = Int[]
+ for i in 1:nfacets(t)
+ for j in ∂(i)
+ push!(inds, j)
+ end
+ push!(inds, nvert + 1)
+ end
+
+ # fill sentinel index with NaN coordinates
+ push!(coords, SVector(ntuple(i -> T(NaN), dim)))
+
+ # extract incident vertices
+ coords[inds]
+ end
+
+ Makie.lines!(plot, coords, color=segmentcolor, linewidth=segmentsize)
+end
+
function segmentsof(topo, vert)
p = first(vert)
- T = coordtype(p)
+ T = Unitful.numtype(Meshes.lentype(p))
Dim = embeddim(p)
- nan = Vec{Dim,T}(ntuple(i -> NaN, Dim))
- xs = coordinates.(vert)
+ nan = SVector(ntuple(i -> T(NaN), Dim))
+ xs = map(p -> ustrip.(to(p)), vert)
coords = map(elements(topo)) do e
inds = indices(e)
@@ -211,7 +225,7 @@ function segmentsof(topo, vert)
end
function segmentsof(topo::GridTopology, vert)
- xs = coordinates.(vert)
+ xs = map(p -> ustrip.(to(p)), vert)
ip = first(isperiodic(topo))
ip ? [xs; [first(xs)]] : xs
end
diff --git a/ext/subcartesiangrid.jl b/ext/subcartesiangrid.jl
deleted file mode 100644
index d0686a914..000000000
--- a/ext/subcartesiangrid.jl
+++ /dev/null
@@ -1,42 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-const SubCartesianGrid{Dim,T} = SubDomain{Dim,T,<:CartesianGrid{Dim,T}}
-
-function Makie.plot!(plot::Viz{<:Tuple{SubCartesianGrid}})
- subgrid = plot[:object]
- color = plot[:color]
- alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
-
- # process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
-
- # retrieve grid paramaters
- gparams = Makie.@lift let
- grid = parent($subgrid)
- dim = embeddim(grid)
- sp = spacing(grid)
-
- # coordinates of centroids
- coord(e) = coordinates(centroid(e))
- coords = [coord(e) .+ sp ./ 2 for e in $subgrid]
-
- # rectangle marker
- marker = Makie.Rect{dim}(-1 .* sp, sp)
-
- # enable shading in 3D
- shading = dim == 3 ? Makie.FastShading : Makie.NoShading
-
- coords, marker, shading
- end
-
- # unpack observable parameters
- coords = Makie.@lift $gparams[1]
- marker = Makie.@lift $gparams[2]
- shading = Makie.@lift $gparams[3]
-
- # all geometries are equal, use mesh scatter
- Makie.meshscatter!(plot, coords, marker=marker, markersize=1, color=colorant, shading=shading)
-end
diff --git a/ext/subdomain.jl b/ext/subdomain.jl
new file mode 100644
index 000000000..bb05d832e
--- /dev/null
+++ b/ext/subdomain.jl
@@ -0,0 +1,90 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+function Makie.plot!(plot::Viz{<:Tuple{SubDomain}})
+ subdom = plot[:object]
+ M = Makie.@lift manifold($subdom)
+ pdim = Makie.@lift paramdim($subdom)
+ edim = Makie.@lift embeddim($subdom)
+ vizsubdom!(plot, M[], Val(pdim[]), Val(edim[]))
+end
+
+function vizsubdom!(plot, ::Type{<:🌐}, pdim::Val, edim::Val)
+ vizsubdom!(plot, 𝔼, pdim, edim)
+end
+
+function vizsubdom!(plot, ::Type{<:𝔼}, ::Val, ::Val)
+ subdom = plot[:object]
+ color = plot[:color]
+ alpha = plot[:alpha]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ showsegments = plot[:showsegments]
+ segmentcolor = plot[:segmentcolor]
+ segmentsize = plot[:segmentsize]
+ showpoints = plot[:showpoints]
+ pointmarker = plot[:pointmarker]
+ pointcolor = plot[:pointcolor]
+ pointsize = plot[:pointsize]
+
+ # construct the geometry set
+ gset = Makie.@lift GeometrySet(collect($subdom))
+
+ # forward attributes
+ viz!(
+ plot,
+ gset;
+ color,
+ alpha,
+ colormap,
+ colorrange,
+ showsegments,
+ segmentcolor,
+ segmentsize,
+ showpoints,
+ pointmarker,
+ pointcolor,
+ pointsize
+ )
+end
+
+const SubCartesianGrid{M,CRS} = SubDomain{M,CRS,<:CartesianGrid}
+
+function vizsubdom!(plot::Viz{<:Tuple{SubCartesianGrid}}, ::Type{<:𝔼}, ::Val, ::Val)
+ subgrid = plot[:object]
+ color = plot[:color]
+ alpha = plot[:alpha]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+
+ # process color spec into colorant
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
+
+ # retrieve grid paramaters
+ gparams = Makie.@lift let
+ grid = parent($subgrid)
+ dim = embeddim(grid)
+ sp = ustrip.(spacing(grid))
+
+ # coordinates of centroids
+ coord(e) = ustrip.(to(centroid(e)))
+ coords = [coord(e) .+ sp ./ 2 for e in $subgrid]
+
+ # rectangle marker
+ marker = Makie.Rect{dim}(-1 .* sp, sp)
+
+ # enable shading in 3D
+ shading = dim == 3 ? Makie.FastShading : Makie.NoShading
+
+ coords, marker, shading
+ end
+
+ # unpack observable parameters
+ coords = Makie.@lift $gparams[1]
+ marker = Makie.@lift $gparams[2]
+ shading = Makie.@lift $gparams[3]
+
+ # all geometries are equal, use mesh scatter
+ Makie.meshscatter!(plot, coords, marker=marker, markersize=1, color=colorant, shading=shading)
+end
diff --git a/ext/utils.jl b/ext/utils.jl
index 78ceb425e..59df6ef12 100644
--- a/ext/utils.jl
+++ b/ext/utils.jl
@@ -17,7 +17,7 @@ concat(mesh₁::Mesh, mesh₂::Mesh) = merge(mesh₁, mesh₂)
function vizmany!(plot, objs, color)
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
+ colormap = plot[:colormap]
pointsize = plot[:pointsize]
segmentsize = plot[:segmentsize]
@@ -25,5 +25,36 @@ function vizmany!(plot, objs, color)
colors = Makie.@lift mayberepeat($color, $objs)
alphas = Makie.@lift mayberepeat($alpha, $objs)
- viz!(plot, object, color=colors, alpha=alphas, colorscheme=colorscheme, pointsize=pointsize, segmentsize=segmentsize)
+ viz!(plot, object, color=colors, alpha=alphas, colormap=colormap, pointsize=pointsize, segmentsize=segmentsize)
+end
+
+asmakie(geoms::AbstractVector{<:Geometry}) = asmakie.(geoms)
+
+asmakie(multis::AbstractVector{<:Multi}) = mapreduce(m -> asmakie.(parent(m)), vcat, multis)
+
+function asmakie(poly::Polygon)
+ rs = rings(poly)
+ outer = map(asmakie, eachvertex(rs[1]))
+ if hasholes(poly)
+ inners = map(i -> map(asmakie, eachvertex(rs[i])), 2:length(rs))
+ Makie.Polygon(outer, inners)
+ else
+ Makie.Polygon(outer)
+ end
+end
+
+asmakie(p::Point) = Makie.Point{embeddim(p),numtype(lentype(p))}(ustrip.(Tuple(to(p))))
+
+asmakie(v::Vec) = Makie.Vec{length(v),numtype(eltype(v))}(ustrip.(Tuple(v)))
+
+_discretize(geom) = discretize(geom)
+
+function _discretize(box::Box{🌐})
+ T = numtype(Meshes.lentype(box))
+ discretize(box, MaxLengthDiscretization(T(100) * u"km"))
+end
+
+function _discretize(chain::Chain{🌐})
+ T = numtype(Meshes.lentype(chain))
+ discretize(chain, MaxLengthDiscretization(T(1000) * u"km"))
end
diff --git a/ext/vector.jl b/ext/vector.jl
index 369c06d52..48e077296 100644
--- a/ext/vector.jl
+++ b/ext/vector.jl
@@ -2,21 +2,23 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-function Makie.plot!(plot::Viz{<:Tuple{AbstractVector{Vec{Dim,T}}}}) where {Dim,T}
+function Makie.plot!(plot::Viz{<:Tuple{AbstractVector{Vec{Dim,ℒ}}}}) where {Dim,ℒ}
vecs = plot[:object]
color = plot[:color]
alpha = plot[:alpha]
- colorscheme = plot[:colorscheme]
+ colormap = plot[:colormap]
+ colorrange = plot[:colorrange]
+ segmentsize = plot[:segmentsize]
- if Dim ∉ (2, 3)
- error("not implemented")
- end
+ Dim ∈ (2, 3) || error("not implemented")
# process color spec into colorant
- colorant = Makie.@lift process($color, $colorscheme, $alpha)
+ colorant = Makie.@lift process($color, $colormap, $colorrange, $alpha)
# visualize as built-in arrows
- origins = Makie.@lift fill(zero(Makie.Point{Dim,T}), length($vecs))
- directions = Makie.@lift asmakie.($vecs)
- Makie.arrows!(plot, origins, directions, color=colorant)
+ T = Unitful.numtype(ℒ)
+ orig = Makie.@lift fill(zero(Makie.Point{Dim,T}), length($vecs))
+ dirs = Makie.@lift asmakie.($vecs)
+ size = Makie.@lift 0.1 * $segmentsize
+ Makie.arrows!(plot, orig, dirs, color=colorant, arrowsize=size)
end
diff --git a/src/Meshes.jl b/src/Meshes.jl
index 1643fd4cd..cf5b60247 100644
--- a/src/Meshes.jl
+++ b/src/Meshes.jl
@@ -4,6 +4,7 @@
module Meshes
+using CoordRefSystems
using StaticArrays
using SparseArrays
using CircularArrays
@@ -14,10 +15,20 @@ using Random
using Bessels: gamma
using Unitful: AbstractQuantity, numtype
using StatsBase: AbstractWeights, Weights, quantile
-using Distances: PreMetric, Euclidean, Mahalanobis, evaluate
-using Rotations: Rotation, QuatRotation, Angle2d, rotation_between
-using NearestNeighbors: KDTree, BallTree, knn, inrange
-using Transducers: Filter, Map, TakeWhile, tcollect, ⨟
+using Distances: PreMetric, Euclidean, Mahalanobis
+using Distances: Haversine, SphericalAngle
+using Distances: evaluate, result_type
+using Rotations: Rotation, QuatRotation, Angle2d
+using Rotations: rotation_between
+using TiledIteration: TileIterator
+using CoordRefSystems: Basic, Projected, Geographic
+using NearestNeighbors: KDTree, BallTree
+using NearestNeighbors: knn, inrange
+using DelaunayTriangulation: triangulate, voronoi
+using DelaunayTriangulation: each_solid_triangle
+using DelaunayTriangulation: get_polygons
+using DelaunayTriangulation: get_polygon_points
+using ScopedValues: ScopedValue
using Base.Cartesian: @nloops, @nref, @ntuple
using Base: @propagate_inbounds
@@ -25,6 +36,7 @@ import Random
import Base: sort
import Base: ==, !
import Base: +, -, *
+import Base: <, >, ≤, ≥
import StatsBase: sample
import Distances: evaluate
import NearestNeighbors: MinkowskiMetric
@@ -33,7 +45,13 @@ import NearestNeighbors: MinkowskiMetric
import TransformsBase: Transform, →
import TransformsBase: isrevertible, isinvertible
import TransformsBase: apply, revert, reapply, inverse
-import TransformsBase: parameters
+import TransformsBase: parameters, preprocess
+
+# CoordRefSystems API
+import CoordRefSystems: lentype
+
+# unit utils
+include("units.jl")
# IO utils
include("ioutils.jl")
@@ -44,6 +62,9 @@ include("tolerances.jl")
# basic vector type
include("vectors.jl")
+# manifold types
+include("manifolds.jl")
+
# geometries
include("geometries.jl")
@@ -58,8 +79,8 @@ include("domains.jl")
# utilities
include("utils.jl")
-# domain views
-include("viewing.jl")
+# domain indices
+include("indices.jl")
# domain partitions
include("partitions.jl")
@@ -79,11 +100,12 @@ include("neighborsearch.jl")
include("predicates.jl")
# operations
+include("centroid.jl")
+include("measures.jl")
+include("boundary.jl")
include("winding.jl")
include("sideof.jl")
include("orientation.jl")
-include("measures.jl")
-include("boundary.jl")
include("merging.jl")
include("clipping.jl")
include("clamping.jl")
@@ -94,13 +116,16 @@ include("boundingboxes.jl")
include("hulls.jl")
include("sampling.jl")
include("pointification.jl")
+include("tesselation.jl")
include("discretization.jl")
include("refinement.jl")
+include("coarsening.jl")
# transforms
include("transforms.jl")
# miscellaneous
+include("rand.jl")
include("distances.jl")
include("supportfun.jl")
include("matrices.jl")
@@ -112,40 +137,33 @@ include("viz.jl")
export
# vectors
Vec,
- Vec1,
- Vec2,
- Vec3,
- Vec1f,
- Vec2f,
- Vec3f,
∠,
⋅,
×,
+ # manifolds
+ 𝔼,
+ 🌐,
+
# geometries
Geometry,
embeddim,
paramdim,
- coordtype,
- center,
- centroid,
+ crs,
+ manifold,
# primitives
Primitive,
Point,
- Point1,
- Point2,
- Point3,
- Point1f,
- Point2f,
- Point3f,
Ray,
Line,
BezierCurve,
+ ParametrizedCurve,
Plane,
Box,
Ball,
Sphere,
+ Ellipsoid,
Disk,
Circle,
Cylinder,
@@ -161,7 +179,9 @@ export
degree,
Horner,
DeCasteljau,
- coordinates,
+ coords,
+ to,
+ center,
radius,
radii,
plane,
@@ -174,10 +194,7 @@ export
sides,
diagonal,
focallength,
- ⪯,
- ≺,
- ⪰,
- ≻,
+ direction,
# polytopes
Polytope,
@@ -198,16 +215,19 @@ export
PolyArea,
Polyhedron,
Tetrahedron,
- Pyramid,
Hexahedron,
+ Pyramid,
+ Wedge,
vertex,
vertices,
nvertices,
+ eachvertex,
rings,
segments,
angles,
innerangles,
normal,
+ ≗,
# multi-geometries
Multi,
@@ -218,6 +238,9 @@ export
MultiPolygon,
MultiPolyhedron,
+ # transformed geometry
+ TransformedGeometry,
+
# connectivities
Connectivity,
paramdim,
@@ -269,7 +292,8 @@ export
SubDomain,
embeddim,
paramdim,
- coordtype,
+ crs,
+ manifold,
element,
nelements,
@@ -280,16 +304,17 @@ export
# meshes
Mesh,
Grid,
- SubGrid,
+ SimpleMesh,
+ TransformedMesh,
+ RegularGrid,
CartesianGrid,
RectilinearGrid,
StructuredGrid,
- SimpleMesh,
- TransformedMesh,
TransformedGrid,
vertex,
vertices,
nvertices,
+ eachvertex,
element,
elements,
nelements,
@@ -304,7 +329,7 @@ export
# trajectories
CylindricalTrajectory,
- # viewing
+ # indices
indices,
# partitions
@@ -377,6 +402,22 @@ export
intersects,
iscollinear,
iscoplanar,
+ ≺,
+ ≻,
+ ⪯,
+ ⪰,
+
+ # centroids
+ centroid,
+
+ # measures
+ measure,
+ area,
+ volume,
+ perimeter,
+
+ # boundary
+ boundary,
# winding number
winding,
@@ -391,26 +432,14 @@ export
RIGHT,
# orientation
- OrientationMethod,
- WindingOrientation,
- TriangleOrientation,
orientation,
OrientationType,
CW,
CCW,
- # measures
- measure,
- area,
- volume,
- perimeter,
-
- # boundary
- boundary,
-
# clipping
ClippingMethod,
- SutherlandHodgman,
+ SutherlandHodgmanClipping,
clip,
# intersections
@@ -434,10 +463,10 @@ export
# simplification
SimplificationMethod,
- DouglasPeucker,
- Selinger,
+ SelingerSimplification,
+ DouglasPeuckerSimplification,
+ MinMaxSimplification,
simplify,
- decimate,
# bounding boxes
boundingbox,
@@ -460,20 +489,29 @@ export
RegularSampling,
HomogeneousSampling,
MinDistanceSampling,
+ FibonacciSampling,
sampleinds,
sample,
# pointification
pointify,
+ # tesselation
+ TesselationMethod,
+ DelaunayTesselation,
+ VoronoiTesselation,
+ tesselate,
+
# discretization
DiscretizationMethod,
- BoundaryDiscretizationMethod,
+ BoundaryTriangulationMethod,
FanTriangulation,
+ DehnTriangulation,
+ HeldTriangulation,
+ DelaunayTriangulation,
+ ManualSimplexification,
RegularDiscretization,
- FIST,
- Dehn1899,
- Tetrahedralization,
+ MaxLengthDiscretization,
discretize,
discretizewithin,
simplexify,
@@ -482,25 +520,38 @@ export
RefinementMethod,
TriRefinement,
QuadRefinement,
- CatmullClark,
+ RegularRefinement,
+ CatmullClarkRefinement,
TriSubdivision,
refine,
+ # coarsening
+ CoarseningMethod,
+ RegularCoarsening,
+ coarsen,
+
# transforms
GeometricTransform,
CoordinateTransform,
Rotate,
Translate,
- Affine,
Scale,
+ Affine,
Stretch,
StdCoords,
+ Proj,
+ Morphological,
+ LengthUnit,
+ Shadow,
+ Slice,
Repair,
Bridge,
LambdaMuSmoothing,
LaplaceSmoothing,
TaubinSmoothing,
isaffine,
+ isinvertible,
+ inverse,
# miscellaneous
signarea,
diff --git a/src/boundary.jl b/src/boundary.jl
index 1d7826f45..a4b85c262 100644
--- a/src/boundary.jl
+++ b/src/boundary.jl
@@ -21,62 +21,75 @@ function boundary(b::BezierCurve)
p₁ ≈ p₂ ? nothing : Multi([p₁, p₂])
end
+function boundary(c::ParametrizedCurve)
+ p₁, p₂ = extrema(c)
+ p₁ ≈ p₂ ? nothing : Multi([p₁, p₂])
+end
+
boundary(::Plane) = nothing
-boundary(b::Box{1}) = Multi([minimum(b), maximum(b)])
+boundary(b::Box{𝔼{1}}) = Multi([minimum(b), maximum(b)])
-function boundary(b::Box{2})
- A = coordinates(minimum(b))
- B = coordinates(maximum(b))
- v = Point.([(A[1], A[2]), (B[1], A[2]), (B[1], B[2]), (A[1], B[2])])
+function boundary(b::Box{𝔼{2}})
+ A = convert(Cartesian, coords(minimum(b)))
+ B = convert(Cartesian, coords(maximum(b)))
+ v = [withcrs(b, (A.x, A.y)), withcrs(b, (B.x, A.y)), withcrs(b, (B.x, B.y)), withcrs(b, (A.x, B.y))]
Ring(v)
end
-function boundary(b::Box{3})
- A = coordinates(minimum(b))
- B = coordinates(maximum(b))
- v =
- Point.([
- (A[1], A[2], A[3]),
- (B[1], A[2], A[3]),
- (B[1], B[2], A[3]),
- (A[1], B[2], A[3]),
- (A[1], A[2], B[3]),
- (B[1], A[2], B[3]),
- (B[1], B[2], B[3]),
- (A[1], B[2], B[3])
- ])
+function boundary(b::Box{𝔼{3}})
+ A = convert(Cartesian, coords(minimum(b)))
+ B = convert(Cartesian, coords(maximum(b)))
+ v = [
+ withcrs(b, (A.x, A.y, A.z)),
+ withcrs(b, (B.x, A.y, A.z)),
+ withcrs(b, (B.x, B.y, A.z)),
+ withcrs(b, (A.x, B.y, A.z)),
+ withcrs(b, (A.x, A.y, B.z)),
+ withcrs(b, (B.x, A.y, B.z)),
+ withcrs(b, (B.x, B.y, B.z)),
+ withcrs(b, (A.x, B.y, B.z))
+ ]
c = [(4, 3, 2, 1), (6, 5, 1, 2), (3, 7, 6, 2), (4, 8, 7, 3), (1, 5, 8, 4), (6, 7, 8, 5)]
SimpleMesh(v, connect.(c))
end
+function boundary(b::Box{🌐})
+ A = convert(LatLon, coords(minimum(b)))
+ B = convert(LatLon, coords(maximum(b)))
+ v = [
+ withcrs(b, (A.lat, A.lon), LatLon),
+ withcrs(b, (A.lat, B.lon), LatLon),
+ withcrs(b, (B.lat, B.lon), LatLon),
+ withcrs(b, (B.lat, A.lon), LatLon)
+ ]
+ Ring(v)
+end
+
boundary(b::Ball) = Sphere(center(b), radius(b))
boundary(::Sphere) = nothing
+boundary(::Ellipsoid) = nothing
+
boundary(d::Disk) = Circle(plane(d), radius(d))
boundary(::Circle) = nothing
-boundary(c::Cone) = ConeSurface(base(c), apex(c))
-
-boundary(::ConeSurface) = nothing
-
boundary(c::Cylinder) = CylinderSurface(bottom(c), top(c), radius(c))
boundary(::CylinderSurface) = nothing
-boundary(::ParaboloidSurface) = nothing
+boundary(c::Cone) = ConeSurface(base(c), apex(c))
-function boundary(p::Pyramid)
- indices = [(4, 3, 2, 1), (5, 1, 2), (5, 4, 1), (5, 3, 4), (5, 2, 3)]
- SimpleMesh(pointify(p), connect.(indices))
-end
+boundary(::ConeSurface) = nothing
boundary(f::Frustum) = FrustumSurface(bottom(f), top(f))
boundary(::FrustumSurface) = nothing
+boundary(::ParaboloidSurface) = nothing
+
boundary(::Torus) = nothing
boundary(s::Segment) = Multi(pointify(s))
@@ -100,8 +113,24 @@ function boundary(h::Hexahedron)
SimpleMesh(pointify(h), connect.(indices))
end
+function boundary(p::Pyramid)
+ indices = [(4, 3, 2, 1), (5, 1, 2), (5, 4, 1), (5, 3, 4), (5, 2, 3)]
+ SimpleMesh(pointify(p), connect.(indices))
+end
+
+function boundary(w::Wedge)
+ indices = [(1, 3, 2), (4, 5, 6), (1, 2, 5, 4), (2, 3, 6, 5), (3, 1, 4, 6)]
+ SimpleMesh(pointify(w), connect.(indices))
+end
+
function boundary(m::Multi)
bounds = [boundary(geom) for geom in parent(m)]
valid = filter(!isnothing, bounds)
isempty(valid) ? nothing : reduce(merge, valid)
end
+
+function boundary(g::TransformedGeometry)
+ b = boundary(parent(g))
+ t = transform(g)
+ hasdistortedboundary(g) ? TransformedGeometry(b, t) : t(b)
+end
diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl
index e1088042e..51e824dc0 100644
--- a/src/boundingboxes.jl
+++ b/src/boundingboxes.jl
@@ -13,7 +13,7 @@ function boundingbox end
# FALLBACKS
# ----------
-boundingbox(p::Polytope) = _pboxes(vertices(p))
+boundingbox(p::Polytope) = _pboxes(eachvertex(p))
boundingbox(p::Primitive) = boundingbox(boundary(p))
@@ -29,43 +29,56 @@ boundingbox(p::Point) = Box(p, p)
boundingbox(b::Box) = b
-function boundingbox(r::Ray{Dim,T}) where {Dim,T}
- lower(p, v) = v < 0 ? typemin(T) : p
- upper(p, v) = v > 0 ? typemax(T) : p
+function boundingbox(r::Ray)
+ lower(p, v) = v < zero(v) ? typemin(p) : p
+ upper(p, v) = v > zero(v) ? typemax(p) : p
p = r(0)
v = r(1) - r(0)
- l = lower.(coordinates(p), v)
- u = upper.(coordinates(p), v)
- Box(Point(l), Point(u))
+ l = lower.(to(p), v)
+ u = upper.(to(p), v)
+ Box(withcrs(r, l), withcrs(r, u))
end
-function boundingbox(s::Sphere{Dim,T}) where {Dim,T}
+function boundingbox(s::Sphere)
c = center(s)
r = radius(s)
- r⃗ = Vec(ntuple(i -> r, Dim))
+ r⃗ = Vec(ntuple(i -> r, embeddim(s)))
Box(c - r⃗, c + r⃗)
end
-function boundingbox(p::ParaboloidSurface{T}) where {T}
+function boundingbox(c::CylinderSurface)
+ us = (0, 1 / 4, 1 / 2, 3 / 4)
+ vs = (0, 1 / 2, 1)
+ ps = [c(u, v) for (u, v) in Iterators.product(us, vs)]
+ boundingbox(ps)
+end
+
+function boundingbox(c::ConeSurface)
+ us = (0, 1 / 4, 1 / 2, 3 / 4)
+ vs = (0,)
+ ps = [c(u, v) for (u, v) in Iterators.product(us, vs)]
+ boundingbox([ps; apex(c)])
+end
+
+function boundingbox(p::ParaboloidSurface)
v = apex(p)
r = radius(p)
f = focallength(p)
- Box(v + Vec(-r, -r, T(0)), v + Vec(r, r, r^2 / (4f)))
+ Box(v + Vec(-r, -r, zero(r)), v + Vec(r, r, r^2 / (4f)))
end
boundingbox(t::Torus) = _pboxes(pointify(t))
-boundingbox(g::CartesianGrid) = Box(extrema(g)...)
+boundingbox(g::OrthoRegularGrid) = Box(extrema(g)...)
-boundingbox(g::RectilinearGrid) = Box(extrema(g)...)
+boundingbox(g::OrthoRectilinearGrid) = Box(extrema(g)...)
-boundingbox(g::TransformedGrid{Dim,T,<:CartesianGrid{Dim,T}}) where {Dim,T} =
- boundingbox(parent(g)) |> transform(g) |> boundingbox
+boundingbox(g::TransformedGrid{<:Any,<:Any,<:OrthoRegularGrid}) = boundingbox(parent(g)) |> transform(g) |> boundingbox
-boundingbox(g::TransformedGrid{Dim,T,<:RectilinearGrid{Dim,T}}) where {Dim,T} =
+boundingbox(g::TransformedGrid{<:Any,<:Any,<:OrthoRectilinearGrid}) =
boundingbox(parent(g)) |> transform(g) |> boundingbox
-boundingbox(m::Mesh) = _pboxes(vertices(m))
+boundingbox(m::Mesh) = _pboxes(eachvertex(m))
# ----------------
# IMPLEMENTATIONS
@@ -73,16 +86,64 @@ boundingbox(m::Mesh) = _pboxes(vertices(m))
_bboxes(boxes) = _pboxes(point for box in boxes for point in extrema(box))
-function _pboxes(points)
+_pboxes(points) = _pboxes(manifold(first(points)), points)
+
+function _pboxes(::Type{𝔼{1}}, points)
+ p = first(points)
+ ℒ = lentype(p)
+ xmin = typemax(ℒ)
+ xmax = typemin(ℒ)
+ for p in points
+ c = convert(Cartesian, coords(p))
+ xmin = min(c.x, xmin)
+ xmax = max(c.x, xmax)
+ end
+ Box(withcrs(p, (xmin,)), withcrs(p, (xmax,)))
+end
+
+function _pboxes(::Type{𝔼{2}}, points)
+ p = first(points)
+ ℒ = lentype(p)
+ xmin, ymin = typemax(ℒ), typemax(ℒ)
+ xmax, ymax = typemin(ℒ), typemin(ℒ)
+ for p in points
+ c = convert(Cartesian, coords(p))
+ xmin = min(c.x, xmin)
+ ymin = min(c.y, ymin)
+ xmax = max(c.x, xmax)
+ ymax = max(c.y, ymax)
+ end
+ Box(withcrs(p, (xmin, ymin)), withcrs(p, (xmax, ymax)))
+end
+
+function _pboxes(::Type{𝔼{3}}, points)
+ p = first(points)
+ ℒ = lentype(p)
+ xmin, ymin, zmin = typemax(ℒ), typemax(ℒ), typemax(ℒ)
+ xmax, ymax, zmax = typemin(ℒ), typemin(ℒ), typemin(ℒ)
+ for p in points
+ c = convert(Cartesian, coords(p))
+ xmin = min(c.x, xmin)
+ ymin = min(c.y, ymin)
+ zmin = min(c.z, zmin)
+ xmax = max(c.x, xmax)
+ ymax = max(c.y, ymax)
+ zmax = max(c.z, zmax)
+ end
+ Box(withcrs(p, (xmin, ymin, zmin)), withcrs(p, (xmax, ymax, zmax)))
+end
+
+function _pboxes(::Type{🌐}, points)
p = first(points)
- T = coordtype(p)
- Dim = embeddim(p)
- xmin = MVector(ntuple(i -> typemax(T), Dim))
- xmax = MVector(ntuple(i -> typemin(T), Dim))
+ T = numtype(lentype(p))
+ lonmin, latmin = T(180) * u"°", T(90) * u"°"
+ lonmax, latmax = T(-180) * u"°", T(-90) * u"°"
for p in points
- x = coordinates(p)
- @. xmin = min(x, xmin)
- @. xmax = max(x, xmax)
+ c = convert(LatLon, coords(p))
+ lonmin = min(c.lon, lonmin)
+ latmin = min(c.lat, latmin)
+ lonmax = max(c.lon, lonmax)
+ latmax = max(c.lat, latmax)
end
- Box(Point(xmin), Point(xmax))
+ Box(withcrs(p, (latmin, lonmin), LatLon), withcrs(p, (latmax, lonmax), LatLon))
end
diff --git a/src/centroid.jl b/src/centroid.jl
new file mode 100644
index 000000000..066430bf2
--- /dev/null
+++ b/src/centroid.jl
@@ -0,0 +1,80 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ centroid(geometry)
+
+The centroid of the `geometry`.
+"""
+centroid(g::Geometry) = center(g) # some geometries have a natural center
+
+centroid(p::Point) = p
+
+centroid(p::Polygon) = centroid(first(rings(p)))
+
+centroid(p::Polytope) = coordmean(vertices(p))
+
+centroid(b::Box) = coordmean(extrema(b))
+
+centroid(p::Plane) = p(0, 0)
+
+centroid(c::Cylinder) = centroid(boundary(c))
+
+function centroid(c::CylinderSurface)
+ a = centroid(bottom(c))
+ b = centroid(top(c))
+ withcrs(c, (to(a) + to(b)) / 2)
+end
+
+function centroid(p::ParaboloidSurface)
+ c = apex(p)
+ r = radius(p)
+ f = focallength(p)
+ z = r^2 / 4f
+ x = zero(z)
+ y = zero(z)
+ c + Vec(x, y, z / 2)
+end
+
+centroid(m::Multi) = centroid(GeometrySet(parent(m)))
+
+centroid(g::TransformedGeometry) = transform(g)(centroid(parent(g)))
+
+"""
+ centroid(domain)
+
+The centroid of the `domain`.
+"""
+function centroid(d::Domain)
+ vector(i) = to(centroid(d, i))
+ volume(i) = measure(element(d, i))
+ n = nelements(d)
+ x = vector.(1:n)
+ w = volume.(1:n)
+ all(iszero, w) && (w = ones(eltype(w), n))
+ withcrs(d, sum(w .* x) / sum(w))
+end
+
+"""
+ centroid(domain, ind)
+
+The centroid of the `ind`-th element of the `domain`.
+"""
+centroid(d::Domain, ind::Int) = centroid(d[ind])
+
+centroid(d::SubDomain, ind::Int) = centroid(parent(d), parentindices(d)[ind])
+
+function centroid(g::OrthoRegularGrid, ind::Int)
+ ijk = elem2cart(topology(g), ind)
+ vertex(g, ijk) + Vec(spacing(g) ./ 2)
+end
+
+function centroid(g::OrthoRectilinearGrid, ind::Int)
+ ijk = elem2cart(topology(g), ind)
+ p1 = vertex(g, ijk)
+ p2 = vertex(g, ijk .+ 1)
+ withcrs(g, (to(p1) + to(p2)) / 2)
+end
+
+centroid(m::TransformedMesh, ind::Int) = transform(m)(centroid(parent(m), ind))
diff --git a/src/clamping.jl b/src/clamping.jl
index d1f954227..ab58ea7ca 100644
--- a/src/clamping.jl
+++ b/src/clamping.jl
@@ -10,11 +10,11 @@ Clamp the coordinates of a [`Point`](@ref) to the edges of a [`Box`](@ref).
For each dimension, coordinates outside of the box are moved to the nearest
edge of the box. The point and box must have an equal number of dimensions.
"""
-function Base.clamp(point::Point{Dim}, box::Box{Dim}) where {Dim}
- x = coordinates(point)
- lo = coordinates(minimum(box))
- hi = coordinates(maximum(box))
- ntuple(Dim) do i
+function Base.clamp(point::Point, box::Box)
+ x = to(point)
+ lo = to(minimum(box))
+ hi = to(maximum(box))
+ ntuple(embeddim(point)) do i
clamp(x[i], lo[i], hi[i])
end |> Point
end
diff --git a/src/clipping/sutherlandhodgman.jl b/src/clipping/sutherlandhodgman.jl
index af74f20d0..aad74942b 100644
--- a/src/clipping/sutherlandhodgman.jl
+++ b/src/clipping/sutherlandhodgman.jl
@@ -3,7 +3,7 @@
# ------------------------------------------------------------------
"""
- SutherlandHodgman()
+ SutherlandHodgmanClipping()
The Sutherland-Hodgman algorithm for clipping polygons.
@@ -16,15 +16,15 @@ The Sutherland-Hodgman algorithm for clipping polygons.
* The algorithm assumes that the clipping geometry is convex.
"""
-struct SutherlandHodgman <: ClippingMethod end
+struct SutherlandHodgmanClipping <: ClippingMethod end
-function clip(poly::Polygon, other::Geometry, method::SutherlandHodgman)
+function clip(poly::Polygon, other::Geometry, method::SutherlandHodgmanClipping)
c = [clip(ring, boundary(other), method) for ring in rings(poly)]
r = [r for r in c if !isnothing(r)]
isempty(r) ? nothing : PolyArea(r)
end
-function clip(ring::Ring{Dim,T}, other::Ring{Dim,T}, ::SutherlandHodgman) where {Dim,T}
+function clip(ring::Ring, other::Ring, ::SutherlandHodgmanClipping)
# make sure other ring is CCW
occw = orientation(other) == CCW ? other : reverse(other)
@@ -36,7 +36,7 @@ function clip(ring::Ring{Dim,T}, other::Ring{Dim,T}, ::SutherlandHodgman) where
n = length(r)
- u = Point{Dim,T}[]
+ u = Vector{eltype(r)}()
for j in 1:n
r₁ = r[j]
r₂ = r[mod1(j + 1, n)]
@@ -49,9 +49,9 @@ function clip(ring::Ring{Dim,T}, other::Ring{Dim,T}, ::SutherlandHodgman) where
push!(u, r₁)
elseif isinside₁ && !isinside₂
push!(u, r₁)
- push!(u, lᵣ ∩ lₒ)
+ push!(u, intersectpoint(lᵣ, lₒ))
elseif !isinside₁ && isinside₂
- push!(u, lᵣ ∩ lₒ)
+ push!(u, intersectpoint(lᵣ, lₒ))
end
end
@@ -60,3 +60,10 @@ function clip(ring::Ring{Dim,T}, other::Ring{Dim,T}, ::SutherlandHodgman) where
isempty(r) ? nothing : Ring(unique(r))
end
+
+# helper function to find any intersection point
+# between crossing or overlapping lines
+function intersectpoint(l₁::Line, l₂::Line)
+ λ(I) = type(I) == Overlapping ? l₁(0) : get(I)
+ intersection(λ, l₁, l₂)
+end
diff --git a/src/coarsening.jl b/src/coarsening.jl
new file mode 100644
index 000000000..899eec22b
--- /dev/null
+++ b/src/coarsening.jl
@@ -0,0 +1,23 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ CoarseningMethod
+
+A method for coarsening meshes.
+"""
+abstract type CoarseningMethod end
+
+"""
+ coarsen(mesh, method)
+
+Coarsen `mesh` with coarsening `method`.
+"""
+function coarsen end
+
+# ----------------
+# IMPLEMENTATIONS
+# ----------------
+
+include("coarsening/regular.jl")
diff --git a/src/coarsening/regular.jl b/src/coarsening/regular.jl
new file mode 100644
index 000000000..e485c29c6
--- /dev/null
+++ b/src/coarsening/regular.jl
@@ -0,0 +1,64 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ RegularCoarsening(f₁, f₂, ..., fₙ)
+
+Coarsen each dimension of the grid by given factors `f₁`, `f₂`, ..., `fₙ`.
+
+## Examples
+
+```julia
+coarsen(grid2D, RegularCoarsening(2, 3))
+coarsen(grid3D, RegularCoarsening(2, 3, 1))
+```
+"""
+struct RegularCoarsening{N} <: CoarseningMethod
+ factors::Dims{N}
+end
+
+RegularCoarsening(factors::Vararg{Int,N}) where {N} = RegularCoarsening(factors)
+
+function coarsen(grid::OrthoRegularGrid, method::RegularCoarsening)
+ factors = fitdims(method.factors, paramdim(grid))
+ dims = _coarsesize(grid, factors)
+ RegularGrid(minimum(grid), maximum(grid), dims=dims)
+end
+
+function coarsen(grid::RectilinearGrid, method::RegularCoarsening)
+ factors = fitdims(method.factors, paramdim(grid))
+ inds = _coarseinds(grid, factors)
+ xyzₛ = xyz(grid)
+ xyzₜ = ntuple(i -> xyzₛ[i][inds[i]], paramdim(grid))
+ RectilinearGrid{manifold(grid),crs(grid)}(xyzₜ)
+end
+
+function coarsen(grid::StructuredGrid, method::RegularCoarsening)
+ factors = fitdims(method.factors, paramdim(grid))
+ inds = _coarseinds(grid, factors)
+ XYZₛ = XYZ(grid)
+ XYZₜ = ntuple(i -> XYZₛ[i][inds...], paramdim(grid))
+ StructuredGrid{manifold(grid),crs(grid)}(XYZₜ)
+end
+
+coarsen(grid::TransformedGrid, method::RegularCoarsening) =
+ TransformedGrid(coarsen(parent(grid), method), transform(grid))
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function _coarsesize(grid, factors)
+ dims = size(grid)
+ axes = ntuple(i -> 1:dims[i], paramdim(grid))
+ size(TileIterator(axes, factors))
+end
+
+_coarsevsize(grid, factors) = _coarsesize(grid, factors) .+ .!isperiodic(grid)
+
+function _coarseinds(grid, factors)
+ dims = vsize(grid)
+ tdims = _coarsevsize(grid, factors)
+ ntuple(i -> floor.(Int, range(start=1, stop=dims[i], length=tdims[i])), paramdim(grid))
+end
diff --git a/src/complement.jl b/src/complement.jl
index 1f1b3895e..17604712e 100644
--- a/src/complement.jl
+++ b/src/complement.jl
@@ -11,11 +11,11 @@ respect to its bounding box.
!(g::Geometry) = _complement(_boxboundary(g), boundary(g))
function _boxboundary(g)
- T = coordtype(g)
+ ℒ = lentype(g)
b = boundingbox(g)
- c = coordinates(center(b))
+ c = to(centroid(b))
l = sides(b)
- α = (l .+ 2atol(T)) ./ l
+ α = (l .+ 2atol(ℒ)) ./ l
t = Translate(-c...) → Scale(α) → Translate(c...)
boundary(t(b))
end
diff --git a/src/connectivities.jl b/src/connectivities.jl
index 80194d354..923e6b69c 100644
--- a/src/connectivities.jl
+++ b/src/connectivities.jl
@@ -14,9 +14,9 @@ struct Connectivity{PL<:Polytope,N}
indices::NTuple{N,Int}
function Connectivity{PL,N}(indices) where {PL,N}
- @assert nvertices(PL) == N "cannot create a $PL with $N vertices"
+ assertion(nvertices(PL) == N, lazy"cannot create a $PL with $N vertices")
if PL <: Ngon
- @assert N ≥ 3 "Ngon requires 3 or more vertices"
+ assertion(N ≥ 3, "Ngon requires 3 or more vertices")
end
new(indices)
end
diff --git a/src/discretization.jl b/src/discretization.jl
index 97944f527..6108fba4d 100644
--- a/src/discretization.jl
+++ b/src/discretization.jl
@@ -9,6 +9,13 @@ A method for discretizing geometries into meshes.
"""
abstract type DiscretizationMethod end
+"""
+ TriangulationMethod
+
+A method for discretizing geometries into triangular meshes.
+"""
+abstract type TriangulationMethod <: DiscretizationMethod end
+
"""
discretize(geometry, [method])
@@ -20,11 +27,11 @@ used with a specific number of elements.
function discretize end
"""
- BoundaryDiscretizationMethod
+ BoundaryTriangulationMethod
-A method for discretizing geometries based on their boundary.
+A method for discretizing geometries into triangular meshes based on their boundary.
"""
-abstract type BoundaryDiscretizationMethod <: DiscretizationMethod end
+abstract type BoundaryTriangulationMethod <: TriangulationMethod end
"""
discretizewithin(boundary, method)
@@ -33,23 +40,79 @@ Discretize geometry within `boundary` with boundary discretization `method`.
"""
function discretizewithin end
-discretize(geometry::Geometry, method::BoundaryDiscretizationMethod) = discretizewithin(boundary(geometry), method)
+# -----------
+# DISCRETIZE
+# -----------
+
+discretize(geometry) = simplexify(geometry)
+
+discretize(ball::Ball{𝔼{2}}) = discretize(ball, RegularDiscretization(50))
+
+discretize(disk::Disk) = discretize(disk, RegularDiscretization(50))
+
+discretize(sphere::Sphere{𝔼{3}}) = discretize(sphere, RegularDiscretization(50))
+
+discretize(ellipsoid::Ellipsoid) = discretize(ellipsoid, RegularDiscretization(50))
+
+discretize(torus::Torus) = discretize(torus, RegularDiscretization(50))
+
+discretize(cyl::Cylinder) = discretize(cyl, RegularDiscretization(2, 50, 2))
+
+discretize(cylsurf::CylinderSurface) = discretize(cylsurf, RegularDiscretization(50, 2))
+
+discretize(consurf::ConeSurface) = discretize(consurf, RegularDiscretization(50, 2))
+
+discretize(frustsurf::FrustumSurface) = discretize(frustsurf, RegularDiscretization(50, 2))
+
+discretize(parsurf::ParaboloidSurface) = discretize(parsurf, RegularDiscretization(50))
+
+discretize(multi::Multi) = mapreduce(discretize, merge, parent(multi))
+
+function discretize(geometry::TransformedGeometry)
+ T = numtype(lentype(geometry))
+ mesh = if hasdistortedboundary(geometry)
+ discretize(parent(geometry), MaxLengthDiscretization(T(1000) * u"km"))
+ else
+ discretize(parent(geometry))
+ end
+ transform(geometry)(mesh)
+end
+
+discretize(mesh::Mesh) = mesh
+
+# ----------
+# FALLBACKS
+# ----------
+
+discretize(multi::Multi, method::DiscretizationMethod) =
+ mapreduce(geom -> discretize(geom, method), merge, parent(multi))
+
+discretize(geometry::TransformedGeometry, method::DiscretizationMethod) =
+ transform(geometry)(discretize(parent(geometry), method))
+
+# -----------------
+# BOUNDARY METHODS
+# -----------------
-function discretize(polygon::Polygon{Dim,T}, method::BoundaryDiscretizationMethod) where {Dim,T}
+discretize(geometry, method::BoundaryTriangulationMethod) = discretizewithin(boundary(geometry), method)
+
+discretize(multi::Multi, method::BoundaryTriangulationMethod) =
+ mapreduce(geom -> discretize(geom, method), merge, parent(multi))
+
+function discretize(polygon::Polygon, method::BoundaryTriangulationMethod)
# clean up polygon if necessary
- cpoly = polygon |> Repair{0}() |> Repair{8}()
+ cpoly = polygon |> Repair(0) |> Repair(8)
# handle degenerate polygons
if nvertices(cpoly) == 1
- v = first(vertices(cpoly))
- points = [v, v, v]
+ points = fill(vertex(cpoly, 1), 3)
connec = [connect((1, 2, 3))]
return SimpleMesh(points, connec)
end
# build bridges in case the polygon has holes,
# i.e. reduce to a single outer boundary
- bpoly, dups = apply(Bridge(2atol(T)), cpoly)
+ bpoly, dups = apply(Bridge(2atol(lentype(polygon))), cpoly)
# discretize using outer boundary
mesh = discretizewithin(boundary(bpoly), method)
@@ -81,51 +144,37 @@ function discretize(polygon::Polygon{Dim,T}, method::BoundaryDiscretizationMetho
end
end
end
- connec = connect.(Tuple.(einds))
+ connec = [connect(ntuple(i -> inds[i], 3)) for inds in einds]
# return mesh without duplicates
SimpleMesh(points, connec)
end
end
-discretize(multi::Multi, method::BoundaryDiscretizationMethod) =
- mapreduce(geom -> discretize(geom, method), merge, parent(multi))
-
-function discretizewithin(ring::Ring{3}, method::BoundaryDiscretizationMethod)
- # collect vertices to get rid of static containers
- points = collect(vertices(ring))
+function discretizewithin(ring::Ring, method::BoundaryTriangulationMethod)
+ # retrieve vertices of ring
+ points = collect(eachvertex(ring))
# discretize within 2D ring with given method
- ring2D = Ring(proj2D(points))
+ ring2D = Ring(_proj2D(manifold(ring), points))
mesh = discretizewithin(ring2D, method)
# return mesh with original points
SimpleMesh(points, topology(mesh))
end
-# ----------------
-# DEFAULT METHODS
-# ----------------
-
-discretize(geometry) = simplexify(geometry)
-
-discretize(ball::Ball{2}) = discretize(ball, RegularDiscretization(50))
-
-discretize(disk::Disk) = discretize(disk, RegularDiscretization(50))
-
-discretize(sphere::Sphere{3}) = discretize(sphere, RegularDiscretization(50))
-
-discretize(torus::Torus) = discretize(torus, RegularDiscretization(50))
-
-discretize(cylsurf::CylinderSurface) = discretize(cylsurf, RegularDiscretization(50, 2))
-
-discretize(consurf::ConeSurface) = discretize(consurf, RegularDiscretization(50, 2))
+_proj2D(::Type{𝔼{3}}, points) = proj2D(points)
-discretize(parsurf::ParaboloidSurface) = discretize(parsurf, RegularDiscretization(50))
-
-discretize(multi::Multi) = mapreduce(discretize, merge, parent(multi))
+function _proj2D(::Type{🌐}, points)
+ map(points) do p
+ latlon = convert(LatLon, coords(p))
+ flat(Point(latlon))
+ end
+end
-discretize(mesh::Mesh) = mesh
+# -----------
+# SIMPLEXIFY
+# -----------
"""
simplexify(object)
@@ -142,15 +191,13 @@ function simplexify end
simplexify(geometry) = simplexify(discretize(geometry))
-simplexify(box::Box{1}) = SimpleMesh(collect(extrema(box)), GridTopology(1))
-
-simplexify(seg::Segment) = SimpleMesh(pointify(seg), GridTopology(1))
+simplexify(box::Box) = discretize(box, ManualSimplexification())
function simplexify(chain::Chain)
np = nvertices(chain) + isclosed(chain)
ip = isperiodic(chain)
- points = collect(vertices(chain))
+ points = collect(eachvertex(chain))
topo = GridTopology((np - 1,), ip)
SimpleMesh(points, topo)
@@ -158,41 +205,42 @@ end
simplexify(bezier::BezierCurve) = discretize(bezier, RegularDiscretization(50))
-simplexify(sphere::Sphere{2}) = discretize(sphere, RegularDiscretization(50))
+simplexify(curve::ParametrizedCurve) = discretize(curve, RegularDiscretization(50))
-simplexify(circle::Circle) = discretize(circle, RegularDiscretization(50))
-
-simplexify(box::Box{2}) = discretize(box, FanTriangulation())
+simplexify(sphere::Sphere{𝔼{2}}) = discretize(sphere, RegularDiscretization(50))
-simplexify(box::Box{3}) = discretize(box, Tetrahedralization())
+simplexify(circle::Circle) = discretize(circle, RegularDiscretization(50))
-simplexify(poly::Polygon) = discretize(poly, nvertices(poly) > 5000 ? FIST() : Dehn1899())
+simplexify(poly::Polygon) = discretize(poly, nvertices(poly) > 5000 ? DelaunayTriangulation() : DehnTriangulation())
-simplexify(poly::Polyhedron) = discretize(poly, Tetrahedralization())
+simplexify(poly::Polyhedron) = discretize(poly, ManualSimplexification())
simplexify(multi::Multi) = mapreduce(simplexify, merge, parent(multi))
function simplexify(mesh::Mesh)
+ # retrieve vertices and topology
points = vertices(mesh)
- elems = elements(mesh)
topo = topology(mesh)
- connec = elements(topo)
+
+ # check if there is something to do
+ all(issimplex, elements(topo)) && return mesh
# initialize vector of global indices
ginds = Vector{Int}[]
# simplexify each element and append global indices
- for (e, c) in zip(elems, connec)
- # simplexify single element
- mesh′ = simplexify(e)
- topo′ = topology(mesh′)
- connec′ = elements(topo′)
+ for connec in elements(topo)
+ # materialize element and indices
+ elem = materialize(connec, points)
+ inds = indices(connec)
- # global indices
- inds = indices(c)
+ # simplexify element
+ mesh′ = simplexify(elem)
+ topo′ = topology(mesh′)
+ connecs′ = elements(topo′)
# convert from local to global indices
- einds = [[inds[i] for i in indices(c′)] for c′ in connec′]
+ einds = [[inds[i] for i in indices(c′)] for c′ in connecs′]
# save global indices
append!(ginds, einds)
@@ -200,11 +248,12 @@ function simplexify(mesh::Mesh)
# simplex type for parametric dimension
PL = paramdim(mesh) == 2 ? Triangle : Tetrahedron
+ NV = nvertices(PL)
# new connectivities
- newconnec = connect.(Tuple.(ginds), PL)
+ newconnecs = [connect(ntuple(i -> inds[i], NV), PL) for inds in ginds]
- SimpleMesh(points, newconnec)
+ SimpleMesh(points, newconnecs)
end
# ----------------
@@ -212,7 +261,9 @@ end
# ----------------
include("discretization/fan.jl")
-include("discretization/regular.jl")
-include("discretization/fist.jl")
include("discretization/dehn.jl")
-include("discretization/tetra.jl")
+include("discretization/held.jl")
+include("discretization/delaunay.jl")
+include("discretization/manual.jl")
+include("discretization/regular.jl")
+include("discretization/maxlength.jl")
diff --git a/src/discretization/dehn.jl b/src/discretization/dehn.jl
index 37373708d..b04f3b380 100644
--- a/src/discretization/dehn.jl
+++ b/src/discretization/dehn.jl
@@ -3,7 +3,7 @@
# ------------------------------------------------------------------
"""
- Dehn1899()
+ DehnTriangulation()
Max Dehns' triangulation proved in 1899.
@@ -19,25 +19,21 @@ with small number of vertices.
* Devadoss, S & Rourke, J. 2011. [Discrete and computational geometry]
(https://press.princeton.edu/books/hardcover/9780691145532/discrete-and-computational-geometry)
"""
-struct Dehn1899 <: BoundaryDiscretizationMethod end
+struct DehnTriangulation <: BoundaryTriangulationMethod end
-function discretizewithin(ring::Ring{2}, ::Dehn1899)
- # points on resulting mesh
- points = collect(vertices(ring))
-
- # Dehn's recursion
+function discretizewithin(ring::Ring{𝔼{2}}, ::DehnTriangulation)
+ points = collect(eachvertex(ring))
connec = dehn1899(points, 1:length(points))
-
SimpleMesh(points, connec)
end
-function dehn1899(v::AbstractVector{Point{Dim,T}}, inds) where {Dim,T}
+function dehn1899(v::AbstractVector{<:Point}, inds)
I = CircularVector(inds)
n = length(I)
if n > 3 # split chain
# find lowerleft vertex
- i = first(sortperm(coordinates.(v[I])))
+ i = first(sortperm(to.(v[I])))
# left/right chains
linds = (i - 1):(i + 1)
@@ -70,6 +66,6 @@ function dehn1899(v::AbstractVector{Point{Dim,T}}, inds) where {Dim,T}
[left; right]
else
# return the triangle
- [connect(Tuple(inds), Triangle)]
+ [connect(ntuple(i -> inds[i], 3))]
end
end
diff --git a/src/discretization/delaunay.jl b/src/discretization/delaunay.jl
new file mode 100644
index 000000000..fba17590d
--- /dev/null
+++ b/src/discretization/delaunay.jl
@@ -0,0 +1,34 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ DelaunayTriangulation()
+
+Constrained Delaunay triangulation of polygons.
+Optionally, specify the random number generator `rng`.
+
+## References
+
+* Cheng et al. 2012. [Delaunay Mesh Generation]
+ (https://people.eecs.berkeley.edu/~jrs/meshbook.html)
+
+### Notes
+
+Wraps DelaunayTriangulation.jl. For any internal errors, file an issue at
+[DelaunayTriangulation.jl](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/issues/new)
+"""
+struct DelaunayTriangulation{RNG<:AbstractRNG} <: BoundaryTriangulationMethod
+ rng::RNG
+end
+
+DelaunayTriangulation(rng=Random.default_rng()) = DelaunayTriangulation(rng)
+
+function discretizewithin(ring::Ring{𝔼{2}}, method::DelaunayTriangulation)
+ points = collect(eachvertex(ring))
+ coords = map(p -> ustrip.(to(p)), points)
+ bnodes = [1:nvertices(ring); 1]
+ triang = triangulate(coords, boundary_nodes=bnodes, rng=method.rng)
+ connec = connect.(each_solid_triangle(triang))
+ SimpleMesh(points, connec)
+end
diff --git a/src/discretization/fan.jl b/src/discretization/fan.jl
index 31f8bb801..3e4bef8ce 100644
--- a/src/discretization/fan.jl
+++ b/src/discretization/fan.jl
@@ -9,14 +9,10 @@ The fan triangulation algorithm for convex polygons.
See [https://en.wikipedia.org/wiki/Fan_triangulation]
(https://en.wikipedia.org/wiki/Fan_triangulation).
"""
-struct FanTriangulation <: BoundaryDiscretizationMethod end
+struct FanTriangulation <: BoundaryTriangulationMethod end
-discretizewithin(ring::Ring{2}, ::FanTriangulation) = fan(ring)
-
-discretizewithin(ring::Ring{3}, ::FanTriangulation) = fan(ring)
-
-function fan(ring::Ring)
- points = collect(vertices(ring))
+function discretizewithin(ring::Ring, ::FanTriangulation)
+ points = collect(eachvertex(ring))
connec = [connect((1, i, i + 1)) for i in 2:(nvertices(ring) - 1)]
SimpleMesh(points, connec)
end
diff --git a/src/discretization/fist.jl b/src/discretization/held.jl
similarity index 60%
rename from src/discretization/fist.jl
rename to src/discretization/held.jl
index 8e5b89c94..875e9d17a 100644
--- a/src/discretization/fist.jl
+++ b/src/discretization/held.jl
@@ -3,7 +3,7 @@
# ------------------------------------------------------------------
"""
- FIST([rng]; shuffle=true)
+ HeldTriangulation([rng]; shuffle=true)
Fast Industrial-Strength Triangulation (FIST) of polygons.
@@ -26,32 +26,32 @@ generator `rng`.
constrained Delaunay triangulation of polygons]
(https://www.sciencedirect.com/science/article/pii/S092577211830004X)
"""
-struct FIST{RNG<:AbstractRNG} <: BoundaryDiscretizationMethod
+struct HeldTriangulation{RNG<:AbstractRNG} <: BoundaryTriangulationMethod
rng::RNG
shuffle::Bool
end
-FIST(rng=Random.default_rng(); shuffle=true) = FIST(rng, shuffle)
+HeldTriangulation(rng=Random.default_rng(); shuffle=true) = HeldTriangulation(rng, shuffle)
-function discretizewithin(ring::Ring{2}, method::FIST)
+function discretizewithin(ring::Ring{𝔼{2}}, method::HeldTriangulation)
# helper function to shuffle ears
earshuffle!(𝒬) = method.shuffle && shuffle!(method.rng, 𝒬)
# input ring
- O = orientation(ring, TriangleOrientation())
+ O = orientation(ring)
ℛ = O == CCW ? ring : reverse(ring)
# standardize coordinates
𝒫 = ℛ |> StdCoords()
# points of resulting mesh
- points = collect(vertices(ℛ))
+ points = collect(eachvertex(ℛ))
# standardized points for algorithm
- stdpts = collect(vertices(𝒫))
+ stdpts = collect(eachvertex(𝒫))
# keep track of global indices
- inds = CircularVector(1:nvertices(𝒫))
+ I = CircularVector(1:nvertices(𝒫))
# perform ear clipping
𝒬 = earsccw(𝒫)
@@ -65,10 +65,10 @@ function discretizewithin(ring::Ring{2}, method::FIST)
i = pop!(𝒬)
𝒬[𝒬 .> i] .-= 1
# 1. push a new triangle to 𝒯
- push!(𝒯, connect((inds[i - 1], inds[i], inds[i + 1])))
+ push!(𝒯, connect((I[i - 1], I[i], I[i + 1])))
# 2. remove the vertex from 𝒫
- inds = inds[setdiff(1:n, mod1(i, n))]
- 𝒫 = Ring(stdpts[inds])
+ I = I[setdiff(1:n, mod1(i, n))]
+ 𝒫 = Ring(stdpts[I])
n = nvertices(𝒫)
# 3. update 𝒬 near clipped ear
for j in (i - 1, i)
@@ -93,19 +93,51 @@ function discretizewithin(ring::Ring{2}, method::FIST)
λ(I) = type(I) == Crossing
if intersection(λ, s1, s2)
# 1. push a new triangle to 𝒯
- push!(𝒯, connect((inds[i], inds[i + 1], inds[i + 2])))
+ push!(𝒯, connect((I[i], I[i + 1], I[i + 2])))
# 2. remove the vertex from 𝒫
- inds = inds[setdiff(1:n, mod1(i + 1, n))]
- 𝒫 = Ring(stdpts[inds])
+ I = I[setdiff(1:n, mod1(i + 1, n))]
+ 𝒫 = Ring(stdpts[I])
n = nvertices(𝒫)
clipped = true
break
end
end
+
+ # consecutive vertices vᵢ-1, vᵢ, vᵢ+1 form a valid ear
+ # if vᵢ-1 lies on the edge vᵢ+1 -- vᵢ+2
+ v = vertices(𝒫)
+ for i in 1:n
+ if v[i - 1] ∈ Segment(v[i + 1], v[i + 2])
+ # 1. push a new triangle to 𝒯
+ push!(𝒯, connect((I[i - 1], I[i], I[i + 1])))
+ # 2. remove the vertex from 𝒫
+ I = I[setdiff(1:n, mod1(i, n))]
+ 𝒫 = Ring(stdpts[I])
+ n = nvertices(𝒫)
+ clipped = true
+ break
+ end
+ end
+
+ # enter in "desperate" mode and clip ears at random
+ if !clipped
+ # attempt to clip a convex vertex
+ isconvex(i) = vexity(v, i) == :CONVEX
+ j = findfirst(isconvex, 1:n)
+ i = isnothing(j) ? rand(method.rng, 1:n) : j
+ # 1. push a new triangle to 𝒯
+ push!(𝒯, connect((I[i - 1], I[i], I[i + 1])))
+ # 2. remove the vertex from 𝒫
+ I = I[setdiff(1:n, mod1(i, n))]
+ 𝒫 = Ring(stdpts[I])
+ n = nvertices(𝒫)
+ clipped = true
+ end
end
end
+
# remaining polygonal area is the last triangle
- push!(𝒯, connect((inds[1], inds[2], inds[3])))
+ push!(𝒯, connect((I[1], I[2], I[3])))
SimpleMesh(points, 𝒯)
end
@@ -116,29 +148,11 @@ earsccw(𝒫) = filter(i -> isearccw(𝒫, i), 1:nvertices(𝒫))
# tells whether or not vertex i is an ear of 𝒫
# assuming that 𝒫 has counter-clockwise orientation
-function isearccw(𝒫::Ring{Dim,T}, i) where {Dim,T}
+function isearccw(𝒫::Ring, i)
v = vertices(𝒫)
- # helper function to compute the vexity of vertex i
- function vexity(i)
- α = ∠(v[i - 1], v[i], v[i + 1]) # oriented angle
- θ = α > 0 ? 2 * T(π) - α : -α # inner angle
- θ < π ? :CONVEX : :REFLEX
- end
-
- # helper function to check if vertex j is inside cone i
- function incone(j, i)
- s1 = sideof(v[j], Line(v[i], v[i - 1]))
- s2 = sideof(v[j], Line(v[i], v[i + 1]))
- if vexity(i) == :CONVEX
- s1 != LEFT && s2 != RIGHT
- else
- s1 != LEFT || s2 != RIGHT
- end
- end
-
# CE1.1: classify angle as convex vs. reflex
- isconvex = vexity(i) == :CONVEX
+ isconvex = vexity(v, i) == :CONVEX
# CE1.2: check if segment vᵢ-₁ -- vᵢ+₁ intersects 𝒫
λ(I) = !(type(I) == CornerTouching || type(I) == NotIntersecting)
@@ -153,7 +167,25 @@ function isearccw(𝒫::Ring{Dim,T}, i) where {Dim,T}
end
# CE1.3: check if vᵢ-1 ∈ C(vᵢ, vᵢ+1, vᵢ+2) and vᵢ+1 ∈ C(vᵢ-2, vᵢ-1, vᵢ)
- incones = incone(i - 1, i + 1) && incone(i + 1, i - 1)
+ incones = incone(v, i - 1, i + 1) && incone(v, i + 1, i - 1)
isconvex && !hasintersect && incones
end
+
+# helper function to compute the vexity of vertex i
+function vexity(v, i)
+ α = ∠(v[i - 1], v[i], v[i + 1]) # oriented angle
+ θ = α > 0 ? oftype(α, 2π) - α : -α # inner angle
+ θ < π ? :CONVEX : :REFLEX
+end
+
+# helper function to check if vertex j is inside cone i
+function incone(v, j, i)
+ s1 = sideof(v[j], Line(v[i], v[i - 1]))
+ s2 = sideof(v[j], Line(v[i], v[i + 1]))
+ if vexity(v, i) == :CONVEX
+ s1 != LEFT && s2 != RIGHT
+ else
+ s1 != LEFT || s2 != RIGHT
+ end
+end
diff --git a/src/discretization/manual.jl b/src/discretization/manual.jl
new file mode 100644
index 000000000..7429791f1
--- /dev/null
+++ b/src/discretization/manual.jl
@@ -0,0 +1,42 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ ManualSimplexification()
+
+Simplexify geometries manually using indices of vertices.
+"""
+struct ManualSimplexification <: DiscretizationMethod end
+
+discretize(box::Box{𝔼{1}}, ::ManualSimplexification) = SimpleMesh(collect(extrema(box)), GridTopology(1))
+
+function discretize(box::Box{𝔼{2}}, ::ManualSimplexification)
+ indices = [(1, 2, 3), (1, 3, 4)]
+ SimpleMesh(pointify(box), connect.(indices, Triangle))
+end
+
+function discretize(box::Box{𝔼{3}}, ::ManualSimplexification)
+ indices = [(1, 5, 6, 8), (1, 3, 4, 8), (1, 3, 6, 8), (1, 2, 3, 6), (3, 6, 7, 8)]
+ SimpleMesh(pointify(box), connect.(indices, Tetrahedron))
+end
+
+function discretize(box::Box{🌐}, ::ManualSimplexification)
+ indices = [(1, 2, 3), (1, 3, 4)]
+ SimpleMesh(pointify(box), connect.(indices, Triangle))
+end
+
+function discretize(hexa::Hexahedron, ::ManualSimplexification)
+ indices = [(1, 5, 6, 8), (1, 3, 4, 8), (1, 3, 6, 8), (1, 2, 3, 6), (3, 6, 7, 8)]
+ SimpleMesh(pointify(hexa), connect.(indices, Tetrahedron))
+end
+
+function discretize(pyramid::Pyramid, ::ManualSimplexification)
+ indices = [(1, 2, 4, 5), (3, 4, 2, 5)]
+ SimpleMesh(pointify(pyramid), connect.(indices, Tetrahedron))
+end
+
+function discretize(wedge::Wedge, ::ManualSimplexification)
+ indices = [(1, 2, 3, 4), (4, 5, 6, 2), (4, 5, 6, 3)]
+ SimpleMesh(pointify(wedge), connect.(indices, Tetrahedron))
+end
diff --git a/src/discretization/maxlength.jl b/src/discretization/maxlength.jl
new file mode 100644
index 000000000..99bb8ba5b
--- /dev/null
+++ b/src/discretization/maxlength.jl
@@ -0,0 +1,61 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ MaxLengthDiscretization(length)
+
+Discretize geometries into parts with sides of maximum `length` in length units (default to meters).
+"""
+struct MaxLengthDiscretization{ℒ<:Len} <: DiscretizationMethod
+ length::ℒ
+ MaxLengthDiscretization(length::ℒ) where {ℒ<:Len} = new{float(ℒ)}(length)
+end
+
+MaxLengthDiscretization(length) = MaxLengthDiscretization(addunit(length, u"m"))
+
+function discretize(box::Box, method::MaxLengthDiscretization)
+ sizes = ceil.(Int, _sides(box) ./ method.length)
+ discretize(box, RegularDiscretization(sizes))
+end
+
+function discretize(segment::Segment, method::MaxLengthDiscretization)
+ size = ceil(Int, measure(segment) / method.length)
+ discretize(segment, RegularDiscretization(size))
+end
+
+discretize(chain::Chain, method::MaxLengthDiscretization) =
+ mapreduce(s -> discretize(s, method), merge, segments(chain))
+
+discretize(multi::Multi, method::MaxLengthDiscretization) = _iterativerefinement(multi, method)
+
+discretize(geometry::TransformedGeometry, method::MaxLengthDiscretization) =
+ transform(geometry)(discretize(parent(geometry), method))
+
+discretize(geometry::Geometry, method::MaxLengthDiscretization) = _iterativerefinement(geometry, method)
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function _iterativerefinement(geometry, method)
+ iscoarse(e) = perimeter(e) > method.length * nvertices(e)
+ mesh = simplexify(geometry)
+ while any(iscoarse, mesh)
+ mesh = refine(mesh, TriSubdivision())
+ end
+ mesh
+end
+
+_sides(box::Box{<:𝔼}) = sides(box)
+
+function _sides(box::Box{<:🌐})
+ A, B = extrema(box)
+ a = convert(LatLon, coords(A))
+ b = convert(LatLon, coords(B))
+ P = withcrs(box, (a.lat, b.lon), LatLon)
+
+ AP = Segment(A, P)
+ PB = Segment(P, B)
+ (measure(AP), measure(PB))
+end
diff --git a/src/discretization/regular.jl b/src/discretization/regular.jl
index 914fd46ba..ac783377b 100644
--- a/src/discretization/regular.jl
+++ b/src/discretization/regular.jl
@@ -35,32 +35,35 @@ end
function wrapgrid(g, m)
sz = fitdims(m.sizes, paramdim(g))
- pd = perdims(g)
+ pd = isperiodic(g)
np = @. sz + !pd
ps = sample(g, RegularSampling(np))
tg = GridTopology(sz, pd)
ps, tg
end
-perdims(g) = isperiodic(g)
-perdims(::Sphere{3}) = (false, true)
-
# ------------------------
# append to grid topology
# ------------------------
appendtopo(g, tg) = tg
-appendtopo(::Ball{2}, tg) = _appendcenter(tg)
+appendtopo(::Ball{𝔼{2}}, tg) = _appendcenter(tg)
appendtopo(::Disk, tg) = _appendcenter(tg)
-appendtopo(::Sphere{3}, tg) = _appendpoles(tg, 2, true)
+appendtopo(::Sphere{𝔼{3}}, tg) = _appendpoles(tg, 2, true)
+
+appendtopo(::Ellipsoid, tg) = _appendpoles(tg, 2, true)
+
+appendtopo(::Cylinder, tg) = _appendaxis(tg)
appendtopo(::CylinderSurface, tg) = _appendpoles(tg, 1, false)
appendtopo(::ConeSurface, tg) = _appendpoles(tg, 1, false)
+appendtopo(::FrustumSurface, tg) = _appendpoles(tg, 1, false)
+
function _appendcenter(tg)
# auxiliary variables
_, ny = size(tg)
@@ -84,6 +87,41 @@ function _appendcenter(tg)
SimpleTopology([quads; tris])
end
+function _appendaxis(tg)
+ # auxiliary variables
+ _, ny, nz = size(tg)
+
+ # number of grid vertices
+ nvert = nvertices(tg)
+
+ # connect hexahedra in the volume
+ hexas = collect(elements(tg))
+
+ # connect axis with wedges
+ inds = NTuple{6,Int}[]
+ for k in 1:nz
+ for j in 1:(ny - 1)
+ a1 = nvert + k
+ b1 = cart2corner(tg, 1, j, k)
+ c1 = cart2corner(tg, 1, j + 1, k)
+ a2 = nvert + k + 1
+ b2 = cart2corner(tg, 1, j, k + 1)
+ c2 = cart2corner(tg, 1, j + 1, k + 1)
+ push!(inds, (a1, b1, c1, a2, b2, c2))
+ end
+ a1 = nvert + k
+ b1 = cart2corner(tg, 1, ny, k)
+ c1 = cart2corner(tg, 1, 1, k)
+ a2 = nvert + k + 1
+ b2 = cart2corner(tg, 1, ny, k + 1)
+ c2 = cart2corner(tg, 1, 1, k + 1)
+ push!(inds, (a1, b1, c1, a2, b2, c2))
+ end
+ wedges = [connect(ind, Wedge) for ind in inds]
+
+ SimpleTopology([hexas; wedges])
+end
+
# connect north and south poles to
# grid topology along given dimension
# and counter-clockwise orientation
@@ -104,7 +142,7 @@ function _appendpoles(tg, d, ccw)
# connect north pole with triangles
north = map(1:(sz[d] - 1)) do j
- iᵤ = ntuple(i -> i == d ? j : 1, nd)
+ iᵤ = ntuple(i -> i == d ? j : 1, nd)
iᵥ = ntuple(i -> i == d ? j + 1 : 1, nd)
u = cart2corner(tg, iᵤ...)
v = cart2corner(tg, iᵥ...)
@@ -118,14 +156,14 @@ function _appendpoles(tg, d, ccw)
# connect south pole with triangles
south = map(1:(sz[d] - 1)) do j
- iᵤ = ntuple(i -> i == d ? j : sz[i] + 1, nd)
+ iᵤ = ntuple(i -> i == d ? j : sz[i] + 1, nd)
iᵥ = ntuple(i -> i == d ? j + 1 : sz[i] + 1, nd)
u = cart2corner(tg, iᵤ...)
v = cart2corner(tg, iᵥ...)
connect((s, swap(v, u)...))
end
iᵤ = ntuple(i -> i == d ? sz[d] : sz[i] + 1, nd)
- iᵥ = ntuple(i -> i == d ? 1 : sz[i] + 1, nd)
+ iᵥ = ntuple(i -> i == d ? 1 : sz[i] + 1, nd)
u = cart2corner(tg, iᵤ...)
v = cart2corner(tg, iᵥ...)
push!(south, connect((s, swap(v, u)...)))
@@ -139,5 +177,5 @@ end
function discretize(box::Box, method::RegularDiscretization)
sz = fitdims(method.sizes, paramdim(box))
- CartesianGrid(extrema(box)..., dims=sz)
+ RegularGrid(extrema(box)..., dims=sz)
end
diff --git a/src/discretization/tetra.jl b/src/discretization/tetra.jl
deleted file mode 100644
index 6044cbb5f..000000000
--- a/src/discretization/tetra.jl
+++ /dev/null
@@ -1,25 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Tetrahedralization()
-
-A method to discretize geometries into tetrahedra.
-"""
-struct Tetrahedralization <: DiscretizationMethod end
-
-function discretize(box::Box, ::Tetrahedralization)
- indices = [(1, 5, 6, 8), (1, 3, 4, 8), (1, 3, 6, 8), (1, 2, 3, 6), (3, 6, 7, 8)]
- SimpleMesh(pointify(box), connect.(indices, Tetrahedron))
-end
-
-function discretize(hexa::Hexahedron, ::Tetrahedralization)
- indices = [(1, 5, 6, 8), (1, 3, 4, 8), (1, 3, 6, 8), (1, 2, 3, 6), (3, 6, 7, 8)]
- SimpleMesh(pointify(hexa), connect.(indices, Tetrahedron))
-end
-
-function discretize(pyramid::Pyramid, ::Tetrahedralization)
- indices = [(1, 2, 4, 5), (3, 4, 2, 5)]
- SimpleMesh(pointify(pyramid), connect.(indices, Tetrahedron))
-end
diff --git a/src/distances.jl b/src/distances.jl
index 464406d1e..569f063c6 100644
--- a/src/distances.jl
+++ b/src/distances.jl
@@ -6,9 +6,9 @@
evaluate(d::PreMetric, g::Geometry, p::Point) = evaluate(d, p, g)
"""
- evaluate(Euclidean(), point, line)
+ evaluate(distance::Euclidean, point, line)
-Evaluate the Euclidean distance between `point` and `line`.
+Evaluate the Euclidean `distance` between `point` and `line`.
"""
function evaluate(::Euclidean, p::Point, l::Line)
a, b = l(0), l(1)
@@ -19,24 +19,60 @@ function evaluate(::Euclidean, p::Point, l::Line)
end
"""
- evaluate(Euclidean(), line1, line2)
-Evaluate the minimum Euclidean distance between `line1` and `line2`.
+ evaluate(distance::Euclidean, line₁, line₂)
+
+Evaluate the minimum Euclidean `distance` between `line₁` and `line₂`.
"""
-function evaluate(::Euclidean, line1::Line{Dim,T}, line2::Line{Dim,T}) where {Dim,T}
- λ₁, λ₂, r, rₐ = intersectparameters(line1(0), line1(1), line2(0), line2(1))
+function evaluate(d::Euclidean, l₁::Line, l₂::Line)
+ λ₁, λ₂, r, rₐ = intersectparameters(l₁(0), l₁(1), l₂(0), l₂(1))
if (r == rₐ == 2) || (r == rₐ == 1) # lines intersect or are colinear
- return T(0)
+ return zero(result_type(d, lentype(l₁), lentype(l₂)))
elseif (r == 1) && (rₐ == 2) # lines are parallel
- return evaluate(Euclidean(), line1(0), line2)
+ return evaluate(d, l₁(0), l₂)
else # get distance between closest points on each line
- return evaluate(Euclidean(), line1(λ₁), line2(λ₂))
+ return evaluate(d, l₁(λ₁), l₂(λ₂))
end
end
"""
- evaluate(::PreMetric, point1, point2)
+ evaluate(distance::PreMetric, point₁, point₂)
-Evaluate pre-metric between coordinates of `point1` and `point2`.
+Evaluate pre-metric `distance` between coordinates of `point₁` and `point₂`.
"""
-evaluate(d::PreMetric, p1::Point, p2::Point) = evaluate(d, coordinates(p1), coordinates(p2))
+function evaluate(d::PreMetric, p₁::Point, p₂::Point)
+ u₁ = unit(Meshes.lentype(p₁))
+ u₂ = unit(Meshes.lentype(p₂))
+ u = Unitful.promote_unit(u₁, u₂)
+ v₁ = ustrip.(u, to(p₁))
+ v₂ = ustrip.(u, to(p₂))
+ evaluate(d, v₁, v₂) * u
+end
+
+# --------------
+# SPECIAL CASES
+# --------------
+
+evaluate(d::Haversine, p₁::Point, p₂::Point) = _evaluate(d, coords(p₁), coords(p₂))
+
+function _evaluate(d::Haversine, coords₁::LatLon, coords₂::LatLon)
+ uᵣ = unit(d.radius)
+ # add default unit if necessary
+ u = uᵣ === NoUnits ? u"m" : NoUnits
+ v₁ = SVector(coords₁.lon, coords₁.lat)
+ v₂ = SVector(coords₂.lon, coords₂.lat)
+ evaluate(d, v₁, v₂) * u
+end
+
+_evaluate(d::Haversine, coords₁::CRS, coords₂::CRS) = _evaluate(d, convert(LatLon, coords₁), convert(LatLon, coords₂))
+
+evaluate(d::SphericalAngle, p₁::Point, p₂::Point) = _evaluate(d, coords(p₁), coords(p₂))
+
+function _evaluate(d::SphericalAngle, coords₁::LatLon, coords₂::LatLon)
+ v₁ = SVector(deg2rad(coords₁.lon), deg2rad(coords₁.lat))
+ v₂ = SVector(deg2rad(coords₂.lon), deg2rad(coords₂.lat))
+ evaluate(d, v₁, v₂) * u"rad"
+end
+
+_evaluate(d::SphericalAngle, coords₁::CRS, coords₂::CRS) =
+ _evaluate(d, convert(LatLon, coords₁), convert(LatLon, coords₂))
diff --git a/src/domains.jl b/src/domains.jl
index dcea5481d..c47c4af2c 100644
--- a/src/domains.jl
+++ b/src/domains.jl
@@ -3,11 +3,13 @@
# ------------------------------------------------------------------
"""
- Domain
+ Domain{M,CRS}
-A domain is an indexable collection of geometries (e.g. mesh).
+A domain is an indexable collection of geometries (e.g. mesh)
+in a given manifold `M` with point coordinates specified in a
+coordinate reference system `CRS`.
"""
-abstract type Domain{Dim,T} end
+abstract type Domain{M<:Manifold,C<:CRS} end
"""
element(domain, ind)
@@ -29,11 +31,12 @@ function nelements end
==(d1::Domain, d2::Domain) = nelements(d1) == nelements(d2) && all(d1[i] == d2[i] for i in 1:nelements(d1))
-Base.isapprox(d1::Domain, d2::Domain) = nelements(d1) == nelements(d2) && all(d1[i] ≈ d2[i] for i in 1:nelements(d1))
+Base.isapprox(d1::Domain, d2::Domain; kwargs...) =
+ nelements(d1) == nelements(d2) && all(isapprox(d1[i], d2[i]; kwargs...) for i in 1:nelements(d1))
Base.getindex(d::Domain, ind::Int) = element(d, ind)
-Base.getindex(d::Domain, inds::AbstractVector) = [element(d, ind) for ind in inds]
+Base.getindex(d::Domain, inds::AbstractVector) = [d[ind] for ind in inds]
Base.firstindex(d::Domain) = 1
@@ -55,12 +58,16 @@ Base.vcat(d1::Domain, d2::Domain) = GeometrySet(vcat(collect(d1), collect(d2)))
Base.vcat(ds::Domain...) = reduce(vcat, ds)
+Base.view(domain::Domain, inds::AbstractVector{Int}) = SubDomain(domain, inds)
+
+Base.view(domain::Domain, geometry::Geometry) = view(domain, indices(domain, geometry))
+
"""
embeddim(domain)
Return the number of dimensions of the space where the `domain` is embedded.
"""
-embeddim(::Type{<:Domain{Dim,T}}) where {Dim,T} = Dim
+embeddim(::Type{<:Domain{M,CRS}}) where {M,CRS} = CoordRefSystems.ndims(CRS)
embeddim(d::Domain) = embeddim(typeof(d))
"""
@@ -72,35 +79,28 @@ parametric dimensions of its elements.
paramdim(d::Domain) = paramdim(first(d))
"""
- coordtype(domain)
+ crs(domain)
-Return the machine type of each coordinate used to describe the `domain`.
+Return the coordinate reference system (CRS) of the `domain`.
"""
-coordtype(::Type{<:Domain{Dim,T}}) where {Dim,T} = T
-coordtype(d::Domain) = coordtype(typeof(d))
+crs(::Type{<:Domain{M,CRS}}) where {M,CRS} = CRS
+crs(d::Domain) = crs(typeof(d))
"""
- centroid(domain, ind)
+ manifold(domain)
-Return the centroid of the `ind`-th element in the `domain`.
+Return the manifold where the `domain` is defined.
"""
-centroid(d::Domain, ind::Int) = centroid(d[ind])
+manifold(::Type{<:Domain{M,CRS}}) where {M,CRS} = M
+manifold(d::Domain) = manifold(typeof(d))
"""
- centroid(domain)
+ lentype(domain)
-Return the centroid of the `domain`, i.e. the centroid of all
-its element's centroids.
+Return the length type of the `domain`.
"""
-function centroid(d::Domain{Dim,T}) where {Dim,T}
- coords(i) = coordinates(centroid(d, i))
- volume(i) = measure(element(d, i))
- n = nelements(d)
- x = coords.(1:n)
- w = volume.(1:n)
- all(iszero, w) && (w = ones(T, n))
- Point(sum(w .* x) / sum(w))
-end
+lentype(::Type{<:Domain{M,CRS}}) where {M,CRS} = lentype(CRS)
+lentype(d::Domain) = lentype(typeof(d))
"""
extrema(domain)
@@ -121,10 +121,10 @@ topology(d::Domain) = d.topology
# IO METHODS
# -----------
-function Base.summary(io::IO, d::Domain{Dim,T}) where {Dim,T}
+function Base.summary(io::IO, d::Domain)
nelm = nelements(d)
name = prettyname(d)
- print(io, "$nelm $name{$Dim,$T}")
+ print(io, "$nelm $name")
end
Base.show(io::IO, d::Domain) = summary(io, d)
@@ -139,10 +139,10 @@ end
# IMPLEMENTATIONS
# ----------------
-include("subdomains.jl")
-include("sets.jl")
-include("mesh.jl")
-include("trajecs.jl")
+include("domains/sets.jl")
+include("domains/meshes.jl")
+include("domains/trajecs.jl")
+include("domains/subdomains.jl")
# ------------
# CONVERSIONS
@@ -152,6 +152,6 @@ Base.convert(::Type{GeometrySet}, d::Domain) = GeometrySet(collect(d))
Base.convert(::Type{SimpleMesh}, m::Mesh) = SimpleMesh(vertices(m), topology(m))
-Base.convert(::Type{StructuredGrid}, g::Grid) = StructuredGrid(XYZ(g))
+Base.convert(::Type{StructuredGrid}, g::Grid) = StructuredGrid{manifold(g),crs(g)}(XYZ(g))
-Base.convert(::Type{RectilinearGrid}, g::CartesianGrid) = RectilinearGrid(xyz(g))
+Base.convert(::Type{RectilinearGrid}, g::RegularGrid) = RectilinearGrid{manifold(g),crs(g)}(xyz(g))
diff --git a/src/mesh.jl b/src/domains/meshes.jl
similarity index 60%
rename from src/mesh.jl
rename to src/domains/meshes.jl
index 332ffdddf..ee1b78a4b 100644
--- a/src/mesh.jl
+++ b/src/domains/meshes.jl
@@ -3,12 +3,13 @@
# ------------------------------------------------------------------
"""
- Mesh{Dim,T,TP}
+ Mesh{M,CRS,TP}
-A mesh embedded in a `Dim`-dimensional space with coordinates of type `T`
-and topology of type `TP`.
+A mesh of geometries in a given manifold `M` with point coordinates specified
+in a coordinate reference system `CRS`. Unlike a general domain, a mesh has a
+well-defined topology `TP`.
"""
-abstract type Mesh{Dim,T,TP<:Topology} <: Domain{Dim,T} end
+abstract type Mesh{M<:Manifold,C<:CRS,TP<:Topology} <: Domain{M,C} end
"""
vertex(mesh, ind)
@@ -22,7 +23,7 @@ function vertex end
Return the vertices of the `mesh`.
"""
-vertices(m::Mesh) = [vertex(m, ind) for ind in 1:nvertices(m)]
+vertices(m::Mesh) = collect(eachvertex(m))
"""
nvertices(mesh)
@@ -31,6 +32,13 @@ Return the number of vertices of the `mesh`.
"""
nvertices(m::Mesh) = nvertices(topology(m))
+"""
+ eachvertex(mesh)
+
+Return an iterator for the vertices of the `mesh`.
+"""
+eachvertex(m::Mesh) = (vertex(m, i) for i in 1:nvertices(m))
+
"""
faces(mesh, rank)
@@ -123,41 +131,40 @@ topoconvert(TP::Type{<:Topology}, m::Mesh) = SimpleMesh(vertices(m), convert(TP,
==(m₁::Mesh, m₂::Mesh) = vertices(m₁) == vertices(m₂) && topology(m₁) == topology(m₂)
-function Base.show(io::IO, ::MIME"text/plain", m::Mesh{Dim,T}) where {Dim,T}
+function Base.show(io::IO, ::MIME"text/plain", m::Mesh)
t = topology(m)
- verts = vertices(m)
- elems = elements(t)
nvert = nvertices(m)
nelms = nelements(m)
summary(io, m)
println(io)
println(io, " $nvert vertices")
- printelms(io, verts, " ")
+ printelms(io, m, nelms=nvert, getelm=vertex, tab=" ")
println(io)
println(io, " $nelms elements")
- printitr(io, elems, " ")
+ printelms(io, t, nelms=nelms, getelm=element, tab=" ")
end
"""
- Grid{Dim,T}
+ Grid{M,CRS,Dim}
-A grid embedded in a `Dim`-dimensional space with coordinates of type `T`.
+A grid of geometries in a given manifold `M` with points coordinates specified
+in a coordinate reference system `CRS`, which is embedded in `Dim` dimensions.
"""
-const Grid{Dim,T} = Mesh{Dim,T,GridTopology{Dim}}
+const Grid{M<:Manifold,C<:CRS,Dim} = Mesh{M,C,GridTopology{Dim}}
"""
- SubGrid{Dim,T}
+ vertex(grid, ijk)
-A view of a grid in a `Dim`-dimensinoal space with coordinates of type `T`.
+Convert Cartesian index `ijk` to vertex on `grid`.
"""
-const SubGrid{Dim,T} = SubDomain{Dim,T,<:Grid{Dim,T}}
+vertex(g::Grid, ijk::CartesianIndex) = vertex(g, ijk.I)
"""
- vertex(grid, ijk)
+ vsize(grid)
-Convert Cartesian index `ijk` to vertex on `grid`.
+Number of vertices along each dimension of the `grid`.
"""
-vertex(g::Grid{Dim}, ijk::CartesianIndex{Dim}) where {Dim} = vertex(g, ijk.I)
+vsize(g::Grid) = size(g) .+ .!isperiodic(g)
"""
xyz(grid)
@@ -181,38 +188,55 @@ function XYZ end
Base.size(g::Grid) = size(topology(g))
-vertex(g::Grid, ind::Int) = vertex(g, CartesianIndices(size(g) .+ 1)[ind])
+paramdim(g::Grid) = length(size(g))
+
+vertex(g::Grid, ind::Int) = vertex(g, CartesianIndices(vsize(g))[ind])
-vertex(g::Grid{Dim}, ijk::Dims{Dim}) where {Dim} = vertex(g, LinearIndices(size(g) .+ 1)[ijk...])
+vertex(g::Grid, ijk::Dims) = vertex(g, LinearIndices(vsize(g))[ijk...])
-Base.minimum(g::Grid{Dim}) where {Dim} = vertex(g, ntuple(i -> 1, Dim))
-Base.maximum(g::Grid{Dim}) where {Dim} = vertex(g, size(g) .+ 1)
-Base.extrema(g::Grid{Dim}) where {Dim} = minimum(g), maximum(g)
+Base.minimum(g::Grid) = vertex(g, ntuple(i -> 1, paramdim(g)))
+Base.maximum(g::Grid) = vertex(g, vsize(g))
+Base.extrema(g::Grid) = minimum(g), maximum(g)
function element(g::Grid, ind::Int)
elem = element(topology(g), ind)
type = pltype(elem)
einds = indices(elem)
- cinds = CartesianIndices(size(g) .+ 1)
+ cinds = CartesianIndices(vsize(g))
verts = ntuple(i -> vertex(g, cinds[einds[i]]), nvertices(type))
type(verts)
end
Base.eltype(g::Grid) = typeof(first(g))
-Base.getindex(g::Grid{Dim}, ijk::Vararg{Int,Dim}) where {Dim} = element(g, LinearIndices(size(g))[ijk...])
+Base.getindex(g::Grid, ind::Int) = element(g, ind)
+
+Base.getindex(g::Grid, inds::AbstractVector) = [element(g, ind) for ind in inds]
-@propagate_inbounds function Base.getindex(g::Grid{Dim}, ijk::Vararg{Union{UnitRange{Int},Colon,Int},Dim}) where {Dim}
+Base.getindex(g::Grid, ijk::Int...) = element(g, LinearIndices(size(g))[ijk...])
+
+@propagate_inbounds function Base.getindex(g::Grid, ijk...)
dims = size(g)
- ranges = ntuple(i -> _asrange(dims[i], ijk[i]), Dim)
+ ranges = ntuple(i -> _asrange(dims[i], ijk[i]), paramdim(g))
getindex(g, CartesianIndices(ranges))
end
+function Base.getindex(g::Grid, I::CartesianIndices)
+ @boundscheck _checkbounds(g, I)
+ dims = size(I)
+ odims = size(g)
+ cinds = first(I):CartesianIndex(Tuple(last(I)) .+ 1)
+ inds = vec(LinearIndices(odims .+ 1)[cinds])
+ points = [vertex(g, ind) for ind in inds]
+ periodic = isperiodic(topology(g)) .&& dims .== odims
+ SimpleMesh(points, GridTopology(dims, periodic))
+end
+
_asrange(::Int, r::UnitRange{Int}) = r
_asrange(d::Int, ::Colon) = 1:d
_asrange(::Int, i::Int) = i:i
-function _checkbounds(g::Grid{Dim}, I::CartesianIndices{Dim}) where {Dim}
+function _checkbounds(g, I)
dims = size(g)
ranges = I.indices
if !all(first(r) ≥ 1 && last(r) ≤ d for (d, r) in zip(dims, ranges))
@@ -224,8 +248,14 @@ end
# IMPLEMENTATIONS
# ----------------
-include("mesh/cartesiangrid.jl")
-include("mesh/rectilineargrid.jl")
-include("mesh/structuredgrid.jl")
-include("mesh/simplemesh.jl")
-include("mesh/transformedmesh.jl")
+include("meshes/regulargrid.jl")
+include("meshes/cartesiangrid.jl")
+include("meshes/rectilineargrid.jl")
+include("meshes/structuredgrid.jl")
+include("meshes/simplemesh.jl")
+include("meshes/transformedmesh.jl")
+
+# aliases for dispatch purposes
+const OrthoRegularGrid{M<:𝔼,C<:Union{Cartesian,Projected}} = RegularGrid{M,C}
+const OrthoRectilinearGrid{M<:𝔼,C<:Union{Cartesian,Projected}} = RectilinearGrid{M,C}
+const OrthoStructuredGrid{M<:𝔼,C<:Union{Cartesian,Projected}} = StructuredGrid{M,C}
diff --git a/src/domains/meshes/cartesiangrid.jl b/src/domains/meshes/cartesiangrid.jl
new file mode 100644
index 000000000..b972ff0dc
--- /dev/null
+++ b/src/domains/meshes/cartesiangrid.jl
@@ -0,0 +1,108 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ CartesianGrid(dims, origin, spacing)
+
+A Cartesian grid with dimensions `dims`, lower left corner at `origin`
+and cell spacing `spacing`. The three arguments must have the same length.
+
+ CartesianGrid(dims, origin, spacing, offset)
+
+A Cartesian grid with dimensions `dims`, with lower left corner of element
+`offset` at `origin` and cell spacing `spacing`.
+
+ CartesianGrid(start, finish, dims=dims)
+
+Alternatively, construct a Cartesian grid from a `start` point (lower left)
+to a `finish` point (upper right).
+
+ CartesianGrid(start, finish, spacing)
+
+Alternatively, construct a Cartesian grid from a `start` point to a `finish`
+point using a given `spacing`.
+
+ CartesianGrid(dims)
+ CartesianGrid(dim1, dim2, ...)
+
+Finally, a Cartesian grid can be constructed by only passing the dimensions
+`dims` as a tuple, or by passing each dimension `dim1`, `dim2`, ... separately.
+In this case, the origin and spacing default to (0,0,...) and (1,1,...).
+
+`CartesianGrid` is an alias to [`RegularGrid`](@ref) with `Cartesian` CRS.
+
+## Examples
+
+Create a 3D grid with 100x100x50 hexahedrons:
+
+```julia
+julia> CartesianGrid(100, 100, 50)
+```
+
+Create a 2D grid with 100 x 100 quadrangles and origin at (10.0, 20.0):
+
+```julia
+julia> CartesianGrid((100, 100), (10.0, 20.0), (1.0, 1.0))
+```
+
+Create a 1D grid from -1 to 1 with 100 segments:
+
+```julia
+julia> CartesianGrid((-1.0,), (1.0,), dims=(100,))
+```
+
+See also [`RegularGrid`](@ref).
+"""
+const CartesianGrid{M<:𝔼,C<:Cartesian} = RegularGrid{M,C}
+
+CartesianGrid(
+ origin::Point{𝔼{Dim}},
+ spacing::NTuple{Dim,Number},
+ offset::Dims{Dim},
+ topology::GridTopology{Dim}
+) where {Dim} = RegularGrid(_cartpoint(origin), spacing, offset, topology)
+
+CartesianGrid(
+ dims::Dims{Dim},
+ origin::Point{𝔼{Dim}},
+ spacing::NTuple{Dim,Number},
+ offset::Dims{Dim}=ntuple(i -> 1, Dim)
+) where {Dim} = RegularGrid(dims, _cartpoint(origin), spacing, offset)
+
+CartesianGrid(
+ dims::Dims{Dim},
+ origin::NTuple{Dim,Number},
+ spacing::NTuple{Dim,Number},
+ offset::Dims{Dim}=ntuple(i -> 1, Dim)
+) where {Dim} = CartesianGrid(dims, Point(origin), spacing, offset)
+
+CartesianGrid(start::Point{𝔼{Dim}}, finish::Point{𝔼{Dim}}, spacing::NTuple{Dim,Number}) where {Dim} =
+ RegularGrid(_cartpoint(start), _cartpoint(finish), spacing)
+
+CartesianGrid(start::NTuple{Dim,Number}, finish::NTuple{Dim,Number}, spacing::NTuple{Dim,Number}) where {Dim} =
+ CartesianGrid(Point(start), Point(finish), spacing)
+
+CartesianGrid(start::Point{𝔼{Dim}}, finish::Point{𝔼{Dim}}; dims::Dims{Dim}=ntuple(i -> 100, Dim)) where {Dim} =
+ RegularGrid(_cartpoint(start), _cartpoint(finish); dims)
+
+CartesianGrid(
+ start::NTuple{Dim,Number},
+ finish::NTuple{Dim,Number};
+ dims::Dims{Dim}=ntuple(i -> 100, Dim)
+) where {Dim} = CartesianGrid(Point(start), Point(finish); dims)
+
+function CartesianGrid(dims::Dims{Dim}) where {Dim}
+ origin = ntuple(i -> 0.0, Dim)
+ spacing = ntuple(i -> 1.0, Dim)
+ offset = ntuple(i -> 1, Dim)
+ CartesianGrid(dims, origin, spacing, offset)
+end
+
+CartesianGrid(dims::Int...) = CartesianGrid(dims)
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+_cartpoint(p) = Point(convert(Cartesian, coords(p)))
diff --git a/src/domains/meshes/rectilineargrid.jl b/src/domains/meshes/rectilineargrid.jl
new file mode 100644
index 000000000..310947f02
--- /dev/null
+++ b/src/domains/meshes/rectilineargrid.jl
@@ -0,0 +1,93 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ RectilinearGrid(x, y, z, ...)
+ RectilinearGrid{M,C}(x, y, z, ...)
+
+A rectilinear grid with vertices at sorted coordinates `x`, `y`, `z`, ...,
+manifold `M` (default to `𝔼`) and CRS type `C` (default to `Cartesian`).
+
+## Examples
+
+Create a 2D rectilinear grid with regular spacing in `x` dimension
+and irregular spacing in `y` dimension:
+
+```julia
+julia> x = 0.0:0.2:1.0
+julia> y = [0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
+julia> RectilinearGrid(x, y)
+```
+"""
+struct RectilinearGrid{M<:Manifold,C<:CRS,N,X<:NTuple{N,AbstractVector}} <: Grid{M,C,N}
+ xyz::X
+ topology::GridTopology{N}
+ RectilinearGrid{M,C,N,X}(xyz, topology) where {M<:Manifold,C<:CRS,N,X<:NTuple{N,AbstractVector}} = new(xyz, topology)
+end
+
+function RectilinearGrid{M,C}(xyz::NTuple{N,AbstractVector}, topology::GridTopology{N}) where {M<:Manifold,C<:CRS,N}
+ if M <: 🌐 && !(C <: LatLon)
+ throw(ArgumentError("rectilinear grid on `🌐` requires `LatLon` coordinates"))
+ end
+
+ T = CoordRefSystems.mactype(C)
+ nc = CoordRefSystems.ncoords(C)
+ us = CoordRefSystems.units(C)
+
+ if N ≠ nc
+ throw(ArgumentError("""
+ A $N-dimensional rectilinear grid requires a CRS with $N coordinates.
+ The provided CRS has $nc coordinates.
+ """))
+ end
+
+ xyz′ = ntuple(i -> numconvert.(T, withunit.(xyz[i], us[i])), nc)
+
+ RectilinearGrid{M,C,N,typeof(xyz′)}(xyz′, topology)
+end
+
+function RectilinearGrid{M,C}(xyz::NTuple{N,AbstractVector}) where {M<:Manifold,C<:CRS,N}
+ topology = GridTopology(length.(xyz) .- 1)
+ RectilinearGrid{M,C}(xyz, topology)
+end
+
+RectilinearGrid{M,C}(xyz::AbstractVector...) where {M<:Manifold,C<:CRS} = RectilinearGrid{M,C}(xyz)
+
+function RectilinearGrid(xyz::NTuple{N,AbstractVector}) where {N}
+ L = promote_type(ntuple(i -> aslentype(eltype(xyz[i])), N)...)
+ M = 𝔼{N}
+ C = Cartesian{NoDatum,N,L}
+ RectilinearGrid{M,C}(xyz)
+end
+
+RectilinearGrid(xyz::AbstractVector...) = RectilinearGrid(xyz)
+
+function vertex(g::RectilinearGrid, ijk::Dims)
+ ctor = CoordRefSystems.constructor(crs(g))
+ Point(ctor(getindex.(g.xyz, ijk)...))
+end
+
+xyz(g::RectilinearGrid) = g.xyz
+
+XYZ(g::RectilinearGrid) = XYZ(xyz(g))
+
+@generated function Base.getindex(g::RectilinearGrid{M,C,N}, I::CartesianIndices) where {M,C,N}
+ exprs = ntuple(N) do i
+ :(g.xyz[$i][start[$i]:stop[$i]])
+ end
+
+ quote
+ @boundscheck _checkbounds(g, I)
+ dims = size(I)
+ start = Tuple(first(I))
+ stop = Tuple(last(I)) .+ 1
+ xyz = ($(exprs...),)
+ RectilinearGrid{M,C}(xyz, GridTopology(dims))
+ end
+end
+
+function Base.summary(io::IO, g::RectilinearGrid)
+ join(io, size(g), "×")
+ print(io, " RectilinearGrid")
+end
diff --git a/src/domains/meshes/regulargrid.jl b/src/domains/meshes/regulargrid.jl
new file mode 100644
index 000000000..ad05d5941
--- /dev/null
+++ b/src/domains/meshes/regulargrid.jl
@@ -0,0 +1,190 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ RegularGrid(dims, origin, spacing)
+
+A regular grid with dimensions `dims`, lower left corner at `origin`
+and cell spacing `spacing`. The three arguments must have the same length.
+
+ RegularGrid(dims, origin, spacing, offset)
+
+A regular grid with dimensions `dims`, with lower left corner of element
+`offset` at `origin` and cell spacing `spacing`.
+
+ RegularGrid(start, finish, dims=dims)
+
+Alternatively, construct a regular grid from a `start` point to a `finish`
+with dimensions `dims`.
+
+ RegularGrid(start, finish, spacing)
+
+Alternatively, construct a regular grid from a `start` point to a `finish`
+point using a given `spacing`.
+
+## Examples
+
+```
+RegularGrid((10, 20), Point(LatLon(30.0°, 60.0°)), (1.0, 1.0)) # add coordinate units to spacing
+RegularGrid((10, 20), Point(Polar(0.0cm, 0.0rad)), (10.0mm, 1.0rad)) # convert spacing units to coordinate units
+RegularGrid((10, 20), Point(Mercator(0.0, 0.0)), (1.5, 1.5))
+RegularGrid((10, 20, 30), Point(Cylindrical(0.0, 0.0, 0.0)), (3.0, 2.0, 1.0))
+```
+
+See also [`CartesianGrid`](@ref).
+"""
+struct RegularGrid{M<:Manifold,C<:CRS,N,S<:NTuple{N,Quantity}} <: Grid{M,C,N}
+ origin::Point{M,C}
+ spacing::S
+ offset::Dims{N}
+ topology::GridTopology{N}
+
+ function RegularGrid{M,C,N,S}(origin, spacing, offset, topology) where {M<:Manifold,C<:CRS,N,S<:NTuple{N,Quantity}}
+ if !all(s -> s > zero(s), spacing)
+ throw(ArgumentError("spacing must be positive"))
+ end
+ new(origin, spacing, offset, topology)
+ end
+end
+
+function RegularGrid(
+ origin::Point{M,C},
+ spacing::NTuple{N,Number},
+ offset::Dims{N},
+ topology::GridTopology{N}
+) where {M<:Manifold,C<:CRS,N}
+ _checkorigin(origin)
+
+ nc = CoordRefSystems.ncoords(C)
+
+ if N ≠ nc
+ throw(ArgumentError("""
+ A $N-dimensional regular grid requires an origin with $N coordinates.
+ The provided origin has $nc coordinates.
+ """))
+ end
+
+ spac = _spacing(origin, spacing)
+
+ RegularGrid{M,C,N,typeof(spac)}(origin, spac, offset, topology)
+end
+
+function RegularGrid(
+ dims::Dims{N},
+ origin::Point,
+ spacing::NTuple{N,Number},
+ offset::Dims{N}=ntuple(i -> 1, N)
+) where {N}
+ if !all(>(0), dims)
+ throw(ArgumentError("dimensions must be positive"))
+ end
+ RegularGrid(origin, spacing, offset, GridTopology(dims))
+end
+
+function RegularGrid(start::Point, finish::Point, spacing::NTuple{N,Number}) where {N}
+ _checkorigin(start)
+ svals, fvals = _startfinish(start, finish)
+ spac = _spacing(start, spacing)
+ dims = ceil.(Int, (fvals .- svals) ./ spac)
+ RegularGrid(dims, start, spac)
+end
+
+function RegularGrid(start::Point, finish::Point; dims::Dims=ntuple(i -> 100, CoordRefSystems.ncoords(crs(start))))
+ _checkorigin(start)
+ svals, fvals = _startfinish(start, finish)
+ spacing = (fvals .- svals) ./ dims
+ RegularGrid(dims, start, spacing)
+end
+
+spacing(g::RegularGrid) = g.spacing
+
+offset(g::RegularGrid) = g.offset
+
+function vertex(g::RegularGrid, ijk::Dims)
+ ctor = CoordRefSystems.constructor(crs(g))
+ orig = CoordRefSystems.values(coords(g.origin))
+ vals = orig .+ (ijk .- g.offset) .* g.spacing
+ Point(ctor(vals...))
+end
+
+@generated function xyz(g::RegularGrid{M,C,N}) where {M,C,N}
+ exprs = ntuple(N) do i
+ :(range(start=orig[$i], step=spac[$i], length=(dims[$i] + 1)))
+ end
+
+ quote
+ dims = size(g)
+ spac = spacing(g)
+ orig = CoordRefSystems.values(coords(g.origin))
+ ($(exprs...),)
+ end
+end
+
+XYZ(g::RegularGrid) = XYZ(xyz(g))
+
+function Base.getindex(g::RegularGrid, I::CartesianIndices)
+ @boundscheck _checkbounds(g, I)
+ dims = size(I)
+ offset = g.offset .- Tuple(first(I)) .+ 1
+ RegularGrid(dims, g.origin, g.spacing, offset)
+end
+
+function ==(g₁::RegularGrid, g₂::RegularGrid)
+ orig₁ = CoordRefSystems.values(coords(g₁.origin))
+ orig₂ = CoordRefSystems.values(coords(g₂.origin))
+ g₁.topology == g₂.topology && g₁.spacing == g₂.spacing && orig₁ .- orig₂ == (g₁.offset .- g₂.offset) .* g₁.spacing
+end
+
+# -----------
+# IO METHODS
+# -----------
+
+function Base.summary(io::IO, g::RegularGrid)
+ dims = join(size(g.topology), "×")
+ name = prettyname(g)
+ print(io, "$dims $name")
+end
+
+function Base.show(io::IO, ::MIME"text/plain", g::RegularGrid)
+ summary(io, g)
+ println(io)
+ println(io, "├─ minimum: ", minimum(g))
+ println(io, "├─ maximum: ", maximum(g))
+ print(io, "└─ spacing: ", spacing(g))
+end
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function _checkorigin(origin)
+ if manifold(origin) <: 🌐 && !(crs(origin) <: LatLon)
+ throw(ArgumentError("regular spacing on `🌐` requires `LatLon` coordinates"))
+ end
+end
+
+function _spacing(origin, spacing)
+ C = crs(origin)
+ T = CoordRefSystems.mactype(C)
+ nc = CoordRefSystems.ncoords(C)
+ us = CoordRefSystems.units(C)
+ ntuple(i -> numconvert(T, withunit(spacing[i], us[i])), nc)
+end
+
+function _startfinish(start::Point{<:𝔼}, finish::Point{<:𝔼})
+ scoords = coords(start)
+ fcoords = convert(crs(start), coords(finish))
+ svals = CoordRefSystems.values(scoords)
+ fvals = CoordRefSystems.values(fcoords)
+ svals, fvals
+end
+
+function _startfinish(start::Point{<:🌐}, finish::Point{<:🌐})
+ slatlon = convert(LatLon, coords(start))
+ flatlon = convert(LatLon, coords(finish))
+ slon = flatlon.lon < slatlon.lon ? slatlon.lon - 360u"°" : slatlon.lon
+ svals = (slatlon.lat, slon)
+ fvals = (flatlon.lat, flatlon.lon)
+ svals, fvals
+end
diff --git a/src/mesh/simplemesh.jl b/src/domains/meshes/simplemesh.jl
similarity index 69%
rename from src/mesh/simplemesh.jl
rename to src/domains/meshes/simplemesh.jl
index d4b378fbd..fe40785f6 100644
--- a/src/mesh/simplemesh.jl
+++ b/src/domains/meshes/simplemesh.jl
@@ -31,12 +31,12 @@ See also [`Topology`](@ref), [`GridTopology`](@ref),
of the mesh to a [`HalfEdgeTopology`](@ref) instead of a
[`SimpleTopology`](@ref).
"""
-struct SimpleMesh{Dim,T,V<:AbstractVector{Point{Dim,T}},TP<:Topology} <: Mesh{Dim,T,TP}
+struct SimpleMesh{M<:Manifold,C<:CRS,V<:AbstractVector{Point{M,C}},TP<:Topology} <: Mesh{M,C,TP}
vertices::V
topology::TP
end
-SimpleMesh(coords::AbstractVector{<:NTuple}, topology::Topology) = SimpleMesh(Point.(coords), topology)
+SimpleMesh(coords::AbstractVector{<:Tuple}, topology::Topology) = SimpleMesh(Point.(coords), topology)
function SimpleMesh(vertices, connec::AbstractVector{<:Connectivity}; relations=false)
topology = relations ? HalfEdgeTopology(connec) : SimpleTopology(connec)
@@ -48,13 +48,3 @@ vertex(m::SimpleMesh, ind::Int) = m.vertices[ind]
vertices(m::SimpleMesh) = m.vertices
nvertices(m::SimpleMesh) = length(m.vertices)
-
-function Base.getindex(m::SimpleMesh{Dim,T,V,GridTopology{Dim}}, I::CartesianIndices{Dim}) where {Dim,T,V}
- @boundscheck _checkbounds(m, I)
- dims = size(I)
- odims = size(m)
- cinds = first(I):CartesianIndex(Tuple(last(I)) .+ 1)
- inds = vec(LinearIndices(odims .+ 1)[cinds])
- periodic = isperiodic(topology(m)) .&& dims .== odims
- SimpleMesh(m.vertices[inds], GridTopology(dims, periodic))
-end
diff --git a/src/domains/meshes/structuredgrid.jl b/src/domains/meshes/structuredgrid.jl
new file mode 100644
index 000000000..501b8df17
--- /dev/null
+++ b/src/domains/meshes/structuredgrid.jl
@@ -0,0 +1,101 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ StructuredGrid(X, Y, Z, ...)
+ StructuredGrid{M,C}(X, Y, Z, ...)
+
+A structured grid with vertices at sorted coordinates `X`, `Y`, `Z`, ...,
+manifold `M` (default to `𝔼`) and CRS type `C` (default to `Cartesian`).
+
+## Examples
+
+Create a 2D structured grid with regular spacing in `x` dimension
+and irregular spacing in `y` dimension:
+
+```julia
+julia> X = repeat(0.0:0.2:1.0, 1, 6)
+julia> Y = repeat([0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
+julia> StructuredGrid(X, Y)
+```
+"""
+struct StructuredGrid{M<:Manifold,C<:CRS,N,X<:NTuple{N,AbstractArray}} <: Grid{M,C,N}
+ XYZ::X
+ topology::GridTopology{N}
+ StructuredGrid{M,C,N,X}(XYZ, topology) where {M<:Manifold,C<:CRS,N,X<:NTuple{N,AbstractArray}} = new(XYZ, topology)
+end
+
+function StructuredGrid{M,C}(XYZ::NTuple{N,AbstractArray}, topology::GridTopology{N}) where {M<:Manifold,C<:CRS,N}
+ if M <: 🌐 && !(C <: LatLon)
+ throw(ArgumentError("rectilinear grid on `🌐` requires `LatLon` coordinates"))
+ end
+
+ T = CoordRefSystems.mactype(C)
+ nc = CoordRefSystems.ncoords(C)
+ us = CoordRefSystems.units(C)
+
+ if N ≠ nc
+ throw(ArgumentError("""
+ A $N-dimensional structured grid requires a CRS with $N coordinates.
+ The provided CRS has $nc coordinates.
+ """))
+ end
+
+ XYZ′ = ntuple(i -> numconvert.(T, withunit.(XYZ[i], us[i])), nc)
+
+ StructuredGrid{M,C,N,typeof(XYZ′)}(XYZ′, topology)
+end
+
+function StructuredGrid{M,C}(XYZ::NTuple{N,AbstractArray}) where {M<:Manifold,C<:CRS,N}
+ if !allequal(size(X) for X in XYZ)
+ throw(ArgumentError("all coordinate arrays must be the same size"))
+ end
+
+ nd = ndims(first(XYZ))
+
+ if nd ≠ N
+ throw(ArgumentError("""
+ A $N-dimensional structured grid requires coordinate arrays with $N dimensions.
+ The provided coordinate arrays have $nd dimensions.
+ """))
+ end
+
+ topology = GridTopology(size(first(XYZ)) .- 1)
+ StructuredGrid{M,C}(XYZ, topology)
+end
+
+StructuredGrid{M,C}(XYZ::AbstractArray...) where {M<:Manifold,C<:CRS} = StructuredGrid{M,C}(XYZ)
+
+function StructuredGrid(XYZ::NTuple{N,AbstractArray}) where {N}
+ L = promote_type(ntuple(i -> aslentype(eltype(XYZ[i])), N)...)
+ M = 𝔼{N}
+ C = Cartesian{NoDatum,N,L}
+ StructuredGrid{M,C}(XYZ)
+end
+
+StructuredGrid(XYZ::AbstractArray...) = StructuredGrid(XYZ)
+
+function vertex(g::StructuredGrid, ijk::Dims)
+ ctor = CoordRefSystems.constructor(crs(g))
+ Point(ctor(ntuple(d -> g.XYZ[d][ijk...], paramdim(g))...))
+end
+
+XYZ(g::StructuredGrid) = g.XYZ
+
+@generated function Base.getindex(g::StructuredGrid{M,C,N}, I::CartesianIndices) where {M,C,N}
+ exprs = ntuple(i -> :(g.XYZ[$i][cinds]), N)
+
+ quote
+ @boundscheck _checkbounds(g, I)
+ dims = size(I)
+ cinds = first(I):CartesianIndex(Tuple(last(I)) .+ 1)
+ XYZ = ($(exprs...),)
+ StructuredGrid{M,C}(XYZ, GridTopology(dims))
+ end
+end
+
+function Base.summary(io::IO, g::StructuredGrid)
+ join(io, size(g), "×")
+ print(io, " StructuredGrid")
+end
diff --git a/src/domains/meshes/transformedmesh.jl b/src/domains/meshes/transformedmesh.jl
new file mode 100644
index 000000000..170bba589
--- /dev/null
+++ b/src/domains/meshes/transformedmesh.jl
@@ -0,0 +1,49 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ TransformedMesh(mesh, transform)
+
+Lazy representation of a coordinate `transform` applied to a `mesh`.
+"""
+struct TransformedMesh{M<:Manifold,C<:CRS,TP<:Topology,MS<:Mesh,TR<:Transform} <: Mesh{M,C,TP}
+ mesh::MS
+ transform::TR
+
+ function TransformedMesh{M,C}(mesh::MS, transform::TR) where {M<:Manifold,C<:CRS,MS<:Mesh,TR<:Transform}
+ TP = typeof(topology(mesh))
+ new{M,C,TP,MS,TR}(mesh, transform)
+ end
+end
+
+function TransformedMesh(m::Mesh, t::Transform)
+ p = t(vertex(m, 1))
+ TransformedMesh{manifold(p),crs(p)}(m, t)
+end
+
+# specialize constructor to avoid deep structures
+TransformedMesh(m::TransformedMesh, t::Transform) = TransformedMesh(m.mesh, m.transform → t)
+
+Base.parent(m::TransformedMesh) = m.mesh
+
+transform(m::TransformedMesh) = m.transform
+
+topology(m::TransformedMesh) = topology(m.mesh)
+
+vertex(m::TransformedMesh, ind::Int) = m.transform(vertex(m.mesh, ind))
+
+==(m₁::TransformedMesh, m₂::TransformedMesh) = m₁.transform == m₂.transform && m₁.mesh == m₂.mesh
+
+# alias to improve readability in IO methods
+const TransformedGrid{M<:Manifold,C<:CRS,Dim,G<:Grid,TR<:Transform} = TransformedMesh{M,C,GridTopology{Dim},G,TR}
+
+TransformedGrid(g::Grid, t::Transform) = TransformedMesh(g, t)
+
+@propagate_inbounds Base.getindex(g::TransformedGrid, I::CartesianIndices) =
+ TransformedGrid(getindex(g.mesh, I), g.transform)
+
+function Base.summary(io::IO, g::TransformedGrid)
+ join(io, size(g), "×")
+ print(io, " TransformedGrid")
+end
diff --git a/src/sets.jl b/src/domains/sets.jl
similarity index 70%
rename from src/sets.jl
rename to src/domains/sets.jl
index c00d35956..c8688ebc9 100644
--- a/src/sets.jl
+++ b/src/domains/sets.jl
@@ -14,13 +14,26 @@ Set containing two balls centered at `(0.0, 0.0)` and `(1.0, 1.0)`:
```julia
julia> GeometrySet([Ball((0.0, 0.0)), Ball((1.0, 1.0))])
```
+
+### Notes
+
+* Geometries with different CRS will be projected to the CRS of the first geometry.
"""
-struct GeometrySet{Dim,T,G<:Geometry{Dim,T}} <: Domain{Dim,T}
+struct GeometrySet{M<:Manifold,C<:CRS,G<:Geometry{M,C}} <: Domain{M,C}
geoms::Vector{G}
end
# constructor with iterator of geometries
-GeometrySet(geoms) = GeometrySet(map(identity, geoms))
+function GeometrySet(geoms)
+ # project all geometries to the same CRS if necessary
+ fun = if allequal(crs(g) for g in geoms)
+ identity # narrow types
+ else
+ Proj(crs(first(geoms)))
+ end
+
+ GeometrySet(map(fun, geoms))
+end
element(d::GeometrySet, ind::Int) = d.geoms[ind]
@@ -37,7 +50,7 @@ Base.vcat(d1::Domain, d2::GeometrySet) = GeometrySet(vcat(collect(d1), d2.geoms)
# SPECIAL CASE: POINT SET
# ------------------------
-const PointSet{Dim,T} = GeometrySet{Dim,T,Point{Dim,T}}
+const PointSet{M<:Manifold,C<:CRS} = GeometrySet{M,C,Point{M,C}}
"""
PointSet(points)
@@ -53,22 +66,12 @@ julia> PointSet([Point(1,2,3), Point(4,5,6)])
julia> PointSet(Point(1,2,3), Point(4,5,6))
julia> PointSet([(1,2,3), (4,5,6)])
julia> PointSet((1,2,3), (4,5,6))
-julia> PointSet([[1,2,3], [4,5,6]])
-julia> PointSet([1,2,3], [4,5,6])
-julia> PointSet([1 4; 2 5; 3 6])
```
"""
-PointSet(points::AbstractVector{P}) where {P<:Point} = PointSet{embeddim(P),coordtype(P)}(points)
+PointSet(points::AbstractVector{Point{M,C}}) where {M<:Manifold,C<:CRS} = PointSet{M,C}(points)
PointSet(points::Vararg{P}) where {P<:Point} = PointSet(collect(points))
PointSet(coords::AbstractVector{TP}) where {TP<:Tuple} = PointSet(Point.(coords))
PointSet(coords::Vararg{TP}) where {TP<:Tuple} = PointSet(collect(coords))
-PointSet(coords::AbstractVector{V}) where {V<:AbstractVector} = PointSet(Point.(coords))
-PointSet(coords::Vararg{V}) where {V<:AbstractVector} = PointSet(collect(coords))
-PointSet(coords::AbstractMatrix) = PointSet(Tuple.(eachcol(coords)))
# constructor with iterator of points
PointSet(points) = PointSet(map(identity, points))
-
-centroid(d::PointSet, ind::Int) = d[ind]
-
-centroid(d::PointSet) = Point(sum(coordinates, d) / nelements(d))
diff --git a/src/subdomains.jl b/src/domains/subdomains.jl
similarity index 78%
rename from src/subdomains.jl
rename to src/domains/subdomains.jl
index 8e1742a50..d95d1c566 100644
--- a/src/subdomains.jl
+++ b/src/domains/subdomains.jl
@@ -11,7 +11,7 @@
A partial view of a `domain` containing only the elements at `indices`.
"""
-struct SubDomain{Dim,T,D<:Domain{Dim,T},I<:AbstractVector{Int}} <: Domain{Dim,T}
+struct SubDomain{M<:Manifold,C<:CRS,D<:Domain{M,C},I<:AbstractVector{Int}} <: Domain{M,C}
domain::D
inds::I
end
@@ -19,6 +19,14 @@ end
# specialize constructor to avoid infinite loops
SubDomain(d::SubDomain, inds::AbstractVector{Int}) = SubDomain(d.domain, d.inds[inds])
+"""
+ SubGrid{M,CRS,Dim}
+
+A subgrid of geometries in a given manifold `M` with point coordinates specified
+in a coordinate reference system `CRS`, which is embedded in `Dim` dimensions.
+"""
+const SubGrid{M<:Manifold,C<:CRS,Dim} = SubDomain{M,C,<:Grid{M,C,Dim}}
+
# -----------------
# DOMAIN INTERFACE
# -----------------
@@ -27,8 +35,6 @@ element(d::SubDomain, ind::Int) = element(d.domain, d.inds[ind])
nelements(d::SubDomain) = length(d.inds)
-centroid(d::SubDomain, ind::Int) = centroid(d.domain, d.inds[ind])
-
# specializations
Base.eltype(d::SubDomain) = eltype(d.domain)
@@ -70,10 +76,10 @@ Base.parentindices(d::SubDomain) = d.inds
# IO METHODS
# -----------
-function Base.summary(io::IO, d::SubDomain{Dim,T}) where {Dim,T}
+function Base.summary(io::IO, d::SubDomain)
name = prettyname(d.domain)
nelm = length(d.inds)
- print(io, "$nelm view(::$name{$Dim,$T}, ")
+ print(io, "$nelm view(::$name, ")
printinds(io, d.inds)
print(io, ")")
end
diff --git a/src/trajecs.jl b/src/domains/trajecs.jl
similarity index 53%
rename from src/trajecs.jl
rename to src/domains/trajecs.jl
index 94197e2f6..43db14b93 100644
--- a/src/trajecs.jl
+++ b/src/domains/trajecs.jl
@@ -7,32 +7,40 @@
Trajectory of cylinders of given `radius` positioned at the `centroids`.
"""
-struct CylindricalTrajectory{T} <: Domain{3,T}
- centroids::Vector{Point{3,T}}
- radius::T
+struct CylindricalTrajectory{C<:CRS,Mₚ<:Manifold,ℒ<:Len} <: Domain{𝔼{3},C}
+ centroids::Vector{Point{Mₚ,C}}
+ radius::ℒ
+ CylindricalTrajectory(centroids::Vector{Point{Mₚ,C}}, radius::ℒ) where {C<:CRS,Mₚ<:Manifold,ℒ<:Len} =
+ new{C,Mₚ,float(ℒ)}(centroids, radius)
end
-CylindricalTrajectory(centroids::AbstractVector{Point{3,T}}, radius) where {T} =
- CylindricalTrajectory(centroids, T(radius))
+CylindricalTrajectory(centroids, radius::Len) = CylindricalTrajectory(collect(centroids), radius)
-CylindricalTrajectory(centroids) = CylindricalTrajectory(centroids, 1)
+CylindricalTrajectory(centroids, radius) = CylindricalTrajectory(centroids, addunit(radius, u"m"))
+
+CylindricalTrajectory(centroids::Vector{P}) where {P<:Point} = CylindricalTrajectory(centroids, oneunit(lentype(P)))
+
+CylindricalTrajectory(centroids) = CylindricalTrajectory(collect(centroids))
topology(t::CylindricalTrajectory) = GridTopology(length(t.centroids))
-function element(t::CylindricalTrajectory{T}, ind::Int) where {T}
+function element(t::CylindricalTrajectory, ind::Int)
+ ℒ = lentype(t)
+ T = numtype(ℒ)
+ u = unit(ℒ)
c = t.centroids
r = t.radius
n = length(c)
if n == 1 # single vertical cylinder
- p₁ = c[1] - Vec{3,T}(0, 0, 0.5)
- p₂ = c[1] + Vec{3,T}(0, 0, 0.5)
+ p₁ = c[1] - Vec(T(0) * u, T(0) * u, T(0.5) * u)
+ p₂ = c[1] + Vec(T(0) * u, T(0) * u, T(0.5) * u)
return Cylinder(p₁, p₂, r)
end
if ind == 1 # head of trajectory
# points at cylinder planes
- p₂ = center(Segment(c[ind], c[ind + 1]))
+ p₂ = centroid(Segment(c[ind], c[ind + 1]))
p₁ = p₂ - 2 * (p₂ - c[ind])
# normals to cylinder planes
@@ -40,7 +48,7 @@ function element(t::CylindricalTrajectory{T}, ind::Int) where {T}
n₁ = n₂
elseif ind == n # tail of trajectory
# points at cylinder planes
- p₁ = center(Segment(c[ind - 1], c[ind]))
+ p₁ = centroid(Segment(c[ind - 1], c[ind]))
p₂ = p₁ + 2 * (c[ind] - p₁)
# normals to cylinder planes
@@ -48,8 +56,8 @@ function element(t::CylindricalTrajectory{T}, ind::Int) where {T}
n₂ = n₁
else # middle of trajectory
# points at cylinder planes
- p₁ = center(Segment(c[ind - 1], c[ind]))
- p₂ = center(Segment(c[ind], c[ind + 1]))
+ p₁ = centroid(Segment(c[ind - 1], c[ind]))
+ p₂ = centroid(Segment(c[ind], c[ind + 1]))
# normals to cylinder planes
n₁ = c[ind] - c[ind - 1]
@@ -64,4 +72,6 @@ nelements(t::CylindricalTrajectory) = length(t.centroids)
Base.eltype(t::CylindricalTrajectory) = typeof(first(t))
+centroid(t::CylindricalTrajectory, ind::Int) = t.centroids[ind]
+
radius(t::CylindricalTrajectory) = t.radius
diff --git a/src/geometries.jl b/src/geometries.jl
index d51cd11c2..7e5f5b615 100644
--- a/src/geometries.jl
+++ b/src/geometries.jl
@@ -3,11 +3,12 @@
# ------------------------------------------------------------------
"""
- Geometry{Dim,T}
+ Geometry{M,CRS}
-A geometry embedded in a `Dim`-dimensional space with coordinates of type `T`.
+A geometry in a given manifold `M` with point coordinates specified
+in a coordinate reference system `CRS`.
"""
-abstract type Geometry{Dim,T} end
+abstract type Geometry{M<:Manifold,C<:CRS} end
Broadcast.broadcastable(g::Geometry) = Ref(g)
@@ -16,7 +17,7 @@ Broadcast.broadcastable(g::Geometry) = Ref(g)
Return the number of dimensions of the space where the `geometry` is embedded.
"""
-embeddim(::Type{<:Geometry{Dim,T}}) where {Dim,T} = Dim
+embeddim(::Type{<:Geometry{M,CRS}}) where {M,CRS} = CoordRefSystems.ndims(CRS)
embeddim(g::Geometry) = embeddim(typeof(g))
"""
@@ -30,19 +31,28 @@ See also [`isparametrized`](@ref).
paramdim(g::Geometry) = paramdim(typeof(g))
"""
- coordtype(geometry)
+ crs(geometry)
-Return the machine type of each coordinate used to describe the `geometry`.
+Return the coordinate reference system (CRS) of the `geometry`.
"""
-coordtype(::Type{<:Geometry{Dim,T}}) where {Dim,T} = T
-coordtype(g::Geometry) = coordtype(typeof(g))
+crs(::Type{<:Geometry{M,CRS}}) where {M,CRS} = CRS
+crs(g::Geometry) = crs(typeof(g))
"""
- centroid(geometry)
+ manifold(geometry)
-Return the centroid of the `geometry`.
+Return the manifold where the `geometry` is defined.
"""
-centroid(g::Geometry) = center(g)
+manifold(::Type{<:Geometry{M,CRS}}) where {M,CRS} = M
+manifold(g::Geometry) = manifold(typeof(g))
+
+"""
+ lentype(geometry)
+
+Return the length type of the `geometry`.
+"""
+lentype(::Type{<:Geometry{M,CRS}}) where {M,CRS} = lentype(CRS)
+lentype(g::Geometry) = lentype(typeof(g))
"""
extrema(geometry)
@@ -56,20 +66,40 @@ Base.extrema(g::Geometry) = extrema(boundingbox(g))
# IO METHODS
# -----------
-Base.summary(io::IO, geom::Geometry{Dim,T}) where {Dim,T} = print(io, "$(prettyname(geom)){$Dim,$T}")
+Base.summary(io::IO, geom::Geometry) = print(io, prettyname(geom))
+
+function Base.show(io::IO, geom::Geometry)
+ name = prettyname(geom)
+ ioctx = IOContext(io, :compact => true)
+ print(io, "$name(")
+ printfields(ioctx, geom, singleline=true)
+ print(io, ")")
+end
+
+function Base.show(io::IO, ::MIME"text/plain", geom::Geometry)
+ summary(io, geom)
+ printfields(io, geom)
+end
# ----------------
# IMPLEMENTATIONS
# ----------------
-include("primitives.jl")
-include("polytopes.jl")
-include("multigeoms.jl")
+include("geometries/primitives.jl")
+include("geometries/polytopes.jl")
+include("geometries/multigeom.jl")
+include("geometries/transformedgeom.jl")
# ------------
# CONVERSIONS
# ------------
-Base.convert(::Type{<:Quadrangle}, b::Box{2}) = Quadrangle(vertices(boundary(b))...)
+function Base.convert(::Type{<:Quadrangle}, b::Box)
+ checkdim(b, 2)
+ Quadrangle(vertices(boundary(b))...)
+end
-Base.convert(::Type{<:Hexahedron}, b::Box{3}) = Hexahedron(vertices(boundary(b))...)
+function Base.convert(::Type{<:Hexahedron}, b::Box)
+ checkdim(b, 3)
+ Hexahedron(vertices(boundary(b))...)
+end
diff --git a/src/geometries/multigeom.jl b/src/geometries/multigeom.jl
new file mode 100644
index 000000000..19696ae0a
--- /dev/null
+++ b/src/geometries/multigeom.jl
@@ -0,0 +1,93 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Multi(geoms)
+
+A collection of geometries `geoms` seen as a single [`Geometry`](@ref).
+
+In geographic information systems (GIS) it is common to represent
+multiple polygons as a single entity (e.g. country with islands).
+
+### Notes
+
+- Type aliases are [`MultiPoint`](@ref), [`MultiSegment`](@ref),
+ [`MultiRope`](@ref), [`MultiRing`](@ref), [`MultiPolygon`](@ref).
+"""
+struct Multi{M<:Manifold,C<:CRS,G<:Geometry{M,C}} <: Geometry{M,C}
+ geoms::Vector{G}
+end
+
+# constructor with iterator of geometries
+Multi(geoms) = Multi(collect(geoms))
+
+# type aliases for convenience
+const MultiPoint{M<:Manifold,C<:CRS} = Multi{M,C,<:Point{M,C}}
+const MultiSegment{M<:Manifold,C<:CRS} = Multi{M,C,<:Segment{M,C}}
+const MultiRope{M<:Manifold,C<:CRS} = Multi{M,C,<:Rope{M,C}}
+const MultiRing{M<:Manifold,C<:CRS} = Multi{M,C,<:Ring{M,C}}
+const MultiPolygon{M<:Manifold,C<:CRS} = Multi{M,C,<:Polygon{M,C}}
+const MultiPolyhedron{M<:Manifold,C<:CRS} = Multi{M,C,<:Polyhedron{M,C}}
+const MultiPolytope{K,M<:Manifold,C<:CRS} = Multi{M,C,<:Polytope{K,M,C}}
+
+Base.parent(m::Multi) = m.geoms
+
+# ---------
+# GEOMETRY
+# ---------
+
+paramdim(m::Multi) = maximum(paramdim, m.geoms)
+
+==(m₁::Multi, m₂::Multi) = m₁.geoms == m₂.geoms
+
+Base.isapprox(m₁::Multi, m₂::Multi; atol=atol(lentype(m₁)), kwargs...) =
+ length(m₁.geoms) == length(m₂.geoms) && all(isapprox(g₁, g₂; atol, kwargs...) for (g₁, g₂) in zip(m₁.geoms, m₂.geoms))
+
+# ---------
+# POLYTOPE
+# ---------
+
+vertex(m::MultiPolytope, ind) = first(Iterators.drop(eachvertex(m), ind - 1))
+
+vertices(m::MultiPolytope) = collect(eachvertex(m))
+
+nvertices(m::MultiPolytope) = sum(nvertices, m.geoms)
+
+eachvertex(m::MultiPolytope) = (v for g in m.geoms for v in eachvertex(g))
+
+Base.unique(m::MultiPolytope) = unique!(deepcopy(m))
+
+function Base.unique!(m::MultiPolytope)
+ foreach(unique!, m.geoms)
+ m
+end
+
+# --------
+# POLYGON
+# --------
+
+rings(m::MultiPolygon) = [ring for poly in m.geoms for ring in rings(poly)]
+
+# -----------
+# IO METHODS
+# -----------
+
+function Base.summary(io::IO, m::Multi)
+ name = prettyname(eltype(m.geoms))
+ print(io, "Multi$name")
+end
+
+function Base.show(io::IO, m::Multi)
+ print(io, "Multi(")
+ geoms = prettyname.(m.geoms)
+ counts = ("$(count(==(g), geoms))×$g" for g in unique(geoms))
+ join(io, counts, ", ")
+ print(io, ")")
+end
+
+function Base.show(io::IO, ::MIME"text/plain", m::Multi)
+ summary(io, m)
+ println(io)
+ printelms(io, m.geoms)
+end
diff --git a/src/polytopes.jl b/src/geometries/polytopes.jl
similarity index 77%
rename from src/polytopes.jl
rename to src/geometries/polytopes.jl
index 2c73b1d0c..ac16288f9 100644
--- a/src/polytopes.jl
+++ b/src/geometries/polytopes.jl
@@ -3,13 +3,13 @@
# ------------------------------------------------------------------
"""
- Polytope{K,Dim,T}
+ Polytope{K,M,CRS}
We say that a geometry is a K-polytope when it is a collection of "flat" sides
that constitute a `K`-dimensional subspace. They are called chain, polygon and
-polyhedron respectively for 1D (`K=1`), 2D (`K=2`) and 3D (`K=3`) subspaces,
-embedded in a `Dim`-dimensional space. The parameter `K` is also known as the
-rank or parametric dimension of the polytope: .
+polyhedron respectively for 1D (`K=1`), 2D (`K=2`) and 3D (`K=3`) subspaces.
+The parameter `K` is also known as the rank or parametric dimension
+of the polytope ().
The term polytope expresses a particular combinatorial structure. A polyhedron,
for example, can be decomposed into faces. Each face can then be decomposed into
@@ -25,18 +25,32 @@ have (K-1)-polytopes in common. See .
- Type aliases are `Chain`, `Polygon`, `Polyhedron`.
"""
-abstract type Polytope{K,Dim,T} <: Geometry{Dim,T} end
+abstract type Polytope{K,M<:Manifold,C<:CRS} <: Geometry{M,C} end
# heper macro to define polytopes
macro polytope(type, K, N)
- expr = quote
- $Base.@__doc__ struct $type{Dim,T} <: Polytope{$K,Dim,T}
- vertices::NTuple{$N,Point{Dim,T}}
+ structexpr = if K == 3
+ quote
+ struct $type{C<:CRS,Mₚ<:Manifold} <: Polytope{$K,𝔼{3},C}
+ vertices::SVector{$N,Point{Mₚ,C}}
+ end
+ end
+ else
+ quote
+ struct $type{M<:Manifold,C<:CRS} <: Polytope{$K,M,C}
+ vertices::SVector{$N,Point{M,C}}
+ end
end
+ end
+ expr = quote
+ $Base.@__doc__ $structexpr
+
+ $type(vertices::NTuple{$N,P}) where {P<:Point} = $type(SVector(vertices))
$type(vertices::Vararg{Tuple,$N}) = $type(Point.(vertices))
- $type(vertices::Vararg{Point{Dim,T},$N}) where {Dim,T} = $type{Dim,T}(vertices)
+ $type(vertices::Vararg{P,$N}) where {P<:Point} = $type(vertices)
end
+
esc(expr)
end
@@ -45,7 +59,7 @@ end
# -------------------
"""
- Chain{Dim,T}
+ Chain{M,CRS}
A chain is a 1-polytope, i.e. a polytope with parametric dimension 1.
See .
@@ -85,16 +99,16 @@ function Base.open(::Chain) end
Remove duplicate vertices in the `chain`.
Closed chains remain closed.
"""
-function Base.unique!(c::Chain{Dim,T}) where {Dim,T}
+function Base.unique!(c::Chain)
# sort vertices lexicographically
verts = vertices(open(c))
- perms = sortperm(coordinates.(verts))
+ perms = sortperm(to.(verts))
# remove true duplicates
keep = Int[]
sorted = @view verts[perms]
for i in 1:(length(sorted) - 1)
- if !isapprox(sorted[i], sorted[i + 1], atol=atol(T))
+ if !isapprox(sorted[i], sorted[i + 1])
# save index in the original vector
push!(keep, perms[i])
end
@@ -152,7 +166,7 @@ include("polytopes/ring.jl")
# ---------------------
"""
- Polygon{Dim,T}
+ Polygon{M,CRS}
A polygon is a 2-polytope, i.e. a polytope with parametric dimension 2.
@@ -160,6 +174,21 @@ See also [`Ngon`](@ref) and [`PolyArea`](@ref).
"""
const Polygon = Polytope{2}
+"""
+ ≗(polygon₁, polygon₂)
+
+Tells whether or not the `polygon₁` and `polygon₂`
+are equal regardless of circular shifts.
+"""
+function ≗(p₁::Polygon, p₂::Polygon)
+ rings₁ = rings(p₁)
+ rings₂ = rings(p₂)
+ nring₁ = length(rings₁)
+ nring₂ = length(rings₂)
+ nring₁ == nring₂ || return false
+ all(r₁ ≗ r₂ for (r₁, r₂) in zip(rings₁, rings₂))
+end
+
"""
rings(polygon)
@@ -176,7 +205,7 @@ include("polytopes/polyarea.jl")
# ------------------------
"""
- Polyhedron{Dim,T}
+ Polyhedron{M,CRS}
A polyhedron is a 3-polytope, i.e. a polytope with parametric dimension 3.
@@ -188,6 +217,7 @@ const Polyhedron = Polytope{3}
include("polytopes/tetrahedron.jl")
include("polytopes/hexahedron.jl")
include("polytopes/pyramid.jl")
+include("polytopes/wedge.jl")
# -----------------------
# N-POLYTOPE (FALLBACKS)
@@ -217,16 +247,16 @@ vertices(p::Polytope) = p.vertices
"""
nvertices(polytope)
-Return the number of vertices in the `polytope`.
+Return the number of vertices of the `polytope`.
"""
nvertices(p::Polytope) = nvertices(typeof(p))
"""
- centroid(polytope)
+ eachvertex(polytope)
-Return the centroid of the `polytope`.
+Return an iterator for the vertices of the `polytope`.
"""
-centroid(p::Polytope) = Point(sum(coordinates, vertices(p)) / length(vertices(p)))
+eachvertex(p::Polytope) = (vertex(p, i) for i in 1:nvertices(p))
"""
unique(polytope)
@@ -235,9 +265,6 @@ Return a new `polytope` without duplicate vertices.
"""
Base.unique(p::Polytope) = unique!(deepcopy(p))
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{PL}) where {PL<:Polytope} =
- PL(ntuple(i -> rand(rng, Point{embeddim(PL),coordtype(PL)}), nvertices(PL)))
-
# -----------
# IO METHODS
# -----------
diff --git a/src/polytopes/hexahedron.jl b/src/geometries/polytopes/hexahedron.jl
similarity index 70%
rename from src/polytopes/hexahedron.jl
rename to src/geometries/polytopes/hexahedron.jl
index f610ce685..25a82ae60 100644
--- a/src/polytopes/hexahedron.jl
+++ b/src/geometries/polytopes/hexahedron.jl
@@ -11,15 +11,18 @@ A hexahedron with points `p1`, `p2`, ..., `p8`.
nvertices(::Type{<:Hexahedron}) = 8
-Base.isapprox(h₁::Hexahedron, h₂::Hexahedron; kwargs...) =
- all(isapprox(v₁, v₂; kwargs...) for (v₁, v₂) in zip(h₁.vertices, h₂.vertices))
+==(h₁::Hexahedron, h₂::Hexahedron) = h₁.vertices == h₂.vertices
+
+Base.isapprox(h₁::Hexahedron, h₂::Hexahedron; atol=atol(lentype(h₁)), kwargs...) =
+ all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(h₁.vertices, h₂.vertices))
function (h::Hexahedron)(u, v, w)
if (u < 0 || u > 1) || (v < 0 || v > 1) || (w < 0 || w > 1)
throw(DomainError((u, v, w), "h(u, v, w) is not defined for u, v, w outside [0, 1]³."))
end
- A1, A2, A4, A3, A5, A6, A8, A7 = coordinates.(h.vertices)
- Point(
+ A1, A2, A4, A3, A5, A6, A8, A7 = to.(h.vertices)
+ withcrs(
+ h,
(1 - u) * (1 - v) * (1 - w) * A1 +
u * (1 - v) * (1 - w) * A2 +
(1 - u) * v * (1 - w) * A3 +
diff --git a/src/polytopes/ngon.jl b/src/geometries/polytopes/ngon.jl
similarity index 62%
rename from src/polytopes/ngon.jl
rename to src/geometries/polytopes/ngon.jl
index 8daeb789f..604750d80 100644
--- a/src/polytopes/ngon.jl
+++ b/src/geometries/polytopes/ngon.jl
@@ -20,9 +20,9 @@ are `Triangle` (N=3), `Quadrangle` (N=4), `Pentagon` (N=5), etc.
- Type aliases are `Triangle`, `Quadrangle`, `Pentagon`, `Hexagon`,
`Heptagon`, `Octagon`, `Nonagon`, `Decagon`.
"""
-struct Ngon{N,Dim,T} <: Polygon{Dim,T}
- vertices::NTuple{N,Point{Dim,T}}
- function Ngon{N,Dim,T}(vertices::NTuple{N,Point{Dim,T}}) where {N,Dim,T}
+struct Ngon{N,M<:Manifold,C<:CRS} <: Polygon{M,C}
+ vertices::SVector{N,Point{M,C}}
+ function Ngon{N,M,C}(vertices) where {N,M<:Manifold,C<:CRS}
if N < 3
throw(ArgumentError("the number of vertices must be greater than or equal to 3"))
end
@@ -30,12 +30,14 @@ struct Ngon{N,Dim,T} <: Polygon{Dim,T}
end
end
-Ngon{N}(vertices::NTuple{N,Point{Dim,T}}) where {N,Dim,T} = Ngon{N,Dim,T}(vertices)
-Ngon{N}(vertices::Vararg{Point{Dim,T},N}) where {N,Dim,T} = Ngon{N}(vertices)
+Ngon{N}(vertices::SVector{N,Point{M,C}}) where {N,M<:Manifold,C<:CRS} = Ngon{N,M,C}(vertices)
+Ngon{N}(vertices::NTuple{N,P}) where {N,P<:Point} = Ngon{N}(SVector(vertices))
+Ngon{N}(vertices::Vararg{P,N}) where {N,P<:Point} = Ngon{N}(vertices)
Ngon{N}(vertices::Vararg{Tuple,N}) where {N} = Ngon{N}(Point.(vertices))
-Ngon(vertices::NTuple{N,Point{Dim,T}}) where {N,Dim,T} = Ngon{N,Dim,T}(vertices)
-Ngon(vertices::Point{Dim,T}...) where {Dim,T} = Ngon(vertices)
+Ngon(vertices::SVector{N,Point{M,C}}) where {N,M<:Manifold,C<:CRS} = Ngon{N,M,C}(vertices)
+Ngon(vertices::NTuple{N,P}) where {N,P<:Point} = Ngon(SVector(vertices))
+Ngon(vertices::P...) where {P<:Point} = Ngon(vertices)
Ngon(vertices::Tuple...) = Ngon(Point.(vertices))
# type aliases for convenience
@@ -52,10 +54,10 @@ Base.unique!(ngon::Ngon) = ngon
nvertices(::Type{<:Ngon{N}}) where {N} = N
-function Base.isapprox(p₁::Ngon, p₂::Ngon; kwargs...)
- nvertices(p₁) ≠ nvertices(p₂) && return false
- all(isapprox(v₁, v₂; kwargs...) for (v₁, v₂) in zip(p₁.vertices, p₂.vertices))
-end
+==(p₁::Ngon, p₂::Ngon) = p₁.vertices == p₂.vertices
+
+Base.isapprox(p₁::Ngon, p₂::Ngon; atol=atol(lentype(p₁)), kwargs...) =
+ nvertices(p₁) == nvertices(p₂) && all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(p₁.vertices, p₂.vertices))
rings(ngon::Ngon) = [Ring(pointify(ngon))]
@@ -63,22 +65,14 @@ angles(ngon::Ngon) = angles(boundary(ngon))
innerangles(ngon::Ngon) = innerangles(boundary(ngon))
-signarea(ngon::Ngon) = sum(signarea, simplexify(ngon))
-
# ----------
# TRIANGLES
# ----------
-function signarea(t::Triangle{2})
- v = t.vertices
- signarea(v[1], v[2], v[3])
-end
-
-signarea(::Triangle{3}) = error("signed area only defined for triangles embedded in R², use `area` instead")
-
-function normal(t::Triangle{3})
+function normal(t::Triangle)
+ checkdim(t, 3)
A, B, C = t.vertices
- ((B - A) × (C - A)) / 2
+ unormalize(ucross((B - A), (C - A)))
end
function (t::Triangle)(u, v)
@@ -86,8 +80,8 @@ function (t::Triangle)(u, v)
if (u < 0 || u > 1) || (v < 0 || v > 1) || (w < 0 || w > 1)
throw(DomainError((u, v), "invalid barycentric coordinates for triangle."))
end
- v₁, v₂, v₃ = coordinates.(t.vertices)
- Point(v₁ * w + v₂ * u + v₃ * v)
+ v₁, v₂, v₃ = t.vertices
+ coordsum((v₁, v₂, v₃), weights=(w, u, v))
end
# ------------
@@ -99,6 +93,10 @@ function (q::Quadrangle)(u, v)
if (u < 0 || u > 1) || (v < 0 || v > 1)
throw(DomainError((u, v), "q(u, v) is not defined for u, v outside [0, 1]²."))
end
- c₀₀, c₀₁, c₁₁, c₁₀ = coordinates.(q.vertices)
- Point(c₀₀ * (1 - u) * (1 - v) + c₀₁ * u * (1 - v) + c₁₀ * (1 - u) * v + c₁₁ * u * v)
+ c₀₀, c₀₁, c₁₁, c₁₀ = q.vertices
+ w₀₀ = (1 - u) * (1 - v)
+ w₀₁ = u * (1 - v)
+ w₁₀ = (1 - u) * v
+ w₁₁ = u * v
+ coordsum((c₀₀, c₀₁, c₁₀, c₁₁), weights=(w₀₀, w₀₁, w₁₀, w₁₁))
end
diff --git a/src/geometries/polytopes/polyarea.jl b/src/geometries/polytopes/polyarea.jl
new file mode 100644
index 000000000..4980b9f91
--- /dev/null
+++ b/src/geometries/polytopes/polyarea.jl
@@ -0,0 +1,84 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ PolyArea(outer)
+ PolyArea([outer, inner₁, inner₂, ..., innerₖ])
+
+A polygonal area with `outer` ring, and optional inner
+rings `inner₁`, `inner₂`, ..., `innerₖ`.
+
+Rings can be a vector of [`Point`](@ref) or a
+vector of tuples with coordinates for convenience,
+in which case the first point should *not* be repeated
+at the end of the vector.
+"""
+struct PolyArea{M<:Manifold,C<:CRS,R<:Ring{M,C},V<:AbstractVector{R}} <: Polygon{M,C}
+ rings::V
+end
+
+PolyArea(vertices::AbstractVector{<:AbstractVector}) = PolyArea([Ring(v) for v in vertices])
+
+PolyArea(outer::Ring) = PolyArea([outer])
+
+PolyArea(outer::AbstractVector) = PolyArea(Ring(outer))
+
+PolyArea(outer...) = PolyArea(collect(outer))
+
+==(p₁::PolyArea, p₂::PolyArea) = p₁.rings == p₂.rings
+
+Base.isapprox(p₁::PolyArea, p₂::PolyArea; atol=atol(lentype(p₁)), kwargs...) =
+ length(p₁.rings) == length(p₂.rings) && all(isapprox(r₁, r₂; atol, kwargs...) for (r₁, r₂) in zip(p₁.rings, p₂.rings))
+
+function vertex(p::PolyArea, ind)
+ offset = 0
+ for r in p.rings
+ nverts = nvertices(r)
+ if ind ≤ offset + nverts
+ return vertex(r, ind - offset)
+ end
+ offset += nverts
+ end
+ throw(BoundsError(p, ind))
+end
+
+vertices(p::PolyArea) = collect(eachvertex(p))
+
+nvertices(p::PolyArea) = mapreduce(nvertices, +, p.rings)
+
+rings(p::PolyArea) = p.rings
+
+function Base.unique!(p::PolyArea)
+ foreach(unique!, p.rings)
+ inds = findall(r -> nvertices(r) ≤ 2, p.rings)
+ setdiff!(inds, 1) # don't remove outer ring
+ isempty(inds) || deleteat!(p.rings, inds)
+ p
+end
+
+function Base.show(io::IO, p::PolyArea)
+ rings = p.rings
+ print(io, "PolyArea(")
+ if length(rings) == 1
+ r = first(rings)
+ printverts(io, vertices(r))
+ else
+ nverts = nvertices.(rings)
+ join(io, ("$n-Ring" for n in nverts), ", ")
+ end
+ print(io, ")")
+end
+
+function Base.show(io::IO, ::MIME"text/plain", p::PolyArea)
+ rings = p.rings
+ summary(io, p)
+ println(io)
+ println(io, " outer")
+ print(io, " └─ $(rings[1])")
+ if length(rings) > 1
+ println(io)
+ println(io, " inner")
+ printelms(io, @view(rings[2:end]), tab=" ")
+ end
+end
diff --git a/src/polytopes/pyramid.jl b/src/geometries/polytopes/pyramid.jl
similarity index 58%
rename from src/polytopes/pyramid.jl
rename to src/geometries/polytopes/pyramid.jl
index 02bc2e62d..ad7f9c79c 100644
--- a/src/polytopes/pyramid.jl
+++ b/src/geometries/polytopes/pyramid.jl
@@ -11,5 +11,7 @@ A pyramid with points `p1`, `p2`, `p3`, `p4`, `p5`.
nvertices(::Type{<:Pyramid}) = 5
-Base.isapprox(p₁::Pyramid, p₂::Pyramid; kwargs...) =
- all(isapprox(v₁, v₂; kwargs...) for (v₁, v₂) in zip(p₁.vertices, p₂.vertices))
+==(p₁::Pyramid, p₂::Pyramid) = p₁.vertices == p₂.vertices
+
+Base.isapprox(p₁::Pyramid, p₂::Pyramid; atol=atol(lentype(p₁)), kwargs...) =
+ all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(p₁.vertices, p₂.vertices))
diff --git a/src/geometries/polytopes/ring.jl b/src/geometries/polytopes/ring.jl
new file mode 100644
index 000000000..6e3c726b1
--- /dev/null
+++ b/src/geometries/polytopes/ring.jl
@@ -0,0 +1,60 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Ring(p1, p2, ..., pn)
+
+A closed polygonal chain from a sequence of points `p1`, `p2`, ..., `pn`.
+
+See also [`Chain`](@ref) and [`Rope`](@ref).
+"""
+struct Ring{M<:Manifold,C<:CRS,V<:CircularVector{Point{M,C}}} <: Chain{M,C}
+ vertices::V
+end
+
+Ring(vertices::Tuple...) = Ring([Point(v) for v in vertices])
+Ring(vertices::P...) where {P<:Point} = Ring(collect(vertices))
+Ring(vertices::AbstractVector{<:Tuple}) = Ring(Point.(vertices))
+Ring(vertices::AbstractVector{<:Point}) = Ring(CircularVector(vertices))
+
+nvertices(r::Ring) = length(r.vertices)
+
+==(r₁::Ring, r₂::Ring) = r₁.vertices == r₂.vertices
+
+"""
+ ≗(ring₁, ring₂)
+
+Tells whether or not the `ring₁` and `ring₂`
+are equal regardless of circular shifts.
+"""
+function ≗(r₁::Ring, r₂::Ring)
+ n = length(r₁.vertices)
+ i = findfirst(==(first(r₁.vertices)), r₂.vertices)
+ isnothing(i) && return false
+ r₁.vertices == r₂.vertices[i:(i + n - 1)]
+end
+
+Base.isapprox(r₁::Ring, r₂::Ring; atol=atol(lentype(r₁)), kwargs...) =
+ nvertices(r₁) == nvertices(r₂) && all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(r₁.vertices, r₂.vertices))
+
+Base.close(r::Ring) = r
+
+# call `open` again to avoid issues with nested CircularVector
+Base.open(r::Ring) = open(Rope(parent(r.vertices)))
+
+# do not change which vertex comes first in reverse order
+Base.reverse!(r::Ring) = (reverse!(@view r.vertices[(begin + 1):end]); r)
+
+"""
+ innerangles(ring)
+
+Return inner angles of the `ring`. Inner
+angles are always positive, and unlike
+`angles` they can be greater than `π`.
+"""
+function innerangles(r::Ring)
+ # correct sign of angles in case orientation is CW
+ θs = orientation(r) == CW ? -angles(r) : angles(r)
+ [θ > 0 ? 2 * oftype(θ, π) - θ : -θ for θ in θs]
+end
diff --git a/src/polytopes/rope.jl b/src/geometries/polytopes/rope.jl
similarity index 60%
rename from src/polytopes/rope.jl
rename to src/geometries/polytopes/rope.jl
index 2701922a3..0cae2a71d 100644
--- a/src/polytopes/rope.jl
+++ b/src/geometries/polytopes/rope.jl
@@ -9,28 +9,23 @@ An open polygonal chain from a sequence of points `p1`, `p2`, ..., `pn`.
See also [`Chain`](@ref) and [`Ring`](@ref).
"""
-struct Rope{Dim,T,V<:AbstractVector{Point{Dim,T}}} <: Chain{Dim,T}
+struct Rope{M<:Manifold,C<:CRS,V<:AbstractVector{Point{M,C}}} <: Chain{M,C}
vertices::V
end
Rope(vertices::Tuple...) = Rope([Point(v) for v in vertices])
-Rope(vertices::Point{Dim,T}...) where {Dim,T} = Rope(collect(vertices))
+Rope(vertices::P...) where {P<:Point} = Rope(collect(vertices))
Rope(vertices::AbstractVector{<:Tuple}) = Rope(Point.(vertices))
nvertices(r::Rope) = length(r.vertices)
==(r₁::Rope, r₂::Rope) = r₁.vertices == r₂.vertices
-function Base.isapprox(r₁::Rope, r₂::Rope; kwargs...)
- nvertices(r₁) ≠ nvertices(r₂) && return false
- all(isapprox(v₁, v₂; kwargs...) for (v₁, v₂) in zip(r₁.vertices, r₂.vertices))
-end
+Base.isapprox(r₁::Rope, r₂::Rope; atol=atol(lentype(r₁)), kwargs...) =
+ nvertices(r₁) == nvertices(r₂) && all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(r₁.vertices, r₂.vertices))
Base.close(r::Rope) = Ring(r.vertices)
Base.open(r::Rope) = r
Base.reverse!(r::Rope) = (reverse!(r.vertices); r)
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{<:Rope{Dim,T}}) where {Dim,T} =
- Rope(rand(rng, Point{Dim,T}, rand(2:50)))
diff --git a/src/polytopes/segment.jl b/src/geometries/polytopes/segment.jl
similarity index 69%
rename from src/polytopes/segment.jl
rename to src/geometries/polytopes/segment.jl
index 1a7b50c4d..28e523dae 100644
--- a/src/polytopes/segment.jl
+++ b/src/geometries/polytopes/segment.jl
@@ -21,18 +21,17 @@ Base.maximum(s::Segment) = s.vertices[2]
Base.extrema(s::Segment) = s.vertices[1], s.vertices[2]
-function center(s::Segment)
- a, b = extrema(s)
- Point((coordinates(a) + coordinates(b)) / 2)
-end
+==(s₁::Segment, s₂::Segment) = s₁.vertices == s₂.vertices
-Base.isapprox(s₁::Segment, s₂::Segment; kwargs...) =
- all(isapprox(v₁, v₂; kwargs...) for (v₁, v₂) in zip(s₁.vertices, s₂.vertices))
+Base.isapprox(s₁::Segment, s₂::Segment; atol=atol(lentype(s₁)), kwargs...) =
+ all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(s₁.vertices, s₂.vertices))
function (s::Segment)(t)
if t < 0 || t > 1
throw(DomainError(t, "s(t) is not defined for t outside [0, 1]."))
end
a, b = s.vertices
- a + t * (b - a)
+ coordsum((a, b), weights=((1 - t), t))
end
+
+Base.reverse(s::Segment) = Segment(reverse(extrema(s)))
diff --git a/src/polytopes/tetrahedron.jl b/src/geometries/polytopes/tetrahedron.jl
similarity index 62%
rename from src/polytopes/tetrahedron.jl
rename to src/geometries/polytopes/tetrahedron.jl
index ab6df655e..b6aa7cec7 100644
--- a/src/polytopes/tetrahedron.jl
+++ b/src/geometries/polytopes/tetrahedron.jl
@@ -11,14 +11,16 @@ A tetrahedron with points `p1`, `p2`, `p3`, `p4`.
nvertices(::Type{<:Tetrahedron}) = 4
-Base.isapprox(t₁::Tetrahedron, t₂::Tetrahedron; kwargs...) =
- all(isapprox(v₁, v₂; kwargs...) for (v₁, v₂) in zip(t₁.vertices, t₂.vertices))
+==(t₁::Tetrahedron, t₂::Tetrahedron) = t₁.vertices == t₂.vertices
+
+Base.isapprox(t₁::Tetrahedron, t₂::Tetrahedron; atol=atol(lentype(t₁)), kwargs...) =
+ all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(t₁.vertices, t₂.vertices))
function (t::Tetrahedron)(u, v, w)
z = (1 - u - v - w)
if (u < 0 || u > 1) || (v < 0 || v > 1) || (w < 0 || w > 1) || (z < 0 || z > 1)
throw(DomainError((u, v, w), "invalid barycentric coordinates for tetrahedron."))
end
- v₁, v₂, v₃, v₄ = coordinates.(t.vertices)
- Point(v₁ * z + v₂ * u + v₃ * v + v₄ * w)
+ v₁, v₂, v₃, v₄ = to.(t.vertices)
+ withcrs(t, v₁ * z + v₂ * u + v₃ * v + v₄ * w)
end
diff --git a/src/geometries/polytopes/wedge.jl b/src/geometries/polytopes/wedge.jl
new file mode 100644
index 000000000..1c8766dc5
--- /dev/null
+++ b/src/geometries/polytopes/wedge.jl
@@ -0,0 +1,17 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Wedge(p1, p2, p3, p4, p5, p6)
+
+A Wedge with points `p1`, `p2`, `p3`, `p4`, `p5`, `p6`.
+"""
+@polytope Wedge 3 6
+
+nvertices(::Type{<:Wedge}) = 6
+
+==(t₁::Wedge, t₂::Wedge) = t₁.vertices == t₂.vertices
+
+Base.isapprox(t₁::Wedge, t₂::Wedge; atol=atol(lentype(t₁)), kwargs...) =
+ all(isapprox(v₁, v₂; atol, kwargs...) for (v₁, v₂) in zip(t₁.vertices, t₂.vertices))
diff --git a/src/primitives.jl b/src/geometries/primitives.jl
similarity index 76%
rename from src/primitives.jl
rename to src/geometries/primitives.jl
index 5953b80ee..9b75338f4 100644
--- a/src/primitives.jl
+++ b/src/geometries/primitives.jl
@@ -3,42 +3,32 @@
# ------------------------------------------------------------------
"""
- Primitive{Dim,T}
+ Primitive{M,CRS}
We say that a geometry is a primitive when it can be expressed as a single
entity with no parts (a.k.a. atomic). For example, a sphere is a primitive
described in terms of a mathematical expression involving a metric and a radius.
See .
"""
-abstract type Primitive{Dim,T} <: Geometry{Dim,T} end
-
-function Base.show(io::IO, geom::Primitive)
- name = prettyname(geom)
- print(io, "$name(")
- printfields(io, geom, compact=true)
- print(io, ")")
-end
-
-function Base.show(io::IO, ::MIME"text/plain", geom::Primitive)
- summary(io, geom)
- printfields(io, geom)
-end
+abstract type Primitive{M<:Manifold,C<:CRS} <: Geometry{M,C} end
include("primitives/point.jl")
include("primitives/ray.jl")
include("primitives/line.jl")
include("primitives/bezier.jl")
+include("primitives/parametrizedcurve.jl")
include("primitives/plane.jl")
include("primitives/box.jl")
include("primitives/ball.jl")
include("primitives/sphere.jl")
+include("primitives/ellipsoid.jl")
include("primitives/disk.jl")
include("primitives/circle.jl")
include("primitives/cylinder.jl")
include("primitives/cylindersurface.jl")
-include("primitives/paraboloidsurface.jl")
include("primitives/cone.jl")
include("primitives/conesurface.jl")
include("primitives/frustum.jl")
include("primitives/frustumsurface.jl")
+include("primitives/paraboloidsurface.jl")
include("primitives/torus.jl")
diff --git a/src/geometries/primitives/ball.jl b/src/geometries/primitives/ball.jl
new file mode 100644
index 000000000..2ef1919e3
--- /dev/null
+++ b/src/geometries/primitives/ball.jl
@@ -0,0 +1,64 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Ball(center, radius)
+
+A ball with `center` and `radius`.
+
+See also [`Sphere`](@ref).
+"""
+struct Ball{M<:Manifold,C<:CRS,ℒ<:Len} <: Primitive{M,C}
+ center::Point{M,C}
+ radius::ℒ
+ Ball(center::Point{M,C}, radius::ℒ) where {M<:Manifold,C<:CRS,ℒ<:Len} = new{M,C,float(ℒ)}(center, radius)
+end
+
+Ball(center::Point, radius) = Ball(center, addunit(radius, u"m"))
+
+Ball(center::Tuple, radius) = Ball(Point(center), radius)
+
+Ball(center::Point) = Ball(center, oneunit(lentype(center)))
+
+Ball(center::Tuple) = Ball(Point(center))
+
+paramdim(::Type{<:Ball{𝔼{Dim}}}) where {Dim} = Dim
+
+paramdim(::Type{<:Ball{🌐}}) = 2
+
+center(b::Ball) = b.center
+
+radius(b::Ball) = b.radius
+
+==(b₁::Ball, b₂::Ball) = b₁.center == b₂.center && b₁.radius == b₂.radius
+
+Base.isapprox(b₁::Ball, b₂::Ball; atol=atol(lentype(b₁)), kwargs...) =
+ isapprox(b₁.center, b₂.center; atol, kwargs...) && isapprox(b₁.radius, b₂.radius; atol, kwargs...)
+
+function (b::Ball{𝔼{2}})(ρ, φ)
+ T = numtype(lentype(b))
+ if (ρ < 0 || ρ > 1) || (φ < 0 || φ > 1)
+ throw(DomainError((ρ, φ), "b(ρ, φ) is not defined for ρ, φ outside [0, 1]²."))
+ end
+ c = b.center
+ r = b.radius
+ ρ′ = T(ρ) * r
+ φ′ = T(φ) * 2 * T(π) * u"rad"
+ p = Point(convert(crs(b), Polar(ρ′, φ′)))
+ p + to(c)
+end
+
+function (b::Ball{𝔼{3}})(ρ, θ, φ)
+ T = numtype(lentype(b))
+ if (ρ < 0 || ρ > 1) || (θ < 0 || θ > 1) || (φ < 0 || φ > 1)
+ throw(DomainError((ρ, θ, φ), "b(ρ, θ, φ) is not defined for ρ, θ, φ outside [0, 1]³."))
+ end
+ c = b.center
+ r = b.radius
+ ρ′ = T(ρ) * r
+ θ′ = T(θ) * T(π) * u"rad"
+ φ′ = T(φ) * 2 * T(π) * u"rad"
+ p = Point(convert(crs(b), Spherical(ρ′, θ′, φ′)))
+ p + to(c)
+end
diff --git a/src/primitives/bezier.jl b/src/geometries/primitives/bezier.jl
similarity index 72%
rename from src/primitives/bezier.jl
rename to src/geometries/primitives/bezier.jl
index 6cc305570..3e35edcb0 100644
--- a/src/primitives/bezier.jl
+++ b/src/geometries/primitives/bezier.jl
@@ -17,15 +17,15 @@ large number of points but less precise, can be used via
## Examples
```julia
-BezierCurve(Point2[(0.,0.),(1.,-1.)])
+BezierCurve([(0.,0.),(1.,-1.)])
```
"""
-struct BezierCurve{Dim,T,V<:AbstractVector{Point{Dim,T}}} <: Primitive{Dim,T}
+struct BezierCurve{M<:Manifold,C<:CRS,V<:AbstractVector{Point{M,C}}} <: Primitive{M,C}
controls::V
end
BezierCurve(points::AbstractVector{<:Tuple}) = BezierCurve(Point.(points))
-BezierCurve(points::Vararg) = BezierCurve(collect(points))
+BezierCurve(points...) = BezierCurve(collect(points))
paramdim(::Type{<:BezierCurve}) = 1
@@ -35,6 +35,12 @@ ncontrols(b::BezierCurve) = length(b.controls)
degree(b::BezierCurve) = ncontrols(b) - 1
+==(b₁::BezierCurve, b₂::BezierCurve) = b₁.controls == b₂.controls
+
+Base.isapprox(b₁::BezierCurve, b₂::BezierCurve; atol=atol(lentype(b₁)), kwargs...) =
+ length(b₁.controls) == length(b₂.controls) &&
+ all(isapprox(p₁, p₂; atol, kwargs...) for (p₁, p₂) in zip(b₁.controls, b₂.controls))
+
"""
Evaluation method used to obtain a point along
a Bézier curve from a parametric expression.
@@ -79,14 +85,15 @@ end
# curve, aᵢ = binomial(n, i) * pᵢ * t̄ⁿ⁻ⁱ and t̄ = (1 - t).
# Horner's rule recursively reconstructs B from a sequence bᵢ
# with bₙ = aₙ and bᵢ₋₁ = aᵢ₋₁ + bᵢ * t until b₀ = B.
-function (curve::BezierCurve{Dim,T})(t, ::Horner) where {Dim,T}
+function (curve::BezierCurve)(t, ::Horner)
+ T = numtype(lentype(curve))
if t < 0 || t > 1
throw(DomainError(t, "b(t) is not defined for t outside [0, 1]."))
end
cs = curve.controls
t̄ = one(T) - t
n = degree(curve)
- pₙ = coordinates(last(cs))
+ pₙ = to(last(cs))
aₙ = pₙ
# initialization with i = n + 1, so bᵢ₋₁ = bₙ = aₙ
@@ -95,15 +102,31 @@ function (curve::BezierCurve{Dim,T})(t, ::Horner) where {Dim,T}
t̄ⁿ⁻ⁱ = one(T)
for i in n:-1:1
cᵢ₋₁ *= i / (n - i + one(T))
- pᵢ₋₁ = coordinates(cs[i])
+ pᵢ₋₁ = to(cs[i])
t̄ⁿ⁻ⁱ *= t̄
aᵢ₋₁ = cᵢ₋₁ * pᵢ₋₁ * t̄ⁿ⁻ⁱ
bᵢ₋₁ = aᵢ₋₁ + bᵢ₋₁ * t
end
b₀ = bᵢ₋₁
- Point(b₀)
+ withcrs(curve, b₀)
+end
+
+# -----------
+# IO METHODS
+# -----------
+
+function Base.show(io::IO, b::BezierCurve)
+ ioctx = IOContext(io, :compact => true)
+ print(io, "BezierCurve(controls: [")
+ join(ioctx, b.controls, ", ")
+ print(io, "])")
end
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{BezierCurve{Dim,T}}) where {Dim,T} =
- BezierCurve(rand(rng, Point{Dim,T}, 5))
+function Base.show(io::IO, ::MIME"text/plain", b::BezierCurve)
+ summary(io, b)
+ println(io)
+ print(io, "└─ controls: [")
+ join(io, b.controls, ", ")
+ print(io, "]")
+end
diff --git a/src/geometries/primitives/box.jl b/src/geometries/primitives/box.jl
new file mode 100644
index 000000000..4eb3e2947
--- /dev/null
+++ b/src/geometries/primitives/box.jl
@@ -0,0 +1,68 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Box(min, max)
+
+A (geodesic) box with `min` and `max` points on a given manifold.
+
+## Examples
+
+Construct a 3D box using points with Cartesian coordinates:
+
+```julia
+Box((0, 0, 0), (1, 1, 1))
+```
+
+Likewise, construct a 2D box on the plane:
+
+```julia
+Box((0, 0), (1, 1))
+```
+
+Construct a geodesic box on the ellipsoid:
+
+```julia
+Box(Point(LatLon(0, 0)), Point(LatLon(1, 1)))
+```
+"""
+struct Box{M<:Manifold,C<:CRS} <: Primitive{M,C}
+ min::Point{M,C}
+ max::Point{M,C}
+
+ function Box{M,C}(min, max) where {M<:Manifold,C<:CRS}
+ assertion(min ⪯ max, "can only construct box with min ⪯ max")
+ new(min, max)
+ end
+end
+
+Box(min::Point{M,C}, max::Point{M,C}) where {M<:Manifold,C<:CRS} = Box{M,C}(min, max)
+
+Box(min::Tuple, max::Tuple) = Box(Point(min), Point(max))
+
+paramdim(::Type{<:Box{𝔼{Dim}}}) where {Dim} = Dim
+
+paramdim(::Type{<:Box{🌐}}) = 2
+
+Base.minimum(b::Box) = b.min
+
+Base.maximum(b::Box) = b.max
+
+Base.extrema(b::Box) = b.min, b.max
+
+diagonal(b::Box{<:𝔼}) = norm(b.max - b.min)
+
+sides(b::Box{<:𝔼}) = Tuple(b.max - b.min)
+
+==(b₁::Box, b₂::Box) = b₁.min == b₂.min && b₁.max == b₂.max
+
+Base.isapprox(b₁::Box, b₂::Box; atol=atol(lentype(b₁)), kwargs...) =
+ isapprox(b₁.min, b₂.min; atol, kwargs...) && isapprox(b₁.max, b₂.max; atol, kwargs...)
+
+function (b::Box{<:𝔼})(uv...)
+ if !all(x -> 0 ≤ x ≤ 1, uv)
+ throw(DomainError(uv, "b(u, v, ...) is not defined for u, v, ... outside [0, 1]ⁿ."))
+ end
+ b.min + uv .* (b.max - b.min)
+end
diff --git a/src/primitives/circle.jl b/src/geometries/primitives/circle.jl
similarity index 55%
rename from src/primitives/circle.jl
rename to src/geometries/primitives/circle.jl
index 8dd91ef63..81a42af01 100644
--- a/src/primitives/circle.jl
+++ b/src/geometries/primitives/circle.jl
@@ -10,34 +10,35 @@ given `plane` with given `radius`.
See also [`Disk`](@ref).
"""
-struct Circle{T} <: Primitive{3,T}
- plane::Plane{T}
- radius::T
+struct Circle{C<:CRS,P<:Plane{C},ℒ<:Len} <: Primitive{𝔼{3},C}
+ plane::P
+ radius::ℒ
+ Circle(plane::P, radius::ℒ) where {C<:CRS,P<:Plane{C},ℒ<:Len} = new{C,P,float(ℒ)}(plane, radius)
end
+Circle(plane::Plane, radius) = Circle(plane, addunit(radius, u"m"))
+
"""
Circle(p1, p2, p3)
A circle passing through points `p1`, `p2` and `p3`.
"""
-function Circle(p1::Point{3}, p2::Point{3}, p3::Point{3})
+function Circle(p1::Point, p2::Point, p3::Point)
v12 = p2 - p1
v13 = p3 - p1
- m12 = coordinates(p1 + v12 / 2)
- m13 = coordinates(p1 + v13 / 2)
+ m12 = to(p1 + v12 / 2)
+ m13 = to(p1 + v13 / 2)
n⃗ = normal(Plane(p1, p2, p3))
- F = coordinates(p1) ⋅ n⃗
+ F = to(p1) ⋅ n⃗
M = transpose([n⃗ v12 v13])
u = [F, m12 ⋅ v12, m13 ⋅ v13]
- O = Point(inv(M) * u)
+ O = withcrs(p1, uinv(M) * u)
r = norm(p1 - O)
Circle(Plane(O, n⃗), r)
end
Circle(p1::Tuple, p2::Tuple, p3::Tuple) = Circle(Point(p1), Point(p2), Point(p3))
-Circle(plane::Plane{T}, radius) where {T} = Circle(plane, T(radius))
-
paramdim(::Type{<:Circle}) = 1
plane(c::Circle) = c.plane
@@ -46,17 +47,20 @@ center(c::Circle) = c.plane(0, 0)
radius(c::Circle) = c.radius
-function (c::Circle{T})(φ) where {T}
+==(c₁::Circle, c₂::Circle) = c₁.plane == c₂.plane && c₁.radius == c₂.radius
+
+Base.isapprox(c₁::Circle, c₂::Circle; atol=atol(lentype(c₁)), kwargs...) =
+ isapprox(c₁.plane, c₂.plane; atol, kwargs...) && isapprox(c₁.radius, c₂.radius; atol, kwargs...)
+
+function (c::Circle)(φ)
+ T = numtype(lentype(c))
if (φ < 0 || φ > 1)
throw(DomainError(φ, "c(φ) is not defined for φ outside [0, 1]."))
end
r = c.radius
- l = T(r)
+ l = r
sφ, cφ = sincospi(2 * T(φ))
- u = l * cφ
- v = l * sφ
+ u = ustrip(l * cφ)
+ v = ustrip(l * sφ)
c.plane(u, v)
end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Circle{T}}) where {T} =
- Circle(rand(rng, Plane{T}), rand(rng, T))
diff --git a/src/geometries/primitives/cone.jl b/src/geometries/primitives/cone.jl
new file mode 100644
index 000000000..61bf73d77
--- /dev/null
+++ b/src/geometries/primitives/cone.jl
@@ -0,0 +1,45 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Cone(base, apex)
+
+A cone with `base` disk and `apex`.
+See .
+
+See also [`ConeSurface`](@ref).
+"""
+struct Cone{C<:CRS,D<:Disk{C},Mₚ<:Manifold} <: Primitive{𝔼{3},C}
+ base::D
+ apex::Point{Mₚ,C}
+end
+
+function Cone(base::Disk{C}, apex::Tuple) where {C<:Cartesian}
+ coords = convert(C, Cartesian{datum(C)}(apex))
+ Cone(base, Point(coords))
+end
+
+paramdim(::Type{<:Cone}) = 3
+
+base(c::Cone) = c.base
+
+apex(c::Cone) = c.apex
+
+height(c::Cone) = norm(center(base(c)) - apex(c))
+
+halfangle(c::Cone) = atan(radius(base(c)), height(c))
+
+==(c₁::Cone, c₂::Cone) = boundary(c₁) == boundary(c₂)
+
+Base.isapprox(c₁::Cone, c₂::Cone; atol=atol(lentype(c₁)), kwargs...) =
+ isapprox(boundary(c₁), boundary(c₂); atol, kwargs...)
+
+function (c::Cone)(r, φ, h)
+ if (r < 0 || r > 1) || (φ < 0 || φ > 1) || (h < 0 || h > 1)
+ throw(DomainError((r, φ, h), "c(r, φ, h) is not defined for r, φ, h outside [0, 1]³."))
+ end
+ a = c.apex
+ b = c.base(r, φ)
+ Segment(b, a)(h)
+end
diff --git a/src/geometries/primitives/conesurface.jl b/src/geometries/primitives/conesurface.jl
new file mode 100644
index 000000000..5679fd434
--- /dev/null
+++ b/src/geometries/primitives/conesurface.jl
@@ -0,0 +1,42 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ ConeSurface(base, apex)
+
+A cone surface with `base` disk and `apex`.
+See .
+
+See also [`Cone`](@ref).
+"""
+struct ConeSurface{C<:CRS,D<:Disk{C},Mₚ<:Manifold} <: Primitive{𝔼{3},C}
+ base::D
+ apex::Point{Mₚ,C}
+end
+
+function ConeSurface(base::Disk{C}, apex::Tuple) where {C<:Cartesian}
+ coords = convert(C, Cartesian{datum(C)}(apex))
+ ConeSurface(base, Point(coords))
+end
+
+paramdim(::Type{<:ConeSurface}) = 2
+
+base(c::ConeSurface) = c.base
+
+apex(c::ConeSurface) = c.apex
+
+==(c₁::ConeSurface, c₂::ConeSurface) = c₁.base == c₂.base && c₁.apex == c₂.apex
+
+Base.isapprox(c₁::ConeSurface, c₂::ConeSurface; atol=atol(lentype(c₁)), kwargs...) =
+ isapprox(c₁.base, c₂.base; atol, kwargs...) && isapprox(c₁.apex, c₂.apex; atol, kwargs...)
+
+function (c::ConeSurface)(φ, h)
+ T = numtype(lentype(c))
+ if (φ < 0 || φ > 1) || (h < 0 || h > 1)
+ throw(DomainError((φ, h), "c(φ, h) is not defined for φ, h outside [0, 1]²."))
+ end
+ a = c.apex
+ b = c.base(one(T), φ)
+ Segment(b, a)(h)
+end
diff --git a/src/primitives/cylinder.jl b/src/geometries/primitives/cylinder.jl
similarity index 59%
rename from src/primitives/cylinder.jl
rename to src/geometries/primitives/cylinder.jl
index deb2f5aff..c1ccf1eed 100644
--- a/src/primitives/cylinder.jl
+++ b/src/geometries/primitives/cylinder.jl
@@ -24,26 +24,33 @@ Finally, construct a right vertical circular cylinder with given `radius`.
See .
"""
-struct Cylinder{T} <: Primitive{3,T}
- bot::Plane{T}
- top::Plane{T}
- radius::T
+struct Cylinder{C<:CRS,P<:Plane{C},ℒ<:Len} <: Primitive{𝔼{3},C}
+ bot::P
+ top::P
+ radius::ℒ
+ Cylinder(bot::P, top::P, radius::ℒ) where {C<:CRS,P<:Plane{C},ℒ<:Len} = new{C,P,float(ℒ)}(bot, top, radius)
end
-function Cylinder(start::Point{3,T}, finish::Point{3,T}, radius) where {T}
+Cylinder(bot::P, top::P, radius) where {P<:Plane} = Cylinder(bot, top, addunit(radius, u"m"))
+
+function Cylinder(start::Point, finish::Point, radius)
dir = finish - start
bot = Plane(start, dir)
top = Plane(finish, dir)
- Cylinder(bot, top, T(radius))
+ Cylinder(bot, top, radius)
end
Cylinder(start::Tuple, finish::Tuple, radius) = Cylinder(Point(start), Point(finish), radius)
-Cylinder(start::Point{3,T}, finish::Point{3,T}) where {T} = Cylinder(start, finish, T(1))
+Cylinder(start::Point, finish::Point) = Cylinder(start, finish, oneunit(lentype(start)))
Cylinder(start::Tuple, finish::Tuple) = Cylinder(Point(start), Point(finish))
-Cylinder(radius::T) where {T} = Cylinder(Point(T(0), T(0), T(0)), Point(T(0), T(0), T(1)), radius)
+function Cylinder(radius)
+ z = zero(radius)
+ o = oneunit(radius)
+ Cylinder(Point(z, z, z), Point(z, z, o), radius)
+end
paramdim(::Type{<:Cylinder}) = 3
@@ -53,17 +60,20 @@ bottom(c::Cylinder) = c.bot
top(c::Cylinder) = c.top
-center(c::Cylinder) = center(boundary(c))
-
axis(c::Cylinder) = axis(boundary(c))
isright(c::Cylinder) = isright(boundary(c))
hasintersectingplanes(c::Cylinder) = hasintersectingplanes(boundary(c))
-Base.isapprox(c₁::Cylinder, c₂::Cylinder) = boundary(c₁) ≈ boundary(c₂)
+==(c₁::Cylinder, c₂::Cylinder) = boundary(c₁) == boundary(c₂)
-function (c::Cylinder{T})(ρ, φ, z) where {T}
+Base.isapprox(c₁::Cylinder, c₂::Cylinder; atol=atol(lentype(c₁)), kwargs...) =
+ isapprox(boundary(c₁), boundary(c₂); atol, kwargs...)
+
+function (c::Cylinder)(ρ, φ, z)
+ ℒ = lentype(c)
+ T = numtype(ℒ)
if (ρ < 0 || ρ > 1) || (φ < 0 || φ > 1) || (z < 0 || z > 1)
throw(DomainError((ρ, φ, z), "c(ρ, φ, z) is not defined for ρ, φ, z outside [0, 1]³."))
end
@@ -73,19 +83,17 @@ function (c::Cylinder{T})(ρ, φ, z) where {T}
a = axis(c)
d = a(T(1)) - a(T(0))
h = norm(d)
- o = b(0, 0)
+ o = b(T(0), T(0))
# rotation to align z axis with cylinder axis
- Q = rotation_between(Vec{3,T}(0, 0, 1), d)
+ Q = urotbetween(Vec(zero(ℒ), zero(ℒ), oneunit(ℒ)), d)
# project a parametric segment between the top and bottom planes
- lsφ, lcφ = T(ρ) * r .* sincospi(2 * T(φ))
- p₁ = o + Q * Vec(lcφ, lsφ, T(0))
- p₂ = o + Q * Vec(lcφ, lsφ, h)
- l = Line(p₁, p₂)
+ ρ′ = T(ρ) * r
+ φ′ = T(φ) * 2 * T(π) * u"rad"
+ p₁ = Point(convert(crs(c), Cylindrical(ρ′, φ′, zero(ℒ))))
+ p₂ = Point(convert(crs(c), Cylindrical(ρ′, φ′, h)))
+ l = Line(p₁, p₂) |> Affine(Q, to(o))
s = Segment(l ∩ b, l ∩ t)
s(T(z))
end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Cylinder{T}}) where {T} =
- Cylinder(rand(rng, Plane{T}), rand(rng, Plane{T}), rand(rng, T))
diff --git a/src/geometries/primitives/cylindersurface.jl b/src/geometries/primitives/cylindersurface.jl
new file mode 100644
index 000000000..b82581340
--- /dev/null
+++ b/src/geometries/primitives/cylindersurface.jl
@@ -0,0 +1,91 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ CylinderSurface(bottom, top, radius)
+
+A circular cylinder surface embedded in R³ with given `radius`,
+delimited by `bottom` and `top` planes.
+
+ CylinderSurface(start, finish, radius)
+
+Alternatively, construct a right circular cylinder surface with given `radius`
+along the segment with `start` and `finish` end points.
+
+ CylinderSurface(start, finish)
+
+Or construct a right circular cylinder surface with unit radius along the segment
+with `start` and `finish` end points.
+
+ CylinderSurface(radius)
+
+Finally, construct a right vertical circular cylinder surface with given `radius`.
+
+See .
+"""
+struct CylinderSurface{C<:CRS,P<:Plane{C},ℒ<:Len} <: Primitive{𝔼{3},C}
+ bot::P
+ top::P
+ radius::ℒ
+ CylinderSurface(bot::P, top::P, radius::ℒ) where {C<:CRS,P<:Plane{C},ℒ<:Len} = new{C,P,float(ℒ)}(bot, top, radius)
+end
+
+CylinderSurface(bot::P, top::P, radius) where {P<:Plane} = CylinderSurface(bot, top, addunit(radius, u"m"))
+
+function CylinderSurface(start::Point, finish::Point, radius)
+ dir = finish - start
+ bot = Plane(start, dir)
+ top = Plane(finish, dir)
+ CylinderSurface(bot, top, radius)
+end
+
+CylinderSurface(start::Tuple, finish::Tuple, radius) = CylinderSurface(Point(start), Point(finish), radius)
+
+CylinderSurface(start::Point, finish::Point) = CylinderSurface(start, finish, oneunit(lentype(start)))
+
+CylinderSurface(start::Tuple, finish::Tuple) = CylinderSurface(Point(start), Point(finish))
+
+function CylinderSurface(radius)
+ z = zero(radius)
+ o = oneunit(radius)
+ CylinderSurface(Point(z, z, z), Point(z, z, o), radius)
+end
+
+paramdim(::Type{<:CylinderSurface}) = 2
+
+radius(c::CylinderSurface) = c.radius
+
+bottom(c::CylinderSurface) = c.bot
+
+top(c::CylinderSurface) = c.top
+
+axis(c::CylinderSurface) = Line(c.bot(0, 0), c.top(0, 0))
+
+function isright(c::CylinderSurface)
+ ℒ = lentype(c)
+ T = numtype(ℒ)
+ # cylinder is right if axis
+ # is aligned with plane normals
+ a = axis(c)
+ d = a(T(1)) - a(T(0))
+ v = normal(c.bot)
+ w = normal(c.top)
+ isparallelv = isapproxzero(norm(d × v))
+ isparallelw = isapproxzero(norm(d × w))
+ isparallelv && isparallelw
+end
+
+==(c₁::CylinderSurface, c₂::CylinderSurface) = c₁.bot == c₂.bot && c₁.top == c₂.top && c₁.radius == c₂.radius
+
+Base.isapprox(c₁::CylinderSurface, c₂::CylinderSurface; atol=atol(lentype(c₁)), kwargs...) =
+ isapprox(c₁.bot, c₂.bot; atol, kwargs...) &&
+ isapprox(c₁.top, c₂.top; atol, kwargs...) &&
+ isapprox(c₁.radius, c₂.radius; atol, kwargs...)
+
+(c::CylinderSurface)(φ, z) = Cylinder(bottom(c), top(c), radius(c))(1, φ, z)
+
+function hasintersectingplanes(c::CylinderSurface)
+ x = c.bot ∩ c.top
+ !isnothing(x) && evaluate(Euclidean(), axis(c), x) < c.radius
+end
diff --git a/src/primitives/disk.jl b/src/geometries/primitives/disk.jl
similarity index 54%
rename from src/primitives/disk.jl
rename to src/geometries/primitives/disk.jl
index 08567b843..a084c4698 100644
--- a/src/primitives/disk.jl
+++ b/src/geometries/primitives/disk.jl
@@ -10,12 +10,13 @@ given `plane` with given `radius`.
See also [`Circle`](@ref).
"""
-struct Disk{T} <: Primitive{3,T}
- plane::Plane{T}
- radius::T
+struct Disk{C<:CRS,P<:Plane{C},ℒ<:Len} <: Primitive{𝔼{3},C}
+ plane::P
+ radius::ℒ
+ Disk(plane::P, radius::ℒ) where {C<:CRS,P<:Plane{C},ℒ<:Len} = new{C,P,float(ℒ)}(plane, radius)
end
-Disk(plane::Plane{T}, radius) where {T} = Disk(plane, T(radius))
+Disk(plane::Plane, radius) = Disk(plane, addunit(radius, u"m"))
paramdim(::Type{<:Disk}) = 2
@@ -27,16 +28,20 @@ radius(d::Disk) = d.radius
normal(d::Disk) = normal(d.plane)
-function (d::Disk{T})(ρ, φ) where {T}
+==(d₁::Disk, d₂::Disk) = d₁.plane == d₂.plane && d₁.radius == d₂.radius
+
+Base.isapprox(d₁::Disk, d₂::Disk; atol=atol(lentype(d₁)), kwargs...) =
+ isapprox(d₁.plane, d₂.plane; atol, kwargs...) && isapprox(d₁.radius, d₂.radius; atol, kwargs...)
+
+function (d::Disk)(ρ, φ)
+ T = numtype(lentype(d))
if (ρ < 0 || ρ > 1) || (φ < 0 || φ > 1)
throw(DomainError((ρ, φ), "d(ρ, φ) is not defined for ρ, φ outside [0, 1]²."))
end
r = d.radius
l = T(ρ) * r
sφ, cφ = sincospi(2 * T(φ))
- u = l * cφ
- v = l * sφ
+ u = ustrip(l * cφ)
+ v = ustrip(l * sφ)
d.plane(u, v)
end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Disk{T}}) where {T} = Disk(rand(rng, Plane{T}), rand(rng, T))
diff --git a/src/geometries/primitives/ellipsoid.jl b/src/geometries/primitives/ellipsoid.jl
new file mode 100644
index 000000000..da5c26c0a
--- /dev/null
+++ b/src/geometries/primitives/ellipsoid.jl
@@ -0,0 +1,58 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Ellipsoid(radii, center=(0, 0, 0), rotation=I)
+
+A 3D ellipsoid with given `radii`, `center` and `rotation`.
+"""
+struct Ellipsoid{C<:CRS,Mₚ<:Manifold,R,ℒ<:Len} <: Primitive{𝔼{3},C}
+ radii::NTuple{3,ℒ}
+ center::Point{Mₚ,C}
+ rotation::R
+ Ellipsoid(radii::NTuple{3,ℒ}, center::Point{Mₚ,C}, rotation::R) where {C<:CRS,Mₚ<:Manifold,R,ℒ<:Len} =
+ new{C,Mₚ,R,float(ℒ)}(radii, center, rotation)
+end
+
+Ellipsoid(radii::Tuple, center::Point, rotation) = Ellipsoid(addunit.(radii, u"m"), center, rotation)
+
+Ellipsoid(radii::Tuple, center::Tuple, rotation) = Ellipsoid(radii, Point(center), rotation)
+
+Ellipsoid(radii::Tuple, center=(_zero(radii), _zero(radii), _zero(radii)), rotation=I) =
+ Ellipsoid(radii, center, rotation)
+
+_zero(radii) = zero(first(radii))
+
+paramdim(::Type{<:Ellipsoid}) = 2
+
+radii(e::Ellipsoid) = e.radii
+
+center(e::Ellipsoid) = e.center
+
+rotation(e::Ellipsoid) = e.rotation
+
+==(e₁::Ellipsoid, e₂::Ellipsoid) = e₁.radii == e₂.radii && e₁.center == e₂.center && e₁.rotation == e₂.rotation
+
+function Base.isapprox(e₁::Ellipsoid, e₂::Ellipsoid; atol=atol(lentype(e₁)), kwargs...)
+ u = Unitful.promote_unit(unit(lentype(e₁)), unit(lentype(e₂)))
+ all(isapprox(r₁, r₂; atol, kwargs...) for (r₁, r₂) in zip(e₁.radii, e₂.radii)) &&
+ isapprox(e₁.center, e₂.center; atol, kwargs...) &&
+ isapprox(e₁.rotation, e₂.rotation; atol=ustrip(u, atol), kwargs...)
+end
+
+function (e::Ellipsoid)(θ, φ)
+ T = numtype(lentype(e))
+ if (θ < 0 || θ > 1) || (φ < 0 || φ > 1)
+ throw(DomainError((θ, φ), "e(θ, φ) is not defined for θ, φ outside [0, 1]²."))
+ end
+ r = e.radii
+ c = e.center
+ R = e.rotation
+ sθ, cθ = sincospi(T(θ))
+ sφ, cφ = sincospi(2 * T(φ))
+ x = r[1] * sθ * cφ
+ y = r[2] * sθ * sφ
+ z = r[3] * cθ
+ c + R * Vec(x, y, z)
+end
diff --git a/src/geometries/primitives/frustum.jl b/src/geometries/primitives/frustum.jl
new file mode 100644
index 000000000..505c840a4
--- /dev/null
+++ b/src/geometries/primitives/frustum.jl
@@ -0,0 +1,42 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Frustum(bot, top)
+
+A frustum (truncated cone) with `bot` and `top` disks.
+See .
+
+See also [`FrustumSurface`](@ref).
+"""
+struct Frustum{C<:CRS,D<:Disk{C}} <: Primitive{𝔼{3},C}
+ bot::D
+ top::D
+
+ function Frustum{C,D}(bot, top) where {C<:CRS,D<:Disk{C}}
+ bn = normal(plane(bot))
+ tn = normal(plane(top))
+ a = bn ⋅ tn
+ assertion(a ≈ oneunit(a), "Bottom and top plane must be parallel")
+ assertion(center(bot) ≉ center(top), "Bottom and top centers need to be distinct")
+ new(bot, top)
+ end
+end
+
+Frustum(bot::D, top::D) where {C<:CRS,D<:Disk{C}} = Frustum{C,D}(bot, top)
+
+paramdim(::Type{<:Frustum}) = 3
+
+bottom(f::Frustum) = f.bot
+
+top(f::Frustum) = f.top
+
+height(f::Frustum) = height(boundary(f))
+
+axis(f::Frustum) = axis(boundary(f))
+
+==(f₁::Frustum, f₂::Frustum) = boundary(f₁) == boundary(f₂)
+
+Base.isapprox(f₁::Frustum, f₂::Frustum; atol=atol(lentype(f₁)), kwargs...) =
+ isapprox(boundary(f₁), boundary(f₂); atol, kwargs...)
diff --git a/src/geometries/primitives/frustumsurface.jl b/src/geometries/primitives/frustumsurface.jl
new file mode 100644
index 000000000..23ea9f134
--- /dev/null
+++ b/src/geometries/primitives/frustumsurface.jl
@@ -0,0 +1,70 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ FrustumSurface(bot, top)
+
+A frustum (truncated cone) surface with `bot` and `top` disks.
+See .
+
+See also [`Frustum`](@ref).
+"""
+struct FrustumSurface{C<:CRS,D<:Disk{C}} <: Primitive{𝔼{3},C}
+ bot::D
+ top::D
+
+ function FrustumSurface{C,D}(bot, top) where {C<:CRS,D<:Disk{C}}
+ bn = normal(plane(bot))
+ tn = normal(plane(top))
+ a = bn ⋅ tn
+ assertion(a ≈ oneunit(a), "Bottom and top plane must be parallel")
+ assertion(center(bot) ≉ center(top), "Bottom and top centers need to be distinct")
+ new(bot, top)
+ end
+end
+
+FrustumSurface(bot::D, top::D) where {C<:CRS,D<:Disk{C}} = FrustumSurface{C,D}(bot, top)
+
+paramdim(::Type{<:FrustumSurface}) = 2
+
+bottom(f::FrustumSurface) = f.bot
+
+top(f::FrustumSurface) = f.top
+
+height(f::FrustumSurface) = norm(center(bottom(f)) - center(top(f)))
+
+axis(f::FrustumSurface) = Line(center(bottom(f)), center(top(f)))
+
+==(f₁::FrustumSurface, f₂::FrustumSurface) = f₁.bot == f₂.bot && f₁.top == f₂.top
+
+Base.isapprox(f₁::FrustumSurface, f₂::FrustumSurface; atol=atol(lentype(f₁)), kwargs...) =
+ isapprox(f₁.bot, f₂.bot; atol, kwargs...) && isapprox(f₁.top, f₂.top; atol, kwargs...)
+
+function (f::FrustumSurface)(φ, z)
+ ℒ = lentype(f)
+ T = numtype(ℒ)
+ if (φ < 0 || φ > 1) || (z < 0 || z > 1)
+ throw(DomainError((φ, z), "f(φ, z) is not defined for φ, z outside [0, 1]²."))
+ end
+ rb = radius(bottom(f))
+ rt = radius(top(f))
+ a = axis(f)
+ d = a(1) - a(0)
+ l = norm(d)
+
+ # rotation to align z axis with cylinder axis
+ Q = urotbetween(d, Vec(zero(ℒ), zero(ℒ), oneunit(ℒ)))
+
+ # scale coordinates
+ φₛ = 2T(π) * φ
+ zₛ = z * l
+
+ # local coordinates, that will be transformed with rotation and position of the FrustumSurface
+ x = cos(φₛ) * (rb * (l - zₛ) + rt * zₛ) / l
+ y = sin(φₛ) * (rb * (l - zₛ) + rt * zₛ) / l
+ z = zₛ
+ p = Vec(x, y, z)
+
+ center(bottom(f)) + Q' * p
+end
diff --git a/src/primitives/line.jl b/src/geometries/primitives/line.jl
similarity index 56%
rename from src/primitives/line.jl
rename to src/geometries/primitives/line.jl
index fdae89530..97fbd8c1f 100644
--- a/src/primitives/line.jl
+++ b/src/geometries/primitives/line.jl
@@ -9,9 +9,9 @@ A line passing through points `a` and `b`.
See also [`Segment`](@ref).
"""
-struct Line{Dim,T} <: Primitive{Dim,T}
- a::Point{Dim,T}
- b::Point{Dim,T}
+struct Line{M<:Manifold,C<:CRS} <: Primitive{M,C}
+ a::Point{M,C}
+ b::Point{M,C}
end
Line(a::Tuple, b::Tuple) = Line(Point(a), Point(b))
@@ -20,7 +20,8 @@ paramdim(::Type{<:Line}) = 1
==(l₁::Line, l₂::Line) = l₁.a ∈ l₂ && l₁.b ∈ l₂ && l₂.a ∈ l₁ && l₂.b ∈ l₁
-(l::Line)(t) = l.a + t * (l.b - l.a)
+Base.isapprox(l₁::Line, l₂::Line; atol=atol(lentype(l₁)), kwargs...) =
+ isapproxzero(norm(ucross(l₁.b - l₁.a, l₂.b - l₂.a)); atol, kwargs...) &&
+ isapproxzero(norm(ucross(l₁.b - l₂.a, l₂.b - l₂.a)); atol, kwargs...)
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Line{Dim,T}}) where {Dim,T} =
- Line(rand(rng, Point{Dim,T}, 2)...)
+(l::Line)(t) = coordsum((l.a, l.b), weights=((1 - t), t))
diff --git a/src/primitives/paraboloidsurface.jl b/src/geometries/primitives/paraboloidsurface.jl
similarity index 63%
rename from src/primitives/paraboloidsurface.jl
rename to src/geometries/primitives/paraboloidsurface.jl
index c7da41f1f..fc74e1b14 100644
--- a/src/primitives/paraboloidsurface.jl
+++ b/src/geometries/primitives/paraboloidsurface.jl
@@ -32,21 +32,26 @@ Same as above, but here the apex is at `Apex(0, 0, 0)`.
See also .
"""
-struct ParaboloidSurface{T} <: Primitive{3,T}
- apex::Point{3,T}
- radius::T
- focallength::T
+struct ParaboloidSurface{C<:CRS,Mₚ<:Manifold,ℒ<:Len} <: Primitive{𝔼{3},C}
+ apex::Point{Mₚ,C}
+ radius::ℒ
+ focallength::ℒ
+ ParaboloidSurface(apex::Point{Mₚ,C}, radius::ℒ, focallength::ℒ) where {C<:CRS,Mₚ<:Manifold,ℒ<:Len} =
+ new{C,Mₚ,float(ℒ)}(apex, radius, focallength)
end
-ParaboloidSurface(apex::Point{3,T}, radius, focallength) where {T} = ParaboloidSurface{T}(apex, radius, focallength)
+ParaboloidSurface(apex::Point, radius::Len, focallength::Len) = ParaboloidSurface(apex, promote(radius, focallength)...)
+
+ParaboloidSurface(apex::Point, radius, focallength) =
+ ParaboloidSurface(apex, addunit(radius, u"m"), addunit(focallength, u"m"))
ParaboloidSurface(apex::Tuple, radius, focallength) = ParaboloidSurface(Point(apex), radius, focallength)
-ParaboloidSurface(apex::Point{3,T}, radius) where {T} = ParaboloidSurface(apex, T(radius), T(1))
+ParaboloidSurface(apex::Point, radius) = ParaboloidSurface(apex, radius, oneunit(radius))
ParaboloidSurface(apex::Tuple, radius) = ParaboloidSurface(Point(apex), radius)
-ParaboloidSurface(apex::Point{3,T}) where {T} = ParaboloidSurface(apex, T(1))
+ParaboloidSurface(apex::Point) = ParaboloidSurface(apex, oneunit(lentype(apex)))
ParaboloidSurface(apex::Tuple) = ParaboloidSurface(Point(apex))
@@ -81,14 +86,21 @@ apex(p::ParaboloidSurface) = p.apex
Return the focal axis, connecting the focus with the apex of the paraboloid.
The axis is always aligned with the z direction.
"""
-axis(p::ParaboloidSurface{T}) where {T} = Line(p.apex, p.apex + Vec(T(0), T(0), p.focallength))
+function axis(p::ParaboloidSurface)
+ f = p.focallength
+ Line(p.apex, p.apex + Vec(zero(f), zero(f), f))
+end
-Base.isapprox(p₁::ParaboloidSurface{T}, p₂::ParaboloidSurface{T}) where {T} =
- p₁.apex ≈ p₂.apex &&
- isapprox(p₁.focallength, p₂.focallength, atol=atol(T)) &&
- isapprox(p₁.radius, p₂.radius, atol=atol(T))
+==(p₁::ParaboloidSurface, p₂::ParaboloidSurface) =
+ p₁.apex == p₂.apex && p₁.radius == p₂.radius && p₁.focallength == p₂.focallength
-function (p::ParaboloidSurface{T})(ρ, θ) where {T}
+Base.isapprox(p₁::ParaboloidSurface, p₂::ParaboloidSurface; atol=atol(lentype(p₁)), kwargs...) =
+ isapprox(p₁.apex, p₂.apex; atol, kwargs...) &&
+ isapprox(p₁.focallength, p₂.focallength; atol, kwargs...) &&
+ isapprox(p₁.radius, p₂.radius; atol, kwargs...)
+
+function (p::ParaboloidSurface)(ρ, θ)
+ T = numtype(lentype(p))
if (ρ < 0 || ρ > 1)
throw(DomainError((ρ, θ), "p(ρ, θ) is not defined for ρ outside [0, 1]."))
end
@@ -102,6 +114,3 @@ function (p::ParaboloidSurface{T})(ρ, θ) where {T}
z = (x^2 + y^2) / 4f
c + Vec(x, y, z)
end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{ParaboloidSurface{T}}) where {T} =
- ParaboloidSurface(rand(rng, Point{3,T}), rand(rng, T), rand(rng, T))
diff --git a/src/geometries/primitives/parametrizedcurve.jl b/src/geometries/primitives/parametrizedcurve.jl
new file mode 100644
index 000000000..b7017e313
--- /dev/null
+++ b/src/geometries/primitives/parametrizedcurve.jl
@@ -0,0 +1,44 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ ParametrizedCurve(fun, range = (0.0, 1.0))
+
+A parametrized curve is a curve defined by a function `fun` that maps a
+(unitless) parameter `t` in the given `range` to a `Point` in space.
+
+## Examples
+
+```julia
+ParametrizedCurve(t -> Point(cos(t), sin(t)), (0, 2π))
+```
+"""
+struct ParametrizedCurve{M<:Manifold,C<:CRS,F<:Function,R<:Tuple} <: Primitive{M,C}
+ fun::F
+ range::R
+ ParametrizedCurve{M,C}(fun::F, range::R) where {M<:Manifold,C<:CRS,F<:Function,R<:Tuple} = new{M,C,F,R}(fun, range)
+end
+
+function ParametrizedCurve(fun, range=(0.0, 1.0))
+ a, b = promote(range...)
+ r = (a, b)
+ p = fun(a)
+ ParametrizedCurve{manifold(p),crs(p)}(fun, r)
+end
+
+paramdim(::Type{<:ParametrizedCurve}) = 1
+
+Base.minimum(curve::ParametrizedCurve) = curve.fun(first(curve.range))
+
+Base.maximum(curve::ParametrizedCurve) = curve.fun(last(curve.range))
+
+Base.extrema(curve::ParametrizedCurve) = minimum(curve), maximum(curve)
+
+function (curve::ParametrizedCurve)(t)
+ if t < 0 || t > 1
+ throw(DomainError(t, "c(t) is not defined for t outside [0, 1]."))
+ end
+ a, b = curve.range
+ curve.fun(a + t * (b - a))
+end
diff --git a/src/primitives/plane.jl b/src/geometries/primitives/plane.jl
similarity index 52%
rename from src/primitives/plane.jl
rename to src/geometries/primitives/plane.jl
index 8dcb0887d..0be6b6434 100644
--- a/src/primitives/plane.jl
+++ b/src/geometries/primitives/plane.jl
@@ -13,26 +13,24 @@ defined by non-parallel vectors `u` and `v`.
Alternatively specify point `p` and a given normal
vector `n` to the plane.
"""
-struct Plane{T} <: Primitive{3,T}
- p::Point{3,T}
- u::Vec{3,T}
- v::Vec{3,T}
+struct Plane{C<:CRS,Mₚ<:Manifold,V<:Vec{3}} <: Primitive{𝔼{3},C}
+ p::Point{Mₚ,C}
+ u::V
+ v::V
end
-function Plane{T}(p::Point{3,T}, n::Vec{3,T}) where {T}
+function Plane(p::Point, n::Vec)
u, v = householderbasis(n)
- Plane{T}(p, u, v)
+ Plane(p, u, v)
end
-Plane(p::Point{3,T}, n::Vec{3,T}) where {T} = Plane{T}(p, n)
-
Plane(p::Tuple, u::Tuple, v::Tuple) = Plane(Point(p), Vec(u), Vec(v))
Plane(p::Tuple, n::Tuple) = Plane(Point(p), Vec(n))
-function Plane(p1::Point{3,T}, p2::Point{3,T}, p3::Point{3,T}) where {T}
+function Plane(p1::Point, p2::Point, p3::Point)
t = Triangle(p1, p2, p3)
- if isapprox(area(t), zero(T), atol=atol(T))
+ if isapproxzero(area(t))
throw(ArgumentError("The three points are colinear."))
end
Plane(p1, normal(t))
@@ -40,19 +38,14 @@ end
paramdim(::Type{<:Plane}) = 2
-normal(p::Plane) = normalize(p.u × p.v)
+normal(p::Plane) = unormalize(ucross(p.u, p.v))
==(p₁::Plane, p₂::Plane) =
p₁(0, 0) ∈ p₂ && p₁(1, 0) ∈ p₂ && p₁(0, 1) ∈ p₂ && p₂(0, 0) ∈ p₁ && p₂(1, 0) ∈ p₁ && p₂(0, 1) ∈ p₁
-Base.isapprox(p₁::Plane{T}, p₂::Plane{T}) where {T} =
- isapprox((p₁(0, 0) - p₂(0, 0)) ⋅ normal(p₂), zero(T), atol=atol(T)) &&
- isapprox((p₂(0, 0) - p₁(0, 0)) ⋅ normal(p₁), zero(T), atol=atol(T)) &&
- isapprox(_area(normal(p₁), normal(p₂)), zero(T), atol=atol(T))
-
-_area(v₁::Vec, v₂::Vec) = norm(v₁ × v₂)
+Base.isapprox(p₁::Plane, p₂::Plane; atol=atol(lentype(p₁)), kwargs...) =
+ isapproxzero(norm(ucross(normal(p₁), normal(p₂))); atol, kwargs...) &&
+ isapproxzero(udot(p₁(0, 0) - p₂(0, 0), normal(p₂)); atol, kwargs...) &&
+ isapproxzero(udot(p₂(0, 0) - p₁(0, 0), normal(p₁)); atol, kwargs...)
(p::Plane)(u, v) = p.p + u * p.u + v * p.v
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Plane{T}}) where {T} =
- Plane(rand(rng, Point{3,T}), rand(rng, Vec{3,T}))
diff --git a/src/geometries/primitives/point.jl b/src/geometries/primitives/point.jl
new file mode 100644
index 000000000..1c0e57001
--- /dev/null
+++ b/src/geometries/primitives/point.jl
@@ -0,0 +1,166 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Point(x₁, x₂, ..., xₙ)
+ Point((x₁, x₂, ..., xₙ))
+
+A point in `Dim`-dimensional space with coordinates in length units (default to meters).
+
+The coordinates of the point are given with respect to the canonical
+Euclidean basis, and integer coordinates are converted to float.
+
+## Examples
+
+```julia
+# 2D points
+Point(1.0, 2.0) # add default units
+Point(1.0m, 2.0m) # double precision as expected
+Point(1f0km, 2f0km) # single precision as expected
+Point(1m, 2m) # integer is converted to float by design
+
+# 3D points
+Point(1.0, 2.0, 3.0) # add default units
+Point(1.0m, 2.0m, 3.0m) # double precision as expected
+Point(1f0km, 2f0km, 3f0km) # single precision as expected
+Point(1m, 2m, 3m) # integer is converted to float by design
+```
+
+### Notes
+
+- Integer coordinates are not supported because most geometric processing
+ algorithms assume a continuous space. The conversion to float avoids
+ `InexactError` and other unexpected results.
+"""
+struct Point{M<:Manifold,C<:CRS} <: Primitive{M,C}
+ coords::C
+end
+
+Point{M}(coords::C) where {M<:Manifold,C<:CRS} = Point{M,C}(coords)
+
+Point(coords::CRS) = Point{_manifold(coords)}(coords)
+
+# convenience constructor
+Point(coords...) = Point(Cartesian(coords...))
+
+# conversion
+Base.convert(::Type{Point{M,CRSₜ}}, p::Point{M,CRSₛ}) where {M,CRSₜ,CRSₛ} = Point{M}(convert(CRSₜ, p.coords))
+Base.convert(::Type{Point{M,CRS}}, p::Point{M,CRS}) where {M,CRS} = p
+
+# promotion
+function Base.promote(A::Point, B::Point)
+ a, b = promote(coords(A), coords(B))
+ Point(a), Point(b)
+end
+
+paramdim(::Type{<:Point}) = 0
+
+function ==(A::Point, B::Point)
+ A′, B′ = promote(A, B)
+ to(A′) == to(B′)
+end
+
+function ==(A::Point{🌐,<:LatLon}, B::Point{🌐,<:LatLon})
+ A′, B′ = promote(A, B)
+ lat₁, lon₁ = A′.coords.lat, A′.coords.lon
+ lat₂, lon₂ = B′.coords.lat, B′.coords.lon
+ lat₁ == lat₂ && lon₁ == lon₂ || (abs(lon₁) == 180u"°" && lon₁ == -lon₂)
+end
+
+function Base.isapprox(A::Point, B::Point; atol=atol(lentype(A)), kwargs...)
+ A′, B′ = promote(A, B)
+ isapprox(to(A′), to(B′); atol, kwargs...)
+end
+
+"""
+ coords(point)
+
+Return the coordinates of the `point`.
+"""
+coords(A::Point) = A.coords
+
+"""
+ to(point)
+
+Return the vector from the origin to the `point`.
+"""
+to(A::Point) = Vec(CoordRefSystems.values(convert(Cartesian, A.coords)))
+
+"""
+ -(A::Point, B::Point)
+
+Return the [`Vec`](@ref) associated with the direction
+from point `B` to point `A`.
+"""
+-(A::Point, B::Point) = to(A) - to(B)
+
+"""
+ +(A::Point, v::Vec)
+ +(v::Vec, A::Point)
+
+Return the point at the end of the vector `v` placed
+at a reference (or start) point `A`.
+"""
++(A::Point, v::Vec) = withcrs(A, to(A) + v)
++(v::Vec, A::Point) = A + v
+
+"""
+ -(A::Point, v::Vec)
+ -(v::Vec, A::Point)
+
+Return the point at the end of the vector `-v` placed
+at a reference (or start) point `A`.
+"""
+-(A::Point, v::Vec) = withcrs(A, to(A) - v)
+-(v::Vec, A::Point) = A - v
+
+"""
+ ∠(A, B, C)
+
+Angle ∠ABC between rays BA and BC.
+See .
+
+Uses the two-argument form of `atan` returning value in range [-π, π]
+in 2D and [0, π] in 3D.
+See .
+
+## Examples
+
+```julia
+∠(Point(1,0), Point(0,0), Point(0,1)) == π/2
+```
+"""
+∠(A::P, B::P, C::P) where {P<:Point} = ∠(A - B, C - B)
+
+# -----------
+# IO METHODS
+# -----------
+
+function Base.show(io::IO, point::Point)
+ if get(io, :compact, false)
+ print(io, "(")
+ else
+ print(io, "Point(")
+ end
+ values = CoordRefSystems.values(point.coords)
+ names = CoordRefSystems.names(point.coords)
+ printfields(io, values, names, singleline=true)
+ print(io, ")")
+end
+
+function Base.show(io::IO, mime::MIME"text/plain", point::Point)
+ print(io, "Point with ")
+ show(io, mime, point.coords)
+end
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+_manifold(coords::CRS) = 𝔼{CoordRefSystems.ndims(coords)}
+_manifold(::LatLon) = 🌐
+_manifold(::GeocentricLatLon) = 🌐
+_manifold(::AuthalicLatLon) = 🌐
+
+_lat(P) = convert(LatLon, P.coords).lat
diff --git a/src/primitives/ray.jl b/src/geometries/primitives/ray.jl
similarity index 60%
rename from src/primitives/ray.jl
rename to src/geometries/primitives/ray.jl
index bce7edb7d..14183f227 100644
--- a/src/primitives/ray.jl
+++ b/src/geometries/primitives/ray.jl
@@ -9,16 +9,19 @@ A ray originating at point `p`, pointed in direction `v`.
It can be called as `r(t)` with `t > 0` to cast it at
`p + t * v`.
"""
-struct Ray{Dim,T} <: Primitive{Dim,T}
- p::Point{Dim,T}
- v::Vec{Dim,T}
+struct Ray{C<:CRS,Mₚ<:Manifold,Dim,V<:Vec{Dim}} <: Primitive{𝔼{Dim},C}
+ p::Point{Mₚ,C}
+ v::V
end
Ray(p::Tuple, v::Tuple) = Ray(Point(p), Vec(v))
paramdim(::Type{<:Ray}) = 1
-==(r₁::Ray, r₂::Ray) = (r₁.p ≈ r₂.p) && (r₁.p + r₁.v) ∈ r₂ && (r₂.p + r₂.v) ∈ r₁
+==(r₁::Ray, r₂::Ray) = r₁.p == r₂.p && (r₁.p + r₁.v) ∈ r₂ && (r₂.p + r₂.v) ∈ r₁
+
+Base.isapprox(r₁::Ray, r₂::Ray; atol=atol(lentype(r₁)), kwargs...) =
+ isapprox(r₁.p, r₂.p; atol, kwargs...) && isapprox(r₁.v, r₂.v; atol, kwargs...)
function (r::Ray)(t)
if t < 0
@@ -26,6 +29,3 @@ function (r::Ray)(t)
end
r.p + t * r.v
end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Ray{Dim,T}}) where {Dim,T} =
- Ray(rand(rng, Point{Dim,T}), rand(rng, Vec{Dim,T}))
diff --git a/src/primitives/sphere.jl b/src/geometries/primitives/sphere.jl
similarity index 53%
rename from src/primitives/sphere.jl
rename to src/geometries/primitives/sphere.jl
index 9a046396a..f80d54298 100644
--- a/src/primitives/sphere.jl
+++ b/src/geometries/primitives/sphere.jl
@@ -9,16 +9,17 @@ A sphere with `center` and `radius`.
See also [`Ball`](@ref).
"""
-struct Sphere{Dim,T} <: Primitive{Dim,T}
- center::Point{Dim,T}
- radius::T
+struct Sphere{M<:Manifold,C<:CRS,ℒ<:Len} <: Primitive{M,C}
+ center::Point{M,C}
+ radius::ℒ
+ Sphere(center::Point{M,C}, radius::ℒ) where {M<:Manifold,C<:CRS,ℒ<:Len} = new{M,C,float(ℒ)}(center, radius)
end
-Sphere(center::Point{Dim,T}, radius) where {Dim,T} = Sphere(center, T(radius))
+Sphere(center::Point, radius) = Sphere(center, addunit(radius, u"m"))
Sphere(center::Tuple, radius) = Sphere(Point(center), radius)
-Sphere(center::Point{Dim,T}) where {Dim,T} = Sphere(center, T(1))
+Sphere(center::Point) = Sphere(center, oneunit(lentype(center)))
Sphere(center::Tuple) = Sphere(Point(center))
@@ -27,7 +28,7 @@ Sphere(center::Tuple) = Sphere(Point(center))
A 2D sphere passing through points `p1`, `p2` and `p3`.
"""
-function Sphere(p1::Point{2}, p2::Point{2}, p3::Point{2})
+function Sphere(p1::Point, p2::Point, p3::Point)
x1, y1 = p2 - p1
x2, y2 = p3 - p2
c1 = centroid(Segment(p1, p2))
@@ -46,50 +47,30 @@ Sphere(p1::Tuple, p2::Tuple, p3::Tuple) = Sphere(Point(p1), Point(p2), Point(p3)
A 3D sphere passing through points `p1`, `p2`, `p3` and `p4`.
"""
-function Sphere(p1::Point{3}, p2::Point{3}, p3::Point{3}, p4::Point{3})
+function Sphere(p1::Point, p2::Point, p3::Point, p4::Point)
v1 = p1 - p4
v2 = p2 - p4
v3 = p3 - p4
V = volume(Tetrahedron(p1, p2, p3, p4))
r⃗ = ((v3 ⋅ v3) * (v1 × v2) + (v2 ⋅ v2) * (v3 × v1) + (v1 ⋅ v1) * (v2 × v3)) / 12V
- center = p4 + r⃗
+ center = p4 + Vec(r⃗)
radius = norm(r⃗)
Sphere(center, radius)
end
Sphere(p1::Tuple, p2::Tuple, p3::Tuple, p4::Tuple) = Sphere(Point(p1), Point(p2), Point(p3), Point(p4))
-paramdim(::Type{<:Sphere{Dim}}) where {Dim} = Dim - 1
+paramdim(::Type{<:Sphere{𝔼{Dim}}}) where {Dim} = Dim - 1
+
+paramdim(::Type{<:Sphere{🌐}}) = 1
center(s::Sphere) = s.center
radius(s::Sphere) = s.radius
-function (s::Sphere{2,T})(φ) where {T}
- if (φ < 0 || φ > 1)
- throw(DomainError(φ, "s(φ) is not defined for φ outside [0, 1]."))
- end
- c = s.center
- r = s.radius
- sφ, cφ = sincospi(2 * T(φ))
- x = r * cφ
- y = r * sφ
- c + Vec(x, y)
-end
+==(s₁::Sphere, s₂::Sphere) = s₁.center == s₂.center && s₁.radius == s₂.radius
-function (s::Sphere{3,T})(θ, φ) where {T}
- if (θ < 0 || θ > 1) || (φ < 0 || φ > 1)
- throw(DomainError((θ, φ), "s(θ, φ) is not defined for θ, φ outside [0, 1]²."))
- end
- c = s.center
- r = s.radius
- sθ, cθ = sincospi(T(θ))
- sφ, cφ = sincospi(2 * T(φ))
- x = r * sθ * cφ
- y = r * sθ * sφ
- z = r * cθ
- c + Vec(x, y, z)
-end
+Base.isapprox(s₁::Sphere, s₂::Sphere; atol=atol(lentype(s₁)), kwargs...) =
+ isapprox(s₁.center, s₂.center; atol, kwargs...) && isapprox(s₁.radius, s₂.radius; atol, kwargs...)
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Sphere{Dim,T}}) where {Dim,T} =
- Sphere(rand(rng, Point{Dim,T}), rand(rng, T))
+(s::Sphere)(uv...) = Ball(center(s), radius(s))(1, uv...)
diff --git a/src/geometries/primitives/torus.jl b/src/geometries/primitives/torus.jl
new file mode 100644
index 000000000..e3785080f
--- /dev/null
+++ b/src/geometries/primitives/torus.jl
@@ -0,0 +1,81 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Torus(center, direction, major, minor)
+
+A torus centered at `center` with axis of revolution directed by
+`direction` and with radii `major` and `minor`.
+
+"""
+struct Torus{C<:CRS,Mₚ<:Manifold,V<:Vec{3},ℒ<:Len} <: Primitive{𝔼{3},C}
+ center::Point{Mₚ,C}
+ direction::V
+ major::ℒ
+ minor::ℒ
+ Torus(center::Point{Mₚ,C}, direction::V, major::ℒ, minor::ℒ) where {C<:CRS,Mₚ<:Manifold,V<:Vec{3},ℒ<:Len} =
+ new{C,Mₚ,V,float(ℒ)}(center, direction, major, minor)
+end
+
+Torus(center::Point, direction::Vec, major::Len, minor::Len) = Torus(center, direction, promote(major, minor)...)
+
+Torus(center::Point, direction::Vec, major, minor) =
+ Torus(center, direction, addunit(major, u"m"), addunit(minor, u"m"))
+
+Torus(center::Tuple, direction::Tuple, major, minor) = Torus(Point(center), Vec(direction), major, minor)
+
+"""
+ Torus(p1, p2, p3, minor)
+
+The torus whose centerline passes through points `p1`, `p2` and `p3` and with
+minor radius `minor`.
+"""
+function Torus(p1::Point, p2::Point, p3::Point, minor::Len)
+ c = Circle(p1, p2, p3)
+ p = Plane(p1, p2, p3)
+ Torus(center(c), normal(p), radius(c), minor)
+end
+
+Torus(p1::Point, p2::Point, p3::Point, minor) = Torus(p1, p2, p3, addunit(minor, u"m"))
+
+Torus(p1::Tuple, p2::Tuple, p3::Tuple, minor) = Torus(Point(p1), Point(p2), Point(p3), minor)
+
+paramdim(::Type{<:Torus}) = 2
+
+center(t::Torus) = t.center
+
+direction(t::Torus) = t.direction
+
+radii(t::Torus) = (t.major, t.minor)
+
+axis(t::Torus) = Line(t.center, t.center + t.direction)
+
+==(t₁::Torus, t₂::Torus) =
+ t₁.center == t₂.center && t₁.direction == t₂.direction && t₁.major == t₂.major && t₁.minor == t₂.minor
+
+Base.isapprox(t₁::Torus, t₂::Torus; atol=atol(lentype(t₁)), kwargs...) =
+ isapprox(t₁.center, t₂.center; atol, kwargs...) &&
+ isapprox(t₁.direction, t₂.direction; atol, kwargs...) &&
+ isapprox(t₁.major, t₂.major; atol, kwargs...) &&
+ isapprox(t₁.minor, t₂.minor; atol, kwargs...)
+
+function (t::Torus)(θ, φ)
+ ℒ = lentype(t)
+ T = numtype(ℒ)
+ if (θ < 0 || θ > 1) || (φ < 0 || φ > 1)
+ throw(DomainError((θ, φ), "t(θ, φ) is not defined for θ, φ outside [0, 1]²."))
+ end
+ c, n⃗ = t.center, t.direction
+ R, r = t.major, t.minor
+
+ Q = urotbetween(Vec(zero(ℒ), zero(ℒ), oneunit(ℒ)), n⃗)
+
+ sθ, cθ = sincospi(2 * T(-θ))
+ sφ, cφ = sincospi(2 * T(φ))
+ x = (R + r * cθ) * cφ
+ y = (R + r * cθ) * sφ
+ z = r * sθ
+
+ c + Q * Vec(x, y, z)
+end
diff --git a/src/geometries/transformedgeom.jl b/src/geometries/transformedgeom.jl
new file mode 100644
index 000000000..2a0dafa63
--- /dev/null
+++ b/src/geometries/transformedgeom.jl
@@ -0,0 +1,109 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ TransformedGeometry(geometry, transform)
+
+Lazy representation of a coordinate `transform` applied to a `geometry`.
+"""
+struct TransformedGeometry{M<:Manifold,C<:CRS,G<:Geometry,T<:Transform} <: Geometry{M,C}
+ geometry::G
+ transform::T
+
+ function TransformedGeometry{M,C}(geometry::G, transform::T) where {M<:Manifold,C<:CRS,G<:Geometry,T<:Transform}
+ new{M,C,G,T}(geometry, transform)
+ end
+end
+
+function TransformedGeometry(g::Geometry, t::Transform)
+ D = paramdim(g)
+ T = numtype(lentype(g))
+ p = t(isparametrized(g) ? g(ntuple(i -> zero(T), D)...) : centroid(g))
+ TransformedGeometry{manifold(p),crs(p)}(g, t)
+end
+
+# specialize constructor to avoid deep structures
+TransformedGeometry(g::TransformedGeometry, t::Transform) = TransformedGeometry(g.geometry, g.transform → t)
+
+# type aliases for convenience
+const TransformedPoint{M<:Manifold,C<:CRS,T<:Transform} = TransformedGeometry{M,C,<:Point,T}
+const TransformedSegment{M<:Manifold,C<:CRS,T<:Transform} = TransformedGeometry{M,C,<:Segment,T}
+const TransformedRope{M<:Manifold,C<:CRS,T<:Transform} = TransformedGeometry{M,C,<:Rope,T}
+const TransformedRing{M<:Manifold,C<:CRS,T<:Transform} = TransformedGeometry{M,C,<:Ring,T}
+const TransformedPolygon{M<:Manifold,C<:CRS,T<:Transform} = TransformedGeometry{M,C,<:Polygon,T}
+const TransformedPolyhedron{M<:Manifold,C<:CRS,T<:Transform} = TransformedGeometry{M,C,<:Polyhedron,T}
+const TransformedPolytope{M<:Manifold,C<:CRS,T<:Transform} = TransformedGeometry{M,C,<:Polytope,T}
+
+Base.parent(g::TransformedGeometry) = g.geometry
+
+transform(g::TransformedGeometry) = g.transform
+
+hasdistortedboundary(g::TransformedGeometry) = _hasdistortion(manifold(g), manifold(parent(g)))
+
+_hasdistortion(::Type, ::Type) = false
+
+_hasdistortion(::Type{<:𝔼}, ::Type{<:🌐}) = true
+
+_hasdistortion(::Type{<:🌐}, ::Type{<:𝔼}) = true
+
+# ---------
+# GEOMETRY
+# ---------
+
+paramdim(g::TransformedGeometry) = paramdim(g.geometry)
+
+==(g₁::TransformedGeometry, g₂::TransformedGeometry) = _isequal(g₁, g₂)
+
+==(g₁::TransformedGeometry, g₂::Geometry) = _isequal(g₁, g₂)
+
+==(g₁::Geometry, g₂::TransformedGeometry) = _isequal(g₁, g₂)
+
+Base.isapprox(g₁::TransformedGeometry, g₂::TransformedGeometry; atol=atol(lentype(g₁)), kwargs...) =
+ _isapprox(g₁, g₂; atol, kwargs...)
+
+Base.isapprox(g₁::TransformedGeometry, g₂::Geometry; atol=atol(lentype(g₁)), kwargs...) =
+ _isapprox(g₁, g₂; atol, kwargs...)
+
+Base.isapprox(g₁::Geometry, g₂::TransformedGeometry; atol=atol(lentype(g₁)), kwargs...) =
+ _isapprox(g₁, g₂; atol, kwargs...)
+
+(g::TransformedGeometry)(uvw...) = g.transform(g.geometry(uvw...))
+
+# ---------
+# POLYTOPE
+# ---------
+
+vertex(p::TransformedPolytope, ind) = p.transform(vertex(p.geometry, ind))
+
+vertices(p::TransformedPolytope) = map(p.transform, vertices(p.geometry))
+
+nvertices(p::TransformedPolytope) = nvertices(p.geometry)
+
+Base.unique(p::TransformedPolytope) = unique!(deepcopy(p))
+
+Base.unique!(p::TransformedPolytope) = (unique!(p.geometry); p)
+
+# --------
+# POLYGON
+# --------
+
+rings(p::TransformedPolygon) = map(p.transform, rings(p.geometry))
+
+# -----------
+# IO METHODS
+# -----------
+
+prettyname(g::TransformedGeometry) = "Transformed$(prettyname(g.geometry))"
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+_isequal(g₁, g₂) = pointify(g₁) == pointify(g₂)
+
+function _isapprox(g₁, g₂; kwargs...)
+ ps₁ = pointify(g₁)
+ ps₂ = pointify(g₂)
+ length(ps₁) == length(ps₂) && all(isapprox(p₁, p₂; atol, kwargs...) for (p₁, p₂) in zip(ps₁, ps₂))
+end
diff --git a/src/hulls.jl b/src/hulls.jl
index 05b2c9765..1d3611600 100644
--- a/src/hulls.jl
+++ b/src/hulls.jl
@@ -38,7 +38,7 @@ function convexhull end
# FALLBACKS
# ----------
-convexhull(p::Polytope) = _pconvexhull(vertices(p))
+convexhull(p::Polytope) = _pconvexhull(eachvertex(p))
convexhull(p::Primitive) = convexhull(boundary(p))
@@ -62,7 +62,7 @@ convexhull(t::Triangle) = t
convexhull(g::Grid) = Box(extrema(g)...)
-convexhull(m::Mesh) = _pconvexhull(vertices(m))
+convexhull(m::Mesh) = _pconvexhull(eachvertex(m))
# ----------------
# IMPLEMENTATIONS
diff --git a/src/hulls/graham.jl b/src/hulls/graham.jl
index 4f1153af6..a30b443bb 100644
--- a/src/hulls/graham.jl
+++ b/src/hulls/graham.jl
@@ -21,9 +21,10 @@ struct GrahamScan <: HullMethod end
function hull(points, ::GrahamScan)
pₒ = first(points)
Dim = embeddim(pₒ)
- T = coordtype(pₒ)
+ ℒ = lentype(pₒ)
+ T = numtype(ℒ)
- @assert Dim == 2 "Graham's scan only defined in 2D"
+ assertion(Dim == 2, "Graham's scan only defined in 2D")
# remove duplicates
p = unique(points)
@@ -34,17 +35,17 @@ function hull(points, ::GrahamScan)
n == 2 && return Segment(p[1], p[2])
# sort points lexicographically
- p = p[sortperm(coordinates.(p))]
+ p = p[sortperm(to.(p))]
# sort points by polar angle
O = p[1]
q = p[2:n]
- A = O + Vec{2,T}(0, -1)
+ A = O + Vec(zero(ℒ), -oneunit(ℒ))
θ = [∠(A, O, B) for B in q]
q = q[sortperm(θ)]
# skip collinear points at beginning
- y(p) = coordinates(p)[2]
+ y(p) = to(p)[2]
i = findfirst(qᵢ -> y(qᵢ) ≠ y(O), q)
# all points are collinear, return segment
diff --git a/src/hulls/jarvis.jl b/src/hulls/jarvis.jl
index 7894090ef..ee77912c1 100644
--- a/src/hulls/jarvis.jl
+++ b/src/hulls/jarvis.jl
@@ -22,9 +22,9 @@ struct JarvisMarch <: HullMethod end
function hull(points, ::JarvisMarch)
pₒ = first(points)
Dim = embeddim(pₒ)
- T = coordtype(pₒ)
+ ℒ = lentype(pₒ)
- @assert Dim == 2 "Jarvis's march only defined in 2D"
+ assertion(Dim == 2, "Jarvis's march only defined in 2D")
# remove duplicates
p = unique(points)
@@ -35,14 +35,14 @@ function hull(points, ::JarvisMarch)
n == 2 && return Segment(p[1], p[2])
# find bottom-left point
- i = argmin(l -> coordinates(p[l]), 1:n)
+ i = argmin(l -> to(p[l]), 1:n)
# candidates for next point
𝒞 = [1:(i - 1); (i + 1):n]
# find next point with smallest angle
O = p[i]
- A = O + Vec{2,T}(0, -1)
+ A = O + Vec(zero(ℒ), -oneunit(ℒ))
j = argmin(l -> ∠(A, O, p[l]), 𝒞)
# initialize ring of indices
diff --git a/src/indices.jl b/src/indices.jl
new file mode 100644
index 000000000..722379bd7
--- /dev/null
+++ b/src/indices.jl
@@ -0,0 +1,285 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+# ---------------
+# LINEAR INDICES
+# ---------------
+
+"""
+ indices(domain, geometry)
+
+Return the indices of the elements of the `domain` that intersect with the `geometry`.
+"""
+indices(domain::Domain, geometry::Geometry) = findall(intersects(geometry), domain)
+
+function indices(grid::OrthoRegularGrid, point::Point)
+ point ∉ grid && return Int[]
+
+ # grid properties
+ orig = minimum(grid)
+ spac = spacing(grid)
+ dims = size(grid)
+
+ # integer coordinates
+ coords = ceil.(Int, (point - orig) ./ spac)
+
+ # fix coordinates that are on the grid border
+ coords = clamp.(coords, 1, dims)
+
+ # convert to linear index
+ [LinearIndices(dims)[coords...]]
+end
+
+function indices(grid::OrthoRegularGrid, chain::Chain)
+ dims = size(grid)
+ mask = falses(dims)
+
+ for segment in segments(chain)
+ p₁, p₂ = vertices(segment)
+ _bresenham!(mask, grid, true, p₁, p₂)
+ end
+
+ LinearIndices(dims)[mask]
+end
+
+function indices(grid::OrthoRegularGrid, poly::Polygon)
+ dims = size(grid)
+ mask = zeros(Int, dims)
+ cpoly = poly ∩ boundingbox(grid)
+ isnothing(cpoly) && return Int[]
+
+ for (i, triangle) in enumerate(simplexify(cpoly))
+ _fill!(mask, grid, i, triangle)
+ end
+
+ LinearIndices(dims)[mask .> 0]
+end
+
+function indices(grid::OrthoRegularGrid, box::Box)
+ # cartesian range
+ range = cartesianrange(grid, box)
+
+ # convert to linear indices
+ LinearIndices(size(grid))[range] |> vec
+end
+
+indices(grid::OrthoRegularGrid, multi::Multi) = mapreduce(geom -> indices(grid, geom), vcat, parent(multi)) |> unique
+
+function indices(grid::OrthoRectilinearGrid, box::Box)
+ # cartesian range
+ range = cartesianrange(grid, box)
+
+ # convert to linear indices
+ LinearIndices(size(grid))[range] |> vec
+end
+
+# ----------------
+# CARTESIAN RANGE
+# ----------------
+
+"""
+ cartesianrange(grid, box)
+
+Return the Cartesian range of the elements of the `grid` that intersect with the `box`.
+"""
+cartesianrange(grid::Grid{M}, box::Box{M}) where {M} = _manifoldrange(M, grid, box)
+
+_manifoldrange(::Type{<:𝔼}, grid::Grid, box::Box) = _euclideanrange(grid, box)
+
+_manifoldrange(::Type{<:🌐}, grid::Grid, box::Box) = _geodesicrange(grid, box)
+
+function _euclideanrange(grid::OrthoRegularGrid, box::Box)
+ # grid properties
+ or = minimum(grid)
+ sp = spacing(grid)
+ sz = size(grid)
+
+ # intersection of boxes
+ lo, up = extrema(boundingbox(grid) ∩ box)
+
+ # Cartesian indices of new corners
+ ijkₛ = max.(ceil.(Int, (lo - or) ./ sp), 1)
+ ijkₑ = min.(floor.(Int, (up - or) ./ sp) .+ 1, sz)
+
+ # Cartesian range from corner to corner
+ CartesianIndex(Tuple(ijkₛ)):CartesianIndex(Tuple(ijkₑ))
+end
+
+function _euclideanrange(grid::OrthoRectilinearGrid, box::Box)
+ # grid properties
+ nd = paramdim(grid)
+
+ # intersection of boxes
+ lo, up = to.(extrema(boundingbox(grid) ∩ box))
+
+ # integer coordinates of lower point
+ ijkₛ = ntuple(nd) do i
+ findlast(x -> x ≤ lo[i], xyz(grid)[i])
+ end
+
+ # integer coordinates of upper point
+ ijkₑ = ntuple(nd) do i
+ findfirst(x -> x ≥ up[i], xyz(grid)[i])
+ end
+
+ # integer coordinates of elements
+ CartesianIndex(ijkₛ):CartesianIndex(ijkₑ .- 1)
+end
+
+function _geodesicrange(grid::Grid, box::Box)
+ nlon, nlat = vsize(grid)
+
+ boxmin = convert(LatLon, coords(minimum(box)))
+ boxmax = convert(LatLon, coords(maximum(box)))
+
+ a = convert(LatLon, coords(vertex(grid, (1, 1))))
+ b = convert(LatLon, coords(vertex(grid, (nlon, 1))))
+ c = convert(LatLon, coords(vertex(grid, (1, nlat))))
+
+ swaplon = a.lon > b.lon
+ swaplat = a.lat > c.lat
+
+ loninds = swaplon ? (nlon:-1:1) : (1:1:nlon)
+ latinds = swaplat ? (nlat:-1:1) : (1:1:nlat)
+
+ gridlonₛ, gridlonₑ = swaplon ? (b.lon, a.lon) : (a.lon, b.lon)
+ gridlatₛ, gridlatₑ = swaplat ? (c.lat, a.lat) : (a.lat, c.lat)
+
+ lonmin = max(boxmin.lon, gridlonₛ)
+ latmin = max(boxmin.lat, gridlatₛ)
+ lonmax = min(boxmax.lon, gridlonₑ)
+ latmax = min(boxmax.lat, gridlatₑ)
+
+ iₛ = findlast(loninds) do i
+ p = vertex(grid, (i, 1))
+ c = convert(LatLon, coords(p))
+ c.lon ≤ lonmin
+ end
+ iₑ = findfirst(loninds) do i
+ p = vertex(grid, (i, 1))
+ c = convert(LatLon, coords(p))
+ c.lon ≥ lonmax
+ end
+
+ jₛ = findlast(latinds) do i
+ p = vertex(grid, (1, i))
+ c = convert(LatLon, coords(p))
+ c.lat ≤ latmin
+ end
+ jₑ = findfirst(latinds) do i
+ p = vertex(grid, (1, i))
+ c = convert(LatLon, coords(p))
+ c.lat ≥ latmax
+ end
+
+ if iₛ == iₑ || jₛ == jₑ
+ throw(ArgumentError("the passed limits are not valid for the grid"))
+ end
+
+ iₛ, iₑ = swaplon ? (iₑ, iₛ) : (iₛ, iₑ)
+ jₛ, jₑ = swaplat ? (jₑ, jₛ) : (jₛ, jₑ)
+
+ CartesianIndex(loninds[iₛ], latinds[jₛ]):CartesianIndex(loninds[iₑ] - 1, latinds[jₑ] - 1)
+end
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function _fill!(mask, grid, val, triangle)
+ v = vertices(triangle)
+
+ # fill edges of triangle
+ _bresenham!(mask, grid, val, v[1], v[2])
+ _bresenham!(mask, grid, val, v[2], v[3])
+ _bresenham!(mask, grid, val, v[3], v[1])
+
+ # fill interior of triangle
+ j₁ = findfirst(==(val), mask).I[2]
+ j₂ = findlast(==(val), mask).I[2]
+ for j in j₁:j₂
+ i₁ = findfirst(==(val), @view(mask[:, j]))
+ i₂ = findlast(==(val), @view(mask[:, j]))
+ mask[i₁:i₂, j] .= val
+ end
+end
+
+# Bresenham's line algorithm: https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
+function _bresenham!(mask, grid, val, p₁, p₂)
+ o = minimum(grid)
+ s = spacing(grid)
+
+ # integer coordinates
+ x₁, y₁ = ceil.(Int, (p₁ - o) ./ s)
+ x₂, y₂ = ceil.(Int, (p₂ - o) ./ s)
+
+ # fix coordinates of points that are on the grid border
+ xmax, ymax = size(grid)
+ x₁ = clamp(x₁, 1, xmax)
+ y₁ = clamp(y₁, 1, ymax)
+ x₂ = clamp(x₂, 1, xmax)
+ y₂ = clamp(y₂, 1, ymax)
+
+ if abs(y₂ - y₁) < abs(x₂ - x₁)
+ if x₁ > x₂
+ _bresenhamlow!(mask, val, x₂, y₂, x₁, y₁)
+ else
+ _bresenhamlow!(mask, val, x₁, y₁, x₂, y₂)
+ end
+ else
+ if y₁ > y₂
+ _bresenhamhigh!(mask, val, x₂, y₂, x₁, y₁)
+ else
+ _bresenhamhigh!(mask, val, x₁, y₁, x₂, y₂)
+ end
+ end
+end
+
+function _bresenhamlow!(mask, val, x₁, y₁, x₂, y₂)
+ dx = x₂ - x₁
+ dy = y₂ - y₁
+ yi = 1
+ if dy < 0
+ yi = -1
+ dy = -dy
+ end
+
+ D = 2dy - dx
+ y = y₁
+
+ for x in x₁:x₂
+ mask[x, y] = val
+
+ if D > 0
+ y = y + yi
+ D = D + 2dy - 2dx
+ else
+ D = D + 2dy
+ end
+ end
+end
+
+function _bresenhamhigh!(mask, val, x₁, y₁, x₂, y₂)
+ dx = x₂ - x₁
+ dy = y₂ - y₁
+ xi = 1
+ if dx < 0
+ xi = -1
+ dx = -dx
+ end
+
+ D = 2dx - dy
+ x = x₁
+
+ for y in y₁:y₂
+ mask[x, y] = val
+
+ if D > 0
+ x = x + xi
+ D = D + 2dx - 2dy
+ else
+ D = D + 2dx
+ end
+ end
+end
diff --git a/src/intersections.jl b/src/intersections.jl
index db733f283..f46455bcb 100644
--- a/src/intersections.jl
+++ b/src/intersections.jl
@@ -70,7 +70,7 @@ end
Return the intersection of two geometries or domains `g₁` and `g₂`
as a new (multi-)geometry.
"""
-Base.intersect(g₁::Union{Geometry,Domain}, g₂::Union{Geometry,Domain}) = get(intersection(g₁, g₂))
+Base.intersect(g₁::GeometryOrDomain, g₂::GeometryOrDomain) = get(intersection(g₁, g₂))
"""
intersection([f], g₁, g₂)
diff --git a/src/intersections/boxes.jl b/src/intersections/boxes.jl
index 295094c35..f7419499d 100644
--- a/src/intersections/boxes.jl
+++ b/src/intersections/boxes.jl
@@ -8,19 +8,19 @@
# 2. intersect at corner point (CornerTouching -> Point)
# 3. intersect at one of the facets (Touching -> Box)
# 4. do not overlap nor intersect (NotIntersecting -> Nothing)
-function intersection(f, box₁::Box{Dim,T}, box₂::Box{Dim,T}) where {Dim,T}
+function intersection(f, box₁::Box, box₂::Box)
# retrieve corner points
- m1, M1 = coordinates.(extrema(box₁))
- m2, M2 = coordinates.(extrema(box₂))
+ m1, M1 = to.(extrema(box₁))
+ m2, M2 = to.(extrema(box₂))
# relevant vertices
- u = Point(max.(m1, m2))
- v = Point(min.(M1, M2))
+ u = withcrs(box₁, max.(promote(m1, m2)...))
+ v = withcrs(box₁, min.(promote(M1, M2)...))
# auxiliary variables
δ = v - u
δ̄ = abs.(δ)
- τ = atol(T)
+ τ = atol(eltype(δ))
# branch on possible configurations
if all(>(τ), δ)
diff --git a/src/intersections/domains.jl b/src/intersections/domains.jl
index 272c5e979..d82808540 100644
--- a/src/intersections/domains.jl
+++ b/src/intersections/domains.jl
@@ -13,9 +13,9 @@ end
intersection(f, dom::Domain, pset::PointSet) = intersection(f, Multi(collect(dom)), pset)
-function intersection(f, dom₁::Domain{Dim,T}, dom₂::Domain{Dim,T}) where {Dim,T}
+function intersection(f, dom₁::Domain, dom₂::Domain)
# loop over all geometries
- gs = Geometry{Dim,T}[]
+ gs = Geometry[]
for g₁ in dom₁, g₂ in dom₂
g = g₁ ∩ g₂
isnothing(g) || push!(gs, g)
diff --git a/src/intersections/planes.jl b/src/intersections/planes.jl
index eaa98ebff..361cb8995 100644
--- a/src/intersections/planes.jl
+++ b/src/intersections/planes.jl
@@ -3,37 +3,40 @@
# ------------------------------------------------------------------
# (https://en.wikipedia.org/wiki/Plane-plane_intersection)
-function intersection(f, plane1::Plane{T}, plane2::Plane{T}) where {T}
- n1 = normal(plane1)
- n2 = normal(plane2)
+function intersection(f, plane1::Plane, plane2::Plane)
+ u = unit(lentype(plane1))
+ n1 = ustrip.(normal(plane1))
+ n2 = ustrip.(normal(plane2))
+ o1 = ustrip.(to(plane1.p))
+ o2 = ustrip.(to(plane2.p))
n1n2 = n1 ⋅ n2
- if isapprox(abs(n1n2), one(T), atol=atol(T))
+ if isapproxone(abs(n1n2))
# planes are parallel and do not intersect
return @IT NotIntersecting nothing f
else
d = n1 × n2
- h1 = n1 ⋅ plane1.p.coords
- h2 = n2 ⋅ plane2.p.coords
+ h1 = n1 ⋅ o1
+ h2 = n2 ⋅ o2
c1 = (h1 - h2 * n1n2) / (1 - n1n2^2)
c2 = (h2 - h1 * n1n2) / (1 - n1n2^2)
p1 = (c1 * n1) + (c2 * n2)
p2 = p1 + d
- return @IT Intersecting Line(Point(p1), Point(p2)) f
+ return @IT Intersecting Line(withcrs(plane1, p1 * u), withcrs(plane1, p2 * u)) f
end
end
-const LineLike{T} = Union{Line{3,T},Ray{3,T},Segment{3,T}}
+const LineLike = Union{Segment,Ray,Line}
# (https://en.wikipedia.org/wiki/Line-plane_intersection)
-function intersection(f, line::LineLike{T}, plane::Plane{T}) where {T}
+function intersection(f, line::LineLike, plane::Plane)
# auxiliary parameters
d = line(1) - line(0)
n = normal(plane)
a = (plane(0, 0) - line(0)) ⋅ n
b = d ⋅ n
- if isapprox(b, zero(T), atol=atol(T))
- if isapprox(a, zero(T), atol=atol(T))
+ if isapproxzero(b)
+ if isapproxzero(a)
return @IT Overlapping line f
else
return @IT NotIntersecting nothing f
@@ -49,19 +52,19 @@ end
# λ < 0 or λ > 1 ⟹ NotIntersecting
# λ ≈ 0 or λ ≈ 1 ⟹ Touching
# λ > 0 and λ < 1 ⟹ Crossing
-function _intersection(f, seg::Segment{3,T}, λ) where {T}
+function _intersection(f, seg::Segment, λ)
# if λ is approximately 0, set as so to prevent any domain errors
- if isapprox(λ, zero(T), atol=atol(T))
+ if isapproxzero(λ)
return @IT Touching seg(0) f
end
# if λ is approximately 1, set as so to prevent any domain errors
- if isapprox(λ, one(T), atol=atol(T))
+ if isapproxone(λ)
return @IT Touching seg(1) f
end
# if λ is out of bounds for the segment, then there is no intersection
- if (λ < zero(T) || λ > one(T))
+ if (λ < zero(λ) || λ > one(λ))
return @IT NotIntersecting nothing f
else
return @IT Crossing seg(λ) f
@@ -74,14 +77,14 @@ end
# λ < 0 ⟹ NotIntersecting
# λ ≈ 0 ⟹ Touching
# λ > 0 ⟹ Crossing
-function _intersection(f, ray::Ray{3,T}, λ) where {T}
+function _intersection(f, ray::Ray, λ)
# if λ is approximately 0, set as so to prevent any domain errors
- if isapprox(λ, zero(T), atol=atol(T))
+ if isapproxzero(λ)
return @IT Touching ray(0) f
end
# if λ is out of bounds for the ray, then there is no intersection
- if (λ < zero(T))
+ if (λ < zero(λ))
return @IT NotIntersecting nothing f
else
return @IT Crossing ray(λ) f
diff --git a/src/intersections/polygons.jl b/src/intersections/polygons.jl
index 930c02f5e..908c9fd13 100644
--- a/src/intersections/polygons.jl
+++ b/src/intersections/polygons.jl
@@ -5,9 +5,9 @@
function intersection(f, poly₁::Polygon, poly₂::Polygon)
# TODO: use Weiler-Atherton or other more general clipping method
clipped = if isconvex(poly₂)
- clip(poly₁, poly₂, SutherlandHodgman())
+ clip(poly₁, poly₂, SutherlandHodgmanClipping())
elseif isconvex(poly₁)
- clip(poly₂, poly₁, SutherlandHodgman())
+ clip(poly₂, poly₁, SutherlandHodgmanClipping())
else
throw(ErrorException("intersection not implemented between two non-convex polygons"))
end
@@ -19,4 +19,4 @@ function intersection(f, poly₁::Polygon, poly₂::Polygon)
end
end
-intersection(f, poly::Polygon{2}, box::Box{2}) = intersection(f, poly, convert(Quadrangle, box))
+intersection(f, poly::Polygon, box::Box) = intersection(f, poly, convert(Quadrangle, box))
diff --git a/src/intersections/rays.jl b/src/intersections/rays.jl
index 8de035cdc..e9dc1b1df 100644
--- a/src/intersections/rays.jl
+++ b/src/intersections/rays.jl
@@ -10,12 +10,13 @@
# 4. overlap with aligned vectors (PosOverlapping -> Ray)
# 5. overlap with colliding vectors (NegOverlapping -> Segment)
# 6. do not overlap nor intersect (NotIntersecting -> Nothing)
-function intersection(f, ray₁::Ray{N,T}, ray₂::Ray{N,T}) where {N,T}
+function intersection(f, ray₁::Ray, ray₂::Ray)
a, b = ray₁(0), ray₁(1)
c, d = ray₂(0), ray₂(1)
# normalize points to gain parameters λ₁, λ₂ corresponding to arc lengths
- l₁, l₂ = norm(b - a), norm(d - c)
+ l₁ = ustrip(norm(b - a))
+ l₂ = ustrip(norm(d - c))
b₀ = a + 1 / l₁ * (b - a)
d₀ = c + 1 / l₂ * (d - c)
@@ -26,8 +27,8 @@ function intersection(f, ray₁::Ray{N,T}, ray₂::Ray{N,T}) where {N,T}
return @IT NotIntersecting nothing f #CASE 6
# collinear
elseif r == rₐ == 1
- if (b - a) ⋅ (d - c) ≥ 0 # rays aligned in same direction
- if (a - c) ⋅ (b - a) ≥ 0 # origin of ray₁ ∈ ray₂
+ if isnonnegative((b - a) ⋅ (d - c)) # rays aligned in same direction
+ if isnonnegative((a - c) ⋅ (b - a)) # origin of ray₁ ∈ ray₂
return @IT PosOverlapping ray₁ f # CASE 4: ray₁
else
return @IT PosOverlapping ray₂ f # CASE 4: ray₂
@@ -43,8 +44,8 @@ function intersection(f, ray₁::Ray{N,T}, ray₂::Ray{N,T}) where {N,T}
end
# in same plane, not parallel
else
- λ₁ = mayberound(λ₁, zero(T))
- λ₂ = mayberound(λ₂, zero(T))
+ λ₁ = mayberound(λ₁, zero(λ₁))
+ λ₂ = mayberound(λ₂, zero(λ₂))
if λ₁ < 0 || λ₂ < 0
return @IT NotIntersecting nothing f # CASE 6
elseif λ₁ == 0
@@ -69,12 +70,12 @@ end
# 2. intersect at origin of ray (Touching -> Point)
# 3. overlap of line and ray (Overlapping -> Ray)
# 4. do not overlap nor intersect (NotIntersecting -> Nothing)
-function intersection(f, ray::Ray{N,T}, line::Line{N,T}) where {N,T}
+function intersection(f, ray::Ray, line::Line)
a, b = ray(0), ray(1)
c, d = line(0), line(1)
# rescaling of point b necessary to gain a parameter λ₁ representing the arc length
- l₁ = norm(b - a)
+ l₁ = ustrip(norm(b - a))
b₀ = a + 1 / l₁ * (b - a)
λ₁, _, r, rₐ = intersectparameters(a, b₀, c, d)
@@ -84,7 +85,7 @@ function intersection(f, ray::Ray{N,T}, line::Line{N,T}) where {N,T}
elseif r == rₐ == 1 # collinear
return @IT Overlapping ray f # CASE 3
else # in same plane, not parallel
- λ₁ = mayberound(λ₁, zero(T))
+ λ₁ = mayberound(λ₁, zero(λ₁))
if λ₁ > 0
return @IT Crossing ray(λ₁ / l₁) f # CASE 1
elseif λ₁ == 0
@@ -97,21 +98,24 @@ end
# Williams A, Barrus S, Morley R K, et al., 2005.
# (https://dl.acm.org/doi/abs/10.1145/1198555.1198748)
-function intersection(f, ray::Ray{Dim,T}, box::Box{Dim,T}) where {Dim,T}
- invdir = one(T) ./ (ray(1) - ray(0))
- lo, up = coordinates.(extrema(box))
- orig = coordinates(ray(0))
+function intersection(f, ray::Ray, box::Box)
+ ℒ = lentype(ray)
+ invdir = inv.(ray(1) - ray(0))
+ lo, up = to.(extrema(box))
+ orig = to(ray(0))
+ T = numtype(ℒ)
tmin = zero(T)
tmax = typemax(T)
# check for intersection with slabs along with each axis
- for i in 1:Dim
+ for i in 1:embeddim(ray)
imin = (lo[i] - orig[i]) * invdir[i]
imax = (up[i] - orig[i]) * invdir[i]
# swap variables if necessary
- invdir[i] < zero(T) && ((imin, imax) = (imax, imin))
+ iinv = invdir[i]
+ iinv < zero(iinv) && ((imin, imax) = (imax, imin))
# the ray is on a face of the box, avoid NaN
(isnan(imin) || isnan(imax)) && continue
@@ -138,7 +142,7 @@ end
#
# Möller, T. & Trumbore, B., 1997.
# (https://www.tandfonline.com/doi/abs/10.1080/10867651.1997.10487468)
-function intersection(f, ray::Ray{3,T}, tri::Triangle{3,T}) where {T}
+function intersection(f, ray::Ray, tri::Triangle)
vs = vertices(tri)
o = ray(0)
d = ray(1) - ray(0)
@@ -149,21 +153,21 @@ function intersection(f, ray::Ray{3,T}, tri::Triangle{3,T}) where {T}
det = e₁ ⋅ p
# keep det > 0, modify T accordingly
- if det > atol(T)
+ if det > atol(det)
τ = o - vs[1]
else
τ = vs[1] - o
det = -det
end
- if det < atol(T)
+ if det < atol(det)
# This ray is parallel to the plane of the triangle.
return @IT NotIntersecting nothing f
end
# calculate u parameter and test bounds
u = τ ⋅ p
- if u < -atol(T) || u > det
+ if u < -atol(u) || u > det
return @IT NotIntersecting nothing f
end
@@ -171,36 +175,36 @@ function intersection(f, ray::Ray{3,T}, tri::Triangle{3,T}) where {T}
# calculate v parameter and test bounds
v = d ⋅ q
- if v < -atol(T) || u + v > det
+ if v < -atol(v) || u + v > det
return @IT NotIntersecting nothing f
end
- λ = (e₂ ⋅ q) * (one(T) / det)
+ λ = (e₂ ⋅ q) * inv(det)
- if λ < -atol(T)
+ if λ < -atol(λ)
return @IT NotIntersecting nothing f
end
# assemble barycentric weights
- w = Vec(u, v, det - u - v)
+ w = (u, v, det - u - v)
- if any(isapprox.(o, vs, atol=atol(T)))
+ if any(isapprox.(o, vs))
return @IT CornerTouching ray(λ) f
- elseif isapprox(λ, zero(T), atol=atol(T))
- if all(>(zero(T)), w)
+ elseif isapproxzero(λ)
+ if all(x -> x > zero(x), w)
return @IT Touching ray(λ) f
else
return @IT EdgeTouching ray(λ) f
end
end
- if count(x -> isapprox(x, zero(T), atol=atol(T)), w) == 1
+ if count(x -> isapproxzero(x), w) == 1
return @IT EdgeCrossing ray(λ) f
- elseif count(x -> isapprox(x, det, atol=atol(T)), w) == 1
+ elseif count(x -> isapproxequal(x, det), w) == 1
return @IT CornerCrossing ray(λ) f
end
- λ = clamp(λ, zero(T), typemax(T))
+ λ = clamp(λ, zero(λ), typemax(λ))
return @IT Crossing ray(λ) f
end
diff --git a/src/intersections/segments.jl b/src/intersections/segments.jl
index 032ecf4b3..e2cfd8f4a 100644
--- a/src/intersections/segments.jl
+++ b/src/intersections/segments.jl
@@ -9,7 +9,7 @@
# 3. intersect at one endpoint of both segments (CornerTouching -> Point)
# 4. overlap of segments (Overlapping -> Segments)
# 5. do not overlap nor intersect (NotIntersecting -> Nothing)
-function intersection(f, seg₁::Segment{N,T}, seg₂::Segment{N,T}) where {N,T}
+function intersection(f, seg₁::Segment, seg₂::Segment)
a, b = vertices(seg₁)
c, d = vertices(seg₂)
@@ -38,8 +38,8 @@ function intersection(f, seg₁::Segment{N,T}, seg₂::Segment{N,T}) where {N,T}
end
end
- l₁ = length(seg₁)
- l₂ = length(seg₂)
+ l₁ = ustrip(length(seg₁))
+ l₂ = ustrip(length(seg₂))
b₀ = a + 1 / l₁ * (b - a)
d₀ = c + 1 / l₂ * (d - c)
@@ -57,8 +57,8 @@ function intersection(f, seg₁::Segment{N,T}, seg₂::Segment{N,T}) where {N,T}
vd = d - a
λc = vc[i] / v[i]
λd = vd[i] / v[i]
- λc = mayberound(mayberound(λc, zero(T)), l₁)
- λd = mayberound(mayberound(λd, zero(T)), l₁)
+ λc = mayberound(mayberound(λc, zero(λc)), l₁)
+ λd = mayberound(mayberound(λd, zero(λd)), l₁)
if (λc > l₁ && λd > l₁) || (λc < 0 && λd < 0)
return @IT NotIntersecting nothing f # CASE 5
elseif (λc == 0 && λd < 0) || (λd == 0 && λc < 0)
@@ -66,14 +66,14 @@ function intersection(f, seg₁::Segment{N,T}, seg₂::Segment{N,T}) where {N,T}
elseif (λc == l₁ && λd > l₁) || (λd == l₁ && λc > l₁)
return @IT CornerTouching b f # CASE 3
else
- t₁, t₂ = _sort4vals(zero(T), one(T), λc / l₁, λd / l₁)
+ t₁, t₂ = _sort4vals(zero(λc), one(λc), λc / l₁, λd / l₁)
p₁ = seg₁(t₁)
p₂ = seg₁(t₂)
return @IT Overlapping Segment(p₁, p₂) f # CASE 4
end
else # in same plane, not parallel
- λ₁ = mayberound(mayberound(λ₁, zero(T)), l₁)
- λ₂ = mayberound(mayberound(λ₂, zero(T)), l₂)
+ λ₁ = mayberound(mayberound(λ₁, zero(λ₁)), l₁)
+ λ₂ = mayberound(mayberound(λ₂, zero(λ₂)), l₂)
if λ₁ < 0 || λ₂ < 0 || λ₁ > l₁ || λ₂ > l₂
return @IT NotIntersecting nothing f # CASE 5
# 8 cases remain
@@ -104,12 +104,14 @@ end
# 3. intersects at one end point of segment and origin of ray (CornerTouching -> Point)
# 4. overlap at more than one point (Overlapping -> Segment)
# 5. do not overlap nor intersect (NotIntersecting -> Nothing)
-function intersection(f, seg::Segment{N,T}, ray::Ray{N,T}) where {N,T}
+function intersection(f, seg::Segment, ray::Ray)
+ Dim = embeddim(seg)
a, b = ray(0), ray(1)
c, d = seg(0), seg(1)
# normalize points to gain parameters λ₁, λ₂ corresponding to arc lengths
- l₁, l₂ = norm(b - a), length(seg)
+ l₁ = ustrip(norm(b - a))
+ l₂ = ustrip(length(seg))
b₀ = a + 1 / l₁ * (b - a)
d₀ = c + 1 / l₂ * (d - c)
@@ -120,10 +122,10 @@ function intersection(f, seg::Segment{N,T}, ray::Ray{N,T}) where {N,T}
return @IT NotIntersecting nothing f # CASE 5
# collinear
elseif r == rₐ == 1
- rc = sum((c - a) ./ (b - a)) / N
- rd = sum((d - a) ./ (b - a)) / N
- rc = mayberound(rc, zero(T))
- rd = mayberound(rd, zero(T))
+ rc = sum((c - a) ./ (b - a)) / Dim
+ rd = sum((d - a) ./ (b - a)) / Dim
+ rc = mayberound(rc, zero(rc))
+ rd = mayberound(rd, zero(rd))
if rc > 0 # c ∈ ray
if rd ≥ 0
return @IT Overlapping seg f # CASE 4
@@ -147,8 +149,8 @@ function intersection(f, seg::Segment{N,T}, ray::Ray{N,T}) where {N,T}
end
# in same plane, not parallel
else
- λ₁ = mayberound(λ₁, zero(T))
- λ₂ = mayberound(mayberound(λ₂, zero(T)), l₂)
+ λ₁ = mayberound(λ₁, zero(λ₁))
+ λ₂ = mayberound(mayberound(λ₂, zero(λ₂)), l₂)
if λ₁ < 0 || (λ₂ < 0 || λ₂ > l₂)
return @IT NotIntersecting nothing f
elseif λ₁ == 0
@@ -172,12 +174,12 @@ end
# 2. intersect at an end point of segment (Touching -> Point)
# 3. overlap of line and segment (Overlapping -> Segment)
# 4. do not overlap nor intersect (NotIntersecting -> Nothing)
-function intersection(f, seg::Segment{N,T}, line::Line{N,T}) where {N,T}
+function intersection(f, seg::Segment, line::Line)
a, b = line(0), line(1)
c, d = seg(0), seg(1)
# normalize points to gain parameter λ₂ corresponding to arc lengths
- l₂ = length(seg)
+ l₂ = ustrip(length(seg))
d₀ = c + 1 / l₂ * (d - c)
_, λ₂, r, rₐ = intersectparameters(a, b, c, d₀)
@@ -190,7 +192,7 @@ function intersection(f, seg::Segment{N,T}, line::Line{N,T}) where {N,T}
return @IT Overlapping seg f # CASE 3
# in same plane, not parallel
else
- λ₂ = mayberound(mayberound(λ₂, zero(T)), l₂)
+ λ₂ = mayberound(mayberound(λ₂, zero(λ₂)), l₂)
if λ₂ > 0 && λ₂ < l₂
return @IT Crossing seg(λ₂ / l₂) f # CASE 1, equal to line(λ₁)
elseif λ₂ == 0 || λ₂ == l₂
@@ -203,7 +205,7 @@ end
# Algorithm 4 of Jiménez, J., Segura, R. and Feito, F. 2009.
# (https://www.sciencedirect.com/science/article/pii/S0925772109001448?via%3Dihub)
-function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
+function intersection(f, seg::Segment, tri::Triangle)
Q1, Q2 = vertices(seg)
V1, V2, V3 = vertices(tri)
@@ -224,9 +226,9 @@ function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
D = Q2 - V3
s = D ⋅ W₁
- if w > atol(T)
+ if w > atol(w)
# rejection 2
- if s > atol(T)
+ if s > atol(s)
return @IT NotIntersecting nothing f
end
@@ -234,14 +236,14 @@ function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
t = W₂ ⋅ C
# rejection 3
- if t < -atol(T)
+ if t < -atol(t)
return @IT NotIntersecting nothing f
end
u = -(W₂ ⋅ B)
# rejection 4
- if u < -atol(T)
+ if u < -atol(u)
return @IT NotIntersecting nothing f
end
@@ -249,9 +251,9 @@ function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
if w < (s + t + u)
return @IT NotIntersecting nothing f
end
- elseif w < -atol(T)
+ elseif w < -atol(w)
# rejection 2
- if s < -atol(T)
+ if s < -atol(s)
return @IT NotIntersecting nothing f
end
@@ -259,14 +261,14 @@ function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
t = W₂ ⋅ C
# rejection 3
- if t > atol(T)
+ if t > atol(t)
return @IT NotIntersecting nothing f
end
u = -(W₂ ⋅ B)
# rejection 4
- if u > atol(T)
+ if u > atol(u)
return @IT NotIntersecting nothing f
end
@@ -275,19 +277,19 @@ function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
return @IT NotIntersecting nothing f
end
else # w ≈ 0
- if s > atol(T)
+ if s > atol(s)
W₂ = D × A
t = W₂ ⋅ C
# rejection 3
- if t < -atol(T)
+ if t < -atol(t)
return @IT NotIntersecting nothing f
end
u = -(W₂ ⋅ B)
# rejection 4
- if u < -atol(T)
+ if u < -atol(u)
return @IT NotIntersecting nothing f
end
@@ -295,19 +297,19 @@ function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
if -s < (t + u)
return @IT NotIntersecting nothing f
end
- elseif s < -atol(T)
+ elseif s < -atol(s)
W₂ = D × A
t = W₂ ⋅ C
# rejection 3
- if t > atol(T)
+ if t > atol(t)
return @IT NotIntersecting nothing f
end
u = -(W₂ ⋅ B)
# rejection 4
- if u > atol(T)
+ if u > atol(u)
return @IT NotIntersecting nothing f
end
@@ -321,7 +323,8 @@ function intersection(f, seg::Segment{3,T}, tri::Triangle{3,T}) where {T}
end
end
- λ = clamp(w / (w - s), zero(T), one(T))
+ λ = w / (w - s)
+ λ = clamp(λ, zero(λ), one(λ))
p = Segment(Q1, Q2)(λ)
diff --git a/src/ioutils.jl b/src/ioutils.jl
index fba6dde99..407d8086b 100644
--- a/src/ioutils.jl
+++ b/src/ioutils.jl
@@ -11,27 +11,22 @@ function prettyname(T::Type)
replace(name, r".+\." => "")
end
-# helper function to print a large indexable collection
-# in multiple lines with a given tabulation
-function printelms(io::IO, elms, tab="")
- N = length(elms)
- I, J = N > 10 ? (5, N - 4) : (N - 1, N)
+# helper function to print the elements of an object
+# in multiple lines with a given number of elements, getter and tabulation
+function printelms(io::IO, obj; nelms=length(obj), getelm=getindex, tab="")
+ I, J = nelms > 10 ? (5, nelms - 4) : (nelms - 1, nelms)
for i in 1:I
- println(io, "$(tab)├─ $(elms[i])")
+ println(io, "$(tab)├─ $(getelm(obj, i))")
end
- if N > 10
+ if nelms > 10
println(io, "$(tab)⋮")
end
- for i in J:(N - 1)
- println(io, "$(tab)├─ $(elms[i])")
+ for i in J:(nelms - 1)
+ println(io, "$(tab)├─ $(getelm(obj, i))")
end
- print(io, "$(tab)└─ $(elms[N])")
+ print(io, "$(tab)└─ $(getelm(obj, nelms))")
end
-# helper function to print a large iterable
-# calling the printelms function
-printitr(io::IO, itr, tab="") = printelms(io, collect(itr), tab)
-
# helper function to print the polygons vertices
function printverts(io::IO, verts)
ioctx = IOContext(io, :compact => true)
@@ -59,12 +54,11 @@ printinds(io::IO, inds::AbstractRange) = print(io, inds)
printfields(io, obj; kwargs...) = printfields(io, obj, fieldnames(typeof(obj)); kwargs...)
-function printfields(io, obj, fnames; compact=false)
- if compact
- ioctx = IOContext(io, :compact => true)
+function printfields(io, obj, fnames; singleline=false)
+ if singleline
vals = map(enumerate(fnames)) do (i, field)
val = getfield(obj, i)
- str = repr(val, context=ioctx)
+ str = repr(val, context=io)
"$field: $str"
end
join(io, vals, ", ")
diff --git a/src/manifolds.jl b/src/manifolds.jl
new file mode 100644
index 000000000..403d13cf7
--- /dev/null
+++ b/src/manifolds.jl
@@ -0,0 +1,24 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Manifold
+
+A manifold where geometries and domains are defined.
+"""
+abstract type Manifold end
+
+"""
+ 𝔼{Dim}
+
+Euclidean manifold with dimension `Dim`.
+"""
+abstract type 𝔼{Dim} <: Manifold end
+
+"""
+ 🌐
+
+Ellipsoid manifold for geodesic geometry.
+"""
+abstract type 🌐 <: Manifold end
diff --git a/src/matrices.jl b/src/matrices.jl
index 314e08cc0..1f525e080 100644
--- a/src/matrices.jl
+++ b/src/matrices.jl
@@ -2,16 +2,25 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
+# helper function to select default Laplacian discretization
+laplacekind(mesh) = eltype(mesh) <: Triangle ? :cotangent : :uniform
+
+# helper function to convert topology if necessary
+adjusttopo(topo::SimpleTopology) = convert(HalfEdgeTopology, topo)
+adjusttopo(topo) = topo
+
"""
- laplacematrix(mesh; weights=:cotangent)
+ laplacematrix(mesh; kind=nothing)
The Laplace-Beltrami (a.k.a. Laplacian) matrix of the `mesh`.
-Optionally specify the discretization `weights`.
+Optionally, specify the `kind` of discretization.
-## Weights
+## Available discretizations
-* `:uniform` - `Lᵢⱼ = 1 / |𝒩(i)|, ∀j ∈ 𝒩(i)`
-* `:cotangent` - `Lᵢⱼ = cot(αᵢⱼ) + cot(βᵢⱼ), ∀j ∈ 𝒩(i)`
+* `:uniform` - `Lᵢⱼ = 1 / |𝒜(i)|, ∀j ∈ 𝒜(i)`
+* `:cotangent` - `Lᵢⱼ = cot(αᵢⱼ) + cot(βᵢⱼ), ∀j ∈ 𝒜(i)`
+
+where `𝒜(i)` is the adjacency relation at vertex `i`.
## References
@@ -20,35 +29,37 @@ Optionally specify the discretization `weights`.
* Pinkall, U. & Polthier, K. 1993. [Computing discrete minimal surfaces and their conjugates]
(https://projecteuclid.org/journals/experimental-mathematics/volume-2/issue-1/Computing-discrete-minimal-surfaces-and-their-conjugates/em/1062620735.full).
"""
-function laplacematrix(mesh; weights=:cotangent)
- # convert to half-edge topology
- ℳ = topoconvert(HalfEdgeTopology, mesh)
+function laplacematrix(mesh; kind=nothing)
+ # select default discretization
+ 𝒦 = isnothing(kind) ? laplacekind(mesh) : kind
+
+ # sanity checks
+ 𝒦 == :cotangent && assertion(eltype(mesh) <: Triangle, "cotangent weights only defined for triangle meshes")
+
+ # adjust topology if necessary
+ 𝒯 = adjusttopo(topology(mesh))
# retrieve adjacency relation
- 𝒩 = Adjacency{0}(topology(ℳ))
+ 𝒜 = Adjacency{0}(𝒯)
# initialize matrix
- n = nvertices(ℳ)
+ n = nvertices(mesh)
L = spzeros(n, n)
- # fill matrix with weights
- if weights == :uniform
- uniformlaplacian!(L, 𝒩)
- elseif weights == :cotangent
- v = vertices(ℳ)
- @assert eltype(ℳ) <: Triangle "cotangent weights only defined for triangle meshes"
- cotangentlaplacian!(L, 𝒩, v)
- else
- throw(ArgumentError("invalid discretization weights"))
+ # fill matrix
+ if 𝒦 == :uniform
+ uniformlaplacian!(L, 𝒜)
+ elseif 𝒦 == :cotangent
+ cotangentlaplacian!(L, 𝒜, vertices(mesh))
end
L
end
-function uniformlaplacian!(L, 𝒩)
+function uniformlaplacian!(L, 𝒜)
n = size(L, 1)
for i in 1:n
- js = 𝒩(i)
+ js = 𝒜(i)
for j in js
L[i, j] = 1 / length(js)
end
@@ -56,19 +67,22 @@ function uniformlaplacian!(L, 𝒩)
end
end
-function cotangentlaplacian!(L, 𝒩, v)
+function cotangentlaplacian!(L, 𝒜, v)
n = size(L, 1)
for i in 1:n
- js = CircularVector(𝒩(i))
- for k in 1:length(js)
- j₋, j, j₊ = js[k - 1], js[k], js[k + 1]
+ js = 𝒜(i)
+ m = length(js)
+ for k in 1:m
+ j₋ = js[mod1(k - 1, m)]
+ j = js[mod1(k, m)]
+ j₊ = js[mod1(k + 1, m)]
vᵢ, vⱼ = v[i], v[j]
v₋, v₊ = v[j₋], v[j₊]
αᵢⱼ = ∠(vⱼ, v₋, vᵢ)
βᵢⱼ = ∠(vᵢ, v₊, vⱼ)
L[i, j] = cot(αᵢⱼ) + cot(βᵢⱼ)
end
- L[i, i] = -sum(L[i, js])
+ L[i, i] = -sum(j -> L[i, j], js)
end
end
@@ -84,22 +98,26 @@ as `Δ = M⁻¹L`. When solving systems of the form `Δu = f`, it
is useful to write `Lu = Mf` and exploit the symmetry of `L`.
"""
function measurematrix(mesh)
- # convert to half-edge topology
- ℳ = topoconvert(HalfEdgeTopology, mesh)
+ # adjust topology if necessary
+ 𝒯 = adjusttopo(topology(mesh))
- # retrieve coboundary relation
- ∂ = Coboundary{0,2}(topology(ℳ))
+ # parametric dimension
+ D = paramdim(mesh)
- # initialize matrix
- n = nvertices(ℳ)
- M = 1.0 * I(n)
+ # retrieve coboundary relation
+ 𝒞 = Coboundary{0,D}(𝒯)
# pre-compute all measures
- A = measure.(ℳ)
+ A = measure.(mesh)
+
+ # initialize matrix
+ n = nvertices(mesh)
+ M = oneunit(eltype(A)) * I(n)
- # fill matrix with measures
+ # fill matrix
for i in 1:n
- Aᵢ = sum(A[∂(i)]) / 3
+ js = 𝒞(i)
+ Aᵢ = sum(j -> A[j], js) / 3
M[i, i] = 2Aᵢ
end
@@ -107,18 +125,20 @@ function measurematrix(mesh)
end
"""
- adjacencymatrix(mesh)
+ adjacencymatrix(mesh; rank=paramdim(mesh))
-Return the adjacency matrix of the elements of the `mesh`
-using the adjacency relation of the underlying topology.
+The adjacency matrix of the `mesh` using the adjacency
+relation of given `rank` for the underlying topology.
"""
-function adjacencymatrix(mesh)
- t = topology(mesh)
- D = paramdim(mesh)
- 𝒜 = Adjacency{D}(t)
+function adjacencymatrix(mesh; rank=paramdim(mesh))
+ # adjust topology if necessary
+ 𝒯 = adjusttopo(topology(mesh))
+
+ # retrieve adjacency relation
+ 𝒜 = Adjacency{rank}(𝒯)
# initialize matrix
- n = nelements(mesh)
+ n = nfaces(mesh, rank)
A = spzeros(Int, n, n)
# fill in matrix
diff --git a/src/measures.jl b/src/measures.jl
index e670dbda4..d15cce6c8 100644
--- a/src/measures.jl
+++ b/src/measures.jl
@@ -9,71 +9,85 @@
"""
measure(object)
-Return the measure or "volume" of the `object`.
+Return the measure of the geometric `object`.
-### Notes
-
-- Type aliases are [`length`](@ref), [`area`](@ref), [`volume`](@ref)
+This function is also known as [`length`](@ref),
+[`area`](@ref) or [`volume`](@ref) depending on
+the parametric dimension of the object.
"""
function measure end
-measure(::Point{Dim,T}) where {Dim,T} = zero(T)
+measure(p::Point) = zero(lentype(p))
-measure(::Ray{Dim,T}) where {Dim,T} = typemax(T)
+measure(r::Ray) = typemax(lentype(r))
-measure(::Line{Dim,T}) where {Dim,T} = typemax(T)
+measure(l::Line) = typemax(lentype(l))
-measure(::Plane{T}) where {T} = typemax(T)
+measure(p::Plane) = typemax(lentype(p))^2
-measure(b::Box) = prod(maximum(b) - minimum(b))
+measure(b::Box{<:𝔼}) = prod(maximum(b) - minimum(b))
# https://en.wikipedia.org/wiki/Volume_of_an_n-ball
-function measure(b::Ball{Dim}) where {Dim}
- r, n = radius(b), Dim
- (π^(n / 2) * r^n) / gamma(n / 2 + 1)
+function measure(b::Ball{<:𝔼})
+ T = numtype(lentype(b))
+ r, n = radius(b), embeddim(b)
+ T(π)^T(n / 2) * r^n / gamma(T(n / 2) + 1)
end
# https://en.wikipedia.org/wiki/N-sphere#Volume_and_surface_area
-function measure(s::Sphere{Dim}) where {Dim}
- r, n = radius(s), Dim
- 2π^(n / 2) * r^(n - 1) / gamma(n / 2)
+function measure(s::Sphere{<:𝔼})
+ T = numtype(lentype(s))
+ r, n = radius(s), embeddim(s)
+ 2 * T(π)^T(n / 2) * r^(n - 1) / gamma(T(n / 2))
end
-measure(d::Disk{T}) where {T} = T(π) * radius(d)^2
+measure(d::Disk) = π * radius(d)^2
-measure(c::Circle{T}) where {T} = 2 * T(π) * radius(c)
+measure(c::Circle) = 2 * (π * radius(c))
-function measure(c::Cylinder{T}) where {T}
+function measure(c::Cylinder)
t = top(c)
b = bottom(c)
r = radius(c)
- norm(t(0, 0) - b(0, 0)) * T(π) * r^2
+ h = norm(t(0, 0) - b(0, 0))
+ π * r^2 * h
end
-function measure(c::CylinderSurface{T}) where {T}
+function measure(c::CylinderSurface)
t = top(c)
b = bottom(c)
r = radius(c)
- (norm(t(0, 0) - b(0, 0)) + r) * 2 * r * T(π)
+ h = norm(t(0, 0) - b(0, 0))
+ 2 * (π * r) * (h + r)
end
-function measure(p::ParaboloidSurface{T}) where {T}
+function measure(p::ParaboloidSurface)
+ T = numtype(lentype(p))
r = radius(p)
f = focallength(p)
- T(8π / 3) * f^2 * ((T(1) + r^2 / (2f)^2)^(3 / 2) - T(1))
+ (8 * T(π) / 3) * f^2 * ((1 + r^2 / (2f)^2)^T(3 / 2) - 1)
end
# https://en.wikipedia.org/wiki/Torus
-function measure(t::Torus{T}) where {T}
+function measure(t::Torus)
+ T = numtype(lentype(t))
R, r = radii(t)
- 4T(π)^2 * R * r
+ 4 * T(π)^2 * R * r
end
-measure(s::Segment) = norm(maximum(s) - minimum(s))
+measure(s::Segment{<:𝔼}) = norm(maximum(s) - minimum(s))
+
+# TODO: replace Haversine by an appropriate geodesic distance
+# that considers the west-east orientation of segments
+function measure(s::Segment{<:🌐})
+ T = numtype(lentype(s))
+ 🌎 = ellipsoid(datum(crs(s)))
+ r = numconvert(T, majoraxis(🌎))
-measure(t::Triangle{2}) = abs(signarea(t))
+ evaluate(Haversine(r), extrema(s)...)
+end
-function measure(t::Triangle{3})
+function measure(t::Triangle)
A, B, C = vertices(t)
norm((B - A) × (C - A)) / 2
end
@@ -83,13 +97,23 @@ function measure(t::Tetrahedron)
abs((A - D) ⋅ ((B - D) × (C - D))) / 6
end
+function measure(p::Polygon{𝔼{2}})
+ Σ = sum(rings(p)) do r
+ v = vertices(r)
+ n = nvertices(r)
+ c = centroid(r)
+ sum(signarea(c, v[i], v[i + 1]) for i in 1:n)
+ end
+ abs(Σ)
+end
+
measure(c::Chain) = sum(measure, segments(c))
measure(g::Geometry) = sum(measure, simplexify(g))
measure(m::Multi) = sum(measure, parent(m))
-measure(::PointSet{Dim,T}) where {Dim,T} = zero(T)
+measure(d::PointSet) = zero(lentype(d))
measure(d::Domain) = sum(measure, d)
@@ -151,8 +175,10 @@ the [`measure`](@ref) of its [`boundary`](@ref).
"""
perimeter(g) = measure(boundary(g))
-perimeter(::Line{Dim,T}) where {Dim,T} = zero(T)
+perimeter(l::Line) = zero(lentype(l))
+
+perimeter(p::Plane) = zero(lentype(p))
-perimeter(::Plane{T}) where {T} = zero(T)
+perimeter(s::Sphere) = zero(lentype(s))
-perimeter(::Sphere{Dim,T}) where {Dim,T} = zero(T)
+perimeter(e::Ellipsoid) = zero(lentype(e))
diff --git a/src/mesh/cartesiangrid.jl b/src/mesh/cartesiangrid.jl
deleted file mode 100644
index 0904592a5..000000000
--- a/src/mesh/cartesiangrid.jl
+++ /dev/null
@@ -1,165 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- CartesianGrid(dims, origin, spacing)
-
-A Cartesian grid with dimensions `dims`, lower left corner at `origin`
-and cell spacing `spacing`. The three arguments must have the same length.
-
- CartesianGrid(dims, origin, spacing, offset)
-
-A Cartesian grid with dimensions `dims`, with lower left corner of element
-`offset` at `origin` and cell spacing `spacing`.
-
- CartesianGrid(start, finish, dims=dims)
-
-Alternatively, construct a Cartesian grid from a `start` point (lower left)
-to a `finish` point (upper right).
-
- CartesianGrid(start, finish, spacing)
-
-Alternatively, construct a Cartesian grid from a `start` point to a `finish`
-point using a given `spacing`.
-
- CartesianGrid(dims)
- CartesianGrid(dim1, dim2, ...)
-
-Finally, a Cartesian grid can be constructed by only passing the dimensions
-`dims` as a tuple, or by passing each dimension `dim1`, `dim2`, ... separately.
-In this case, the origin and spacing default to (0,0,...) and (1,1,...).
-
-## Examples
-
-Create a 3D grid with 100x100x50 hexahedrons:
-
-```julia
-julia> CartesianGrid(100, 100, 50)
-```
-
-Create a 2D grid with 100 x 100 quadrangles and origin at (10.0, 20.0):
-
-```julia
-julia> CartesianGrid((100, 100), (10.0, 20.0), (1.0, 1.0))
-```
-
-Create a 1D grid from -1 to 1 with 100 segments:
-
-```julia
-julia> CartesianGrid((-1.0,), (1.0,), dims=(100,))
-```
-"""
-struct CartesianGrid{Dim,T} <: Grid{Dim,T}
- origin::Point{Dim,T}
- spacing::NTuple{Dim,T}
- offset::Dims{Dim}
- topology::GridTopology{Dim}
-end
-
-function CartesianGrid(
- dims::Dims{Dim},
- origin::Point{Dim,T},
- spacing::NTuple{Dim,T},
- offset::Dims{Dim}=ntuple(i -> 1, Dim)
-) where {Dim,T}
- @assert all(>(0), dims) "dimensions must be positive"
- @assert all(>(zero(T)), spacing) "spacing must be positive"
- CartesianGrid{Dim,T}(origin, spacing, offset, GridTopology(dims))
-end
-
-CartesianGrid(
- dims::Dims{Dim},
- origin::NTuple{Dim,T},
- spacing::NTuple{Dim,T},
- offset::Dims{Dim}=ntuple(i -> 1, Dim)
-) where {Dim,T} = CartesianGrid(dims, Point(origin), spacing, offset)
-
-function CartesianGrid(start::Point{Dim,T}, finish::Point{Dim,T}, spacing::NTuple{Dim,T}) where {Dim,T}
- dims = Tuple(ceil.(Int, (finish - start) ./ spacing))
- origin = start
- offset = ntuple(i -> 1, Dim)
- CartesianGrid(dims, origin, spacing, offset)
-end
-
-CartesianGrid(start::NTuple{Dim,T}, finish::NTuple{Dim,T}, spacing::NTuple{Dim,T}) where {Dim,T} =
- CartesianGrid(Point(start), Point(finish), spacing)
-
-function CartesianGrid(start::Point{Dim,T}, finish::Point{Dim,T}; dims::Dims{Dim}=ntuple(i -> 100, Dim)) where {Dim,T}
- origin = start
- spacing = Tuple((finish - start) ./ dims)
- offset = ntuple(i -> 1, Dim)
- CartesianGrid(dims, origin, spacing, offset)
-end
-
-CartesianGrid(start::NTuple{Dim,T}, finish::NTuple{Dim,T}; dims::Dims{Dim}=ntuple(i -> 100, Dim)) where {Dim,T} =
- CartesianGrid(Point(start), Point(finish); dims=dims)
-
-function CartesianGrid{T}(dims::Dims{Dim}) where {Dim,T}
- origin = ntuple(i -> zero(T), Dim)
- spacing = ntuple(i -> oneunit(T), Dim)
- offset = ntuple(i -> 1, Dim)
- CartesianGrid(dims, origin, spacing, offset)
-end
-
-CartesianGrid{T}(dims::Vararg{Int,Dim}) where {Dim,T} = CartesianGrid{T}(dims)
-
-CartesianGrid(dims::Dims{Dim}) where {Dim} = CartesianGrid{Float64}(dims)
-
-CartesianGrid(dims::Vararg{Int,Dim}) where {Dim} = CartesianGrid{Float64}(dims)
-
-vertex(g::CartesianGrid{Dim}, ijk::Dims{Dim}) where {Dim} =
- Point(coordinates(g.origin) .+ (ijk .- g.offset) .* g.spacing)
-
-spacing(g::CartesianGrid) = g.spacing
-
-offset(g::CartesianGrid) = g.offset
-
-function xyz(g::CartesianGrid{Dim}) where {Dim}
- dims = size(g)
- spac = spacing(g)
- orig = coordinates(minimum(g))
- ntuple(Dim) do i
- o, s, d = orig[i], spac[i], dims[i]
- range(start=o, step=s, length=(d + 1))
- end
-end
-
-XYZ(g::CartesianGrid) = XYZ(convert(RectilinearGrid, g))
-
-function centroid(g::CartesianGrid, ind::Int)
- ijk = elem2cart(topology(g), ind)
- p = vertex(g, ijk)
- δ = Vec(spacing(g) ./ 2)
- p + δ
-end
-
-function Base.getindex(g::CartesianGrid{Dim}, I::CartesianIndices{Dim}) where {Dim}
- @boundscheck _checkbounds(g, I)
- dims = size(I)
- offset = g.offset .- Tuple(first(I)) .+ 1
- CartesianGrid(dims, g.origin, g.spacing, offset)
-end
-
-==(g1::CartesianGrid, g2::CartesianGrid) =
- g1.topology == g2.topology &&
- g1.spacing == g2.spacing &&
- Tuple(g1.origin - g2.origin) == (g1.offset .- g2.offset) .* g1.spacing
-
-# -----------
-# IO METHODS
-# -----------
-
-function Base.summary(io::IO, g::CartesianGrid{Dim,T}) where {Dim,T}
- dims = join(size(g.topology), "×")
- print(io, "$dims CartesianGrid{$Dim,$T}")
-end
-
-Base.show(io::IO, g::CartesianGrid) = summary(io, g)
-
-function Base.show(io::IO, ::MIME"text/plain", g::CartesianGrid)
- println(io, g)
- println(io, " minimum: ", minimum(g))
- println(io, " maximum: ", maximum(g))
- print(io, " spacing: ", spacing(g))
-end
diff --git a/src/mesh/rectilineargrid.jl b/src/mesh/rectilineargrid.jl
deleted file mode 100644
index 41391c3af..000000000
--- a/src/mesh/rectilineargrid.jl
+++ /dev/null
@@ -1,72 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- RectilinearGrid(x, y, z, ...)
-
-A rectilinear grid with vertices at sorted coordinates `x`, `y`, `z`, ...
-
-## Examples
-
-Create a 2D rectilinear grid with regular spacing in `x` dimension
-and irregular spacing in `y` dimension:
-
-```julia
-julia> x = 0.0:0.2:1.0
-julia> y = [0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
-julia> RectilinearGrid(x, y)
-```
-"""
-struct RectilinearGrid{Dim,T,V<:AbstractVector{T}} <: Grid{Dim,T}
- xyz::NTuple{Dim,V}
- topology::GridTopology{Dim}
-end
-
-function RectilinearGrid(xyz::Tuple)
- coords = promote(collect.(xyz)...)
- topology = GridTopology(length.(coords) .- 1)
- RectilinearGrid(coords, topology)
-end
-
-RectilinearGrid(xyz...) = RectilinearGrid(xyz)
-
-vertex(g::RectilinearGrid{Dim}, ijk::Dims{Dim}) where {Dim} = Point(getindex.(g.xyz, ijk))
-
-xyz(g::RectilinearGrid) = g.xyz
-
-@generated function XYZ(g::RectilinearGrid{Dim,T}) where {Dim,T}
- exprs = ntuple(Dim) do d
- quote
- a = g.xyz[$d]
- N = length(a)
- A = Array{T,Dim}(undef, @ntuple($Dim, i -> N))
- @nloops $Dim i A begin
- @nref($Dim, A, i) = a[$(Symbol(:i_, d))]
- end
- A
- end
- end
- Expr(:tuple, exprs...)
-end
-
-function centroid(g::RectilinearGrid, ind::Int)
- ijk = elem2cart(topology(g), ind)
- p1 = vertex(g, ijk)
- p2 = vertex(g, ijk .+ 1)
- Point((coordinates(p1) + coordinates(p2)) / 2)
-end
-
-function Base.getindex(g::RectilinearGrid{Dim}, I::CartesianIndices{Dim}) where {Dim}
- @boundscheck _checkbounds(g, I)
- dims = size(I)
- start = Tuple(first(I))
- stop = Tuple(last(I)) .+ 1
- xyz = ntuple(i -> g.xyz[i][start[i]:stop[i]], Dim)
- RectilinearGrid(xyz, GridTopology(dims))
-end
-
-function Base.summary(io::IO, g::RectilinearGrid{Dim,T}) where {Dim,T}
- join(io, size(g), "×")
- print(io, " RectilinearGrid{$Dim,$T}")
-end
diff --git a/src/mesh/structuredgrid.jl b/src/mesh/structuredgrid.jl
deleted file mode 100644
index 64145afe1..000000000
--- a/src/mesh/structuredgrid.jl
+++ /dev/null
@@ -1,49 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- StructuredGrid(X, Y, Z, ...)
-
-A structured grid with vertices at sorted coordinates `X`, `Y`, `Z`, ...
-
-## Examples
-
-Create a 2D structured grid with regular spacing in `x` dimension
-and irregular spacing in `y` dimension:
-
-```julia
-julia> X = repeat(0.0:0.2:1.0, 1, 6)
-julia> Y = repeat([0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
-julia> StructuredGrid(X, Y)
-```
-"""
-struct StructuredGrid{Dim,T,A<:AbstractArray{T}} <: Grid{Dim,T}
- XYZ::NTuple{Dim,A}
- topology::GridTopology{Dim}
-end
-
-function StructuredGrid(XYZ::Tuple)
- coords = promote(XYZ...)
- topology = GridTopology(size(first(coords)) .- 1)
- StructuredGrid(coords, topology)
-end
-
-StructuredGrid(XYZ...) = StructuredGrid(XYZ)
-
-vertex(g::StructuredGrid{Dim}, ijk::Dims{Dim}) where {Dim} = Point(ntuple(d -> g.XYZ[d][ijk...], Dim))
-
-XYZ(g::StructuredGrid) = g.XYZ
-
-function Base.getindex(g::StructuredGrid{Dim}, I::CartesianIndices{Dim}) where {Dim}
- @boundscheck _checkbounds(g, I)
- dims = size(I)
- cinds = first(I):CartesianIndex(Tuple(last(I)) .+ 1)
- XYZ = ntuple(i -> g.XYZ[i][cinds], Dim)
- StructuredGrid(XYZ, GridTopology(dims))
-end
-
-function Base.summary(io::IO, g::StructuredGrid{Dim,T}) where {Dim,T}
- join(io, size(g), "×")
- print(io, " StructuredGrid{$Dim,$T}")
-end
diff --git a/src/mesh/transformedmesh.jl b/src/mesh/transformedmesh.jl
deleted file mode 100644
index 81b12a39d..000000000
--- a/src/mesh/transformedmesh.jl
+++ /dev/null
@@ -1,37 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- TransformedMesh(mesh, transform)
-
-Lazy representation of a geometric `transform` applied to a `mesh`.
-"""
-struct TransformedMesh{Dim,T,TP<:Topology,M<:Mesh{Dim,T,TP},TR<:Transform} <: Mesh{Dim,T,TP}
- mesh::M
- transform::TR
-end
-
-# specialize constructor to avoid deep structures
-TransformedMesh(m::TransformedMesh, t::Transform) = TransformedMesh(m.mesh, m.transform → t)
-
-Base.parent(m::TransformedMesh) = m.mesh
-
-transform(m::TransformedMesh) = m.transform
-
-topology(m::TransformedMesh) = topology(m.mesh)
-
-vertex(m::TransformedMesh, ind::Int) = m.transform(vertex(m.mesh, ind))
-
-# alias to improve readability in IO methods
-const TransformedGrid{Dim,T,G<:Grid{Dim,T},TR} = TransformedMesh{Dim,T,GridTopology{Dim},G,TR}
-
-TransformedGrid(g::Grid, t::Transform) = TransformedMesh(g, t)
-
-@propagate_inbounds Base.getindex(g::TransformedGrid{Dim}, I::CartesianIndices{Dim}) where {Dim} =
- TransformedGrid(getindex(g.mesh, I), g.transform)
-
-function Base.summary(io::IO, g::TransformedGrid{Dim,T}) where {Dim,T}
- join(io, size(g), "×")
- print(io, " TransformedGrid{$Dim,$T}")
-end
diff --git a/src/multigeoms.jl b/src/multigeoms.jl
deleted file mode 100644
index 31ed7026a..000000000
--- a/src/multigeoms.jl
+++ /dev/null
@@ -1,82 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Multi(geoms)
-
-A collection of geometries `geoms` seen as a single [`Geometry`](@ref).
-
-In geographic information systems (GIS) it is common to represent
-multiple polygons as a single entity (e.g. country with islands).
-
-### Notes
-
-- Type aliases are [`MultiPoint`](@ref), [`MultiSegment`](@ref),
- [`MultiRope`](@ref), [`MultiRing`](@ref), [`MultiPolygon`](@ref).
-"""
-struct Multi{Dim,T,G<:Geometry{Dim,T}} <: Geometry{Dim,T}
- geoms::Vector{G}
-end
-
-# constructor with iterator of geometries
-Multi(geoms) = Multi(collect(geoms))
-
-# type aliases for convenience
-const MultiPoint{Dim,T} = Multi{Dim,T,<:Point{Dim,T}}
-const MultiSegment{Dim,T} = Multi{Dim,T,<:Segment{Dim,T}}
-const MultiRope{Dim,T} = Multi{Dim,T,<:Rope{Dim,T}}
-const MultiRing{Dim,T} = Multi{Dim,T,<:Ring{Dim,T}}
-const MultiPolygon{Dim,T} = Multi{Dim,T,<:Polygon{Dim,T}}
-const MultiPolyhedron{Dim,T} = Multi{Dim,T,<:Polyhedron{Dim,T}}
-
-paramdim(m::Multi) = maximum(paramdim, m.geoms)
-
-vertex(m::Multi, ind) = vertices(m)[ind]
-
-vertices(m::Multi) = [vertex for geom in m.geoms for vertex in vertices(geom)]
-
-nvertices(m::Multi) = sum(nvertices, m.geoms)
-
-Base.unique(m::Multi) = unique!(deepcopy(m))
-
-function Base.unique!(m::Multi)
- foreach(unique!, m.geoms)
- m
-end
-
-function centroid(m::Multi)
- cs = coordinates.(centroid.(m.geoms))
- Point(sum(cs) / length(cs))
-end
-
-rings(m::MultiPolygon{Dim,T}) where {Dim,T} = [ring for poly in m.geoms for ring in rings(poly)]
-
-Base.parent(m::Multi) = m.geoms
-
-==(m₁::Multi, m₂::Multi) = length(m₁.geoms) == length(m₂.geoms) && all(g -> g[1] == g[2], zip(m₁.geoms, m₂.geoms))
-
-Base.isapprox(m₁::Multi, m₂::Multi) = all(g -> g[1] ≈ g[2], zip(m₁.geoms, m₂.geoms))
-
-# -----------
-# IO METHODS
-# -----------
-
-function Base.summary(io::IO, m::Multi{Dim,T}) where {Dim,T}
- name = prettyname(eltype(m.geoms))
- print(io, "Multi$name{$Dim,$T}")
-end
-
-function Base.show(io::IO, m::Multi)
- print(io, "Multi(")
- geoms = prettyname.(m.geoms)
- counts = ("$(count(==(g), geoms))×$g" for g in unique(geoms))
- join(io, counts, ", ")
- print(io, ")")
-end
-
-function Base.show(io::IO, ::MIME"text/plain", m::Multi)
- summary(io, m)
- println(io)
- printelms(io, m.geoms)
-end
diff --git a/src/neighborhoods/metricball.jl b/src/neighborhoods/metricball.jl
index acc16ab52..2ad8c5262 100644
--- a/src/neighborhoods/metricball.jl
+++ b/src/neighborhoods/metricball.jl
@@ -31,20 +31,23 @@ Axis-aligned 3D ellipsoid with radii `(3.0, 2.0, 1.0)`:
julia> mahalanobis = MetricBall((3.0, 2.0, 1.0))
```
"""
-struct MetricBall{L,R,M} <: Neighborhood
- radii::L
+struct MetricBall{Dim,ℒ<:Len,R,M} <: Neighborhood
+ radii::NTuple{Dim,ℒ}
rotation::R
# state fields
metric::M
+
+ MetricBall(radii::NTuple{Dim,ℒ}, rotation::R, metric::M) where {Dim,ℒ<:Len,R,M} =
+ new{Dim,float(ℒ),R,M}(radii, rotation, metric)
end
-function MetricBall(radii::SVector{Dim,T}, rotation=default_rotation(Val{Dim}(), T)) where {Dim,T}
+function MetricBall(radii::NTuple{Dim,ℒ}, rotation=nothing) where {Dim,ℒ<:Len}
# scaling matrix
- Λ = Diagonal(one(T) ./ radii .^ 2)
+ Λ = Diagonal(SVector((oneunit(ℒ) ./ radii) .^ 2))
# rotation matrix
- R = rotation
+ R = isnothing(rotation) ? default_rotation(Val(Dim), float(numtype(ℒ))) : rotation
# anisotropy matrix
M = Symmetric(R * Λ * R')
@@ -52,17 +55,16 @@ function MetricBall(radii::SVector{Dim,T}, rotation=default_rotation(Val{Dim}(),
# Mahalanobis metric
metric = Mahalanobis(M)
- MetricBall(radii, rotation, metric)
+ MetricBall(radii, R, metric)
end
-MetricBall(radii::NTuple{Dim,T}, rotation=default_rotation(Val{Dim}(), T)) where {Dim,T} =
- MetricBall(SVector(radii), rotation)
+MetricBall(radii::NTuple{Dim,Len}, rotation=nothing) where {Dim} = MetricBall(promote(radii...), rotation)
+
+MetricBall(radii::Tuple, rotation=nothing) = MetricBall(addunit.(radii, u"m"), rotation)
-# avoid silent calls to inner constructor
-MetricBall(radii::AbstractVector{T}, rotation=default_rotation(Val{length(radii)}(), T)) where {T} =
- MetricBall(SVector{length(radii),T}(radii), rotation)
+MetricBall(radius::Len, metric=Euclidean()) = MetricBall((radius,), nothing, metric)
-MetricBall(radius::T, metric=Euclidean()) where {T<:Number} = MetricBall(SVector(radius), nothing, metric)
+MetricBall(radius::Number, metric=Euclidean()) = MetricBall(addunit(radius, u"m"), metric)
default_rotation(::Val{2}, T) = one(Angle2d{T})
default_rotation(::Val{3}, T) = one(QuatRotation{T})
@@ -97,7 +99,7 @@ and `||v|| > r, ∀ v ∉ ball``.
"""
function radius(ball::MetricBall)
r = first(ball.radii)
- ball.metric isa Mahalanobis ? one(r) : r
+ ball.metric isa Mahalanobis ? oneunit(r) : r
end
"""
@@ -106,7 +108,7 @@ end
Tells whether or not the metric `ball` is isotropic,
i.e. if all its radii are equal.
"""
-isisotropic(ball::MetricBall) = length(unique(ball.radii)) == 1
+isisotropic(ball::MetricBall) = allequal(ball.radii)
function *(α::Real, ball::MetricBall)
if ball.metric isa Mahalanobis
@@ -118,7 +120,7 @@ end
function Base.show(io::IO, ball::MetricBall)
n = length(ball.radii)
- r = n > 1 ? Tuple(ball.radii) : first(ball.radii)
+ r = n > 1 ? ball.radii : first(ball.radii)
m = nameof(typeof(ball.metric))
print(io, "MetricBall($r, $m)")
end
diff --git a/src/neighborsearch.jl b/src/neighborsearch.jl
index 6bc08b1a8..ae0f22ae3 100644
--- a/src/neighborsearch.jl
+++ b/src/neighborsearch.jl
@@ -10,60 +10,68 @@ A method for searching neighbors given a reference point.
abstract type NeighborSearchMethod end
"""
- search!(neighbors, pₒ, method; mask=nothing)
+ search(pₒ, method, mask=nothing)
-Update `neighbors` of point `pₒ` using `method` and return
-number of neighbors found. Optionally, specify a `mask` for
-all indices of the domain.
+Return neighbors of point `pₒ` using `method`. Optionally,
+specify a `mask` for all indices of the domain.
"""
-function search! end
+function search end
"""
- searchdists!(neighbors, distances, pₒ, method; mask=nothing)
+ BoundedNeighborSearchMethod
-Update `neighbors` and `distances` of point `pₒ` using `method`
-and return number of neighbors found. Optionally, specify a
-`mask` for all indices of the domain.
+A method for searching neighbors with the property that the number of neighbors
+is bounded above by a known constant (e.g. k-nearest neighbors).
"""
-function searchdists! end
+abstract type BoundedNeighborSearchMethod <: NeighborSearchMethod end
"""
- search(pₒ, method, mask=nothing)
+ maxneighbors(method)
-Return neighbors of point `pₒ` using `method`. Optionally,
-specify a `mask` for all indices of the domain.
+Return the maximum number of neighbors obtained with bounded search `method`.
+
+See [`BoundedNeighborSearchMethod`](@ref) for additional details.
"""
-function search end
+function maxneighbors end
"""
- searchdists(pₒ, method, mask=nothing)
+ search!(neighbors, pₒ, method; mask=nothing)
-Return neighbors and distances of point `pₒ` using `method`.
-Optionally, specify a `mask` for all indices of the domain.
+Update `neighbors` of point `pₒ` using bounded search `method` and return
+number of neighbors found. Optionally, specify a `mask` for all indices of
+the domain.
+
+See [`BoundedNeighborSearchMethod`](@ref) for additional details.
"""
-function searchdists end
+function search! end
"""
- BoundedNeighborSearchMethod
+ searchdists!(neighbors, distances, pₒ, method; mask=nothing)
-A method for searching neighbors with the property that the number of neighbors
-is bounded above by a known constant (e.g. k-nearest neighbors).
+Update `neighbors` and `distances` of point `pₒ` using bounded search `method`
+and return number of neighbors found. Optionally, specify a `mask` for all
+indices of the domain.
+
+See [`BoundedNeighborSearchMethod`](@ref) for additional details.
"""
-abstract type BoundedNeighborSearchMethod <: NeighborSearchMethod end
+function searchdists! end
"""
- maxneighbors(method)
+ searchdists(pₒ, method, mask=nothing)
+
+Return neighbors and distances of point `pₒ` using bounded search `method`.
+Optionally, specify a `mask` for all indices of the domain.
-Return the maximum number of neighbors obtained with `method`.
+See [`BoundedNeighborSearchMethod`](@ref) for additional details.
"""
-function maxneighbors end
+function searchdists end
# ----------
# FALLBACKS
# ----------
function search!(neighbors, pₒ::Point, method::BoundedNeighborSearchMethod; mask=nothing)
- distances = Vector{coordtype(pₒ)}(undef, maxneighbors(method))
+ distances = Vector{lentype(pₒ)}(undef, maxneighbors(method))
searchdists!(neighbors, distances, pₒ, method; mask)
end
@@ -75,7 +83,7 @@ end
function searchdists(pₒ::Point, method::BoundedNeighborSearchMethod; mask=nothing)
neighbors = Vector{Int}(undef, maxneighbors(method))
- distances = Vector{coordtype(pₒ)}(undef, maxneighbors(method))
+ distances = Vector{lentype(pₒ)}(undef, maxneighbors(method))
nneigh = searchdists!(neighbors, distances, pₒ, method; mask)
view(neighbors, 1:nneigh), view(distances, 1:nneigh)
end
@@ -87,3 +95,12 @@ end
include("neighborsearch/ball.jl")
include("neighborsearch/knearest.jl")
include("neighborsearch/kball.jl")
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+# raw coordinates of point as SVector
+# needed because NearestNeighbors.jl only accepts vectors
+_rawcoords(p::Point) = _rawcoords(coords(p))
+_rawcoords(c::CRS) = SVector(CoordRefSystems.raw(c))
diff --git a/src/neighborsearch/ball.jl b/src/neighborsearch/ball.jl
index a3497b70b..f967cfd86 100644
--- a/src/neighborsearch/ball.jl
+++ b/src/neighborsearch/ball.jl
@@ -5,7 +5,9 @@
"""
BallSearch(domain, ball)
-A method for searching neighbors in `domain` inside `ball`.
+A method for searching neighbors in `domain` inside metric `ball`.
+
+See [`MetricBall`](@ref) for additional details.
"""
struct BallSearch{D<:Domain,B<:MetricBall,T} <: NeighborSearchMethod
# input fields
@@ -18,7 +20,7 @@ end
function BallSearch(domain::D, ball::B) where {D<:Domain,B<:MetricBall}
m = metric(ball)
- xs = [coordinates(centroid(domain, i)) for i in 1:nelements(domain)]
+ xs = [_rawcoords(centroid(domain, i)) for i in 1:nelements(domain)]
tree = m isa MinkowskiMetric ? KDTree(xs, m) : BallTree(xs, m)
BallSearch{D,B,typeof(tree)}(domain, ball, tree)
end
@@ -26,10 +28,17 @@ end
BallSearch(geoms, ball) = BallSearch(GeometrySet(geoms), ball)
function search(pₒ::Point, method::BallSearch; mask=nothing)
+ C = crs(method.domain)
+ u = unit(lentype(method.domain))
tree = method.tree
- dmax = radius(method.ball)
- inds = inrange(tree, coordinates(pₒ), dmax)
+ # adjust unit of query radius
+ r = ustrip(u, radius(method.ball))
+
+ # adjust CRS of query point
+ x = _rawcoords(convert(C, coords(pₒ)))
+
+ inds = inrange(tree, x, r)
if isnothing(mask)
inds
diff --git a/src/neighborsearch/kball.jl b/src/neighborsearch/kball.jl
index 69ad9c4ab..53c0a3e4f 100644
--- a/src/neighborsearch/kball.jl
+++ b/src/neighborsearch/kball.jl
@@ -6,7 +6,9 @@
KBallSearch(domain, k, ball)
A method that searches `k` nearest neighbors and then filters
-these neighbors using a norm `ball`.
+these neighbors using a metric `ball`.
+
+See [`MetricBall`](@ref) for additional details.
"""
struct KBallSearch{D<:Domain,B<:MetricBall,T} <: BoundedNeighborSearchMethod
# input fields
@@ -20,7 +22,7 @@ end
function KBallSearch(domain::D, k::Int, ball::B) where {D<:Domain,B<:MetricBall}
m = metric(ball)
- xs = [coordinates(centroid(domain, i)) for i in 1:nelements(domain)]
+ xs = [_rawcoords(centroid(domain, i)) for i in 1:nelements(domain)]
tree = m isa MinkowskiMetric ? KDTree(xs, m) : BallTree(xs, m)
KBallSearch{D,B,typeof(tree)}(domain, k, ball, tree)
end
@@ -30,14 +32,21 @@ KBallSearch(geoms, k, ball) = KBallSearch(GeometrySet(geoms), k, ball)
maxneighbors(method::KBallSearch) = method.k
function searchdists!(neighbors, distances, pₒ::Point, method::KBallSearch; mask=nothing)
+ C = crs(method.domain)
+ u = unit(lentype(method.domain))
tree = method.tree
- dmax = radius(method.ball)
k = method.k
- inds, dists = knn(tree, coordinates(pₒ), k, true)
+ # adjust unit of query radius
+ r = ustrip(u, radius(method.ball))
+
+ # adjust CRS of query point
+ x = _rawcoords(convert(C, coords(pₒ)))
+
+ inds, dists = knn(tree, x, k, true)
# keep neighbors inside ball
- keep = dists .≤ dmax
+ keep = dists .≤ r
# possibly mask some of the neighbors
isnothing(mask) || (keep .*= mask[inds])
@@ -47,7 +56,7 @@ function searchdists!(neighbors, distances, pₒ::Point, method::KBallSearch; ma
if keep[i]
nneigh += 1
neighbors[nneigh] = inds[i]
- distances[nneigh] = dists[i]
+ distances[nneigh] = dists[i] * u
end
end
diff --git a/src/neighborsearch/knearest.jl b/src/neighborsearch/knearest.jl
index e73a2e299..64f607a35 100644
--- a/src/neighborsearch/knearest.jl
+++ b/src/neighborsearch/knearest.jl
@@ -18,7 +18,7 @@ struct KNearestSearch{D<:Domain,T} <: BoundedNeighborSearchMethod
end
function KNearestSearch(domain::D, k::Int; metric=Euclidean()) where {D<:Domain}
- xs = [coordinates(centroid(domain, i)) for i in 1:nelements(domain)]
+ xs = [_rawcoords(centroid(domain, i)) for i in 1:nelements(domain)]
tree = metric isa MinkowskiMetric ? KDTree(xs, metric) : BallTree(xs, metric)
KNearestSearch{D,typeof(tree)}(domain, k, tree)
end
@@ -28,16 +28,21 @@ KNearestSearch(geoms, k; metric=Euclidean()) = KNearestSearch(GeometrySet(geoms)
maxneighbors(method::KNearestSearch) = method.k
function searchdists!(neighbors, distances, pₒ::Point, method::KNearestSearch; mask=nothing)
+ C = crs(method.domain)
+ u = unit(lentype(method.domain))
tree = method.tree
k = method.k
- inds, dists = knn(tree, coordinates(pₒ), k, true)
+ # adjust CRS of query point
+ x = _rawcoords(convert(C, coords(pₒ)))
+
+ inds, dists = knn(tree, x, k, true)
if isnothing(mask)
nneigh = k
@inbounds for i in 1:k
neighbors[i] = inds[i]
- distances[i] = dists[i]
+ distances[i] = dists[i] * u
end
else
nneigh = 0
@@ -45,7 +50,7 @@ function searchdists!(neighbors, distances, pₒ::Point, method::KNearestSearch;
if mask[inds[i]]
nneigh += 1
neighbors[nneigh] = inds[i]
- distances[nneigh] = dists[i]
+ distances[nneigh] = dists[i] * u
end
end
end
diff --git a/src/orientation.jl b/src/orientation.jl
index 11ee7c69b..7894cacab 100644
--- a/src/orientation.jl
+++ b/src/orientation.jl
@@ -14,74 +14,25 @@ Possible values are `CW` and `CCW`.
end
"""
- OrientationMethod
-
-A method for finding the orientation of rings and polygons.
-"""
-abstract type OrientationMethod end
-
-"""
- orientation(geom, [method])
+ orientation(geom)
Returns the orientation of the geometry `geom` as
either counter-clockwise (CCW) or clockwise (CW).
-
-Optionally, specify the orientation `method`.
-
-See also [`WindingOrientation`](@ref),
-[`TriangleOrientation`](@ref).
"""
function orientation end
-orientation(p::Polygon) = orientation(p, WindingOrientation())
-
-orientation(r::Ring) = orientation(r, WindingOrientation())
-
-function orientation(p::Polygon, method)
- o = [orientation(ring, method) for ring in rings(p)]
+function orientation(p::Polygon)
+ o = [orientation(ring) for ring in rings(p)]
hasholes(p) ? o : first(o)
end
-orientation(r::Ring{3}, method) = orientation(proj2D(r), method)
-
-"""
- WindingOrientation()
-
-A method for finding the orientatino of rings and polygons
-based on the winding number.
-
-## References
-
-* Balbes, R. and Siegel, J. 1990. [A robust method for calculating
- the simplicity and orientation of planar polygons]
- (https://www.sciencedirect.com/science/article/abs/pii/0167839691900198)
-"""
-struct WindingOrientation <: OrientationMethod end
-
-function orientation(r::Ring{2,T}, ::WindingOrientation) where {T}
- # pick any segment
- x1, x2 = r.vertices[1:2]
- x̄ = center(Segment(x1, x2))
- w = T(2π) * winding(x̄, r) - ∠(x1, x̄, x2)
- isapprox(w, T(π), atol=atol(T)) ? CCW : CW
-end
-
-"""
- TriangleOrientation()
-
-A method for finding the orientation of rings and polygons
-based on signed triangular areas.
-
-## References
-
-* Held, M. 1998. [FIST: Fast Industrial-Strength Triangulation of Polygons]
- (https://link.springer.com/article/10.1007/s00453-001-0028-4)
-"""
-struct TriangleOrientation <: OrientationMethod end
+orientation(r::Ring{𝔼{3}}) = orientation(proj2D(r))
-function orientation(r::Ring{2,T}, ::TriangleOrientation) where {T}
+function orientation(r::Ring)
+ ℒ = lentype(r)
v = vertices(r)
- Δ(i) = signarea(v[1], v[i], v[i + 1])
- a = mapreduce(Δ, +, 2:(length(v) - 1))
- a ≥ zero(T) ? CCW : CW
+ n = nvertices(r)
+ A(i) = signarea(flat(v[1]), flat(v[i]), flat(v[i + 1]))
+ Σ = sum(A, 2:(n - 1), init=zero(ℒ)^2)
+ Σ ≥ zero(Σ) ? CCW : CW
end
diff --git a/src/partitioning.jl b/src/partitioning.jl
index 447738640..1c848108c 100644
--- a/src/partitioning.jl
+++ b/src/partitioning.jl
@@ -70,11 +70,11 @@ function partitioninds(rng::AbstractRNG, domain::Domain, method::SPredicateParti
subsets = Vector{Int}[]
for i in randperm(rng, nelms)
p = centroid(domain, i)
- x = coordinates(p)
+ x = to(p)
inserted = false
for subset in subsets
q = centroid(domain, subset[1])
- y = coordinates(q)
+ y = to(q)
if method(x, y)
push!(subset, i)
inserted = true
diff --git a/src/partitioning/ball.jl b/src/partitioning/ball.jl
index 63fd90934..e6e26a04e 100644
--- a/src/partitioning/ball.jl
+++ b/src/partitioning/ball.jl
@@ -8,11 +8,14 @@
A method for partitioning spatial objects into balls of a given
`radius` using a `metric`.
"""
-struct BallPartition{T,M} <: SPredicatePartitionMethod
- radius::T
+struct BallPartition{ℒ<:Len,M} <: SPredicatePartitionMethod
+ radius::ℒ
metric::M
+ BallPartition(radius::ℒ, metric::M) where {ℒ<:Len,M} = new{float(ℒ),M}(radius, metric)
end
-BallPartition(radius::T; metric::M=Euclidean()) where {T,M} = BallPartition{T,M}(radius, metric)
+BallPartition(radius, metric) = BallPartition(addunit(radius, u"m"), metric)
+
+BallPartition(radius; metric=Euclidean()) = BallPartition(radius, metric)
(p::BallPartition)(x, y) = evaluate(p.metric, x, y) < p.radius
diff --git a/src/partitioning/bisectfraction.jl b/src/partitioning/bisectfraction.jl
index da93bbfea..4987a8583 100644
--- a/src/partitioning/bisectfraction.jl
+++ b/src/partitioning/bisectfraction.jl
@@ -9,41 +9,39 @@ A method for partitioning spatial objects into two half spaces
defined by a `normal` direction and a `fraction` of points.
The partition is returned within `maxiter` bisection iterations.
"""
-struct BisectFractionPartition{Dim,T} <: PartitionMethod
- normal::Vec{Dim,T}
+struct BisectFractionPartition{V<:Vec} <: PartitionMethod
+ normal::V
fraction::Float64
maxiter::Int
-
- function BisectFractionPartition{Dim,T}(normal, fraction, maxiter) where {Dim,T}
- new(normalize(normal), fraction, maxiter)
- end
+ BisectFractionPartition{V}(normal, fraction, maxiter) where {V<:Vec} = new(unormalize(normal), fraction, maxiter)
end
-BisectFractionPartition(normal::Vec{Dim,T}, fraction=0.5, maxiter=10) where {Dim,T} =
- BisectFractionPartition{Dim,T}(normal, fraction, maxiter)
+BisectFractionPartition(normal::V, fraction=0.5, maxiter=10) where {V<:Vec} =
+ BisectFractionPartition{V}(normal, fraction, maxiter)
-BisectFractionPartition(normal::NTuple{Dim,T}, fraction=0.5, maxiter=10) where {Dim,T} =
+BisectFractionPartition(normal::Tuple, fraction=0.5, maxiter=10) =
BisectFractionPartition(Vec(normal), fraction, maxiter)
function partitioninds(rng::AbstractRNG, domain::Domain, method::BisectFractionPartition)
+ u = unit(lentype(domain))
bbox = boundingbox(domain)
n = method.normal
f = method.fraction
- c = coordinates(center(bbox))
+ c = to(centroid(bbox))
d = diagonal(bbox)
# maximum number of bisections
maxiter = method.maxiter
iter = 0
- a = c - d / 2 * n
- b = c + d / 2 * n
+ a = c - d / 2u * n
+ b = c + d / 2u * n
subsets = Vector{Int}[]
metadata = Dict()
while iter < maxiter
m = (a + b) / 2
- bisectpoint = BisectPointPartition(n, Point(m))
+ bisectpoint = BisectPointPartition(n, withcrs(domain, m))
subsets, metadata = partitioninds(rng, domain, bisectpoint)
g = length(subsets[1]) / nelements(domain)
diff --git a/src/partitioning/bisectpoint.jl b/src/partitioning/bisectpoint.jl
index 23721f5ab..356c16ce6 100644
--- a/src/partitioning/bisectpoint.jl
+++ b/src/partitioning/bisectpoint.jl
@@ -8,18 +8,15 @@
A method for partitioning spatial objects into two half spaces
defined by a `normal` direction and a reference `point`.
"""
-struct BisectPointPartition{Dim,T} <: PartitionMethod
- normal::Vec{Dim,T}
- point::Point{Dim,T}
-
- function BisectPointPartition{Dim,T}(normal, point) where {Dim,T}
- new(normalize(normal), point)
- end
+struct BisectPointPartition{V<:Vec,P<:Point} <: PartitionMethod
+ normal::V
+ point::P
+ BisectPointPartition{V,P}(normal, point) where {V<:Vec,P<:Point} = new(unormalize(normal), point)
end
-BisectPointPartition(normal::Vec{Dim,T}, point::Point{Dim,T}) where {Dim,T} = BisectPointPartition{Dim,T}(normal, point)
+BisectPointPartition(normal::V, point::P) where {V<:Vec,P<:Point} = BisectPointPartition{V,P}(normal, point)
-BisectPointPartition(normal::NTuple{Dim,T}, point::NTuple{Dim,T}) where {Dim,T} =
+BisectPointPartition(normal::NTuple{Dim}, point::NTuple{Dim}) where {Dim} =
BisectPointPartition(Vec(normal), Point(point))
function partitioninds(::AbstractRNG, domain::Domain, method::BisectPointPartition)
@@ -29,7 +26,7 @@ function partitioninds(::AbstractRNG, domain::Domain, method::BisectPointPartiti
left, right = Int[], Int[]
for location in 1:nelements(domain)
pₒ = centroid(domain, location)
- if (pₒ - p) ⋅ n < zero(coordtype(domain))
+ if isnegative((pₒ - p) ⋅ n)
push!(left, location)
else
push!(right, location)
diff --git a/src/partitioning/block.jl b/src/partitioning/block.jl
index 698f4eaee..d931c08aa 100644
--- a/src/partitioning/block.jl
+++ b/src/partitioning/block.jl
@@ -12,12 +12,15 @@ Optionally, compute the `neighbors` of a block as the metadata.
Alternatively, specify the sides `side₁`, `side₂`, ..., `sideₙ`.
"""
-struct BlockPartition{S} <: PartitionMethod
- sides::S
+struct BlockPartition{Dim,ℒ<:Len} <: PartitionMethod
+ sides::NTuple{Dim,ℒ}
neighbors::Bool
+ BlockPartition(sides::NTuple{Dim,ℒ}, neighbors::Bool) where {Dim,ℒ<:Len} = new{Dim,float(ℒ)}(sides, neighbors)
end
-BlockPartition(sides; neighbors=false) = BlockPartition(sides, neighbors)
+BlockPartition(sides::NTuple{Dim,Len}; neighbors=false) where {Dim} = BlockPartition(promote(sides...), neighbors)
+
+BlockPartition(sides::Tuple; neighbors=false) = BlockPartition(addunit.(sides, u"m"), neighbors)
BlockPartition(sides...; neighbors=false) = BlockPartition(sides; neighbors=neighbors)
@@ -28,7 +31,7 @@ function partitioninds(::AbstractRNG, domain::Domain, method::BlockPartition)
bsides = sides(bbox)
Dim = length(bsides)
- @assert all(psides .≤ bsides) "invalid block sides"
+ assertion(all(psides .≤ bsides), "invalid block sides")
# bounding box properties
ce = centroid(bbox)
@@ -40,7 +43,7 @@ function partitioninds(::AbstractRNG, domain::Domain, method::BlockPartition)
nblocks = @. nleft + nright
# top left corner of first block
- start = coordinates(ce) .- nleft .* psides
+ start = to(ce) .- nleft .* psides
subsets = [Int[] for i in 1:prod(nblocks)]
@@ -48,7 +51,7 @@ function partitioninds(::AbstractRNG, domain::Domain, method::BlockPartition)
linear = LinearIndices(Dims(nblocks))
for j in 1:nelements(domain)
- coords = coordinates(centroid(domain, j))
+ coords = to(centroid(domain, j))
# find block coordinates
c = @. floor(Int, (coords - start) / psides) + 1
diff --git a/src/partitioning/direction.jl b/src/partitioning/direction.jl
index e21677ff4..6a84b44f6 100644
--- a/src/partitioning/direction.jl
+++ b/src/partitioning/direction.jl
@@ -3,26 +3,26 @@
# ------------------------------------------------------------------
"""
- DirectionPartition(direction; tol=1e-6)
+ DirectionPartition(direction; [tol])
A method for partitioning spatial objects along a given `direction`
with bandwidth tolerance `tol`.
"""
-struct DirectionPartition{Dim,T} <: SPredicatePartitionMethod
- direction::Vec{Dim,T}
- tol::Float64
-
- function DirectionPartition{Dim,T}(direction, tol) where {Dim,T}
- new(normalize(direction), tol)
- end
+struct DirectionPartition{V<:Vec,ℒ<:Len} <: SPredicatePartitionMethod
+ direction::V
+ tol::ℒ
+ DirectionPartition(direction::V, tol::ℒ) where {V<:Vec,ℒ<:Len} = new{V,float(ℒ)}(unormalize(direction), tol)
end
-DirectionPartition(direction::Vec{Dim,T}; tol=1e-6) where {Dim,T} = DirectionPartition{Dim,T}(direction, tol)
+DirectionPartition(direction::Vec, tol) = DirectionPartition(direction, addunit(tol, u"m"))
+
+DirectionPartition(direction::Vec; tol=atol(eltype(direction))) = DirectionPartition(direction, tol)
-DirectionPartition(direction::NTuple{Dim,T}; tol=1e-6) where {Dim,T} = DirectionPartition(Vec(direction), tol=tol)
+DirectionPartition(direction::Tuple; kwargs...) = DirectionPartition(Vec(direction); kwargs...)
function (p::DirectionPartition)(x, y)
δ = x - y
d = p.direction
- norm(δ - (δ ⋅ d) * d) < p.tol
+ k = ustrip(δ ⋅ d)
+ norm(δ - k * d) < p.tol
end
diff --git a/src/partitioning/fraction.jl b/src/partitioning/fraction.jl
index 6d33bd3e9..86535e907 100644
--- a/src/partitioning/fraction.jl
+++ b/src/partitioning/fraction.jl
@@ -13,7 +13,7 @@ struct FractionPartition <: PartitionMethod
shuffle::Bool
function FractionPartition(fraction, shuffle)
- @assert 0 < fraction < 1 "fraction must be in interval (0,1)"
+ assertion(0 < fraction < 1, "fraction must be in interval (0,1)")
new(fraction, shuffle)
end
end
diff --git a/src/partitioning/plane.jl b/src/partitioning/plane.jl
index caf57d52b..5e2399012 100644
--- a/src/partitioning/plane.jl
+++ b/src/partitioning/plane.jl
@@ -3,23 +3,22 @@
# ------------------------------------------------------------------
"""
- PlanePartition(normal; tol=1e-6)
+ PlanePartition(normal; [tol])
A method for partitioning spatial objects into a family of hyperplanes
defined by a `normal` direction. Two points `x` and `y` belong to the same
hyperplane when `(x - y) ⋅ normal < tol`.
"""
-struct PlanePartition{Dim,T} <: SPredicatePartitionMethod
- normal::Vec{Dim,T}
- tol::Float64
-
- function PlanePartition{Dim,T}(normal, tol) where {Dim,T}
- new(normalize(normal), tol)
- end
+struct PlanePartition{V<:Vec,ℒ<:Len} <: SPredicatePartitionMethod
+ normal::V
+ tol::ℒ
+ PlanePartition(normal::V, tol::ℒ) where {V<:Vec,ℒ<:Len} = new{V,float(ℒ)}(unormalize(normal), tol)
end
-PlanePartition(normal::Vec{Dim,T}; tol=1e-6) where {Dim,T} = PlanePartition{Dim,T}(normal, tol)
+PlanePartition(normal::Vec, tol) = PlanePartition(normal, addunit(tol, u"m"))
+
+PlanePartition(normal::Vec; tol=atol(eltype(normal))) = PlanePartition(normal, tol)
-PlanePartition(normal::NTuple{Dim,T}; tol=1e-6) where {Dim,T} = PlanePartition(Vec(normal), tol=tol)
+PlanePartition(normal::Tuple; kwargs...) = PlanePartition(Vec(normal); kwargs...)
-(p::PlanePartition)(x, y) = abs((x - y) ⋅ p.normal) < p.tol
+(p::PlanePartition)(x, y) = abs(udot(x - y, p.normal)) < p.tol
diff --git a/src/partitioning/uniform.jl b/src/partitioning/uniform.jl
index a9dd0c5d1..4260d332a 100644
--- a/src/partitioning/uniform.jl
+++ b/src/partitioning/uniform.jl
@@ -20,7 +20,7 @@ function partitioninds(rng::AbstractRNG, domain::Domain, method::UniformPartitio
n = nelements(domain)
k = method.k
- @assert k ≤ n "number of subsets must be smaller than number of points"
+ assertion(k ≤ n, "number of subsets must be smaller than number of points")
inds = method.shuffle ? shuffle(rng, 1:n) : collect(1:n)
subsets = collect(Iterators.partition(inds, n ÷ k))
diff --git a/src/pointification.jl b/src/pointification.jl
index 1b7bbca3b..bcccde16f 100644
--- a/src/pointification.jl
+++ b/src/pointification.jl
@@ -16,10 +16,13 @@ function pointify end
pointify(p::Primitive) = pointify(boundary(p))
-pointify(p::Polytope) = collect(vertices(p))
+pointify(p::Polytope) = collect(eachvertex(p))
pointify(m::Multi) = pointify(parent(m))
+pointify(g::TransformedGeometry) =
+ hasdistortedboundary(g) ? pointify(discretize(g)) : map(transform(g), pointify(parent(g)))
+
pointify(geoms) = mapreduce(pointify, vcat, geoms)
# ----------------
@@ -32,6 +35,8 @@ pointify(s::Sphere) = _rsample(s)
pointify(t::Torus) = _rsample(t)
+pointify(c::CylinderSurface) = _rsample(c)
+
pointify(p::PolyArea) = vertices(p)
pointify(r::Ring) = vertices(r)
diff --git a/src/polytopes/polyarea.jl b/src/polytopes/polyarea.jl
deleted file mode 100644
index cdf25fa72..000000000
--- a/src/polytopes/polyarea.jl
+++ /dev/null
@@ -1,119 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- PolyArea(outer; fix=true)
- PolyArea([outer, inner₁, inner₂, ..., innerₖ]; fix=true)
-
-A polygonal area with `outer` ring, and optional inner
-rings `inner₁`, `inner₂`, ..., `innerₖ`.
-
-Rings can be a vector of [`Point`](@ref) or a
-vector of tuples with coordinates for convenience,
-in which case the first point should *not* be repeated
-at the end of the vector.
-
-The option `fix` tries to correct issues with polygons
-in the real world, including issues with:
-
-* `orientation` - Most algorithms assume that the
- outer ring is oriented counter-clockwise (CCW) and
- that all inner rings are oriented clockwise (CW).
-
-* `degeneracy` - Sometimes data is shared with
- degenerate rings (e.g. only 2 vertices).
-"""
-struct PolyArea{Dim,T,R<:Ring{Dim,T}} <: Polygon{Dim,T}
- rings::Vector{R}
-
- function PolyArea{Dim,T,R}(rings; fix=true) where {Dim,T,R<:Ring{Dim,T}}
- if isempty(rings)
- throw(ArgumentError("cannot create PolyArea without rings"))
- end
-
- if fix
- outer = rings[begin]
- inners = length(rings) > 1 ? rings[(begin + 1):end] : R[]
-
- # fix orientation
- ofix(r, o) = orientation(r) == o ? r : reverse(r)
- outer = ofix(outer, CCW)
- inners = ofix.(inners, CW)
-
- # fix degeneracy
- if nvertices(outer) == 2
- v = vertices(outer)
- A, B = v[1], v[2]
- M = center(Segment(A, B))
- outer = Ring(A, M, B)
- end
- inners = filter(r -> nvertices(r) > 2, inners)
-
- rings = [outer; inners]
- end
-
- new(rings)
- end
-end
-
-PolyArea(rings::AbstractVector{R}; fix=true) where {Dim,T,R<:Ring{Dim,T}} = PolyArea{Dim,T,R}(rings; fix)
-
-PolyArea(vertices::AbstractVector{<:AbstractVector}; fix=true) = PolyArea([Ring(v) for v in vertices]; fix)
-
-PolyArea(outer::Ring; fix=true) = PolyArea([outer]; fix)
-
-PolyArea(outer::AbstractVector; fix=true) = PolyArea(Ring(outer); fix)
-
-PolyArea(outer...; fix=true) = PolyArea(collect(outer); fix)
-
-==(p₁::PolyArea, p₂::PolyArea) = p₁.rings == p₂.rings
-
-function Base.isapprox(p₁::PolyArea, p₂::PolyArea; kwargs...)
- length(p₁.rings) ≠ length(p₂.rings) && return false
- all(isapprox(r₁, r₂; kwargs...) for (r₁, r₂) in zip(p₁.rings, p₂.rings))
-end
-
-vertices(p::PolyArea) = mapreduce(vertices, vcat, p.rings)
-
-nvertices(p::PolyArea) = mapreduce(nvertices, +, p.rings)
-
-centroid(p::PolyArea) = centroid(first(p.rings))
-
-rings(p::PolyArea) = p.rings
-
-function Base.unique!(p::PolyArea)
- foreach(unique!, p.rings)
- inds = findall(r -> nvertices(r) ≤ 2, p.rings)
- setdiff!(inds, 1) # don't remove outer ring
- isempty(inds) || deleteat!(p.rings, inds)
- p
-end
-
-function Base.show(io::IO, p::PolyArea)
- rings = p.rings
- print(io, "PolyArea(")
- if length(rings) == 1
- r = first(rings)
- printverts(io, vertices(r))
- else
- nverts = nvertices.(rings)
- join(io, ("$n-Ring" for n in nverts), ", ")
- end
- print(io, ")")
-end
-
-function Base.show(io::IO, ::MIME"text/plain", p::PolyArea{Dim,T}) where {Dim,T}
- rings = p.rings
- println(io, "PolyArea{$Dim,$T}")
- println(io, " outer")
- print(io, " └─ $(rings[1])")
- if length(rings) > 1
- println(io)
- println(io, " inner")
- printelms(io, @view(rings[2:end]), " ")
- end
-end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{<:PolyArea{Dim,T}}) where {Dim,T} =
- PolyArea(rand(rng, Ring{Dim,T}))
diff --git a/src/polytopes/ring.jl b/src/polytopes/ring.jl
deleted file mode 100644
index 30f25a89b..000000000
--- a/src/polytopes/ring.jl
+++ /dev/null
@@ -1,71 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Ring(p1, p2, ..., pn)
-
-A closed polygonal chain from a sequence of points `p1`, `p2`, ..., `pn`.
-
-See also [`Chain`](@ref) and [`Rope`](@ref).
-"""
-struct Ring{Dim,T,V<:CircularVector{Point{Dim,T}}} <: Chain{Dim,T}
- vertices::V
-
- function Ring{Dim,T,V}(vertices) where {Dim,T,V}
- if first(vertices) == last(vertices) && length(vertices) ≥ 2
- throw(ArgumentError("""
- First and last vertices of `Ring` constructor must be different
- in the latest version of Meshes.jl. The type itself now holds
- this connectivity information.
- """))
- end
- new(vertices)
- end
-end
-
-Ring(vertices::CircularVector{Point{Dim,T}}) where {Dim,T} = Ring{Dim,T,typeof(vertices)}(vertices)
-Ring(vertices::Tuple...) = Ring([Point(v) for v in vertices])
-Ring(vertices::Point{Dim,T}...) where {Dim,T} = Ring(collect(vertices))
-Ring(vertices::AbstractVector{<:Tuple}) = Ring(Point.(vertices))
-Ring(vertices::AbstractVector{Point{Dim,T}}) where {Dim,T} = Ring(CircularVector(vertices))
-
-nvertices(r::Ring) = length(r.vertices)
-
-==(r₁::Ring, r₂::Ring) = r₁.vertices == r₂.vertices
-
-function Base.isapprox(r₁::Ring, r₂::Ring; kwargs...)
- nvertices(r₁) ≠ nvertices(r₂) && return false
- all(isapprox(v₁, v₂; kwargs...) for (v₁, v₂) in zip(r₁.vertices, r₂.vertices))
-end
-
-Base.close(r::Ring) = r
-
-# call `open` again to avoid issues in case of nested CircularVector
-Base.open(r::Ring) = open(Rope(parent(r.vertices)))
-
-# do not change which vertex comes first for closed chains
-Base.reverse!(r::Ring) = (reverse!(@view r.vertices[(begin + 1):end]); r)
-
-function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{<:Ring{Dim,T}}) where {Dim,T}
- v = rand(rng, Point{Dim,T}, rand(3:50))
- while first(v) == last(v)
- v = rand(rng, Point{Dim,T}, rand(3:50))
- end
- Ring(v)
-end
-
-"""
- innerangles(ring)
-
-Return inner angles of the `ring`. Inner
-angles are always positive, and unlike
-`angles` they can be greater than `π`.
-"""
-function innerangles(r::Ring{2,T}) where {T}
- # correct sign of angles in case orientation is CW
- θs = orientation(r) == CW ? -angles(r) : angles(r)
- [θ > 0 ? 2 * T(π) - θ : -θ for θ in θs]
-end
-
-innerangles(r::Ring{3}) = innerangles(Ring(proj2D(vertices(r))))
diff --git a/src/predicates.jl b/src/predicates.jl
index ddce13b0b..87ff25075 100644
--- a/src/predicates.jl
+++ b/src/predicates.jl
@@ -15,6 +15,7 @@ include("predicates/hasholes.jl")
include("predicates/in.jl")
include("predicates/issubset.jl")
include("predicates/intersects.jl")
+include("predicates/ordering.jl")
# other predicates
include("predicates/iscollinear.jl")
diff --git a/src/predicates/in.jl b/src/predicates/in.jl
index 580613f86..e170111ba 100644
--- a/src/predicates/in.jl
+++ b/src/predicates/in.jl
@@ -7,19 +7,20 @@
Tells whether or not the `point` is in the `geometry`.
"""
-Base.in(p::Point, g::Geometry) = sideof(p, boundary(g)) == IN
+Base.in(p::Point, g::Geometry) = sideof(p, boundary(g)) != OUT
Base.in(p₁::Point, p₂::Point) = p₁ == p₂
-function Base.in(p::Point{Dim,T}, s::Segment{Dim,T}) where {Dim,T}
+function Base.in(p::Point, s::Segment)
# given collinear points (a, b, p), the point p intersects
# segment ab if and only if vectors satisfy 0 ≤ ap ⋅ ab ≤ ||ab||²
a, b = vertices(s)
ab, ap = b - a, p - a
- iscollinear(a, b, p) && zero(T) ≤ ab ⋅ ap ≤ ab ⋅ ab
+ iscollinear(a, b, p) && (abap = ab ⋅ ap;
+ isnonnegative(abap) && abap ≤ ab ⋅ ab)
end
-Base.in(p::Point, r::Ray) = p ∈ Line(r(0), r(1)) && (p - r(0)) ⋅ (r(1) - r(0)) ≥ 0
+Base.in(p::Point, r::Ray) = p ∈ Line(r(0), r(1)) && isnonnegative((p - r(0)) ⋅ (r(1) - r(0)))
function Base.in(p::Point, l::Line)
w = norm(l(1) - l(0))
@@ -29,65 +30,65 @@ end
Base.in(p::Point, c::Chain) = any(s -> p ∈ s, segments(c))
-Base.in(p::Point{3,T}, pl::Plane{T}) where {T} = isapprox(normal(pl) ⋅ (p - pl(0, 0)), zero(T), atol=atol(T))
+Base.in(p::Point, pl::Plane) = isapproxzero(udot(normal(pl), p - pl(0, 0)))
Base.in(p::Point, b::Box) = minimum(b) ⪯ p ⪯ maximum(b)
-function Base.in(p::Point{Dim,T}, b::Ball{Dim,T}) where {Dim,T}
+function Base.in(p::Point, b::Ball)
c = center(b)
r = radius(b)
s = norm(p - c)
- s < r || isapprox(s, r, atol=atol(T))
+ s < r || isapproxequal(s, r)
end
-function Base.in(p::Point{Dim,T}, s::Sphere{Dim,T}) where {Dim,T}
+function Base.in(p::Point, s::Sphere)
c = center(s)
r = radius(s)
s = norm(p - c)
- isapprox(s, r, atol=atol(T))
+ isapproxequal(s, r)
end
-function Base.in(p::Point{3,T}, d::Disk{T}) where {T}
+function Base.in(p::Point, d::Disk)
p ∉ plane(d) && return false
c = center(d)
r = radius(d)
s = norm(p - c)
- s < r || isapprox(s, r, atol=atol(T))
+ s < r || isapproxequal(s, r)
end
-function Base.in(p::Point{3,T}, c::Circle{T}) where {T}
+function Base.in(p::Point, c::Circle)
p ∉ plane(c) && return false
o = center(c)
r = radius(c)
s = norm(p - o)
- isapprox(s, r, atol=atol(T))
+ isapproxequal(s, r)
end
-function Base.in(p::Point{3}, c::Cone)
+function Base.in(p::Point, c::Cone)
a = apex(c)
b = center(base(c))
ax = a - b
- (a - p) ⋅ ax ≥ 0 || return false
- (b - p) ⋅ ax ≤ 0 || return false
+ isnonnegative((a - p) ⋅ ax) || return false
+ isnonpositive((b - p) ⋅ ax) || return false
∠(b, a, p) ≤ halfangle(c)
end
-function Base.in(p::Point{3}, c::Cylinder)
+function Base.in(p::Point, c::Cylinder)
b = bottom(c)(0, 0)
t = top(c)(0, 0)
r = radius(c)
a = t - b
- (p - b) ⋅ a ≥ 0 || return false
- (p - t) ⋅ a ≤ 0 || return false
+ isnonnegative((p - b) ⋅ a) || return false
+ isnonpositive((p - t) ⋅ a) || return false
norm((p - b) × a) / norm(a) ≤ r
end
-function Base.in(p::Point{3}, f::Frustum)
+function Base.in(p::Point, f::Frustum)
t = center(top(f))
b = center(bottom(f))
ax = b - t
- (p - t) ⋅ ax ≥ 0 || return false
- (p - b) ⋅ ax ≤ 0 || return false
+ isnonnegative((p - t) ⋅ ax) || return false
+ isnonpositive((p - b) ⋅ ax) || return false
# axial distance of p
ad = (p - t) ⋅ normalize(ax)
adrel = ad / norm(ax)
@@ -100,32 +101,29 @@ function Base.in(p::Point{3}, f::Frustum)
rd ≤ r
end
-function Base.in(p::Point{3,T}, t::Torus{T}) where {T}
+function Base.in(p::Point, t::Torus)
+ ℒ = lentype(p)
R, r = radii(t)
- c, n = center(t), normal(t)
- Q = rotation_between(n, Vec{3,T}(0, 0, 1))
+ c, n = center(t), direction(t)
+ Q = urotbetween(n, Vec(zero(ℒ), zero(ℒ), oneunit(ℒ)))
x, y, z = Q * (p - c)
(R - √(x^2 + y^2))^2 + z^2 ≤ r^2
end
-function Base.in(p::Point{2}, t::Triangle{2})
- # given coordinates
- a, b, c = vertices(t)
- x₁, y₁ = coordinates(a)
- x₂, y₂ = coordinates(b)
- x₃, y₃ = coordinates(c)
- x, y = coordinates(p)
-
- # barycentric coordinates
- λ₁ = ((y₂ - y₃) * (x - x₃) + (x₃ - x₂) * (y - y₃)) / ((y₂ - y₃) * (x₁ - x₃) + (x₃ - x₂) * (y₁ - y₃))
- λ₂ = ((y₃ - y₁) * (x - x₃) + (x₁ - x₃) * (y - y₃)) / ((y₂ - y₃) * (x₁ - x₃) + (x₃ - x₂) * (y₁ - y₃))
- λ₃ = 1 - λ₁ - λ₂
-
- # barycentric check
- 0 ≤ λ₁ ≤ 1 && 0 ≤ λ₂ ≤ 1 && 0 ≤ λ₃ ≤ 1
+function Base.in(point::Point, poly::Polygon{𝔼{2}})
+ r = rings(poly)
+ inside = sideof(point, first(r)) != OUT
+ if hasholes(poly)
+ outside = all(sideof(point, r[i]) == OUT for i in 2:length(r))
+ inside && outside
+ else
+ inside
+ end
end
-function Base.in(p::Point{3}, t::Triangle{3})
+Base.in(p::Point, poly::Polygon{𝔼{3}}) = any(Δ -> p ∈ Δ, simplexify(poly))
+
+function Base.in(p::Point, t::Triangle{𝔼{3}})
# given coordinates
a, b, c = vertices(t)
@@ -152,19 +150,6 @@ function Base.in(p::Point{3}, t::Triangle{3})
λ₂ ≥ 0 && λ₃ ≥ 0 && (λ₂ + λ₃) ≤ 1
end
-Base.in(p::Point, ngon::Ngon) = any(Δ -> p ∈ Δ, simplexify(ngon))
-
-function Base.in(p::Point, poly::PolyArea)
- r = rings(poly)
- inside = sideof(p, first(r)) == IN
- if hasholes(poly)
- outside = all(sideof(p, r[i]) == OUT for i in 2:length(r))
- inside && outside
- else
- inside
- end
-end
-
Base.in(p::Point, m::Multi) = any(g -> p ∈ g, parent(m))
"""
diff --git a/src/predicates/intersects.jl b/src/predicates/intersects.jl
index 791ccfbdd..1f35c7ec9 100644
--- a/src/predicates/intersects.jl
+++ b/src/predicates/intersects.jl
@@ -61,11 +61,14 @@ intersects(s::Segment, c::Chain) = intersects(c, s)
intersects(c₁::Chain, c₂::Chain) = intersects(segments(c₁), segments(c₂))
-intersects(c::Chain, g::Geometry) = any(∈(g), vertices(c)) || intersects(c, boundary(g))
+intersects(c::Chain, g::Geometry) = any(∈(g), eachvertex(c)) || intersects(c, boundary(g))
intersects(g::Geometry, c::Chain) = intersects(c, g)
-function intersects(g₁::Geometry{Dim,T}, g₂::Geometry{Dim,T}) where {Dim,T}
+function intersects(g₁::Geometry, g₂::Geometry)
+ Dim = embeddim(g₁)
+ ℒ = lentype(g₁)
+
# must have intersection of bounding boxes
intersects(boundingbox(g₁), boundingbox(g₂)) || return false
@@ -80,13 +83,13 @@ function intersects(g₁::Geometry{Dim,T}, g₂::Geometry{Dim,T}) where {Dim,T}
# initial direction
c₁, c₂ = centroid(g₁), centroid(g₂)
- d = c₁ ≈ c₂ ? rand(Vec{Dim,T}) : c₂ - c₁
+ d = c₁ ≈ c₂ ? rand(Vec{Dim,ℒ}) : c₂ - c₁
# first point in Minkowski difference
P = minkowskipoint(g₁, g₂, d)
# origin of coordinate system
- O = minkowskiorigin(Dim, T)
+ O = minkowskiorigin(Dim, ℒ)
# initialize simplex vertices
points = [P]
@@ -95,7 +98,7 @@ function intersects(g₁::Geometry{Dim,T}, g₂::Geometry{Dim,T}) where {Dim,T}
d = O - P
while true
P = minkowskipoint(g₁, g₂, d)
- if (P - O) ⋅ d < zero(T)
+ if isnegative((P - O) ⋅ d)
return false
end
push!(points, P)
@@ -106,7 +109,7 @@ function intersects(g₁::Geometry{Dim,T}, g₂::Geometry{Dim,T}) where {Dim,T}
end
"""
- gjk!(O::Point{Dim,T}, points) where {Dim,T}
+ gjk!(O::Point{Dim}, points) where {Dim}
Perform one iteration of the GJK algorithm.
@@ -119,27 +122,27 @@ make room for the next point. A complete simplex must have `Dim + 1` points.
See also [`intersects`](@ref).
"""
-function gjk! end
+gjk!(O::Point, points) = _gjk!(Val(embeddim(O)), O, points)
-function gjk!(O::Point{2,T}, points) where {T}
+function _gjk!(::Val{2}, O, points)
# line segment case
if length(points) == 2
B, A = points
AB = B - A
AO = O - A
- d = perpendicular(AB, AO)
+ d = perphint(AB, AO)
else
# triangle simplex case
C, B, A = points
AB = B - A
AC = C - A
AO = O - A
- ABᵀ = -perpendicular(AB, AC)
- ACᵀ = -perpendicular(AC, AB)
- if ABᵀ ⋅ AO > zero(T)
+ ABᵀ = -perphint(AB, AC)
+ ACᵀ = -perphint(AC, AB)
+ if ispositive(ABᵀ ⋅ AO)
popat!(points, 1) # pop C
d = ABᵀ
- elseif ACᵀ ⋅ AO > zero(T)
+ elseif ispositive(ACᵀ ⋅ AO)
popat!(points, 2) # pop B
d = ACᵀ
else
@@ -149,21 +152,21 @@ function gjk!(O::Point{2,T}, points) where {T}
d
end
-function gjk!(O::Point{3,T}, points) where {T}
+function _gjk!(::Val{3}, O, points)
# line segment case
if length(points) == 2
B, A = points
AB = B - A
AO = O - A
- d = perpendicular(AB, AO)
+ d = perphint(AB, AO)
elseif length(points) == 3
# triangle case
C, B, A = points
AB = B - A
AC = C - A
AO = O - A
- ABCᵀ = AB × AC
- if ABCᵀ ⋅ AO < 0
+ ABCᵀ = ucross(AB, AC)
+ if isnegative(ABCᵀ ⋅ AO)
points[1], points[2] = points[2], points[1]
ABCᵀ = -ABCᵀ
end
@@ -185,16 +188,16 @@ function gjk!(O::Point{3,T}, points) where {T}
AC = C - A
AD = D - A
AO = O - A
- ABCᵀ = AB × AC
- ADBᵀ = AD × AB
- ACDᵀ = AC × AD
- if ABCᵀ ⋅ AO > zero(T)
+ ABCᵀ = ucross(AB, AC)
+ ADBᵀ = ucross(AD, AB)
+ ACDᵀ = ucross(AC, AD)
+ if ispositive(ABCᵀ ⋅ AO)
popat!(points, 1) # pop D
d = ABCᵀ
- elseif ADBᵀ ⋅ AO > zero(T)
+ elseif ispositive(ADBᵀ ⋅ AO)
popat!(points, 2) # pop C
d = ADBᵀ
- elseif ACDᵀ ⋅ AO > zero(T)
+ elseif ispositive(ACDᵀ ⋅ AO)
popat!(points, 3) # pop B
d = ACDᵀ
else
@@ -243,19 +246,19 @@ intersects(m::Multi, c::Chain) = intersects(c, m)
# ------------------
# support point in Minkowski difference
-minkowskipoint(g₁::Geometry, g₂::Geometry, d) = Point(supportfun(g₁, d) - supportfun(g₂, -d))
+minkowskipoint(g₁::Geometry, g₂::Geometry, d) = withcrs(g₁, supportfun(g₁, d) - supportfun(g₂, -d))
# origin of coordinate system
-minkowskiorigin(Dim, T) = Point(ntuple(i -> zero(T), Dim))
+minkowskiorigin(Dim, ℒ) = Point(ntuple(i -> zero(ℒ), Dim))
# find a vector perpendicular to `v` using vector `d` as some direction hint
-# expect that `perpendicular(v, d) ⋅ d ≥ 0` or, in other words,
+# expect that `perphint(v, d) ⋅ d ≥ 0` or, in other words,
# that the angle between the result vector and `d` is less or equal than 90º
-function perpendicular(v::Vec{2,T}, d::Vec{2,T}) where {T}
- a = Vec(v[1], v[2], zero(T))
- b = Vec(d[1], d[2], zero(T))
- r = a × b × a
+function perphint(v::Vec{2,ℒ}, d::Vec{2,ℒ}) where {ℒ}
+ a = Vec(v[1], v[2], zero(ℒ))
+ b = Vec(d[1], d[2], zero(ℒ))
+ r = ucross(a, b, a)
Vec(r[1], r[2])
end
-perpendicular(v::Vec{3}, d::Vec{3}) = v × d × v
+perphint(v::Vec{3,ℒ}, d::Vec{3,ℒ}) where {ℒ} = ucross(v, d, v)
diff --git a/src/predicates/isclosed.jl b/src/predicates/isclosed.jl
index 4f55ed21f..fd683a1e7 100644
--- a/src/predicates/isclosed.jl
+++ b/src/predicates/isclosed.jl
@@ -16,11 +16,3 @@ isclosed(::Type{<:Segment}) = false
isclosed(::Type{<:Rope}) = false
isclosed(::Type{<:Ring}) = true
-
-"""
- isclosed(topology)
-
-Tells whether or not the `topology` is closed
-along each parametric dimension.
-"""
-isclosed(t::GridTopology) = .!isopen(t)
diff --git a/src/predicates/iscollinear.jl b/src/predicates/iscollinear.jl
index 5ac7ed381..3b5a61bb2 100644
--- a/src/predicates/iscollinear.jl
+++ b/src/predicates/iscollinear.jl
@@ -7,16 +7,17 @@
Tells whether or not the points `A`, `B` and `C` are collinear.
"""
-function iscollinear(A::Point{Dim,T}, B::Point{Dim,T}, C::Point{Dim,T}) where {Dim,T}
+function iscollinear(A::Point, B::Point, C::Point)
# points A, B, C are collinear if and only if the
# cross-products for segments AB and AC with respect
# to all possible pairs of coordinates are zero
+ Dim = embeddim(A)
AB, AC = B - A, C - A
result = true
for i in 1:Dim, j in (i + 1):Dim
u = Vec(AB[i], AB[j])
v = Vec(AC[i], AC[j])
- if !isapprox(u × v, zero(T), atol=atol(T)^2)
+ if !isapproxzero(u × v)
result = false
break
end
diff --git a/src/predicates/isconvex.jl b/src/predicates/isconvex.jl
index 7a5bd82ac..5bf015718 100644
--- a/src/predicates/isconvex.jl
+++ b/src/predicates/isconvex.jl
@@ -58,20 +58,21 @@ isconvex(::Triangle) = true
isconvex(::Tetrahedron) = true
-isconvex(p::Polygon{2}) = Set(vertices(convexhull(p))) == Set(vertices(p))
+isconvex(p::Polygon) = _isconvex(p, Val(embeddim(p)))
-isconvex(m::Multi{Dim,T}) where {Dim,T} = isapprox(measure(convexhull(m)), measure(m), atol=atol(T))
+_isconvex(p::Polygon, ::Val{2}) = Set(eachvertex(convexhull(p))) == Set(eachvertex(p))
+
+_isconvex(p::Polygon, ::Val{3}) = isconvex(proj2D(p))
+
+isconvex(m::Multi) = isapproxequal(measure(convexhull(m)), measure(m))
# --------------
# OPTIMIZATIONS
# --------------
-function isconvex(q::Quadrangle{2})
- v = vertices(q)
- d1 = Segment(v[1], v[3])
- d2 = Segment(v[2], v[4])
+function isconvex(q::Quadrangle)
+ A, B, C, D = vertices(q)
+ d1 = Segment(A, C)
+ d2 = Segment(B, D)
intersects(d1, d2)
end
-
-# temporary workaround in 3D
-isconvex(p::Polygon{3}) = isconvex(proj2D(p))
diff --git a/src/predicates/iscoplanar.jl b/src/predicates/iscoplanar.jl
index 5c9d36777..d1a0a3f55 100644
--- a/src/predicates/iscoplanar.jl
+++ b/src/predicates/iscoplanar.jl
@@ -7,7 +7,4 @@
Tells whether or not the points `A`, `B`, `C` and `D` are coplanar.
"""
-function iscoplanar(A::Point{3,T}, B::Point{3,T}, C::Point{3,T}, D::Point{3,T}) where {T}
- vol = volume(Tetrahedron(A, B, C, D))
- isapprox(vol, zero(T), atol=atol(T))
-end
+iscoplanar(A::Point, B::Point, C::Point, D::Point) = isapproxzero(volume(Tetrahedron(A, B, C, D)))
diff --git a/src/predicates/isparametrized.jl b/src/predicates/isparametrized.jl
index 6a49b889d..9a3816983 100644
--- a/src/predicates/isparametrized.jl
+++ b/src/predicates/isparametrized.jl
@@ -3,18 +3,15 @@
# ------------------------------------------------------------------
"""
- isparametrized(geometry)
+ isparametrized(object)
-Tells whether or not the `geometry` is parametrized,
-i.e. can be called as `geometry(u₁, u₂, ..., uₙ)` with
-local coordinates `(u₁, u₂, ..., uₙ) ∈ [0,1]ⁿ` where
-`n` is the parametric dimension.
+Tells whether or not the geometric `object` is parametrized, i.e.
+can be called as `object(u₁, u₂, ..., uₙ)` with local coordinates
+`(u₁, u₂, ..., uₙ) ∈ [0,1]ⁿ` where `n` is the parametric dimension.
See also [`paramdim`](@ref).
"""
-function isparametrized end
-
-isparametrized(g::Geometry) = isparametrized(typeof(g))
+isparametrized(g) = isparametrized(typeof(g))
isparametrized(::Type{<:Geometry}) = false
@@ -28,11 +25,15 @@ isparametrized(::Type{<:Plane}) = true
isparametrized(::Type{<:BezierCurve}) = true
-isparametrized(::Type{<:Box}) = true
+isparametrized(::Type{<:ParametrizedCurve}) = true
+
+isparametrized(::Type{<:Box{<:𝔼}}) = true
+
+isparametrized(::Type{<:Ball{<:𝔼}}) = true
-isparametrized(::Type{<:Ball}) = true
+isparametrized(::Type{<:Sphere{<:𝔼}}) = true
-isparametrized(::Type{<:Sphere}) = true
+isparametrized(::Type{<:Ellipsoid}) = true
isparametrized(::Type{<:Disk}) = true
@@ -42,8 +43,12 @@ isparametrized(::Type{<:Cylinder}) = true
isparametrized(::Type{<:CylinderSurface}) = true
+isparametrized(::Type{<:Cone}) = true
+
isparametrized(::Type{<:ConeSurface}) = true
+isparametrized(::Type{<:FrustumSurface}) = true
+
isparametrized(::Type{<:ParaboloidSurface}) = true
isparametrized(::Type{<:Torus}) = true
@@ -56,6 +61,6 @@ isparametrized(::Type{<:Tetrahedron}) = true
isparametrized(::Type{<:Hexahedron}) = true
-isparametrized(d::Domain) = isparametrized(typeof(d))
+isparametrized(::Type{<:TransformedGeometry{M,C,G}}) where {M,C,G} = isparametrized(G)
isparametrized(::Type{<:Domain}) = false
diff --git a/src/predicates/isperiodic.jl b/src/predicates/isperiodic.jl
index d0eeffd69..32c9327e7 100644
--- a/src/predicates/isperiodic.jl
+++ b/src/predicates/isperiodic.jl
@@ -18,36 +18,46 @@ isperiodic(::Type{<:Line}) = (false,)
isperiodic(b::BezierCurve) = (first(controls(b)) == last(controls(b)),)
+isperiodic(c::ParametrizedCurve) = (minimum(c) == maximum(c),)
+
isperiodic(::Type{<:Plane}) = (false, false)
-isperiodic(::Type{<:Box{Dim}}) where {Dim} = ntuple(i -> false, Dim)
+isperiodic(B::Type{<:Box}) = ntuple(i -> false, embeddim(B))
+
+isperiodic(B::Type{<:Ball}) = ntuple(i -> i == embeddim(B), embeddim(B))
-isperiodic(::Type{<:Ball{Dim}}) where {Dim} = ntuple(i -> i != 1, Dim)
+isperiodic(S::Type{<:Sphere}) = ntuple(i -> i == embeddim(S) - 1, embeddim(S) - 1)
-isperiodic(::Type{<:Sphere{Dim}}) where {Dim} = ntuple(i -> true, Dim - 1)
+isperiodic(::Type{<:Ellipsoid}) = (false, true)
isperiodic(::Type{<:Disk}) = (false, true)
isperiodic(::Type{<:Circle}) = (true,)
+isperiodic(::Type{<:Cylinder}) = (false, true, false)
+
isperiodic(::Type{<:CylinderSurface}) = (true, false)
isperiodic(::Type{<:ConeSurface}) = (true, false)
+isperiodic(::Type{<:FrustumSurface}) = (true, false)
+
isperiodic(::Type{<:ParaboloidSurface}) = (false, true)
isperiodic(::Type{<:Torus}) = (true, true)
-isperiodic(c::Type{<:Chain}) = (isclosed(c),)
+isperiodic(::Type{<:Rope}) = (false,)
+
+isperiodic(::Type{<:Ring}) = (true,)
isperiodic(::Type{<:Quadrangle}) = (false, false)
isperiodic(::Type{<:Hexahedron}) = (false, false, false)
"""
- isperiodic(topology)
+ isperiodic(grid)
-Tells whether or not the `topology` is periodic
+Tells whether or not the `grid` is periodic
along each parametric dimension.
"""
-isperiodic(t::GridTopology) = isclosed(t)
+isperiodic(g::Grid) = isperiodic(topology(g))
diff --git a/src/predicates/ordering.jl b/src/predicates/ordering.jl
new file mode 100644
index 000000000..51cbbc724
--- /dev/null
+++ b/src/predicates/ordering.jl
@@ -0,0 +1,103 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+# ----------------------
+# LEXICOGRAPHICAL ORDER
+# ----------------------
+
+"""
+ <(A::Point, B::Point)
+
+The lexicographical order of points `A` and `B` (`<`).
+
+`A < B` if the tuples of coordinates satisfy `(a₁, a₂, ...) < (b₁, b₂, ...)`.
+
+See
+"""
+<(A::Point, B::Point) = CoordRefSystems.values(coords(A)) < CoordRefSystems.values(coords(B))
+
+"""
+ >(A::Point, B::Point)
+
+The lexicographical order of points `A` and `B` (`>`).
+
+`A > B` if the tuples of coordinates satisfy `(a₁, a₂, ...) > (b₁, b₂, ...)`.
+
+See
+"""
+>(A::Point, B::Point) = CoordRefSystems.values(coords(A)) > CoordRefSystems.values(coords(B))
+
+"""
+ ≤(A::Point, B::Point)
+
+The lexicographical order of points `A` and `B` (`\\le`).
+
+`A ≤ B` if the tuples of coordinates satisfy `(a₁, a₂, ...) ≤ (b₁, b₂, ...)`.
+
+See
+"""
+≤(A::Point, B::Point) = CoordRefSystems.values(coords(A)) ≤ CoordRefSystems.values(coords(B))
+
+"""
+ ≥(A::Point, B::Point)
+
+The lexicographical order of points `A` and `B` (`\\ge`).
+
+`A ≥ B` if the tuples of coordinates satisfy `(a₁, a₂, ...) ≥ (b₁, b₂, ...)`.
+
+See
+"""
+≥(A::Point, B::Point) = CoordRefSystems.values(coords(A)) ≥ CoordRefSystems.values(coords(B))
+
+# --------------
+# PRODUCT ORDER
+# --------------
+
+"""
+ ≺(A::Point, B::Point)
+
+The product order of points `A` and `B` (`\\prec`).
+
+`A ≺ B` if `aᵢ < bᵢ` for all coordinates `aᵢ` and `bᵢ`.
+
+See
+"""
+≺(A::Point, B::Point) = all(x -> x > zero(x), B - A)
+≺(A::Point{🌐}, B::Point{🌐}) = _lat(A) < _lat(B)
+
+"""
+ ≻(A::Point, B::Point)
+
+The product order of points `A` and `B` (`\\succ`).
+
+`A ≻ B` if `aᵢ > bᵢ` for all coordinates `aᵢ` and `bᵢ`.
+
+See
+"""
+≻(A::Point, B::Point) = all(x -> x > zero(x), A - B)
+≻(A::Point{🌐}, B::Point{🌐}) = _lat(A) > _lat(B)
+
+"""
+ ⪯(A::Point, B::Point)
+
+The product order of points `A` and `B` (`\\preceq`).
+
+`A ⪯ B` if `aᵢ ≤ bᵢ` for all coordinates `aᵢ` and `bᵢ`.
+
+See
+"""
+⪯(A::Point, B::Point) = all(x -> x ≥ zero(x), B - A)
+⪯(A::Point{🌐}, B::Point{🌐}) = _lat(A) ≤ _lat(B)
+
+"""
+ ⪰(A::Point, B::Point)
+
+The product order of points `A` and `B` (`\\succeq`).
+
+`A ⪰ B` if `aᵢ ≥ bᵢ` for all coordinates `aᵢ` and `bᵢ`.
+
+See
+"""
+⪰(A::Point, B::Point) = all(x -> x ≥ zero(x), A - B)
+⪰(A::Point{🌐}, B::Point{🌐}) = _lat(A) ≥ _lat(B)
diff --git a/src/primitives/ball.jl b/src/primitives/ball.jl
deleted file mode 100644
index fb6a87e1f..000000000
--- a/src/primitives/ball.jl
+++ /dev/null
@@ -1,60 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Ball(center, radius)
-
-A ball with `center` and `radius`.
-
-See also [`Sphere`](@ref).
-"""
-struct Ball{Dim,T} <: Primitive{Dim,T}
- center::Point{Dim,T}
- radius::T
-end
-
-Ball(center::Point{Dim,T}, radius) where {Dim,T} = Ball(center, T(radius))
-
-Ball(center::Tuple, radius) = Ball(Point(center), radius)
-
-Ball(center::Point{Dim,T}) where {Dim,T} = Ball(center, T(1))
-
-Ball(center::Tuple) = Ball(Point(center))
-
-paramdim(::Type{<:Ball{Dim}}) where {Dim} = Dim
-
-center(b::Ball) = b.center
-
-radius(b::Ball) = b.radius
-
-function (b::Ball{2,T})(ρ, φ) where {T}
- if (ρ < 0 || ρ > 1) || (φ < 0 || φ > 1)
- throw(DomainError((ρ, φ), "b(ρ, φ) is not defined for ρ, φ outside [0, 1]²."))
- end
- c = b.center
- r = b.radius
- l = T(ρ) * r
- sφ, cφ = sincospi(2 * T(φ))
- x = l * cφ
- y = l * sφ
- c + Vec(x, y)
-end
-
-function (b::Ball{3,T})(ρ, θ, φ) where {T}
- if (ρ < 0 || ρ > 1) || (θ < 0 || θ > 1) || (φ < 0 || φ > 1)
- throw(DomainError((ρ, θ, φ), "b(ρ, θ, φ) is not defined for ρ, θ, φ outside [0, 1]³."))
- end
- c = b.center
- r = b.radius
- l = T(ρ) * r
- sθ, cθ = sincospi(T(θ))
- sφ, cφ = sincospi(2 * T(φ))
- x = l * sθ * cφ
- y = l * sθ * sφ
- z = l * cθ
- c + Vec(x, y, z)
-end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Ball{Dim,T}}) where {Dim,T} =
- Ball(rand(rng, Point{Dim,T}), rand(rng, T))
diff --git a/src/primitives/box.jl b/src/primitives/box.jl
deleted file mode 100644
index da039b42a..000000000
--- a/src/primitives/box.jl
+++ /dev/null
@@ -1,59 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Box(min, max)
-
-An axis-aligned box with `min` and `max` corners.
-See .
-
-## Examples
-
-```julia
-Box(Point(0, 0, 0), Point(1, 1, 1))
-Box((0, 0), (1, 1))
-```
-"""
-struct Box{Dim,T} <: Primitive{Dim,T}
- min::Point{Dim,T}
- max::Point{Dim,T}
-
- function Box{Dim,T}(min, max) where {Dim,T}
- @assert min ⪯ max "`min` must be less than or equal to `max`"
- new(min, max)
- end
-end
-
-Box(min::Point{Dim,T}, max::Point{Dim,T}) where {Dim,T} = Box{Dim,T}(min, max)
-
-Box(min::Tuple, max::Tuple) = Box(Point(min), Point(max))
-
-paramdim(::Type{<:Box{Dim}}) where {Dim} = Dim
-
-Base.minimum(b::Box) = b.min
-
-Base.maximum(b::Box) = b.max
-
-Base.extrema(b::Box) = b.min, b.max
-
-center(b::Box) = Point((coordinates(b.max) + coordinates(b.min)) / 2)
-
-diagonal(b::Box) = norm(b.max - b.min)
-
-sides(b::Box) = Tuple(b.max - b.min)
-
-Base.isapprox(b₁::Box, b₂::Box) = b₁.min ≈ b₂.min && b₁.max ≈ b₂.max
-
-function (b::Box{Dim,T})(uv...) where {Dim,T}
- if !all(x -> zero(T) ≤ x ≤ one(T), uv)
- throw(DomainError(uv, "b(u, v, ...) is not defined for u, v, ... outside [0, 1]ⁿ."))
- end
- b.min + uv .* (b.max - b.min)
-end
-
-function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Box{Dim,T}}) where {Dim,T}
- min = rand(rng, Point{Dim,T})
- max = min + rand(rng, Vec{Dim,T})
- Box(min, max)
-end
diff --git a/src/primitives/cone.jl b/src/primitives/cone.jl
deleted file mode 100644
index 21cd250a5..000000000
--- a/src/primitives/cone.jl
+++ /dev/null
@@ -1,31 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Cone(base, apex)
-
-A cone with `base` disk and `apex`.
-See .
-
-See also [`ConeSurface`](@ref).
-"""
-struct Cone{T} <: Primitive{3,T}
- base::Disk{T}
- apex::Point{3,T}
-end
-
-Cone(base::Disk, apex::Tuple) = Cone(base, Point(apex))
-
-paramdim(::Type{<:Cone}) = 3
-
-base(c::Cone) = c.base
-
-apex(c::Cone) = c.apex
-
-height(c::Cone) = norm(center(base(c)) - apex(c))
-
-halfangle(c::Cone) = atan(radius(base(c)), height(c))
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Cone{T}}) where {T} =
- Cone(rand(rng, Disk{T}), rand(rng, Point{3,T}))
diff --git a/src/primitives/conesurface.jl b/src/primitives/conesurface.jl
deleted file mode 100644
index 6d5d6845f..000000000
--- a/src/primitives/conesurface.jl
+++ /dev/null
@@ -1,41 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- ConeSurface(base, apex)
-
-A cone surface with `base` disk and `apex`.
-See .
-
-See also [`Cone`](@ref).
-"""
-struct ConeSurface{T} <: Primitive{3,T}
- base::Disk{T}
- apex::Point{3,T}
-end
-
-ConeSurface(base::Disk, apex::Tuple) = ConeSurface(base, Point(apex))
-
-paramdim(::Type{<:ConeSurface}) = 2
-
-base(c::ConeSurface) = c.base
-
-apex(c::ConeSurface) = c.apex
-
-function (c::ConeSurface{T})(φ, h) where {T}
- if (φ < 0 || φ > 1) || (h < 0 || h > 1)
- throw(DomainError((φ, h), "c(φ, h) is not defined for φ, h outside [0, 1]²."))
- end
- n = -normal(c.base)
- v = c.base(T(0), T(0)) - c.apex
- l = norm(v)
- θ = ∠(n, v)
- o = c.apex + T(h) * v
- r = T(h) * l * cos(θ)
- s = Circle(Plane(o, n), r)
- s(T(φ))
-end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{ConeSurface{T}}) where {T} =
- ConeSurface(rand(rng, Disk{T}), rand(rng, Point{3,T}))
diff --git a/src/primitives/cylindersurface.jl b/src/primitives/cylindersurface.jl
deleted file mode 100644
index aa0e28501..000000000
--- a/src/primitives/cylindersurface.jl
+++ /dev/null
@@ -1,117 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- CylinderSurface(bottom, top, radius)
-
-A circular cylinder surface embedded in R³ with given `radius`,
-delimited by `bottom` and `top` planes.
-
- CylinderSurface(start, finish, radius)
-
-Alternatively, construct a right circular cylinder surface with given `radius`
-along the segment with `start` and `finish` end points.
-
- CylinderSurface(start, finish)
-
-Or construct a right circular cylinder surface with unit radius along the segment
-with `start` and `finish` end points.
-
- CylinderSurface(radius)
-
-Finally, construct a right vertical circular cylinder surface with given `radius`.
-
-See .
-"""
-struct CylinderSurface{T} <: Primitive{3,T}
- bot::Plane{T}
- top::Plane{T}
- radius::T
-end
-
-function CylinderSurface(start::Point{3,T}, finish::Point{3,T}, radius) where {T}
- dir = finish - start
- bot = Plane(start, dir)
- top = Plane(finish, dir)
- CylinderSurface(bot, top, T(radius))
-end
-
-CylinderSurface(start::Tuple, finish::Tuple, radius) = CylinderSurface(Point(start), Point(finish), radius)
-
-CylinderSurface(start::Point{3,T}, finish::Point{3,T}) where {T} = CylinderSurface(start, finish, T(1))
-
-CylinderSurface(start::Tuple, finish::Tuple) = CylinderSurface(Point(start), Point(finish))
-
-CylinderSurface(radius::T) where {T} = CylinderSurface(Point(T(0), T(0), T(0)), Point(T(0), T(0), T(1)), radius)
-
-paramdim(::Type{<:CylinderSurface}) = 2
-
-radius(c::CylinderSurface) = c.radius
-
-bottom(c::CylinderSurface) = c.bot
-
-top(c::CylinderSurface) = c.top
-
-function center(c::CylinderSurface)
- a = coordinates(c.bot(0, 0))
- b = coordinates(c.top(0, 0))
- Point((a .+ b) ./ 2)
-end
-
-axis(c::CylinderSurface) = Line(c.bot(0, 0), c.top(0, 0))
-
-function isright(c::CylinderSurface{T}) where {T}
- # cylinder is right if axis
- # is aligned with plane normals
- a = axis(c)
- d = a(T(1)) - a(T(0))
- v = normal(c.bot)
- w = normal(c.top)
- isparallelv = isapprox(norm(d × v), zero(T), atol=atol(T))
- isparallelw = isapprox(norm(d × w), zero(T), atol=atol(T))
- isparallelv && isparallelw
-end
-
-Base.isapprox(c₁::CylinderSurface{T}, c₂::CylinderSurface{T}) where {T} =
- c₁.bot ≈ c₂.bot && c₁.top ≈ c₂.top && isapprox(c₁.radius, c₂.radius, atol=atol(T))
-
-function (c::CylinderSurface{T})(φ, z) where {T}
- if (φ < 0 || φ > 1) || (z < 0 || z > 1)
- throw(DomainError((φ, z), "c(φ, z) is not defined for φ, z outside [0, 1]²."))
- end
- t = top(c)
- b = bottom(c)
- r = radius(c)
- a = axis(c)
- d = a(T(1)) - a(T(0))
- h = norm(d)
- o = center(c)
-
- # rotation to align z axis with cylinder axis
- Q = rotation_between(d, Vec{3,T}(0, 0, 1))
-
- # new normals of planes in new rotated system
- nᵦ = Q * normal(b)
- nₜ = Q * normal(t)
-
- # given cylindrical coordinates (r*cos(φ), r*sin(φ), z) and the
- # equation of the plane, we can solve for z and find all points
- # along the ellipse obtained by intersection
- rsφ, rcφ = r .* sincospi(2 * T(φ))
- zᵦ = -h / 2 - (rcφ * nᵦ[1] + rsφ * nᵦ[2]) / nᵦ[3]
- zₜ = +h / 2 - (rcφ * nₜ[1] + rsφ * nₜ[2]) / nₜ[3]
- pᵦ = Point(rcφ, rsφ, zᵦ)
- pₜ = Point(rcφ, rsφ, zₜ)
-
- p = pᵦ + T(z) * (pₜ - pᵦ)
- o + Q' * coordinates(p)
-end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{CylinderSurface{T}}) where {T} =
- CylinderSurface(rand(rng, Plane{T}), rand(rng, Plane{T}), rand(rng, T))
-
-function hasintersectingplanes(c::CylinderSurface)
- x = c.bot ∩ c.top
- !isnothing(x) && evaluate(Euclidean(), axis(c), x) < c.radius
-end
diff --git a/src/primitives/frustum.jl b/src/primitives/frustum.jl
deleted file mode 100644
index 64cd06dc1..000000000
--- a/src/primitives/frustum.jl
+++ /dev/null
@@ -1,40 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Frustum(bot, top)
-
-A frustum (truncated cone) with `bot` and `top` disks.
-See .
-
-See also [`FrustumSurface`](@ref).
-"""
-struct Frustum{T} <: Primitive{3,T}
- bot::Disk{T}
- top::Disk{T}
-
- function Frustum{T}(bot, top) where {T}
- bn = normal(plane(bot))
- tn = normal(plane(top))
- @assert bn ⋅ tn ≈ 1 "Bottom and top plane must be parallel"
- @assert center(bot) ≉ center(top) "Bottom and top centers need to be distinct"
- new(bot, top)
- end
-end
-
-Frustum(bot::Disk{T}, top::Disk{T}) where {T} = Frustum{T}(bot, top)
-
-bottom(f::Frustum) = f.bot
-
-top(f::Frustum) = f.top
-
-height(f::Frustum) = norm(center(bottom(f)) - center(top(f)))
-
-function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Frustum{T}}) where {T}
- bottom = rand(rng, Disk{T})
- ax = normal(plane(bottom))
- topplane = Plane{T}(center(bottom) + rand(T) * ax, ax)
- top = Disk{T}(topplane, rand(T))
- Frustum(bottom, top)
-end
diff --git a/src/primitives/frustumsurface.jl b/src/primitives/frustumsurface.jl
deleted file mode 100644
index 9e7d7d9bc..000000000
--- a/src/primitives/frustumsurface.jl
+++ /dev/null
@@ -1,40 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- FrustumSurface(bot, top)
-
-A frustum (truncated cone) surface with `bot` and `top` disks.
-See .
-
-See also [`Frustum`](@ref).
-"""
-struct FrustumSurface{T} <: Primitive{3,T}
- bot::Disk{T}
- top::Disk{T}
-
- function FrustumSurface{T}(bot, top) where {T}
- bn = normal(plane(bot))
- tn = normal(plane(top))
- @assert bn ⋅ tn ≈ 1 "Bottom and top plane must be parallel"
- @assert center(bot) ≉ center(top) "Bottom and top centers need to be distinct"
- new(bot, top)
- end
-end
-
-FrustumSurface(bot::Disk{T}, top::Disk{T}) where {T} = FrustumSurface{T}(bot, top)
-
-bottom(f::FrustumSurface) = f.bot
-
-top(f::FrustumSurface) = f.top
-
-height(f::FrustumSurface) = norm(center(bottom(f)) - center(top(f)))
-
-function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{FrustumSurface{T}}) where {T}
- bottom = rand(rng, Disk{T})
- ax = normal(plane(bottom))
- topplane = Plane{T}(center(bottom) + rand(T) * ax, ax)
- top = Disk{T}(topplane, rand(T))
- FrustumSurface(bottom, top)
-end
diff --git a/src/primitives/point.jl b/src/primitives/point.jl
deleted file mode 100644
index 67a5a311f..000000000
--- a/src/primitives/point.jl
+++ /dev/null
@@ -1,150 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Point(x₁, x₂, ..., xₙ)
- Point((x₁, x₂, ..., xₙ))
- Point{Dim,T}(x₁, x₂, ..., xₙ)
- Point{Dim,T}((x₁, x₂, ..., xₙ))
-
-A point in `Dim`-dimensional space with coordinates of type `T`.
-
-The coordinates of the point are given with respect to the canonical
-Euclidean basis, and `Integer` coordinates are converted to `Float64`.
-
-## Examples
-
-```julia
-# 2D points
-A = Point(0.0, 1.0) # double precision as expected
-B = Point(0f0, 1f0) # single precision as expected
-C = Point(0, 0) # Integer is converted to Float64 by design
-D = Point2(0, 1) # explicitly ask for double precision
-E = Point2f(0, 1) # explicitly ask for single precision
-
-# 3D points
-F = Point(1.0, 2.0, 3.0) # double precision as expected
-G = Point(1f0, 2f0, 3f0) # single precision as expected
-H = Point(1, 2, 3) # Integer is converted to Float64 by design
-I = Point3(1, 2, 3) # explicitly ask for double precision
-J = Point3f(1, 2, 3) # explicitly ask for single precision
-```
-
-### Notes
-
-- Type aliases are `Point1`, `Point2`, `Point3`, `Point1f`, `Point2f`, `Point3f`
-- `Integer` coordinates are not supported because most geometric processing
- algorithms assume a continuous space. The conversion to `Float64` avoids
- `InexactError` and other unexpected results.
-"""
-struct Point{Dim,T} <: Primitive{Dim,T}
- coords::Vec{Dim,T}
- Point(coords::Vec{Dim,T}) where {Dim,T} = new{Dim,T}(coords)
-end
-
-# convenience constructors
-Point{Dim,T}(coords...) where {Dim,T} = Point(Vec{Dim,T}(coords...))
-Point(coords...) = Point(Vec(coords...))
-
-# coordinate type conversions
-Base.convert(::Type{Point{Dim,T}}, coords) where {Dim,T} = Point{Dim,T}(coords)
-Base.convert(::Type{Point{Dim,T}}, p::Point) where {Dim,T} = Point{Dim,T}(p.coords)
-Base.convert(::Type{Point}, coords) = Point{length(coords),eltype(coords)}(coords)
-
-# type aliases for convenience
-const Point1 = Point{1,Float64}
-const Point2 = Point{2,Float64}
-const Point3 = Point{3,Float64}
-const Point1f = Point{1,Float32}
-const Point2f = Point{2,Float32}
-const Point3f = Point{3,Float32}
-
-paramdim(::Type{Point{Dim,T}}) where {Dim,T} = 0
-
-center(p::Point) = p
-
-==(A::Point, B::Point) = A.coords == B.coords
-
-Base.isapprox(A::Point{Dim,T}, B::Point{Dim,T}; atol=atol(T), kwargs...) where {Dim,T} =
- isapprox(A.coords, B.coords; atol, kwargs...)
-
-"""
- coordinates(point)
-
-Return the coordinates of the `point` with respect to the
-canonical Euclidean basis.
-"""
-coordinates(A::Point) = A.coords
-
-"""
- -(A::Point, B::Point)
-
-Return the [`Vec`](@ref) associated with the direction
-from point `B` to point `A`.
-"""
--(A::Point, B::Point) = A.coords - B.coords
-
-"""
- +(A::Point, v::Vec)
- +(v::Vec, A::Point)
-
-Return the point at the end of the vector `v` placed
-at a reference (or start) point `A`.
-"""
-+(A::Point, v::Vec) = Point(A.coords + v)
-+(v::Vec, A::Point) = A + v
-
-"""
- -(A::Point, v::Vec)
- -(v::Vec, A::Point)
-
-Return the point at the end of the vector `-v` placed
-at a reference (or start) point `A`.
-"""
--(A::Point, v::Vec) = Point(A.coords - v)
--(v::Vec, A::Point) = A - v
-
-"""
- ⪯(A::Point, B::Point)
- ⪰(A::Point, B::Point)
- ≺(A::Point, B::Point)
- ≻(A::Point, B::Point)
-
-Generalized inequality for non-negative orthant Rⁿ₊.
-"""
-⪯(A::Point{Dim,T}, B::Point{Dim,T}) where {Dim,T} = all(≥(zero(T)), B - A)
-⪰(A::Point{Dim,T}, B::Point{Dim,T}) where {Dim,T} = all(≥(zero(T)), A - B)
-≺(A::Point{Dim,T}, B::Point{Dim,T}) where {Dim,T} = all(>(zero(T)), B - A)
-≻(A::Point{Dim,T}, B::Point{Dim,T}) where {Dim,T} = all(>(zero(T)), A - B)
-
-"""
- ∠(A, B, C)
-
-Angle ∠ABC between rays BA and BC.
-See .
-
-Uses the two-argument form of `atan` returning value in range [-π, π]
-in 2D and [0, π] in 3D.
-See .
-
-## Examples
-
-```julia
-∠(Point(1,0), Point(0,0), Point(0,1)) == π/2
-```
-"""
-∠(A::P, B::P, C::P) where {P<:Point{2}} = ∠(A - B, C - B)
-∠(A::P, B::P, C::P) where {P<:Point{3}} = ∠(A - B, C - B)
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Point{Dim,T}}) where {Dim,T} = Point(rand(rng, Vec{Dim,T}))
-
-function Base.show(io::IO, point::Point)
- if get(io, :compact, false)
- print(io, Tuple(point.coords))
- else
- print(io, "Point$(Tuple(point.coords))")
- end
-end
-
-Base.show(io::IO, ::MIME"text/plain", point::Point) = show(io, point)
diff --git a/src/primitives/torus.jl b/src/primitives/torus.jl
deleted file mode 100644
index bc6e6bd5e..000000000
--- a/src/primitives/torus.jl
+++ /dev/null
@@ -1,67 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
- Torus(center, normal, major, minor)
-
-A torus centered at `center` with axis of revolution directed by
-`normal` and with radii `major` and `minor`.
-
-"""
-struct Torus{T} <: Primitive{3,T}
- center::Point{3,T}
- normal::Vec{3,T}
- major::T
- minor::T
-end
-
-Torus(center::Point{3,T}, normal::Vec{3,T}, major, minor) where {T} = Torus(center, normal, T(major), T(minor))
-
-Torus(center::Tuple, normal::Tuple, major, minor) = Torus(Point(center), Vec(normal), major, minor)
-
-"""
- Torus(p1, p2, p3, minor)
-
-The torus whose centerline passes through points `p1`, `p2` and `p3` and with
-minor radius `minor`.
-"""
-function Torus(p1::Point{3,T}, p2::Point{3,T}, p3::Point{3,T}, minor) where {T}
- c = Circle(p1, p2, p3)
- p = Plane(p1, p2, p3)
- Torus(center(c), normal(p), radius(c), T(minor))
-end
-
-Torus(p1::Tuple, p2::Tuple, p3::Tuple, minor) = Torus(Point(p1), Point(p2), Point(p3), minor)
-
-paramdim(::Type{<:Torus}) = 2
-
-center(t::Torus) = t.center
-
-normal(t::Torus) = t.normal
-
-radii(t::Torus) = (t.major, t.minor)
-
-axis(t::Torus) = Line(t.center, t.center + t.normal)
-
-function (t::Torus{T})(u, v) where {T}
- if (u < 0 || u > 1) || (v < 0 || v > 1)
- throw(DomainError((u, v), "t(u, v) is not defined for u, v outside [0, 1]²."))
- end
-
- c, n⃗ = t.center, t.normal
- R, r = t.major, t.minor
-
- Q = rotation_between(Vec{3,T}(0, 0, 1), n⃗)
-
- θ = u * T(2π)
- ϕ = v * T(2π)
- x = (R + r * cos(θ)) * cos(ϕ)
- y = (R + r * cos(θ)) * sin(ϕ)
- z = r * sin(θ)
-
- c + Q * Vec{3,T}(x, y, z)
-end
-
-Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{Torus{T}}) where {T} =
- Torus(rand(rng, Point{3,T}), rand(rng, Vec{3,T}), rand(rng, T), rand(rng, T))
diff --git a/src/projecting.jl b/src/projecting.jl
index 2f65e1ffa..6f0e8c46c 100644
--- a/src/projecting.jl
+++ b/src/projecting.jl
@@ -5,8 +5,8 @@
"""
proj2D(geometry)
-Project 3D `geometry` onto a 2D plane of
-maximum variance using singular values.
+Project 3D `geometry` onto a 2D plane of maximum
+variance using singular value decomposition.
"""
function proj2D end
@@ -14,7 +14,7 @@ proj2D(r::Rope) = Ring(proj2D(vertices(r)))
proj2D(r::Ring) = Ring(proj2D(vertices(r)))
-proj2D(p::Ngon) = Ngon(proj2D(collect(vertices(p)))...)
+proj2D(p::Ngon) = Ngon(proj2D(vertices(p)))
proj2D(p::PolyArea) = PolyArea(proj2D.(rings(p)))
@@ -22,7 +22,7 @@ proj2D(p::PolyArea) = PolyArea(proj2D.(rings(p)))
# IMPLEMENTATION
# ---------------
-proj2D(points::AbstractVector{<:Point{3}}) = proj(points, svdbasis(points))
+proj2D(points::AbstractVector{<:Point}) = proj(points, svdbasis(points))
function proj(points, basis)
# retrieve basis
@@ -34,6 +34,8 @@ function proj(points, basis)
# project points
map(points) do p
d = p - c
- Point(d ⋅ u, d ⋅ v)
+ x = udot(d, u)
+ y = udot(d, v)
+ Point(x, y)
end
end
diff --git a/src/rand.jl b/src/rand.jl
new file mode 100644
index 000000000..734d14812
--- /dev/null
+++ b/src/rand.jl
@@ -0,0 +1,138 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ rand([rng], G, crs=Cartesian3D)
+
+Generate a random geometry of type `G` with CRS `crs`,
+optionally passing a random number generator `rng`.
+
+# Examples
+
+```julia
+rand(Point)
+rand(Triangle)
+rand(Point, crs=Cartesian2D)
+rand(Triangle, crs=LatLon)
+```
+"""
+Random.rand(G::Type{<:Geometry}; kwargs...) = rand(Random.default_rng(), G; kwargs...)
+Random.rand(rng::Random.AbstractRNG, G::Type{<:Geometry}; crs=Cartesian3D) = _rand(rng, G, crs)
+
+"""
+ rand([rng], G, n, crs=Cartesian3D)
+
+Generate a vector of `n` random geometries of type `G` with CRS `crs`,
+optionally passing a random number generator `rng`.
+
+# Examples
+
+```julia
+rand(Point, 10)
+rand(Triangle, 10)
+rand(Point, 10, crs=Cartesian2D)
+rand(Triangle, 10, crs=LatLon)
+```
+"""
+Random.rand(G::Type{<:Geometry}, n::Int; kwargs...) = rand(Random.default_rng(), G, n; kwargs...)
+Random.rand(rng::Random.AbstractRNG, G::Type{<:Geometry}, n::Int; kwargs...) = [rand(rng, G; kwargs...) for _ in 1:n]
+
+# ----------------
+# IMPLEMENTATIONS
+# ----------------
+
+_rand(rng::Random.AbstractRNG, ::Type{Point}, CRS) = Point(rand(rng, CRS))
+
+_rand(rng::Random.AbstractRNG, ::Type{Ray}, CRS) = Ray(_rand(rng, Point, CRS), _rvec(rng, CRS))
+
+_rand(rng::Random.AbstractRNG, ::Type{Line}, CRS) = Line(_rand(rng, Point, CRS), _rand(rng, Point, CRS))
+
+_rand(rng::Random.AbstractRNG, ::Type{BezierCurve}, CRS) = BezierCurve(_rvector(rng, CRS, 5))
+
+_rand(rng::Random.AbstractRNG, ::Type{Plane}, CRS) = Plane(_rand(rng, Point, CRS), _rvec(rng, CRS))
+
+function _rand(rng::Random.AbstractRNG, ::Type{Box}, CRS)
+ min = _rand(rng, Point, CRS)
+ max = min + _rvec(rng, CRS)
+ Box(min, max)
+end
+
+_rand(rng::Random.AbstractRNG, ::Type{Ball}, CRS) = Ball(_rand(rng, Point, CRS), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{Sphere}, CRS) = Sphere(_rand(rng, Point, CRS), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{Ellipsoid}, CRS) =
+ Ellipsoid((_rlen(rng), _rlen(rng), _rlen(rng)), _rand(rng, Point, CRS), rand(rng, QuatRotation))
+
+_rand(rng::Random.AbstractRNG, ::Type{Disk}, CRS) = Disk(_rand(rng, Plane, CRS), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{Circle}, CRS) = Circle(_rand(rng, Plane, CRS), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{Cylinder}, CRS) =
+ Cylinder(_rand(rng, Plane, CRS), _rand(rng, Plane, CRS), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{CylinderSurface}, CRS) =
+ CylinderSurface(_rand(rng, Plane, CRS), _rand(rng, Plane, CRS), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{Cone}, CRS) = Cone(_rand(rng, Disk, CRS), _rand(rng, Point, CRS))
+
+_rand(rng::Random.AbstractRNG, ::Type{ConeSurface}, CRS) = ConeSurface(_rand(rng, Disk, CRS), _rand(rng, Point, CRS))
+
+function _rand(rng::Random.AbstractRNG, ::Type{Frustum}, CRS)
+ bottom = _rand(rng, Disk, CRS)
+ ax = normal(plane(bottom))
+ topplane = Plane(center(bottom) + rand(rng) * ax, ax)
+ top = Disk(topplane, _rlen(rng))
+ Frustum(bottom, top)
+end
+
+function _rand(rng::Random.AbstractRNG, ::Type{FrustumSurface}, CRS)
+ bottom = _rand(rng, Disk, CRS)
+ ax = normal(plane(bottom))
+ topplane = Plane(center(bottom) + rand(rng) * ax, ax)
+ top = Disk(topplane, _rlen(rng))
+ FrustumSurface(bottom, top)
+end
+
+_rand(rng::Random.AbstractRNG, ::Type{ParaboloidSurface}, CRS) =
+ ParaboloidSurface(_rand(rng, Point, CRS), _rlen(rng), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{Torus}, CRS) =
+ Torus(_rand(rng, Point, CRS), _rvec(rng, CRS), _rlen(rng), _rlen(rng))
+
+_rand(rng::Random.AbstractRNG, ::Type{Segment}, CRS) = Segment(_rtuple(rng, CRS, 2))
+
+_rand(rng::Random.AbstractRNG, ::Type{Rope}, CRS) = Rope(_rvector(rng, CRS, rand(rng, 2:50)))
+
+function _rand(rng::Random.AbstractRNG, ::Type{Ring}, CRS)
+ v = _rvector(rng, CRS, rand(rng, 3:50))
+ while first(v) == last(v)
+ v = _rvector(rng, CRS, rand(rng, 3:50))
+ end
+ Ring(v)
+end
+
+_rand(rng::Random.AbstractRNG, ::Type{Ngon{N}}, CRS) where {N} = Ngon{N}(_rtuple(rng, CRS, N))
+
+_rand(rng::Random.AbstractRNG, ::Type{PolyArea}, CRS) = PolyArea(_rand(rng, Ring, CRS))
+
+_rand(rng::Random.AbstractRNG, ::Type{Tetrahedron}, CRS) = Tetrahedron(_rtuple(rng, CRS, 4))
+
+_rand(rng::Random.AbstractRNG, ::Type{Hexahedron}, CRS) = Hexahedron(_rtuple(rng, CRS, 8))
+
+_rand(rng::Random.AbstractRNG, ::Type{Pyramid}, CRS) = Pyramid(_rtuple(rng, CRS, 5))
+
+_rand(rng::Random.AbstractRNG, ::Type{Wedge}, CRS) = Wedge(_rtuple(rng, CRS, 6))
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+_rvector(rng::Random.AbstractRNG, CRS, n) = [_rand(rng, Point, CRS) for _ in 1:n]
+
+_rtuple(rng::Random.AbstractRNG, CRS, n) = ntuple(_ -> _rand(rng, Point, CRS), n)
+
+_rvec(rng::Random.AbstractRNG, CRS) = rand(rng, Vec{CoordRefSystems.ndims(CRS),Met{Float64}})
+
+_rlen(rng::Random.AbstractRNG) = rand(rng, Met{Float64})
diff --git a/src/refinement.jl b/src/refinement.jl
index 954f9b262..271a4e284 100644
--- a/src/refinement.jl
+++ b/src/refinement.jl
@@ -22,5 +22,6 @@ function refine end
include("refinement/tri.jl")
include("refinement/quad.jl")
+include("refinement/regular.jl")
include("refinement/catmullclark.jl")
include("refinement/trisubdivision.jl")
diff --git a/src/refinement/catmullclark.jl b/src/refinement/catmullclark.jl
index ba2dc54ce..bcf0fcf69 100644
--- a/src/refinement/catmullclark.jl
+++ b/src/refinement/catmullclark.jl
@@ -3,7 +3,7 @@
# ------------------------------------------------------------------
"""
- CatmullClark()
+ CatmullClarkRefinement()
Catmull-Clark refinement of polygonal meshes.
@@ -19,9 +19,9 @@ surface.
B-spline surfaces on arbitrary topological meshes]
(https://www.sciencedirect.com/science/article/abs/pii/0010448578901100)
"""
-struct CatmullClark <: RefinementMethod end
+struct CatmullClarkRefinement <: RefinementMethod end
-function refine(mesh, ::CatmullClark)
+function refine(mesh, ::CatmullClarkRefinement)
# retrieve geometry and topology
points = vertices(mesh)
connec = topology(mesh)
@@ -32,21 +32,21 @@ function refine(mesh, ::CatmullClark)
# add centroids of elements
∂₂₀ = Boundary{2,0}(t)
epts = map(1:nelements(t)) do elem
- ps = view(points, ∂₂₀(elem))
- cₒ = sum(coordinates, ps) / length(ps)
- Point(cₒ)
+ is = ∂₂₀(elem)
+ cₒ = sum(j -> to(points[j]), is) / length(is)
+ withcrs(mesh, cₒ)
end
# add midpoints of edges
∂₁₂ = Coboundary{1,2}(t)
∂₁₀ = Boundary{1,0}(t)
fpts = map(1:nfacets(t)) do edge
- ps = view(epts, ∂₁₂(edge))
- qs = view(points, ∂₁₀(edge))
- ∑p = sum(coordinates, ps)
- ∑q = sum(coordinates, qs)
- M = length(ps) + length(qs)
- Point((∑p + ∑q) / M)
+ is = ∂₁₂(edge)
+ js = ∂₁₀(edge)
+ ∑p = sum(i -> to(epts[i]), is)
+ ∑q = sum(j -> to(points[j]), js)
+ M = length(is) + length(js)
+ withcrs(mesh, (∑p + ∑q) / M)
end
# move original vertices
@@ -54,21 +54,18 @@ function refine(mesh, ::CatmullClark)
∂₀₀ = Adjacency{0}(t)
vpts = map(1:nvertices(t)) do u
# original point
- P = coordinates(points[u])
+ P = to(points[u])
# average of centroids
- ps = view(epts, ∂₀₂(u))
- F = sum(coordinates, ps) / length(ps)
+ is = ∂₀₂(u)
+ F = sum(i -> to(epts[i]), is) / length(is)
# average of midpoints
vs = ∂₀₀(u)
n = length(vs)
- R = sum(vs) do v
- uv = view(points, [u, v])
- sum(coordinates, uv) / 2
- end / n
+ R = sum(v -> to(points[u]) + to(points[v]), vs) / 2n
- Point((F + 2R + (n - 3)P) / n)
+ withcrs(mesh, (F + 2R + (n - 3)P) / n)
end
# new points in refined mesh
@@ -81,13 +78,15 @@ function refine(mesh, ::CatmullClark)
∂₂₁ = Boundary{2,1}(t)
newconnec = Connectivity{Quadrangle,4}[]
for elem in 1:nelements(t)
- verts = CircularVector(∂₂₀(elem))
- edges = CircularVector(∂₂₁(elem))
- for i in 1:length(edges)
+ verts = ∂₂₀(elem)
+ edges = ∂₂₁(elem)
+ nv = length(verts)
+ ne = length(edges)
+ for i in 1:ne
u = elem + offset₁
- v = edges[i] + offset₂
- w = verts[i + 1]
- z = edges[i + 1] + offset₂
+ v = edges[mod1(i, ne)] + offset₂
+ w = verts[mod1(i + 1, nv)]
+ z = edges[mod1(i + 1, ne)] + offset₂
quad = connect((u, v, w, z))
push!(newconnec, quad)
end
diff --git a/src/refinement/quad.jl b/src/refinement/quad.jl
index 7819f653c..8fc61c5e0 100644
--- a/src/refinement/quad.jl
+++ b/src/refinement/quad.jl
@@ -21,17 +21,15 @@ function refine(mesh, ::QuadRefinement)
# add centroids of elements
∂₂₀ = Boundary{2,0}(t)
epts = map(1:nelements(t)) do elem
- ps = view(points, ∂₂₀(elem))
- cₒ = sum(coordinates, ps) / length(ps)
- Point(cₒ)
+ is = ∂₂₀(elem)
+ coordmean(points[i] for i in is)
end
# add midpoints of edges
∂₁₀ = Boundary{1,0}(t)
fpts = map(1:nfacets(t)) do edge
- ps = view(points, ∂₁₀(edge))
- cₒ = sum(coordinates, ps) / length(ps)
- Point(cₒ)
+ is = ∂₁₀(edge)
+ coordmean(points[i] for i in is)
end
# original vertices
@@ -47,13 +45,15 @@ function refine(mesh, ::QuadRefinement)
∂₂₁ = Boundary{2,1}(t)
newconnec = Connectivity{Quadrangle,4}[]
for elem in 1:nelements(t)
- verts = CircularVector(∂₂₀(elem))
- edges = CircularVector(∂₂₁(elem))
- for i in 1:length(edges)
+ verts = ∂₂₀(elem)
+ edges = ∂₂₁(elem)
+ nv = length(verts)
+ ne = length(edges)
+ for i in 1:ne
u = elem + offset₁
- v = edges[i] + offset₂
- w = verts[i + 1]
- z = edges[i + 1] + offset₂
+ v = edges[mod1(i, ne)] + offset₂
+ w = verts[mod1(i + 1, nv)]
+ z = edges[mod1(i + 1, ne)] + offset₂
quad = connect((u, v, w, z))
push!(newconnec, quad)
end
diff --git a/src/refinement/regular.jl b/src/refinement/regular.jl
new file mode 100644
index 000000000..f272bf21b
--- /dev/null
+++ b/src/refinement/regular.jl
@@ -0,0 +1,110 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ RegularRefinement(f₁, f₂, ..., fₙ)
+
+Refine each dimension of the grid by given factors `f₁`, `f₂`, ..., `fₙ`.
+
+## Examples
+
+```julia
+refine(grid2D, RegularRefinement(2, 3))
+refine(grid3D, RegularRefinement(2, 3, 1))
+```
+"""
+struct RegularRefinement{N} <: RefinementMethod
+ factors::Dims{N}
+end
+
+RegularRefinement(factors::Vararg{Int,N}) where {N} = RegularRefinement(factors)
+
+function refine(grid::OrthoRegularGrid, method::RegularRefinement)
+ factors = fitdims(method.factors, paramdim(grid))
+ RegularGrid(minimum(grid), maximum(grid), dims=size(grid) .* factors)
+end
+
+function refine(grid::RectilinearGrid, method::RegularRefinement)
+ factors = fitdims(method.factors, paramdim(grid))
+ xyzₛ = xyz(grid)
+ xyzₜ = ntuple(i -> _refinedims(xyzₛ[i], factors[i]), paramdim(grid))
+ RectilinearGrid{manifold(grid),crs(grid)}(xyzₜ)
+end
+
+function refine(grid::OrthoStructuredGrid, method::RegularRefinement)
+ factors = fitdims(method.factors, paramdim(grid))
+ XYZ′ = _XYZ(grid, factors)
+ StructuredGrid{manifold(grid),crs(grid)}(XYZ′)
+end
+
+refine(grid::TransformedGrid, method::RegularRefinement) =
+ TransformedGrid(refine(parent(grid), method), transform(grid))
+
+function _refinedims(x, f)
+ x′ = mapreduce(vcat, 1:(length(x) - 1)) do i
+ range(x[i], x[i + 1], f + 1)[begin:(end - 1)]
+ end
+ push!(x′, last(x))
+ x′
+end
+
+_XYZ(grid::OrthoStructuredGrid, factors::Dims) = _XYZ(grid, Val(paramdim(grid)), factors)
+
+function _XYZ(grid::OrthoStructuredGrid, ::Val{2}, factors::Dims{2})
+ T = numtype(lentype(grid))
+ fᵢ, fⱼ = factors
+ sᵢ, sⱼ = size(grid)
+ us = 0:T(1 / fᵢ):1
+ vs = 0:T(1 / fⱼ):1
+ catᵢ(A...) = cat(A..., dims=Val(1))
+ catⱼ(A...) = cat(A..., dims=Val(2))
+
+ mat(quad) = [to(quad(u, v)) for u in us, v in vs]
+ M = [mat(grid[i, j]) for i in 1:sᵢ, j in 1:sⱼ]
+
+ C = mapreduce(catⱼ, 1:sⱼ) do j
+ Mⱼ = mapreduce(catᵢ, 1:sᵢ) do i
+ Mᵢⱼ = M[i, j]
+ i == sᵢ ? Mᵢⱼ : Mᵢⱼ[begin:(end - 1), :]
+ end
+ j == sⱼ ? Mⱼ : Mⱼ[:, begin:(end - 1)]
+ end
+
+ X = getindex.(C, 1)
+ Y = getindex.(C, 2)
+
+ (X, Y)
+end
+
+function _XYZ(grid::OrthoStructuredGrid, ::Val{3}, factors::Dims{3})
+ T = numtype(lentype(grid))
+ fᵢ, fⱼ, fₖ = factors
+ sᵢ, sⱼ, sₖ = size(grid)
+ us = 0:T(1 / fᵢ):1
+ vs = 0:T(1 / fⱼ):1
+ ws = 0:T(1 / fₖ):1
+ catᵢ(A...) = cat(A..., dims=Val(1))
+ catⱼ(A...) = cat(A..., dims=Val(2))
+ catₖ(A...) = cat(A..., dims=Val(3))
+
+ mat(hex) = [to(hex(u, v, w)) for u in us, v in vs, w in ws]
+ M = [mat(grid[i, j, k]) for i in 1:sᵢ, j in 1:sⱼ, k in 1:sₖ]
+
+ C = mapreduce(catₖ, 1:sₖ) do k
+ Mₖ = mapreduce(catⱼ, 1:sⱼ) do j
+ Mⱼₖ = mapreduce(catᵢ, 1:sᵢ) do i
+ Mᵢⱼₖ = M[i, j, k]
+ i == sᵢ ? Mᵢⱼₖ : Mᵢⱼₖ[begin:(end - 1), :, :]
+ end
+ j == sⱼ ? Mⱼₖ : Mⱼₖ[:, begin:(end - 1), :]
+ end
+ k == sₖ ? Mₖ : Mₖ[:, :, begin:(end - 1)]
+ end
+
+ X = getindex.(C, 1)
+ Y = getindex.(C, 2)
+ Z = getindex.(C, 3)
+
+ (X, Y, Z)
+end
diff --git a/src/refinement/tri.jl b/src/refinement/tri.jl
index 2bf4554fe..c03a772f5 100644
--- a/src/refinement/tri.jl
+++ b/src/refinement/tri.jl
@@ -3,52 +3,73 @@
# ------------------------------------------------------------------
"""
- TriRefinement()
+ TriRefinement([pred])
Refinement of polygonal meshes into triangles.
-A n-gon is subdivided into n triangles.
+A n-gon for which the predicate `pred` holds true
+is subdivided into n triangles. The method refines all
+n-gons if the `pred` is ommited.
"""
-struct TriRefinement <: RefinementMethod end
+struct TriRefinement{F} <: RefinementMethod
+ pred::F
+end
+
+TriRefinement() = TriRefinement(nothing)
-function refine(mesh, ::TriRefinement)
- @assert paramdim(mesh) == 2 "TriRefinement only defined for surface meshes"
+function refine(mesh, method::TriRefinement)
+ assertion(paramdim(mesh) == 2, "TriRefinement only defined for surface meshes")
(eltype(mesh) <: Triangle) || return simplexify(mesh)
# retrieve geometry and topology
points = vertices(mesh)
- connec = topology(mesh)
+ topo = topology(mesh)
- # convert to half-edge structure
- t = convert(HalfEdgeTopology, connec)
+ # indices to refine
+ rinds = if isnothing(method.pred)
+ 1:nelements(topo)
+ else
+ filter(i -> method.pred(mesh[i]), 1:nelements(topo))
+ end
+
+ # indices to preserve
+ pinds = setdiff(1:nelements(topo), rinds)
# add centroids of elements
- ∂₂₀ = Boundary{2,0}(t)
- epts = map(1:nelements(t)) do elem
- ps = view(points, ∂₂₀(elem))
- cₒ = sum(coordinates, ps) / length(ps)
- Point(cₒ)
+ ∂₂₀ = Boundary{2,0}(topo)
+ rpts = map(rinds) do elem
+ is = ∂₂₀(elem)
+ coordmean(points[i] for i in is)
end
# original vertices
vpts = points
# new points in refined mesh
- newpoints = [vpts; epts]
+ newpoints = [vpts; rpts]
+ # new connectivities in refined mesh
+ newconnec = Connectivity{Triangle,3}[]
+
+ # offset to new vertex indices
offset = length(vpts)
- # connect vertices into new triangles
- newconnec = Connectivity{Triangle,3}[]
- for elem in 1:nelements(t)
- verts = CircularVector(∂₂₀(elem))
- for i in 1:length(verts)
- u = elem + offset
- v = verts[i]
- w = verts[i + 1]
+ # connectivities of new triangles
+ for (i, elem) in enumerate(rinds)
+ verts = ∂₂₀(elem)
+ nv = length(verts)
+ for j in 1:nv
+ u = i + offset
+ v = verts[mod1(j, nv)]
+ w = verts[mod1(j + 1, nv)]
tri = connect((u, v, w))
push!(newconnec, tri)
end
end
+ # connectivities of preserved elements
+ for elem in pinds
+ push!(newconnec, element(topo, elem))
+ end
+
SimpleMesh(newpoints, newconnec)
end
diff --git a/src/refinement/trisubdivision.jl b/src/refinement/trisubdivision.jl
index 10822a1e1..f7d8692ca 100644
--- a/src/refinement/trisubdivision.jl
+++ b/src/refinement/trisubdivision.jl
@@ -3,21 +3,21 @@
# ------------------------------------------------------------------
"""
- TriSubdivision()
+ TriSubdivision()
Refinement of a mesh by preliminarly triangulating it if needed and
then subdividing each triangle into four triangles.
## References
-* Charles Loop. 1987. [Smooth subdivision surfaces based on
- triangles](https://charlesloop.com/thesis.pdf).
+* Charles Loop. 1987. [Smooth subdivision surfaces based on
+ triangles](https://charlesloop.com/thesis.pdf).
Master's thesis, University of Utah.
"""
struct TriSubdivision <: RefinementMethod end
function refine(mesh, ::TriSubdivision)
- @assert paramdim(mesh) == 2 "TriSubdivision only defined for surface meshes"
+ assertion(paramdim(mesh) == 2, "TriSubdivision only defined for surface meshes")
# triangulate mesh if necessary
tmesh = eltype(mesh) <: Triangle ? mesh : simplexify(mesh)
@@ -36,10 +36,10 @@ function refine(mesh, ::TriSubdivision)
midpoints = Dict{Tuple{Int,Int},Int}()
∂₁₀ = Boundary{1,0}(t)
for eind in 1:nfacets(t)
- i, j = sort(∂₁₀(eind))
+ i, j = ∂₁₀(eind)
edge = Segment(points[i], points[j])
- push!(points, center(edge))
- midpoints[(i, j)] = (np += 1)
+ push!(points, centroid(edge))
+ midpoints[_ordered(i, j)] = (np += 1)
end
# construct subtriangles of faces
diff --git a/src/sampling.jl b/src/sampling.jl
index b42dd9541..2e229651f 100644
--- a/src/sampling.jl
+++ b/src/sampling.jl
@@ -65,6 +65,7 @@ sample(rng::AbstractRNG, g::Geometry, method::ContinuousSamplingMethod) = sample
include("sampling/regular.jl")
include("sampling/homogeneous.jl")
include("sampling/mindistance.jl")
+include("sampling/fibonacci.jl")
# ----------
# UTILITIES
diff --git a/src/sampling/ball.jl b/src/sampling/ball.jl
index 625777200..1874f24b8 100644
--- a/src/sampling/ball.jl
+++ b/src/sampling/ball.jl
@@ -13,13 +13,16 @@ according to a norm-ball of given `radius`.
* `metric` - Metric for the ball (default to `Euclidean()`)
* `maxsize` - Maximum size of the resulting sample (default to none)
"""
-struct BallSampling{T,M} <: DiscreteSamplingMethod
- radius::T
+struct BallSampling{ℒ<:Len,M} <: DiscreteSamplingMethod
+ radius::ℒ
metric::M
maxsize::Union{Int,Nothing}
+ BallSampling(radius::ℒ, metric::M, maxsize) where {ℒ<:Len,M} = new{float(ℒ),M}(radius, metric, maxsize)
end
-BallSampling(radius; metric=Euclidean(), maxsize=nothing) = BallSampling(radius, metric, maxsize)
+BallSampling(radius::Len; metric=Euclidean(), maxsize=nothing) = BallSampling(radius, metric, maxsize)
+
+BallSampling(radius; kwargs...) = BallSampling(addunit(radius, u"m"); kwargs...)
function sampleinds(rng::AbstractRNG, d::Domain, method::BallSampling)
radius = method.radius
diff --git a/src/sampling/block.jl b/src/sampling/block.jl
index 5bce56a83..7238926ee 100644
--- a/src/sampling/block.jl
+++ b/src/sampling/block.jl
@@ -12,10 +12,15 @@ A method for sampling objects that are `sides` apart using a
Alternatively, specify the sides `side₁`, `side₂`, ..., `sideₙ`.
"""
-struct BlockSampling{S} <: DiscreteSamplingMethod
- sides::S
+struct BlockSampling{Dim,ℒ<:Len} <: DiscreteSamplingMethod
+ sides::NTuple{Dim,ℒ}
+ BlockSampling(sides::NTuple{Dim,ℒ}) where {Dim,ℒ<:Len} = new{Dim,float(ℒ)}(sides)
end
+BlockSampling(sides::NTuple{Dim,Len}) where {Dim} = BlockSampling(promote(sides...))
+
+BlockSampling(sides::Tuple) = BlockSampling(addunit.(sides, u"m"))
+
BlockSampling(sides...) = BlockSampling(sides)
function sampleinds(::AbstractRNG, d::Domain, method::BlockSampling)
diff --git a/src/sampling/fibonacci.jl b/src/sampling/fibonacci.jl
new file mode 100644
index 000000000..67f35f84a
--- /dev/null
+++ b/src/sampling/fibonacci.jl
@@ -0,0 +1,50 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ FibonacciSampling(n, ϕ = (1 + √5)/2)
+
+Generate `n` Fibonacci points with parameter `ϕ`.
+
+The golden ratio is used as the default value of `ϕ`,
+but other irrational numbers can be used.
+
+See
+and .
+"""
+struct FibonacciSampling{T<:Real} <: ContinuousSamplingMethod
+ n::Int
+ ϕ::T
+
+ function FibonacciSampling(n::Int, ϕ::T) where {T<:Real}
+ if n ≤ 0
+ throw(ArgumentError("Size must be positive"))
+ end
+ new{T}(n, ϕ)
+ end
+end
+
+FibonacciSampling(n::Int) = FibonacciSampling(n, (1 + √5) / 2)
+
+function sample(geom::Geometry, method::FibonacciSampling)
+ if paramdim(geom) != 2
+ throw(ArgumentError("Fibonacci sampling only defined for 2D geometries"))
+ end
+
+ fib = _fibmap(geom)
+
+ function point(i)
+ u = mod(i / method.ϕ, 1)
+ v = i / (method.n - 1)
+ geom(fib(u, v)...)
+ end
+
+ (point(i) for i in 0:(method.n - 1))
+end
+
+_fibmap(g) = (u, v) -> (u, v)
+_fibmap(d::Disk) = (u, v) -> (√u, v)
+_fibmap(b::Ball{𝔼{2}}) = (u, v) -> (√u, v)
+_fibmap(b::Ball{🌐}) = (u, v) -> (√u, v)
+_fibmap(s::Sphere{𝔼{3}}) = (u, v) -> (acos(1 - 2v) / π, u)
diff --git a/src/sampling/homogeneous.jl b/src/sampling/homogeneous.jl
index 103b6204a..5a68d5e1b 100644
--- a/src/sampling/homogeneous.jl
+++ b/src/sampling/homogeneous.jl
@@ -21,7 +21,7 @@ function sample(rng::AbstractRNG, d::Domain, method::HomogeneousSampling)
weights = isnothing(method.weights) ? measure.(d) : method.weights
# sample elements with weights
- w = WeightedSampling(size, weights, replace=true)
+ w = WeightedSampling(size, ustrip.(weights), replace=true)
# within each element sample a single point
h = HomogeneousSampling(1)
@@ -29,9 +29,9 @@ function sample(rng::AbstractRNG, d::Domain, method::HomogeneousSampling)
(first(sample(rng, e, h)) for e in sample(rng, d, w))
end
-function sample(rng::AbstractRNG, geom::Geometry{Dim,T}, method::HomogeneousSampling) where {Dim,T}
+function sample(rng::AbstractRNG, geom::Geometry, method::HomogeneousSampling)
if isparametrized(geom)
- randpoint() = geom(rand(rng, T, paramdim(geom))...)
+ randpoint() = geom(rand(rng, numtype(lentype(geom)), paramdim(geom))...)
(randpoint() for _ in 1:(method.size))
else
sample(rng, discretize(geom), method)
@@ -42,32 +42,34 @@ end
# SPECIAL CASES
# --------------
-function sample(rng::AbstractRNG, triangle::Triangle{Dim,T}, method::HomogeneousSampling) where {Dim,T}
+function sample(rng::AbstractRNG, triangle::Triangle, method::HomogeneousSampling)
function randpoint()
# sample barycentric coordinates
- u₁, u₂ = rand(rng, T, 2)
+ u₁, u₂ = rand(rng, numtype(lentype(triangle)), 2)
λ₁, λ₂ = 1 - √u₁, u₂ * √u₁
triangle(λ₁, λ₂)
end
(randpoint() for _ in 1:(method.size))
end
-function sample(rng::AbstractRNG, tetrahedron::Tetrahedron{Dim,T}, method::HomogeneousSampling) where {Dim,T}
+function sample(rng::AbstractRNG, tetrahedron::Tetrahedron, method::HomogeneousSampling)
@error "not implemented"
end
-function sample(rng::AbstractRNG, ball::Ball{2,T}, method::HomogeneousSampling) where {T}
+sample(rng::AbstractRNG, ball::Ball, method::HomogeneousSampling) = _sample(rng, ball, Val(embeddim(ball)), method)
+
+function _sample(rng::AbstractRNG, ball::Ball, ::Val{2}, method::HomogeneousSampling)
function randpoint()
- u₁, u₂ = rand(rng, T, 2)
+ u₁, u₂ = rand(rng, numtype(lentype(ball)), 2)
ball(√u₁, u₂)
end
(randpoint() for _ in 1:(method.size))
end
-function sample(rng::AbstractRNG, ball::Ball{3,T}, method::HomogeneousSampling) where {T}
+function _sample(rng::AbstractRNG, ball::Ball, ::Val{3}, method::HomogeneousSampling)
function randpoint()
- u₁, u₂, u₃ = rand(rng, T, 3)
- ball(∛u₁, acos(1 - 2u₂) / T(π), u₃)
+ u₁, u₂, u₃ = rand(rng, numtype(lentype(ball)), 3)
+ ball(∛u₁, acos(1 - 2u₂) / π, u₃)
end
(randpoint() for _ in 1:(method.size))
end
diff --git a/src/sampling/mindistance.jl b/src/sampling/mindistance.jl
index f411e3352..50e87d008 100644
--- a/src/sampling/mindistance.jl
+++ b/src/sampling/mindistance.jl
@@ -1,4 +1,3 @@
-using Base: BitSignedSmall
# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
@@ -24,16 +23,25 @@ or blue noise sampling in the computer graphics community.
* Medeiros et al. 2014. [Fast adaptive blue noise on polygonal surfaces]
(https://www.sciencedirect.com/science/article/abs/pii/S1524070313000313)
"""
-struct MinDistanceSampling{T,M} <: ContinuousSamplingMethod
- α::T
- ρ::T
+struct MinDistanceSampling{ℒ<:Len,M} <: ContinuousSamplingMethod
+ α::ℒ
+ ρ::ℒ
δ::Int
metric::M
+ MinDistanceSampling(α::ℒ, ρ::ℒ, δ, metric::M) where {ℒ<:Len,M} = new{float(ℒ),M}(α, ρ, δ, metric)
end
+MinDistanceSampling(α::Len, ρ::Len, δ, metric) = MinDistanceSampling(promote(α, ρ)..., δ, metric)
+
+MinDistanceSampling(α, ρ, δ, metric) = MinDistanceSampling(addunit(α, u"m"), addunit(ρ, u"m"), δ, metric)
+
MinDistanceSampling(α::T; ρ=T(0.65), δ=100, metric=Euclidean()) where {T} = MinDistanceSampling(α, ρ, δ, metric)
-function sample(rng::AbstractRNG, d::Domain, method::MinDistanceSampling)
+sample(rng::AbstractRNG, d::Domain, method::MinDistanceSampling) = _sample(rng, d, method)
+
+sample(rng::AbstractRNG, b::Ball, method::MinDistanceSampling) = _sample(rng, b, method)
+
+function _sample(rng::AbstractRNG, obj, method::MinDistanceSampling)
# retrieve parameters
α = method.α
ρ = method.ρ
@@ -41,17 +49,17 @@ function sample(rng::AbstractRNG, d::Domain, method::MinDistanceSampling)
m = method.metric
# total volume/area of the object
- V = sum(measure, d)
+ V = measure(obj)
# expected number of Poisson samples
# for relative radius (Lagae & Dutré 2007)
N = 2V / √3 * (ρ / α)^2
# number of oversamples (Medeiros et al. 2014)
- O = ceil(Int, δ * N)
+ O = ceil(Int, δ * ustrip(N))
# oversample the object
- points = sample(rng, d, HomogeneousSampling(O))
+ points = sample(rng, obj, HomogeneousSampling(O))
# collect points into point set
𝒫 = PointSet(collect(points))
diff --git a/src/sampling/regular.jl b/src/sampling/regular.jl
index afb8b25d4..6f6cfb648 100644
--- a/src/sampling/regular.jl
+++ b/src/sampling/regular.jl
@@ -23,51 +23,70 @@ end
RegularSampling(sizes::Vararg{Int,N}) where {N} = RegularSampling(sizes)
-function sample(::AbstractRNG, geom::Geometry{Dim,T}, method::RegularSampling) where {Dim,T}
- V = floattype(T)
+function sample(::AbstractRNG, geom::Geometry, method::RegularSampling)
+ T = numtype(lentype(geom))
D = paramdim(geom)
sz = fitdims(method.sizes, D)
δₛ = firstoffset(geom)
δₑ = lastoffset(geom)
- tₛ = ntuple(i -> V(0 + δₛ[i](sz[i])), D)
- tₑ = ntuple(i -> V(1 - δₑ[i](sz[i])), D)
+ tₛ = ntuple(i -> T(0 + δₛ[i](sz[i])), D)
+ tₑ = ntuple(i -> T(1 - δₑ[i](sz[i])), D)
rs = (range(tₛ[i], stop=tₑ[i], length=sz[i]) for i in 1:D)
iᵣ = (geom(uv...) for uv in Iterators.product(rs...))
- iₚ = (p for p in extrapoints(geom))
+ iₚ = (p for p in extrapoints(geom, sz))
Iterators.flatmap(identity, (iᵣ, iₚ))
end
-floattype(T::Type{<:Quantity}) = floattype(Unitful.numtype(T))
-floattype(T::Type) = float(T)
+firstoffset(g::Geometry) = _firstoffset(g, Val(embeddim(g)))
+lastoffset(g::Geometry) = _lastoffset(g, Val(embeddim(g)))
+extrapoints(g::Geometry, sz) = _extrapoints(g, Val(embeddim(g)), sz)
-firstoffset(g::Geometry) = ntuple(i -> (n -> zero(n)), paramdim(g))
-lastoffset(g::Geometry) = ntuple(i -> (n -> isperiodic(g)[i] ? inv(n) : zero(n)), paramdim(g))
-extrapoints(::Geometry) = ()
+_firstoffset(g::Geometry, ::Val) = ntuple(i -> (n -> zero(n)), paramdim(g))
+_lastoffset(g::Geometry, ::Val) = ntuple(i -> (n -> isperiodic(g)[i] ? inv(n) : zero(n)), paramdim(g))
+_extrapoints(::Geometry, ::Val, sz) = ()
firstoffset(d::Disk) = (n -> inv(n), firstoffset(boundary(d))...)
lastoffset(d::Disk) = (n -> zero(n), lastoffset(boundary(d))...)
-extrapoints(d::Disk) = (center(d),)
+extrapoints(d::Disk, sz) = (center(d),)
firstoffset(b::Ball) = (n -> inv(n), firstoffset(boundary(b))...)
lastoffset(b::Ball) = (n -> zero(n), lastoffset(boundary(b))...)
-extrapoints(b::Ball) = (center(b),)
-
-firstoffset(::Sphere{3}) = (n -> inv(n + 1), n -> zero(n))
-lastoffset(::Sphere{3}) = (n -> inv(n + 1), n -> inv(n))
-extrapoints(s::Sphere{3}) = (s(0, 0), s(1, 0))
+extrapoints(b::Ball, sz) = (center(b),)
+
+_firstoffset(::Sphere, ::Val{3}) = (n -> inv(n + 1), n -> zero(n))
+_lastoffset(::Sphere, ::Val{3}) = (n -> inv(n + 1), n -> inv(n))
+_extrapoints(s::Sphere, ::Val{3}, sz) = (s(0, 0), s(1, 0))
+
+firstoffset(::Ellipsoid) = (n -> inv(n + 1), n -> zero(n))
+lastoffset(::Ellipsoid) = (n -> inv(n + 1), n -> inv(n))
+extrapoints(e::Ellipsoid, sz) = (e(0, 0), e(1, 0))
+
+firstoffset(::Cylinder) = (n -> inv(n), n -> zero(n), n -> zero(n))
+lastoffset(::Cylinder) = (n -> zero(n), n -> inv(n), n -> zero(n))
+function extrapoints(c::Cylinder, sz)
+ T = numtype(lentype(c))
+ b = bottom(c)(0, 0)
+ t = top(c)(0, 0)
+ s = Segment(b, t)
+ [s(t) for t in range(zero(T), one(T), sz[3])]
+end
firstoffset(::CylinderSurface) = (n -> zero(n), n -> zero(n))
lastoffset(::CylinderSurface) = (n -> inv(n), n -> zero(n))
-extrapoints(c::CylinderSurface) = (bottom(c)(0, 0), top(c)(0, 0))
+extrapoints(c::CylinderSurface, sz) = (bottom(c)(0, 0), top(c)(0, 0))
+
+firstoffset(::ConeSurface) = (n -> zero(n), n -> zero(n))
+lastoffset(::ConeSurface) = (n -> inv(n), n -> inv(n))
+extrapoints(c::ConeSurface, sz) = (base(c)(0, 0), apex(c))
-firstoffset(::ConeSurface) = (n -> zero(n), n -> inv(n))
-lastoffset(::ConeSurface) = (n -> inv(n), n -> zero(n))
-extrapoints(c::ConeSurface) = (apex(c), base(c)(0, 0))
+firstoffset(::FrustumSurface) = (n -> zero(n), n -> zero(n))
+lastoffset(::FrustumSurface) = (n -> inv(n), n -> zero(n))
+extrapoints(c::FrustumSurface, sz) = (bottom(c)(0, 0), top(c)(0, 0))
# --------------
# SPECIAL CASES
# --------------
-function sample(rng::AbstractRNG, grid::CartesianGrid, method::RegularSampling)
+function sample(rng::AbstractRNG, grid::OrthoRegularGrid, method::RegularSampling)
sample(rng, boundingbox(grid), method)
end
diff --git a/src/sideof.jl b/src/sideof.jl
index 9e2248f35..66f39f9bd 100644
--- a/src/sideof.jl
+++ b/src/sideof.jl
@@ -39,20 +39,129 @@ Possible results are `LEFT`, `RIGHT` or `ON` the `line`.
* Assumes the orientation of `Segment(line(0), line(1))`.
"""
-function sideof(point::Point{2,T}, line::Line{2,T}) where {T}
+function sideof(point::Point, line::Line)
a = signarea(point, line(0), line(1))
- ifelse(a > atol(T), LEFT, ifelse(a < -atol(T), RIGHT, ON))
+ ifelse(a > atol(a), LEFT, ifelse(a < -atol(a), RIGHT, ON))
end
"""
sideof(point, ring)
Determines on which side the `point` is in relation to the `ring`.
-Possible results are `IN` or `OUT` the `ring`.
+Possible results are `IN`, `OUT` or `ON` the `ring`.
+
+## References
+
+* Hao et al. 2018. [Optimal Reliable Point-in-Polygon Test and
+ Differential Coding Boolean Operations on Polygons]
+ (https://www.mdpi.com/2073-8994/10/10/477)
"""
-function sideof(point::Point{2,T}, ring::Ring{2,T}) where {T}
- w = winding(point, ring)
- ifelse(isapprox(w, zero(T), atol=atol(T)), OUT, IN)
+function sideof(point::Point, ring::Ring)
+ assertion(CoordRefSystems.ncoords(crs(point)) == 2, "points must have 2 coordinates")
+ point′ = point |> Proj(crs(ring))
+ if nvertices(ring) ≤ 1000 || Threads.nthreads() == 1
+ _sideofserial(point′, ring)
+ else
+ _sideofthreads(point′, ring)
+ end
+end
+
+function _sideofserial(p::Point, r::Ring)
+ v = vertices(r)
+ k = 0
+ for i in eachindex(v)
+ ison, addk = _sideofcore(p, v[i], v[i + 1])
+ ison && return ON
+ addk && (k += 1)
+ end
+ iseven(k) ? OUT : IN
+end
+
+function _sideofthreads(p::Point, r::Ring)
+ v = vertices(r)
+ k = Threads.Atomic{Int}(0)
+ on = Threads.Atomic{Bool}(false)
+ Threads.@threads for i in eachindex(v)
+ ison, addk = _sideofcore(p, v[i], v[i + 1])
+ (on[] = ison) && break
+ addk && Threads.atomic_add!(k, 1)
+ end
+ on[] ? ON : (iseven(k[]) ? OUT : IN)
+end
+
+function _sideofcore(p::Point, pᵢ::Point, pⱼ::Point)
+ # flat coordinates of query point
+ cₚ = flat(coords(p))
+ xₚ, yₚ = cₚ.x, cₚ.y
+
+ # possible return values for readability
+ ISON = (true, false) # ison=true, addk=false
+ ADDK = (false, true) # ison=false, addk=true
+ NONE = (false, false) # ison=false, addk=false
+
+ # flat coordinates of segment i -- i+1
+ cᵢ = flat(coords(pᵢ))
+ cⱼ = flat(coords(pⱼ))
+ xᵢ, yᵢ = cᵢ.x, cᵢ.y
+ xⱼ, yⱼ = cⱼ.x, cⱼ.y
+
+ v₁ = yᵢ - yₚ
+ v₂ = yⱼ - yₚ
+
+ if (isnegative(v₁) && isnegative(v₂)) || (ispositive(v₁) && ispositive(v₂))
+ # case 11, 26
+ return NONE
+ end
+
+ u₁ = xᵢ - xₚ
+ u₂ = xⱼ - xₚ
+
+ if ispositive(v₂) && isnonpositive(v₁)
+ # case 3, 9, 16, 21, 13, 24
+ f = u₁ * v₂ - u₂ * v₁
+ if ispositive(f)
+ # case 3, 9
+ return ADDK
+ elseif isequalzero(f)
+ # case 16, 21
+ return ISON
+ end
+ elseif ispositive(v₁) && isnonpositive(v₂)
+ # case 4, 10, 19, 20, 12, 25
+ f = u₁ * v₂ - u₂ * v₁
+ if isnegative(f)
+ # case 4, 10
+ return ADDK
+ elseif isequalzero(f)
+ # case 19, 20
+ return ISON
+ end
+ elseif isequalzero(v₂) && isnegative(v₁)
+ # case 7, 14, 17
+ f = u₁ * v₂ - u₂ * v₁
+ if isequalzero(f)
+ # case 17
+ return ISON
+ end
+ elseif isequalzero(v₁) && isnegative(v₂)
+ # case 8, 15, 18
+ f = u₁ * v₂ - u₂ * v₁
+ if isequalzero(f)
+ # case 18
+ return ISON
+ end
+ elseif isequalzero(v₁) && isequalzero(v₂)
+ # case 1, 2, 5, 6, 22, 23
+ if isnonpositive(u₂) && isnonnegative(u₁)
+ # case 1
+ return ISON
+ elseif isnonpositive(u₁) && isnonnegative(u₂)
+ # case 2
+ return ISON
+ end
+ end
+ # case 5, 6, 7, 8, 12, 13, 14, 15, 22, 23, 24, 25
+ return NONE
end
# -----
@@ -65,21 +174,23 @@ end
Determines on which side the `point` is in relation to the surface `mesh`.
Possible results are `IN` or `OUT` the `mesh`.
"""
-sideof(point::Point{3}, mesh::Mesh{3}) = sideof((point,), mesh) |> first
+sideof(point::Point, mesh::Mesh) = sideof((point,), mesh) |> first
# ----------
# FALLBACKS
# ----------
-sideof(points, line::Line{2}) = map(point -> sideof(point, line), points)
+sideof(points, line::Line) = map(point -> sideof(point, line), points)
function sideof(points, object::GeometryOrDomain)
- T = coordtype(object)
bbox = boundingbox(object)
- isin = tcollect(point ∈ bbox for point in points)
+ isin = [point ∈ bbox for point in points]
inds = findall(isin)
- wind = winding(collectat(points, inds), object)
side = fill(OUT, length(isin))
- side[inds] .= ifelse.(isapprox.(wind, zero(T), atol=atol(T)), OUT, IN)
+ side[inds] .= sidewithinbox(collectat(points, inds), object)
side
end
+
+sidewithinbox(points, ring::Ring) = map(point -> sideof(point, ring), points)
+
+sidewithinbox(points, mesh::Mesh) = map(w -> ifelse(isapproxzero(w), OUT, IN), winding(points, mesh))
diff --git a/src/simplification.jl b/src/simplification.jl
index 1668d0edd..148d2f0c5 100644
--- a/src/simplification.jl
+++ b/src/simplification.jl
@@ -12,13 +12,11 @@ abstract type SimplificationMethod end
"""
simplify(object, method)
-Simplify `object` with given `method`.
-
-See also [`decimate`](@ref).
+Simplify geometric `object` with given `method`.
"""
function simplify end
-simplify(box::Box{2}, method::SimplificationMethod) = PolyArea(simplify(boundary(box), method))
+simplify(box::Box{𝔼{2}}, method::SimplificationMethod) = PolyArea(simplify(boundary(box), method))
simplify(polygon::Polygon, method::SimplificationMethod) = PolyArea([simplify(ring, method) for ring in rings(polygon)])
@@ -30,22 +28,6 @@ simplify(domain::Domain, method::SimplificationMethod) = GeometrySet([simplify(e
# IMPLEMENTATIONS
# ----------------
-include("simplification/douglaspeucker.jl")
include("simplification/selinger.jl")
-
-# ----------
-# UTILITIES
-# ----------
-
-"""
- decimate(object, [ϵ]; min=3, max=typemax(Int), maxiter=10)
-
-Simplify `object` with an appropriate simplification method
-and deviation tolerance `ϵ`.
-
-If the tolerance `ϵ` is not provided, perform binary search until
-the number of vertices is between `min` and `max` or until the
-number of iterations reaches a maximum `maxiter`.
-"""
-decimate(object, ϵ=nothing; min=3, max=typemax(Int), maxiter=10) =
- simplify(object, DouglasPeucker(ϵ, min=min, max=max, maxiter=maxiter))
+include("simplification/douglaspeucker.jl")
+include("simplification/minmax.jl")
diff --git a/src/simplification/douglaspeucker.jl b/src/simplification/douglaspeucker.jl
index 9887c901a..d6bba5c10 100644
--- a/src/simplification/douglaspeucker.jl
+++ b/src/simplification/douglaspeucker.jl
@@ -3,14 +3,12 @@
# ------------------------------------------------------------------
"""
- DouglasPeucker([ϵ]; min=3, max=typemax(Int), maxiter=10)
+ DouglasPeuckerSimplification(τ)
-Simplify geometries with Douglas-Peucker algorithm. The higher
-is the tolerance `ϵ`, the more aggressive is the simplification.
+Douglas-Peucker's simplification algorithm with tolerance `τ` in length units
+(default to meter).
-If the tolerance `ϵ` is not provided, perform binary search until
-the number of vertices is between `min` and `max` or until the
-number of iterations reaches a maximum `maxiter`.
+The higher is the tolerance, the more aggressive is the simplification.
## References
@@ -18,67 +16,23 @@ number of iterations reaches a maximum `maxiter`.
the Number of Points Required to Represent a Digitized Line or its
Caricature](https://www.sciencedirect.com/science/article/abs/pii/0167839691900198)
"""
-struct DouglasPeucker{T} <: SimplificationMethod
- ϵ::T
- min::Int
- max::Int
- maxiter::Int
+struct DouglasPeuckerSimplification{ℒ<:Len} <: SimplificationMethod
+ τ::ℒ
+ DouglasPeuckerSimplification(τ::ℒ) where {ℒ<:Len} = new{float(ℒ)}(τ)
end
-DouglasPeucker(ϵ=nothing; min=3, max=typemax(Int), maxiter=10) = DouglasPeucker(ϵ, min, max, maxiter)
+DouglasPeuckerSimplification(τ) = DouglasPeuckerSimplification(addunit(τ, u"m"))
-function simplify(chain::Chain, method::DouglasPeucker)
- v = if isnothing(method.ϵ)
- # perform binary search with other parameters
- βsimplify(vertices(chain), method.min, method.max, method.maxiter)
- else
- # perform Douglas-Peucker ϵ-simplification
- ϵsimplify(vertices(chain), method.ϵ)
- end |> collect
- isclosed(chain) ? Ring(v) : Rope(v)
-end
-
-# simplification by means of binary search
-function βsimplify(v::AbstractVector{Point{Dim,T}}, min, max, maxiter) where {Dim,T}
- i = 0
- u = v
- n = length(u)
- a = zero(T)
- b = initeps(u)
- while !(min ≤ n ≤ max) && i < maxiter
- # midpoint candidate
- ϵ = (a + b) / 2
-
- # evaluate at midpoint
- u = ϵsimplify(v, ϵ)
- n = length(u)
-
- # binary search
- n < min && (b = ϵ)
- n > max && (a = ϵ)
-
- i += 1
- end
-
- u
-end
-
-# initial ϵ guess for a given chain
-function initeps(v::AbstractVector{Point{Dim,T}}) where {Dim,T}
- n = length(v)
- ϵ = typemax(T)
- l = Line(first(v), last(v))
- d = [evaluate(Euclidean(), v[i], l) for i in 2:(n - 1)]
- ϵ = quantile(d, 0.25)
- 2ϵ
+function simplify(chain::Chain, method::DouglasPeuckerSimplification)
+ verts = _douglaspeucker(vertices(chain), method.τ)
+ isclosed(chain) ? Ring(verts) : Rope(verts)
end
# simplify chain assuming it is open
-function ϵsimplify(v::AbstractVector{Point{Dim,T}}, ϵ) where {Dim,T}
- # find vertex with maximum distance
- # to reference line
+function _douglaspeucker(v::AbstractVector{P}, τ) where {P<:Point}
+ # find vertex with maximum distance to reference line
l = Line(first(v), last(v))
- imax, dmax = 0, zero(T)
+ imax, dmax = 0, zero(lentype(P))
for i in 2:(length(v) - 1)
d = evaluate(Euclidean(), v[i], l)
if d > dmax
@@ -87,11 +41,11 @@ function ϵsimplify(v::AbstractVector{Point{Dim,T}}, ϵ) where {Dim,T}
end
end
- if dmax < ϵ
+ if dmax < τ
[first(v), last(v)]
else
- v₁ = ϵsimplify(v[begin:imax], ϵ)
- v₂ = ϵsimplify(v[imax:end], ϵ)
+ v₁ = _douglaspeucker(v[begin:imax], τ)
+ v₂ = _douglaspeucker(v[imax:end], τ)
[v₁[begin:(end - 1)]; v₂]
end
end
diff --git a/src/simplification/minmax.jl b/src/simplification/minmax.jl
new file mode 100644
index 000000000..72caf794c
--- /dev/null
+++ b/src/simplification/minmax.jl
@@ -0,0 +1,54 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ MinMaxSimplification(method; min=3, max=typemax(Int), maxiter=10)
+
+Simplify geometries with binary search algorithm and a parent simplification `method`.
+
+The simplification is performed until the number of vertices is in the `[min, max]`
+range or until a maximum number of iterations `maxiter` is reached.
+"""
+struct MinMaxSimplification{M} <: SimplificationMethod
+ method::M
+ min::Int
+ max::Int
+ maxiter::Int
+end
+
+MinMaxSimplification(method; min=3, max=typemax(Int), maxiter=10) = MinMaxSimplification(method, min, max, maxiter)
+
+function simplify(c::Chain, m::MinMaxSimplification)
+ i = 0
+ s = c
+ n = nvertices(c)
+ a, b = _initrange(c)
+ while !(m.min ≤ n ≤ m.max) && i < m.maxiter
+ # midpoint candidate
+ τ = (a + b) / 2
+
+ # evaluate at midpoint
+ s = simplify(c, m.method(τ))
+ n = nvertices(s)
+
+ # binary search
+ n < m.min && (b = τ)
+ n > m.max && (a = τ)
+
+ i += 1
+ end
+
+ s
+end
+
+# initial range for binary search
+function _initrange(c)
+ v = vertices(c)
+ n = length(v)
+ l = Line(first(v), last(v))
+ d = [evaluate(Euclidean(), v[i], l) for i in 2:(n - 1)]
+ z = zero(lentype(c))
+ τ = quantile(d, 0.25)
+ (z, 2τ)
+end
diff --git a/src/simplification/selinger.jl b/src/simplification/selinger.jl
index e16a7f9a1..856f1d14b 100644
--- a/src/simplification/selinger.jl
+++ b/src/simplification/selinger.jl
@@ -3,24 +3,31 @@
# ------------------------------------------------------------------
"""
- Selinger(ϵ)
+ SelingerSimplification(τ)
-Simplify geometries with Selinger's algorithm, which attempts to
-minimize the number of vertices and the deviation of vertices
-to the resulting segments based on deviation tolerance `ϵ`.
+Selinger's simplification algorithm with tolerance `τ` in length units
+(default to meter).
+
+The higher is the tolerance, the more aggressive is the simplification.
## References
-* Selinger, P. 2003. [Potrace: A polygon-based tracing algorithm]
+* SelingerSimplification, P. 2003. [Potrace: A polygon-based tracing algorithm]
(https://potrace.sourceforge.net/potrace.pdf)
"""
-struct Selinger{T} <: SimplificationMethod
- ϵ::T
+struct SelingerSimplification{ℒ<:Len} <: SimplificationMethod
+ τ::ℒ
+ SelingerSimplification(τ::ℒ) where {ℒ<:Len} = new{float(ℒ)}(τ)
end
-function simplify(chain::Chain{Dim,T}, method::Selinger) where {Dim,T}
+SelingerSimplification(τ) = SelingerSimplification(addunit(τ, u"m"))
+
+function simplify(chain::Chain, method::SelingerSimplification)
+ ℒ = lentype(chain)
+ 𝒜 = typeof(zero(ℒ)^2)
+
# retrieve parameters
- ϵ = method.ϵ
+ τ = method.τ
# vertices as circular vector
v = vertices(chain)
@@ -28,18 +35,15 @@ function simplify(chain::Chain{Dim,T}, method::Selinger) where {Dim,T}
# penalty for each possible segment
n = length(p)
- P = Dict{Tuple{Int,Int},T}()
+ P = Dict{Tuple{Int,Int},𝒜}()
for i in 1:n, o in 1:(n - 2)
j = i + o
- i₊ = i + 1
- j₋ = j - 1
- jₙ = mod1(j, n)
l = Line(p[i], p[j])
- δ = [evaluate(Euclidean(), p[k], l) for k in i₊:j₋]
- if all(<(ϵ), δ)
+ δ = [evaluate(Euclidean(), p[k], l) for k in (i + 1):(j - 1)]
+ if all(<(τ), δ)
dᵢⱼ = norm(p[j] - p[i])
- σᵢⱼ = o == 1 ? zero(T) : sqrt(sum(abs2, δ) / length(δ))
- P[(i, jₙ)] = dᵢⱼ * σᵢⱼ
+ σᵢⱼ = o == 1 ? zero(ℒ) : norm(δ)
+ P[(i, mod1(j, n))] = dᵢⱼ * σᵢⱼ
end
end
@@ -65,8 +69,7 @@ function simplify(chain::Chain{Dim,T}, method::Selinger) where {Dim,T}
end
end
- @assert first(bestpath) == last(bestpath)
- Ring(collect(v[bestpath[begin:(end - 1)]]))
+ Ring(v[bestpath[begin:(end - 1)]])
end
function dijkstra(I, s, t)
diff --git a/src/sorting/direction.jl b/src/sorting/direction.jl
index e5c881add..4ca9650db 100644
--- a/src/sorting/direction.jl
+++ b/src/sorting/direction.jl
@@ -7,7 +7,7 @@
Sort geometric objects along a given `direction` vector.
"""
-struct DirectionSort{V} <: SortingMethod
+struct DirectionSort{V<:Vec} <: SortingMethod
direction::V
end
@@ -17,7 +17,7 @@ function sortinds(domain::Domain, method::DirectionSort)
v = method.direction
t = map(1:nelements(domain)) do i
c = centroid(domain, i)
- u = coordinates(c)
+ u = to(c)
(u ⋅ v) / (v ⋅ v)
end
sortperm(t)
diff --git a/src/tesselation.jl b/src/tesselation.jl
new file mode 100644
index 000000000..949d0051d
--- /dev/null
+++ b/src/tesselation.jl
@@ -0,0 +1,28 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ DiscretizationMethod
+
+A method for tesselating point sets into meshes.
+"""
+abstract type TesselationMethod end
+
+"""
+ tesselate(pointset, [method])
+
+Tesselate `pointset` with tesselation `method`.
+
+If the `method` is ommitted, a default algorithm is used.
+"""
+function tesselate end
+
+tesselate(points::AbstractVector{<:Point}, method::TesselationMethod) = tesselate(PointSet(points), method)
+
+# ----------------
+# IMPLEMENTATIONS
+# ----------------
+
+include("tesselation/delaunay.jl")
+include("tesselation/voronoi.jl")
diff --git a/src/tesselation/delaunay.jl b/src/tesselation/delaunay.jl
new file mode 100644
index 000000000..a517b3703
--- /dev/null
+++ b/src/tesselation/delaunay.jl
@@ -0,0 +1,35 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ DelaunayTesselation([rng])
+
+Unconstrained Delaunay tesselation of point sets.
+Optionally, specify the random number generator `rng`.
+
+## References
+
+* Cheng et al. 2012. [Delaunay Mesh Generation]
+ (https://people.eecs.berkeley.edu/~jrs/meshbook.html)
+
+### Notes
+
+Wraps DelaunayTriangulation.jl. For any internal errors, file an issue at
+[DelaunayTriangulation.jl](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/issues/new)
+"""
+struct DelaunayTesselation{RNG<:AbstractRNG} <: TesselationMethod
+ rng::RNG
+end
+
+DelaunayTesselation(rng=Random.default_rng()) = DelaunayTesselation(rng)
+
+function tesselate(pset::PointSet, method::DelaunayTesselation)
+ assertion(CoordRefSystems.ncoords(crs(pset)) == 2, "points must have 2 coordinates")
+
+ # perform tesselation with raw coordinates
+ rawval = map(p -> CoordRefSystems.raw(coords(p)), pset)
+ triang = triangulate(rawval, rng=method.rng)
+ connec = connect.(each_solid_triangle(triang))
+ SimpleMesh(collect(pset), connec)
+end
diff --git a/src/tesselation/voronoi.jl b/src/tesselation/voronoi.jl
new file mode 100644
index 000000000..1cfa92a0e
--- /dev/null
+++ b/src/tesselation/voronoi.jl
@@ -0,0 +1,52 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ VoronoiTesselation([rng])
+
+Unconstrained Voronoi tesselation of point sets.
+Optionally, specify the random number generator `rng`.
+
+## References
+
+* Cheng et al. 2012. [Delaunay Mesh Generation]
+ (https://people.eecs.berkeley.edu/~jrs/meshbook.html)
+
+### Notes
+
+Wraps DelaunayTriangulation.jl. For any internal errors, file an issue at
+[DelaunayTriangulation.jl](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/issues/new)
+"""
+struct VoronoiTesselation{RNG<:AbstractRNG} <: TesselationMethod
+ rng::RNG
+end
+
+VoronoiTesselation(rng=Random.default_rng()) = VoronoiTesselation(rng)
+
+function tesselate(pset::PointSet, method::VoronoiTesselation)
+ C = crs(pset)
+ T = numtype(lentype(pset))
+ assertion(CoordRefSystems.ncoords(C) == 2, "points must have 2 coordinates")
+
+ # perform tesselation with raw coordinates
+ rawval = map(p -> CoordRefSystems.raw(coords(p)), pset)
+ triang = triangulate(rawval, rng=method.rng)
+ vorono = voronoi(triang, clip=true)
+
+ # mesh with all (possibly unused) points
+ points = map(get_polygon_points(vorono)) do xy
+ coords = CoordRefSystems.reconstruct(C, T.(xy))
+ Point(coords)
+ end
+ polygs = get_polygons(vorono)
+ connec = Vector{Connectivity}(undef, length(polygs))
+ for (i, inds) in polygs
+ tup = ntuple(j -> inds[j], length(inds) - 1)
+ connec[i] = connect(tup, Ngon)
+ end
+ mesh = SimpleMesh(points, connec)
+
+ # remove unused points
+ mesh |> Repair(1)
+end
diff --git a/src/tolerances.jl b/src/tolerances.jl
index 6025c455d..3ba29034c 100644
--- a/src/tolerances.jl
+++ b/src/tolerances.jl
@@ -2,8 +2,12 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
+const ATOL64 = ScopedValue(1.0e-10)
+const ATOL32 = ScopedValue(1.0f-5)
+
"""
atol(T)
+ atol(x::T)
Absolute tolerance used in algorithms for approximate
comparison with numbers of type `T`. It is used in the
@@ -13,6 +17,9 @@ source code in calls to the [`isapprox`](@ref) function:
isapprox(a::T, b::T, atol=atol(T))
```
"""
-atol(::Type{Float64}) = 1e-10
-atol(::Type{Float32}) = 1.0f-5
-atol(::Type{Q}) where {Q<:AbstractQuantity} = atol(numtype(Q)) * unit(Q)
+atol(x) = atol(typeof(x))
+atol(::Type{Float64}) = ATOL64[]
+atol(::Type{Float32}) = ATOL32[]
+atol(ℒ::Type{<:Len}) = atol(numtype(ℒ)) * unit(ℒ)
+atol(𝒜::Type{<:Area}) = atol(numtype(𝒜))^2 * unit(𝒜)
+atol(𝒱::Type{<:Vol}) = atol(numtype(𝒱))^3 * unit(𝒱)
diff --git a/src/topologies.jl b/src/topologies.jl
index 5449b768e..11e2f0416 100644
--- a/src/topologies.jl
+++ b/src/topologies.jl
@@ -55,7 +55,9 @@ segments = faces(topology, 1)
"""
function faces(t::Topology, rank)
D = paramdim(t)
- if rank == D
+ if rank == 0
+ vertices(t)
+ elseif rank == D
elements(t)
elseif rank == D - 1
facets(t)
@@ -71,7 +73,9 @@ Return the number of `rank`-faces of the `topology`.
"""
function nfaces(t::Topology, rank)
D = paramdim(t)
- if rank == D
+ if rank == 0
+ nvertices(t)
+ elseif rank == D
nelements(t)
elseif rank == D - 1
nfacets(t)
@@ -128,6 +132,26 @@ Return the number of facets of the `topology`.
"""
function nfacets(::Topology) end
+# ----------
+# FALLBACKS
+# ----------
+
+Base.getindex(t::Topology, ind::Int) = element(t, ind)
+
+Base.getindex(t::Topology, inds::AbstractVector) = [t[ind] for ind in inds]
+
+Base.firstindex(t::Topology) = 1
+
+Base.lastindex(t::Topology) = nelements(t)
+
+Base.length(t::Topology) = nelements(t)
+
+Base.iterate(t::Topology, state=1) = state > nelements(t) ? nothing : (t[state], state + 1)
+
+Base.eltype(t::Topology) = eltype([t[i] for i in 1:nelements(t)])
+
+Base.keys(t::Topology) = 1:nelements(t)
+
# ----------------
# IMPLEMENTATIONS
# ----------------
diff --git a/src/topologies/grid.jl b/src/topologies/grid.jl
index c82d43130..98da6005e 100644
--- a/src/topologies/grid.jl
+++ b/src/topologies/grid.jl
@@ -14,7 +14,6 @@ to aperiodic dimensions.
```julia
julia> GridTopology((10,20)) # 10x20 elements in a grid
julia> GridTopology((10,20), (true,false)) # cylinder topology
-julia> GridTopology((10,20), (true,true)) # sphere topology
```
"""
struct GridTopology{D} <: Topology
@@ -36,7 +35,13 @@ paramdim(::GridTopology{D}) where {D} = D
Base.size(t::GridTopology) = t.dims
-isopen(t::GridTopology) = t.open
+"""
+ isperiodic(topology)
+
+Tells whether or not the `topology` is periodic
+along each parametric dimension.
+"""
+isperiodic(t::GridTopology) = .!t.open
"""
elem2cart(t, e)
@@ -115,7 +120,7 @@ nvertices(t::GridTopology) = prod(t.dims .+ t.open)
function element(t::GridTopology{D}, ind) where {D}
∂ = Boundary{D,0}(t)
T = elementtype(t)
- connect(Tuple(∂(ind)), T)
+ connect(∂(ind), T)
end
nelements(t::GridTopology) = prod(t.dims)
@@ -123,7 +128,7 @@ nelements(t::GridTopology) = prod(t.dims)
function facet(t::GridTopology{D}, ind) where {D}
∂ = Boundary{D - 1,0}(t)
T = facettype(t)
- connect(Tuple(∂(ind)), T)
+ connect(∂(ind), T)
end
nfacets(t::GridTopology{1}) = t.dims[1] + t.open[1]
diff --git a/src/topologies/halfedge.jl b/src/topologies/halfedge.jl
index 71b2d889c..07a1e84c8 100644
--- a/src/topologies/halfedge.jl
+++ b/src/topologies/halfedge.jl
@@ -130,7 +130,7 @@ function HalfEdgeTopology(halves::AbstractVector{Tuple{HalfEdge,HalfEdge}})
end
function HalfEdgeTopology(elems::AbstractVector{<:Connectivity}; sort=true)
- @assert all(e -> paramdim(e) == 2, elems) "invalid element for half-edge topology"
+ assertion(all(e -> paramdim(e) == 2, elems), "invalid element for half-edge topology")
# sort elements to make sure that they
# are traversed in adjacent-first order
diff --git a/src/toporelations/adjacency.jl b/src/toporelations/adjacency.jl
index 498dad2e6..26bbd75fa 100644
--- a/src/toporelations/adjacency.jl
+++ b/src/toporelations/adjacency.jl
@@ -15,7 +15,7 @@ function Adjacency{P}(topology) where {P}
D = paramdim(topology)
T = typeof(topology)
- @assert D ≥ P "invalid adjacency relation"
+ assertion(D ≥ P, "invalid adjacency relation")
Adjacency{P,D,T}(topology)
end
@@ -34,6 +34,7 @@ function (𝒜::Adjacency{0,D,T})(ind::Integer) where {D,T<:GridTopology}
# construct topology for vertices
vtopo = GridTopology(dims .+ 1, cycl)
𝒜vert = Adjacency{D}(vtopo)
+
𝒜vert(ind)
end
@@ -44,25 +45,25 @@ function (𝒜::Adjacency{D,D,T})(ind::Integer) where {D,T<:GridTopology}
cycl = isperiodic(topo)
cind = elem2cart(topo, ind)
- # offsets along each dimension
- offsets = [ntuple(i -> i == d ? s : 0, D) for d in 1:D for s in (-1, 1)]
+ inds = Int[]
+ for d in 1:D, s in (-1, 1)
+ # offset along each dimension
+ offset = ntuple(i -> i == d ? s : 0, D)
- ninds = NTuple{D,Int}[]
- for offset in offsets
# apply offset to center index
sind = cind .+ offset
# wrap indices in case of periodic dimension
- wrap(i) = mod1(sind[i], dims[i])
- wind = ntuple(i -> cycl[i] ? wrap(i) : sind[i], D)
+ wind = ntuple(D) do i
+ cycl[i] ? mod1(sind[i], dims[i]) : sind[i]
+ end
# discard invalid indices
valid(i) = 1 ≤ wind[i] ≤ dims[i]
- all(valid, 1:D) && push!(ninds, wind)
+ all(valid, 1:D) && push!(inds, cart2elem(topo, wind...))
end
- # return linear index of element
- [cart2elem(topo, ind...) for ind in ninds]
+ ntuple(i -> inds[i], length(inds))
end
# -------------------
@@ -74,13 +75,13 @@ function (𝒜::Adjacency{0,2,T})(vert::Integer) where {T<:HalfEdgeTopology}
e = half4vert(𝒜.topology, vert)
# initialize result
- vertices = [e.half.head]
+ inds = [e.half.head]
# search in CCW orientation
p = e.prev
h = p.half
while !isnothing(h.elem) && h != e
- push!(vertices, p.head)
+ push!(inds, p.head)
p = h.prev
h = p.half
end
@@ -88,18 +89,18 @@ function (𝒜::Adjacency{0,2,T})(vert::Integer) where {T<:HalfEdgeTopology}
# if border edge is hit
if isnothing(h.elem)
# add last arm manually
- push!(vertices, p.head)
+ push!(inds, p.head)
# search in CW orientation
h = e.half
while !isnothing(h.elem)
n = h.next
h = n.half
- pushfirst!(vertices, h.head)
+ pushfirst!(inds, h.head)
end
end
- vertices
+ ntuple(i -> inds[i], length(inds))
end
# adjacent elements in a 2D half-edge topology
@@ -117,5 +118,5 @@ function (𝒜::Adjacency{2,2,T})(ind::Integer) where {T<:HalfEdgeTopology}
n = n.next
end
- inds
+ ntuple(i -> inds[i], length(inds))
end
diff --git a/src/toporelations/boundary.jl b/src/toporelations/boundary.jl
index 32be63989..e4b71d369 100644
--- a/src/toporelations/boundary.jl
+++ b/src/toporelations/boundary.jl
@@ -16,7 +16,7 @@ function Boundary{P,Q}(topology) where {P,Q}
D = paramdim(topology)
T = typeof(topology)
- @assert D ≥ P > Q "invalid boundary relation"
+ assertion(D ≥ P > Q, "invalid boundary relation")
Boundary{P,Q,D,T}(topology)
end
@@ -63,7 +63,7 @@ function (∂::Boundary{3,2,3,T})(ind::Integer) where {T<:GridTopology}
i5 += oz
i6 += oz
- [i1, i2, i3, i4, i5, i6]
+ (i1, i2, i3, i4, i5, i6)
end
# vertices of hexahedron on 3D grid
@@ -85,12 +85,13 @@ function (∂::Boundary{3,0,3,T})(ind::Integer) where {T<:GridTopology}
i6 = cart2corner(t, i₊, j, k₊)
i7 = cart2corner(t, i₊, j₊, k₊)
i8 = cart2corner(t, i, j₊, k₊)
- [i1, i2, i3, i4, i5, i6, i7, i8]
+
+ (i1, i2, i3, i4, i5, i6, i7, i8)
end
# vertices of quadrangle on 3D grid
function (∂::Boundary{2,0,3,T})(ind::Integer) where {T<:GridTopology}
- @error "not implemented"
+ throw(ErrorException("not implemented"))
end
# segments making up quadrangles in 2D grid
@@ -122,7 +123,7 @@ function (∂::Boundary{2,1,2,T})(ind::Integer) where {T<:GridTopology}
i3 += oy
i4 += oy
- [i1, i2, i3, i4]
+ (i1, i2, i3, i4)
end
# vertices of quadrangle on 2D grid
@@ -139,7 +140,8 @@ function (∂::Boundary{2,0,2,T})(ind::Integer) where {T<:GridTopology}
i2 = cart2corner(t, i₊, j)
i3 = cart2corner(t, i₊, j₊)
i4 = cart2corner(t, i, j₊)
- [i1, i2, i3, i4]
+
+ (i1, i2, i3, i4)
end
# vertices of segment on 2D grid
@@ -164,7 +166,7 @@ function (∂::Boundary{1,0,2,T})(ind::Integer) where {T<:GridTopology}
i2 = cart2corner(t, i₊, j)
end
- [i1, i2]
+ (i1, i2)
end
# vertices of segment on 1D grid
@@ -176,7 +178,7 @@ function (∂::Boundary{1,0,1,T})(ind::Integer) where {T<:GridTopology}
i1 = ind
i2 = c ? mod1(ind + 1, n) : ind + 1
- [i1, i2]
+ (i1, i2)
end
# -------------------
@@ -186,17 +188,23 @@ end
function (∂::Boundary{2,1,2,T})(elem::Integer) where {T<:HalfEdgeTopology}
t = ∂.topology
l = loop(half4elem(t, elem))
- v = CircularVector(l)
- [edge4pair(t, (v[i], v[i + 1])) for i in 1:length(v)]
+ n = length(l)
+ ntuple(n) do i
+ edge4pair(t, (l[mod1(i, n)], l[mod1(i + 1, n)]))
+ end
end
function (∂::Boundary{2,0,2,T})(elem::Integer) where {T<:HalfEdgeTopology}
- loop(half4elem(∂.topology, elem))
+ t = ∂.topology
+ l = loop(half4elem(t, elem))
+ n = length(l)
+ ntuple(i -> l[i], n)
end
function (∂::Boundary{1,0,2,T})(edge::Integer) where {T<:HalfEdgeTopology}
- e = half4edge(∂.topology, edge)
- [e.head, e.half.head]
+ t = ∂.topology
+ e = half4edge(t, edge)
+ (e.head, e.half.head)
end
# ----------------
@@ -204,5 +212,5 @@ end
# ----------------
function (∂::Boundary{D,0,D,T})(ind::Integer) where {D,T<:SimpleTopology}
- collect(connec4elem(∂.topology, ind))
+ connec4elem(∂.topology, ind)
end
diff --git a/src/toporelations/coboundary.jl b/src/toporelations/coboundary.jl
index 90283589c..9d20fe7a2 100644
--- a/src/toporelations/coboundary.jl
+++ b/src/toporelations/coboundary.jl
@@ -16,32 +16,66 @@ function Coboundary{P,Q}(topology) where {P,Q}
D = paramdim(topology)
T = typeof(topology)
- @assert P < Q ≤ D "invalid coboundary relation"
+ assertion(P < Q ≤ D, "invalid coboundary relation")
Coboundary{P,Q,D,T}(topology)
end
+# --------------
+# GRID TOPOLOGY
+# --------------
+
+# elements sharing vertex in grid
+function (𝒞::Coboundary{0,D,D,T})(ind::Integer) where {D,T<:GridTopology}
+ topo = 𝒞.topology
+ dims = size(topo)
+ cycl = isperiodic(topo)
+ cind = corner2cart(topo, ind)
+
+ inds = Int[]
+ for offset in CartesianIndices(ntuple(i -> -1:0, D))
+ # apply offset to center index
+ sind = cind .+ Tuple(offset)
+
+ # wrap indices in case of periodic dimension
+ wind = ntuple(D) do i
+ cycl[i] ? mod1(sind[i], dims[i]) : sind[i]
+ end
+
+ # discard invalid indices
+ valid(i) = 1 ≤ wind[i] ≤ dims[i]
+ all(valid, 1:D) && push!(inds, cart2elem(topo, wind...))
+ end
+
+ ntuple(i -> inds[i], length(inds))
+end
+
# -------------------
# HALF-EDGE TOPOLOGY
# -------------------
-function (𝒞::Coboundary{0,1,2,T})(vert::Integer) where {T<:HalfEdgeTopology}
+# segments sharing a vertex in 2D mesh
+function (𝒞::Coboundary{0,1,2,T})(ind::Integer) where {T<:HalfEdgeTopology}
t = 𝒞.topology
𝒜 = Adjacency{0}(t)
- [edge4pair(t, (vert, other)) for other in 𝒜(vert)]
+ o = 𝒜(ind)
+ ntuple(length(o)) do i
+ edge4pair(t, (ind, o[i]))
+ end
end
-function (𝒞::Coboundary{0,2,2,T})(vert::Integer) where {T<:HalfEdgeTopology}
- e = half4vert(𝒞.topology, vert)
+# elements sharing a vertex in 2D mesh
+function (𝒞::Coboundary{0,2,2,T})(ind::Integer) where {T<:HalfEdgeTopology}
+ e = half4vert(𝒞.topology, ind)
# initialize result
- elements = [e.elem]
+ inds = [e.elem]
# search in CCW orientation
p = e.prev
h = p.half
while !isnothing(h.elem) && h != e
- push!(elements, h.elem)
+ push!(inds, h.elem)
p = h.prev
h = p.half
end
@@ -51,16 +85,17 @@ function (𝒞::Coboundary{0,2,2,T})(vert::Integer) where {T<:HalfEdgeTopology}
# search in CW orientation
h = e.half
while !isnothing(h.elem)
- pushfirst!(elements, h.elem)
+ pushfirst!(inds, h.elem)
n = h.next
h = n.half
end
end
- elements
+ ntuple(i -> inds[i], length(inds))
end
-function (𝒞::Coboundary{1,2,2,T})(edge::Integer) where {T<:HalfEdgeTopology}
- e = half4edge(𝒞.topology, edge)
- isnothing(e.half.elem) ? [e.elem] : [e.elem, e.half.elem]
+# elements sharing a segment in 2D mesh
+function (𝒞::Coboundary{1,2,2,T})(ind::Integer) where {T<:HalfEdgeTopology}
+ e = half4edge(𝒞.topology, ind)
+ isnothing(e.half.elem) ? (e.elem,) : (e.elem, e.half.elem)
end
diff --git a/src/transforms.jl b/src/transforms.jl
index bad0b5b58..be49b250a 100644
--- a/src/transforms.jl
+++ b/src/transforms.jl
@@ -36,8 +36,7 @@ end
CoordinateTransform
A method to transform the coordinates of objects.
-See [https://en.wikipedia.org/wiki/List_of_common_coordinate_transformations]
-(https://en.wikipedia.org/wiki/List_of_common_coordinate_transformations).
+See .
"""
abstract type CoordinateTransform <: GeometricTransform end
@@ -63,31 +62,84 @@ apply(t::CoordinateTransform, g::GeometryOrDomain) = applycoord(t, g), nothing
revert(t::CoordinateTransform, g::GeometryOrDomain, c) = applycoord(inverse(t), g)
# apply transform recursively
-applycoord(t::CoordinateTransform, g::G) where {G<:GeometryOrDomain} =
- G((applycoord(t, getfield(g, n)) for n in fieldnames(G))...)
+@generated function applycoord(t::CoordinateTransform, g::G) where {G<:GeometryOrDomain}
+ ctor = constructor(G)
+ names = fieldnames(G)
+ exprs = (:(applycoord(t, g.$name)) for name in names)
+ :($ctor($(exprs...)))
+end
# stop recursion at non-geometric types
applycoord(::CoordinateTransform, x) = x
+# special treatment for TransformedGeometry
+applycoord(t::CoordinateTransform, g::TransformedGeometry) = TransformedGeometry(g, t)
+
# special treatment for TransformedMesh
applycoord(t::CoordinateTransform, m::TransformedMesh) = TransformedMesh(m, t)
# special treatment for lists of geometries
-applycoord(t::CoordinateTransform, g::NTuple{<:Any,<:Geometry}) = map(gᵢ -> applycoord(t, gᵢ), g)
-applycoord(t::CoordinateTransform, g::AbstractVector{<:Geometry}) = tcollect(applycoord(t, gᵢ) for gᵢ in g)
-applycoord(t::CoordinateTransform, g::CircularVector{<:Geometry}) =
- CircularVector(tcollect(applycoord(t, gᵢ) for gᵢ in g))
+applycoord(t::CoordinateTransform, g::StaticVector{<:Any,<:Geometry}) = map(gᵢ -> applycoord(t, gᵢ), g)
+applycoord(t::CoordinateTransform, g::AbstractVector{<:Geometry}) = [applycoord(t, gᵢ) for gᵢ in g]
+applycoord(t::CoordinateTransform, g::CircularVector{<:Geometry}) = CircularVector([applycoord(t, gᵢ) for gᵢ in g])
# ----------------
# IMPLEMENTATIONS
# ----------------
-include("transforms/scale.jl")
include("transforms/rotate.jl")
include("transforms/translate.jl")
+include("transforms/scale.jl")
include("transforms/affine.jl")
include("transforms/stretch.jl")
include("transforms/stdcoords.jl")
+include("transforms/proj.jl")
+include("transforms/morphological.jl")
+include("transforms/lengthunit.jl")
+include("transforms/shadow.jl")
+include("transforms/slice.jl")
include("transforms/repair.jl")
include("transforms/bridge.jl")
include("transforms/smoothing.jl")
+
+# --------------
+# OPTIMIZATIONS
+# --------------
+
+function →(t₁::Rotate, t₂::Rotate)
+ rot₁ = parameters(t₁).rot
+ rot₂ = parameters(t₂).rot
+ Rotate(rot₂ * rot₁)
+end
+
+function →(t₁::Translate, t₂::Translate)
+ offsets₁ = parameters(t₁).offsets
+ offsets₂ = parameters(t₂).offsets
+ Translate(offsets₁ .+ offsets₂)
+end
+
+function →(t₁::Scale, t₂::Scale)
+ factors₁ = parameters(t₁).factors
+ factors₂ = parameters(t₂).factors
+ Scale(factors₁ .* factors₂)
+end
+
+function →(t₁::Affine, t₂::Affine)
+ A₁ = parameters(t₁).A
+ A₂ = parameters(t₂).A
+ b₁ = parameters(t₁).b
+ b₂ = parameters(t₂).b
+ Affine(A₂ * A₁, A₂ * b₁ + b₂)
+end
+
+function →(t₁::Stretch, t₂::Stretch)
+ factors₁ = parameters(t₁).factors
+ factors₂ = parameters(t₂).factors
+ Stretch(factors₁ .* factors₂)
+end
+
+function →(t₁::Rotate, t₂::Translate)
+ rot = parameters(t₁).rot
+ offsets = parameters(t₂).offsets
+ Affine(rot, SVector(offsets))
+end
diff --git a/src/transforms/affine.jl b/src/transforms/affine.jl
index 45168d11f..bda86e237 100644
--- a/src/transforms/affine.jl
+++ b/src/transforms/affine.jl
@@ -15,11 +15,18 @@ Affine(Angle2d(π / 2), SVector(2, -2))
Affine([0 -1; 1 0], [-2, 2])
```
"""
-struct Affine{Dim,M<:StaticMatrix{Dim,Dim},V<:StaticVector{Dim}} <: CoordinateTransform
+struct Affine{Dim,M<:StaticMatrix{Dim,Dim},V<:StaticVector{Dim,<:Len}} <: CoordinateTransform
A::M
b::V
+ function Affine(A::StaticMatrix{Dim,Dim}, b::StaticVector{Dim,<:Len}) where {Dim}
+ fA = float(A)
+ fb = float(b)
+ new{Dim,typeof(fA),typeof(fb)}(fA, fb)
+ end
end
+Affine(A::StaticMatrix{Dim,Dim}, b::StaticVector{Dim}) where {Dim} = Affine(A, addunit(b, u"m"))
+
function Affine(A::AbstractMatrix, b::AbstractVector)
sz = size(A)
if !allequal(sz)
@@ -38,10 +45,7 @@ isaffine(::Type{<:Affine}) = true
isrevertible(t::Affine) = isinvertible(t)
-function isinvertible(t::Affine)
- d = det(t.A)
- !isapprox(d, zero(d), atol=atol(typeof(d)))
-end
+isinvertible(t::Affine) = !isapproxzero(det(t.A))
function inverse(t::Affine)
A = inv(t.A)
@@ -49,17 +53,39 @@ function inverse(t::Affine)
Affine(A, b)
end
-applycoord(t::Affine, v::Vec) = t.A * v
+applycoord(t::Affine, p::Point) = withcrs(p, muladd(t.A, to(p), t.b))
-applycoord(t::Affine, p::Point) = Point(t.A * coordinates(p) + t.b)
+applycoord(t::Affine, v::Vec) = t.A * v
# --------------
# SPECIAL CASES
# --------------
-applycoord(t::Affine, b::Box{2}) = applycoord(t, convert(Quadrangle, b))
+applycoord(t::Affine, b::Box) = TransformedGeometry(b, t)
+
+applycoord(t::Affine, b::Ball) = TransformedGeometry(b, t)
+
+applycoord(t::Affine, s::Sphere) = TransformedGeometry(s, t)
+
+applycoord(t::Affine, e::Ellipsoid) = TransformedGeometry(e, t)
+
+applycoord(t::Affine, d::Disk) = TransformedGeometry(d, t)
+
+applycoord(t::Affine, c::Circle) = TransformedGeometry(c, t)
+
+applycoord(t::Affine, c::Cylinder) = TransformedGeometry(c, t)
+
+applycoord(t::Affine, c::CylinderSurface) = TransformedGeometry(c, t)
+
+applycoord(t::Affine, p::ParaboloidSurface) = TransformedGeometry(p, t)
+
+applycoord(t::Affine, tr::Torus) = TransformedGeometry(tr, t)
+
+applycoord(t::Affine, g::RegularGrid) = TransformedGrid(g, t)
+
+applycoord(t::Affine, g::RectilinearGrid) = TransformedGrid(g, t)
-applycoord(t::Affine, b::Box{3}) = applycoord(t, convert(Hexahedron, b))
+applycoord(t::Affine, g::StructuredGrid) = TransformedGrid(g, t)
# -----------------
# HELPER FUNCTIONS
diff --git a/src/transforms/bridge.jl b/src/transforms/bridge.jl
index d192178a8..1ec9e1a30 100644
--- a/src/transforms/bridge.jl
+++ b/src/transforms/bridge.jl
@@ -13,31 +13,36 @@ via bridges of given width `δ` as described in Held 1998.
* Held. 1998. [FIST: Fast Industrial-Strength Triangulation of Polygons]
(https://link.springer.com/article/10.1007/s00453-001-0028-4)
"""
-struct Bridge{T} <: GeometricTransform
- δ::T
+struct Bridge{ℒ<:Len} <: GeometricTransform
+ δ::ℒ
+ Bridge(δ::ℒ) where {ℒ<:Len} = new{float(ℒ)}(δ)
end
-Bridge() = Bridge(0)
+Bridge(δ) = Bridge(addunit(δ, u"m"))
+
+Bridge() = Bridge(0.0u"m")
parameters(t::Bridge) = (; δ=t.δ)
-function apply(transform::Bridge, poly::PolyArea{Dim,T}) where {Dim,T}
+function apply(transform::Bridge, poly::PolyArea)
+ ℒ = lentype(poly)
+
# sort rings lexicographically
- rpoly, rinds = apply(Repair{9}(), poly)
+ rpoly, rinds = apply(Repair(9), poly)
# retrieve bridge width
- δ = T(transform.δ)
+ δ = convert(ℒ, transform.δ)
ring, dups = if hasholes(rpoly)
bridge(rings(rpoly), rinds, δ)
else
- first(rings(rpoly)), []
+ first(rings(rpoly)), Tuple{Int,Int}[]
end
PolyArea(ring), dups
end
-apply(::Bridge, poly::Ngon) = poly, []
+apply(::Bridge, poly::Ngon) = poly, Tuple{Int,Int}[]
function bridge(rings, rinds, δ)
# extract vertices and indices
@@ -45,22 +50,22 @@ function bridge(rings, rinds, δ)
vinds = rinds
# retrieve coordinate type
- T = coordtype(first(rings))
+ ℒ = lentype(first(rings))
# initialize outer boundary
- outer = verts[1]
+ outer = flat.(verts[1])
oinds = vinds[1]
# merge holes into outer boundary
for i in 2:length(verts)
- inner = verts[i]
+ inner = flat.(verts[i])
iinds = vinds[i]
# find closest pair of vertices (A, B)
# connecting outer and inner rings
omax = 0
imax = 0
- dmin = typemax(T)
+ dmin = typemax(ℒ)
for jₒ in 1:length(outer), jᵢ in 1:length(inner)
d = sum(abs, outer[jₒ] - inner[jᵢ])
if d < dmin
@@ -75,15 +80,15 @@ function bridge(rings, rinds, δ)
# direction and normal to segment A--B
v = B - A
u = Vec(-v[2], v[1])
- n = u / norm(u)
+ n = norm(u)
# the point A is split into A′ and A′′ and
# the point B is split into B′ and B′′ based
# on a given bridge width δ
- A′ = A + (δ / 2) * n
- A′′ = A - (δ / 2) * n
- B′ = B + (δ / 2) * n
- B′′ = B - (δ / 2) * n
+ A′ = A + (δ / 2n) * u
+ A′′ = A - (δ / 2n) * u
+ B′ = B + (δ / 2n) * u
+ B′′ = B - (δ / 2n) * u
# insert hole at closest vertex
outer = [
@@ -112,5 +117,11 @@ function bridge(rings, rinds, δ)
end
end
- Ring(outer), dups
+ points = map(outer) do p
+ C = crs(first(rings))
+ c = CoordRefSystems.raw(coords(p))
+ Point(CoordRefSystems.reconstruct(C, c))
+ end
+
+ Ring(points), dups
end
diff --git a/src/transforms/lengthunit.jl b/src/transforms/lengthunit.jl
new file mode 100644
index 000000000..7af2198a3
--- /dev/null
+++ b/src/transforms/lengthunit.jl
@@ -0,0 +1,76 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ LengthUnit(unit)
+
+Convert the length unit of coordinates of a geometry or domain to `unit`.
+
+## Examples
+
+```julia
+LengthUnit(u"cm")
+LengthUnit(u"km")
+```
+"""
+struct LengthUnit{U} <: CoordinateTransform
+ unit::U
+end
+
+parameters(t::LengthUnit) = (; unit=t.unit)
+
+applycoord(t::LengthUnit, p::Point) = Point(_lenunit(coords(p), t.unit))
+
+applycoord(t::LengthUnit, v::Vec) = uconvert.(t.unit, v)
+
+# --------------
+# SPECIAL CASES
+# --------------
+
+applycoord(t::LengthUnit, len::Len) = uconvert(t.unit, len)
+
+applycoord(t::LengthUnit, lens::NTuple{Dim,Len}) where {Dim} = uconvert.(t.unit, lens)
+
+function applycoord(t::LengthUnit, g::RegularGrid)
+ dims = size(g)
+ orig = applycoord(t, minimum(g))
+ spac = map(s -> applycoord(t, s), spacing(g))
+ offs = offset(g)
+ RegularGrid(dims, orig, spac, offs)
+end
+
+applycoord(t::LengthUnit, g::RectilinearGrid) = TransformedGrid(g, t)
+
+applycoord(t::LengthUnit, g::StructuredGrid) = TransformedGrid(g, t)
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function _lenunit(c::Cartesian, u)
+ d = datum(c)
+ v = CoordRefSystems.values(c)
+ Cartesian{d}(uconvert.(u, v))
+end
+
+function _lenunit(c::Polar, u)
+ d = datum(c)
+ ρ = uconvert(u, c.ρ)
+ Polar{d}(ρ, c.ϕ)
+end
+
+function _lenunit(c::Cylindrical, u)
+ d = datum(c)
+ ρ = uconvert(u, c.ρ)
+ z = uconvert(u, c.z)
+ Cylindrical{d}(ρ, c.ϕ, z)
+end
+
+function _lenunit(c::Spherical, u)
+ d = datum(c)
+ r = uconvert(u, c.r)
+ Spherical{d}(r, c.θ, c.ϕ)
+end
+
+_lenunit(c, _) = throw(ArgumentError("the length unit of $(prettyname(c)) cannot be changed"))
diff --git a/src/transforms/morphological.jl b/src/transforms/morphological.jl
new file mode 100644
index 000000000..5d10b944a
--- /dev/null
+++ b/src/transforms/morphological.jl
@@ -0,0 +1,48 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Morphological(fun)
+
+Morphological transform given by a function `fun`
+that maps the coordinates of a geometry or a domain
+to new coordinates (`coords -> newcoords`).
+
+## Examples
+
+```julia
+ball = Ball((0, 0), 1)
+ball |> Morphological(c -> Cartesian(c.x + c.y, c.y, c.x - c.y))
+
+triangle = Triangle(Point(LatLon(0, 0)), Point(LatLon(0, 45)), Point(LatLon(45, 0)))
+triangle |> Morphological(c -> LatLonAlt(c.lat, c.lon, 0.0m))
+```
+
+### Notes
+
+* By default, only the vertices of the polytopes are transformed,
+ disregarding distortions that occur in manifold conversions.
+ To handle this case, use [`TransformedGeometry`](@ref).
+"""
+struct Morphological{F<:Function} <: CoordinateTransform
+ fun::F
+end
+
+parameters(t::Morphological) = (; fun=t.fun)
+
+applycoord(t::Morphological, p::Point) = Point(t.fun(coords(p)))
+
+applycoord(::Morphological, v::Vec) = v
+
+# --------------
+# SPECIAL CASES
+# --------------
+
+applycoord(t::Morphological, g::Primitive) = TransformedGeometry(g, t)
+
+applycoord(t::Morphological, g::RegularGrid) = TransformedGrid(g, t)
+
+applycoord(t::Morphological, g::RectilinearGrid) = TransformedGrid(g, t)
+
+applycoord(t::Morphological, g::StructuredGrid) = TransformedGrid(g, t)
diff --git a/src/transforms/proj.jl b/src/transforms/proj.jl
new file mode 100644
index 000000000..6ca5ae2db
--- /dev/null
+++ b/src/transforms/proj.jl
@@ -0,0 +1,90 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Proj(CRS)
+ Proj(code)
+
+Convert the coordinates of geometry or domain to a given
+coordinate reference system `CRS` or EPSG/ESRI `code`.
+
+Optionally, the transform samples the `boundary` of
+polytopes, if this option is `true`, to handle distortions
+that occur in manifold conversions.
+
+## Examples
+
+```julia
+Proj(Polar)
+Proj(WebMercator)
+Proj(Mercator{WGS84Latest})
+Proj(EPSG{3395})
+Proj(ESRI{54017})
+```
+
+### Notes
+
+* By default, only the vertices of the polytopes are transformed,
+ disregarding distortions that occur in manifold conversions.
+ To handle this case, use [`TransformedGeometry`](@ref).
+"""
+struct Proj{CRS} <: CoordinateTransform end
+
+Proj(CRS) = Proj{CRS}()
+
+Proj(code::Type{<:EPSG}) = Proj(CoordRefSystems.get(code))
+
+Proj(code::Type{<:ESRI}) = Proj(CoordRefSystems.get(code))
+
+parameters(::Proj{CRS}) where {CRS} = (; CRS)
+
+# avoid constructing a new geometry or domain when the CRS is the same
+function apply(t::Proj{CRS}, g::GeometryOrDomain) where {CRS}
+ g′ = crs(g) <: CRS ? g : applycoord(t, g)
+ g′, nothing
+end
+
+# convert the CRS and preserve the manifold
+applycoord(::Proj{CRS}, p::Point{<:🌐}) where {CRS<:Basic} = Point{🌐}(convert(CRS, coords(p)))
+
+# convert the CRS and (possibly) change the manifold
+applycoord(::Proj{CRS}, p::Point{<:🌐}) where {CRS<:Projected} = _proj(CRS, p)
+applycoord(::Proj{CRS}, p::Point{<:🌐}) where {CRS<:Geographic} = _proj(CRS, p)
+applycoord(::Proj{CRS}, p::Point{<:𝔼}) where {CRS<:Basic} = _proj(CRS, p)
+applycoord(::Proj{CRS}, p::Point{<:𝔼}) where {CRS<:Projected} = _proj(CRS, p)
+applycoord(::Proj{CRS}, p::Point{<:𝔼}) where {CRS<:Geographic} = _proj(CRS, p)
+
+applycoord(::Proj, v::Vec) = v
+
+# --------------
+# SPECIAL CASES
+# --------------
+
+applycoord(t::Proj{<:Projected}, g::Primitive{<:🌐}) = TransformedGeometry(g, t)
+
+applycoord(t::Proj{<:Geographic}, g::Primitive{<:𝔼}) = TransformedGeometry(g, t)
+
+applycoord(t::Proj, g::RegularGrid) = TransformedGrid(g, t)
+
+applycoord(t::Proj, g::RectilinearGrid) = TransformedGrid(g, t)
+
+applycoord(t::Proj, g::StructuredGrid) = TransformedGrid(g, t)
+
+# -----------
+# IO METHODS
+# -----------
+
+Base.show(io::IO, ::Proj{CRS}) where {CRS} = print(io, "Proj(CRS: $CRS)")
+
+function Base.show(io::IO, ::MIME"text/plain", t::Proj{CRS}) where {CRS}
+ summary(io, t)
+ println(io)
+ print(io, "└─ CRS: $CRS")
+end
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+_proj(CRS, p) = Point(convert(CRS, coords(p)))
diff --git a/src/transforms/repair.jl b/src/transforms/repair.jl
index 086bc8e0f..d315e795f 100644
--- a/src/transforms/repair.jl
+++ b/src/transforms/repair.jl
@@ -3,7 +3,7 @@
# ------------------------------------------------------------------
"""
- Repair{K}
+ Repair(K)
Perform repairing operation with code `K`.
@@ -19,24 +19,28 @@ Perform repairing operation with code `K`.
- K = 7: faces are coherently oriented
- K = 8: zero-area ears are removed
- K = 9: rings of polygon are sorted
-- K = 10: outer rings are expanded
+- K = 10: outer rings of polygon are expanded
+- K = 11: rings of polygon are coherently oriented
+- K = 12: degenerate rings of polygon are removed
## Examples
```
# remove duplicates and degenerates
-mesh |> Repair{0}() |> Repair{3}()
+mesh |> Repair(0) |> Repair(3)
```
"""
struct Repair{K} <: GeometricTransform end
+Repair(K) = Repair{K}()
+
# --------------
# OPERATION (0)
# --------------
apply(::Repair{0}, geom::Polytope) = unique(geom), nothing
-apply(::Repair{0}, mesh::Mesh) = @error "not implemented"
+apply(::Repair{0}, mesh::Mesh) = error("not implemented")
# --------------
# OPERATION (1)
@@ -59,7 +63,7 @@ function apply(::Repair{1}, mesh::Mesh)
ntuple(i -> inds[elem[i]], length(elem))
end
- points = vertices(mesh)[seen]
+ points = [vertex(mesh, ind) for ind in seen]
connec = connect.(elems)
@@ -72,8 +76,7 @@ end
# OPERATION (7)
# --------------
-# HalfEdgeTopology constructor
-# performs orientation of faces
+# HalfEdgeTopology constructor performs orientation of faces
apply(::Repair{7}, mesh::Mesh) = topoconvert(HalfEdgeTopology, mesh), nothing
# --------------
@@ -95,16 +98,15 @@ function apply(::Repair{8}, ring::Ring)
Ring(v), nothing
end
-repair8(v) = repair8(collect(v))
-
repair8(v::AbstractVector) = repair8(CircularVector(v))
-function repair8(v::CircularVector{Point{Dim,T}}) where {Dim,T}
+function repair8(v::CircularVector{<:Point})
n = length(v)
keep = Int[]
for i in 1:n
t = Triangle(v[i - 1], v[i], v[i + 1])
- area(t) > atol(T)^2 && push!(keep, i)
+ a = area(t)
+ a > atol(a) && push!(keep, i)
end
isempty(keep) ? v[begin] : v[keep]
end
@@ -123,7 +125,7 @@ apply(::Repair{9}, poly::Ngon) = poly, []
function repair9(r::AbstractVector{<:Ring})
# sort vertices lexicographically
verts = vertices.(r)
- coord = coordinates.(reduce(vcat, verts))
+ coord = to.(reduce(vcat, verts))
vperm = sortperm(sortperm(coord))
# each ring has its own set of indices
@@ -163,8 +165,67 @@ function revert(::Repair{10}, poly::PolyArea, c)
PolyArea([o; r[2:end]])
end
-apply(::Repair{10}, poly::Ngon) = poly, nothing
+function _stretch10(g::Geometry)
+ T = numtype(lentype(g))
+ Stretch(ntuple(i -> one(T) + 10atol(T), embeddim(g)))
+end
+
+# ---------------
+# OPERATION (11)
+# ---------------
+
+function apply(::Repair{11}, poly::PolyArea)
+ r = rings(poly)
+
+ # fix orientation
+ ofix(r, o) = orientation(r) == o ? r : reverse(r)
+ outer = ofix(first(r), CCW)
+ inners = ofix.(r[2:end], CW)
-revert(::Repair{10}, poly::Ngon, cache) = poly
+ PolyArea([outer; inners]), nothing
+end
-_stretch10(g::Geometry{Dim,T}) where {Dim,T} = Stretch(ntuple(i -> T(1) + 10atol(T), Dim))
+# ---------------
+# OPERATION (12)
+# ---------------
+
+function apply(::Repair{12}, poly::PolyArea)
+ r = rings(poly)
+
+ # fix degeneracy
+ oring = first(r)
+ outer = if nvertices(oring) == 2
+ A, B = vertices(oring)
+ P = centroid(Segment(A, B))
+ Ring(A, P, B)
+ else
+ oring
+ end
+
+ # remove degenerated rings
+ inners = filter(r -> nvertices(r) > 2, r[2:end])
+
+ PolyArea([outer; inners]), nothing
+end
+
+# ----------
+# FALLBACKS
+# ----------
+
+apply(::Repair, geom::Geometry) = geom, nothing
+
+apply(t::Repair, multi::Multi) = Multi([t(g) for g in parent(multi)]), nothing
+
+apply(t::Repair, dom::Domain) = GeometrySet([t(g) for g in dom]), nothing
+
+# -----------
+# IO METHODS
+# -----------
+
+Base.show(io::IO, ::Repair{K}) where {K} = print(io, "Repair(K: $K)")
+
+function Base.show(io::IO, ::MIME"text/plain", t::Repair{K}) where {K}
+ summary(io, t)
+ println(io)
+ print(io, "└─ K: $K")
+end
diff --git a/src/transforms/rotate.jl b/src/transforms/rotate.jl
index 88c5a6818..ac9660266 100644
--- a/src/transforms/rotate.jl
+++ b/src/transforms/rotate.jl
@@ -3,52 +3,39 @@
# ------------------------------------------------------------------
"""
- Rotate(rot)
+ Rotate(R)
-Rotate geometry or mesh with rotation `rot`
-from Rotations.jl.
+Rotate geometry or domain with rotation `R` from Rotations.jl.
-## Examples
-
-```julia
-Rotate(one(RotXYZ{Float64})) # Generate identity rotation
-Rotate(AngleAxis(0.2, 1.0, 0.0, 0.0)) # Rotate 0.2 radians around X-axis
-Rotate(rand(QuatRotation{Float64})) # Generate random rotation
-```
-"""
-struct Rotate{R<:Rotation} <: CoordinateTransform
- rot::R
-end
-
-"""
Rotate(u, v)
Rotation mapping the axis directed by `u` to the axis directed by `v`.
More precisely, it maps the plane passing through the origin with normal
vector `u` to the plane passing through the origin with normal vector `v`.
-## Examples
-
-```julia
-Rotate(Vec(1, 0, 0), Vec(1, 1, 1))
-```
-"""
-Rotate(u::Vec, v::Vec) = Rotate(rotation_between(u, v))
-
-Rotate(u::Tuple, v::Tuple) = Rotate(Vec(u), Vec(v))
-
-"""
Rotate(θ)
-Rotate the 2D geometry or mesh by angle `θ`, in radians,
-using the `Angle2d` rotation.
+Rotate the 2D geometry or domain by angle `θ`, in radians, using the
+`Angle2d` rotation.
## Examples
```julia
-Rotate(π / 2)
+Rotate(one(RotXYZ{Float64})) # identity rotation
+Rotate(AngleAxis(0.2, 1.0, 0.0, 0.0)) # rotate 0.2 radians around X axis
+Rotate(rand(QuatRotation{Float64})) # random rotation
+Rotate(Vec(1, 0, 0), Vec(1, 1, 1)) # rotation from (1, 0, 0) to (1, 1, 1)
+Rotate(π / 2) # 2D rotation with angle in radians
```
"""
+struct Rotate{R<:Rotation} <: CoordinateTransform
+ rot::R
+end
+
+Rotate(u::Vec, v::Vec) = Rotate(urotbetween(u, v))
+
+Rotate(u::Tuple, v::Tuple) = Rotate(Vec(u), Vec(v))
+
Rotate(θ) = Rotate(Angle2d(θ))
parameters(t::Rotate) = (; rot=t.rot)
@@ -61,14 +48,20 @@ isinvertible(::Type{<:Rotate}) = true
inverse(t::Rotate) = Rotate(inv(t.rot))
-applycoord(t::Rotate, v::Vec) = t.rot * v
+applycoord(t::Rotate, p::Point) = withcrs(p, applycoord(t, to(p)))
+
+applycoord(t::Rotate, v::Vec) = urotapply(t.rot, v)
# --------------
# SPECIAL CASES
# --------------
-applycoord(t::Rotate, b::Box{2}) = applycoord(t, convert(Quadrangle, b))
+applycoord(t::Rotate, b::Box) = TransformedGeometry(b, t)
+
+applycoord(t::Rotate, e::Ellipsoid) = Ellipsoid(radii(e), applycoord(t, center(e)), t.rot * rotation(e))
+
+applycoord(t::Rotate, g::RegularGrid) = TransformedGrid(g, t)
-applycoord(t::Rotate, b::Box{3}) = applycoord(t, convert(Hexahedron, b))
+applycoord(t::Rotate, g::RectilinearGrid) = TransformedGrid(g, t)
-applycoord(t::Rotate, g::CartesianGrid) = TransformedGrid(g, t)
+applycoord(t::Rotate, g::StructuredGrid) = TransformedGrid(g, t)
diff --git a/src/transforms/scale.jl b/src/transforms/scale.jl
index bac7c88b6..b5fc489d8 100644
--- a/src/transforms/scale.jl
+++ b/src/transforms/scale.jl
@@ -38,16 +38,50 @@ isinvertible(::Type{<:Scale}) = true
inverse(t::Scale) = Scale(1 ./ t.factors)
+applycoord(t::Scale, p::Point) = withcrs(p, applycoord(t, to(p)))
+
applycoord(t::Scale, v::Vec) = t.factors .* v
# --------------
# SPECIAL CASES
# --------------
-function applycoord(t::Scale, g::CartesianGrid)
+applycoord(t::Scale, b::Ball) = TransformedGeometry(b, t)
+
+applycoord(t::Scale{1}, b::Ball) = Ball(applycoord(t, center(b)), t.factors[1] * radius(b))
+
+applycoord(t::Scale, s::Sphere) = TransformedGeometry(s, t)
+
+applycoord(t::Scale{1}, s::Sphere) = Sphere(applycoord(t, center(s)), t.factors[1] * radius(s))
+
+applycoord(t::Scale{3}, s::Sphere{𝔼{3}}) = Ellipsoid(t.factors .* radius(s), applycoord(t, center(s)))
+
+applycoord(t::Scale, e::Ellipsoid) = TransformedGeometry(e, t)
+
+applycoord(t::Scale{1}, e::Ellipsoid) = Ellipsoid(t.factors[1] .* radii(e), applycoord(t, center(e)), rotation(e))
+
+applycoord(t::Scale, d::Disk) = TransformedGeometry(d, t)
+
+applycoord(t::Scale, c::Circle) = TransformedGeometry(c, t)
+
+applycoord(t::Scale, c::Cylinder) = TransformedGeometry(c, t)
+
+applycoord(t::Scale, c::CylinderSurface) = TransformedGeometry(c, t)
+
+applycoord(t::Scale, p::ParaboloidSurface) = TransformedGeometry(p, t)
+
+applycoord(t::Scale, tr::Torus) = TransformedGeometry(tr, t)
+
+function applycoord(t::Scale, g::RegularGrid)
dims = size(g)
orig = applycoord(t, minimum(g))
spac = t.factors .* spacing(g)
offs = offset(g)
- CartesianGrid(dims, orig, spac, offs)
+ RegularGrid(dims, orig, spac, offs)
end
+
+applycoord(t::Scale, g::RectilinearGrid) =
+ RectilinearGrid{manifold(g),crs(g)}(ntuple(i -> t.factors[i] * xyz(g)[i], paramdim(g)))
+
+applycoord(t::Scale, g::StructuredGrid) =
+ StructuredGrid{manifold(g),crs(g)}(ntuple(i -> t.factors[i] * XYZ(g)[i], paramdim(g)))
diff --git a/src/transforms/shadow.jl b/src/transforms/shadow.jl
new file mode 100644
index 000000000..777cddaf1
--- /dev/null
+++ b/src/transforms/shadow.jl
@@ -0,0 +1,77 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Shadow(dims)
+
+Project the geometry or domain onto the given `dims`,
+producing a "shadow" of the original object.
+
+## Examples
+
+```julia
+Shadow(:xy)
+Shadow("xz")
+Shadow(1, 2)
+Shadow((1, 3))
+```
+"""
+struct Shadow{Dim} <: GeometricTransform
+ dims::Dims{Dim}
+end
+
+Shadow(dims::Int...) = Shadow(dims)
+
+Shadow(dims::AbstractString) = Shadow(Dims(_index(d) for d in dims))
+
+Shadow(dims::Symbol) = Shadow(string(dims))
+
+parameters(t::Shadow) = (; dims=t.dims)
+
+apply(t::Shadow, v::Vec) = v[_sort(t.dims)], nothing
+
+function apply(t::Shadow, g::GeometryOrDomain)
+ dims = _sort(t.dims)
+ m = Morphological() do coords
+ cart = convert(Cartesian, coords)
+ vals = CoordRefSystems.values(cart)
+ Cartesian{datum(coords)}(vals[dims])
+ end
+ apply(m, g)
+end
+
+# --------------
+# SPECIAL CASES
+# --------------
+
+apply(t::Shadow, b::Box{<:𝔼}) = Box(t(minimum(b)), t(maximum(b))), nothing
+
+apply(::Shadow, ::Plane) = throw(ArgumentError("Shadow transform doesn't yet support planes"))
+
+function apply(t::Shadow, g::CartesianGrid)
+ dims = _sort(t.dims)
+ sz = size(g)[dims]
+ or = t(minimum(g))
+ sp = spacing(g)[dims]
+ of = offset(g)[dims]
+ CartesianGrid(sz, or, sp, of), nothing
+end
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function _index(d)
+ if d == 'x'
+ 1
+ elseif d == 'y'
+ 2
+ elseif d == 'z'
+ 3
+ else
+ throw(ArgumentError("'$d' isn't a valid dimension name"))
+ end
+end
+
+_sort(dims) = sort(SVector(dims))
diff --git a/src/transforms/slice.jl b/src/transforms/slice.jl
new file mode 100644
index 000000000..d61d607e2
--- /dev/null
+++ b/src/transforms/slice.jl
@@ -0,0 +1,85 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ Slice(x=(xmin, xmax), y=(ymin, ymax), z=(zmin, zmax))
+ Slice(lat=(latmin, latmax), lon=(lonmin, lonmax))
+
+Retain the domain elements within `x` limits [`xmax`,`xmax`],
+`y` limits [`ymax`,`ymax`] and `z` limits [`zmin`,`zmax`]
+in length units (default to meters), or within `lat` limits
+[`latmin`,`latmax`] and `lon` limits [`lonmin`,`lonmax`]
+in degree units.
+
+## Examples
+
+```julia
+Slice(x=(1000km, 3000km))
+Slice(x=(1000km, 2000km), y=(2000km, 5000km))
+Slice(lon=(0°, 90°))
+Slice(lon=(0°, 45°), lat=(0°, 45°))
+```
+"""
+struct Slice{T} <: GeometricTransform
+ limits::T
+end
+
+Slice(; kwargs...) = Slice(values(kwargs))
+
+parameters(t::Slice) = (; limits=t.limits)
+
+preprocess(t::Slice, d::Domain) = _sliceinds(d, _slicebox(boundingbox(d), t.limits))
+
+apply(t::Slice, d::Domain) = _slice(d, preprocess(t, d)), nothing
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+_slice(d::Domain, inds) = view(d, inds)
+_slice(g::Grid, inds::CartesianIndices) = getindex(g, inds)
+
+_sliceinds(d::Domain, b) = indices(d, b)
+_sliceinds(g::OrthoRegularGrid, b) = cartesianrange(g, b)
+_sliceinds(g::OrthoRectilinearGrid, b) = cartesianrange(g, b)
+_sliceinds(g::Grid{🌐}, b::Box{🌐}) = cartesianrange(g, b)
+
+function _slicebox(box::Box{𝔼{2}}, limits)
+ min = convert(Cartesian, coords(minimum(box)))
+ max = convert(Cartesian, coords(maximum(box)))
+ xmin, xmax = get(limits, :x, (min.x, max.x))
+ ymin, ymax = get(limits, :y, (min.y, max.y))
+ bmin = _aslen.((xmin, ymin))
+ bmax = _aslen.((xmax, ymax))
+ Box(withcrs(box, bmin), withcrs(box, bmax))
+end
+
+function _slicebox(box::Box{𝔼{3}}, limits)
+ min = convert(Cartesian, coords(minimum(box)))
+ max = convert(Cartesian, coords(maximum(box)))
+ xmin, xmax = get(limits, :x, (min.x, max.x))
+ ymin, ymax = get(limits, :y, (min.y, max.y))
+ zmin, zmax = get(limits, :z, (min.z, max.z))
+ bmin = _aslen.((xmin, ymin, zmin))
+ bmax = _aslen.((xmax, ymax, zmax))
+ Box(withcrs(box, bmin), withcrs(box, bmax))
+end
+
+function _slicebox(box::Box{🌐}, limits)
+ min = convert(LatLon, coords(minimum(box)))
+ max = convert(LatLon, coords(maximum(box)))
+ latmin, latmax = get(limits, :lat, (min.lat, max.lat))
+ lonmin, lonmax = get(limits, :lon, (min.lon, max.lon))
+ bmin = _asdeg.((latmin, lonmin))
+ bmax = _asdeg.((latmax, lonmax))
+ Box(withcrs(box, bmin, LatLon), withcrs(box, bmax, LatLon))
+end
+
+_aslen(x::Len) = float(x)
+_aslen(x::Number) = float(x) * u"m"
+_aslen(::Quantity) = throw(ArgumentError("invalid units, please check the documentation"))
+
+_asdeg(x::Deg) = float(x)
+_asdeg(x::Number) = float(x) * u"°"
+_asdeg(::Quantity) = throw(ArgumentError("invalid units, please check the documentation"))
diff --git a/src/transforms/smoothing.jl b/src/transforms/smoothing.jl
index a650d8794..f707bf947 100644
--- a/src/transforms/smoothing.jl
+++ b/src/transforms/smoothing.jl
@@ -40,14 +40,14 @@ function revert(transform::LambdaMuSmoothing, mesh::Mesh, cache)
_smooth(mesh, L, n, λ, μ, revert=true)
end
-_laplacian(mesh) = laplacematrix(mesh, weights=:uniform)
+_laplacian(mesh) = laplacematrix(mesh, kind=:uniform)
function _smooth(mesh, L, n, λ, μ; revert=false)
# retrieve vertices
- points = vertices(mesh)
+ points = eachvertex(mesh)
# matrix with coordinates (nvertices x ndims)
- X = reduce(hcat, coordinates.(points)) |> transpose
+ X = reduce(hcat, to.(points)) |> transpose
# choose between apply and revert mode
λ₁, λ₂ = revert ? (-μ, -λ) : (λ, μ)
@@ -66,7 +66,7 @@ function _smooth(mesh, L, n, λ, μ; revert=false)
end
"""
-LaplaceSmoothing(n, λ=0.5)
+ LaplaceSmoothing(n, λ=0.5)
Perform `n` iterations of Laplace smoothing with parameter `λ`.
@@ -78,7 +78,7 @@ Perform `n` iterations of Laplace smoothing with parameter `λ`.
LaplaceSmoothing(n, λ=0.5) = LambdaMuSmoothing(n, λ, zero(λ))
"""
-TaubinSmoothing(n, λ=0.5)
+ TaubinSmoothing(n, λ=0.5)
Perform `n` iterations of Taubin smoothing with parameter `0 < λ < 1`.
diff --git a/src/transforms/stdcoords.jl b/src/transforms/stdcoords.jl
index c01ac39b7..2ce7baa4d 100644
--- a/src/transforms/stdcoords.jl
+++ b/src/transforms/stdcoords.jl
@@ -34,7 +34,7 @@ reapply(t::StdCoords, g::GeometryOrDomain, c) = reapply(c[1], g, c[2])
function _stdcoords(t, g)
b = boundingbox(g)
- t = Translate(coordinates(center(b))...)
- s = Scale(sides(b))
+ t = Translate(to(centroid(b))...)
+ s = Scale(ustrip.(sides(b)))
inverse(t) → inverse(s)
end
diff --git a/src/transforms/stretch.jl b/src/transforms/stretch.jl
index 6eb6cb96a..0b79e553e 100644
--- a/src/transforms/stretch.jl
+++ b/src/transforms/stretch.jl
@@ -42,17 +42,9 @@ function apply(t::Stretch, g::GeometryOrDomain)
n, (p, c)
end
-revert(t::Stretch, g::GeometryOrDomain, c) = revert(c[1], g, c[2])
+revert(::Stretch, g::GeometryOrDomain, c) = revert(c[1], g, c[2])
-reapply(t::Stretch, g::GeometryOrDomain, c) = reapply(c[1], g, c[2])
-
-function _stretch(t, g)
- o = coordinates(_origin(g))
- Translate(-o...) → Scale(t.factors) → Translate(o...)
-end
-
-_origin(g) = centroid(g)
-_origin(p::Plane) = p(0, 0)
+reapply(::Stretch, g::GeometryOrDomain, c) = reapply(c[1], g, c[2])
# --------------
# SPECIAL CASES
@@ -63,3 +55,15 @@ apply(t::Stretch, v::Vec) = apply(Scale(t.factors), v)
revert(t::Stretch, v::Vec, c) = revert(Scale(t.factors), v, c)
reapply(t::Stretch, v::Vec, c) = reapply(Scale(t.factors), v, c)
+
+# -----------------
+# HELPER FUNCTIONS
+# -----------------
+
+function _stretch(t, g)
+ o = to(_origin(g))
+ Translate(-o...) → Scale(t.factors) → Translate(o...)
+end
+
+_origin(g) = centroid(g)
+_origin(p::Plane) = p(0, 0)
diff --git a/src/transforms/translate.jl b/src/transforms/translate.jl
index b48b7de77..083d2e843 100644
--- a/src/transforms/translate.jl
+++ b/src/transforms/translate.jl
@@ -8,10 +8,15 @@
Translate coordinates of geometry or mesh by
given offsets `o₁, o₂, ...`.
"""
-struct Translate{Dim,T} <: CoordinateTransform
- offsets::NTuple{Dim,T}
+struct Translate{Dim,ℒ<:Len} <: CoordinateTransform
+ offsets::NTuple{Dim,ℒ}
+ Translate(offsets::NTuple{Dim,ℒ}) where {Dim,ℒ<:Len} = new{Dim,float(ℒ)}(offsets)
end
+Translate(offsets::NTuple{Dim,Len}) where {Dim} = Translate(promote(offsets...))
+
+Translate(offsets::Tuple) = Translate(addunit.(offsets, u"m"))
+
Translate(offsets...) = Translate(offsets)
parameters(t::Translate) = (; offsets=t.offsets)
@@ -24,20 +29,24 @@ isinvertible(::Type{<:Translate}) = true
inverse(t::Translate) = Translate(-1 .* t.offsets)
-applycoord(t::Translate, v::Vec) = v
-
applycoord(t::Translate, p::Point) = p + Vec(t.offsets)
-# ----------------
-# SPECIALIZATIONS
-# ----------------
+applycoord(::Translate, v::Vec) = v
+
+# --------------
+# SPECIAL CASES
+# --------------
+
+applycoord(t::Translate, g::RegularGrid) = TransformedGrid(g, t)
+
+applycoord(t::Translate, g::OrthoRegularGrid) = RegularGrid(size(g), applycoord(t, minimum(g)), spacing(g), offset(g))
-apply(t::Translate{Dim}, g::RectilinearGrid{Dim}) where {Dim} =
- RectilinearGrid(ntuple(i -> xyz(g)[i] .+ t.offsets[i], Dim)), nothing
+applycoord(t::Translate, g::RectilinearGrid) = TransformedGrid(g, t)
-revert(t::Translate, g::RectilinearGrid, c) = first(apply(inverse(t), g))
+applycoord(t::Translate, g::OrthoRectilinearGrid) =
+ RectilinearGrid{manifold(g),crs(g)}(ntuple(i -> xyz(g)[i] .+ t.offsets[i], paramdim(g)))
-apply(t::Translate{Dim}, g::StructuredGrid{Dim}) where {Dim} =
- StructuredGrid(ntuple(i -> XYZ(g)[i] .+ t.offsets[i], Dim)), nothing
+applycoord(t::Translate, g::StructuredGrid) = TransformedGrid(g, t)
-revert(t::Translate, g::StructuredGrid, c) = first(apply(inverse(t), g))
+applycoord(t::Translate, g::OrthoStructuredGrid) =
+ StructuredGrid{manifold(g),crs(g)}(ntuple(i -> XYZ(g)[i] .+ t.offsets[i], paramdim(g)))
diff --git a/src/traversing/multigrid.jl b/src/traversing/multigrid.jl
index e73a406db..04a649f6d 100644
--- a/src/traversing/multigrid.jl
+++ b/src/traversing/multigrid.jl
@@ -10,7 +10,8 @@ the coarsest scale and moves to progressively finer scales.
"""
struct MultiGridPath <: Path end
-function traverse(grid::Grid{Dim}, ::MultiGridPath) where {Dim}
+function traverse(grid::Grid, ::MultiGridPath)
+ Dim = embeddim(grid)
dims = size(grid)
nelems = prod(dims)
linear = LinearIndices(dims)
diff --git a/src/traversing/source.jl b/src/traversing/source.jl
index 47cdf821f..2fb4854d5 100644
--- a/src/traversing/source.jl
+++ b/src/traversing/source.jl
@@ -18,12 +18,13 @@ SourcePath(sources) = SourcePath(sources, 10^3)
function traverse(domain, path::SourcePath)
sources = path.sources
batchsize = path.batchsize
- @assert allunique(sources) "non-unique sources"
- @assert all(1 .≤ sources .≤ nelements(domain)) "sources must be valid locations"
- @assert length(sources) ≤ nelements(domain) "more sources than points in object"
+ assertion(allunique(sources), "non-unique sources")
+ assertion(all(1 .≤ sources .≤ nelements(domain)), "sources must be valid locations")
+ assertion(length(sources) ≤ nelements(domain), "more sources than points in object")
# fit search tree
- kdtree = KDTree(coordinates.([centroid(domain, s) for s in sources]))
+ xs = [ustrip.(to(centroid(domain, s))) for s in sources]
+ kdtree = KDTree(xs)
# other locations that are not sources
others = setdiff(1:nelements(domain), sources)
@@ -34,7 +35,7 @@ function traverse(domain, path::SourcePath)
# compute distances to sources
dists = []
for batch in batches
- coords = coordinates.([centroid(domain, b) for b in batch])
+ coords = [ustrip.(to(centroid(domain, b))) for b in batch]
_, ds = knn(kdtree, coords, length(sources), true)
append!(dists, ds)
end
diff --git a/src/units.jl b/src/units.jl
new file mode 100644
index 000000000..bf5126b30
--- /dev/null
+++ b/src/units.jl
@@ -0,0 +1,46 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+# helper type alias
+const Len{T} = Quantity{T,u"𝐋"}
+const Area{T} = Quantity{T,u"𝐋^2"}
+const Vol{T} = Quantity{T,u"𝐋^3"}
+const Met{T} = Quantity{T,u"𝐋",typeof(u"m")}
+const Deg{T} = Quantity{T,NoDims,typeof(u"°")}
+
+"""
+ addunit(x, u)
+
+Adds the unit only if the argument is not a quantity, otherwise an error is thrown.
+"""
+addunit(x::Number, u) = x * u
+addunit(x::AbstractArray{<:Number}, u) = x * u
+addunit(::Quantity, _) = throw(ArgumentError("invalid units, please check the documentation"))
+addunit(::AbstractArray{<:Quantity}, _) = throw(ArgumentError("invalid units, please check the documentation"))
+
+"""
+ numconvert(T, x)
+
+Converts the number type of quantity `x` to `T`.
+"""
+numconvert(::Type{T}, x::Quantity{S,D,U}) where {T,S,D,U} = convert(Quantity{T,D,U}, x)
+
+"""
+ withunit(x, u)
+
+Adds the unit if the argument is not a quantity,
+otherwise, converts the unit of `x` to `u`.
+"""
+withunit(x::Number, u) = x * u
+withunit(x::Quantity, u) = uconvert(u, x)
+
+"""
+ aslentype(T)
+
+If `T` has length unit, return it. If `T` is number, return it with meter unit.
+Otherwise, throw an error.
+"""
+aslentype(::Type{T}) where {T<:Len} = T
+aslentype(::Type{T}) where {T<:Number} = Met{T}
+aslentype(::Type{<:Quantity}) = throw(ArgumentError("invalid units, please use a valid length unit"))
diff --git a/src/utils.jl b/src/utils.jl
index dc4c7eaed..adc3a611f 100644
--- a/src/utils.jl
+++ b/src/utils.jl
@@ -5,133 +5,9 @@
# auxiliary type for dispatch purposes
const GeometryOrDomain = Union{Geometry,Domain}
-"""
- fitdims(dims, D)
-
-Fit tuple `dims` to a given length `D` by repeating the last dimension.
-"""
-function fitdims(dims::Dims{N}, D) where {N}
- ntuple(i -> i ≤ N ? dims[i] : last(dims), D)
-end
-
-"""
- collectat(iter, inds)
-
-Collect iterator `iter` at indices `inds` without materialization.
-"""
-function collectat(iter, inds)
- if isempty(inds)
- eltype(iter)[]
- else
- selectat(inds) = enumerate ⨟ TakeWhile(x -> first(x) ≤ last(inds)) ⨟ Filter(y -> first(y) ∈ inds) ⨟ Map(last)
- iter |> selectat(inds) |> tcollect
- end
-end
-
-"""
- signarea(A, B, C)
-
-Compute signed area of triangle formed by points `A`, `B` and `C`.
-"""
-function signarea(A::Point{2}, B::Point{2}, C::Point{2})
- ((B - A) × (C - A)) / 2
-end
-
-"""
- householderbasis(n)
-
-Returns a pair of orthonormal tangent vectors `u` and `v` from a normal `n`,
-such that `u`, `v`, and `n` form a right-hand orthogonal system.
-
-## References
-
-* D.S. Lopes et al. 2013. ["Tangent vectors to a 3-D surface normal: A geometric tool
- to find orthogonal vectors based on the Householder transformation"]
- (https://doi.org/10.1016/j.cad.2012.11.003)
-"""
-function householderbasis(n::Vec{3,T}) where {T}
- n̂ = norm(n)
- i = argmax(n .+ n̂)
- eᵢ = Vec(ntuple(j -> j == i ? T(1) : T(0), 3))
- h = n + n̂ * eᵢ
- H = I - 2h * transpose(h) / (transpose(h) * h)
- u, v = [H[:, j] for j in 1:3 if j != i]
- i == 2 && ((u, v) = (v, u))
- Vec(u), Vec(v)
-end
-
-"""
- svdbasis(points)
-
-Returns the 2D basis that retains most of the variance in the list of 3D `points`
-using the singular value decomposition (SVD).
-
-See .
-"""
-function svdbasis(p::AbstractVector{Point{3,T}}) where {T}
- X = reduce(hcat, coordinates.(p))
- μ = sum(X, dims=2) / size(X, 2)
- Z = X .- μ
- U = svd(Z).U
- u = Vec(U[:, 1]...)
- v = Vec(U[:, 2]...)
- n = Vec{3,T}(0, 0, 1)
- (u × v) ⋅ n < 0 ? (v, u) : (u, v)
-end
-
-"""
- mayberound(λ, x, tol)
-
-Round `λ` to `x` if it is within the tolerance `tol`.
-"""
-function mayberound(λ::T, x::T, atol=atol(T)) where {T}
- isapprox(λ, x, atol=atol) ? x : λ
-end
-
-"""
- intersectparameters(a, b, c, d)
-
-Compute the parameters `λ₁` and `λ₂` of the lines
-`a + λ₁ ⋅ v⃗₁`, with `v⃗₁ = b - a` and
-`c + λ₂ ⋅ v⃗₂`, with `v⃗₂ = d - c` spanned by the input
-points `a`, `b` resp. `c`, `d` such that to yield line
-points with minimal distance or the intersection point
-(if lines intersect).
-
-Furthermore, the ranks `r` of the matrix of the linear
-system `A ⋅ λ⃗ = y⃗`, with `A = [v⃗₁ -v⃗₂], y⃗ = c - a`
-and the rank `rₐ` of the augmented matrix `[A y⃗]` are
-calculated in order to identify the intersection type:
-
-- Intersection: r == rₐ == 2
-- Colinear: r == rₐ == 1
-- No intersection: r != rₐ
- - No intersection and parallel: r == 1, rₐ == 2
- - No intersection, skew lines: r == 2, rₐ == 3
-"""
-function intersectparameters(a::Point{Dim,T}, b::Point{Dim,T}, c::Point{Dim,T}, d::Point{Dim,T}) where {Dim,T}
- A = [(b - a) (c - d)]
- y = c - a
-
- # calculate the rank of the augmented matrix by checking
- # the zero entries of the diagonal of R
- _, R = qr([A y])
-
- # for Dim == 2 one has to check the L1 norm of rows as
- # there are more columns than rows
- τ = atol(T)
- rₐ = sum(>(τ), sum(abs, R, dims=2))
-
- # calculate the rank of the rectangular matrix
- r = sum(>(τ), sum(abs, view(R, :, 1:2), dims=2))
-
- # calculate parameters of intersection or closest point
- if r ≥ 2
- λ = A \ y
- λ₁, λ₂ = λ[1], λ[2]
- else # parallel or collinear
- λ₁, λ₂ = zero(T), zero(T)
- end
-
- λ₁, λ₂, r, rₐ
-end
+include("utils/basic.jl")
+include("utils/assert.jl")
+include("utils/cmp.jl")
+include("utils/units.jl")
+include("utils/crs.jl")
+include("utils/misc.jl")
diff --git a/src/utils/assert.jl b/src/utils/assert.jl
new file mode 100644
index 000000000..6d187cf1f
--- /dev/null
+++ b/src/utils/assert.jl
@@ -0,0 +1,19 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ assertion(cond, msg)
+
+Throws an `AssertionError(msg)` if `cond` is `false`.
+"""
+assertion(cond, msg) = cond || throw(AssertionError(msg))
+
+"""
+ checkdim(geom, dim)
+
+Throws an `ArgumentError` if the `embeddim` of the geometry `geom`
+is different than the specified dimension `dim`.
+"""
+checkdim(geom, dim) =
+ embeddim(geom) ≠ dim && throw(ArgumentError("geometry must be embedded in $dim-dimensional space"))
diff --git a/src/utils/basic.jl b/src/utils/basic.jl
new file mode 100644
index 000000000..e1b015914
--- /dev/null
+++ b/src/utils/basic.jl
@@ -0,0 +1,57 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ constructor(G)
+
+Given a (parametric) type `G{T₁,T₂,...}`, return the type `G`.
+"""
+constructor(G::Type) = getfield(Meshes, nameof(G))
+
+"""
+ fitdims(dims, D)
+
+Fit tuple `dims` to a given length `D` by repeating the last dimension.
+"""
+function fitdims(dims::Dims{N}, D) where {N}
+ ntuple(i -> i ≤ N ? dims[i] : last(dims), D)
+end
+
+"""
+ collectat(iter, inds)
+
+Collect iterable `iter` at indices `inds` efficiently.
+"""
+function collectat(iter, inds)
+ if isempty(inds)
+ eltype(iter)[]
+ else
+ m = maximum(inds)
+ e = Iterators.enumerate(iter)
+ w = Iterators.takewhile(x -> (first(x) ≤ m), e)
+ f = Iterators.filter(x -> (first(x) ∈ inds), w)
+ map(last, f)
+ end
+end
+
+collectat(vec::AbstractVector, inds) = vec[inds]
+
+"""
+ XYZ(xyz)
+
+Generate the coordinate arrays `XYZ` from the coordinate vectors `xyz`.
+"""
+@generated function XYZ(xyz::NTuple{Dim,AbstractVector}) where {Dim}
+ exprs = ntuple(Dim) do d
+ quote
+ a = xyz[$d]
+ A = Array{eltype(a),Dim}(undef, length.(xyz))
+ @nloops $Dim i A begin
+ @nref($Dim, A, i) = a[$(Symbol(:i_, d))]
+ end
+ A
+ end
+ end
+ Expr(:tuple, exprs...)
+end
diff --git a/src/utils/cmp.jl b/src/utils/cmp.jl
new file mode 100644
index 000000000..9f7f0f2e4
--- /dev/null
+++ b/src/utils/cmp.jl
@@ -0,0 +1,26 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+# Comparisons between unitful and non-unitful quantities
+
+isequalzero(x) = x == zero(x)
+isequalone(x) = x == oneunit(x)
+
+isapproxequal(x, y; atol=atol(x), kwargs...) = isapprox(x, y; atol, kwargs...)
+isapproxzero(x; atol=atol(x), kwargs...) = isapprox(x, zero(x); atol, kwargs...)
+isapproxone(x; atol=atol(x), kwargs...) = isapprox(x, oneunit(x); atol, kwargs...)
+
+ispositive(x) = x > zero(x)
+isnegative(x) = x < zero(x)
+isnonpositive(x) = x ≤ zero(x)
+isnonnegative(x) = x ≥ zero(x)
+
+"""
+ mayberound(λ, x, tol)
+
+Round `λ` to `x` if it is within the tolerance `tol`.
+"""
+function mayberound(λ::T, x::T, atol=atol(T)) where {T}
+ isapprox(λ, x, atol=atol) ? x : λ
+end
diff --git a/src/utils/crs.jl b/src/utils/crs.jl
new file mode 100644
index 000000000..297607901
--- /dev/null
+++ b/src/utils/crs.jl
@@ -0,0 +1,83 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ withcrs(g, coords, CRS=Cartesian)
+
+Point with the same CRS of `g` from another point with `coords` in given `CRS`.
+"""
+function withcrs(g::GeometryOrDomain, coords::Tuple, ::Type{CRS}) where {CRS}
+ M = manifold(g)
+ C = crs(g)
+ D = datum(C)
+ c = convert(C, CRS{D}(coords...))
+ Point{M}(c)
+end
+
+withcrs(g::GeometryOrDomain, coords::Tuple) = withcrs(g, coords, Cartesian)
+
+"""
+ withcrs(g, v)
+
+Point at the end of the vector `v` with the same CRS of `g`.
+"""
+withcrs(g::GeometryOrDomain, v::StaticVector) = withcrs(g, Tuple(v), Cartesian)
+
+"""
+ flat(p)
+
+Flatten coordinates of point `p` to Cartesian coordinates,
+ignoring the original units of the coordinate reference system.
+"""
+flat(p::Point) = Point(flat(coords(p)))
+flat(c::CRS) = Cartesian{datum(c)}(CoordRefSystems.raw(c))
+
+"""
+ coordsum(points; weights=nothing)
+
+Sum of the base coordinates of the points, `Cartesian` for `𝔼` and `LatLon` for `🌐`.
+If `weights` is passed, the weighted sum will be returned.
+"""
+function coordsum(points; weights=nothing)
+ values = _coordsum(points, weights)
+ fromvalues(first(points), values)
+end
+
+"""
+ coordmean(points; weights=nothing)
+
+Mean of the base coordinates of the points, `Cartesian` for `𝔼` and `LatLon` for `🌐`.
+If `weights` is passed, the weighted mean will be returned.
+"""
+function coordmean(points; weights=nothing)
+ den = if isnothing(weights)
+ length(points)
+ else
+ sum(weights)
+ end
+ values = _coordsum(points, weights) ./ den
+ fromvalues(first(points), values)
+end
+
+function tovalues(p)
+ CRS = _basecrs(manifold(p))
+ c = convert(CRS, coords(p))
+ CoordRefSystems.values(c)
+end
+
+function fromvalues(g, values)
+ CRS = _basecrs(manifold(g))
+ withcrs(g, values, CRS)
+end
+
+function _coordsum(points, weights)
+ if isnothing(weights)
+ mapreduce(tovalues, .+, points)
+ else
+ mapreduce((p, w) -> tovalues(p) .* w, .+, points, weights)
+ end
+end
+
+_basecrs(::Type{<:𝔼}) = Cartesian
+_basecrs(::Type{<:🌐}) = LatLon
diff --git a/src/utils/misc.jl b/src/utils/misc.jl
new file mode 100644
index 000000000..f10a3d786
--- /dev/null
+++ b/src/utils/misc.jl
@@ -0,0 +1,106 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+ signarea(A, B, C)
+
+Compute signed area of triangle formed by points `A`, `B` and `C`.
+"""
+function signarea(A::Point, B::Point, C::Point)
+ checkdim(A, 2)
+ ((B - A) × (C - A)) / 2
+end
+
+"""
+ householderbasis(n)
+
+Returns a pair of orthonormal tangent vectors `u` and `v` from a normal `n`,
+such that `u`, `v`, and `n` form a right-hand orthogonal system.
+
+## References
+
+* D.S. Lopes et al. 2013. ["Tangent vectors to a 3-D surface normal: A geometric tool
+ to find orthogonal vectors based on the Householder transformation"]
+ (https://doi.org/10.1016/j.cad.2012.11.003)
+"""
+function householderbasis(n::Vec{3,ℒ}) where {ℒ}
+ n̂ = norm(n)
+ i = argmax(n .+ n̂)
+ n̂ᵢ = Vec(ntuple(j -> j == i ? n̂ : zero(ℒ), 3))
+ h = n + n̂ᵢ
+ H = (I - 2h * transpose(h) / (transpose(h) * h)) * unit(ℒ)
+ u, v = [H[:, j] for j in 1:3 if j != i]
+ i == 2 && ((u, v) = (v, u))
+ Vec(u), Vec(v)
+end
+
+"""
+ svdbasis(points)
+
+Returns the 2D basis that retains most of the variance in the list of 3D `points`
+using the singular value decomposition (SVD).
+
+See .
+"""
+function svdbasis(p::AbstractVector{<:Point})
+ checkdim(first(p), 3)
+ ℒ = lentype(eltype(p))
+ X = reduce(hcat, to.(p))
+ μ = sum(X, dims=2) / size(X, 2)
+ Z = X .- μ
+ U = usvd(Z).U
+ u = Vec(U[:, 1]...)
+ v = Vec(U[:, 2]...)
+ n = Vec(zero(ℒ), zero(ℒ), oneunit(ℒ))
+ isnegative((u × v) ⋅ n) ? (v, u) : (u, v)
+end
+
+"""
+ intersectparameters(a, b, c, d)
+
+Compute the parameters `λ₁` and `λ₂` of the lines
+`a + λ₁ ⋅ v⃗₁`, with `v⃗₁ = b - a` and
+`c + λ₂ ⋅ v⃗₂`, with `v⃗₂ = d - c` spanned by the input
+points `a`, `b` resp. `c`, `d` such that to yield line
+points with minimal distance or the intersection point
+(if lines intersect).
+
+Furthermore, the ranks `r` of the matrix of the linear
+system `A ⋅ λ⃗ = y⃗`, with `A = [v⃗₁ -v⃗₂], y⃗ = c - a`
+and the rank `rₐ` of the augmented matrix `[A y⃗]` are
+calculated in order to identify the intersection type:
+
+- Intersection: r == rₐ == 2
+- Colinear: r == rₐ == 1
+- No intersection: r != rₐ
+ - No intersection and parallel: r == 1, rₐ == 2
+ - No intersection, skew lines: r == 2, rₐ == 3
+"""
+function intersectparameters(a::Point, b::Point, c::Point, d::Point)
+ A = ustrip.([(b - a) (c - d)])
+ y = ustrip.(c - a)
+ T = eltype(A)
+
+ # calculate the rank of the augmented matrix by checking
+ # the zero entries of the diagonal of R
+ _, R = qr([A y])
+
+ # for Dim == 2 one has to check the L1 norm of rows as
+ # there are more columns than rows
+ τ = atol(T)
+ rₐ = sum(>(τ), sum(abs, R, dims=2))
+
+ # calculate the rank of the rectangular matrix
+ r = sum(>(τ), sum(abs, view(R, :, 1:2), dims=2))
+
+ # calculate parameters of intersection or closest point
+ if r ≥ 2
+ λ = A \ y
+ λ₁, λ₂ = λ[1], λ[2]
+ else # parallel or collinear
+ λ₁, λ₂ = zero(T), zero(T)
+ end
+
+ λ₁, λ₂, r, rₐ
+end
diff --git a/src/utils/sweepline.jl b/src/utils/sweepline.jl
new file mode 100644
index 000000000..ddfb7429f
--- /dev/null
+++ b/src/utils/sweepline.jl
@@ -0,0 +1,128 @@
+# Implementation of Bentley-Ottmann algorith
+# https://en.wikipedia.org/wiki/Bentley%E2%80%93Ottmann_algorithm
+
+using BinaryTrees
+
+
+"""
+ bentleyottmann(segments)
+
+Compute pairwise intersections between n `segments`
+in O(n⋅log(n)) time using Bentley-Ottmann sweep line
+algorithm.
+"""
+function bentleyottmann(segments)
+ # adjust vertices of segments
+ segs = map(segments) do s
+ a, b = extrema(s)
+ a > b ? reverse(s) : s
+ end
+
+ # retrieve relevant info
+ s = first(segs)
+ p = minimum(s)
+ P = typeof(p)
+ S = Tuple{P,P}
+
+ # initialization
+ 𝒬 = BinaryTrees.AVLTree{P}()
+ 𝒯 = BinaryTrees.AVLTree{S}()
+ ℒ = Dict{P,Vector{S}}()
+ 𝒰 = Dict{P,Vector{S}}()
+ 𝒞 = Dict{P,Vector{S}}()
+ for s in segs
+ a, b = extrema(s)
+ BinaryTrees.insert!(𝒬, a)
+ BinaryTrees.insert!(𝒬, b)
+ haskey(ℒ, a) ? push!(ℒ[a], (a, b)) : (ℒ[a] = [(a, b)])
+ haskey(𝒰, b) ? push!(𝒰[b], (a, b)) : (𝒰[b] = [(a, b)])
+ haskey(ℒ, b) || (ℒ[b] = S[])
+ haskey(𝒰, a) || (𝒰[a] = S[])
+ haskey(𝒞, a) || (𝒞[a] = S[])
+ haskey(𝒞, b) || (𝒞[b] = S[])
+ end
+ m = Point(-Inf, -Inf)
+ M = Point(Inf, Inf)
+ BinaryTrees.insert!(𝒯, (m, m))
+ BinaryTrees.insert!(𝒯, (M, M))
+
+ # sweep line
+ I = Dict{P,Vector{S}}()
+ while !isnothing(BinaryTrees.root(𝒬))
+ p = _key(BinaryTrees.root(𝒬))
+ BinaryTrees.delete!(𝒬, p)
+ handle!(I, p, 𝒬, 𝒯, ℒ, 𝒰, 𝒞)
+ end
+ I
+end
+
+function handle!(I, p, 𝒬, 𝒯, ℒ, 𝒰, 𝒞)
+ # Segments that start, end, or intersect at p
+ start_segments = ℒ[p]
+ end_segments = 𝒰[p]
+ intersection_segments = 𝒞[p]
+
+ # If there are multiple segments intersecting at p, record the intersection
+ if length(start_segments ∪ end_segments ∪ intersection_segments) > 1
+ I[p] = start_segments ∪ end_segments ∪ intersection_segments
+ end
+
+ # Remove segments that end at p from the status structure
+ for s in end_segments ∪ intersection_segments
+ BinaryTrees.delete!(𝒯, s)
+ end
+
+ # Insert segments that start at p into the status structure
+ for s in start_segments ∪ intersection_segments
+ BinaryTrees.insert!(𝒯, s)
+ end
+ node = BinaryTrees.root(𝒬)
+
+ # Find new event points caused by the insertion or deletion of segments
+ for s in start_segments
+ s = Segment(s)
+ pred = BinaryTrees.left(node)
+ succ = BinaryTrees.right(node)
+ ns = Segment(pred, succ)
+ if pred !== nothing
+ new_geom, new_type = _newevent(s, ns)
+ if new_geom == IntersectionType(0)
+ BinaryTrees.insert!(𝒬, new_geom)
+ end
+ end
+ if succ !== nothing
+ new_geom, new_type = _newevent(s, ns)
+ if new_type == IntersectionType(0)
+ BinaryTrees.insert!(𝒬, new_geom)
+ end
+ end
+ end
+
+ for s in end_segments
+ s = Segment(s)
+ pred = BinaryTrees.left(node)
+ succ = BinaryTrees.right(node)
+ ns = Segment(pred, succ)
+ if pred !== nothing && succ !== nothing
+ new_geom, new_type = _newevent(s, ns)
+ if new_type == IntersectionType(0)
+ BinaryTrees.insert!(𝒬, new_geom)
+ end
+ end
+ end
+end
+
+_key(node::BinaryTrees.AVLNode) = node.key
+_geom(intersect::Intersection) = intersect.geom
+_type(intersect::Intersection) = intersect.type
+function _newevent(s₁::Segment, s₂::Segment)
+ new_event = intersection(s₁, s₂)
+ _geom(new_event), _type(new_event)
+end
+
+
+function Segment(node₁::BinaryTrees.BinaryNode, node₂::BinaryTrees.BinaryNode)
+ node₁ = _key(node₁)
+ node₂ = _key(node₂)
+ Segment((node₁, node₂))
+end
diff --git a/src/utils/units.jl b/src/utils/units.jl
new file mode 100644
index 000000000..d31b6a128
--- /dev/null
+++ b/src/utils/units.jl
@@ -0,0 +1,28 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+# The result units of some operations, such as dot and cross,
+# are treated in a special way to handle Meshes.jl use cases
+
+function usvd(A)
+ u = unit(eltype(A))
+ F = svd(ustrip.(A))
+ SVD(F.U * u, F.S * u, F.Vt * u)
+end
+
+uinv(A) = inv(ustrip.(A)) * unit(eltype(A))^-1
+
+unormalize(a::Vec{Dim,ℒ}) where {Dim,ℒ} = Vec(normalize(a) * unit(ℒ))
+
+udot(a::Vec{Dim,ℒ}, b::Vec{Dim,ℒ}) where {Dim,ℒ} = ustrip(a ⋅ b) * unit(ℒ)
+udot(a::Vec{Dim,ℒ₁}, b::Vec{Dim,ℒ₂}) where {Dim,ℒ₁,ℒ₂} = udot(promote(a, b)...)
+
+ucross(a::Vec{Dim,ℒ}, b::Vec{Dim,ℒ}) where {Dim,ℒ} = Vec(ustrip.(a × b) * unit(ℒ))
+ucross(a::Vec{Dim,ℒ₁}, b::Vec{Dim,ℒ₂}) where {Dim,ℒ₁,ℒ₂} = ucross(promote(a, b)...)
+
+ucross(a::Vec{Dim,ℒ}, b::Vec{Dim,ℒ}, c::Vec{Dim,ℒ}) where {Dim,ℒ} = Vec(ustrip.(a × b × c) * unit(ℒ))
+
+urotbetween(u::Vec, v::Vec) = rotation_between(ustrip.(u), ustrip.(v))
+
+urotapply(R::Rotation, v::Vec{Dim,ℒ}) where {Dim,ℒ} = Vec(R * ustrip.(v) * unit(ℒ))
diff --git a/src/vectors.jl b/src/vectors.jl
index d5bfc9596..831b03383 100644
--- a/src/vectors.jl
+++ b/src/vectors.jl
@@ -2,15 +2,12 @@
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
-const Continuous = Union{AbstractFloat,Quantity{<:AbstractFloat}}
-
"""
Vec(x₁, x₂, ..., xₙ)
Vec((x₁, x₂, ..., xₙ))
- Vec{Dim,T}(x₁, x₂, ..., xₙ)
- Vec{Dim,T}((x₁, x₂, ..., xₙ))
-A geometric vector in `Dim`-dimensional space with coordinates of type `T` for linear algebra.
+A geometric vector in `Dim`-dimensional space with coordinates
+in length units (default to meters) for linear algebra.
By default, integer coordinates are converted to float.
@@ -24,63 +21,41 @@ B = Point(1.0, 0.0)
v = B - A
# 2D vectors
-Vec(0.0, 1.0) # double precision as expected
-Vec(0f0, 1f0) # single precision as expected
-Vec(0, 0) # integer is converted to float by design
-Vec2(0, 1) # explicitly ask for double precision
-Vec2f(0, 1) # explicitly ask for single precision
+Vec(1.0, 2.0) # add default units
+Vec(1.0m, 2.0m) # double precision as expected
+Vec(1f0km, 2f0km) # single precision as expected
+Vec(1m, 2m) # integer is converted to float by design
# 3D vectors
-Vec(1.0, 2.0, 3.0) # double precision as expected
-Vec(1f0, 2f0, 3f0) # single precision as expected
-Vec(1, 2, 3) # integer is converted to float by design
-Vec3(1, 2, 3) # explicitly ask for double precision
-Vec3f(1, 2, 3) # explicitly ask for single precision
+Vec(1.0, 2.0, 3.0) # add default units
+Vec(1.0m, 2.0m, 3.0m) # double precision as expected
+Vec(1f0km, 2f0km, 3f0km) # single precision as expected
+Vec(1m, 2m, 3m) # integer is converted to float by design
```
### Notes
- A `Vec` is a subtype of `StaticVector` from StaticArrays.jl
-- Type aliases are `Vec1`, `Vec2`, `Vec3`, `Vec1f`, `Vec2f`, `Vec3f`
"""
-struct Vec{Dim,T<:Continuous} <: StaticVector{Dim,T}
- coords::NTuple{Dim,T}
- Vec{Dim,T}(coords::NTuple{Dim,T}) where {Dim,T<:Continuous} = new(coords)
-end
-
-# convenience constructors
-Vec{Dim,T}(coords...) where {Dim,T<:Continuous} = Vec{Dim,T}(coords)
-function Vec{Dim,T}(coords::Union{Tuple,AbstractVector}) where {Dim,T<:Continuous}
- if Dim ≠ length(coords)
- throw(DimensionMismatch("the number of coordinates must be equal to the number of dimensions"))
- end
- Vec{Dim,T}(NTuple{Dim,T}(coords))
+struct Vec{Dim,ℒ<:Len} <: StaticVector{Dim,ℒ}
+ coords::NTuple{Dim,ℒ}
+ Vec{Dim,ℒ}(coords::NTuple{Dim}) where {Dim,ℒ<:Len} = new(coords)
end
-Vec(coords...) = Vec(coords)
-Vec(coords::Tuple) = Vec(promote(coords...))
-Vec(coords::NTuple{Dim,T}) where {Dim,T} = Vec(float.(coords))
-Vec(coords::NTuple{Dim,T}) where {Dim,T<:Continuous} = Vec{Dim,T}(coords)
-
-# StaticVector constructors
-Vec(coords::StaticVector{Dim,T}) where {Dim,T<:Continuous} = Vec{Dim,T}(coords)
-Vec{Dim,T}(coords::StaticVector) where {Dim,T<:Continuous} = Vec{Dim,T}(Tuple(coords))
+Vec(coords::NTuple{Dim,ℒ}) where {Dim,ℒ<:Len} = Vec{Dim,float(ℒ)}(coords)
+Vec(coords::NTuple{Dim,Len}) where {Dim} = Vec(promote(coords...))
+Vec(coords::NTuple{Dim,Number}) where {Dim} = Vec(addunit.(coords, u"m"))
-# type aliases for convenience
-const Vec1 = Vec{1,Float64}
-const Vec2 = Vec{2,Float64}
-const Vec3 = Vec{3,Float64}
-const Vec1f = Vec{1,Float32}
-const Vec2f = Vec{2,Float32}
-const Vec3f = Vec{3,Float32}
+Vec(coords::Number...) = Vec(coords)
# StaticVector interface
Base.Tuple(v::Vec) = getfield(v, :coords)
Base.getindex(v::Vec, i::Int) = getindex(getfield(v, :coords), i)
+Base.promote_rule(::Type{Vec{Dim,ℒ₁}}, ::Type{Vec{Dim,ℒ₂}}) where {Dim,ℒ₁,ℒ₂} = Vec{Dim,promote_type(ℒ₁, ℒ₂)}
function StaticArrays.similar_type(::Type{<:Vec}, ::Type{T}, ::Size{S}) where {T,S}
L = prod(S)
N = length(S)
- isone(N) && T <: Continuous ? Vec{L,T} : SArray{Tuple{S...},T,N,L}
+ isone(N) && T <: Len ? Vec{L,T} : SArray{Tuple{S...},T,N,L}
end
"""
@@ -99,12 +74,15 @@ See .
```
"""
function ∠(u::Vec{2}, v::Vec{2}) # preserve sign
- θ = atan(u × v, u ⋅ v)
- T = typeof(θ)
- θ == -T(π) ? -θ : θ
+ θ = atan(u × v, u ⋅ v) * u"rad"
+ θ == oftype(θ, -π) ? -θ : θ
end
-∠(u::Vec{3}, v::Vec{3}) = atan(norm(u × v), u ⋅ v) # discard sign
+∠(u::Vec{3}, v::Vec{3}) = atan(norm(u × v), u ⋅ v) * u"rad" # discard sign
+
+# -----------
+# IO METHODS
+# -----------
function Base.show(io::IO, v::Vec)
if get(io, :compact, false)
diff --git a/src/viewing.jl b/src/viewing.jl
deleted file mode 100644
index ce56a69d1..000000000
--- a/src/viewing.jl
+++ /dev/null
@@ -1,194 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-# -------------------
-# VIEWS WITH INDICES
-# -------------------
-
-Base.view(domain::Domain, inds::AbstractVector{Int}) = SubDomain(domain, inds)
-
-# ----------------------
-# VIEWS WITH GEOMETRIES
-# ----------------------
-
-"""
- view(domain, geometry)
-
-Return a view of the `domain` containing all elements that
-intersect with the `geometry`.
-"""
-Base.view(domain::Domain, geometry::Geometry) = view(domain, indices(domain, geometry))
-
-"""
- indices(domain, geometry)
-
-Return the indices of the elements of the `domain`
-that intersect with the `geometry`.
-"""
-indices(domain::Domain, geometry::Geometry) = findall(intersects(geometry), domain)
-
-function indices(grid::Grid{Dim}, point::Point{Dim}) where {Dim}
- point ∉ grid && return Int[]
-
- # grid properties
- orig = minimum(grid)
- spac = spacing(grid)
- dims = size(grid)
-
- # integer coordinates
- coords = ceil.(Int, (point - orig) ./ spac)
-
- # fix coordinates that are on the grid border
- coords = clamp.(coords, 1, dims)
-
- # convert to linear index
- [LinearIndices(dims)[coords...]]
-end
-
-function indices(grid::Grid{2}, poly::Polygon{2})
- dims = size(grid)
- mask = zeros(Int, dims)
- cpoly = poly ∩ boundingbox(grid)
- isnothing(cpoly) && return Int[]
-
- for (i, triangle) in enumerate(simplexify(cpoly))
- _fill!(mask, grid, i, triangle)
- end
-
- # convert to linear indices
- LinearIndices(dims)[mask .> 0]
-end
-
-function indices(grid::Grid{2}, chain::Chain{2})
- dims = size(grid)
- mask = falses(dims)
-
- for segment in segments(chain)
- p₁, p₂ = vertices(segment)
- _bresenham!(mask, grid, true, p₁, p₂)
- end
-
- # convert to linear indices
- LinearIndices(dims)[mask]
-end
-
-indices(domain::Domain, multi::Multi) = mapreduce(geom -> indices(domain, geom), vcat, parent(multi)) |> unique
-
-function indices(grid::CartesianGrid, box::Box)
- # grid properties
- or = minimum(grid)
- sp = spacing(grid)
- sz = size(grid)
-
- # intersection of boxes
- lo, up = extrema(boundingbox(grid) ∩ box)
-
- # Cartesian indices of new corners
- ilo = max.(ceil.(Int, (lo - or) ./ sp), 1)
- iup = min.(floor.(Int, (up - or) ./ sp) .+ 1, sz)
-
- # Cartesian range from corner to corner
- range = CartesianIndex(Tuple(ilo)):CartesianIndex(Tuple(iup))
-
- # convert to linear indices
- LinearIndices(sz)[range] |> vec
-end
-
-# utils
-function _fill!(mask, grid, val, triangle)
- v = vertices(triangle)
-
- # fill edges of triangle
- _bresenham!(mask, grid, val, v[1], v[2])
- _bresenham!(mask, grid, val, v[2], v[3])
- _bresenham!(mask, grid, val, v[3], v[1])
-
- # fill interior of triangle
- j₁ = findfirst(==(val), mask).I[2]
- j₂ = findlast(==(val), mask).I[2]
- for j in j₁:j₂
- i₁ = findfirst(==(val), @view(mask[:, j]))
- i₂ = findlast(==(val), @view(mask[:, j]))
- mask[i₁:i₂, j] .= val
- end
-end
-
-# Bresenham's line algorithm: https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
-function _bresenham!(mask, grid, val, p₁, p₂)
- o = minimum(grid)
- s = spacing(grid)
-
- # integer coordinates
- x₁, y₁ = ceil.(Int, (p₁ - o) ./ s)
- x₂, y₂ = ceil.(Int, (p₂ - o) ./ s)
-
- # fix coordinates of points that are on the grid border
- xmax, ymax = size(grid)
- x₁ = clamp(x₁, 1, xmax)
- y₁ = clamp(y₁, 1, ymax)
- x₂ = clamp(x₂, 1, xmax)
- y₂ = clamp(y₂, 1, ymax)
-
- if abs(y₂ - y₁) < abs(x₂ - x₁)
- if x₁ > x₂
- _bresenhamlow!(mask, val, x₂, y₂, x₁, y₁)
- else
- _bresenhamlow!(mask, val, x₁, y₁, x₂, y₂)
- end
- else
- if y₁ > y₂
- _bresenhamhigh!(mask, val, x₂, y₂, x₁, y₁)
- else
- _bresenhamhigh!(mask, val, x₁, y₁, x₂, y₂)
- end
- end
-end
-
-function _bresenhamlow!(mask, val, x₁, y₁, x₂, y₂)
- dx = x₂ - x₁
- dy = y₂ - y₁
- yi = 1
- if dy < 0
- yi = -1
- dy = -dy
- end
-
- D = 2dy - dx
- y = y₁
-
- for x in x₁:x₂
- mask[x, y] = val
-
- if D > 0
- y = y + yi
- D = D + 2dy - 2dx
- else
- D = D + 2dy
- end
- end
-end
-
-function _bresenhamhigh!(mask, val, x₁, y₁, x₂, y₂)
- dx = x₂ - x₁
- dy = y₂ - y₁
- xi = 1
- if dx < 0
- xi = -1
- dx = -dx
- end
-
- D = 2dx - dy
- x = x₁
-
- for y in y₁:y₂
- mask[x, y] = val
-
- if D > 0
- x = x + xi
- D = D + 2dx - 2dy
- else
- D = D + 2dx
- end
- end
-end
diff --git a/src/viz.jl b/src/viz.jl
index 61db03f0f..ae308d898 100644
--- a/src/viz.jl
+++ b/src/viz.jl
@@ -9,13 +9,17 @@ Visualize Meshes.jl `object` with various `options`.
## Available options
-* `color` - color of geometries
-* `alpha` - transparency in [0,1]
-* `colorscheme` - color scheme from ColorSchemes.jl
-* `pointsize` - size of points in point set
-* `segmentsize` - size (or width) of segments
-* `showfacets` - enable visualization of facets
-* `facetcolor` - color of facets (e.g. edges)
+* `color` - color of geometries
+* `alpha` - transparency in [0,1]
+* `colormap` - color scheme/map from ColorSchemes.jl
+* `colorrange` - minimum and maximum color values
+* `showsegments` - visualize segments
+* `segmentcolor` - color of segments
+* `segmentsize` - width of segments
+* `showpoints` - visualize points
+* `pointmarker` - marker of points
+* `pointcolor` - color of points
+* `pointsize` - size of points
The option `color` can be a single scalar or a vector
of scalars. For [`Mesh`](@ref) subtypes, the length of
@@ -35,11 +39,11 @@ viz(mesh, color = 1:nelements(mesh))
```
Different strategies to show the boundary of
-geometries (showfacets vs. boundary):
+geometries (showsegments vs. boundary):
```
-# visualize boundary as facets
-viz(polygon, showfacets = true)
+# visualize boundary with showsegments
+viz(polygon, showsegments = true)
# visualize boundary with separate call
viz(polygon)
@@ -61,32 +65,3 @@ Visualize Meshes.jl `object` in an existing
scene with `options` forwarded to [`viz`](@ref).
"""
function viz! end
-
-"""
- ascolors(values, colorscheme)
-
-Convert vector of `values` to Colors.jl,
-using `colorscheme` from ColorSchemes.jl.
-
-### Notes
-
-This function is intended for developers
-who may be interested in the visualization
-of custom Julia objects in the `color`
-argument of the [`viz`](@ref) function.
-"""
-function ascolors end
-
-"""
- defaultscheme(values)
-
-Return default colorscheme for `values`.
-
-### Notes
-
-This function is intended for developers
-who may be interested in the automatic
-selection of colorschemes from ColorSchemes.jl
-for custom Julia objects.
-"""
-function defaultscheme end
diff --git a/src/winding.jl b/src/winding.jl
index d9b5f72bc..6f65737ee 100644
--- a/src/winding.jl
+++ b/src/winding.jl
@@ -18,20 +18,31 @@ Generalized winding number of `points` with respect to the geometric `object`.
"""
function winding end
-function winding(points, ring::Ring{2,T}) where {T}
+# ------
+# RINGS
+# ------
+
+function winding(points, ring::Ring)
v = vertices(ring)
n = nvertices(ring)
- w(p) = sum(∠(v[i], p, v[i + 1]) for i in 1:n) / T(2π)
+ function w(p)
+ Σ = sum(∠(flat(v[i]), flat(p), flat(v[i + 1])) for i in 1:n)
+ Σ / oftype(Σ, 2π)
+ end
- tcollect(w(p) for p in points)
+ [w(p) for p in points]
end
-winding(point::Point{2,T}, ring::Ring{2,T}) where {T} = winding((point,), ring) |> first
+winding(point::Point, ring::Ring) = winding((point,), ring) |> first
+
+# -------
+# MESHES
+# -------
# Jacobson et al 2013.
-function winding(points, mesh::Mesh{3,T}) where {T}
- @assert paramdim(mesh) == 2 "winding number only defined for surface meshes"
+function winding(points, mesh::Mesh)
+ assertion(paramdim(mesh) == 2, "winding number only defined for surface meshes")
(eltype(mesh) <: Triangle) || return winding(points, simplexify(mesh))
function w(p)
@@ -47,10 +58,10 @@ function winding(points, mesh::Mesh{3,T}) where {T}
d = a * b * c + (a⃗ ⋅ b⃗) * c + (b⃗ ⋅ c⃗) * a + (c⃗ ⋅ a⃗) * b
2atan(n, d)
end
- ∑ / T(4π)
+ ∑ / oftype(∑, 4π)
end
- tcollect(w(p) for p in points)
+ [w(p) for p in points]
end
-winding(point::Point{3,T}, mesh::Mesh{3,T}) where {T} = winding((point,), mesh) |> first
+winding(point::Point, mesh::Mesh) = winding((point,), mesh) |> first
diff --git a/test/Project.toml b/test/Project.toml
index 3abc56481..96147db24 100644
--- a/test/Project.toml
+++ b/test/Project.toml
@@ -2,17 +2,20 @@
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
CircularArrays = "7a955b69-7140-5f4e-a0ed-f168c5e2e749"
+CoordRefSystems = "b46f11dc-f210-4604-bfba-323c1ec968cb"
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
PlyIO = "42171d58-473b-503a-8d5f-782019eb09ec"
-Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf"
Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
+TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe"
TransformsBase = "28dd2a49-a57a-4bfb-84ca-1a49db9b96b8"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
diff --git a/test/boundingboxes.jl b/test/boundingboxes.jl
index da621f1f5..4ade2d1d4 100644
--- a/test/boundingboxes.jl
+++ b/test/boundingboxes.jl
@@ -1,105 +1,151 @@
-@testset "Bounding boxes" begin
- p = P2(0, 0)
+@testitem "Bounding boxes" setup = [Setup] begin
+ p = cart(0, 0)
@test boundingbox(p) == Box(p, p)
@test @allocated(boundingbox(p)) < 50
- b = Box(P2(0, 0), P2(1, 1))
+ b = Box(cart(0, 0), cart(1, 1))
@test boundingbox(b) == b
@test @allocated(boundingbox(b)) < 50
- r = Ray(P2(0, 0), V2(1, 0))
- @test boundingbox(r) == Box(P2(0, 0), P2(T(Inf), 0))
+ r = Ray(cart(0, 0), vector(1, 0))
+ @test boundingbox(r) == Box(cart(0, 0), cart(T(Inf), 0))
@test @allocated(boundingbox(r)) < 50
- r = Ray(P2(1, 1), V2(0, 1))
- @test boundingbox(r) == Box(P2(1, 1), P2(1, T(Inf)))
+ r = Ray(cart(1, 1), vector(0, 1))
+ @test boundingbox(r) == Box(cart(1, 1), cart(1, T(Inf)))
@test @allocated(boundingbox(r)) < 50
- r = Ray(P2(1, 1), V2(-1, -1))
- @test boundingbox(r) == Box(P2(T(-Inf), T(-Inf)), P2(1, 1))
+ r = Ray(cart(1, 1), vector(-1, -1))
+ @test boundingbox(r) == Box(cart(T(-Inf), T(-Inf)), cart(1, 1))
@test @allocated(boundingbox(r)) < 50
- r = Ray(P2(-1, 1), V2(1, -1))
- @test boundingbox(r) == Box(P2(-1, T(-Inf)), P2(T(Inf), 1))
+ r = Ray(cart(-1, 1), vector(1, -1))
+ @test boundingbox(r) == Box(cart(-1, T(-Inf)), cart(T(Inf), 1))
@test @allocated(boundingbox(r)) < 50
- s = Sphere(P2(0, 0), T(1))
- @test boundingbox(s) == Box(P2(-1, -1), P2(1, 1))
+ b = Ball(cart(0, 0), T(1))
+ @test boundingbox(b) == Box(cart(-1, -1), cart(1, 1))
+ @test @allocated(boundingbox(b)) < 50
+ b = Ball(cart(1, 1), T(1))
+ @test boundingbox(b) == Box(cart(0, 0), cart(2, 2))
+ @test @allocated(boundingbox(b)) < 50
+
+ s = Sphere(cart(0, 0), T(1))
+ @test boundingbox(s) == Box(cart(-1, -1), cart(1, 1))
@test @allocated(boundingbox(s)) < 50
- s = Sphere(P2(1, 1), T(1))
- @test boundingbox(s) == Box(P2(0, 0), P2(2, 2))
+ s = Sphere(cart(1, 1), T(1))
+ @test boundingbox(s) == Box(cart(0, 0), cart(2, 2))
@test @allocated(boundingbox(s)) < 50
- b = Ball(P2(0, 0), T(1))
- @test boundingbox(b) == Box(P2(-1, -1), P2(1, 1))
- @test @allocated(boundingbox(b)) < 50
- b = Ball(P2(1, 1), T(1))
- @test boundingbox(b) == Box(P2(0, 0), P2(2, 2))
- @test @allocated(boundingbox(b)) < 50
+ c = Cylinder(T(1))
+ b = boundingbox(c)
+ @test b == Box(cart(-1, -1, 0), cart(1, 1, 1))
+
+ c = CylinderSurface(T(1))
+ b = boundingbox(c)
+ @test b == Box(cart(-1, -1, 0), cart(1, 1, 1))
+
+ c = Cone(Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(1)), cart(0, 0, 1))
+ b = boundingbox(c)
+ @test b == Box(cart(-1, -1, 0), cart(1, 1, 1))
+
+ c = ConeSurface(Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(1)), cart(0, 0, 1))
+ b = boundingbox(c)
+ @test b == Box(cart(-1, -1, 0), cart(1, 1, 1))
- b = Box(P2(-3, -1), P2(0.5, 0.5))
- s = Sphere(P2(0, 0), T(2))
+ b = Box(cart(-3, -1), cart(0.5, 0.5))
+ s = Sphere(cart(0, 0), T(2))
m = Multi([b, s])
d = GeometrySet([b, s])
- @test boundingbox(m) == Box(P2(-3, -2), P2(2, 2))
- @test boundingbox(d) == Box(P2(-3, -2), P2(2, 2))
- @test @allocated(boundingbox(m)) < 2500
- @test @allocated(boundingbox(d)) < 2500
+ @test boundingbox(m) == Box(cart(-3, -2), cart(2, 2))
+ @test boundingbox(d) == Box(cart(-3, -2), cart(2, 2))
+ if Sys.iswindows() && VERSION < v"1.10"
+ @test @allocated(boundingbox(m)) < 4100
+ @test @allocated(boundingbox(d)) < 4100
+ else
+ @test @allocated(boundingbox(m)) < 2600
+ @test @allocated(boundingbox(d)) < 2600
+ end
- b1 = Box(P2(0, 0), P2(1, 1))
- b2 = Box(P2(-1, -1), P2(0.5, 0.5))
+ b1 = Box(cart(0, 0), cart(1, 1))
+ b2 = Box(cart(-1, -1), cart(0.5, 0.5))
m = Multi([b1, b2])
d = GeometrySet([b1, b2])
- @test boundingbox(m) == Box(P2(-1, -1), P2(1, 1))
- @test boundingbox(d) == Box(P2(-1, -1), P2(1, 1))
+ @test boundingbox(m) == Box(cart(-1, -1), cart(1, 1))
+ @test boundingbox(d) == Box(cart(-1, -1), cart(1, 1))
@test @allocated(boundingbox(m)) < 50
@test @allocated(boundingbox(d)) < 50
- d = PointSet(T[0 1 2; 0 2 1])
- @test boundingbox(d) == Box(P2(0, 0), P2(2, 2))
+ d = PointSet(cart(0, 0), cart(1, 2), cart(2, 1))
+ @test boundingbox(d) == Box(cart(0, 0), cart(2, 2))
@test @allocated(boundingbox(d)) < 50
- d = PointSet(T[1 2; 2 1])
- @test boundingbox(d) == Box(P2(1, 1), P2(2, 2))
+ d = PointSet(cart(1, 2), cart(2, 1))
+ @test boundingbox(d) == Box(cart(1, 1), cart(2, 2))
@test @allocated(boundingbox(d)) < 50
- d = CartesianGrid{T}(10, 10)
- @test boundingbox(d) == Box(P2(0, 0), P2(10, 10))
+ d = cartgrid(10, 10)
+ @test boundingbox(d) == Box(cart(0, 0), cart(10, 10))
@test @allocated(boundingbox(d)) < 50
- d = CartesianGrid{T}(100, 200)
- @test boundingbox(d) == Box(P2(0, 0), P2(100, 200))
+ d = cartgrid(100, 200)
+ @test boundingbox(d) == Box(cart(0, 0), cart(100, 200))
@test @allocated(boundingbox(d)) < 50
d = CartesianGrid((10, 10), T.((1, 1)), T.((1, 1)))
- @test boundingbox(d) == Box(P2(1, 1), P2(11, 11))
+ @test boundingbox(d) == Box(cart(1, 1), cart(11, 11))
@test @allocated(boundingbox(d)) < 50
- d = PointSet(T[0 1 2; 0 2 1])
+ d = PointSet(cart(0, 0), cart(1, 2), cart(2, 1))
v = view(d, 1:2)
- @test boundingbox(v) == Box(P2(0, 0), P2(1, 2))
+ @test boundingbox(v) == Box(cart(0, 0), cart(1, 2))
@test @allocated(boundingbox(v)) < 50
- d = CartesianGrid{T}(10, 10)
+ d = cartgrid(10, 10)
v = view(d, 1:2)
- @test boundingbox(v) == Box(P2(0, 0), P2(2, 1))
- @test @allocated(boundingbox(v)) < 9000
+ @test boundingbox(v) == Box(cart(0, 0), cart(2, 1))
+ @test @allocated(boundingbox(v)) < 10000
- g = CartesianGrid{T}(10, 10)
+ g = cartgrid(10, 10)
d = convert(RectilinearGrid, g)
- @test boundingbox(d) == Box(P2(0, 0), P2(10, 10))
+ @test boundingbox(d) == Box(cart(0, 0), cart(10, 10))
@test @allocated(boundingbox(d)) < 50
- g = CartesianGrid{T}(10, 10)
+ g = cartgrid(10, 10)
d = TransformedGrid(g, Rotate(T(π / 2)))
- @test boundingbox(d) ≈ Box(P2(-10, 0), P2(0, 10))
- @test @allocated(boundingbox(d)) < 2300
+ @test boundingbox(d) ≈ Box(cart(-10, 0), cart(0, 10))
+ @test @allocated(boundingbox(d)) < 3000
- g = CartesianGrid{T}(10, 10)
+ g = cartgrid(10, 10)
rg = convert(RectilinearGrid, g)
d = TransformedGrid(rg, Rotate(T(π / 2)))
- @test boundingbox(d) ≈ Box(P2(-10, 0), P2(0, 10))
- @test @allocated(boundingbox(d)) < 2300
+ @test boundingbox(d) ≈ Box(cart(-10, 0), cart(0, 10))
+ @test @allocated(boundingbox(d)) < 3000
- g = CartesianGrid{T}(10, 10)
+ g = cartgrid(10, 10)
m = convert(SimpleMesh, g)
- @test boundingbox(m) == Box(P2(0, 0), P2(10, 10))
+ @test boundingbox(m) == Box(cart(0, 0), cart(10, 10))
@test @allocated(boundingbox(m)) < 50
- p = ParaboloidSurface{T}(P3(1, 2, 3), T(5), T(4))
- @test boundingbox(p) ≈ Box(P3(-4, -3, 3), P3(6, 7, 73 / 16))
+ p = ParaboloidSurface(cart(1, 2, 3), T(5), T(4))
+ @test boundingbox(p) ≈ Box(cart(-4, -3, 3), cart(6, 7, 73 / 16))
+
+ # latlon coordinates
+ t = Triangle(latlon(0, 0), latlon(0, 2), latlon(1, 1))
+ @test boundingbox(t) == Box(latlon(0, 0), latlon(1, 2))
+ @test @allocated(boundingbox(t)) < 50
+
+ p = Pentagon(latlon(1, 6), latlon(10, 2), latlon(16, 10), latlon(10, 18), latlon(1, 14))
+ @test boundingbox(p) == Box(latlon(1, 2), latlon(16, 18))
+ @test @allocated(boundingbox(p)) < 50
+
+ d = PointSet(latlon(0, 0), latlon(2, 1), latlon(1, 2))
+ @test boundingbox(d) == Box(latlon(0, 0), latlon(2, 2))
+ @test @allocated(boundingbox(d)) < 50
+
+ # CRS propagation
+ r = Ray(merc(-1, 1), vector(1, -1))
+ @test crs(boundingbox(r)) === crs(r)
+ g = CartesianGrid((10, 10), merc(0, 0), (T(1), T(1)))
+ m = convert(SimpleMesh, g)
+ @test crs(boundingbox(m)) === crs(m)
+
+ # 1D segment
+ s = Segment(cart(0), cart(1))
+ b = boundingbox(s)
+ @test b == Box(cart(0), cart(1))
end
diff --git a/test/clamping.jl b/test/clamping.jl
index d185d58ff..7a725905c 100644
--- a/test/clamping.jl
+++ b/test/clamping.jl
@@ -1,13 +1,13 @@
-@testset "Clamping" begin
+@testitem "Clamping" setup = [Setup] begin
box = Box((zero(T), zero(T)), (one(T), one(T)))
- @test clamp(P2(0.5, 0.5), box) == P2(0.5, 0.5)
- @test clamp(P2(-1, 0.5), box) == P2(0, 0.5)
- @test clamp(P2(0.5, -1), box) == P2(0.5, 0)
- @test clamp(P2(2, 0.5), box) == P2(1, 0.5)
- @test clamp(P2(0.5, 2), box) == P2(0.5, 1)
- @test clamp(P2(2, 2), box) == P2(1, 1)
- @test clamp(P2(-1, -1), box) == P2(0, 0)
+ @test clamp(cart(0.5, 0.5), box) == cart(0.5, 0.5)
+ @test clamp(cart(-1, 0.5), box) == cart(0, 0.5)
+ @test clamp(cart(0.5, -1), box) == cart(0.5, 0)
+ @test clamp(cart(2, 0.5), box) == cart(1, 0.5)
+ @test clamp(cart(0.5, 2), box) == cart(0.5, 1)
+ @test clamp(cart(2, 2), box) == cart(1, 1)
+ @test clamp(cart(-1, -1), box) == cart(0, 0)
- points = PointSet(P2(0.5, 0.5), P2(-1, 0.5), P2(0.5, 2))
- @test clamp(points, box) == PointSet(P2(0.5, 0.5), P2(0, 0.5), P2(0.5, 1))
+ points = PointSet(cart(0.5, 0.5), cart(-1, 0.5), cart(0.5, 2))
+ @test clamp(points, box) == PointSet(cart(0.5, 0.5), cart(0, 0.5), cart(0.5, 1))
end
diff --git a/test/clipping.jl b/test/clipping.jl
index 8377751df..117f8a8ce 100644
--- a/test/clipping.jl
+++ b/test/clipping.jl
@@ -1,77 +1,76 @@
-@testset "Clipping" begin
- @testset "SutherlandHodgman" begin
- # triangle
- poly = Triangle(P2(6, 2), P2(3, 5), P2(0, 2))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- clipped = clip(poly, other, SutherlandHodgman())
- @test issimple(clipped)
- @test all(vertices(clipped) .≈ [P2(5, 3), P2(4, 4), P2(2, 4), P2(0, 2), P2(5, 2)])
+@testitem "SutherlandHodgman" setup = [Setup] begin
+ # triangle
+ poly = Triangle(cart(6, 2), cart(3, 5), cart(0, 2))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ @test issimple(clipped)
+ @test all(vertices(clipped) .≈ [cart(5, 3), cart(4, 4), cart(2, 4), cart(0, 2), cart(5, 2)])
- # octagon
- poly = Octagon(P2(8, -2), P2(8, 5), P2(2, 5), P2(4, 3), P2(6, 3), P2(4, 1), P2(2, 1), P2(2, -2))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- clipped = clip(poly, other, SutherlandHodgman())
- @test !issimple(clipped)
- @test all(
- vertices(clipped) .≈ [P2(3, 4), P2(4, 3), P2(5, 3), P2(5, 2), P2(4, 1), P2(2, 1), P2(2, 0), P2(5, 0), P2(5, 4)]
- )
+ # octagon
+ poly = Octagon(cart(8, -2), cart(8, 5), cart(2, 5), cart(4, 3), cart(6, 3), cart(4, 1), cart(2, 1), cart(2, -2))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ @test !issimple(clipped)
+ @test all(
+ vertices(clipped) .≈
+ [cart(3, 4), cart(4, 3), cart(5, 3), cart(5, 2), cart(4, 1), cart(2, 1), cart(2, 0), cart(5, 0), cart(5, 4)]
+ )
- # inside
- poly = Quadrangle(P2(1, 0), P2(1, 1), P2(0, 1), P2(0, 0))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- clipped = clip(poly, other, SutherlandHodgman())
- @test issimple(clipped)
- @test all(vertices(clipped) .≈ vertices(poly))
+ # inside
+ poly = Quadrangle(cart(1, 0), cart(1, 1), cart(0, 1), cart(0, 0))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ @test issimple(clipped)
+ @test all(vertices(clipped) .≈ vertices(poly))
- # outside
- poly = Quadrangle(P2(7, 6), P2(7, 7), P2(6, 7), P2(6, 6))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- clipped = clip(poly, other, SutherlandHodgman())
- @test isnothing(clipped)
+ # outside
+ poly = Quadrangle(cart(7, 6), cart(7, 7), cart(6, 7), cart(6, 6))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ @test isnothing(clipped)
- # surrounded
- poly = Hexagon(P2(0, 2), P2(-2, 2), P2(-2, 0), P2(0, -2), P2(2, -2), P2(2, 0))
- other = Hexagon(P2(1, 0), P2(0, 1), P2(-1, 1), P2(-1, 0), P2(0, -1), P2(1, -1))
- clipped = clip(poly, other, SutherlandHodgman())
- @test issimple(clipped)
- @test all(vertices(clipped) .≈ vertices(other))
+ # surrounded
+ poly = Hexagon(cart(0, 2), cart(-2, 2), cart(-2, 0), cart(0, -2), cart(2, -2), cart(2, 0))
+ other = Hexagon(cart(1, 0), cart(0, 1), cart(-1, 1), cart(-1, 0), cart(0, -1), cart(1, -1))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ @test issimple(clipped)
+ @test all(vertices(clipped) .≈ vertices(other))
- # PolyArea with box
- outer = Ring(P2(8, 0), P2(4, 8), P2(2, 8), P2(-2, 0), P2(0, 0), P2(1, 2), P2(5, 2), P2(6, 0))
- inner = Ring(P2(4, 4), P2(2, 4), P2(3, 6))
- poly = PolyArea([outer, inner])
- other = Box(P2(0, 1), P2(3, 7))
- clipped = clip(poly, other, SutherlandHodgman())
- crings = rings(clipped)
- @test !issimple(clipped)
- @test all(
- vertices(crings[1]) .≈
- [P2(1.5, 7.0), P2(0.0, 4.0), P2(0.0, 1.0), P2(0.5, 1.0), P2(1.0, 2.0), P2(3.0, 2.0), P2(3.0, 7.0)]
- )
- @test all(vertices(crings[2]) .≈ [P2(3.0, 4.0), P2(2.0, 4.0), P2(3.0, 6.0)])
+ # PolyArea with box
+ outer = Ring(cart(8, 0), cart(4, 8), cart(2, 8), cart(-2, 0), cart(0, 0), cart(1, 2), cart(5, 2), cart(6, 0))
+ inner = Ring(cart(4, 4), cart(2, 4), cart(3, 6))
+ poly = PolyArea([outer, inner])
+ other = Box(cart(0, 1), cart(3, 7))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ crings = rings(clipped)
+ @test !issimple(clipped)
+ @test all(
+ vertices(crings[1]) .≈
+ [cart(1.5, 7.0), cart(0.0, 4.0), cart(0.0, 1.0), cart(0.5, 1.0), cart(1.0, 2.0), cart(3.0, 2.0), cart(3.0, 7.0)]
+ )
+ @test all(vertices(crings[2]) .≈ [cart(3.0, 4.0), cart(2.0, 4.0), cart(3.0, 6.0)])
- # PolyArea with outer ring outside and inner ring inside
- outer = Ring(P2(8, 0), P2(2, 6), P2(-4, 0))
- inner = Ring(P2(1, 3), P2(3, 3), P2(3, 1), P2(1, 1))
- poly = PolyArea([outer, inner])
- other = Quadrangle(P2(4, 4), P2(0, 4), P2(0, 0), P2(4, 0))
- clipped = clip(poly, other, SutherlandHodgman())
- @test !issimple(clipped)
- crings = rings(clipped)
- @test all(vertices(crings[1]) .≈ vertices(other))
- @test all(vertices(crings[2]) .≈ vertices(inner))
+ # PolyArea with outer ring outside and inner ring inside
+ outer = Ring(cart(8, 0), cart(2, 6), cart(-4, 0))
+ inner = Ring(cart(1, 3), cart(3, 3), cart(3, 1), cart(1, 1))
+ poly = PolyArea([outer, inner])
+ other = Quadrangle(cart(4, 4), cart(0, 4), cart(0, 0), cart(4, 0))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ @test !issimple(clipped)
+ crings = rings(clipped)
+ @test all(vertices(crings[1]) .≈ vertices(other))
+ @test all(vertices(crings[2]) .≈ vertices(inner))
- # PolyArea with one inner ring inside `other` and another inner ring outside `other`
- outer = Ring(P2(6, 4), P2(6, 7), P2(1, 6), P2(1, 1), P2(5, 2))
- inner₁ = Ring(P2(3, 3), P2(3, 4), P2(4, 3))
- inner₂ = Ring(P2(2, 5), P2(2, 6), P2(3, 5))
- poly = PolyArea([outer, inner₁, inner₂])
- other = PolyArea(Ring(P2(6, 1), P2(7, 2), P2(6, 5), P2(0, 2), P2(1, 1)))
- clipped = clip(poly, other, SutherlandHodgman())
- crings = rings(clipped)
- @test !issimple(clipped)
- @test length(crings) == 2
- @test all(vertices(crings[1]) .≈ [P2(6, 4), P2(6, 5), P2(1, 2.5), P2(1, 1), P2(5, 2)])
- @test all(vertices(crings[2]) .≈ [P2(3.0, 3.0), P2(3.0, 3.5), P2(10 / 3, 11 / 3), P2(4.0, 3.0)])
- end
+ # PolyArea with one inner ring inside `other` and another inner ring outside `other`
+ outer = Ring(cart(6, 4), cart(6, 7), cart(1, 6), cart(1, 1), cart(5, 2))
+ inner₁ = Ring(cart(3, 3), cart(3, 4), cart(4, 3))
+ inner₂ = Ring(cart(2, 5), cart(2, 6), cart(3, 5))
+ poly = PolyArea([outer, inner₁, inner₂])
+ other = PolyArea(Ring(cart(6, 1), cart(7, 2), cart(6, 5), cart(0, 2), cart(1, 1)))
+ clipped = clip(poly, other, SutherlandHodgmanClipping())
+ crings = rings(clipped)
+ @test !issimple(clipped)
+ @test length(crings) == 2
+ @test all(vertices(crings[1]) .≈ [cart(6, 4), cart(6, 5), cart(1, 2.5), cart(1, 1), cart(5, 2)])
+ @test all(vertices(crings[2]) .≈ [cart(3.0, 3.0), cart(3.0, 3.5), cart(10 / 3, 11 / 3), cart(4.0, 3.0)])
end
diff --git a/test/coarsening.jl b/test/coarsening.jl
new file mode 100644
index 000000000..fb09f956e
--- /dev/null
+++ b/test/coarsening.jl
@@ -0,0 +1,58 @@
+@testitem "RegularCoarsening" setup = [Setup] begin
+ # 2D grids
+ grid = CartesianGrid(cart(0.0, 0.0), cart(10.0, 10.0), dims=(20, 20))
+ tgrid = CartesianGrid(cart(0.0, 0.0), cart(10.0, 10.0), dims=(10, 10))
+ @test coarsen(grid, RegularCoarsening(2)) == tgrid
+ rgrid = convert(RectilinearGrid, grid)
+ trgrid = convert(RectilinearGrid, tgrid)
+ @test coarsen(rgrid, RegularCoarsening(2)) == trgrid
+ sgrid = convert(StructuredGrid, grid)
+ tsgrid = convert(StructuredGrid, tgrid)
+ @test coarsen(sgrid, RegularCoarsening(2)) == tsgrid
+ tfgrid = TransformedGrid(grid, Identity())
+ @test coarsen(tfgrid, RegularCoarsening(2)) == coarsen(grid, RegularCoarsening(2))
+
+ grid = CartesianGrid(cart(0.0, 0.0), cart(10.0, 10.0), dims=(20, 20))
+ tgrid = CartesianGrid(cart(0.0, 0.0), cart(10.0, 10.0), dims=(10, 5))
+ @test coarsen(grid, RegularCoarsening(2, 4)) == tgrid
+
+ # non-multiple dimensions
+ grid = CartesianGrid(cart(0, 0), cart(13, 17), dims=(13, 17))
+ tgrid = CartesianGrid(cart(0, 0), cart(13, 17), dims=(3, 6))
+ @test coarsen(grid, RegularCoarsening(5, 3)) == tgrid
+ rgrid = convert(RectilinearGrid, grid)
+ @test size(coarsen(rgrid, RegularCoarsening(5, 3))) == (3, 6)
+ sgrid = convert(StructuredGrid, grid)
+ @test size(coarsen(sgrid, RegularCoarsening(5, 3))) == (3, 6)
+ tfgrid = TransformedGrid(grid, Identity())
+ @test size(coarsen(tfgrid, RegularCoarsening(5, 3))) == (3, 6)
+
+ # large grid
+ grid = CartesianGrid(cart(0, 0), cart(16200, 8100), dims=(16200, 8100))
+ tgrid = CartesianGrid(cart(0, 0), cart(16200, 8100), dims=(203, 203))
+ @test coarsen(grid, RegularCoarsening(80, 40)) == tgrid
+
+ # 3D grids
+ grid = cartgrid(100, 100, 100)
+ tgrid = CartesianGrid(minimum(grid), maximum(grid), dims=(50, 25, 20))
+ @test coarsen(grid, RegularCoarsening(2, 4, 5)) == tgrid
+ rgrid = convert(RectilinearGrid, grid)
+ trgrid = convert(RectilinearGrid, tgrid)
+ @test coarsen(rgrid, RegularCoarsening(2, 4, 5)) == trgrid
+ sgrid = convert(StructuredGrid, grid)
+ tsgrid = convert(StructuredGrid, tgrid)
+ @test coarsen(sgrid, RegularCoarsening(2, 4, 5)) == tsgrid
+ tfgrid = TransformedGrid(grid, Identity())
+ @test coarsen(tfgrid, RegularCoarsening(2, 4, 5)) == coarsen(grid, RegularCoarsening(2, 4, 5))
+
+ # non-multiple dimensions
+ grid = CartesianGrid(cart(0, 0, 0), cart(13, 17, 23), dims=(13, 17, 23))
+ tgrid = CartesianGrid(cart(0, 0, 0), cart(13, 17, 23), dims=(2, 4, 8))
+ @test coarsen(grid, RegularCoarsening(7, 5, 3)) == tgrid
+ rgrid = convert(RectilinearGrid, grid)
+ @test size(coarsen(rgrid, RegularCoarsening(7, 5, 3))) == (2, 4, 8)
+ sgrid = convert(StructuredGrid, grid)
+ @test size(coarsen(sgrid, RegularCoarsening(7, 5, 3))) == (2, 4, 8)
+ tfgrid = TransformedGrid(grid, Identity())
+ @test size(coarsen(tfgrid, RegularCoarsening(7, 5, 3))) == (2, 4, 8)
+end
diff --git a/test/complement.jl b/test/complement.jl
index 566a23ac2..4d79fb693 100644
--- a/test/complement.jl
+++ b/test/complement.jl
@@ -1,57 +1,57 @@
-@testset "complement" begin
+@testitem "Complement of geometries" setup = [Setup] begin
τ = atol(T)
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
p = !t
r = rings(p)
@test p isa PolyArea
@test length(r) == 2
- @test r[1] ≈ Ring(P2[(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)])
- @test r[2] == Ring(P2[(0, 0), (1, 1), (1, 0)])
+ @test r[1] ≈ Ring(cart.([(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)]))
+ @test r[2] == Ring(cart.([(0, 0), (1, 1), (1, 0)]))
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
p = !q
r = rings(p)
@test p isa PolyArea
@test length(r) == 2
- @test r[1] ≈ Ring(P2[(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)])
- @test r[2] == Ring(P2[(0, 0), (0, 1), (1, 1), (1, 0)])
+ @test r[1] ≈ Ring(cart.([(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)]))
+ @test r[2] == Ring(cart.([(0, 0), (0, 1), (1, 1), (1, 0)]))
- p = PolyArea(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
+ p = PolyArea(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
n = !p
r = rings(n)
@test n isa PolyArea
@test length(r) == 2
- @test r[1] ≈ Ring(P2[(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)])
- @test r[2] == Ring(P2[(0, 0), (0, 1), (1, 1), (1, 0)])
+ @test r[1] ≈ Ring(cart.([(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)]))
+ @test r[2] == Ring(cart.([(0, 0), (0, 1), (1, 1), (1, 0)]))
- o = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- i1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- i2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- p = PolyArea([o, i1, i2])
+ o = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ i1 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ i2 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ p = PolyArea([o, reverse(i1), reverse(i2)])
m = !p
r = rings(m)
@test m isa MultiPolygon
@test length(r) == 4
g = parent(m)
@test length(g) == 3
- @test rings(g[1])[1] ≈ Ring(P2[(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)])
- @test rings(g[1])[2] == Ring(P2[(0, 0), (0, 1), (1, 1), (1, 0)])
- @test rings(g[2]) == [Ring(P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)])]
- @test rings(g[3]) == [Ring(P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)])]
+ @test rings(g[1])[1] ≈ Ring(cart.([(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)]))
+ @test rings(g[1])[2] == Ring(cart.([(0, 0), (0, 1), (1, 1), (1, 0)]))
+ @test rings(g[2]) == [Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))]
+ @test rings(g[3]) == [Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))]
- b = Box(P2(0, 0), P2(1, 1))
+ b = Box(cart(0, 0), cart(1, 1))
p = !b
r = rings(p)
@test p isa PolyArea
@test length(r) == 2
- @test r[1] ≈ Ring(P2[(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)])
- @test r[2] == Ring(P2[(0, 0), (0, 1), (1, 1), (1, 0)])
+ @test r[1] ≈ Ring(cart.([(0 - τ, 0 - τ), (1 + τ, 0 - τ), (1 + τ, 1 + τ), (0 - τ, 1 + τ)]))
+ @test r[2] == Ring(cart.([(0, 0), (0, 1), (1, 1), (1, 0)]))
- b = Ball(P2(0, 0), T(1))
+ b = Ball(cart(0, 0), T(1))
p = !b
r = rings(p)
@test p isa PolyArea
@test length(r) == 2
- @test r[1] ≈ Ring(P2[(-1 - τ, -1 - τ), (1 + τ, -1 - τ), (1 + τ, 1 + τ), (-1 - τ, 1 + τ)])
+ @test r[1] ≈ Ring(cart.([(-1 - τ, -1 - τ), (1 + τ, -1 - τ), (1 + τ, 1 + τ), (-1 - τ, 1 + τ)]))
end
diff --git a/test/connectivities.jl b/test/connectivities.jl
index c615d91ea..a16195418 100644
--- a/test/connectivities.jl
+++ b/test/connectivities.jl
@@ -1,11 +1,11 @@
-@testset "Connectivities" begin
+@testitem "Connectivities" setup = [Setup] begin
# basic tests
c = connect((1, 2, 3), Triangle)
@test pltype(c) == Triangle
@test paramdim(c) == 2
@test issimplex(c)
@test indices(c) == (1, 2, 3)
- @test materialize(c, P2[(0, 0), (1, 0), (0, 1)]) == Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
+ @test materialize(c, cart.([(0, 0), (1, 0), (0, 1)])) == Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
# tuple from other collections
c = connect(Tuple([1, 2, 3]), Triangle)
@@ -13,7 +13,7 @@
@test paramdim(c) == 2
@test issimplex(c)
@test indices(c) == (1, 2, 3)
- @test materialize(c, P2[(0, 0), (1, 0), (0, 1)]) == Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
+ @test materialize(c, cart.([(0, 0), (1, 0), (0, 1)])) == Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
# incorrect number of vertices for polytope
@test_throws AssertionError connect((1, 2, 3, 4), Triangle)
@@ -44,6 +44,6 @@
@test paramdim(c) == 3
@test issimplex(c)
@test indices(c) == (1, 2, 3, 4)
- points = P3[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]
+ points = cart.([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)])
@test materialize(c, points) == Tetrahedron(points...)
end
diff --git a/test/crs.jl b/test/crs.jl
new file mode 100644
index 000000000..31b9bcd40
--- /dev/null
+++ b/test/crs.jl
@@ -0,0 +1,150 @@
+@testitem "Projected CRS" setup = [Setup] begin
+ g = merc(1, 1)
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Ray(merc(0, 0), vector(1, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Line(merc(0, 0), merc(1, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = BezierCurve(merc(0, 0), merc(1, 1), merc(2, 0))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = ParametrizedCurve(t -> merc(cos(t), sin(t)), (T(0), T(2π)))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Box(merc(0, 0), merc(1, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Ball(merc(0, 0), T(1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Sphere(merc(0, 0), T(1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Segment(merc(0, 0), merc(1, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Rope(merc(0, 0), merc(1, 0), merc(0, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Ring(merc(0, 0), merc(1, 0), merc(0, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Triangle(merc(0, 0), merc(1, 0), merc(0, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Quadrangle(merc(0, 0), merc(1, 0), merc(1, 1), merc(0, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = PolyArea(merc(0, 0), merc(1, 0), merc(0, 1))
+ @test crs(g) <: Mercator{WGS84Latest}
+ g = Multi([merc(0, 0), merc(1, 1)])
+ @test crs(g) <: Mercator{WGS84Latest}
+ t1 = Triangle(merc(0, 0), merc(1, 0), merc(0, 1))
+ t2 = Triangle(merc(1, 1), merc(2, 1), merc(1, 2))
+ d = GeometrySet([t1, t2])
+ @test crs(d) <: Mercator{WGS84Latest}
+ d = PointSet([merc(0, 0), merc(1, 1)])
+ @test crs(d) <: Mercator{WGS84Latest}
+ d = RegularGrid((10, 10), merc(0, 0), (T(1), T(1)))
+ @test crs(d) <: Mercator{WGS84Latest}
+ p = merc.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ @test crs(d) <: Mercator{WGS84Latest}
+end
+
+@testitem "Geographic CRS" setup = [Setup] begin
+ g = latlon(1, 1)
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Ray(latlon(0, 0), vector(1, 1, 1))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Line(latlon(0, 0), latlon(1, 1))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Plane(latlon(0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = BezierCurve(latlon(0, 0), latlon(1, 1), latlon(0, 2))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Box(latlon(0, 180), latlon(45, 90))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Ball(latlon(0, 0), T(1))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Sphere(latlon(0, 0), T(1))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Ellipsoid((T(3), T(2), T(1)), latlon(0, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ p = Plane(latlon(0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ g = Disk(p, T(2))
+ @test crs(g) <: LatLon{WGS84Latest}
+ p = Plane(latlon(0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ g = Circle(p, T(2))
+ @test crs(g) <: LatLon{WGS84Latest}
+ b = Plane(latlon(90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ t = Plane(latlon(-90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ g = Cylinder(b, t, T(5))
+ @test crs(g) <: LatLon{WGS84Latest}
+ b = Plane(latlon(-90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ t = Plane(latlon(90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ g = CylinderSurface(b, t, T(5))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = ParaboloidSurface(latlon(0, 0), T(1), T(2))
+ @test crs(g) <: LatLon{WGS84Latest}
+ p = Plane(latlon(-90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ d = Disk(p, T(2))
+ a = latlon(90, 0)
+ g = Cone(d, a)
+ @test crs(g) <: LatLon{WGS84Latest}
+ p = Plane(latlon(-90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ d = Disk(p, T(2))
+ a = latlon(90, 0)
+ g = ConeSurface(d, a)
+ @test crs(g) <: LatLon{WGS84Latest}
+ pb = Plane(latlon(-90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ db = Disk(pb, T(1))
+ pt = Plane(latlon(90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ dt = Disk(pt, T(2))
+ g = Frustum(db, dt)
+ @test crs(g) <: LatLon{WGS84Latest}
+ pb = Plane(latlon(-90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ db = Disk(pb, T(1))
+ pt = Plane(latlon(90, 0), vector(1, 0, 0), vector(0, 1, 0))
+ dt = Disk(pt, T(2))
+ g = FrustumSurface(db, dt)
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Torus(latlon(0, 0), vector(1, 0, 0), T(2), T(1))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Segment(latlon(0, 0), latlon(1, 1))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Rope(latlon(0, 0), latlon(0, 1), latlon(1, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Ring(latlon(0, 0), latlon(0, 1), latlon(1, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Triangle(latlon(0, 0), latlon(0, 1), latlon(1, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Quadrangle(latlon(0, 0), latlon(0, 1), latlon(1, 1), latlon(1, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = PolyArea(latlon(0, 0), latlon(0, 1), latlon(1, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Tetrahedron(latlon(0, 0), latlon(0, 90), latlon(0, -90), latlon(90, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Hexahedron(
+ latlon(0, 45),
+ latlon(0, 135),
+ latlon(0, -135),
+ latlon(0, -45),
+ latlon(1, 45),
+ latlon(1, 135),
+ latlon(1, -135),
+ latlon(1, -45)
+ )
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Pyramid(latlon(0, 45), latlon(0, 135), latlon(0, -135), latlon(0, -45), latlon(90, 0))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Wedge(latlon(0, 0), latlon(0, 90), latlon(0, -90), latlon(1, 0), latlon(1, 90), latlon(1, -90))
+ @test crs(g) <: LatLon{WGS84Latest}
+ g = Multi([latlon(0, 0), latlon(1, 1)])
+ @test crs(g) <: LatLon{WGS84Latest}
+ t1 = Triangle(latlon(0, 0), latlon(0, 1), latlon(1, 0))
+ t2 = Triangle(latlon(1, 1), latlon(1, 2), latlon(2, 1))
+ d = GeometrySet([t1, t2])
+ @test crs(d) <: LatLon{WGS84Latest}
+ d = PointSet([latlon(0, 0), latlon(1, 1)])
+ @test crs(d) <: LatLon{WGS84Latest}
+ d = RegularGrid((10, 10), latlon(0, 0), (T(1), T(1)))
+ @test crs(d) <: LatLon{WGS84Latest}
+ p = latlon.([(0, 0), (0, 1), (1, 0), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ @test crs(d) <: LatLon{WGS84Latest}
+ d = CylindricalTrajectory([latlon(0, 0), latlon(1, 1), latlon(0, 2)])
+ @test crs(d) <: LatLon{WGS84Latest}
+end
diff --git a/test/data/random-path-6x6.png b/test/data/random-path-6x6.png
index 83a6443a3..cfc1eb1b9 100644
Binary files a/test/data/random-path-6x6.png and b/test/data/random-path-6x6.png differ
diff --git a/test/data/random-path-7x7.png b/test/data/random-path-7x7.png
index 32ecaa620..51c2a09e6 100644
Binary files a/test/data/random-path-7x7.png and b/test/data/random-path-7x7.png differ
diff --git a/test/discretization.jl b/test/discretization.jl
index 602d8a13e..0bd88b158 100644
--- a/test/discretization.jl
+++ b/test/discretization.jl
@@ -1,509 +1,704 @@
-@testset "Discretization" begin
- @testset "FanTriangulation" begin
- pts = P2[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.75, 1.5), (0.25, 1.5), (0.0, 1.0)]
- tris = [Triangle(pts[1], pts[i], pts[i + 1]) for i in 2:(length(pts) - 1)]
- hex = Hexagon(pts...)
- mesh = discretize(hex, FanTriangulation())
- @test nvertices(mesh) == 6
- @test nelements(mesh) == 4
- @test eltype(mesh) <: Triangle
- @test vertices(mesh) == pts
- @test collect(elements(mesh)) == tris
- end
+@testitem "FanTriangulation" setup = [Setup] begin
+ pts = cart.([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.75, 1.5), (0.25, 1.5), (0.0, 1.0)])
+ tris = [Triangle(pts[1], pts[i], pts[i + 1]) for i in 2:(length(pts) - 1)]
+ hex = Hexagon(pts...)
+ mesh = discretize(hex, FanTriangulation())
+ @test nvertices(mesh) == 6
+ @test nelements(mesh) == 4
+ @test eltype(mesh) <: Triangle
+ @test vertices(mesh) == pts
+ @test collect(elements(mesh)) == tris
+
+ # type stability tests
+ poly = PolyArea(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @inferred discretize(poly, FanTriangulation())
+end
- @testset "RegularDiscretization" begin
- bezier = BezierCurve([P2(0, 0), P2(1, 0), P2(1, 1)])
- mesh = discretize(bezier, RegularDiscretization(10))
- @test nvertices(mesh) == 11
- @test nelements(mesh) == 10
- @test eltype(mesh) <: Segment
- @test nvertices.(mesh) ⊆ [2]
-
- box = Box(P2(0, 0), P2(2, 2))
- mesh = discretize(box, RegularDiscretization(10))
- @test mesh isa CartesianGrid
- @test nvertices(mesh) == 121
- @test nelements(mesh) == 100
- @test eltype(mesh) <: Quadrangle
- @test nvertices.(mesh) ⊆ [4]
-
- sphere = Sphere(P2(0, 0), T(1))
- mesh = discretize(sphere, RegularDiscretization(10))
- @test nvertices(mesh) == 10
- @test nelements(mesh) == 10
- @test eltype(mesh) <: Segment
- @test nvertices.(mesh) ⊆ [2]
-
- sphere = Sphere(P3(0, 0, 0), T(1))
- mesh = discretize(sphere, RegularDiscretization(10))
- @test nvertices(mesh) == 11 * 10 + 2
- @test nelements(mesh) == 10 * 10 + 2 * 10
- @test eltype(mesh) <: Ngon
- @test nvertices.(mesh) ⊆ [3, 4]
-
- ball = Ball(P2(0, 0), T(1))
- mesh = discretize(ball, RegularDiscretization(10))
- @test nvertices(mesh) == 11 * 10 + 1
- @test nelements(mesh) == 10 * 10 + 10
- @test eltype(mesh) <: Ngon
- @test nvertices.(mesh) ⊆ [3, 4]
-
- disk = Disk(Plane(P3(0, 0, 0), V3(0, 0, 1)), T(1))
- mesh = discretize(disk, RegularDiscretization(10))
- @test nvertices(mesh) == 11 * 10 + 1
- @test nelements(mesh) == 10 * 10 + 10
- @test eltype(mesh) <: Ngon
- @test nvertices.(mesh) ⊆ [3, 4]
-
- cylsurf = CylinderSurface(Plane(P3(0, 0, 0), V3(0, 0, 1)), Plane(P3(1, 1, 1), V3(0, 0, 1)), T(1))
- mesh = discretize(cylsurf, RegularDiscretization(10))
- @test nvertices(mesh) == 10 * 11 + 2
- @test nelements(mesh) == 10 * 10 + 2 * 10
- @test eltype(mesh) <: Ngon
- @test nvertices.(mesh) ⊆ [3, 4]
-
- consurf = ConeSurface(Disk(Plane(P3(0, 0, 0), V3(0, 0, 1)), T(1)), P3(0, 0, 1))
- mesh = discretize(consurf, RegularDiscretization(10))
- @test nvertices(mesh) == 10 * 11 + 2
- @test nelements(mesh) == 10 * 10 + 2 * 10
- @test eltype(mesh) <: Ngon
- @test nvertices.(mesh) ⊆ [3, 4]
-
- parsurf = rand(ParaboloidSurface{T})
- mesh = discretize(parsurf, RegularDiscretization(10))
- @test nvertices(mesh) == 10 * (10 + 1)
- @test nelements(mesh) == 10 * 10
- @test eltype(mesh) <: Ngon
- @test nvertices.(mesh) ⊆ [3, 4]
-
- poly = PolyArea(P2[(0, 0), (0, 1), (1, 2), (2, 1), (2, 0)])
- mesh = discretize(poly, RegularDiscretization(50))
- @test mesh isa SubGrid{2,T}
- grid = parent(mesh)
- @test grid isa CartesianGrid
- @test eltype(mesh) <: Quadrangle
- @test all(intersects(poly), mesh)
- end
+@testitem "DehnTriangulation" setup = [Setup] begin
+ octa = Octagon(
+ cart(0.2, 0.2),
+ cart(0.5, -0.5),
+ cart(0.8, 0.2),
+ cart(1.5, 0.5),
+ cart(0.8, 0.8),
+ cart(0.5, 1.5),
+ cart(0.2, 0.8),
+ cart(-0.5, 0.5)
+ )
+ mesh = discretize(octa, DehnTriangulation())
+ @test nvertices(mesh) == 8
+ @test nelements(mesh) == 6
+ @test eltype(mesh) <: Triangle
+
+ octa = Octagon(
+ cart(0.2, 0.2, 0.0),
+ cart(0.5, -0.5, 0.0),
+ cart(0.8, 0.2, 0.0),
+ cart(1.5, 0.5, 0.0),
+ cart(0.8, 0.8, 0.0),
+ cart(0.5, 1.5, 0.0),
+ cart(0.2, 0.8, 0.0),
+ cart(-0.5, 0.5, 0.0)
+ )
+ mesh = discretize(octa, DehnTriangulation())
+ @test nvertices(mesh) == 8
+ @test nelements(mesh) == 6
+ @test eltype(mesh) <: Triangle
+
+ # type stability tests
+ poly = PolyArea(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @inferred discretize(poly, DehnTriangulation())
+end
- @testset "Dehn1899" begin
- octa = Octagon(
- P2(0.2, 0.2),
- P2(0.5, -0.5),
- P2(0.8, 0.2),
- P2(1.5, 0.5),
- P2(0.8, 0.8),
- P2(0.5, 1.5),
- P2(0.2, 0.8),
- P2(-0.5, 0.5)
- )
- mesh = discretize(octa, Dehn1899())
- @test nvertices(mesh) == 8
- @test nelements(mesh) == 6
+@testitem "HeldTriangulation" setup = [Setup] begin
+ 𝒫 = Ring(cart.([(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2)]))
+ @test Meshes.earsccw(𝒫) == [2, 4, 5]
+
+ 𝒫 = Ring(cart.([(0, 0), (1, 0), (1, 1), (2, 1), (1, 2)]))
+ @test Meshes.earsccw(𝒫) == [2, 4]
+
+ 𝒫 = Ring(cart.([(0, 0), (1, 0), (1, 1), (1, 2)]))
+ @test Meshes.earsccw(𝒫) == [2, 4]
+
+ 𝒫 = Ring(cart.([(0, 0), (1, 1), (1, 2)]))
+ @test Meshes.earsccw(𝒫) == []
+
+ 𝒫 = Ring(
+ cart.([
+ (0.443339268495331, 0.283757618605357),
+ (0.497822414616971, 0.398142813114205),
+ (0.770343126156527, 0.201815462842808),
+ (0.761236456732531, 0.330085709922366),
+ (0.985658085510286, 0.221530395507904),
+ (0.877899962498139, 0.325516131702896),
+ (0.561404274882782, 0.540334008885703),
+ (0.949459768187313, 0.396227653478068),
+ (0.594962560615951, 0.584927547374551),
+ (0.324208409133154, 0.607290684450708),
+ (0.424085089823892, 0.493532112641353),
+ (0.209843417261654, 0.590030658255966),
+ (0.27993878548962, 0.525162463476181),
+ (0.385557753911967, 0.322338556632868)
+ ])
+ )
+ @test Meshes.earsccw(𝒫) == [1, 3, 5, 6, 8, 10, 12, 14]
+
+ points = cart.([(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2)])
+ connec = connect.([(4, 5, 6), (3, 4, 6), (3, 6, 1), (1, 2, 3)], Triangle)
+ target = SimpleMesh(points, connec)
+ poly = PolyArea(points)
+ mesh = discretize(poly, HeldTriangulation(shuffle=false))
+ @test mesh == target
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/675
+ poly = PolyArea(
+ cart.([
+ (1.1794224993e7, 1.7289506814e7),
+ (1.1794045018e7, 1.7289446822e7),
+ (1.1793985026e7, 1.7289486817e7),
+ (1.1793965029e7, 1.7289586803e7),
+ (1.1794105009e7, 1.7289766778e7),
+ (1.1794184998e7, 1.7289866764e7),
+ (1.179424499e7, 1.728996675e7),
+ (1.179424499e7, 1.7290106731e7),
+ (1.1794344976e7, 1.7290246711e7),
+ (1.1794364973e7, 1.7290386692e7),
+ (1.1794504954e7, 1.7290406689e7),
+ (1.1794724923e7, 1.729018672e7),
+ (1.1794624937e7, 1.7289946753e7),
+ (1.1794624937e7, 1.7289806772e7),
+ (1.1794564946e7, 1.7289706786e7),
+ (1.1794424965e7, 1.7289626797e7)
+ ])
+ )
+ rng = StableRNG(123)
+ mesh = discretize(poly, HeldTriangulation(rng))
+ @test nvertices(mesh) == 16
+ @test nelements(mesh) == 14
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/738
+ poly = PolyArea(
+ cart.([
+ (-0.5, 0.3296139),
+ (-0.19128194, -0.5),
+ (-0.37872985, 0.29592824),
+ (0.21377224, -0.0076110554),
+ (-0.20127837, 0.24671146)
+ ])
+ )
+ rng = StableRNG(123)
+ mesh = discretize(poly, HeldTriangulation(rng))
+ @test nvertices(mesh) == 5
+ @test nelements(mesh) == 3
+
+ # type stability tests
+ poly = PolyArea(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @inferred discretize(poly, HeldTriangulation())
+end
+
+@testitem "DelaunayTriangulation" setup = [Setup] begin
+ rng = StableRNG(123)
+ poly = Pentagon(cart(0, 0), cart(1, 0), cart(1, 1), cart(0.5, 2), cart(0, 1))
+ mesh = discretize(poly, DelaunayTriangulation(rng))
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+end
+
+@testitem "Misc triangulations" setup = [Setup] begin
+ rng = StableRNG(123)
+ for method in [DehnTriangulation(), HeldTriangulation(rng), DelaunayTriangulation(rng)]
+ triangle = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ mesh = discretize(triangle, method)
+ @test vertices(mesh) == [cart(0, 0), cart(1, 0), cart(0, 1)]
+ @test nelements(mesh) == 1
+ @test mesh[1] ≗ triangle
+
+ quadrangle = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ mesh = discretize(quadrangle, method)
+ elms = collect(elements(mesh))
+ @test vertices(mesh) == [cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1)]
+ @test eltype(elms) <: Triangle
+ @test length(elms) == 2
+
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ t = Triangle(cart(1, 0), cart(2, 1), cart(1, 1))
+ m = Multi([q, t])
+ mesh = discretize(m, method)
+ elms = collect(elements(mesh))
+ @test vertices(mesh) == [pointify(q); pointify(t)]
+ @test vertices(elms[1]) ⊆ vertices(q)
+ @test vertices(elms[2]) ⊆ vertices(q)
+ @test vertices(elms[3]) ⊆ vertices(t)
+ @test eltype(elms) <: Triangle
+ @test length(elms) == 3
+
+ outer = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ hole1 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ hole2 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ poly = PolyArea([outer, reverse(hole1), reverse(hole2)])
+ bpoly = poly |> Bridge(T(0.01))
+ mesh = discretizewithin(boundary(bpoly), method)
+ @test nvertices(mesh) == 16
+ @test nelements(mesh) == 14
+ @test all(t -> area(t) > zero(ℳ)^2, mesh)
+
+ # 3D chains
+ chain = Ring(cart.([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 1)]))
+ mesh = discretizewithin(chain, method)
+ @test vertices(mesh) == vertices(chain)
@test eltype(mesh) <: Triangle
+ @test nelements(mesh) == 2
- octa = Octagon(
- P3(0.2, 0.2, 0.0),
- P3(0.5, -0.5, 0.0),
- P3(0.8, 0.2, 0.0),
- P3(1.5, 0.5, 0.0),
- P3(0.8, 0.8, 0.0),
- P3(0.5, 1.5, 0.0),
- P3(0.2, 0.8, 0.0),
- P3(-0.5, 0.5, 0.0)
- )
- mesh = discretize(octa, Dehn1899())
- @test nvertices(mesh) == 8
- @test nelements(mesh) == 6
+ # latlon coordinates
+ poly = PolyArea(latlon(0, 0), latlon(0, 1), latlon(1, 1), latlon(1, 0))
+ mesh = discretize(poly, method)
+ @test vertices(mesh) == vertices(poly)
@test eltype(mesh) <: Triangle
+ @test nelements(mesh) == 2
+
+ # preserves order of vertices
+ poly = Quadrangle(cart(0, 1, 0), cart(1, 1, 0), cart(1, 0, 0), cart(0, 0, 0))
+ mesh = simplexify(poly)
+ @test pointify(mesh) == pointify(poly)
end
+end
- @testset "FIST" begin
- 𝒫 = Ring(P2[(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2)])
- @test Meshes.earsccw(𝒫) == [2, 4, 5]
-
- 𝒫 = Ring(P2[(0, 0), (1, 0), (1, 1), (2, 1), (1, 2)])
- @test Meshes.earsccw(𝒫) == [2, 4]
-
- 𝒫 = Ring(P2[(0, 0), (1, 0), (1, 1), (1, 2)])
- @test Meshes.earsccw(𝒫) == [2, 4]
-
- 𝒫 = Ring(P2[(0, 0), (1, 1), (1, 2)])
- @test Meshes.earsccw(𝒫) == []
-
- 𝒫 = Ring(
- P2[
- (0.443339268495331, 0.283757618605357),
- (0.497822414616971, 0.398142813114205),
- (0.770343126156527, 0.201815462842808),
- (0.761236456732531, 0.330085709922366),
- (0.985658085510286, 0.221530395507904),
- (0.877899962498139, 0.325516131702896),
- (0.561404274882782, 0.540334008885703),
- (0.949459768187313, 0.396227653478068),
- (0.594962560615951, 0.584927547374551),
- (0.324208409133154, 0.607290684450708),
- (0.424085089823892, 0.493532112641353),
- (0.209843417261654, 0.590030658255966),
- (0.27993878548962, 0.525162463476181),
- (0.385557753911967, 0.322338556632868)
- ]
- )
- @test Meshes.earsccw(𝒫) == [1, 3, 5, 6, 8, 10, 12, 14]
-
- points = P2[(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2)]
- connec = connect.([(4, 5, 6), (3, 4, 6), (3, 6, 1), (1, 2, 3)], Triangle)
- target = SimpleMesh(points, connec)
- poly = PolyArea(points)
- mesh = discretize(poly, FIST(shuffle=false))
- @test mesh == target
+@testitem "Difficult triangulations" setup = [Setup] begin
+ rng = StableRNG(123)
+ for method in [DehnTriangulation(), HeldTriangulation(rng)]
+ poly = readpoly(T, joinpath(datadir, "taubin.line"))
+ mesh = discretize(poly, method)
@test Set(vertices(poly)) == Set(vertices(mesh))
@test nelements(mesh) == length(vertices(mesh)) - 2
- # https://github.com/JuliaGeometry/Meshes.jl/issues/675
- poly = PolyArea(
- P2[
- (1.1794224993e7, 1.7289506814e7),
- (1.1794045018e7, 1.7289446822e7),
- (1.1793985026e7, 1.7289486817e7),
- (1.1793965029e7, 1.7289586803e7),
- (1.1794105009e7, 1.7289766778e7),
- (1.1794184998e7, 1.7289866764e7),
- (1.179424499e7, 1.728996675e7),
- (1.179424499e7, 1.7290106731e7),
- (1.1794344976e7, 1.7290246711e7),
- (1.1794364973e7, 1.7290386692e7),
- (1.1794504954e7, 1.7290406689e7),
- (1.1794724923e7, 1.729018672e7),
- (1.1794624937e7, 1.7289946753e7),
- (1.1794624937e7, 1.7289806772e7),
- (1.1794564946e7, 1.7289706786e7),
- (1.1794424965e7, 1.7289626797e7)
- ]
- )
- rng = MersenneTwister(123)
- mesh = discretize(poly, FIST(rng))
- @test nvertices(mesh) == 16
- @test nelements(mesh) == 14
- end
+ poly = readpoly(T, joinpath(datadir, "poly1.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "poly2.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "poly3.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "poly4.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "poly5.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "smooth1.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
- @testset "Miscellaneous" begin
- rng = MersenneTwister(123)
- for method in [FIST(rng), Dehn1899()]
- triangle = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- mesh = discretize(triangle, method)
- @test vertices(mesh) == [P2(0, 0), P2(1, 0), P2(0, 1)]
- @test collect(elements(mesh)) == [triangle]
-
- quadrangle = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- mesh = discretize(quadrangle, method)
- elms = collect(elements(mesh))
- @test vertices(mesh) == [P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1)]
- @test eltype(elms) <: Triangle
- @test length(elms) == 2
-
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- t = Triangle(P2(1, 0), P2(2, 1), P2(1, 1))
- m = Multi([q, t])
- mesh = discretize(m, method)
- elms = collect(elements(mesh))
- @test vertices(mesh) == [pointify(q); pointify(t)]
- @test vertices(elms[1]) ⊆ vertices(q)
- @test vertices(elms[2]) ⊆ vertices(q)
- @test vertices(elms[3]) ⊆ vertices(t)
- @test eltype(elms) <: Triangle
- @test length(elms) == 3
-
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly = PolyArea([outer, hole1, hole2])
- bpoly = poly |> Bridge(T(0.01))
- mesh = discretizewithin(boundary(bpoly), method)
- @test nvertices(mesh) == 16
- @test nelements(mesh) == 14
- @test all(t -> area(t) > zero(T), mesh)
-
- # 3D chains
- chain = Ring(P3[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 1)])
- mesh = discretizewithin(chain, method)
- @test vertices(mesh) == vertices(chain)
- @test eltype(mesh) <: Triangle
- @test nelements(mesh) == 2
-
- # preserves order of vertices
- poly = Quadrangle(P3(0, 1, 0), P3(1, 1, 0), P3(1, 0, 0), P3(0, 0, 0))
- mesh = simplexify(poly)
- @test pointify(mesh) == pointify(poly)
- end
+ poly = readpoly(T, joinpath(datadir, "smooth2.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "smooth3.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "smooth4.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ poly = readpoly(T, joinpath(datadir, "smooth5.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/738
+ poly = readpoly(T, joinpath(datadir, "hole1.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == 32
+
+ poly = readpoly(T, joinpath(datadir, "hole2.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == 30
+
+ poly = readpoly(T, joinpath(datadir, "hole3.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == 32
+
+ poly = readpoly(T, joinpath(datadir, "hole4.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == 30
+
+ poly = readpoly(T, joinpath(datadir, "hole5.line"))
+ mesh = discretize(poly, method)
+ @test Set(vertices(poly)) == Set(vertices(mesh))
+ @test nelements(mesh) == 32
end
- @testset "Difficult examples" begin
- rng = MersenneTwister(123)
- for method in [FIST(rng), Dehn1899()]
- poly = readpoly(T, joinpath(datadir, "taubin.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "poly1.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "poly2.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "poly3.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "poly4.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "poly5.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "smooth1.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "smooth2.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "smooth3.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "smooth4.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- poly = readpoly(T, joinpath(datadir, "smooth5.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/738
- #poly = readpoly(T, joinpath(datadir, "hole1.line"))
- #mesh = discretize(poly, method)
- #@test Set(vertices(poly)) == Set(vertices(mesh))
- #@test nelements(mesh) == 32
-
- poly = readpoly(T, joinpath(datadir, "hole2.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == 30
-
- poly = readpoly(T, joinpath(datadir, "hole3.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == 32
-
- poly = readpoly(T, joinpath(datadir, "hole4.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == 30
-
- poly = readpoly(T, joinpath(datadir, "hole5.line"))
- mesh = discretize(poly, method)
- @test Set(vertices(poly)) == Set(vertices(mesh))
- @test nelements(mesh) == 32
- end
-
- if T == Float64
- poly = PolyArea(
- P2[
- (-48.03012478813999, -18.323912004531923),
- (-48.030125176275845, -18.323904748608573),
- (-48.03017873307118, -18.323925747019675),
- (-48.03017945243984, -18.32393728592407),
- (-48.030185785831904, -18.32394021501982),
- (-48.03017951837907, -18.323938343610457),
- (-48.030124261780436, -18.32392184444903),
- (-48.0301218833633, -18.323910661117687)
- ]
- )
- mesh = discretize(poly)
- @test nvertices(mesh) == 8
- @test nelements(mesh) == 6
- end
-
- # degenerate triangle
- poly = PolyArea(P2[(0, 0), (1, 1), (1, 1)])
+ if T == Float64
+ poly = PolyArea(
+ cart.([
+ (-48.03012478813999, -18.323912004531923),
+ (-48.030125176275845, -18.323904748608573),
+ (-48.03017873307118, -18.323925747019675),
+ (-48.03017945243984, -18.32393728592407),
+ (-48.030185785831904, -18.32394021501982),
+ (-48.03017951837907, -18.323938343610457),
+ (-48.030124261780436, -18.32392184444903),
+ (-48.0301218833633, -18.323910661117687)
+ ])
+ )
mesh = discretize(poly)
- @test nvertices(mesh) == 3
- @test nelements(mesh) == 1
- @test vertices(mesh) == [P2(0, 0), P2(0, 0), P2(0, 0)]
- @test mesh[1] == Triangle(P2(0, 0), P2(0, 0), P2(0, 0))
+ @test nvertices(mesh) == 8
+ @test nelements(mesh) == 6
end
- @testset "Tetrahedralization" begin
- box = Box(P3(0, 0, 0), P3(1, 1, 1))
- hexa = Hexahedron(pointify(box)...)
- bmesh = discretize(box, Tetrahedralization())
- hmesh = discretize(hexa, Tetrahedralization())
- @test bmesh == hmesh
- @test nvertices(bmesh) == 8
- @test nelements(bmesh) == 5
- end
+ # degenerate triangle
+ poly = PolyArea(cart.([(0, 0), (1, 1), (1, 1)]))
+ mesh = discretize(poly)
+ @test nvertices(mesh) == 3
+ @test nelements(mesh) == 1
+ @test vertices(mesh) == [cart(0, 0), cart(0, 0), cart(0, 0)]
+ @test mesh[1] == Triangle(cart(0, 0), cart(0, 0), cart(0, 0))
+end
- @testset "Discretize" begin
- ball = Ball(P2(0, 0), T(1))
- mesh = discretize(ball)
- @test !(eltype(mesh) <: Triangle)
- @test !(eltype(mesh) <: Quadrangle)
- @test nelements(mesh) == 2550
-
- sphere = Sphere(P3(0, 0, 0), T(1))
- mesh = discretize(sphere)
- @test !(eltype(mesh) <: Triangle)
- @test !(eltype(mesh) <: Quadrangle)
- @test nelements(mesh) == 2600
-
- cylsurf = CylinderSurface(T(1))
- mesh = discretize(cylsurf)
- @test !(eltype(mesh) <: Triangle)
- @test !(eltype(mesh) <: Quadrangle)
- @test nelements(mesh) == 200
-
- grid = CartesianGrid(10)
- @test discretize(grid) == grid
-
- mesh = SimpleMesh(rand(P2, 3), connect.([(1, 2, 3)]))
- @test discretize(mesh) == mesh
- end
+@testitem "ManualSimplexification" setup = [Setup] begin
+ box = Box(cart(0), cart(1))
+ mesh = discretize(box, ManualSimplexification())
+ @test nvertices(mesh) == 2
+ @test nelements(mesh) == 1
+ @test eltype(mesh) <: Segment
+
+ box = Box(cart(0, 0), cart(1, 1))
+ mesh = discretize(box, ManualSimplexification())
+ @test nvertices(mesh) == 4
+ @test nelements(mesh) == 2
+ @test eltype(mesh) <: Triangle
+
+ box = Box(cart(0, 0, 0), cart(1, 1, 1))
+ mesh = discretize(box, ManualSimplexification())
+ @test nvertices(mesh) == 8
+ @test nelements(mesh) == 5
+ @test eltype(mesh) <: Tetrahedron
+
+ box = Box(latlon(0, 0), latlon(45, 45))
+ mesh = discretize(box, ManualSimplexification())
+ @test nvertices(mesh) == 4
+ @test nelements(mesh) == 2
+ @test eltype(mesh) <: Triangle
+
+ box = Box(cart(0, 0, 0), cart(1, 1, 1))
+ hexa = Hexahedron(pointify(box)...)
+ bmesh = discretize(box, ManualSimplexification())
+ hmesh = discretize(hexa, ManualSimplexification())
+ @test bmesh == hmesh
+end
- @testset "Simplexify" begin
- # simplexify is a helper function that calls an
- # appropriate discretization method depending on
- # the geometry type that is given to it
- box = Box(P1(0), P1(1))
- msh = simplexify(box)
- @test eltype(msh) <: Segment
- @test topology(msh) == GridTopology(1)
- @test nvertices(msh) == 2
- @test nelements(msh) == 1
- @test msh[1] == Segment(P1(0), P1(1))
-
- seg = Segment(P1(0), P1(1))
- msh = simplexify(seg)
- @test eltype(msh) <: Segment
- @test topology(msh) == GridTopology(1)
- @test nvertices(msh) == 2
- @test nelements(msh) == 1
- @test msh[1] == Segment(P1(0), P1(1))
-
- chn = Rope(P2[(0, 0), (1, 0), (1, 1)])
- msh = simplexify(chn)
- @test eltype(msh) <: Segment
- @test nvertices(msh) == 3
- @test nelements(msh) == 2
- @test msh[1] == Segment(P2(0, 0), P2(1, 0))
- @test msh[2] == Segment(P2(1, 0), P2(1, 1))
- chn = Ring(P2[(0, 0), (1, 0), (1, 1)])
- msh = simplexify(chn)
- @test eltype(msh) <: Segment
- @test nvertices(msh) == 3
- @test nelements(msh) == 3
- @test msh[1] == Segment(P2(0, 0), P2(1, 0))
- @test msh[2] == Segment(P2(1, 0), P2(1, 1))
- @test msh[3] == Segment(P2(1, 1), P2(0, 0))
-
- sph = Sphere(P2(0, 0), T(1))
- msh = simplexify(sph)
- @test eltype(msh) <: Segment
- @test nvertices(msh) == nelements(msh)
-
- bez = BezierCurve(P2[(0, 0), (1, 0), (1, 1)])
- msh = simplexify(bez)
- @test eltype(msh) <: Segment
- @test nvertices(msh) == nelements(msh) + 1
-
- box = Box(P2(0, 0), P2(1, 1))
- ngon = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- poly = readpoly(T, joinpath(datadir, "taubin.line"))
- for geom in [box, ngon, poly]
- bound = boundary(geom)
- mesh = simplexify(geom)
- @test Set(vertices(bound)) == Set(vertices(mesh))
- @test nelements(mesh) == length(vertices(mesh)) - 2
- end
-
- # triangulation of multi geometries
- box1 = Box(P2(0, 0), P2(1, 1))
- box2 = Box(P2(1, 1), P2(2, 2))
- multi = Multi([box1, box2])
- mesh = simplexify(multi)
- @test nvertices(mesh) == 8
- @test nelements(mesh) == 4
+@testitem "RegularDiscretization" setup = [Setup] begin
+ bezier = BezierCurve([cart(0, 0), cart(1, 0), cart(1, 1)])
+ mesh = discretize(bezier, RegularDiscretization(10))
+ @test nvertices(mesh) == 11
+ @test nelements(mesh) == 10
+ @test eltype(mesh) <: Segment
+ @test nvertices.(mesh) ⊆ [2]
+
+ curve = ParametrizedCurve(t -> cart(cos(t), sin(t)), (T(0), T(2π)))
+ mesh = discretize(curve, RegularDiscretization(10))
+ @test nvertices(mesh) == 11
+ @test nelements(mesh) == 10
+ @test eltype(mesh) <: Segment
+ @test nvertices.(mesh) ⊆ [2]
+
+ box = Box(cart(0, 0), cart(2, 2))
+ mesh = discretize(box, RegularDiscretization(10))
+ @test mesh isa CartesianGrid
+ @test nvertices(mesh) == 121
+ @test nelements(mesh) == 100
+ @test eltype(mesh) <: Quadrangle
+ @test nvertices.(mesh) ⊆ [4]
+
+ box = Box(merc(0, 0), merc(2, 2))
+ mesh = discretize(box, RegularDiscretization(10))
+ @test mesh isa RegularGrid
+ @test crs(mesh) <: Mercator
+ @test nvertices(mesh) == 121
+ @test nelements(mesh) == 100
+ @test eltype(mesh) <: Quadrangle
+ @test nvertices.(mesh) ⊆ [4]
+
+ box = Box(latlon(-50, 150), latlon(50, 30))
+ mesh = discretize(box, RegularDiscretization(10))
+ @test mesh isa RegularGrid
+ @test crs(mesh) <: LatLon
+ @test nvertices(mesh) == 121
+ @test nelements(mesh) == 100
+ @test eltype(mesh) <: Quadrangle
+ @test nvertices.(mesh) ⊆ [4]
+
+ sphere = Sphere(cart(0, 0), T(1))
+ mesh = discretize(sphere, RegularDiscretization(10))
+ @test nvertices(mesh) == 10
+ @test nelements(mesh) == 10
+ @test eltype(mesh) <: Segment
+ @test nvertices.(mesh) ⊆ [2]
+
+ sphere = Sphere(cart(0, 0, 0), T(1))
+ mesh = discretize(sphere, RegularDiscretization(10))
+ @test nvertices(mesh) == 11 * 10 + 2
+ @test nelements(mesh) == 10 * 10 + 2 * 10
+ @test eltype(mesh) <: Ngon
+ @test nvertices.(mesh) ⊆ [3, 4]
+
+ ellips = Ellipsoid((T(3), T(2), T(1)))
+ mesh = discretize(ellips, RegularDiscretization(10))
+ @test nvertices(mesh) == 11 * 10 + 2
+ @test nelements(mesh) == 10 * 10 + 2 * 10
+ @test eltype(mesh) <: Ngon
+ @test nvertices.(mesh) ⊆ [3, 4]
+
+ ball = Ball(cart(0, 0), T(1))
+ mesh = discretize(ball, RegularDiscretization(10))
+ @test nvertices(mesh) == 11 * 10 + 1
+ @test nelements(mesh) == 10 * 10 + 10
+ @test eltype(mesh) <: Ngon
+ @test nvertices.(mesh) ⊆ [3, 4]
+
+ disk = Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(1))
+ mesh = discretize(disk, RegularDiscretization(10))
+ @test nvertices(mesh) == 11 * 10 + 1
+ @test nelements(mesh) == 10 * 10 + 10
+ @test eltype(mesh) <: Ngon
+ @test nvertices.(mesh) ⊆ [3, 4]
+
+ cyl = Cylinder(Plane(cart(0, 0, 0), vector(0, 0, 1)), Plane(cart(1, 1, 1), vector(0, 0, 1)), T(1))
+ mesh = discretize(cyl, RegularDiscretization(10))
+ @test nvertices(mesh) == 11 * 10 * 11 + 11
+ @test nelements(mesh) == 11 * 10 * 10
+ @test eltype(mesh) <: Polyhedron
+ @test nvertices.(mesh) ⊆ [6, 8]
+
+ cylsurf = CylinderSurface(Plane(cart(0, 0, 0), vector(0, 0, 1)), Plane(cart(1, 1, 1), vector(0, 0, 1)), T(1))
+ mesh = discretize(cylsurf, RegularDiscretization(10))
+ @test nvertices(mesh) == 10 * 11 + 2
+ @test nelements(mesh) == 10 * 10 + 2 * 10
+ @test eltype(mesh) <: Ngon
+ @test nvertices.(mesh) ⊆ [3, 4]
+
+ consurf = ConeSurface(Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(1)), cart(0, 0, 1))
+ mesh = discretize(consurf, RegularDiscretization(10))
+ @test nvertices(mesh) == 10 * 11 + 2
+ @test nelements(mesh) == 10 * 10 + 2 * 10
+ @test eltype(mesh) <: Ngon
+ @test nvertices.(mesh) ⊆ [3, 4]
+
+ parsurf = rand(ParaboloidSurface)
+ mesh = discretize(parsurf, RegularDiscretization(10))
+ @test nvertices(mesh) == 10 * (10 + 1)
+ @test nelements(mesh) == 10 * 10
+ @test eltype(mesh) <: Ngon
+ @test nvertices.(mesh) ⊆ [3, 4]
+
+ poly = PolyArea(cart.([(0, 0), (0, 1), (1, 2), (2, 1), (2, 0)]))
+ mesh = discretize(poly, RegularDiscretization(50))
+ @test mesh isa Meshes.SubGrid
+ grid = parent(mesh)
+ @test grid isa CartesianGrid
+ @test eltype(mesh) <: Quadrangle
+ @test all(intersects(poly), mesh)
+end
- # triangulation of spheres
- sphere = Sphere(P3(0, 0, 0), T(1))
- mesh = simplexify(sphere)
- @test eltype(mesh) <: Triangle
- xs = coordinates.(vertices(mesh))
- @test all(x -> norm(x) ≈ T(1), xs)
+@testitem "MaxLengthDiscretization" setup = [Setup] begin
+ box = Box(cart(0, 0), cart(10, 10))
+ mesh = discretize(box, MaxLengthDiscretization(T(1)))
+ @test nvertices(mesh) == 11 * 11
+ @test nelements(mesh) == 10 * 10
+ @test eltype(mesh) <: Quadrangle
+ @test nvertices.(mesh) ⊆ [4]
+
+ box = Box(latlon(0, 0), latlon(45, 45))
+ mesh = discretize(box, MaxLengthDiscretization(T(1e5)))
+ @test nvertices(mesh) == 52 * 52
+ @test nelements(mesh) == 51 * 51
+ @test eltype(mesh) <: Quadrangle
+ @test nvertices.(mesh) ⊆ [4]
+
+ seg = Segment(cart(0, 0), cart(0, 1))
+ mesh = discretize(seg, MaxLengthDiscretization(T(0.1)))
+ @test nvertices(mesh) == 11
+ @test nelements(mesh) == 10
+ @test eltype(mesh) <: Segment
+ @test nvertices.(mesh) ⊆ [2]
+
+ seg = Segment(latlon(0, 0), latlon(0, 45))
+ mesh = discretize(seg, MaxLengthDiscretization(T(1e5)))
+ @test nvertices(mesh) == 52
+ @test nelements(mesh) == 51
+ @test eltype(mesh) <: Segment
+ @test nvertices.(mesh) ⊆ [2]
+
+ rope = Rope(cart(0, 0), cart(1, 0), cart(0, 1))
+ mesh = discretize(rope, MaxLengthDiscretization(T(0.1)))
+ @test nvertices(mesh) == 27
+ @test nelements(mesh) == 25
+ @test eltype(mesh) <: Segment
+ @test nvertices.(mesh) ⊆ [2]
+
+ ring = Ring(latlon(-45, 90), latlon(45, 90), latlon(45, -90), latlon(-45, -90))
+ mesh = discretize(ring, MaxLengthDiscretization(T(1e5)))
+ @test nvertices(mesh) == 408
+ @test nelements(mesh) == 404
+ @test eltype(mesh) <: Segment
+ @test nvertices.(mesh) ⊆ [2]
+
+ tri = Triangle(cart(0, 0), cart(10, 0), cart(0, 10))
+ mesh = discretize(tri, MaxLengthDiscretization(T(3)))
+ @test nvertices(mesh) == 15
+ @test nelements(mesh) == 16
+ @test eltype(mesh) <: Triangle
+ @test nvertices.(mesh) ⊆ [3]
+
+ quad = Quadrangle(latlon(0, 0), latlon(0, 45), latlon(45, 45), latlon(45, 0))
+ mesh = discretize(quad, MaxLengthDiscretization(T(1e6)))
+ @test nvertices(mesh) == 81
+ @test nelements(mesh) == 128
+ @test eltype(mesh) <: Triangle
+ @test nvertices.(mesh) ⊆ [3]
+
+ quad1 = Quadrangle(latlon(0, 0), latlon(0, 45), latlon(45, 45), latlon(45, 0))
+ quad2 = Quadrangle(latlon(0, 0), latlon(-45, 0), latlon(-45, 45), latlon(0, 45))
+ multi = Multi([quad1, quad2])
+ mesh = discretize(multi, MaxLengthDiscretization(T(1e6)))
+ @test nvertices(mesh) == 162
+ @test nelements(mesh) == 256
+ @test eltype(mesh) <: Triangle
+ @test nvertices.(mesh) ⊆ [3]
+
+ box = Box(latlon(0, 0), latlon(45, 45))
+ tbox = TransformedGeometry(box, Proj(Mercator))
+ mesh = discretize(tbox, MaxLengthDiscretization(T(1e5)))
+ @test nvertices(mesh) == 52 * 52
+ @test nelements(mesh) == 51 * 51
+ @test eltype(mesh) <: Quadrangle
+ @test nvertices.(mesh) ⊆ [4]
+end
- # triangulation of cylinder surfaces
- cylsurf = CylinderSurface(T(1))
- mesh = simplexify(cylsurf)
- @test eltype(mesh) <: Triangle
- xs = coordinates.(vertices(mesh))
- @test all(x -> T(-1) ≤ x[1] ≤ T(1), xs)
- @test all(x -> T(-1) ≤ x[2] ≤ T(1), xs)
- @test all(x -> T(0) ≤ x[3] ≤ T(1), xs)
-
- # triangulation of balls
- ball = Ball(P2(0, 0), T(1))
- mesh = simplexify(ball)
- @test eltype(mesh) <: Triangle
- xs = coordinates.(vertices(mesh))
- @test all(x -> norm(x) ≤ T(1) + eps(T), xs)
+@testitem "Discretize" setup = [Setup] begin
+ ball = Ball(cart(0, 0), T(1))
+ mesh = discretize(ball)
+ @test !(eltype(mesh) <: Triangle)
+ @test !(eltype(mesh) <: Quadrangle)
+ @test nelements(mesh) == 2550
+
+ sphere = Sphere(cart(0, 0, 0), T(1))
+ mesh = discretize(sphere)
+ @test !(eltype(mesh) <: Triangle)
+ @test !(eltype(mesh) <: Quadrangle)
+ @test nelements(mesh) == 2600
+
+ cyl = Cylinder(T(1))
+ mesh = discretize(cyl)
+ @test !(eltype(mesh) <: Wedge)
+ @test !(eltype(mesh) <: Hexahedron)
+ @test nelements(mesh) == 300
+
+ cylsurf = CylinderSurface(T(1))
+ mesh = discretize(cylsurf)
+ @test !(eltype(mesh) <: Triangle)
+ @test !(eltype(mesh) <: Quadrangle)
+ @test nelements(mesh) == 200
+
+ box = Box(latlon(0, 0), latlon(45, 45))
+ tbox = TransformedGeometry(box, Proj(Mercator))
+ mesh = discretize(tbox)
+ @test nvertices(mesh) == 49
+ @test nelements(mesh) == 36
+ @test eltype(mesh) <: Quadrangle
+
+ grid = CartesianGrid(10)
+ @test discretize(grid) == grid
+
+ mesh = SimpleMesh(randpoint2(3), connect.([(1, 2, 3)]))
+ @test discretize(mesh) == mesh
+end
- # triangulation of meshes
- grid = CartesianGrid{T}(3, 3)
+@testitem "Simplexify" setup = [Setup] begin
+ # simplexify is a helper function that calls an
+ # appropriate discretization method depending on
+ # the geometry type that is given to it
+ box = Box(cart(0), cart(1))
+ msh = simplexify(box)
+ @test eltype(msh) <: Segment
+ @test topology(msh) == GridTopology(1)
+ @test nvertices(msh) == 2
+ @test nelements(msh) == 1
+ @test msh[1] == Segment(cart(0), cart(1))
+
+ seg = Segment(cart(0), cart(1))
+ msh = simplexify(seg)
+ @test eltype(msh) <: Segment
+ @test topology(msh) == GridTopology(1)
+ @test nvertices(msh) == 2
+ @test nelements(msh) == 1
+ @test msh[1] == Segment(cart(0), cart(1))
+
+ chn = Rope(cart.([(0, 0), (1, 0), (1, 1)]))
+ msh = simplexify(chn)
+ @test eltype(msh) <: Segment
+ @test nvertices(msh) == 3
+ @test nelements(msh) == 2
+ @test msh[1] == Segment(cart(0, 0), cart(1, 0))
+ @test msh[2] == Segment(cart(1, 0), cart(1, 1))
+ chn = Ring(cart.([(0, 0), (1, 0), (1, 1)]))
+ msh = simplexify(chn)
+ @test eltype(msh) <: Segment
+ @test nvertices(msh) == 3
+ @test nelements(msh) == 3
+ @test msh[1] == Segment(cart(0, 0), cart(1, 0))
+ @test msh[2] == Segment(cart(1, 0), cart(1, 1))
+ @test msh[3] == Segment(cart(1, 1), cart(0, 0))
+
+ sph = Sphere(cart(0, 0), T(1))
+ msh = simplexify(sph)
+ @test eltype(msh) <: Segment
+ @test nvertices(msh) == nelements(msh)
+
+ bez = BezierCurve(cart.([(0, 0), (1, 0), (1, 1)]))
+ msh = simplexify(bez)
+ @test eltype(msh) <: Segment
+ @test nvertices(msh) == nelements(msh) + 1
+
+ curve = ParametrizedCurve(t -> cart(cos(t), sin(t)), (T(0), T(2π)))
+ msh = simplexify(curve)
+ @test eltype(msh) <: Segment
+ @test nvertices(msh) == nelements(msh) + 1
+
+ box = Box(cart(0, 0), cart(1, 1))
+ ngon = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ poly = readpoly(T, joinpath(datadir, "taubin.line"))
+ for geom in [box, ngon, poly]
+ bound = boundary(geom)
+ mesh = simplexify(geom)
+ @test Set(vertices(bound)) == Set(vertices(mesh))
+ @test nelements(mesh) == length(vertices(mesh)) - 2
+ end
+
+ # triangulation of multi geometries
+ box1 = Box(cart(0, 0), cart(1, 1))
+ box2 = Box(cart(1, 1), cart(2, 2))
+ multi = Multi([box1, box2])
+ mesh = simplexify(multi)
+ @test nvertices(mesh) == 8
+ @test nelements(mesh) == 4
+
+ # triangulation of spheres
+ sphere = Sphere(cart(0, 0, 0), T(1))
+ mesh = simplexify(sphere)
+ @test eltype(mesh) <: Triangle
+ xs = to.(vertices(mesh))
+ @test all(x -> norm(x) ≈ oneunit(ℳ), xs)
+
+ # triangulation of cylinder surfaces
+ cylsurf = CylinderSurface(T(1))
+ mesh = simplexify(cylsurf)
+ @test eltype(mesh) <: Triangle
+ xs = to.(vertices(mesh))
+ @test all(x -> -oneunit(ℳ) ≤ x[1] ≤ oneunit(ℳ), xs)
+ @test all(x -> -oneunit(ℳ) ≤ x[2] ≤ oneunit(ℳ), xs)
+ @test all(x -> zero(ℳ) ≤ x[3] ≤ oneunit(ℳ), xs)
+
+ # triangulation of balls
+ ball = Ball(cart(0, 0), T(1))
+ mesh = simplexify(ball)
+ @test eltype(mesh) <: Triangle
+ xs = to.(vertices(mesh))
+ @test all(x -> norm(x) ≤ oneunit(ℳ) + eps(T) * u"m", xs)
+
+ # triangulation of meshes
+ grid = cartgrid(3, 3)
+ mesh = simplexify(grid)
+ gpts = vertices(grid)
+ mpts = vertices(mesh)
+ @test nvertices(mesh) == 16
+ @test nelements(mesh) == 18
+ @test collect(mpts) == collect(gpts)
+ @test eltype(mesh) <: Triangle
+ @test measure(mesh) == measure(grid)
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/499
+ quad = Quadrangle(cart(0, 1, -1), cart(0, 1, 1), cart(0, -1, 1), cart(0, -1, -1))
+ mesh = simplexify(quad)
+ @test vertices(mesh) == pointify(quad)
+
+ if visualtests
+ grid = cartgrid(3, 3)
mesh = simplexify(grid)
- gpts = vertices(grid)
- mpts = vertices(mesh)
- @test nvertices(mesh) == 16
- @test nelements(mesh) == 18
- @test collect(mpts) == collect(gpts)
- @test eltype(mesh) <: Triangle
- @test measure(mesh) == measure(grid)
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/499
- quad = Quadrangle(P3(0, 1, -1), P3(0, 1, 1), P3(0, -1, 1), P3(0, -1, -1))
- mesh = simplexify(quad)
- @test vertices(mesh) == pointify(quad)
-
- if visualtests
- grid = CartesianGrid{T}(3, 3)
- mesh = simplexify(grid)
- fig = Mke.Figure(size=(600, 300))
- viz(fig[1, 1], grid, showfacets=true)
- viz(fig[1, 2], mesh, showfacets=true)
- @test_reference "data/triangulate-$T.png" fig
- end
-
- # tetrahedralization
- box = Box(P3(0, 0, 0), P3(1, 1, 1))
- hex = Hexahedron(pointify(box)...)
- bmesh = simplexify(box)
- hmesh = simplexify(hex)
- @test bmesh == hmesh
- @test nvertices(bmesh) == 8
- @test nelements(bmesh) == 5
+ fig = Mke.Figure(size=(600, 300))
+ viz(fig[1, 1], grid, showsegments=true)
+ viz(fig[1, 2], mesh, showsegments=true)
+ @test_reference "data/triangulate-$T.png" fig
end
+
+ # tetrahedralization
+ box = Box(cart(0, 0, 0), cart(1, 1, 1))
+ hex = Hexahedron(pointify(box)...)
+ bmesh = simplexify(box)
+ hmesh = simplexify(hex)
+ @test bmesh == hmesh
+ @test nvertices(bmesh) == 8
+ @test nelements(bmesh) == 5
end
diff --git a/test/distances.jl b/test/distances.jl
index 5b628e1cc..ff107856a 100644
--- a/test/distances.jl
+++ b/test/distances.jl
@@ -1,23 +1,41 @@
-@testset "Distances" begin
- p = P2(0, 1)
- l = Line(P2(0, 0), P2(1, 0))
- @test evaluate(Euclidean(), p, l) == T(1)
- @test evaluate(Euclidean(), l, p) == T(1)
+@testitem "Distances" setup = [Setup] begin
+ p = cart(0, 1)
+ l = Line(cart(0, 0), cart(1, 0))
+ @test evaluate(Euclidean(), p, l) == T(1) * u"m"
+ @test evaluate(Euclidean(), l, p) == T(1) * u"m"
- p1, p2 = P2(1, 0), P2(0, 1)
- @test evaluate(Chebyshev(), p1, p2) == T(1)
+ p = cart(68, 259)
+ l = Line(cart(68, 260), cart(69, 261))
+ @test evaluate(Euclidean(), p, l) ≤ T(0.8) * u"m"
- p = P2(68, 259)
- l = Line(P2(68, 260), P2(69, 261))
- @test evaluate(Euclidean(), p, l) ≤ T(0.8)
+ line1 = Line(cart(-1, 0, 0), cart(1, 0, 0))
+ line2 = Line(cart(0, -1, 1), cart(0, 1, 1)) # line2 ⟂ line1, z++
+ line3 = Line(cart(-1, 1, 0), cart(1, 1, 0)) # line3 ∥ line1
+ line4 = Line(cart(-2, 0, 0), cart(2, 0, 0)) # line4 colinear with line1
+ line5 = Line(cart(0, -1, 0), cart(0, 1, 0)) # line5 intersects line1
+ @test evaluate(Euclidean(), line1, line2) ≈ T(1) * u"m"
+ @test evaluate(Euclidean(), line1, line3) ≈ T(1) * u"m"
+ @test evaluate(Euclidean(), line1, line4) ≈ T(0) * u"m"
+ @test evaluate(Euclidean(), line1, line5) ≈ T(0) * u"m"
- line1 = Line(P3(-1, 0, 0), P3(1, 0, 0))
- line2 = Line(P3(0, -1, 1), P3(0, 1, 1)) # line2 ⟂ line1, z++
- line3 = Line(P3(-1, 1, 0), P3(1, 1, 0)) # line3 ∥ line1
- line4 = Line(P3(-2, 0, 0), P3(2, 0, 0)) # line4 colinear with line1
- line5 = Line(P3(0, -1, 0), P3(0, 1, 0)) # line5 intersects line1
- @test evaluate(Euclidean(), line1, line2) ≈ T(1)
- @test evaluate(Euclidean(), line1, line3) ≈ T(1)
- @test evaluate(Euclidean(), line1, line4) ≈ T(0)
- @test evaluate(Euclidean(), line1, line5) ≈ T(0)
+ p1, p2 = cart(1, 0), cart(0, 1)
+ @test evaluate(Chebyshev(), p1, p2) == T(1) * u"m"
+ @test evaluate(Euclidean(), p1, p2) == T(√2) * u"m"
+
+ latlon1 = LatLon(T(0), T(0))
+ latlon2 = LatLon(T(1), T(0))
+ cart1 = convert(Cartesian, latlon1)
+ cart2 = convert(Cartesian, latlon2)
+ p1 = Point(latlon1)
+ p2 = Point(latlon2)
+ p3 = Point(cart1)
+ p4 = Point(cart2)
+ @test evaluate(Haversine(), p1, p2) ≈ T(111194.92664455874) * u"m"
+ @test evaluate(Haversine(), p3, p4) ≈ T(111194.92664455874) * u"m"
+ @test evaluate(Haversine(6371000u"m"), p1, p2) ≈ T(111194.92664455874) * u"m"
+ @test evaluate(Haversine(6371000u"m"), p3, p4) ≈ T(111194.92664455874) * u"m"
+ @test evaluate(Haversine(6371u"km"), p1, p2) ≈ T(111.19492664455874) * u"km"
+ @test evaluate(Haversine(6371u"km"), p3, p4) ≈ T(111.19492664455874) * u"km"
+ @test evaluate(SphericalAngle(), p1, p2) ≈ deg2rad(T(1) * u"°")
+ @test evaluate(SphericalAngle(), p3, p4) ≈ deg2rad(T(1) * u"°")
end
diff --git a/test/domains.jl b/test/domains.jl
index 1e33d0f77..019b26eda 100644
--- a/test/domains.jl
+++ b/test/domains.jl
@@ -1,39 +1,44 @@
-@testset "Domain" begin
+@testitem "Domain" setup = [Setup] begin
# basic properties
- dom = DummyDomain(P2(0, 0))
+ dom = DummyDomain(cart(0, 0))
@test embeddim(dom) == 2
- @test coordtype(dom) == T
+ @test crs(dom) <: Cartesian{NoDatum}
+ @test Meshes.lentype(dom) == ℳ
@test !isparametrized(dom)
# indexable/iterable interface
- dom = DummyDomain(P2(0, 0))
- @test dom[begin] == Ball(P2(1, 1), T(1))
- @test dom[end] == Ball(P2(3, 3), T(1))
- @test eltype(dom) <: Ball{2,T}
+ dom = DummyDomain(cart(0, 0))
+ @test dom[begin] == Ball(cart(1, 1), T(1))
+ @test dom[end] == Ball(cart(3, 3), T(1))
+ @test eltype(dom) <: Ball
@test length(dom) == 3
@test keys(dom) == 1:3
- @test collect(dom) == [Ball(P2(i, i), T(1)) for i in 1:3]
- @test dom[1:2] == [Ball(P2(i, i), T(1)) for i in 1:2]
+ @test collect(dom) == [Ball(cart(i, i), T(1)) for i in 1:3]
+ @test dom[1:2] == [Ball(cart(i, i), T(1)) for i in 1:2]
# coordinates of centroids
- dom = DummyDomain(P2(1, 1))
+ dom = DummyDomain(cart(1, 1))
pts = centroid.(Ref(dom), 1:3)
- @test pts == P2[(2, 2), (3, 3), (4, 4)]
+ @test pts == cart.([(2, 2), (3, 3), (4, 4)])
# concatenation
- dom1 = DummyDomain(P2(0, 0))
- dom2 = DummyDomain(P2(3, 3))
- dom3 = PointSet(rand(P2, 3))
+ dom1 = DummyDomain(cart(0, 0))
+ dom2 = DummyDomain(cart(3, 3))
+ dom3 = PointSet(randpoint2(3))
@test vcat(dom1, dom2) == GeometrySet([collect(dom1); collect(dom2)])
@test vcat(dom2, dom3) == GeometrySet([collect(dom2); collect(dom3)])
@test vcat(dom3, dom1) == GeometrySet([collect(dom3); collect(dom1)])
@test vcat(dom1, dom2, dom3) == GeometrySet([collect(dom1); collect(dom2); collect(dom3)])
- dom = DummyDomain(P2(0, 0))
- @test sprint(show, dom) == "3 DummyDomain{2,$T}"
+ # CRS propagation
+ dom = DummyDomain(merc(1, 1))
+ @test crs(centroid(dom)) === crs(dom)
+
+ dom = DummyDomain(cart(0, 0))
+ @test sprint(show, dom) == "3 DummyDomain"
@test sprint(show, MIME"text/plain"(), dom) == """
- 3 DummyDomain{2,$T}
- ├─ Ball(center: (1.0, 1.0), radius: 1.0)
- ├─ Ball(center: (2.0, 2.0), radius: 1.0)
- └─ Ball(center: (3.0, 3.0), radius: 1.0)"""
+ 3 DummyDomain
+ ├─ Ball(center: (x: 1.0 m, y: 1.0 m), radius: 1.0 m)
+ ├─ Ball(center: (x: 2.0 m, y: 2.0 m), radius: 1.0 m)
+ └─ Ball(center: (x: 3.0 m, y: 3.0 m), radius: 1.0 m)"""
end
diff --git a/test/dummy.jl b/test/dummy.jl
deleted file mode 100644
index 79711a1c5..000000000
--- a/test/dummy.jl
+++ /dev/null
@@ -1,10 +0,0 @@
-# dummy type implementing the Domain trait
-struct DummyDomain{Dim,T} <: Domain{Dim,T}
- origin::Point{Dim,T}
-end
-function Meshes.element(domain::DummyDomain{Dim,T}, ind::Int) where {Dim,T}
- c = domain.origin + Vec(ntuple(i -> T(ind), Dim))
- r = one(T)
- Ball(c, r)
-end
-Meshes.nelements(d::DummyDomain) = 3
diff --git a/test/hulls.jl b/test/hulls.jl
index 2d5314378..dfa4476cc 100644
--- a/test/hulls.jl
+++ b/test/hulls.jl
@@ -1,42 +1,42 @@
-@testset "Hulls" begin
- @testset "Basic" begin
- for method in [GrahamScan(), JarvisMarch()]
- # basic test
- pts = rand(P2, 100)
- chul = hull(pts, method)
- @test all(pts .∈ Ref(chul))
-
- # duplicated points
- pts = [rand(P2, 100); rand(P2, 100)]
- chul = hull(pts, method)
- @test all(pts .∈ Ref(chul))
-
- # corner cases
- pts = P2[(0, 0)]
- chul = hull(pts, method)
- @test chul == P2(0, 0)
- pts = P2[(0, 1), (1, 0)]
- chul = hull(pts, method)
- @test chul == Segment(P2(0, 1), P2(1, 0))
- pts = P2[(1, 0), (0, 0), (0, 1)]
- chul = hull(pts, method)
- @test vertices(chul) == P2[(0, 0), (1, 0), (0, 1)]
-
- # original point set is already in hull
- pts = P2[(0, 0), (1, 0), (1, 1), (0, 1), (0.5, -1)]
- chul = hull(pts, method)
- verts = vertices(chul)
- @test verts == P2[(0, 0), (0.5, -1), (1, 0), (1, 1), (0, 1)]
-
- # random points in interior do not affect result
- p1 = P2[(0, 0), (1, 0), (1, 1), (0, 1), (0.5, -1)]
- p2 = P2[0.5 .* (rand(), rand()) .+ 0.5 for _ in 1:10]
- pts = [p1; p2]
- chul = hull(pts, method)
- verts = vertices(chul)
- @test verts == P2[(0, 0), (0.5, -1), (1, 0), (1, 1), (0, 1)]
-
- pts = P2[
+@testitem "Hulls" setup = [Setup] begin
+ for method in [GrahamScan(), JarvisMarch()]
+ # basic test
+ pts = randpoint2(100)
+ chul = hull(pts, method)
+ @test all(pts .∈ Ref(chul))
+
+ # duplicated points
+ pts = [randpoint2(100); randpoint2(100)]
+ chul = hull(pts, method)
+ @test all(pts .∈ Ref(chul))
+
+ # corner cases
+ pts = cart.([(0, 0)])
+ chul = hull(pts, method)
+ @test chul == cart(0, 0)
+ pts = cart.([(0, 1), (1, 0)])
+ chul = hull(pts, method)
+ @test chul == Segment(cart(0, 1), cart(1, 0))
+ pts = cart.([(1, 0), (0, 0), (0, 1)])
+ chul = hull(pts, method)
+ @test vertices(chul) == cart.([(0, 0), (1, 0), (0, 1)])
+
+ # original point set is already in hull
+ pts = cart.([(0, 0), (1, 0), (1, 1), (0, 1), (0.5, -1)])
+ chul = hull(pts, method)
+ verts = vertices(chul)
+ @test verts == cart.([(0, 0), (0.5, -1), (1, 0), (1, 1), (0, 1)])
+
+ # random points in interior do not affect result
+ p1 = cart.([(0, 0), (1, 0), (1, 1), (0, 1), (0.5, -1)])
+ p2 = cart.([0.5 .* (rand(), rand()) .+ 0.5 for _ in 1:10])
+ pts = [p1; p2]
+ chul = hull(pts, method)
+ verts = vertices(chul)
+ @test verts == cart.([(0, 0), (0.5, -1), (1, 0), (1, 1), (0, 1)])
+
+ pts =
+ cart.([
(0, 5),
(1, 5),
(1, 4),
@@ -63,104 +63,103 @@
(1, 7),
(0, 7),
(0, 6)
- ]
- chul = hull(pts, method)
- @test nvertices(chul) < length(pts)
-
- poly = readpoly(T, joinpath(datadir, "hull.line"))
- pts = vertices(poly)
- chul = hull(pts, method)
- @test nvertices(chul) < length(pts)
-
- if method == GrahamScan()
- # simplifying rectangular hull / triangular
- points = [P2(i - 1, j - 1) for i in 1:11 for j in 1:11]
- chull = hull(points, method)
- @test vertices(chull) == [P2(0, 0), P2(10, 0), P2(10, 10), P2(0, 10)]
- for _ in 1:100 # test presence of interior points doesn't affect the result
- push!(points, P2(10 * rand(), 10 * rand()))
- end
- chull = hull(points, method)
- @test vertices(chull) == [P2(0, 0), P2(10, 0), P2(10, 10), P2(0, 10)]
-
- points = [P2(-1, 0), P2(0, 0), P2(1, 0), P2(0, 2)]
- chull = hull(points, method)
- @test vertices(chull) == [P2(-1, 0), P2(1, 0), P2(0, 2)]
-
- # degenerate cases
- points = [P2(0, 0), P2(1, 0), P2(2, 0)]
- chull = hull(points, method)
- @test vertices(chull) == (P2(0, 0), P2(2, 0))
-
- points = [P2(0, 0), P2(1, 0), P2(2, 0), P2(10, 0), P2(100, 0)]
- chull = hull(points, method)
- @test vertices(chull) == (P2(0, 0), P2(100, 0))
-
- # partially collinear
- points = [
- P2(2, 0),
- P2(4, 0),
- P2(6, 0),
- P2(10, 0),
- P2(12, 1),
- P2(14, 3),
- P2(14, 6),
- P2(14, 9),
- P2(13, 10),
- P2(11, 11),
- P2(8, 12),
- P2(3, 11),
- P2(0, 8),
- P2(0, 7),
- P2(0, 6),
- P2(0, 5),
- P2(0, 4),
- P2(0, 3),
- P2(0, 2),
- P2(1, 0)
- ]
- chull = hull(points, method)
- truth = [
- P2(0, 2),
- P2(1, 0),
- P2(10, 0),
- P2(12, 1),
- P2(14, 3),
- P2(14, 9),
- P2(13, 10),
- P2(11, 11),
- P2(8, 12),
- P2(3, 11),
- P2(0, 8)
- ]
- @test vertices(chull) == truth
- push!(points, P2(4, 8), P2(2, 6), P2(6, 2), P2(10, 8), P2(8, 8), P2(10, 6))
- chull = hull(points, method)
- @test vertices(chull) == truth
+ ])
+ chul = hull(pts, method)
+ @test nvertices(chul) < length(pts)
+
+ poly = readpoly(T, joinpath(datadir, "hull.line"))
+ pts = vertices(poly)
+ chul = hull(pts, method)
+ @test nvertices(chul) < length(pts)
+
+ if method == GrahamScan()
+ # simplifying rectangular hull / triangular
+ points = [cart(i - 1, j - 1) for i in 1:11 for j in 1:11]
+ chull = hull(points, method)
+ @test vertices(chull) == [cart(0, 0), cart(10, 0), cart(10, 10), cart(0, 10)]
+ for _ in 1:100 # test presence of interior points doesn't affect the result
+ push!(points, cart(10 * rand(), 10 * rand()))
end
+ chull = hull(points, method)
+ @test vertices(chull) == [cart(0, 0), cart(10, 0), cart(10, 10), cart(0, 10)]
+
+ points = [cart(-1, 0), cart(0, 0), cart(1, 0), cart(0, 2)]
+ chull = hull(points, method)
+ @test vertices(chull) == [cart(-1, 0), cart(1, 0), cart(0, 2)]
+
+ # degenerate cases
+ points = [cart(0, 0), cart(1, 0), cart(2, 0)]
+ chull = hull(points, method)
+ @test vertices(chull) == SVector(cart(0, 0), cart(2, 0))
+
+ points = [cart(0, 0), cart(1, 0), cart(2, 0), cart(10, 0), cart(100, 0)]
+ chull = hull(points, method)
+ @test vertices(chull) == SVector(cart(0, 0), cart(100, 0))
+
+ # partially collinear
+ points = [
+ cart(2, 0),
+ cart(4, 0),
+ cart(6, 0),
+ cart(10, 0),
+ cart(12, 1),
+ cart(14, 3),
+ cart(14, 6),
+ cart(14, 9),
+ cart(13, 10),
+ cart(11, 11),
+ cart(8, 12),
+ cart(3, 11),
+ cart(0, 8),
+ cart(0, 7),
+ cart(0, 6),
+ cart(0, 5),
+ cart(0, 4),
+ cart(0, 3),
+ cart(0, 2),
+ cart(1, 0)
+ ]
+ chull = hull(points, method)
+ truth = [
+ cart(0, 2),
+ cart(1, 0),
+ cart(10, 0),
+ cart(12, 1),
+ cart(14, 3),
+ cart(14, 9),
+ cart(13, 10),
+ cart(11, 11),
+ cart(8, 12),
+ cart(3, 11),
+ cart(0, 8)
+ ]
+ @test vertices(chull) == truth
+ push!(points, cart(4, 8), cart(2, 6), cart(6, 2), cart(10, 8), cart(8, 8), cart(10, 6))
+ chull = hull(points, method)
+ @test vertices(chull) == truth
end
end
+end
- @testset "convexhull" begin
- @test convexhull(P2(0, 0)) == P2(0, 0)
+@testitem "Convex hulls" setup = [Setup] begin
+ @test convexhull(cart(0, 0)) == cart(0, 0)
- @test convexhull(Box(P2(0, 0), P2(1, 1))) == Box(P2(0, 0), P2(1, 1))
+ @test convexhull(Box(cart(0, 0), cart(1, 1))) == Box(cart(0, 0), cart(1, 1))
- @test convexhull(Ball(P2(0, 0), T(1))) == Ball(P2(0, 0), T(1))
- @test convexhull(Ball(P2(1, 1), T(1))) == Ball(P2(1, 1), T(1))
+ @test convexhull(Ball(cart(0, 0), T(1))) == Ball(cart(0, 0), T(1))
+ @test convexhull(Ball(cart(1, 1), T(1))) == Ball(cart(1, 1), T(1))
- @test convexhull(Sphere(P2(0, 0), T(1))) == Ball(P2(0, 0), T(1))
- @test convexhull(Sphere(P2(1, 1), T(1))) == Ball(P2(1, 1), T(1))
+ @test convexhull(Sphere(cart(0, 0), T(1))) == Ball(cart(0, 0), T(1))
+ @test convexhull(Sphere(cart(1, 1), T(1))) == Ball(cart(1, 1), T(1))
- b1 = Box(P2(0, 0), P2(1, 1))
- b2 = Box(P2(-1, -1), P2(0.5, 0.5))
- @test convexhull(Multi([b1, b2])) == PolyArea(P2[(-1, -1), (0.5, -1), (1, 0), (1, 1), (0, 1), (-1, 0.5)])
- @test convexhull(GeometrySet([b1, b2])) == PolyArea(P2[(-1, -1), (0.5, -1), (1, 0), (1, 1), (0, 1), (-1, 0.5)])
+ b1 = Box(cart(0, 0), cart(1, 1))
+ b2 = Box(cart(-1, -1), cart(0.5, 0.5))
+ @test convexhull(Multi([b1, b2])) == PolyArea(cart.([(-1, -1), (0.5, -1), (1, 0), (1, 1), (0, 1), (-1, 0.5)]))
+ @test convexhull(GeometrySet([b1, b2])) == PolyArea(cart.([(-1, -1), (0.5, -1), (1, 0), (1, 1), (0, 1), (-1, 0.5)]))
- b1 = Ball(P2(0, 0), T(1))
- b2 = Box(P2(-1, -1), P2(0, 0))
- h = convexhull(Multi([b1, b2]))
- @test P2(-0.8, -0.8) ∈ h
- @test P2(0.2, 0.2) ∈ h
- end
+ b1 = Ball(cart(0, 0), T(1))
+ b2 = Box(cart(-1, -1), cart(0, 0))
+ h = convexhull(Multi([b1, b2]))
+ @test cart(-0.8, -0.8) ∈ h
+ @test cart(0.2, 0.2) ∈ h
end
diff --git a/test/indices.jl b/test/indices.jl
new file mode 100644
index 000000000..6d5f2ccc4
--- /dev/null
+++ b/test/indices.jl
@@ -0,0 +1,205 @@
+@testitem "Indices" setup = [Setup] begin
+ g = cartgrid(10, 10)
+ b = Box(cart(1, 1), cart(5, 5))
+ v = view(g, b)
+ @test v == CartesianGrid(cart(0, 0), cart(6, 6), dims=(6, 6))
+
+ p = PointSet(vertices(g))
+ v = view(p, b)
+ @test centroid(v, 1) == cart(1, 1)
+ @test centroid(v, nelements(v)) == cart(5, 5)
+
+ # boxes
+ g = cartgrid(10, 10)
+ p = PointSet(vertices(g))
+ b = Ball(cart(0, 0), T(2))
+ v = view(g, b)
+ @test nelements(v) == 4
+ @test v[1] == g[1]
+ v = view(p, b)
+ @test nelements(v) == 6
+ @test to.(v) == vector.([(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (0, 2)])
+
+ b1 = Box(cart(0.6, 0.7), cart(1.0, 1.0))
+ b2 = Box(cart(0.6, 0.7), cart(1.2, 1.2))
+ b3 = Box(cart(0.0, 0.0), cart(0.6, 0.3))
+ b4 = Box(cart(-0.2, -0.2), cart(0.6, 0.3))
+ b5 = Box(cart(0.25, 0.15), cart(0.95, 0.85))
+ b6 = Box(cart(0.35, 0.25), cart(0.85, 0.75))
+ b7 = Box(cart(0.05, 0.05), cart(0.65, 0.35))
+ b8 = Box(cart(0.55, 0.65), cart(0.95, 0.95))
+ x = range(zero(T), stop=one(T), length=6)
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
+ g = RectilinearGrid(x, y)
+ linds = LinearIndices(size(g))
+ @test issetequal(indices(g, b1), [linds[4, 4], linds[5, 4], linds[4, 5], linds[5, 5]])
+ @test issetequal(indices(g, b2), [linds[4, 4], linds[5, 4], linds[4, 5], linds[5, 5]])
+ @test issetequal(indices(g, b3), [linds[1, 1], linds[2, 1], linds[3, 1], linds[1, 2], linds[2, 2], linds[3, 2]])
+ @test issetequal(indices(g, b4), [linds[1, 1], linds[2, 1], linds[3, 1], linds[1, 2], linds[2, 2], linds[3, 2]])
+ @test issetequal(
+ indices(g, b5),
+ [
+ linds[2, 2],
+ linds[3, 2],
+ linds[4, 2],
+ linds[5, 2],
+ linds[2, 3],
+ linds[3, 3],
+ linds[4, 3],
+ linds[5, 3],
+ linds[2, 4],
+ linds[3, 4],
+ linds[4, 4],
+ linds[5, 4]
+ ]
+ )
+ @test issetequal(
+ indices(g, b6),
+ [
+ linds[2, 2],
+ linds[3, 2],
+ linds[4, 2],
+ linds[5, 2],
+ linds[2, 3],
+ linds[3, 3],
+ linds[4, 3],
+ linds[5, 3],
+ linds[2, 4],
+ linds[3, 4],
+ linds[4, 4],
+ linds[5, 4]
+ ]
+ )
+ @test issetequal(
+ indices(g, b7),
+ [
+ linds[1, 1],
+ linds[2, 1],
+ linds[3, 1],
+ linds[4, 1],
+ linds[1, 2],
+ linds[2, 2],
+ linds[3, 2],
+ linds[4, 2],
+ linds[1, 3],
+ linds[2, 3],
+ linds[3, 3],
+ linds[4, 3]
+ ]
+ )
+ @test issetequal(
+ indices(g, b8),
+ [
+ linds[3, 3],
+ linds[4, 3],
+ linds[5, 3],
+ linds[3, 4],
+ linds[4, 4],
+ linds[5, 4],
+ linds[3, 5],
+ linds[4, 5],
+ linds[5, 5]
+ ]
+ )
+
+ # convex polygons
+ tri = Triangle(cart(5, 7), cart(10, 12), cart(15, 7))
+ pent = Pentagon(cart(6, 1), cart(2, 10), cart(10, 16), cart(18, 10), cart(14, 1))
+
+ grid = cartgrid(20, 20)
+ linds = LinearIndices(size(grid))
+ @test linds[10, 10] ∈ indices(grid, tri)
+ @test linds[10, 6] ∈ indices(grid, pent)
+
+ grid = CartesianGrid(cart(-2, -2), cart(20, 20), T.((0.5, 1.5)))
+ linds = LinearIndices(size(grid))
+ @test linds[21, 7] ∈ indices(grid, tri)
+ @test linds[21, 4] ∈ indices(grid, pent)
+
+ grid = CartesianGrid(cart(-100, -100), cart(20, 20), T.((2, 2)))
+ linds = LinearIndices(size(grid))
+ @test linds[57, 54] ∈ indices(grid, tri)
+ @test linds[55, 53] ∈ indices(grid, pent)
+
+ # non-convex polygons
+ poly1 = PolyArea(cart.([(3, 3), (9, 9), (3, 15), (17, 15), (17, 3)]))
+ poly2 = PolyArea([pointify(pent), pointify(tri)])
+
+ grid = cartgrid(20, 20)
+ linds = LinearIndices(size(grid))
+ @test linds[12, 6] ∈ indices(grid, poly1)
+ @test linds[10, 3] ∈ indices(grid, poly2)
+
+ grid = CartesianGrid(cart(-2, -2), cart(20, 20), T.((0.5, 1.5)))
+ linds = LinearIndices(size(grid))
+ @test linds[22, 6] ∈ indices(grid, poly1)
+ @test linds[17, 4] ∈ indices(grid, poly2)
+
+ grid = CartesianGrid(cart(-100, -100), cart(20, 20), T.((2, 2)))
+ linds = LinearIndices(size(grid))
+ @test linds[57, 54] ∈ indices(grid, poly1)
+ @test linds[55, 53] ∈ indices(grid, poly2)
+
+ # rotate
+ poly1 = poly1 |> Rotate(Angle2d(T(π / 2)))
+ poly2 = poly2 |> Rotate(Angle2d(T(π / 2)))
+
+ grid = CartesianGrid(cart(-20, 0), cart(0, 20), T.((1, 1)))
+ linds = LinearIndices(size(grid))
+ @test linds[12, 12] ∈ indices(grid, poly1)
+ @test linds[16, 11] ∈ indices(grid, poly2)
+
+ grid = CartesianGrid(cart(-22, -2), cart(0, 20), T.((0.5, 1.5)))
+ linds = LinearIndices(size(grid))
+ @test linds[26, 8] ∈ indices(grid, poly1)
+ @test linds[36, 9] ∈ indices(grid, poly2)
+
+ grid = CartesianGrid(cart(-100, -100), cart(20, 20), T.((2, 2)))
+ linds = LinearIndices(size(grid))
+ @test linds[46, 57] ∈ indices(grid, poly1)
+ @test linds[48, 55] ∈ indices(grid, poly2)
+
+ # multi
+ multi = Multi([tri, pent])
+ grid = cartgrid(20, 20)
+ linds = LinearIndices(size(grid))
+ @test linds[10, 10] ∈ indices(grid, multi)
+ @test linds[10, 6] ∈ indices(grid, multi)
+
+ # clipping
+ tri = Triangle(cart(-4, 10), cart(5, 19), cart(5, 1))
+ grid = cartgrid(20, 20)
+ linds = LinearIndices(size(grid))
+ @test linds[3, 10] ∈ indices(grid, tri)
+
+ # out of grid
+ tri = Triangle(cart(-12, 8), cart(-8, 14), cart(-4, 8))
+ grid = cartgrid(20, 20)
+ @test isempty(indices(grid, tri))
+
+ # chain
+ seg = Segment(cart(2, 12), cart(16, 18))
+ rope = Rope(cart(8, 1), cart(5, 9), cart(9, 13), cart(17, 10))
+ ring = Ring(cart(8, 1), cart(5, 9), cart(9, 13), cart(17, 10))
+ grid = cartgrid(20, 20)
+ linds = LinearIndices(size(grid))
+ @test linds[9, 15] ∈ indices(grid, seg)
+ @test linds[7, 11] ∈ indices(grid, rope)
+ @test linds[12, 5] ∈ indices(grid, ring)
+
+ # points
+ p1 = cart(0, 0)
+ p2 = cart(0.5, 0.5)
+ p3 = cart(1, 1)
+ p4 = cart(2, 2)
+ p5 = cart(10, 10)
+ p6 = cart(11, 11)
+ grid = cartgrid(10, 10)
+ linds = LinearIndices(size(grid))
+ @test linds[1, 1] == only(indices(grid, p1))
+ @test linds[1, 1] == only(indices(grid, p2))
+ @test linds[1, 1] == only(indices(grid, p3))
+ @test linds[2, 2] == only(indices(grid, p4))
+ @test linds[10, 10] == only(indices(grid, p5))
+ @test isempty(indices(grid, p6))
+end
diff --git a/test/intersections.jl b/test/intersections.jl
index d9c8b1060..edcc1c43f 100644
--- a/test/intersections.jl
+++ b/test/intersections.jl
@@ -1,1169 +1,1189 @@
-@testset "Intersections" begin
- # helper function for type stability tests
- function someornone(g1, g2)
- intersection(g1, g2) do I
- if type(I) == NotIntersecting
- "None"
- else
- "Some"
- end
- end
- end
+@testitem "Point intersection" setup = [Setup] begin
+ p = cart(0, 0)
+ q = cart(-1, -1)
+ b = Box(cart(0, 0), cart(1, 1))
+ @test p ∩ p == p
+ @test q ∩ q == q
+ @test p ∩ b == b ∩ p == p
+ @test isnothing(p ∩ q)
+ @test isnothing(q ∩ b)
+end
- @testset "Points" begin
- p = P2(0, 0)
- q = P2(-1, -1)
- b = Box(P2(0, 0), P2(1, 1))
- @test p ∩ p == p
- @test q ∩ q == q
- @test p ∩ b == b ∩ p == p
- @test isnothing(p ∩ q)
- @test isnothing(q ∩ b)
- end
+@testitem "Segment intersection" setup = [Setup] begin
+ # segments in 2D
+ s1 = Segment(cart(0, 0), cart(1, 0))
+ s2 = Segment(cart(0.5, 0.0), cart(2, 0))
+ @test s1 ∩ s2 ≈ Segment(cart(0.5, 0.0), cart(1, 0))
+ @test s2 ∩ s1 ≈ Segment(cart(0.5, 0.0), cart(1, 0))
+
+ s1 = Segment(cart(0, 0), cart(1, -1))
+ s2 = Segment(cart(0.5, -0.5), cart(1.5, -1.5))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(cart(0.5, -0.5), cart(1, -1))
+
+ s1 = Segment(cart(0, 0), cart(1, 0))
+ s2 = Segment(cart(0, 0), cart(0, 1))
+ @test s1 ∩ s2 ≈ cart(0, 0)
+ @test s2 ∩ s1 ≈ cart(0, 0)
+
+ s1 = Segment(cart(0, 0), cart(1, 0))
+ s2 = Segment(cart(0, 0), cart(-1, 0))
+ @test s1 ∩ s2 ≈ cart(0, 0)
+ @test s2 ∩ s1 ≈ cart(0, 0)
+
+ s1 = Segment(cart(0, 0), cart(0, 1))
+ s2 = Segment(cart(0, 0), cart(0, -1))
+ @test s1 ∩ s2 ≈ cart(0, 0)
+ @test s2 ∩ s1 ≈ cart(0, 0)
+
+ s1 = Segment(cart(1, 1), cart(1, 2))
+ s2 = Segment(cart(1, 1), cart(1, 0))
+ @test s1 ∩ s2 ≈ cart(1, 1)
+ @test s2 ∩ s1 ≈ cart(1, 1)
+
+ s1 = Segment(cart(1, 1), cart(2, 1))
+ s2 = Segment(cart(1, 0), cart(3, 0))
+ @test s1 ∩ s2 === nothing
+ @test s2 ∩ s1 === nothing
+
+ s1 = Segment(cart(0.181429364026879, 0.546811355144474), cart(0.38282226144778, 0.107781953228536))
+ s2 = Segment(cart(0.412498700935005, 0.212081819871479), cart(0.395936725690311, 0.252041094122474))
+ @test s1 ∩ s2 === nothing
+ @test s2 ∩ s1 === nothing
+
+ s1 = Segment(cart(1, 2), cart(1, 0))
+ s2 = Segment(cart(1, 0), cart(1, 1))
+ @test s1 ∩ s2 ≈ Segment(cart(1, 1), cart(1, 0))
+ @test s2 ∩ s1 ≈ Segment(cart(1, 0), cart(1, 1))
+
+ s1 = Segment(cart(0, 0), cart(2, 0))
+ s2 = Segment(cart(-2, 0), cart(-1, 0))
+ s3 = Segment(cart(-1, 0), cart(-2, 0))
+ @test s1 ∩ s2 === s2 ∩ s1 === nothing
+ @test s1 ∩ s3 === s3 ∩ s1 === nothing
+
+ s1 = Segment(cart(-1, 0), cart(0, 0))
+ s2 = Segment(cart(0, 0), cart(2, 0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ cart(0, 0)
+
+ s1 = Segment(cart(-1, 0), cart(1, 0))
+ s2 = Segment(cart(0, 0), cart(3, 0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(cart(0, 0), cart(1, 0))
+
+ s1 = Segment(cart(0, 0), cart(1, 0))
+ s2 = Segment(cart(0, 0), cart(2, 0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(cart(0, 0), cart(1, 0))
+
+ s1 = Segment(cart(0, 0), cart(3, 0))
+ s2 = Segment(cart(1, 0), cart(2, 0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ s2
+
+ s1 = Segment(cart(0, 0), cart(2, 0))
+ s2 = Segment(cart(1, 0), cart(2, 0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ s2
+
+ s1 = Segment(cart(0, 0), cart(2, 0))
+ s2 = Segment(cart(1, 0), cart(3, 0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(cart(1, 0), cart(2, 0))
+
+ s1 = Segment(cart(0, 0), cart(2, 0))
+ s2 = Segment(cart(2, 0), cart(3, 0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ cart(2, 0)
+
+ s1 = Segment(cart(0, 0), cart(2, 0))
+ s2 = Segment(cart(3, 0), cart(4, 0))
+ @test s1 ∩ s2 === s2 ∩ s1 === nothing
+
+ s1 = Segment(cart(2, 1), cart(1, 2))
+ s2 = Segment(cart(1, 0), cart(1, 1))
+ @test s1 ∩ s2 === s2 ∩ s1 === nothing
+
+ s1 = Segment(cart(1.5, 1.5), cart(3.0, 1.5))
+ s2 = Segment(cart(3.0, 1.0), cart(2.0, 2.0))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ cart(2.5, 1.5)
+
+ s1 = Segment(cart(0.94495744, 0.53224397), cart(0.94798386, 0.5344541))
+ s2 = Segment(cart(0.94798386, 0.5344541), cart(0.9472896, 0.5340202))
+ @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ cart(0.94798386, 0.5344541)
+
+ s₁ = Segment(cart(0, 0), cart(3, 4))
+ s₂ = Segment(cart(1, 2), cart(3, -2))
+ s₃ = Segment(cart(2, 0), cart(-2, 0))
+ s₄ = Segment(cart(0, 0), cart(1, 2))
+ s₅ = Segment(cart(1, 2), cart(3, 4))
+ s₆ = Segment(cart(-1, -4 / 3), cart(0, 0))
+ s₇ = Segment(cart(1, 2), cart(0, 4))
+ s₈ = Segment(cart(4, 16 / 3), cart(3, 4))
+
+ s₉ = Segment(cart(-1, 5), cart(1, 4))
+ s₁₀ = Segment(cart(1, 4), cart(-1, 5))
+ s₁₁ = Segment(cart(-2, 5.5), cart(-0.8, 4.9))
+ s₁₂ = Segment(cart(-0.8, 4.9), cart(-2, 5.5))
+ s₁₃ = Segment(cart(-0.5, 4.75), cart(0.2, 4.4))
+ s₁₄ = Segment(cart(0.2, 4.4), cart(-0.5, 4.75))
+ s₁₅ = Segment(cart(0.5, 4.25), cart(1, 4))
+ s₁₆ = Segment(cart(1, 4), cart(0.5, 4.25))
+ s₁₇ = Segment(cart(2, 3.5), cart(1.5, 3.75))
+ s₁₈ = Segment(cart(1.5, 3.75), cart(2, 3.5))
+
+ @test s₁ ∩ s₂ ≈ s₂ ∩ s₁ ≈ cart(1.2, 1.6) # CASE 1: Crossing Segments
+ @test intersection(s₁, s₂) |> type == Crossing
+ @test intersection(s₂, s₁) |> type == Crossing
+
+ @test s₁ ∩ s₃ ≈ s₃ ∩ s₁ ≈ cart(0, 0) # CASE 2: EdgeTouching (s₁(0))
+ @test intersection(s₁, s₃) |> type == EdgeTouching
+ @test intersection(s₃, s₁) |> type == EdgeTouching
+
+ @test s₂ ∩ s₃ ≈ s₃ ∩ s₂ ≈ cart(2, 0) # CASE 2: EdgeTouching (s₃(1))
+ @test intersection(s₂, s₃) |> type == EdgeTouching
+ @test intersection(s₃, s₂) |> type == EdgeTouching
+
+ @test s₁ ∩ s₄ ≈ s₄ ∩ s₁ ≈ cart(0, 0) # CASE 3: CornerTouching (s₁(0), s₄(0))
+ @test intersection(s₁, s₄) |> type == CornerTouching
+ @test intersection(s₄, s₁) |> type == CornerTouching
+
+ @test s₂ ∩ s₄ ≈ s₄ ∩ s₂ ≈ cart(1, 2) # CASE 3: CornerTouching (s₂(0), s₄(1))
+ @test intersection(s₂, s₄) |> type == CornerTouching
+ @test intersection(s₄, s₂) |> type == CornerTouching
+
+ @test s₁ ∩ s₅ ≈ s₅ ∩ s₁ ≈ cart(3, 4) # CASE 3: CornerTouching (s₁(1), s₅(1))
+ @test intersection(s₂, s₄) |> type == CornerTouching
+ @test intersection(s₄, s₂) |> type == CornerTouching
+
+ @test s₁ ∩ s₆ ≈ s₆ ∩ s₁ ≈ cart(0, 0) # CASE 3: CornerTouching (s₁(0), s₆(1)), collinear
+ @test intersection(s₁, s₆) |> type == CornerTouching
+ @test intersection(s₆, s₁) |> type == CornerTouching
+
+ @test s₂ ∩ s₇ ≈ s₇ ∩ s₂ ≈ cart(1, 2) # CASE 3: CornerTouching (s₂(0), s₇(0)), collinear
+ @test intersection(s₂, s₇) |> type == CornerTouching
+ @test intersection(s₇, s₂) |> type == CornerTouching
+
+ @test s₁ ∩ s₈ ≈ s₈ ∩ s₁ ≈ cart(3, 4) # CASE 3: CornerTouching (s₁(1), s₈(1)), collinear
+ @test intersection(s₁, s₈) |> type == CornerTouching
+ @test intersection(s₈, s₁) |> type == CornerTouching
+
+ @test s₉ ∩ s₉ ≈ s₉ # CASE 4: Overlapping (same segment)
+ @test intersection(s₉, s₉) |> type == Overlapping
+
+ @test s₉ ∩ s₁₀ ≈ s₉ # CASE 4: Overlapping (same segment, flipped points)
+ @test s₁₀ ∩ s₉ ≈ s₁₀
+ @test intersection(s₉, s₁₀) |> type == Overlapping
+ @test intersection(s₁₀, s₉) |> type == Overlapping
+
+ @test s₉ ∩ s₁₁ ≈ s₁₁ ∩ s₉ ≈ Segment(cart(-1, 5), cart(-0.8, 4.9)) # CASE 4: Overlapping (same alignment)
+ @test intersection(s₉, s₁₁) |> type == Overlapping
+ @test intersection(s₁₁, s₉) |> type == Overlapping
+
+ @test s₉ ∩ s₁₂ ≈ Segment(cart(-1, 5), cart(-0.8, 4.9)) # CASE 4: Overlapping (opposite alignment, λ = 0 involved)
+ @test s₁₂ ∩ s₉ ≈ Segment(cart(-0.8, 4.9), cart(-1, 5)) # flipped Points in Segment
+ @test intersection(s₉, s₁₂) |> type == Overlapping
+ @test intersection(s₁₂, s₉) |> type == Overlapping
+
+ @test s₁₀ ∩ s₁₁ ≈ Segment(cart(-0.8, 4.9), cart(-1, 5)) # CASE 4: Overlapping (opposite alignment, λ = 1 involved)
+ @test s₁₁ ∩ s₁₀ ≈ Segment(cart(-1, 5), cart(-0.8, 4.9)) # flipped Points in Segment
+ @test intersection(s₁₀, s₁₁) |> type == Overlapping
+ @test intersection(s₁₁, s₁₀) |> type == Overlapping
+
+ @test s₉ ∩ s₁₃ ≈ s₁₃ ∩ s₉ ≈ s₁₃ # CASE 4: Overlapping (same alignment)
+ @test intersection(s₉, s₁₃) |> type == Overlapping
+ @test intersection(s₁₃, s₉) |> type == Overlapping
+
+ @test s₁₄ ∩ s₉ ≈ s₁₄ # CASE 4: Overlapping (opposite alignment)
+ @test s₉ ∩ s₁₄ ≈ s₁₃ # flipped Points in Segment
+ @test intersection(s₉, s₁₄) |> type == Overlapping
+ @test intersection(s₁₄, s₉) |> type == Overlapping
+
+ @test s₉ ∩ s₁₅ ≈ s₁₅ ∩ s₉ ≈ s₁₅ # CASE 4: Overlapping (same alignment, corner case)
+ @test intersection(s₉, s₁₅) |> type == Overlapping
+ @test intersection(s₁₅, s₉) |> type == Overlapping
+
+ @test s₁₅ ∩ s₁₀ ≈ s₁₅ # CASE 4: Overlapping (same alignment, corner case)
+ @test s₁₀ ∩ s₁₅ ≈ s₁₆ # flipped Points in Segment
+ @test intersection(s₁₀, s₁₅) |> type == Overlapping
+ @test intersection(s₁₅, s₁₀) |> type == Overlapping
+
+ @test s₁₆ ∩ s₉ ≈ s₁₆ # CASE 4: Overlapping (opposite alignment, corner case)
+ @test s₉ ∩ s₁₆ ≈ s₁₅ # flipped Points in Segment
+ @test intersection(s₉, s₁₆) |> type == Overlapping
+ @test intersection(s₁₆, s₉) |> type == Overlapping
+
+ @test s₁₀ ∩ s₁₆ ≈ s₁₆ ∩ s₁₀ ≈ s₁₆ # CASE 4: Overlapping (same alignment, corner case)
+ @test intersection(s₁₀, s₁₆) |> type == Overlapping
+ @test intersection(s₁₆, s₁₀) |> type == Overlapping
+
+ @test s₉ ∩ s₁₇ === s₁₇ ∩ s₉ === nothing # CASE 5: NotIntersecting (collinear, same alignment)
+ @test intersection(s₉, s₁₇) |> type == NotIntersecting
+ @test intersection(s₁₇, s₉) |> type == NotIntersecting
+
+ @test s₁₀ ∩ s₁₇ === s₁₇ ∩ s₁₀ === nothing # CASE 5: NotIntersecting (collinear, opposite alignment)
+ @test intersection(s₁₀, s₁₇) |> type == NotIntersecting
+ @test intersection(s₁₇, s₁₀) |> type == NotIntersecting
+
+ @test s₉ ∩ s₁₈ === s₁₈ ∩ s₉ === nothing # CASE 5: NotIntersecting (collinear, opposite alignment)
+ @test intersection(s₉, s₁₈) |> type == NotIntersecting
+ @test intersection(s₁₈, s₉) |> type == NotIntersecting
+
+ @test s₁ ∩ s₉ === s₉ ∩ s₁ === nothing # CASE 5: NotIntersecting, one λ in range
+ @test intersection(s₉, s₁) |> type == NotIntersecting
+ @test intersection(s₁, s₉) |> type == NotIntersecting
+
+ @test s₁ ∩ s₁₀ === s₁₀ ∩ s₁ === nothing # CASE 5: NotIntersecting, one λ in range
+ @test intersection(s₁₀, s₁) |> type == NotIntersecting
+ @test intersection(s₁, s₁₀) |> type == NotIntersecting
+
+ @test s₃ ∩ s₉ === s₉ ∩ s₃ === nothing # CASE 5: NotIntersecting
+ @test intersection(s₉, s₁) |> type == NotIntersecting
+ @test intersection(s₁, s₉) |> type == NotIntersecting
+
+ @test s₃ ∩ s₁₀ === s₁₀ ∩ s₃ === nothing # CASE 5: NotIntersecting
+ @test intersection(s₁₀, s₃) |> type == NotIntersecting
+ @test intersection(s₃, s₁₀) |> type == NotIntersecting
+
+ # segments in 3D
+ s1 = Segment(cart(0.0, 0.0, 0.0), cart(1.0, 0.0, 0.0))
+ s2 = Segment(cart(0.5, 1.0, 0.0), cart(0.5, -1.0, 0.0))
+ s3 = Segment(cart(0.5, 0.0, 0.0), cart(1.5, 0.0, 0.0))
+ s4 = Segment(cart(0.0, 1.0, 0.0), cart(0.0, -2.0, 0.0))
+ s5 = Segment(cart(-1.0, 1.0, 0.0), cart(2.0, -2.0, 0.0))
+ s6 = Segment(cart(0.0, 0.0, 0.0), cart(0.0, 1.0, 0.0))
+ s7 = Segment(cart(-1.0, 1.0, 0.0), cart(-1.0, -1.0, 0.0))
+ s8 = Segment(cart(-1.0, 1.0, 1.0), cart(-1.0, -1.0, 1.0))
+ s9 = Segment(cart(0.5, 1.0, 1.0), cart(0.5, -1.0, 1.0))
+ s10 = Segment(cart(0.0, 1.0, 0.0), cart(1.0, 1.0, 0.0))
+ s11 = Segment(cart(1.5, 0.0, 0.0), cart(2.5, 0.0, 0.0))
+ s12 = Segment(cart(1.0, 0.0, 0.0), cart(2.0, 0.0, 0.0))
+
+ @test intersection(s1, s2) |> type == Crossing
+ @test s1 ∩ s2 ≈ cart(0.5, 0.0, 0.0)
+ @test intersection(s1, s3) |> type == Overlapping
+ @test s1 ∩ s3 ≈ Segment(cart(0.5, 0.0, 0.0), cart(1.0, 0.0, 0.0))
+ @test intersection(s1, s4) |> type == EdgeTouching
+ @test s1 ∩ s4 ≈ cart(0.0, 0.0, 0.0)
+ @test intersection(s1, s5) |> type == EdgeTouching
+ @test s1 ∩ s5 ≈ cart(0.0, 0.0, 0.0)
+ @test intersection(s1, s6) |> type == CornerTouching
+ @test s1 ∩ s6 ≈ cart(0.0, 0.0, 0.0)
+ @test intersection(s1, s7) |> type == NotIntersecting
+ @test isnothing(s1 ∩ s7)
+ @test intersection(s1, s8) |> type == NotIntersecting
+ @test isnothing(s1 ∩ s8)
+ @test intersection(s1, s9) |> type == NotIntersecting
+ @test isnothing(s1 ∩ s9)
+ @test intersection(s1, s10) |> type == NotIntersecting
+ @test isnothing(s1 ∩ s10)
+ @test intersection(s1, s11) |> type == NotIntersecting
+ @test isnothing(s1 ∩ s11)
+ @test intersection(s1, s12) |> type == CornerTouching
+ @test s1 ∩ s12 ≈ cart(1.0, 0.0, 0.0)
+
+ # precision test
+ s1 = Segment(cart(2.0, 2.0), cart(3.0, 1.0))
+ s2 = Segment(cart(2.12505, 1.87503), cart(50000.0, 30000.0))
+ s3 = Segment(cart(2.125005, 1.875003), cart(50000.0, 30000.0))
+ s4 = Segment(cart(2.125005, 1.875003), cart(50002.125005, 30001.875003))
+ @test s1 ∩ s2 === s2 ∩ s1 === nothing
+ @test s1 ∩ s3 === s3 ∩ s1 === ((T == Float32) ? cart(2.125005, 1.875003) : nothing)
+ @test s1 ∩ s4 === s4 ∩ s1 === ((T == Float32) ? cart(2.125005, 1.875003) : nothing)
+
+ # type stability tests
+ s1 = Segment(cart(0, 0), cart(1, 0))
+ s2 = Segment(cart(0.5, 0.0), cart(2, 0))
+ @inferred someornone(s1, s2)
+
+ s1 = Segment(cart(0.0, 0.0, 0.0), cart(1.0, 0.0, 0.0))
+ s2 = Segment(cart(0.5, 1.0, 0.0), cart(0.5, -1.0, 0.0))
+ @inferred someornone(s1, s2)
+
+ # rays and segments in 2D
+ r₁ = Ray(cart(1, 0), vector(2, 1))
+ s₁ = Segment(cart(0, 2), cart(2, -1)) # Crossing
+ s₂ = Segment(cart(0, 2), cart(1, 0.5)) # NotIntersecting
+ s₃ = Segment(cart(0, 2), cart(0.5, -0.5)) # NotIntersecting
+ s₄ = Segment(cart(0.5, 1), cart(1.5, -1)) # EdgeTouching
+ s₅ = Segment(cart(1.5, 0.25), cart(1.5, 2)) # EdgeTouching
+ s₆ = Segment(cart(1, 0), cart(1, -1)) # CornerTouching
+ s₇ = Segment(cart(0.5, -1), cart(1, 0)) # CornerTouching
+
+ @test intersection(r₁, s₁) |> type == Crossing #CASE 1
+ @test r₁ ∩ s₁ ≈ s₁ ∩ r₁ ≈ cart(1.25, 0.125)
+ @test intersection(r₁, s₂) |> type == NotIntersecting # CASE 5
+ @test r₁ ∩ s₂ === s₂ ∩ r₁ === nothing
+ @test intersection(r₁, s₃) |> type == NotIntersecting # CASE 5
+ @test r₁ ∩ s₃ === s₃ ∩ r₁ === nothing
+ @test intersection(r₁, s₄) |> type == EdgeTouching # CASE 2
+ @test r₁ ∩ s₄ ≈ s₄ ∩ r₁ ≈ r₁(0)
+ @test intersection(r₁, s₅) |> type == EdgeTouching # CASE 2
+ @test r₁ ∩ s₅ ≈ s₅ ∩ r₁ ≈ cart(1.5, 0.25)
+ @test intersection(r₁, s₆) |> type == CornerTouching # CASE 3
+ @test r₁ ∩ s₆ ≈ s₆ ∩ r₁ ≈ r₁(0)
+ @test intersection(r₁, s₇) |> type == CornerTouching # CASE 3
+ @test r₁ ∩ s₇ ≈ s₇ ∩ r₁ ≈ r₁(0)
+
+ r₂ = Ray(cart(3, 2), vector(1, 1))
+ s₈ = Segment(cart(4, 3), cart(5, 4)) # Overlapping
+ s₉ = Segment(cart(2.5, 1.5), cart(3.3, 2.3)) # Overlapping s(1)
+ s₁₀ = Segment(cart(3.6, 2.6), cart(2.6, 1.6)) # Overlapping s(0)
+ s₁₁ = Segment(cart(2.2, 1.2), cart(3, 2)) # CornerTouching, colinear, s(1)
+ s₁₂ = Segment(cart(3, 2), cart(2.4, 1.4)) # CornerTouching, colinear, s(0)
+ s₁₃ = Segment(cart(3, 2), cart(3.1, 2.1)) # Overlapping s(0) = r(0)
+ s₁₄ = Segment(cart(3.2, 2.2), cart(3, 2)) # Overlapping s(1) = r(0)
+ s₁₅ = Segment(cart(2, 1), cart(1.6, 0.6)) # No Intersection, colinear
+ s₁₆ = Segment(cart(3, 1), cart(4, 2)) # No Intersection, parallel
+ @test intersection(r₂, s₈) |> type == Overlapping # CASE 4
+ @test r₂ ∩ s₈ === s₈ ∩ r₂ === s₈
+ @test intersection(r₂, s₉) |> type == Overlapping # CASE 4
+ @test r₂ ∩ s₉ == s₉ ∩ r₂ == Segment(r₂(0), s₉(1))
+ @test intersection(r₂, s₁₀) |> type == Overlapping # CASE 4
+ @test r₂ ∩ s₁₀ == s₁₀ ∩ r₂ == Segment(r₂(0), s₁₀(0))
+ @test intersection(r₂, s₁₁) |> type == CornerTouching # CASE 3
+ @test r₂ ∩ s₁₁ ≈ s₁₁ ∩ r₂ ≈ r₂(0)
+ @test intersection(r₂, s₁₂) |> type == CornerTouching # CASE 3
+ @test r₂ ∩ s₁₂ ≈ s₁₂ ∩ r₂ ≈ r₂(0)
+ @test intersection(r₂, s₁₃) |> type == Overlapping # CASE 4
+ @test r₂ ∩ s₁₃ === s₁₃ ∩ r₂ === s₁₃
+ @test intersection(r₂, s₁₄) |> type == Overlapping # CASE 4
+ @test r₂ ∩ s₁₄ === s₁₄ ∩ r₂ === s₁₄
+ @test intersection(r₂, s₁₅) |> type == NotIntersecting # CASE 5
+ @test r₂ ∩ s₁₅ === s₁₅ ∩ r₂ === nothing
+ @test intersection(r₂, s₁₆) |> type == NotIntersecting # CASE 5
+ @test r₂ ∩ s₁₆ === s₁₆ ∩ r₂ === nothing
+
+ # type stability tests
+ r₁ = Ray(cart(0, 0), vector(1, 0))
+ s₁ = Segment(cart(-1, -1), cart(-1, 1))
+ @inferred someornone(r₁, s₁)
+
+ # 3D test
+ r₁ = Ray(cart(1, 2, 3), vector(1, 2, 3))
+ s₁ = Segment(cart(1, 3, 5), cart(3, 5, 7))
+ @test intersection(r₁, s₁) |> type === Crossing # CASE 1
+ @test r₁ ∩ s₁ ≈ s₁ ∩ r₁ ≈ cart(2, 4, 6)
+
+ s₂ = Segment(cart(0, 1, 2), cart(2, 3, 4))
+ @test intersection(r₁, s₂) |> type === EdgeTouching # CASE 2
+ @test r₁ ∩ s₂ == s₂ ∩ r₁ == r₁(0)
+
+ s₃ = Segment(cart(0.23, 1, 2.3), cart(1, 2, 3))
+ @test intersection(r₁, s₃) |> type === CornerTouching # CASE 3
+ @test r₁ ∩ s₃ == s₃ ∩ r₁ == r₁(0)
+
+ s₄ = Segment(cart(0, 0, 0), cart(2, 4, 6))
+ @test intersection(r₁, s₄) |> type === Overlapping # CASE 4
+ @test r₁ ∩ s₄ == s₄ ∩ r₁ == Segment(cart(1, 2, 3), cart(2, 4, 6))
+
+ s₅ = Segment(cart(0, 0, 0), cart(0.5, 1, 1.5))
+ @test intersection(r₁, s₅) |> type === NotIntersecting # CASE 5
+ @test r₁ ∩ s₅ === s₅ ∩ r₁ === nothing
+
+ l₁ = Line(cart(1, 0), cart(3, 1))
+ s₁ = Segment(cart(0, 2), cart(2, -1)) # Crossing
+ s₂ = Segment(cart(0.5, 1), cart(0, 0)) # NotIntersecting
+ s₃ = Segment(cart(0, 2), cart(-2, 1)) # NotIntersecting
+ s₄ = Segment(cart(0.5, -1), cart(1, 0)) # Touching
+ s₅ = Segment(cart(1.5, 0.25), cart(1.5, 2)) # Touching
+ s₆ = Segment(cart(-3, -2), cart(4, 1.5)) # Overlapping
+
+ @test intersection(l₁, s₁) |> type == Crossing #CASE 1
+ @test l₁ ∩ s₁ ≈ s₁ ∩ l₁ ≈ cart(1.25, 0.125)
+ @test intersection(l₁, s₂) |> type == NotIntersecting # CASE 4
+ @test l₁ ∩ s₂ === s₂ ∩ l₁ === nothing
+ @test intersection(l₁, s₃) |> type == NotIntersecting # CASE 4
+ @test l₁ ∩ s₃ === s₃ ∩ l₁ === nothing
+ @test intersection(l₁, s₄) |> type == Touching # CASE 2
+ @test l₁ ∩ s₄ ≈ s₄ ∩ l₁ ≈ s₄(1)
+ @test intersection(l₁, s₅) |> type == Touching # CASE 2
+ @test l₁ ∩ s₅ ≈ s₅ ∩ l₁ ≈ s₅(0)
+ @test intersection(l₁, s₆) |> type == Overlapping # CASE 3
+ @test l₁ ∩ s₆ ≈ s₆ ∩ l₁ ≈ s₆
+
+ # type stability tests
+ @inferred someornone(l₁, s₁)
+ @inferred someornone(l₁, s₂)
+
+ # 3d tests
+ l₁ = Line(cart(1, 0, 1), cart(3, 1, 1))
+ s₁ = Segment(cart(0, 2, 1), cart(2, -1, 1)) # Crossing
+ s₂ = Segment(cart(0.5, 1, 1), cart(0, 0, 1)) # NotIntersecting
+ s₃ = Segment(cart(0, 2, 1), cart(-2, 1, 1)) # NotIntersecting
+ s₄ = Segment(cart(0.5, -1, 1), cart(1, 0, 1)) # Touching
+ s₅ = Segment(cart(1.5, 0.25, 1), cart(1.5, 2, 1)) # Touching
+ s₆ = Segment(cart(-3, -2, 1), cart(4, 1.5, 1)) # Overlapping
+ s₇ = Segment(cart(0, 2, 1), cart(2, -1, 1.1)) # NotIntersecting
+
+ @test intersection(l₁, s₁) |> type == Crossing #CASE 1
+ @test l₁ ∩ s₁ ≈ s₁ ∩ l₁ ≈ cart(1.25, 0.125, 1)
+ @test intersection(l₁, s₂) |> type == NotIntersecting # CASE 4
+ @test l₁ ∩ s₂ === s₂ ∩ l₁ === nothing
+ @test intersection(l₁, s₃) |> type == NotIntersecting # CASE 4
+ @test l₁ ∩ s₃ === s₃ ∩ l₁ === nothing
+ @test intersection(l₁, s₄) |> type == Touching # CASE 2
+ @test l₁ ∩ s₄ ≈ s₄ ∩ l₁ ≈ s₄(1)
+ @test intersection(l₁, s₅) |> type == Touching # CASE 2
+ @test l₁ ∩ s₅ ≈ s₅ ∩ l₁ ≈ s₅(0)
+ @test intersection(l₁, s₆) |> type == Overlapping # CASE 3
+ @test l₁ ∩ s₆ ≈ s₆ ∩ l₁ ≈ s₆
+ @test intersection(l₁, s₇) |> type == NotIntersecting # CASE 4
+ @test l₁ ∩ s₇ === s₇ ∩ l₁ === nothing
+
+ # degenerate segments
+ A = cart(0.0, 0.0)
+ B = cart(0.5, 0.0)
+ C = cart(1.0, 0.0)
+ s₀ = Segment(A, C)
+ s₁ = Segment(A, A)
+ s₂ = Segment(B, B)
+ s₃ = Segment(C, C)
+ @test s₀ ∩ s₁ ≈ s₁ ∩ s₀ ≈ A
+ @test s₀ ∩ s₂ ≈ s₂ ∩ s₀ ≈ B
+ @test s₀ ∩ s₃ ≈ s₃ ∩ s₀ ≈ C
+ @test intersection(s₀, s₁) |> type == CornerTouching
+ @test intersection(s₀, s₂) |> type == EdgeTouching
+ @test intersection(s₀, s₃) |> type == CornerTouching
+ @test s₁ ∩ s₂ === s₂ ∩ s₁ === nothing
+ @test s₁ ∩ s₃ === s₃ ∩ s₁ === nothing
+ @test s₂ ∩ s₃ === s₃ ∩ s₂ === nothing
+ @test intersection(s₁, s₂) |> type == NotIntersecting
+ @test intersection(s₁, s₃) |> type == NotIntersecting
+ @test intersection(s₂, s₃) |> type == NotIntersecting
+ @test s₁ ∩ s₁ ≈ A
+ @test s₂ ∩ s₂ ≈ B
+ @test s₃ ∩ s₃ ≈ C
+ @test intersection(s₁, s₁) |> type == CornerTouching
+ @test intersection(s₂, s₂) |> type == CornerTouching
+ @test intersection(s₃, s₃) |> type == CornerTouching
+
+ # utils
+ @test Meshes._sort4vals(2.5, 1.4, 1.1, 2.0) == (1.4, 2.0)
+ @test Meshes._sort4vals(2.0, 1.1, 1.4, 2.5) == (1.4, 2.0)
+ @test Meshes._sort4vals(2.0, 2.5, 1.1, 1.4) == (1.4, 2.0)
+end
- @testset "Segments" begin
- # segments in 2D
- s1 = Segment(P2(0, 0), P2(1, 0))
- s2 = Segment(P2(0.5, 0.0), P2(2, 0))
- @test s1 ∩ s2 ≈ Segment(P2(0.5, 0.0), P2(1, 0))
- @test s2 ∩ s1 ≈ Segment(P2(0.5, 0.0), P2(1, 0))
-
- s1 = Segment(P2(0, 0), P2(1, -1))
- s2 = Segment(P2(0.5, -0.5), P2(1.5, -1.5))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(P2(0.5, -0.5), P2(1, -1))
-
- s1 = Segment(P2(0, 0), P2(1, 0))
- s2 = Segment(P2(0, 0), P2(0, 1))
- @test s1 ∩ s2 ≈ P2(0, 0)
- @test s2 ∩ s1 ≈ P2(0, 0)
-
- s1 = Segment(P2(0, 0), P2(1, 0))
- s2 = Segment(P2(0, 0), P2(-1, 0))
- @test s1 ∩ s2 ≈ P2(0, 0)
- @test s2 ∩ s1 ≈ P2(0, 0)
-
- s1 = Segment(P2(0, 0), P2(0, 1))
- s2 = Segment(P2(0, 0), P2(0, -1))
- @test s1 ∩ s2 ≈ P2(0, 0)
- @test s2 ∩ s1 ≈ P2(0, 0)
-
- s1 = Segment(P2(1, 1), P2(1, 2))
- s2 = Segment(P2(1, 1), P2(1, 0))
- @test s1 ∩ s2 ≈ P2(1, 1)
- @test s2 ∩ s1 ≈ P2(1, 1)
-
- s1 = Segment(P2(1, 1), P2(2, 1))
- s2 = Segment(P2(1, 0), P2(3, 0))
- @test s1 ∩ s2 === nothing
- @test s2 ∩ s1 === nothing
-
- s1 = Segment(P2(0.181429364026879, 0.546811355144474), P2(0.38282226144778, 0.107781953228536))
- s2 = Segment(P2(0.412498700935005, 0.212081819871479), P2(0.395936725690311, 0.252041094122474))
- @test s1 ∩ s2 === nothing
- @test s2 ∩ s1 === nothing
-
- s1 = Segment(P2(1, 2), P2(1, 0))
- s2 = Segment(P2(1, 0), P2(1, 1))
- @test s1 ∩ s2 ≈ Segment(P2(1, 1), P2(1, 0))
- @test s2 ∩ s1 ≈ Segment(P2(1, 0), P2(1, 1))
-
- s1 = Segment(P2(0, 0), P2(2, 0))
- s2 = Segment(P2(-2, 0), P2(-1, 0))
- s3 = Segment(P2(-1, 0), P2(-2, 0))
- @test s1 ∩ s2 === s2 ∩ s1 === nothing
- @test s1 ∩ s3 === s3 ∩ s1 === nothing
-
- s1 = Segment(P2(-1, 0), P2(0, 0))
- s2 = Segment(P2(0, 0), P2(2, 0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ P2(0, 0)
-
- s1 = Segment(P2(-1, 0), P2(1, 0))
- s2 = Segment(P2(0, 0), P2(3, 0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(P2(0, 0), P2(1, 0))
-
- s1 = Segment(P2(0, 0), P2(1, 0))
- s2 = Segment(P2(0, 0), P2(2, 0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(P2(0, 0), P2(1, 0))
-
- s1 = Segment(P2(0, 0), P2(3, 0))
- s2 = Segment(P2(1, 0), P2(2, 0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ s2
-
- s1 = Segment(P2(0, 0), P2(2, 0))
- s2 = Segment(P2(1, 0), P2(2, 0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ s2
-
- s1 = Segment(P2(0, 0), P2(2, 0))
- s2 = Segment(P2(1, 0), P2(3, 0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ Segment(P2(1, 0), P2(2, 0))
-
- s1 = Segment(P2(0, 0), P2(2, 0))
- s2 = Segment(P2(2, 0), P2(3, 0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ P2(2, 0)
-
- s1 = Segment(P2(0, 0), P2(2, 0))
- s2 = Segment(P2(3, 0), P2(4, 0))
- @test s1 ∩ s2 === s2 ∩ s1 === nothing
-
- s1 = Segment(P2(2, 1), P2(1, 2))
- s2 = Segment(P2(1, 0), P2(1, 1))
- @test s1 ∩ s2 === s2 ∩ s1 === nothing
-
- s1 = Segment(P2(1.5, 1.5), P2(3.0, 1.5))
- s2 = Segment(P2(3.0, 1.0), P2(2.0, 2.0))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ P2(2.5, 1.5)
-
- s1 = Segment(P2(0.94495744, 0.53224397), P2(0.94798386, 0.5344541))
- s2 = Segment(P2(0.94798386, 0.5344541), P2(0.9472896, 0.5340202))
- @test s1 ∩ s2 ≈ s2 ∩ s1 ≈ P2(0.94798386, 0.5344541)
-
- s₁ = Segment(P2(0, 0), P2(3, 4))
- s₂ = Segment(P2(1, 2), P2(3, -2))
- s₃ = Segment(P2(2, 0), P2(-2, 0))
- s₄ = Segment(P2(0, 0), P2(1, 2))
- s₅ = Segment(P2(1, 2), P2(3, 4))
- s₆ = Segment(P2(-1, -4 / 3), P2(0, 0))
- s₇ = Segment(P2(1, 2), P2(0, 4))
- s₈ = Segment(P2(4, 16 / 3), P2(3, 4))
-
- s₉ = Segment(P2(-1, 5), P2(1, 4))
- s₁₀ = Segment(P2(1, 4), P2(-1, 5))
- s₁₁ = Segment(P2(-2, 5.5), P2(-0.8, 4.9))
- s₁₂ = Segment(P2(-0.8, 4.9), P2(-2, 5.5))
- s₁₃ = Segment(P2(-0.5, 4.75), P2(0.2, 4.4))
- s₁₄ = Segment(P2(0.2, 4.4), P2(-0.5, 4.75))
- s₁₅ = Segment(P2(0.5, 4.25), P2(1, 4))
- s₁₆ = Segment(P2(1, 4), P2(0.5, 4.25))
- s₁₇ = Segment(P2(2, 3.5), P2(1.5, 3.75))
- s₁₈ = Segment(P2(1.5, 3.75), P2(2, 3.5))
-
- @test s₁ ∩ s₂ ≈ s₂ ∩ s₁ ≈ P2(1.2, 1.6) # CASE 1: Crossing Segments
- @test intersection(s₁, s₂) |> type == Crossing
- @test intersection(s₂, s₁) |> type == Crossing
-
- @test s₁ ∩ s₃ ≈ s₃ ∩ s₁ ≈ P2(0, 0) # CASE 2: EdgeTouching (s₁(0))
- @test intersection(s₁, s₃) |> type == EdgeTouching
- @test intersection(s₃, s₁) |> type == EdgeTouching
-
- @test s₂ ∩ s₃ ≈ s₃ ∩ s₂ ≈ P2(2, 0) # CASE 2: EdgeTouching (s₃(1))
- @test intersection(s₂, s₃) |> type == EdgeTouching
- @test intersection(s₃, s₂) |> type == EdgeTouching
-
- @test s₁ ∩ s₄ ≈ s₄ ∩ s₁ ≈ P2(0, 0) # CASE 3: CornerTouching (s₁(0), s₄(0))
- @test intersection(s₁, s₄) |> type == CornerTouching
- @test intersection(s₄, s₁) |> type == CornerTouching
-
- @test s₂ ∩ s₄ ≈ s₄ ∩ s₂ ≈ P2(1, 2) # CASE 3: CornerTouching (s₂(0), s₄(1))
- @test intersection(s₂, s₄) |> type == CornerTouching
- @test intersection(s₄, s₂) |> type == CornerTouching
-
- @test s₁ ∩ s₅ ≈ s₅ ∩ s₁ ≈ P2(3, 4) # CASE 3: CornerTouching (s₁(1), s₅(1))
- @test intersection(s₂, s₄) |> type == CornerTouching
- @test intersection(s₄, s₂) |> type == CornerTouching
-
- @test s₁ ∩ s₆ ≈ s₆ ∩ s₁ ≈ P2(0, 0) # CASE 3: CornerTouching (s₁(0), s₆(1)), collinear
- @test intersection(s₁, s₆) |> type == CornerTouching
- @test intersection(s₆, s₁) |> type == CornerTouching
-
- @test s₂ ∩ s₇ ≈ s₇ ∩ s₂ ≈ P2(1, 2) # CASE 3: CornerTouching (s₂(0), s₇(0)), collinear
- @test intersection(s₂, s₇) |> type == CornerTouching
- @test intersection(s₇, s₂) |> type == CornerTouching
-
- @test s₁ ∩ s₈ ≈ s₈ ∩ s₁ ≈ P2(3, 4) # CASE 3: CornerTouching (s₁(1), s₈(1)), collinear
- @test intersection(s₁, s₈) |> type == CornerTouching
- @test intersection(s₈, s₁) |> type == CornerTouching
-
- @test s₉ ∩ s₉ ≈ s₉ # CASE 4: Overlapping (same segment)
- @test intersection(s₉, s₉) |> type == Overlapping
-
- @test s₉ ∩ s₁₀ ≈ s₉ # CASE 4: Overlapping (same segment, flipped points)
- @test s₁₀ ∩ s₉ ≈ s₁₀
- @test intersection(s₉, s₁₀) |> type == Overlapping
- @test intersection(s₁₀, s₉) |> type == Overlapping
-
- @test s₉ ∩ s₁₁ ≈ s₁₁ ∩ s₉ ≈ Segment(P2(-1, 5), P2(-0.8, 4.9)) # CASE 4: Overlapping (same alignment)
- @test intersection(s₉, s₁₁) |> type == Overlapping
- @test intersection(s₁₁, s₉) |> type == Overlapping
-
- @test s₉ ∩ s₁₂ ≈ Segment(P2(-1, 5), P2(-0.8, 4.9)) # CASE 4: Overlapping (opposite alignment, λ = 0 involved)
- @test s₁₂ ∩ s₉ ≈ Segment(P2(-0.8, 4.9), P2(-1, 5)) # flipped Points in Segment
- @test intersection(s₉, s₁₂) |> type == Overlapping
- @test intersection(s₁₂, s₉) |> type == Overlapping
-
- @test s₁₀ ∩ s₁₁ ≈ Segment(P2(-0.8, 4.9), P2(-1, 5)) # CASE 4: Overlapping (opposite alignment, λ = 1 involved)
- @test s₁₁ ∩ s₁₀ ≈ Segment(P2(-1, 5), P2(-0.8, 4.9)) # flipped Points in Segment
- @test intersection(s₁₀, s₁₁) |> type == Overlapping
- @test intersection(s₁₁, s₁₀) |> type == Overlapping
-
- @test s₉ ∩ s₁₃ ≈ s₁₃ ∩ s₉ ≈ s₁₃ # CASE 4: Overlapping (same alignment)
- @test intersection(s₉, s₁₃) |> type == Overlapping
- @test intersection(s₁₃, s₉) |> type == Overlapping
-
- @test s₁₄ ∩ s₉ ≈ s₁₄ # CASE 4: Overlapping (opposite alignment)
- @test s₉ ∩ s₁₄ ≈ s₁₃ # flipped Points in Segment
- @test intersection(s₉, s₁₄) |> type == Overlapping
- @test intersection(s₁₄, s₉) |> type == Overlapping
-
- @test s₉ ∩ s₁₅ ≈ s₁₅ ∩ s₉ ≈ s₁₅ # CASE 4: Overlapping (same alignment, corner case)
- @test intersection(s₉, s₁₅) |> type == Overlapping
- @test intersection(s₁₅, s₉) |> type == Overlapping
-
- @test s₁₅ ∩ s₁₀ ≈ s₁₅ # CASE 4: Overlapping (same alignment, corner case)
- @test s₁₀ ∩ s₁₅ ≈ s₁₆ # flipped Points in Segment
- @test intersection(s₁₀, s₁₅) |> type == Overlapping
- @test intersection(s₁₅, s₁₀) |> type == Overlapping
-
- @test s₁₆ ∩ s₉ ≈ s₁₆ # CASE 4: Overlapping (opposite alignment, corner case)
- @test s₉ ∩ s₁₆ ≈ s₁₅ # flipped Points in Segment
- @test intersection(s₉, s₁₆) |> type == Overlapping
- @test intersection(s₁₆, s₉) |> type == Overlapping
-
- @test s₁₀ ∩ s₁₆ ≈ s₁₆ ∩ s₁₀ ≈ s₁₆ # CASE 4: Overlapping (same alignment, corner case)
- @test intersection(s₁₀, s₁₆) |> type == Overlapping
- @test intersection(s₁₆, s₁₀) |> type == Overlapping
-
- @test s₉ ∩ s₁₇ === s₁₇ ∩ s₉ === nothing # CASE 5: NotIntersecting (collinear, same alignment)
- @test intersection(s₉, s₁₇) |> type == NotIntersecting
- @test intersection(s₁₇, s₉) |> type == NotIntersecting
-
- @test s₁₀ ∩ s₁₇ === s₁₇ ∩ s₁₀ === nothing # CASE 5: NotIntersecting (collinear, opposite alignment)
- @test intersection(s₁₀, s₁₇) |> type == NotIntersecting
- @test intersection(s₁₇, s₁₀) |> type == NotIntersecting
-
- @test s₉ ∩ s₁₈ === s₁₈ ∩ s₉ === nothing # CASE 5: NotIntersecting (collinear, opposite alignment)
- @test intersection(s₉, s₁₈) |> type == NotIntersecting
- @test intersection(s₁₈, s₉) |> type == NotIntersecting
-
- @test s₁ ∩ s₉ === s₉ ∩ s₁ === nothing # CASE 5: NotIntersecting, one λ in range
- @test intersection(s₉, s₁) |> type == NotIntersecting
- @test intersection(s₁, s₉) |> type == NotIntersecting
-
- @test s₁ ∩ s₁₀ === s₁₀ ∩ s₁ === nothing # CASE 5: NotIntersecting, one λ in range
- @test intersection(s₁₀, s₁) |> type == NotIntersecting
- @test intersection(s₁, s₁₀) |> type == NotIntersecting
-
- @test s₃ ∩ s₉ === s₉ ∩ s₃ === nothing # CASE 5: NotIntersecting
- @test intersection(s₉, s₁) |> type == NotIntersecting
- @test intersection(s₁, s₉) |> type == NotIntersecting
-
- @test s₃ ∩ s₁₀ === s₁₀ ∩ s₃ === nothing # CASE 5: NotIntersecting
- @test intersection(s₁₀, s₃) |> type == NotIntersecting
- @test intersection(s₃, s₁₀) |> type == NotIntersecting
-
- # segments in 3D
- s1 = Segment(P3(0.0, 0.0, 0.0), P3(1.0, 0.0, 0.0))
- s2 = Segment(P3(0.5, 1.0, 0.0), P3(0.5, -1.0, 0.0))
- s3 = Segment(P3(0.5, 0.0, 0.0), P3(1.5, 0.0, 0.0))
- s4 = Segment(P3(0.0, 1.0, 0.0), P3(0.0, -2.0, 0.0))
- s5 = Segment(P3(-1.0, 1.0, 0.0), P3(2.0, -2.0, 0.0))
- s6 = Segment(P3(0.0, 0.0, 0.0), P3(0.0, 1.0, 0.0))
- s7 = Segment(P3(-1.0, 1.0, 0.0), P3(-1.0, -1.0, 0.0))
- s8 = Segment(P3(-1.0, 1.0, 1.0), P3(-1.0, -1.0, 1.0))
- s9 = Segment(P3(0.5, 1.0, 1.0), P3(0.5, -1.0, 1.0))
- s10 = Segment(P3(0.0, 1.0, 0.0), P3(1.0, 1.0, 0.0))
- s11 = Segment(P3(1.5, 0.0, 0.0), P3(2.5, 0.0, 0.0))
- s12 = Segment(P3(1.0, 0.0, 0.0), P3(2.0, 0.0, 0.0))
-
- @test intersection(s1, s2) |> type == Crossing
- @test s1 ∩ s2 ≈ P3(0.5, 0.0, 0.0)
- @test intersection(s1, s3) |> type == Overlapping
- @test s1 ∩ s3 ≈ Segment(P3(0.5, 0.0, 0.0), P3(1.0, 0.0, 0.0))
- @test intersection(s1, s4) |> type == EdgeTouching
- @test s1 ∩ s4 ≈ P3(0.0, 0.0, 0.0)
- @test intersection(s1, s5) |> type == EdgeTouching
- @test s1 ∩ s5 ≈ P3(0.0, 0.0, 0.0)
- @test intersection(s1, s6) |> type == CornerTouching
- @test s1 ∩ s6 ≈ P3(0.0, 0.0, 0.0)
- @test intersection(s1, s7) |> type == NotIntersecting
- @test isnothing(s1 ∩ s7)
- @test intersection(s1, s8) |> type == NotIntersecting
- @test isnothing(s1 ∩ s8)
- @test intersection(s1, s9) |> type == NotIntersecting
- @test isnothing(s1 ∩ s9)
- @test intersection(s1, s10) |> type == NotIntersecting
- @test isnothing(s1 ∩ s10)
- @test intersection(s1, s11) |> type == NotIntersecting
- @test isnothing(s1 ∩ s11)
- @test intersection(s1, s12) |> type == CornerTouching
- @test s1 ∩ s12 ≈ P3(1.0, 0.0, 0.0)
-
- # precision test
- s1 = Segment(P2(2.0, 2.0), P2(3.0, 1.0))
- s2 = Segment(P2(2.12505, 1.87503), P2(50000.0, 30000.0))
- s3 = Segment(P2(2.125005, 1.875003), P2(50000.0, 30000.0))
- s4 = Segment(P2(2.125005, 1.875003), P2(50002.125005, 30001.875003))
- @test s1 ∩ s2 === s2 ∩ s1 === nothing
- @test s1 ∩ s3 === s3 ∩ s1 === ((T == Float32) ? P2(2.125005, 1.875003) : nothing)
- @test s1 ∩ s4 === s4 ∩ s1 === ((T == Float32) ? P2(2.125005, 1.875003) : nothing)
-
- # type stability tests
- s1 = Segment(P2(0, 0), P2(1, 0))
- s2 = Segment(P2(0.5, 0.0), P2(2, 0))
- @inferred someornone(s1, s2)
-
- s1 = Segment(P3(0.0, 0.0, 0.0), P3(1.0, 0.0, 0.0))
- s2 = Segment(P3(0.5, 1.0, 0.0), P3(0.5, -1.0, 0.0))
- @inferred someornone(s1, s2)
-
- # rays and segments in 2D
- r₁ = Ray(P2(1, 0), V2(2, 1))
- s₁ = Segment(P2(0, 2), P2(2, -1)) # Crossing
- s₂ = Segment(P2(0, 2), P2(1, 0.5)) # NotIntersecting
- s₃ = Segment(P2(0, 2), P2(0.5, -0.5)) # NotIntersecting
- s₄ = Segment(P2(0.5, 1), P2(1.5, -1)) # EdgeTouching
- s₅ = Segment(P2(1.5, 0.25), P2(1.5, 2)) # EdgeTouching
- s₆ = Segment(P2(1, 0), P2(1, -1)) # CornerTouching
- s₇ = Segment(P2(0.5, -1), P2(1, 0)) # CornerTouching
-
- @test intersection(r₁, s₁) |> type == Crossing #CASE 1
- @test r₁ ∩ s₁ ≈ s₁ ∩ r₁ ≈ P2(1.25, 0.125)
- @test intersection(r₁, s₂) |> type == NotIntersecting # CASE 5
- @test r₁ ∩ s₂ === s₂ ∩ r₁ === nothing
- @test intersection(r₁, s₃) |> type == NotIntersecting # CASE 5
- @test r₁ ∩ s₃ === s₃ ∩ r₁ === nothing
- @test intersection(r₁, s₄) |> type == EdgeTouching # CASE 2
- @test r₁ ∩ s₄ ≈ s₄ ∩ r₁ ≈ r₁(0)
- @test intersection(r₁, s₅) |> type == EdgeTouching # CASE 2
- @test r₁ ∩ s₅ ≈ s₅ ∩ r₁ ≈ P2(1.5, 0.25)
- @test intersection(r₁, s₆) |> type == CornerTouching # CASE 3
- @test r₁ ∩ s₆ ≈ s₆ ∩ r₁ ≈ r₁(0)
- @test intersection(r₁, s₇) |> type == CornerTouching # CASE 3
- @test r₁ ∩ s₇ ≈ s₇ ∩ r₁ ≈ r₁(0)
-
- r₂ = Ray(P2(3, 2), V2(1, 1))
- s₈ = Segment(P2(4, 3), P2(5, 4)) # Overlapping
- s₉ = Segment(P2(2.5, 1.5), P2(3.3, 2.3)) # Overlapping s(1)
- s₁₀ = Segment(P2(3.6, 2.6), P2(2.6, 1.6)) # Overlapping s(0)
- s₁₁ = Segment(P2(2.2, 1.2), P2(3, 2)) # CornerTouching, colinear, s(1)
- s₁₂ = Segment(P2(3, 2), P2(2.4, 1.4)) # CornerTouching, colinear, s(0)
- s₁₃ = Segment(P2(3, 2), P2(3.1, 2.1)) # Overlapping s(0) = r(0)
- s₁₄ = Segment(P2(3.2, 2.2), P2(3, 2)) # Overlapping s(1) = r(0)
- s₁₅ = Segment(P2(2, 1), P2(1.6, 0.6)) # No Intersection, colinear
- s₁₆ = Segment(P2(3, 1), P2(4, 2)) # No Intersection, parallel
- @test intersection(r₂, s₈) |> type == Overlapping # CASE 4
- @test r₂ ∩ s₈ === s₈ ∩ r₂ === s₈
- @test intersection(r₂, s₉) |> type == Overlapping # CASE 4
- @test r₂ ∩ s₉ == s₉ ∩ r₂ == Segment(r₂(0), s₉(1))
- @test intersection(r₂, s₁₀) |> type == Overlapping # CASE 4
- @test r₂ ∩ s₁₀ == s₁₀ ∩ r₂ == Segment(r₂(0), s₁₀(0))
- @test intersection(r₂, s₁₁) |> type == CornerTouching # CASE 3
- @test r₂ ∩ s₁₁ ≈ s₁₁ ∩ r₂ ≈ r₂(0)
- @test intersection(r₂, s₁₂) |> type == CornerTouching # CASE 3
- @test r₂ ∩ s₁₂ ≈ s₁₂ ∩ r₂ ≈ r₂(0)
- @test intersection(r₂, s₁₃) |> type == Overlapping # CASE 4
- @test r₂ ∩ s₁₃ === s₁₃ ∩ r₂ === s₁₃
- @test intersection(r₂, s₁₄) |> type == Overlapping # CASE 4
- @test r₂ ∩ s₁₄ === s₁₄ ∩ r₂ === s₁₄
- @test intersection(r₂, s₁₅) |> type == NotIntersecting # CASE 5
- @test r₂ ∩ s₁₅ === s₁₅ ∩ r₂ === nothing
- @test intersection(r₂, s₁₆) |> type == NotIntersecting # CASE 5
- @test r₂ ∩ s₁₆ === s₁₆ ∩ r₂ === nothing
-
- # type stability tests
- r₁ = Ray(P2(0, 0), V2(1, 0))
- s₁ = Segment(P2(-1, -1), P2(-1, 1))
- @inferred someornone(r₁, s₁)
-
- # 3D test
- r₁ = Ray(P3(1, 2, 3), V3(1, 2, 3))
- s₁ = Segment(P3(1, 3, 5), P3(3, 5, 7))
- @test intersection(r₁, s₁) |> type === Crossing # CASE 1
- @test r₁ ∩ s₁ ≈ s₁ ∩ r₁ ≈ P3(2, 4, 6)
-
- s₂ = Segment(P3(0, 1, 2), P3(2, 3, 4))
- @test intersection(r₁, s₂) |> type === EdgeTouching # CASE 2
- @test r₁ ∩ s₂ == s₂ ∩ r₁ == r₁(0)
-
- s₃ = Segment(P3(0.23, 1, 2.3), P3(1, 2, 3))
- @test intersection(r₁, s₃) |> type === CornerTouching # CASE 3
- @test r₁ ∩ s₃ == s₃ ∩ r₁ == r₁(0)
-
- s₄ = Segment(P3(0, 0, 0), P3(2, 4, 6))
- @test intersection(r₁, s₄) |> type === Overlapping # CASE 4
- @test r₁ ∩ s₄ == s₄ ∩ r₁ == Segment(P3(1, 2, 3), P3(2, 4, 6))
-
- s₅ = Segment(P3(0, 0, 0), P3(0.5, 1, 1.5))
- @test intersection(r₁, s₅) |> type === NotIntersecting # CASE 5
- @test r₁ ∩ s₅ === s₅ ∩ r₁ === nothing
-
- l₁ = Line(P2(1, 0), P2(3, 1))
- s₁ = Segment(P2(0, 2), P2(2, -1)) # Crossing
- s₂ = Segment(P2(0.5, 1), P2(0, 0)) # NotIntersecting
- s₃ = Segment(P2(0, 2), P2(-2, 1)) # NotIntersecting
- s₄ = Segment(P2(0.5, -1), P2(1, 0)) # Touching
- s₅ = Segment(P2(1.5, 0.25), P2(1.5, 2)) # Touching
- s₆ = Segment(P2(-3, -2), P2(4, 1.5)) # Overlapping
-
- @test intersection(l₁, s₁) |> type == Crossing #CASE 1
- @test l₁ ∩ s₁ ≈ s₁ ∩ l₁ ≈ P2(1.25, 0.125)
- @test intersection(l₁, s₂) |> type == NotIntersecting # CASE 4
- @test l₁ ∩ s₂ === s₂ ∩ l₁ === nothing
- @test intersection(l₁, s₃) |> type == NotIntersecting # CASE 4
- @test l₁ ∩ s₃ === s₃ ∩ l₁ === nothing
- @test intersection(l₁, s₄) |> type == Touching # CASE 2
- @test l₁ ∩ s₄ ≈ s₄ ∩ l₁ ≈ s₄(1)
- @test intersection(l₁, s₅) |> type == Touching # CASE 2
- @test l₁ ∩ s₅ ≈ s₅ ∩ l₁ ≈ s₅(0)
- @test intersection(l₁, s₆) |> type == Overlapping # CASE 3
- @test l₁ ∩ s₆ ≈ s₆ ∩ l₁ ≈ s₆
-
- # type stability tests
- @inferred someornone(l₁, s₁)
- @inferred someornone(l₁, s₂)
-
- # 3d tests
- l₁ = Line(P3(1, 0, 1), P3(3, 1, 1))
- s₁ = Segment(P3(0, 2, 1), P3(2, -1, 1)) # Crossing
- s₂ = Segment(P3(0.5, 1, 1), P3(0, 0, 1)) # NotIntersecting
- s₃ = Segment(P3(0, 2, 1), P3(-2, 1, 1)) # NotIntersecting
- s₄ = Segment(P3(0.5, -1, 1), P3(1, 0, 1)) # Touching
- s₅ = Segment(P3(1.5, 0.25, 1), P3(1.5, 2, 1)) # Touching
- s₆ = Segment(P3(-3, -2, 1), P3(4, 1.5, 1)) # Overlapping
- s₇ = Segment(P3(0, 2, 1), P3(2, -1, 1.1)) # NotIntersecting
-
- @test intersection(l₁, s₁) |> type == Crossing #CASE 1
- @test l₁ ∩ s₁ ≈ s₁ ∩ l₁ ≈ P3(1.25, 0.125, 1)
- @test intersection(l₁, s₂) |> type == NotIntersecting # CASE 4
- @test l₁ ∩ s₂ === s₂ ∩ l₁ === nothing
- @test intersection(l₁, s₃) |> type == NotIntersecting # CASE 4
- @test l₁ ∩ s₃ === s₃ ∩ l₁ === nothing
- @test intersection(l₁, s₄) |> type == Touching # CASE 2
- @test l₁ ∩ s₄ ≈ s₄ ∩ l₁ ≈ s₄(1)
- @test intersection(l₁, s₅) |> type == Touching # CASE 2
- @test l₁ ∩ s₅ ≈ s₅ ∩ l₁ ≈ s₅(0)
- @test intersection(l₁, s₆) |> type == Overlapping # CASE 3
- @test l₁ ∩ s₆ ≈ s₆ ∩ l₁ ≈ s₆
- @test intersection(l₁, s₇) |> type == NotIntersecting # CASE 4
- @test l₁ ∩ s₇ === s₇ ∩ l₁ === nothing
-
- # degenerate segments
- A = P2(0.0, 0.0)
- B = P2(0.5, 0.0)
- C = P2(1.0, 0.0)
- s₀ = Segment(A, C)
- s₁ = Segment(A, A)
- s₂ = Segment(B, B)
- s₃ = Segment(C, C)
- @test s₀ ∩ s₁ ≈ s₁ ∩ s₀ ≈ A
- @test s₀ ∩ s₂ ≈ s₂ ∩ s₀ ≈ B
- @test s₀ ∩ s₃ ≈ s₃ ∩ s₀ ≈ C
- @test intersection(s₀, s₁) |> type == CornerTouching
- @test intersection(s₀, s₂) |> type == EdgeTouching
- @test intersection(s₀, s₃) |> type == CornerTouching
- @test s₁ ∩ s₂ === s₂ ∩ s₁ === nothing
- @test s₁ ∩ s₃ === s₃ ∩ s₁ === nothing
- @test s₂ ∩ s₃ === s₃ ∩ s₂ === nothing
- @test intersection(s₁, s₂) |> type == NotIntersecting
- @test intersection(s₁, s₃) |> type == NotIntersecting
- @test intersection(s₂, s₃) |> type == NotIntersecting
- @test s₁ ∩ s₁ ≈ A
- @test s₂ ∩ s₂ ≈ B
- @test s₃ ∩ s₃ ≈ C
- @test intersection(s₁, s₁) |> type == CornerTouching
- @test intersection(s₂, s₂) |> type == CornerTouching
- @test intersection(s₃, s₃) |> type == CornerTouching
-
- # utils
- @test Meshes._sort4vals(2.5, 1.4, 1.1, 2.0) == (1.4, 2.0)
- @test Meshes._sort4vals(2.0, 1.1, 1.4, 2.5) == (1.4, 2.0)
- @test Meshes._sort4vals(2.0, 2.5, 1.1, 1.4) == (1.4, 2.0)
- end
+@testitem "Ray intersection" setup = [Setup] begin
+ # rays in 2D
+ r₁ = Ray(cart(1, 0), vector(2, 1))
+ r₂ = Ray(cart(0, 2), vector(2, -3))
+ r₃ = Ray(cart(0.5, 1), vector(1, -2))
+ r₄ = Ray(cart(0, 2), vector(1, -3))
+ r₅ = Ray(cart(4, 1.5), vector(4, 2))
+ r₆ = Ray(cart(2, 0.5), vector(-0.5, -0.25))
+ r₇ = Ray(cart(4, 0), vector(0, 1))
+ @test intersection(r₁, r₂) |> type == Crossing #CASE 1
+ @test r₁ ∩ r₂ ≈ cart(1.25, 0.125)
+ @test r₁ ∩ r₇ ≈ cart(4, 1.5)
+ @test intersection(r₁, r₃) |> type == EdgeTouching #CASE 2
+ @test r₁ ∩ r₃ ≈ r₁(0) # origin of first ray
+ @test r₅ ∩ r₇ ≈ r₅(0)
+ @test intersection(r₃, r₁) |> type == EdgeTouching
+ @test r₃ ∩ r₁ ≈ r₁(0) # origin of second ray
+ @test r₇ ∩ r₅ ≈ r₅(0)
+ @test intersection(r₂, r₄) |> type == CornerTouching #CASE 3
+ @test r₂ ∩ r₄ ≈ r₂(0) ≈ r₄(0)
+ @test intersection(r₅, r₁) |> type == PosOverlapping #CASE 4
+ @test r₅ ∩ r₁ == r₅ # first ray
+ @test intersection(r₁, r₅) |> type == PosOverlapping #CASE 4
+ @test r₁ ∩ r₅ == r₅ # second ray
+ @test intersection(r₁, r₆) |> type == NegOverlapping #CASE 5
+ @test r₁ ∩ r₆ == Segment(r₁(0), r₆(0))
+ @test intersection(r₁, r₄) |> type == NotIntersecting #CASE 6
+ @test r₁ ∩ r₄ === r₄ ∩ r₁ === nothing
+
+ # lines and rays in 2D
+ l₁ = Line(cart(0, 0), cart(4, 5))
+ r₁ = Ray(cart(3, 4), vector(1, -2)) # crossing ray
+ r₂ = Ray(cart(1, 1.25), vector(1, 0.3)) # touching ray
+ r₃ = Ray(cart(-1, -1.25), vector(-1, -1.25)) # overlapping ray
+ r₄ = Ray(cart(1, 3), vector(1, 1.25)) # parallel ray
+ r₅ = Ray(cart(1, 1), vector(1, -1)) # no Intersection
+
+ @test l₁ ∩ r₁ ≈ r₁ ∩ l₁ ≈ cart(3.0769230769230766, 3.846153846153846) # CASE 1
+ @test intersection(l₁, r₁) |> type === Crossing
+
+ @test l₁ ∩ r₂ == r₂ ∩ l₁ == r₂(0) # CASE 2
+ @test intersection(l₁, r₂) |> type === Touching
+
+ @test l₁ ∩ r₃ == r₃ ∩ l₁ == r₃ # CASE 3
+ @test intersection(l₁, r₃) |> type === Overlapping
+
+ @test l₁ ∩ r₄ == r₄ ∩ l₁ === nothing # CASE 4 parallel
+ @test intersection(l₁, r₄) |> type === NotIntersecting
+
+ @test l₁ ∩ r₅ == r₅ ∩ l₁ === nothing # CASE 4 no intersection
+ @test intersection(l₁, r₅) |> type === NotIntersecting
+
+ # type stability tests
+ @inferred someornone(l₁, r₁)
+ @inferred someornone(l₁, r₅)
+
+ # 3D tests
+ # lines and rays in 3D
+ l₁ = Line(cart(0, 0, 0.1), cart(4, 5, 0.1))
+ r₁ = Ray(cart(3, 4, 0.1), vector(1, -2, 0)) # crossing ray
+ r₂ = Ray(cart(1, 1.25, 0.1), vector(1, 0.3, 0)) # touching ray
+ r₃ = Ray(cart(-1, -1.25, 0.1), vector(-1, -1.25, 0)) # overlapping ray
+ r₄ = Ray(cart(1, 3, 0.1), vector(1, 1.25, 0)) # parallel ray
+ r₅ = Ray(cart(1, 1, 0.1), vector(1, -1, 0)) # no Intersection
+ r₆ = Ray(cart(3, 4, 0), vector(1, -2, 1)) # crossing ray
+
+ @test l₁ ∩ r₁ ≈ r₁ ∩ l₁ ≈ cart(3.0769230769230766, 3.846153846153846, 0.1) # CASE 1
+ @test intersection(l₁, r₁) |> type === Crossing
+
+ @test l₁ ∩ r₂ == r₂ ∩ l₁ == r₂(0) # CASE 2
+ @test intersection(l₁, r₂) |> type === Touching
+
+ @test l₁ ∩ r₃ == r₃ ∩ l₁ == r₃ # CASE 3
+ @test intersection(l₁, r₃) |> type === Overlapping
+
+ @test l₁ ∩ r₄ == r₄ ∩ l₁ === nothing # CASE 4 parallel
+ @test intersection(l₁, r₄) |> type === NotIntersecting
+
+ @test l₁ ∩ r₅ == r₅ ∩ l₁ === nothing # CASE 4 no intersection
+ @test intersection(l₁, r₅) |> type === NotIntersecting
+
+ @test l₁ ∩ r₆ == r₆ ∩ l₁ === nothing # CASE 4 no intersection
+ @test intersection(l₁, r₆) |> type === NotIntersecting
+end
- @testset "Rays" begin
- # rays in 2D
- r₁ = Ray(P2(1, 0), V2(2, 1))
- r₂ = Ray(P2(0, 2), V2(2, -3))
- r₃ = Ray(P2(0.5, 1), V2(1, -2))
- r₄ = Ray(P2(0, 2), V2(1, -3))
- r₅ = Ray(P2(4, 1.5), V2(4, 2))
- r₆ = Ray(P2(2, 0.5), V2(-0.5, -0.25))
- r₇ = Ray(P2(4, 0), V2(0, 1))
- @test intersection(r₁, r₂) |> type == Crossing #CASE 1
- @test r₁ ∩ r₂ ≈ P2(1.25, 0.125)
- @test r₁ ∩ r₇ ≈ P2(4, 1.5)
- @test intersection(r₁, r₃) |> type == EdgeTouching #CASE 2
- @test r₁ ∩ r₃ ≈ r₁(0) # origin of first ray
- @test r₅ ∩ r₇ ≈ r₅(0)
- @test intersection(r₃, r₁) |> type == EdgeTouching
- @test r₃ ∩ r₁ ≈ r₁(0) # origin of second ray
- @test r₇ ∩ r₅ ≈ r₅(0)
- @test intersection(r₂, r₄) |> type == CornerTouching #CASE 3
- @test r₂ ∩ r₄ ≈ r₂(0) ≈ r₄(0)
- @test intersection(r₅, r₁) |> type == PosOverlapping #CASE 4
- @test r₅ ∩ r₁ == r₅ # first ray
- @test intersection(r₁, r₅) |> type == PosOverlapping #CASE 4
- @test r₁ ∩ r₅ == r₅ # second ray
- @test intersection(r₁, r₆) |> type == NegOverlapping #CASE 5
- @test r₁ ∩ r₆ == Segment(r₁(0), r₆(0))
- @test intersection(r₁, r₄) |> type == NotIntersecting #CASE 6
- @test r₁ ∩ r₄ === r₄ ∩ r₁ === nothing
-
- # lines and rays in 2D
- l₁ = Line(P2(0, 0), P2(4, 5))
- r₁ = Ray(P2(3, 4), V2(1, -2)) # crossing ray
- r₂ = Ray(P2(1, 1.25), V2(1, 0.3)) # touching ray
- r₃ = Ray(P2(-1, -1.25), V2(-1, -1.25)) # overlapping ray
- r₄ = Ray(P2(1, 3), V2(1, 1.25)) # parallel ray
- r₅ = Ray(P2(1, 1), V2(1, -1)) # no Intersection
-
- @test l₁ ∩ r₁ ≈ r₁ ∩ l₁ ≈ P2(3.0769230769230766, 3.846153846153846) # CASE 1
- @test intersection(l₁, r₁) |> type === Crossing
-
- @test l₁ ∩ r₂ == r₂ ∩ l₁ == r₂(0) # CASE 2
- @test intersection(l₁, r₂) |> type === Touching
-
- @test l₁ ∩ r₃ == r₃ ∩ l₁ == r₃ # CASE 3
- @test intersection(l₁, r₃) |> type === Overlapping
-
- @test l₁ ∩ r₄ == r₄ ∩ l₁ === nothing # CASE 4 parallel
- @test intersection(l₁, r₄) |> type === NotIntersecting
-
- @test l₁ ∩ r₅ == r₅ ∩ l₁ === nothing # CASE 4 no intersection
- @test intersection(l₁, r₅) |> type === NotIntersecting
-
- # type stability tests
- @inferred someornone(l₁, r₁)
- @inferred someornone(l₁, r₅)
-
- # 3D tests
- # lines and rays in 3D
- l₁ = Line(P3(0, 0, 0.1), P3(4, 5, 0.1))
- r₁ = Ray(P3(3, 4, 0.1), V3(1, -2, 0)) # crossing ray
- r₂ = Ray(P3(1, 1.25, 0.1), V3(1, 0.3, 0)) # touching ray
- r₃ = Ray(P3(-1, -1.25, 0.1), V3(-1, -1.25, 0)) # overlapping ray
- r₄ = Ray(P3(1, 3, 0.1), V3(1, 1.25, 0)) # parallel ray
- r₅ = Ray(P3(1, 1, 0.1), V3(1, -1, 0)) # no Intersection
- r₆ = Ray(P3(3, 4, 0), V3(1, -2, 1)) # crossing ray
-
- @test l₁ ∩ r₁ ≈ r₁ ∩ l₁ ≈ P3(3.0769230769230766, 3.846153846153846, 0.1) # CASE 1
- @test intersection(l₁, r₁) |> type === Crossing
-
- @test l₁ ∩ r₂ == r₂ ∩ l₁ == r₂(0) # CASE 2
- @test intersection(l₁, r₂) |> type === Touching
-
- @test l₁ ∩ r₃ == r₃ ∩ l₁ == r₃ # CASE 3
- @test intersection(l₁, r₃) |> type === Overlapping
-
- @test l₁ ∩ r₄ == r₄ ∩ l₁ === nothing # CASE 4 parallel
- @test intersection(l₁, r₄) |> type === NotIntersecting
-
- @test l₁ ∩ r₅ == r₅ ∩ l₁ === nothing # CASE 4 no intersection
- @test intersection(l₁, r₅) |> type === NotIntersecting
-
- @test l₁ ∩ r₆ == r₆ ∩ l₁ === nothing # CASE 4 no intersection
- @test intersection(l₁, r₆) |> type === NotIntersecting
+@testitem "Line intersection" setup = [Setup] begin
+ # lines in 2D
+ l1 = Line(cart(0, 0), cart(1, 0))
+ l2 = Line(cart(-1, -1), cart(-1, 1))
+ @test l1 ∩ l2 ≈ l2 ∩ l1 ≈ cart(-1, 0)
+
+ l1 = Line(cart(0, 0), cart(1, 0))
+ l2 = Line(cart(0, 1), cart(1, 1))
+ @test l1 ∩ l2 === l2 ∩ l1 === nothing
+
+ l1 = Line(cart(0, 0), cart(1, 0))
+ l2 = Line(cart(1, 0), cart(2, 0))
+ @test l1 == l2
+ @test l1 ∩ l2 == l2 ∩ l1 == l1
+
+ # rounding errors
+ for k in 1:1000
+ δ = k * atol(T)
+ lo = Line(cart(3.0, 1.0), cart(2.0, 2.0))
+ lδ = Line(cart(1.5, 1.5 + δ), cart(3.0, 1.5 + δ))
+ p = cart(2.5 - δ, 1.5 + δ)
+ @test lo ∩ lδ ≈ lδ ∩ lo ≈ p
end
- @testset "Lines" begin
- # lines in 2D
- l1 = Line(P2(0, 0), P2(1, 0))
- l2 = Line(P2(-1, -1), P2(-1, 1))
- @test l1 ∩ l2 ≈ l2 ∩ l1 ≈ P2(-1, 0)
-
- l1 = Line(P2(0, 0), P2(1, 0))
- l2 = Line(P2(0, 1), P2(1, 1))
- @test l1 ∩ l2 === l2 ∩ l1 === nothing
-
- l1 = Line(P2(0, 0), P2(1, 0))
- l2 = Line(P2(1, 0), P2(2, 0))
- @test l1 == l2
- @test l1 ∩ l2 == l2 ∩ l1 == l1
-
- # rounding errors
- l1 = Line(P2(3.0, 1.0), P2(2.0, 2.0))
- for k in 1:1000
- Δ = k * atol(T)
- l2 = Line(P2(1.5, 1.5 + Δ), P2(3.0, 1.5 + Δ))
- p = P2(2.5 - Δ, 1.5 + Δ)
- @test l1 ∩ l2 ≈ l2 ∩ l1 ≈ p
- end
-
- # lines in 3D
- # not in same plane
- l1 = Line(P3(0, 0, 0), P3(1, 0, 0))
- l2 = Line(P3(1, 1, 1), P3(1, 2, 1))
- @test l1 ∩ l2 == l2 ∩ l1 === nothing
-
- # in same plane but parallel
- l1 = Line(P3(0, 0, 0), P3(1, 0, 0))
- l2 = Line(P3(0, 1, 1), P3(1, 1, 1))
- @test l1 ∩ l2 == l2 ∩ l1 === nothing
-
- # in same plane and colinear
- l1 = Line(P3(0, 0, 0), P3(1, 0, 0))
- l2 = Line(P3(2, 0, 0), P3(3, 0, 0))
- @test l1 ∩ l2 == l2 ∩ l1 == l1
-
- # crossing in one point
- l1 = Line(P3(1, 2, 3), P3(2, 1, 0))
- l2 = Line(P3(1, 2, 3), P3(1, 1, 1))
- @test l1 ∩ l2 ≈ l2 ∩ l1 ≈ P3(1, 2, 3)
-
- # type stability tests
- l1 = Line(P2(0, 0), P2(1, 0))
- l2 = Line(P2(-1, -1), P2(-1, 1))
- @inferred someornone(l1, l2)
- end
+ # lines in 3D
+ # not in same plane
+ l1 = Line(cart(0, 0, 0), cart(1, 0, 0))
+ l2 = Line(cart(1, 1, 1), cart(1, 2, 1))
+ @test l1 ∩ l2 == l2 ∩ l1 === nothing
+
+ # in same plane but parallel
+ l1 = Line(cart(0, 0, 0), cart(1, 0, 0))
+ l2 = Line(cart(0, 1, 1), cart(1, 1, 1))
+ @test l1 ∩ l2 == l2 ∩ l1 === nothing
+
+ # in same plane and colinear
+ l1 = Line(cart(0, 0, 0), cart(1, 0, 0))
+ l2 = Line(cart(2, 0, 0), cart(3, 0, 0))
+ @test l1 ∩ l2 == l2 ∩ l1 == l1
+
+ # crossing in one point
+ l1 = Line(cart(1, 2, 3), cart(2, 1, 0))
+ l2 = Line(cart(1, 2, 3), cart(1, 1, 1))
+ @test l1 ∩ l2 ≈ l2 ∩ l1 ≈ cart(1, 2, 3)
+
+ # type stability tests
+ l1 = Line(cart(0, 0), cart(1, 0))
+ l2 = Line(cart(-1, -1), cart(-1, 1))
+ @inferred someornone(l1, l2)
+end
- @testset "Chains" begin
- # https://github.com/JuliaGeometry/Meshes.jl/issues/644
- r = Rope(P2(0, 0), P2(1, 1))
- @test r ∩ r == GeometrySet([Segment(P2(0, 0), P2(1, 1))])
- @inferred someornone(r, r)
- end
+@testitem "Chain intersection" setup = [Setup] begin
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/644
+ r = Rope(cart(0, 0), cart(1, 1))
+ @test r ∩ r == GeometrySet([Segment(cart(0, 0), cart(1, 1))])
+ @inferred someornone(r, r)
+end
- @testset "Planes" begin
- # ---------
- # SEGMENTS
- # ---------
-
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
-
- # intersecting segment and plane
- s = Segment(P3(0, 0, 0), P3(0, 2, 2))
- @test intersection(s, p) |> type == Crossing
- @test s ∩ p == P3(0, 1, 1)
-
- # intersecting segment and plane with λ ≈ 0
- s = Segment(P3(0, 0, 1), P3(0, 2, 2))
- @test intersection(s, p) |> type == Touching
- @test s ∩ p == P3(0, 0, 1)
-
- # intersecting segment and plane with λ ≈ 1
- s = Segment(P3(0, 0, 2), P3(0, 2, 1))
- @test intersection(s, p) |> type == Touching
- @test s ∩ p == P3(0, 2, 1)
-
- # segment contained within plane
- s = Segment(P3(0, 0, 1), P3(0, -2, 1))
- @test intersection(s, p) |> type == Overlapping
- @test s ∩ p == s
-
- # segment below plane, non-intersecting
- s = Segment(P3(0, 0, 0), P3(0, -2, -2))
- @test intersection(s, p) |> type == NotIntersecting
- @test isnothing(s ∩ p)
-
- # segment parallel to plane, offset, non-intersecting
- s = Segment(P3(0, 0, -1), P3(0, -2, -1))
- @test intersection(s, p) |> type == NotIntersecting
- @test isnothing(s ∩ p)
-
- # plane as first argument
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
- s = Segment(P3(0, 0, 0), P3(0, 2, 2))
- @test intersection(p, s) |> type == Crossing
- @test s ∩ p == p ∩ s == P3(0, 1, 1)
-
- # type stability tests
- s = Segment(P3(0, 0, 0), P3(0, 2, 2))
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
- @inferred someornone(s, p)
-
- # -----
- # RAYS
- # -----
-
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
-
- # intersecting ray and plane
- r = Ray(P3(0, 0, 0), V3(0, 2, 2))
- @test intersection(r, p) |> type == Crossing
- @test r ∩ p == P3(0, 1, 1)
-
- # intersecting ray and plane with λ ≈ 0
- r = Ray(P3(0, 0, 1), V3(0, 2, 1))
- @test intersection(r, p) |> type == Touching
- @test r ∩ p == P3(0, 0, 1)
-
- # intersecting ray and plane with λ ≈ 1 (only case where Ray different to Segment)
- r = Ray(P3(0, 0, 2), V3(0, 2, -1))
- @test intersection(r, p) |> type == Crossing
- @test r ∩ p == P3(0, 2, 1)
-
- # ray contained within plane
- r = Ray(P3(0, 0, 1), V3(0, -2, 0))
- @test intersection(r, p) |> type == Overlapping
- @test r ∩ p == r
-
- # ray below plane, non-intersecting
- r = Ray(P3(0, 0, 0), V3(0, -2, -2))
- @test intersection(r, p) |> type == NotIntersecting
- @test isnothing(r ∩ p)
-
- # ray parallel to plane, offset, non-intersecting
- r = Ray(P3(0, 0, -1), V3(0, -2, 0))
- @test intersection(r, p) |> type == NotIntersecting
- @test isnothing(r ∩ p)
-
- # plane as first argument
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
- r = Ray(P3(0, 0, 0), V3(0, 2, 2))
- @test intersection(p, r) |> type == Crossing
- @test r ∩ p == p ∩ r == P3(0, 1, 1)
-
- # ------
- # LINES
- # ------
-
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
-
- # intersecting line and plane
- l = Line(P3(0, 0, 0), P3(0, 2, 2))
- @test intersection(l, p) |> type == Crossing
- @test l ∩ p == P3(0, 1, 1)
-
- # intersecting line and plane with λ ≈ 0
- l = Line(P3(0, 0, 1), P3(0, 2, 2))
- @test intersection(l, p) |> type == Crossing
- @test l ∩ p == P3(0, 0, 1)
-
- # intersecting line and plane with λ ≈ 1
- l = Line(P3(0, 0, 2), P3(0, 2, 1))
- @test intersection(l, p) |> type == Crossing
- @test l ∩ p == P3(0, 2, 1)
-
- # line contained within plane
- l = Line(P3(0, 0, 1), P3(0, -2, 1))
- @test intersection(l, p) |> type == Overlapping
- @test l ∩ p == l
-
- # line below plane, non-intersecting
- l = Line(P3(0, 0, 0), P3(0, -2, -2))
- @test intersection(l, p) |> type == Crossing
- @test l ∩ p == P3(0, 1, 1)
-
- # line parallel to plane, offset, non-intersecting
- l = Line(P3(0, 0, -1), P3(0, -2, -1))
- @test intersection(l, p) |> type == NotIntersecting
- @test isnothing(l ∩ p)
-
- # plane as first argument
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
- l = Line(P3(0, 0, 0), P3(0, 2, 2))
- @test intersection(p, l) |> type == Crossing
- @test l ∩ p == p ∩ l == P3(0, 1, 1)
-
- # type stability tests
- l = Line(P3(0, 0, 0), P3(0, 2, 2))
- p = Plane(P3(0, 0, 1), V3(1, 0, 0), V3(0, 1, 0))
- @inferred someornone(l, p)
-
- # ------
- # PLANES
- # ------
-
- p1 = Plane(P3(0, 0, 0), V3(0, 0, 1))
-
- # p1 parallel to p2
- p2 = Plane(P3(0, 0, 1), V3(0, 0, 1))
- @test intersection(p1, p2) |> type == NotIntersecting
- @test isnothing(p1 ∩ p2)
-
- # p1 intersects p2
- p2 = Plane(P3(0, 0, 1), V3(1 / sqrt(2), 0, 1 / sqrt(2)))
- @test intersection(p1, p2) |> type == Intersecting
- @test p1 ∩ p2 == Line(P3(1, 0, 0), P3(1, 1, 0))
- end
+@testitem "Plane intersection" setup = [Setup] begin
+ # ---------
+ # SEGMENTS
+ # ---------
+
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+
+ # intersecting segment and plane
+ s = Segment(cart(0, 0, 0), cart(0, 2, 2))
+ @test intersection(s, p) |> type == Crossing
+ @test s ∩ p == cart(0, 1, 1)
+
+ # intersecting segment and plane with λ ≈ 0
+ s = Segment(cart(0, 0, 1), cart(0, 2, 2))
+ @test intersection(s, p) |> type == Touching
+ @test s ∩ p == cart(0, 0, 1)
+
+ # intersecting segment and plane with λ ≈ 1
+ s = Segment(cart(0, 0, 2), cart(0, 2, 1))
+ @test intersection(s, p) |> type == Touching
+ @test s ∩ p == cart(0, 2, 1)
+
+ # segment contained within plane
+ s = Segment(cart(0, 0, 1), cart(0, -2, 1))
+ @test intersection(s, p) |> type == Overlapping
+ @test s ∩ p == s
+
+ # segment below plane, non-intersecting
+ s = Segment(cart(0, 0, 0), cart(0, -2, -2))
+ @test intersection(s, p) |> type == NotIntersecting
+ @test isnothing(s ∩ p)
+
+ # segment parallel to plane, offset, non-intersecting
+ s = Segment(cart(0, 0, -1), cart(0, -2, -1))
+ @test intersection(s, p) |> type == NotIntersecting
+ @test isnothing(s ∩ p)
+
+ # plane as first argument
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+ s = Segment(cart(0, 0, 0), cart(0, 2, 2))
+ @test intersection(p, s) |> type == Crossing
+ @test s ∩ p == p ∩ s == cart(0, 1, 1)
+
+ # type stability tests
+ s = Segment(cart(0, 0, 0), cart(0, 2, 2))
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+ @inferred someornone(s, p)
+
+ # -----
+ # RAYS
+ # -----
+
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+
+ # intersecting ray and plane
+ r = Ray(cart(0, 0, 0), vector(0, 2, 2))
+ @test intersection(r, p) |> type == Crossing
+ @test r ∩ p == cart(0, 1, 1)
+
+ # intersecting ray and plane with λ ≈ 0
+ r = Ray(cart(0, 0, 1), vector(0, 2, 1))
+ @test intersection(r, p) |> type == Touching
+ @test r ∩ p == cart(0, 0, 1)
+
+ # intersecting ray and plane with λ ≈ 1 (only case where Ray different to Segment)
+ r = Ray(cart(0, 0, 2), vector(0, 2, -1))
+ @test intersection(r, p) |> type == Crossing
+ @test r ∩ p == cart(0, 2, 1)
+
+ # ray contained within plane
+ r = Ray(cart(0, 0, 1), vector(0, -2, 0))
+ @test intersection(r, p) |> type == Overlapping
+ @test r ∩ p == r
+
+ # ray below plane, non-intersecting
+ r = Ray(cart(0, 0, 0), vector(0, -2, -2))
+ @test intersection(r, p) |> type == NotIntersecting
+ @test isnothing(r ∩ p)
+
+ # ray parallel to plane, offset, non-intersecting
+ r = Ray(cart(0, 0, -1), vector(0, -2, 0))
+ @test intersection(r, p) |> type == NotIntersecting
+ @test isnothing(r ∩ p)
+
+ # plane as first argument
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+ r = Ray(cart(0, 0, 0), vector(0, 2, 2))
+ @test intersection(p, r) |> type == Crossing
+ @test r ∩ p == p ∩ r == cart(0, 1, 1)
+
+ # ------
+ # LINES
+ # ------
+
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+
+ # intersecting line and plane
+ l = Line(cart(0, 0, 0), cart(0, 2, 2))
+ @test intersection(l, p) |> type == Crossing
+ @test l ∩ p == cart(0, 1, 1)
+
+ # intersecting line and plane with λ ≈ 0
+ l = Line(cart(0, 0, 1), cart(0, 2, 2))
+ @test intersection(l, p) |> type == Crossing
+ @test l ∩ p == cart(0, 0, 1)
+
+ # intersecting line and plane with λ ≈ 1
+ l = Line(cart(0, 0, 2), cart(0, 2, 1))
+ @test intersection(l, p) |> type == Crossing
+ @test l ∩ p == cart(0, 2, 1)
+
+ # line contained within plane
+ l = Line(cart(0, 0, 1), cart(0, -2, 1))
+ @test intersection(l, p) |> type == Overlapping
+ @test l ∩ p == l
+
+ # line below plane, non-intersecting
+ l = Line(cart(0, 0, 0), cart(0, -2, -2))
+ @test intersection(l, p) |> type == Crossing
+ @test l ∩ p == cart(0, 1, 1)
+
+ # line parallel to plane, offset, non-intersecting
+ l = Line(cart(0, 0, -1), cart(0, -2, -1))
+ @test intersection(l, p) |> type == NotIntersecting
+ @test isnothing(l ∩ p)
+
+ # plane as first argument
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+ l = Line(cart(0, 0, 0), cart(0, 2, 2))
+ @test intersection(p, l) |> type == Crossing
+ @test l ∩ p == p ∩ l == cart(0, 1, 1)
+
+ # type stability tests
+ l = Line(cart(0, 0, 0), cart(0, 2, 2))
+ p = Plane(cart(0, 0, 1), vector(1, 0, 0), vector(0, 1, 0))
+ @inferred someornone(l, p)
+
+ # ------
+ # PLANES
+ # ------
+
+ p1 = Plane(cart(0, 0, 0), vector(0, 0, 1))
+
+ # p1 parallel to p2
+ p2 = Plane(cart(0, 0, 1), vector(0, 0, 1))
+ @test intersection(p1, p2) |> type == NotIntersecting
+ @test isnothing(p1 ∩ p2)
+
+ # p1 intersects p2
+ p2 = Plane(cart(0, 0, 1), vector(1 / sqrt(2), 0, 1 / sqrt(2)))
+ @test intersection(p1, p2) |> type == Intersecting
+ @test p1 ∩ p2 == Line(cart(1, 0, 0), cart(1, 1, 0))
+
+ # CRS propagation
+ c1 = Cartesian{WGS84Latest}(T(0), T(0), T(0))
+ c2 = Cartesian{WGS84Latest}(T(0), T(0), T(1))
+ p1 = Plane(Point(c1), vector(0, 0, 1))
+ p2 = Plane(Point(c2), vector(1 / sqrt(2), 0, 1 / sqrt(2)))
+ @test crs(p1 ∩ p2) === crs(p1)
+end
- @testset "Boxes" begin
- b1 = Box(P2(0, 0), P2(1, 1))
- b2 = Box(P2(0.5, 0.5), P2(2, 2))
- b3 = Box(P2(2, 2), P2(3, 3))
- b4 = Box(P2(1, 1), P2(2, 2))
- b5 = Box(P2(1.0, 0.5), P2(2, 2))
- b6 = Box(P2(0, 2), P2(1, 3))
- b7 = Box(P2(0, 1), P2(1, 2))
- b8 = Box(P2(0, -1), P2(1, 0))
- b9 = Box(P2(1, 0), P2(2, 1))
- b10 = Box(P2(-1, 0), P2(0, 1))
- @test intersection(b1, b2) |> type == Overlapping
- @test b1 ∩ b2 == Box(P2(0.5, 0.5), P2(1, 1))
- @test intersection(b1, b3) |> type == NotIntersecting
- @test isnothing(b1 ∩ b3)
- @test intersection(b1, b4) |> type == CornerTouching
- @test b1 ∩ b4 == P2(1, 1)
- @test intersection(b1, b5) |> type == Touching
- @test b1 ∩ b5 == Box(P2(1.0, 0.5), P2(1, 1))
- @test intersection(b1, b6) |> type == NotIntersecting
- @test isnothing(b1 ∩ b6)
- @test intersection(b1, b7) |> type == Touching
- @test b1 ∩ b7 == Box(P2(0, 1), P2(1, 1))
- @test intersection(b1, b8) |> type == Touching
- @test b1 ∩ b8 == Box(P2(0, 0), P2(1, 0))
- @test intersection(b1, b9) |> type == Touching
- @test b1 ∩ b9 == Box(P2(1, 0), P2(1, 1))
- @test intersection(b1, b10) |> type == Touching
- @test b1 ∩ b10 == Box(P2(0, 0), P2(0, 1))
-
- # more touching examples
- b1 = Box(P2(0, 0), P2(1, 1))
- b2 = Box(P2(1.0, 0.5), P2(2, 1))
- b3 = Box(P2(-1, 0), P2(0.0, 0.5))
- b4 = Box(P2(0, 1), P2(0.5, 2.0))
- b5 = Box(P2(0.5, -1.0), P2(1, 0))
- @test intersection(b1, b2) |> type == Touching
- @test b1 ∩ b2 == Box(P2(1.0, 0.5), P2(1, 1))
- @test intersection(b1, b3) |> type == Touching
- @test b1 ∩ b3 == Box(P2(0.0, 0.0), P2(0.0, 0.5))
- @test intersection(b1, b4) |> type == Touching
- @test b1 ∩ b4 == Box(P2(0.0, 1.0), P2(0.5, 1.0))
- @test intersection(b1, b5) |> type == Touching
- @test b1 ∩ b5 == Box(P2(0.5, 0.0), P2(1.0, 0.0))
-
- # tricky examples with degenerate boxes
- b1 = Box(P3(0, 0, 0), P3(2, 2, 0))
- b2 = Box(P3(3, 0, 0), P3(5, 2, 0))
- b3 = Box(P3(1, 0, 0), P3(3, 2, 0))
- @test intersection(b1, b2) |> type == NotIntersecting
- @test isnothing(b1 ∩ b2)
- @test intersection(b1, b3) |> type == Touching
- @test b1 ∩ b3 == Box(P3(1, 0, 0), P3(2, 2, 0))
-
- # type stability tests
- b1 = Box(P2(0, 0), P2(1, 1))
- b2 = Box(P2(0.5, 0.5), P2(2, 2))
- @inferred someornone(b1, b2)
-
- # Ray-Box intersection
- b = Box(P3(0, 0, 0), P3(1, 1, 1))
-
- r = Ray(P3(0, 0, 0), V3(1, 1, 1))
- @test intersection(r, b) |> type == Crossing
- @test r ∩ b == Segment(P3(0, 0, 0), P3(1, 1, 1))
-
- r = Ray(P3(-0.5, 0, 0), V3(1.0, 1.0, 1.0))
- @test intersection(r, b) |> type == Crossing
- @test r ∩ b == Segment(P3(0.0, 0.5, 0.5), P3(0.5, 1.0, 1.0))
-
- r = Ray(P3(3.0, 0.0, 0.5), V3(-1.0, 1.0, 0.0))
- @test intersection(r, b) |> type == NotIntersecting
-
- r = Ray(P3(2.0, 0.0, 0.5), V3(-1.0, 1.0, 0.0))
- @test intersection(r, b) |> type == Touching
- @test r ∩ b == P3(1.0, 1.0, 0.5)
-
- # the ray on a face of the box, got NaN in calculation
- r = Ray(P3(1.5, 0.0, 0.0), V3(-1.0, 1.0, 0.0))
- @test intersection(r, b) |> type == Crossing
- @test r ∩ b == Segment(P3(1.0, 0.5, 0.0), P3(0.5, 1.0, 0.0))
- end
+@testitem "Box intersection" setup = [Setup] begin
+ b1 = Box(cart(0, 0), cart(1, 1))
+ b2 = Box(cart(0.5, 0.5), cart(2, 2))
+ b3 = Box(cart(2, 2), cart(3, 3))
+ b4 = Box(cart(1, 1), cart(2, 2))
+ b5 = Box(cart(1.0, 0.5), cart(2, 2))
+ b6 = Box(cart(0, 2), cart(1, 3))
+ b7 = Box(cart(0, 1), cart(1, 2))
+ b8 = Box(cart(0, -1), cart(1, 0))
+ b9 = Box(cart(1, 0), cart(2, 1))
+ b10 = Box(cart(-1, 0), cart(0, 1))
+ @test intersection(b1, b2) |> type == Overlapping
+ @test b1 ∩ b2 == Box(cart(0.5, 0.5), cart(1, 1))
+ @test intersection(b1, b3) |> type == NotIntersecting
+ @test isnothing(b1 ∩ b3)
+ @test intersection(b1, b4) |> type == CornerTouching
+ @test b1 ∩ b4 == cart(1, 1)
+ @test intersection(b1, b5) |> type == Touching
+ @test b1 ∩ b5 == Box(cart(1.0, 0.5), cart(1, 1))
+ @test intersection(b1, b6) |> type == NotIntersecting
+ @test isnothing(b1 ∩ b6)
+ @test intersection(b1, b7) |> type == Touching
+ @test b1 ∩ b7 == Box(cart(0, 1), cart(1, 1))
+ @test intersection(b1, b8) |> type == Touching
+ @test b1 ∩ b8 == Box(cart(0, 0), cart(1, 0))
+ @test intersection(b1, b9) |> type == Touching
+ @test b1 ∩ b9 == Box(cart(1, 0), cart(1, 1))
+ @test intersection(b1, b10) |> type == Touching
+ @test b1 ∩ b10 == Box(cart(0, 0), cart(0, 1))
+
+ # more touching examples
+ b1 = Box(cart(0, 0), cart(1, 1))
+ b2 = Box(cart(1.0, 0.5), cart(2, 1))
+ b3 = Box(cart(-1, 0), cart(0.0, 0.5))
+ b4 = Box(cart(0, 1), cart(0.5, 2.0))
+ b5 = Box(cart(0.5, -1.0), cart(1, 0))
+ @test intersection(b1, b2) |> type == Touching
+ @test b1 ∩ b2 == Box(cart(1.0, 0.5), cart(1, 1))
+ @test intersection(b1, b3) |> type == Touching
+ @test b1 ∩ b3 == Box(cart(0.0, 0.0), cart(0.0, 0.5))
+ @test intersection(b1, b4) |> type == Touching
+ @test b1 ∩ b4 == Box(cart(0.0, 1.0), cart(0.5, 1.0))
+ @test intersection(b1, b5) |> type == Touching
+ @test b1 ∩ b5 == Box(cart(0.5, 0.0), cart(1.0, 0.0))
+
+ # tricky examples with degenerate boxes
+ b1 = Box(cart(0, 0, 0), cart(2, 2, 0))
+ b2 = Box(cart(3, 0, 0), cart(5, 2, 0))
+ b3 = Box(cart(1, 0, 0), cart(3, 2, 0))
+ @test intersection(b1, b2) |> type == NotIntersecting
+ @test isnothing(b1 ∩ b2)
+ @test intersection(b1, b3) |> type == Touching
+ @test b1 ∩ b3 == Box(cart(1, 0, 0), cart(2, 2, 0))
+
+ # different units
+ b1 = Box((T(0) * u"cm", T(0) * u"cm"), (T(100) * u"cm", T(100) * u"cm"))
+ b2 = Box((T(500) * u"mm", T(500) * u"mm"), (T(2000) * u"mm", T(2000) * u"mm"))
+ @test intersection(b1, b2) |> type == Overlapping
+ @test unit(Meshes.lentype(b1 ∩ b2)) == u"cm"
+ @test b1 ∩ b2 == Box(cart(0.5, 0.5), cart(1, 1))
+
+ # type stability tests
+ b1 = Box(cart(0, 0), cart(1, 1))
+ b2 = Box(cart(0.5, 0.5), cart(2, 2))
+ @inferred someornone(b1, b2)
+ b1 = Box((T(0) * u"cm", T(0) * u"cm"), (T(100) * u"cm", T(100) * u"cm"))
+ b2 = Box((T(500) * u"mm", T(500) * u"mm"), (T(2000) * u"mm", T(2000) * u"mm"))
+ @inferred someornone(b1, b2)
+
+ # CRS propagation
+ b1 = Box(merc(0, 0), merc(1, 1))
+ b2 = Box(merc(0.5, 0.5), merc(2, 2))
+ @test crs(b1 ∩ b2) === crs(b1)
+
+ # Ray-Box intersection
+ b = Box(cart(0, 0, 0), cart(1, 1, 1))
+
+ r = Ray(cart(0, 0, 0), vector(1, 1, 1))
+ @test intersection(r, b) |> type == Crossing
+ @test r ∩ b == Segment(cart(0, 0, 0), cart(1, 1, 1))
+
+ r = Ray(cart(-0.5, 0, 0), vector(1.0, 1.0, 1.0))
+ @test intersection(r, b) |> type == Crossing
+ @test r ∩ b == Segment(cart(0.0, 0.5, 0.5), cart(0.5, 1.0, 1.0))
+
+ r = Ray(cart(3.0, 0.0, 0.5), vector(-1.0, 1.0, 0.0))
+ @test intersection(r, b) |> type == NotIntersecting
+
+ r = Ray(cart(2.0, 0.0, 0.5), vector(-1.0, 1.0, 0.0))
+ @test intersection(r, b) |> type == Touching
+ @test r ∩ b == cart(1.0, 1.0, 0.5)
+
+ # the ray on a face of the box, got NaN in calculation
+ r = Ray(cart(1.5, 0.0, 0.0), vector(-1.0, 1.0, 0.0))
+ @test intersection(r, b) |> type == Crossing
+ @test r ∩ b == Segment(cart(1.0, 0.5, 0.0), cart(0.5, 1.0, 0.0))
+end
- @testset "Triangles" begin
- # utility to reverse segments, to more fully
- # test branches in the intersection algorithm
- reverse_segment(s) = Segment(vertices(s)[2], vertices(s)[1])
-
- # intersections with triangle lying in XY plane
- t = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
-
- # intersects through t
- s = Segment(P3(0.2, 0.2, 1.0), P3(0.2, 0.2, -1.0))
- @test intersection(s, t) |> type == Intersecting
- @test s ∩ t == P3(0.2, 0.2, 0.0)
-
- # intersects at a vertex of t
- s = Segment(P3(0.0, 0.0, 1.0), P3(0.0, 0.0, -1.0))
- @test intersection(s, t) |> type == Intersecting
- @test s ∩ t == P3(0.0, 0.0, 0.0)
-
- # normal to, doesn't intersect with t
- s = Segment(P3(0.9, 0.9, 1.0), P3(0.9, 0.9, -1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # coplanar, doesn't intersect with t
- s = Segment(P3(-0.2, -0.2, 0.0), P3(1.2, -0.2, 0.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # parallel, above, doesn't intersect with t
- s = Segment(P3(-0.2, 0.2, 1.0), P3(1.2, 0.2, 1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # parallel, below, doesn't intersect with t
- s = Segment(P3(-0.2, 0.2, -1.0), P3(1.2, 0.2, -1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # coplanar, within bounding box of t, no intersection
- s = Segment(P3(0.7, 0.8, 0.0), P3(0.8, 0.7, 0.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # segment above and to right of t, no intersection
- s = Segment(P3(1.0, 1.0, 0.0), P3(1.0, 1.0, 1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # segment below t, no intersection
- s = Segment(P3(0.5, -1.0, 0.0), P3(0.5, -1.0, 1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # segment left of t, no intersection
- s = Segment(P3(-1.0, 0.5, 0.0), P3(-1.0, 0.5, 1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # segment above and to right of t, no intersection
- s = Segment(P3(1.0, 1.0, 0.0), P3(1.0, 1.0, -1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
- @test intersection(reverse_segment(s), t) |> type == NotIntersecting
- @test isnothing(reverse_segment(s) ∩ t)
-
- # segment below t, no intersection
- s = Segment(P3(0.5, -1.0, 0.0), P3(0.5, -1.0, -1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
- @test intersection(reverse_segment(s), t) |> type == NotIntersecting
- @test isnothing(reverse_segment(s) ∩ t)
-
- # segment left of t, no intersection
- s = Segment(P3(-1.0, 0.5, 0.0), P3(-1.0, 0.5, -1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
- @test intersection(reverse_segment(s), t) |> type == NotIntersecting
- @test isnothing(reverse_segment(s) ∩ t)
-
- # segment above and to right of t, no intersection
- s = Segment(P3(1.0, 1.0, 1.0), P3(1.0, 1.0, 0.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # segment below t, no intersection
- s = Segment(P3(0.5, -1.0, 1.0), P3(0.5, -1.0, 0.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # segment left of t, no intersection
- s = Segment(P3(-1.0, 0.5, 1.0), P3(-1.0, 0.5, 0.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # intersections with an inclined inclined triangle t
- t = Triangle(P3(0, 0, 0), P3(2, 0, 0), P3(0, 2, 2))
-
- # doesn't reach t, no intersection
- s = Segment(P3(0.5, 0.5, 1.9), P3(0.5, 0.5, 1.8))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # parallel, offset from t, no intersection
- s = Segment(P3(0.0, 0.5, 1.0), P3(1.0, 0.5, 1.0))
- @test intersection(s, t) |> type == NotIntersecting
- @test isnothing(s ∩ t)
-
- # triangle as first argument
- t = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
- s = Segment(P3(0.2, 0.2, 1.0), P3(0.2, 0.2, -1.0))
- @test intersection(t, s) |> type == Intersecting
- @test s ∩ t == t ∩ s == P3(0.2, 0.2, 0.0)
-
- # type stability tests
- s = Segment(P3(0.2, 0.2, 1.0), P3(0.2, 0.2, -1.0))
- t = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
- @inferred someornone(s, t)
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/728
- s = Segment(P3(0.5, 0.5, 0.0), P3(0.5, 0.5, 2.0))
- t = Triangle(P3(1.0, 0.0, 0.0), P3(0.0, 1.0, 0.0), P3(0.0, 0.0, 1.0))
- @test intersection(s, t) |> type == Intersecting
- @test s ∩ t == t ∩ s == P3(0.5, 0.5, 0.0)
- s = Segment(P3(0.5, 0.5, 2.0), P3(0.5, 0.5, 0.0))
- @test intersection(s, t) |> type == Intersecting
- @test s ∩ t == t ∩ s == P3(0.5, 0.5, 0.0)
-
- # Intersection for a triangle and a ray
- t = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
-
- # intersects through t
- r = Ray(P3(0.2, 0.2, 1.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == Crossing
- @test r ∩ t == P3(0.2, 0.2, 0.0)
- # origin of ray intersects with middle of triangle
- r = Ray(P3(0.2, 0.2, 0.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == Touching
- @test r ∩ t == P3(0.2, 0.2, 0.0)
- # Special case: the direction vector is not length enough to cross triangle
- r = Ray(P3(0.2, 0.2, 1.0), V3(0.0, 0.0, -0.00001))
- @test intersection(r, t) |> type == Crossing
- if T == Float64
- @test r ∩ t ≈ P3(0.2, 0.2, 0.0)
- end
- # Special case: reverse direction vector should not hit the triangle
- r = Ray(P3(0.2, 0.2, 1.0), V3(0.0, 0.0, 1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # intersects at a vertex of t
- r = Ray(P3(0.0, 0.0, 1.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == CornerCrossing
- @test r ∩ t ≈ P3(0.0, 0.0, 0.0)
-
- # normal to, doesn't intersect with t
- r = Ray(P3(0.9, 0.9, 1.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # coplanar, doesn't intersect with t
- r = Ray(P3(-0.2, -0.2, 0.0), V3(1.0, 0.0, 0.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # parallel, above, doesn't intersect with t
- r = Ray(P3(-0.2, 0.2, 1.0), V3(1.0, 0.0, 0.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # parallel, below, doesn't intersect with t
- r = Ray(P3(-0.2, 0.2, -1.0), V3(1.0, 0.0, 0.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # coplanar, within bounding box of t, no intersection
- r = Ray(P3(0.7, 0.8, 0.0), V3(1.0, -1.0, 0.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray above and to right of t, no intersection
- r = Ray(P3(1.0, 1.0, 0.0), V3(0.0, 0.0, 1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray below t, no intersection
- r = Ray(P3(0.5, -1.0, 0.0), V3(0.0, 0.0, 1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray left of t, no intersection
- r = Ray(P3(-1.0, 0.5, 0.0), V3(0.0, 0.0, 1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray above and to right of t, no intersection
- r = Ray(P3(1.0, 1.0, 0.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray below t, no intersection
- r = Ray(P3(0.5, -1.0, 0.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray left of t, no intersection
- r = Ray(P3(-1.0, 0.5, 0.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray above and to right of t, no intersection
- r = Ray(P3(1.0, 1.0, 1.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray below t, no intersection
- r = Ray(P3(0.5, -1.0, 1.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # ray left of t, no intersection
- r = Ray(P3(-1.0, 0.5, 1.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # intersections with an inclined inclined triangle t
- t = Triangle(P3(0, 0, 0), P3(2, 0, 0), P3(0, 2, 2))
-
- # doesn't reach t, but a ray can hit the triangle
- r = Ray(P3(0.5, 0.5, 1.9), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == Crossing
- @test r ∩ t ≈ P3(0.5, 0.5, 0.5)
-
- # parallel, offset from t, no intersection
- r = Ray(P3(0.0, 0.5, 1.0), V3(1.0, 0.0, 0.0))
- @test intersection(r, t) |> type == NotIntersecting
- @test isnothing(r ∩ t)
-
- # origin of ray intersects with vertex of triangle
- r = Ray(P3(0.0, 0.0, 0.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == CornerTouching
- @test r ∩ t ≈ P3(0.0, 0.0, 0.0)
-
- # origin of ray intersects with edge of triangle
- r = Ray(P3(0.5, 0.0, 0.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == EdgeTouching
- @test r ∩ t ≈ P3(0.5, 0.0, 0.0)
-
- # ray intersects with edge of triangle
- r = Ray(P3(0.5, 0.0, 1.0), V3(0.0, 0.0, -1.0))
- @test intersection(r, t) |> type == EdgeCrossing
- @test r ∩ t ≈ P3(0.5, 0.0, 0.0)
+@testitem "Triangle intersection" setup = [Setup] begin
+ # utility to reverse segments, to more fully
+ # test branches in the intersection algorithm
+ reverse_segment(s) = Segment(vertices(s)[2], vertices(s)[1])
+
+ # intersections with triangle lying in XY plane
+ t = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0))
+
+ # intersects through t
+ s = Segment(cart(0.2, 0.2, 1.0), cart(0.2, 0.2, -1.0))
+ @test intersection(s, t) |> type == Intersecting
+ @test s ∩ t == cart(0.2, 0.2, 0.0)
+
+ # intersects at a vertex of t
+ s = Segment(cart(0.0, 0.0, 1.0), cart(0.0, 0.0, -1.0))
+ @test intersection(s, t) |> type == Intersecting
+ @test s ∩ t == cart(0.0, 0.0, 0.0)
+
+ # normal to, doesn't intersect with t
+ s = Segment(cart(0.9, 0.9, 1.0), cart(0.9, 0.9, -1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # coplanar, doesn't intersect with t
+ s = Segment(cart(-0.2, -0.2, 0.0), cart(1.2, -0.2, 0.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # parallel, above, doesn't intersect with t
+ s = Segment(cart(-0.2, 0.2, 1.0), cart(1.2, 0.2, 1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # parallel, below, doesn't intersect with t
+ s = Segment(cart(-0.2, 0.2, -1.0), cart(1.2, 0.2, -1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # coplanar, within bounding box of t, no intersection
+ s = Segment(cart(0.7, 0.8, 0.0), cart(0.8, 0.7, 0.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # segment above and to right of t, no intersection
+ s = Segment(cart(1.0, 1.0, 0.0), cart(1.0, 1.0, 1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # segment below t, no intersection
+ s = Segment(cart(0.5, -1.0, 0.0), cart(0.5, -1.0, 1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # segment left of t, no intersection
+ s = Segment(cart(-1.0, 0.5, 0.0), cart(-1.0, 0.5, 1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # segment above and to right of t, no intersection
+ s = Segment(cart(1.0, 1.0, 0.0), cart(1.0, 1.0, -1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+ @test intersection(reverse_segment(s), t) |> type == NotIntersecting
+ @test isnothing(reverse_segment(s) ∩ t)
+
+ # segment below t, no intersection
+ s = Segment(cart(0.5, -1.0, 0.0), cart(0.5, -1.0, -1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+ @test intersection(reverse_segment(s), t) |> type == NotIntersecting
+ @test isnothing(reverse_segment(s) ∩ t)
+
+ # segment left of t, no intersection
+ s = Segment(cart(-1.0, 0.5, 0.0), cart(-1.0, 0.5, -1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+ @test intersection(reverse_segment(s), t) |> type == NotIntersecting
+ @test isnothing(reverse_segment(s) ∩ t)
+
+ # segment above and to right of t, no intersection
+ s = Segment(cart(1.0, 1.0, 1.0), cart(1.0, 1.0, 0.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # segment below t, no intersection
+ s = Segment(cart(0.5, -1.0, 1.0), cart(0.5, -1.0, 0.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # segment left of t, no intersection
+ s = Segment(cart(-1.0, 0.5, 1.0), cart(-1.0, 0.5, 0.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # intersections with an inclined inclined triangle t
+ t = Triangle(cart(0, 0, 0), cart(2, 0, 0), cart(0, 2, 2))
+
+ # doesn't reach t, no intersection
+ s = Segment(cart(0.5, 0.5, 1.9), cart(0.5, 0.5, 1.8))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # parallel, offset from t, no intersection
+ s = Segment(cart(0.0, 0.5, 1.0), cart(1.0, 0.5, 1.0))
+ @test intersection(s, t) |> type == NotIntersecting
+ @test isnothing(s ∩ t)
+
+ # triangle as first argument
+ t = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0))
+ s = Segment(cart(0.2, 0.2, 1.0), cart(0.2, 0.2, -1.0))
+ @test intersection(t, s) |> type == Intersecting
+ @test s ∩ t == t ∩ s == cart(0.2, 0.2, 0.0)
+
+ # type stability tests
+ s = Segment(cart(0.2, 0.2, 1.0), cart(0.2, 0.2, -1.0))
+ t = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0))
+ @inferred someornone(s, t)
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/728
+ s = Segment(cart(0.5, 0.5, 0.0), cart(0.5, 0.5, 2.0))
+ t = Triangle(cart(1.0, 0.0, 0.0), cart(0.0, 1.0, 0.0), cart(0.0, 0.0, 1.0))
+ @test intersection(s, t) |> type == Intersecting
+ @test s ∩ t == t ∩ s == cart(0.5, 0.5, 0.0)
+ s = Segment(cart(0.5, 0.5, 2.0), cart(0.5, 0.5, 0.0))
+ @test intersection(s, t) |> type == Intersecting
+ @test s ∩ t == t ∩ s == cart(0.5, 0.5, 0.0)
+
+ # Intersection for a triangle and a ray
+ t = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0))
+
+ # intersects through t
+ r = Ray(cart(0.2, 0.2, 1.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == Crossing
+ @test r ∩ t == cart(0.2, 0.2, 0.0)
+ # origin of ray intersects with middle of triangle
+ r = Ray(cart(0.2, 0.2, 0.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == Touching
+ @test r ∩ t == cart(0.2, 0.2, 0.0)
+ # Special case: the direction vector is not length enough to cross triangle
+ r = Ray(cart(0.2, 0.2, 1.0), vector(0.0, 0.0, -0.00001))
+ @test intersection(r, t) |> type == Crossing
+ if T == Float64
+ @test r ∩ t ≈ cart(0.2, 0.2, 0.0)
end
+ # Special case: reverse direction vector should not hit the triangle
+ r = Ray(cart(0.2, 0.2, 1.0), vector(0.0, 0.0, 1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # intersects at a vertex of t
+ r = Ray(cart(0.0, 0.0, 1.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == CornerCrossing
+ @test r ∩ t ≈ cart(0.0, 0.0, 0.0)
+
+ # normal to, doesn't intersect with t
+ r = Ray(cart(0.9, 0.9, 1.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # coplanar, doesn't intersect with t
+ r = Ray(cart(-0.2, -0.2, 0.0), vector(1.0, 0.0, 0.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # parallel, above, doesn't intersect with t
+ r = Ray(cart(-0.2, 0.2, 1.0), vector(1.0, 0.0, 0.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # parallel, below, doesn't intersect with t
+ r = Ray(cart(-0.2, 0.2, -1.0), vector(1.0, 0.0, 0.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # coplanar, within bounding box of t, no intersection
+ r = Ray(cart(0.7, 0.8, 0.0), vector(1.0, -1.0, 0.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray above and to right of t, no intersection
+ r = Ray(cart(1.0, 1.0, 0.0), vector(0.0, 0.0, 1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray below t, no intersection
+ r = Ray(cart(0.5, -1.0, 0.0), vector(0.0, 0.0, 1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray left of t, no intersection
+ r = Ray(cart(-1.0, 0.5, 0.0), vector(0.0, 0.0, 1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray above and to right of t, no intersection
+ r = Ray(cart(1.0, 1.0, 0.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray below t, no intersection
+ r = Ray(cart(0.5, -1.0, 0.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray left of t, no intersection
+ r = Ray(cart(-1.0, 0.5, 0.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray above and to right of t, no intersection
+ r = Ray(cart(1.0, 1.0, 1.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray below t, no intersection
+ r = Ray(cart(0.5, -1.0, 1.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # ray left of t, no intersection
+ r = Ray(cart(-1.0, 0.5, 1.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # intersections with an inclined inclined triangle t
+ t = Triangle(cart(0, 0, 0), cart(2, 0, 0), cart(0, 2, 2))
+
+ # doesn't reach t, but a ray can hit the triangle
+ r = Ray(cart(0.5, 0.5, 1.9), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == Crossing
+ @test r ∩ t ≈ cart(0.5, 0.5, 0.5)
+
+ # parallel, offset from t, no intersection
+ r = Ray(cart(0.0, 0.5, 1.0), vector(1.0, 0.0, 0.0))
+ @test intersection(r, t) |> type == NotIntersecting
+ @test isnothing(r ∩ t)
+
+ # origin of ray intersects with vertex of triangle
+ r = Ray(cart(0.0, 0.0, 0.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == CornerTouching
+ @test r ∩ t ≈ cart(0.0, 0.0, 0.0)
+
+ # origin of ray intersects with edge of triangle
+ r = Ray(cart(0.5, 0.0, 0.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == EdgeTouching
+ @test r ∩ t ≈ cart(0.5, 0.0, 0.0)
+
+ # ray intersects with edge of triangle
+ r = Ray(cart(0.5, 0.0, 1.0), vector(0.0, 0.0, -1.0))
+ @test intersection(r, t) |> type == EdgeCrossing
+ @test r ∩ t ≈ cart(0.5, 0.0, 0.0)
+end
- @testset "Ngons" begin
- o = Octagon(
- P3(0.0, 0.0, 1.0),
- P3(0.5, -0.5, 0.0),
- P3(1.0, 0.0, 0.0),
- P3(1.5, 0.5, -0.5),
- P3(1.0, 1.0, 0.0),
- P3(0.5, 1.5, 0.0),
- P3(0.0, 1.0, 0.0),
- P3(-0.5, 0.5, 0.0)
- )
-
- r = Ray(P3(-1.0, -1.0, -1.0), V3(1.0, 1.0, 1.0))
- @test intersection(r, o) |> type == Intersecting
- @test r ∩ o == PointSet([P3(0.0, 0.0, 0.0)])
-
- r = Ray(P3(-1.0, -1.0, -1.0), V3(-1.0, -1.0, -1.0))
- @test intersection(r, o) |> type == NotIntersecting
- @test isnothing(r ∩ o)
- end
+@testitem "Ngon intersection" setup = [Setup] begin
+ o = Octagon(
+ cart(0.0, 0.0, 1.0),
+ cart(0.5, -0.5, 0.0),
+ cart(1.0, 0.0, 0.0),
+ cart(1.5, 0.5, -0.5),
+ cart(1.0, 1.0, 0.0),
+ cart(0.5, 1.5, 0.0),
+ cart(0.0, 1.0, 0.0),
+ cart(-0.5, 0.5, 0.0)
+ )
+
+ r = Ray(cart(-1.0, -1.0, -1.0), vector(1.0, 1.0, 1.0))
+ @test intersection(r, o) |> type == Intersecting
+ @test r ∩ o == PointSet(cart(0.0, 0.0, 0.0))
+
+ r = Ray(cart(-1.0, -1.0, -1.0), vector(-1.0, -1.0, -1.0))
+ @test intersection(r, o) |> type == NotIntersecting
+ @test isnothing(r ∩ o)
+
+ t = Triangle(cart(0.9356498598903396, 6.5), cart(1.3571428571428377, 6.5), cart(1.0, 7.0))
+ q = Quadrangle(cart(0.0, 0.0), cart(6.0, 0.0), cart(1.0, 7.0), cart(1.0, 6.0))
+ @test intersection(t, q) |> type == Intersecting
+ @test t ∩ q isa PolyArea
+ @test q ∩ t isa PolyArea
+
+ t1 = Triangle(cart(0.0, 0.0), cart(0.0, 1.000000000000001), cart(1.0, 1.0))
+ t2 = Triangle(cart(0.0, 1.0), cart(0.0, 2.0), cart(1.0, 1.000000000001))
+ @test intersection(t1, t2) |> type == Intersecting
+ @test t1 ∩ t2 isa PolyArea
+end
- @testset "Polygons" begin
- # triangle
- poly = Triangle(P2(6, 2), P2(3, 5), P2(0, 2))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- @test intersection(poly, other) |> type == Intersecting
- @test all(vertices(poly ∩ other) .≈ [P2(5, 3), P2(4, 4), P2(2, 4), P2(0, 2), P2(5, 2)])
-
- # octagon
- poly = Octagon(P2(8, -2), P2(8, 5), P2(2, 5), P2(4, 3), P2(6, 3), P2(4, 1), P2(2, 1), P2(2, -2))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- @test intersection(poly, other) |> type == Intersecting
- @test all(
- vertices(poly ∩ other) .≈
- [P2(3, 4), P2(4, 3), P2(5, 3), P2(5, 2), P2(4, 1), P2(2, 1), P2(2, 0), P2(5, 0), P2(5, 4)]
- )
-
- # inside
- poly = Quadrangle(P2(1, 0), P2(1, 1), P2(0, 1), P2(0, 0))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- @test intersection(poly, other) |> type == Intersecting
- @test all(vertices(poly ∩ other) .≈ vertices(poly))
-
- # outside
- poly = Quadrangle(P2(7, 6), P2(7, 7), P2(6, 7), P2(6, 6))
- other = Quadrangle(P2(5, 0), P2(5, 4), P2(0, 4), P2(0, 0))
- @test intersection(poly, other) |> type == NotIntersecting
- @test isnothing(poly ∩ other)
-
- # convex and non-convex polygons
- quad = Quadrangle(P2(0, 0), P2(0.1, 0.0), P2(0.1, 0.1), P2(0.0, 0.1))
- poly = PolyArea(P2(0, 0), P2(2, 0), P2(1, 1), P2(1, 0.5))
- @test intersection(quad, poly) |> type == Intersecting
- @test all(vertices(quad ∩ poly) .≈ [P2(0, 0), P2(0.1, 0), P2(0.1, 0.05)])
- end
+@testitem "Polygon intersection" setup = [Setup] begin
+ # triangle
+ poly = Triangle(cart(6, 2), cart(3, 5), cart(0, 2))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ @test intersection(poly, other) |> type == Intersecting
+ @test all(vertices(poly ∩ other) .≈ [cart(5, 3), cart(4, 4), cart(2, 4), cart(0, 2), cart(5, 2)])
+
+ # octagon
+ poly = Octagon(cart(8, -2), cart(8, 5), cart(2, 5), cart(4, 3), cart(6, 3), cart(4, 1), cart(2, 1), cart(2, -2))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ @test intersection(poly, other) |> type == Intersecting
+ @test all(
+ vertices(poly ∩ other) .≈
+ [cart(3, 4), cart(4, 3), cart(5, 3), cart(5, 2), cart(4, 1), cart(2, 1), cart(2, 0), cart(5, 0), cart(5, 4)]
+ )
+
+ # inside
+ poly = Quadrangle(cart(1, 0), cart(1, 1), cart(0, 1), cart(0, 0))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ @test intersection(poly, other) |> type == Intersecting
+ @test all(vertices(poly ∩ other) .≈ vertices(poly))
+
+ # outside
+ poly = Quadrangle(cart(7, 6), cart(7, 7), cart(6, 7), cart(6, 6))
+ other = Quadrangle(cart(5, 0), cart(5, 4), cart(0, 4), cart(0, 0))
+ @test intersection(poly, other) |> type == NotIntersecting
+ @test isnothing(poly ∩ other)
+
+ # convex and non-convex polygons
+ quad = Quadrangle(cart(0, 0), cart(0.1, 0.0), cart(0.1, 0.1), cart(0.0, 0.1))
+ poly = PolyArea(cart(0, 0), cart(2, 0), cart(1, 1), cart(1, 0.5))
+ @test intersection(quad, poly) |> type == Intersecting
+ @test all(vertices(quad ∩ poly) .≈ [cart(0, 0), cart(0.1, 0), cart(0.1, 0.05)])
+end
- @testset "Domains" begin
- grid = CartesianGrid{T}(4, 4)
- pset = PointSet(centroid.(grid))
- ball = Ball(P2(0, 0), T(1))
- @test pset ∩ pset == pset
- @test pset ∩ grid == grid ∩ pset == pset
- @test pset ∩ ball == ball ∩ pset == PointSet(P2(0.5, 0.5))
- end
+@testitem "Domain intersection" setup = [Setup] begin
+ grid = cartgrid(4, 4)
+ pset = PointSet(centroid.(grid))
+ ball = Ball(cart(0, 0), T(1))
+ @test pset ∩ pset == pset
+ @test pset ∩ grid == grid ∩ pset == pset
+ @test pset ∩ ball == ball ∩ pset == PointSet(cart(0.5, 0.5))
end
diff --git a/test/matrices.jl b/test/matrices.jl
index 1096dd368..63be9fcf7 100644
--- a/test/matrices.jl
+++ b/test/matrices.jl
@@ -1,9 +1,9 @@
-@testset "Matrices" begin
+@testitem "Laplace matrix" setup = [Setup] begin
# uniform weights for simple mesh
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
mesh = SimpleMesh(points, connec)
- L = laplacematrix(mesh, weights=:uniform)
+ L = laplacematrix(mesh, kind=:uniform)
@test L == [
-1 1/3 1/3 0 1/3
1/3 -1 0 1/3 1/3
@@ -12,23 +12,38 @@
1/4 1/4 1/4 1/4 -1
]
+ # cotangent weights for simple mesh
+ L = laplacematrix(mesh, kind=:cotangent)
+ @test size(L) == (5, 5)
+
# cotangent weights only defined for triangle meshes
- points = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
+ points = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
connec = connect.([(1, 2, 3, 4)], Quadrangle)
mesh = SimpleMesh(points, connec)
- @test_throws AssertionError laplacematrix(mesh, weights=:cotangent)
+ @test_throws AssertionError laplacematrix(mesh, kind=:cotangent)
- # full Laplace-Beltrami operator
- sphere = Sphere(P3(0, 0, 0), T(1))
- mesh = simplexify(sphere)
- L = laplacematrix(mesh)
+ # uniform weights for Cartesian grid
+ grid = CartesianGrid(10, 10)
+ L = laplacematrix(grid, kind=:uniform)
+ @test size(L) == (11 * 11, 11 * 11)
+ grid = CartesianGrid(10, 10, 10)
+ L = laplacematrix(grid, kind=:uniform)
+ @test size(L) == (11 * 11 * 11, 11 * 11 * 11)
+end
+
+@testitem "Measure matrix" setup = [Setup] begin
+ # measure matrix of simple mesh
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
M = measurematrix(mesh)
- @test issymmetric(L)
- @test issparse(L)
+ @test size(M) == (5, 5)
@test isdiag(M)
+end
+@testitem "Adjacency matrix" setup = [Setup] begin
# adjacency of CartesianGrid
- grid = CartesianGrid{T}(100, 100)
+ grid = cartgrid(100, 100)
A = adjacencymatrix(grid)
d = sum(A, dims=2)
@test size(A) == (10000, 10000)
@@ -37,11 +52,32 @@
@test minimum(d) == 2
@test maximum(d) == 4
@test length(findall(==(2), d)) == 4
+ A = adjacencymatrix(grid, rank=0)
+ @test size(A) == (101 * 101, 101 * 101)
# adjacency of SimpleMesh
- points = P2[(0, 0), (1, -1), (1, 1), (2, -1), (2, 1)]
+ points = cart.([(0, 0), (1, -1), (1, 1), (2, -1), (2, 1)])
connec = connect.([(1, 2, 3), (3, 2, 4, 5)])
mesh = SimpleMesh(points, connec, relations=true)
A = adjacencymatrix(mesh)
@test A == [0 1; 1 0]
+ A = adjacencymatrix(mesh, rank=0)
+ @test A == [
+ 0 1 1 0 0
+ 1 0 1 1 0
+ 1 1 0 0 1
+ 0 1 0 0 1
+ 0 0 1 1 0
+ ]
+end
+
+@testitem "Misc matrix" setup = [Setup] begin
+ # full Laplace-Beltrami operator
+ sphere = Sphere(cart(0, 0, 0))
+ mesh = simplexify(sphere)
+ L = laplacematrix(mesh)
+ M = measurematrix(mesh)
+ @test issymmetric(L)
+ @test issparse(L)
+ @test isdiag(M)
end
diff --git a/test/merging.jl b/test/merging.jl
index da108d4d6..b897a1404 100644
--- a/test/merging.jl
+++ b/test/merging.jl
@@ -1,12 +1,12 @@
-@testset "Merging" begin
- s = Sphere(P3(0, 0, 0), T(1))
+@testitem "Merging" setup = [Setup] begin
+ s = Sphere(cart(0, 0, 0), T(1))
c = CylinderSurface(T(1))
m = merge(s, c)
@test m isa Multi
@test eltype(parent(m)) <: Primitive
- s = Sphere(P3(0, 0, 0), T(1))
- b = Box(P3(0, 0, 0), P3(1, 1, 1))
+ s = Sphere(cart(0, 0, 0), T(1))
+ b = Box(cart(0, 0, 0), cart(1, 1, 1))
ms = Multi([s])
mb = Multi([b])
@test merge(ms, b) == merge(ms, mb) == merge(s, mb)
@@ -14,8 +14,8 @@
@test m isa Multi
@test eltype(parent(m)) <: Primitive
- m1 = SimpleMesh(rand(P3, 3), [connect((1, 2, 3))])
- m2 = SimpleMesh(rand(P3, 4), [connect((1, 2, 3, 4))])
+ m1 = SimpleMesh(randpoint3(3), [connect((1, 2, 3))])
+ m2 = SimpleMesh(randpoint3(4), [connect((1, 2, 3, 4))])
m = merge(m1, m2)
@test m isa Mesh
@test eltype(m) <: Ngon
diff --git a/test/mesh.jl b/test/mesh.jl
deleted file mode 100644
index e842e00a1..000000000
--- a/test/mesh.jl
+++ /dev/null
@@ -1,738 +0,0 @@
-@testset "Meshes" begin
- @testset "CartesianGrid" begin
- grid = CartesianGrid{T}(100)
- @test embeddim(grid) == 1
- @test coordtype(grid) == T
- @test size(grid) == (100,)
- @test minimum(grid) == P1(0)
- @test maximum(grid) == P1(100)
- @test extrema(grid) == (P1(0), P1(100))
- @test spacing(grid) == T.((1,))
- @test nelements(grid) == 100
- @test eltype(grid) <: Segment{1,T}
- @test measure(grid) ≈ T(100)
- @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
- @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
- @test grid[1] == Segment(P1(0), P1(1))
- @test grid[100] == Segment(P1(99), P1(100))
-
- grid = CartesianGrid{T}(200, 100)
- @test embeddim(grid) == 2
- @test coordtype(grid) == T
- @test size(grid) == (200, 100)
- @test minimum(grid) == P2(0, 0)
- @test maximum(grid) == P2(200, 100)
- @test extrema(grid) == (P2(0, 0), P2(200, 100))
- @test spacing(grid) == T.((1, 1))
- @test nelements(grid) == 200 * 100
- @test eltype(grid) <: Quadrangle{2,T}
- @test measure(grid) ≈ T(200 * 100)
- @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
- @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
- @test grid[1, 1] == grid[1]
- @test grid[200, 100] == grid[20000]
-
- grid = CartesianGrid((200, 100, 50), T.((0, 0, 0)), T.((1, 1, 1)))
- @test embeddim(grid) == 3
- @test coordtype(grid) == T
- @test size(grid) == (200, 100, 50)
- @test minimum(grid) == P3(0, 0, 0)
- @test maximum(grid) == P3(200, 100, 50)
- @test extrema(grid) == (P3(0, 0, 0), P3(200, 100, 50))
- @test spacing(grid) == T.((1, 1, 1))
- @test nelements(grid) == 200 * 100 * 50
- @test eltype(grid) <: Hexahedron{3,T}
- @test measure(grid) ≈ T(200 * 100 * 50)
- @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
- @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
- @test grid[1, 1, 1] == grid[1]
- @test grid[200, 100, 50] == grid[1000000]
-
- grid = CartesianGrid(T.((0, 0, 0)), T.((1, 1, 1)), T.((0.1, 0.1, 0.1)))
- @test embeddim(grid) == 3
- @test coordtype(grid) == T
- @test size(grid) == (10, 10, 10)
- @test minimum(grid) == P3(0, 0, 0)
- @test maximum(grid) == P3(1, 1, 1)
- @test spacing(grid) == T.((0.1, 0.1, 0.1))
-
- grid = CartesianGrid(T.((-1.0, -1.0)), T.((1.0, 1.0)), dims=(200, 100))
- @test embeddim(grid) == 2
- @test coordtype(grid) == T
- @test size(grid) == (200, 100)
- @test minimum(grid) == P2(-1.0, -1.0)
- @test maximum(grid) == P2(1.0, 1.0)
- @test spacing(grid) == T.((2 / 200, 2 / 100))
- @test nelements(grid) == 200 * 100
- @test eltype(grid) <: Quadrangle{2,T}
-
- grid = CartesianGrid((20, 10, 5), T.((0, 0, 0)), T.((5, 5, 5)))
- @test embeddim(grid) == 3
- @test coordtype(grid) == T
- @test size(grid) == (20, 10, 5)
- @test minimum(grid) == P3(0, 0, 0)
- @test maximum(grid) == P3(100, 50, 25)
- @test extrema(grid) == (P3(0, 0, 0), P3(100, 50, 25))
- @test spacing(grid) == T.((5, 5, 5))
- @test nelements(grid) == 20 * 10 * 5
- @test eltype(grid) <: Hexahedron{3,T}
- @test vertices(grid[1]) ==
- (P3(0, 0, 0), P3(5, 0, 0), P3(5, 5, 0), P3(0, 5, 0), P3(0, 0, 5), P3(5, 0, 5), P3(5, 5, 5), P3(0, 5, 5))
- @test all(centroid(grid, i) == centroid(grid[i]) for i in 1:nelements(grid))
-
- # constructor with offset
- grid = CartesianGrid((10, 10), T.((1.0, 1.0)), T.((1.0, 1.0)), (2, 2))
- @test embeddim(grid) == 2
- @test coordtype(grid) == T
- @test size(grid) == (10, 10)
- @test minimum(grid) == P2(0.0, 0.0)
- @test maximum(grid) == P2(10.0, 10.0)
- @test spacing(grid) == T.((1, 1))
- @test nelements(grid) == 10 * 10
- @test eltype(grid) <: Quadrangle{2,T}
-
- # indexing into a subgrid
- grid = CartesianGrid{T}(10, 10)
- sub = grid[1:2, 1:2]
- @test size(sub) == (2, 2)
- @test spacing(sub) == spacing(grid)
- @test minimum(sub) == minimum(grid)
- @test maximum(sub) == P2(2, 2)
- sub = grid[1:1, 2:3]
- @test size(sub) == (1, 2)
- @test spacing(sub) == spacing(grid)
- @test minimum(sub) == P2(0, 1)
- @test maximum(sub) == P2(1, 3)
- sub = grid[2:4, 3:7]
- @test size(sub) == (3, 5)
- @test spacing(sub) == spacing(grid)
- @test minimum(sub) == P2(1, 2)
- @test maximum(sub) == P2(4, 7)
- grid = CartesianGrid(P2(1, 1), P2(11, 11), dims=(10, 10))
- sub = grid[2:4, 3:7]
- @test size(sub) == (3, 5)
- @test spacing(sub) == spacing(grid)
- @test minimum(sub) == P2(2, 3)
- @test maximum(sub) == P2(5, 8)
- sub = grid[2, 3:7]
- @test size(sub) == (1, 5)
- @test spacing(sub) == spacing(grid)
- @test minimum(sub) == P2(2, 3)
- @test maximum(sub) == P2(3, 8)
- sub = grid[:, 3:7]
- @test size(sub) == (10, 5)
- @test spacing(sub) == spacing(grid)
- @test minimum(sub) == P2(1, 3)
- @test maximum(sub) == P2(11, 8)
- @test_throws BoundsError grid[3:11, :]
-
- # subgrid with comparable vertices of grid
- grid = CartesianGrid((10, 10), P2(0.0, 0.0), T.((1.2, 1.2)))
- sub = grid[2:4, 5:7]
- @test sub == CartesianGrid((3, 3), P2(0.0, 0.0), T.((1.2, 1.2)), (0, -3))
- ind = reshape(reshape(1:121, 11, 11)[2:5, 5:8], :)
- @test vertices(grid)[ind] == vertices(sub)
-
- # subgrid from Cartesian ranges
- grid = CartesianGrid{T}(10, 10)
- sub1 = grid[1:2, 4:6]
- sub2 = grid[CartesianIndex(1, 4):CartesianIndex(2, 6)]
- @test sub1 == sub2
-
- grid = CartesianGrid{T}(200, 100)
- @test centroid(grid, 1) == P2(0.5, 0.5)
- @test centroid(grid, 2) == P2(1.5, 0.5)
- @test centroid(grid, 200 * 100) == P2(199.5, 99.5)
- @test nelements(grid) == 200 * 100
- @test eltype(grid) <: Quadrangle{2,T}
- @test grid[1] == Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test grid[2] == Quadrangle(P2(1, 0), P2(2, 0), P2(2, 1), P2(1, 1))
-
- # expand CartesianGrid with comparable vertices
- grid = CartesianGrid((10, 10), P2(0.0, 0.0), T.((1.0, 1.0)))
- left, right = (1, 1), (1, 1)
- newdim = size(grid) .+ left .+ right
- newoffset = offset(grid) .+ left
- grid2 = CartesianGrid(newdim, minimum(grid), spacing(grid), newoffset)
- @test issubset(vertices(grid), vertices(grid2))
-
- # GridTopology from CartesianGrid
- grid = CartesianGrid{T}(5, 5)
- topo = topology(grid)
- vs = vertices(grid)
- for i in 1:nelements(grid)
- inds = indices(element(topo, i))
- @test vs[[inds...]] == pointify(element(grid, i))
- end
-
- # convert topology
- grid = CartesianGrid{T}(10, 10)
- mesh = topoconvert(HalfEdgeTopology, grid)
- @test mesh isa SimpleMesh
- @test nvertices(mesh) == 121
- @test nelements(mesh) == 100
- @test eltype(mesh) <: Quadrangle
-
- # single vertex access
- grid = CartesianGrid{T}(10, 10)
- @test vertex(grid, 1) == P2(0, 0)
- @test vertex(grid, 121) == P2(10, 10)
-
- # xyz
- g1D = CartesianGrid{T}(10)
- g2D = CartesianGrid{T}(10, 10)
- g3D = CartesianGrid{T}(10, 10, 10)
- @test Meshes.xyz(g1D) == (T.(0:10),)
- @test Meshes.xyz(g2D) == (T.(0:10), T.(0:10))
- @test Meshes.xyz(g3D) == (T.(0:10), T.(0:10), T.(0:10))
-
- # XYZ
- g1D = CartesianGrid{T}(10)
- g2D = CartesianGrid{T}(10, 10)
- g3D = CartesianGrid{T}(10, 10, 10)
- x = T.(0:10)
- y = T.(0:10)'
- z = reshape(T.(0:10), 1, 1, 11)
- @test Meshes.XYZ(g1D) == (x,)
- @test Meshes.XYZ(g2D) == (repeat(x, 1, 11), repeat(y, 11, 1))
- @test Meshes.XYZ(g3D) == (repeat(x, 1, 11, 11), repeat(y, 11, 1, 11), repeat(z, 11, 11, 1))
-
- # units
- Q = typeof(zero(T) * u"m")
- grid = CartesianGrid{Q}(10, 10)
- o = minimum(grid)
- s = spacing(grid)
- @test unit(coordtype(o)) == u"m"
- @test Unitful.numtype(coordtype(o)) === T
- @test unit(eltype(s)) == u"m"
- @test Unitful.numtype(eltype(s)) === T
-
- grid = CartesianGrid{T}(200, 100)
- if T == Float32
- @test sprint(show, MIME"text/plain"(), grid) == """
- 200×100 CartesianGrid{2,Float32}
- minimum: Point(0.0f0, 0.0f0)
- maximum: Point(200.0f0, 100.0f0)
- spacing: (1.0f0, 1.0f0)"""
- elseif T == Float64
- @test sprint(show, MIME"text/plain"(), grid) == """
- 200×100 CartesianGrid{2,Float64}
- minimum: Point(0.0, 0.0)
- maximum: Point(200.0, 100.0)
- spacing: (1.0, 1.0)"""
- end
- end
-
- @testset "RectilinearGrid" begin
- x = range(zero(T), stop=one(T), length=6)
- y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
- grid = RectilinearGrid(x, y)
- @test embeddim(grid) == 2
- @test coordtype(grid) == T
- @test size(grid) == (5, 5)
- @test minimum(grid) == P2(0, 0)
- @test maximum(grid) == P2(1, 1)
- @test extrema(grid) == (P2(0, 0), P2(1, 1))
- @test nelements(grid) == 25
- @test eltype(grid) <: Quadrangle{2,T}
- @test measure(grid) ≈ T(1)
- @test centroid(grid, 1) ≈ P2(0.1, 0.05)
- @test centroid(grid[1]) ≈ P2(0.1, 0.05)
- @test centroid(grid, 2) ≈ P2(0.3, 0.05)
- @test centroid(grid[2]) ≈ P2(0.3, 0.05)
- @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
- @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
- @test grid[1, 1] == grid[1]
- @test grid[5, 5] == grid[25]
- sub = grid[2:4, 3:5]
- @test size(sub) == (3, 3)
- @test minimum(sub) == P2(0.2, 0.3)
- @test maximum(sub) == P2(0.8, 1.0)
- sub = grid[2, 3:5]
- @test size(sub) == (1, 3)
- @test minimum(sub) == P2(0.2, 0.3)
- @test maximum(sub) == P2(0.4, 1.0)
- sub = grid[:, 3:5]
- @test size(sub) == (5, 3)
- @test minimum(sub) == P2(0.0, 0.3)
- @test maximum(sub) == P2(1.0, 1.0)
- @test_throws BoundsError grid[2:6, :]
- @test Meshes.xyz(grid) == (x, y)
- @test Meshes.XYZ(grid) == (repeat(x, 1, 6), repeat(y', 6, 1))
-
- # single vertex access
- grid = RectilinearGrid(T.(0:10), T.(0:10))
- @test vertex(grid, 1) == P2(0, 0)
- @test vertex(grid, 121) == P2(10, 10)
-
- # conversion
- cg = CartesianGrid{T}(10, 10)
- rg = convert(RectilinearGrid, cg)
- @test nvertices(rg) == nvertices(cg)
- @test nelements(rg) == nelements(cg)
- @test topology(rg) == topology(cg)
- @test vertices(rg) == vertices(cg)
-
- x = range(zero(T), stop=one(T), length=6)
- y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
- grid = RectilinearGrid(x, y)
- @test sprint(show, grid) == "5×5 RectilinearGrid{2,$T}"
- if T == Float32
- @test sprint(show, MIME"text/plain"(), grid) == """
- 5×5 RectilinearGrid{2,Float32}
- 36 vertices
- ├─ Point(0.0f0, 0.0f0)
- ├─ Point(0.2f0, 0.0f0)
- ├─ Point(0.4f0, 0.0f0)
- ├─ Point(0.6f0, 0.0f0)
- ├─ Point(0.8f0, 0.0f0)
- ⋮
- ├─ Point(0.2f0, 1.0f0)
- ├─ Point(0.4f0, 1.0f0)
- ├─ Point(0.6f0, 1.0f0)
- ├─ Point(0.8f0, 1.0f0)
- └─ Point(1.0f0, 1.0f0)
- 25 elements
- ├─ Quadrangle(1, 2, 8, 7)
- ├─ Quadrangle(2, 3, 9, 8)
- ├─ Quadrangle(3, 4, 10, 9)
- ├─ Quadrangle(4, 5, 11, 10)
- ├─ Quadrangle(5, 6, 12, 11)
- ⋮
- ├─ Quadrangle(25, 26, 32, 31)
- ├─ Quadrangle(26, 27, 33, 32)
- ├─ Quadrangle(27, 28, 34, 33)
- ├─ Quadrangle(28, 29, 35, 34)
- └─ Quadrangle(29, 30, 36, 35)"""
- elseif T == Float64
- @test sprint(show, MIME"text/plain"(), grid) == """
- 5×5 RectilinearGrid{2,Float64}
- 36 vertices
- ├─ Point(0.0, 0.0)
- ├─ Point(0.2, 0.0)
- ├─ Point(0.4, 0.0)
- ├─ Point(0.6, 0.0)
- ├─ Point(0.8, 0.0)
- ⋮
- ├─ Point(0.2, 1.0)
- ├─ Point(0.4, 1.0)
- ├─ Point(0.6, 1.0)
- ├─ Point(0.8, 1.0)
- └─ Point(1.0, 1.0)
- 25 elements
- ├─ Quadrangle(1, 2, 8, 7)
- ├─ Quadrangle(2, 3, 9, 8)
- ├─ Quadrangle(3, 4, 10, 9)
- ├─ Quadrangle(4, 5, 11, 10)
- ├─ Quadrangle(5, 6, 12, 11)
- ⋮
- ├─ Quadrangle(25, 26, 32, 31)
- ├─ Quadrangle(26, 27, 33, 32)
- ├─ Quadrangle(27, 28, 34, 33)
- ├─ Quadrangle(28, 29, 35, 34)
- └─ Quadrangle(29, 30, 36, 35)"""
- end
- end
-
- @testset "StructuredGrid" begin
- X = repeat(range(zero(T), stop=one(T), length=6), 1, 6)
- Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
- grid = StructuredGrid(X, Y)
- @test embeddim(grid) == 2
- @test coordtype(grid) == T
- @test size(grid) == (5, 5)
- @test minimum(grid) == P2(0, 0)
- @test maximum(grid) == P2(1, 1)
- @test extrema(grid) == (P2(0, 0), P2(1, 1))
- @test nelements(grid) == 25
- @test eltype(grid) <: Quadrangle{2,T}
- @test measure(grid) ≈ T(1)
- @test centroid(grid, 1) ≈ P2(0.1, 0.05)
- @test centroid(grid[1]) ≈ P2(0.1, 0.05)
- @test centroid(grid, 2) ≈ P2(0.3, 0.05)
- @test centroid(grid[2]) ≈ P2(0.3, 0.05)
- @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
- @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
- @test grid[1, 1] == grid[1]
- @test grid[5, 5] == grid[25]
- sub = grid[2:4, 3:5]
- @test size(sub) == (3, 3)
- @test minimum(sub) == P2(0.2, 0.3)
- @test maximum(sub) == P2(0.8, 1.0)
- sub = grid[2, 3:5]
- @test size(sub) == (1, 3)
- @test minimum(sub) == P2(0.2, 0.3)
- @test maximum(sub) == P2(0.4, 1.0)
- sub = grid[:, 3:5]
- @test size(sub) == (5, 3)
- @test minimum(sub) == P2(0.0, 0.3)
- @test maximum(sub) == P2(1.0, 1.0)
- @test_throws BoundsError grid[2:6, :]
- @test Meshes.XYZ(grid) == (X, Y)
-
- # conversion
- cg = CartesianGrid{T}(10, 10)
- sg = convert(StructuredGrid, cg)
- @test nvertices(sg) == nvertices(cg)
- @test nelements(sg) == nelements(cg)
- @test topology(sg) == topology(cg)
- @test vertices(sg) == vertices(cg)
-
- rg = RectilinearGrid(T.(0:10), T.(0:10))
- sg = convert(StructuredGrid, rg)
- @test nvertices(sg) == nvertices(rg)
- @test nelements(sg) == nelements(rg)
- @test topology(sg) == topology(rg)
- @test vertices(sg) == vertices(rg)
-
- X = repeat(range(zero(T), stop=one(T), length=6), 1, 6)
- Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
- grid = StructuredGrid(X, Y)
- @test sprint(show, grid) == "5×5 StructuredGrid{2,$T}"
- if T == Float32
- @test sprint(show, MIME"text/plain"(), grid) == """
- 5×5 StructuredGrid{2,Float32}
- 36 vertices
- ├─ Point(0.0f0, 0.0f0)
- ├─ Point(0.2f0, 0.0f0)
- ├─ Point(0.4f0, 0.0f0)
- ├─ Point(0.6f0, 0.0f0)
- ├─ Point(0.8f0, 0.0f0)
- ⋮
- ├─ Point(0.2f0, 1.0f0)
- ├─ Point(0.4f0, 1.0f0)
- ├─ Point(0.6f0, 1.0f0)
- ├─ Point(0.8f0, 1.0f0)
- └─ Point(1.0f0, 1.0f0)
- 25 elements
- ├─ Quadrangle(1, 2, 8, 7)
- ├─ Quadrangle(2, 3, 9, 8)
- ├─ Quadrangle(3, 4, 10, 9)
- ├─ Quadrangle(4, 5, 11, 10)
- ├─ Quadrangle(5, 6, 12, 11)
- ⋮
- ├─ Quadrangle(25, 26, 32, 31)
- ├─ Quadrangle(26, 27, 33, 32)
- ├─ Quadrangle(27, 28, 34, 33)
- ├─ Quadrangle(28, 29, 35, 34)
- └─ Quadrangle(29, 30, 36, 35)"""
- elseif T == Float64
- @test sprint(show, MIME"text/plain"(), grid) == """
- 5×5 StructuredGrid{2,Float64}
- 36 vertices
- ├─ Point(0.0, 0.0)
- ├─ Point(0.2, 0.0)
- ├─ Point(0.4, 0.0)
- ├─ Point(0.6, 0.0)
- ├─ Point(0.8, 0.0)
- ⋮
- ├─ Point(0.2, 1.0)
- ├─ Point(0.4, 1.0)
- ├─ Point(0.6, 1.0)
- ├─ Point(0.8, 1.0)
- └─ Point(1.0, 1.0)
- 25 elements
- ├─ Quadrangle(1, 2, 8, 7)
- ├─ Quadrangle(2, 3, 9, 8)
- ├─ Quadrangle(3, 4, 10, 9)
- ├─ Quadrangle(4, 5, 11, 10)
- ├─ Quadrangle(5, 6, 12, 11)
- ⋮
- ├─ Quadrangle(25, 26, 32, 31)
- ├─ Quadrangle(26, 27, 33, 32)
- ├─ Quadrangle(27, 28, 34, 33)
- ├─ Quadrangle(28, 29, 35, 34)
- └─ Quadrangle(29, 30, 36, 35)"""
- end
- end
-
- @testset "SimpleMesh" begin
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- mesh = SimpleMesh(points, connec)
- triangles =
- Triangle.([
- (P2(0.0, 0.0), P2(1.0, 0.0), P2(0.5, 0.5)),
- (P2(1.0, 0.0), P2(1.0, 1.0), P2(0.5, 0.5)),
- (P2(1.0, 1.0), P2(0.0, 1.0), P2(0.5, 0.5)),
- (P2(0.0, 1.0), P2(0.0, 0.0), P2(0.5, 0.5))
- ])
- @test vertices(mesh) == points
- @test collect(faces(mesh, 2)) == triangles
- @test collect(elements(mesh)) == triangles
- @test nelements(mesh) == 4
- for i in 1:length(triangles)
- @test mesh[i] == triangles[i]
- end
- @test eltype(mesh) <: Triangle{2,T}
- @test measure(mesh) ≈ T(1)
- @test area(mesh) ≈ T(1)
- @test extrema(mesh) == (P2(0, 0), P2(1, 1))
-
- # test constructors
- coords = [T.((0, 0)), T.((1, 0)), T.((0, 1)), T.((1, 1)), T.((0.5, 0.5))]
- connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- mesh = SimpleMesh(coords, SimpleTopology(connec))
- @test eltype(mesh) <: Triangle{2,T}
- @test topology(mesh) isa SimpleTopology
- @test nvertices(mesh) == 5
- @test nelements(mesh) == 4
- mesh = SimpleMesh(coords, connec)
- @test eltype(mesh) <: Triangle{2,T}
- @test topology(mesh) isa SimpleTopology
- @test nvertices(mesh) == 5
- @test nelements(mesh) == 4
- mesh = SimpleMesh(coords, connec, relations=true)
- @test eltype(mesh) <: Triangle{2,T}
- @test topology(mesh) isa HalfEdgeTopology
- @test nvertices(mesh) == 5
- @test nelements(mesh) == 4
-
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.5), (0.75, 0.5)]
- Δs = connect.([(3, 1, 5), (4, 6, 2)], Triangle)
- □s = connect.([(1, 2, 6, 5), (5, 6, 4, 3)], Quadrangle)
- mesh = SimpleMesh(points, [Δs; □s])
- elms = [
- Triangle(P2(0.0, 1.0), P2(0.0, 0.0), P2(0.25, 0.5)),
- Triangle(P2(1.0, 1.0), P2(0.75, 0.5), P2(1.0, 0.0)),
- Quadrangle(P2(0.0, 0.0), P2(1.0, 0.0), P2(0.75, 0.5), P2(0.25, 0.5)),
- Quadrangle(P2(0.25, 0.5), P2(0.75, 0.5), P2(1.0, 1.0), P2(0.0, 1.0))
- ]
- @test collect(elements(mesh)) == elms
- @test nelements(mesh) == 4
- for i in 1:length(elms)
- @test mesh[i] == elms[i]
- end
- @test eltype(mesh) <: Polygon{2,T}
-
- # test for https://github.com/JuliaGeometry/Meshes.jl/issues/177
- points = P3[(0, 0, 0), (1, 0, 0), (1, 1, 1), (0, 1, 0)]
- connec = connect.([(1, 2, 3, 4), (3, 4, 1)], [Tetrahedron, Triangle])
- mesh = SimpleMesh(points, connec)
- topo = topology(mesh)
- @test collect(faces(topo, 2)) == [connect((3, 4, 1), Triangle)]
- @test collect(faces(topo, 3)) == [connect((1, 2, 3, 4), Tetrahedron)]
-
- # test for https://github.com/JuliaGeometry/Meshes.jl/issues/187
- points = P3[(0, 0, 0), (1, 0, 0), (1, 1, 1), (0, 1, 0)]
- connec = connect.([(1, 2, 3, 4), (3, 4, 1)], [Tetrahedron, Triangle])
- mesh = SimpleMesh(points[4:-1:1], connec)
- meshvp = SimpleMesh(view(points, 4:-1:1), connec)
- @test mesh == meshvp
-
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- mesh = SimpleMesh(points, connec)
- bytes = @allocated faces(mesh, 2)
- @test bytes < 100
- cells = faces(mesh, 2)
- bytes = @allocated collect(cells)
- @test bytes < 800
-
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- mesh = SimpleMesh(points, connec)
- @test centroid(mesh, 1) == centroid(Triangle(P2(0, 0), P2(1, 0), P2(0.5, 0.5)))
- @test centroid(mesh, 2) == centroid(Triangle(P2(1, 0), P2(1, 1), P2(0.5, 0.5)))
- @test centroid(mesh, 3) == centroid(Triangle(P2(1, 1), P2(0, 1), P2(0.5, 0.5)))
- @test centroid(mesh, 4) == centroid(Triangle(P2(0, 1), P2(0, 0), P2(0.5, 0.5)))
-
- # merge operation with 2D geometries
- mesh₁ = SimpleMesh(P2[(0, 0), (1, 0), (0, 1)], connect.([(1, 2, 3)]))
- mesh₂ = SimpleMesh(P2[(1, 0), (1, 1), (0, 1)], connect.([(1, 2, 3)]))
- mesh = merge(mesh₁, mesh₂)
- @test vertices(mesh) == [vertices(mesh₁); vertices(mesh₂)]
- @test collect(elements(topology(mesh))) == connect.([(1, 2, 3), (4, 5, 6)])
-
- # merge operation with 3D geometries
- mesh₁ = SimpleMesh(P3[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)], connect.([(1, 2, 3, 4)], Tetrahedron))
- mesh₂ = SimpleMesh(P3[(1, 0, 0), (1, 1, 0), (0, 1, 0), (1, 1, 1)], connect.([(1, 2, 3, 4)], Tetrahedron))
- mesh = merge(mesh₁, mesh₂)
- @test vertices(mesh) == [vertices(mesh₁); vertices(mesh₂)]
- @test collect(elements(topology(mesh))) == connect.([(1, 2, 3, 4), (5, 6, 7, 8)], Tetrahedron)
-
- # convert any mesh to SimpleMesh
- grid = CartesianGrid{T}(10, 10)
- mesh = convert(SimpleMesh, grid)
- @test mesh isa SimpleMesh
- @test topology(mesh) == GridTopology(10, 10)
- @test nvertices(mesh) == 121
- @test nelements(mesh) == 100
- @test eltype(mesh) <: Quadrangle
- # grid interface
- @test size(mesh) == (10, 10)
- @test minimum(mesh) == P2(0, 0)
- @test maximum(mesh) == P2(10, 10)
- @test extrema(mesh) == (P2(0, 0), P2(10, 10))
- @test vertex(mesh, 1) == vertex(mesh, ntuple(i -> 1, embeddim(mesh)))
- @test vertex(mesh, nvertices(mesh)) == vertex(mesh, size(mesh) .+ 1)
- @test mesh[1, 1] == mesh[1]
- @test mesh[10, 10] == mesh[100]
- sub = mesh[2:4, 3:7]
- @test size(sub) == (3, 5)
- @test minimum(sub) == P2(1, 2)
- @test maximum(sub) == P2(4, 7)
- sub = mesh[2, 3:7]
- @test size(sub) == (1, 5)
- @test minimum(sub) == P2(1, 2)
- @test maximum(sub) == P2(2, 7)
- sub = mesh[:, 3:7]
- @test size(sub) == (10, 5)
- @test minimum(sub) == P2(0, 2)
- @test maximum(sub) == P2(10, 7)
- @test_throws BoundsError grid[3:11, :]
-
- # test for https://github.com/JuliaGeometry/Meshes.jl/issues/261
- points = rand(P2, 5)
- connec = [connect((1, 2, 3))]
- mesh = SimpleMesh(points, connec)
- @test nvertices(mesh) == length(vertices(mesh)) == 5
-
- # single vertex access
- points = rand(P2, 5)
- connec = [connect((1, 2, 3))]
- mesh = SimpleMesh(points, connec)
- @test vertex(mesh, 1) == points[1]
- @test vertex(mesh, 2) == points[2]
- @test vertex(mesh, 3) == points[3]
- @test vertex(mesh, 4) == points[4]
- @test vertex(mesh, 5) == points[5]
-
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- mesh = SimpleMesh(points, connec)
- if T == Float32
- @test sprint(show, MIME"text/plain"(), mesh) == """
- 4 SimpleMesh{2,Float32}
- 5 vertices
- ├─ Point(0.0f0, 0.0f0)
- ├─ Point(1.0f0, 0.0f0)
- ├─ Point(0.0f0, 1.0f0)
- ├─ Point(1.0f0, 1.0f0)
- └─ Point(0.5f0, 0.5f0)
- 4 elements
- ├─ Triangle(1, 2, 5)
- ├─ Triangle(2, 4, 5)
- ├─ Triangle(4, 3, 5)
- └─ Triangle(3, 1, 5)"""
- elseif T == Float64
- @test sprint(show, MIME"text/plain"(), mesh) == """
- 4 SimpleMesh{2,Float64}
- 5 vertices
- ├─ Point(0.0, 0.0)
- ├─ Point(1.0, 0.0)
- ├─ Point(0.0, 1.0)
- ├─ Point(1.0, 1.0)
- └─ Point(0.5, 0.5)
- 4 elements
- ├─ Triangle(1, 2, 5)
- ├─ Triangle(2, 4, 5)
- ├─ Triangle(4, 3, 5)
- └─ Triangle(3, 1, 5)"""
- end
- end
-
- @testset "TransformedMesh" begin
- grid = CartesianGrid{T}(10, 10)
- rgrid = convert(RectilinearGrid, grid)
- sgrid = convert(StructuredGrid, grid)
- mesh = convert(SimpleMesh, grid)
- trans = Identity()
- tmesh = TransformedMesh(mesh, trans)
- @test parent(tmesh) === mesh
- @test Meshes.transform(tmesh) === trans
- @test TransformedMesh(grid, trans) == grid
- @test TransformedMesh(rgrid, trans) == rgrid
- @test TransformedMesh(sgrid, trans) == sgrid
- @test TransformedMesh(mesh, trans) == mesh
- trans = Translate(T(10), T(10)) → Translate(T(-10), T(-10))
- @test TransformedMesh(grid, trans) == grid
- @test TransformedMesh(rgrid, trans) == rgrid
- @test TransformedMesh(sgrid, trans) == sgrid
- @test TransformedMesh(mesh, trans) == mesh
- trans1 = Translate(T(10), T(10))
- trans2 = Translate(T(-10), T(-10))
- @test TransformedMesh(TransformedMesh(grid, trans1), trans2) == TransformedMesh(grid, trans1 → trans2)
- # grid interface
- trans = Identity()
- tgrid = TransformedMesh(grid, trans)
- @test tgrid isa TransformedGrid
- @test size(tgrid) == (10, 10)
- @test minimum(tgrid) == P2(0, 0)
- @test maximum(tgrid) == P2(10, 10)
- @test extrema(tgrid) == (P2(0, 0), P2(10, 10))
- @test vertex(tgrid, 1) == vertex(tgrid, ntuple(i -> 1, embeddim(tgrid)))
- @test vertex(tgrid, nvertices(tgrid)) == vertex(tgrid, size(tgrid) .+ 1)
- @test tgrid[1, 1] == tgrid[1]
- @test tgrid[10, 10] == tgrid[100]
- sub = tgrid[2:4, 3:7]
- @test size(sub) == (3, 5)
- @test minimum(sub) == P2(1, 2)
- @test maximum(sub) == P2(4, 7)
- sub = tgrid[2, 3:7]
- @test size(sub) == (1, 5)
- @test minimum(sub) == P2(1, 2)
- @test maximum(sub) == P2(2, 7)
- sub = tgrid[:, 3:7]
- @test size(sub) == (10, 5)
- @test minimum(sub) == P2(0, 2)
- @test maximum(sub) == P2(10, 7)
- @test sprint(show, tgrid) == "10×10 TransformedGrid{2,$T}"
- if T == Float32
- @test sprint(show, MIME"text/plain"(), tgrid) == """
- 10×10 TransformedGrid{2,Float32}
- 121 vertices
- ├─ Point(0.0f0, 0.0f0)
- ├─ Point(1.0f0, 0.0f0)
- ├─ Point(2.0f0, 0.0f0)
- ├─ Point(3.0f0, 0.0f0)
- ├─ Point(4.0f0, 0.0f0)
- ⋮
- ├─ Point(6.0f0, 10.0f0)
- ├─ Point(7.0f0, 10.0f0)
- ├─ Point(8.0f0, 10.0f0)
- ├─ Point(9.0f0, 10.0f0)
- └─ Point(10.0f0, 10.0f0)
- 100 elements
- ├─ Quadrangle(1, 2, 13, 12)
- ├─ Quadrangle(2, 3, 14, 13)
- ├─ Quadrangle(3, 4, 15, 14)
- ├─ Quadrangle(4, 5, 16, 15)
- ├─ Quadrangle(5, 6, 17, 16)
- ⋮
- ├─ Quadrangle(105, 106, 117, 116)
- ├─ Quadrangle(106, 107, 118, 117)
- ├─ Quadrangle(107, 108, 119, 118)
- ├─ Quadrangle(108, 109, 120, 119)
- └─ Quadrangle(109, 110, 121, 120)"""
- elseif T == Float64
- @test sprint(show, MIME"text/plain"(), tgrid) == """
- 10×10 TransformedGrid{2,Float64}
- 121 vertices
- ├─ Point(0.0, 0.0)
- ├─ Point(1.0, 0.0)
- ├─ Point(2.0, 0.0)
- ├─ Point(3.0, 0.0)
- ├─ Point(4.0, 0.0)
- ⋮
- ├─ Point(6.0, 10.0)
- ├─ Point(7.0, 10.0)
- ├─ Point(8.0, 10.0)
- ├─ Point(9.0, 10.0)
- └─ Point(10.0, 10.0)
- 100 elements
- ├─ Quadrangle(1, 2, 13, 12)
- ├─ Quadrangle(2, 3, 14, 13)
- ├─ Quadrangle(3, 4, 15, 14)
- ├─ Quadrangle(4, 5, 16, 15)
- ├─ Quadrangle(5, 6, 17, 16)
- ⋮
- ├─ Quadrangle(105, 106, 117, 116)
- ├─ Quadrangle(106, 107, 118, 117)
- ├─ Quadrangle(107, 108, 119, 118)
- ├─ Quadrangle(108, 109, 120, 119)
- └─ Quadrangle(109, 110, 121, 120)"""
- end
- @test_throws BoundsError grid[3:11, :]
- end
-end
diff --git a/test/meshes.jl b/test/meshes.jl
new file mode 100644
index 000000000..031354233
--- /dev/null
+++ b/test/meshes.jl
@@ -0,0 +1,1130 @@
+@testitem "RegularGrid" setup = [Setup] begin
+ grid = RegularGrid((10, 20), merc(0, 0), T.((1, 1)))
+ @test embeddim(grid) == 2
+ @test paramdim(grid) == 2
+ @test crs(grid) <: Mercator
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (10, 20)
+ @test minimum(grid) == merc(0, 0)
+ @test maximum(grid) == merc(10, 20)
+ @test extrema(grid) == (merc(0, 0), merc(10, 20))
+ @test spacing(grid) == (T(1) * u"m", T(1) * u"m")
+ @test nelements(grid) == 10 * 20
+ @test eltype(grid) <: Quadrangle
+ @test vertex(grid, 1) == vertex(grid, (1, 1))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, (11, 21))
+ @test centroid(grid, 1) == centroid(grid[1])
+ @test grid[1, 1] == grid[1]
+ @test grid[10, 20] == grid[200]
+
+ grid = RegularGrid((10, 20), latlon(0, 0), T.((1, 1)))
+ @test embeddim(grid) == 3
+ @test paramdim(grid) == 2
+ @test crs(grid) <: LatLon
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (10, 20)
+ @test minimum(grid) == latlon(0, 0)
+ @test maximum(grid) == latlon(10, 20)
+ @test extrema(grid) == (latlon(0, 0), latlon(10, 20))
+ @test spacing(grid) == (T(1) * u"°", T(1) * u"°")
+ @test nelements(grid) == 10 * 20
+ @test eltype(grid) <: Quadrangle
+ @test vertex(grid, 1) == vertex(grid, (1, 1))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, (11, 21))
+ @test centroid(grid, 1) == centroid(grid[1])
+ @test grid[1, 1] == grid[1]
+ @test grid[10, 20] == grid[200]
+
+ grid = RegularGrid((10, 20), Point(Polar(T(0), T(0))), T.((1, 1)))
+ @test embeddim(grid) == 2
+ @test paramdim(grid) == 2
+ @test crs(grid) <: Polar
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (10, 20)
+ @test minimum(grid) == Point(Polar(T(0), T(0)))
+ @test maximum(grid) == Point(Polar(T(10), T(20)))
+ @test extrema(grid) == (Point(Polar(T(0), T(0))), Point(Polar(T(10), T(20))))
+ @test spacing(grid) == (T(1) * u"m", T(1) * u"rad")
+ @test nelements(grid) == 10 * 20
+ @test eltype(grid) <: Quadrangle
+ @test vertex(grid, 1) == vertex(grid, (1, 1))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, (11, 21))
+ @test centroid(grid, 1) == centroid(grid[1])
+ @test grid[1, 1] == grid[1]
+ @test grid[10, 20] == grid[200]
+
+ grid = RegularGrid((10, 20, 30), Point(Cylindrical(T(0), T(0), T(0))), T.((1, 1, 1)))
+ @test embeddim(grid) == 3
+ @test paramdim(grid) == 3
+ @test crs(grid) <: Cylindrical
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (10, 20, 30)
+ @test minimum(grid) == Point(Cylindrical(T(0), T(0), T(0)))
+ @test maximum(grid) == Point(Cylindrical(T(10), T(20), T(30)))
+ @test extrema(grid) == (Point(Cylindrical(T(0), T(0), T(0))), Point(Cylindrical(T(10), T(20), T(30))))
+ @test spacing(grid) == (T(1) * u"m", T(1) * u"rad", T(1) * u"m")
+ @test nelements(grid) == 10 * 20 * 30
+ @test eltype(grid) <: Hexahedron
+ @test vertex(grid, 1) == vertex(grid, (1, 1, 1))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, (11, 21, 31))
+ @test centroid(grid, 1) == centroid(grid[1])
+ @test grid[1, 1, 1] == grid[1]
+ @test grid[10, 20, 30] == grid[6000]
+
+ # constructors with start and finish
+ grid = RegularGrid(merc(0, 0), merc(10, 10), T.((0.1, 0.1)))
+ @test size(grid) == (100, 100)
+ @test minimum(grid) == merc(0, 0)
+ @test maximum(grid) == merc(10, 10)
+ @test spacing(grid) == (T(0.1) * u"m", T(0.1) * u"m")
+
+ grid = RegularGrid(latlon(-50, 150), latlon(50, 30), T.((10, 12)))
+ @test size(grid) == (10, 20)
+ @test minimum(grid) == latlon(-50, 150)
+ @test maximum(grid) == latlon(50, 30)
+ @test spacing(grid) == (T(10) * u"°", T(12) * u"°")
+
+ grid = RegularGrid(merc(0, 0), merc(10, 10), dims=(100, 100))
+ @test size(grid) == (100, 100)
+ @test minimum(grid) == merc(0, 0)
+ @test maximum(grid) == merc(10, 10)
+ @test spacing(grid) == (T(0.1) * u"m", T(0.1) * u"m")
+
+ grid = RegularGrid(latlon(-50, 150), latlon(50, 30), dims=(10, 20))
+ @test size(grid) == (10, 20)
+ @test minimum(grid) == latlon(-50, 150)
+ @test maximum(grid) == latlon(50, 30)
+ @test spacing(grid) == (T(10) * u"°", T(12) * u"°")
+
+ # spacing unit and numtype
+ grid = RegularGrid((10, 20), Point(Polar(T(0) * u"cm", T(0) * u"rad")), (10.0 * u"mm", 1.0f0 * u"rad"))
+ @test unit.(spacing(grid)) == (u"cm", u"rad")
+ @test Unitful.numtype.(spacing(grid)) == (T, T)
+
+ # xyz & XYZ
+ grid = RegularGrid((10, 10), latlon(0, 0), T.((1, 1)))
+ @test Meshes.xyz(grid) == (T.(0:10) * u"°", T.(0:10) * u"°")
+ x = T.(0:10) * u"°"
+ y = T.(0:10)' * u"°"
+ @test Meshes.XYZ(grid) == (repeat(x, 1, 11), repeat(y, 11, 1))
+ grid = RegularGrid((10, 10), Point(Polar(T(0), T(0))), T.((1, 1)))
+ @test Meshes.xyz(grid) == (T.(0:10) * u"m", T.(0:10) * u"rad")
+ x = T.(0:10) * u"m"
+ y = T.(0:10)' * u"rad"
+ @test Meshes.XYZ(grid) == (repeat(x, 1, 11), repeat(y, 11, 1))
+
+ # indexing into a subgrid
+ grid = RegularGrid((10, 10), latlon(0, 0), T.((1, 1)))
+ sub = grid[1:2, 1:2]
+ @test size(sub) == (2, 2)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == minimum(grid)
+ @test maximum(sub) == latlon(2, 2)
+ grid = RegularGrid((10, 10), Point(Polar(T(0), T(0))), T.((1, 1)))
+ sub = grid[2:4, 3:7]
+ @test size(sub) == (3, 5)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == Point(Polar(T(1), T(2)))
+ @test maximum(sub) == Point(Polar(T(4), T(7)))
+
+ # vertex iteration
+ grid = RegularGrid((10, 10), cart(0, 0), T.((1, 1)))
+ vertextest(grid)
+
+ # type stability
+ grid = RegularGrid((10, 20), Point(Polar(T(0), T(0))), T.((1, 1)))
+ @inferred vertex(grid, (1, 1))
+ @inferred grid[1, 1]
+ @inferred grid[1:2, 1:2]
+ @inferred Meshes.xyz(grid)
+ @inferred Meshes.XYZ(grid)
+
+ # error: dimensions must be positive
+ @test_throws ArgumentError RegularGrid((-10, -10), latlon(0, 0), T.((1, 1)))
+ # error: spacing must be positive
+ @test_throws ArgumentError RegularGrid((10, 10), latlon(0, 0), T.((-1, -1)))
+ # error: regular spacing on `🌐` requires `LatLon` coordinates
+ p = latlon(0, 0) |> Proj(Cartesian)
+ @test_throws ArgumentError RegularGrid((10, 10), p, T.((1, 1)))
+ # error: the number of dimensions must be equal to the number of coordinates
+ @test_throws ArgumentError RegularGrid((10, 10, 10), latlon(0, 0), T.((1, 1, 1)))
+
+ grid = RegularGrid((10, 10), latlon(0, 0), T.((1, 1)))
+ if T == Float32
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 10×10 RegularGrid
+ ├─ minimum: Point(lat: 0.0f0°, lon: 0.0f0°)
+ ├─ maximum: Point(lat: 10.0f0°, lon: 10.0f0°)
+ └─ spacing: (1.0f0°, 1.0f0°)"""
+ elseif T == Float64
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 10×10 RegularGrid
+ ├─ minimum: Point(lat: 0.0°, lon: 0.0°)
+ ├─ maximum: Point(lat: 10.0°, lon: 10.0°)
+ └─ spacing: (1.0°, 1.0°)"""
+ end
+end
+
+@testitem "CartesianGrid" setup = [Setup] begin
+ grid = cartgrid(100)
+ @test embeddim(grid) == 1
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (100,)
+ @test minimum(grid) == cart(0)
+ @test maximum(grid) == cart(100)
+ @test extrema(grid) == (cart(0), cart(100))
+ @test spacing(grid) == (T(1) * u"m",)
+ @test nelements(grid) == 100
+ @test eltype(grid) <: Segment
+ @test measure(grid) ≈ T(100) * u"m"
+ @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
+ @test grid[1] == Segment(cart(0), cart(1))
+ @test grid[100] == Segment(cart(99), cart(100))
+
+ grid = cartgrid(200, 100)
+ @test embeddim(grid) == 2
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (200, 100)
+ @test minimum(grid) == cart(0, 0)
+ @test maximum(grid) == cart(200, 100)
+ @test extrema(grid) == (cart(0, 0), cart(200, 100))
+ @test spacing(grid) == (T(1) * u"m", T(1) * u"m")
+ @test nelements(grid) == 200 * 100
+ @test eltype(grid) <: Quadrangle
+ @test measure(grid) ≈ T(200 * 100) * u"m^2"
+ @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
+ @test grid[1, 1] == grid[1]
+ @test grid[200, 100] == grid[20000]
+
+ grid = CartesianGrid((200, 100, 50), T.((0, 0, 0)), T.((1, 1, 1)))
+ @test embeddim(grid) == 3
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (200, 100, 50)
+ @test minimum(grid) == cart(0, 0, 0)
+ @test maximum(grid) == cart(200, 100, 50)
+ @test extrema(grid) == (cart(0, 0, 0), cart(200, 100, 50))
+ @test spacing(grid) == (T(1) * u"m", T(1) * u"m", T(1) * u"m")
+ @test nelements(grid) == 200 * 100 * 50
+ @test eltype(grid) <: Hexahedron
+ @test measure(grid) ≈ T(200 * 100 * 50) * u"m^3"
+ @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
+ @test grid[1, 1, 1] == grid[1]
+ @test grid[200, 100, 50] == grid[1000000]
+
+ grid = CartesianGrid(T.((0, 0, 0)), T.((1, 1, 1)), T.((0.1, 0.1, 0.1)))
+ @test embeddim(grid) == 3
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (10, 10, 10)
+ @test minimum(grid) == cart(0, 0, 0)
+ @test maximum(grid) == cart(1, 1, 1)
+ @test spacing(grid) == (T(0.1) * u"m", T(0.1) * u"m", T(0.1) * u"m")
+
+ grid = CartesianGrid(T.((-1.0, -1.0)), T.((1.0, 1.0)), dims=(200, 100))
+ @test embeddim(grid) == 2
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (200, 100)
+ @test minimum(grid) == cart(-1.0, -1.0)
+ @test maximum(grid) == cart(1.0, 1.0)
+ @test spacing(grid) == (T(2 / 200) * u"m", T(2 / 100) * u"m")
+ @test nelements(grid) == 200 * 100
+ @test eltype(grid) <: Quadrangle
+
+ grid = CartesianGrid((20, 10, 5), T.((0, 0, 0)), T.((5, 5, 5)))
+ @test embeddim(grid) == 3
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (20, 10, 5)
+ @test minimum(grid) == cart(0, 0, 0)
+ @test maximum(grid) == cart(100, 50, 25)
+ @test extrema(grid) == (cart(0, 0, 0), cart(100, 50, 25))
+ @test spacing(grid) == (T(5) * u"m", T(5) * u"m", T(5) * u"m")
+ @test nelements(grid) == 20 * 10 * 5
+ @test eltype(grid) <: Hexahedron
+ @test vertices(grid[1]) == SVector(
+ cart(0, 0, 0),
+ cart(5, 0, 0),
+ cart(5, 5, 0),
+ cart(0, 5, 0),
+ cart(0, 0, 5),
+ cart(5, 0, 5),
+ cart(5, 5, 5),
+ cart(0, 5, 5)
+ )
+ @test all(centroid(grid, i) == centroid(grid[i]) for i in 1:nelements(grid))
+
+ # constructor with offset
+ grid = CartesianGrid((10, 10), T.((1.0, 1.0)), T.((1.0, 1.0)), (2, 2))
+ @test embeddim(grid) == 2
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (10, 10)
+ @test minimum(grid) == cart(0.0, 0.0)
+ @test maximum(grid) == cart(10.0, 10.0)
+ @test spacing(grid) == (T(1) * u"m", T(1) * u"m")
+ @test nelements(grid) == 10 * 10
+ @test eltype(grid) <: Quadrangle
+
+ # mixed units
+ grid = CartesianGrid((10, 10), (T(0) * u"m", T(0) * u"cm"), (T(100) * u"cm", T(1) * u"m"))
+ @test unit(Meshes.lentype(grid)) == u"m"
+ grid = CartesianGrid((T(0) * u"cm", T(0) * u"m"), (T(10) * u"m", T(1000) * u"cm"), (T(100) * u"cm", T(1) * u"m"))
+ @test unit(Meshes.lentype(grid)) == u"m"
+ grid = CartesianGrid((T(0) * u"cm", T(0) * u"m"), (T(10) * u"m", T(1000) * u"cm"), dims=(10, 10))
+ @test unit(Meshes.lentype(grid)) == u"m"
+
+ # indexing into a subgrid
+ grid = cartgrid(10, 10)
+ sub = grid[1:2, 1:2]
+ @test size(sub) == (2, 2)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == minimum(grid)
+ @test maximum(sub) == cart(2, 2)
+ sub = grid[1:1, 2:3]
+ @test size(sub) == (1, 2)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == cart(0, 1)
+ @test maximum(sub) == cart(1, 3)
+ sub = grid[2:4, 3:7]
+ @test size(sub) == (3, 5)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == cart(1, 2)
+ @test maximum(sub) == cart(4, 7)
+ grid = CartesianGrid(cart(1, 1), cart(11, 11), dims=(10, 10))
+ sub = grid[2:4, 3:7]
+ @test size(sub) == (3, 5)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == cart(2, 3)
+ @test maximum(sub) == cart(5, 8)
+ sub = grid[2, 3:7]
+ @test size(sub) == (1, 5)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == cart(2, 3)
+ @test maximum(sub) == cart(3, 8)
+ sub = grid[:, 3:7]
+ @test size(sub) == (10, 5)
+ @test spacing(sub) == spacing(grid)
+ @test minimum(sub) == cart(1, 3)
+ @test maximum(sub) == cart(11, 8)
+ @test_throws BoundsError grid[3:11, :]
+
+ # subgrid with comparable vertices of grid
+ grid = CartesianGrid((10, 10), cart(0.0, 0.0), T.((1.2, 1.2)))
+ sub = grid[2:4, 5:7]
+ @test sub == CartesianGrid((3, 3), cart(0.0, 0.0), T.((1.2, 1.2)), (0, -3))
+ ind = reshape(reshape(1:121, 11, 11)[2:5, 5:8], :)
+ @test vertices(grid)[ind] == vertices(sub)
+
+ # subgrid from Cartesian ranges
+ grid = cartgrid(10, 10)
+ sub1 = grid[1:2, 4:6]
+ sub2 = grid[CartesianIndex(1, 4):CartesianIndex(2, 6)]
+ @test sub1 == sub2
+
+ grid = cartgrid(200, 100)
+ @test centroid(grid, 1) == cart(0.5, 0.5)
+ @test centroid(grid, 2) == cart(1.5, 0.5)
+ @test centroid(grid, 200 * 100) == cart(199.5, 99.5)
+ @test nelements(grid) == 200 * 100
+ @test eltype(grid) <: Quadrangle
+ @test grid[1] == Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test grid[2] == Quadrangle(cart(1, 0), cart(2, 0), cart(2, 1), cart(1, 1))
+
+ # expand CartesianGrid with comparable vertices
+ grid = CartesianGrid((10, 10), cart(0.0, 0.0), T.((1.0, 1.0)))
+ left, right = (1, 1), (1, 1)
+ newdim = size(grid) .+ left .+ right
+ newoffset = offset(grid) .+ left
+ grid2 = CartesianGrid(newdim, minimum(grid), spacing(grid), newoffset)
+ @test issubset(vertices(grid), vertices(grid2))
+
+ # GridTopology from CartesianGrid
+ grid = cartgrid(5, 5)
+ topo = topology(grid)
+ vs = vertices(grid)
+ for i in 1:nelements(grid)
+ inds = indices(element(topo, i))
+ @test vs[[inds...]] == pointify(element(grid, i))
+ end
+
+ # convert topology
+ grid = cartgrid(10, 10)
+ mesh = topoconvert(HalfEdgeTopology, grid)
+ @test mesh isa SimpleMesh
+ @test nvertices(mesh) == 121
+ @test nelements(mesh) == 100
+ @test eltype(mesh) <: Quadrangle
+
+ # single vertex access
+ grid = cartgrid(10, 10)
+ @test vertex(grid, 1) == cart(0, 0)
+ @test vertex(grid, 121) == cart(10, 10)
+
+ # xyz
+ g1D = cartgrid(10)
+ g2D = cartgrid(10, 10)
+ g3D = cartgrid(10, 10, 10)
+ @test Meshes.xyz(g1D) == (T.(0:10) * u"m",)
+ @test Meshes.xyz(g2D) == (T.(0:10) * u"m", T.(0:10) * u"m")
+ @test Meshes.xyz(g3D) == (T.(0:10) * u"m", T.(0:10) * u"m", T.(0:10) * u"m")
+
+ # XYZ
+ g1D = cartgrid(10)
+ g2D = cartgrid(10, 10)
+ g3D = cartgrid(10, 10, 10)
+ x = T.(0:10) * u"m"
+ y = T.(0:10)' * u"m"
+ z = reshape(T.(0:10), 1, 1, 11) * u"m"
+ @test Meshes.XYZ(g1D) == (x,)
+ @test Meshes.XYZ(g2D) == (repeat(x, 1, 11), repeat(y, 11, 1))
+ @test Meshes.XYZ(g3D) == (repeat(x, 1, 11, 11), repeat(y, 11, 1, 11), repeat(z, 11, 11, 1))
+
+ # units
+ grid = CartesianGrid((10, 10), cart(0, 0), (T(1) * u"m", T(1) * u"m"))
+ o = minimum(grid)
+ s = spacing(grid)
+ @test unit(Meshes.lentype(o)) == u"m"
+ @test Unitful.numtype(Meshes.lentype(o)) === T
+ @test unit(eltype(s)) == u"m"
+ @test Unitful.numtype(eltype(s)) === T
+
+ # views
+ grid = cartgrid(10, 10)
+ vgrid = view(grid, 1:3)
+ @test parent(vgrid) == grid
+ @test parentindices(vgrid) == 1:3
+ @test parent(grid) == grid
+ @test parentindices(grid) == 1:100
+
+ grid = cartgrid(200, 100)
+ if T == Float32
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 200×100 CartesianGrid
+ ├─ minimum: Point(x: 0.0f0 m, y: 0.0f0 m)
+ ├─ maximum: Point(x: 200.0f0 m, y: 100.0f0 m)
+ └─ spacing: (1.0f0 m, 1.0f0 m)"""
+ elseif T == Float64
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 200×100 CartesianGrid
+ ├─ minimum: Point(x: 0.0 m, y: 0.0 m)
+ ├─ maximum: Point(x: 200.0 m, y: 100.0 m)
+ └─ spacing: (1.0 m, 1.0 m)"""
+ end
+end
+
+@testitem "RectilinearGrid" setup = [Setup] begin
+ x = range(zero(T), stop=one(T), length=6)
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
+ grid = RectilinearGrid(x, y)
+ @test embeddim(grid) == 2
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (5, 5)
+ @test minimum(grid) == cart(0, 0)
+ @test maximum(grid) == cart(1, 1)
+ @test extrema(grid) == (cart(0, 0), cart(1, 1))
+ @test nelements(grid) == 25
+ @test eltype(grid) <: Quadrangle
+ @test measure(grid) ≈ T(1) * u"m^2"
+ @test centroid(grid, 1) ≈ cart(0.1, 0.05)
+ @test centroid(grid[1]) ≈ cart(0.1, 0.05)
+ @test centroid(grid, 2) ≈ cart(0.3, 0.05)
+ @test centroid(grid[2]) ≈ cart(0.3, 0.05)
+ @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
+ @test grid[1, 1] == grid[1]
+ @test grid[5, 5] == grid[25]
+ sub = grid[2:4, 3:5]
+ @test size(sub) == (3, 3)
+ @test minimum(sub) == cart(0.2, 0.3)
+ @test maximum(sub) == cart(0.8, 1.0)
+ sub = grid[2, 3:5]
+ @test size(sub) == (1, 3)
+ @test minimum(sub) == cart(0.2, 0.3)
+ @test maximum(sub) == cart(0.4, 1.0)
+ sub = grid[:, 3:5]
+ @test size(sub) == (5, 3)
+ @test minimum(sub) == cart(0.0, 0.3)
+ @test maximum(sub) == cart(1.0, 1.0)
+ @test_throws BoundsError grid[2:6, :]
+ @test Meshes.xyz(grid) == (x * u"m", y * u"m")
+ @test Meshes.XYZ(grid) == (repeat(x, 1, 6) * u"m", repeat(y', 6, 1) * u"m")
+
+ # single vertex access
+ grid = RectilinearGrid(T.(0:10), T.(0:10))
+ @test vertex(grid, 1) == cart(0, 0)
+ @test vertex(grid, 121) == cart(10, 10)
+
+ # constructor with manifold and CRS
+ x = range(zero(T), stop=one(T), length=6)
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
+ C = typeof(Mercator(T(0), T(0)))
+ grid = RectilinearGrid{𝔼{2},C}(x, y)
+ @test manifold(grid) === 𝔼{2}
+ @test crs(grid) === C
+ @test crs(grid[1, 1]) === C
+ @test crs(centroid(grid)) === C
+ C = typeof(LatLon(T(0), T(0)))
+ grid = RectilinearGrid{🌐,C}(x, y)
+ @test manifold(grid) === 🌐
+ @test crs(grid) === C
+ @test crs(grid[1, 1]) === C
+ @test crs(centroid(grid)) === C
+
+ # units
+ x = range(zero(T), stop=one(T), length=6) * u"mm"
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0] * u"cm"
+ grid = RectilinearGrid(x, y)
+ @test unit(Meshes.lentype(grid)) == u"m"
+ # error: invalid units for cartesian coordinates
+ x = range(zero(T), stop=one(T), length=6) * u"m"
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0] * u"°"
+ @test_throws ArgumentError RectilinearGrid(x, y)
+
+ # conversion
+ cg = cartgrid(10, 10)
+ rg = convert(RectilinearGrid, cg)
+ @test size(rg) == size(cg)
+ @test nvertices(rg) == nvertices(cg)
+ @test nelements(rg) == nelements(cg)
+ @test topology(rg) == topology(cg)
+ @test vertices(rg) == vertices(cg)
+
+ cg = cartgrid(10, 20, 30)
+ rg = convert(RectilinearGrid, cg)
+ @test size(rg) == size(cg)
+ @test nvertices(rg) == nvertices(cg)
+ @test nelements(rg) == nelements(cg)
+ @test topology(rg) == topology(cg)
+ @test vertices(rg) == vertices(cg)
+
+ # vertex iteration
+ x = range(zero(T), stop=one(T), length=6)
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
+ grid = RectilinearGrid(x, y)
+ vertextest(grid)
+
+ # type stability
+ x = range(zero(T), stop=one(T), length=6) * u"mm"
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0] * u"cm"
+ ρ = range(zero(T), stop=one(T), length=6)
+ ϕ = range(zero(T), stop=T(2π), length=6)
+ C = typeof(Polar(T(0), T(0)))
+ grid = RectilinearGrid{𝔼{2},C}(ρ, ϕ)
+ @inferred RectilinearGrid(x, y)
+ @inferred RectilinearGrid{𝔼{2},C}(ρ, ϕ)
+ @inferred vertex(grid, (1, 1))
+ @inferred grid[1, 1]
+ @inferred grid[1:2, 1:2]
+ @inferred Meshes.XYZ(grid)
+
+ # error: regular spacing on `🌐` requires `LatLon` coordinates
+ x = range(zero(T), stop=one(T), length=6)
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
+ z = T[0.0, 0.15, 0.35, 0.65, 0.85, 1.0]
+ C = typeof(Cartesian(T(0), T(0), T(0)))
+ @test_throws ArgumentError RectilinearGrid{🌐,C}(x, y, z)
+ # error: the number of dimensions must be equal to the number of coordinates
+ C = typeof(LatLon(T(0), T(0)))
+ @test_throws ArgumentError RectilinearGrid{🌐,C}(x, y, z)
+
+ x = range(zero(T), stop=one(T), length=6)
+ y = T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]
+ grid = RectilinearGrid(x, y)
+ @test sprint(show, grid) == "5×5 RectilinearGrid"
+ if T == Float32
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 5×5 RectilinearGrid
+ 36 vertices
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.2f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.4f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.6f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.8f0 m, y: 0.0f0 m)
+ ⋮
+ ├─ Point(x: 0.2f0 m, y: 1.0f0 m)
+ ├─ Point(x: 0.4f0 m, y: 1.0f0 m)
+ ├─ Point(x: 0.6f0 m, y: 1.0f0 m)
+ ├─ Point(x: 0.8f0 m, y: 1.0f0 m)
+ └─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ 25 elements
+ ├─ Quadrangle(1, 2, 8, 7)
+ ├─ Quadrangle(2, 3, 9, 8)
+ ├─ Quadrangle(3, 4, 10, 9)
+ ├─ Quadrangle(4, 5, 11, 10)
+ ├─ Quadrangle(5, 6, 12, 11)
+ ⋮
+ ├─ Quadrangle(25, 26, 32, 31)
+ ├─ Quadrangle(26, 27, 33, 32)
+ ├─ Quadrangle(27, 28, 34, 33)
+ ├─ Quadrangle(28, 29, 35, 34)
+ └─ Quadrangle(29, 30, 36, 35)"""
+ elseif T == Float64
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 5×5 RectilinearGrid
+ 36 vertices
+ ├─ Point(x: 0.0 m, y: 0.0 m)
+ ├─ Point(x: 0.2 m, y: 0.0 m)
+ ├─ Point(x: 0.4 m, y: 0.0 m)
+ ├─ Point(x: 0.6 m, y: 0.0 m)
+ ├─ Point(x: 0.8 m, y: 0.0 m)
+ ⋮
+ ├─ Point(x: 0.2 m, y: 1.0 m)
+ ├─ Point(x: 0.4 m, y: 1.0 m)
+ ├─ Point(x: 0.6 m, y: 1.0 m)
+ ├─ Point(x: 0.8 m, y: 1.0 m)
+ └─ Point(x: 1.0 m, y: 1.0 m)
+ 25 elements
+ ├─ Quadrangle(1, 2, 8, 7)
+ ├─ Quadrangle(2, 3, 9, 8)
+ ├─ Quadrangle(3, 4, 10, 9)
+ ├─ Quadrangle(4, 5, 11, 10)
+ ├─ Quadrangle(5, 6, 12, 11)
+ ⋮
+ ├─ Quadrangle(25, 26, 32, 31)
+ ├─ Quadrangle(26, 27, 33, 32)
+ ├─ Quadrangle(27, 28, 34, 33)
+ ├─ Quadrangle(28, 29, 35, 34)
+ └─ Quadrangle(29, 30, 36, 35)"""
+ end
+end
+
+@testitem "StructuredGrid" setup = [Setup] begin
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6)
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
+ grid = StructuredGrid(X, Y)
+ @test embeddim(grid) == 2
+ @test crs(grid) <: Cartesian{NoDatum}
+ @test Meshes.lentype(grid) == ℳ
+ @test size(grid) == (5, 5)
+ @test minimum(grid) == cart(0, 0)
+ @test maximum(grid) == cart(1, 1)
+ @test extrema(grid) == (cart(0, 0), cart(1, 1))
+ @test nelements(grid) == 25
+ @test eltype(grid) <: Quadrangle
+ @test measure(grid) ≈ T(1) * u"m^2"
+ @test centroid(grid, 1) ≈ cart(0.1, 0.05)
+ @test centroid(grid[1]) ≈ cart(0.1, 0.05)
+ @test centroid(grid, 2) ≈ cart(0.3, 0.05)
+ @test centroid(grid[2]) ≈ cart(0.3, 0.05)
+ @test vertex(grid, 1) == vertex(grid, ntuple(i -> 1, embeddim(grid)))
+ @test vertex(grid, nvertices(grid)) == vertex(grid, size(grid) .+ 1)
+ @test grid[1, 1] == grid[1]
+ @test grid[5, 5] == grid[25]
+ sub = grid[2:4, 3:5]
+ @test size(sub) == (3, 3)
+ @test minimum(sub) == cart(0.2, 0.3)
+ @test maximum(sub) == cart(0.8, 1.0)
+ sub = grid[2, 3:5]
+ @test size(sub) == (1, 3)
+ @test minimum(sub) == cart(0.2, 0.3)
+ @test maximum(sub) == cart(0.4, 1.0)
+ sub = grid[:, 3:5]
+ @test size(sub) == (5, 3)
+ @test minimum(sub) == cart(0.0, 0.3)
+ @test maximum(sub) == cart(1.0, 1.0)
+ @test_throws BoundsError grid[2:6, :]
+ @test Meshes.XYZ(grid) == (X * u"m", Y * u"m")
+
+ # constructor with manifold and CRS
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6)
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
+ C = typeof(Mercator(T(0), T(0)))
+ grid = StructuredGrid{𝔼{2},C}(X, Y)
+ @test manifold(grid) === 𝔼{2}
+ @test crs(grid) === C
+ @test crs(grid[1, 1]) === C
+ @test crs(centroid(grid)) === C
+ C = typeof(LatLon(T(0), T(0)))
+ grid = StructuredGrid{🌐,C}(X, Y)
+ @test manifold(grid) === 🌐
+ @test crs(grid) === C
+ @test crs(grid[1, 1]) === C
+ @test crs(centroid(grid)) === C
+
+ # units
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6) * u"mm"
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1) * u"cm"
+ grid = StructuredGrid(X, Y)
+ @test unit(Meshes.lentype(grid)) == u"m"
+ # error: invalid units for cartesian coordinates
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6) * u"m"
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1) * u"°"
+ @test_throws ArgumentError StructuredGrid(X, Y)
+
+ # conversion
+ cg = cartgrid(10, 10)
+ sg = convert(StructuredGrid, cg)
+ @test size(sg) == size(cg)
+ @test nvertices(sg) == nvertices(cg)
+ @test nelements(sg) == nelements(cg)
+ @test topology(sg) == topology(cg)
+ @test vertices(sg) == vertices(cg)
+
+ cg = cartgrid(10, 20, 30)
+ sg = convert(StructuredGrid, cg)
+ @test size(sg) == size(cg)
+ @test nvertices(sg) == nvertices(cg)
+ @test nelements(sg) == nelements(cg)
+ @test topology(sg) == topology(cg)
+ @test vertices(sg) == vertices(cg)
+
+ rg = RectilinearGrid(T.(0:10), T.(0:10))
+ sg = convert(StructuredGrid, rg)
+ @test size(sg) == size(rg)
+ @test nvertices(sg) == nvertices(rg)
+ @test nelements(sg) == nelements(rg)
+ @test topology(sg) == topology(rg)
+ @test vertices(sg) == vertices(rg)
+
+ rg = RectilinearGrid(T.(0:10), T.(0:20), T.(0:30))
+ sg = convert(StructuredGrid, rg)
+ @test size(sg) == size(rg)
+ @test nvertices(sg) == nvertices(rg)
+ @test nelements(sg) == nelements(rg)
+ @test topology(sg) == topology(rg)
+ @test vertices(sg) == vertices(rg)
+
+ # vertex iteration
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6)
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
+ grid = StructuredGrid(X, Y)
+ vertextest(grid)
+
+ # type stability
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6) * u"mm"
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1) * u"cm"
+ ρ = repeat(range(zero(T), stop=one(T), length=6), 1, 6)
+ ϕ = repeat(range(zero(T), stop=T(2π), length=6)', 6, 1)
+ C = typeof(Polar(T(0), T(0)))
+ grid = StructuredGrid{𝔼{2},C}(ρ, ϕ)
+ @inferred StructuredGrid(X, Y)
+ @inferred StructuredGrid{𝔼{2},C}(ρ, ϕ)
+ @inferred vertex(grid, (1, 1))
+ @inferred grid[1, 1]
+ @inferred grid[1:2, 1:2]
+
+ # error: regular spacing on `🌐` requires `LatLon` coordinates
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6, 6)
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1, 6)
+ Z = repeat(reshape(T[0.0, 0.15, 0.35, 0.65, 0.85, 1.0], 1, 1, 6), 6, 6, 1)
+ C = typeof(Cartesian(T(0), T(0), T(0)))
+ @test_throws ArgumentError StructuredGrid{🌐,C}(X, Y, Z)
+ # error: the number of dimensions must be equal to the number of coordinates
+ C = typeof(LatLon(T(0), T(0)))
+ @test_throws ArgumentError StructuredGrid{🌐,C}(X, Y, Z)
+ # error: all coordinate arrays must be the same size
+ X = rand(T, 6, 6)
+ Y = rand(T, 5, 5)
+ @test_throws ArgumentError StructuredGrid(X, Y)
+ # error: the number of array dimensions must be equal to the number of grid dimensions
+ X = rand(T, 6, 6)
+ Y = rand(T, 6, 6)
+ Z = rand(T, 6, 6)
+ @test_throws ArgumentError StructuredGrid(X, Y, Z)
+
+ X = repeat(range(zero(T), stop=one(T), length=6), 1, 6)
+ Y = repeat(T[0.0, 0.1, 0.3, 0.7, 0.9, 1.0]', 6, 1)
+ grid = StructuredGrid(X, Y)
+ @test sprint(show, grid) == "5×5 StructuredGrid"
+ if T == Float32
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 5×5 StructuredGrid
+ 36 vertices
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.2f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.4f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.6f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.8f0 m, y: 0.0f0 m)
+ ⋮
+ ├─ Point(x: 0.2f0 m, y: 1.0f0 m)
+ ├─ Point(x: 0.4f0 m, y: 1.0f0 m)
+ ├─ Point(x: 0.6f0 m, y: 1.0f0 m)
+ ├─ Point(x: 0.8f0 m, y: 1.0f0 m)
+ └─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ 25 elements
+ ├─ Quadrangle(1, 2, 8, 7)
+ ├─ Quadrangle(2, 3, 9, 8)
+ ├─ Quadrangle(3, 4, 10, 9)
+ ├─ Quadrangle(4, 5, 11, 10)
+ ├─ Quadrangle(5, 6, 12, 11)
+ ⋮
+ ├─ Quadrangle(25, 26, 32, 31)
+ ├─ Quadrangle(26, 27, 33, 32)
+ ├─ Quadrangle(27, 28, 34, 33)
+ ├─ Quadrangle(28, 29, 35, 34)
+ └─ Quadrangle(29, 30, 36, 35)"""
+ elseif T == Float64
+ @test sprint(show, MIME"text/plain"(), grid) == """
+ 5×5 StructuredGrid
+ 36 vertices
+ ├─ Point(x: 0.0 m, y: 0.0 m)
+ ├─ Point(x: 0.2 m, y: 0.0 m)
+ ├─ Point(x: 0.4 m, y: 0.0 m)
+ ├─ Point(x: 0.6 m, y: 0.0 m)
+ ├─ Point(x: 0.8 m, y: 0.0 m)
+ ⋮
+ ├─ Point(x: 0.2 m, y: 1.0 m)
+ ├─ Point(x: 0.4 m, y: 1.0 m)
+ ├─ Point(x: 0.6 m, y: 1.0 m)
+ ├─ Point(x: 0.8 m, y: 1.0 m)
+ └─ Point(x: 1.0 m, y: 1.0 m)
+ 25 elements
+ ├─ Quadrangle(1, 2, 8, 7)
+ ├─ Quadrangle(2, 3, 9, 8)
+ ├─ Quadrangle(3, 4, 10, 9)
+ ├─ Quadrangle(4, 5, 11, 10)
+ ├─ Quadrangle(5, 6, 12, 11)
+ ⋮
+ ├─ Quadrangle(25, 26, 32, 31)
+ ├─ Quadrangle(26, 27, 33, 32)
+ ├─ Quadrangle(27, 28, 34, 33)
+ ├─ Quadrangle(28, 29, 35, 34)
+ └─ Quadrangle(29, 30, 36, 35)"""
+ end
+end
+
+@testitem "SimpleMesh" setup = [Setup] begin
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
+ triangles =
+ Triangle.([
+ (cart(0.0, 0.0), cart(1.0, 0.0), cart(0.5, 0.5)),
+ (cart(1.0, 0.0), cart(1.0, 1.0), cart(0.5, 0.5)),
+ (cart(1.0, 1.0), cart(0.0, 1.0), cart(0.5, 0.5)),
+ (cart(0.0, 1.0), cart(0.0, 0.0), cart(0.5, 0.5))
+ ])
+ @test crs(mesh) <: Cartesian{NoDatum}
+ @test Meshes.lentype(mesh) == ℳ
+ @test vertices(mesh) == points
+ @test collect(faces(mesh, 2)) == triangles
+ @test collect(elements(mesh)) == triangles
+ @test nelements(mesh) == 4
+ for i in 1:length(triangles)
+ @test mesh[i] == triangles[i]
+ end
+ @test eltype(mesh) <: Triangle
+ @test measure(mesh) ≈ T(1) * u"m^2"
+ @test area(mesh) ≈ T(1) * u"m^2"
+ @test extrema(mesh) == (cart(0, 0), cart(1, 1))
+
+ # test constructors
+ coords = [T.((0, 0)), T.((1, 0)), T.((0, 1)), T.((1, 1)), T.((0.5, 0.5))]
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(coords, SimpleTopology(connec))
+ @test eltype(mesh) <: Triangle
+ @test topology(mesh) isa SimpleTopology
+ @test nvertices(mesh) == 5
+ @test nelements(mesh) == 4
+ mesh = SimpleMesh(coords, connec)
+ @test eltype(mesh) <: Triangle
+ @test topology(mesh) isa SimpleTopology
+ @test nvertices(mesh) == 5
+ @test nelements(mesh) == 4
+ mesh = SimpleMesh(coords, connec, relations=true)
+ @test eltype(mesh) <: Triangle
+ @test topology(mesh) isa HalfEdgeTopology
+ @test nvertices(mesh) == 5
+ @test nelements(mesh) == 4
+
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.5), (0.75, 0.5)])
+ Δs = connect.([(3, 1, 5), (4, 6, 2)], Triangle)
+ □s = connect.([(1, 2, 6, 5), (5, 6, 4, 3)], Quadrangle)
+ mesh = SimpleMesh(points, [Δs; □s])
+ elms = [
+ Triangle(cart(0.0, 1.0), cart(0.0, 0.0), cart(0.25, 0.5)),
+ Triangle(cart(1.0, 1.0), cart(0.75, 0.5), cart(1.0, 0.0)),
+ Quadrangle(cart(0.0, 0.0), cart(1.0, 0.0), cart(0.75, 0.5), cart(0.25, 0.5)),
+ Quadrangle(cart(0.25, 0.5), cart(0.75, 0.5), cart(1.0, 1.0), cart(0.0, 1.0))
+ ]
+ @test collect(elements(mesh)) == elms
+ @test nelements(mesh) == 4
+ for i in 1:length(elms)
+ @test mesh[i] == elms[i]
+ end
+ @test eltype(mesh) <: Polygon
+
+ # test for https://github.com/JuliaGeometry/Meshes.jl/issues/177
+ points = cart.([(0, 0, 0), (1, 0, 0), (1, 1, 1), (0, 1, 0)])
+ connec = connect.([(1, 2, 3, 4), (3, 4, 1)], [Tetrahedron, Triangle])
+ mesh = SimpleMesh(points, connec)
+ topo = topology(mesh)
+ @test collect(faces(topo, 2)) == [connect((3, 4, 1), Triangle)]
+ @test collect(faces(topo, 3)) == [connect((1, 2, 3, 4), Tetrahedron)]
+
+ # test for https://github.com/JuliaGeometry/Meshes.jl/issues/187
+ points = cart.([(0, 0, 0), (1, 0, 0), (1, 1, 1), (0, 1, 0)])
+ connec = connect.([(1, 2, 3, 4), (3, 4, 1)], [Tetrahedron, Triangle])
+ mesh = SimpleMesh(points[4:-1:1], connec)
+ meshvp = SimpleMesh(view(points, 4:-1:1), connec)
+ @test mesh == meshvp
+
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
+ bytes = @allocated faces(mesh, 2)
+ @test bytes < 100
+ cells = faces(mesh, 2)
+ bytes = @allocated collect(cells)
+ @test bytes < 800
+
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
+ @test centroid(mesh, 1) == centroid(Triangle(cart(0, 0), cart(1, 0), cart(0.5, 0.5)))
+ @test centroid(mesh, 2) == centroid(Triangle(cart(1, 0), cart(1, 1), cart(0.5, 0.5)))
+ @test centroid(mesh, 3) == centroid(Triangle(cart(1, 1), cart(0, 1), cart(0.5, 0.5)))
+ @test centroid(mesh, 4) == centroid(Triangle(cart(0, 1), cart(0, 0), cart(0.5, 0.5)))
+
+ # merge operation with 2D geometries
+ mesh₁ = SimpleMesh(cart.([(0, 0), (1, 0), (0, 1)]), connect.([(1, 2, 3)]))
+ mesh₂ = SimpleMesh(cart.([(1, 0), (1, 1), (0, 1)]), connect.([(1, 2, 3)]))
+ mesh = merge(mesh₁, mesh₂)
+ @test vertices(mesh) == [vertices(mesh₁); vertices(mesh₂)]
+ @test collect(elements(topology(mesh))) == connect.([(1, 2, 3), (4, 5, 6)])
+
+ # merge operation with 3D geometries
+ mesh₁ = SimpleMesh(cart.([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]), connect.([(1, 2, 3, 4)], Tetrahedron))
+ mesh₂ = SimpleMesh(cart.([(1, 0, 0), (1, 1, 0), (0, 1, 0), (1, 1, 1)]), connect.([(1, 2, 3, 4)], Tetrahedron))
+ mesh = merge(mesh₁, mesh₂)
+ @test vertices(mesh) == [vertices(mesh₁); vertices(mesh₂)]
+ @test collect(elements(topology(mesh))) == connect.([(1, 2, 3, 4), (5, 6, 7, 8)], Tetrahedron)
+
+ # convert any mesh to SimpleMesh
+ grid = cartgrid(10, 10)
+ mesh = convert(SimpleMesh, grid)
+ @test mesh isa SimpleMesh
+ @test topology(mesh) == GridTopology(10, 10)
+ @test nvertices(mesh) == 121
+ @test nelements(mesh) == 100
+ @test eltype(mesh) <: Quadrangle
+ # grid interface
+ @test size(mesh) == (10, 10)
+ @test minimum(mesh) == cart(0, 0)
+ @test maximum(mesh) == cart(10, 10)
+ @test extrema(mesh) == (cart(0, 0), cart(10, 10))
+ @test vertex(mesh, 1) == vertex(mesh, ntuple(i -> 1, embeddim(mesh)))
+ @test vertex(mesh, nvertices(mesh)) == vertex(mesh, size(mesh) .+ 1)
+ @test mesh[1, 1] == mesh[1]
+ @test mesh[10, 10] == mesh[100]
+ sub = mesh[2:4, 3:7]
+ @test size(sub) == (3, 5)
+ @test minimum(sub) == cart(1, 2)
+ @test maximum(sub) == cart(4, 7)
+ sub = mesh[2, 3:7]
+ @test size(sub) == (1, 5)
+ @test minimum(sub) == cart(1, 2)
+ @test maximum(sub) == cart(2, 7)
+ sub = mesh[:, 3:7]
+ @test size(sub) == (10, 5)
+ @test minimum(sub) == cart(0, 2)
+ @test maximum(sub) == cart(10, 7)
+ @test_throws BoundsError grid[3:11, :]
+
+ # test for https://github.com/JuliaGeometry/Meshes.jl/issues/261
+ points = randpoint2(5)
+ connec = [connect((1, 2, 3))]
+ mesh = SimpleMesh(points, connec)
+ @test nvertices(mesh) == length(vertices(mesh)) == 5
+
+ # single vertex access
+ points = randpoint2(5)
+ connec = [connect((1, 2, 3))]
+ mesh = SimpleMesh(points, connec)
+ @test vertex(mesh, 1) == points[1]
+ @test vertex(mesh, 2) == points[2]
+ @test vertex(mesh, 3) == points[3]
+ @test vertex(mesh, 4) == points[4]
+ @test vertex(mesh, 5) == points[5]
+
+ # vertex iteration
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
+ vertextest(mesh)
+
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
+ @test sprint(show, mesh) == "4 SimpleMesh"
+ if T == Float32
+ @test sprint(show, MIME"text/plain"(), mesh) == """
+ 4 SimpleMesh
+ 5 vertices
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 0.0f0 m, y: 1.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ └─ Point(x: 0.5f0 m, y: 0.5f0 m)
+ 4 elements
+ ├─ Triangle(1, 2, 5)
+ ├─ Triangle(2, 4, 5)
+ ├─ Triangle(4, 3, 5)
+ └─ Triangle(3, 1, 5)"""
+ elseif T == Float64
+ @test sprint(show, MIME"text/plain"(), mesh) == """
+ 4 SimpleMesh
+ 5 vertices
+ ├─ Point(x: 0.0 m, y: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m)
+ ├─ Point(x: 0.0 m, y: 1.0 m)
+ ├─ Point(x: 1.0 m, y: 1.0 m)
+ └─ Point(x: 0.5 m, y: 0.5 m)
+ 4 elements
+ ├─ Triangle(1, 2, 5)
+ ├─ Triangle(2, 4, 5)
+ ├─ Triangle(4, 3, 5)
+ └─ Triangle(3, 1, 5)"""
+ end
+end
+
+@testitem "TransformedMesh" setup = [Setup] begin
+ grid = cartgrid(10, 10)
+ rgrid = convert(RectilinearGrid, grid)
+ sgrid = convert(StructuredGrid, grid)
+ mesh = convert(SimpleMesh, grid)
+ trans = Identity()
+ tmesh = TransformedMesh(mesh, trans)
+ @test crs(tmesh) <: Cartesian{NoDatum}
+ @test Meshes.lentype(tmesh) == ℳ
+ @test parent(tmesh) === mesh
+ @test Meshes.transform(tmesh) === trans
+ @test TransformedMesh(grid, trans) == grid
+ @test TransformedMesh(rgrid, trans) == rgrid
+ @test TransformedMesh(sgrid, trans) == sgrid
+ @test TransformedMesh(mesh, trans) == mesh
+ trans = Translate(T(10), T(10)) → Translate(T(-10), T(-10))
+ @test TransformedMesh(grid, trans) == grid
+ @test TransformedMesh(rgrid, trans) == rgrid
+ @test TransformedMesh(sgrid, trans) == sgrid
+ @test TransformedMesh(mesh, trans) == mesh
+ trans1 = Translate(T(10), T(10))
+ trans2 = Translate(T(-10), T(-10))
+ @test TransformedMesh(TransformedMesh(grid, trans1), trans2) == TransformedMesh(grid, trans1 → trans2)
+
+ # vertex iteration
+ trans = Identity()
+ points = latlon.([(0, 0), (0, 1), (1, 0), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
+ tmesh = TransformedMesh(mesh, trans)
+ vertextest(tmesh)
+
+ # transforms that change the Manifold and/or CRS
+ points = latlon.([(0, 0), (0, 1), (1, 0), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ mesh = SimpleMesh(points, connec)
+ trans = Proj(Cartesian)
+ tmesh = TransformedMesh(mesh, trans)
+ @test manifold(tmesh) === 🌐
+ @test crs(tmesh) <: Cartesian
+ trans = Proj(Polar)
+ tgrid = TransformedMesh(grid, trans)
+ @test tgrid isa TransformedGrid
+ @test manifold(tgrid) === 𝔼{2}
+ @test crs(tgrid) <: Polar
+
+ # grid interface
+ trans = Identity()
+ tgrid = TransformedMesh(grid, trans)
+ @test tgrid isa TransformedGrid
+ @test size(tgrid) == (10, 10)
+ @test minimum(tgrid) == cart(0, 0)
+ @test maximum(tgrid) == cart(10, 10)
+ @test extrema(tgrid) == (cart(0, 0), cart(10, 10))
+ @test vertex(tgrid, 1) == vertex(tgrid, ntuple(i -> 1, embeddim(tgrid)))
+ @test vertex(tgrid, nvertices(tgrid)) == vertex(tgrid, size(tgrid) .+ 1)
+ @test tgrid[1, 1] == tgrid[1]
+ @test tgrid[10, 10] == tgrid[100]
+ sub = tgrid[2:4, 3:7]
+ @test size(sub) == (3, 5)
+ @test minimum(sub) == cart(1, 2)
+ @test maximum(sub) == cart(4, 7)
+ sub = tgrid[2, 3:7]
+ @test size(sub) == (1, 5)
+ @test minimum(sub) == cart(1, 2)
+ @test maximum(sub) == cart(2, 7)
+ sub = tgrid[:, 3:7]
+ @test size(sub) == (10, 5)
+ @test minimum(sub) == cart(0, 2)
+ @test maximum(sub) == cart(10, 7)
+
+ # optimization of centroid
+ trans = Rotate(T(π / 4))
+ cgrid = cartgrid(10, 10)
+ tmesh = TransformedMesh(cgrid, trans)
+ centr = centroid(tmesh, 1)
+ @test @allocated(centroid(tmesh, 1)) < 50
+
+ # optimization of ==
+ trans = Rotate(T(π / 4))
+ cgrid = cartgrid(1000, 1000)
+ tmesh = TransformedMesh(cgrid, trans)
+ @test tmesh == tmesh
+
+ @test sprint(show, tgrid) == "10×10 TransformedGrid"
+ if T == Float32
+ @test sprint(show, MIME"text/plain"(), tgrid) == """
+ 10×10 TransformedGrid
+ 121 vertices
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 2.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 3.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 4.0f0 m, y: 0.0f0 m)
+ ⋮
+ ├─ Point(x: 6.0f0 m, y: 10.0f0 m)
+ ├─ Point(x: 7.0f0 m, y: 10.0f0 m)
+ ├─ Point(x: 8.0f0 m, y: 10.0f0 m)
+ ├─ Point(x: 9.0f0 m, y: 10.0f0 m)
+ └─ Point(x: 10.0f0 m, y: 10.0f0 m)
+ 100 elements
+ ├─ Quadrangle(1, 2, 13, 12)
+ ├─ Quadrangle(2, 3, 14, 13)
+ ├─ Quadrangle(3, 4, 15, 14)
+ ├─ Quadrangle(4, 5, 16, 15)
+ ├─ Quadrangle(5, 6, 17, 16)
+ ⋮
+ ├─ Quadrangle(105, 106, 117, 116)
+ ├─ Quadrangle(106, 107, 118, 117)
+ ├─ Quadrangle(107, 108, 119, 118)
+ ├─ Quadrangle(108, 109, 120, 119)
+ └─ Quadrangle(109, 110, 121, 120)"""
+ elseif T == Float64
+ @test sprint(show, MIME"text/plain"(), tgrid) == """
+ 10×10 TransformedGrid
+ 121 vertices
+ ├─ Point(x: 0.0 m, y: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m)
+ ├─ Point(x: 2.0 m, y: 0.0 m)
+ ├─ Point(x: 3.0 m, y: 0.0 m)
+ ├─ Point(x: 4.0 m, y: 0.0 m)
+ ⋮
+ ├─ Point(x: 6.0 m, y: 10.0 m)
+ ├─ Point(x: 7.0 m, y: 10.0 m)
+ ├─ Point(x: 8.0 m, y: 10.0 m)
+ ├─ Point(x: 9.0 m, y: 10.0 m)
+ └─ Point(x: 10.0 m, y: 10.0 m)
+ 100 elements
+ ├─ Quadrangle(1, 2, 13, 12)
+ ├─ Quadrangle(2, 3, 14, 13)
+ ├─ Quadrangle(3, 4, 15, 14)
+ ├─ Quadrangle(4, 5, 16, 15)
+ ├─ Quadrangle(5, 6, 17, 16)
+ ⋮
+ ├─ Quadrangle(105, 106, 117, 116)
+ ├─ Quadrangle(106, 107, 118, 117)
+ ├─ Quadrangle(107, 108, 119, 118)
+ ├─ Quadrangle(108, 109, 120, 119)
+ └─ Quadrangle(109, 110, 121, 120)"""
+ end
+ @test_throws BoundsError grid[3:11, :]
+end
diff --git a/test/multigeoms.jl b/test/multigeoms.jl
index 9cdee8518..5b1dddd6b 100644
--- a/test/multigeoms.jl
+++ b/test/multigeoms.jl
@@ -1,58 +1,66 @@
-@testset "Multi" begin
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
+@testitem "Multigeometries" setup = [Setup] begin
+ outer = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+ hole1 = cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)])
+ hole2 = cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)])
poly = PolyArea([outer, hole1, hole2])
multi = Multi([poly, poly])
@test multi == multi
@test multi ≈ multi
@test paramdim(multi) == 2
+ @test crs(multi) <: Cartesian{NoDatum}
+ @test Meshes.lentype(multi) == ℳ
@test vertex(multi, 1) == vertex(poly, 1)
@test vertices(multi) == [vertices(poly); vertices(poly)]
@test nvertices(multi) == nvertices(poly) + nvertices(poly)
@test boundary(multi) == merge(boundary(poly), boundary(poly))
@test rings(multi) == [rings(poly); rings(poly)]
- poly1 = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- poly2 = PolyArea(P2[(1, 1), (2, 1), (2, 2), (1, 2)])
+ poly1 = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ poly2 = PolyArea(cart.([(1, 1), (2, 1), (2, 2), (1, 2)]))
multi = Multi([poly1, poly2])
@test vertices(multi) == [vertices(poly1); vertices(poly2)]
@test nvertices(multi) == nvertices(poly1) + nvertices(poly2)
@test area(multi) == area(poly1) + area(poly2)
@test perimeter(multi) == perimeter(poly1) + perimeter(poly2)
- @test centroid(multi) == P2(1, 1)
- @test P2(0.5, 0.5) ∈ multi
- @test P2(1.5, 1.5) ∈ multi
- @test P2(1.5, 0.5) ∉ multi
- @test P2(0.5, 1.5) ∉ multi
+ @test centroid(multi) == cart(1, 1)
+ @test cart(0.5, 0.5) ∈ multi
+ @test cart(1.5, 1.5) ∈ multi
+ @test cart(1.5, 0.5) ∉ multi
+ @test cart(0.5, 1.5) ∉ multi
@test sprint(show, multi) == "Multi(2×PolyArea)"
@test sprint(show, MIME"text/plain"(), multi) == """
- MultiPolyArea{2,$T}
- ├─ PolyArea((0.0, 0.0), ..., (0.0, 1.0))
- └─ PolyArea((1.0, 1.0), ..., (1.0, 2.0))"""
+ MultiPolyArea
+ ├─ PolyArea((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
+ └─ PolyArea((x: 1.0 m, y: 1.0 m), ..., (x: 1.0 m, y: 2.0 m))"""
- box1 = Box(P2(0, 0), P2(1, 1))
- box2 = Box(P2(1, 1), P2(2, 2))
+ box1 = Box(cart(0, 0), cart(1, 1))
+ box2 = Box(cart(1, 1), cart(2, 2))
mbox = Multi([box1, box2])
mchn = boundary(mbox)
noth = boundary(mchn)
@test mchn isa Multi
@test isnothing(noth)
- @test length(mchn) == T(8)
+ @test length(mchn) == T(8) * u"m"
@test sprint(show, mbox) == "Multi(2×Box)"
@test sprint(show, MIME"text/plain"(), mbox) == """
- MultiBox{2,$T}
- ├─ Box(min: (0.0, 0.0), max: (1.0, 1.0))
- └─ Box(min: (1.0, 1.0), max: (2.0, 2.0))"""
+ MultiBox
+ ├─ Box(min: (x: 0.0 m, y: 0.0 m), max: (x: 1.0 m, y: 1.0 m))
+ └─ Box(min: (x: 1.0 m, y: 1.0 m), max: (x: 2.0 m, y: 2.0 m))"""
+
+ box1 = Box(cart(0, 0), cart(1, 1))
+ box2 = Box(cart(1, 1), cart(2, 2))
+ mbox = Multi([box1, box2])
+ equaltest(mbox)
+ isapproxtest(mbox)
# constructor with iterator
- grid = CartesianGrid{T}(10, 10)
+ grid = cartgrid(10, 10)
multi = Multi(grid)
@test parent(multi) == collect(grid)
# boundary of multi-3D-geometry
- box1 = Box(P3(0, 0, 0), P3(1, 1, 1))
- box2 = Box(P3(1, 1, 1), P3(2, 2, 2))
+ box1 = Box(cart(0, 0, 0), cart(1, 1, 1))
+ box2 = Box(cart(1, 1, 1), cart(2, 2, 2))
mbox = Multi([box1, box2])
mesh = boundary(mbox)
@test mesh isa Mesh
@@ -60,27 +68,49 @@
@test nelements(mesh) == 12
# unique vertices
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- quad = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ quad = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
multi = Multi([poly, quad])
@test unique(multi) == multi
@test sprint(show, multi) == "Multi(1×PolyArea, 1×Quadrangle)"
@test sprint(show, MIME"text/plain"(), multi) == """
- MultiPolygon{2,$T}
- ├─ PolyArea((0.0, 0.0), ..., (0.0, 1.0))
- └─ Quadrangle((0.0, 0.0), ..., (0.0, 1.0))"""
+ MultiPolygon
+ ├─ PolyArea((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
+ └─ Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))"""
# type aliases
- point = P2(0, 0)
- segm = Segment(P2(0, 0), P2(1, 1))
- rope = Rope(P2[(0, 0), (1, 0), (1, 1)])
- ring = Ring(P2[(0, 0), (1, 0), (1, 1)])
- tri = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test Multi([point, point]) isa MultiPoint
+ p = cart(0, 0)
+ segm = Segment(cart(0, 0), cart(1, 1))
+ rope = Rope(cart.([(0, 0), (1, 0), (1, 1)]))
+ ring = Ring(cart.([(0, 0), (1, 0), (1, 1)]))
+ tri = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test Multi([p, p]) isa MultiPoint
@test Multi([segm, segm]) isa MultiSegment
@test Multi([rope, rope]) isa MultiRope
@test Multi([ring, ring]) isa MultiRing
@test Multi([tri, tri]) isa MultiPolygon
@test Multi([poly, poly]) isa MultiPolygon
+
+ # CRS propagation
+ poly1 = PolyArea(merc.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ poly2 = PolyArea(merc.([(1, 1), (2, 1), (2, 2), (1, 2)]))
+ multi = Multi([poly1, poly2])
+ @test crs(centroid(multi)) === crs(multi)
+
+ # vertex iteration
+ ring1 = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ ring2 = Ring(cart.([(0, 0), (2, 0), (2, 2), (0, 2)]))
+ ring3 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ ring4 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ poly1 = PolyArea(ring1)
+ poly2 = PolyArea(ring2)
+ poly3 = PolyArea([ring1, ring3])
+ poly4 = PolyArea([ring2, ring4])
+ multi1 = Multi([ring1, ring2, ring3, ring4])
+ multi2 = Multi([poly1, poly2])
+ multi3 = Multi([poly3, poly4])
+ vertextest(multi1)
+ vertextest(multi2)
+ vertextest(multi3)
end
diff --git a/test/neighborhoods.jl b/test/neighborhoods.jl
index 4a6019298..c540b2743 100644
--- a/test/neighborhoods.jl
+++ b/test/neighborhoods.jl
@@ -1,67 +1,75 @@
-@testset "Neighborhoods" begin
- @testset "MetricBall" begin
- # Euclidean metric
- b = MetricBall(T(1 / 2))
- r = radius(b)
- m = metric(b)
- @test evaluate(m, T[0], T[0]) ≤ r
- @test evaluate(m, T[0], T[1]) > r
- @test radii(b) == T[1 / 2]
+@testitem "MetricBall" setup = [Setup] begin
+ # Euclidean metric
+ b = MetricBall(T(1 / 2))
+ r = radius(b)
+ m = metric(b)
+ @test evaluate(m, T[0] * u"m", T[0] * u"m") ≤ r
+ @test evaluate(m, T[0] * u"m", T[1] * u"m") > r
+ @test radii(b) == (T(1 / 2) * u"m",)
- b = MetricBall(T(1))
- r = radius(b)
- m = metric(b)
- @test evaluate(m, T[0, 0], T[0, 0]) ≤ r
- @test evaluate(m, T[0, 0], T[1, 0]) ≤ r
- @test evaluate(m, T[0, 0], T[0, 1]) ≤ r
- @test isisotropic(b)
- @test sprint(show, b) == "MetricBall(1.0, Euclidean)"
+ b = MetricBall(T(1))
+ r = radius(b)
+ m = metric(b)
+ @test evaluate(m, T[0, 0] * u"m", T[0, 0] * u"m") ≤ r
+ @test evaluate(m, T[0, 0] * u"m", T[1, 0] * u"m") ≤ r
+ @test evaluate(m, T[0, 0] * u"m", T[0, 1] * u"m") ≤ r
+ @test isisotropic(b)
+ if T === Float32
+ @test sprint(show, b) == "MetricBall(1.0f0 m, Euclidean)"
+ else
+ @test sprint(show, b) == "MetricBall(1.0 m, Euclidean)"
+ end
- # Chebyshev metric
- b = MetricBall(T(1 / 2), Chebyshev())
- r = radius(b)
- m = metric(b)
- @test evaluate(m, T[0], T[0]) ≤ r
- @test evaluate(m, T[0], T[1]) > r
+ # Chebyshev metric
+ b = MetricBall(T(1 / 2), Chebyshev())
+ r = radius(b)
+ m = metric(b)
+ @test evaluate(m, T[0] * u"m", T[0] * u"m") ≤ r
+ @test evaluate(m, T[0] * u"m", T[1] * u"m") > r
- for r in [1.0, 2.0, 3.0, 4.0, 5.0]
- b = MetricBall(r, Chebyshev())
- r = radius(b)
- m = metric(b)
- for i in 0.0:1.0:r, j in 0.0:1.0:r
- @test evaluate(m, T[0, 0], T[i, j]) ≤ r
- end
+ for r in T[1.0, 2.0, 3.0, 4.0, 5.0]
+ o = MetricBall(r, Chebyshev())
+ r = radius(o)
+ d = metric(o)
+ for i in zero(r):oneunit(r):r, j in zero(r):oneunit(r):r
+ @test evaluate(d, T[0, 0] * u"m", [i, j]) ≤ r
end
+ end
- # 2D simple test of default convention
- m = metric(MetricBall(T.((1, 1))))
- @test evaluate(m, T[1, 0], T[0, 0]) == evaluate(m, T[0, 1], T[0, 0])
+ # 2D simple test of default convention
+ b = MetricBall(T.((1, 1)))
+ m = metric(b)
+ @test radius(b) == oneunit(ℳ)
+ @test evaluate(m, T[1, 0] * u"m", T[0, 0] * u"m") == evaluate(m, T[0, 1] * u"m", T[0, 0] * u"m")
- m = metric(MetricBall(T.((1, 2))))
- @test evaluate(m, T[1, 0], T[0, 0]) != evaluate(m, T[0, 1], T[0, 0])
+ b = MetricBall(T.((1, 2)))
+ m = metric(b)
+ @test radius(b) == oneunit(ℳ)
+ @test evaluate(m, T[1, 0] * u"m", T[0, 0] * u"m") != evaluate(m, T[0, 1] * u"m", T[0, 0] * u"m")
- # 3D simple test of default convention
- m = metric(MetricBall(T.((1.0, 0.5, 0.5)), RotZYX(T(-π / 4), T(0), T(0))))
- @test evaluate(m, [1.0, 1.0, 0.0], [0.0, 0.0, 0.0]) ≈ √T(8)
- @test evaluate(m, [-1.0, 1.0, 0.0], [0.0, 0.0, 0.0]) ≈ √T(2)
+ # 3D simple test of default convention
+ b = MetricBall(T.((1.0, 0.5, 0.5)), RotZYX(T(-π / 4), T(0), T(0)))
+ m = metric(b)
+ @test radius(b) == oneunit(ℳ)
+ @test evaluate(m, T[1.0, 1.0, 0.0] * u"m", T[0.0, 0.0, 0.0] * u"m") ≈ √T(8) * u"m"
+ @test evaluate(m, T[-1.0, 1.0, 0.0] * u"m", T[0.0, 0.0, 0.0] * u"m") ≈ √T(2) * u"m"
- # make sure the correct constructor is called
- m = metric(MetricBall(T[1.0, 0.5, 0.2], RotXYX(T(0), T(0), T(0))))
- @test m isa Mahalanobis
+ # make sure the correct constructor is called
+ m = metric(MetricBall(T.((1.0, 0.5, 0.2)), RotXYX(T(0), T(0), T(0))))
+ @test m isa Mahalanobis
- # make sure the angle is clockwise
- m = metric(MetricBall(T[20.0, 5.0], Angle2d(T(π / 2))))
- @test m isa Mahalanobis
- @test evaluate(m, [1.0, 0.0], [0.0, 0.0]) ≈ T(0.2)
- @test evaluate(m, [0.0, 1.0], [0.0, 0.0]) ≈ T(0.05)
+ # make sure the angle is clockwise
+ m = metric(MetricBall(T.((20.0, 5.0)), Angle2d(T(π / 2))))
+ @test m isa Mahalanobis
+ @test evaluate(m, T[1.0, 0.0] * u"m", T[0.0, 0.0] * u"m") ≈ T(0.2) * u"m"
+ @test evaluate(m, T[0.0, 1.0] * u"m", T[0.0, 0.0] * u"m") ≈ T(0.05) * u"m"
- # basic multiplication
- @test 2MetricBall(T(1)) == MetricBall(T(2))
- @test 2MetricBall(T[1, 2, 3]) == MetricBall(T[2, 4, 6])
+ # basic multiplication
+ @test 2MetricBall(T(1)) == MetricBall(T(2))
+ @test 2MetricBall(T.((1, 2, 3))) == MetricBall(T.((2, 4, 6)))
- # access to rotation
- @test rotation(MetricBall(T(1))) == I
- @test rotation(MetricBall(T[1, 2, 3])) == I
- @test rotation(MetricBall(T[1, 2], Angle2d(T(π / 2)))) == Angle2d(T(π / 2))
- end
+ # access to rotation
+ @test rotation(MetricBall(T(1))) == I
+ @test rotation(MetricBall(T.((1, 2, 3)))) == I
+ @test rotation(MetricBall(T.((1, 2)), Angle2d(T(π / 2)))) == Angle2d(T(π / 2))
end
diff --git a/test/neighborsearch.jl b/test/neighborsearch.jl
index fb493f036..006178d8e 100644
--- a/test/neighborsearch.jl
+++ b/test/neighborsearch.jl
@@ -1,113 +1,150 @@
-@testset "Neighbor search" begin
- @testset "BallSearch" begin
- 𝒟 = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
-
- s = BallSearch(𝒟, MetricBall(T(1)))
- n = search(P2(0, 0), s)
- @test Set(n) == Set([1, 2, 11])
- n = search(P2(9, 0), s)
- @test Set(n) == Set([9, 10, 20])
- n = search(P2(0, 9), s)
- @test Set(n) == Set([91, 81, 92])
- n = search(P2(9, 9), s)
- @test Set(n) == Set([100, 99, 90])
-
- s = BallSearch(𝒟, MetricBall(T(√2 + eps(T))))
- n = search(P2(0, 0), s)
- @test Set(n) == Set([1, 2, 11, 12])
- n = search(P2(9, 0), s)
- @test Set(n) == Set([9, 10, 19, 20])
- n = search(P2(0, 9), s)
- @test Set(n) == Set([81, 82, 91, 92])
- n = search(P2(9, 9), s)
- @test Set(n) == Set([89, 90, 99, 100])
-
- # non MinkowskiMetric example
- 𝒟 = CartesianGrid((360, 180), T.((0.0, -90.0)), T.((1.0, 1.0)))
- s = BallSearch(𝒟, MetricBall(T(150), Haversine(T(6371))))
- n = search(P2(0, 0), s)
- @test Set(n) == Set([32041, 32400, 32401, 32760])
-
- # construct from vector of geometries
- s = BallSearch(rand(P2, 100), MetricBall(T(1)))
- @test s isa BallSearch
- end
-
- @testset "KNearestSearch" begin
- 𝒟 = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
- s = KNearestSearch(𝒟, 3)
- n = search(P2(0, 0), s)
- @test Set(n) == Set([1, 2, 11])
- n = search(P2(9, 0), s)
- @test Set(n) == Set([9, 10, 20])
- n = search(P2(0, 9), s)
- @test Set(n) == Set([91, 81, 92])
- n = search(P2(9, 9), s)
- @test Set(n) == Set([100, 99, 90])
- n, d = searchdists(P2(9, 9), s)
- @test Set(n) == Set([100, 99, 90])
- @test length(d) == 3
- n = Vector{Int}(undef, maxneighbors(s))
- nn = search!(n, P2(9, 9), s)
- @test nn == 3
- @test Set(n[1:nn]) == Set([100, 99, 90])
- n = Vector{Int}(undef, maxneighbors(s))
- d = Vector{T}(undef, maxneighbors(s))
- nn = searchdists!(n, d, P2(9, 9), s)
- @test nn == 3
- @test Set(n[1:nn]) == Set([100, 99, 90])
-
- # construct from vector of geometries
- s = KNearestSearch(rand(P2, 100), 3)
- @test s isa KNearestSearch
- end
-
- @testset "KBallSearch" begin
- 𝒟 = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
-
- s = KBallSearch(𝒟, 10, MetricBall(T(100)))
- n = search(P2(5, 5), s)
- @test length(n) == 10
-
- s = KBallSearch(𝒟, 10, MetricBall(T.((100, 100))))
- n = search(P2(5, 5), s)
- @test length(n) == 10
-
- s = KBallSearch(𝒟, 10, MetricBall(T(1)))
- n = search(P2(5, 5), s)
- @test length(n) == 5
- @test n[1] == 56
-
- s = KBallSearch(𝒟, 10, MetricBall(T(1)))
- n, d = searchdists(P2(5, 5), s)
- @test length(n) == 5
- @test length(d) == 5
-
- s = KBallSearch(𝒟, 10, MetricBall(T(1)))
- n = Vector{Int}(undef, maxneighbors(s))
- nn = search!(n, P2(5, 5), s)
- @test nn == 5
-
- s = KBallSearch(𝒟, 10, MetricBall(T(1)))
- n = Vector{Int}(undef, maxneighbors(s))
- d = Vector{T}(undef, maxneighbors(s))
- nn = searchdists!(n, d, P2(5, 5), s)
- @test nn == 5
-
- mask = trues(nelements(𝒟))
- mask[56] = false
- n = search(P2(5, 5), s, mask=mask)
- @test length(n) == 4
- n = search(P2(-0.2, -0.2), s)
- @test length(n) == 1
- n = search(P2(-10, -10), s)
- @test length(n) == 0
- n, d = searchdists(P2(5, 5), s, mask=mask)
- @test length(n) == 4
- @test length(d) == 4
-
- # construct from vector of geometries
- s = KBallSearch(rand(P2, 100), 10, MetricBall(T(1)))
- @test s isa KBallSearch
- end
+@testitem "BallSearch" setup = [Setup] begin
+ 𝒟 = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+
+ s = BallSearch(𝒟, MetricBall(T(1)))
+ n = search(cart(0, 0), s)
+ @test Set(n) == Set([1, 2, 11])
+ n = search(cart(9, 0), s)
+ @test Set(n) == Set([9, 10, 20])
+ n = search(cart(0, 9), s)
+ @test Set(n) == Set([91, 81, 92])
+ n = search(cart(9, 9), s)
+ @test Set(n) == Set([100, 99, 90])
+
+ s = BallSearch(𝒟, MetricBall(T(√2 + eps(T))))
+ n = search(cart(0, 0), s)
+ @test Set(n) == Set([1, 2, 11, 12])
+ n = search(cart(9, 0), s)
+ @test Set(n) == Set([9, 10, 19, 20])
+ n = search(cart(0, 9), s)
+ @test Set(n) == Set([81, 82, 91, 92])
+ n = search(cart(9, 9), s)
+ @test Set(n) == Set([89, 90, 99, 100])
+
+ # different units
+ s = BallSearch(𝒟, MetricBall(T(10) * u"dm"))
+ n = search(Point(T(900) * u"cm", T(900) * u"cm"), s)
+ @test Set(n) == Set([100, 99, 90])
+ n = search(Point(T(9000) * u"mm", T(9000) * u"mm"), s)
+ @test Set(n) == Set([100, 99, 90])
+
+ # non MinkowskiMetric example
+ 𝒟 = CartesianGrid((360, 180), T.((0.0, -90.0)), T.((1.0, 1.0)))
+ s = BallSearch(𝒟, MetricBall(T(150), Haversine(T(6371))))
+ n = search(cart(0, 0), s)
+ @test Set(n) == Set([32041, 32400, 32401, 32760])
+
+ # construct from vector of geometries
+ s = BallSearch(randpoint2(100), MetricBall(T(1)))
+ @test s isa BallSearch
+
+ # latlon coodinates
+ 𝒟 = RegularGrid((10, 10), latlon(0, 0), T.((1.0, 1.0)))
+ s = BallSearch(𝒟, MetricBall(T(3e5), Haversine()))
+ n = search(latlon(0, 0), s)
+ @test Set(n) == Set([1, 2, 3, 11, 12, 21])
+end
+
+@testitem "KNearestSearch" setup = [Setup] begin
+ 𝒟 = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+ s = KNearestSearch(𝒟, 3)
+ n = search(cart(0, 0), s)
+ @test Set(n) == Set([1, 2, 11])
+ n = search(cart(9, 0), s)
+ @test Set(n) == Set([9, 10, 20])
+ n = search(cart(0, 9), s)
+ @test Set(n) == Set([91, 81, 92])
+ n = search(cart(9, 9), s)
+ @test Set(n) == Set([100, 99, 90])
+ n, d = searchdists(cart(9, 9), s)
+ @test Set(n) == Set([100, 99, 90])
+ @test length(d) == 3
+ n = Vector{Int}(undef, maxneighbors(s))
+ nn = search!(n, cart(9, 9), s)
+ @test nn == 3
+ @test Set(n[1:nn]) == Set([100, 99, 90])
+ n = Vector{Int}(undef, maxneighbors(s))
+ d = Vector{ℳ}(undef, maxneighbors(s))
+ nn = searchdists!(n, d, cart(9, 9), s)
+ @test nn == 3
+ @test Set(n[1:nn]) == Set([100, 99, 90])
+
+ # different units
+ s = KNearestSearch(𝒟, 3)
+ n = search(Point(T(900) * u"cm", T(900) * u"cm"), s)
+ @test Set(n) == Set([100, 99, 90])
+ n = search(Point(T(9000) * u"mm", T(9000) * u"mm"), s)
+ @test Set(n) == Set([100, 99, 90])
+
+ # construct from vector of geometries
+ s = KNearestSearch(randpoint2(100), 3)
+ @test s isa KNearestSearch
+
+ # latlon coodinates
+ 𝒟 = RegularGrid((10, 10), latlon(0, 0), T.((1.0, 1.0)))
+ s = KNearestSearch(𝒟, 3, metric=Haversine())
+ n = search(latlon(0, 0), s)
+ @test Set(n) == Set([1, 2, 11])
+end
+
+@testitem "KBallSearch" setup = [Setup] begin
+ 𝒟 = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+
+ s = KBallSearch(𝒟, 10, MetricBall(T(100)))
+ n = search(cart(5, 5), s)
+ @test length(n) == 10
+
+ s = KBallSearch(𝒟, 10, MetricBall(T.((100, 100))))
+ n = search(cart(5, 5), s)
+ @test length(n) == 10
+
+ s = KBallSearch(𝒟, 10, MetricBall(T(1)))
+ n = search(cart(5, 5), s)
+ @test length(n) == 5
+ @test n[1] == 56
+
+ s = KBallSearch(𝒟, 10, MetricBall(T(1)))
+ n, d = searchdists(cart(5, 5), s)
+ @test length(n) == 5
+ @test length(d) == 5
+
+ s = KBallSearch(𝒟, 10, MetricBall(T(1)))
+ n = Vector{Int}(undef, maxneighbors(s))
+ nn = search!(n, cart(5, 5), s)
+ @test nn == 5
+
+ s = KBallSearch(𝒟, 10, MetricBall(T(1)))
+ n = Vector{Int}(undef, maxneighbors(s))
+ d = Vector{ℳ}(undef, maxneighbors(s))
+ nn = searchdists!(n, d, cart(5, 5), s)
+ @test nn == 5
+
+ mask = trues(nelements(𝒟))
+ mask[56] = false
+ n = search(cart(5, 5), s, mask=mask)
+ @test length(n) == 4
+ n = search(cart(-0.2, -0.2), s)
+ @test length(n) == 1
+ n = search(cart(-10, -10), s)
+ @test length(n) == 0
+ n, d = searchdists(cart(5, 5), s, mask=mask)
+ @test length(n) == 4
+ @test length(d) == 4
+
+ # different units
+ s = KBallSearch(𝒟, 10, MetricBall(T(10) * u"dm"))
+ n = search(Point(T(500) * u"cm", T(500) * u"cm"), s)
+ @test Set(n) == Set([56, 66, 55, 57, 46])
+ n = search(Point(T(5000) * u"mm", T(5000) * u"mm"), s)
+ @test Set(n) == Set([56, 66, 55, 57, 46])
+
+ # construct from vector of geometries
+ s = KBallSearch(randpoint2(100), 10, MetricBall(T(1)))
+ @test s isa KBallSearch
+
+ # latlon coodinates
+ 𝒟 = RegularGrid((10, 10), latlon(0, 0), T.((1.0, 1.0)))
+ s = KBallSearch(𝒟, 10, MetricBall(T(3e5), Haversine()))
+ n = search(latlon(5, 5), s)
+ @test length(n) == 10
end
diff --git a/test/orientation.jl b/test/orientation.jl
index 5fa63451b..a01a835b9 100644
--- a/test/orientation.jl
+++ b/test/orientation.jl
@@ -1,12 +1,18 @@
-@testset "orientation" begin
+@testitem "orientation" setup = [Setup] begin
# test orientation
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
@test orientation(t) == CCW
- t = Triangle(P2(0, 0), P2(0, 1), P2(1, 0))
+ t = Triangle(cart(0, 0), cart(0, 1), cart(1, 0))
@test orientation(t) == CW
# orientation of 3D rings in X-Y plane
- r1 = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- r2 = Ring(P3[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)])
+ r1 = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ r2 = Ring(cart.([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]))
@test orientation(r1) == orientation(r2)
+
+ # orientation of rings with LatLon coordinates
+ r = Ring(latlon.([(0, 0), (0, 90), (90, 0)]))
+ @test orientation(r) == CCW
+ r = Ring(latlon.([(0, 0), (90, 0), (0, 90)]))
+ @test orientation(r) == CW
end
diff --git a/test/partitioning.jl b/test/partitioning.jl
index a15ea3669..34243513e 100644
--- a/test/partitioning.jl
+++ b/test/partitioning.jl
@@ -1,389 +1,370 @@
-@testset "Partitioning" begin
- setify(lists) = Set(Set.(lists))
-
- Random.seed!(123)
- d = CartesianGrid{T}(10, 10)
- p = partition(d, UniformPartition(100))
- @test parent(p) == d
- @test sprint(show, p) == "100 Partition"
- @test sprint(show, MIME"text/plain"(), p) == """
- 100 Partition
- ├─ 1 view(::CartesianGrid{2,$T}, [32])
- ├─ 1 view(::CartesianGrid{2,$T}, [97])
- ├─ 1 view(::CartesianGrid{2,$T}, [3])
- ├─ 1 view(::CartesianGrid{2,$T}, [20])
- ├─ 1 view(::CartesianGrid{2,$T}, [73])
- ⋮
- ├─ 1 view(::CartesianGrid{2,$T}, [89])
- ├─ 1 view(::CartesianGrid{2,$T}, [14])
- ├─ 1 view(::CartesianGrid{2,$T}, [82])
- ├─ 1 view(::CartesianGrid{2,$T}, [78])
- └─ 1 view(::CartesianGrid{2,$T}, [42])"""
-
- @testset "UniformPartition" begin
- Random.seed!(123)
-
- grid = CartesianGrid{T}(3, 3)
- p = partition(grid, UniformPartition(3, false))
- @test setify(indices(p)) == setify([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
- p = partition(grid, UniformPartition(3))
- if VERSION < v"1.7"
- @test setify(indices(p)) == setify([[8, 6, 9], [4, 1, 7], [2, 3, 5]])
- else
- @test setify(indices(p)) == setify([[4, 6, 1], [9, 8, 3], [5, 7, 2]])
- end
-
- grid = CartesianGrid{T}(2, 3)
- p = partition(grid, UniformPartition(3, false))
- @test setify(indices(p)) == setify([[1, 2], [3, 4], [5, 6]])
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, UniformPartition(3))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, UniformPartition(3))
- @test p1 == p2
- end
+@testitem "UniformPartition" setup = [Setup] begin
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p = partition(g, UniformPartition(100))
+ @test parent(p) == g
+ @test length(p) == 100
+
+ rng = StableRNG(123)
+ g = cartgrid(3, 3)
+ p = partition(rng, g, UniformPartition(3, false))
+ @test setify(indices(p)) == setify([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+ rng = StableRNG(123)
+ p = partition(rng, g, UniformPartition(3))
+ @test setify(indices(p)) == setify([[5, 4, 2], [6, 7, 8], [9, 3, 1]])
+
+ g = cartgrid(2, 3)
+ p = partition(g, UniformPartition(3, false))
+ @test setify(indices(p)) == setify([[1, 2], [3, 4], [5, 6]])
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, UniformPartition(3))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, UniformPartition(3))
+ @test p1 == p2
+end
- @testset "DirectionPartition" begin
- grid = CartesianGrid{T}(3, 3)
-
- # basic checks on small regular grid data
- p = partition(grid, DirectionPartition(T.((1, 0))))
- @test setify(indices(p)) == setify([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
-
- p = partition(grid, DirectionPartition(T.((0, 1))))
- @test setify(indices(p)) == setify([[1, 4, 7], [2, 5, 8], [3, 6, 9]])
-
- p = partition(grid, DirectionPartition(T.((1, 1))))
- @test setify(indices(p)) == setify([[1, 5, 9], [2, 6], [3], [4, 8], [7]])
-
- p = partition(grid, DirectionPartition(T.((1, -1))))
- @test setify(indices(p)) == setify([[1], [2, 4], [3, 5, 7], [6, 8], [9]])
-
- # opposite directions produce same partition
- dir1 = (rand(T), rand(T))
- dir2 = .-dir1
- p1 = partition(grid, DirectionPartition(dir1))
- p2 = partition(grid, DirectionPartition(dir2))
- @test setify(indices(p1)) == setify(indices(p2))
-
- # partition of arbitrarily large regular grid always
- # returns the "lines" and "columns" of the grid
- for n in [10, 100, 200]
- grid = CartesianGrid{T}(n, n)
-
- p = partition(grid, DirectionPartition(T.((1, 0))))
- @test setify(indices(p)) == setify([collect(((i - 1) * n + 1):(i * n)) for i in 1:n])
- ns = [nelements(d) for d in p]
- @test all(ns .== n)
-
- p = partition(grid, DirectionPartition(T.((0, 1))))
- @test setify(indices(p)) == setify([collect(i:n:(n * n)) for i in 1:n])
- ns = [nelements(d) for d in p]
- @test all(ns .== n)
- end
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, DirectionPartition(T.((1, 0))))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, DirectionPartition(T.((1, 0))))
- @test p1 == p2
- end
+@testitemm "DirectionPartition" setup = [Setup] begin
+ g = cartgrid(3, 3)
- @testset "FractionPartition" begin
- grid = CartesianGrid{T}(10, 10)
+ # basic checks on small grids
+ p = partition(g, DirectionPartition(T.((1, 0))))
+ @test setify(indices(p)) == setify([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
- p = partition(grid, FractionPartition(T(0.5)))
- @test nelements(p[1]) == nelements(p[2]) == 50
- @test length(p) == 2
+ p = partition(g, DirectionPartition(T.((0, 1))))
+ @test setify(indices(p)) == setify([[1, 4, 7], [2, 5, 8], [3, 6, 9]])
- p = partition(grid, FractionPartition(T(0.7)))
- @test nelements(p[1]) == 70
- @test nelements(p[2]) == 30
+ p = partition(g, DirectionPartition(T.((1, 1))))
+ @test setify(indices(p)) == setify([[1, 5, 9], [2, 6], [3], [4, 8], [7]])
- p = partition(grid, FractionPartition(T(0.3)))
- @test nelements(p[1]) == 30
- @test nelements(p[2]) == 70
+ p = partition(g, DirectionPartition(T.((1, -1))))
+ @test setify(indices(p)) == setify([[1], [2, 4], [3, 5, 7], [6, 8], [9]])
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, FractionPartition(T(0.5)))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, FractionPartition(T(0.5)))
- @test p1 == p2
- end
+ # opposite directions produce same partition
+ dir1 = (rand(T), rand(T))
+ dir2 = .-dir1
+ p1 = partition(g, DirectionPartition(dir1))
+ p2 = partition(g, DirectionPartition(dir2))
+ @test setify(indices(p1)) == setify(indices(p2))
- @testset "BlockPartition" begin
- grid = CartesianGrid{T}(10, 10)
-
- p = partition(grid, BlockPartition(T(5), T(5)))
- @test length(p) == 4
- @test all(nelements.(p) .== 25)
-
- p = partition(grid, BlockPartition(T(5), T(2)))
- @test length(p) == 12
- @test Set(nelements.(p)) == Set([5, 10])
-
- grid = CartesianGrid{T}(50, 50, 50)
-
- p = partition(grid, BlockPartition(T(1.0), T(1.0), T(1.0), neighbors=false))
- @test length(p) == 125000
- @test Set(nelements.(p)) == Set(1)
- @test metadata(p) == Dict{Any,Any}()
-
- p = partition(grid, BlockPartition(T(5.0), T(5.0), T(5.0), neighbors=true))
- @test length(p) == 1000
- @test Set(nelements.(p)) == Set(125)
- n = metadata(p)[:neighbors]
- @test length(n) == length(p)
- @test all(0 .< length.(n) .< 27)
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, BlockPartition(T(5), T(2)))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, BlockPartition(T(5), T(2)))
- @test p1 == p2
-
- m1 = BlockPartition((T(5), T(2)))
- m2 = BlockPartition(T(5), T(2))
- m3 = BlockPartition((T(5), T(2)), neighbors=false)
- m4 = BlockPartition(T(5), T(2), neighbors=false)
- @test m1 == m2 == m3 == m4
- m1 = BlockPartition(T(1))
- m2 = BlockPartition(T(1), neighbors=false)
- @test m1 == m2
- end
+ # partition of arbitrarily large grid always
+ # returns the "lines" and "columns"
+ for n in [10, 100, 200]
+ g = cartgrid(n, n)
- @testset "BisectPointPartition" begin
- grid = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
-
- p = partition(grid, BisectPointPartition(T.((0.0, 1.0)), T.((5.0, 5.1))))
- p1, p2 = p[1], p[2]
- @test nelements(p1) == 60
- @test nelements(p2) == 40
-
- # all points in p1 are below those in p2
- pts1 = [centroid(p1, i) for i in 1:nelements(p1)]
- pts2 = [centroid(p2, i) for i in 1:nelements(p2)]
- X1 = reduce(hcat, coordinates.(pts1))
- X2 = reduce(hcat, coordinates.(pts2))
- M1 = maximum(X1, dims=2)
- m2 = minimum(X2, dims=2)
- @test all(X1[2, j] < m2[2] for j in 1:size(X1, 2))
- @test all(X2[2, j] > M1[2] for j in 1:size(X2, 2))
-
- # flipping normal direction is equivalent to swapping subsets
- p₁ = partition(grid, BisectPointPartition(T.((1.0, 0.0)), T.((5.1, 5.0))))
- p₂ = partition(grid, BisectPointPartition(T.((-1.0, 0.0)), T.((5.1, 5.0))))
- @test nelements(p₁[1]) == nelements(p₂[2]) == 60
- @test nelements(p₁[2]) == nelements(p₂[1]) == 40
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, BisectPointPartition(T.((1, 0)), T.((5, 5))))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, BisectPointPartition(T.((1, 0)), T.((5, 5))))
- @test p1 == p2
- end
+ p = partition(g, DirectionPartition(T.((1, 0))))
+ @test setify(indices(p)) == setify([collect(((i - 1) * n + 1):(i * n)) for i in 1:n])
+ ns = [nelements(d) for d in p]
+ @test all(ns .== n)
- @testset "BisectFractionPartition" begin
- grid = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
-
- p = partition(grid, BisectFractionPartition(T.((1.0, 0.0)), T(0.2)))
- p1, p2 = p[1], p[2]
- @test nelements(p1) == 20
- @test nelements(p2) == 80
-
- # all points in p1 are to the left of p2
- pts1 = [centroid(p1, i) for i in 1:nelements(p1)]
- pts2 = [centroid(p2, i) for i in 1:nelements(p2)]
- X1 = reduce(hcat, coordinates.(pts1))
- X2 = reduce(hcat, coordinates.(pts2))
- M1 = maximum(X1, dims=2)
- m2 = minimum(X2, dims=2)
- @test all(X1[1, j] < m2[1] for j in 1:size(X1, 2))
- @test all(X2[1, j] > M1[1] for j in 1:size(X2, 2))
-
- # flipping normal direction is equivalent to swapping subsets
- p₁ = partition(grid, BisectFractionPartition(T.((1.0, 0.0)), T(0.2)))
- p₂ = partition(grid, BisectFractionPartition(T.((-1.0, 0.0)), T(0.8)))
- @test nelements(p₁[1]) == nelements(p₂[2]) == 20
- @test nelements(p₁[2]) == nelements(p₂[1]) == 80
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, BisectFractionPartition(T.((1, 0)), T(0.5)))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, BisectFractionPartition(T.((1, 0)), T(0.5)))
- @test p1 == p2
+ p = partition(g, DirectionPartition(T.((0, 1))))
+ @test setify(indices(p)) == setify([collect(i:n:(n * n)) for i in 1:n])
+ ns = [nelements(d) for d in p]
+ @test all(ns .== n)
end
- @testset "BallPartition" begin
- pset = PointSet(T[
- 0 1 1 0 0.2
- 0 0 1 1 0.2
- ])
-
- # 3 balls with 1 point, and 1 ball with 2 points
- p = partition(pset, BallPartition(T(0.5)))
- n = nelements.(p)
- @test length(p) == 4
- @test count(i -> i == 1, n) == 3
- @test count(i -> i == 2, n) == 1
- @test setify(indices(p)) == setify([[1, 5], [2], [3], [4]])
-
- # 5 balls with 1 point each
- p = partition(pset, BallPartition(T(0.2)))
- @test length(p) == 5
- @test all(nelements.(p) .== 1)
- @test setify(indices(p)) == setify([[1], [2], [3], [4], [5]])
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, BallPartition(T(2)))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, BallPartition(T(2)))
- @test p1 == p2
- end
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, DirectionPartition(T.((1, 0))))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, DirectionPartition(T.((1, 0))))
+ @test p1 == p2
+end
- @testset "PlanePartition" begin
- grid = CartesianGrid((3, 3), T.((-0.5, -0.5)), T.((1.0, 1.0)))
- p = partition(grid, PlanePartition(T.((0, 1))))
- @test setify(indices(p)) == setify([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
-
- grid = CartesianGrid((4, 4), T.((-0.5, -0.5)), T.((1.0, 1.0)))
- p = partition(grid, PlanePartition(T.((0, 1))))
- @test setify(indices(p)) == setify([1:4, 5:8, 9:12, 13:16])
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, PlanePartition(T.((1, 0))))
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, PlanePartition(T.((1, 0))))
- @test p1 == p2
- end
+@testitem "FractionPartition" setup = [Setup] begin
+ g = cartgrid(10, 10)
- @testset "PredicatePartition" begin
- grid = CartesianGrid((3, 3), T.((-0.5, -0.5)), T.((1.0, 1.0)))
-
- # partition even from odd locations
- pred(i, j) = iseven(i + j)
- partitioner = PredicatePartition(pred)
- p = partition(grid, partitioner)
- @test setify(indices(p)) == setify([1:2:9, 2:2:8])
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, partitioner)
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, partitioner)
- @test p1 == p2
- end
+ p = partition(g, FractionPartition(T(0.5)))
+ @test nelements(p[1]) == nelements(p[2]) == 50
+ @test length(p) == 2
- @testset "SpatialPredicatePartition" begin
- g = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
-
- # check if there are 100 partitions, each one having only 1 point
- sp = SpatialPredicatePartition((x, y) -> norm(x - y) < T(1))
- s = indices(partition(g, sp))
- @test length(s) == 100
- nelms = [nelements(d) for d in partition(g, sp)]
- @test all(nelms .== 1)
-
- # defining a predicate to check if points x and y belong to the square [0.,5.]x[0.,5.]
- pred(x, y) = all(T[0, 0] .<= x .<= T[5, 5]) && all(T[0, 0] .<= y .<= T[5, 5])
- sp = SpatialPredicatePartition(pred)
- p = partition(g, sp)
- s = indices(p)
- n = nelements.(p)
-
- # There will be 65 subsets:
- # 1 subset with 36 points (inside square [0.,5.]x[0.,5.])
- # 64 subsets with only 1 point inside each of them
- @test length(s) == 65
- @test maximum(length.(s)) == 36
- @test count(i -> i == 1, n) == 64
- @test count(i -> i == 36, n) == 1
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, sp)
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, sp)
- @test p1 == p2
- end
+ p = partition(g, FractionPartition(T(0.7)))
+ @test nelements(p[1]) == 70
+ @test nelements(p[2]) == 30
- @testset "ProductPartition" begin
- g = CartesianGrid((100, 100), T.((-0.5, -0.5)), T.((1.0, 1.0)))
- bm = BlockPartition(T(10), T(10))
- bn = BlockPartition(T(5), T(5))
- bmn = ProductPartition(bm, bn)
-
- # Bm x Bn = Bn with m > n
- s1 = indices(partition(g, bmn))
- s2 = indices(partition(g, bn))
- @test setify(s1) == setify(s2)
-
- # pXp=p (for deterministic p)
- for p in [BlockPartition(T(10), T(10)), BisectFractionPartition(T.((0.1, 0.1)))]
- pp = ProductPartition(p, p)
- s1 = indices(partition(g, pp))
- s2 = indices(partition(g, p))
- @test setify(s1) == setify(s2)
- end
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, bmn)
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, bmn)
- @test p1 == p2
- end
+ p = partition(g, FractionPartition(T(0.3)))
+ @test nelements(p[1]) == 30
+ @test nelements(p[2]) == 70
- @testset "HierarchicalPartition" begin
- g = CartesianGrid((100, 100), T.((-0.5, -0.5)), T.((1.0, 1.0)))
- bm = BlockPartition(T(10), T(10))
- bn = BlockPartition(T(5), T(5))
- bmn = HierarchicalPartition(bm, bn)
-
- # Bn -> Bm = Bm with m > n
- s1 = indices(partition(g, bmn))
- s2 = indices(partition(g, bn))
- @test setify(s1) == setify(s2)
-
- # reproducible results with rng
- rng = MersenneTwister(123)
- grid = CartesianGrid{T}(10, 10)
- p1 = partition(rng, grid, bmn)
- rng = MersenneTwister(123)
- p2 = partition(rng, grid, bmn)
- @test p1 == p2
- end
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, FractionPartition(T(0.5)))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, FractionPartition(T(0.5)))
+ @test p1 == p2
+end
+
+@testitem "BlockPartition" setup = [Setup] begin
+ g = cartgrid(10, 10)
+
+ p = partition(g, BlockPartition(T(5), T(5)))
+ @test length(p) == 4
+ @test all(nelements.(p) .== 25)
+
+ p = partition(g, BlockPartition(T(5), T(2)))
+ @test length(p) == 12
+ @test Set(nelements.(p)) == Set([5, 10])
+
+ g = cartgrid(50, 50, 50)
+
+ p = partition(g, BlockPartition(T(1.0), T(1.0), T(1.0), neighbors=false))
+ @test length(p) == 125000
+ @test Set(nelements.(p)) == Set(1)
+ @test metadata(p) == Dict{Any,Any}()
+
+ p = partition(g, BlockPartition(T(5.0), T(5.0), T(5.0), neighbors=true))
+ @test length(p) == 1000
+ @test Set(nelements.(p)) == Set(125)
+ n = metadata(p)[:neighbors]
+ @test length(n) == length(p)
+ @test all(0 .< length.(n) .< 27)
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, BlockPartition(T(5), T(2)))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, BlockPartition(T(5), T(2)))
+ @test p1 == p2
+
+ m1 = BlockPartition((T(5), T(2)))
+ m2 = BlockPartition(T(5), T(2))
+ m3 = BlockPartition((T(5), T(2)), neighbors=false)
+ m4 = BlockPartition(T(5), T(2), neighbors=false)
+ @test m1 == m2 == m3 == m4
+ m1 = BlockPartition(T(1))
+ m2 = BlockPartition(T(1), neighbors=false)
+ @test m1 == m2
+end
- @testset "Mixed Tests" begin
- g = CartesianGrid((100, 100), T.((-0.5, -0.5)), T.((1.0, 1.0)))
- bm = BlockPartition(T(10), T(10))
- bn = BlockPartition(T(5), T(5))
- bmn = ProductPartition(bm, bn)
- hmn = HierarchicalPartition(bm, bn)
-
- # Bm*Bn = Bm->Bn
- s1 = indices(partition(g, bmn))
- s2 = indices(partition(g, hmn))
- @test setify(s1) == setify(s2)
+@testitem "BisectPointPartition" setup = [Setup] begin
+ g = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+
+ p = partition(g, BisectPointPartition(T.((0.0, 1.0)), T.((5.0, 5.1))))
+ p1, p2 = p[1], p[2]
+ @test nelements(p1) == 60
+ @test nelements(p2) == 40
+
+ # all points in p1 are below those in p2
+ pts1 = [centroid(p1, i) for i in 1:nelements(p1)]
+ pts2 = [centroid(p2, i) for i in 1:nelements(p2)]
+ X1 = reduce(hcat, to.(pts1))
+ X2 = reduce(hcat, to.(pts2))
+ M1 = maximum(X1, dims=2)
+ m2 = minimum(X2, dims=2)
+ @test all(X1[2, j] < m2[2] for j in 1:size(X1, 2))
+ @test all(X2[2, j] > M1[2] for j in 1:size(X2, 2))
+
+ # flipping normal direction is equivalent to swapping subsets
+ p₁ = partition(g, BisectPointPartition(T.((1.0, 0.0)), T.((5.1, 5.0))))
+ p₂ = partition(g, BisectPointPartition(T.((-1.0, 0.0)), T.((5.1, 5.0))))
+ @test nelements(p₁[1]) == nelements(p₂[2]) == 60
+ @test nelements(p₁[2]) == nelements(p₂[1]) == 40
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, BisectPointPartition(T.((1, 0)), T.((5, 5))))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, BisectPointPartition(T.((1, 0)), T.((5, 5))))
+ @test p1 == p2
+end
+
+@testitem "BisectFractionPartition" setup = [Setup] begin
+ g = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+
+ p = partition(g, BisectFractionPartition(T.((1.0, 0.0)), T(0.2)))
+ p1, p2 = p[1], p[2]
+ @test nelements(p1) == 20
+ @test nelements(p2) == 80
+
+ # all points in p1 are to the left of p2
+ pts1 = [centroid(p1, i) for i in 1:nelements(p1)]
+ pts2 = [centroid(p2, i) for i in 1:nelements(p2)]
+ X1 = reduce(hcat, to.(pts1))
+ X2 = reduce(hcat, to.(pts2))
+ M1 = maximum(X1, dims=2)
+ m2 = minimum(X2, dims=2)
+ @test all(X1[1, j] < m2[1] for j in 1:size(X1, 2))
+ @test all(X2[1, j] > M1[1] for j in 1:size(X2, 2))
+
+ # flipping normal direction is equivalent to swapping subsets
+ p₁ = partition(g, BisectFractionPartition(T.((1.0, 0.0)), T(0.2)))
+ p₂ = partition(g, BisectFractionPartition(T.((-1.0, 0.0)), T(0.8)))
+ @test nelements(p₁[1]) == nelements(p₂[2]) == 20
+ @test nelements(p₁[2]) == nelements(p₂[1]) == 80
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, BisectFractionPartition(T.((1, 0)), T(0.5)))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, BisectFractionPartition(T.((1, 0)), T(0.5)))
+ @test p1 == p2
+
+ # CRS propagation
+ g = CartesianGrid((10, 10), merc(0, 0), (T(1), T(1)))
+ p = partition(g, BisectFractionPartition(T.((1.0, 0.0)), T(0.2)))
+ @test crs(first(p)) === crs(g)
+end
+
+@testitem "BallPartition" setup = [Setup] begin
+ pset = PointSet(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1), cart(0.2, 0.2))
+
+ # 3 balls with 1 point, and 1 ball with 2 points
+ p = partition(pset, BallPartition(T(0.5)))
+ n = nelements.(p)
+ @test length(p) == 4
+ @test count(i -> i == 1, n) == 3
+ @test count(i -> i == 2, n) == 1
+ @test setify(indices(p)) == setify([[1, 5], [2], [3], [4]])
+
+ # 5 balls with 1 point each
+ p = partition(pset, BallPartition(T(0.2)))
+ @test length(p) == 5
+ @test all(nelements.(p) .== 1)
+ @test setify(indices(p)) == setify([[1], [2], [3], [4], [5]])
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, BallPartition(T(2)))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, BallPartition(T(2)))
+ @test p1 == p2
+end
+
+@testitem "PlanePartition" setup = [Setup] begin
+ g = CartesianGrid((3, 3), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+ p = partition(g, PlanePartition(T.((0, 1))))
+ @test setify(indices(p)) == setify([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+
+ g = CartesianGrid((4, 4), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+ p = partition(g, PlanePartition(T.((0, 1))))
+ @test setify(indices(p)) == setify([1:4, 5:8, 9:12, 13:16])
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, PlanePartition(T.((1, 0))))
+ rng = StableRNG(123)
+ p2 = partition(rng, g, PlanePartition(T.((1, 0))))
+ @test p1 == p2
+end
+
+@testitem "PredicatePartition" setup = [Setup] begin
+ g = CartesianGrid((3, 3), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+
+ # partition even from odd locations
+ pred(i, j) = iseven(i + j)
+ partitioner = PredicatePartition(pred)
+ p = partition(g, partitioner)
+ @test setify(indices(p)) == setify([1:2:9, 2:2:8])
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, partitioner)
+ rng = StableRNG(123)
+ p2 = partition(rng, g, partitioner)
+ @test p1 == p2
+end
+
+@testitem "SpatialPredicatePartition" setup = [Setup] begin
+ g = CartesianGrid((10, 10), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+
+ # check if there are 100 partitions, each one having only 1 point
+ sp = SpatialPredicatePartition((x, y) -> norm(x - y) < T(1) * u"m")
+ s = indices(partition(g, sp))
+ @test length(s) == 100
+ nelms = [nelements(d) for d in partition(g, sp)]
+ @test all(nelms .== 1)
+
+ # defining a predicate to check if points x and y belong to the square [0.,5.]x[0.,5.]
+ pred(x, y) = all(T[0, 0] * u"m" .<= x .<= T[5, 5] * u"m") && all(T[0, 0] * u"m" .<= y .<= T[5, 5] * u"m")
+ sp = SpatialPredicatePartition(pred)
+ p = partition(g, sp)
+ s = indices(p)
+ n = nelements.(p)
+
+ # There will be 65 subsets:
+ # 1 subset with 36 points (inside square [0.,5.]x[0.,5.])
+ # 64 subsets with only 1 point inside each of them
+ @test length(s) == 65
+ @test maximum(length.(s)) == 36
+ @test count(i -> i == 1, n) == 64
+ @test count(i -> i == 36, n) == 1
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, sp)
+ rng = StableRNG(123)
+ p2 = partition(rng, g, sp)
+ @test p1 == p2
+end
+
+@testitem "ProductPartition" setup = [Setup] begin
+ g = CartesianGrid((100, 100), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+ bm = BlockPartition(T(10), T(10))
+ bn = BlockPartition(T(5), T(5))
+ bmn = ProductPartition(bm, bn)
+
+ # Bm x Bn = Bn with m > n
+ s1 = indices(partition(g, bmn))
+ s2 = indices(partition(g, bn))
+ @test setify(s1) == setify(s2)
+
+ # pXp=p (for deterministic p)
+ for p in [BlockPartition(T(10), T(10)), BisectFractionPartition(T.((0.1, 0.1)))]
+ pp = ProductPartition(p, p)
+ i1 = indices(partition(g, pp))
+ i2 = indices(partition(g, p))
+ @test setify(i1) == setify(i2)
end
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, bmn)
+ rng = StableRNG(123)
+ p2 = partition(rng, g, bmn)
+ @test p1 == p2
+end
+
+@testitem "HierarchicalPartition" setup = [Setup] begin
+ g = CartesianGrid((100, 100), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+ bm = BlockPartition(T(10), T(10))
+ bn = BlockPartition(T(5), T(5))
+ bmn = HierarchicalPartition(bm, bn)
+
+ # Bn -> Bm = Bm with m > n
+ s1 = indices(partition(g, bmn))
+ s2 = indices(partition(g, bn))
+ @test setify(s1) == setify(s2)
+
+ # reproducible results with rng
+ rng = StableRNG(123)
+ g = cartgrid(10, 10)
+ p1 = partition(rng, g, bmn)
+ rng = StableRNG(123)
+ p2 = partition(rng, g, bmn)
+ @test p1 == p2
+end
+
+@testitem "Misc partition" setup = [Setup] begin
+ g = CartesianGrid((100, 100), T.((-0.5, -0.5)), T.((1.0, 1.0)))
+ bm = BlockPartition(T(10), T(10))
+ bn = BlockPartition(T(5), T(5))
+ bmn = ProductPartition(bm, bn)
+ hmn = HierarchicalPartition(bm, bn)
+
+ # Bm*Bn = Bm->Bn
+ s1 = indices(partition(g, bmn))
+ s2 = indices(partition(g, hmn))
+ @test setify(s1) == setify(s2)
end
diff --git a/test/pointification.jl b/test/pointification.jl
index 533b7fc28..54db36c35 100644
--- a/test/pointification.jl
+++ b/test/pointification.jl
@@ -1,50 +1,62 @@
-@testset "Pointification" begin
- point = P2(0, 0)
- @test pointify(point) == [P2(0, 0)]
+@testitem "Pointification" setup = [Setup] begin
+ p = cart(0, 0)
+ @test pointify(p) == [cart(0, 0)]
- sphere = Sphere(P2(0, 0), T(1))
+ sphere = Sphere(cart(0, 0), T(1))
points = pointify(sphere)
@test all(∈(sphere), points)
- ball = Ball(P2(0, 0), T(1))
+ ball = Ball(cart(0, 0), T(1))
points = pointify(ball)
@test all(∈(boundary(ball)), points)
- verts = [P2(0, 0), P2(1, 1)]
+ verts = [cart(0, 0), cart(1, 1)]
segment = Segment(verts...)
points = pointify(segment)
@test points == verts
- verts = [P2(0, 0), P2(1, 0), P2(1, 1)]
+ verts = [cart(0, 0), cart(1, 0), cart(1, 1)]
triangle = Triangle(verts...)
points = pointify(triangle)
@test points == verts
- verts = [P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1)]
+ verts = [cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1)]
quadrangle = Quadrangle(verts...)
points = pointify(quadrangle)
@test points == verts
- tri = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- quad = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
+ tri = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ quad = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
multi = Multi([tri, quad])
points = pointify(multi)
@test points == [pointify(tri); pointify(quad)]
- tri = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- quad = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
+ box = Box(cart(0, 0), cart(1, 1))
+ trans = Translate(T(1), T(2))
+ tbox = TransformedGeometry(box, trans)
+ points = pointify(tbox)
+ @test points == trans.(pointify(box))
+
+ box = Box(latlon(0, 0), latlon(45, 45))
+ trans = Proj(Mercator)
+ tbox = TransformedGeometry(box, trans)
+ points = pointify(tbox)
+ @test points == pointify(discretize(tbox))
+
+ tri = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ quad = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
gset = GeometrySet([tri, quad])
points = pointify(gset)
@test points == [pointify(tri); pointify(quad)]
- pts = rand(P2, 100)
+ pts = randpoint2(100)
pset = PointSet(pts)
@test pointify(pset) == pts
- grid = CartesianGrid{T}(10, 10)
+ grid = cartgrid(10, 10)
@test pointify(grid) == vertices(grid)
- grid = CartesianGrid{T}(10, 10)
+ grid = cartgrid(10, 10)
mesh = convert(SimpleMesh, grid)
points = pointify(mesh)
@test points == vertices(mesh)
diff --git a/test/polytopes.jl b/test/polytopes.jl
index 31ea94c6a..2a50b8baa 100644
--- a/test/polytopes.jl
+++ b/test/polytopes.jl
@@ -1,572 +1,613 @@
-@testset "Polytopes" begin
- @testset "Segments" begin
- @test paramdim(Segment) == 1
- @test nvertices(Segment) == 2
-
- s = Segment(P1(1.0), P1(2.0))
- @test vertex(s, 1) == P1(1.0)
- @test vertex(s, 2) == P1(2.0)
- @test all(P1(x) ∈ s for x in 1:0.01:2)
- @test all(P1(x) ∉ s for x in [-1.0, 0.0, 0.99, 2.1, 5.0, 10.0])
- @test s ≈ s
- @test !(s ≈ Segment(P1(2.0), P1(1.0)))
- @test !(s ≈ Segment(P1(-1.0), P1(2.0)))
-
- s = Segment(P2(0, 0), P2(1, 1))
- @test minimum(s) == P2(0, 0)
- @test maximum(s) == P2(1, 1)
- @test extrema(s) == (P2(0, 0), P2(1, 1))
- @test isapprox(length(s), sqrt(T(2)))
- @test s(T(0)) == P2(0, 0)
- @test s(T(1)) == P2(1, 1)
- @test all(P2(x, x) ∈ s for x in 0:0.01:1)
- @test all(p ∉ s for p in [P2(-0.1, -0.1), P2(1.1, 1.1), P2(0.5, 0.49), P2(1, 2)])
- @test_throws DomainError(T(1.2), "s(t) is not defined for t outside [0, 1].") s(T(1.2))
- @test_throws DomainError(T(-0.5), "s(t) is not defined for t outside [0, 1].") s(T(-0.5))
- @test s ≈ s
- @test !(s ≈ Segment(P2(1, 1), P2(0, 0)))
- @test !(s ≈ Segment(P2(1, 2), P2(0, 0)))
-
- s = Segment(P3(0, 0, 0), P3(1, 1, 1))
- @test all(P3(x, x, x) ∈ s for x in 0:0.01:1)
- @test all(p ∉ s for p in [P3(-0.1, -0.1, -0.1), P3(1.1, 1.1, 1.1)])
- @test all(p ∉ s for p in [P3(0.5, 0.5, 0.49), P3(1, 1, 2)])
- @test s ≈ s
- @test !(s ≈ Segment(P3(1, 1, 1), P3(0, 0, 0)))
- @test !(s ≈ Segment(P3(1, 1, 1), P3(0, 1, 0)))
-
- s = Segment(Point(1.0, 1.0, 1.0, 1.0), Point(2.0, 2.0, 2.0, 2.0))
- @test all(Point(x, x, x, x) ∈ s for x in 1:0.01:2)
- @test all(p ∉ s for p in [Point(0.99, 0.99, 0.99, 0.99), Point(2.1, 2.1, 2.1, 2.1)])
- @test all(p ∉ s for p in [Point(1.5, 1.5, 1.5, 1.49), Point(1, 1, 2, 1.0)])
- @test s ≈ s
- @test !(s ≈ Segment(Point(2, 2, 2, 2), Point(1, 1, 1, 1)))
- @test !(s ≈ Segment(Point(1, 1, 2, 1), Point(0, 0, 0, 0)))
-
- s = Segment(P3(0, 0, 0), P3(1, 1, 1))
- @test boundary(s) == Multi([P3(0, 0, 0), P3(1, 1, 1)])
- @test perimeter(s) == zero(T)
- @test center(s) == P3(0.5, 0.5, 0.5)
- @test coordtype(center(s)) == T
-
- # unitful coordinates
- x1 = T(0)u"m"
- x2 = T(1)u"m"
- s = Segment(Point(x1, x1, x1), Point(x2, x2, x2))
- @test boundary(s) == Multi([Point(x1, x1, x1), Point(x2, x2, x2)])
- @test perimeter(s) == 0u"m"
- xm = T(0.5)u"m"
- @test center(s) == Point(xm, xm, xm)
- @test coordtype(center(s)) == typeof(xm)
-
- s = rand(Segment{2,T})
- @test s isa Segment
- @test embeddim(s) == 2
- @test coordtype(s) === T
- s = rand(Segment{3,T})
- @test s isa Segment
- @test embeddim(s) == 3
- @test coordtype(s) === T
-
- s = Segment(P2(0, 0), P2(1, 1))
- @test sprint(show, s) == "Segment((0.0, 0.0), (1.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), s) == """
- Segment{2,Float32}
- ├─ Point(0.0f0, 0.0f0)
- └─ Point(1.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), s) == """
- Segment{2,Float64}
- ├─ Point(0.0, 0.0)
- └─ Point(1.0, 1.0)"""
- end
+@testitem "Segments" setup = [Setup] begin
+ @test paramdim(Segment) == 1
+ @test nvertices(Segment) == 2
+
+ s = Segment(cart(1.0), cart(2.0))
+ @test crs(s) <: Cartesian{NoDatum}
+ @test Meshes.lentype(s) == ℳ
+ @test vertex(s, 1) == cart(1.0)
+ @test vertex(s, 2) == cart(2.0)
+ @test all(cart(x) ∈ s for x in 1:0.01:2)
+ @test all(cart(x) ∉ s for x in [-1.0, 0.0, 0.99, 2.1, 5.0, 10.0])
+ @test s ≈ s
+ @test !(s ≈ Segment(cart(2.0), cart(1.0)))
+ @test !(s ≈ Segment(cart(-1.0), cart(2.0)))
+ @test reverse(s) == Segment(cart(2.0), cart(1.0))
+
+ s = Segment(cart(0, 0), cart(1, 1))
+ @test minimum(s) == cart(0, 0)
+ @test maximum(s) == cart(1, 1)
+ @test extrema(s) == (cart(0, 0), cart(1, 1))
+ @test isapprox(length(s), sqrt(T(2)) * u"m")
+ @test s(T(0)) == cart(0, 0)
+ @test s(T(1)) == cart(1, 1)
+ @test all(cart(x, x) ∈ s for x in 0:0.01:1)
+ @test all(p ∉ s for p in [cart(-0.1, -0.1), cart(1.1, 1.1), cart(0.5, 0.49), cart(1, 2)])
+ @test_throws DomainError(T(1.2), "s(t) is not defined for t outside [0, 1].") s(T(1.2))
+ @test_throws DomainError(T(-0.5), "s(t) is not defined for t outside [0, 1].") s(T(-0.5))
+ @test s ≈ s
+ @test !(s ≈ Segment(cart(1, 1), cart(0, 0)))
+ @test !(s ≈ Segment(cart(1, 2), cart(0, 0)))
+ @test reverse(s) == Segment(cart(1, 1), cart(0, 0))
+
+ s = Segment(cart(0, 0, 0), cart(1, 1, 1))
+ @test all(cart(x, x, x) ∈ s for x in 0:0.01:1)
+ @test all(p ∉ s for p in [cart(-0.1, -0.1, -0.1), cart(1.1, 1.1, 1.1)])
+ @test all(p ∉ s for p in [cart(0.5, 0.5, 0.49), cart(1, 1, 2)])
+ @test s ≈ s
+ @test !(s ≈ Segment(cart(1, 1, 1), cart(0, 0, 0)))
+ @test !(s ≈ Segment(cart(1, 1, 1), cart(0, 1, 0)))
+ @test reverse(s) == Segment(cart(1, 1, 1), cart(0, 0, 0))
+
+ s = Segment(cart(0, 0), cart(1, 1))
+ equaltest(s)
+ isapproxtest(s)
+ vertextest(s)
+
+ s = Segment(Point(1.0, 1.0, 1.0, 1.0), Point(2.0, 2.0, 2.0, 2.0))
+ @test all(Point(x, x, x, x) ∈ s for x in 1:0.01:2)
+ @test all(p ∉ s for p in [Point(0.99, 0.99, 0.99, 0.99), Point(2.1, 2.1, 2.1, 2.1)])
+ @test all(p ∉ s for p in [Point(1.5, 1.5, 1.5, 1.49), Point(1, 1, 2, 1.0)])
+ @test s ≈ s
+ @test !(s ≈ Segment(Point(2, 2, 2, 2), Point(1, 1, 1, 1)))
+ @test !(s ≈ Segment(Point(1, 1, 2, 1), Point(0, 0, 0, 0)))
+
+ s = Segment(cart(0, 0, 0), cart(1, 1, 1))
+ @test boundary(s) == Multi([cart(0, 0, 0), cart(1, 1, 1)])
+ @test perimeter(s) == zero(T) * u"m"
+ @test centroid(s) == cart(0.5, 0.5, 0.5)
+ @test Meshes.lentype(centroid(s)) == ℳ
+
+ # unitful coordinates
+ x1 = T(0)u"m"
+ x2 = T(1)u"m"
+ s = Segment(Point(x1, x1, x1), Point(x2, x2, x2))
+ @test boundary(s) == Multi([Point(x1, x1, x1), Point(x2, x2, x2)])
+ @test perimeter(s) == 0u"m"
+ xm = T(0.5)u"m"
+ @test centroid(s) == Point(xm, xm, xm)
+ @test Meshes.lentype(centroid(s)) == typeof(xm)
+
+ # CRS propagation
+ s = Segment(merc(0, 0), merc(1, 1))
+ @test crs(s(T(0))) === crs(s)
+
+ # measure
+ s = Segment(merc(0, 0), merc(1, 1))
+ @test measure(s) ≈ T(√2) * u"m"
+ s = Segment(latlon(0, 45), latlon(0, 135))
+ r = majoraxis(ellipsoid(datum(crs(s))))
+ C = 2 * T(π) * ℳ(r)
+ @test measure(s) ≈ C / 4
+ # TODO: fix measure of segments on the globe
+ s = Segment(latlon(0, 135), latlon(0, 45))
+ @test_broken measure(s) ≈ 3C / 4
+
+ # parameterization
+ s = Segment(latlon(45, 0), latlon(45, 90))
+ @test s(T(0)) == latlon(45, 0)
+ @test s(T(0.25)) == latlon(45, 22.5)
+ @test s(T(0.5)) == latlon(45, 45)
+ @test s(T(0.75)) == latlon(45, 67.5)
+ @test s(T(1)) == latlon(45, 90)
+
+ s = Segment(cart(0, 0), cart(1, 1))
+ @test sprint(show, s) == "Segment((x: 0.0 m, y: 0.0 m), (x: 1.0 m, y: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), s) == """
+ Segment
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m)
+ └─ Point(x: 1.0f0 m, y: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), s) == """
+ Segment
+ ├─ Point(x: 0.0 m, y: 0.0 m)
+ └─ Point(x: 1.0 m, y: 1.0 m)"""
+ end
+end
+
+@testitem "Chains" setup = [Setup] begin
+ c1 = Rope(cart.([(1, 1), (2, 2)]))
+ c2 = Rope(cart(1, 1), cart(2, 2))
+ c3 = Rope(T.((1, 1.0)), T.((2.0, 2.0)))
+ @test c1 == c2 == c3
+ c1 = Ring(cart.([(1, 1), (2, 2)]))
+ c2 = Ring(cart(1, 1), cart(2, 2))
+ c3 = Ring(T.((1, 1.0)), T.((2.0, 2.0)))
+ @test c1 == c2 == c3
+
+ c = Rope(cart(0, 0), cart(1, 0), cart(0, 1))
+ equaltest(c)
+ isapproxtest(c)
+ vertextest(c)
+
+ c = Ring(cart(0, 0), cart(1, 0), cart(0, 1))
+ equaltest(c)
+ isapproxtest(c)
+ vertextest(c)
+
+ # circular equality
+ c1 = Ring(cart.([(1, 1), (2, 2), (3, 3)]))
+ c2 = Ring(cart.([(2, 2), (3, 3), (1, 1)]))
+ c3 = Ring(cart.([(3, 3), (1, 1), (2, 2)]))
+ @test c1 ≗ c2 ≗ c3
+
+ c = Rope(cart.([(1, 1), (2, 2)]))
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test vertex(c, 1) == cart(1, 1)
+ @test vertex(c, 2) == cart(2, 2)
+ c = Ring(cart.([(1, 1), (2, 2)]))
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test vertex(c, 0) == cart(2, 2)
+ @test vertex(c, 1) == cart(1, 1)
+ @test vertex(c, 2) == cart(2, 2)
+ @test vertex(c, 3) == cart(1, 1)
+ @test vertex(c, 4) == cart(2, 2)
+
+ c = Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+ @test collect(segments(c)) == [Segment(cart(1, 1), cart(2, 2)), Segment(cart(2, 2), cart(3, 3))]
+ c = Ring(cart.([(1, 1), (2, 2), (3, 3)]))
+ @test collect(segments(c)) ==
+ [Segment(cart(1, 1), cart(2, 2)), Segment(cart(2, 2), cart(3, 3)), Segment(cart(3, 3), cart(1, 1))]
+
+ c = Rope(cart.([(1, 1), (2, 2), (2, 2), (3, 3)]))
+ @test unique(c) == Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+ @test c == Rope(cart.([(1, 1), (2, 2), (2, 2), (3, 3)]))
+ unique!(c)
+ @test c == Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+
+ c = Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+ @test close(c) == Ring(cart.([(1, 1), (2, 2), (3, 3)]))
+ c = Ring(cart.([(1, 1), (2, 2), (3, 3)]))
+ @test open(c) == Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+
+ c = Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+ reverse!(c)
+ @test c == Rope(cart.([(3, 3), (2, 2), (1, 1)]))
+ c = Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+ @test reverse(c) == Rope(cart.([(3, 3), (2, 2), (1, 1)]))
+
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test angles(c) ≈ [-π / 2, -π / 2, -π / 2, -π / 2]
+ c = Rope(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test angles(c) ≈ [-π / 2, -π / 2]
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2)]))
+ @test angles(c) ≈ [-atan(2), -π / 2, +π / 2, -π / 2, -π / 2, -(π - atan(2))]
+ @test innerangles(c) ≈ [atan(2), π / 2, 3π / 2, π / 2, π / 2, π - atan(2)]
+
+ c1 = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ c2 = Ring(vertices(c1))
+ @test c1 == c2
+
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test centroid(c) == cart(0.5, 0.5)
+
+ c = Rope(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test boundary(c) == Multi(cart.([(0, 0), (0, 1)]))
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test isnothing(boundary(c))
+
+ # degenerate rings with 1 or 2 vertices are allowed
+ r = Ring(cart.([(0, 0)]))
+ @test isclosed(r)
+ @test nvertices(r) == 1
+ @test collect(segments(r)) == [Segment(cart(0, 0), cart(0, 0))]
+ r = Ring(cart.([(0, 0), (1, 1)]))
+ @test isclosed(r)
+ @test nvertices(r) == 2
+ @test collect(segments(r)) == [Segment(cart(0, 0), cart(1, 1)), Segment(cart(1, 1), cart(0, 0))]
+
+ p1 = cart(1, 1)
+ p2 = cart(3, 1)
+ p3 = cart(1, 0)
+ p4 = cart(3, 0)
+ pts = cart.([(0, 0), (2, 2), (4, 0)])
+ r = Ring(pts)
+ @test p1 ∈ r
+ @test p2 ∈ r
+ @test p3 ∈ r
+ @test p4 ∈ r
+ r = Rope(pts)
+ @test p1 ∈ r
+ @test p2 ∈ r
+ @test p3 ∉ r
+ @test p4 ∉ r
+
+ # approximately equal vertices
+ pts =
+ cart.(
+ [
+ (-48.04448403189499, -18.326530800015174)
+ (-48.044478457836675, -18.326503670869467)
+ (-48.04447845783733, -18.326503670869915)
+ (-48.04447835073269, -18.326503149587666)
+ (-48.044468448930644, -18.326490894176693)
+ (-48.04447208741723, -18.326486301018672)
+ (-48.044459173572015, -18.32646700775326)
+ (-48.04445616736389, -18.326461847186216)
+ (-48.044459897846174, -18.326466190774774)
+ (-48.044462696066695, -18.32646303439271)
+ (-48.044473299571635, -18.326478565399572)
+ (-48.044473299571635, -18.326478565399565)
+ (-48.044484052460334, -18.326494315209573)
+ (-48.04449288424675, -18.326504598503668)
+ (-48.044492356262886, -18.32650647783081)
+ (-48.0444943180541, -18.326509351276243)
+ (-48.044492458690776, -18.32651322842786)
+ (-48.04450917793127, -18.326524641668517)
+ (-48.044501408820125, -18.326551273900744)
+ ]
+ )
+ r1 = Rope(pts)
+ r2 = Ring(pts)
+ ur1 = unique(r1)
+ ur2 = unique(r2)
+ @test nvertices(ur1) < nvertices(r1)
+ @test nvertices(ur2) < nvertices(r2)
+ if T === Float32
+ @test nvertices(ur1) == 10
+ @test nvertices(ur2) == 10
+ else
+ @test nvertices(ur1) == 17
+ @test nvertices(ur2) == 17
end
- @testset "Ropes/Rings" begin
- c1 = Rope(P2[(1, 1), (2, 2)])
- c2 = Rope(P2(1, 1), P2(2, 2))
- c3 = Rope(T.((1, 1.0)), T.((2.0, 2.0)))
- @test c1 == c2 == c3
- c1 = Ring(P2[(1, 1), (2, 2)])
- c2 = Ring(P2(1, 1), P2(2, 2))
- c3 = Ring(T.((1, 1.0)), T.((2.0, 2.0)))
- @test c1 == c2 == c3
-
- c = Rope(P2[(1, 1), (2, 2)])
- @test vertex(c, 1) == P2(1, 1)
- @test vertex(c, 2) == P2(2, 2)
- c = Ring(P2[(1, 1), (2, 2)])
- @test vertex(c, 0) == P2(2, 2)
- @test vertex(c, 1) == P2(1, 1)
- @test vertex(c, 2) == P2(2, 2)
- @test vertex(c, 3) == P2(1, 1)
- @test vertex(c, 4) == P2(2, 2)
-
- c = Rope(P2[(1, 1), (2, 2), (3, 3)])
- @test collect(segments(c)) == [Segment(P2(1, 1), P2(2, 2)), Segment(P2(2, 2), P2(3, 3))]
- c = Ring(P2[(1, 1), (2, 2), (3, 3)])
- @test collect(segments(c)) ==
- [Segment(P2(1, 1), P2(2, 2)), Segment(P2(2, 2), P2(3, 3)), Segment(P2(3, 3), P2(1, 1))]
-
- c = Rope(P2[(1, 1), (2, 2), (2, 2), (3, 3)])
- @test unique(c) == Rope(P2[(1, 1), (2, 2), (3, 3)])
- @test c == Rope(P2[(1, 1), (2, 2), (2, 2), (3, 3)])
- unique!(c)
- @test c == Rope(P2[(1, 1), (2, 2), (3, 3)])
-
- c = Rope(P2[(1, 1), (2, 2), (3, 3)])
- @test close(c) == Ring(P2[(1, 1), (2, 2), (3, 3)])
- c = Ring(P2[(1, 1), (2, 2), (3, 3)])
- @test open(c) == Rope(P2[(1, 1), (2, 2), (3, 3)])
-
- c = Rope(P2[(1, 1), (2, 2), (3, 3)])
- reverse!(c)
- @test c == Rope(P2[(3, 3), (2, 2), (1, 1)])
- c = Rope(P2[(1, 1), (2, 2), (3, 3)])
- @test reverse(c) == Rope(P2[(3, 3), (2, 2), (1, 1)])
-
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test angles(c) ≈ [-π / 2, -π / 2, -π / 2, -π / 2]
- c = Rope(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test angles(c) ≈ [-π / 2, -π / 2]
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2)])
- @test angles(c) ≈ [-atan(2), -π / 2, +π / 2, -π / 2, -π / 2, -(π - atan(2))]
- @test innerangles(c) ≈ [atan(2), π / 2, 3π / 2, π / 2, π / 2, π - atan(2)]
-
- c1 = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- c2 = Ring(vertices(c1))
- @test c1 == c2
-
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test centroid(c) == P2(0.5, 0.5)
-
- c = Rope(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test boundary(c) == Multi(P2[(0, 0), (0, 1)])
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test isnothing(boundary(c))
-
- # should not repeat the first vertex manually
- @test_throws ArgumentError Ring(P2[(0, 0), (0, 0)])
- @test_throws ArgumentError Ring(P2[(0, 0), (1, 0), (1, 1), (0, 0)])
-
- # degenerate rings with 1 or 2 vertices are allowed
- r = Ring(P2[(0, 0)])
- @test isclosed(r)
- @test nvertices(r) == 1
- @test collect(segments(r)) == [Segment(P2(0, 0), P2(0, 0))]
- r = Ring(P2[(0, 0), (1, 1)])
- @test isclosed(r)
- @test nvertices(r) == 2
- @test collect(segments(r)) == [Segment(P2(0, 0), P2(1, 1)), Segment(P2(1, 1), P2(0, 0))]
-
- p1 = P2(1, 1)
- p2 = P2(3, 1)
- p3 = P2(1, 0)
- p4 = P2(3, 0)
- pts = P2[(0, 0), (2, 2), (4, 0)]
- r = Ring(pts)
- @test p1 ∈ r
- @test p2 ∈ r
- @test p3 ∈ r
- @test p4 ∈ r
- r = Rope(pts)
- @test p1 ∈ r
- @test p2 ∈ r
- @test p3 ∉ r
- @test p4 ∉ r
-
- # approximately equal vertices
- pts = P2[
- (-48.04448403189499, -18.326530800015174)
- (-48.044478457836675, -18.326503670869467)
- (-48.04447845783733, -18.326503670869915)
- (-48.04447835073269, -18.326503149587666)
- (-48.044468448930644, -18.326490894176693)
- (-48.04447208741723, -18.326486301018672)
- (-48.044459173572015, -18.32646700775326)
- (-48.04445616736389, -18.326461847186216)
- (-48.044459897846174, -18.326466190774774)
- (-48.044462696066695, -18.32646303439271)
- (-48.044473299571635, -18.326478565399572)
- (-48.044473299571635, -18.326478565399565)
- (-48.044484052460334, -18.326494315209573)
- (-48.04449288424675, -18.326504598503668)
- (-48.044492356262886, -18.32650647783081)
- (-48.0444943180541, -18.326509351276243)
- (-48.044492458690776, -18.32651322842786)
- (-48.04450917793127, -18.326524641668517)
- (-48.044501408820125, -18.326551273900744)
- ]
- r1 = Rope(pts)
- r2 = Ring(pts)
- ur1 = unique(r1)
- ur2 = unique(r2)
- @test nvertices(ur1) < nvertices(r1)
- @test nvertices(ur2) < nvertices(r2)
- if T === Float32
- @test nvertices(ur1) == 10
- @test nvertices(ur2) == 10
- else
- @test nvertices(ur1) == 17
- @test nvertices(ur2) == 17
- end
-
- r = rand(Rope{2,T})
- @test r isa Rope
- @test embeddim(r) == 2
- @test coordtype(r) === T
- r = rand(Rope{3,T})
- @test r isa Rope
- @test embeddim(r) == 3
- @test coordtype(r) === T
-
- r = rand(Ring{2,T})
- @test r isa Ring
- @test embeddim(r) == 2
- @test coordtype(r) === T
- r = rand(Ring{3,T})
- @test r isa Ring
- @test embeddim(r) == 3
- @test coordtype(r) === T
-
- # issimple benchmark
- r = Sphere(P2(0, 0), T(1)) |> pointify |> Ring
- @test issimple(r)
- @test @elapsed(issimple(r)) < 0.02
- @test @allocated(issimple(r)) < 950000
-
- # innerangles in 3D is obtained via projection
- r1 = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- r2 = Ring(P3[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)])
- @test innerangles(r1) ≈ innerangles(r2)
-
- ri = Ring(P2[(1, 1), (2, 2), (3, 3)])
- ro = Rope(P2[(1, 1), (2, 2), (3, 3)])
- @test sprint(show, ri) == "Ring((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))"
- @test sprint(show, ro) == "Rope((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), ri) == """
- Ring{2,Float32}
- ├─ Point(1.0f0, 1.0f0)
- ├─ Point(2.0f0, 2.0f0)
- └─ Point(3.0f0, 3.0f0)"""
- @test sprint(show, MIME("text/plain"), ro) == """
- Rope{2,Float32}
- ├─ Point(1.0f0, 1.0f0)
- ├─ Point(2.0f0, 2.0f0)
- └─ Point(3.0f0, 3.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), ri) == """
- Ring{2,Float64}
- ├─ Point(1.0, 1.0)
- ├─ Point(2.0, 2.0)
- └─ Point(3.0, 3.0)"""
- @test sprint(show, MIME("text/plain"), ro) == """
- Rope{2,Float64}
- ├─ Point(1.0, 1.0)
- ├─ Point(2.0, 2.0)
- └─ Point(3.0, 3.0)"""
- end
+ # issimple benchmark
+ r = Sphere(cart(0, 0), T(1)) |> pointify |> Ring
+ @test issimple(r)
+ @test @elapsed(issimple(r)) < 0.02
+ @test @allocated(issimple(r)) < 950000
+
+ # CRS propagation
+ r = Ring(merc.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test crs(centroid(r)) === crs(r)
+
+ ri = Ring(cart.([(1, 1), (2, 2), (3, 3)]))
+ ro = Rope(cart.([(1, 1), (2, 2), (3, 3)]))
+ @test sprint(show, ri) == "Ring((x: 1.0 m, y: 1.0 m), (x: 2.0 m, y: 2.0 m), (x: 3.0 m, y: 3.0 m))"
+ @test sprint(show, ro) == "Rope((x: 1.0 m, y: 1.0 m), (x: 2.0 m, y: 2.0 m), (x: 3.0 m, y: 3.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), ri) == """
+ Ring
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ ├─ Point(x: 2.0f0 m, y: 2.0f0 m)
+ └─ Point(x: 3.0f0 m, y: 3.0f0 m)"""
+ @test sprint(show, MIME("text/plain"), ro) == """
+ Rope
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ ├─ Point(x: 2.0f0 m, y: 2.0f0 m)
+ └─ Point(x: 3.0f0 m, y: 3.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), ri) == """
+ Ring
+ ├─ Point(x: 1.0 m, y: 1.0 m)
+ ├─ Point(x: 2.0 m, y: 2.0 m)
+ └─ Point(x: 3.0 m, y: 3.0 m)"""
+ @test sprint(show, MIME("text/plain"), ro) == """
+ Rope
+ ├─ Point(x: 1.0 m, y: 1.0 m)
+ ├─ Point(x: 2.0 m, y: 2.0 m)
+ └─ Point(x: 3.0 m, y: 3.0 m)"""
end
+end
- @testset "Ngons" begin
- pts = (P2(0, 0), P2(1, 0), P2(0, 1))
- tups = (T.((0, 0)), T.((1, 0)), T.((0, 1)))
- @test paramdim(Ngon) == 2
- @test vertices(Ngon(pts)) == pts
- @test vertices(Ngon(pts...)) == pts
- @test vertices(Ngon(tups...)) == pts
- @test vertices(Ngon{3}(pts)) == pts
- @test vertices(Ngon{3}(pts...)) == pts
- @test vertices(Ngon{3}(tups...)) == pts
-
- NGONS = [Triangle, Quadrangle, Pentagon, Hexagon, Heptagon, Octagon, Nonagon, Decagon]
- NVERT = 3:10
- for (i, NGON) in enumerate(NGONS)
- @test paramdim(NGON) == 2
- @test nvertices(NGON) == NVERT[i]
-
- n = rand(NGON{2,T})
- @test n isa NGON
- @test embeddim(n) == 2
- @test coordtype(n) === T
- n = rand(NGON{3,T})
- @test n isa NGON
- @test embeddim(n) == 3
- @test coordtype(n) === T
- end
-
- # error: the number of vertices must be greater than or equal to 3
- @test_throws ArgumentError Ngon(P2(0, 0), P2(1, 1))
- @test_throws ArgumentError Ngon{2}(P2(0, 0), P2(1, 1))
-
- # ---------
- # TRIANGLE
- # ---------
-
- # Triangle in 2D space
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test vertex(t, 1) == P2(0, 0)
- @test vertex(t, 2) == P2(1, 0)
- @test vertex(t, 3) == P2(0, 1)
- @test signarea(t) == T(0.5)
- @test area(t) == T(0.5)
- t = Triangle(P2(0, 0), P2(0, 1), P2(1, 0))
- @test signarea(t) == T(-0.5)
- @test area(t) == T(0.5)
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- for p in P2[(0, 0), (1, 0), (1, 1), (0.5, 0.0), (1.0, 0.5), (0.5, 0.5)]
- @test p ∈ t
- end
- for p in P2[(-1, 0), (0, -1), (0.5, 1.0)]
- @test p ∉ t
- end
- t = Triangle(P2(0.4, 0.4), P2(0.6, 0.4), P2(0.8, 0.4))
- @test P2(0.2, 0.4) ∉ t
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test t(T(0.0), T(0.0)) == P2(0, 0)
- @test t(T(1.0), T(0.0)) == P2(1, 0)
- @test t(T(0.0), T(1.0)) == P2(0, 1)
- @test t(T(0.5), T(0.5)) == P2(0.5, 0.5)
- @test_throws DomainError((T(-0.5), T(0.0)), "invalid barycentric coordinates for triangle.") t(T(-0.5), T(0.0))
- @test_throws DomainError((T(1), T(1)), "invalid barycentric coordinates for triangle.") t(T(1), T(1))
- @test !hasholes(t)
- @test unique(t) == t
- @test boundary(t) == first(rings(t))
- @test rings(t) == [Ring(P2(0, 0), P2(1, 0), P2(0, 1))]
- @test convexhull(t) == t
-
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test perimeter(t) ≈ T(1 + 1 + √2)
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/333
- t = Triangle((0.0f0, 0.0f0), (1.0f0, 0.0f0), (0.5f0, 1.0f0))
- @test Point(0.5f0, 0.5f0) ∈ t
- @test Point(0.5e0, 0.5e0) ∈ t
-
- # point at edge of triangle
- @test P2(3, 1) ∈ Triangle(P2(1, 1), P2(5, 1), P2(3, 3))
-
- # test angles
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test all(isapprox.(rad2deg.(angles(t)), T[-90, -45, -45], atol=8 * eps(T)))
- @test all(isapprox.(rad2deg.(innerangles(t)), T[90, 45, 45], atol=8 * eps(T)))
-
- # Triangle in 3D space
- t = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
- @test area(t) == T(0.5)
- t = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 1))
- @test area(t) > T(0.7)
- for p in P3[(0, 0, 0), (1, 0, 0), (0, 1, 1), (0, 0.2, 0.2)]
- @test p ∈ t
- end
- for p in P3[(-1, 0, 0), (1, 2, 0), (0, 1, 2)]
- @test p ∉ t
- end
- t = Triangle(P3(0, 0, 0), P3(0, 1, 0), P3(0, 0, 1))
- @test t(T(0.0), T(0.0)) == P3(0, 0, 0)
- @test t(T(1.0), T(0.0)) == P3(0, 1, 0)
- @test t(T(0.0), T(1.0)) == P3(0, 0, 1)
- @test t(T(0.5), T(0.5)) == P3(0, 0.5, 0.5)
- @test_throws DomainError((T(-0.5), T(0.0)), "invalid barycentric coordinates for triangle.") t(T(-0.5), T(0.0))
- @test_throws DomainError((T(1), T(1)), "invalid barycentric coordinates for triangle.") t(T(1), T(1))
- @test isapprox(normal(t), V3(0.5, 0, 0))
- t = Triangle(P3(0, 0, 0), P3(2, 0, 0), P3(0, 2, 2))
- @test isapprox(normal(t), V3(0, -2, 2))
-
- t = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
- @test_throws ErrorException("signed area only defined for triangles embedded in R², use `area` instead") signarea(t)
-
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test sprint(show, t) == "Triangle((0.0, 0.0), (1.0, 0.0), (0.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), t) == """
- Triangle{2,Float32}
- ├─ Point(0.0f0, 0.0f0)
- ├─ Point(1.0f0, 0.0f0)
- └─ Point(0.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), t) == """
- Triangle{2,Float64}
- ├─ Point(0.0, 0.0)
- ├─ Point(1.0, 0.0)
- └─ Point(0.0, 1.0)"""
- end
-
- # -----------
- # QUADRANGLE
- # -----------
-
- # test periodicity of Quadrangle
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
-
- # Quadrangle in 2D space
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test vertex(q, 1) == P2(0, 0)
- @test vertex(q, 2) == P2(1, 0)
- @test vertex(q, 3) == P2(1, 1)
- @test vertex(q, 4) == P2(0, 1)
- @test area(q) == T(1)
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1.5, 1.0), P2(0.5, 1.0))
- @test area(q) == T(1)
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1.5, 1.0), P2(0.5, 1.0))
- for p in P2[(0, 0), (1, 0), (1.5, 1.0), (0.5, 1.0), (0.5, 0.5)]
- @test p ∈ q
- end
- for p in P2[(0, 1), (1.5, 0.0)]
- @test p ∉ q
- end
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test !hasholes(q)
- @test unique(q) == q
- @test boundary(q) == first(rings(q))
- @test rings(q) == [Ring(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))]
- @test q(T(0), T(0)) == P2(0, 0)
- @test q(T(1), T(0)) == P2(1, 0)
- @test q(T(1), T(1)) == P2(1, 1)
- @test q(T(0), T(1)) == P2(0, 1)
-
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test_throws DomainError((T(1.2), T(1.2)), "q(u, v) is not defined for u, v outside [0, 1]².") q(T(1.2), T(1.2))
-
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test perimeter(q) ≈ T(4)
-
- # Quadrangle in 3D space
- q = Quadrangle(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0))
- @test area(q) == T(1)
- q = Quadrangle(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 1))
- @test area(q) > T(1)
- @test q(T(0), T(0)) == P3(0, 0, 0)
- @test q(T(1), T(0)) == P3(1, 0, 0)
- @test q(T(1), T(1)) == P3(1, 1, 0)
- @test q(T(0), T(1)) == P3(0, 1, 1)
-
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test sprint(show, q) == "Quadrangle((0.0, 0.0), ..., (0.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), q) == """
- Quadrangle{2,Float32}
- ├─ Point(0.0f0, 0.0f0)
- ├─ Point(1.0f0, 0.0f0)
- ├─ Point(1.0f0, 1.0f0)
- └─ Point(0.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), q) == """
- Quadrangle{2,Float64}
- ├─ Point(0.0, 0.0)
- ├─ Point(1.0, 0.0)
- ├─ Point(1.0, 1.0)
- └─ Point(0.0, 1.0)"""
- end
+@testitem "Ngons" setup = [Setup] begin
+ pts = (cart(0, 0), cart(1, 0), cart(0, 1))
+ tups = (T.((0, 0)), T.((1, 0)), T.((0, 1)))
+ verts = SVector(pts)
+ @test paramdim(Ngon) == 2
+ @test vertices(Ngon(pts)) == verts
+ @test vertices(Ngon(pts...)) == verts
+ @test vertices(Ngon(tups...)) == verts
+ @test vertices(Ngon{3}(pts)) == verts
+ @test vertices(Ngon{3}(pts...)) == verts
+ @test vertices(Ngon{3}(tups...)) == verts
+
+ NGONS = [Triangle, Quadrangle, Pentagon, Hexagon, Heptagon, Octagon, Nonagon, Decagon]
+ NVERT = 3:10
+ for (i, NGON) in enumerate(NGONS)
+ @test paramdim(NGON) == 2
+ @test nvertices(NGON) == NVERT[i]
end
- @testset "PolyAreas" begin
- @test paramdim(PolyArea) == 2
-
- # equality and approximate equality
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly = PolyArea([outer, hole1, hole2])
- @test poly == poly
- @test poly ≈ poly
-
- # outer chain with 2 vertices is fixed by default
- poly = PolyArea(P2[(0, 0), (1, 0)])
- @test rings(poly) == [Ring(P2[(0, 0), (0.5, 0.0), (1, 0)])]
-
- # inner chain with 2 vertices is removed by default
- poly = PolyArea([P2[(0, 0), (1, 0), (1, 1), (0, 1)], P2[(1, 2), (2, 3)]])
- @test rings(poly) == [Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])]
-
- # orientation of chains is fixed by default
- poly = PolyArea(P2[(0, 0), (0, 1), (1, 1), (1, 0)])
- @test vertices(poly) == CircularVector(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- poly = PolyArea(P2[(0, 0), (0, 1), (1, 1), (1, 0)], fix=false)
- @test vertices(poly) == CircularVector(P2[(0, 0), (0, 1), (1, 1), (1, 0)])
-
- # test accessor methods
- poly = PolyArea(P2[(1, 2), (2, 3)], fix=false)
- @test vertices(poly) == CircularVector(P2[(1, 2), (2, 3)])
- poly = PolyArea([P2[(1, 2), (2, 3)], P2[(1.1, 2.54), (1.4, 1.5)]], fix=false)
- @test vertices(poly) == CircularVector(P2[(1, 2), (2, 3), (1.1, 2.54), (1.4, 1.5)])
-
- # COMMAND USED TO GENERATE TEST FILES (VARY --seed = 1, 2, ..., 5)
- # rpg --cluster 30 --algo 2opt --format line --seed 1 --output poly1
- fnames = ["poly$i.line" for i in 1:5]
- polys1 = [readpoly(T, joinpath(datadir, fname)) for fname in fnames]
- for poly in polys1
- @test !hasholes(poly)
- @test issimple(poly)
- @test boundary(poly) == first(rings(poly))
- @test nvertices(poly) == 30
- for algo in [WindingOrientation(), TriangleOrientation()]
- @test orientation(poly, algo) == CCW
- end
- @test unique(poly) == poly
- end
-
- # COMMAND USED TO GENERATE TEST FILES (VARY --seed = 1, 2, ..., 5)
- # rpg --cluster 30 --algo 2opt --format line --seed 1 --output smooth1 --smooth 2
- fnames = ["smooth$i.line" for i in 1:5]
- polys2 = [readpoly(T, joinpath(datadir, fname)) for fname in fnames]
- for poly in polys2
- @test !hasholes(poly)
- @test issimple(poly)
- @test boundary(poly) == first(rings(poly))
- @test nvertices(poly) == 120
- for algo in [WindingOrientation(), TriangleOrientation()]
- @test orientation(poly, algo) == CCW
- end
- @test unique(poly) == poly
- end
-
- # COMMAND USED TO GENERATE TEST FILES (VARY --seed = 1, 2, ..., 5)
- # rpg --cluster 30 --algo 2opt --format line --seed 1 --output hole1 --holes 2
- fnames = ["hole$i.line" for i in 1:5]
- polys3 = [readpoly(T, joinpath(datadir, fname)) for fname in fnames]
- for poly in polys3
- rs = rings(poly)
- @test hasholes(poly)
- @test !issimple(poly)
- @test boundary(poly) == Multi(rs)
- @test nvertices(first(rs)) < 30
- @test all(nvertices.(rs[2:end]) .< 18)
- for algo in [WindingOrientation(), TriangleOrientation()]
- orients = orientation(poly, algo)
- @test orients[1] == CCW
- @test all(orients[2:end] .== CW)
- end
- @test unique(poly) == poly
- end
-
- # test bridges
- for poly in [polys1; polys2; polys3]
- b = poly |> Bridge()
- nb = nvertices(b)
- np = nvertices.(rings(poly))
- @test nb ≥ sum(np)
- # triangle orientation always works even
- # in the presence of self-intersections
- @test orientation(b, TriangleOrientation()) == CCW
- # winding orientation is only suitable
- # for simple polygonal chains
- if issimple(b)
- @test orientation(b, WindingOrientation()) == CCW
- end
- end
-
- # test uniqueness
- points = P2[(1, 1), (2, 2), (2, 2), (3, 3)]
- poly = PolyArea(points)
- unique!(poly)
- @test first(rings(poly)) == Ring(P2[(1, 1), (2, 2), (3, 3)])
-
- # approximately equal vertices
- poly = PolyArea(
- P2[
+ # error: the number of vertices must be greater than or equal to 3
+ @test_throws ArgumentError Ngon(cart(0, 0), cart(1, 1))
+ @test_throws ArgumentError Ngon{2}(cart(0, 0), cart(1, 1))
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ # Triangle in 2D space
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test crs(t) <: Cartesian{NoDatum}
+ @test Meshes.lentype(t) == ℳ
+ @test vertex(t, 1) == cart(0, 0)
+ @test vertex(t, 2) == cart(1, 0)
+ @test vertex(t, 3) == cart(0, 1)
+ @test area(t) == T(0.5) * u"m^2"
+ t = Triangle(cart(0, 0), cart(0, 1), cart(1, 0))
+ @test area(t) == T(0.5) * u"m^2"
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ for p in cart.([(0, 0), (1, 0), (1, 1), (0.5, 0.0), (1.0, 0.5), (0.5, 0.5)])
+ @test p ∈ t
+ end
+ for p in cart.([(-1, 0), (0, -1), (0.5, 1.0)])
+ @test p ∉ t
+ end
+ t = Triangle(cart(0.4, 0.4), cart(0.6, 0.4), cart(0.8, 0.4))
+ @test cart(0.2, 0.4) ∉ t
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test t(T(0.0), T(0.0)) == cart(0, 0)
+ @test t(T(1.0), T(0.0)) == cart(1, 0)
+ @test t(T(0.0), T(1.0)) == cart(0, 1)
+ @test t(T(0.5), T(0.5)) == cart(0.5, 0.5)
+ @test_throws DomainError((T(-0.5), T(0.0)), "invalid barycentric coordinates for triangle.") t(T(-0.5), T(0.0))
+ @test_throws DomainError((T(1), T(1)), "invalid barycentric coordinates for triangle.") t(T(1), T(1))
+ @test !hasholes(t)
+ @test unique(t) == t
+ @test boundary(t) == first(rings(t))
+ @test rings(t) == [Ring(cart(0, 0), cart(1, 0), cart(0, 1))]
+ @test convexhull(t) == t
+
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ equaltest(t)
+ isapproxtest(t)
+ vertextest(t)
+
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test perimeter(t) ≈ T(1 + 1 + √2) * u"m"
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/333
+ t = Triangle((0.0f0, 0.0f0), (1.0f0, 0.0f0), (0.5f0, 1.0f0))
+ @test Point(0.5f0, 0.5f0) ∈ t
+ @test Point(0.5e0, 0.5e0) ∈ t
+
+ # circular equality
+ t1 = Triangle(T.((1, 1)), T.((2, 2)), T.((3, 3)))
+ t2 = Triangle(T.((2, 2)), T.((3, 3)), T.((1, 1)))
+ t3 = Triangle(T.((3, 3)), T.((1, 1)), T.((2, 2)))
+ @test t1 ≗ t2 ≗ t3
+
+ # point at edge of triangle
+ @test cart(3, 1) ∈ Triangle(cart(1, 1), cart(5, 1), cart(3, 3))
+
+ # test angles
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test all(isapprox.(rad2deg.(angles(t)), T[-90, -45, -45] * u"°", atol=8 * eps(T)))
+ @test all(isapprox.(rad2deg.(innerangles(t)), T[90, 45, 45] * u"°", atol=8 * eps(T)))
+
+ # Triangle in 3D space
+ t = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0))
+ @test area(t) == T(0.5) * u"m^2"
+ t = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 1))
+ @test area(t) > T(0.7) * u"m^2"
+ for p in cart.([(0, 0, 0), (1, 0, 0), (0, 1, 1), (0, 0.2, 0.2)])
+ @test p ∈ t
+ end
+ for p in cart.([(-1, 0, 0), (1, 2, 0), (0, 1, 2)])
+ @test p ∉ t
+ end
+ t = Triangle(cart(0, 0, 0), cart(0, 1, 0), cart(0, 0, 1))
+ @test t(T(0.0), T(0.0)) == cart(0, 0, 0)
+ @test t(T(1.0), T(0.0)) == cart(0, 1, 0)
+ @test t(T(0.0), T(1.0)) == cart(0, 0, 1)
+ @test t(T(0.5), T(0.5)) == cart(0, 0.5, 0.5)
+ @test_throws DomainError((T(-0.5), T(0.0)), "invalid barycentric coordinates for triangle.") t(T(-0.5), T(0.0))
+ @test_throws DomainError((T(1), T(1)), "invalid barycentric coordinates for triangle.") t(T(1), T(1))
+ @test isapprox(normal(t), vector(1, 0, 0))
+ @test isapprox(norm(normal(t)), oneunit(ℳ))
+ t = Triangle(cart(0, 0, 0), cart(2, 0, 0), cart(0, 2, 2))
+ @test isapprox(normal(t), vector(0, -0.7071067811865475, 0.7071067811865475))
+ @test isapprox(norm(normal(t)), oneunit(ℳ))
+
+ # CRS propagation
+ t = Triangle(merc(0, 0), merc(1, 0), merc(0, 1))
+ @test crs(t(T(0), T(0))) === crs(t)
+
+ # parameterization
+ t = Triangle(latlon(0, 0), latlon(0, 45), latlon(45, 0))
+ @test t(T(0), T(0)) == latlon(0, 0)
+ @test t(T(0.5), T(0)) == latlon(0, 22.5)
+ @test t(T(1), T(0)) == latlon(0, 45)
+ @test t(T(0), T(0.5)) == latlon(22.5, 0)
+ @test t(T(0), T(1)) == latlon(45, 0)
+
+ # centroid
+ t = Triangle(latlon(0, 0), latlon(0, 45), latlon(45, 0))
+ @test centroid(t) == latlon(15, 15)
+
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test sprint(show, t) == "Triangle((x: 0.0 m, y: 0.0 m), (x: 1.0 m, y: 0.0 m), (x: 0.0 m, y: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), t) == """
+ Triangle
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m)
+ └─ Point(x: 0.0f0 m, y: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), t) == """
+ Triangle
+ ├─ Point(x: 0.0 m, y: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m)
+ └─ Point(x: 0.0 m, y: 1.0 m)"""
+ end
+
+ # -----------
+ # QUADRANGLE
+ # -----------
+
+ # Quadrangle in 2D space
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test crs(q) <: Cartesian{NoDatum}
+ @test Meshes.lentype(q) == ℳ
+ @test vertex(q, 1) == cart(0, 0)
+ @test vertex(q, 2) == cart(1, 0)
+ @test vertex(q, 3) == cart(1, 1)
+ @test vertex(q, 4) == cart(0, 1)
+ @test area(q) == T(1) * u"m^2"
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1.5, 1.0), cart(0.5, 1.0))
+ @test area(q) == T(1) * u"m^2"
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1.5, 1.0), cart(0.5, 1.0))
+ for p in cart.([(0, 0), (1, 0), (1.5, 1.0), (0.5, 1.0), (0.5, 0.5)])
+ @test p ∈ q
+ end
+ for p in cart.([(0, 1), (1.5, 0.0)])
+ @test p ∉ q
+ end
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test !hasholes(q)
+ @test unique(q) == q
+ @test boundary(q) == first(rings(q))
+ @test rings(q) == [Ring(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))]
+ @test q(T(0), T(0)) == cart(0, 0)
+ @test q(T(1), T(0)) == cart(1, 0)
+ @test q(T(1), T(1)) == cart(1, 1)
+ @test q(T(0), T(1)) == cart(0, 1)
+
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ equaltest(q)
+ isapproxtest(q)
+ vertextest(q)
+
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test_throws DomainError((T(1.2), T(1.2)), "q(u, v) is not defined for u, v outside [0, 1]².") q(T(1.2), T(1.2))
+
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test perimeter(q) ≈ T(4) * u"m"
+
+ # Quadrangle in 3D space
+ q = Quadrangle(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0))
+ @test area(q) == T(1) * u"m^2"
+ q = Quadrangle(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 1))
+ @test area(q) > T(1) * u"m^2"
+ @test q(T(0), T(0)) == cart(0, 0, 0)
+ @test q(T(1), T(0)) == cart(1, 0, 0)
+ @test q(T(1), T(1)) == cart(1, 1, 0)
+ @test q(T(0), T(1)) == cart(0, 1, 1)
+
+ # CRS propagation
+ q = Quadrangle(merc(0, 0), merc(1, 0), merc(1, 1), merc(0, 1))
+ @test crs(q(T(0), T(0))) === crs(q)
+
+ # parameterization
+ q = Quadrangle(latlon(0, 0), latlon(0, 45), latlon(45, 45), latlon(45, 0))
+ @test q(T(0), T(0)) == latlon(0, 0)
+ @test q(T(0.5), T(0)) == latlon(0, 22.5)
+ @test q(T(1), T(0)) == latlon(0, 45)
+ @test q(T(1), T(0.5)) == latlon(22.5, 45)
+ @test q(T(1), T(1)) == latlon(45, 45)
+ @test q(T(0.5), T(1)) == latlon(45, 22.5)
+ @test q(T(0), T(1)) == latlon(45, 0)
+ @test q(T(0), T(0.5)) == latlon(22.5, 0)
+
+ # centroid
+ q = Quadrangle(latlon(0, 0), latlon(0, 45), latlon(45, 45), latlon(45, 0))
+ @test centroid(q) == latlon(22.5, 22.5)
+
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test sprint(show, q) == "Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), q) == """
+ Quadrangle
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ └─ Point(x: 0.0f0 m, y: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), q) == """
+ Quadrangle
+ ├─ Point(x: 0.0 m, y: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 1.0 m)
+ └─ Point(x: 0.0 m, y: 1.0 m)"""
+ end
+end
+
+@testitem "PolyAreas" setup = [Setup] begin
+ @test paramdim(PolyArea) == 2
+
+ # equality and approximate equality
+ outer = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+ hole1 = cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)])
+ hole2 = cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)])
+ poly = PolyArea([outer, hole1, hole2])
+ @test poly == poly
+ @test poly ≈ poly
+ @test crs(poly) <: Cartesian{NoDatum}
+ @test Meshes.lentype(poly) == ℳ
+
+ p = PolyArea(cart(0, 0), cart(1, 0), cart(0, 1))
+ equaltest(p)
+ isapproxtest(p)
+ vertextest(p)
+
+ # COMMAND USED TO GENERATE TEST FILES (VARY --seed = 1, 2, ..., 5)
+ # rpg --cluster 30 --algo 2opt --format line --seed 1 --output poly1
+ fnames = ["poly$i.line" for i in 1:5]
+ polys1 = [readpoly(T, joinpath(datadir, fname)) for fname in fnames]
+ for poly in polys1
+ @test !hasholes(poly)
+ @test issimple(poly)
+ @test boundary(poly) == first(rings(poly))
+ @test nvertices(poly) == 30
+ @test orientation(poly) == CCW
+ @test unique(poly) == poly
+ end
+
+ # COMMAND USED TO GENERATE TEST FILES (VARY --seed = 1, 2, ..., 5)
+ # rpg --cluster 30 --algo 2opt --format line --seed 1 --output smooth1 --smooth 2
+ fnames = ["smooth$i.line" for i in 1:5]
+ polys2 = [readpoly(T, joinpath(datadir, fname)) for fname in fnames]
+ for poly in polys2
+ @test !hasholes(poly)
+ @test issimple(poly)
+ @test boundary(poly) == first(rings(poly))
+ @test nvertices(poly) == 120
+ @test orientation(poly) == CCW
+ @test unique(poly) == poly
+ end
+
+ # COMMAND USED TO GENERATE TEST FILES (VARY --seed = 1, 2, ..., 5)
+ # rpg --cluster 30 --algo 2opt --format line --seed 1 --output hole1 --holes 2
+ fnames = ["hole$i.line" for i in 1:5]
+ polys3 = [readpoly(T, joinpath(datadir, fname)) for fname in fnames]
+ for poly in polys3
+ rs = rings(poly)
+ @test hasholes(poly)
+ @test !issimple(poly)
+ @test boundary(poly) == Multi(rs)
+ @test nvertices(first(rs)) < 30
+ @test all(nvertices.(rs[2:end]) .< 18)
+ o = orientation(poly)
+ @test o[1] == CCW
+ @test all(o[2:end] .== CW)
+ @test unique(poly) == poly
+ end
+
+ # test bridges
+ for poly in [polys1; polys2; polys3]
+ b = poly |> Bridge()
+ nb = nvertices(b)
+ np = nvertices.(rings(poly))
+ @test nb ≥ sum(np)
+ # orientation always works even
+ # in the presence of self-intersections
+ @test orientation(b) == CCW
+ end
+
+ # test uniqueness
+ points = cart.([(1, 1), (2, 2), (2, 2), (3, 3)])
+ poly = PolyArea(points)
+ unique!(poly)
+ @test first(rings(poly)) == Ring(cart.([(1, 1), (2, 2), (3, 3)]))
+
+ # approximately equal vertices
+ poly = PolyArea(
+ cart.(
+ [
(-48.04448403189499, -18.326530800015174)
(-48.044478457836675, -18.326503670869467)
(-48.04447845783733, -18.326503670869915)
@@ -588,240 +629,367 @@
(-48.044501408820125, -18.326551273900744)
]
)
- upoly = unique(poly)
- @test nvertices(upoly) < nvertices(poly)
- if T === Float32
- @test nvertices(upoly) == 10
- else
- @test nvertices(upoly) == 17
- end
-
- # invalid inner
- outer = rand(P2, 10)
- v1, v2 = rand(V2, 2)
- inner = [Point(v1), Point(v1), Point(v2)]
- poly = PolyArea([outer, inner])
- upoly = unique(poly)
- @test hasholes(poly)
- @test !hasholes(upoly)
-
- # centroid
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test centroid(poly) == P2(0.5, 0.5)
-
- # single vertex access
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- @test vertex(poly, 1) == P2(0, 0)
- @test vertex(poly, 4) == P2(0, 1)
-
- # point in polygonal area
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly = PolyArea([outer, hole1, hole2])
- @test all(p ∈ poly for p in outer)
- @test P2(0.5, 0.5) ∈ poly
- @test P2(0.2, 0.6) ∈ poly
- @test P2(1.5, 0.5) ∉ poly
- @test P2(-0.5, 0.5) ∉ poly
- @test P2(0.25, 0.25) ∉ poly
- @test P2(0.75, 0.25) ∉ poly
- @test P2(0.75, 0.75) ∈ poly
-
- # area
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly = PolyArea([outer, hole1, hole2])
- @test area(poly) ≈ T(0.92)
-
- p = rand(PolyArea{2,T})
- @test p isa PolyArea
- @test embeddim(p) == 2
- @test coordtype(p) === T
- p = rand(PolyArea{3,T})
- @test p isa PolyArea
- @test embeddim(p) == 3
- @test coordtype(p) === T
-
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly1 = PolyArea(outer)
- poly2 = PolyArea([outer, hole1, hole2])
- @test sprint(show, poly1) == "PolyArea((0.0, 0.0), ..., (0.0, 1.0))"
- @test sprint(show, poly2) == "PolyArea(4-Ring, 4-Ring, 4-Ring)"
- @test sprint(show, MIME("text/plain"), poly1) == """
- PolyArea{2,$T}
- outer
- └─ Ring((0.0, 0.0), ..., (0.0, 1.0))"""
- @test sprint(show, MIME("text/plain"), poly2) == """
- PolyArea{2,$T}
- outer
- └─ Ring((0.0, 0.0), ..., (0.0, 1.0))
- inner
- ├─ Ring((0.2, 0.2), ..., (0.4, 0.2))
- └─ Ring((0.6, 0.2), ..., (0.8, 0.2))"""
-
- # should not repeat the first vertex manually
- @test_throws ArgumentError PolyArea(P2[(0, 0), (0, 0)])
- @test_throws ArgumentError PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 0)])
+ )
+ upoly = unique(poly)
+ @test nvertices(upoly) < nvertices(poly)
+ if T === Float32
+ @test nvertices(upoly) == 10
+ else
+ @test nvertices(upoly) == 17
+ end
+
+ # invalid inner
+ outer = Ring(randpoint2(10))
+ p1, p2 = randpoint2(2)
+ inner = Ring(p1, p1, p2)
+ poly = PolyArea([outer, inner])
+ upoly = unique(poly)
+ @test hasholes(poly)
+ @test !hasholes(upoly)
+
+ # centroid
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ @test centroid(poly) == cart(0.5, 0.5)
+
+ # single vertex access
+ outer = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+ hole1 = cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)])
+ hole2 = cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)])
+ poly = PolyArea([outer, hole1, hole2])
+ @test vertex(poly, 1) == cart(0, 0)
+ @test vertex(poly, 2) == cart(1, 0)
+ @test vertex(poly, 3) == cart(1, 1)
+ @test vertex(poly, 4) == cart(0, 1)
+ @test vertex(poly, 5) == cart(0.2, 0.2)
+ @test vertex(poly, 6) == cart(0.4, 0.2)
+ @test vertex(poly, 7) == cart(0.4, 0.4)
+ @test vertex(poly, 8) == cart(0.2, 0.4)
+ @test vertex(poly, 9) == cart(0.6, 0.2)
+ @test vertex(poly, 10) == cart(0.8, 0.2)
+ @test vertex(poly, 11) == cart(0.8, 0.4)
+ @test vertex(poly, 12) == cart(0.6, 0.4)
+ @test_throws BoundsError vertex(poly, 13)
+ # type stability
+ @inferred vertex(poly, 4)
+ @inferred vertex(poly, 8)
+ @inferred vertex(poly, 12)
+
+ # point in polygonal area
+ outer = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+ hole1 = cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)])
+ hole2 = cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)])
+ poly = PolyArea([outer, hole1, hole2])
+ @test all(p ∈ poly for p in outer)
+ @test cart(0.5, 0.5) ∈ poly
+ @test cart(0.2, 0.6) ∈ poly
+ @test cart(1.5, 0.5) ∉ poly
+ @test cart(-0.5, 0.5) ∉ poly
+ @test cart(0.25, 0.25) ∉ poly
+ @test cart(0.75, 0.25) ∉ poly
+ @test cart(0.75, 0.75) ∈ poly
+
+ # area
+ outer = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ hole1 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ hole2 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ poly = PolyArea([outer, reverse(hole1), reverse(hole2)])
+ @test area(poly) ≈ T(0.92) * u"m^2"
+
+ outer = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ hole1 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ hole2 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ poly1 = PolyArea(outer)
+ poly2 = PolyArea([outer, reverse(hole1), reverse(hole2)])
+ @test sprint(show, poly1) == "PolyArea((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))"
+ @test sprint(show, poly2) == "PolyArea(4-Ring, 4-Ring, 4-Ring)"
+ @test sprint(show, MIME("text/plain"), poly1) == """
+ PolyArea
+ outer
+ └─ Ring((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))"""
+ @test sprint(show, MIME("text/plain"), poly2) == """
+ PolyArea
+ outer
+ └─ Ring((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
+ inner
+ ├─ Ring((x: 0.2 m, y: 0.2 m), ..., (x: 0.4 m, y: 0.2 m))
+ └─ Ring((x: 0.6 m, y: 0.2 m), ..., (x: 0.8 m, y: 0.2 m))"""
+end
+
+@testitem "Polyhedra" setup = [Setup] begin
+ @test paramdim(Tetrahedron) == 3
+ @test nvertices(Tetrahedron) == 4
+
+ t = Tetrahedron(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0), cart(0, 0, 1))
+ @test crs(t) <: Cartesian{NoDatum}
+ @test Meshes.lentype(t) == ℳ
+ @test vertex(t, 1) == cart(0, 0, 0)
+ @test vertex(t, 2) == cart(1, 0, 0)
+ @test vertex(t, 3) == cart(0, 1, 0)
+ @test vertex(t, 4) == cart(0, 0, 1)
+ @test measure(t) == T(1 / 6) * u"m^3"
+ m = boundary(t)
+ n = normal.(m)
+ @test m isa Mesh
+ @test nvertices(m) == 4
+ @test nelements(m) == 4
+ @test n[1] == vector(0, 0, -1)
+ @test n[2] == vector(0, -1, 0)
+ @test n[3] == vector(-1, 0, 0)
+ @test all(>(T(0) * u"m"), n[4])
+ @test t(T(0), T(0), T(0)) ≈ cart(0, 0, 0)
+ @test t(T(1), T(0), T(0)) ≈ cart(1, 0, 0)
+ @test t(T(0), T(1), T(0)) ≈ cart(0, 1, 0)
+ @test t(T(0), T(0), T(1)) ≈ cart(0, 0, 1)
+ @test_throws DomainError((T(1), T(1), T(1)), "invalid barycentric coordinates for tetrahedron.") t(T(1), T(1), T(1))
+
+ t = Tetrahedron(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0), cart(0, 0, 1))
+ equaltest(t)
+ isapproxtest(t)
+ vertextest(t)
+
+ # CRS propagation
+ c1 = Cartesian{WGS84Latest}(T(0), T(0), T(0))
+ c2 = Cartesian{WGS84Latest}(T(1), T(0), T(0))
+ c3 = Cartesian{WGS84Latest}(T(0), T(1), T(0))
+ c4 = Cartesian{WGS84Latest}(T(0), T(0), T(1))
+ t = Tetrahedron(Point(c1), Point(c2), Point(c3), Point(c4))
+ @test crs(t(T(0), T(0), T(0))) === crs(t)
+
+ t = Tetrahedron(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0), cart(0, 0, 1))
+ @test sprint(show, t) == "Tetrahedron((x: 0.0 m, y: 0.0 m, z: 0.0 m), ..., (x: 0.0 m, y: 0.0 m, z: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), t) == """
+ Tetrahedron
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 0.0f0 m, y: 1.0f0 m, z: 0.0f0 m)
+ └─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), t) == """
+ Tetrahedron
+ ├─ Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 0.0 m, y: 1.0 m, z: 0.0 m)
+ └─ Point(x: 0.0 m, y: 0.0 m, z: 1.0 m)"""
+ end
+
+ @test paramdim(Hexahedron) == 3
+ @test nvertices(Hexahedron) == 8
+
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+ @test crs(h) <: Cartesian{NoDatum}
+ @test Meshes.lentype(h) == ℳ
+ @test vertex(h, 1) == cart(0, 0, 0)
+ @test vertex(h, 8) == cart(0, 1, 1)
+ @test h(T(0), T(0), T(0)) == cart(0, 0, 0)
+ @test h(T(0), T(0), T(1)) == cart(0, 0, 1)
+ @test h(T(0), T(1), T(0)) == cart(0, 1, 0)
+ @test h(T(0), T(1), T(1)) == cart(0, 1, 1)
+ @test h(T(1), T(0), T(0)) == cart(1, 0, 0)
+ @test h(T(1), T(0), T(1)) == cart(1, 0, 1)
+ @test h(T(1), T(1), T(0)) == cart(1, 1, 0)
+ @test h(T(1), T(1), T(1)) == cart(1, 1, 1)
+
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+ equaltest(h)
+ isapproxtest(h)
+ vertextest(t)
+
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+ @test volume(h) ≈ T(1 * 1 * 1) * u"m^3"
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(2, 0, 0),
+ cart(2, 2, 0),
+ cart(0, 2, 0),
+ cart(0, 0, 2),
+ cart(2, 0, 2),
+ cart(2, 2, 2),
+ cart(0, 2, 2)
+ )
+ @test volume(h) ≈ T(2 * 2 * 2) * u"m^3"
+
+ # volume formula of a frustum of a prism is V = 1/3*H*(S₁+S₂+sqrt(S₁*S₂))
+ # here we build a hexahedron which is a frustum of a prism with
+ # bottom area S₁= 4, top area S₂= 1, height H = 2
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(2, 0, 0),
+ cart(2, 2, 0),
+ cart(0, 2, 0),
+ cart(0, 0, 2),
+ cart(1, 0, 2),
+ cart(1, 1, 2),
+ cart(0, 1, 2)
+ )
+ @test volume(h) ≈ T(1 / 3 * 2 * (1 + 4 + sqrt(1 * 4))) * u"m^3"
+
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+ m = boundary(h)
+ @test m isa Mesh
+ @test nvertices(m) == 8
+ @test nelements(m) == 6
+
+ # CRS propagation
+ c1 = Cartesian{WGS84Latest}(T(0), T(0), T(0))
+ c2 = Cartesian{WGS84Latest}(T(1), T(0), T(0))
+ c3 = Cartesian{WGS84Latest}(T(1), T(1), T(0))
+ c4 = Cartesian{WGS84Latest}(T(0), T(1), T(0))
+ c5 = Cartesian{WGS84Latest}(T(0), T(0), T(1))
+ c6 = Cartesian{WGS84Latest}(T(1), T(0), T(1))
+ c7 = Cartesian{WGS84Latest}(T(1), T(1), T(1))
+ c8 = Cartesian{WGS84Latest}(T(0), T(1), T(1))
+ h = Hexahedron(Point(c1), Point(c2), Point(c3), Point(c4), Point(c5), Point(c6), Point(c7), Point(c8))
+ @test crs(h(T(0), T(0), T(0))) === crs(h)
+
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+ @test sprint(show, h) == "Hexahedron((x: 0.0 m, y: 0.0 m, z: 0.0 m), ..., (x: 0.0 m, y: 1.0 m, z: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), h) == """
+ Hexahedron
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 0.0f0 m, y: 1.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 1.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m, z: 1.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m, z: 1.0f0 m)
+ └─ Point(x: 0.0f0 m, y: 1.0f0 m, z: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), h) == """
+ Hexahedron
+ ├─ Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 1.0 m, z: 0.0 m)
+ ├─ Point(x: 0.0 m, y: 1.0 m, z: 0.0 m)
+ ├─ Point(x: 0.0 m, y: 0.0 m, z: 1.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m, z: 1.0 m)
+ ├─ Point(x: 1.0 m, y: 1.0 m, z: 1.0 m)
+ └─ Point(x: 0.0 m, y: 1.0 m, z: 1.0 m)"""
+ end
+
+ @test paramdim(Pyramid) == 3
+ @test nvertices(Pyramid) == 5
+
+ p = Pyramid(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0), cart(0, 0, 1))
+ @test crs(p) <: Cartesian{NoDatum}
+ @test Meshes.lentype(p) == ℳ
+ @test volume(p) ≈ T(1 / 3) * u"m^3"
+ m = boundary(p)
+ @test m isa Mesh
+ @test nelements(m) == 5
+ @test m[1] isa Quadrangle
+ @test m[2] isa Triangle
+ @test m[3] isa Triangle
+ @test m[4] isa Triangle
+ @test m[5] isa Triangle
+ equaltest(p)
+ isapproxtest(p)
+ vertextest(p)
+
+ p = Pyramid(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0), cart(0, 0, 1))
+ @test sprint(show, p) == "Pyramid((x: 0.0 m, y: 0.0 m, z: 0.0 m), ..., (x: 0.0 m, y: 0.0 m, z: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), p) == """
+ Pyramid
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 0.0f0 m, y: 1.0f0 m, z: 0.0f0 m)
+ └─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), p) == """
+ Pyramid
+ ├─ Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 1.0 m, z: 0.0 m)
+ ├─ Point(x: 0.0 m, y: 1.0 m, z: 0.0 m)
+ └─ Point(x: 0.0 m, y: 0.0 m, z: 1.0 m)"""
end
- @testset "Polyhedra" begin
- @test paramdim(Tetrahedron) == 3
- @test nvertices(Tetrahedron) == 4
-
- t = Tetrahedron(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0), P3(0, 0, 1))
- @test vertex(t, 1) == P3(0, 0, 0)
- @test vertex(t, 2) == P3(1, 0, 0)
- @test vertex(t, 3) == P3(0, 1, 0)
- @test vertex(t, 4) == P3(0, 0, 1)
- @test measure(t) == T(1 / 6)
- m = boundary(t)
- n = normal.(m)
- @test m isa Mesh
- @test nvertices(m) == 4
- @test nelements(m) == 4
- @test n[1] == T[0, 0, -0.5]
- @test n[2] == T[0, -0.5, 0]
- @test n[3] == T[-0.5, 0, 0]
- @test all(>(0), n[4])
- @test t(T(0), T(0), T(0)) ≈ P3(0, 0, 0)
- @test t(T(1), T(0), T(0)) ≈ P3(1, 0, 0)
- @test t(T(0), T(1), T(0)) ≈ P3(0, 1, 0)
- @test t(T(0), T(0), T(1)) ≈ P3(0, 0, 1)
- @test_throws DomainError((T(1), T(1), T(1)), "invalid barycentric coordinates for tetrahedron.") t(T(1), T(1), T(1))
-
- t = rand(Tetrahedron{3,T})
- @test t isa Tetrahedron
- @test embeddim(t) == 3
- @test coordtype(t) === T
-
- t = Tetrahedron(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0), P3(0, 0, 1))
- @test sprint(show, t) == "Tetrahedron((0.0, 0.0, 0.0), ..., (0.0, 0.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), t) == """
- Tetrahedron{3,Float32}
- ├─ Point(0.0f0, 0.0f0, 0.0f0)
- ├─ Point(1.0f0, 0.0f0, 0.0f0)
- ├─ Point(0.0f0, 1.0f0, 0.0f0)
- └─ Point(0.0f0, 0.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), t) == """
- Tetrahedron{3,Float64}
- ├─ Point(0.0, 0.0, 0.0)
- ├─ Point(1.0, 0.0, 0.0)
- ├─ Point(0.0, 1.0, 0.0)
- └─ Point(0.0, 0.0, 1.0)"""
- end
-
- @test paramdim(Hexahedron) == 3
- @test nvertices(Hexahedron) == 8
-
- h =
- Hexahedron(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0), P3(0, 0, 1), P3(1, 0, 1), P3(1, 1, 1), P3(0, 1, 1))
- @test vertex(h, 1) == P3(0, 0, 0)
- @test vertex(h, 8) == P3(0, 1, 1)
- @test h(T(0), T(0), T(0)) == P3(0, 0, 0)
- @test h(T(0), T(0), T(1)) == P3(0, 0, 1)
- @test h(T(0), T(1), T(0)) == P3(0, 1, 0)
- @test h(T(0), T(1), T(1)) == P3(0, 1, 1)
- @test h(T(1), T(0), T(0)) == P3(1, 0, 0)
- @test h(T(1), T(0), T(1)) == P3(1, 0, 1)
- @test h(T(1), T(1), T(0)) == P3(1, 1, 0)
- @test h(T(1), T(1), T(1)) == P3(1, 1, 1)
-
- h =
- Hexahedron(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0), P3(0, 0, 1), P3(1, 0, 1), P3(1, 1, 1), P3(0, 1, 1))
- @test volume(h) ≈ T(1 * 1 * 1)
- h =
- Hexahedron(P3(0, 0, 0), P3(2, 0, 0), P3(2, 2, 0), P3(0, 2, 0), P3(0, 0, 2), P3(2, 0, 2), P3(2, 2, 2), P3(0, 2, 2))
- @test volume(h) ≈ T(2 * 2 * 2)
-
- # volume formula of a frustum of a prism is V = 1/3*H*(S₁+S₂+sqrt(S₁*S₂))
- # here we build a hexahedron which is a frustum of a prism with
- # bottom area S₁= 4, top area S₂= 1, height H = 2
- h =
- Hexahedron(P3(0, 0, 0), P3(2, 0, 0), P3(2, 2, 0), P3(0, 2, 0), P3(0, 0, 2), P3(1, 0, 2), P3(1, 1, 2), P3(0, 1, 2))
- @test volume(h) ≈ T(1 / 3 * 2 * (1 + 4 + sqrt(1 * 4)))
-
- h =
- Hexahedron(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0), P3(0, 0, 1), P3(1, 0, 1), P3(1, 1, 1), P3(0, 1, 1))
- m = boundary(h)
- @test m isa Mesh
- @test nvertices(m) == 8
- @test nelements(m) == 6
-
- h = rand(Hexahedron{3,T})
- @test h isa Hexahedron
- @test embeddim(h) == 3
- @test coordtype(h) === T
-
- h =
- Hexahedron(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0), P3(0, 0, 1), P3(1, 0, 1), P3(1, 1, 1), P3(0, 1, 1))
- @test sprint(show, h) == "Hexahedron((0.0, 0.0, 0.0), ..., (0.0, 1.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), h) == """
- Hexahedron{3,Float32}
- ├─ Point(0.0f0, 0.0f0, 0.0f0)
- ├─ Point(1.0f0, 0.0f0, 0.0f0)
- ├─ Point(1.0f0, 1.0f0, 0.0f0)
- ├─ Point(0.0f0, 1.0f0, 0.0f0)
- ├─ Point(0.0f0, 0.0f0, 1.0f0)
- ├─ Point(1.0f0, 0.0f0, 1.0f0)
- ├─ Point(1.0f0, 1.0f0, 1.0f0)
- └─ Point(0.0f0, 1.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), h) == """
- Hexahedron{3,Float64}
- ├─ Point(0.0, 0.0, 0.0)
- ├─ Point(1.0, 0.0, 0.0)
- ├─ Point(1.0, 1.0, 0.0)
- ├─ Point(0.0, 1.0, 0.0)
- ├─ Point(0.0, 0.0, 1.0)
- ├─ Point(1.0, 0.0, 1.0)
- ├─ Point(1.0, 1.0, 1.0)
- └─ Point(0.0, 1.0, 1.0)"""
- end
-
- @test paramdim(Pyramid) == 3
- @test nvertices(Pyramid) == 5
-
- p = Pyramid(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0), P3(0, 0, 1))
- @test volume(p) ≈ T(1 / 3)
- m = boundary(p)
- @test m isa Mesh
- @test nelements(m) == 5
- @test m[1] isa Quadrangle
- @test m[2] isa Triangle
- @test m[3] isa Triangle
- @test m[4] isa Triangle
- @test m[5] isa Triangle
-
- p = rand(Pyramid{3,T})
- @test p isa Pyramid
- @test embeddim(p) == 3
- @test coordtype(p) === T
-
- p = Pyramid(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0), P3(0, 0, 1))
- @test sprint(show, p) == "Pyramid((0.0, 0.0, 0.0), ..., (0.0, 0.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), p) == """
- Pyramid{3,Float32}
- ├─ Point(0.0f0, 0.0f0, 0.0f0)
- ├─ Point(1.0f0, 0.0f0, 0.0f0)
- ├─ Point(1.0f0, 1.0f0, 0.0f0)
- ├─ Point(0.0f0, 1.0f0, 0.0f0)
- └─ Point(0.0f0, 0.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), p) == """
- Pyramid{3,Float64}
- ├─ Point(0.0, 0.0, 0.0)
- ├─ Point(1.0, 0.0, 0.0)
- ├─ Point(1.0, 1.0, 0.0)
- ├─ Point(0.0, 1.0, 0.0)
- └─ Point(0.0, 0.0, 1.0)"""
- end
+ @test paramdim(Wedge) == 3
+ @test nvertices(Wedge) == 6
+
+ w = Wedge(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0), cart(0, 0, 1), cart(1, 0, 1), cart(0, 1, 1))
+ @test crs(w) <: Cartesian{NoDatum}
+ @test Meshes.lentype(w) == ℳ
+ @test volume(w) ≈ T(1 / 2) * u"m^3"
+ m = boundary(w)
+ @test m isa Mesh
+ @test nelements(m) == 5
+ @test m[1] isa Triangle
+ @test m[2] isa Triangle
+ @test m[3] isa Quadrangle
+ @test m[4] isa Quadrangle
+ @test m[5] isa Quadrangle
+ equaltest(w)
+ isapproxtest(w)
+ vertextest(w)
+
+ w = Wedge(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0), cart(0, 0, 1), cart(1, 0, 1), cart(0, 1, 1))
+ @test sprint(show, w) == "Wedge((x: 0.0 m, y: 0.0 m, z: 0.0 m), ..., (x: 0.0 m, y: 1.0 m, z: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), w) == """
+ Wedge
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 0.0f0 m, y: 1.0f0 m, z: 0.0f0 m)
+ ├─ Point(x: 0.0f0 m, y: 0.0f0 m, z: 1.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m, z: 1.0f0 m)
+ └─ Point(x: 0.0f0 m, y: 1.0f0 m, z: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), w) == """
+ Wedge
+ ├─ Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ Point(x: 0.0 m, y: 1.0 m, z: 0.0 m)
+ ├─ Point(x: 0.0 m, y: 0.0 m, z: 1.0 m)
+ ├─ Point(x: 1.0 m, y: 0.0 m, z: 1.0 m)
+ └─ Point(x: 0.0 m, y: 1.0 m, z: 1.0 m)"""
end
end
diff --git a/test/predicates.jl b/test/predicates.jl
index ae09886de..9e65c95ed 100644
--- a/test/predicates.jl
+++ b/test/predicates.jl
@@ -1,541 +1,599 @@
-@testset "Predicates" begin
- @testset "issimplex" begin
- @test issimplex(Segment)
- @test issimplex(Segment(P2(0, 0), P2(1, 0)))
+@testitem "issimplex" setup = [Setup] begin
+ @test issimplex(Segment)
+ @test issimplex(Segment(cart(0, 0), cart(1, 0)))
- @test issimplex(Triangle)
- @test issimplex(Triangle(P2(0, 0), P2(1, 0), P2(0, 1)))
+ @test issimplex(Triangle)
+ @test issimplex(Triangle(cart(0, 0), cart(1, 0), cart(0, 1)))
- @test issimplex(Tetrahedron)
- @test issimplex(Tetrahedron(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0), P3(0, 0, 1)))
- end
+ @test issimplex(Tetrahedron)
+ @test issimplex(Tetrahedron(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0), cart(0, 0, 1)))
+end
- @testset "isconvex" begin
- # primitives
- r = Ray(P2(0, 0), V2(1, 1))
- @test isconvex(r)
- l = Line(P2(0, 0), P2(1, 1))
- @test isconvex(l)
- p = Plane(P3(0, 0, 0), V3(1, 0, 0), V3(0, 1, 0))
- @test isconvex(p)
- b = Box(P1(0), P1(1))
- @test isconvex(b)
- b = Box(P2(0, 0), P2(1, 1))
- @test isconvex(b)
- b = Box(P3(0, 0, 0), P3(1, 1, 1))
- b = Ball(P3(1, 2, 3), T(5))
- @test isconvex(b)
- @test isconvex(b)
- s = Sphere(P2(0, 0), T(1))
- @test !isconvex(s)
- s = Sphere(P3(0, 0, 0), T(1))
- @test !isconvex(s)
- d = Disk(Plane(P3(0, 0, 0), V3(0, 0, 1)), T(2))
- @test isconvex(d)
- c = Circle(Plane(P3(0, 0, 0), V3(0, 0, 1)), T(2))
- @test !isconvex(c)
- b = BezierCurve(P2[(0, 0), (1, 0), (2, 0)])
- @test isconvex(b)
- b = BezierCurve(P2[(0, 0), (1, 1), (2, 2)])
- @test isconvex(b)
- b = BezierCurve(P2[(0, 0)])
- @test isconvex(b)
- b = BezierCurve(P2[(0, 0), (1, 0)])
- @test isconvex(b)
- b = BezierCurve(P2[(0, 0), (5, 3), (-10, 3), (17, 20)])
- @test !isconvex(b)
- b = BezierCurve(P2[(5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (5, 10), (5, 11)])
- @test isconvex(b)
- b = BezierCurve(P2[])
- @test isconvex(b)
- c = Cylinder(Plane(P3(1, 2, 3), V3(0, 0, 1)), Plane(P3(4, 5, 6), V3(0, 0, 1)), T(5))
- @test isconvex(c)
- c = CylinderSurface(T(2))
- @test !isconvex(c)
- d = Disk(Plane(P3(0, 0, 0), V3(0, 0, 1)), T(2))
- a = P3(0, 0, 1)
- c = Cone(d, a)
- @test isconvex(c)
- d = Disk(Plane(P3(0, 0, 0), V3(0, 0, 1)), T(2))
- a = P3(0, 0, 1)
- c = ConeSurface(d, a)
- @test !isconvex(c)
- t = Torus(T.((1, 1, 1)), T.((1, 0, 0)), 2, 1)
- @test !isconvex(t)
-
- # polytopes
- s = Segment(P2(0, 0), P2(1, 1))
- @test isconvex(s)
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test isconvex(t)
- q1 = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- q2 = Quadrangle(P2(0.8, 0.8), P2(1, 0), P2(1, 1), P2(0, 1))
- q3 = Quadrangle(P2(0, 0), P2(0.2, 0.8), P2(1, 1), P2(0, 1))
- q4 = Quadrangle(P2(0, 0), P2(1, 0), P2(0.2, 0.2), P2(0, 1))
- q5 = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0.8, 0.2))
- @test isconvex(q1)
- @test !isconvex(q2)
- @test !isconvex(q3)
- @test !isconvex(q4)
- @test !isconvex(q5)
- q1 = Quadrangle(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0))
- q2 = Quadrangle(P3(0.8, 0.8, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0))
- q3 = Quadrangle(P3(0, 0, 0), P3(0.2, 0.8, 0), P3(1, 1, 0), P3(0, 1, 0))
- q4 = Quadrangle(P3(0, 0, 0), P3(1, 0, 0), P3(0.2, 0.2, 0), P3(0, 1, 0))
- q5 = Quadrangle(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0.8, 0.2, 0))
- @test isconvex(q1)
- @test !isconvex(q2)
- @test !isconvex(q3)
- @test !isconvex(q4)
- @test !isconvex(q5)
- t = Tetrahedron(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0), P3(0, 0, 1))
- @test isconvex(t)
- outer = P2[(6, 1), (2, 10), (10, 16), (18, 10), (14, 1)]
- inner = P2[(5, 7), (10, 12), (15, 7)]
- pent = Pentagon(outer...)
- tri = Triangle(inner...)
- poly = PolyArea([outer, inner])
- multi = Multi([poly, tri])
- @test isconvex(pent)
- @test isconvex(tri)
- @test !isconvex(poly)
- @test isconvex(multi)
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly1 = PolyArea(outer)
- poly2 = PolyArea([outer, hole1, hole2])
- @test isconvex(poly1)
- @test !isconvex(poly2)
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0.5, 0.5), (0, 1)])
- @test !isconvex(poly)
- end
+@testitem "isconvex" setup = [Setup] begin
+ # primitives
+ r = Ray(cart(0, 0), vector(1, 1))
+ @test isconvex(r)
+ l = Line(cart(0, 0), cart(1, 1))
+ @test isconvex(l)
+ p = Plane(cart(0, 0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ @test isconvex(p)
+ b = Box(cart(0), cart(1))
+ @test isconvex(b)
+ b = Box(cart(0, 0), cart(1, 1))
+ @test isconvex(b)
+ b = Box(cart(0, 0, 0), cart(1, 1, 1))
+ b = Ball(cart(1, 2, 3), T(5))
+ @test isconvex(b)
+ @test isconvex(b)
+ s = Sphere(cart(0, 0), T(1))
+ @test !isconvex(s)
+ s = Sphere(cart(0, 0, 0), T(1))
+ @test !isconvex(s)
+ d = Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ @test isconvex(d)
+ c = Circle(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ @test !isconvex(c)
+ b = BezierCurve(cart.([(0, 0), (1, 0), (2, 0)]))
+ @test isconvex(b)
+ b = BezierCurve(cart.([(0, 0), (1, 1), (2, 2)]))
+ @test isconvex(b)
+ b = BezierCurve(cart.([(0, 0)]))
+ @test isconvex(b)
+ b = BezierCurve(cart.([(0, 0), (1, 0)]))
+ @test isconvex(b)
+ b = BezierCurve(cart.([(0, 0), (5, 3), (-10, 3), (17, 20)]))
+ @test !isconvex(b)
+ b = BezierCurve(cart.([(5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (5, 10), (5, 11)]))
+ @test isconvex(b)
+ P = typeof(cart(0, 0))
+ b = BezierCurve(P[])
+ @test isconvex(b)
+ c = Cylinder(Plane(cart(1, 2, 3), vector(0, 0, 1)), Plane(cart(4, 5, 6), vector(0, 0, 1)), T(5))
+ @test isconvex(c)
+ c = CylinderSurface(T(2))
+ @test !isconvex(c)
+ d = Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ a = cart(0, 0, 1)
+ c = Cone(d, a)
+ @test isconvex(c)
+ d = Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ a = cart(0, 0, 1)
+ c = ConeSurface(d, a)
+ @test !isconvex(c)
+ t = Torus(T.((1, 1, 1)), T.((1, 0, 0)), 2, 1)
+ @test !isconvex(t)
+
+ # polytopes
+ s = Segment(cart(0, 0), cart(1, 1))
+ @test isconvex(s)
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test isconvex(t)
+ q1 = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ q2 = Quadrangle(cart(0.8, 0.8), cart(1, 0), cart(1, 1), cart(0, 1))
+ q3 = Quadrangle(cart(0, 0), cart(0.2, 0.8), cart(1, 1), cart(0, 1))
+ q4 = Quadrangle(cart(0, 0), cart(1, 0), cart(0.2, 0.2), cart(0, 1))
+ q5 = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0.8, 0.2))
+ @test isconvex(q1)
+ @test !isconvex(q2)
+ @test !isconvex(q3)
+ @test !isconvex(q4)
+ @test !isconvex(q5)
+ q1 = Quadrangle(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0))
+ q2 = Quadrangle(cart(0.8, 0.8, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0))
+ q3 = Quadrangle(cart(0, 0, 0), cart(0.2, 0.8, 0), cart(1, 1, 0), cart(0, 1, 0))
+ q4 = Quadrangle(cart(0, 0, 0), cart(1, 0, 0), cart(0.2, 0.2, 0), cart(0, 1, 0))
+ q5 = Quadrangle(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0.8, 0.2, 0))
+ @test isconvex(q1)
+ @test !isconvex(q2)
+ @test !isconvex(q3)
+ @test !isconvex(q4)
+ @test !isconvex(q5)
+ t = Tetrahedron(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0), cart(0, 0, 1))
+ @test isconvex(t)
+ outer = cart.([(14, 1), (18, 10), (10, 16), (2, 10), (6, 1)])
+ inner = cart.([(15, 7), (10, 12), (5, 7)])
+ pent = Pentagon(outer...)
+ tri = Triangle(inner...)
+ poly = PolyArea([outer, reverse(inner)])
+ multi = Multi([poly, tri])
+ @test isconvex(pent)
+ @test isconvex(tri)
+ @test !isconvex(poly)
+ @test isconvex(multi)
+ outer = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ hole1 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ hole2 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ poly1 = PolyArea(outer)
+ poly2 = PolyArea([outer, reverse(hole1), reverse(hole2)])
+ @test isconvex(poly1)
+ @test !isconvex(poly2)
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0.5, 0.5), (0, 1)]))
+ @test !isconvex(poly)
+end
- @testset "isparametrized" begin
- # primitives
- @test isparametrized(Ray)
- @test isparametrized(Line)
- @test isparametrized(Plane)
- @test isparametrized(Box)
- @test isparametrized(Ball)
- @test isparametrized(Sphere)
- @test isparametrized(Disk)
- @test isparametrized(Circle)
- @test isparametrized(BezierCurve)
- @test isparametrized(Cylinder)
- @test isparametrized(CylinderSurface)
- @test isparametrized(ConeSurface)
- @test isparametrized(ParaboloidSurface)
- @test isparametrized(Torus)
-
- # polytopes
- @test isparametrized(Segment)
- @test isparametrized(Triangle)
- @test isparametrized(Quadrangle)
- @test isparametrized(Hexahedron)
- end
+@testitem "isparametrized" setup = [Setup] begin
+ # primitives
+ @test isparametrized(Ray)
+ @test isparametrized(Line)
+ @test isparametrized(Plane)
+ @test isparametrized(Box{<:𝔼})
+ @test isparametrized(Ball{<:𝔼})
+ @test isparametrized(Sphere{<:𝔼})
+ @test isparametrized(Ellipsoid)
+ @test isparametrized(Disk)
+ @test isparametrized(Circle)
+ @test isparametrized(BezierCurve)
+ @test isparametrized(ParametrizedCurve)
+ @test isparametrized(Cylinder)
+ @test isparametrized(CylinderSurface)
+ @test isparametrized(Cone)
+ @test isparametrized(ConeSurface)
+ @test isparametrized(ParaboloidSurface)
+ @test isparametrized(Torus)
+
+ # polytopes
+ @test isparametrized(Segment)
+ @test isparametrized(Triangle)
+ @test isparametrized(Quadrangle)
+ @test isparametrized(Hexahedron)
+end
- @testset "isperiodic" begin
- # primitives
- @test isperiodic(Box{1}) == (false,)
- @test isperiodic(Box{2}) == (false, false)
- @test isperiodic(Box{3}) == (false, false, false)
- @test isperiodic(Ball{2}) == (false, true)
- @test isperiodic(Ball{3}) == (false, true, true)
- @test isperiodic(Sphere{2}) == (true,)
- @test isperiodic(Sphere{3}) == (true, true)
- @test isperiodic(ParaboloidSurface) == (false, true)
- @test isperiodic(Torus) == (true, true)
-
- # polytopes
- @test isperiodic(Segment) == (false,)
- @test isperiodic(Quadrangle) == (false, false)
- @test isperiodic(Hexahedron) == (false, false, false)
- end
+@testitem "isperiodic" setup = [Setup] begin
+ # primitives
+ @test isperiodic(Box{𝔼{2},Cartesian2D}) == (false, false)
+ @test isperiodic(Box{𝔼{3},Cartesian3D}) == (false, false, false)
+ @test isperiodic(Ball{𝔼{2},Cartesian2D}) == (false, true)
+ @test isperiodic(Ball{𝔼{3},Cartesian3D}) == (false, false, true)
+ @test isperiodic(Sphere{𝔼{2},Cartesian2D}) == (true,)
+ @test isperiodic(Sphere{𝔼{3},Cartesian3D}) == (false, true)
+ @test isperiodic(Ellipsoid) == (false, true)
+ @test isperiodic(Cylinder) == (false, true, false)
+ @test isperiodic(CylinderSurface) == (true, false)
+ @test isperiodic(ParaboloidSurface) == (false, true)
+ @test isperiodic(Torus) == (true, true)
+
+ # polytopes
+ @test isperiodic(Segment) == (false,)
+ @test isperiodic(Quadrangle) == (false, false)
+ @test isperiodic(Hexahedron) == (false, false, false)
+
+ @test isperiodic(cartgrid(10, 10)) == (false, false)
+ @test isperiodic(cartgrid(10, 10, 10)) == (false, false, false)
+end
- @testset "in" begin
- h = first(CartesianGrid{T}(10, 10, 10))
- @test P3(0, 0, 0) ∈ h
- @test P3(0.5, 0.5, 0.5) ∈ h
- @test P3(-1, 0, 0) ∉ h
- @test P3(0, 2, 0) ∉ h
- end
+@testitem "in" setup = [Setup] begin
+ h = first(cartgrid(10, 10, 10))
+ @test cart(0, 0, 0) ∈ h
+ @test cart(0.5, 0.5, 0.5) ∈ h
+ @test cart(-1, 0, 0) ∉ h
+ @test cart(0, 2, 0) ∉ h
+
+ outer = [merc(0, 0), merc(1, 0), merc(1, 1), merc(0, 1)]
+ hole1 = [merc(0.2, 0.2), merc(0.4, 0.2), merc(0.4, 0.4), merc(0.2, 0.4)]
+ hole2 = [merc(0.6, 0.2), merc(0.8, 0.2), merc(0.8, 0.4), merc(0.6, 0.4)]
+ poly = PolyArea([outer, hole1, hole2])
+ @test all(p ∈ poly for p in outer)
+ @test merc(0.5, 0.5) ∈ poly
+ @test merc(0.2, 0.6) ∈ poly
+ @test merc(1.5, 0.5) ∉ poly
+ @test merc(-0.5, 0.5) ∉ poly
+ @test merc(0.25, 0.25) ∉ poly
+ @test merc(0.75, 0.25) ∉ poly
+ @test merc(0.75, 0.75) ∈ poly
+end
- @testset "issubset" begin
- point = P2(0.5, 0.5)
- box = Box(P2(0, 0), P2(1, 1))
- ball = Ball(P2(0, 0))
- tri = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- quad = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test point ⊆ box
- @test point ⊆ ball
- @test point ⊆ tri
- @test point ⊆ quad
- @test point ⊆ point
- @test quad ⊆ quad
-
- s1 = Segment(P2(0, 0), P2(1, 1))
- s2 = Segment(P2(0.5, 0.5), P2(1, 1))
- s3 = Segment(P2(0, 0), P2(0.5, 0.5))
- @test s2 ⊆ s1
- @test s3 ⊆ s1
- @test s1 ⊆ s1
-
- seg = Segment(P2(0, 0), P2(1, 1))
- box = Box(P2(0, 0), P2(1, 1))
- ball = Ball(P2(0, 0))
- @test seg ⊆ box
- @test !(seg ⊆ ball)
-
- t1 = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- t2 = Triangle(P2(0, 0), P2(1, 0), P2(0.8, 0.8))
- t3 = Triangle(P2(0, 0), P2(1, 0), P2(1.1, 1.1))
- @test t2 ⊆ t1
- @test !(t3 ⊆ t1)
-
- tri = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- box = Box(P2(0, 0), P2(1, 1))
- ball = Ball(P2(0, 0))
- quad = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- pent = Pentagon(P2(0, 0), P2(1, 0), P2(1, 1), P2(0.5, 1.5), P2(0, 1))
- poly = PolyArea(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test tri ⊆ quad
- @test !(quad ⊆ tri)
- @test tri ⊆ box
- @test !(box ⊆ tri)
- @test !(tri ⊆ ball)
- @test !(ball ⊆ tri)
- @test tri ⊆ pent
- @test !(pent ⊆ tri)
- @test quad ⊆ pent
- @test !(pent ⊆ quad)
- @test tri ⊆ poly
- @test !(poly ⊆ tri)
- @test quad ⊆ poly
- @test poly ⊆ quad
-
- quad1 = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- quad2 = Quadrangle(P2(0, 0), P2(1.1, 0), P2(1, 1), P2(0, 1))
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- multi = Multi([poly])
- @test quad1 ⊆ poly
- @test !(quad2 ⊆ poly)
- @test quad1 ⊆ multi
- @test !(quad2 ⊆ multi)
-
- p1 = P2(-1.0, 0.0)
- p2 = P2(0.0, 0.0)
- p3 = P2(1.0, 0.0)
- l1 = Line(p1, p3)
- l2 = Line(p2, p3)
- @test l1 ⊆ l2
- @test l2 ⊆ l1
- @test l1 ⊆ l1
- @test l2 ⊆ l2
-
- pts1 = P2[(5, 7), (10, 12), (15, 7)]
- pts2 = P2[(6, 1), (2, 10), (10, 16), (18, 10), (14, 1)]
- pent = Pentagon(pts2...)
- tri = Triangle(pts1...)
- poly1 = PolyArea(pts2)
- poly2 = PolyArea([pts2, pts1])
- multi = Multi([poly2, tri])
- @test tri ⊆ pent
- @test tri ⊆ poly1
- @test tri ⊈ poly2
- @test tri ⊆ multi
- @test pent ⊆ poly1
- @test pent ⊈ poly2
- @test pent ⊆ multi
-
- poly1 = PolyArea(P2[(4, 12), (11, 11), (16, 8), (16, 1), (13, -2), (2, -2), (-3, 4), (-2, 8)])
- poly2 = PolyArea(P2[(3, 0), (1, 2), (3, 4), (1, 6), (4, 7), (10, 7), (11, 4), (9, 0)])
- poly3 = PolyArea(P2[(3, 2), (4, 4), (3, 8), (12, 8), (14, 4), (12, 1)])
- poly4 = PolyArea(P2[(8, 2), (5, 4), (5, 6), (9, 6), (10, 4)])
- poly5 = PolyArea(P2[(3, 9), (6, 11), (10, 10), (10, 9)])
- @test poly2 ⊆ poly1
- @test poly3 ⊆ poly1
- @test poly4 ⊆ poly1
- @test poly5 ⊆ poly1
- @test poly4 ⊆ poly2
- @test poly4 ⊆ poly3
- @test poly5 ⊈ poly2
- @test poly5 ⊈ poly3
- end
+@testitem "issubset" setup = [Setup] begin
+ p = cart(0.5, 0.5)
+ box = Box(cart(0, 0), cart(1, 1))
+ ball = Ball(cart(0, 0))
+ tri = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ quad = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test p ⊆ box
+ @test p ⊆ ball
+ @test p ⊆ tri
+ @test p ⊆ quad
+ @test p ⊆ p
+ @test quad ⊆ quad
+
+ s1 = Segment(cart(0, 0), cart(1, 1))
+ s2 = Segment(cart(0.5, 0.5), cart(1, 1))
+ s3 = Segment(cart(0, 0), cart(0.5, 0.5))
+ @test s2 ⊆ s1
+ @test s3 ⊆ s1
+ @test s1 ⊆ s1
+
+ seg = Segment(cart(0, 0), cart(1, 1))
+ box = Box(cart(0, 0), cart(1, 1))
+ ball = Ball(cart(0, 0))
+ @test seg ⊆ box
+ @test !(seg ⊆ ball)
+
+ t1 = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ t2 = Triangle(cart(0, 0), cart(1, 0), cart(0.8, 0.8))
+ t3 = Triangle(cart(0, 0), cart(1, 0), cart(1.1, 1.1))
+ @test t2 ⊆ t1
+ @test !(t3 ⊆ t1)
+
+ tri = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ box = Box(cart(0, 0), cart(1, 1))
+ ball = Ball(cart(0, 0))
+ quad = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ pent = Pentagon(cart(0, 0), cart(1, 0), cart(1, 1), cart(0.5, 1.5), cart(0, 1))
+ poly = PolyArea(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test tri ⊆ quad
+ @test !(quad ⊆ tri)
+ @test tri ⊆ box
+ @test !(box ⊆ tri)
+ @test !(tri ⊆ ball)
+ @test !(ball ⊆ tri)
+ @test tri ⊆ pent
+ @test !(pent ⊆ tri)
+ @test quad ⊆ pent
+ @test !(pent ⊆ quad)
+ @test tri ⊆ poly
+ @test !(poly ⊆ tri)
+ @test quad ⊆ poly
+ @test poly ⊆ quad
+
+ quad1 = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ quad2 = Quadrangle(cart(0, 0), cart(1.1, 0), cart(1, 1), cart(0, 1))
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ multi = Multi([poly])
+ @test quad1 ⊆ poly
+ @test !(quad2 ⊆ poly)
+ @test quad1 ⊆ multi
+ @test !(quad2 ⊆ multi)
+
+ p1 = cart(-1.0, 0.0)
+ p2 = cart(0.0, 0.0)
+ p3 = cart(1.0, 0.0)
+ l1 = Line(p1, p3)
+ l2 = Line(p2, p3)
+ @test l1 ⊆ l2
+ @test l2 ⊆ l1
+ @test l1 ⊆ l1
+ @test l2 ⊆ l2
+
+ outer = cart.([(14, 1), (18, 10), (10, 16), (2, 10), (6, 1)])
+ inner = cart.([(15, 7), (10, 12), (5, 7)])
+ pent = Pentagon(outer...)
+ tri = Triangle(inner...)
+ poly1 = PolyArea(outer)
+ poly2 = PolyArea([outer, reverse(inner)])
+ multi = Multi([poly2, tri])
+ @test tri ⊆ pent
+ @test tri ⊆ poly1
+ @test tri ⊈ poly2
+ @test tri ⊆ multi
+ @test pent ⊆ poly1
+ @test pent ⊈ poly2
+ @test pent ⊆ multi
+
+ poly1 = PolyArea(cart.([(-2, 8), (-3, 4), (2, -2), (13, -2), (16, 1), (16, 8), (11, 11), (4, 12)]))
+ poly2 = PolyArea(cart.([(9, 0), (11, 4), (10, 7), (4, 7), (1, 6), (3, 4), (1, 2), (3, 0)]))
+ poly3 = PolyArea(cart.([(12, 1), (14, 4), (12, 8), (3, 8), (4, 4), (3, 2)]))
+ poly4 = PolyArea(cart.([(10, 4), (9, 6), (5, 6), (5, 4), (8, 2)]))
+ poly5 = PolyArea(cart.([(10, 9), (10, 10), (6, 11), (3, 9)]))
+ @test poly2 ⊆ poly1
+ @test poly3 ⊆ poly1
+ @test poly4 ⊆ poly1
+ @test poly5 ⊆ poly1
+ @test poly4 ⊆ poly2
+ @test poly4 ⊆ poly3
+ @test poly5 ⊈ poly2
+ @test poly5 ⊈ poly3
+end
- @testset "intersects" begin
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- q = Quadrangle(P2(1, 1), P2(2, 1), P2(2, 2), P2(1, 2))
- @test intersects(t, t)
- @test intersects(q, q)
- @test !intersects(t, q)
- @test !intersects(q, t)
-
- t = Triangle(P2(1, 0), P2(2, 0), P2(1, 1))
- q = Quadrangle(P2(1.3, 0.5), P2(2.3, 0.5), P2(2.3, 1.5), P2(1.3, 1.5))
- @test intersects(t, t)
- @test intersects(q, q)
- @test intersects(t, q)
- @test intersects(q, t)
-
- t = Triangle(P2(1, 0), P2(2, 0), P2(1, 1))
- q = Quadrangle(P2(1.3, 0.5), P2(2.3, 0.5), P2(2.3, 1.5), P2(1.3, 1.5))
- m = Multi([t, q])
- @test intersects(m, t)
- @test intersects(t, m)
- @test intersects(m, q)
- @test intersects(q, m)
- @test intersects(m, m)
-
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- b = Ball(P2(0, 0), T(1))
- @test intersects(t, t)
- @test intersects(b, b)
- @test intersects(t, b)
- @test intersects(b, t)
-
- t = Triangle(P2(1, 0), P2(2, 0), P2(1, 1))
- b = Ball(P2(0, 0), T(1))
- @test intersects(t, t)
- @test intersects(b, b)
- @test intersects(t, b)
- @test intersects(b, t)
-
- t = Triangle(P2(1, 0), P2(2, 0), P2(1, 1))
- b = Ball(P2(-0.01, 0), T(1))
- @test intersects(t, t)
- @test intersects(b, b)
- @test !intersects(t, b)
- @test !intersects(b, t)
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/250
- t1 = Triangle(P3(0, 0, 0), P3(2, 0, 0), P3(1, 2, 0))
- t2 = Triangle(P3(1, 0, 0), P3(3, 0, 0), P3(2, 2, 0))
- t3 = Triangle(P3(3, 0, 0), P3(5, 0, 0), P3(4, 2, 0))
- @test intersects(t1, t2)
- @test intersects(t2, t3)
- @test !intersects(t1, t3)
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/639
- r = Ray(P2(0.41169768366272996, 0.8990554132423699), V2(0.47249211625247445, 0.2523149692768657))
- b = Box(P2(1.0, 1.0), P2(5.0, 2.0))
- @test intersects(r, b)
- @test intersects(b, r)
-
- t = Triangle(P3(0, 0, 0), P3(2, 0, 0), P3(1, 2, 0))
- r1 = Ray(P3(1, 1, 1), V3(0, 0, -1))
- r2 = Ray(P3(1, 1, 1), V3(0, 0, 1))
- @test intersects(r1, t)
- @test intersects(t, r1)
- @test !intersects(r2, t)
- @test !intersects(t, r2)
-
- r = Ray(P2(0, 0), V2(1, 0))
- s1 = Sphere(P2(3, 0), T(1))
- s2 = Sphere(P2(0, 3), T(1))
- @test intersects(r, s1)
- @test !intersects(r, s2)
-
- # result doesn't change under translation
- t1 = Translate(T(10), T(0))
- t2 = Translate(T(0), T(10))
- t3 = Translate(T(-10), T(0))
- t4 = Translate(T(0), T(-10))
- for t in [t1, t2, t3, t4]
- @test intersects(t(r), t(s1))
- @test !intersects(t(r), t(s2))
- end
-
- # result doesn't change under rotation
- r1 = Rotate(Angle2d(T(π / 2)))
- r2 = Rotate(Angle2d(T(-π / 2)))
- r3 = Rotate(Angle2d(T(π)))
- r4 = Rotate(Angle2d(T(-π)))
- for t in [r1, r2, r3, r4]
- @test intersects(t(r), t(s1))
- @test !intersects(t(r), t(s2))
- end
-
- r = Ray(P2(0, 0), V2(1, 0))
- s = Sphere(P2(floatmax(Float32) / 2, 0), 1)
- @test intersects(r, s)
-
- r = Ray(P3(0, 0, 0), V3(1, 0, 0))
- s1 = Sphere(P3(5, 0, 1 - eps(T(1))), T(1))
- s2 = Sphere(P3(5, 0, 1 + eps(T(1))), T(1))
- @test intersects(r, s1)
- @test !intersects(r, s2)
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/635
- q1 = Quadrangle(P3(4.0, 4.0, 0.0), P3(3.0, 3.0, 2.0), P3(3.0, 1.0, 2.0), P3(4.0, 0.0, 0.0))
- q2 = Quadrangle(P3(3.6, 3.0, 1.0), P3(5.6, 3.0, 1.0), P3(5.6, 1.0, 1.0), P3(3.6, 1.0, 1.0))
- q3 = Quadrangle(P3(3.6, 1.0, 1.0), P3(5.6, 1.0, 1.0), P3(5.6, -1.0, 1.0), P3(3.6, -1.0, 1.0))
- q4 = Quadrangle(P3(2.1, 1.0, 1.0), P3(4.1, 1.0, 1.0), P3(4.1, -1.0, 1.0), P3(2.1, -1.0, 1.0))
- @test !intersects(q1, q2)
- @test !intersects(q1, q3)
- @test intersects(q1, q1)
- @test intersects(q1, q4)
-
- h1 = Tetrahedron(P3(1, 1, 0), P3(4, 4, 0), P3(2.5, 2.5, 1.5), P3(1, 3, 2))
- h2 = Tetrahedron(P3(-1.0, 2.0, 1.0), P3(2.0, 1.0, 1.0), P3(-1.0, 4.0, 0.0), P3(0.5, 2.5, 1.5))
- h3 = Tetrahedron(P3(-1.3, 2.0, 1.0), P3(1.7, 1.0, 1.0), P3(-1.3, 4.0, 0.0), P3(0.2, 2.5, 1.5))
- @test intersects(h1, h2)
- @test !intersects(h1, h3)
-
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly1 = PolyArea(outer)
- poly2 = PolyArea([outer, hole1, hole2])
- ball1 = Ball(P2(0.5, 0.5), T(0.05))
- ball2 = Ball(P2(0.3, 0.3), T(0.05))
- ball3 = Ball(P2(0.7, 0.3), T(0.05))
- ball4 = Ball(P2(0.3, 0.3), T(0.15))
- @test intersects(poly1, poly1)
- @test intersects(poly2, poly2)
- @test intersects(poly1, poly2)
- @test intersects(poly2, poly1)
- @test intersects(poly1, ball1)
- @test intersects(poly2, ball1)
- @test intersects(poly1, ball2)
- @test !intersects(poly2, ball2)
- @test intersects(poly1, ball3)
- @test !intersects(poly2, ball3)
- @test intersects(poly1, ball4)
- @test intersects(poly2, ball4)
- mesh1 = discretize(poly1, Dehn1899())
- mesh2 = discretize(poly2, Dehn1899())
- @test intersects(mesh1, mesh1)
- @test intersects(mesh2, mesh2)
- @test intersects(mesh1, mesh2)
- @test intersects(mesh2, mesh1)
-
- point = P2(0.5, 0.5)
- ball = Ball(P2(0, 0), T(1))
- @test intersects(point, ball)
- @test intersects(ball, point)
- @test intersects(point, point)
- @test !intersects(point, point + V2(1, 1))
-
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- box = Box(P2(0, 0), P2(2, 2))
- @test intersects(poly, box)
-
- b1 = Box(P2(0, 0), P2(2, 2))
- b2 = Box(P2(2, 0), P2(4, 2))
- p1 = P2(1, 1)
- p2 = P2(3, 1)
- m = Multi([b1, b2])
- @test intersects(p1, b1)
- @test !intersects(p2, b1)
- @test intersects(p2, b2)
- @test !intersects(p1, b2)
- @test intersects(m, p1)
- @test intersects(p1, m)
- @test intersects(m, p2)
- @test intersects(p2, m)
-
- s1 = Segment(P2(0, 0), P2(4, 4))
- s2 = Segment(P2(4, 0), P2(0, 4))
- s3 = Segment(P2(2, 0), P2(4, 2))
- @test intersects(s1, s2)
- @test intersects(s2, s3)
- @test !intersects(s1, s3)
-
- s1 = Segment(P2(4, 0), P2(0, 4))
- s2 = Segment(P2(4, 0), P2(8, 4))
- s3 = Segment(P2(0, 8), P2(8, 8))
- r1 = Rope(P2[(0, 0), (4, 4), (8, 0)])
- r2 = Ring(P2[(0, 2), (4, 6), (8, 2)])
- @test intersects(s1, r1)
- @test intersects(s2, r1)
- @test !intersects(s3, r1)
- @test intersects(s1, r2)
- @test intersects(s2, r2)
- @test !intersects(s3, r2)
- @test intersects(r1, r2)
-
- r1 = Rope(P2[(0, 0), (2, 2), (4, 0)])
- r2 = Rope(P2[(3, 0), (5, 2), (7, 0)])
- r3 = Rope(P2[(6, 0), (8, 2), (10, 0)])
- @test intersects(r1, r2)
- @test intersects(r2, r3)
- @test !intersects(r1, r3)
-
- r1 = Ring(P2[(0, 0), (2, 2), (4, 0)])
- r2 = Ring(P2[(3, 0), (5, 2), (7, 0)])
- r3 = Ring(P2[(6, 0), (8, 2), (10, 0)])
- @test intersects(r1, r2)
- @test intersects(r2, r3)
- @test !intersects(r1, r3)
-
- t = Triangle(P2(3, 1), P2(7, 5), P2(11, 1))
- q = Quadrangle(P2(2, 0), P2(2, 7), P2(12, 7), P2(12, 0))
- b = Box(P2(2, 0), P2(12, 7))
- s1 = Segment(P2(5, 2), P2(9, 2))
- s2 = Segment(P2(0, 3), P2(5, 3))
- s3 = Segment(P2(4, 4), P2(10, 4))
- s4 = Segment(P2(1, 6), P2(13, 6))
- s5 = Segment(P2(0, 9), P2(14, 9))
- r1 = Ring(P2[(1, 2), (7, 8), (13, 2)])
- r2 = Rope(P2[(1, 2), (7, 8), (13, 2)])
- @test intersects(s1, t)
- @test intersects(s2, t)
- @test intersects(s3, t)
- @test !intersects(s4, t)
- @test !intersects(s5, t)
- @test intersects(s1, q)
- @test intersects(s2, q)
- @test intersects(s3, q)
- @test intersects(s4, q)
- @test !intersects(s5, q)
- @test intersects(s1, b)
- @test intersects(s2, b)
- @test intersects(s3, b)
- @test intersects(s4, b)
- @test !intersects(s5, b)
- @test intersects(r1, t)
- @test !intersects(r2, t)
- @test intersects(r1, q)
- @test intersects(r2, q)
- @test intersects(r1, b)
- @test intersects(r2, b)
-
- # performance test
- b1 = Box(P2(0, 0), P2(3, 3))
- b2 = Box(P2(2, 2), P2(5, 5))
- @test intersects(b1, b2)
- @test intersects(b2, b1)
- @test @elapsed(intersects(b1, b2)) < 5e-5
- @test @allocated(intersects(b1, b2)) < 100
-
- # partial application
- points = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- poly = PolyArea(points)
- box = Box(P2(0, 0), P2(2, 2))
- @test intersects(box)(poly)
- @test all(intersects(box), points)
-
- # method ambiguities
- point = P2(3, 1)
- ring = Ring(P2[(0, 0), (2, 2), (4, 0)])
- rope = Rope(P2[(2, 0), (4, 2), (6, 0)])
- seg = Segment(P2(0, 1), P2(6, 1))
- multi = Multi([ring])
- @test intersects(point, ring)
- @test intersects(point, rope)
- @test intersects(point, seg)
- @test intersects(point, multi)
- @test intersects(ring, multi)
- @test intersects(rope, multi)
- @test intersects(seg, multi)
+@testitem "intersects" setup = [Setup] begin
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ q = Quadrangle(cart(1, 1), cart(2, 1), cart(2, 2), cart(1, 2))
+ @test intersects(t, t)
+ @test intersects(q, q)
+ @test !intersects(t, q)
+ @test !intersects(q, t)
+
+ t = Triangle(cart(1, 0), cart(2, 0), cart(1, 1))
+ q = Quadrangle(cart(1.3, 0.5), cart(2.3, 0.5), cart(2.3, 1.5), cart(1.3, 1.5))
+ @test intersects(t, t)
+ @test intersects(q, q)
+ @test intersects(t, q)
+ @test intersects(q, t)
+
+ t = Triangle(cart(1, 0), cart(2, 0), cart(1, 1))
+ q = Quadrangle(cart(1.3, 0.5), cart(2.3, 0.5), cart(2.3, 1.5), cart(1.3, 1.5))
+ m = Multi([t, q])
+ @test intersects(m, t)
+ @test intersects(t, m)
+ @test intersects(m, q)
+ @test intersects(q, m)
+ @test intersects(m, m)
+
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ b = Ball(cart(0, 0), T(1))
+ @test intersects(t, t)
+ @test intersects(b, b)
+ @test intersects(t, b)
+ @test intersects(b, t)
+
+ t = Triangle(cart(1, 0), cart(2, 0), cart(1, 1))
+ b = Ball(cart(0, 0), T(1))
+ @test intersects(t, t)
+ @test intersects(b, b)
+ @test intersects(t, b)
+ @test intersects(b, t)
+
+ t = Triangle(cart(1, 0), cart(2, 0), cart(1, 1))
+ b = Ball(cart(-0.01, 0), T(1))
+ @test intersects(t, t)
+ @test intersects(b, b)
+ @test !intersects(t, b)
+ @test !intersects(b, t)
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/250
+ t1 = Triangle(cart(0, 0, 0), cart(2, 0, 0), cart(1, 2, 0))
+ t2 = Triangle(cart(1, 0, 0), cart(3, 0, 0), cart(2, 2, 0))
+ t3 = Triangle(cart(3, 0, 0), cart(5, 0, 0), cart(4, 2, 0))
+ @test intersects(t1, t2)
+ @test intersects(t2, t3)
+ @test !intersects(t1, t3)
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/639
+ r = Ray(cart(0.41169768366272996, 0.8990554132423699), vector(0.47249211625247445, 0.2523149692768657))
+ b = Box(cart(1.0, 1.0), cart(5.0, 2.0))
+ @test intersects(r, b)
+ @test intersects(b, r)
+
+ t = Triangle(cart(0, 0, 0), cart(2, 0, 0), cart(1, 2, 0))
+ r1 = Ray(cart(1, 1, 1), vector(0, 0, -1))
+ r2 = Ray(cart(1, 1, 1), vector(0, 0, 1))
+ @test intersects(r1, t)
+ @test intersects(t, r1)
+ @test !intersects(r2, t)
+ @test !intersects(t, r2)
+
+ r = Ray(cart(0, 0), vector(1, 0))
+ s1 = Sphere(cart(3, 0), T(1))
+ s2 = Sphere(cart(0, 3), T(1))
+ @test intersects(r, s1)
+ @test !intersects(r, s2)
+
+ # result doesn't change under translation
+ t1 = Translate(T(10), T(0))
+ t2 = Translate(T(0), T(10))
+ t3 = Translate(T(-10), T(0))
+ t4 = Translate(T(0), T(-10))
+ for t in [t1, t2, t3, t4]
+ @test intersects(t(r), t(s1))
+ @test !intersects(t(r), t(s2))
end
- @testset "iscollinear" begin
- @test iscollinear(P2(0, 0), P2(1, 1), P2(2, 2))
+ # result doesn't change under rotation
+ r1 = Rotate(Angle2d(T(π / 2)))
+ r2 = Rotate(Angle2d(T(-π / 2)))
+ r3 = Rotate(Angle2d(T(π)))
+ r4 = Rotate(Angle2d(T(-π)))
+ for t in [r1, r2, r3, r4]
+ @test intersects(t(r), t(s1))
+ @test !intersects(t(r), t(s2))
end
- @testset "iscoplanar" begin
- @test iscoplanar(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0))
- end
+ r = Ray(cart(0, 0), vector(1, 0))
+ s = Sphere(cart(floatmax(Float32) / 2, 0), 1)
+ @test intersects(r, s)
+
+ r = Ray(cart(0, 0, 0), vector(1, 0, 0))
+ s1 = Sphere(cart(5, 0, 1 - eps(T(1))), T(1))
+ s2 = Sphere(cart(5, 0, 1 + eps(T(1))), T(1))
+ @test intersects(r, s1)
+ @test !intersects(r, s2)
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/635
+ q1 = Quadrangle(cart(4.0, 4.0, 0.0), cart(3.0, 3.0, 2.0), cart(3.0, 1.0, 2.0), cart(4.0, 0.0, 0.0))
+ q2 = Quadrangle(cart(3.6, 3.0, 1.0), cart(5.6, 3.0, 1.0), cart(5.6, 1.0, 1.0), cart(3.6, 1.0, 1.0))
+ q3 = Quadrangle(cart(3.6, 1.0, 1.0), cart(5.6, 1.0, 1.0), cart(5.6, -1.0, 1.0), cart(3.6, -1.0, 1.0))
+ q4 = Quadrangle(cart(2.1, 1.0, 1.0), cart(4.1, 1.0, 1.0), cart(4.1, -1.0, 1.0), cart(2.1, -1.0, 1.0))
+ @test !intersects(q1, q2)
+ @test !intersects(q1, q3)
+ @test intersects(q1, q1)
+ @test intersects(q1, q4)
+
+ h1 = Tetrahedron(cart(1, 1, 0), cart(4, 4, 0), cart(2.5, 2.5, 1.5), cart(1, 3, 2))
+ h2 = Tetrahedron(cart(-1.0, 2.0, 1.0), cart(2.0, 1.0, 1.0), cart(-1.0, 4.0, 0.0), cart(0.5, 2.5, 1.5))
+ h3 = Tetrahedron(cart(-1.3, 2.0, 1.0), cart(1.7, 1.0, 1.0), cart(-1.3, 4.0, 0.0), cart(0.2, 2.5, 1.5))
+ @test intersects(h1, h2)
+ @test !intersects(h1, h3)
+
+ outer = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ hole1 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ hole2 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ poly1 = PolyArea(outer)
+ poly2 = PolyArea([outer, reverse(hole1), reverse(hole2)])
+ ball1 = Ball(cart(0.5, 0.5), T(0.05))
+ ball2 = Ball(cart(0.3, 0.3), T(0.05))
+ ball3 = Ball(cart(0.7, 0.3), T(0.05))
+ ball4 = Ball(cart(0.3, 0.3), T(0.15))
+ @test intersects(poly1, poly1)
+ @test intersects(poly2, poly2)
+ @test intersects(poly1, poly2)
+ @test intersects(poly2, poly1)
+ @test intersects(poly1, ball1)
+ @test intersects(poly2, ball1)
+ @test intersects(poly1, ball2)
+ @test !intersects(poly2, ball2)
+ @test intersects(poly1, ball3)
+ @test !intersects(poly2, ball3)
+ @test intersects(poly1, ball4)
+ @test intersects(poly2, ball4)
+ mesh1 = discretize(poly1, DehnTriangulation())
+ mesh2 = discretize(poly2, DehnTriangulation())
+ @test intersects(mesh1, mesh1)
+ @test intersects(mesh2, mesh2)
+ @test intersects(mesh1, mesh2)
+ @test intersects(mesh2, mesh1)
+
+ p = cart(0.5, 0.5)
+ ball = Ball(cart(0, 0), T(1))
+ @test intersects(p, ball)
+ @test intersects(ball, p)
+ @test intersects(p, p)
+ @test !intersects(p, p + vector(1, 1))
+
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ box = Box(cart(0, 0), cart(2, 2))
+ @test intersects(poly, box)
+
+ b1 = Box(cart(0, 0), cart(2, 2))
+ b2 = Box(cart(2, 0), cart(4, 2))
+ p1 = cart(1, 1)
+ p2 = cart(3, 1)
+ m = Multi([b1, b2])
+ @test intersects(p1, b1)
+ @test !intersects(p2, b1)
+ @test intersects(p2, b2)
+ @test !intersects(p1, b2)
+ @test intersects(m, p1)
+ @test intersects(p1, m)
+ @test intersects(m, p2)
+ @test intersects(p2, m)
+
+ s1 = Segment(cart(0, 0), cart(4, 4))
+ s2 = Segment(cart(4, 0), cart(0, 4))
+ s3 = Segment(cart(2, 0), cart(4, 2))
+ @test intersects(s1, s2)
+ @test intersects(s2, s3)
+ @test !intersects(s1, s3)
+
+ s1 = Segment(cart(4, 0), cart(0, 4))
+ s2 = Segment(cart(4, 0), cart(8, 4))
+ s3 = Segment(cart(0, 8), cart(8, 8))
+ r1 = Rope(cart.([(0, 0), (4, 4), (8, 0)]))
+ r2 = Ring(cart.([(0, 2), (4, 6), (8, 2)]))
+ @test intersects(s1, r1)
+ @test intersects(s2, r1)
+ @test !intersects(s3, r1)
+ @test intersects(s1, r2)
+ @test intersects(s2, r2)
+ @test !intersects(s3, r2)
+ @test intersects(r1, r2)
+
+ r1 = Rope(cart.([(0, 0), (2, 2), (4, 0)]))
+ r2 = Rope(cart.([(3, 0), (5, 2), (7, 0)]))
+ r3 = Rope(cart.([(6, 0), (8, 2), (10, 0)]))
+ @test intersects(r1, r2)
+ @test intersects(r2, r3)
+ @test !intersects(r1, r3)
+
+ r1 = Ring(cart.([(0, 0), (2, 2), (4, 0)]))
+ r2 = Ring(cart.([(3, 0), (5, 2), (7, 0)]))
+ r3 = Ring(cart.([(6, 0), (8, 2), (10, 0)]))
+ @test intersects(r1, r2)
+ @test intersects(r2, r3)
+ @test !intersects(r1, r3)
+
+ t = Triangle(cart(3, 1), cart(7, 5), cart(11, 1))
+ q = Quadrangle(cart(2, 0), cart(2, 7), cart(12, 7), cart(12, 0))
+ b = Box(cart(2, 0), cart(12, 7))
+ s1 = Segment(cart(5, 2), cart(9, 2))
+ s2 = Segment(cart(0, 3), cart(5, 3))
+ s3 = Segment(cart(4, 4), cart(10, 4))
+ s4 = Segment(cart(1, 6), cart(13, 6))
+ s5 = Segment(cart(0, 9), cart(14, 9))
+ r1 = Ring(cart.([(1, 2), (7, 8), (13, 2)]))
+ r2 = Rope(cart.([(1, 2), (7, 8), (13, 2)]))
+ @test intersects(s1, t)
+ @test intersects(s2, t)
+ @test intersects(s3, t)
+ @test !intersects(s4, t)
+ @test !intersects(s5, t)
+ @test intersects(s1, q)
+ @test intersects(s2, q)
+ @test intersects(s3, q)
+ @test intersects(s4, q)
+ @test !intersects(s5, q)
+ @test intersects(s1, b)
+ @test intersects(s2, b)
+ @test intersects(s3, b)
+ @test intersects(s4, b)
+ @test !intersects(s5, b)
+ @test intersects(r1, t)
+ @test !intersects(r2, t)
+ @test intersects(r1, q)
+ @test intersects(r2, q)
+ @test intersects(r1, b)
+ @test intersects(r2, b)
+
+ # performance test
+ b1 = Box(cart(0, 0), cart(3, 3))
+ b2 = Box(cart(2, 2), cart(5, 5))
+ @test intersects(b1, b2)
+ @test intersects(b2, b1)
+ @test @elapsed(intersects(b1, b2)) < 5e-5
+ @test @allocated(intersects(b1, b2)) < 100
+
+ # partial application
+ points = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+ poly = PolyArea(points)
+ box = Box(cart(0, 0), cart(2, 2))
+ @test intersects(box)(poly)
+ @test all(intersects(box), points)
+
+ # method ambiguities
+ p = cart(3, 1)
+ ring = Ring(cart.([(0, 0), (2, 2), (4, 0)]))
+ rope = Rope(cart.([(2, 0), (4, 2), (6, 0)]))
+ seg = Segment(cart(0, 1), cart(6, 1))
+ multi = Multi([ring])
+ @test intersects(p, ring)
+ @test intersects(p, rope)
+ @test intersects(p, seg)
+ @test intersects(p, multi)
+ @test intersects(ring, multi)
+ @test intersects(rope, multi)
+ @test intersects(seg, multi)
+end
+
+@testitem "ordering" setup = [Setup] begin
+ # lexicographical order
+ @test cart(0, 0) < cart(1, 1)
+ @test cart(0, 0) < cart(0, 1)
+ @test cart(1, 0) < cart(1, 1)
+ @test !(cart(1, 0) < cart(1, 0))
+ @test !(cart(1, 0) < cart(0, 0))
+ @test cart(1, 1) > cart(0, 0)
+ @test cart(0, 1) > cart(0, 0)
+ @test cart(1, 1) > cart(1, 0)
+ @test cart(1, 0) ≥ cart(1, 0)
+ @test cart(1, 0) ≥ cart(0, 0)
+ @test cart(0, 0) ≤ cart(0, 0)
+
+ # product order
+ @test cart(0, 0) ≺ cart(1, 1)
+ @test !(cart(0, 0) ≺ cart(0, 1))
+ @test !(cart(1, 0) ≺ cart(1, 1))
+ @test !(cart(1, 0) ≺ cart(1, 0))
+ @test !(cart(1, 0) ≺ cart(0, 0))
+ @test cart(1, 1) ≻ cart(0, 0)
+ @test !(cart(0, 1) ≻ cart(0, 0))
+ @test !(cart(1, 1) ≻ cart(1, 0))
+ @test cart(1, 0) ⪰ cart(1, 0)
+ @test cart(1, 0) ⪰ cart(0, 0)
+ @test cart(0, 0) ⪯ cart(0, 0)
+
+ # product order
+ @test cart(1, 1) ⪯ cart(1, 1)
+ @test !(cart(1, 1) ≺ cart(1, 1))
+ @test cart(1, 2) ⪯ cart(3, 4)
+ @test cart(1, 2) ≺ cart(3, 4)
+ @test cart(1, 1) ⪰ cart(1, 1)
+ @test !(cart(1, 1) ≻ cart(1, 1))
+ @test cart(3, 4) ⪰ cart(1, 2)
+ @test cart(3, 4) ≻ cart(1, 2)
+end
+
+@testitem "iscollinear" setup = [Setup] begin
+ @test iscollinear(cart(0, 0), cart(1, 1), cart(2, 2))
+end
+
+@testitem "iscoplanar" setup = [Setup] begin
+ @test iscoplanar(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0))
end
diff --git a/test/primitives.jl b/test/primitives.jl
index 439eaa129..1ef66b29d 100644
--- a/test/primitives.jl
+++ b/test/primitives.jl
@@ -1,1203 +1,1401 @@
-@testset "Primitives" begin
- @testset "Point" begin
- @test embeddim(Point(1)) == 1
- @test embeddim(Point(1, 2)) == 2
- @test embeddim(Point(1, 2, 3)) == 3
- @test coordtype(Point(1, 1)) == Float64
- @test coordtype(Point(1.0, 1.0)) == Float64
- @test coordtype(Point(1.0f0, 1.0f0)) == Float32
- @test coordtype(Point1(1)) == Float64
- @test coordtype(Point2(1, 1)) == Float64
- @test coordtype(Point3(1, 1, 1)) == Float64
- @test coordtype(Point1f(1)) == Float32
- @test coordtype(Point2f(1, 1)) == Float32
- @test coordtype(Point3f(1, 1, 1)) == Float32
-
- @test coordtype(Point{2,T}((1, 1))) == T
- @test coordtype(Point{2,T}(1, 1)) == T
-
- @test coordinates(P1(1)) == T[1]
- @test coordinates(P2(1, 2)) == T[1, 2]
- @test coordinates(P3(1, 2, 3)) == T[1, 2, 3]
-
- @test P1(1) - P1(1) == T[0]
- @test P2(1, 2) - P2(1, 1) == T[0, 1]
- @test P3(1, 2, 3) - P3(1, 1, 1) == T[0, 1, 2]
- @test_throws DimensionMismatch P2(1, 2) - P3(1, 2, 3)
-
- @test P1(1) + V1(0) == P1(1)
- @test P1(2) + V1(2) == P1(4)
- @test P2(1, 2) + V2(0, 0) == P2(1, 2)
- @test P2(2, 3) + V2(2, 1) == P2(4, 4)
- @test P3(1, 2, 3) + V3(0, 0, 0) == P3(1, 2, 3)
- @test P3(2, 3, 4) + V3(2, 1, 0) == P3(4, 4, 4)
- @test_throws DimensionMismatch P2(1, 2) + V3(1, 2, 3)
-
- @test P1(1) - V1(0) == P1(1)
- @test P1(2) - V1(2) == P1(0)
- @test P2(1, 2) - V2(0, 0) == P2(1, 2)
- @test P2(2, 3) - V2(2, 1) == P2(0, 2)
- @test P3(1, 2, 3) - V3(0, 0, 0) == P3(1, 2, 3)
- @test P3(2, 3, 4) - V3(2, 1, 0) == P3(0, 2, 4)
-
- @test embeddim(rand(P1)) == 1
- @test embeddim(rand(P2)) == 2
- @test embeddim(rand(P3)) == 3
- @test coordtype(rand(P1)) == T
- @test coordtype(rand(P2)) == T
- @test coordtype(rand(P3)) == T
-
- @test eltype(rand(P1, 3)) == P1
- @test eltype(rand(P2, 3)) == P2
- @test eltype(rand(P3, 3)) == P3
-
- @test P1(1) ≈ P1(1 + eps(T))
- @test P2(1, 2) ≈ P2(1 + eps(T), T(2))
- @test P3(1, 2, 3) ≈ P3(1 + eps(T), T(2), T(3))
-
- @test embeddim(Point((1,))) == 1
- @test coordtype(Point((1,))) == Float64
- @test coordtype(Point((1.0,))) == Float64
-
- @test embeddim(Point((1, 2))) == 2
- @test coordtype(Point((1, 2))) == Float64
- @test coordtype(Point((1.0, 2.0))) == Float64
-
- @test embeddim(Point((1, 2, 3))) == 3
- @test coordtype(Point((1, 2, 3))) == Float64
- @test coordtype(Point((1.0, 2.0, 3.0))) == Float64
-
- # check all 1D Point constructors, because those tend to make trouble
- @test Point(1) == Point((1,))
- @test Point{1,T}(-2) == Point{1,T}((-2,))
- @test Point{1,T}(0) == Point{1,T}((0,))
-
- @test_throws DimensionMismatch Point{2,T}(1)
- @test_throws DimensionMismatch Point{3,T}((2, 3))
- @test_throws DimensionMismatch Point{-3,T}((4, 5, 6))
-
- # There are 2 cases that throw a MethodError instead of a DimensionMismatch:
- # `Point{1,T}((2,3))` because it tries to take the tuple as a whole and convert to T and:
- # `Point{1,T}(2,3)` which does about the same.
- # I don't think this can reasonably be fixed here without hurting performance
-
- # check that input of mixed coordinate types is allowed and works as expected
- @test Point(1, 0.2) == Point{2,Float64}(1.0, 0.2)
- @test Point((3.0, 4)) == Point{2,Float64}(3.0, 4.0)
- @test Point((5.0, 6.0, 7)) == Point{3,Float64}(5.0, 6.0, 7.0)
- @test Point{2,T}(8, 9.0) == Point{2,T}((8.0, 9.0))
- @test Point{2,T}((-1.0, -2)) == Point{2,T}((-1, -2))
- @test Point{4,T}((0, -1.0, +2, -4.0)) == Point{4,T}((0.0f0, -1.0f0, +2.0f0, -4.0f0))
-
- # Integer coordinates converted to Float64
- @test coordtype(Point(1)) == Float64
- @test coordtype(Point(1, 2)) == Float64
- @test coordtype(Point(1, 2, 3)) == Float64
-
- # Unitful coordinates
- point = Point(1u"m", 1u"m")
- @test unit(coordtype(point)) == u"m"
- @test Unitful.numtype(coordtype(point)) === Float64
- point = Point(1.0u"m", 1.0u"m")
- @test unit(coordtype(point)) == u"m"
- @test Unitful.numtype(coordtype(point)) === Float64
- point = Point(1.0f0u"m", 1.0f0u"m")
- @test unit(coordtype(point)) == u"m"
- @test Unitful.numtype(coordtype(point)) === Float32
-
- # generalized inequality
- @test P2(1, 1) ⪯ P2(1, 1)
- @test !(P2(1, 1) ≺ P2(1, 1))
- @test P2(1, 2) ⪯ P2(3, 4)
- @test P2(1, 2) ≺ P2(3, 4)
- @test P2(1, 1) ⪰ P2(1, 1)
- @test !(P2(1, 1) ≻ P2(1, 1))
- @test P2(3, 4) ⪰ P2(1, 2)
- @test P2(3, 4) ≻ P2(1, 2)
-
- # center and centroid
- @test Meshes.center(P2(1, 1)) == P2(1, 1)
- @test centroid(P2(1, 1)) == P2(1, 1)
-
- # measure of points is zero
- @test measure(P2(1, 2)) == zero(T)
- @test measure(P3(1, 2, 3)) == zero(T)
-
- # boundary of points is nothing
- @test isnothing(boundary(rand(P1)))
- @test isnothing(boundary(rand(P2)))
- @test isnothing(boundary(rand(P3)))
-
- # check broadcasting works as expected
- @test P2(2, 2) .- [P2(2, 3), P2(3, 1)] == [[0.0, -1.0], [-1.0, 1.0]]
- @test P3(2, 2, 2) .- [P3(2, 3, 1), P3(3, 1, 4)] == [[0.0, -1.0, 1.0], [-1.0, 1.0, -2.0]]
-
- # angles between 2D points
- @test ∠(P2(0, 1), P2(0, 0), P2(1, 0)) ≈ T(-π / 2)
- @test ∠(P2(1, 0), P2(0, 0), P2(0, 1)) ≈ T(π / 2)
- @test ∠(P2(-1, 0), P2(0, 0), P2(0, 1)) ≈ T(-π / 2)
- @test ∠(P2(0, 1), P2(0, 0), P2(-1, 0)) ≈ T(π / 2)
- @test ∠(P2(0, -1), P2(0, 0), P2(1, 0)) ≈ T(π / 2)
- @test ∠(P2(1, 0), P2(0, 0), P2(0, -1)) ≈ T(-π / 2)
- @test ∠(P2(0, -1), P2(0, 0), P2(-1, 0)) ≈ T(-π / 2)
- @test ∠(P2(-1, 0), P2(0, 0), P2(0, -1)) ≈ T(π / 2)
-
- # angles between 3D points
- @test ∠(P3(1, 0, 0), P3(0, 0, 0), P3(0, 1, 0)) ≈ T(π / 2)
- @test ∠(P3(1, 0, 0), P3(0, 0, 0), P3(0, 0, 1)) ≈ T(π / 2)
- @test ∠(P3(0, 1, 0), P3(0, 0, 0), P3(1, 0, 0)) ≈ T(π / 2)
- @test ∠(P3(0, 1, 0), P3(0, 0, 0), P3(0, 0, 1)) ≈ T(π / 2)
- @test ∠(P3(0, 0, 1), P3(0, 0, 0), P3(1, 0, 0)) ≈ T(π / 2)
- @test ∠(P3(0, 0, 1), P3(0, 0, 0), P3(0, 1, 0)) ≈ T(π / 2)
-
- # a point pertains to itself
- p = P2(0, 0)
- q = P2(1, 1)
- @test p ∈ p
- @test q ∈ q
- @test p ∉ q
- @test q ∉ p
- p = P3(0, 0, 0)
- q = P3(1, 1, 1)
- @test p ∈ p
- @test q ∈ q
- @test p ∉ q
- @test q ∉ p
-
- p = P2(0, 1)
- @test sprint(show, p, context=:compact => true) == "(0.0, 1.0)"
- if T === Float32
- @test sprint(show, p) == "Point(0.0f0, 1.0f0)"
- else
- @test sprint(show, p) == "Point(0.0, 1.0)"
- end
+@testitem "Point" setup = [Setup] begin
+ @test embeddim(Point(1)) == 1
+ @test embeddim(Point(1, 2)) == 2
+ @test embeddim(Point(1, 2, 3)) == 3
+ @test paramdim(Point(1)) == 0
+ @test paramdim(Point(1, 2)) == 0
+ @test paramdim(Point(1, 2, 3)) == 0
+ @test crs(cart(1, 1)) <: Cartesian{NoDatum}
+ @test crs(Point(Polar(T(√2), T(π / 4)))) <: Polar{NoDatum}
+ @test crs(Point(Cylindrical(T(√2), T(π / 4), T(1)))) <: Cylindrical{NoDatum}
+ @test Meshes.lentype(Point(1, 1)) == Meshes.Met{Float64}
+ @test Meshes.lentype(Point(1.0, 1.0)) == Meshes.Met{Float64}
+ @test Meshes.lentype(Point(1.0f0, 1.0f0)) == Meshes.Met{Float32}
+ @test Meshes.lentype(Point((T(1), T(1)))) == ℳ
+ @test Meshes.lentype(Point(T(1), T(1))) == ℳ
+
+ # conversion
+ P = typeof(cart(1, 1))
+ p1 = Point(1.0, 1.0)
+ p2 = convert(P, p1)
+ @test p2 isa P
+ p1 = Point(1.0f0, 1.0f0)
+ p2 = convert(P, p1)
+ @test p2 isa P
+
+ # promotion
+ p1 = Point(T(1), T(1))
+ p2 = Point(1.0, 1.0)
+ p3 = Point(1.0f0, 1.0f0)
+ ps = promote(p1, p2)
+ @test allequal(Meshes.lentype.(ps))
+ @test Meshes.lentype(first(ps)) == Meshes.Met{Float64}
+ ps = promote(p1, p3)
+ @test allequal(Meshes.lentype.(ps))
+ @test Meshes.lentype(first(ps)) == Meshes.Met{T}
+
+ equaltest(cart(1))
+ equaltest(cart(1, 2))
+ equaltest(cart(1, 2, 3))
+ isapproxtest(cart(1))
+ isapproxtest(cart(1, 2))
+ isapproxtest(cart(1, 2, 3))
+
+ # different datums
+ p1 = Point(Cartesian{WGS84{1762}}(T(1), T(1), T(1)))
+ p2 = Point(Cartesian{ITRF{2008}}(T(1), T(1), T(1)))
+ @test p1 == p2
+ @test p1 ≈ p2
+
+ # latlon special cases
+ @test latlon(45, 180) == latlon(45, -180)
+ @test latlon(45, -180) == latlon(45, 180)
+
+ @test to(cart(1)) == vector(1)
+ @test to(cart(1, 2)) == vector(1, 2)
+ @test to(cart(1, 2, 3)) == vector(1, 2, 3)
+ @test to(Point(Polar(T(√2), T(π / 4)))) ≈ vector(1, 1)
+ @test to(Point(Cylindrical(T(√2), T(π / 4), T(1)))) ≈ vector(1, 1, 1)
+
+ @test cart(1) - cart(1) == vector(0)
+ @test cart(1, 2) - cart(1, 1) == vector(0, 1)
+ @test cart(1, 2, 3) - cart(1, 1, 1) == vector(0, 1, 2)
+ @test_throws DimensionMismatch cart(1, 2) - cart(1, 2, 3)
+
+ @test cart(1) + vector(0) == cart(1)
+ @test cart(2) + vector(2) == cart(4)
+ @test cart(1, 2) + vector(0, 0) == cart(1, 2)
+ @test cart(2, 3) + vector(2, 1) == cart(4, 4)
+ @test cart(1, 2, 3) + vector(0, 0, 0) == cart(1, 2, 3)
+ @test cart(2, 3, 4) + vector(2, 1, 0) == cart(4, 4, 4)
+ @test_throws DimensionMismatch cart(1, 2) + vector(1, 2, 3)
+
+ @test cart(1) - vector(0) == cart(1)
+ @test cart(2) - vector(2) == cart(0)
+ @test cart(1, 2) - vector(0, 0) == cart(1, 2)
+ @test cart(2, 3) - vector(2, 1) == cart(0, 2)
+ @test cart(1, 2, 3) - vector(0, 0, 0) == cart(1, 2, 3)
+ @test cart(2, 3, 4) - vector(2, 1, 0) == cart(0, 2, 4)
+
+ @test cart(1) ≈ cart(1 + eps(T))
+ @test cart(1, 2) ≈ cart(1 + eps(T), T(2))
+ @test cart(1, 2, 3) ≈ cart(1 + eps(T), T(2), T(3))
+
+ @test embeddim(Point((1,))) == 1
+ @test Meshes.lentype(Point((1,))) == Meshes.Met{Float64}
+ @test Meshes.lentype(Point((1.0,))) == Meshes.Met{Float64}
+
+ @test embeddim(Point((1, 2))) == 2
+ @test Meshes.lentype(Point((1, 2))) == Meshes.Met{Float64}
+ @test Meshes.lentype(Point((1.0, 2.0))) == Meshes.Met{Float64}
+
+ @test embeddim(Point((1, 2, 3))) == 3
+ @test Meshes.lentype(Point((1, 2, 3))) == Meshes.Met{Float64}
+ @test Meshes.lentype(Point((1.0, 2.0, 3.0))) == Meshes.Met{Float64}
+
+ # check all 1D Point constructors, because those tend to make trouble
+ @test Point(1) == Point((1,))
+ @test Point(T(-2)) == Point((T(-2),))
+ @test Point(T(0)) == Point((T(0),))
+
+ # check that input of mixed coordinate types is allowed and works as expected
+ @test Point(1, 0.2) == Point(1.0, 0.2)
+ @test Point((3.0, 4)) == Point(3.0, 4.0)
+ @test Point((5.0, 6.0, 7)) == Point(5.0, 6.0, 7.0)
+ @test Point(8, T(9.0)) == Point((T(8.0), T(9.0)))
+ @test Point((T(-1.0), -2)) == Point((T(-1.0), T(-2.0)))
+ @test Point((0, T(-1.0), +2, T(-4.0))) == Point((T(0.0), T(-1.0), T(+2.0), T(-4.0)))
+
+ # Integer coordinates converted to Float64
+ @test Meshes.lentype(Point(1)) == Meshes.Met{Float64}
+ @test Meshes.lentype(Point(1, 2)) == Meshes.Met{Float64}
+ @test Meshes.lentype(Point(1, 2, 3)) == Meshes.Met{Float64}
+
+ # Unitful coordinates
+ p = Point(1u"m", 1u"m")
+ @test unit(Meshes.lentype(p)) == u"m"
+ @test Unitful.numtype(Meshes.lentype(p)) === Float64
+ p = Point(1.0u"m", 1.0u"m")
+ @test unit(Meshes.lentype(p)) == u"m"
+ @test Unitful.numtype(Meshes.lentype(p)) === Float64
+ p = Point(1.0f0u"m", 1.0f0u"m")
+ @test unit(Meshes.lentype(p)) == u"m"
+ @test Unitful.numtype(Meshes.lentype(p)) === Float32
+
+ # centroid
+ @test centroid(cart(1, 1)) == cart(1, 1)
+
+ # measure of points is zero
+ @test measure(cart(1, 2)) == zero(ℳ)
+ @test measure(cart(1, 2, 3)) == zero(ℳ)
+
+ # boundary of points is nothing
+ @test isnothing(boundary(cart(1)))
+ @test isnothing(boundary(cart(1, 2)))
+ @test isnothing(boundary(cart(1, 2, 3)))
+
+ # check broadcasting works as expected
+ @test cart(2, 2) .- [cart(2, 3), cart(3, 1)] == [vector(0.0, -1.0), vector(-1.0, 1.0)]
+ @test cart(2, 2, 2) .- [cart(2, 3, 1), cart(3, 1, 4)] == [vector(0.0, -1.0, 1.0), vector(-1.0, 1.0, -2.0)]
+
+ # angles between 2D points
+ @test ∠(cart(0, 1), cart(0, 0), cart(1, 0)) ≈ T(-π / 2)
+ @test ∠(cart(1, 0), cart(0, 0), cart(0, 1)) ≈ T(π / 2)
+ @test ∠(cart(-1, 0), cart(0, 0), cart(0, 1)) ≈ T(-π / 2)
+ @test ∠(cart(0, 1), cart(0, 0), cart(-1, 0)) ≈ T(π / 2)
+ @test ∠(cart(0, -1), cart(0, 0), cart(1, 0)) ≈ T(π / 2)
+ @test ∠(cart(1, 0), cart(0, 0), cart(0, -1)) ≈ T(-π / 2)
+ @test ∠(cart(0, -1), cart(0, 0), cart(-1, 0)) ≈ T(-π / 2)
+ @test ∠(cart(-1, 0), cart(0, 0), cart(0, -1)) ≈ T(π / 2)
+
+ # angles between 3D points
+ @test ∠(cart(1, 0, 0), cart(0, 0, 0), cart(0, 1, 0)) ≈ T(π / 2)
+ @test ∠(cart(1, 0, 0), cart(0, 0, 0), cart(0, 0, 1)) ≈ T(π / 2)
+ @test ∠(cart(0, 1, 0), cart(0, 0, 0), cart(1, 0, 0)) ≈ T(π / 2)
+ @test ∠(cart(0, 1, 0), cart(0, 0, 0), cart(0, 0, 1)) ≈ T(π / 2)
+ @test ∠(cart(0, 0, 1), cart(0, 0, 0), cart(1, 0, 0)) ≈ T(π / 2)
+ @test ∠(cart(0, 0, 1), cart(0, 0, 0), cart(0, 1, 0)) ≈ T(π / 2)
+
+ # a point pertains to itself
+ p = cart(0, 0)
+ q = cart(1, 1)
+ @test p ∈ p
+ @test q ∈ q
+ @test p ∉ q
+ @test q ∉ p
+ p = cart(0, 0, 0)
+ q = cart(1, 1, 1)
+ @test p ∈ p
+ @test q ∈ q
+ @test p ∉ q
+ @test q ∉ p
+
+ # CRS propagation
+ p = merc(1, 1)
+ @test crs(p + vector(1, 1)) === crs(p)
+ @test crs(p - vector(1, 1)) === crs(p)
+
+ p = cart(0, 1)
+ @test sprint(show, p, context=:compact => true) == "(x: 0.0 m, y: 1.0 m)"
+ if T === Float32
+ @test sprint(show, p) == "Point(x: 0.0f0 m, y: 1.0f0 m)"
+ @test sprint(show, MIME("text/plain"), p) == """
+ Point with Cartesian{NoDatum} coordinates
+ ├─ x: 0.0f0 m
+ └─ y: 1.0f0 m"""
+ else
+ @test sprint(show, p) == "Point(x: 0.0 m, y: 1.0 m)"
+ @test sprint(show, MIME("text/plain"), p) == """
+ Point with Cartesian{NoDatum} coordinates
+ ├─ x: 0.0 m
+ └─ y: 1.0 m"""
end
+end
- @testset "Ray" begin
- r = Ray(P2(0, 0), V2(1, 1))
- @test paramdim(r) == 1
- @test measure(r) == T(Inf)
- @test length(r) == T(Inf)
- @test boundary(r) == P2(0, 0)
- @test perimeter(r) == zero(T)
-
- r = Ray(P2(0, 0), V2(1, 1))
- @test r(T(0.0)) == P2(0, 0)
- @test r(T(1.0)) == P2(1, 1)
- @test r(T(Inf)) == P2(Inf, Inf)
- @test r(T(1.0)) - r(T(0.0)) == V2(1, 1)
- @test_throws DomainError(T(-1), "r(t) is not defined for t < 0.") r(T(-1))
-
- p₁ = P3(3, 3, 3)
- p₂ = P3(-3, -3, -3)
- p₃ = P3(1, 0, 0)
- r = Ray(P3(0, 0, 0), V3(1, 1, 1))
- @test p₁ ∈ r
- @test p₂ ∉ r
- @test p₃ ∉ r
-
- r1 = Ray(P3(0, 0, 0), V3(1, 0, 0))
- r2 = Ray(P3(1, 1, 1), V3(1, 2, 1))
- @test r1 != r2
-
- r1 = Ray(P3(0, 0, 0), V3(1, 0, 0))
- r2 = Ray(P3(1, 0, 0), V3(-1, 0, 0))
- @test r1 != r2
-
- r1 = Ray(P3(0, 0, 0), V3(1, 0, 0))
- r2 = Ray(P3(1, 0, 0), V3(1, 0, 0))
- @test r1 != r2
-
- r1 = Ray(P3(0, 0, 0), V3(2, 0, 0))
- r2 = Ray(P3(0, 0, 0), V3(1, 0, 0))
- @test r1 == r2
-
- r2 = rand(Ray{2,T})
- r3 = rand(Ray{3,T})
- @test r2 isa Ray
- @test r3 isa Ray
- @test embeddim(r2) == 2
- @test embeddim(r3) == 3
-
- r = Ray(P2(0, 0), V2(1, 1))
- @test sprint(show, r) == "Ray(p: (0.0, 0.0), v: (1.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), r) == """
- Ray{2,Float32}
- ├─ p: Point(0.0f0, 0.0f0)
- └─ v: Vec(1.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), r) == """
- Ray{2,Float64}
- ├─ p: Point(0.0, 0.0)
- └─ v: Vec(1.0, 1.0)"""
- end
+@testitem "Ray" setup = [Setup] begin
+ r = Ray(cart(0, 0), vector(1, 1))
+ @test paramdim(r) == 1
+ @test crs(r) <: Cartesian{NoDatum}
+ @test Meshes.lentype(r) == ℳ
+ @test measure(r) == typemax(ℳ)
+ @test length(r) == typemax(ℳ)
+ @test boundary(r) == cart(0, 0)
+ @test perimeter(r) == zero(ℳ)
+
+ r = Ray(cart(0, 0), vector(1, 1))
+ equaltest(r)
+ isapproxtest(r)
+
+ r = Ray(cart(0, 0), vector(1, 1))
+ @test r(T(0.0)) == cart(0, 0)
+ @test r(T(1.0)) == cart(1, 1)
+ @test r(T(Inf)) == cart(Inf, Inf)
+ @test r(T(1.0)) - r(T(0.0)) == vector(1, 1)
+ @test_throws DomainError(T(-1), "r(t) is not defined for t < 0.") r(T(-1))
+
+ p₁ = cart(3, 3, 3)
+ p₂ = cart(-3, -3, -3)
+ p₃ = cart(1, 0, 0)
+ r = Ray(cart(0, 0, 0), vector(1, 1, 1))
+ @test p₁ ∈ r
+ @test p₂ ∉ r
+ @test p₃ ∉ r
+
+ r1 = Ray(cart(0, 0, 0), vector(1, 0, 0))
+ r2 = Ray(cart(1, 1, 1), vector(1, 2, 1))
+ @test r1 != r2
+
+ r1 = Ray(cart(0, 0, 0), vector(1, 0, 0))
+ r2 = Ray(cart(1, 0, 0), vector(-1, 0, 0))
+ @test r1 != r2
+
+ r1 = Ray(cart(0, 0, 0), vector(1, 0, 0))
+ r2 = Ray(cart(1, 0, 0), vector(1, 0, 0))
+ @test r1 != r2
+
+ r1 = Ray(cart(0, 0, 0), vector(2, 0, 0))
+ r2 = Ray(cart(0, 0, 0), vector(1, 0, 0))
+ @test r1 == r2
+
+ r = Ray(cart(0, 0), vector(1, 1))
+ @test sprint(show, r) == "Ray(p: (x: 0.0 m, y: 0.0 m), v: (1.0 m, 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), r) == """
+ Ray
+ ├─ p: Point(x: 0.0f0 m, y: 0.0f0 m)
+ └─ v: Vec(1.0f0 m, 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), r) == """
+ Ray
+ ├─ p: Point(x: 0.0 m, y: 0.0 m)
+ └─ v: Vec(1.0 m, 1.0 m)"""
end
+end
- @testset "Line" begin
- l = Line(P2(0, 0), P2(1, 1))
- @test paramdim(l) == 1
- @test measure(l) == T(Inf)
- @test length(l) == T(Inf)
- @test isnothing(boundary(l))
- @test perimeter(l) == zero(T)
-
- l = Line(P2(0, 0), P2(1, 1))
- @test (l(0), l(1)) == (P2(0, 0), P2(1, 1))
-
- l2 = rand(Line{2,T})
- l3 = rand(Line{3,T})
- @test l2 isa Line
- @test l3 isa Line
- @test embeddim(l2) == 2
- @test embeddim(l3) == 3
-
- l = Line(P2(0, 0), P2(1, 1))
- @test sprint(show, l) == "Line(a: (0.0, 0.0), b: (1.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), l) == """
- Line{2,Float32}
- ├─ a: Point(0.0f0, 0.0f0)
- └─ b: Point(1.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), l) == """
- Line{2,Float64}
- ├─ a: Point(0.0, 0.0)
- └─ b: Point(1.0, 1.0)"""
- end
+@testitem "Line" setup = [Setup] begin
+ l = Line(cart(0, 0), cart(1, 1))
+ @test paramdim(l) == 1
+ @test crs(l) <: Cartesian{NoDatum}
+ @test Meshes.lentype(l) == ℳ
+ @test measure(l) == typemax(ℳ)
+ @test length(l) == typemax(ℳ)
+ @test isnothing(boundary(l))
+ @test perimeter(l) == zero(ℳ)
+
+ l = Line(cart(0, 0), cart(1, 1))
+ equaltest(l)
+ isapproxtest(l)
+
+ l = Line(cart(0, 0), cart(1, 1))
+ @test (l(0), l(1)) == (cart(0, 0), cart(1, 1))
+
+ l = Line(latlon(45, 0), latlon(45, 90))
+ @test l(T(0)) == latlon(45, 0)
+ @test l(T(0.25)) == latlon(45, 22.5)
+ @test l(T(0.5)) == latlon(45, 45)
+ @test l(T(0.75)) == latlon(45, 67.5)
+ @test l(T(1)) == latlon(45, 90)
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/1138
+ l1 = Line(cart(0, 0), cart(1, 0))
+ l2 = Line(cart(0, 0), cart(2, 0))
+ l3 = Line(cart(0, 0), cart(-1, 0))
+ l4 = Line(cart(0, 0), cart(0, 1))
+ l5 = Line(cart(0, 0), cart(0, 2))
+ l6 = Line(cart(0, 0), cart(0, -1))
+ @test l1 == l2 == l3
+ @test l1 ≈ l2 ≈ l3
+ @test l4 == l5 == l6
+ @test l4 ≈ l5 ≈ l6
+
+ l = Line(cart(0, 0), cart(1, 1))
+ @test sprint(show, l) == "Line(a: (x: 0.0 m, y: 0.0 m), b: (x: 1.0 m, y: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), l) == """
+ Line
+ ├─ a: Point(x: 0.0f0 m, y: 0.0f0 m)
+ └─ b: Point(x: 1.0f0 m, y: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), l) == """
+ Line
+ ├─ a: Point(x: 0.0 m, y: 0.0 m)
+ └─ b: Point(x: 1.0 m, y: 1.0 m)"""
end
+end
- @testset "Plane" begin
- p = Plane(P3(0, 0, 0), V3(1, 0, 0), V3(0, 1, 0))
- @test p(T(1), T(0)) == P3(1, 0, 0)
- @test paramdim(p) == 2
- @test embeddim(p) == 3
- @test measure(p) == T(Inf)
- @test area(p) == T(Inf)
- @test p(T(0), T(0)) == P3(0, 0, 0)
- @test normal(p) == Vec(0, 0, 1)
- @test isnothing(boundary(p))
- @test perimeter(p) == zero(T)
-
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- @test p(T(1), T(0)) == P3(1, 0, 0)
- @test p(T(0), T(1)) == P3(0, 1, 0)
-
- p₁ = Plane(P3(0, 0, 0), V3(1, 0, 0), V3(0, 1, 0))
- p₂ = Plane(P3(0, 0, 0), V3(0, 1, 0), V3(1, 0, 0))
- @test p₁ ≈ p₂
- p₁ = Plane(P3(0, 0, 0), V3(1, 1, 0))
- p₂ = Plane(P3(0, 0, 0), -V3(1, 1, 0))
- @test p₁ ≈ p₂
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/624
- p₁ = Plane(P3(0, 0, 0), V3(0, 0, 1))
- p₂ = Plane(P3(0, 0, 10), V3(0, 0, 1))
- @test !(p₁ ≈ p₂)
-
- # normal to plane has norm one regardless of basis
- p = Plane(P3(0, 0, 0), V3(2, 0, 0), V3(0, 3, 0))
- n = normal(p)
- @test isapprox(norm(n), T(1), atol=atol(T))
-
- # plane passing through three points
- p₁ = P3(0, 0, 0)
- p₂ = P3(1, 2, 3)
- p₃ = P3(3, 2, 1)
- p = Plane(p₁, p₂, p₃)
- @test p₁ ∈ p
- @test p₂ ∈ p
- @test p₃ ∈ p
-
- p = rand(Plane{T})
- @test p isa Plane
- @test embeddim(p) == 3
-
- p = Plane(P3(0, 0, 0), V3(1, 0, 0), V3(0, 1, 0))
- @test sprint(show, p) == "Plane(p: (0.0, 0.0, 0.0), u: (1.0, 0.0, 0.0), v: (0.0, 1.0, 0.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), p) == """
- Plane{3,Float32}
- ├─ p: Point(0.0f0, 0.0f0, 0.0f0)
- ├─ u: Vec(1.0f0, 0.0f0, 0.0f0)
- └─ v: Vec(0.0f0, 1.0f0, 0.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), p) == """
- Plane{3,Float64}
- ├─ p: Point(0.0, 0.0, 0.0)
- ├─ u: Vec(1.0, 0.0, 0.0)
- └─ v: Vec(0.0, 1.0, 0.0)"""
- end
+@testitem "Plane" setup = [Setup] begin
+ p = Plane(cart(0, 0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ @test p(T(1), T(0)) == cart(1, 0, 0)
+ @test paramdim(p) == 2
+ @test embeddim(p) == 3
+ @test crs(p) <: Cartesian{NoDatum}
+ @test Meshes.lentype(p) == ℳ
+ @test measure(p) == typemax(ℳ)^2
+ @test area(p) == typemax(ℳ)^2
+ @test p(T(0), T(0)) == cart(0, 0, 0)
+ @test normal(p) == Vec(0, 0, 1)
+ @test isnothing(boundary(p))
+ @test perimeter(p) == zero(ℳ)
+
+ p = Plane(cart(0, 0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ equaltest(p)
+ isapproxtest(p)
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ @test p(T(1), T(0)) == cart(1, 0, 0)
+ @test p(T(0), T(1)) == cart(0, 1, 0)
+
+ p₁ = Plane(cart(0, 0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ p₂ = Plane(cart(0, 0, 0), vector(0, 1, 0), vector(1, 0, 0))
+ @test p₁ ≈ p₂
+ p₁ = Plane(cart(0, 0, 0), vector(1, 1, 0))
+ p₂ = Plane(cart(0, 0, 0), -vector(1, 1, 0))
+ @test p₁ ≈ p₂
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/624
+ p₁ = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ p₂ = Plane(cart(0, 0, 10), vector(0, 0, 1))
+ @test !(p₁ ≈ p₂)
+
+ # normal to plane has norm one regardless of basis
+ p = Plane(cart(0, 0, 0), vector(2, 0, 0), vector(0, 3, 0))
+ n = normal(p)
+ @test isapprox(norm(n), oneunit(ℳ), atol=atol(ℳ))
+
+ # plane passing through three points
+ p₁ = cart(0, 0, 0)
+ p₂ = cart(1, 2, 3)
+ p₃ = cart(3, 2, 1)
+ p = Plane(p₁, p₂, p₃)
+ @test p₁ ∈ p
+ @test p₂ ∈ p
+ @test p₃ ∈ p
+
+ p = Plane(cart(0, 0, 0), vector(1, 0, 0), vector(0, 1, 0))
+ @test sprint(show, p) ==
+ "Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, 0.0 m, 0.0 m), v: (0.0 m, 1.0 m, 0.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), p) == """
+ Plane
+ ├─ p: Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ u: Vec(1.0f0 m, 0.0f0 m, 0.0f0 m)
+ └─ v: Vec(0.0f0 m, 1.0f0 m, 0.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), p) == """
+ Plane
+ ├─ p: Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ u: Vec(1.0 m, 0.0 m, 0.0 m)
+ └─ v: Vec(0.0 m, 1.0 m, 0.0 m)"""
end
+end
- @testset "BezierCurve" begin
- b = BezierCurve(P2(0, 0), P2(0.5, 1), P2(1, 0))
- @test embeddim(b) == 2
- @test paramdim(b) == 1
-
- b = BezierCurve(P2(0, 0), P2(0.5, 1), P2(1, 0))
- for method in [DeCasteljau(), Horner()]
- @test b(T(0), method) == P2(0, 0)
- @test b(T(1), method) == P2(1, 0)
- @test b(T(0.5), method) == P2(0.5, 0.5)
- @test b(T(0.5), method) == P2(0.5, 0.5)
- @test_throws DomainError(T(-0.1), "b(t) is not defined for t outside [0, 1].") b(T(-0.1), method)
- @test_throws DomainError(T(1.2), "b(t) is not defined for t outside [0, 1].") b(T(1.2), method)
- end
-
- @test boundary(b) == Multi([P2(0, 0), P2(1, 0)])
- b = BezierCurve(P2(0, 0), P2(1, 1))
- @test boundary(b) == Multi([P2(0, 0), P2(1, 1)])
- @test perimeter(b) == zero(T)
-
- b = BezierCurve(P2.(randn(100), randn(100)))
- t1 = @timed b(T(0.2))
- t2 = @timed b(T(0.2), Horner())
- @test t1.time > t2.time
- @test t2.bytes < 100
-
- b2 = rand(BezierCurve{2,T})
- b3 = rand(BezierCurve{3,T})
- @test b2 isa BezierCurve
- @test b3 isa BezierCurve
- @test embeddim(b2) == 2
- @test embeddim(b3) == 3
-
- b = BezierCurve(P2(0, 0), P2(0.5, 1), P2(1, 0))
- if T === Float32
- @test sprint(show, b) == "BezierCurve(controls: Point2f[(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)])"
- @test sprint(show, MIME("text/plain"), b) == """
- BezierCurve{2,Float32}
- └─ controls: Point2f[Point(0.0f0, 0.0f0), Point(0.5f0, 1.0f0), Point(1.0f0, 0.0f0)]"""
- else
- @test sprint(show, b) == "BezierCurve(controls: Point2[(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)])"
- @test sprint(show, MIME("text/plain"), b) == """
- BezierCurve{2,Float64}
- └─ controls: Point2[Point(0.0, 0.0), Point(0.5, 1.0), Point(1.0, 0.0)]"""
- end
+@testitem "BezierCurve" setup = [Setup] begin
+ b = BezierCurve(cart(0, 0), cart(0.5, 1), cart(1, 0))
+ @test embeddim(b) == 2
+ @test paramdim(b) == 1
+ @test crs(b) <: Cartesian{NoDatum}
+ @test Meshes.lentype(b) == ℳ
+
+ b = BezierCurve(cart(0, 0), cart(1, 1))
+ equaltest(b)
+ isapproxtest(b)
+
+ b = BezierCurve(cart(0, 0), cart(0.5, 1), cart(1, 0))
+ for method in [DeCasteljau(), Horner()]
+ @test b(T(0), method) == cart(0, 0)
+ @test b(T(1), method) == cart(1, 0)
+ @test b(T(0.5), method) == cart(0.5, 0.5)
+ @test b(T(0.5), method) == cart(0.5, 0.5)
+ @test_throws DomainError(T(-0.1), "b(t) is not defined for t outside [0, 1].") b(T(-0.1), method)
+ @test_throws DomainError(T(1.2), "b(t) is not defined for t outside [0, 1].") b(T(1.2), method)
end
- @testset "Box" begin
- b = Box(P1(0), P1(1))
- @test embeddim(b) == 1
- @test paramdim(b) == 1
- @test coordtype(b) == T
- @test minimum(b) == P1(0)
- @test maximum(b) == P1(1)
- @test extrema(b) == (P1(0), P1(1))
-
- b = Box(P2(0, 0), P2(1, 1))
- @test embeddim(b) == 2
- @test paramdim(b) == 2
- @test coordtype(b) == T
- @test minimum(b) == P2(0, 0)
- @test maximum(b) == P2(1, 1)
- @test extrema(b) == (P2(0, 0), P2(1, 1))
-
- b = Box(P3(0, 0, 0), P3(1, 1, 1))
- @test embeddim(b) == 3
- @test paramdim(b) == 3
- @test coordtype(b) == T
- @test minimum(b) == P3(0, 0, 0)
- @test maximum(b) == P3(1, 1, 1)
- @test extrema(b) == (P3(0, 0, 0), P3(1, 1, 1))
-
- b = Box(P1(0), P1(1))
- @test boundary(b) == Multi([P1(0), P1(1)])
- @test measure(b) == T(1)
- @test P1(0) ∈ b
- @test P1(1) ∈ b
- @test P1(0.5) ∈ b
- @test P1(-0.5) ∉ b
- @test P1(1.5) ∉ b
-
- b = Box(P2(0, 0), P2(1, 1))
- @test measure(b) == area(b) == T(1)
- @test P2(1, 1) ∈ b
- @test perimeter(b) ≈ T(4)
-
- b = Box(P2(1, 1), P2(2, 2))
- @test sides(b) == T.((1, 1))
- @test Meshes.center(b) == P2(1.5, 1.5)
- @test diagonal(b) == √T(2)
-
- b = Box(P2(1, 2), P2(3, 4))
- v = P2[(1, 2), (3, 2), (3, 4), (1, 4)]
- @test boundary(b) == Ring(v)
-
- b = Box(P3(1, 2, 3), P3(4, 5, 6))
- v = P3[(1, 2, 3), (4, 2, 3), (4, 5, 3), (1, 5, 3), (1, 2, 6), (4, 2, 6), (4, 5, 6), (1, 5, 6)]
- c = connect.([(4, 3, 2, 1), (6, 5, 1, 2), (3, 7, 6, 2), (4, 8, 7, 3), (1, 5, 8, 4), (6, 7, 8, 5)])
- @test boundary(b) == SimpleMesh(v, c)
-
- b = Box(P2(0, 0), P2(1, 1))
- @test boundary(b) == Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
-
- b = Box(P3(0, 0, 0), P3(1, 1, 1))
- m = boundary(b)
- @test m isa Mesh
- @test nvertices(m) == 8
- @test nelements(m) == 6
-
- # subsetting with boxes
- b1 = Box(P2(0, 0), P2(0.5, 0.5))
- b2 = Box(P2(0.1, 0.1), P2(0.5, 0.5))
- b3 = Box(P2(0, 0), P2(1, 1))
- @test b1 ⊆ b3
- @test b2 ⊆ b3
- @test !(b1 ⊆ b2)
- @test !(b3 ⊆ b1)
- @test !(b3 ⊆ b1)
-
- b = Box(P2(0, 0), P2(10, 20))
- @test b(T(0.0), T(0.0)) == P2(0, 0)
- @test b(T(0.5), T(0.0)) == P2(5, 0)
- @test b(T(1.0), T(0.0)) == P2(10, 0)
- @test b(T(0.0), T(0.5)) == P2(0, 10)
- @test b(T(0.0), T(1.0)) == P2(0, 20)
-
- b = Box(P3(0, 0, 0), P3(10, 20, 30))
- @test b(T(0.0), T(0.0), T(0.0)) == P3(0, 0, 0)
- @test b(T(1.0), T(1.0), T(1.0)) == P3(10, 20, 30)
-
- b1 = rand(Box{1,T})
- b2 = rand(Box{2,T})
- b3 = rand(Box{3,T})
- @test b1 isa Box
- @test b2 isa Box
- @test b3 isa Box
- @test embeddim(b1) == 1
- @test embeddim(b2) == 2
- @test embeddim(b3) == 3
-
- @test_throws AssertionError Box(P1(1), P1(0))
- @test_throws AssertionError Box(P2(1, 1), P2(0, 0))
- @test_throws AssertionError Box(P3(1, 1, 1), P3(0, 0, 0))
-
- b = Box(P2(0, 0), P2(1, 1))
- q = convert(Quadrangle, b)
- @test q isa Quadrangle
- @test q == Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
-
- b = Box(P3(0, 0, 0), P3(1, 1, 1))
- h = convert(Hexahedron, b)
- @test h isa Hexahedron
- @test h == Hexahedron(
- P3(0, 0, 0),
- P3(1, 0, 0),
- P3(1, 1, 0),
- P3(0, 1, 0),
- P3(0, 0, 1),
- P3(1, 0, 1),
- P3(1, 1, 1),
- P3(0, 1, 1)
- )
-
- b = Box(P2(0, 0), P2(1, 1))
- @test sprint(show, b) == "Box(min: (0.0, 0.0), max: (1.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), b) == """
- Box{2,Float32}
- ├─ min: Point(0.0f0, 0.0f0)
- └─ max: Point(1.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), b) == """
- Box{2,Float64}
- ├─ min: Point(0.0, 0.0)
- └─ max: Point(1.0, 1.0)"""
- end
+ @test boundary(b) == Multi([cart(0, 0), cart(1, 0)])
+ b = BezierCurve(cart(0, 0), cart(1, 1))
+ @test boundary(b) == Multi([cart(0, 0), cart(1, 1)])
+ @test perimeter(b) == zero(ℳ)
+
+ # CRS propagation
+ b = BezierCurve(merc(0, 0), merc(0.5, 1), merc(1, 0))
+ @test crs(b(T(0), Horner())) === crs(b)
+
+ b = BezierCurve(cart(0, 0), cart(0.5, 1), cart(1, 0))
+ @test sprint(show, b) == "BezierCurve(controls: [(x: 0.0 m, y: 0.0 m), (x: 0.5 m, y: 1.0 m), (x: 1.0 m, y: 0.0 m)])"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), b) == """
+ BezierCurve
+ └─ controls: [Point(x: 0.0f0 m, y: 0.0f0 m), Point(x: 0.5f0 m, y: 1.0f0 m), Point(x: 1.0f0 m, y: 0.0f0 m)]"""
+ else
+ @test sprint(show, MIME("text/plain"), b) == """
+ BezierCurve
+ └─ controls: [Point(x: 0.0 m, y: 0.0 m), Point(x: 0.5 m, y: 1.0 m), Point(x: 1.0 m, y: 0.0 m)]"""
end
+end
- @testset "Ball" begin
- b = Ball(P3(1, 2, 3), T(5))
- @test embeddim(b) == 3
- @test paramdim(b) == 3
- @test coordtype(b) == T
- @test Meshes.center(b) == P3(1, 2, 3)
- @test radius(b) == T(5)
-
- b = Ball(P3(1, 2, 3), 4)
- @test coordtype(b) == T
-
- b1 = Ball(P2(0, 0), T(1))
- b2 = Ball(P2(0, 0))
- b3 = Ball(T.((0, 0)))
- @test b1 == b2 == b3
-
- b = Ball(P2(0, 0), T(2))
- @test measure(b) ≈ T(π) * (T(2)^2)
- b = Ball(P3(0, 0, 0), T(2))
- @test measure(b) ≈ T(4 / 3) * T(π) * (T(2)^3)
- @test_throws ArgumentError length(b)
- @test_throws ArgumentError area(b)
-
- b = Ball(P2(0, 0), T(2))
- @test P2(1, 0) ∈ b
- @test P2(0, 1) ∈ b
- @test P2(2, 0) ∈ b
- @test P2(0, 2) ∈ b
- @test P2(3, 5) ∉ b
- @test perimeter(b) ≈ T(4π)
-
- b = Ball(P3(0, 0, 0), T(2))
- @test P3(1, 0, 0) ∈ b
- @test P3(0, 0, 1) ∈ b
- @test P3(2, 0, 0) ∈ b
- @test P3(0, 0, 2) ∈ b
- @test P3(3, 5, 2) ∉ b
-
- b = Ball(P2(0, 0), T(2))
- @test b(T(0), T(0)) ≈ P2(0, 0)
- @test b(T(1), T(0)) ≈ P2(2, 0)
-
- b = Ball(P2(7, 7), T(1.5))
- ps = b.(1, rand(T, 100))
- all(∈(b), ps)
-
- b = Ball(P3(0, 0, 0), T(2))
- @test b(T(0), T(0), T(0)) ≈ P3(0, 0, 0)
- @test b(T(1), T(0), T(0)) ≈ P3(0, 0, 2)
-
- b = Ball(P3(7, 7, 7), T(1.5))
- ps = b.(1, rand(T, 100), rand(T, 100))
- all(∈(b), ps)
-
- b1 = rand(Ball{1,T})
- b2 = rand(Ball{2,T})
- b3 = rand(Ball{3,T})
- @test b1 isa Ball
- @test b2 isa Ball
- @test b3 isa Ball
- @test embeddim(b1) == 1
- @test embeddim(b2) == 2
- @test embeddim(b3) == 3
-
- b = Ball(P2(0, 0), T(1))
- @test sprint(show, b) == "Ball(center: (0.0, 0.0), radius: 1.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), b) == """
- Ball{2,Float32}
- ├─ center: Point(0.0f0, 0.0f0)
- └─ radius: 1.0f0"""
- else
- @test sprint(show, MIME("text/plain"), b) == """
- Ball{2,Float64}
- ├─ center: Point(0.0, 0.0)
- └─ radius: 1.0"""
- end
+@testitem "Box" setup = [Setup] begin
+ b = Box(cart(0), cart(1))
+ @test embeddim(b) == 1
+ @test paramdim(b) == 1
+ @test crs(b) <: Cartesian{NoDatum}
+ @test Meshes.lentype(b) == ℳ
+ @test minimum(b) == cart(0)
+ @test maximum(b) == cart(1)
+ @test extrema(b) == (cart(0), cart(1))
+
+ b = Box(cart(0, 0), cart(1, 1))
+ @test embeddim(b) == 2
+ @test paramdim(b) == 2
+ @test crs(b) <: Cartesian{NoDatum}
+ @test Meshes.lentype(b) == ℳ
+ @test minimum(b) == cart(0, 0)
+ @test maximum(b) == cart(1, 1)
+ @test extrema(b) == (cart(0, 0), cart(1, 1))
+
+ b = Box(cart(0, 0, 0), cart(1, 1, 1))
+ @test embeddim(b) == 3
+ @test paramdim(b) == 3
+ @test crs(b) <: Cartesian{NoDatum}
+ @test Meshes.lentype(b) == ℳ
+ @test minimum(b) == cart(0, 0, 0)
+ @test maximum(b) == cart(1, 1, 1)
+ @test extrema(b) == (cart(0, 0, 0), cart(1, 1, 1))
+
+ b = Box(latlon(0, 0), latlon(45, 90))
+ @test embeddim(b) == 3
+ @test paramdim(b) == 2
+
+ b = Box(cart(0, 0), cart(1, 1))
+ equaltest(b)
+ isapproxtest(b)
+
+ b = Box(cart(0), cart(1))
+ @test boundary(b) == Multi([cart(0), cart(1)])
+ @test measure(b) == T(1) * u"m"
+ @test cart(0) ∈ b
+ @test cart(1) ∈ b
+ @test cart(0.5) ∈ b
+ @test cart(-0.5) ∉ b
+ @test cart(1.5) ∉ b
+
+ b = Box(cart(0, 0), cart(1, 1))
+ @test measure(b) == area(b) == T(1) * u"m^2"
+ @test cart(1, 1) ∈ b
+ @test perimeter(b) ≈ T(4) * u"m"
+
+ b = Box(cart(1, 1), cart(2, 2))
+ @test sides(b) == (T(1) * u"m", T(1) * u"m")
+ @test centroid(b) == cart(1.5, 1.5)
+ @test diagonal(b) == √T(2) * u"m"
+
+ b = Box(cart(1, 2), cart(3, 4))
+ v = cart.([(1, 2), (3, 2), (3, 4), (1, 4)])
+ @test boundary(b) == Ring(v)
+
+ b = Box(cart(1, 2, 3), cart(4, 5, 6))
+ v = cart.([(1, 2, 3), (4, 2, 3), (4, 5, 3), (1, 5, 3), (1, 2, 6), (4, 2, 6), (4, 5, 6), (1, 5, 6)])
+ c = connect.([(4, 3, 2, 1), (6, 5, 1, 2), (3, 7, 6, 2), (4, 8, 7, 3), (1, 5, 8, 4), (6, 7, 8, 5)])
+ @test boundary(b) == SimpleMesh(v, c)
+
+ b = Box(cart(0, 0), cart(1, 1))
+ @test boundary(b) == Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+
+ b = Box(latlon(0, 0), latlon(1, 1))
+ @test boundary(b) == Ring(latlon.([(0, 0), (0, 1), (1, 1), (1, 0)]))
+
+ b = Box(cart(0, 0, 0), cart(1, 1, 1))
+ m = boundary(b)
+ @test m isa Mesh
+ @test nvertices(m) == 8
+ @test nelements(m) == 6
+
+ # subsetting with boxes
+ b1 = Box(cart(0, 0), cart(0.5, 0.5))
+ b2 = Box(cart(0.1, 0.1), cart(0.5, 0.5))
+ b3 = Box(cart(0, 0), cart(1, 1))
+ @test b1 ⊆ b3
+ @test b2 ⊆ b3
+ @test !(b1 ⊆ b2)
+ @test !(b3 ⊆ b1)
+ @test !(b3 ⊆ b1)
+
+ b = Box(cart(0, 0), cart(10, 20))
+ @test b(T(0.0), T(0.0)) == cart(0, 0)
+ @test b(T(0.5), T(0.0)) == cart(5, 0)
+ @test b(T(1.0), T(0.0)) == cart(10, 0)
+ @test b(T(0.0), T(0.5)) == cart(0, 10)
+ @test b(T(0.0), T(1.0)) == cart(0, 20)
+
+ b = Box(cart(0, 0, 0), cart(10, 20, 30))
+ @test b(T(0.0), T(0.0), T(0.0)) == cart(0, 0, 0)
+ @test b(T(1.0), T(1.0), T(1.0)) == cart(10, 20, 30)
+
+ @test_throws AssertionError Box(cart(1), cart(0))
+ @test_throws AssertionError Box(cart(1, 1), cart(0, 0))
+ @test_throws AssertionError Box(cart(1, 1, 1), cart(0, 0, 0))
+
+ b = Box(cart(0, 0), cart(1, 1))
+ q = convert(Quadrangle, b)
+ @test q isa Quadrangle
+ @test q == Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+
+ b = Box(cart(0, 0, 0), cart(1, 1, 1))
+ h = convert(Hexahedron, b)
+ @test h isa Hexahedron
+ @test h == Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+
+ # CRS propagation
+ b = Box(merc(0, 0), merc(1, 1))
+ @test crs(centroid(b)) === crs(b)
+
+ # centroid
+ b = Box(latlon(0, 0), latlon(30, 60))
+ @test centroid(b) == latlon(15, 30)
+
+ b = Box(cart(0, 0), cart(1, 1))
+ @test sprint(show, b) == "Box(min: (x: 0.0 m, y: 0.0 m), max: (x: 1.0 m, y: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), b) == """
+ Box
+ ├─ min: Point(x: 0.0f0 m, y: 0.0f0 m)
+ └─ max: Point(x: 1.0f0 m, y: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), b) == """
+ Box
+ ├─ min: Point(x: 0.0 m, y: 0.0 m)
+ └─ max: Point(x: 1.0 m, y: 1.0 m)"""
end
+end
- @testset "Sphere" begin
- s = Sphere(P3(0, 0, 0), T(1))
- @test embeddim(s) == 3
- @test paramdim(s) == 2
- @test coordtype(s) == T
- @test Meshes.center(s) == P3(0, 0, 0)
- @test radius(s) == T(1)
- @test extrema(s) == (P3(-1, -1, -1), P3(1, 1, 1))
- @test isnothing(boundary(s))
- @test perimeter(s) == zero(T)
-
- s = Sphere(P3(1, 2, 3), 4)
- @test coordtype(s) == T
-
- s = Sphere(P2(0, 0), T(1))
- @test embeddim(s) == 2
- @test paramdim(s) == 1
- @test coordtype(s) == T
- @test Meshes.center(s) == P2(0, 0)
- @test radius(s) == T(1)
- @test extrema(s) == (P2(-1, -1), P2(1, 1))
- @test isnothing(boundary(s))
-
- s1 = Sphere(P2(0, 0), T(1))
- s2 = Sphere(P2(0, 0))
- s3 = Sphere(T.((0, 0)))
- @test s1 == s2 == s3
-
- s = Sphere(P2(0, 0), T(2))
- @test measure(s) ≈ 2π * 2
- @test length(s) ≈ 2π * 2
- @test extrema(s) == (P2(-2, -2), P2(2, 2))
- s = Sphere(P3(0, 0, 0), T(2))
- @test measure(s) ≈ 4π * (2^2)
- @test area(s) ≈ 4π * (2^2)
-
- s = Sphere(P2(0, 0), T(2))
- @test P2(1, 0) ∉ s
- @test P2(0, 1) ∉ s
- @test P2(2, 0) ∈ s
- @test P2(0, 2) ∈ s
- @test P2(3, 5) ∉ s
-
- s = Sphere(P3(0, 0, 0), T(2))
- @test P3(1, 0, 0) ∉ s
- @test P3(0, 0, 1) ∉ s
- @test P3(2, 0, 0) ∈ s
- @test P3(0, 0, 2) ∈ s
- @test P3(3, 5, 2) ∉ s
-
- # 2D sphere passing through 3 points
- s = Sphere(P2(0, 0), P2(0.5, 0), P2(1, 1))
- @test Meshes.center(s) == P2(0.25, 0.75)
- @test radius(s) == T(0.7905694150420949)
- s = Sphere(P2(0, 0), P2(1, 0), P2(0, 1))
- @test Meshes.center(s) == P2(0.5, 0.5)
- @test radius(s) == T(0.7071067811865476)
- s = Sphere(P2(0, 0), P2(1, 0), P2(1, 1))
- @test Meshes.center(s) == P2(0.5, 0.5)
- @test radius(s) == T(0.7071067811865476)
-
- # 3D sphere passing through 4 points
- s = Sphere(P3(0, 0, 0), P3(5, 0, 1), P3(1, 1, 1), P3(3, 2, 1))
- @test P3(0, 0, 0) ∈ s
- @test P3(5, 0, 1) ∈ s
- @test P3(1, 1, 1) ∈ s
- @test P3(3, 2, 1) ∈ s
- O = Meshes.center(s)
- r = radius(s)
- @test isapprox(r, norm(P3(0, 0, 0) - O))
-
- s = Sphere(P2(0, 0), T(2))
- @test s(T(0)) ≈ P2(2, 0)
- @test s(T(0.5)) ≈ P2(-2, 0)
-
- s = Sphere(P3(0, 0, 0), T(2))
- @test s(T(0), T(0)) ≈ P3(0, 0, 2)
- @test s(T(0.5), T(0.5)) ≈ P3(-2, 0, 0)
-
- s1 = rand(Sphere{1,T})
- s2 = rand(Sphere{2,T})
- s3 = rand(Sphere{3,T})
- @test s1 isa Sphere
- @test s2 isa Sphere
- @test s3 isa Sphere
- @test embeddim(s1) == 1
- @test embeddim(s2) == 2
- @test embeddim(s3) == 3
-
- s = Sphere(P3(0, 0, 0), T(1))
- @test sprint(show, s) == "Sphere(center: (0.0, 0.0, 0.0), radius: 1.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), s) == """
- Sphere{3,Float32}
- ├─ center: Point(0.0f0, 0.0f0, 0.0f0)
- └─ radius: 1.0f0"""
- else
- @test sprint(show, MIME("text/plain"), s) == """
- Sphere{3,Float64}
- ├─ center: Point(0.0, 0.0, 0.0)
- └─ radius: 1.0"""
- end
+@testitem "Ball" setup = [Setup] begin
+ b = Ball(cart(1, 2, 3), T(5))
+ @test embeddim(b) == 3
+ @test paramdim(b) == 3
+ @test crs(b) <: Cartesian{NoDatum}
+ @test Meshes.lentype(b) == ℳ
+ @test center(b) == cart(1, 2, 3)
+ @test radius(b) == T(5) * u"m"
+
+ b = Ball(latlon(0, 0), T(5))
+ @test embeddim(b) == 3
+ @test paramdim(b) == 2
+
+ b = Ball(cart(0, 0), T(1))
+ equaltest(b)
+ isapproxtest(b)
+
+ b = Ball(cart(1, 2, 3), 4)
+ @test Meshes.lentype(b) == ℳ
+
+ b1 = Ball(cart(0, 0), T(1))
+ b2 = Ball(cart(0, 0))
+ b3 = Ball(T.((0, 0)))
+ @test b1 == b2 == b3
+
+ b = Ball(cart(0, 0), T(2))
+ @test measure(b) ≈ T(π) * (T(2)^2) * u"m^2"
+ b = Ball(cart(0, 0, 0), T(2))
+ @test measure(b) ≈ T(4 / 3) * T(π) * (T(2)^3) * u"m^3"
+ @test_throws ArgumentError length(b)
+ @test_throws ArgumentError area(b)
+
+ b = Ball(cart(0, 0), T(2))
+ @test cart(1, 0) ∈ b
+ @test cart(0, 1) ∈ b
+ @test cart(2, 0) ∈ b
+ @test cart(0, 2) ∈ b
+ @test cart(3, 5) ∉ b
+ @test perimeter(b) ≈ T(4π) * u"m"
+
+ b = Ball(cart(0, 0, 0), T(2))
+ @test cart(1, 0, 0) ∈ b
+ @test cart(0, 0, 1) ∈ b
+ @test cart(2, 0, 0) ∈ b
+ @test cart(0, 0, 2) ∈ b
+ @test cart(3, 5, 2) ∉ b
+
+ b = Ball(cart(0, 0), T(2))
+ @test b(T(0), T(0)) ≈ cart(0, 0)
+ @test b(T(1), T(0)) ≈ cart(2, 0)
+
+ # machine type is preserved in parameterization
+ b = Ball(cart(0, 0), T(2))
+ @test Meshes.lentype(b(0, 0)) == ℳ
+ @test Meshes.lentype(b(0.0, 0.0)) == ℳ
+ @test Meshes.lentype(b(0.0f0, 0.0f0)) == ℳ
+
+ b = Ball(cart(7, 7), T(1.5))
+ ps = b.(1, rand(T, 100))
+ all(∈(b), ps)
+
+ b = Ball(cart(0, 0, 0), T(2))
+ @test b(T(0), T(0), T(0)) ≈ cart(0, 0, 0)
+ @test b(T(1), T(0), T(0)) ≈ cart(0, 0, 2)
+
+ b = Ball(cart(7, 7, 7), T(1.5))
+ ps = b.(1, rand(T, 100), rand(T, 100))
+ all(∈(b), ps)
+
+ b = Ball(cart(0, 0), T(1))
+ @test sprint(show, b) == "Ball(center: (x: 0.0 m, y: 0.0 m), radius: 1.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), b) == """
+ Ball
+ ├─ center: Point(x: 0.0f0 m, y: 0.0f0 m)
+ └─ radius: 1.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), b) == """
+ Ball
+ ├─ center: Point(x: 0.0 m, y: 0.0 m)
+ └─ radius: 1.0 m"""
end
+end
- @testset "Disk" begin
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- d = Disk(p, T(2))
- @test embeddim(d) == 3
- @test paramdim(d) == 2
- @test coordtype(d) == T
- @test plane(d) == p
- @test Meshes.center(d) == P3(0, 0, 0)
- @test radius(d) == T(2)
- @test normal(d) == V3(0, 0, 1)
- @test measure(d) == T(π) * T(2)^2
- @test area(d) == measure(d)
- @test P3(0, 0, 0) ∈ d
- @test P3(0, 0, 1) ∉ d
- @test boundary(d) == Circle(p, T(2))
-
- d = rand(Disk{T})
- @test d isa Disk
- @test embeddim(d) == 3
-
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- d = Disk(p, T(2))
- @test sprint(show, d) ==
- "Disk(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), d) == """
- Disk{3,Float32}
- ├─ plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 2.0f0"""
- else
- @test sprint(show, MIME("text/plain"), d) == """
- Disk{3,Float64}
- ├─ plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 2.0"""
- end
+@testitem "Sphere" setup = [Setup] begin
+ s = Sphere(cart(0, 0, 0), T(1))
+ @test embeddim(s) == 3
+ @test paramdim(s) == 2
+ @test crs(s) <: Cartesian{NoDatum}
+ @test Meshes.lentype(s) == ℳ
+ @test center(s) == cart(0, 0, 0)
+ @test radius(s) == T(1) * u"m"
+ @test extrema(s) == (cart(-1, -1, -1), cart(1, 1, 1))
+ @test isnothing(boundary(s))
+ @test perimeter(s) == zero(ℳ)
+
+ s = Sphere(latlon(0, 0), T(1))
+ @test embeddim(s) == 3
+ @test paramdim(s) == 1
+
+ s = Sphere(cart(0, 0), T(1))
+ equaltest(s)
+ isapproxtest(s)
+
+ s = Sphere(cart(1, 2, 3), 4)
+ @test Meshes.lentype(s) == ℳ
+
+ s = Sphere(cart(0, 0), T(1))
+ @test embeddim(s) == 2
+ @test paramdim(s) == 1
+ @test Meshes.lentype(s) == ℳ
+ @test center(s) == cart(0, 0)
+ @test radius(s) == T(1) * u"m"
+ @test extrema(s) == (cart(-1, -1), cart(1, 1))
+ @test isnothing(boundary(s))
+
+ s1 = Sphere(cart(0, 0), T(1))
+ s2 = Sphere(cart(0, 0))
+ s3 = Sphere(T.((0, 0)))
+ @test s1 == s2 == s3
+
+ s = Sphere(cart(0, 0), T(2))
+ @test measure(s) ≈ T(2π) * 2 * u"m"
+ @test length(s) ≈ T(2π) * 2 * u"m"
+ @test extrema(s) == (cart(-2, -2), cart(2, 2))
+ s = Sphere(cart(0, 0, 0), T(2))
+ @test measure(s) ≈ T(4π) * (2^2) * u"m^2"
+ @test area(s) ≈ T(4π) * (2^2) * u"m^2"
+
+ s = Sphere(cart(0, 0), T(2))
+ @test cart(1, 0) ∉ s
+ @test cart(0, 1) ∉ s
+ @test cart(2, 0) ∈ s
+ @test cart(0, 2) ∈ s
+ @test cart(3, 5) ∉ s
+
+ s = Sphere(cart(0, 0, 0), T(2))
+ @test cart(1, 0, 0) ∉ s
+ @test cart(0, 0, 1) ∉ s
+ @test cart(2, 0, 0) ∈ s
+ @test cart(0, 0, 2) ∈ s
+ @test cart(3, 5, 2) ∉ s
+
+ # 2D sphere passing through 3 points
+ s = Sphere(cart(0, 0), cart(0.5, 0), cart(1, 1))
+ @test center(s) == cart(0.25, 0.75)
+ @test radius(s) == T(0.7905694150420949) * u"m"
+ s = Sphere(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test center(s) == cart(0.5, 0.5)
+ @test radius(s) == T(0.7071067811865476) * u"m"
+ s = Sphere(cart(0, 0), cart(1, 0), cart(1, 1))
+ @test center(s) == cart(0.5, 0.5)
+ @test radius(s) == T(0.7071067811865476) * u"m"
+
+ # 3D sphere passing through 4 points
+ s = Sphere(cart(0, 0, 0), cart(5, 0, 1), cart(1, 1, 1), cart(3, 2, 1))
+ @test cart(0, 0, 0) ∈ s
+ @test cart(5, 0, 1) ∈ s
+ @test cart(1, 1, 1) ∈ s
+ @test cart(3, 2, 1) ∈ s
+ O = center(s)
+ r = radius(s)
+ @test isapprox(r, norm(cart(0, 0, 0) - O))
+
+ s = Sphere(cart(0, 0), T(2))
+ @test s(T(0)) ≈ cart(2, 0)
+ @test s(T(0.5)) ≈ cart(-2, 0)
+
+ s = Sphere(cart(0, 0, 0), T(2))
+ @test s(T(0), T(0)) ≈ cart(0, 0, 2)
+ @test s(T(0.5), T(0.5)) ≈ cart(-2, 0, 0)
+
+ # machine type is preserved in parameterization
+ s = Sphere(cart(0, 0), T(2))
+ @test Meshes.lentype(s(0)) == ℳ
+ @test Meshes.lentype(s(0.0)) == ℳ
+ @test Meshes.lentype(s(0.0f0)) == ℳ
+
+ s = Sphere(cart(0, 0, 0), T(1))
+ @test sprint(show, s) == "Sphere(center: (x: 0.0 m, y: 0.0 m, z: 0.0 m), radius: 1.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), s) == """
+ Sphere
+ ├─ center: Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ └─ radius: 1.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), s) == """
+ Sphere
+ ├─ center: Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ └─ radius: 1.0 m"""
end
+end
- @testset "Circle" begin
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- c = Circle(p, T(2))
- @test embeddim(c) == 3
- @test paramdim(c) == 1
- @test coordtype(c) == T
- @test plane(c) == p
- @test Meshes.center(c) == P3(0, 0, 0)
- @test radius(c) == T(2)
- @test measure(c) == 2 * T(π) * T(2)
- @test length(c) == measure(c)
- @test P3(2, 0, 0) ∈ c
- @test P3(0, 2, 0) ∈ c
- @test P3(0, 0, 0) ∉ c
- @test isnothing(boundary(c))
-
- # 3D circumcircle
- p1 = P3(0, 4, 0)
- p2 = P3(0, -4, 0)
- p3 = P3(0, 0, 4)
- c = Circle(p1, p2, p3)
- @test p1 ∈ c
- @test p2 ∈ c
- @test p3 ∈ c
-
- # circle parametrization
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- c = Circle(p, T(2))
- @test c(T(0)) ≈ P3(2, 0, 0)
- @test c(T(0.25)) ≈ P3(0, 2, 0)
- @test c(T(0.5)) ≈ P3(-2, 0, 0)
- @test c(T(0.75)) ≈ P3(0, -2, 0)
- @test c(T(1)) ≈ P3(2, 0, 0)
-
- c = rand(Circle{T})
- @test c isa Circle
- @test embeddim(c) == 3
-
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- c = Circle(p, T(2))
- @test sprint(show, c) ==
- "Circle(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), c) == """
- Circle{3,Float32}
- ├─ plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 2.0f0"""
- else
- @test sprint(show, MIME("text/plain"), c) == """
- Circle{3,Float64}
- ├─ plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 2.0"""
- end
+@testitem "Ellipsoid" setup = [Setup] begin
+ e = Ellipsoid((T(3), T(2), T(1)))
+ @test embeddim(e) == 3
+ @test paramdim(e) == 2
+ @test crs(e) <: Cartesian{NoDatum}
+ @test Meshes.lentype(e) == ℳ
+ @test radii(e) == (T(3) * u"m", T(2) * u"m", T(1) * u"m")
+ @test center(e) == cart(0, 0, 0)
+ @test isnothing(boundary(e))
+ @test perimeter(e) == zero(ℳ)
+
+ e = Ellipsoid((T(3), T(2), T(1)))
+ equaltest(e)
+ isapproxtest(e)
+
+ e = Ellipsoid((T(3), T(2), T(1)))
+ @test sprint(show, e) ==
+ "Ellipsoid(radii: (3.0 m, 2.0 m, 1.0 m), center: (x: 0.0 m, y: 0.0 m, z: 0.0 m), rotation: UniformScaling{Bool}(true))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), e) == """
+ Ellipsoid
+ ├─ radii: (3.0f0 m, 2.0f0 m, 1.0f0 m)
+ ├─ center: Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ └─ rotation: LinearAlgebra.UniformScaling{Bool}(true)"""
+ else
+ @test sprint(show, MIME("text/plain"), e) == """
+ Ellipsoid
+ ├─ radii: (3.0 m, 2.0 m, 1.0 m)
+ ├─ center: Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ └─ rotation: LinearAlgebra.UniformScaling{Bool}(true)"""
end
+end
- @testset "Cylinder" begin
- c = Cylinder(Plane(P3(1, 2, 3), V3(0, 0, 1)), Plane(P3(4, 5, 6), V3(0, 0, 1)), T(5))
- @test embeddim(c) == 3
- @test paramdim(c) == 3
- @test coordtype(c) == T
- @test radius(c) == T(5)
- @test bottom(c) == Plane(P3(1, 2, 3), V3(0, 0, 1))
- @test top(c) == Plane(P3(4, 5, 6), V3(0, 0, 1))
- @test axis(c) == Line(P3(1, 2, 3), P3(4, 5, 6))
- @test !isright(c)
- @test measure(c) == volume(c) ≈ T(5)^2 * pi * T(3) * sqrt(T(3))
- @test P3(1, 2, 3) ∈ c
- @test P3(4, 5, 6) ∈ c
- @test P3(0.99, 1.99, 2.99) ∉ c
- @test P3(4.01, 5.01, 6.01) ∉ c
- @test !Meshes.hasintersectingplanes(c)
- @test c(0, 0, 0) ≈ bottom(c)(0, 0)
- @test c(0, 0, 1) ≈ top(c)(0, 0)
- @test c(1, 0.25, 0.5) ≈ Point(T(4.330127018922193), T(10.330127018922191), T(4.5))
- @test_throws DomainError c(1.1, 0, 0)
-
- c = Cylinder(Plane(P3(0, 0, 0), V3(0, 0, 1)), Plane(P3(0, 0, 1), V3(1, 0, 1)), T(5))
- @test Meshes.hasintersectingplanes(c)
-
- c1 = Cylinder(P3(0, 0, 0), P3(0, 0, 1), T(1))
- c2 = Cylinder(P3(0, 0, 0), P3(0, 0, 1))
- c3 = Cylinder(T(1))
- @test c1 == c2 == c3
- @test c1 ≈ c2 ≈ c3
-
- c = Cylinder(T(1))
- @test coordtype(c) == T
- c = Cylinder(1)
- @test coordtype(c) == Float64
-
- c = Cylinder(P3(0, 0, 0), P3(0, 0, 1), T(1))
- @test radius(c) == T(1)
- @test bottom(c) == Plane(P3(0, 0, 0), V3(0, 0, 1))
- @test top(c) == Plane(P3(0, 0, 1), V3(0, 0, 1))
- @test center(c) == P3(0.0, 0.0, 0.5)
- @test centroid(c) == P3(0.0, 0.0, 0.5)
- @test axis(c) == Line(P3(0, 0, 0), P3(0, 0, 1))
- @test isright(c)
- @test boundary(c) == CylinderSurface(P3(0, 0, 0), P3(0, 0, 1), T(1))
- @test measure(c) == volume(c) ≈ pi
- @test P3(0, 0, 0) ∈ c
- @test P3(0, 0, 1) ∈ c
- @test P3(1, 0, 0) ∈ c
- @test P3(0, 1, 0) ∈ c
- @test P3(cosd(60), sind(60), 0.5) ∈ c
- @test P3(0, 0, -0.001) ∉ c
- @test P3(0, 0, 1.001) ∉ c
- @test P3(1, 1, 1) ∉ c
-
- c = rand(Cylinder{T})
- @test c isa Cylinder
- @test embeddim(c) == 3
-
- c = Cylinder(P3(0, 0, 0), P3(0, 0, 1), T(1))
- @test sprint(show, c) ==
- "Cylinder(bot: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), top: Plane(p: (0.0, 0.0, 1.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 1.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), c) == """
- Cylinder{3,Float32}
- ├─ bot: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- ├─ top: Plane(p: (0.0, 0.0, 1.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 1.0f0"""
- else
- @test sprint(show, MIME("text/plain"), c) == """
- Cylinder{3,Float64}
- ├─ bot: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- ├─ top: Plane(p: (0.0, 0.0, 1.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 1.0"""
- end
+@testitem "Disk" setup = [Setup] begin
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ @test embeddim(d) == 3
+ @test paramdim(d) == 2
+ @test crs(d) <: Cartesian{NoDatum}
+ @test Meshes.lentype(d) == ℳ
+ @test plane(d) == p
+ @test center(d) == cart(0, 0, 0)
+ @test radius(d) == T(2) * u"m"
+ @test normal(d) == vector(0, 0, 1)
+ @test measure(d) == T(π) * T(2)^2 * u"m^2"
+ @test area(d) == measure(d)
+ @test cart(0, 0, 0) ∈ d
+ @test cart(0, 0, 1) ∉ d
+ @test boundary(d) == Circle(p, T(2))
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ equaltest(d)
+ isapproxtest(d)
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ @test sprint(show, d) ==
+ "Disk(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), d) == """
+ Disk
+ ├─ plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 2.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), d) == """
+ Disk
+ ├─ plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 2.0 m"""
end
+end
- @testset "CylinderSurface" begin
- c = CylinderSurface(T(2))
- @test embeddim(c) == 3
- @test paramdim(c) == 2
- @test coordtype(c) == T
- @test radius(c) == T(2)
- @test bottom(c) == Plane(P3(0, 0, 0), V3(0, 0, 1))
- @test top(c) == Plane(P3(0, 0, 1), V3(0, 0, 1))
- @test center(c) == P3(0.0, 0.0, 0.5)
- @test centroid(c) == P3(0.0, 0.0, 0.5)
- @test axis(c) == Line(P3(0, 0, 0), P3(0, 0, 1))
- @test isright(c)
- @test isnothing(boundary(c))
- @test measure(c) == area(c) ≈ 2 * T(2)^2 * pi + 2 * T(2) * pi
- @test !Meshes.hasintersectingplanes(c)
-
- c = CylinderSurface(Plane(P3(0, 0, 0), V3(0, 0, 1)), Plane(P3(0, 0, 1), V3(1, 0, 1)), T(5))
- @test Meshes.hasintersectingplanes(c)
-
- c1 = CylinderSurface(P3(0, 0, 0), P3(0, 0, 1), T(1))
- c2 = CylinderSurface(P3(0, 0, 0), P3(0, 0, 1))
- c3 = CylinderSurface(T(1))
- @test c1 == c2 == c3
- @test c1 ≈ c2 ≈ c3
-
- c = CylinderSurface(Plane(P3(1, 2, 3), V3(0, 0, 1)), Plane(P3(4, 5, 6), V3(0, 0, 1)), T(5))
- @test measure(c) == area(c) ≈ 2 * T(5)^2 * pi + 2 * T(5) * pi * sqrt(3 * T(3)^2)
-
- c = CylinderSurface(T(1))
- @test c(T(0), T(0)) ≈ P3(1, 0, 0)
- @test c(T(0.5), T(0)) ≈ P3(-1, 0, 0)
- @test c(T(0), T(1)) ≈ P3(1, 0, 1)
- @test c(T(0.5), T(1)) ≈ P3(-1, 0, 1)
-
- c = CylinderSurface(1.0)
- @test coordtype(c) == Float64
- c = CylinderSurface(1.0f0)
- @test coordtype(c) == Float32
- c = CylinderSurface(1)
- @test coordtype(c) == Float64
-
- c = rand(CylinderSurface{T})
- @test c isa CylinderSurface
- @test embeddim(c) == 3
-
- c = CylinderSurface(T(1))
- @test sprint(show, c) ==
- "CylinderSurface(bot: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), top: Plane(p: (0.0, 0.0, 1.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 1.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), c) == """
- CylinderSurface{3,Float32}
- ├─ bot: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- ├─ top: Plane(p: (0.0, 0.0, 1.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 1.0f0"""
- else
- @test sprint(show, MIME("text/plain"), c) == """
- CylinderSurface{3,Float64}
- ├─ bot: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- ├─ top: Plane(p: (0.0, 0.0, 1.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0))
- └─ radius: 1.0"""
- end
+@testitem "Circle" setup = [Setup] begin
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ c = Circle(p, T(2))
+ @test embeddim(c) == 3
+ @test paramdim(c) == 1
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test plane(c) == p
+ @test center(c) == cart(0, 0, 0)
+ @test radius(c) == T(2) * u"m"
+ @test measure(c) == 2 * T(π) * T(2) * u"m"
+ @test length(c) == measure(c)
+ @test cart(2, 0, 0) ∈ c
+ @test cart(0, 2, 0) ∈ c
+ @test cart(0, 0, 0) ∉ c
+ @test isnothing(boundary(c))
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ c = Circle(p, T(2))
+ equaltest(c)
+ isapproxtest(c)
+
+ # 3D circumcircle
+ p1 = cart(0, 4, 0)
+ p2 = cart(0, -4, 0)
+ p3 = cart(0, 0, 4)
+ c = Circle(p1, p2, p3)
+ @test p1 ∈ c
+ @test p2 ∈ c
+ @test p3 ∈ c
+
+ # circle parametrization
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ c = Circle(p, T(2))
+ @test c(T(0)) ≈ cart(2, 0, 0)
+ @test c(T(0.25)) ≈ cart(0, 2, 0)
+ @test c(T(0.5)) ≈ cart(-2, 0, 0)
+ @test c(T(0.75)) ≈ cart(0, -2, 0)
+ @test c(T(1)) ≈ cart(2, 0, 0)
+
+ # CRS propagation
+ c1 = Cartesian{WGS84Latest}(T(0), T(4), T(0))
+ c2 = Cartesian{WGS84Latest}(T(0), T(-4), T(0))
+ c3 = Cartesian{WGS84Latest}(T(0), T(0), T(4))
+ c = Circle(Point(c1), Point(c2), Point(c3))
+ @test crs(c) === typeof(c1)
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ c = Circle(p, T(2))
+ @test sprint(show, c) ==
+ "Circle(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), c) == """
+ Circle
+ ├─ plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 2.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), c) == """
+ Circle
+ ├─ plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 2.0 m"""
end
+end
- @testset "ParaboloidSurface" begin
- p = ParaboloidSurface(P3(0, 0, 0), T(1), T(2))
- @test embeddim(p) == 3
- @test paramdim(p) == 2
- @test coordtype(p) == T
- @test focallength(p) == T(2)
- @test radius(p) == T(1)
- @test axis(p) == Line(P3(0, 0, 0), P3(0, 0, T(2)))
- @test measure(p) == area(p) ≈ T(32π / 3 * (17√17 / 64 - 1))
-
- p1 = ParaboloidSurface(P3(1, 2, 3), T(1), T(1))
- p2 = ParaboloidSurface(P3(1, 2, 3), T(1))
- p3 = ParaboloidSurface(P3(1, 2, 3))
- @test p1 == p2 == p3
- @test p1 ≈ p2 ≈ p3
-
- p1 = ParaboloidSurface((1, 2, 3), 1.0, 1.0)
- p2 = ParaboloidSurface((1, 2, 3), 1.0)
- p3 = ParaboloidSurface((1, 2, 3))
- @test p1 == p2 == p3
- @test p1 ≈ p2 ≈ p3
-
- p = ParaboloidSurface((1.0, 2.0, 3.0), 4.0, 5.0)
- @test coordtype(p) == Float64
- @test radius(p) == 4.0
- @test focallength(p) == 5.0
-
- p = ParaboloidSurface(P3(1, 5, 2), T(3), T(4))
- @test measure(p) == area(p) ≈ T(128π / 3 * (73√73 / 512 - 1))
- @test p(T(0), T(0)) ≈ P3(1, 5, 2)
- @test p(T(1), T(0)) ≈ P3(4, 5, 2 + 3^2 / (4 * 4))
- @test_throws DomainError p(T(-0.1), T(0))
- @test_throws DomainError p(T(1.1), T(0))
-
- p = ParaboloidSurface()
- @test coordtype(p) == Float64
- @test p(0.0, 0.0) ≈ Point3(0, 0, 0)
- @test p(0.5, 0.0) ≈ Point3(0.5, 0, 0.5^2 / 4)
- @test p(0.0, 0.5) ≈ Point3(0, 0, 0)
- @test p(0.5, 0.5) ≈ Point3(-0.5, 0, 0.5^2 / 4)
-
- p = ParaboloidSurface(Point3(0, 0, 0))
- @test coordtype(p) == Float64
- p = ParaboloidSurface(Point3f(0, 0, 0))
- @test coordtype(p) == Float32
-
- p = rand(ParaboloidSurface{T})
- @test p isa ParaboloidSurface
- @test embeddim(p) == 3
-
- p = ParaboloidSurface(P3(0, 0, 0))
- @test sprint(show, p) == "ParaboloidSurface(apex: (0.0, 0.0, 0.0), radius: 1.0, focallength: 1.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), p) == """
- ParaboloidSurface{3,Float32}
- ├─ apex: Point(0.0f0, 0.0f0, 0.0f0)
- ├─ radius: 1.0f0
- └─ focallength: 1.0f0"""
- else
- @test sprint(show, MIME("text/plain"), p) == """
- ParaboloidSurface{3,Float64}
- ├─ apex: Point(0.0, 0.0, 0.0)
- ├─ radius: 1.0
- └─ focallength: 1.0"""
- end
+@testitem "Cylinder" setup = [Setup] begin
+ c = Cylinder(Plane(cart(1, 2, 3), vector(0, 0, 1)), Plane(cart(4, 5, 6), vector(0, 0, 1)), T(5))
+ @test embeddim(c) == 3
+ @test paramdim(c) == 3
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test radius(c) == T(5) * u"m"
+ @test bottom(c) == Plane(cart(1, 2, 3), vector(0, 0, 1))
+ @test top(c) == Plane(cart(4, 5, 6), vector(0, 0, 1))
+ @test axis(c) == Line(cart(1, 2, 3), cart(4, 5, 6))
+ @test !isright(c)
+ @test measure(c) == volume(c) ≈ T(5)^2 * pi * T(3) * sqrt(T(3)) * u"m^3"
+ @test cart(1, 2, 3) ∈ c
+ @test cart(4, 5, 6) ∈ c
+ @test cart(0.99, 1.99, 2.99) ∉ c
+ @test cart(4.01, 5.01, 6.01) ∉ c
+ @test !Meshes.hasintersectingplanes(c)
+ @test c(0, 0, 0) ≈ bottom(c)(0, 0)
+ @test c(0, 0, 1) ≈ top(c)(0, 0)
+ @test c(1, 0.25, 0.5) ≈ Point(T(4.330127018922193), T(10.330127018922191), T(4.5))
+ @test_throws DomainError c(1.1, 0, 0)
+
+ c = Cylinder(T(1))
+ equaltest(c)
+ isapproxtest(c)
+
+ c = Cylinder(Plane(cart(0, 0, 0), vector(0, 0, 1)), Plane(cart(0, 0, 1), vector(1, 0, 1)), T(5))
+ @test Meshes.hasintersectingplanes(c)
+
+ c1 = Cylinder(cart(0, 0, 0), cart(0, 0, 1), T(1))
+ c2 = Cylinder(cart(0, 0, 0), cart(0, 0, 1))
+ c3 = Cylinder(T(1))
+ @test c1 == c2 == c3
+ @test c1 ≈ c2 ≈ c3
+
+ c = Cylinder(T(1))
+ @test Meshes.lentype(c) == ℳ
+ c = Cylinder(1)
+ @test Meshes.lentype(c) == Meshes.Met{Float64}
+
+ c = Cylinder(cart(0, 0, 0), cart(0, 0, 1), T(1))
+ @test radius(c) == T(1) * u"m"
+ @test bottom(c) == Plane(cart(0, 0, 0), vector(0, 0, 1))
+ @test top(c) == Plane(cart(0, 0, 1), vector(0, 0, 1))
+ @test centroid(c) == cart(0.0, 0.0, 0.5)
+ @test axis(c) == Line(cart(0, 0, 0), cart(0, 0, 1))
+ @test isright(c)
+ @test boundary(c) == CylinderSurface(cart(0, 0, 0), cart(0, 0, 1), T(1))
+ @test measure(c) == volume(c) ≈ T(π) * u"m^3"
+ @test cart(0, 0, 0) ∈ c
+ @test cart(0, 0, 1) ∈ c
+ @test cart(1, 0, 0) ∈ c
+ @test cart(0, 1, 0) ∈ c
+ @test cart(cosd(60), sind(60), 0.5) ∈ c
+ @test cart(0, 0, -0.001) ∉ c
+ @test cart(0, 0, 1.001) ∉ c
+ @test cart(1, 1, 1) ∉ c
+
+ # machine type is preserved in parameterization
+ c = Cylinder(T(1))
+ @test Meshes.lentype(c(0, 0, 0)) == ℳ
+ @test Meshes.lentype(c(0.0, 0.0, 0.0)) == ℳ
+ @test Meshes.lentype(c(0.0f0, 0.0f0, 0.0f0)) == ℳ
+
+ c = Cylinder(cart(0, 0, 0), cart(0, 0, 1), T(1))
+ @test sprint(show, c) ==
+ "Cylinder(bot: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), top: Plane(p: (x: 0.0 m, y: 0.0 m, z: 1.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 1.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), c) == """
+ Cylinder
+ ├─ bot: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ ├─ top: Plane(p: (x: 0.0 m, y: 0.0 m, z: 1.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 1.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), c) == """
+ Cylinder
+ ├─ bot: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ ├─ top: Plane(p: (x: 0.0 m, y: 0.0 m, z: 1.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 1.0 m"""
end
+end
- @testset "Cone" begin
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- d = Disk(p, T(2))
- a = P3(0, 0, 1)
- c = Cone(d, a)
- @test embeddim(c) == 3
- @test paramdim(c) == 3
- @test coordtype(c) == T
- @test boundary(c) == ConeSurface(d, a)
-
- c = rand(Cone{T})
- @test c isa Cone
- @test embeddim(c) == 3
-
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- d = Disk(p, T(2))
- a = P3(0, 0, 1)
- c = Cone(d, a)
- @test sprint(show, c) ==
- "Cone(base: Disk(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0), apex: (0.0, 0.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), c) == """
- Cone{3,Float32}
- ├─ base: Disk(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0)
- └─ apex: Point(0.0f0, 0.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), c) == """
- Cone{3,Float64}
- ├─ base: Disk(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0)
- └─ apex: Point(0.0, 0.0, 1.0)"""
- end
-
- # cone: apex at (5,4,3); base center at (5,1,3)
- # halfangle: 30° -> radius: sqrt(3)
- # axis of the cone is parallel to y axis
- p = Plane(P3(5, 1, 3), V3(0, 1, 0))
- d = Disk(p, sqrt(T(3)))
- a = P3(5, 4, 3)
- c = Cone(d, a)
-
- @test rad2deg(Meshes.halfangle(c)) ≈ T(30)
- @test Meshes.height(c) ≈ T(3)
-
- @test P3(5, 1, 3) ∈ c
- @test P3(5, 4, 3) ∈ c
- @test P3(5, 1, 3 - sqrt(3)) ∈ c
- @test P3(5, 1, 3 + sqrt(3)) ∈ c
- @test P3(5 - sqrt(3), 1, 3) ∈ c
- @test P3(5 + sqrt(3), 1, 3) ∈ c
- @test P3(5, 2.5, 3) ∈ c
- @test P3(5 + sqrt(3) / 2, 2.5, 3) ∈ c
- @test P3(5 - sqrt(3) / 2, 2.5, 3) ∈ c
-
- @test P3(5, 0.9, 3) ∉ c
- @test P3(5, 4.1, 3) ∉ c
- @test P3(5, 1, 1) ∉ c
- @test P3(5 + sqrt(3) + 0.01, 1, 3) ∉ c
- @test P3(5 + sqrt(3) / 2 + 0.01, 2.5, 3) ∉ c
- @test P3(5 - sqrt(3) / 2 - 0.01, 2.5, 3) ∉ c
+@testitem "CylinderSurface" setup = [Setup] begin
+ c = CylinderSurface(T(2))
+ @test embeddim(c) == 3
+ @test paramdim(c) == 2
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test radius(c) == T(2) * u"m"
+ @test bottom(c) == Plane(cart(0, 0, 0), vector(0, 0, 1))
+ @test top(c) == Plane(cart(0, 0, 1), vector(0, 0, 1))
+ @test centroid(c) == cart(0.0, 0.0, 0.5)
+ @test axis(c) == Line(cart(0, 0, 0), cart(0, 0, 1))
+ @test isright(c)
+ @test isnothing(boundary(c))
+ @test measure(c) == area(c) ≈ (2 * T(2)^2 * pi + 2 * T(2) * pi) * u"m^2"
+ @test !Meshes.hasintersectingplanes(c)
+
+ c = CylinderSurface(T(1))
+ equaltest(c)
+ isapproxtest(c)
+
+ c = CylinderSurface(Plane(cart(0, 0, 0), vector(0, 0, 1)), Plane(cart(0, 0, 1), vector(1, 0, 1)), T(5))
+ @test Meshes.hasintersectingplanes(c)
+
+ c1 = CylinderSurface(cart(0, 0, 0), cart(0, 0, 1), T(1))
+ c2 = CylinderSurface(cart(0, 0, 0), cart(0, 0, 1))
+ c3 = CylinderSurface(T(1))
+ @test c1 == c2 == c3
+ @test c1 ≈ c2 ≈ c3
+
+ c = CylinderSurface(Plane(cart(1, 2, 3), vector(0, 0, 1)), Plane(cart(4, 5, 6), vector(0, 0, 1)), T(5))
+ @test measure(c) == area(c) ≈ (2 * T(5)^2 * pi + 2 * T(5) * pi * sqrt(3 * T(3)^2)) * u"m^2"
+
+ c = CylinderSurface(T(1))
+ @test c(T(0), T(0)) ≈ cart(1, 0, 0)
+ @test c(T(0.5), T(0)) ≈ cart(-1, 0, 0)
+ @test c(T(0), T(1)) ≈ cart(1, 0, 1)
+ @test c(T(0.5), T(1)) ≈ cart(-1, 0, 1)
+
+ # machine type is preserved in parameterization
+ c = CylinderSurface(T(1))
+ @test Meshes.lentype(c(0, 0)) == ℳ
+ @test Meshes.lentype(c(0.0, 0.0)) == ℳ
+ @test Meshes.lentype(c(0.0f0, 0.0f0)) == ℳ
+
+ c = CylinderSurface(1.0)
+ @test Meshes.lentype(c) == Meshes.Met{Float64}
+ c = CylinderSurface(1.0f0)
+ @test Meshes.lentype(c) == Meshes.Met{Float32}
+ c = CylinderSurface(1)
+ @test Meshes.lentype(c) == Meshes.Met{Float64}
+
+ # CRS propagation
+ c1 = Cartesian{WGS84Latest}(T(0), T(0), T(0))
+ c2 = Cartesian{WGS84Latest}(T(0), T(0), T(1))
+ c = CylinderSurface(Point(c1), Point(c2), T(1))
+ @test crs(centroid(c)) === crs(c)
+
+ c = CylinderSurface(T(1))
+ @test sprint(show, c) ==
+ "CylinderSurface(bot: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), top: Plane(p: (x: 0.0 m, y: 0.0 m, z: 1.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 1.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), c) == """
+ CylinderSurface
+ ├─ bot: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ ├─ top: Plane(p: (x: 0.0 m, y: 0.0 m, z: 1.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 1.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), c) == """
+ CylinderSurface
+ ├─ bot: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ ├─ top: Plane(p: (x: 0.0 m, y: 0.0 m, z: 1.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m))
+ └─ radius: 1.0 m"""
end
+end
- @testset "ConeSurface" begin
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- d = Disk(p, T(2))
- a = P3(0, 0, 1)
- s = ConeSurface(d, a)
- @test embeddim(s) == 3
- @test paramdim(s) == 2
- @test coordtype(s) == T
- @test isnothing(boundary(s))
-
- c = rand(ConeSurface{T})
- @test c isa ConeSurface
- @test embeddim(c) == 3
-
- p = Plane(P3(0, 0, 0), V3(0, 0, 1))
- d = Disk(p, T(2))
- a = P3(0, 0, 1)
- s = ConeSurface(d, a)
- @test sprint(show, s) ==
- "ConeSurface(base: Disk(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0), apex: (0.0, 0.0, 1.0))"
- if T === Float32
- @test sprint(show, MIME("text/plain"), s) == """
- ConeSurface{3,Float32}
- ├─ base: Disk(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0)
- └─ apex: Point(0.0f0, 0.0f0, 1.0f0)"""
- else
- @test sprint(show, MIME("text/plain"), s) == """
- ConeSurface{3,Float64}
- ├─ base: Disk(plane: Plane(p: (0.0, 0.0, 0.0), u: (1.0, -0.0, -0.0), v: (-0.0, 1.0, -0.0)), radius: 2.0)
- └─ apex: Point(0.0, 0.0, 1.0)"""
- end
+@testitem "ParaboloidSurface" setup = [Setup] begin
+ p = ParaboloidSurface(cart(0, 0, 0), T(1), T(2))
+ @test embeddim(p) == 3
+ @test paramdim(p) == 2
+ @test crs(p) <: Cartesian{NoDatum}
+ @test Meshes.lentype(p) == ℳ
+ @test focallength(p) == T(2) * u"m"
+ @test radius(p) == T(1) * u"m"
+ @test axis(p) == Line(cart(0, 0, 0), cart(0, 0, T(2)))
+ @test measure(p) == area(p) ≈ T(32π / 3 * (17√17 / 64 - 1)) * u"m^2"
+ @test centroid(p) == cart(0, 0, 1 / 16)
+
+ p = ParaboloidSurface(cart(0, 0, 0), T(1), T(2))
+ equaltest(p)
+ isapproxtest(p)
+
+ p1 = ParaboloidSurface(cart(1, 2, 3), T(1), T(1))
+ p2 = ParaboloidSurface(cart(1, 2, 3), T(1))
+ p3 = ParaboloidSurface(cart(1, 2, 3))
+ @test p1 == p2 == p3
+ @test p1 ≈ p2 ≈ p3
+
+ p1 = ParaboloidSurface((1, 2, 3), 1.0, 1.0)
+ p2 = ParaboloidSurface((1, 2, 3), 1.0)
+ p3 = ParaboloidSurface((1, 2, 3))
+ @test p1 == p2 == p3
+ @test p1 ≈ p2 ≈ p3
+
+ p = ParaboloidSurface((1.0, 2.0, 3.0), 4.0, 5.0)
+ @test Meshes.lentype(p) == Meshes.Met{Float64}
+ @test radius(p) == 4.0 * u"m"
+ @test focallength(p) == 5.0 * u"m"
+
+ p = ParaboloidSurface(cart(1, 5, 2), T(3), T(4))
+ @test measure(p) == area(p) ≈ T(128π / 3 * (73√73 / 512 - 1)) * u"m^2"
+ @test p(T(0), T(0)) ≈ cart(1, 5, 2)
+ @test p(T(1), T(0)) ≈ cart(4, 5, 2 + 3^2 / (4 * 4))
+ @test_throws DomainError p(T(-0.1), T(0))
+ @test_throws DomainError p(T(1.1), T(0))
+
+ p = ParaboloidSurface()
+ @test Meshes.lentype(p) == Meshes.Met{Float64}
+ @test p(0.0, 0.0) ≈ Point(0, 0, 0)
+ @test p(0.5, 0.0) ≈ Point(0.5, 0, 0.5^2 / 4)
+ @test p(0.0, 0.5) ≈ Point(0, 0, 0)
+ @test p(0.5, 0.5) ≈ Point(-0.5, 0, 0.5^2 / 4)
+
+ p = ParaboloidSurface(Point(0.0, 0.0, 0.0))
+ @test Meshes.lentype(p) == Meshes.Met{Float64}
+ p = ParaboloidSurface(Point(0.0f0, 0.0f0, 0.0f0))
+ @test Meshes.lentype(p) == Meshes.Met{Float32}
+
+ p = ParaboloidSurface(cart(0, 0, 0), T(1), T(1))
+ @test sprint(show, p) == "ParaboloidSurface(apex: (x: 0.0 m, y: 0.0 m, z: 0.0 m), radius: 1.0 m, focallength: 1.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), p) == """
+ ParaboloidSurface
+ ├─ apex: Point(x: 0.0f0 m, y: 0.0f0 m, z: 0.0f0 m)
+ ├─ radius: 1.0f0 m
+ └─ focallength: 1.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), p) == """
+ ParaboloidSurface
+ ├─ apex: Point(x: 0.0 m, y: 0.0 m, z: 0.0 m)
+ ├─ radius: 1.0 m
+ └─ focallength: 1.0 m"""
end
+end
- @testset "Frustum" begin
- pb = Plane(P3(0, 0, 0), V3(0, 0, 1))
- db = Disk(pb, T(1))
- pt = Plane(P3(0, 0, 10), V3(0, 0, 1))
- dt = Disk(pt, T(2))
- f = Frustum(db, dt)
- @test embeddim(f) == 3
- @test coordtype(f) == T
- @test boundary(f) == FrustumSurface(db, dt)
-
- @test_throws AssertionError Frustum(db, db)
-
- f = rand(Frustum{T})
- @test f isa Frustum
-
- f = Frustum(db, dt)
- @test P3(0, 0, 0) ∈ f
- @test P3(0, 0, 10) ∈ f
- @test P3(1, 0, 0) ∈ f
- @test P3(2, 0, 10) ∈ f
- @test P3(1, 0, 5) ∈ f
-
- @test P3(1, 1, 0) ∉ f
- @test P3(2, 2, 10) ∉ f
- @test P3(0, 0, -0.01) ∉ f
- @test P3(0, 0, 10.01) ∉ f
-
- # reverse order, when top is larger than bottom
- # the frustum is the same geometry
- f = Frustum(dt, db)
- @test P3(0, 0, 0) ∈ f
- @test P3(0, 0, 10) ∈ f
- @test P3(1, 0, 0) ∈ f
- @test P3(2, 0, 10) ∈ f
- @test P3(1, 0, 5) ∈ f
-
- @test P3(1, 1, 0) ∉ f
- @test P3(2, 2, 10) ∉ f
- @test P3(0, 0, -0.01) ∉ f
- @test P3(0, 0, 10.01) ∉ f
+@testitem "Cone" setup = [Setup] begin
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ c = Cone(d, a)
+ @test embeddim(c) == 3
+ @test paramdim(c) == 3
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test boundary(c) == ConeSurface(d, a)
+ @test_throws DomainError c(T(0), T(0), nextfloat(T(1)))
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ c = Cone(d, a)
+ @test embeddim(c) == 3
+ @test paramdim(c) == 3
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test c(T(0), T(0), T(0)) ≈ centroid(p)
+ @test c(T(0), T(0), T(1)) ≈ a
+ @test c(T(1.0), T(0.25), T(0.0)) ≈ cart(0, 2, 0)
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ c = Cone(d, a)
+ equaltest(c)
+ isapproxtest(c)
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ c = Cone(d, a)
+ @test sprint(show, c) ==
+ "Cone(base: Disk(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m), apex: (x: 0.0 m, y: 0.0 m, z: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), c) == """
+ Cone
+ ├─ base: Disk(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m)
+ └─ apex: Point(x: 0.0f0 m, y: 0.0f0 m, z: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), c) == """
+ Cone
+ ├─ base: Disk(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m)
+ └─ apex: Point(x: 0.0 m, y: 0.0 m, z: 1.0 m)"""
end
- @testset "FrustumSurface" begin
- pb = Plane(P3(0, 0, 0), V3(0, 0, 1))
- db = Disk(pb, T(1))
- pt = Plane(P3(0, 0, 10), V3(0, 0, 1))
- dt = Disk(pt, T(2))
- f = FrustumSurface(db, dt)
- @test embeddim(f) == 3
- @test coordtype(f) == T
- @test isnothing(boundary(f))
-
- @test_throws AssertionError FrustumSurface(db, db)
+ # cone: apex at (5,4,3); base center at (5,1,3)
+ # halfangle: 30° -> radius: sqrt(3)
+ # axis of the cone is parallel to y axis
+ p = Plane(cart(5, 1, 3), vector(0, 1, 0))
+ d = Disk(p, sqrt(T(3)))
+ a = cart(5, 4, 3)
+ c = Cone(d, a)
+
+ @test rad2deg(Meshes.halfangle(c)) ≈ T(30)
+ @test Meshes.height(c) ≈ T(3) * u"m"
+
+ @test cart(5, 1, 3) ∈ c
+ @test cart(5, 4, 3) ∈ c
+ @test cart(5, 1, 3 - sqrt(3)) ∈ c
+ @test cart(5, 1, 3 + sqrt(3)) ∈ c
+ @test cart(5 - sqrt(3), 1, 3) ∈ c
+ @test cart(5 + sqrt(3), 1, 3) ∈ c
+ @test cart(5, 2.5, 3) ∈ c
+ @test cart(5 + sqrt(3) / 2, 2.5, 3) ∈ c
+ @test cart(5 - sqrt(3) / 2, 2.5, 3) ∈ c
+
+ @test cart(5, 0.9, 3) ∉ c
+ @test cart(5, 4.1, 3) ∉ c
+ @test cart(5, 1, 1) ∉ c
+ @test cart(5 + sqrt(3) + 0.01, 1, 3) ∉ c
+ @test cart(5 + sqrt(3) / 2 + 0.01, 2.5, 3) ∉ c
+ @test cart(5 - sqrt(3) / 2 - 0.01, 2.5, 3) ∉ c
+end
- f = rand(FrustumSurface{T})
- @test f isa FrustumSurface
+@testitem "ConeSurface" setup = [Setup] begin
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ s = ConeSurface(d, a)
+ @test embeddim(s) == 3
+ @test paramdim(s) == 2
+ @test crs(s) <: Cartesian{NoDatum}
+ @test Meshes.lentype(s) == ℳ
+ @test isnothing(boundary(s))
+ @test_throws DomainError s(T(0), nextfloat(T(1)))
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ c = ConeSurface(d, a)
+ @test embeddim(c) == 3
+ @test paramdim(c) == 2
+ @test crs(c) <: Cartesian{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+ @test c(T(0), T(0)) ≈ cart(2, 0, 0)
+ @test c(T(0), T(1)) ≈ a
+ @test c(T(0.25), T(0)) ≈ cart(0, 2, 0)
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ c = ConeSurface(d, a)
+ equaltest(c)
+ isapproxtest(c)
+
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ d = Disk(p, T(2))
+ a = cart(0, 0, 1)
+ s = ConeSurface(d, a)
+ @test sprint(show, s) ==
+ "ConeSurface(base: Disk(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m), apex: (x: 0.0 m, y: 0.0 m, z: 1.0 m))"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), s) == """
+ ConeSurface
+ ├─ base: Disk(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m)
+ └─ apex: Point(x: 0.0f0 m, y: 0.0f0 m, z: 1.0f0 m)"""
+ else
+ @test sprint(show, MIME("text/plain"), s) == """
+ ConeSurface
+ ├─ base: Disk(plane: Plane(p: (x: 0.0 m, y: 0.0 m, z: 0.0 m), u: (1.0 m, -0.0 m, -0.0 m), v: (-0.0 m, 1.0 m, -0.0 m)), radius: 2.0 m)
+ └─ apex: Point(x: 0.0 m, y: 0.0 m, z: 1.0 m)"""
end
+end
+
+@testitem "Frustum" setup = [Setup] begin
+ pb = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ db = Disk(pb, T(1))
+ pt = Plane(cart(0, 0, 10), vector(0, 0, 1))
+ dt = Disk(pt, T(2))
+ f = Frustum(db, dt)
+ @test embeddim(f) == 3
+ @test crs(f) <: Cartesian{NoDatum}
+ @test Meshes.lentype(f) == ℳ
+ @test boundary(f) == FrustumSurface(db, dt)
+
+ @test_throws AssertionError Frustum(db, db)
+
+ pb = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ db = Disk(pb, T(1))
+ pt = Plane(cart(0, 0, 10), vector(0, 0, 1))
+ dt = Disk(pt, T(2))
+ f = Frustum(db, dt)
+ equaltest(f)
+ isapproxtest(f)
+
+ f = Frustum(db, dt)
+ @test cart(0, 0, 0) ∈ f
+ @test cart(0, 0, 10) ∈ f
+ @test cart(1, 0, 0) ∈ f
+ @test cart(2, 0, 10) ∈ f
+ @test cart(1, 0, 5) ∈ f
+
+ @test cart(1, 1, 0) ∉ f
+ @test cart(2, 2, 10) ∉ f
+ @test cart(0, 0, -0.01) ∉ f
+ @test cart(0, 0, 10.01) ∉ f
+
+ # reverse order, when top is larger than bottom
+ # the frustum is the same geometry
+ f = Frustum(dt, db)
+ @test cart(0, 0, 0) ∈ f
+ @test cart(0, 0, 10) ∈ f
+ @test cart(1, 0, 0) ∈ f
+ @test cart(2, 0, 10) ∈ f
+ @test cart(1, 0, 5) ∈ f
+
+ @test cart(1, 1, 0) ∉ f
+ @test cart(2, 2, 10) ∉ f
+ @test cart(0, 0, -0.01) ∉ f
+ @test cart(0, 0, 10.01) ∉ f
+end
+
+@testitem "FrustumSurface" setup = [Setup] begin
+ pb = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ db = Disk(pb, T(1))
+ pt = Plane(cart(0, 0, 10), vector(0, 0, 1))
+ dt = Disk(pt, T(2))
+ f = FrustumSurface(db, dt)
+ @test embeddim(f) == 3
+ @test paramdim(f) == 2
+ @test crs(f) <: Cartesian{NoDatum}
+ @test Meshes.lentype(f) == ℳ
+ @test isnothing(boundary(f))
+
+ @test_throws AssertionError FrustumSurface(db, db)
+
+ pb = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ db = Disk(pb, T(1))
+ pt = Plane(cart(0, 0, 10), vector(0, 0, 1))
+ dt = Disk(pt, T(2))
+ f = FrustumSurface(db, dt)
+ equaltest(f)
+ isapproxtest(f)
+end
+
+@testitem "ParametrizedCurve" setup = [Setup] begin
+ fun(t) = Point(Polar(T(1), T(t)))
+ c = ParametrizedCurve(fun, (T(0), T(2π)))
+ @test embeddim(c) == 2
+ @test paramdim(c) == 1
+ @test crs(c) <: Polar{NoDatum}
+ @test Meshes.lentype(c) == ℳ
+
+ equaltest(c)
+
+ @test c(T(0)) == fun(T(0))
+ @test c(T(1)) == fun(T(2π))
+ @test c(T(0.5)) == fun(T(π))
+ @test_throws DomainError(T(-0.1), "c(t) is not defined for t outside [0, 1].") c(T(-0.1))
+ @test_throws DomainError(T(1.2), "c(t) is not defined for t outside [0, 1].") c(T(1.2))
+
+ @test boundary(c) === nothing
+
+ c = ParametrizedCurve(t -> cart(cospi(t), sinpi(t)), (T(0), T(1)))
+ @test boundary(c) == Multi([cart(1, 0), cart(-1, 0)])
+ @test perimeter(c) == zero(ℳ)
+
+ # CRS propagation
+ foo(t) = merc(t, 2t)
+ c = ParametrizedCurve(foo, (T(0), T(1)))
+ @test crs(c(T(0))) === crs(c)
+
+ @test sprint(show, c) == "ParametrizedCurve(fun: foo, range: (0.0, 1.0))"
+end
- @testset "Torus" begin
- t = Torus(T.((1, 1, 1)), T.((1, 0, 0)), 2, 1)
- @test P3(1, 1, -1) ∈ t
- @test P3(1, 1, 1) ∉ t
- @test paramdim(t) == 2
- @test Meshes.center(t) == P3(1, 1, 1)
- @test normal(t) == V3(1, 0, 0)
- @test radii(t) == (T(2), T(1))
- @test axis(t) == Line(P3(1, 1, 1), P3(2, 1, 1))
- @test measure(t) ≈ 8 * T(π)^2
- @test_throws ArgumentError length(t)
- @test_throws ArgumentError volume(t)
-
- # torus passing through three points
- p₁ = P3(0, 0, 0)
- p₂ = P3(1, 2, 3)
- p₃ = P3(3, 2, 1)
- t = Torus(p₁, p₂, p₃, T(1))
- c = center(t)
- R, r = radii(t)
- @test r == 1
- @test norm(p₁ - c) ≈ R
- @test norm(p₂ - c) ≈ R
- @test norm(p₃ - c) ≈ R
- @test p₁ ∈ t
- @test p₂ ∈ t
- @test p₃ ∈ t
-
- # constructor with tuples
- c₁ = T.((0, 0, 0))
- c₂ = T.((1, 2, 3))
- c₃ = T.((3, 2, 1))
- q = Torus(c₁, c₂, c₃, 1)
- @test q == t
-
- t = rand(Torus{T})
- @test t isa Torus
- @test embeddim(t) == 3
- @test coordtype(t) == T
- @test isnothing(boundary(t))
-
- t = Torus(P3(1, 1, 1), V3(1, 0, 0), 2, 1)
- @test sprint(show, t) == "Torus(center: (1.0, 1.0, 1.0), normal: (1.0, 0.0, 0.0), major: 2.0, minor: 1.0)"
- if T === Float32
- @test sprint(show, MIME("text/plain"), t) == """
- Torus{3,Float32}
- ├─ center: Point(1.0f0, 1.0f0, 1.0f0)
- ├─ normal: Vec(1.0f0, 0.0f0, 0.0f0)
- ├─ major: 2.0f0
- └─ minor: 1.0f0"""
- else
- @test sprint(show, MIME("text/plain"), t) == """
- Torus{3,Float64}
- ├─ center: Point(1.0, 1.0, 1.0)
- ├─ normal: Vec(1.0, 0.0, 0.0)
- ├─ major: 2.0
- └─ minor: 1.0"""
- end
+@testitem "Torus" setup = [Setup] begin
+ t = Torus(T.((1, 1, 1)), T.((1, 0, 0)), 2, 1)
+ @test cart(1, 1, -1) ∈ t
+ @test cart(1, 1, 1) ∉ t
+ @test paramdim(t) == 2
+ @test crs(t) <: Cartesian{NoDatum}
+ @test Meshes.lentype(t) == ℳ
+ @test center(t) == cart(1, 1, 1)
+ @test direction(t) == vector(1, 0, 0)
+ @test radii(t) == (T(2) * u"m", T(1) * u"m")
+ @test axis(t) == Line(cart(1, 1, 1), cart(2, 1, 1))
+ @test measure(t) ≈ 8 * T(π)^2 * u"m^2"
+ @test_throws ArgumentError length(t)
+ @test_throws ArgumentError volume(t)
+
+ t = Torus(cart(1, 1, 1), vector(1, 0, 0), T(2), T(1))
+ equaltest(t)
+ isapproxtest(t)
+
+ # torus passing through three points
+ p₁ = cart(0, 0, 0)
+ p₂ = cart(1, 2, 3)
+ p₃ = cart(3, 2, 1)
+ t = Torus(p₁, p₂, p₃, T(1))
+ c = center(t)
+ R, r = radii(t)
+ @test r == T(1) * u"m"
+ @test norm(p₁ - c) ≈ R
+ @test norm(p₂ - c) ≈ R
+ @test norm(p₃ - c) ≈ R
+ @test p₁ ∈ t
+ @test p₂ ∈ t
+ @test p₃ ∈ t
+
+ # constructor with tuples
+ c₁ = T.((0, 0, 0))
+ c₂ = T.((1, 2, 3))
+ c₃ = T.((3, 2, 1))
+ q = Torus(c₁, c₂, c₃, 1)
+ @test q == t
+
+ t = Torus(cart(1, 1, 1), vector(1, 0, 0), T(2), T(1))
+ @test sprint(show, t) ==
+ "Torus(center: (x: 1.0 m, y: 1.0 m, z: 1.0 m), direction: (1.0 m, 0.0 m, 0.0 m), major: 2.0 m, minor: 1.0 m)"
+ if T === Float32
+ @test sprint(show, MIME("text/plain"), t) == """
+ Torus
+ ├─ center: Point(x: 1.0f0 m, y: 1.0f0 m, z: 1.0f0 m)
+ ├─ direction: Vec(1.0f0 m, 0.0f0 m, 0.0f0 m)
+ ├─ major: 2.0f0 m
+ └─ minor: 1.0f0 m"""
+ else
+ @test sprint(show, MIME("text/plain"), t) == """
+ Torus
+ ├─ center: Point(x: 1.0 m, y: 1.0 m, z: 1.0 m)
+ ├─ direction: Vec(1.0 m, 0.0 m, 0.0 m)
+ ├─ major: 2.0 m
+ └─ minor: 1.0 m"""
end
end
diff --git a/test/rand.jl b/test/rand.jl
new file mode 100644
index 000000000..d746ab2f2
--- /dev/null
+++ b/test/rand.jl
@@ -0,0 +1,260 @@
+@testitem "rand" setup = [Setup] begin
+ p = rand(Point)
+ @test p isa Point
+ @test crs(p) <: Cartesian3D
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+ p = rand(Point, crs=Cartesian2D)
+ @test p isa Point
+ @test crs(p) <: Cartesian2D
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+
+ r = rand(Ray)
+ @test r isa Ray
+ @test crs(r) <: Cartesian3D
+ @test Meshes.lentype(r) === Meshes.Met{Float64}
+ r = rand(Ray, crs=Cartesian2D)
+ @test r isa Ray
+ @test crs(r) <: Cartesian2D
+ @test Meshes.lentype(r) === Meshes.Met{Float64}
+
+ l = rand(Line)
+ @test l isa Line
+ @test crs(l) <: Cartesian3D
+ @test Meshes.lentype(l) === Meshes.Met{Float64}
+ l = rand(Line, crs=Cartesian2D)
+ @test l isa Line
+ @test crs(l) <: Cartesian2D
+ @test Meshes.lentype(l) === Meshes.Met{Float64}
+
+ p = rand(Plane)
+ @test p isa Plane
+ @test crs(p) <: Cartesian3D
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+ p = rand(Plane, crs=LatLon)
+ @test p isa Plane
+ @test crs(p) <: LatLon
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+
+ b = rand(BezierCurve)
+ @test b isa BezierCurve
+ @test crs(b) <: Cartesian3D
+ @test Meshes.lentype(b) === Meshes.Met{Float64}
+ b = rand(BezierCurve, crs=Cartesian2D)
+ @test b isa BezierCurve
+ @test crs(b) <: Cartesian2D
+ @test Meshes.lentype(b) === Meshes.Met{Float64}
+
+ b = rand(Box)
+ @test b isa Box
+ @test crs(b) <: Cartesian3D
+ @test Meshes.lentype(b) === Meshes.Met{Float64}
+ b = rand(Box, crs=Cartesian2D)
+ @test b isa Box
+ @test crs(b) <: Cartesian2D
+ @test Meshes.lentype(b) === Meshes.Met{Float64}
+
+ b = rand(Ball)
+ @test b isa Ball
+ @test crs(b) <: Cartesian3D
+ @test Meshes.lentype(b) === Meshes.Met{Float64}
+ b = rand(Ball, crs=Cartesian2D)
+ @test b isa Ball
+ @test crs(b) <: Cartesian2D
+ @test Meshes.lentype(b) === Meshes.Met{Float64}
+
+ s = rand(Sphere)
+ @test s isa Sphere
+ @test crs(s) <: Cartesian3D
+ @test Meshes.lentype(s) === Meshes.Met{Float64}
+ s = rand(Sphere, crs=Cartesian2D)
+ @test s isa Sphere
+ @test crs(s) <: Cartesian2D
+ @test Meshes.lentype(s) === Meshes.Met{Float64}
+
+ e = rand(Ellipsoid)
+ @test e isa Ellipsoid
+ @test crs(e) <: Cartesian3D
+ @test Meshes.lentype(e) === Meshes.Met{Float64}
+ e = rand(Ellipsoid, crs=LatLon)
+ @test e isa Ellipsoid
+ @test crs(e) <: LatLon
+ @test Meshes.lentype(e) === Meshes.Met{Float64}
+
+ d = rand(Disk)
+ @test d isa Disk
+ @test crs(d) <: Cartesian3D
+ @test Meshes.lentype(d) === Meshes.Met{Float64}
+ d = rand(Disk, crs=LatLon)
+ @test d isa Disk
+ @test crs(d) <: LatLon
+ @test Meshes.lentype(d) === Meshes.Met{Float64}
+
+ c = rand(Circle)
+ @test c isa Circle
+ @test crs(c) <: Cartesian3D
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+ c = rand(Circle, crs=LatLon)
+ @test c isa Circle
+ @test crs(c) <: LatLon
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+
+ c = rand(Cylinder)
+ @test c isa Cylinder
+ @test crs(c) <: Cartesian3D
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+ c = rand(Cylinder, crs=LatLon)
+ @test c isa Cylinder
+ @test crs(c) <: LatLon
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+
+ c = rand(CylinderSurface)
+ @test c isa CylinderSurface
+ @test crs(c) <: Cartesian3D
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+ c = rand(CylinderSurface, crs=LatLon)
+ @test c isa CylinderSurface
+ @test crs(c) <: LatLon
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+
+ p = rand(ParaboloidSurface)
+ @test p isa ParaboloidSurface
+ @test crs(p) <: Cartesian3D
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+ p = rand(ParaboloidSurface, crs=LatLon)
+ @test p isa ParaboloidSurface
+ @test crs(p) <: LatLon
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+
+ c = rand(Cone)
+ @test c isa Cone
+ @test crs(c) <: Cartesian3D
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+ c = rand(Cone, crs=LatLon)
+ @test c isa Cone
+ @test crs(c) <: LatLon
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+
+ c = rand(ConeSurface)
+ @test c isa ConeSurface
+ @test crs(c) <: Cartesian3D
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+ c = rand(ConeSurface, crs=LatLon)
+ @test c isa ConeSurface
+ @test crs(c) <: LatLon
+ @test Meshes.lentype(c) === Meshes.Met{Float64}
+
+ f = rand(Frustum)
+ @test f isa Frustum
+ @test crs(f) <: Cartesian3D
+ @test Meshes.lentype(f) === Meshes.Met{Float64}
+ f = rand(Frustum, crs=LatLon)
+ @test f isa Frustum
+ @test crs(f) <: LatLon
+ @test Meshes.lentype(f) === Meshes.Met{Float64}
+
+ f = rand(FrustumSurface)
+ @test f isa FrustumSurface
+ @test crs(f) <: Cartesian3D
+ @test Meshes.lentype(f) === Meshes.Met{Float64}
+ f = rand(FrustumSurface, crs=LatLon)
+ @test f isa FrustumSurface
+ @test crs(f) <: LatLon
+ @test Meshes.lentype(f) === Meshes.Met{Float64}
+
+ t = rand(Torus)
+ @test t isa Torus
+ @test crs(t) <: Cartesian3D
+ @test Meshes.lentype(t) === Meshes.Met{Float64}
+ t = rand(Torus, crs=LatLon)
+ @test t isa Torus
+ @test crs(t) <: LatLon
+ @test Meshes.lentype(t) === Meshes.Met{Float64}
+
+ s = rand(Segment)
+ @test s isa Segment
+ @test crs(s) <: Cartesian3D
+ @test Meshes.lentype(s) === Meshes.Met{Float64}
+ s = rand(Segment, crs=Cartesian2D)
+ @test s isa Segment
+ @test crs(s) <: Cartesian2D
+ @test Meshes.lentype(s) === Meshes.Met{Float64}
+
+ r = rand(Rope)
+ @test r isa Rope
+ @test crs(r) <: Cartesian3D
+ @test Meshes.lentype(r) === Meshes.Met{Float64}
+ r = rand(Rope, crs=Cartesian2D)
+ @test r isa Rope
+ @test crs(r) <: Cartesian2D
+ @test Meshes.lentype(r) === Meshes.Met{Float64}
+
+ r = rand(Ring)
+ @test r isa Ring
+ @test crs(r) <: Cartesian3D
+ @test Meshes.lentype(r) === Meshes.Met{Float64}
+ r = rand(Ring, crs=Cartesian2D)
+ @test r isa Ring
+ @test crs(r) <: Cartesian2D
+ @test Meshes.lentype(r) === Meshes.Met{Float64}
+
+ NGONS = [Triangle, Quadrangle, Pentagon, Hexagon, Heptagon, Octagon, Nonagon, Decagon]
+ for NGON in NGONS
+ n = rand(NGON)
+ @test n isa NGON
+ @test crs(n) <: Cartesian3D
+ @test Meshes.lentype(n) === Meshes.Met{Float64}
+ n = rand(NGON, crs=Cartesian2D)
+ @test n isa NGON
+ @test crs(n) <: Cartesian2D
+ @test Meshes.lentype(n) === Meshes.Met{Float64}
+ end
+
+ p = rand(PolyArea)
+ @test p isa PolyArea
+ @test crs(p) <: Cartesian3D
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+ p = rand(PolyArea, crs=Cartesian2D)
+ @test p isa PolyArea
+ @test crs(p) <: Cartesian2D
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+
+ t = rand(Tetrahedron)
+ @test t isa Tetrahedron
+ @test crs(t) <: Cartesian3D
+ @test Meshes.lentype(t) === Meshes.Met{Float64}
+ t = rand(Tetrahedron, crs=LatLon)
+ @test t isa Tetrahedron
+ @test crs(t) <: LatLon
+ @test Meshes.lentype(t) === Meshes.Met{Float64}
+
+ h = rand(Hexahedron)
+ @test h isa Hexahedron
+ @test crs(h) <: Cartesian3D
+ @test Meshes.lentype(h) === Meshes.Met{Float64}
+ h = rand(Hexahedron, crs=LatLon)
+ @test h isa Hexahedron
+ @test crs(h) <: LatLon
+ @test Meshes.lentype(h) === Meshes.Met{Float64}
+
+ p = rand(Pyramid)
+ @test p isa Pyramid
+ @test crs(p) <: Cartesian3D
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+ p = rand(Pyramid, crs=LatLon)
+ @test p isa Pyramid
+ @test crs(p) <: LatLon
+ @test Meshes.lentype(p) === Meshes.Met{Float64}
+
+ w = rand(Wedge)
+ @test w isa Wedge
+ @test crs(w) <: Cartesian3D
+ @test Meshes.lentype(w) === Meshes.Met{Float64}
+ w = rand(Wedge, crs=LatLon)
+ @test w isa Wedge
+ @test crs(w) <: LatLon
+ @test Meshes.lentype(w) === Meshes.Met{Float64}
+
+ # vector of random geometries
+ ps = rand(Point, 10)
+ @test eltype(ps) <: Point
+end
diff --git a/test/refinement.jl b/test/refinement.jl
index e72e89a41..7c69c16a2 100644
--- a/test/refinement.jl
+++ b/test/refinement.jl
@@ -1,96 +1,168 @@
-@testset "Refinement" begin
- @testset "TriRefinement" begin
- grid = CartesianGrid{T}(3, 3)
- ref1 = refine(grid, TriRefinement())
- ref2 = refine(ref1, TriRefinement())
-
- if visualtests
- fig = Mke.Figure(size=(900, 300))
- viz(fig[1, 1], grid, showfacets=true)
- viz(fig[1, 2], ref1, showfacets=true)
- viz(fig[1, 3], ref2, showfacets=true)
- @test_reference "data/trirefine-$T.png" fig
- end
+@testitem "TriRefinement" setup = [Setup] begin
+ grid = cartgrid(3, 3)
+ ref1 = refine(grid, TriRefinement())
+ ref2 = refine(ref1, TriRefinement())
+
+ if visualtests
+ fig = Mke.Figure(size=(900, 300))
+ viz(fig[1, 1], grid, showsegments=true)
+ viz(fig[1, 2], ref1, showsegments=true)
+ viz(fig[1, 3], ref2, showsegments=true)
+ @test_reference "data/trirefine-$T.png" fig
+ end
+
+ # CRS propagation
+ grid = CartesianGrid((3, 3), merc(0, 0), (T(1), T(1)))
+ ref = refine(grid, TriRefinement())
+ @test crs(ref) === crs(grid)
+
+ # predicate
+ points = cart.([(0, 0), (4, 0), (8, 0), (3, 1), (5, 1), (2, 2), (4, 2), (6, 2), (4, 4)])
+ connec = connect.([(1, 2, 6), (2, 3, 8), (6, 8, 9), (2, 5, 4), (4, 5, 7), (4, 7, 6), (5, 8, 7)])
+ mesh = SimpleMesh(points, connec)
+ ref = refine(mesh, TriRefinement(e -> measure(e) > T(1) * u"m^2"))
+ @test nelements(ref) == 13
+ @test nvertices(ref) == 12
+ ref = refine(mesh, TriRefinement(e -> measure(e) ≤ T(1) * u"m^2"))
+ @test nelements(ref) == 15
+ @test nvertices(ref) == 13
+
+ # latlon
+ points = latlon.([(0, 0), (0, 4), (0, 8), (1, 3), (1, 5), (2, 2), (2, 4), (2, 6), (4, 4)])
+ connec = connect.([(1, 2, 6), (2, 3, 8), (6, 8, 9), (2, 5, 4), (4, 5, 7), (4, 7, 6), (5, 8, 7)])
+ mesh = SimpleMesh(points, connec)
+ ref = refine(mesh, TriRefinement())
+ @test nelements(ref) == 21
+ @test nvertices(ref) == 16
+end
+
+@testitem "QuadRefinement" setup = [Setup] begin
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.25), (0.75, 0.25), (0.5, 0.75)])
+ connec = connect.([(1, 2, 6, 5), (1, 5, 7, 3), (2, 4, 7, 6), (3, 7, 4)])
+ mesh = SimpleMesh(points, connec)
+ ref1 = refine(mesh, QuadRefinement())
+ ref2 = refine(ref1, QuadRefinement())
+ ref3 = refine(ref2, QuadRefinement())
+
+ if visualtests
+ fig = Mke.Figure(size=(900, 300))
+ viz(fig[1, 1], ref1, showsegments=true)
+ viz(fig[1, 2], ref2, showsegments=true)
+ viz(fig[1, 3], ref3, showsegments=true)
+ @test_reference "data/quadrefine-$T.png" fig
+ end
+
+ # CRS propagation
+ points = merc.([(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.25), (0.75, 0.25), (0.5, 0.75)])
+ connec = connect.([(1, 2, 6, 5), (1, 5, 7, 3), (2, 4, 7, 6), (3, 7, 4)])
+ mesh = SimpleMesh(points, connec)
+ ref = refine(mesh, QuadRefinement())
+ @test crs(ref) === crs(mesh)
+
+ # latlon
+ points = latlon.([(0, 0), (0, 1), (1, 0), (1, 1), (0.25, 0.25), (0.25, 0.75), (0.75, 0.5)])
+ connec = connect.([(1, 2, 6, 5), (1, 5, 7, 3), (2, 4, 7, 6), (3, 7, 4)])
+ mesh = SimpleMesh(points, connec)
+ ref = refine(mesh, QuadRefinement())
+ @test nelements(ref) == 15
+ @test nvertices(ref) == 22
+end
+
+@testitem "RegularRefinement" setup = [Setup] begin
+ # 2D grids
+ grid = CartesianGrid(cart(0, 0), cart(10, 10), dims=(10, 10))
+ tgrid = CartesianGrid(cart(0, 0), cart(10, 10), dims=(20, 20))
+ @test refine(grid, RegularRefinement(2)) == tgrid
+ rgrid = convert(RectilinearGrid, grid)
+ trgrid = convert(RectilinearGrid, tgrid)
+ @test refine(rgrid, RegularRefinement(2)) == trgrid
+ sgrid = convert(StructuredGrid, grid)
+ tsgrid = convert(StructuredGrid, tgrid)
+ @test refine(sgrid, RegularRefinement(2)) == tsgrid
+ tfgrid = TransformedGrid(grid, Identity())
+ @test refine(tfgrid, RegularRefinement(2)) == refine(grid, RegularRefinement(2))
+
+ # 3D grids
+ grid = cartgrid(3, 3, 3)
+ tgrid = CartesianGrid(minimum(grid), maximum(grid), dims=(6, 6, 6))
+ @test refine(grid, RegularRefinement(2)) == tgrid
+ rgrid = convert(RectilinearGrid, grid)
+ trgrid = convert(RectilinearGrid, tgrid)
+ @test refine(rgrid, RegularRefinement(2)) == trgrid
+ sgrid = convert(StructuredGrid, grid)
+ tsgrid = convert(StructuredGrid, tgrid)
+ @test refine(sgrid, RegularRefinement(2)) == tsgrid
+ tfgrid = TransformedGrid(grid, Identity())
+ @test refine(tfgrid, RegularRefinement(2)) == refine(grid, RegularRefinement(2))
+end
+
+@testitem "CatmullClark" setup = [Setup] begin
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)])
+ mesh = SimpleMesh(points, connec)
+ ref1 = refine(mesh, CatmullClarkRefinement())
+ ref2 = refine(ref1, CatmullClarkRefinement())
+ ref3 = refine(ref2, CatmullClarkRefinement())
+
+ if visualtests
+ fig = Mke.Figure(size=(900, 300))
+ viz(fig[1, 1], ref1, showsegments=true)
+ viz(fig[1, 2], ref2, showsegments=true)
+ viz(fig[1, 3], ref3, showsegments=true)
+ @test_reference "data/catmullclark-1-$T.png" fig
end
- @testset "QuadRefinement" begin
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.25), (0.75, 0.25), (0.5, 0.75)]
- connec = connect.([(1, 2, 6, 5), (1, 5, 7, 3), (2, 4, 7, 6), (3, 7, 4)])
- mesh = SimpleMesh(points, connec)
- ref1 = refine(mesh, QuadRefinement())
- ref2 = refine(ref1, QuadRefinement())
- ref3 = refine(ref2, QuadRefinement())
-
- if visualtests
- fig = Mke.Figure(size=(900, 300))
- viz(fig[1, 1], ref1, showfacets=true)
- viz(fig[1, 2], ref2, showfacets=true)
- viz(fig[1, 3], ref3, showfacets=true)
- @test_reference "data/quadrefine-$T.png" fig
- end
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.25), (0.75, 0.25), (0.5, 0.75)])
+ connec = connect.([(1, 2, 6, 5), (1, 5, 7, 3), (2, 4, 7, 6), (3, 7, 4)])
+ mesh = SimpleMesh(points, connec)
+ ref1 = refine(mesh, CatmullClarkRefinement())
+ ref2 = refine(ref1, CatmullClarkRefinement())
+ ref3 = refine(ref2, CatmullClarkRefinement())
+
+ if visualtests
+ fig = Mke.Figure(size=(900, 300))
+ viz(fig[1, 1], ref1, showsegments=true)
+ viz(fig[1, 2], ref2, showsegments=true)
+ viz(fig[1, 3], ref3, showsegments=true)
+ @test_reference "data/catmullclark-2-$T.png" fig
end
- @testset "CatmullClark" begin
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)])
- mesh = SimpleMesh(points, connec)
- ref1 = refine(mesh, CatmullClark())
- ref2 = refine(ref1, CatmullClark())
- ref3 = refine(ref2, CatmullClark())
-
- if visualtests
- fig = Mke.Figure(size=(900, 300))
- viz(fig[1, 1], ref1, showfacets=true)
- viz(fig[1, 2], ref2, showfacets=true)
- viz(fig[1, 3], ref3, showfacets=true)
- @test_reference "data/catmullclark-1-$T.png" fig
- end
-
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.25), (0.75, 0.25), (0.5, 0.75)]
- connec = connect.([(1, 2, 6, 5), (1, 5, 7, 3), (2, 4, 7, 6), (3, 7, 4)])
- mesh = SimpleMesh(points, connec)
- ref1 = refine(mesh, CatmullClark())
- ref2 = refine(ref1, CatmullClark())
- ref3 = refine(ref2, CatmullClark())
-
- if visualtests
- fig = Mke.Figure(size=(900, 300))
- viz(fig[1, 1], ref1, showfacets=true)
- viz(fig[1, 2], ref2, showfacets=true)
- viz(fig[1, 3], ref3, showfacets=true)
- @test_reference "data/catmullclark-2-$T.png" fig
- end
-
- points = P3[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0), (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)]
- connec = connect.([(1, 4, 3, 2), (5, 6, 7, 8), (1, 2, 6, 5), (3, 4, 8, 7), (1, 5, 8, 4), (2, 3, 7, 6)])
- mesh = SimpleMesh(points, connec)
- ref1 = refine(mesh, CatmullClark())
- ref2 = refine(ref1, CatmullClark())
- ref3 = refine(ref2, CatmullClark())
-
- if visualtests
- fig = Mke.Figure(size=(900, 300))
- viz(fig[1, 1], ref1, showfacets=true)
- viz(fig[1, 2], ref2, showfacets=true)
- viz(fig[1, 3], ref3, showfacets=true)
- @test_reference "data/catmullclark-3-$T.png" fig
- end
+ points = cart.([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0), (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)])
+ connec = connect.([(1, 4, 3, 2), (5, 6, 7, 8), (1, 2, 6, 5), (3, 4, 8, 7), (1, 5, 8, 4), (2, 3, 7, 6)])
+ mesh = SimpleMesh(points, connec)
+ ref1 = refine(mesh, CatmullClarkRefinement())
+ ref2 = refine(ref1, CatmullClarkRefinement())
+ ref3 = refine(ref2, CatmullClarkRefinement())
+
+ if visualtests
+ fig = Mke.Figure(size=(900, 300))
+ viz(fig[1, 1], ref1, showsegments=true)
+ viz(fig[1, 2], ref2, showsegments=true)
+ viz(fig[1, 3], ref3, showsegments=true)
+ @test_reference "data/catmullclark-3-$T.png" fig
end
- @testset "TriSubdivision" begin
- points = P3[(-1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)]
- connec = connect.([(1, 2, 3), (3, 2, 4), (4, 2, 1), (1, 3, 4)])
- mesh = SimpleMesh(points, connec)
- ref1 = refine(mesh, TriSubdivision())
- ref2 = refine(ref1, TriSubdivision())
- ref3 = refine(ref2, TriSubdivision())
-
- if visualtests
- fig = Mke.Figure(size=(900, 300))
- viz(fig[1, 1], ref1, showfacets=true)
- viz(fig[1, 2], ref2, showfacets=true)
- viz(fig[1, 3], ref3, showfacets=true)
- @test_reference "data/trisubdivision-$T.png" fig
- end
+ # CRS propagation
+ points = merc.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)])
+ mesh = SimpleMesh(points, connec)
+ ref = refine(mesh, CatmullClarkRefinement())
+ @test crs(ref) === crs(mesh)
+end
+
+@testitem "TriSubdivision" setup = [Setup] begin
+ points = cart.([(-1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)])
+ connec = connect.([(1, 2, 3), (3, 2, 4), (4, 2, 1), (1, 3, 4)])
+ mesh = SimpleMesh(points, connec)
+ ref1 = refine(mesh, TriSubdivision())
+ ref2 = refine(ref1, TriSubdivision())
+ ref3 = refine(ref2, TriSubdivision())
+
+ if visualtests
+ fig = Mke.Figure(size=(900, 300))
+ viz(fig[1, 1], ref1, showsegments=true)
+ viz(fig[1, 2], ref2, showsegments=true)
+ viz(fig[1, 3], ref3, showsegments=true)
+ @test_reference "data/trisubdivision-$T.png" fig
end
end
diff --git a/test/runtests.jl b/test/runtests.jl
index 1fc854854..aec6b958d 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,141 +1,45 @@
-using Meshes
-using Tables
-using Distances
-using Statistics
-using LinearAlgebra
-using CategoricalArrays
-using CircularArrays
-using StaticArrays
-using SparseArrays
-using PlyIO
-using Unitful
-using Rotations
-using Test, Random
-using ReferenceTests, ImageIO
-
-using TransformsBase: Identity, →
-
-import TransformsBase as TB
-import CairoMakie as Mke
-
-# environment settings
-isCI = "CI" ∈ keys(ENV)
-islinux = Sys.islinux()
-visualtests = !isCI || (isCI && islinux)
-datadir = joinpath(@__DIR__, "data")
-
-# helper function to read *.line files containing polygons
-# generated with RPG (https://github.com/cgalab/genpoly-rpg)
-function readpoly(T, fname)
- open(fname, "r") do f
- # read outer chain
- n = parse(Int, readline(f))
- outer = map(1:n) do _
- coords = readline(f)
- x, y = parse.(T, split(coords))
- Point(x, y)
+using TestItems
+using TestItemRunner
+
+@run_package_tests
+
+@testsnippet Setup begin
+ using Tables
+ using Distances
+ using Statistics
+ using LinearAlgebra
+ using CoordRefSystems
+ using CategoricalArrays
+ using CircularArrays
+ using StaticArrays
+ using SparseArrays
+ using PlyIO
+ using Unitful
+ using Rotations
+ using StableRNGs
+ using ReferenceTests, ImageIO
+
+ using TransformsBase: Identity, →
+
+ import TransformsBase as TB
+ import CairoMakie as Mke
+
+ # environment settings
+ isCI = "CI" ∈ keys(ENV)
+ islinux = Sys.islinux()
+ visualtests = !isCI || (isCI && islinux)
+ datadir = joinpath(@__DIR__, "data")
+
+ # float settings
+ T = if isCI
+ if ENV["FLOAT_TYPE"] == "Float32"
+ Float32
+ elseif ENV["FLOAT_TYPE"] == "Float64"
+ Float64
end
-
- # read inner chains
- inners = []
- while !eof(f)
- n = parse(Int, readline(f))
- inner = map(1:n) do _
- coords = readline(f)
- x, y = parse.(T, split(coords))
- Point(x, y)
- end
- push!(inners, inner)
- end
-
- # return polygonal area
- @assert first(outer) == last(outer)
- @assert all(first(i) == last(i) for i in inners)
- rings = [outer, inners...]
- PolyArea([r[begin:(end - 1)] for r in rings])
- end
-end
-
-# helper function to read *.ply files containing meshes
-function readply(T, fname)
- ply = load_ply(fname)
- x = ply["vertex"]["x"]
- y = ply["vertex"]["y"]
- z = ply["vertex"]["z"]
- points = Point{3,T}.(x, y, z)
- connec = [connect(Tuple(c .+ 1)) for c in ply["face"]["vertex_indices"]]
- SimpleMesh(points, connec)
-end
-
-# dummy definitions
-include("dummy.jl")
-
-# list of tests
-testfiles = [
- "vectors.jl",
- "primitives.jl",
- "polytopes.jl",
- "multigeoms.jl",
- "connectivities.jl",
- "topologies.jl",
- "toporelations.jl",
- "domains.jl",
- "subdomains.jl",
- "sets.jl",
- "mesh.jl",
- "trajecs.jl",
- "utils.jl",
- "viewing.jl",
- "partitioning.jl",
- "sorting.jl",
- "traversing.jl",
- "neighborhoods.jl",
- "neighborsearch.jl",
- "predicates.jl",
- "winding.jl",
- "sideof.jl",
- "orientation.jl",
- "merging.jl",
- "clipping.jl",
- "clamping.jl",
- "intersections.jl",
- "complement.jl",
- "simplification.jl",
- "boundingboxes.jl",
- "hulls.jl",
- "sampling.jl",
- "pointification.jl",
- "discretization.jl",
- "refinement.jl",
- "transforms.jl",
- "distances.jl",
- "supportfun.jl",
- "matrices.jl",
- "tolerances.jl"
-]
-
-# --------------------------------
-# RUN TESTS WITH SINGLE PRECISION
-# --------------------------------
-T = Float32
-P1, P2, P3 = Point{1,T}, Point{2,T}, Point{3,T}
-V1, V2, V3 = Vec{1,T}, Vec{2,T}, Vec{3,T}
-@testset "Meshes.jl ($T)" begin
- for testfile in testfiles
- println("Testing $testfile...")
- include(testfile)
+ else
+ Float64
end
-end
-# --------------------------------
-# RUN TESTS WITH DOUBLE PRECISION
-# --------------------------------
-T = Float64
-P1, P2, P3 = Point{1,T}, Point{2,T}, Point{3,T}
-V1, V2, V3 = Vec{1,T}, Vec{2,T}, Vec{3,T}
-@testset "Meshes.jl ($T)" begin
- for testfile in testfiles
- println("Testing $testfile...")
- include(testfile)
- end
+ include("testutils.jl")
end
diff --git a/test/sampling.jl b/test/sampling.jl
index 3d90a4a73..a3451f267 100644
--- a/test/sampling.jl
+++ b/test/sampling.jl
@@ -1,112 +1,136 @@
-@testset "Sampling" begin
- @testset "UniformSampling" begin
- Random.seed!(2021)
- d = CartesianGrid{T}(100, 100)
- s = sample(d, UniformSampling(100))
- μ = mean(coordinates.([centroid(s, i) for i in 1:nelements(s)]))
- @test nelements(s) == 100
- @test isapprox(μ, T[50.0, 50.0], atol=T(10))
-
- # availability of option ordered
- s = sample(d, UniformSampling(100, ordered=true))
- μ = mean(coordinates.([centroid(s, i) for i in 1:nelements(s)]))
- @test nelements(s) == 100
- @test isapprox(μ, T[50.0, 50.0], atol=T(10))
- end
+@testitem "UniformSampling" setup = [Setup] begin
+ rng = StableRNG(123)
+ d = cartgrid(100, 100)
+ s = sample(rng, d, UniformSampling(100))
+ μ = mean(to.([centroid(s, i) for i in 1:nelements(s)]))
+ @test nelements(s) == 100
+ @test isapprox(μ, vector(50.0, 50.0), atol=T(10) * u"m")
+
+ # availability of option ordered
+ s = sample(rng, d, UniformSampling(100, ordered=true))
+ μ = mean(to.([centroid(s, i) for i in 1:nelements(s)]))
+ @test nelements(s) == 100
+ @test isapprox(μ, vector(50.0, 50.0), atol=T(10) * u"m")
+end
- @testset "WeightedSampling" begin
- # uniform weights => uniform sampler
- Random.seed!(2020)
- d = CartesianGrid{T}(100, 100)
- s = sample(d, WeightedSampling(100))
- μ = mean(coordinates.([centroid(s, i) for i in 1:nelements(s)]))
- @test nelements(s) == 100
- @test isapprox(μ, T[50.0, 50.0], atol=T(10))
-
- # availability of option ordered
- s = sample(d, WeightedSampling(100, ordered=true))
- μ = mean(coordinates.([centroid(s, i) for i in 1:nelements(s)]))
- @test nelements(s) == 100
- @test isapprox(μ, T[50.0, 50.0], atol=T(10))
-
- # utility method
- s = sample(d, 100, ordered=true)
- μ = mean(coordinates.([centroid(s, i) for i in 1:nelements(s)]))
- @test nelements(s) == 100
- @test isapprox(μ, T[50.0, 50.0], atol=T(10))
- s = sample(d, 100, fill(1, 10000), ordered=true)
- μ = mean(coordinates.([centroid(s, i) for i in 1:nelements(s)]))
- @test nelements(s) == 100
- @test isapprox(μ, T[50.0, 50.0], atol=T(10))
- end
+@testitem "WeightedSampling" setup = [Setup] begin
+ # uniform weights => uniform sampler
+ rng = StableRNG(123)
+ d = cartgrid(100, 100)
+ s = sample(rng, d, WeightedSampling(100))
+ μ = mean(to.([centroid(s, i) for i in 1:nelements(s)]))
+ @test nelements(s) == 100
+ @test isapprox(μ, vector(50.0, 50.0), atol=T(10) * u"m")
+
+ # availability of option ordered
+ s = sample(rng, d, WeightedSampling(100, ordered=true))
+ μ = mean(to.([centroid(s, i) for i in 1:nelements(s)]))
+ @test nelements(s) == 100
+ @test isapprox(μ, vector(50.0, 50.0), atol=T(10) * u"m")
+
+ # utility method
+ s = sample(rng, d, 100, ordered=true)
+ μ = mean(to.([centroid(s, i) for i in 1:nelements(s)]))
+ @test nelements(s) == 100
+ @test isapprox(μ, vector(50.0, 50.0), atol=T(10) * u"m")
+ s = sample(rng, d, 100, fill(1, 10000), ordered=true)
+ μ = mean(to.([centroid(s, i) for i in 1:nelements(s)]))
+ @test nelements(s) == 100
+ @test isapprox(μ, vector(50.0, 50.0), atol=T(10) * u"m")
+end
- @testset "BallSampling" begin
- d = CartesianGrid{T}(100, 100)
- s = sample(d, BallSampling(T(10)))
- n = nelements(s)
- x = coordinates(centroid(s, 1))
- y = coordinates(centroid(s, 17))
- @test n < 100
- @test sqrt(sum((x - y) .^ 2)) ≥ T(10)
-
- d = CartesianGrid{T}(100, 100)
- s = sample(d, BallSampling(T(20)))
- n = nelements(s)
- x = coordinates(centroid(s, 1))
- y = coordinates(centroid(s, 17))
- @test n < 50
- @test sqrt(sum((x - y) .^ 2)) ≥ T(20)
- end
+@testitem "BallSampling" setup = [Setup] begin
+ d = cartgrid(100, 100)
+ s = sample(d, BallSampling(T(10)))
+ n = nelements(s)
+ x = to(centroid(s, 1))
+ y = to(centroid(s, 17))
+ @test n < 100
+ @test sqrt(sum((x - y) .^ 2)) ≥ T(10) * u"m"
+
+ d = cartgrid(100, 100)
+ s = sample(d, BallSampling(T(20)))
+ n = nelements(s)
+ x = to(centroid(s, 1))
+ y = to(centroid(s, 17))
+ @test n < 50
+ @test sqrt(sum((x - y) .^ 2)) ≥ T(20) * u"m"
+end
- @testset "BlockSampling" begin
- g = CartesianGrid{T}(100, 100)
- s = sample(g, BlockSampling(T(10)))
- @test nelements(s) == 100
- x = coordinates.(centroid.(s))
- D = pairwise(Euclidean(), x)
- d = [D[i, j] for i in 1:length(x) for j in 1:(i - 1)]
- @test all(≥(T(10)), d)
+@testitem "BlockSampling" setup = [Setup] begin
+ g = cartgrid(100, 100)
+ s = sample(g, BlockSampling(T(10)))
+ @test nelements(s) == 100
+ x = to.(centroid.(s))
+ D = pairwise(Euclidean(), x)
+ d = [D[i, j] for i in 1:length(x) for j in 1:(i - 1)]
+ @test all(≥(T(10) * u"m"), d)
+end
+
+@testitem "RegularSampling" setup = [Setup] begin
+ b = Box(cart(0, 0), cart(2, 2))
+ ps = sample(b, RegularSampling(3))
+ @test collect(ps) == cart.([(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)])
+ ps = sample(b, RegularSampling(2, 3))
+ @test collect(ps) == cart.([(0, 0), (2, 0), (0, 1), (2, 1), (0, 2), (2, 2)])
+
+ b = BezierCurve([cart(0, 0), cart(1, 0), cart(1, 1)])
+ ps = sample(b, RegularSampling(4))
+ ts =
+ cart.([(0.0, 0.0), (0.5555555555555556, 0.1111111111111111), (0.8888888888888888, 0.4444444444444444), (1.0, 1.0)])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
end
- @testset "RegularSampling" begin
- b = Box(P2(0, 0), P2(2, 2))
- ps = sample(b, RegularSampling(3))
- @test collect(ps) == P2[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
- ps = sample(b, RegularSampling(2, 3))
- @test collect(ps) == P2[(0, 0), (2, 0), (0, 1), (2, 1), (0, 2), (2, 2)]
-
- b = BezierCurve([P2(0, 0), P2(1, 0), P2(1, 1)])
- ps = sample(b, RegularSampling(4))
- ts = P2[(0.0, 0.0), (0.5555555555555556, 0.1111111111111111), (0.8888888888888888, 0.4444444444444444), (1.0, 1.0)]
- for (p, t) in zip(ps, ts)
- @test p ≈ t
- end
+ c = ParametrizedCurve(t -> cart(cos(t), sin(t)), (T(0), T(2π)))
+ ps = sample(c, RegularSampling(4))
+ ts = cart.([(1.0, 0.0), (-0.5, 0.8660254037844387), (-0.5, -0.8660254037844385), (1.0, 0.0)])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
+ end
- s = Sphere(P2(0, 0), T(2))
- ps = sample(s, RegularSampling(4))
- ts = P2[(2, 0), (0, 2), (-2, 0), (0, -2)]
- for (p, t) in zip(ps, ts)
- @test p ≈ t
- end
+ s = Sphere(cart(0, 0), T(2))
+ ps = sample(s, RegularSampling(4))
+ ts = cart.([(2, 0), (0, 2), (-2, 0), (0, -2)])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
+ end
- s = Sphere(P3(0, 0, 0), T(2))
- ps = sample(s, RegularSampling(2, 2))
- ts = P3[
+ s = Sphere(cart(0, 0, 0), T(2))
+ ps = sample(s, RegularSampling(2, 2))
+ ts =
+ cart.([
(1.7320508075688772, 0.0, 1.0),
(1.7320508075688772, 0.0, -1.0),
(-1.7320508075688772, 0.0, 1.0),
(-1.7320508075688772, 0.0, -1.0),
(0.0, 0.0, 2.0),
(0.0, 0.0, -2.0)
- ]
- for (p, t) in zip(ps, ts)
- @test p ≈ t
- end
+ ])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
+ end
+
+ e = Ellipsoid((T(3), T(2), T(1)), cart(1, 1, 1), RotZYX(T(π / 4), T(π / 4), T(π / 4)))
+ ps = sample(e, RegularSampling(2, 2))
+ ts =
+ cart.([
+ (2.725814800973295, 2.225814800973295, -0.5871173070873834),
+ (1.872261410380021, 2.372261410380021, -1.0871173070873832),
+ (0.12773858961997864, -0.37226141038002103, 3.0871173070873836),
+ (-0.725814800973295, -0.22581480097329454, 2.587117307087383),
+ (1.8535533905932737, 0.8535533905932737, 1.5),
+ (0.14644660940672627, 1.1464466094067263, 0.4999999999999999)
+ ])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
+ end
- b = Ball(P2(0, 0), T(2))
- ps = sample(b, RegularSampling(3, 4))
- @test all(∈(b), ps)
- ts = P2[
+ b = Ball(cart(0, 0), T(2))
+ ps = sample(b, RegularSampling(3, 4))
+ @test all(∈(b), ps)
+ ts =
+ cart.([
(0.6666666666666666, 0.0),
(1.3333333333333333, 0.0),
(2.0, 0.0),
@@ -120,15 +144,16 @@
(0.0, -1.3333333333333333),
(0.0, -2.0),
(0.0, 0.0)
- ]
- for (p, t) in zip(ps, ts)
- @test p ≈ t
- end
+ ])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
+ end
- b = Ball(P2(10, 10), T(2))
- ps = sample(b, RegularSampling(4, 3))
- @test all(∈(b), ps)
- ts = P2[
+ b = Ball(cart(10, 10), T(2))
+ ps = sample(b, RegularSampling(4, 3))
+ @test all(∈(b), ps)
+ ts =
+ cart.([
(10.5, 10.0),
(11.0, 10.0),
(11.5, 10.0),
@@ -142,15 +167,16 @@
(9.25, 8.700961894323342),
(9.0, 8.267949192431121),
(10.0, 10.0)
- ]
- for (p, t) in zip(ps, ts)
- @test p ≈ t
- end
+ ])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
+ end
- b = Ball(P3(0, 0, 0), T(2))
- ps = sample(b, RegularSampling(3, 2, 3))
- @test all(∈(b), ps)
- ts = P3[
+ b = Ball(cart(0, 0, 0), T(2))
+ ps = sample(b, RegularSampling(3, 2, 3))
+ @test all(∈(b), ps)
+ ts =
+ cart.([
(0.5773502691896257, 0.0, 0.3333333333333333),
(1.1547005383792515, 0.0, 0.6666666666666666),
(1.7320508075688772, 0.0, 1.0),
@@ -170,224 +196,299 @@
(-0.5773502691896252, -1.0, -0.666666666666667),
(-0.8660254037844378, -1.5000000000000002, -1.0000000000000004),
(0.0, 0.0, 0.0)
- ]
- for (p, t) in zip(ps, ts)
- @test p ≈ t
- end
+ ])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
+ end
- b = Ball(P3(10, 10, 10), T(2))
- ps = sample(b, RegularSampling(3, 2, 3))
- @test all(∈(b), ps)
-
- # cylinder surface with parallel planes
- c = CylinderSurface(Plane(P3(0, 0, 0), V3(0, 0, 1)), Plane(P3(0, 0, 1), V3(0, 0, 1)), T(1))
- ps = sample(c, RegularSampling(20, 10))
- cs = coordinates.(ps)
- xs = getindex.(cs, 1)
- ys = getindex.(cs, 2)
- zs = getindex.(cs, 3)
- @test length(cs) == 200 + 2
- @test all(T(-1) ≤ x ≤ T(1) for x in xs)
- @test all(T(-1) ≤ y ≤ T(1) for y in ys)
- @test all(T(0) ≤ z ≤ T(1) for z in zs)
-
- # cylinder surface with parallel shifted planes
- c = CylinderSurface(Plane(P3(0, 0, 0), V3(0, 0, 1)), Plane(P3(1, 1, 1), V3(0, 0, 1)), T(1))
- ps = sample(c, RegularSampling(20, 10))
- cs = coordinates.(ps)
- xs = getindex.(cs, 1)
- ys = getindex.(cs, 2)
- zs = getindex.(cs, 3)
- @test length(cs) == 200 + 2
-
- # cylinder surface with non-parallel planes
- c = CylinderSurface(Plane(P3(0, 0, 0), V3(1, 0, 1)), Plane(P3(1, 1, 1), V3(0, 1, 1)), T(1))
- ps = sample(c, RegularSampling(20, 10))
- cs = coordinates.(ps)
- @test length(cs) == 200 + 2
-
- s = Segment(P2(0, 0), P2(1, 1))
- ps = sample(s, RegularSampling(2))
- @test collect(ps) == P2[(0, 0), (1, 1)]
- ps = sample(s, RegularSampling(3))
- @test collect(ps) == P2[(0, 0), (0.5, 0.5), (1, 1)]
-
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- ps = sample(q, RegularSampling(2, 2))
- @test collect(ps) == P2[(0, 0), (1, 0), (0, 1), (1, 1)]
- ps = sample(q, RegularSampling(3, 3))
- @test collect(ps) == P2[(0, 0), (0.5, 0), (1, 0), (0, 0.5), (0.5, 0.5), (1, 0.5), (0, 1), (0.5, 1), (1, 1)]
-
- h =
- Hexahedron(P3(0, 0, 0), P3(1, 0, 0), P3(1, 1, 0), P3(0, 1, 0), P3(0, 0, 1), P3(1, 0, 1), P3(1, 1, 1), P3(0, 1, 1))
- ps = sample(h, RegularSampling(2, 2, 2))
- @test collect(ps) == P3[(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0), (0, 0, 1), (1, 0, 1), (0, 1, 1), (1, 1, 1)]
- ps = sample(h, RegularSampling(3, 2, 2))
- @test collect(ps) == P3[
- (0, 0, 0),
- (0.5, 0, 0),
- (1, 0, 0),
- (0, 1, 0),
- (0.5, 1, 0),
- (1, 1, 0),
- (0, 0, 1),
- (0.5, 0, 1),
- (1, 0, 1),
- (0, 1, 1),
- (0.5, 1, 1),
- (1, 1, 1)
- ]
-
- torus = Torus(P3(0, 0, 0), V3(1, 0, 0), T(2), T(1))
- ps = sample(torus, RegularSampling(3, 3))
- ts = P3[
+ b = Ball(cart(10, 10, 10), T(2))
+ ps = sample(b, RegularSampling(3, 2, 3))
+ @test all(∈(b), ps)
+
+ # cylinder with parallel planes
+ c = Cylinder(Plane(cart(0, 0, 0), vector(0, 0, 1)), Plane(cart(0, 0, 1), vector(0, 0, 1)), T(1))
+ ps = sample(c, RegularSampling(2, 20, 10))
+ cs = to.(ps)
+ xs = getindex.(cs, 1)
+ ys = getindex.(cs, 2)
+ zs = getindex.(cs, 3)
+ @test length(cs) == 200 + 200 + 10
+ @test all(-oneunit(ℳ) ≤ x ≤ oneunit(ℳ) for x in xs)
+ @test all(-oneunit(ℳ) ≤ y ≤ oneunit(ℳ) for y in ys)
+ @test all(zero(ℳ) ≤ z ≤ oneunit(ℳ) for z in zs)
+
+ # cylinder surface with parallel planes
+ c = CylinderSurface(Plane(cart(0, 0, 0), vector(0, 0, 1)), Plane(cart(0, 0, 1), vector(0, 0, 1)), T(1))
+ ps = sample(c, RegularSampling(20, 10))
+ cs = to.(ps)
+ xs = getindex.(cs, 1)
+ ys = getindex.(cs, 2)
+ zs = getindex.(cs, 3)
+ @test length(cs) == 200 + 2
+ @test all(-oneunit(ℳ) ≤ x ≤ oneunit(ℳ) for x in xs)
+ @test all(-oneunit(ℳ) ≤ y ≤ oneunit(ℳ) for y in ys)
+ @test all(zero(ℳ) ≤ z ≤ oneunit(ℳ) for z in zs)
+
+ # cylinder surface with parallel shifted planes
+ c = CylinderSurface(Plane(cart(0, 0, 0), vector(0, 0, 1)), Plane(cart(1, 1, 1), vector(0, 0, 1)), T(1))
+ ps = sample(c, RegularSampling(20, 10))
+ cs = to.(ps)
+ xs = getindex.(cs, 1)
+ ys = getindex.(cs, 2)
+ zs = getindex.(cs, 3)
+ @test length(cs) == 200 + 2
+
+ # cylinder surface with non-parallel planes
+ c = CylinderSurface(Plane(cart(0, 0, 0), vector(1, 0, 1)), Plane(cart(1, 1, 1), vector(0, 1, 1)), T(1))
+ ps = sample(c, RegularSampling(20, 10))
+ cs = to.(ps)
+ @test length(cs) == 200 + 2
+
+ s = Segment(cart(0, 0), cart(1, 1))
+ ps = sample(s, RegularSampling(2))
+ @test collect(ps) == cart.([(0, 0), (1, 1)])
+ ps = sample(s, RegularSampling(3))
+ @test collect(ps) == cart.([(0, 0), (0.5, 0.5), (1, 1)])
+
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ ps = sample(q, RegularSampling(2, 2))
+ @test collect(ps) == cart.([(0, 0), (1, 0), (0, 1), (1, 1)])
+ ps = sample(q, RegularSampling(3, 3))
+ @test collect(ps) == cart.([(0, 0), (0.5, 0), (1, 0), (0, 0.5), (0.5, 0.5), (1, 0.5), (0, 1), (0.5, 1), (1, 1)])
+
+ h = Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+ ps = sample(h, RegularSampling(2, 2, 2))
+ @test collect(ps) == cart.([(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0), (0, 0, 1), (1, 0, 1), (0, 1, 1), (1, 1, 1)])
+ ps = sample(h, RegularSampling(3, 2, 2))
+ @test collect(ps) ==
+ cart.([
+ (0, 0, 0),
+ (0.5, 0, 0),
+ (1, 0, 0),
+ (0, 1, 0),
+ (0.5, 1, 0),
+ (1, 1, 0),
+ (0, 0, 1),
+ (0.5, 0, 1),
+ (1, 0, 1),
+ (0, 1, 1),
+ (0.5, 1, 1),
+ (1, 1, 1)
+ ])
+
+ torus = Torus(cart(0, 0, 0), vector(1, 0, 0), T(2), T(1))
+ ps = sample(torus, RegularSampling(3, 3))
+ ts =
+ cart.([
(0, 0, -3),
- (sqrt(3) / 2, 0, -1.5),
(-sqrt(3) / 2, 0, -1.5),
+ (sqrt(3) / 2, 0, -1.5),
(0, 3sqrt(3) / 2, 1.5),
- (sqrt(3) / 2, 3sqrt(3) / 4, 0.75),
(-sqrt(3) / 2, 3sqrt(3) / 4, 0.75),
+ (sqrt(3) / 2, 3sqrt(3) / 4, 0.75),
(0, -3sqrt(3) / 2, 1.5),
- (sqrt(3) / 2, -3sqrt(3) / 4, 0.75),
- (-sqrt(3) / 2, -3sqrt(3) / 4, 0.75)
- ]
- for (p, t) in zip(ps, ts)
- @test p ≈ t
- end
-
- grid = CartesianGrid{T}(10, 10)
- points = sample(grid, RegularSampling(100, 200))
- @test length(collect(points)) == 20000
+ (-sqrt(3) / 2, -3sqrt(3) / 4, 0.75),
+ (sqrt(3) / 2, -3sqrt(3) / 4, 0.75)
+ ])
+ for (p, t) in zip(ps, ts)
+ @test p ≈ t
end
- @testset "HomogeneousSampling" begin
- s = Segment(P2(0, 0), P2(1, 0))
- ps = sample(s, HomogeneousSampling(100))
- @test first(ps) isa P2
- @test all(0 ≤ coords[1] ≤ 1 for coords in coordinates.(ps))
- @test all(coords[2] == 0 for coords in coordinates.(ps))
-
- s = Segment(P2(0, 0), P2(0, 1))
- ps = sample(s, HomogeneousSampling(100))
- @test first(ps) isa P2
- @test all(coords[1] == 0 for coords in coordinates.(ps))
- @test all(0 ≤ coords[2] ≤ 1 for coords in coordinates.(ps))
-
- s = Segment(P2(0, 0), P2(1, 1))
- ps = sample(s, HomogeneousSampling(100))
- @test first(ps) isa P2
- @test all(0 ≤ coords[1] == coords[2] ≤ 1 for coords in coordinates.(ps))
-
- c = Rope(P2(0, 0), P2(1, 0), P2(0, 1), P2(1, 1))
- ps = sample(c, HomogeneousSampling(100))
- @test first(ps) isa P2
- @test all(coords[1] + coords[2] == 1 || (0 ≤ coords[1] ≤ 1 && coords[2] ∈ [0, 1]) for coords in coordinates.(ps))
-
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- ps = sample(t, HomogeneousSampling(100))
- @test first(ps) isa P2
- @test all(∈(t), ps)
-
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- ps = sample(q, HomogeneousSampling(100))
- @test first(ps) isa P2
- @test all(∈(q), ps)
-
- b = Ball(P2(10, 10), T(3))
- ps = sample(b, HomogeneousSampling(100))
- @test first(ps) isa P2
- @test all(∈(b), ps)
-
- b = Ball(P3(10, 10, 10), T(10))
- ps = sample(b, HomogeneousSampling(100))
- @test first(ps) isa P3
- @test all(∈(b), ps)
-
- poly1 = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- poly2 = PolyArea(P2[(1, 1), (2, 1), (2, 2), (1, 2)])
- multi = Multi([poly1, poly2])
- ps = sample(multi, HomogeneousSampling(100))
- @test all(p -> (P2(0, 0) ⪯ p ⪯ P2(1, 1)) || (P2(1, 1) ⪯ p ⪯ P2(2, 2)), ps)
-
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.5), (0.75, 0.5)]
- connec = connect.([(3, 1, 5), (4, 6, 2), (1, 2, 6, 5), (5, 6, 4, 3)])
- mesh = SimpleMesh(points, connec)
- ps = sample(mesh, HomogeneousSampling(400))
- @test first(ps) isa P2
- @test all(∈(mesh), ps)
- ps = sample(mesh, HomogeneousSampling(400, 1:nelements(mesh)))
- @test first(ps) isa P2
- @test all(∈(mesh), ps)
- end
+ grid = cartgrid(10, 10)
+ points = sample(grid, RegularSampling(100, 200))
+ @test length(collect(points)) == 20000
+end
+
+@testitem "HomogeneousSampling" setup = [Setup] begin
+ s = Segment(cart(0, 0), cart(1, 0))
+ ps = sample(s, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(zero(ℳ) ≤ coords[1] ≤ oneunit(ℳ) for coords in to.(ps))
+ @test all(coords[2] == zero(ℳ) for coords in to.(ps))
+
+ s = Segment(cart(0, 0), cart(0, 1))
+ ps = sample(s, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(coords[1] == zero(ℳ) for coords in to.(ps))
+ @test all(zero(ℳ) ≤ coords[2] ≤ oneunit(ℳ) for coords in to.(ps))
+
+ s = Segment(cart(0, 0), cart(1, 1))
+ ps = sample(s, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(zero(ℳ) ≤ coords[1] == coords[2] ≤ oneunit(ℳ) for coords in to.(ps))
+
+ c = Rope(cart(0, 0), cart(1, 0), cart(0, 1), cart(1, 1))
+ ps = sample(c, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(
+ coords[1] + coords[2] == oneunit(ℳ) || (zero(ℳ) ≤ coords[1] ≤ oneunit(ℳ) && coords[2] ∈ [zero(ℳ), oneunit(ℳ)]) for
+ coords in to.(ps)
+ )
+
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ ps = sample(t, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(∈(t), ps)
+
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ ps = sample(q, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(∈(q), ps)
+
+ b = Ball(cart(10, 10), T(3))
+ ps = sample(b, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(∈(b), ps)
+
+ b = Ball(cart(10, 10, 10), T(10))
+ ps = sample(b, HomogeneousSampling(100))
+ @test first(ps) isa Point
+ @test all(∈(b), ps)
+
+ poly1 = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ poly2 = PolyArea(cart.([(1, 1), (2, 1), (2, 2), (1, 2)]))
+ multi = Multi([poly1, poly2])
+ ps = sample(multi, HomogeneousSampling(100))
+ @test all(p -> (cart(0, 0) ⪯ p ⪯ cart(1, 1)) || (cart(1, 1) ⪯ p ⪯ cart(2, 2)), ps)
+
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.5), (0.75, 0.5)])
+ connec = connect.([(3, 1, 5), (4, 6, 2), (1, 2, 6, 5), (5, 6, 4, 3)])
+ mesh = SimpleMesh(points, connec)
+ ps = sample(mesh, HomogeneousSampling(400))
+ @test first(ps) isa Point
+ @test all(∈(mesh), ps)
+ ps = sample(mesh, HomogeneousSampling(400, 1:nelements(mesh)))
+ @test first(ps) isa Point
+ @test all(∈(mesh), ps)
+end
+
+@testitem "MinDistanceSampling" setup = [Setup] begin
+ poly1 = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ poly2 = PolyArea(cart.([(1, 1), (2, 1), (2, 2), (1, 2)]))
+ multi = Multi([poly1, poly2])
+ ps = sample(multi, MinDistanceSampling(0.1))
+ @test all(p -> (cart(0, 0) ⪯ p ⪯ cart(1, 1)) || (cart(1, 1) ⪯ p ⪯ cart(2, 2)), ps)
+
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.5), (0.75, 0.5)])
+ connec = connect.([(3, 1, 5), (4, 6, 2), (1, 2, 6, 5), (5, 6, 4, 3)])
+ mesh = SimpleMesh(points, connec)
+ ps = sample(mesh, MinDistanceSampling(0.2))
+ n = length(ps)
+ @test first(ps) isa Point
+ @test all(∈(mesh), ps)
+ @test all(norm(ps[i] - ps[j]) ≥ T(0.2) * u"m" for i in 1:n for j in (i + 1):n)
+
+ # geometries with almost zero measure
+ # can still be sampled (at least one point)
+ poly = PolyArea(cart.([(-44.20065308, -21.12284851), (-44.20324135, -21.122799875), (-44.20582962, -21.12275124)]))
+ ps = sample(poly, MinDistanceSampling(3.2423333333753135e-5))
+ @test length(ps) > 0
+
+ ball = Ball(cart(10, 10), T(3))
+ ps = sample(ball, MinDistanceSampling(1.0))
+ n = length(ps)
+ @test all(∈(ball), ps)
+ @test all(norm(ps[i] - ps[j]) ≥ T(1.0) * u"m" for i in 1:n for j in (i + 1):n)
+end
+
+@testitem "FibonacciSampling" setup = [Setup] begin
+ @test_throws ArgumentError sample(Box(cart(0, 0), cart(1, 1)), FibonacciSampling(-1))
+ @test_throws ArgumentError sample(Box(Point(0, 0, 0), Point(1, 1, 1)), FibonacciSampling(100))
+
+ box = Box(cart(1, 1), cart(4, 2))
+ ps = sample(box, FibonacciSampling(100)) |> collect
+ @test first(ps) isa Point
+ @test first(ps) ≈ cart(1, 1)
+ @test all(∈(box), ps)
+
+ box = Box(cart(0, 0), cart(1, 1))
+ ps = sample(box, FibonacciSampling(100, π)) |> collect
+ @test first(ps) isa Point
+ @test all(∈(box), ps)
+ @test ps[2] ≈ cart(mod(1 / π, 1), 1 / 99)
+
+ tbox = Box(cart(0, 0), cart(1, 1))
+ af = Affine(T[1 1; 0 1], T[2, 0])
+ tbox = af(tbox)
+ ps = sample(tbox, FibonacciSampling(100)) |> collect
+ @test first(ps) isa Point
+ @test first(ps) ≈ af(cart(0, 0))
+ @test all(∈(tbox), ps)
+
+ disk = Disk(Plane(cart(3, 0, 0), Vec(1, 0, 0)), T(2))
+ ps = sample(disk, FibonacciSampling(100)) |> collect
+ @test first(ps) isa Point
+ @test first(ps) ≈ centroid(disk)
+ @test all(p -> coords(p).x ≈ 3u"m", ps)
+ @test all(p -> -2u"m" < coords(p).y || coords(p).y < 2u"m" || isapprox(coords(p).y, 2u"m"; atol=1e-5u"m"), ps)
+ @test all(p -> -2u"m" < coords(p).z || coords(p).z < 2u"m" || isapprox(coords(p).z, 2u"m"; atol=1e-5u"m"), ps)
+
+ sphere = Sphere(cart(1, 1, 1), T(2))
+ ps = sample(sphere, FibonacciSampling(100)) |> collect
+ @test first(ps) isa Point
+ @test first(ps) ≈ cart(1, 1, 3)
+ @test all(∈(sphere), ps)
+
+ ball = Ball(cart(2, 1), T(0.1))
+ ps = sample(ball, FibonacciSampling(100)) |> collect
+ @test first(ps) isa Point
+ @test first(ps) ≈ centroid(ball)
+ @test all(∈(ball), ps)
+end
- @testset "MinDistanceSampling" begin
- poly1 = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- poly2 = PolyArea(P2[(1, 1), (2, 1), (2, 2), (1, 2)])
- multi = Multi([poly1, poly2])
- ps = sample(multi, MinDistanceSampling(0.1))
- @test all(p -> (P2(0, 0) ⪯ p ⪯ P2(1, 1)) || (P2(1, 1) ⪯ p ⪯ P2(2, 2)), ps)
-
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.25, 0.5), (0.75, 0.5)]
- connec = connect.([(3, 1, 5), (4, 6, 2), (1, 2, 6, 5), (5, 6, 4, 3)])
- mesh = SimpleMesh(points, connec)
- ps = sample(mesh, MinDistanceSampling(0.2))
- n = length(ps)
- @test first(ps) isa P2
- @test all(∈(mesh), ps)
- @test all(norm(ps[i] - ps[j]) ≥ 0.2 for i in 1:n for j in (i + 1):n)
-
- # geometries with almost zero measure
- # can still be sampled (at least one point)
- poly = PolyArea(P2[(-44.20065308, -21.12284851), (-44.20324135, -21.122799875), (-44.20582962, -21.12275124)])
- ps = sample(poly, MinDistanceSampling(3.2423333333753135e-5))
- @test length(ps) > 0
+@testitem "RNGs" setup = [Setup] begin
+ dom = cartgrid(100, 100)
+ for method in [UniformSampling(100), WeightedSampling(100), BallSampling(T(10))]
+ rng = StableRNG(2021)
+ s1 = sample(rng, dom, method)
+ rng = StableRNG(2021)
+ s2 = sample(rng, dom, method)
+ @test collect(s1) == collect(s2)
end
- @testset "RNGs" begin
- dom = CartesianGrid{T}(100, 100)
- for method in [UniformSampling(100), WeightedSampling(100), BallSampling(T(10))]
- rng = MersenneTwister(2021)
+ # cannot test some sampling methods with T = Float32
+ # because of https://github.com/JuliaStats/StatsBase.jl/issues/695
+ if T == Float64
+ for method in [HomogeneousSampling(100), MinDistanceSampling(T(5))]
+ rng = StableRNG(2021)
s1 = sample(rng, dom, method)
- rng = MersenneTwister(2021)
+ rng = StableRNG(2021)
s2 = sample(rng, dom, method)
@test collect(s1) == collect(s2)
end
+ end
- # cannot test some sampling methods with T = Float32
- # because of https://github.com/JuliaStats/StatsBase.jl/issues/695
- if T == Float64
- for method in [HomogeneousSampling(100), MinDistanceSampling(T(5))]
- rng = MersenneTwister(2021)
- s1 = sample(rng, dom, method)
- rng = MersenneTwister(2021)
- s2 = sample(rng, dom, method)
- @test collect(s1) == collect(s2)
- end
- end
-
- method = RegularSampling(10)
- for geom in [
- Box(P2(0, 0), P2(2, 2))
- Sphere(P2(0, 0), T(2))
- Ball(P2(0, 0), T(2))
- Segment(P2(0, 0), P2(1, 1))
- Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- Hexahedron(
- P3(0, 0, 0),
- P3(1, 0, 0),
- P3(1, 1, 0),
- P3(0, 1, 0),
- P3(0, 0, 1),
- P3(1, 0, 1),
- P3(1, 1, 1),
- P3(0, 1, 1)
- )
- ]
- rng = MersenneTwister(2021)
- s1 = sample(rng, geom, method)
- rng = MersenneTwister(2021)
- s2 = sample(rng, geom, method)
- @test collect(s1) == collect(s2)
- end
+ method = RegularSampling(10)
+ for geom in [
+ Box(cart(0, 0), cart(2, 2))
+ Sphere(cart(0, 0), T(2))
+ Ball(cart(0, 0), T(2))
+ Segment(cart(0, 0), cart(1, 1))
+ Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ Hexahedron(
+ cart(0, 0, 0),
+ cart(1, 0, 0),
+ cart(1, 1, 0),
+ cart(0, 1, 0),
+ cart(0, 0, 1),
+ cart(1, 0, 1),
+ cart(1, 1, 1),
+ cart(0, 1, 1)
+ )
+ ]
+ rng = StableRNG(2021)
+ s1 = sample(rng, geom, method)
+ rng = StableRNG(2021)
+ s2 = sample(rng, geom, method)
+ @test collect(s1) == collect(s2)
end
end
diff --git a/test/sets.jl b/test/sets.jl
index 0af0f7a9d..d253653ea 100644
--- a/test/sets.jl
+++ b/test/sets.jl
@@ -1,98 +1,130 @@
-@testset "Sets" begin
- @testset "GeometrySet" begin
- s = Segment(P2(0, 0), P2(1, 1))
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- p = PolyArea(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
- gset = GeometrySet([s, t, p])
- @test [centroid(gset, i) for i in 1:3] == P2[(1 / 2, 1 / 2), (1 / 3, 1 / 3), (1 / 2, 1 / 2)]
+@testitem "GeometrySet" setup = [Setup] begin
+ s = Segment(cart(0, 0), cart(1, 1))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ p = PolyArea(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ gset = GeometrySet([s, t, p])
+ @test crs(gset) <: Cartesian{NoDatum}
+ @test Meshes.lentype(gset) == ℳ
+ @test [centroid(gset, i) for i in 1:3] == cart.([(1 / 2, 1 / 2), (1 / 3, 1 / 3), (1 / 2, 1 / 2)])
- s = Segment(P2(0, 0), P2(1, 1))
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- geoms = [s, t]
- gset1 = GeometrySet(geoms)
- gset2 = GeometrySet(g for g in geoms)
- @test gset1 == gset2
- @test parent(gset1) === geoms
+ s = Segment(cart(0, 0), cart(1, 1))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ geoms = [s, t]
+ gset1 = GeometrySet(geoms)
+ gset2 = GeometrySet(g for g in geoms)
+ @test gset1 == gset2
+ @test parent(gset1) === geoms
- # make sure that eltype is inferred properly
- # https://github.com/JuliaGeometry/Meshes.jl/issues/643
- geoms = Vector{Segment}()
- push!(geoms, Segment(P2(0, 0), P2(1, 0)))
- push!(geoms, Segment(P2(1, 0), P2(1, 1)))
- push!(geoms, Segment(P2(1, 1), P2(0, 0)))
- gset = GeometrySet(geoms)
- @test eltype(gset) <: Segment{2,T}
+ # make sure that eltype is inferred properly
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/643
+ geoms = Vector{Segment}()
+ push!(geoms, Segment(cart(0, 0), cart(1, 0)))
+ push!(geoms, Segment(cart(1, 0), cart(1, 1)))
+ push!(geoms, Segment(cart(1, 1), cart(0, 0)))
+ gset = GeometrySet(geoms)
+ @test eltype(gset) <: Segment
- # conversion
- grid = CartesianGrid{T}(10, 10)
- gset = convert(GeometrySet, grid)
- @test gset isa GeometrySet
- @test nelements(gset) == 100
- @test eltype(gset) <: Quadrangle
- end
+ # different CRS
+ s = Segment(latlon(0, 0), latlon(1, 1))
+ t = Triangle(latlon(0, 0), latlon(0, 1), latlon(1, 0)) |> Proj(PlateCarree)
+ q = Quadrangle(latlon(0, 0), latlon(0, 1), latlon(1, 1), latlon(1, 0)) |> Proj(WebMercator)
+ geoms = [s, t, q]
+ gset = GeometrySet(geoms)
+ @test eltype(gset) <: Polytope
+ @test crs(gset) <: LatLon
+ gset = GeometrySet(g for g in geoms)
+ @test eltype(gset) <: Polytope
+ @test crs(gset) <: LatLon
+ geoms = [t, s, q]
+ gset = GeometrySet(geoms)
+ @test eltype(gset) <: Polytope
+ @test crs(gset) <: PlateCarree
+ gset = GeometrySet(g for g in geoms)
+ @test eltype(gset) <: Polytope
+ @test crs(gset) <: PlateCarree
+ geoms = [q, s, t]
+ gset = GeometrySet(geoms)
+ @test eltype(gset) <: Polytope
+ @test crs(gset) <: WebMercator
+ gset = GeometrySet(g for g in geoms)
+ @test eltype(gset) <: Polytope
+ @test crs(gset) <: WebMercator
+
+ # conversion
+ grid = cartgrid(10, 10)
+ gset = convert(GeometrySet, grid)
+ @test gset isa GeometrySet
+ @test nelements(gset) == 100
+ @test eltype(gset) <: Quadrangle
+end
+
+@testitem "PointSet" setup = [Setup] begin
+ pset = PointSet(randpoint1(100))
+ @test embeddim(pset) == 1
+ @test crs(pset) <: Cartesian{NoDatum}
+ @test Meshes.lentype(pset) === ℳ
+ @test nelements(pset) == 100
+ @test eltype(pset) <: Point
- @testset "PointSet" begin
- pset = PointSet(rand(P1, 100))
- @test embeddim(pset) == 1
- @test coordtype(pset) == T
- @test nelements(pset) == 100
- @test eltype(pset) <: P1
+ pset = PointSet(randpoint2(100))
+ @test embeddim(pset) == 2
+ @test crs(pset) <: Cartesian{NoDatum}
+ @test Meshes.lentype(pset) === ℳ
+ @test nelements(pset) == 100
+ @test eltype(pset) <: Point
- pset = PointSet(rand(P2, 100))
- @test embeddim(pset) == 2
- @test coordtype(pset) == T
- @test nelements(pset) == 100
- @test eltype(pset) <: P2
+ pset = PointSet(randpoint3(100))
+ @test embeddim(pset) == 3
+ @test crs(pset) <: Cartesian{NoDatum}
+ @test Meshes.lentype(pset) === ℳ
+ @test nelements(pset) == 100
+ @test eltype(pset) <: Point
- pset = PointSet(rand(P3, 100))
+ pset1 = PointSet([cart(1, 2, 3), cart(4, 5, 6)])
+ pset2 = PointSet(cart(1, 2, 3), cart(4, 5, 6))
+ pset3 = PointSet([T.((1, 2, 3)), T.((4, 5, 6))])
+ pset4 = PointSet(T.((1, 2, 3)), T.((4, 5, 6)))
+ @test pset1 == pset2 == pset3 == pset4
+ for pset in [pset1, pset2, pset3, pset4]
@test embeddim(pset) == 3
- @test coordtype(pset) == T
- @test nelements(pset) == 100
- @test eltype(pset) <: P3
+ @test Meshes.lentype(pset) === ℳ
+ @test nelements(pset) == 2
+ @test pset[1] == cart(1, 2, 3)
+ @test pset[2] == cart(4, 5, 6)
+ end
- pset1 = PointSet([P3(1, 2, 3), P3(4, 5, 6)])
- pset2 = PointSet(P3(1, 2, 3), P3(4, 5, 6))
- pset3 = PointSet([T.((1, 2, 3)), T.((4, 5, 6))])
- pset4 = PointSet(T.((1, 2, 3)), T.((4, 5, 6)))
- pset5 = PointSet(T[1 4; 2 5; 3 6])
- @test pset1 == pset2 == pset3 == pset4 == pset5
- for pset in [pset1, pset2, pset3, pset4, pset5]
- @test embeddim(pset) == 3
- @test coordtype(pset) == T
- @test nelements(pset) == 2
- @test pset[1] == P3(1, 2, 3)
- @test pset[2] == P3(4, 5, 6)
- end
+ pset = PointSet(cart.([(0, 0), (1, 0), (0, 1)]))
+ @test centroid(pset) == cart(1 / 3, 1 / 3)
- pset = PointSet(P2[(0, 0), (1, 0), (0, 1)])
- @test centroid(pset) == P2(1 / 3, 1 / 3)
+ pset = PointSet(cart.([(1, 0), (0, 1)]))
+ @test nelements(pset) == 2
+ @test centroid(pset, 1) == cart(1, 0)
+ @test centroid(pset, 2) == cart(0, 1)
- pset = PointSet(P2[(1, 0), (0, 1)])
- @test nelements(pset) == 2
- @test centroid(pset, 1) == P2(1, 0)
- @test centroid(pset, 2) == P2(0, 1)
+ pset = PointSet(cart.([(0, 0), (1, 0), (0, 1)]))
+ @test measure(pset) == zero(T) * u"m"
- pset = PointSet(P2[(0, 0), (1, 0), (0, 1)])
- @test measure(pset) == zero(T)
+ # constructor with iterator
+ points = cart.([(1, 0), (0, 1)])
+ pset1 = PointSet(points)
+ pset2 = PointSet(p for p in points)
+ @test pset1 == pset2
- # constructor with iterator
- points = P2[(1, 0), (0, 1)]
- pset1 = PointSet(points)
- pset2 = PointSet(p for p in points)
- @test pset1 == pset2
+ # CRS propagation
+ pset = PointSet(merc.([(0, 0), (1, 0), (0, 1)]))
+ @test crs(centroid(pset)) === crs(pset)
- pset = PointSet(P2[(1, 0), (0, 1)])
- @test sprint(show, pset) == "2 PointSet{2,$T}"
- if T == Float32
- @test sprint(show, MIME"text/plain"(), pset) == """
- 2 PointSet{2,Float32}
- ├─ Point(1.0f0, 0.0f0)
- └─ Point(0.0f0, 1.0f0)"""
- elseif T == Float64
- @test sprint(show, MIME"text/plain"(), pset) == """
- 2 PointSet{2,Float64}
- ├─ Point(1.0, 0.0)
- └─ Point(0.0, 1.0)"""
- end
+ pset = PointSet(cart.([(1, 0), (0, 1)]))
+ @test sprint(show, pset) == "2 PointSet"
+ if T == Float32
+ @test sprint(show, MIME"text/plain"(), pset) == """
+ 2 PointSet
+ ├─ Point(x: 1.0f0 m, y: 0.0f0 m)
+ └─ Point(x: 0.0f0 m, y: 1.0f0 m)"""
+ elseif T == Float64
+ @test sprint(show, MIME"text/plain"(), pset) == """
+ 2 PointSet
+ ├─ Point(x: 1.0 m, y: 0.0 m)
+ └─ Point(x: 0.0 m, y: 1.0 m)"""
end
end
diff --git a/test/sideof.jl b/test/sideof.jl
index ad2bc25ec..5ea40bc59 100644
--- a/test/sideof.jl
+++ b/test/sideof.jl
@@ -1,57 +1,79 @@
-@testset "sideof" begin
- p1, p2, p3 = P2(0, 0), P2(1, 1), P2(0.25, 0.5)
- l = Line(P2(0.5, 0.0), P2(0.0, 1.0))
+@testitem "sideof" setup = [Setup] begin
+ p1, p2, p3 = cart(0, 0), cart(1, 1), cart(0.25, 0.5)
+ l = Line(cart(0.5, 0.0), cart(0.0, 1.0))
@test sideof(p1, l) == LEFT
@test sideof(p2, l) == RIGHT
@test sideof(p3, l) == ON
pts = [p1, p2, p3]
@test sideof(pts, l) == [LEFT, RIGHT, ON]
- p1, p2, p3 = P2(0.5, 0.5), P2(1.5, 0.5), P2(1, 1)
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
+ p1, p2, p3 = cart(0.5, 0.5), cart(1.5, 0.5), cart(1, 1)
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
@test sideof(p1, c) == IN
@test sideof(p2, c) == OUT
- @test sideof(p3, c) == IN
+ @test sideof(p3, c) == ON
pts = [p1, p2, p3]
- @test sideof(pts, c) == [IN, OUT, IN]
+ @test sideof(pts, c) == [IN, OUT, ON]
- points = P3[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0.25, 0.25, 1)]
+ c′ = c |> LengthUnit(Unitful.mm)
+ @test sideof(p1, c′) == IN
+ @test sideof(p2, c′) == OUT
+ @test sideof(p3, c′) == ON
+ pts = [p1, p2, p3]
+ @test sideof(pts, c′) == [IN, OUT, ON]
+
+ p1, p2, p3 = merc(0.5, 0.5), merc(1.5, 0.5), merc(1, 1)
+ c = Ring([merc(0, 0), merc(1, 0), merc(1, 1), merc(0, 1)])
+ @test sideof(p1, c) == IN
+ @test sideof(p2, c) == OUT
+ @test sideof(p3, c) == ON
+ pts = [p1, p2, p3]
+ @test sideof(pts, c) == [IN, OUT, ON]
+
+ points = cart.([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0.25, 0.25, 1)])
connec = connect.([(1, 3, 2), (1, 2, 4), (1, 4, 3), (2, 3, 4)], Triangle)
mesh = SimpleMesh(points, connec)
- @test sideof(P3(0.25, 0.25, 0.1), mesh) == IN
- @test sideof(P3(0.25, 0.25, -0.1), mesh) == OUT
- pts = P3[(0.25, 0.25, 0.1), (0.25, 0.25, -0.1)]
+ @test sideof(cart(0.25, 0.25, 0.1), mesh) == IN
+ @test sideof(cart(0.25, 0.25, -0.1), mesh) == OUT
+ pts = cart.([(0.25, 0.25, 0.1), (0.25, 0.25, -0.1)])
@test sideof(pts, mesh) == [IN, OUT]
# ray goes through vertex
- @test sideof(P3(0.25, 0.25, 0.1), mesh) == IN
- @test sideof(P3(0.25, 0.25, -0.1), mesh) == OUT
+ @test sideof(cart(0.25, 0.25, 0.1), mesh) == IN
+ @test sideof(cart(0.25, 0.25, -0.1), mesh) == OUT
# ray goes through edge of triangle
- @test sideof(P3(0.1, 0.1, 0.1), mesh) == IN
- @test sideof(P3(0.1, 0.1, -0.1), mesh) == OUT
+ @test sideof(cart(0.1, 0.1, 0.1), mesh) == IN
+ @test sideof(cart(0.1, 0.1, -0.1), mesh) == OUT
# point coincides with edge of triangle
- @test sideof(P3(0.5, 0.0, 0.0), mesh) == IN
+ @test sideof(cart(0.5, 0.0, 0.0), mesh) == IN
# point coincides with corner of triangle
- @test sideof(P3(0.0, 0.0, 0.0), mesh) == IN
+ @test sideof(cart(0.0, 0.0, 0.0), mesh) == IN
- points = P3[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]
+ points = cart.([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)])
mesh = SimpleMesh(points, connec)
# ray collinear with edge
- @test sideof(P3(0.0, 0.0, 0.1), mesh) == IN
- @test sideof(P3(0.0, 0.0, -0.1), mesh) == OUT
+ @test sideof(cart(0.0, 0.0, 0.1), mesh) == IN
+ @test sideof(cart(0.0, 0.0, -0.1), mesh) == OUT
# sideof for meshes that have elements > 3-gons.
- points = P3[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0.25, 0.25, 1), (1, 1, 0)]
+ points = cart.([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0.25, 0.25, 1), (1, 1, 0)])
connec = connect.([(1, 2, 4), (1, 4, 3), (2, 3, 4), (1, 2, 5, 3)])
mesh = SimpleMesh(points, connec)
- @test sideof(P3(0.25, 0.25, 0.1), mesh) == IN
+ @test sideof(cart(0.25, 0.25, 0.1), mesh) == IN
# sideof only defined for surface meshes
- points = P3[(0, 0, 0), (1, 0, 0), (1, 1, 1), (0, 1, 0)]
+ points = cart.([(0, 0, 0), (1, 0, 0), (1, 1, 1), (0, 1, 0)])
connec = connect.([(1, 2, 3, 4)], [Tetrahedron])
mesh = SimpleMesh(points, connec)
- @test_throws AssertionError("winding number only defined for surface meshes") sideof(P3(0, 0, 0), mesh)
+ @test_throws AssertionError("winding number only defined for surface meshes") sideof(cart(0, 0, 0), mesh)
+
+ # sideof serial vs threads
+ p = first(randpoint2(1))
+ r = Ring(randpoint2(500))
+ serial = Meshes._sideofserial(p, r)
+ threads = Meshes._sideofthreads(p, r)
+ @test serial == threads
end
diff --git a/test/simplification.jl b/test/simplification.jl
index f8f64f081..afba919ca 100644
--- a/test/simplification.jl
+++ b/test/simplification.jl
@@ -1,60 +1,45 @@
-@testset "Simplification" begin
- @testset "DouglasPeucker" begin
- c = Ring(P2[(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)])
- s1 = simplify(c, DouglasPeucker(T(0.1)))
- s2 = simplify(c, DouglasPeucker(T(0.5)))
- @test s1 == Ring(P2[(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)])
- @test s2 == Ring(P2[(0, 0), (1.5, 0.5), (0, 1)])
-
- p = PolyArea(Ring(P2[(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)]))
- s1 = simplify(p, DouglasPeucker(T(0.5)))
- @test s1 == PolyArea(Ring(P2[(0, 0), (1.5, 0.5), (0, 1)]))
- m = Multi([p, p])
- s2 = simplify(m, DouglasPeucker(T(0.5)))
- @test s2 == Multi([s1, s1])
- d = GeometrySet([p, p])
- s3 = simplify(d, DouglasPeucker(T(0.5)))
- @test s3 == GeometrySet([s1, s1])
-
- # perform binary search for ϵ tolerance
- c = Ring(P2[(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)])
- s1 = simplify(c, DouglasPeucker(T(0.1)))
- s2 = simplify(c, DouglasPeucker(max=6))
- @test s1 == s2
- s1 = simplify(c, DouglasPeucker(T(0.5)))
- s2 = simplify(c, DouglasPeucker(max=4))
- @test s1 == s2
- end
+@testitem "Selinger" setup = [Setup] begin
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2), (0, 2), (0, 1)]))
+ s1 = simplify(c, SelingerSimplification(T(0.1)))
+ s2 = simplify(c, SelingerSimplification(T(0.5)))
+ @test s1 == Ring(cart.([(1, 0), (1, 1), (2, 1), (2, 2), (0, 2), (0, 0)]))
+ @test s2 == Ring(cart.([(1, 0), (2, 2), (0, 2), (0, 0)]))
+end
- @testset "Selinger" begin
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2), (0, 2), (0, 1)])
- s1 = simplify(c, Selinger(0.1))
- s2 = simplify(c, Selinger(0.5))
- @test s1 == Ring(P2[(1, 0), (1, 1), (2, 1), (2, 2), (0, 2), (0, 0)])
- @test s2 == Ring(P2[(1, 0), (2, 2), (0, 2), (0, 0)])
- end
+@testitem "DouglasPeucker" setup = [Setup] begin
+ c = Ring(cart.([(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)]))
+ s1 = simplify(c, DouglasPeuckerSimplification(T(0.1)))
+ s2 = simplify(c, DouglasPeuckerSimplification(T(0.5)))
+ @test s1 == Ring(cart.([(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)]))
+ @test s2 == Ring(cart.([(0, 0), (1.5, 0.5), (0, 1)]))
- @testset "Utilities" begin
- # decimate is a helper function to simplify
- # geometries with an appropriate method
- b = Box(P2(0, 0), P2(1, 1))
- s = decimate(b, 1.0)
- @test s isa Polygon
- @test nvertices(s) == 3
- @test boundary(s) == Ring(P2[(0, 0), (1, 0), (0, 1)])
+ p = PolyArea(Ring(cart.([(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)])))
+ s1 = simplify(p, DouglasPeuckerSimplification(T(0.5)))
+ @test s1 == PolyArea(Ring(cart.([(0, 0), (1.5, 0.5), (0, 1)])))
+ m = Multi([p, p])
+ s2 = simplify(m, DouglasPeuckerSimplification(T(0.5)))
+ @test s2 == Multi([s1, s1])
+ d = GeometrySet([p, p])
+ s3 = simplify(d, DouglasPeuckerSimplification(T(0.5)))
+ @test s3 == GeometrySet([s1, s1])
+end
- c = Ring(P2[(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)])
- s1 = decimate(c, T(0.1))
- s2 = decimate(c, T(0.5))
- @test s1 == Ring(P2[(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)])
- @test s2 == Ring(P2[(0, 0), (1.5, 0.5), (0, 1)])
+@testitem "MinMax" setup = [Setup] begin
+ # Selinger
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2), (0, 2), (0, 1)]))
+ s1 = simplify(c, SelingerSimplification(T(0.1)))
+ s2 = simplify(c, MinMaxSimplification(SelingerSimplification, max=6))
+ @test nvertices(s2) ≤ nvertices(s1)
+ s1 = simplify(c, SelingerSimplification(T(0.5)))
+ s2 = simplify(c, MinMaxSimplification(SelingerSimplification, max=4))
+ @test nvertices(s2) ≤ nvertices(s1)
- c = Ring(P2[(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)])
- s1 = decimate(c, T(0.1))
- s2 = decimate(c, max=6)
- @test s1 == s2
- s1 = decimate(c, T(0.5))
- s2 = decimate(c, max=4)
- @test s1 == s2
- end
+ # Douglas-Peucker
+ c = Ring(cart.([(0, 0), (1, 0), (1.5, 0.5), (1, 1), (0, 1)]))
+ s1 = simplify(c, DouglasPeuckerSimplification(T(0.1)))
+ s2 = simplify(c, MinMaxSimplification(DouglasPeuckerSimplification, max=6))
+ @test s1 ≗ s2
+ s1 = simplify(c, DouglasPeuckerSimplification(T(0.5)))
+ s2 = simplify(c, MinMaxSimplification(DouglasPeuckerSimplification, max=4))
+ @test s1 ≗ s2
end
diff --git a/test/sorting.jl b/test/sorting.jl
index 2fbf2aab5..ad5aad24a 100644
--- a/test/sorting.jl
+++ b/test/sorting.jl
@@ -1,8 +1,16 @@
-@testset "Sorting" begin
- @testset "DirectionSort" begin
- g = CartesianGrid{T}(3, 3)
- s = sort(g, DirectionSort((T(1), T(1))))
- @test centroid.(s) ==
- P2[(0.5, 0.5), (1.5, 0.5), (0.5, 1.5), (2.5, 0.5), (1.5, 1.5), (0.5, 2.5), (2.5, 1.5), (1.5, 2.5), (2.5, 2.5)]
- end
+@testitem "DirectionSort" setup = [Setup] begin
+ g = cartgrid(3, 3)
+ s = sort(g, DirectionSort((T(1), T(1))))
+ @test centroid.(s) ==
+ cart.([
+ (0.5, 0.5),
+ (1.5, 0.5),
+ (0.5, 1.5),
+ (2.5, 0.5),
+ (1.5, 1.5),
+ (0.5, 2.5),
+ (2.5, 1.5),
+ (1.5, 2.5),
+ (2.5, 2.5)
+ ])
end
diff --git a/test/subdomains.jl b/test/subdomains.jl
index e6470fadb..9459f6e72 100644
--- a/test/subdomains.jl
+++ b/test/subdomains.jl
@@ -1,30 +1,36 @@
-@testset "SubDomains" begin
- pset = PointSet(rand(P3, 100))
+@testitem "SubDomains" setup = [Setup] begin
+ pset = PointSet(randpoint3(100))
inds = rand(1:100, 3)
v = view(pset, inds)
@test nelements(v) == 3
+ @test crs(v) <: Cartesian{NoDatum}
+ @test Meshes.lentype(v) == ℳ
for i in 1:3
p = pset[inds[i]]
@test v[i] == p
@test centroid(v, i) == p
end
- grid = CartesianGrid{T}(10, 10)
+ grid = cartgrid(10, 10)
inds = rand(1:100, 3)
v = view(grid, inds)
@test nelements(v) == 3
+ @test crs(v) <: Cartesian{NoDatum}
+ @test Meshes.lentype(v) == ℳ
for i in 1:3
e = grid[inds[i]]
@test v[i] == e
@test centroid(v, i) == centroid(e)
end
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
connec = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
mesh = SimpleMesh(points, connec)
inds = rand(1:4, 3)
v = view(mesh, inds)
@test nelements(v) == 3
+ @test crs(v) <: Cartesian{NoDatum}
+ @test Meshes.lentype(v) == ℳ
for i in 1:3
e = mesh[inds[i]]
@test v[i] == e
@@ -32,101 +38,101 @@
end
# view of view stores the correct domain
- g = CartesianGrid{T}(10, 10)
+ g = cartgrid(10, 10)
v = view(view(g, 11:20), 1:3)
- @test v isa SubGrid{2,T}
+ @test v isa Meshes.SubGrid
@test v[1] == g[11]
@test v[2] == g[12]
@test v[3] == g[13]
# centroid of view of PointSet
- points = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
+ points = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
pview = view(PointSet(points), 1:4)
- @test centroid(pview) == P2(0.5, 0.5)
+ @test centroid(pview) == cart(0.5, 0.5)
# measure of view
- g = CartesianGrid{T}(10, 10)
+ g = cartgrid(10, 10)
v = view(g, 1:3)
- @test measure(v) ≈ T(3)
+ @test measure(v) ≈ T(3) * u"m^2"
# concatenation with same parent
- g = CartesianGrid{T}(10, 10)
+ g = cartgrid(10, 10)
vg = vcat(view(g, 50:70), view(g, 10:30))
- @test vg isa SubGrid{2,T}
+ @test vg isa Meshes.SubGrid
@test vg == view(g, [50:70; 10:30])
# concatenation with different parents
- g1 = CartesianGrid{T}(10, 10)
- g2 = CartesianGrid{T}(20, 20)
+ g1 = cartgrid(10, 10)
+ g2 = cartgrid(20, 20)
vg = vcat(view(g1, 50:70), view(g2, 10:30))
@test vg isa GeometrySet
@test vg == GeometrySet([g1[50:70]; g2[10:30]])
# eltype
- d1 = CartesianGrid{T}(1000, 1000)
- d2 = CartesianGrid{T}(1000, 1000, 1000)
- d3 = GeometrySet([P2(0, 0), Box(P2(0, 0), P2(1, 1)), P2(2, 2)])
+ d1 = cartgrid(1000, 1000)
+ d2 = cartgrid(1000, 1000, 1000)
+ d3 = GeometrySet([cart(0, 0), Box(cart(0, 0), cart(1, 1)), cart(2, 2)])
v1 = view(d1, 1:500000)
v2 = view(d2, 1:500000000)
v3 = view(d3, [1, 3])
- @test eltype(v1) === Quadrangle{2,T}
- @test eltype(v2) === Hexahedron{3,T}
- @test eltype(v3) === Primitive{2,T}
+ @test eltype(v1) <: Quadrangle
+ @test eltype(v2) <: Hexahedron
+ @test eltype(v3) <: Primitive
# show
- pset = PointSet(P2.(1:100, 1:100))
+ pset = PointSet(cart.(1:100, 1:100))
v1 = view(pset, 1:10)
v2 = view(pset, [4, 8, 10, 7, 9, 1, 2, 3, 6, 5])
- @test sprint(show, v1) == "10 view(::PointSet{2,$T}, 1:10)"
- @test sprint(show, v2) == "10 view(::PointSet{2,$T}, [4, 8, 10, 7, ..., 2, 3, 6, 5])"
+ @test sprint(show, v1) == "10 view(::PointSet, 1:10)"
+ @test sprint(show, v2) == "10 view(::PointSet, [4, 8, 10, 7, ..., 2, 3, 6, 5])"
if T === Float32
@test sprint(show, MIME"text/plain"(), v1) == """
- 10 view(::PointSet{2,Float32}, 1:10)
- ├─ Point(1.0f0, 1.0f0)
- ├─ Point(2.0f0, 2.0f0)
- ├─ Point(3.0f0, 3.0f0)
- ├─ Point(4.0f0, 4.0f0)
- ├─ Point(5.0f0, 5.0f0)
- ├─ Point(6.0f0, 6.0f0)
- ├─ Point(7.0f0, 7.0f0)
- ├─ Point(8.0f0, 8.0f0)
- ├─ Point(9.0f0, 9.0f0)
- └─ Point(10.0f0, 10.0f0)"""
+ 10 view(::PointSet, 1:10)
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ ├─ Point(x: 2.0f0 m, y: 2.0f0 m)
+ ├─ Point(x: 3.0f0 m, y: 3.0f0 m)
+ ├─ Point(x: 4.0f0 m, y: 4.0f0 m)
+ ├─ Point(x: 5.0f0 m, y: 5.0f0 m)
+ ├─ Point(x: 6.0f0 m, y: 6.0f0 m)
+ ├─ Point(x: 7.0f0 m, y: 7.0f0 m)
+ ├─ Point(x: 8.0f0 m, y: 8.0f0 m)
+ ├─ Point(x: 9.0f0 m, y: 9.0f0 m)
+ └─ Point(x: 10.0f0 m, y: 10.0f0 m)"""
@test sprint(show, MIME"text/plain"(), v2) == """
- 10 view(::PointSet{2,Float32}, [4, 8, 10, 7, ..., 2, 3, 6, 5])
- ├─ Point(4.0f0, 4.0f0)
- ├─ Point(8.0f0, 8.0f0)
- ├─ Point(10.0f0, 10.0f0)
- ├─ Point(7.0f0, 7.0f0)
- ├─ Point(9.0f0, 9.0f0)
- ├─ Point(1.0f0, 1.0f0)
- ├─ Point(2.0f0, 2.0f0)
- ├─ Point(3.0f0, 3.0f0)
- ├─ Point(6.0f0, 6.0f0)
- └─ Point(5.0f0, 5.0f0)"""
+ 10 view(::PointSet, [4, 8, 10, 7, ..., 2, 3, 6, 5])
+ ├─ Point(x: 4.0f0 m, y: 4.0f0 m)
+ ├─ Point(x: 8.0f0 m, y: 8.0f0 m)
+ ├─ Point(x: 10.0f0 m, y: 10.0f0 m)
+ ├─ Point(x: 7.0f0 m, y: 7.0f0 m)
+ ├─ Point(x: 9.0f0 m, y: 9.0f0 m)
+ ├─ Point(x: 1.0f0 m, y: 1.0f0 m)
+ ├─ Point(x: 2.0f0 m, y: 2.0f0 m)
+ ├─ Point(x: 3.0f0 m, y: 3.0f0 m)
+ ├─ Point(x: 6.0f0 m, y: 6.0f0 m)
+ └─ Point(x: 5.0f0 m, y: 5.0f0 m)"""
else
@test sprint(show, MIME"text/plain"(), v1) == """
- 10 view(::PointSet{2,Float64}, 1:10)
- ├─ Point(1.0, 1.0)
- ├─ Point(2.0, 2.0)
- ├─ Point(3.0, 3.0)
- ├─ Point(4.0, 4.0)
- ├─ Point(5.0, 5.0)
- ├─ Point(6.0, 6.0)
- ├─ Point(7.0, 7.0)
- ├─ Point(8.0, 8.0)
- ├─ Point(9.0, 9.0)
- └─ Point(10.0, 10.0)"""
+ 10 view(::PointSet, 1:10)
+ ├─ Point(x: 1.0 m, y: 1.0 m)
+ ├─ Point(x: 2.0 m, y: 2.0 m)
+ ├─ Point(x: 3.0 m, y: 3.0 m)
+ ├─ Point(x: 4.0 m, y: 4.0 m)
+ ├─ Point(x: 5.0 m, y: 5.0 m)
+ ├─ Point(x: 6.0 m, y: 6.0 m)
+ ├─ Point(x: 7.0 m, y: 7.0 m)
+ ├─ Point(x: 8.0 m, y: 8.0 m)
+ ├─ Point(x: 9.0 m, y: 9.0 m)
+ └─ Point(x: 10.0 m, y: 10.0 m)"""
@test sprint(show, MIME"text/plain"(), v2) == """
- 10 view(::PointSet{2,Float64}, [4, 8, 10, 7, ..., 2, 3, 6, 5])
- ├─ Point(4.0, 4.0)
- ├─ Point(8.0, 8.0)
- ├─ Point(10.0, 10.0)
- ├─ Point(7.0, 7.0)
- ├─ Point(9.0, 9.0)
- ├─ Point(1.0, 1.0)
- ├─ Point(2.0, 2.0)
- ├─ Point(3.0, 3.0)
- ├─ Point(6.0, 6.0)
- └─ Point(5.0, 5.0)"""
+ 10 view(::PointSet, [4, 8, 10, 7, ..., 2, 3, 6, 5])
+ ├─ Point(x: 4.0 m, y: 4.0 m)
+ ├─ Point(x: 8.0 m, y: 8.0 m)
+ ├─ Point(x: 10.0 m, y: 10.0 m)
+ ├─ Point(x: 7.0 m, y: 7.0 m)
+ ├─ Point(x: 9.0 m, y: 9.0 m)
+ ├─ Point(x: 1.0 m, y: 1.0 m)
+ ├─ Point(x: 2.0 m, y: 2.0 m)
+ ├─ Point(x: 3.0 m, y: 3.0 m)
+ ├─ Point(x: 6.0 m, y: 6.0 m)
+ └─ Point(x: 5.0 m, y: 5.0 m)"""
end
end
diff --git a/test/supportfun.jl b/test/supportfun.jl
index f15074471..7d93c5261 100644
--- a/test/supportfun.jl
+++ b/test/supportfun.jl
@@ -1,20 +1,20 @@
-@testset "Support function" begin
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test supportfun(t, V2(1, 0)) == P2(1, 0)
- @test supportfun(t, V2(0, 1)) == P2(0, 1)
- @test supportfun(t, V2(-1, -1)) == P2(0, 0)
- @test supportfun(t, V2(-1, 1)) == P2(0, 1)
+@testitem "Support function" setup = [Setup] begin
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test supportfun(t, vector(1, 0)) == cart(1, 0)
+ @test supportfun(t, vector(0, 1)) == cart(0, 1)
+ @test supportfun(t, vector(-1, -1)) == cart(0, 0)
+ @test supportfun(t, vector(-1, 1)) == cart(0, 1)
- b = Ball(P2(0, 0), T(2))
- @test supportfun(b, V2(1, 1)) ≈ P2(√2, √2)
- @test supportfun(b, V2(1, 0)) ≈ P2(2, 0)
- @test supportfun(b, V2(0, 1)) ≈ P2(0, 2)
- @test supportfun(b, V2(-1, 1)) ≈ P2(-√2, √2)
+ b = Ball(cart(0, 0), T(2))
+ @test supportfun(b, vector(1, 1)) ≈ cart(√2, √2)
+ @test supportfun(b, vector(1, 0)) ≈ cart(2, 0)
+ @test supportfun(b, vector(0, 1)) ≈ cart(0, 2)
+ @test supportfun(b, vector(-1, 1)) ≈ cart(-√2, √2)
- b = Box(P2(0, 0), P2(1, 1))
- @test supportfun(b, V2(1, 1)) ≈ P2(1, 1)
- @test supportfun(b, V2(1, 0)) ≈ P2(1, 0)
- @test supportfun(b, V2(-1, 0)) ≈ P2(0, 0)
- @test supportfun(b, V2(-1, -1)) ≈ P2(0, 0)
- @test supportfun(b, V2(-1, 1)) ≈ P2(0, 1)
+ b = Box(cart(0, 0), cart(1, 1))
+ @test supportfun(b, vector(1, 1)) ≈ cart(1, 1)
+ @test supportfun(b, vector(1, 0)) ≈ cart(1, 0)
+ @test supportfun(b, vector(-1, 0)) ≈ cart(0, 0)
+ @test supportfun(b, vector(-1, -1)) ≈ cart(0, 0)
+ @test supportfun(b, vector(-1, 1)) ≈ cart(0, 1)
end
diff --git a/test/tesselation.jl b/test/tesselation.jl
new file mode 100644
index 000000000..93abc8638
--- /dev/null
+++ b/test/tesselation.jl
@@ -0,0 +1,47 @@
+@testitem "Delaunay" setup = [Setup] begin
+ pts = randpoint2(10)
+ pset = PointSet(pts)
+ mesh1 = tesselate(pts, DelaunayTesselation(StableRNG(2024)))
+ mesh2 = tesselate(pset, DelaunayTesselation(StableRNG(2024)))
+ @test mesh1 == mesh2
+
+ # CRS propagation
+ tuples = [(rand(T) * u"km", rand(T) * u"km") for _ in 1:10]
+ pset = PointSet(Point.(Cartesian{WGS84Latest}.(tuples)))
+ mesh = tesselate(pset, DelaunayTesselation(StableRNG(2024)))
+ @test crs(mesh) === crs(pset)
+
+ coords = [LatLon(rand(-90:T(0.1):90), rand(-180:T(0.1):180)) for _ in 1:10]
+ pset = PointSet(Point.(coords))
+ mesh = tesselate(pset, DelaunayTesselation(StableRNG(2024)))
+ @test crs(mesh) === crs(pset)
+
+ # error: the number of coordinates of the points must be 2
+ pts = randpoint3(10)
+ pset = PointSet(pts)
+ @test_throws AssertionError tesselate(pset, DelaunayTesselation(StableRNG(2024)))
+end
+
+@testitem "Voronoi" setup = [Setup] begin
+ pts = randpoint2(10)
+ pset = PointSet(pts)
+ mesh1 = tesselate(pts, VoronoiTesselation(StableRNG(2024)))
+ mesh2 = tesselate(pset, VoronoiTesselation(StableRNG(2024)))
+ @test mesh1 == mesh2
+
+ # CRS propagation
+ tuples = [(rand(T) * u"km", rand(T) * u"km") for _ in 1:10]
+ pset = PointSet(Point.(Cartesian{WGS84Latest}.(tuples)))
+ mesh = tesselate(pset, VoronoiTesselation(StableRNG(2024)))
+ @test crs(mesh) === crs(pset)
+
+ # error: the number of coordinates of the points must be 2
+ pts = randpoint3(10)
+ pset = PointSet(pts)
+ @test_throws AssertionError tesselate(pset, VoronoiTesselation(StableRNG(2024)))
+
+ # Test polygon order is the same as input points order
+ pts = randpoint2(10)
+ mesh = tesselate(pts, VoronoiTesselation(StableRNG(2024)))
+ @test all(p ∈ poly for (p, poly) in zip(pts, mesh))
+end
diff --git a/test/testutils.jl b/test/testutils.jl
new file mode 100644
index 000000000..26c233dcf
--- /dev/null
+++ b/test/testutils.jl
@@ -0,0 +1,192 @@
+# -------------
+# HELPER TYPES
+# -------------
+
+# meter type
+ℳ = Meshes.Met{T}
+
+# dummy type implementing the Domain trait
+struct DummyDomain{M<:Meshes.Manifold,C<:CRS} <: Domain{M,C}
+ origin::Point{M,C}
+end
+
+function Meshes.element(domain::DummyDomain, ind::Int)
+ ℒ = Meshes.lentype(domain)
+ T = Unitful.numtype(ℒ)
+ c = domain.origin + Vec(ntuple(i -> T(ind) * unit(ℒ), embeddim(domain)))
+ r = oneunit(ℒ)
+ Ball(c, r)
+end
+
+Meshes.nelements(d::DummyDomain) = 3
+
+# -------------
+# IO FUNCTIONS
+# -------------
+
+# helper function to read *.line files containing polygons
+# generated with RPG (https://github.com/cgalab/genpoly-rpg)
+function readpoly(T, fname)
+ open(fname, "r") do f
+ # read outer chain
+ n = parse(Int, readline(f))
+ outer = map(1:n) do _
+ coords = readline(f)
+ x, y = parse.(T, split(coords))
+ Point(x, y)
+ end
+
+ # read inner chains
+ inners = []
+ while !eof(f)
+ n = parse(Int, readline(f))
+ inner = map(1:n) do _
+ coords = readline(f)
+ x, y = parse.(T, split(coords))
+ Point(x, y)
+ end
+ push!(inners, inner)
+ end
+
+ # return polygonal area
+ @assert first(outer) == last(outer)
+ @assert all(first(i) == last(i) for i in inners)
+ rings = [outer, inners...]
+ PolyArea([r[begin:(end - 1)] for r in rings])
+ end
+end
+
+# helper function to read *.ply files containing meshes
+function readply(T, fname)
+ ply = load_ply(fname)
+ x = T.(ply["vertex"]["x"])
+ y = T.(ply["vertex"]["y"])
+ z = T.(ply["vertex"]["z"])
+ points = Point.(x, y, z)
+ connec = [connect(Tuple(c .+ 1)) for c in ply["face"]["vertex_indices"]]
+ SimpleMesh(points, connec)
+end
+
+# --------------
+# CRS FUNCTIONS
+# --------------
+
+cart(T::Type, coords...) = cart(T, coords)
+cart(T::Type, coords::Tuple) = Point(T.(coords))
+
+merc(T::Type, coords...) = merc(T, coords)
+merc(T::Type, coords::Tuple) = Point(Mercator(T.(coords)...))
+
+latlon(T::Type, coords...) = latlon(T, coords)
+latlon(T::Type, coords::Tuple) = Point(LatLon(T.(coords)...))
+
+vector(T::Type, coords...) = vector(T, coords)
+vector(T::Type, coords::Tuple) = Vec(T.(coords))
+
+cartgrid(args...) = cartgrid(T, args...)
+cartgrid(T::Type, dims...) = cartgrid(T, dims)
+function cartgrid(T::Type, dims::Dims{Dim}) where {Dim}
+ origin = ntuple(i -> T(0.0), Dim)
+ spacing = ntuple(i -> T(1.0), Dim)
+ offset = ntuple(i -> 1, Dim)
+ CartesianGrid(dims, origin, spacing, offset)
+end
+
+randcart(T, Dim, n) = [Point(ntuple(i -> rand(T), Dim)) for _ in 1:n]
+
+# methods with fixed T
+cart(xs...) = cart(T, xs...)
+merc(xs...) = merc(T, xs...)
+latlon(xs...) = latlon(T, xs...)
+vector(xs...) = vector(T, xs...)
+randpoint1(n) = randcart(T, 1, n)
+randpoint2(n) = randcart(T, 2, n)
+randpoint3(n) = randcart(T, 3, n)
+
+# ----------------
+# OTHER FUNCTIONS
+# ----------------
+
+numconvert(T, x::Quantity{S,D,U}) where {S,D,U} = convert(Quantity{T,D,U}, x)
+
+withprecision(_, x) = x
+withprecision(T, v::Vec) = numconvert.(T, v)
+withprecision(T, p::Point) = Meshes.withcrs(p, withprecision(T, to(p)))
+withprecision(T, len::Meshes.Len) = numconvert(T, len)
+withprecision(T, lens::NTuple{Dim,Meshes.Len}) where {Dim} = numconvert.(T, lens)
+withprecision(T, geoms::StaticVector{Dim,<:Geometry}) where {Dim} = withprecision.(T, geoms)
+withprecision(T, geoms::AbstractVector{<:Geometry}) = [withprecision(T, g) for g in geoms]
+withprecision(T, geoms::CircularVector{<:Geometry}) = CircularVector([withprecision(T, g) for g in geoms])
+@generated function withprecision(T, g::G) where {G<:Meshes.GeometryOrDomain}
+ ctor = Meshes.constructor(G)
+ names = fieldnames(G)
+ exprs = (:(withprecision(T, g.$name)) for name in names)
+ :($ctor($(exprs...)))
+end
+
+# helper function for type stability tests
+function someornone(g1, g2)
+ intersection(g1, g2) do I
+ if type(I) == NotIntersecting
+ "None"
+ else
+ "Some"
+ end
+ end
+end
+
+setify(lists) = Set(Set.(lists))
+
+function equaltest(g)
+ @test g == withprecision(Float64, g)
+ @test g == withprecision(Float32, g)
+end
+
+isapproxtest(g::Geometry) = _isapproxtest(g, Val(embeddim(g)))
+
+function _isapproxtest(g::Geometry, ::Val{1})
+ τ64 = Meshes.atol(Float64) * u"m"
+ τ32 = Meshes.atol(Float32) * u"m"
+ g64 = withprecision(Float64, g)
+ g32 = withprecision(Float32, g)
+ @test isapprox(g, Translate(τ64)(g64), atol=1.1τ64)
+ @test isapprox(g, Translate(τ32)(g32), atol=1.1τ32)
+end
+
+function _isapproxtest(g::Geometry, ::Val{2})
+ τ64 = Meshes.atol(Float64) * u"m"
+ τ32 = Meshes.atol(Float32) * u"m"
+ g64 = withprecision(Float64, g)
+ g32 = withprecision(Float32, g)
+ @test isapprox(g, Translate(τ64, 0u"m")(g64), atol=1.1τ64)
+ @test isapprox(g, Translate(0u"m", τ64)(g64), atol=1.1τ64)
+ @test isapprox(g, Translate(τ32, 0u"m")(g32), atol=1.1τ32)
+ @test isapprox(g, Translate(0u"m", τ32)(g32), atol=1.1τ32)
+end
+
+function _isapproxtest(g::Geometry, ::Val{3})
+ τ64 = Meshes.atol(Float64) * u"m"
+ τ32 = Meshes.atol(Float32) * u"m"
+ g64 = withprecision(Float64, g)
+ g32 = withprecision(Float32, g)
+ @test isapprox(g, Translate(τ64, 0u"m", 0u"m")(g64), atol=1.1τ64)
+ @test isapprox(g, Translate(0u"m", τ64, 0u"m")(g64), atol=1.1τ64)
+ @test isapprox(g, Translate(0u"m", 0u"m", τ64)(g64), atol=1.1τ64)
+ @test isapprox(g, Translate(τ32, 0u"m", 0u"m")(g32), atol=1.1τ32)
+ @test isapprox(g, Translate(0u"m", τ32, 0u"m")(g32), atol=1.1τ32)
+ @test isapprox(g, Translate(0u"m", 0u"m", τ32)(g32), atol=1.1τ32)
+end
+
+function eachvertexalloc(g)
+ iterate(eachvertex(g)) # precompile run
+ @allocated for _ in eachvertex(g)
+ end
+end
+
+function vertextest(g)
+ @test collect(eachvertex(g)) == vertices(g)
+ @test eachvertexalloc(g) == 0
+ # type stability
+ @test isconcretetype(eltype(vertices(g)))
+ @inferred vertices(g)
+end
diff --git a/test/tolerances.jl b/test/tolerances.jl
index f2bcd74f0..44de98111 100644
--- a/test/tolerances.jl
+++ b/test/tolerances.jl
@@ -1,10 +1,24 @@
-@testset "tolerances" begin
- Q = typeof(zero(T) * u"m")
+@testitem "Tolerances" setup = [Setup] begin
+ ℒ = ℳ
+ 𝒜 = typeof(zero(ℳ)^2)
+ 𝒱 = typeof(zero(ℳ)^3)
if T === Float32
@test atol(T) == 1.0f-5
- @test atol(Q) == 1.0f-5 * u"m"
+ @test atol(ℒ) == 1.0f-5 * u"m"
+ @test atol(𝒜) == 1.0f-5^2 * u"m^2"
+ @test atol(𝒱) == 1.0f-5^3 * u"m^3"
else
- @test atol(T) == 1e-10
- @test atol(Q) == 1e-10 * u"m"
+ @test atol(T) == 1.0e-10
+ @test atol(ℒ) == 1.0e-10 * u"m"
+ @test atol(𝒜) == 1.0e-10^2 * u"m^2"
+ @test atol(𝒱) == 1.0e-10^3 * u"m^3"
end
+ @test atol(zero(T)) == atol(T)
+ @test atol(zero(ℒ)) == atol(ℒ)
+ @test atol(zero(𝒜)) == atol(𝒜)
+ @test atol(zero(𝒱)) == atol(𝒱)
+ @inferred atol(T)
+ @inferred atol(ℒ)
+ @inferred atol(𝒜)
+ @inferred atol(𝒱)
end
diff --git a/test/topologies.jl b/test/topologies.jl
index 590145748..3e51a0b7e 100644
--- a/test/topologies.jl
+++ b/test/topologies.jl
@@ -1,529 +1,576 @@
-@testset "Topology" begin
- @testset "GridTopology" begin
- t = GridTopology(3)
- @test paramdim(t) == 1
- @test size(t) == (3,)
- @test elementtype(t) == Segment
- @test facettype(t) == Point
- @test elem2cart(t, 1) == (1,)
- @test elem2cart(t, 2) == (2,)
- @test elem2cart(t, 3) == (3,)
- @test cart2corner(t, 1) == 1
- @test cart2corner(t, 2) == 2
- @test cart2corner(t, 3) == 3
- @test elem2corner(t, 1) == 1
- @test elem2corner(t, 2) == 2
- @test elem2corner(t, 3) == 3
- @test corner2elem(t, 1) == 1
- @test corner2elem(t, 2) == 2
- @test corner2elem(t, 3) == 3
- @test nelements(t) == 3
- @test nfacets(t) == 4
- @test nvertices(t) == 4
- @test nfaces(t, 1) == 3
- @test element(t, 1) == connect((1, 2))
- @test element(t, 2) == connect((2, 3))
- @test element(t, 3) == connect((3, 4))
- @test faces(t, 1) == elements(t)
- @test vertices(t) == 1:4
- @test vertex(t, 1) == 1
- @test vertex(t, 4) == 4
+@testitem "GridTopology" setup = [Setup] begin
+ t = GridTopology(3)
+ @test paramdim(t) == 1
+ @test size(t) == (3,)
+ @test elementtype(t) == Segment
+ @test facettype(t) == Point
+ @test elem2cart(t, 1) == (1,)
+ @test elem2cart(t, 2) == (2,)
+ @test elem2cart(t, 3) == (3,)
+ @test cart2corner(t, 1) == 1
+ @test cart2corner(t, 2) == 2
+ @test cart2corner(t, 3) == 3
+ @test elem2corner(t, 1) == 1
+ @test elem2corner(t, 2) == 2
+ @test elem2corner(t, 3) == 3
+ @test corner2elem(t, 1) == 1
+ @test corner2elem(t, 2) == 2
+ @test corner2elem(t, 3) == 3
+ @test nelements(t) == 3
+ @test nfacets(t) == 4
+ @test nvertices(t) == 4
+ @test nfaces(t, 1) == 3
+ @test nfaces(t, 0) == 4
+ @test element(t, 1) == connect((1, 2))
+ @test element(t, 2) == connect((2, 3))
+ @test element(t, 3) == connect((3, 4))
+ @test faces(t, 1) == elements(t)
+ @test faces(t, 0) == vertices(t)
+ @test vertices(t) == 1:4
+ @test vertex(t, 1) == 1
+ @test vertex(t, 4) == 4
- t = GridTopology(3, 4)
- @test paramdim(t) == 2
- @test size(t) == (3, 4)
- @test elementtype(t) == Quadrangle
- @test facettype(t) == Segment
- @test elem2cart(t, 1) == (1, 1)
- @test elem2cart(t, 2) == (2, 1)
- @test elem2cart(t, 3) == (3, 1)
- @test elem2cart(t, 4) == (1, 2)
- @test elem2cart(t, 5) == (2, 2)
- @test elem2cart(t, 6) == (3, 2)
- @test elem2cart(t, 7) == (1, 3)
- @test elem2cart(t, 8) == (2, 3)
- @test elem2cart(t, 9) == (3, 3)
- @test elem2cart(t, 10) == (1, 4)
- @test elem2cart(t, 11) == (2, 4)
- @test elem2cart(t, 12) == (3, 4)
- @test cart2corner(t, 1, 1) == 1
- @test cart2corner(t, 2, 1) == 2
- @test cart2corner(t, 3, 1) == 3
- @test cart2corner(t, 1, 2) == 5
- @test cart2corner(t, 2, 2) == 6
- @test cart2corner(t, 3, 2) == 7
- @test cart2corner(t, 1, 3) == 9
- @test cart2corner(t, 2, 3) == 10
- @test cart2corner(t, 3, 3) == 11
- @test cart2corner(t, 1, 4) == 13
- @test cart2corner(t, 2, 4) == 14
- @test cart2corner(t, 3, 4) == 15
- @test elem2corner(t, 1) == 1
- @test elem2corner(t, 2) == 2
- @test elem2corner(t, 3) == 3
- @test elem2corner(t, 4) == 5
- @test elem2corner(t, 5) == 6
- @test elem2corner(t, 6) == 7
- @test elem2corner(t, 7) == 9
- @test elem2corner(t, 8) == 10
- @test elem2corner(t, 9) == 11
- @test elem2corner(t, 10) == 13
- @test elem2corner(t, 11) == 14
- @test elem2corner(t, 12) == 15
- @test corner2elem(t, 1) == 1
- @test corner2elem(t, 2) == 2
- @test corner2elem(t, 3) == 3
- @test corner2elem(t, 5) == 4
- @test corner2elem(t, 6) == 5
- @test corner2elem(t, 7) == 6
- @test corner2elem(t, 9) == 7
- @test corner2elem(t, 10) == 8
- @test corner2elem(t, 11) == 9
- @test corner2elem(t, 13) == 10
- @test corner2elem(t, 14) == 11
- @test corner2elem(t, 15) == 12
- @test nelements(t) == 12
- @test nfacets(t) == 31
- @test nvertices(t) == 20
- @test nfaces(t, 2) == 12
- @test nfaces(t, 1) == 31
- @test element(t, 1) == connect((1, 2, 6, 5))
- @test element(t, 5) == connect((6, 7, 11, 10))
- @test faces(t, 2) == elements(t)
- @test vertices(t) == 1:20
- @test vertex(t, 1) == 1
- @test vertex(t, 20) == 20
- @test facet.(Ref(t), 1:31) ==
- connect.([
- (1, 5),
- (2, 6),
- (3, 7),
- (4, 8),
- (5, 9),
- (6, 10),
- (7, 11),
- (8, 12),
- (9, 13),
- (10, 14),
- (11, 15),
- (12, 16),
- (13, 17),
- (14, 18),
- (15, 19),
- (16, 20),
- (1, 2),
- (5, 6),
- (9, 10),
- (13, 14),
- (17, 18),
- (2, 3),
- (6, 7),
- (10, 11),
- (14, 15),
- (18, 19),
- (3, 4),
- (7, 8),
- (11, 12),
- (15, 16),
- (19, 20)
- ])
+ t = GridTopology(3, 4)
+ @test paramdim(t) == 2
+ @test size(t) == (3, 4)
+ @test elementtype(t) == Quadrangle
+ @test facettype(t) == Segment
+ @test elem2cart(t, 1) == (1, 1)
+ @test elem2cart(t, 2) == (2, 1)
+ @test elem2cart(t, 3) == (3, 1)
+ @test elem2cart(t, 4) == (1, 2)
+ @test elem2cart(t, 5) == (2, 2)
+ @test elem2cart(t, 6) == (3, 2)
+ @test elem2cart(t, 7) == (1, 3)
+ @test elem2cart(t, 8) == (2, 3)
+ @test elem2cart(t, 9) == (3, 3)
+ @test elem2cart(t, 10) == (1, 4)
+ @test elem2cart(t, 11) == (2, 4)
+ @test elem2cart(t, 12) == (3, 4)
+ @test cart2corner(t, 1, 1) == 1
+ @test cart2corner(t, 2, 1) == 2
+ @test cart2corner(t, 3, 1) == 3
+ @test cart2corner(t, 1, 2) == 5
+ @test cart2corner(t, 2, 2) == 6
+ @test cart2corner(t, 3, 2) == 7
+ @test cart2corner(t, 1, 3) == 9
+ @test cart2corner(t, 2, 3) == 10
+ @test cart2corner(t, 3, 3) == 11
+ @test cart2corner(t, 1, 4) == 13
+ @test cart2corner(t, 2, 4) == 14
+ @test cart2corner(t, 3, 4) == 15
+ @test elem2corner(t, 1) == 1
+ @test elem2corner(t, 2) == 2
+ @test elem2corner(t, 3) == 3
+ @test elem2corner(t, 4) == 5
+ @test elem2corner(t, 5) == 6
+ @test elem2corner(t, 6) == 7
+ @test elem2corner(t, 7) == 9
+ @test elem2corner(t, 8) == 10
+ @test elem2corner(t, 9) == 11
+ @test elem2corner(t, 10) == 13
+ @test elem2corner(t, 11) == 14
+ @test elem2corner(t, 12) == 15
+ @test corner2elem(t, 1) == 1
+ @test corner2elem(t, 2) == 2
+ @test corner2elem(t, 3) == 3
+ @test corner2elem(t, 5) == 4
+ @test corner2elem(t, 6) == 5
+ @test corner2elem(t, 7) == 6
+ @test corner2elem(t, 9) == 7
+ @test corner2elem(t, 10) == 8
+ @test corner2elem(t, 11) == 9
+ @test corner2elem(t, 13) == 10
+ @test corner2elem(t, 14) == 11
+ @test corner2elem(t, 15) == 12
+ @test nelements(t) == 12
+ @test nfacets(t) == 31
+ @test nvertices(t) == 20
+ @test nfaces(t, 2) == 12
+ @test nfaces(t, 1) == 31
+ @test nfaces(t, 0) == 20
+ @test element(t, 1) == connect((1, 2, 6, 5))
+ @test element(t, 5) == connect((6, 7, 11, 10))
+ @test faces(t, 2) == elements(t)
+ @test faces(t, 0) == vertices(t)
+ @test vertices(t) == 1:20
+ @test vertex(t, 1) == 1
+ @test vertex(t, 20) == 20
+ @test facet.(Ref(t), 1:31) ==
+ connect.([
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (4, 8),
+ (5, 9),
+ (6, 10),
+ (7, 11),
+ (8, 12),
+ (9, 13),
+ (10, 14),
+ (11, 15),
+ (12, 16),
+ (13, 17),
+ (14, 18),
+ (15, 19),
+ (16, 20),
+ (1, 2),
+ (5, 6),
+ (9, 10),
+ (13, 14),
+ (17, 18),
+ (2, 3),
+ (6, 7),
+ (10, 11),
+ (14, 15),
+ (18, 19),
+ (3, 4),
+ (7, 8),
+ (11, 12),
+ (15, 16),
+ (19, 20)
+ ])
- t = GridTopology(3, 4, 2)
- @test paramdim(t) == 3
- @test size(t) == (3, 4, 2)
- @test elementtype(t) == Hexahedron
- @test facettype(t) == Quadrangle
- @test elem2cart(t, 1) == (1, 1, 1)
- @test elem2cart(t, 2) == (2, 1, 1)
- @test elem2cart(t, 3) == (3, 1, 1)
- @test elem2cart(t, 4) == (1, 2, 1)
- @test elem2cart(t, 5) == (2, 2, 1)
- @test elem2cart(t, 6) == (3, 2, 1)
- @test elem2cart(t, 7) == (1, 3, 1)
- @test elem2cart(t, 8) == (2, 3, 1)
- @test elem2cart(t, 9) == (3, 3, 1)
- @test elem2cart(t, 10) == (1, 4, 1)
- @test elem2cart(t, 11) == (2, 4, 1)
- @test elem2cart(t, 12) == (3, 4, 1)
- @test elem2cart(t, 13) == (1, 1, 2)
- @test elem2cart(t, 14) == (2, 1, 2)
- @test elem2cart(t, 15) == (3, 1, 2)
- @test elem2cart(t, 16) == (1, 2, 2)
- @test elem2cart(t, 17) == (2, 2, 2)
- @test elem2cart(t, 18) == (3, 2, 2)
- @test elem2cart(t, 19) == (1, 3, 2)
- @test elem2cart(t, 20) == (2, 3, 2)
- @test elem2cart(t, 21) == (3, 3, 2)
- @test elem2cart(t, 22) == (1, 4, 2)
- @test elem2cart(t, 23) == (2, 4, 2)
- @test elem2cart(t, 24) == (3, 4, 2)
- @test cart2corner(t, 1, 1, 1) == 1
- @test cart2corner(t, 2, 1, 1) == 2
- @test cart2corner(t, 3, 1, 1) == 3
- @test cart2corner(t, 1, 2, 1) == 5
- @test cart2corner(t, 2, 2, 1) == 6
- @test cart2corner(t, 3, 2, 1) == 7
- @test cart2corner(t, 1, 3, 1) == 9
- @test cart2corner(t, 2, 3, 1) == 10
- @test cart2corner(t, 3, 3, 1) == 11
- @test cart2corner(t, 1, 4, 1) == 13
- @test cart2corner(t, 2, 4, 1) == 14
- @test cart2corner(t, 3, 4, 1) == 15
- @test cart2corner(t, 1, 1, 2) == 21
- @test cart2corner(t, 2, 1, 2) == 22
- @test cart2corner(t, 3, 1, 2) == 23
- @test cart2corner(t, 1, 2, 2) == 25
- @test cart2corner(t, 2, 2, 2) == 26
- @test cart2corner(t, 3, 2, 2) == 27
- @test cart2corner(t, 1, 3, 2) == 29
- @test cart2corner(t, 2, 3, 2) == 30
- @test cart2corner(t, 3, 3, 2) == 31
- @test cart2corner(t, 1, 4, 2) == 33
- @test cart2corner(t, 2, 4, 2) == 34
- @test cart2corner(t, 3, 4, 2) == 35
- @test elem2corner(t, 1) == 1
- @test elem2corner(t, 2) == 2
- @test elem2corner(t, 3) == 3
- @test elem2corner(t, 4) == 5
- @test elem2corner(t, 5) == 6
- @test elem2corner(t, 6) == 7
- @test elem2corner(t, 7) == 9
- @test elem2corner(t, 8) == 10
- @test elem2corner(t, 9) == 11
- @test elem2corner(t, 10) == 13
- @test elem2corner(t, 11) == 14
- @test elem2corner(t, 12) == 15
- @test elem2corner(t, 13) == 21
- @test elem2corner(t, 14) == 22
- @test elem2corner(t, 15) == 23
- @test elem2corner(t, 16) == 25
- @test elem2corner(t, 17) == 26
- @test elem2corner(t, 18) == 27
- @test elem2corner(t, 19) == 29
- @test elem2corner(t, 20) == 30
- @test elem2corner(t, 21) == 31
- @test elem2corner(t, 22) == 33
- @test elem2corner(t, 23) == 34
- @test elem2corner(t, 24) == 35
- @test corner2elem(t, 1) == 1
- @test corner2elem(t, 2) == 2
- @test corner2elem(t, 3) == 3
- @test corner2elem(t, 5) == 4
- @test corner2elem(t, 6) == 5
- @test corner2elem(t, 7) == 6
- @test corner2elem(t, 9) == 7
- @test corner2elem(t, 10) == 8
- @test corner2elem(t, 11) == 9
- @test corner2elem(t, 13) == 10
- @test corner2elem(t, 14) == 11
- @test corner2elem(t, 15) == 12
- @test corner2elem(t, 21) == 13
- @test corner2elem(t, 22) == 14
- @test corner2elem(t, 23) == 15
- @test corner2elem(t, 25) == 16
- @test corner2elem(t, 26) == 17
- @test corner2elem(t, 27) == 18
- @test corner2elem(t, 29) == 19
- @test corner2elem(t, 30) == 20
- @test corner2elem(t, 31) == 21
- @test corner2elem(t, 33) == 22
- @test corner2elem(t, 34) == 23
- @test corner2elem(t, 35) == 24
- @test nelements(t) == 24
- @test nfacets(t) == 3 * 24 + 3 * 4 + 4 * 2 + 3 * 2
- @test nvertices(t) == 60
- @test nfaces(t, 3) == 24
- @test nfaces(t, 2) == 3 * 24 + 3 * 4 + 4 * 2 + 3 * 2
- @test element(t, 1) == connect((1, 2, 6, 5, 21, 22, 26, 25), Hexahedron)
- @test element(t, 5) == connect((6, 7, 11, 10, 26, 27, 31, 30), Hexahedron)
- @test faces(t, 3) == elements(t)
- @test vertices(t) == 1:60
- @test vertex(t, 1) == 1
- @test vertex(t, 60) == 60
+ t = GridTopology(3, 4, 2)
+ @test paramdim(t) == 3
+ @test size(t) == (3, 4, 2)
+ @test elementtype(t) == Hexahedron
+ @test facettype(t) == Quadrangle
+ @test elem2cart(t, 1) == (1, 1, 1)
+ @test elem2cart(t, 2) == (2, 1, 1)
+ @test elem2cart(t, 3) == (3, 1, 1)
+ @test elem2cart(t, 4) == (1, 2, 1)
+ @test elem2cart(t, 5) == (2, 2, 1)
+ @test elem2cart(t, 6) == (3, 2, 1)
+ @test elem2cart(t, 7) == (1, 3, 1)
+ @test elem2cart(t, 8) == (2, 3, 1)
+ @test elem2cart(t, 9) == (3, 3, 1)
+ @test elem2cart(t, 10) == (1, 4, 1)
+ @test elem2cart(t, 11) == (2, 4, 1)
+ @test elem2cart(t, 12) == (3, 4, 1)
+ @test elem2cart(t, 13) == (1, 1, 2)
+ @test elem2cart(t, 14) == (2, 1, 2)
+ @test elem2cart(t, 15) == (3, 1, 2)
+ @test elem2cart(t, 16) == (1, 2, 2)
+ @test elem2cart(t, 17) == (2, 2, 2)
+ @test elem2cart(t, 18) == (3, 2, 2)
+ @test elem2cart(t, 19) == (1, 3, 2)
+ @test elem2cart(t, 20) == (2, 3, 2)
+ @test elem2cart(t, 21) == (3, 3, 2)
+ @test elem2cart(t, 22) == (1, 4, 2)
+ @test elem2cart(t, 23) == (2, 4, 2)
+ @test elem2cart(t, 24) == (3, 4, 2)
+ @test cart2corner(t, 1, 1, 1) == 1
+ @test cart2corner(t, 2, 1, 1) == 2
+ @test cart2corner(t, 3, 1, 1) == 3
+ @test cart2corner(t, 1, 2, 1) == 5
+ @test cart2corner(t, 2, 2, 1) == 6
+ @test cart2corner(t, 3, 2, 1) == 7
+ @test cart2corner(t, 1, 3, 1) == 9
+ @test cart2corner(t, 2, 3, 1) == 10
+ @test cart2corner(t, 3, 3, 1) == 11
+ @test cart2corner(t, 1, 4, 1) == 13
+ @test cart2corner(t, 2, 4, 1) == 14
+ @test cart2corner(t, 3, 4, 1) == 15
+ @test cart2corner(t, 1, 1, 2) == 21
+ @test cart2corner(t, 2, 1, 2) == 22
+ @test cart2corner(t, 3, 1, 2) == 23
+ @test cart2corner(t, 1, 2, 2) == 25
+ @test cart2corner(t, 2, 2, 2) == 26
+ @test cart2corner(t, 3, 2, 2) == 27
+ @test cart2corner(t, 1, 3, 2) == 29
+ @test cart2corner(t, 2, 3, 2) == 30
+ @test cart2corner(t, 3, 3, 2) == 31
+ @test cart2corner(t, 1, 4, 2) == 33
+ @test cart2corner(t, 2, 4, 2) == 34
+ @test cart2corner(t, 3, 4, 2) == 35
+ @test elem2corner(t, 1) == 1
+ @test elem2corner(t, 2) == 2
+ @test elem2corner(t, 3) == 3
+ @test elem2corner(t, 4) == 5
+ @test elem2corner(t, 5) == 6
+ @test elem2corner(t, 6) == 7
+ @test elem2corner(t, 7) == 9
+ @test elem2corner(t, 8) == 10
+ @test elem2corner(t, 9) == 11
+ @test elem2corner(t, 10) == 13
+ @test elem2corner(t, 11) == 14
+ @test elem2corner(t, 12) == 15
+ @test elem2corner(t, 13) == 21
+ @test elem2corner(t, 14) == 22
+ @test elem2corner(t, 15) == 23
+ @test elem2corner(t, 16) == 25
+ @test elem2corner(t, 17) == 26
+ @test elem2corner(t, 18) == 27
+ @test elem2corner(t, 19) == 29
+ @test elem2corner(t, 20) == 30
+ @test elem2corner(t, 21) == 31
+ @test elem2corner(t, 22) == 33
+ @test elem2corner(t, 23) == 34
+ @test elem2corner(t, 24) == 35
+ @test corner2elem(t, 1) == 1
+ @test corner2elem(t, 2) == 2
+ @test corner2elem(t, 3) == 3
+ @test corner2elem(t, 5) == 4
+ @test corner2elem(t, 6) == 5
+ @test corner2elem(t, 7) == 6
+ @test corner2elem(t, 9) == 7
+ @test corner2elem(t, 10) == 8
+ @test corner2elem(t, 11) == 9
+ @test corner2elem(t, 13) == 10
+ @test corner2elem(t, 14) == 11
+ @test corner2elem(t, 15) == 12
+ @test corner2elem(t, 21) == 13
+ @test corner2elem(t, 22) == 14
+ @test corner2elem(t, 23) == 15
+ @test corner2elem(t, 25) == 16
+ @test corner2elem(t, 26) == 17
+ @test corner2elem(t, 27) == 18
+ @test corner2elem(t, 29) == 19
+ @test corner2elem(t, 30) == 20
+ @test corner2elem(t, 31) == 21
+ @test corner2elem(t, 33) == 22
+ @test corner2elem(t, 34) == 23
+ @test corner2elem(t, 35) == 24
+ @test nelements(t) == 24
+ @test nfacets(t) == 3 * 24 + 3 * 4 + 4 * 2 + 3 * 2
+ @test nvertices(t) == 60
+ @test nfaces(t, 3) == 24
+ @test nfaces(t, 2) == 3 * 24 + 3 * 4 + 4 * 2 + 3 * 2
+ @test nfaces(t, 0) == 60
+ @test element(t, 1) == connect((1, 2, 6, 5, 21, 22, 26, 25), Hexahedron)
+ @test element(t, 5) == connect((6, 7, 11, 10, 26, 27, 31, 30), Hexahedron)
+ @test faces(t, 3) == elements(t)
+ @test faces(t, 0) == vertices(t)
+ @test vertices(t) == 1:60
+ @test vertex(t, 1) == 1
+ @test vertex(t, 60) == 60
- t = GridTopology((3,), (true,))
- @test isperiodic(t) == (true,)
- @test nvertices(t) == 3
- @test nelements(t) == 3
- @test nfacets(t) == 3
- @test element(t, 1) == connect((1, 2))
- @test element(t, 2) == connect((2, 3))
- @test element(t, 3) == connect((3, 1))
+ t = GridTopology((3,), (true,))
+ @test isperiodic(t) == (true,)
+ @test nvertices(t) == 3
+ @test nelements(t) == 3
+ @test nfacets(t) == 3
+ @test element(t, 1) == connect((1, 2))
+ @test element(t, 2) == connect((2, 3))
+ @test element(t, 3) == connect((3, 1))
- t = GridTopology((2, 3), (true, true))
- @test isperiodic(t) == (true, true)
- @test nvertices(t) == 2 * 3
- @test nelements(t) == 6
- @test nfacets(t) == 12
- @test element(t, 1) == connect((1, 2, 4, 3))
- @test element(t, 2) == connect((2, 1, 3, 4))
- @test element(t, 3) == connect((3, 4, 6, 5))
- @test element(t, 4) == connect((4, 3, 5, 6))
- @test element(t, 5) == connect((5, 6, 2, 1))
- @test element(t, 6) == connect((6, 5, 1, 2))
+ t = GridTopology((2, 3), (true, true))
+ @test isperiodic(t) == (true, true)
+ @test nvertices(t) == 2 * 3
+ @test nelements(t) == 6
+ @test nfacets(t) == 12
+ @test element(t, 1) == connect((1, 2, 4, 3))
+ @test element(t, 2) == connect((2, 1, 3, 4))
+ @test element(t, 3) == connect((3, 4, 6, 5))
+ @test element(t, 4) == connect((4, 3, 5, 6))
+ @test element(t, 5) == connect((5, 6, 2, 1))
+ @test element(t, 6) == connect((6, 5, 1, 2))
- t = GridTopology((2, 3), (false, true))
- @test isperiodic(t) == (false, true)
- @test nvertices(t) == 3 * 3
- @test nelements(t) == 6
- @test nfacets(t) == 15
- @test element(t, 1) == connect((1, 2, 5, 4))
- @test element(t, 2) == connect((2, 3, 6, 5))
- @test element(t, 3) == connect((4, 5, 8, 7))
- @test element(t, 4) == connect((5, 6, 9, 8))
- @test element(t, 5) == connect((7, 8, 2, 1))
- @test element(t, 6) == connect((8, 9, 3, 2))
+ t = GridTopology((2, 3), (false, true))
+ @test isperiodic(t) == (false, true)
+ @test nvertices(t) == 3 * 3
+ @test nelements(t) == 6
+ @test nfacets(t) == 15
+ @test element(t, 1) == connect((1, 2, 5, 4))
+ @test element(t, 2) == connect((2, 3, 6, 5))
+ @test element(t, 3) == connect((4, 5, 8, 7))
+ @test element(t, 4) == connect((5, 6, 9, 8))
+ @test element(t, 5) == connect((7, 8, 2, 1))
+ @test element(t, 6) == connect((8, 9, 3, 2))
- t = GridTopology((2, 3), (true, false))
- @test isperiodic(t) == (true, false)
- @test nvertices(t) == 2 * 4
- @test nelements(t) == 6
- @test nfacets(t) == 14
- @test element(t, 1) == connect((1, 2, 4, 3))
- @test element(t, 2) == connect((2, 1, 3, 4))
- @test element(t, 3) == connect((3, 4, 6, 5))
- @test element(t, 4) == connect((4, 3, 5, 6))
- @test element(t, 5) == connect((5, 6, 8, 7))
- @test element(t, 6) == connect((6, 5, 7, 8))
+ t = GridTopology((2, 3), (true, false))
+ @test isperiodic(t) == (true, false)
+ @test nvertices(t) == 2 * 4
+ @test nelements(t) == 6
+ @test nfacets(t) == 14
+ @test element(t, 1) == connect((1, 2, 4, 3))
+ @test element(t, 2) == connect((2, 1, 3, 4))
+ @test element(t, 3) == connect((3, 4, 6, 5))
+ @test element(t, 4) == connect((4, 3, 5, 6))
+ @test element(t, 5) == connect((5, 6, 8, 7))
+ @test element(t, 6) == connect((6, 5, 7, 8))
- t = GridTopology((2, 3, 4), (true, true, true))
- @test isperiodic(t) == (true, true, true)
- @test nvertices(t) == 2 * 3 * 4
- @test nelements(t) == 2 * 3 * 4
- @test nfacets(t) == 3 * (2 * 3 * 4)
- @test element(t, 1) == connect((1, 2, 4, 3, 7, 8, 10, 9), Hexahedron)
- @test element(t, 2) == connect((2, 1, 3, 4, 8, 7, 9, 10), Hexahedron)
- @test element(t, 24) == connect((24, 23, 19, 20, 6, 5, 1, 2), Hexahedron)
+ t = GridTopology((2, 3, 4), (true, true, true))
+ @test isperiodic(t) == (true, true, true)
+ @test nvertices(t) == 2 * 3 * 4
+ @test nelements(t) == 2 * 3 * 4
+ @test nfacets(t) == 3 * (2 * 3 * 4)
+ @test element(t, 1) == connect((1, 2, 4, 3, 7, 8, 10, 9), Hexahedron)
+ @test element(t, 2) == connect((2, 1, 3, 4, 8, 7, 9, 10), Hexahedron)
+ @test element(t, 24) == connect((24, 23, 19, 20, 6, 5, 1, 2), Hexahedron)
- t = GridTopology((2, 3, 4), (false, true, true))
- @test isperiodic(t) == (false, true, true)
- @test nvertices(t) == 3 * 3 * 4
- @test nelements(t) == 2 * 3 * 4
- @test nfacets(t) == 3 * (2 * 3 * 4) + 3 * 4
- @test element(t, 1) == connect((1, 2, 5, 4, 10, 11, 14, 13), Hexahedron)
- @test element(t, 2) == connect((2, 3, 6, 5, 11, 12, 15, 14), Hexahedron)
- @test element(t, 24) == connect((35, 36, 30, 29, 8, 9, 3, 2), Hexahedron)
+ t = GridTopology((2, 3, 4), (false, true, true))
+ @test isperiodic(t) == (false, true, true)
+ @test nvertices(t) == 3 * 3 * 4
+ @test nelements(t) == 2 * 3 * 4
+ @test nfacets(t) == 3 * (2 * 3 * 4) + 3 * 4
+ @test element(t, 1) == connect((1, 2, 5, 4, 10, 11, 14, 13), Hexahedron)
+ @test element(t, 2) == connect((2, 3, 6, 5, 11, 12, 15, 14), Hexahedron)
+ @test element(t, 24) == connect((35, 36, 30, 29, 8, 9, 3, 2), Hexahedron)
- t = GridTopology((2, 3, 4), (true, false, true))
- @test isperiodic(t) == (true, false, true)
- @test nvertices(t) == 2 * 4 * 4
- @test nelements(t) == 2 * 3 * 4
- @test nfacets(t) == 3 * (2 * 3 * 4) + 2 * 4
- @test element(t, 1) == connect((1, 2, 4, 3, 9, 10, 12, 11), Hexahedron)
- @test element(t, 2) == connect((2, 1, 3, 4, 10, 9, 11, 12), Hexahedron)
- @test element(t, 24) == connect((30, 29, 31, 32, 6, 5, 7, 8), Hexahedron)
+ t = GridTopology((2, 3, 4), (true, false, true))
+ @test isperiodic(t) == (true, false, true)
+ @test nvertices(t) == 2 * 4 * 4
+ @test nelements(t) == 2 * 3 * 4
+ @test nfacets(t) == 3 * (2 * 3 * 4) + 2 * 4
+ @test element(t, 1) == connect((1, 2, 4, 3, 9, 10, 12, 11), Hexahedron)
+ @test element(t, 2) == connect((2, 1, 3, 4, 10, 9, 11, 12), Hexahedron)
+ @test element(t, 24) == connect((30, 29, 31, 32, 6, 5, 7, 8), Hexahedron)
- t = GridTopology((2, 3, 4), (true, true, false))
- @test isperiodic(t) == (true, true, false)
- @test nvertices(t) == 2 * 3 * 5
- @test nelements(t) == 2 * 3 * 4
- @test nfacets(t) == 3 * (2 * 3 * 4) + 2 * 3
- @test element(t, 1) == connect((1, 2, 4, 3, 7, 8, 10, 9), Hexahedron)
- @test element(t, 2) == connect((2, 1, 3, 4, 8, 7, 9, 10), Hexahedron)
- @test element(t, 24) == connect((24, 23, 19, 20, 30, 29, 25, 26), Hexahedron)
+ t = GridTopology((2, 3, 4), (true, true, false))
+ @test isperiodic(t) == (true, true, false)
+ @test nvertices(t) == 2 * 3 * 5
+ @test nelements(t) == 2 * 3 * 4
+ @test nfacets(t) == 3 * (2 * 3 * 4) + 2 * 3
+ @test element(t, 1) == connect((1, 2, 4, 3, 7, 8, 10, 9), Hexahedron)
+ @test element(t, 2) == connect((2, 1, 3, 4, 8, 7, 9, 10), Hexahedron)
+ @test element(t, 24) == connect((24, 23, 19, 20, 30, 29, 25, 26), Hexahedron)
- t = GridTopology((2, 3, 4), (true, false, false))
- @test isperiodic(t) == (true, false, false)
- @test nvertices(t) == 2 * 4 * 5
- @test nelements(t) == 2 * 3 * 4
- @test nfacets(t) == 3 * (2 * 3 * 4) + 2 * 4 + 2 * 3
- @test element(t, 1) == connect((1, 2, 4, 3, 9, 10, 12, 11), Hexahedron)
- @test element(t, 2) == connect((2, 1, 3, 4, 10, 9, 11, 12), Hexahedron)
- @test element(t, 24) == connect((30, 29, 31, 32, 38, 37, 39, 40), Hexahedron)
+ t = GridTopology((2, 3, 4), (true, false, false))
+ @test isperiodic(t) == (true, false, false)
+ @test nvertices(t) == 2 * 4 * 5
+ @test nelements(t) == 2 * 3 * 4
+ @test nfacets(t) == 3 * (2 * 3 * 4) + 2 * 4 + 2 * 3
+ @test element(t, 1) == connect((1, 2, 4, 3, 9, 10, 12, 11), Hexahedron)
+ @test element(t, 2) == connect((2, 1, 3, 4, 10, 9, 11, 12), Hexahedron)
+ @test element(t, 24) == connect((30, 29, 31, 32, 38, 37, 39, 40), Hexahedron)
- t = GridTopology((2, 3, 4), (false, true, false))
- @test isperiodic(t) == (false, true, false)
- @test nvertices(t) == 3 * 3 * 5
- @test nelements(t) == 2 * 3 * 4
- @test nfacets(t) == 3 * (2 * 3 * 4) + 3 * 4 + 2 * 3
- @test element(t, 1) == connect((1, 2, 5, 4, 10, 11, 14, 13), Hexahedron)
- @test element(t, 2) == connect((2, 3, 6, 5, 11, 12, 15, 14), Hexahedron)
- @test element(t, 24) == connect((35, 36, 30, 29, 44, 45, 39, 38), Hexahedron)
+ t = GridTopology((2, 3, 4), (false, true, false))
+ @test isperiodic(t) == (false, true, false)
+ @test nvertices(t) == 3 * 3 * 5
+ @test nelements(t) == 2 * 3 * 4
+ @test nfacets(t) == 3 * (2 * 3 * 4) + 3 * 4 + 2 * 3
+ @test element(t, 1) == connect((1, 2, 5, 4, 10, 11, 14, 13), Hexahedron)
+ @test element(t, 2) == connect((2, 3, 6, 5, 11, 12, 15, 14), Hexahedron)
+ @test element(t, 24) == connect((35, 36, 30, 29, 44, 45, 39, 38), Hexahedron)
- t = GridTopology((2, 3, 4), (false, false, true))
- @test isperiodic(t) == (false, false, true)
- @test nvertices(t) == 3 * 4 * 4
- @test nelements(t) == 2 * 3 * 4
- @test nfacets(t) == 3 * (2 * 3 * 4) + 3 * 4 + 2 * 4
- @test element(t, 1) == connect((1, 2, 5, 4, 13, 14, 17, 16), Hexahedron)
- @test element(t, 2) == connect((2, 3, 6, 5, 14, 15, 18, 17), Hexahedron)
- @test element(t, 24) == connect((44, 45, 48, 47, 8, 9, 12, 11), Hexahedron)
+ t = GridTopology((2, 3, 4), (false, false, true))
+ @test isperiodic(t) == (false, false, true)
+ @test nvertices(t) == 3 * 4 * 4
+ @test nelements(t) == 2 * 3 * 4
+ @test nfacets(t) == 3 * (2 * 3 * 4) + 3 * 4 + 2 * 4
+ @test element(t, 1) == connect((1, 2, 5, 4, 13, 14, 17, 16), Hexahedron)
+ @test element(t, 2) == connect((2, 3, 6, 5, 14, 15, 18, 17), Hexahedron)
+ @test element(t, 24) == connect((44, 45, 48, 47, 8, 9, 12, 11), Hexahedron)
+
+ # indexable api
+ t = GridTopology(10, 10)
+ @test t[begin] == connect((1, 2, 13, 12), Quadrangle)
+ @test t[end] == connect((109, 110, 121, 120), Quadrangle)
+ @test t[10] == connect((10, 11, 22, 21), Quadrangle)
+ @test length(t) == 100
+ @test eltype(t) == Connectivity{Quadrangle,4}
+ for e in t
+ @test e isa Connectivity{Quadrangle,4}
end
+end
- @testset "HalfEdgeTopology" begin
- function test_halfedge(elems, topology)
- @test nelements(topology) == length(elems)
- for e in 1:nelements(topology)
- he = half4elem(topology, e)
- inds = indices(elems[e])
- @test he.elem == e
- @test he.head ∈ inds
- end
+@testitem "HalfEdgeTopology" setup = [Setup] begin
+ function test_halfedge(elems, topology)
+ @test nelements(topology) == length(elems)
+ for e in 1:nelements(topology)
+ he = half4elem(topology, e)
+ inds = indices(elems[e])
+ @test he.elem == e
+ @test he.head ∈ inds
end
+ end
- # 2 triangles as a list of half-edges
- h1 = HalfEdge(1, 1)
- h2 = HalfEdge(2, nothing)
- h3 = HalfEdge(2, 1)
- h4 = HalfEdge(3, 2)
- h5 = HalfEdge(3, 1)
- h6 = HalfEdge(1, nothing)
- h7 = HalfEdge(2, 2)
- h8 = HalfEdge(4, nothing)
- h9 = HalfEdge(4, 2)
- h10 = HalfEdge(3, nothing)
- h1.half = h2
- h2.half = h1
- h3.half = h4
- h4.half = h3
- h5.half = h6
- h6.half = h5
- h7.half = h8
- h8.half = h7
- h9.half = h10
- h10.half = h9
- h1.prev = h5
- h1.next = h3
- h3.prev = h1
- h3.next = h5
- h4.prev = h9
- h4.next = h7
- h5.prev = h3
- h5.next = h1
- h7.prev = h4
- h7.next = h9
- h9.prev = h7
- h9.next = h4
- halves = [(h1, h2), (h3, h4), (h5, h6), (h7, h8), (h9, h10)]
- struc = HalfEdgeTopology(halves)
- @test half4elem(struc, 1) == h1
- @test half4elem(struc, 2) == h4
- @test half4vert(struc, 1) == h1
- @test half4vert(struc, 2) == h3
- @test half4vert(struc, 3) == h4
- @test half4vert(struc, 4) == h9
- @test edge4pair(struc, (1, 2)) == 1
- @test edge4pair(struc, (2, 1)) == 1
- @test edge4pair(struc, (2, 3)) == 2
- @test edge4pair(struc, (3, 2)) == 2
- @test edge4pair(struc, (3, 1)) == 3
- @test edge4pair(struc, (1, 3)) == 3
- @test edge4pair(struc, (2, 4)) == 4
- @test edge4pair(struc, (4, 2)) == 4
- @test edge4pair(struc, (4, 3)) == 5
- @test edge4pair(struc, (3, 4)) == 5
+ # 2 triangles as a list of half-edges
+ h1 = HalfEdge(1, 1)
+ h2 = HalfEdge(2, nothing)
+ h3 = HalfEdge(2, 1)
+ h4 = HalfEdge(3, 2)
+ h5 = HalfEdge(3, 1)
+ h6 = HalfEdge(1, nothing)
+ h7 = HalfEdge(2, 2)
+ h8 = HalfEdge(4, nothing)
+ h9 = HalfEdge(4, 2)
+ h10 = HalfEdge(3, nothing)
+ h1.half = h2
+ h2.half = h1
+ h3.half = h4
+ h4.half = h3
+ h5.half = h6
+ h6.half = h5
+ h7.half = h8
+ h8.half = h7
+ h9.half = h10
+ h10.half = h9
+ h1.prev = h5
+ h1.next = h3
+ h3.prev = h1
+ h3.next = h5
+ h4.prev = h9
+ h4.next = h7
+ h5.prev = h3
+ h5.next = h1
+ h7.prev = h4
+ h7.next = h9
+ h9.prev = h7
+ h9.next = h4
+ halves = [(h1, h2), (h3, h4), (h5, h6), (h7, h8), (h9, h10)]
+ struc = HalfEdgeTopology(halves)
+ @test half4elem(struc, 1) == h1
+ @test half4elem(struc, 2) == h4
+ @test half4vert(struc, 1) == h1
+ @test half4vert(struc, 2) == h3
+ @test half4vert(struc, 3) == h4
+ @test half4vert(struc, 4) == h9
+ @test edge4pair(struc, (1, 2)) == 1
+ @test edge4pair(struc, (2, 1)) == 1
+ @test edge4pair(struc, (2, 3)) == 2
+ @test edge4pair(struc, (3, 2)) == 2
+ @test edge4pair(struc, (3, 1)) == 3
+ @test edge4pair(struc, (1, 3)) == 3
+ @test edge4pair(struc, (2, 4)) == 4
+ @test edge4pair(struc, (4, 2)) == 4
+ @test edge4pair(struc, (4, 3)) == 5
+ @test edge4pair(struc, (3, 4)) == 5
- # 2 triangles
- elems = connect.([(1, 2, 3), (4, 3, 2)])
- t = HalfEdgeTopology(elems)
- @test paramdim(t) == 2
- @test nelements(t) == 2
- @test nfacets(t) == 5
- @test nvertices(t) == 4
- @test nfaces(t, 2) == 2
- @test nfaces(t, 1) == 5
- test_halfedge(elems, t)
+ # 2 triangles
+ elems = connect.([(1, 2, 3), (4, 3, 2)])
+ t = HalfEdgeTopology(elems)
+ @test paramdim(t) == 2
+ @test nelements(t) == 2
+ @test nfacets(t) == 5
+ @test nvertices(t) == 4
+ @test nfaces(t, 2) == 2
+ @test nfaces(t, 1) == 5
+ @test nfaces(t, 0) == 4
+ test_halfedge(elems, t)
- # 2 triangles + 2 quadrangles
- elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
- t = HalfEdgeTopology(elems)
- @test paramdim(t) == 2
- @test nelements(t) == 4
- @test nfacets(t) == 9
- @test nvertices(t) == 6
- @test nfaces(t, 2) == 4
- @test nfaces(t, 1) == 9
- test_halfedge(elems, t)
+ # 2 triangles + 2 quadrangles
+ elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
+ t = HalfEdgeTopology(elems)
+ @test paramdim(t) == 2
+ @test nelements(t) == 4
+ @test nfacets(t) == 9
+ @test nvertices(t) == 6
+ @test nfaces(t, 2) == 4
+ @test nfaces(t, 1) == 9
+ @test nfaces(t, 0) == 6
+ test_halfedge(elems, t)
- # 1 triangle + 3 quadrangles + 1 triangle hole
- elems = connect.([(1, 2, 6, 5), (2, 4, 7, 6), (4, 3, 7), (3, 1, 5, 7)])
- t = HalfEdgeTopology(elems)
- @test paramdim(t) == 2
- @test nelements(t) == 4
- @test nfacets(t) == 11
- @test nvertices(t) == 7
- @test nfaces(t, 2) == 4
- @test nfaces(t, 1) == 11
- @test vertices(t) == 1:7
- @test vertex(t, 1) == 1
- @test vertex(t, 7) == 7
- test_halfedge(elems, t)
+ # 1 triangle + 3 quadrangles + 1 triangle hole
+ elems = connect.([(1, 2, 6, 5), (2, 4, 7, 6), (4, 3, 7), (3, 1, 5, 7)])
+ t = HalfEdgeTopology(elems)
+ @test paramdim(t) == 2
+ @test nelements(t) == 4
+ @test nfacets(t) == 11
+ @test nvertices(t) == 7
+ @test nfaces(t, 2) == 4
+ @test nfaces(t, 1) == 11
+ @test nfaces(t, 0) == 7
+ @test vertices(t) == 1:7
+ @test vertex(t, 1) == 1
+ @test vertex(t, 7) == 7
+ test_halfedge(elems, t)
- # no need to sort elements with consistent orientation
- elems = connect.([(1, 2, 6, 5), (2, 4, 7, 6), (4, 3, 7), (3, 1, 5, 7)])
- t = HalfEdgeTopology(elems, sort=false)
- @test paramdim(t) == 2
- @test nelements(t) == 4
- @test nfacets(t) == 11
- @test nvertices(t) == 7
- @test nfaces(t, 2) == 4
- @test nfaces(t, 1) == 11
- test_halfedge(elems, t)
+ # no need to sort elements with consistent orientation
+ elems = connect.([(1, 2, 6, 5), (2, 4, 7, 6), (4, 3, 7), (3, 1, 5, 7)])
+ t = HalfEdgeTopology(elems, sort=false)
+ @test paramdim(t) == 2
+ @test nelements(t) == 4
+ @test nfacets(t) == 11
+ @test nvertices(t) == 7
+ @test nfaces(t, 2) == 4
+ @test nfaces(t, 1) == 11
+ @test nfaces(t, 0) == 7
+ test_halfedge(elems, t)
- # correct construction from inconsistent orientation
- e = connect.([(1, 2, 3), (3, 4, 2), (4, 3, 5), (6, 3, 1)])
- t = HalfEdgeTopology(e)
- n = collect(elements(t))
- @test n[1] == e[1]
- @test n[2] != e[2]
- @test n[3] != e[3]
- @test n[4] != e[4]
+ # correct construction from inconsistent orientation
+ e = connect.([(1, 2, 3), (3, 4, 2), (4, 3, 5), (6, 3, 1)])
+ t = HalfEdgeTopology(e)
+ n = collect(elements(t))
+ @test n[1] == e[1]
+ @test n[2] != e[2]
+ @test n[3] != e[3]
+ @test n[4] != e[4]
- # more challenging case with inconsistent orientation
- e = connect.([(4, 1, 5), (2, 6, 4), (3, 5, 6), (4, 5, 6)])
- t = HalfEdgeTopology(e)
- n = collect(elements(t))
- @test n == connect.([(5, 4, 1), (6, 2, 4), (6, 5, 3), (4, 5, 6)])
+ # more challenging case with inconsistent orientation
+ e = connect.([(4, 1, 5), (2, 6, 4), (3, 5, 6), (4, 5, 6)])
+ t = HalfEdgeTopology(e)
+ n = collect(elements(t))
+ @test n == connect.([(5, 4, 1), (6, 2, 4), (6, 5, 3), (4, 5, 6)])
+
+ # indexable api
+ g = GridTopology(10, 10)
+ t = convert(HalfEdgeTopology, g)
+ @test t[begin] == connect((13, 12, 1, 2), Quadrangle)
+ @test t[end] == connect((110, 121, 120, 109), Quadrangle)
+ @test t[10] == connect((22, 21, 10, 11), Quadrangle)
+ @test length(t) == 100
+ @test eltype(t) == Connectivity{Quadrangle,4}
+ for e in t
+ @test e isa Connectivity{Quadrangle,4}
end
+end
+
+@testitem "SimpleTopology" setup = [Setup] begin
+ # 2 triangles
+ elems = connect.([(1, 2, 3), (4, 3, 2)])
+ t = SimpleTopology(elems)
+ @test paramdim(t) == 2
+ @test connec4elem(t, 1) == (1, 2, 3)
+ @test connec4elem(t, 2) == (4, 3, 2)
+ @test nvertices(t) == 4
+ @test nelements(t) == 2
+ @test vertices(t) == 1:4
+ @test vertex(t, 1) == 1
+ @test vertex(t, 4) == 4
+ @test nfaces(t, 2) == 2
+ @test nfaces(t, 1) == 0
+ @test nfaces(t, 0) == 4
- @testset "SimpleTopology" begin
- # 2 triangles
- elems = connect.([(1, 2, 3), (4, 3, 2)])
- t = SimpleTopology(elems)
- @test paramdim(t) == 2
- @test connec4elem(t, 1) == (1, 2, 3)
- @test connec4elem(t, 2) == (4, 3, 2)
- @test nvertices(t) == 4
- @test nelements(t) == 2
- @test vertices(t) == 1:4
- @test vertex(t, 1) == 1
- @test vertex(t, 4) == 4
- @test nfaces(t, 2) == 2
- @test nfaces(t, 1) == 0
+ # 2 triangles + 2 quadrangles
+ elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
+ t = SimpleTopology(elems)
+ @test connec4elem(t, 1) == (1, 2, 6, 5)
+ @test connec4elem(t, 2) == (2, 4, 6)
+ @test connec4elem(t, 3) == (4, 3, 5, 6)
+ @test connec4elem(t, 4) == (1, 5, 3)
+ @test nelements(t) == 4
+ @test nfacets(t) == 0
+ @test nvertices(t) == 6
+ @test nfaces(t, 2) == 4
+ @test nfaces(t, 1) == 0
+ @test nfaces(t, 0) == 6
- # 2 triangles + 2 quadrangles
- elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
- t = SimpleTopology(elems)
- @test connec4elem(t, 1) == (1, 2, 6, 5)
- @test connec4elem(t, 2) == (2, 4, 6)
- @test connec4elem(t, 3) == (4, 3, 5, 6)
- @test connec4elem(t, 4) == (1, 5, 3)
- @test nelements(t) == 4
- @test nfacets(t) == 0
- @test nvertices(t) == 6
- @test nfaces(t, 2) == 4
- @test nfaces(t, 1) == 0
+ # 1 triangle + 3 quadrangles + 1 triangle hole
+ elems = connect.([(1, 2, 6, 5), (2, 4, 7, 6), (4, 3, 7), (3, 1, 5, 7)])
+ t = SimpleTopology(elems)
+ @test connec4elem(t, 1) == (1, 2, 6, 5)
+ @test connec4elem(t, 2) == (2, 4, 7, 6)
+ @test connec4elem(t, 3) == (4, 3, 7)
+ @test connec4elem(t, 4) == (3, 1, 5, 7)
+ @test nelements(t) == 4
+ @test nfacets(t) == 0
+ @test nvertices(t) == 7
+ @test nfaces(t, 2) == 4
+ @test nfaces(t, 1) == 0
+ @test nfaces(t, 0) == 7
- # 1 triangle + 3 quadrangles + 1 triangle hole
- elems = connect.([(1, 2, 6, 5), (2, 4, 7, 6), (4, 3, 7), (3, 1, 5, 7)])
- t = SimpleTopology(elems)
- @test connec4elem(t, 1) == (1, 2, 6, 5)
- @test connec4elem(t, 2) == (2, 4, 7, 6)
- @test connec4elem(t, 3) == (4, 3, 7)
- @test connec4elem(t, 4) == (3, 1, 5, 7)
- @test nelements(t) == 4
- @test nfacets(t) == 0
- @test nvertices(t) == 7
- @test nfaces(t, 2) == 4
- @test nfaces(t, 1) == 0
+ # convert from other topologies
+ g = GridTopology(2, 2)
+ t = convert(SimpleTopology, g)
+ @test nelements(t) == 4
+ @test nfacets(t) == 12
+ @test nvertices(t) == 9
+ @test nfaces(t, 2) == 4
+ @test nfaces(t, 1) == 12
+ @test nfaces(t, 0) == 9
- # convert from other topologies
- g = GridTopology(2, 2)
- t = convert(SimpleTopology, g)
- @test nelements(t) == 4
- @test nfacets(t) == 12
- @test nvertices(t) == 9
- @test nfaces(t, 2) == 4
- @test nfaces(t, 1) == 12
+ # indexable api
+ g = GridTopology(10, 10)
+ t = convert(SimpleTopology, g)
+ @test t[begin] == connect((1, 2, 13, 12), Quadrangle)
+ @test t[end] == connect((109, 110, 121, 120), Quadrangle)
+ @test t[10] == connect((10, 11, 22, 21), Quadrangle)
+ @test length(t) == 100
+ @test eltype(t) == Connectivity{Quadrangle,4}
+ for e in t
+ @test e isa Connectivity{Quadrangle,4}
end
end
diff --git a/test/toporelations.jl b/test/toporelations.jl
index 4ab9b8751..4b158ade1 100644
--- a/test/toporelations.jl
+++ b/test/toporelations.jl
@@ -1,495 +1,570 @@
-@testset "TopologicalRelation" begin
- @testset "GridTopology" begin
- # 3 segments
- t = GridTopology(3)
- ∂ = Boundary{1,0}(t)
- @test ∂(1) == [1, 2]
- @test ∂(2) == [2, 3]
- @test ∂(3) == [3, 4]
-
- # quadrangles in 2D grid
- t = GridTopology(2, 3)
- ∂ = Boundary{2,0}(t)
- @test ∂(1) == [1, 2, 5, 4]
- @test ∂(2) == [2, 3, 6, 5]
- @test ∂(3) == [4, 5, 8, 7]
- @test ∂(4) == [5, 6, 9, 8]
- @test ∂(5) == [7, 8, 11, 10]
- @test ∂(6) == [8, 9, 12, 11]
-
- # segments of quadrangles in 2D grid
- t = GridTopology(2, 3)
- ∂ = Boundary{1,0}(t)
- @test ∂(1) == [1, 4]
- @test ∂(2) == [2, 5]
- @test ∂(3) == [3, 6]
- @test ∂(4) == [4, 7]
- @test ∂(5) == [5, 8]
- @test ∂(6) == [6, 9]
- @test ∂(7) == [7, 10]
- @test ∂(8) == [8, 11]
- @test ∂(9) == [9, 12]
- @test ∂(10) == [1, 2]
- @test ∂(11) == [4, 5]
- @test ∂(12) == [7, 8]
- @test ∂(13) == [10, 11]
- @test ∂(14) == [2, 3]
- @test ∂(15) == [5, 6]
- @test ∂(16) == [8, 9]
- @test ∂(17) == [11, 12]
-
- # segments of quadrangles in 2D (periodic) grid
- t = GridTopology((2, 2), (true, false))
- ∂ = Boundary{1,0}(t)
- @test nfacets(t) == 10
- @test ∂(1) == [1, 3]
- @test ∂(2) == [2, 4]
- @test ∂(3) == [3, 5]
- @test ∂(4) == [4, 6]
- @test ∂(5) == [1, 2]
- @test ∂(6) == [3, 4]
- @test ∂(7) == [5, 6]
- @test ∂(8) == [2, 1]
- @test ∂(9) == [4, 3]
- @test ∂(10) == [6, 5]
-
- # segments of quadrangles in 2D (periodic) grid
- t = GridTopology((2, 2), (false, true))
- ∂ = Boundary{1,0}(t)
- @test nfacets(t) == 10
- @test ∂(1) == [1, 4]
- @test ∂(2) == [2, 5]
- @test ∂(3) == [3, 6]
- @test ∂(4) == [4, 1]
- @test ∂(5) == [5, 2]
- @test ∂(6) == [6, 3]
- @test ∂(7) == [1, 2]
- @test ∂(8) == [4, 5]
- @test ∂(9) == [2, 3]
- @test ∂(10) == [5, 6]
-
- # segments of quadrangles in 2D (periodic) grid
- t = GridTopology((2, 2), (true, true))
- ∂ = Boundary{1,0}(t)
- @test nfacets(t) == 8
- @test ∂(1) == [1, 3]
- @test ∂(2) == [2, 4]
- @test ∂(3) == [3, 1]
- @test ∂(4) == [4, 2]
- @test ∂(5) == [1, 2]
- @test ∂(6) == [3, 4]
- @test ∂(7) == [2, 1]
- @test ∂(8) == [4, 3]
-
- # quadrangles of hexahedrons in 3D grid
- t = GridTopology(2, 2, 2)
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 13, 14, 25, 26]
- @test ∂(2) == [2, 3, 16, 17, 28, 29]
- @test ∂(3) == [4, 5, 14, 15, 31, 32]
- @test ∂(4) == [5, 6, 17, 18, 34, 35]
- @test ∂(5) == [7, 8, 19, 20, 26, 27]
- @test ∂(6) == [8, 9, 22, 23, 29, 30]
- @test ∂(7) == [10, 11, 20, 21, 32, 33]
- @test ∂(8) == [11, 12, 23, 24, 35, 36]
-
- # quadrangles of hexahedrons in 3D (periodic) grid
- t = GridTopology((2, 2, 2), (true, false, false))
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 9, 10, 21, 22]
- @test ∂(2) == [2, 1, 12, 13, 24, 25]
- @test ∂(3) == [3, 4, 10, 11, 27, 28]
- @test ∂(4) == [4, 3, 13, 14, 30, 31]
- @test ∂(5) == [5, 6, 15, 16, 22, 23]
- @test ∂(6) == [6, 5, 18, 19, 25, 26]
- @test ∂(7) == [7, 8, 16, 17, 28, 29]
- @test ∂(8) == [8, 7, 19, 20, 31, 32]
-
- # quadrangles of hexahedrons in 3D (periodic) grid
- t = GridTopology((2, 2, 2), (false, true, false))
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 13, 14, 21, 22]
- @test ∂(2) == [2, 3, 15, 16, 24, 25]
- @test ∂(3) == [4, 5, 14, 13, 27, 28]
- @test ∂(4) == [5, 6, 16, 15, 30, 31]
- @test ∂(5) == [7, 8, 17, 18, 22, 23]
- @test ∂(6) == [8, 9, 19, 20, 25, 26]
- @test ∂(7) == [10, 11, 18, 17, 28, 29]
- @test ∂(8) == [11, 12, 20, 19, 31, 32]
-
- # quadrangles of hexahedrons in 3D (periodic) grid
- t = GridTopology((2, 2, 2), (false, false, true))
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 13, 14, 25, 26]
- @test ∂(2) == [2, 3, 16, 17, 27, 28]
- @test ∂(3) == [4, 5, 14, 15, 29, 30]
- @test ∂(4) == [5, 6, 17, 18, 31, 32]
- @test ∂(5) == [7, 8, 19, 20, 26, 25]
- @test ∂(6) == [8, 9, 22, 23, 28, 27]
- @test ∂(7) == [10, 11, 20, 21, 30, 29]
- @test ∂(8) == [11, 12, 23, 24, 32, 31]
-
- # quadrangles of hexahedrons in 3D (periodic) grid
- t = GridTopology((2, 2, 2), (true, true, false))
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 9, 10, 17, 18]
- @test ∂(2) == [2, 1, 11, 12, 20, 21]
- @test ∂(3) == [3, 4, 10, 9, 23, 24]
- @test ∂(4) == [4, 3, 12, 11, 26, 27]
- @test ∂(5) == [5, 6, 13, 14, 18, 19]
- @test ∂(6) == [6, 5, 15, 16, 21, 22]
- @test ∂(7) == [7, 8, 14, 13, 24, 25]
- @test ∂(8) == [8, 7, 16, 15, 27, 28]
-
- # quadrangles of hexahedrons in 3D (periodic) grid
- t = GridTopology((2, 2, 2), (true, false, true))
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 9, 10, 21, 22]
- @test ∂(2) == [2, 1, 12, 13, 23, 24]
- @test ∂(3) == [3, 4, 10, 11, 25, 26]
- @test ∂(4) == [4, 3, 13, 14, 27, 28]
- @test ∂(5) == [5, 6, 15, 16, 22, 21]
- @test ∂(6) == [6, 5, 18, 19, 24, 23]
- @test ∂(7) == [7, 8, 16, 17, 26, 25]
- @test ∂(8) == [8, 7, 19, 20, 28, 27]
-
- # quadrangles of hexahedrons in 3D (periodic) grid
- t = GridTopology((2, 2, 2), (false, true, true))
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 13, 14, 21, 22]
- @test ∂(2) == [2, 3, 15, 16, 23, 24]
- @test ∂(3) == [4, 5, 14, 13, 25, 26]
- @test ∂(4) == [5, 6, 16, 15, 27, 28]
- @test ∂(5) == [7, 8, 17, 18, 22, 21]
- @test ∂(6) == [8, 9, 19, 20, 24, 23]
- @test ∂(7) == [10, 11, 18, 17, 26, 25]
- @test ∂(8) == [11, 12, 20, 19, 28, 27]
-
- # quadrangles of hexahedrons in 3D (periodic) grid
- t = GridTopology((2, 2, 2), (true, true, true))
- ∂ = Boundary{3,2}(t)
- @test ∂(1) == [1, 2, 9, 10, 17, 18]
- @test ∂(2) == [2, 1, 11, 12, 19, 20]
- @test ∂(3) == [3, 4, 10, 9, 21, 22]
- @test ∂(4) == [4, 3, 12, 11, 23, 24]
- @test ∂(5) == [5, 6, 13, 14, 18, 17]
- @test ∂(6) == [6, 5, 15, 16, 20, 19]
- @test ∂(7) == [7, 8, 14, 13, 22, 21]
- @test ∂(8) == [8, 7, 16, 15, 24, 23]
-
- # edges of quadrangles in 2D grid
- t = GridTopology(3, 4)
- ∂ = Boundary{2,1}(t)
- @test ∂(1) == [1, 2, 17, 18]
- @test ∂(2) == [2, 3, 22, 23]
- @test ∂(3) == [3, 4, 27, 28]
- @test ∂(4) == [5, 6, 18, 19]
- @test ∂(5) == [6, 7, 23, 24]
- @test ∂(6) == [7, 8, 28, 29]
- @test ∂(7) == [9, 10, 19, 20]
- @test ∂(8) == [10, 11, 24, 25]
- @test ∂(9) == [11, 12, 29, 30]
- @test ∂(10) == [13, 14, 20, 21]
- @test ∂(11) == [14, 15, 25, 26]
- @test ∂(12) == [15, 16, 30, 31]
-
- # edges of quadrangles in 2D (periodic) grid
- t = GridTopology((3, 4), (true, false))
- ∂ = Boundary{2,1}(t)
- @test ∂(1) == [1, 2, 13, 14]
- @test ∂(2) == [2, 3, 18, 19]
- @test ∂(3) == [3, 1, 23, 24]
- @test ∂(4) == [4, 5, 14, 15]
- @test ∂(5) == [5, 6, 19, 20]
- @test ∂(6) == [6, 4, 24, 25]
- @test ∂(7) == [7, 8, 15, 16]
- @test ∂(8) == [8, 9, 20, 21]
- @test ∂(9) == [9, 7, 25, 26]
- @test ∂(10) == [10, 11, 16, 17]
- @test ∂(11) == [11, 12, 21, 22]
- @test ∂(12) == [12, 10, 26, 27]
-
- # edges of quadrangles in 2D (periodic) grid
- t = GridTopology((3, 4), (false, true))
- ∂ = Boundary{2,1}(t)
- @test ∂(1) == [1, 2, 17, 18]
- @test ∂(2) == [2, 3, 21, 22]
- @test ∂(3) == [3, 4, 25, 26]
- @test ∂(4) == [5, 6, 18, 19]
- @test ∂(5) == [6, 7, 22, 23]
- @test ∂(6) == [7, 8, 26, 27]
- @test ∂(7) == [9, 10, 19, 20]
- @test ∂(8) == [10, 11, 23, 24]
- @test ∂(9) == [11, 12, 27, 28]
- @test ∂(10) == [13, 14, 20, 17]
- @test ∂(11) == [14, 15, 24, 21]
- @test ∂(12) == [15, 16, 28, 25]
-
- # edges of quadrangles in 2D (periodic) grid
- t = GridTopology((3, 4), (true, true))
- ∂ = Boundary{2,1}(t)
- @test ∂(1) == [1, 2, 13, 14]
- @test ∂(2) == [2, 3, 17, 18]
- @test ∂(3) == [3, 1, 21, 22]
- @test ∂(4) == [4, 5, 14, 15]
- @test ∂(5) == [5, 6, 18, 19]
- @test ∂(6) == [6, 4, 22, 23]
- @test ∂(7) == [7, 8, 15, 16]
- @test ∂(8) == [8, 9, 19, 20]
- @test ∂(9) == [9, 7, 23, 24]
- @test ∂(10) == [10, 11, 16, 13]
- @test ∂(11) == [11, 12, 20, 17]
- @test ∂(12) == [12, 10, 24, 21]
-
- # 2x3x2 hexahedrons
- t = GridTopology(2, 3, 2)
- ∂ = Boundary{3,0}(t)
- @test ∂(1) == [1, 2, 5, 4, 13, 14, 17, 16]
- @test ∂(2) == [2, 3, 6, 5, 14, 15, 18, 17]
- @test ∂(3) == [4, 5, 8, 7, 16, 17, 20, 19]
- @test ∂(12) == [20, 21, 24, 23, 32, 33, 36, 35]
-
- # quadrangles in 2D grid
- t = GridTopology(2, 3)
- 𝒜 = Adjacency{2}(t)
- @test 𝒜(1) == [2, 3]
- @test 𝒜(2) == [1, 4]
- @test 𝒜(3) == [4, 1, 5]
- @test 𝒜(4) == [3, 2, 6]
- @test 𝒜(5) == [6, 3]
- @test 𝒜(6) == [5, 4]
-
- # quadrangles in 2D grid
- t = GridTopology(3, 3)
- 𝒜 = Adjacency{2}(t)
- @test 𝒜(1) == [2, 4]
- @test 𝒜(2) == [1, 3, 5]
- @test 𝒜(3) == [2, 6]
- @test 𝒜(4) == [5, 1, 7]
- @test 𝒜(5) == [4, 6, 2, 8]
- @test 𝒜(6) == [5, 3, 9]
- @test 𝒜(7) == [8, 4]
- @test 𝒜(8) == [7, 9, 5]
- @test 𝒜(9) == [8, 6]
-
- # quadrangles in 2D grid with periodicity
- t = GridTopology((3, 3), (true, false))
- 𝒜 = Adjacency{2}(t)
- @test 𝒜(1) == [3, 2, 4]
- @test 𝒜(2) == [1, 3, 5]
- @test 𝒜(3) == [2, 1, 6]
- @test 𝒜(4) == [6, 5, 1, 7]
- @test 𝒜(5) == [4, 6, 2, 8]
- @test 𝒜(6) == [5, 4, 3, 9]
- @test 𝒜(7) == [9, 8, 4]
- @test 𝒜(8) == [7, 9, 5]
- @test 𝒜(9) == [8, 7, 6]
-
- # quadrangles in 2D grid with periodicity
- t = GridTopology((3, 3), (true, true))
- 𝒜 = Adjacency{2}(t)
- @test 𝒜(1) == [3, 2, 7, 4]
- @test 𝒜(2) == [1, 3, 8, 5]
- @test 𝒜(3) == [2, 1, 9, 6]
- @test 𝒜(4) == [6, 5, 1, 7]
- @test 𝒜(5) == [4, 6, 2, 8]
- @test 𝒜(6) == [5, 4, 3, 9]
- @test 𝒜(7) == [9, 8, 4, 1]
- @test 𝒜(8) == [7, 9, 5, 2]
- @test 𝒜(9) == [8, 7, 6, 3]
-
- # quadrangles in 3D grid
- t = GridTopology(2, 2, 2)
- 𝒜 = Adjacency{3}(t)
- @test 𝒜(1) == [2, 3, 5]
- @test 𝒜(2) == [1, 4, 6]
- @test 𝒜(3) == [4, 1, 7]
- @test 𝒜(4) == [3, 2, 8]
- @test 𝒜(5) == [6, 7, 1]
- @test 𝒜(6) == [5, 8, 2]
- @test 𝒜(7) == [8, 5, 3]
- @test 𝒜(8) == [7, 6, 4]
-
- # quadrangles in 3D grid
- t = GridTopology(3, 2, 2)
- 𝒜 = Adjacency{3}(t)
- @test 𝒜(1) == [2, 4, 7]
- @test 𝒜(2) == [1, 3, 5, 8]
- @test 𝒜(3) == [2, 6, 9]
- @test 𝒜(4) == [5, 1, 10]
- @test 𝒜(5) == [4, 6, 2, 11]
- @test 𝒜(6) == [5, 3, 12]
- @test 𝒜(7) == [8, 10, 1]
- @test 𝒜(8) == [7, 9, 11, 2]
- @test 𝒜(9) == [8, 12, 3]
- @test 𝒜(10) == [11, 7, 4]
- @test 𝒜(11) == [10, 12, 8, 5]
- @test 𝒜(12) == [11, 9, 6]
-
- # quadrangles in 3D grid with periodicity
- t = GridTopology((3, 2, 2), (true, false, false))
- 𝒜 = Adjacency{3}(t)
- @test 𝒜(1) == [3, 2, 4, 7]
- @test 𝒜(2) == [1, 3, 5, 8]
- @test 𝒜(3) == [2, 1, 6, 9]
- @test 𝒜(4) == [6, 5, 1, 10]
- @test 𝒜(5) == [4, 6, 2, 11]
- @test 𝒜(6) == [5, 4, 3, 12]
- @test 𝒜(7) == [9, 8, 10, 1]
- @test 𝒜(8) == [7, 9, 11, 2]
- @test 𝒜(9) == [8, 7, 12, 3]
- @test 𝒜(10) == [12, 11, 7, 4]
- @test 𝒜(11) == [10, 12, 8, 5]
- @test 𝒜(12) == [11, 10, 9, 6]
-
- # vertices in 2D grid
- t = GridTopology(2, 2)
- 𝒜 = Adjacency{0}(t)
- @test 𝒜(1) == [2, 4]
- @test 𝒜(2) == [1, 3, 5]
- @test 𝒜(3) == [2, 6]
- @test 𝒜(4) == [5, 1, 7]
- @test 𝒜(5) == [4, 6, 2, 8]
- @test 𝒜(6) == [5, 3, 9]
- @test 𝒜(7) == [8, 4]
- @test 𝒜(8) == [7, 9, 5]
- @test 𝒜(9) == [8, 6]
-
- # invalid relations
- t = GridTopology(2, 3)
- @test_throws AssertionError Boundary{3,0}(t)
- @test_throws AssertionError Coboundary{0,3}(t)
- @test_throws AssertionError Adjacency{3}(t)
- @test_throws AssertionError Boundary{0,2}(t)
- @test_throws AssertionError Coboundary{2,0}(t)
- end
-
- @testset "HalfEdgeTopology" begin
- # 2 triangles
- elems = connect.([(1, 2, 3), (4, 3, 2)])
- t = HalfEdgeTopology(elems)
- ∂ = Boundary{2,0}(t)
- @test ∂(1) == [2, 3, 1]
- @test ∂(2) == [3, 2, 4]
- ∂ = Boundary{2,1}(t)
- @test ∂(1) == [1, 3, 2]
- @test ∂(2) == [1, 4, 5]
- ∂ = Boundary{1,0}(t)
- @test ∂(1) == [3, 2]
- @test ∂(2) == [1, 2]
- @test ∂(3) == [3, 1]
- @test ∂(4) == [2, 4]
- @test ∂(5) == [4, 3]
- 𝒞 = Coboundary{0,1}(t)
- @test 𝒞(1) == [2, 3]
- @test 𝒞(2) == [4, 1, 2]
- @test 𝒞(3) == [3, 1, 5]
- @test 𝒞(4) == [5, 4]
- 𝒞 = Coboundary{0,2}(t)
- @test 𝒞(1) == [1]
- @test 𝒞(2) == [2, 1]
- @test 𝒞(3) == [1, 2]
- @test 𝒞(4) == [2]
- 𝒞 = Coboundary{1,2}(t)
- @test 𝒞(1) == [2, 1]
- @test 𝒞(2) == [1]
- @test 𝒞(3) == [1]
- @test 𝒞(4) == [2]
- @test 𝒞(5) == [2]
- 𝒜 = Adjacency{0}(t)
- @test 𝒜(1) == [2, 3]
- @test 𝒜(2) == [4, 3, 1]
- @test 𝒜(3) == [1, 2, 4]
- @test 𝒜(4) == [3, 2]
-
- # 2 triangles + 2 quadrangles
- elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
- t = HalfEdgeTopology(elems)
- ∂ = Boundary{2,0}(t)
- @test ∂(1) == [1, 2, 6, 5]
- @test ∂(2) == [6, 2, 4]
- @test ∂(3) == [6, 4, 3, 5]
- @test ∂(4) == [3, 1, 5]
- ∂ = Boundary{2,1}(t)
- @test ∂(1) == [1, 3, 5, 6]
- @test ∂(2) == [3, 9, 4]
- @test ∂(3) == [4, 7, 8, 5]
- @test ∂(4) == [2, 6, 8]
- ∂ = Boundary{1,0}(t)
- @test ∂(1) == [1, 2]
- @test ∂(2) == [3, 1]
- @test ∂(3) == [6, 2]
- @test ∂(4) == [4, 6]
- @test ∂(5) == [5, 6]
- @test ∂(6) == [1, 5]
- @test ∂(7) == [4, 3]
- @test ∂(8) == [3, 5]
- @test ∂(9) == [2, 4]
- 𝒞 = Coboundary{0,1}(t)
- @test 𝒞(1) == [1, 6, 2]
- @test 𝒞(2) == [9, 3, 1]
- @test 𝒞(3) == [2, 8, 7]
- @test 𝒞(4) == [7, 4, 9]
- @test 𝒞(5) == [5, 8, 6]
- @test 𝒞(6) == [3, 4, 5]
- 𝒞 = Coboundary{0,2}(t)
- @test 𝒞(1) == [1, 4]
- @test 𝒞(2) == [2, 1]
- @test 𝒞(3) == [4, 3]
- @test 𝒞(4) == [3, 2]
- @test 𝒞(5) == [3, 4, 1]
- @test 𝒞(6) == [2, 3, 1]
- 𝒞 = Coboundary{1,2}(t)
- @test 𝒞(1) == [1]
- @test 𝒞(2) == [4]
- @test 𝒞(3) == [2, 1]
- @test 𝒞(4) == [2, 3]
- @test 𝒞(5) == [3, 1]
- @test 𝒞(6) == [4, 1]
- @test 𝒞(7) == [3]
- @test 𝒞(8) == [3, 4]
- @test 𝒞(9) == [2]
- 𝒜 = Adjacency{0}(t)
- @test 𝒜(1) == [2, 5, 3]
- @test 𝒜(2) == [4, 6, 1]
- @test 𝒜(3) == [1, 5, 4]
- @test 𝒜(4) == [3, 6, 2]
- @test 𝒜(5) == [6, 3, 1]
- @test 𝒜(6) == [2, 4, 5]
-
- # 2 triangles + 2 quadrangles
- elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
- t = HalfEdgeTopology(elems)
- 𝒜 = Adjacency{2}(t)
- @test 𝒜(1) == [2, 3, 4]
- @test 𝒜(2) == [1, 3]
- @test 𝒜(3) == [2, 4, 1]
- @test 𝒜(4) == [1, 3]
-
- # 4 quadrangles in a grid
- elems = connect.([(1, 2, 5, 4), (2, 3, 6, 5), (4, 5, 8, 7), (5, 6, 9, 8)])
- t = HalfEdgeTopology(elems)
- 𝒜 = Adjacency{2}(t)
- @test 𝒜(1) == [3, 2]
- @test 𝒜(2) == [1, 4]
- @test 𝒜(3) == [1, 4]
- @test 𝒜(4) == [3, 2]
+@testitem "GridTopology" setup = [Setup] begin
+ # 3 grid
+ t = GridTopology(3)
+ ∂ = Boundary{1,0}(t)
+ @test ∂(1) == (1, 2)
+ @test ∂(2) == (2, 3)
+ @test ∂(3) == (3, 4)
+ 𝒞 = Coboundary{0,1}(t)
+ @test 𝒞(1) == (1,)
+ @test 𝒞(2) == (1, 2)
+ @test 𝒞(3) == (2, 3)
+ @test 𝒞(4) == (3,)
+ 𝒜 = Adjacency{1}(t)
+ @test 𝒜(1) == (2,)
+ @test 𝒜(2) == (1, 3)
+ @test 𝒜(3) == (2,)
+ 𝒜 = Adjacency{0}(t)
+ @test 𝒜(1) == (2,)
+ @test 𝒜(2) == (1, 3)
+ @test 𝒜(3) == (2, 4)
+ @test 𝒜(4) == (3,)
+
+ # 2x3 grid
+ t = GridTopology(2, 3)
+ ∂ = Boundary{2,0}(t)
+ @test ∂(1) == (1, 2, 5, 4)
+ @test ∂(2) == (2, 3, 6, 5)
+ @test ∂(3) == (4, 5, 8, 7)
+ @test ∂(4) == (5, 6, 9, 8)
+ @test ∂(5) == (7, 8, 11, 10)
+ @test ∂(6) == (8, 9, 12, 11)
+ 𝒞 = Coboundary{0,2}(t)
+ @test 𝒞(1) == (1,)
+ @test 𝒞(2) == (1, 2)
+ @test 𝒞(3) == (2,)
+ @test 𝒞(4) == (1, 3)
+ @test 𝒞(5) == (1, 2, 3, 4)
+ @test 𝒞(6) == (2, 4)
+ @test 𝒞(7) == (3, 5)
+ @test 𝒞(8) == (3, 4, 5, 6)
+ @test 𝒞(9) == (4, 6)
+ @test 𝒞(10) == (5,)
+ @test 𝒞(11) == (5, 6)
+ @test 𝒞(12) == (6,)
+ ∂ = Boundary{1,0}(t)
+ @test ∂(1) == (1, 4)
+ @test ∂(2) == (2, 5)
+ @test ∂(3) == (3, 6)
+ @test ∂(4) == (4, 7)
+ @test ∂(5) == (5, 8)
+ @test ∂(6) == (6, 9)
+ @test ∂(7) == (7, 10)
+ @test ∂(8) == (8, 11)
+ @test ∂(9) == (9, 12)
+ @test ∂(10) == (1, 2)
+ @test ∂(11) == (4, 5)
+ @test ∂(12) == (7, 8)
+ @test ∂(13) == (10, 11)
+ @test ∂(14) == (2, 3)
+ @test ∂(15) == (5, 6)
+ @test ∂(16) == (8, 9)
+ @test ∂(17) == (11, 12)
+ 𝒜 = Adjacency{2}(t)
+ @test 𝒜(1) == (2, 3)
+ @test 𝒜(2) == (1, 4)
+ @test 𝒜(3) == (4, 1, 5)
+ @test 𝒜(4) == (3, 2, 6)
+ @test 𝒜(5) == (6, 3)
+ @test 𝒜(6) == (5, 4)
+
+ # 2x2 grid
+ t = GridTopology(2, 2)
+ ∂ = Boundary{2,0}(t)
+ @test ∂(1) == (1, 2, 5, 4)
+ @test ∂(2) == (2, 3, 6, 5)
+ @test ∂(3) == (4, 5, 8, 7)
+ @test ∂(4) == (5, 6, 9, 8)
+ 𝒞 = Coboundary{0,2}(t)
+ @test 𝒞(1) == (1,)
+ @test 𝒞(2) == (1, 2)
+ @test 𝒞(3) == (2,)
+ @test 𝒞(4) == (1, 3)
+ @test 𝒞(5) == (1, 2, 3, 4)
+ @test 𝒞(6) == (2, 4)
+ @test 𝒞(7) == (3,)
+ @test 𝒞(8) == (3, 4)
+ @test 𝒞(9) == (4,)
+ 𝒜 = Adjacency{0}(t)
+ @test 𝒜(1) == (2, 4)
+ @test 𝒜(2) == (1, 3, 5)
+ @test 𝒜(3) == (2, 6)
+ @test 𝒜(4) == (5, 1, 7)
+ @test 𝒜(5) == (4, 6, 2, 8)
+ @test 𝒜(6) == (5, 3, 9)
+ @test 𝒜(7) == (8, 4)
+ @test 𝒜(8) == (7, 9, 5)
+ @test 𝒜(9) == (8, 6)
+
+ # 2x2 (periodic x aperiodic) grid
+ t = GridTopology((2, 2), (true, false))
+ ∂ = Boundary{1,0}(t)
+ @test ∂(1) == (1, 3)
+ @test ∂(2) == (2, 4)
+ @test ∂(3) == (3, 5)
+ @test ∂(4) == (4, 6)
+ @test ∂(5) == (1, 2)
+ @test ∂(6) == (3, 4)
+ @test ∂(7) == (5, 6)
+ @test ∂(8) == (2, 1)
+ @test ∂(9) == (4, 3)
+ @test ∂(10) == (6, 5)
+ 𝒞 = Coboundary{0,2}(t)
+ @test 𝒞(1) == (2, 1)
+ @test 𝒞(2) == (1, 2)
+ @test 𝒞(3) == (2, 1, 4, 3)
+ @test 𝒞(4) == (1, 2, 3, 4)
+ @test 𝒞(5) == (4, 3)
+ @test 𝒞(6) == (3, 4)
+
+ # 2x2 (aperiodic x periodic) grid
+ t = GridTopology((2, 2), (false, true))
+ ∂ = Boundary{1,0}(t)
+ @test ∂(1) == (1, 4)
+ @test ∂(2) == (2, 5)
+ @test ∂(3) == (3, 6)
+ @test ∂(4) == (4, 1)
+ @test ∂(5) == (5, 2)
+ @test ∂(6) == (6, 3)
+ @test ∂(7) == (1, 2)
+ @test ∂(8) == (4, 5)
+ @test ∂(9) == (2, 3)
+ @test ∂(10) == (5, 6)
+ 𝒞 = Coboundary{0,2}(t)
+ @test 𝒞(1) == (3, 1)
+ @test 𝒞(2) == (3, 4, 1, 2)
+ @test 𝒞(3) == (4, 2)
+ @test 𝒞(4) == (1, 3)
+ @test 𝒞(5) == (1, 2, 3, 4)
+ @test 𝒞(6) == (2, 4)
+
+ # 2x2 (periodic x periodic) grid
+ t = GridTopology((2, 2), (true, true))
+ ∂ = Boundary{1,0}(t)
+ @test ∂(1) == (1, 3)
+ @test ∂(2) == (2, 4)
+ @test ∂(3) == (3, 1)
+ @test ∂(4) == (4, 2)
+ @test ∂(5) == (1, 2)
+ @test ∂(6) == (3, 4)
+ @test ∂(7) == (2, 1)
+ @test ∂(8) == (4, 3)
+ 𝒞 = Coboundary{0,2}(t)
+ @test 𝒞(1) == (4, 3, 2, 1)
+ @test 𝒞(2) == (3, 4, 1, 2)
+ @test 𝒞(3) == (2, 1, 4, 3)
+ @test 𝒞(4) == (1, 2, 3, 4)
+
+ # 3x3 grid
+ t = GridTopology(3, 3)
+ 𝒜 = Adjacency{2}(t)
+ @test 𝒜(1) == (2, 4)
+ @test 𝒜(2) == (1, 3, 5)
+ @test 𝒜(3) == (2, 6)
+ @test 𝒜(4) == (5, 1, 7)
+ @test 𝒜(5) == (4, 6, 2, 8)
+ @test 𝒜(6) == (5, 3, 9)
+ @test 𝒜(7) == (8, 4)
+ @test 𝒜(8) == (7, 9, 5)
+ @test 𝒜(9) == (8, 6)
+
+ # 3x3 grid (periodic x aperiodic) grid
+ t = GridTopology((3, 3), (true, false))
+ 𝒜 = Adjacency{2}(t)
+ @test 𝒜(1) == (3, 2, 4)
+ @test 𝒜(2) == (1, 3, 5)
+ @test 𝒜(3) == (2, 1, 6)
+ @test 𝒜(4) == (6, 5, 1, 7)
+ @test 𝒜(5) == (4, 6, 2, 8)
+ @test 𝒜(6) == (5, 4, 3, 9)
+ @test 𝒜(7) == (9, 8, 4)
+ @test 𝒜(8) == (7, 9, 5)
+ @test 𝒜(9) == (8, 7, 6)
+
+ # 3x3 grid (periodic x periodic) grid
+ t = GridTopology((3, 3), (true, true))
+ 𝒜 = Adjacency{2}(t)
+ @test 𝒜(1) == (3, 2, 7, 4)
+ @test 𝒜(2) == (1, 3, 8, 5)
+ @test 𝒜(3) == (2, 1, 9, 6)
+ @test 𝒜(4) == (6, 5, 1, 7)
+ @test 𝒜(5) == (4, 6, 2, 8)
+ @test 𝒜(6) == (5, 4, 3, 9)
+ @test 𝒜(7) == (9, 8, 4, 1)
+ @test 𝒜(8) == (7, 9, 5, 2)
+ @test 𝒜(9) == (8, 7, 6, 3)
+
+ # 3x4 grid
+ t = GridTopology(3, 4)
+ ∂ = Boundary{2,1}(t)
+ @test ∂(1) == (1, 2, 17, 18)
+ @test ∂(2) == (2, 3, 22, 23)
+ @test ∂(3) == (3, 4, 27, 28)
+ @test ∂(4) == (5, 6, 18, 19)
+ @test ∂(5) == (6, 7, 23, 24)
+ @test ∂(6) == (7, 8, 28, 29)
+ @test ∂(7) == (9, 10, 19, 20)
+ @test ∂(8) == (10, 11, 24, 25)
+ @test ∂(9) == (11, 12, 29, 30)
+ @test ∂(10) == (13, 14, 20, 21)
+ @test ∂(11) == (14, 15, 25, 26)
+ @test ∂(12) == (15, 16, 30, 31)
+
+ # 3x4 (periodic x aperiodic) grid
+ t = GridTopology((3, 4), (true, false))
+ ∂ = Boundary{2,1}(t)
+ @test ∂(1) == (1, 2, 13, 14)
+ @test ∂(2) == (2, 3, 18, 19)
+ @test ∂(3) == (3, 1, 23, 24)
+ @test ∂(4) == (4, 5, 14, 15)
+ @test ∂(5) == (5, 6, 19, 20)
+ @test ∂(6) == (6, 4, 24, 25)
+ @test ∂(7) == (7, 8, 15, 16)
+ @test ∂(8) == (8, 9, 20, 21)
+ @test ∂(9) == (9, 7, 25, 26)
+ @test ∂(10) == (10, 11, 16, 17)
+ @test ∂(11) == (11, 12, 21, 22)
+ @test ∂(12) == (12, 10, 26, 27)
+
+ # 3x4 (aperiodic x periodic) grid
+ t = GridTopology((3, 4), (false, true))
+ ∂ = Boundary{2,1}(t)
+ @test ∂(1) == (1, 2, 17, 18)
+ @test ∂(2) == (2, 3, 21, 22)
+ @test ∂(3) == (3, 4, 25, 26)
+ @test ∂(4) == (5, 6, 18, 19)
+ @test ∂(5) == (6, 7, 22, 23)
+ @test ∂(6) == (7, 8, 26, 27)
+ @test ∂(7) == (9, 10, 19, 20)
+ @test ∂(8) == (10, 11, 23, 24)
+ @test ∂(9) == (11, 12, 27, 28)
+ @test ∂(10) == (13, 14, 20, 17)
+ @test ∂(11) == (14, 15, 24, 21)
+ @test ∂(12) == (15, 16, 28, 25)
+
+ # 3x4 (periodic x periodic) grid
+ t = GridTopology((3, 4), (true, true))
+ ∂ = Boundary{2,1}(t)
+ @test ∂(1) == (1, 2, 13, 14)
+ @test ∂(2) == (2, 3, 17, 18)
+ @test ∂(3) == (3, 1, 21, 22)
+ @test ∂(4) == (4, 5, 14, 15)
+ @test ∂(5) == (5, 6, 18, 19)
+ @test ∂(6) == (6, 4, 22, 23)
+ @test ∂(7) == (7, 8, 15, 16)
+ @test ∂(8) == (8, 9, 19, 20)
+ @test ∂(9) == (9, 7, 23, 24)
+ @test ∂(10) == (10, 11, 16, 13)
+ @test ∂(11) == (11, 12, 20, 17)
+ @test ∂(12) == (12, 10, 24, 21)
+
+ # 2x2x2 grid
+ t = GridTopology(2, 2, 2)
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 13, 14, 25, 26)
+ @test ∂(2) == (2, 3, 16, 17, 28, 29)
+ @test ∂(3) == (4, 5, 14, 15, 31, 32)
+ @test ∂(4) == (5, 6, 17, 18, 34, 35)
+ @test ∂(5) == (7, 8, 19, 20, 26, 27)
+ @test ∂(6) == (8, 9, 22, 23, 29, 30)
+ @test ∂(7) == (10, 11, 20, 21, 32, 33)
+ @test ∂(8) == (11, 12, 23, 24, 35, 36)
+ 𝒞 = Coboundary{0,3}(t)
+ @test 𝒞(1) == (1,)
+ @test 𝒞(2) == (1, 2)
+ @test 𝒞(3) == (2,)
+ @test 𝒞(4) == (1, 3)
+ @test 𝒞(5) == (1, 2, 3, 4)
+ @test 𝒞(6) == (2, 4)
+ @test 𝒞(7) == (3,)
+ @test 𝒞(8) == (3, 4)
+ @test 𝒞(9) == (4,)
+ @test 𝒞(10) == (1, 5)
+ @test 𝒞(11) == (1, 2, 5, 6)
+ @test 𝒞(12) == (2, 6)
+ @test 𝒞(13) == (1, 3, 5, 7)
+ @test 𝒞(14) == (1, 2, 3, 4, 5, 6, 7, 8)
+ @test 𝒞(15) == (2, 4, 6, 8)
+ @test 𝒞(16) == (3, 7)
+ @test 𝒞(17) == (3, 4, 7, 8)
+ @test 𝒞(18) == (4, 8)
+ @test 𝒞(19) == (5,)
+ @test 𝒞(20) == (5, 6)
+ @test 𝒞(21) == (6,)
+ @test 𝒞(22) == (5, 7)
+ @test 𝒞(23) == (5, 6, 7, 8)
+ @test 𝒞(24) == (6, 8)
+ @test 𝒞(25) == (7,)
+ @test 𝒞(26) == (7, 8)
+ @test 𝒞(27) == (8,)
+ 𝒜 = Adjacency{3}(t)
+ @test 𝒜(1) == (2, 3, 5)
+ @test 𝒜(2) == (1, 4, 6)
+ @test 𝒜(3) == (4, 1, 7)
+ @test 𝒜(4) == (3, 2, 8)
+ @test 𝒜(5) == (6, 7, 1)
+ @test 𝒜(6) == (5, 8, 2)
+ @test 𝒜(7) == (8, 5, 3)
+ @test 𝒜(8) == (7, 6, 4)
+
+ # 2x2x2 (periodic x aperiodic x aperiodic) grid
+ t = GridTopology((2, 2, 2), (true, false, false))
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 9, 10, 21, 22)
+ @test ∂(2) == (2, 1, 12, 13, 24, 25)
+ @test ∂(3) == (3, 4, 10, 11, 27, 28)
+ @test ∂(4) == (4, 3, 13, 14, 30, 31)
+ @test ∂(5) == (5, 6, 15, 16, 22, 23)
+ @test ∂(6) == (6, 5, 18, 19, 25, 26)
+ @test ∂(7) == (7, 8, 16, 17, 28, 29)
+ @test ∂(8) == (8, 7, 19, 20, 31, 32)
+
+ # 2x2x2 (aperiodic x periodic x aperiodic) grid
+ t = GridTopology((2, 2, 2), (false, true, false))
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 13, 14, 21, 22)
+ @test ∂(2) == (2, 3, 15, 16, 24, 25)
+ @test ∂(3) == (4, 5, 14, 13, 27, 28)
+ @test ∂(4) == (5, 6, 16, 15, 30, 31)
+ @test ∂(5) == (7, 8, 17, 18, 22, 23)
+ @test ∂(6) == (8, 9, 19, 20, 25, 26)
+ @test ∂(7) == (10, 11, 18, 17, 28, 29)
+ @test ∂(8) == (11, 12, 20, 19, 31, 32)
+
+ # 2x2x2 (aperiodic x aperiodic x periodic) grid
+ t = GridTopology((2, 2, 2), (false, false, true))
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 13, 14, 25, 26)
+ @test ∂(2) == (2, 3, 16, 17, 27, 28)
+ @test ∂(3) == (4, 5, 14, 15, 29, 30)
+ @test ∂(4) == (5, 6, 17, 18, 31, 32)
+ @test ∂(5) == (7, 8, 19, 20, 26, 25)
+ @test ∂(6) == (8, 9, 22, 23, 28, 27)
+ @test ∂(7) == (10, 11, 20, 21, 30, 29)
+ @test ∂(8) == (11, 12, 23, 24, 32, 31)
+
+ # 2x2x2 (periodic x periodic x aperiodic) grid
+ t = GridTopology((2, 2, 2), (true, true, false))
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 9, 10, 17, 18)
+ @test ∂(2) == (2, 1, 11, 12, 20, 21)
+ @test ∂(3) == (3, 4, 10, 9, 23, 24)
+ @test ∂(4) == (4, 3, 12, 11, 26, 27)
+ @test ∂(5) == (5, 6, 13, 14, 18, 19)
+ @test ∂(6) == (6, 5, 15, 16, 21, 22)
+ @test ∂(7) == (7, 8, 14, 13, 24, 25)
+ @test ∂(8) == (8, 7, 16, 15, 27, 28)
+
+ # 2x2x2 (periodic x aperiodic x periodic) grid
+ t = GridTopology((2, 2, 2), (true, false, true))
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 9, 10, 21, 22)
+ @test ∂(2) == (2, 1, 12, 13, 23, 24)
+ @test ∂(3) == (3, 4, 10, 11, 25, 26)
+ @test ∂(4) == (4, 3, 13, 14, 27, 28)
+ @test ∂(5) == (5, 6, 15, 16, 22, 21)
+ @test ∂(6) == (6, 5, 18, 19, 24, 23)
+ @test ∂(7) == (7, 8, 16, 17, 26, 25)
+ @test ∂(8) == (8, 7, 19, 20, 28, 27)
+
+ # 2x2x2 (aperiodic x periodic x periodic) grid
+ t = GridTopology((2, 2, 2), (false, true, true))
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 13, 14, 21, 22)
+ @test ∂(2) == (2, 3, 15, 16, 23, 24)
+ @test ∂(3) == (4, 5, 14, 13, 25, 26)
+ @test ∂(4) == (5, 6, 16, 15, 27, 28)
+ @test ∂(5) == (7, 8, 17, 18, 22, 21)
+ @test ∂(6) == (8, 9, 19, 20, 24, 23)
+ @test ∂(7) == (10, 11, 18, 17, 26, 25)
+ @test ∂(8) == (11, 12, 20, 19, 28, 27)
+
+ # 2x2x2 (periodic x periodic x periodic) grid
+ t = GridTopology((2, 2, 2), (true, true, true))
+ ∂ = Boundary{3,2}(t)
+ @test ∂(1) == (1, 2, 9, 10, 17, 18)
+ @test ∂(2) == (2, 1, 11, 12, 19, 20)
+ @test ∂(3) == (3, 4, 10, 9, 21, 22)
+ @test ∂(4) == (4, 3, 12, 11, 23, 24)
+ @test ∂(5) == (5, 6, 13, 14, 18, 17)
+ @test ∂(6) == (6, 5, 15, 16, 20, 19)
+ @test ∂(7) == (7, 8, 14, 13, 22, 21)
+ @test ∂(8) == (8, 7, 16, 15, 24, 23)
+
+ # 2x3x2 grid
+ t = GridTopology(2, 3, 2)
+ ∂ = Boundary{3,0}(t)
+ @test ∂(1) == (1, 2, 5, 4, 13, 14, 17, 16)
+ @test ∂(2) == (2, 3, 6, 5, 14, 15, 18, 17)
+ @test ∂(3) == (4, 5, 8, 7, 16, 17, 20, 19)
+ @test ∂(12) == (20, 21, 24, 23, 32, 33, 36, 35)
+
+ # 3x2x2 grid
+ t = GridTopology(3, 2, 2)
+ 𝒜 = Adjacency{3}(t)
+ @test 𝒜(1) == (2, 4, 7)
+ @test 𝒜(2) == (1, 3, 5, 8)
+ @test 𝒜(3) == (2, 6, 9)
+ @test 𝒜(4) == (5, 1, 10)
+ @test 𝒜(5) == (4, 6, 2, 11)
+ @test 𝒜(6) == (5, 3, 12)
+ @test 𝒜(7) == (8, 10, 1)
+ @test 𝒜(8) == (7, 9, 11, 2)
+ @test 𝒜(9) == (8, 12, 3)
+ @test 𝒜(10) == (11, 7, 4)
+ @test 𝒜(11) == (10, 12, 8, 5)
+ @test 𝒜(12) == (11, 9, 6)
+
+ # 3x2x2 (periodic x aperiodic x aperiodic) grid
+ t = GridTopology((3, 2, 2), (true, false, false))
+ 𝒜 = Adjacency{3}(t)
+ @test 𝒜(1) == (3, 2, 4, 7)
+ @test 𝒜(2) == (1, 3, 5, 8)
+ @test 𝒜(3) == (2, 1, 6, 9)
+ @test 𝒜(4) == (6, 5, 1, 10)
+ @test 𝒜(5) == (4, 6, 2, 11)
+ @test 𝒜(6) == (5, 4, 3, 12)
+ @test 𝒜(7) == (9, 8, 10, 1)
+ @test 𝒜(8) == (7, 9, 11, 2)
+ @test 𝒜(9) == (8, 7, 12, 3)
+ @test 𝒜(10) == (12, 11, 7, 4)
+ @test 𝒜(11) == (10, 12, 8, 5)
+ @test 𝒜(12) == (11, 10, 9, 6)
+
+ # invalid relations
+ t = GridTopology(2, 3)
+ @test_throws AssertionError Boundary{3,0}(t)
+ @test_throws AssertionError Coboundary{0,3}(t)
+ @test_throws AssertionError Adjacency{3}(t)
+ @test_throws AssertionError Boundary{0,2}(t)
+ @test_throws AssertionError Coboundary{2,0}(t)
+end
- # invalid relations
- elems = connect.([(1, 2, 3), (4, 3, 2)])
- t = HalfEdgeTopology(elems)
- @test_throws AssertionError Boundary{3,0}(t)
- @test_throws AssertionError Coboundary{0,3}(t)
- @test_throws AssertionError Adjacency{3}(t)
- @test_throws AssertionError Boundary{0,2}(t)
- @test_throws AssertionError Coboundary{2,0}(t)
- end
+@testitem "HalfEdgeTopology" setup = [Setup] begin
+ # 2 triangles
+ elems = connect.([(1, 2, 3), (4, 3, 2)])
+ t = HalfEdgeTopology(elems)
+ ∂ = Boundary{2,0}(t)
+ @test ∂(1) == (2, 3, 1)
+ @test ∂(2) == (3, 2, 4)
+ ∂ = Boundary{2,1}(t)
+ @test ∂(1) == (1, 3, 2)
+ @test ∂(2) == (1, 4, 5)
+ ∂ = Boundary{1,0}(t)
+ @test ∂(1) == (3, 2)
+ @test ∂(2) == (1, 2)
+ @test ∂(3) == (3, 1)
+ @test ∂(4) == (2, 4)
+ @test ∂(5) == (4, 3)
+ 𝒞 = Coboundary{0,1}(t)
+ @test 𝒞(1) == (2, 3)
+ @test 𝒞(2) == (4, 1, 2)
+ @test 𝒞(3) == (3, 1, 5)
+ @test 𝒞(4) == (5, 4)
+ 𝒞 = Coboundary{0,2}(t)
+ @test 𝒞(1) == (1,)
+ @test 𝒞(2) == (2, 1)
+ @test 𝒞(3) == (1, 2)
+ @test 𝒞(4) == (2,)
+ 𝒞 = Coboundary{1,2}(t)
+ @test 𝒞(1) == (2, 1)
+ @test 𝒞(2) == (1,)
+ @test 𝒞(3) == (1,)
+ @test 𝒞(4) == (2,)
+ @test 𝒞(5) == (2,)
+ 𝒜 = Adjacency{0}(t)
+ @test 𝒜(1) == (2, 3)
+ @test 𝒜(2) == (4, 3, 1)
+ @test 𝒜(3) == (1, 2, 4)
+ @test 𝒜(4) == (3, 2)
+
+ # 2 triangles + 2 quadrangles
+ elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
+ t = HalfEdgeTopology(elems)
+ ∂ = Boundary{2,0}(t)
+ @test ∂(1) == (1, 2, 6, 5)
+ @test ∂(2) == (6, 2, 4)
+ @test ∂(3) == (6, 4, 3, 5)
+ @test ∂(4) == (3, 1, 5)
+ ∂ = Boundary{2,1}(t)
+ @test ∂(1) == (1, 3, 5, 6)
+ @test ∂(2) == (3, 9, 4)
+ @test ∂(3) == (4, 7, 8, 5)
+ @test ∂(4) == (2, 6, 8)
+ ∂ = Boundary{1,0}(t)
+ @test ∂(1) == (1, 2)
+ @test ∂(2) == (3, 1)
+ @test ∂(3) == (6, 2)
+ @test ∂(4) == (4, 6)
+ @test ∂(5) == (5, 6)
+ @test ∂(6) == (1, 5)
+ @test ∂(7) == (4, 3)
+ @test ∂(8) == (3, 5)
+ @test ∂(9) == (2, 4)
+ 𝒞 = Coboundary{0,1}(t)
+ @test 𝒞(1) == (1, 6, 2)
+ @test 𝒞(2) == (9, 3, 1)
+ @test 𝒞(3) == (2, 8, 7)
+ @test 𝒞(4) == (7, 4, 9)
+ @test 𝒞(5) == (5, 8, 6)
+ @test 𝒞(6) == (3, 4, 5)
+ 𝒞 = Coboundary{0,2}(t)
+ @test 𝒞(1) == (1, 4)
+ @test 𝒞(2) == (2, 1)
+ @test 𝒞(3) == (4, 3)
+ @test 𝒞(4) == (3, 2)
+ @test 𝒞(5) == (3, 4, 1)
+ @test 𝒞(6) == (2, 3, 1)
+ 𝒞 = Coboundary{1,2}(t)
+ @test 𝒞(1) == (1,)
+ @test 𝒞(2) == (4,)
+ @test 𝒞(3) == (2, 1)
+ @test 𝒞(4) == (2, 3)
+ @test 𝒞(5) == (3, 1)
+ @test 𝒞(6) == (4, 1)
+ @test 𝒞(7) == (3,)
+ @test 𝒞(8) == (3, 4)
+ @test 𝒞(9) == (2,)
+ 𝒜 = Adjacency{0}(t)
+ @test 𝒜(1) == (2, 5, 3)
+ @test 𝒜(2) == (4, 6, 1)
+ @test 𝒜(3) == (1, 5, 4)
+ @test 𝒜(4) == (3, 6, 2)
+ @test 𝒜(5) == (6, 3, 1)
+ @test 𝒜(6) == (2, 4, 5)
+
+ # 2 triangles + 2 quadrangles
+ elems = connect.([(1, 2, 6, 5), (2, 4, 6), (4, 3, 5, 6), (1, 5, 3)])
+ t = HalfEdgeTopology(elems)
+ 𝒜 = Adjacency{2}(t)
+ @test 𝒜(1) == (2, 3, 4)
+ @test 𝒜(2) == (1, 3)
+ @test 𝒜(3) == (2, 4, 1)
+ @test 𝒜(4) == (1, 3)
+
+ # 4 quadrangles in a grid
+ elems = connect.([(1, 2, 5, 4), (2, 3, 6, 5), (4, 5, 8, 7), (5, 6, 9, 8)])
+ t = HalfEdgeTopology(elems)
+ 𝒜 = Adjacency{2}(t)
+ @test 𝒜(1) == (3, 2)
+ @test 𝒜(2) == (1, 4)
+ @test 𝒜(3) == (1, 4)
+ @test 𝒜(4) == (3, 2)
+
+ # invalid relations
+ elems = connect.([(1, 2, 3), (4, 3, 2)])
+ t = HalfEdgeTopology(elems)
+ @test_throws AssertionError Boundary{3,0}(t)
+ @test_throws AssertionError Coboundary{0,3}(t)
+ @test_throws AssertionError Adjacency{3}(t)
+ @test_throws AssertionError Boundary{0,2}(t)
+ @test_throws AssertionError Coboundary{2,0}(t)
+end
- @testset "SimpleTopology" begin
- elems = connect.([(1, 2, 3), (4, 3, 2)])
- t = SimpleTopology(elems)
- ∂ = Boundary{2,0}(t)
- @test ∂(1) == [1, 2, 3]
- @test ∂(2) == [4, 3, 2]
- end
+@testitem "SimpleTopology" setup = [Setup] begin
+ elems = connect.([(1, 2, 3), (4, 3, 2)])
+ t = SimpleTopology(elems)
+ ∂ = Boundary{2,0}(t)
+ @test ∂(1) == (1, 2, 3)
+ @test ∂(2) == (4, 3, 2)
end
diff --git a/test/trajecs.jl b/test/trajecs.jl
index 4e883cfaf..f71cca970 100644
--- a/test/trajecs.jl
+++ b/test/trajecs.jl
@@ -1,27 +1,35 @@
-@testset "Trajectories" begin
- @testset "CylindricalTrajectory" begin
- s = Segment(P3(0, 0, 0), P3(0, 0, 1))
- c = [s(t) for t in range(T(0), stop=T(1), length=10)]
- t = CylindricalTrajectory(c)
- @test eltype(t) <: Cylinder
- @test nelements(t) == 10
- @test radius(t) == T(1)
- @test topology(t) == GridTopology(10)
+@testitem "CylindricalTrajectory" setup = [Setup] begin
+ s = Segment(cart(0, 0, 0), cart(0, 0, 1))
+ c = [s(t) for t in range(T(0), stop=T(1), length=10)]
+ t = CylindricalTrajectory(c)
+ @test crs(t) <: Cartesian{NoDatum}
+ @test Meshes.lentype(t) == ℳ
+ @test eltype(t) <: Cylinder
+ @test nelements(t) == 10
+ @test radius(t) == T(1) * u"m"
+ @test topology(t) == GridTopology(10)
+ @test centroid(t, 1) == cart(0, 0, 0)
+ @test centroid(t, 10) == cart(0, 0, 1)
- b = BezierCurve([P3(0, 0, 0), P3(3, 3, 0), P3(3, 0, 7)])
- c = [b(t) for t in range(T(0), stop=T(1), length=20)]
- t = CylindricalTrajectory(c, T(2))
- @test eltype(t) <: Cylinder
- @test nelements(t) == 20
- @test radius(t) == T(2)
- @test topology(t) == GridTopology(20)
+ b = BezierCurve([cart(0, 0, 0), cart(3, 3, 0), cart(3, 0, 7)])
+ c = [b(t) for t in range(T(0), stop=T(1), length=20)]
+ t = CylindricalTrajectory(c, T(2))
+ @test crs(t) <: Cartesian{NoDatum}
+ @test Meshes.lentype(t) == ℳ
+ @test eltype(t) <: Cylinder
+ @test nelements(t) == 20
+ @test radius(t) == T(2) * u"m"
+ @test topology(t) == GridTopology(20)
+ @test centroid(t, 1) == cart(0, 0, 0)
+ @test centroid(t, 20) == cart(3, 0, 7)
- # trajectory with single cylinder
- t = CylindricalTrajectory([P3(0, 0, 0)], T(1))
- @test eltype(t) <: Cylinder
- @test nelements(t) == 1
- @test radius(t) == T(1)
- @test topology(t) == GridTopology(1)
- @test t[1] == Cylinder(P3(0, 0, -0.5), P3(0, 0, 0.5), T(1))
- end
+ # trajectory with single cylinder
+ t = CylindricalTrajectory([cart(0, 0, 0)], T(1))
+ @test crs(t) <: Cartesian{NoDatum}
+ @test Meshes.lentype(t) == ℳ
+ @test eltype(t) <: Cylinder
+ @test nelements(t) == 1
+ @test radius(t) == T(1) * u"m"
+ @test topology(t) == GridTopology(1)
+ @test t[1] == Cylinder(cart(0, 0, -0.5), cart(0, 0, 0.5), T(1))
end
diff --git a/test/transformedgeoms.jl b/test/transformedgeoms.jl
new file mode 100644
index 000000000..ad1fd027f
--- /dev/null
+++ b/test/transformedgeoms.jl
@@ -0,0 +1,93 @@
+@testitem "TransformedGeometry" setup = [Setup] begin
+ b = Box(cart(0, 0), cart(1, 1))
+ t = Translate(T(1), T(2))
+ tb = TransformedGeometry(b, t)
+ @test parent(tb) == b
+ @test Meshes.transform(tb) == t
+ t2 = Scale(T(2), T(3))
+ tb2 = TransformedGeometry(tb, t2)
+ @test Meshes.transform(tb2) == (t → t2)
+ @test paramdim(tb) == paramdim(b)
+ @test tb == tb
+ @test tb ≈ tb
+ @test tb(T(0.5), T(0.5)) == t(b(T(0.5), T(0.5)))
+ @test centroid(tb) == t(centroid(b))
+ @test discretize(tb) == t(discretize(b))
+ t3 = Scale(T(2), T(2))
+ tb3 = TransformedGeometry(b, t3)
+ @test measure(tb3) == 4 * measure(b)
+ equaltest(tb)
+ isapproxtest(tb)
+
+ b = Ball(latlon(0, 0), T(1))
+ t = Proj(Cartesian)
+ tb = TransformedGeometry(b, t)
+ @test paramdim(tb) == paramdim(b)
+ @test centroid(tb) == t(centroid(b))
+
+ s = Sphere(latlon(0, 0), T(1))
+ t = Proj(Cartesian)
+ ts = TransformedGeometry(s, t)
+ @test paramdim(ts) == paramdim(s)
+ @test centroid(ts) == t(centroid(s))
+
+ s = Segment(cart(0, 0), cart(1, 1))
+ t = Translate(T(1), T(2))
+ ts = TransformedGeometry(s, t)
+ @test vertex(ts, 1) == t(vertex(s, 1))
+ @test vertices(ts) == t.(vertices(s))
+ @test nvertices(ts) == nvertices(s)
+ equaltest(ts)
+ isapproxtest(ts)
+
+ p = PolyArea(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ t = Translate(T(1), T(2))
+ tp = TransformedGeometry(p, t)
+ @test vertex(tp, 1) == t(vertex(p, 1))
+ @test vertices(tp) == t.(vertices(p))
+ @test nvertices(tp) == nvertices(p)
+ @test rings(tp) == t.(rings(p))
+ p2 = PolyArea(cart(0, 0), cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ tp2 = TransformedGeometry(p2, t)
+ @test unique(tp2) == tp
+ equaltest(tp)
+ isapproxtest(tp)
+
+ # has distorted boundary
+ b = Box(cart(0, 0), cart(1, 1))
+ t = Translate(T(1), T(2))
+ tb = TransformedGeometry(b, t)
+ @test !Meshes.hasdistortedboundary(tb)
+ b = Box(latlon(0, 0), latlon(1, 1))
+ t = Proj(Mercator)
+ tb = TransformedGeometry(b, t)
+ @test Meshes.hasdistortedboundary(tb)
+ b = Box(merc(0, 0), merc(1, 1))
+ t = Proj(LatLon)
+ tb = TransformedGeometry(b, t)
+ @test Meshes.hasdistortedboundary(tb)
+ b = Box(latlon(0, 0), latlon(1, 1))
+ t = Morphological(c -> Cartesian(ustrip(c.lon), ustrip(c.lat)))
+ tb = TransformedGeometry(b, t)
+ @test Meshes.hasdistortedboundary(tb)
+
+ # boundary
+ b = Box(cart(0, 0), cart(1, 1))
+ t = Translate(T(1), T(2))
+ tb = TransformedGeometry(b, t)
+ @test boundary(tb) == t(boundary(b))
+ b = Box(latlon(0, 0), latlon(1, 1))
+ t = Proj(Mercator)
+ tb = TransformedGeometry(b, t)
+ @test boundary(tb) == TransformedGeometry(boundary(b), t)
+
+ b = Box(cart(0, 0), cart(1, 1))
+ t = Translate(T(1), T(2))
+ tb = TransformedGeometry(b, t)
+ @test sprint(show, tb) ==
+ "TransformedBox(geometry: Box(min: (x: 0.0 m, y: 0.0 m), max: (x: 1.0 m, y: 1.0 m)), transform: Translate(offsets: (1.0 m, 2.0 m)))"
+ @test sprint(show, MIME"text/plain"(), tb) == """
+ TransformedBox
+ ├─ geometry: Box(min: (x: 0.0 m, y: 0.0 m), max: (x: 1.0 m, y: 1.0 m))
+ └─ transform: Translate(offsets: (1.0 m, 2.0 m))"""
+end
diff --git a/test/transforms.jl b/test/transforms.jl
index 04ba70ce2..c91c1a024 100644
--- a/test/transforms.jl
+++ b/test/transforms.jl
@@ -1,1008 +1,2231 @@
-@testset "Transforms" begin
- @testset "Rotate" begin
- @test isaffine(Rotate)
- @test TB.isrevertible(Rotate)
- @test TB.isinvertible(Rotate)
- @test TB.inverse(Rotate(Angle2d(T(π / 2)))) == Rotate(Angle2d(-T(π / 2)))
- rot = Angle2d(T(π / 2))
- f = Rotate(rot)
- @test TB.parameters(f) == (; rot)
-
- # ----
- # VEC
- # ----
-
- f = Rotate(Angle2d(T(π / 2)))
- v = V2(1, 0)
- r, c = TB.apply(f, v)
- @test r ≈ V2(0, 1)
- @test TB.revert(f, r, c) ≈ v
-
- # ------
- # POINT
- # ------
-
- f = Rotate(Angle2d(T(π / 2)))
- g = P2(1, 0)
- r, c = TB.apply(f, g)
- @test r ≈ P2(0, 1)
- @test TB.revert(f, r, c) ≈ g
-
- # --------
- # SEGMENT
- # --------
-
- f = Rotate(Angle2d(T(π / 2)))
- g = Segment(P2(0, 0), P2(1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Segment(P2(0, 0), P2(0, 1))
- @test TB.revert(f, r, c) ≈ g
-
- # ----
- # BOX
- # ----
-
- f = Rotate(Angle2d(T(π / 2)))
- g = Box(P2(0, 0), P2(1, 1))
- r, c = TB.apply(f, g)
- @test r isa Quadrangle
- @test r ≈ Quadrangle(P2(0, 0), P2(0, 1), P2(-1, 1), P2(-1, 0))
- q = TB.revert(f, r, c)
- @test q isa Quadrangle
- @test q ≈ convert(Quadrangle, g)
-
- f = Rotate(V3(1, 0, 0), V3(0, 1, 0))
- g = Box(P3(0, 0, 0), P3(1, 1, 1))
- r, c = TB.apply(f, g)
- @test r isa Hexahedron
- @test r ≈ Hexahedron(
- P3(0, 0, 0),
- P3(0, 1, 0),
- P3(-1, 1, 0),
- P3(-1, 0, 0),
- P3(0, 0, 1),
- P3(0, 1, 1),
- P3(-1, 1, 1),
- P3(-1, 0, 1)
- )
- h = TB.revert(f, r, c)
- @test h isa Hexahedron
- @test h ≈ convert(Hexahedron, g)
-
- # ----------
- # ROPE/RING
- # ----------
-
- f = Rotate(Angle2d(T(π / 2)))
- g = Rope(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Rope(P2(0, 0), P2(0, 1), P2(-1, 1), P2(-1, 0))
- @test TB.revert(f, r, c) ≈ g
-
- f = Rotate(Angle2d(T(π / 2)))
- g = Ring(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Ring(P2(0, 0), P2(0, 1), P2(-1, 1), P2(-1, 0))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # TRIANGLE
- # ---------
-
- f = Rotate(AngleAxis(T(π / 2), T(0), T(0), T(1)))
- g = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Triangle(P3(0, 0, 0), P3(0, 1, 0), P3(-1, 0, 0))
- @test TB.revert(f, r, c) ≈ g
-
- f = Rotate(V3(0, 0, 1), V3(1, 0, 0))
- g = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Triangle(P3(0, 0, 0), P3(0, 0, -1), P3(0, 1, 0))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # POLYAREA
- # ---------
-
- f = Rotate(Angle2d(T(π / 2)))
- p = PolyArea(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- r, c = TB.apply(f, p)
- @test r ≈ PolyArea(P2(0, 0), P2(0, 1), P2(-1, 1), P2(-1, 0))
- @test TB.revert(f, r, c) ≈ p
-
- # ----------
- # MULTIGEOM
- # ----------
-
- f = Rotate(Angle2d(T(π / 2)))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- g = Multi([t, t])
- r, c = TB.apply(f, g)
- @test r ≈ Multi([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ g
-
- # ------
- # PLANE
- # ------
-
- f = Rotate(V3(0, 0, 1), V3(1, 0, 0))
- g = Plane(P3(0, 0, 0), V3(0, 0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Plane(P3(0, 0, 0), V3(1, 0, 0))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # CYLINDER
- # ---------
-
- f = Rotate(V3(0, 0, 1), V3(1, 0, 0))
- g = Cylinder(T(1))
- r, c = TB.apply(f, g)
- @test r ≈ Cylinder(P3(0, 0, 0), P3(1, 0, 0))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # POINTSET
- # ---------
-
- f = Rotate(Angle2d(T(π / 2)))
- d = PointSet([P2(0, 0), P2(1, 0), P2(1, 1)])
- r, c = TB.apply(f, d)
- @test r ≈ PointSet([P2(0, 0), P2(0, 1), P2(-1, 1)])
- @test TB.revert(f, r, c) ≈ d
-
- # ------------
- # GEOMETRYSET
- # ------------
-
- f = Rotate(Angle2d(T(π / 2)))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- d = GeometrySet([t, t])
- r, c = TB.apply(f, d)
- @test r ≈ GeometrySet([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ d
- d = [t, t]
- r, c = TB.apply(f, d)
- @test all(r .≈ [f(t), f(t)])
- @test all(TB.revert(f, r, c) .≈ d)
-
- # --------------
- # CARTESIANGRID
- # --------------
-
- f = Rotate(Angle2d(T(π / 2)))
- d = CartesianGrid{T}(10, 10)
- r, c = TB.apply(f, d)
- @test r isa TransformedMesh
- @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
- @test TB.revert(f, r, c) ≈ d
-
- # -----------
- # SIMPLEMESH
- # -----------
-
- f = Rotate(Angle2d(T(π / 2)))
- p = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- d = SimpleMesh(p, c)
- r, c = TB.apply(f, d)
- @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
- @test TB.revert(f, r, c) ≈ d
-
- # ---------
- # FALLBACK
- # ---------
-
- f = Rotate(T(π / 2))
- v = V2(1, 0)
- r, c = TB.apply(f, v)
- @test r ≈ V2(0, 1)
- @test TB.revert(f, r, c) ≈ v
- end
-
- @testset "Translate" begin
- @test isaffine(Translate)
- @test TB.isrevertible(Translate)
- @test TB.isinvertible(Translate)
- @test TB.inverse(Translate(T(1), T(2))) == Translate(T(-1), T(-2))
- offsets = (T(1), T(2))
- f = Translate(offsets)
- @test TB.parameters(f) == (; offsets)
-
- # ----
- # VEC
- # ----
-
- f = Translate(T(1), T(1))
- v = V2(1, 0)
- r, c = TB.apply(f, v)
- @test r ≈ V2(1, 0)
- @test TB.revert(f, r, c) ≈ v
-
- # ------
- # POINT
- # ------
-
- f = Translate(T(1), T(1))
- g = P2(1, 0)
- r, c = TB.apply(f, g)
- @test r ≈ P2(2, 1)
- @test TB.revert(f, r, c) ≈ g
-
- # --------
- # SEGMENT
- # --------
-
- f = Translate(T(1), T(1))
- g = Segment(P2(0, 0), P2(1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Segment(P2(1, 1), P2(2, 1))
- @test TB.revert(f, r, c) ≈ g
-
- # ----
- # BOX
- # ----
-
- f = Translate(T(1), T(1))
- g = Box(P2(0, 0), P2(1, 1))
- r, c = TB.apply(f, g)
- @test r isa Box
- @test r ≈ Box(P2(1, 1), P2(2, 2))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # TRIANGLE
- # ---------
-
- f = Translate(T(1), T(2), T(3))
- g = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Triangle(P3(1, 2, 3), P3(2, 2, 3), P3(1, 3, 4))
- @test TB.revert(f, r, c) ≈ g
-
- # ----------
- # MULTIGEOM
- # ----------
-
- f = Translate(T(1), T(1))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- g = Multi([t, t])
- r, c = TB.apply(f, g)
- @test r ≈ Multi([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ g
-
- # ------
- # PLANE
- # ------
-
- f = Translate(T(0), T(0), T(1))
- g = Plane(P3(0, 0, 0), V3(0, 0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Plane(P3(0, 0, 1), V3(0, 0, 1))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # CYLINDER
- # ---------
-
- f = Translate(T(0), T(0), T(1))
- g = Cylinder(T(1))
- r, c = TB.apply(f, g)
- @test r ≈ Cylinder(P3(0, 0, 1), P3(0, 0, 2))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # POINTSET
- # ---------
-
- f = Translate(T(1), T(1))
- d = PointSet([P2(0, 0), P2(1, 0), P2(1, 1)])
- r, c = TB.apply(f, d)
- @test r ≈ PointSet([P2(1, 1), P2(2, 1), P2(2, 2)])
- @test TB.revert(f, r, c) ≈ d
-
- # ------------
- # GEOMETRYSET
- # ------------
-
- f = Translate(T(1), T(1))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- d = GeometrySet([t, t])
- r, c = TB.apply(f, d)
- @test r ≈ GeometrySet([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ d
- d = [t, t]
- r, c = TB.apply(f, d)
- @test all(r .≈ [f(t), f(t)])
- @test all(TB.revert(f, r, c) .≈ d)
-
- # --------------
- # CARTESIANGRID
- # --------------
-
- f = Translate(T(1), T(1))
- d = CartesianGrid{T}(10, 10)
- r, c = TB.apply(f, d)
- @test r isa CartesianGrid
- @test r ≈ CartesianGrid(P2(1, 1), P2(11, 11), dims=(10, 10))
- @test TB.revert(f, r, c) ≈ d
-
- # ----------------
- # RECTILINEARGRID
- # ----------------
-
- f = Translate(T(1), T(1))
- d = RectilinearGrid(T.(0:10), T.(0:10))
- r, c = TB.apply(f, d)
- @test r isa RectilinearGrid
- @test r ≈ RectilinearGrid(T.(1:11), T.(1:11))
- @test TB.revert(f, r, c) ≈ d
-
- # ---------------
- # STRUCTUREDGRID
- # ---------------
-
- f = Translate(T(1), T(1))
- d = StructuredGrid(repeat(T.(0:10), 1, 11), repeat(T.(0:10)', 11, 1))
- r, c = TB.apply(f, d)
- @test r isa StructuredGrid
- @test r ≈ StructuredGrid(repeat(T.(1:11), 1, 11), repeat(T.(1:11)', 11, 1))
- @test TB.revert(f, r, c) ≈ d
-
- # -----------
- # SIMPLEMESH
- # -----------
-
- f = Translate(T(1), T(1))
- p = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- d = SimpleMesh(p, c)
- r, c = TB.apply(f, d)
- @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
- @test TB.revert(f, r, c) ≈ d
- end
-
- @testset "Affine" begin
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- @test isaffine(f)
- @test TB.isrevertible(f)
- @test TB.isinvertible(f)
- @test TB.inverse(f) == Affine(Angle2d(-T(π / 2)), Angle2d(-T(π / 2)) * -T[1, 1])
- f = Affine(T[6 3; 10 5], T[1, 1])
- @test !TB.isrevertible(f)
- @test !TB.isinvertible(f)
- A, b = Angle2d(T(π / 2)), T[1, 1]
- f = Affine(A, b)
- @test TB.parameters(f) == (; A, b)
-
- # ----
- # VEC
- # ----
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- v = V2(1, 0)
- r, c = TB.apply(f, v)
- @test r ≈ V2(0, 1)
- @test TB.revert(f, r, c) ≈ v
-
- # ------
- # POINT
- # ------
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- g = P2(1, 0)
- r, c = TB.apply(f, g)
- @test r ≈ P2(1, 2)
- @test TB.revert(f, r, c) ≈ g
-
- # --------
- # SEGMENT
- # --------
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- g = Segment(P2(0, 0), P2(1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Segment(P2(1, 1), P2(1, 2))
- @test TB.revert(f, r, c) ≈ g
-
- # ----
- # BOX
- # ----
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- g = Box(P2(0, 0), P2(1, 1))
- r, c = TB.apply(f, g)
- @test r isa Quadrangle
- @test r ≈ Quadrangle(P2(1, 1), P2(1, 2), P2(0, 2), P2(0, 1))
- q = TB.revert(f, r, c)
- @test q isa Quadrangle
- @test q ≈ convert(Quadrangle, g)
-
- f = Affine(rotation_between(V3(0, 0, 1), V3(1, 0, 0)), T[1, 2, 3])
- g = Box(P3(0, 0, 0), P3(1, 1, 1))
- r, c = TB.apply(f, g)
- @test r isa Hexahedron
- @test r ≈ Hexahedron(
- P3(1, 2, 3),
- P3(1, 2, 2),
- P3(1, 3, 2),
- P3(1, 3, 3),
- P3(2, 2, 3),
- P3(2, 2, 2),
- P3(2, 3, 2),
- P3(2, 3, 3)
- )
- h = TB.revert(f, r, c)
- @test h isa Hexahedron
- @test h ≈ convert(Hexahedron, g)
-
- # ---------
- # TRIANGLE
- # ---------
-
- f = Affine(rotation_between(V3(0, 0, 1), V3(1, 0, 0)), T[1, 2, 3])
- g = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Triangle(P3(1, 2, 3), P3(1, 2, 2), P3(2, 3, 3))
- @test TB.revert(f, r, c) ≈ g
-
- # ----------
- # MULTIGEOM
- # ----------
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- g = Multi([t, t])
- r, c = TB.apply(f, g)
- @test r ≈ Multi([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ g
-
- # ------
- # PLANE
- # ------
-
- f = Affine(rotation_between(V3(0, 0, 1), V3(1, 0, 0)), T[0, 0, 1])
- g = Plane(P3(0, 0, 0), V3(0, 0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Plane(P3(0, 0, 1), V3(1, 0, 0))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # CYLINDER
- # ---------
-
- f = Affine(rotation_between(V3(0, 0, 1), V3(1, 0, 0)), T[0, 0, 1])
- g = Cylinder(T(1))
- r, c = TB.apply(f, g)
- @test r ≈ Cylinder(P3(0, 0, 1), P3(1, 0, 1))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # POINTSET
- # ---------
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- d = PointSet([P2(0, 0), P2(1, 0), P2(1, 1)])
- r, c = TB.apply(f, d)
- @test r ≈ PointSet([P2(1, 1), P2(1, 2), P2(0, 2)])
- @test TB.revert(f, r, c) ≈ d
-
- # ------------
- # GEOMETRYSET
- # ------------
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- d = GeometrySet([t, t])
- r, c = TB.apply(f, d)
- @test r ≈ GeometrySet([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ d
- d = [t, t]
- r, c = TB.apply(f, d)
- @test all(r .≈ [f(t), f(t)])
- @test all(TB.revert(f, r, c) .≈ d)
-
- # ----------
- # TRANSFORM
- # ----------
-
- f = Affine(Angle2d(T(π / 2)), T[1, 1])
- s = Rotate(T(π / 2)) → Translate(T(1), T(1))
- v = V2(1, 0)
- g1 = P2(1, 0)
- g2 = Segment(P2(0, 0), P2(1, 0))
- g3 = Box(P2(0, 0), P2(1, 1))
- @test f(v) ≈ s(v)
- @test f(g1) ≈ s(g1)
- @test f(g2) ≈ s(g2)
- @test f(g3) ≈ s(g3)
-
- # ------------
- # CONSTRUCTOR
- # ------------
-
- # conversion to SArray
- f = Affine(T[0 -1; 1 0], SVector{2}(T[1, 1]))
- @test f.A isa SMatrix
- f = Affine(SMatrix{2,2}(T[0 -1; 1 0]), T[1, 1])
- @test f.b isa SVector
- f = Affine(T[0 -1; 1 0], T[1, 1])
- @test f.A isa SMatrix
- @test f.b isa SVector
-
- # error: A must be a square matrix
- @test_throws ArgumentError Affine(T[1 1; 2 2; 3 3], T[1, 2])
- # error: A and b must have the same dimension
- @test_throws ArgumentError Affine(T[1 1; 2 2], T[1, 2, 3])
- end
-
- @testset "Scale" begin
- @test isaffine(Scale)
- @test TB.isrevertible(Scale)
- @test TB.isinvertible(Scale)
- @test TB.inverse(Scale(T(1), T(2))) == Scale(T(1), T(1 / 2))
- factors = (T(1), T(2))
- f = Scale(factors)
- @test TB.parameters(f) == (; factors)
-
- # ----
- # VEC
- # ----
-
- f = Scale(T(1), T(2))
- v = V2(1, 1)
- r, c = TB.apply(f, v)
- @test r ≈ V2(1, 2)
- @test TB.revert(f, r, c) ≈ v
-
- # ------
- # POINT
- # ------
-
- f = Scale(T(1), T(2))
- g = P2(1, 1)
- r, c = TB.apply(f, g)
- @test r ≈ P2(1, 2)
- @test TB.revert(f, r, c) ≈ g
-
- # --------
- # SEGMENT
- # --------
-
- f = Scale(T(1), T(2))
- g = Segment(P2(0, 0), P2(1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Segment(P2(0, 0), P2(1, 0))
- @test TB.revert(f, r, c) ≈ g
-
- f = Scale(T(2), T(1))
- g = Segment(P2(0, 0), P2(1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Segment(P2(0, 0), P2(2, 0))
- @test TB.revert(f, r, c) ≈ g
-
- # ----
- # BOX
- # ----
-
- f = Scale(T(1), T(2))
- g = Box(P2(0, 0), P2(1, 1))
- r, c = TB.apply(f, g)
- @test r isa Box
- @test r ≈ Box(P2(0, 0), P2(1, 2))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # TRIANGLE
- # ---------
-
- f = Scale(T(1), T(2), T(3))
- g = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 2, 3))
- @test TB.revert(f, r, c) ≈ g
-
- # ----------
- # MULTIGEOM
- # ----------
-
- f = Scale(T(1), T(2))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- g = Multi([t, t])
- r, c = TB.apply(f, g)
- @test r ≈ Multi([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ g
-
- # ------
- # PLANE
- # ------
-
- f = Scale(T(1), T(1), T(2))
- g = Plane(P3(1, 1, 1), V3(0, 0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Plane(P3(1, 1, 2), V3(0, 0, 1))
- @test TB.revert(f, r, c) ≈ g
-
- f = Scale(T(2), T(1), T(1))
- g = Plane(P3(1, 1, 1), V3(0, 0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ g
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # CYLINDER
- # ---------
-
- f = Scale(T(1), T(1), T(2))
- g = Cylinder(T(1))
- r, c = TB.apply(f, g)
- @test r ≈ Cylinder(P3(0, 0, 0), P3(0, 0, 2))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # POINTSET
- # ---------
-
- f = Scale(T(1), T(2))
- d = PointSet([P2(0, 0), P2(1, 0), P2(1, 1)])
- r, c = TB.apply(f, d)
- @test r ≈ PointSet([P2(0, 0), P2(1, 0), P2(1, 2)])
- @test TB.revert(f, r, c) ≈ d
-
- # ------------
- # GEOMETRYSET
- # ------------
-
- f = Scale(T(1), T(2))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- d = GeometrySet([t, t])
- r, c = TB.apply(f, d)
- @test r ≈ GeometrySet([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ d
- d = [t, t]
- r, c = TB.apply(f, d)
- @test all(r .≈ [f(t), f(t)])
- @test all(TB.revert(f, r, c) .≈ d)
-
- # --------------
- # CARTESIANGRID
- # --------------
-
- f = Scale(T(1), T(2))
- d = CartesianGrid(P2(1, 1), P2(11, 11), dims=(10, 10))
- r, c = TB.apply(f, d)
- @test r isa CartesianGrid
- @test r ≈ CartesianGrid(P2(1, 2), P2(11, 22), dims=(10, 10))
- @test TB.revert(f, r, c) ≈ d
-
- # -----------
- # SIMPLEMESH
- # -----------
-
- f = Scale(T(1), T(2))
- p = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- d = SimpleMesh(p, c)
- r, c = TB.apply(f, d)
- @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
- @test TB.revert(f, r, c) ≈ d
- end
-
- @testset "Stretch" begin
- @test !isaffine(Stretch)
- @test TB.isrevertible(Stretch)
- @test TB.isinvertible(Stretch)
- @test TB.inverse(Stretch(T(1), T(2))) == Stretch(T(1), T(1 / 2))
- factors = (T(1), T(2))
- f = Stretch(factors)
- @test TB.parameters(f) == (; factors)
-
- # ----
- # VEC
- # ----
-
- f = Stretch(T(1), T(2))
- v = V2(1, 1)
- r, c = TB.apply(f, v)
- @test r ≈ V2(1, 2)
- @test TB.revert(f, r, c) ≈ v
-
- # ------
- # POINT
- # ------
-
- f = Stretch(T(1), T(2))
- g = P2(1, 1)
- r, c = TB.apply(f, g)
- @test r ≈ P2(1, 1)
- @test TB.revert(f, r, c) ≈ g
-
- # --------
- # SEGMENT
- # --------
-
- f = Stretch(T(1), T(2))
- g = Segment(P2(0, 0), P2(1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Segment(P2(0, 0), P2(1, 0))
- @test TB.revert(f, r, c) ≈ g
-
- f = Stretch(T(2), T(1))
- g = Segment(P2(0, 0), P2(1, 0))
- r, c = TB.apply(f, g)
- @test r ≈ Segment(P2(-0.5, 0), P2(1.5, 0))
- @test TB.revert(f, r, c) ≈ g
-
- # ----
- # BOX
- # ----
-
- f = Stretch(T(1), T(2))
- g = Box(P2(0, 0), P2(1, 1))
- r, c = TB.apply(f, g)
- @test r isa Box
- @test r ≈ Box(P2(0, -0.5), P2(1, 1.5))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # TRIANGLE
- # ---------
-
- f = Stretch(T(1), T(2), T(2))
- g = Triangle(P3(0, 0, 0), P3(1, 0, 0), P3(0, 1, 1))
- r, c = TB.apply(f, g)
- @test r ≈ Triangle(P3(0, -1 / 3, -1 / 3), P3(1, -1 / 3, -1 / 3), P3(0, 10 / 6, 10 / 6))
- @test TB.revert(f, r, c) ≈ g
-
- # ----------
- # MULTIGEOM
- # ----------
-
- f = Stretch(T(1), T(2))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- g = Multi([t, t])
- r, c = TB.apply(f, g)
- @test r ≈ Multi([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ g
-
- # ------
- # PLANE
- # ------
-
- f = Stretch(T(1), T(1), T(2))
- g = Plane(P3(1, 1, 1), V3(0, 0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ g
- @test TB.revert(f, r, c) ≈ g
-
- f = Stretch(T(2), T(1), T(1))
- g = Plane(P3(1, 1, 1), V3(0, 0, 1))
- r, c = TB.apply(f, g)
- @test r ≈ g
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # CYLINDER
- # ---------
-
- f = Stretch(T(1), T(1), T(2))
- g = Cylinder(T(1))
- r, c = TB.apply(f, g)
- @test r ≈ Cylinder(P3(0, 0, -0.5), P3(0, 0, 1.5))
- @test TB.revert(f, r, c) ≈ g
-
- # ---------
- # POINTSET
- # ---------
-
- f = Stretch(T(1), T(2))
- d = PointSet([P2(0, 0), P2(1, 0), P2(1, 1)])
- r, c = TB.apply(f, d)
- @test r ≈ PointSet([P2(0, -1 / 3), P2(1, -1 / 3), P2(1, 10 / 6)])
- @test TB.revert(f, r, c) ≈ d
-
- # ------------
- # GEOMETRYSET
- # ------------
-
- f = Stretch(T(1), T(2))
- t = Triangle(P2(0, 0), P2(1, 0), P2(1, 1))
- d = GeometrySet([t, t])
- r, c = TB.apply(f, d)
- @test r ≈ GeometrySet([f(t), f(t)])
- @test TB.revert(f, r, c) ≈ d
- d = [t, t]
- r, c = TB.apply(f, d)
- @test all(r .≈ [f(t), f(t)])
- @test all(TB.revert(f, r, c) .≈ d)
-
- # --------------
- # CARTESIANGRID
- # --------------
-
- f = Stretch(T(1), T(2))
- d = CartesianGrid(P2(1, 1), P2(11, 11), dims=(10, 10))
- r, c = TB.apply(f, d)
- @test r isa CartesianGrid
- @test r ≈ CartesianGrid(P2(1, -4), P2(11, 16), dims=(10, 10))
- @test TB.revert(f, r, c) ≈ d
-
- # -----------
- # SIMPLEMESH
- # -----------
-
- f = Stretch(T(1), T(2))
- p = P2[(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]
- c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
- d = SimpleMesh(p, c)
- r, c = TB.apply(f, d)
- @test r ≈ SimpleMesh(f(vertices(d)), topology(d))
- @test TB.revert(f, r, c) ≈ d
- end
-
- @testset "StdCoords" begin
- @test !isaffine(StdCoords)
- @test TB.isrevertible(StdCoords)
-
- # ---------
- # POINTSET
- # ---------
-
- f = StdCoords()
- d = view(PointSet(rand(P2, 100)), 1:50)
- r, c = TB.apply(f, d)
- @test all(sides(boundingbox(r)) .≤ T(1))
- @test TB.revert(f, r, c) ≈ d
- r2 = TB.reapply(f, d, c)
- @test r == r2
-
- # --------------
- # CARTESIANGRID
- # --------------
-
- f = StdCoords()
- d = CartesianGrid(P2(1, 1), P2(11, 11), dims=(10, 10))
- r, c = TB.apply(f, d)
- @test r isa CartesianGrid
- @test r ≈ CartesianGrid(P2(-0.5, -0.5), P2(0.5, 0.5), dims=(10, 10))
- @test TB.revert(f, r, c) ≈ d
-
- f = StdCoords()
- d = CartesianGrid{T}(10, 20)
- r, c = TB.apply(f, d)
- @test r ≈ CartesianGrid(P2(-0.5, -0.5), P2(0.5, 0.5), dims=(10, 20))
- @test TB.revert(f, r, c) ≈ d
- r2 = TB.reapply(f, d, c)
- @test r == r2
- end
-
- @testset "Repair{0}" begin
- @test !isaffine(Repair)
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 0), (1, 1), (0, 1), (0, 1)])
- rpoly = poly |> Repair{0}()
- @test nvertices(rpoly) == 4
- @test vertices(rpoly) == P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- end
-
- @testset "Repair{1}" begin
- # a tetrahedron with an unused vertex
- points = P3[(0, 0, 0), (0, 0, 1), (5, 5, 5), (0, 1, 0), (1, 0, 0)]
- connec = connect.([(1, 2, 4), (1, 2, 5), (1, 4, 5), (2, 4, 5)])
- mesh = SimpleMesh(points, connec)
- rmesh = mesh |> Repair{1}()
- @test nvertices(rmesh) == nvertices(mesh) - 1
- @test nelements(rmesh) == nelements(mesh)
- @test P3(5, 5, 5) ∉ vertices(rmesh)
- end
-
- @testset "Repair{2}" begin end
-
- @testset "Repair{3}" begin end
-
- @testset "Repair{4}" begin end
-
- @testset "Repair{5}" begin end
-
- @testset "Repair{6}" begin end
-
- @testset "Repair{7}" begin
- # mesh with inconsistent orientation
- points = rand(P3, 6)
- connec = connect.([(1, 2, 3), (3, 4, 2), (4, 3, 5), (6, 3, 1)])
- mesh = SimpleMesh(points, connec)
- rmesh = mesh |> Repair{7}()
- topo = topology(mesh)
- rtopo = topology(rmesh)
- e = collect(elements(topo))
- n = collect(elements(rtopo))
- @test n[1] == e[1]
- @test n[2] != e[2]
- @test n[3] != e[3]
- @test n[4] != e[4]
- end
-
- @testset "Repair{8}" begin
- poly =
- PolyArea(P2[(0.0, 0.0), (0.5, -0.5), (1.0, 0.0), (1.5, 0.5), (1.0, 1.0), (0.5, 1.5), (0.0, 1.0), (-0.5, 0.5)])
- rpoly = poly |> Repair{8}()
- @test nvertices(rpoly) == 4
- @test vertices(rpoly) == P2[(0.5, -0.5), (1.5, 0.5), (0.5, 1.5), (-0.5, 0.5)]
-
- # degenerate triangle with repeated vertices
- poly = PolyArea(P2[(0, 0), (1, 1), (1, 1)])
- rpoly = poly |> Repair{8}()
- @test !hasholes(rpoly)
- @test rings(rpoly) == [Ring(P2(0, 0))]
- @test vertices(rpoly) == [P2(0, 0)]
- end
-
- @testset "Repair{9}" begin
- poly = Quadrangle(P3(0, 1, 0), P3(1, 1, 0), P3(1, 0, 0), P3(0, 0, 0))
- bpoly = poly |> Repair{9}()
- @test bpoly isa Quadrangle
- @test bpoly == poly
- end
-
- @testset "Repair{10}" begin
- outer = Ring(P2[(0, 0), (0, 3), (2, 3), (2, 2), (3, 2), (3, 0)])
- inner = Ring(P2[(1, 1), (1, 2), (2, 2), (2, 1)])
- poly = PolyArea(outer, inner)
- repair = Repair{10}()
- rpoly, cache = TB.apply(repair, poly)
- @test nvertices(rpoly) == nvertices(poly)
- @test length(first(rings(rpoly))) > length(first(rings(poly)))
- opoly = TB.revert(repair, rpoly, cache)
- @test opoly == poly
- end
-
- @testset "Bridge" begin
- @test !isaffine(Bridge)
- δ = T(0.01)
- f = Bridge(δ)
- @test TB.parameters(f) == (; δ)
-
- # https://github.com/JuliaGeometry/Meshes.jl/issues/566
- outer = Ring(P2(6, 4), P2(6, 7), P2(1, 6), P2(1, 1), P2(5, 2))
- inner₁ = Ring(P2(3, 3), P2(3, 4), P2(4, 3))
- inner₂ = Ring(P2(2, 5), P2(2, 6), P2(3, 5))
- poly = PolyArea([outer, inner₁, inner₂])
- bpoly = poly |> Bridge(T(0.1))
- @test !hasholes(bpoly)
- @test nvertices(bpoly) == 15
-
- # unique and bridges
- poly = PolyArea(P2[(0, 0), (1, 0), (1, 0), (1, 1), (1, 2), (0, 2), (0, 1), (0, 1)])
- cpoly = poly |> Repair{0}() |> Bridge()
- @test cpoly == PolyArea(P2[(0, 0), (1, 0), (1, 1), (1, 2), (0, 2), (0, 1)])
-
- # basic ngon tests
- t = Triangle(P2(0, 0), P2(1, 0), P2(0, 1))
- @test (t |> Bridge() |> boundary) == boundary(t)
- q = Quadrangle(P2(0, 0), P2(1, 0), P2(1, 1), P2(0, 1))
- @test (q |> Bridge() |> boundary) == boundary(q)
-
- # bridges between holes
- outer = P2[(0, 0), (1, 0), (1, 1), (0, 1)]
- hole1 = P2[(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]
- hole2 = P2[(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]
- poly = PolyArea([outer, hole1, hole2])
- @test vertices(poly) == P2[
- (0, 0),
- (1, 0),
- (1, 1),
- (0, 1),
- (0.2, 0.2),
- (0.2, 0.4),
- (0.4, 0.4),
- (0.4, 0.2),
- (0.6, 0.2),
- (0.6, 0.4),
- (0.8, 0.4),
- (0.8, 0.2)
- ]
- bpoly = poly |> Bridge(T(0.01))
- target = P2[
+@testitem "Rotate" setup = [Setup] begin
+ @test isaffine(Rotate)
+ @test TB.isrevertible(Rotate)
+ @test TB.isinvertible(Rotate)
+ @test TB.inverse(Rotate(Angle2d(T(π / 2)))) == Rotate(Angle2d(-T(π / 2)))
+ rot = Angle2d(T(π / 2))
+ f = Rotate(rot)
+ @test TB.parameters(f) == (; rot)
+
+ # ----
+ # VEC
+ # ----
+
+ f = Rotate(Angle2d(T(π / 2)))
+ v = vector(1, 0)
+ r, c = TB.apply(f, v)
+ @test r ≈ vector(0, 1)
+ @test TB.revert(f, r, c) ≈ v
+
+ # ------
+ # POINT
+ # ------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ g = cart(1, 0)
+ r, c = TB.apply(f, g)
+ @test r ≈ cart(0, 1)
+ @test TB.revert(f, r, c) ≈ g
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ g = Segment(cart(0, 0), cart(1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(0, 0), cart(0, 1))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----
+ # BOX
+ # ----
+
+ f = Rotate(Angle2d(T(π / 2)))
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Quadrangle(cart(0, 0), cart(0, 1), cart(-1, 1), cart(-1, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Rotate(vector(1, 0, 0), vector(0, 1, 0))
+ g = Box(cart(0, 0, 0), cart(1, 1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Hexahedron(
+ cart(0, 0, 0),
+ cart(0, 1, 0),
+ cart(-1, 1, 0),
+ cart(-1, 0, 0),
+ cart(0, 0, 1),
+ cart(0, 1, 1),
+ cart(-1, 1, 1),
+ cart(-1, 0, 1)
+ )
+ h = TB.revert(f, r, c)
+ @test h ≈ convert(Hexahedron, g)
+
+ # ----------
+ # ROPE/RING
+ # ----------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ g = Rope(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Rope(cart(0, 0), cart(0, 1), cart(-1, 1), cart(-1, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Rotate(Angle2d(T(π / 2)))
+ g = Ring(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Ring(cart(0, 0), cart(0, 1), cart(-1, 1), cart(-1, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Rotate(AngleAxis(T(π / 2), T(0), T(0), T(1)))
+ g = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(cart(0, 0, 0), cart(0, 1, 0), cart(-1, 0, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Rotate(vector(0, 0, 1), vector(1, 0, 0))
+ g = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(cart(0, 0, 0), cart(0, 0, -1), cart(0, 1, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # POLYAREA
+ # ---------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ p = PolyArea(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ r, c = TB.apply(f, p)
+ @test r ≈ PolyArea(cart(0, 0), cart(0, 1), cart(-1, 1), cart(-1, 0))
+ @test TB.revert(f, r, c) ≈ p
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ g
+
+ # ------
+ # PLANE
+ # ------
+
+ f = Rotate(vector(0, 0, 1), vector(1, 0, 0))
+ g = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Plane(cart(0, 0, 0), vector(1, 0, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # CYLINDER
+ # ---------
+
+ f = Rotate(vector(0, 0, 1), vector(1, 0, 0))
+ g = Cylinder(T(1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Cylinder(cart(0, 0, 0), cart(1, 0, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----------
+ # ELLIPSOID
+ # ----------
+
+ R = RotXYZ(T(π / 4), T(π / 5), T(π / 3))
+ f = Rotate(R)
+ g = Ellipsoid((T(3), T(2), T(1)), (T(1), T(1), T(1)))
+ r, c = TB.apply(f, g)
+ @test center(r) == center(g) |> Rotate(R)
+ @test rotation(r) == R
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ d = PointSet([cart(0, 0), cart(1, 0), cart(1, 1)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([cart(0, 0), cart(0, 1), cart(-1, 1)])
+ @test TB.revert(f, r, c) ≈ d
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ d
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+ @test all(TB.revert(f, r, c) .≈ d)
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ d = cartgrid(10, 10)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ------------
+ # REGULARGRID
+ # ------------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ d = RegularGrid(merc(0, 0), merc(1, 1), dims=(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ d = convert(StructuredGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = Rotate(Angle2d(T(π / 2)))
+ p = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ---------
+ # FALLBACK
+ # ---------
+
+ f = Rotate(T(π / 2))
+ v = vector(1, 0)
+ r, c = TB.apply(f, v)
+ @test r ≈ vector(0, 1)
+ @test TB.revert(f, r, c) ≈ v
+
+ # CRS propagation
+ f = Rotate(Angle2d(T(π / 2)))
+ p = merc(1, 0)
+ @test crs(f(p)) === crs(p)
+end
+
+@testitem "Translate" setup = [Setup] begin
+ @test isaffine(Translate)
+ @test TB.isrevertible(Translate)
+ @test TB.isinvertible(Translate)
+ @test TB.inverse(Translate(T(1), T(2))) == Translate(T(-1), T(-2))
+ offsets = (T(1) * u"m", T(2) * u"m")
+ f = Translate(offsets)
+ @test TB.parameters(f) == (; offsets)
+ f = Translate(T(1), T(2))
+ @test TB.parameters(f) == (; offsets)
+ f = Translate(T(1), 2)
+ @test TB.parameters(f) == (; offsets)
+ f = Translate(1, 2)
+ @test TB.parameters(f) == (; offsets)
+
+ # ----
+ # VEC
+ # ----
+
+ f = Translate(T(1), T(1))
+ v = vector(1, 0)
+ r, c = TB.apply(f, v)
+ @test r ≈ vector(1, 0)
+ @test TB.revert(f, r, c) ≈ v
+
+ # ------
+ # POINT
+ # ------
+
+ f = Translate(T(1), T(1))
+ g = cart(1, 0)
+ r, c = TB.apply(f, g)
+ @test r ≈ cart(2, 1)
+ @test TB.revert(f, r, c) ≈ g
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Translate(T(1), T(1))
+ g = Segment(cart(0, 0), cart(1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(1, 1), cart(2, 1))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----
+ # BOX
+ # ----
+
+ f = Translate(T(1), T(1))
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r isa Box
+ @test r ≈ Box(cart(1, 1), cart(2, 2))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Translate(T(1), T(2), T(3))
+ g = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(cart(1, 2, 3), cart(2, 2, 3), cart(1, 3, 4))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Translate(T(1), T(1))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ g
+
+ # ------
+ # PLANE
+ # ------
+
+ f = Translate(T(0), T(0), T(1))
+ g = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Plane(cart(0, 0, 1), vector(0, 0, 1))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # CYLINDER
+ # ---------
+
+ f = Translate(T(0), T(0), T(1))
+ g = Cylinder(T(1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Cylinder(cart(0, 0, 1), cart(0, 0, 2))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Translate(T(1), T(1))
+ d = PointSet([cart(0, 0), cart(1, 0), cart(1, 1)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([cart(1, 1), cart(2, 1), cart(2, 2)])
+ @test TB.revert(f, r, c) ≈ d
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Translate(T(1), T(1))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ d
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+ @test all(TB.revert(f, r, c) .≈ d)
+
+ # ------------
+ # REGULARGRID
+ # ------------
+
+ f = Translate(T(1), T(1))
+ d = RegularGrid((8, 8), Point(Polar(T(0), T(0))), (T(1), T(π / 4)))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Translate(T(1), T(1))
+ d = cartgrid(10, 10)
+ r, c = TB.apply(f, d)
+ @test r isa CartesianGrid
+ @test r ≈ CartesianGrid(cart(1, 1), cart(11, 11), dims=(10, 10))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Translate(T(1), T(1))
+ d = RectilinearGrid(T.(0:10), T.(0:10))
+ r, c = TB.apply(f, d)
+ @test r isa RectilinearGrid
+ @test r ≈ RectilinearGrid(T.(1:11), T.(1:11))
+ @test TB.revert(f, r, c) ≈ d
+
+ f = Translate(T(1), T(1))
+ g = RegularGrid((8, 8), Point(Polar(T(0), T(0))), (T(1), T(π / 4)))
+ d = convert(RectilinearGrid, g)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Translate(T(1), T(1))
+ d = StructuredGrid(repeat(T.(0:10), 1, 11), repeat(T.(0:10)', 11, 1))
+ r, c = TB.apply(f, d)
+ @test r isa StructuredGrid
+ @test r ≈ StructuredGrid(repeat(T.(1:11), 1, 11), repeat(T.(1:11)', 11, 1))
+ @test TB.revert(f, r, c) ≈ d
+
+ f = Translate(T(1), T(1))
+ g = RegularGrid((8, 8), Point(Polar(T(0), T(0))), (T(1), T(π / 4)))
+ d = convert(StructuredGrid, g)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = Translate(T(1), T(1))
+ p = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+end
+
+@testitem "Affine" setup = [Setup] begin
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ @test isaffine(f)
+ @test TB.isrevertible(f)
+ @test TB.isinvertible(f)
+ @test TB.inverse(f) == Affine(Angle2d(-T(π / 2)), Angle2d(-T(π / 2)) * -T[1, 1])
+ f = Affine(T[6 3; 10 5], T[1, 1])
+ @test !TB.isrevertible(f)
+ @test !TB.isinvertible(f)
+ A, b = Angle2d(T(π / 2)), SVector(T(1) * u"m", T(1) * u"m")
+ f = Affine(A, b)
+ @test TB.parameters(f) == (; A, b)
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ @test TB.parameters(f) == (; A, b)
+ f = Affine(Angle2d(T(π / 2)), [1, 1])
+ @test TB.parameters(f) == (; A, b)
+
+ # ----
+ # VEC
+ # ----
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ v = vector(1, 0)
+ r, c = TB.apply(f, v)
+ @test r ≈ vector(0, 1)
+ @test TB.revert(f, r, c) ≈ v
+
+ # ------
+ # POINT
+ # ------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ g = cart(1, 0)
+ r, c = TB.apply(f, g)
+ @test r ≈ cart(1, 2)
+ @test TB.revert(f, r, c) ≈ g
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ g = Segment(cart(0, 0), cart(1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(1, 1), cart(1, 2))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----
+ # BOX
+ # ----
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Quadrangle(cart(1, 1), cart(1, 2), cart(0, 2), cart(0, 1))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Affine(rotation_between(SVector{3,T}(0, 0, 1), SVector{3,T}(1, 0, 0)), T[1, 2, 3])
+ g = Box(cart(0, 0, 0), cart(1, 1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Hexahedron(
+ cart(1, 2, 3),
+ cart(1, 2, 2),
+ cart(1, 3, 2),
+ cart(1, 3, 3),
+ cart(2, 2, 3),
+ cart(2, 2, 2),
+ cart(2, 3, 2),
+ cart(2, 3, 3)
+ )
+ h = TB.revert(f, r, c)
+ @test h ≈ convert(Hexahedron, g)
+
+ # -----
+ # BALL
+ # -----
+
+ f = Affine(Diagonal(T[1, 2]), T[1, 1])
+ g = Ball(cart(1, 2), T(3))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # -------
+ # SPHERE
+ # -------
+
+ f = Affine(Diagonal(T[1, 2]), T[1, 1])
+ g = Sphere(cart(1, 2), T(3))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ----------
+ # ELLIPSOID
+ # ----------
+
+ f = Affine(Diagonal(T[1, 2, 3]), T[1, 1, 1])
+ g = Ellipsoid(T.((4, 5, 6)), cart(1, 2, 3))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # -----
+ # DISK
+ # -----
+
+ f = Affine(Diagonal(T[1, 2, 3]), T[1, 1, 1])
+ g = Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # -------
+ # CIRCLE
+ # -------
+
+ f = Affine(Diagonal(T[1, 2, 3]), T[1, 1, 1])
+ g = Circle(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ----------------
+ # CYLINDERSURFACE
+ # ----------------
+
+ f = Affine(Diagonal(T[1, 2, 3]), T[1, 1, 1])
+ g = CylinderSurface(T(1))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ------------------
+ # PARABOLOIDSURFACE
+ # ------------------
+
+ f = Affine(Diagonal(T[1, 2, 3]), T[1, 1, 1])
+ g = ParaboloidSurface(cart(0, 0, 0), T(1), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ------
+ # TORUS
+ # ------
+
+ f = Affine(Diagonal(T[1, 2, 3]), T[1, 1, 1])
+ g = Torus(cart(1, 1, 1), vector(1, 0, 0), T(2), T(1))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Affine(rotation_between(SVector{3,T}(0, 0, 1), SVector{3,T}(1, 0, 0)), T[1, 2, 3])
+ g = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(cart(1, 2, 3), cart(1, 2, 2), cart(2, 3, 3))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ g
+
+ # ------
+ # PLANE
+ # ------
+
+ f = Affine(rotation_between(SVector{3,T}(0, 0, 1), SVector{3,T}(1, 0, 0)), T[0, 0, 1])
+ g = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Plane(cart(0, 0, 1), vector(1, 0, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # CYLINDER
+ # ---------
+
+ f = Affine(rotation_between(SVector{3,T}(0, 0, 1), SVector{3,T}(1, 0, 0)), T[0, 0, 1])
+ g = Cylinder(T(1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Cylinder(cart(0, 0, 1), cart(1, 0, 1))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ d = PointSet([cart(0, 0), cart(1, 0), cart(1, 1)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([cart(1, 1), cart(1, 2), cart(0, 2)])
+ @test TB.revert(f, r, c) ≈ d
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ d
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+ @test all(TB.revert(f, r, c) .≈ d)
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ d = cartgrid(10, 10)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ d = convert(StructuredGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ----------
+ # TRANSFORM
+ # ----------
+
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ s = Rotate(T(π / 2)) → Translate(T(1), T(1))
+ v = vector(1, 0)
+ g1 = cart(1, 0)
+ g2 = Segment(cart(0, 0), cart(1, 0))
+ g3 = Box(cart(0, 0), cart(1, 1))
+ @test f(v) ≈ s(v)
+ @test f(g1) ≈ s(g1)
+ @test f(g2) ≈ s(g2)
+ @test f(g3) ≈ s(g3)
+
+ # ------------
+ # CONSTRUCTOR
+ # ------------
+
+ # conversion to SArray
+ f = Affine(T[0 -1; 1 0], SVector{2}(T[1, 1]))
+ @test f.A isa SMatrix
+ f = Affine(SMatrix{2,2}(T[0 -1; 1 0]), T[1, 1])
+ @test f.b isa SVector
+ f = Affine(T[0 -1; 1 0], T[1, 1])
+ @test f.A isa SMatrix
+ @test f.b isa SVector
+
+ # CRS propagation
+ f = Affine(Angle2d(T(π / 2)), T[1, 1])
+ p = merc(1, 0)
+ @test crs(f(p)) === crs(p)
+
+ # error: A must be a square matrix
+ @test_throws ArgumentError Affine(T[1 1; 2 2; 3 3], T[1, 2])
+ # error: A and b must have the same dimension
+ @test_throws ArgumentError Affine(T[1 1; 2 2], T[1, 2, 3])
+end
+
+@testitem "Scale" setup = [Setup] begin
+ @test isaffine(Scale)
+ @test TB.isrevertible(Scale)
+ @test TB.isinvertible(Scale)
+ @test TB.inverse(Scale(T(1), T(2))) == Scale(T(1), T(1 / 2))
+ factors = (T(1), T(2))
+ f = Scale(factors)
+ @test TB.parameters(f) == (; factors)
+
+ # ----
+ # VEC
+ # ----
+
+ f = Scale(T(1), T(2))
+ v = vector(1, 1)
+ r, c = TB.apply(f, v)
+ @test r ≈ vector(1, 2)
+ @test TB.revert(f, r, c) ≈ v
+
+ # ------
+ # POINT
+ # ------
+
+ f = Scale(T(1), T(2))
+ g = cart(1, 1)
+ r, c = TB.apply(f, g)
+ @test r ≈ cart(1, 2)
+ @test TB.revert(f, r, c) ≈ g
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Scale(T(1), T(2))
+ g = Segment(cart(0, 0), cart(1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(0, 0), cart(1, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Scale(T(2), T(1))
+ g = Segment(cart(0, 0), cart(1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(0, 0), cart(2, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----
+ # BOX
+ # ----
+
+ f = Scale(T(1), T(2))
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r isa Box
+ @test r ≈ Box(cart(0, 0), cart(1, 2))
+ @test TB.revert(f, r, c) ≈ g
+
+ # -----
+ # BALL
+ # -----
+
+ f = Scale(T(1), T(2))
+ g = Ball(cart(1, 2), T(3))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ f = Scale(T(2))
+ g = Ball(cart(1, 2), T(3))
+ r, c = TB.apply(f, g)
+ @test r isa Ball
+ @test r ≈ Ball(cart(2, 4), T(6))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Scale(T(2))
+ g = Ball(cart(1, 2, 3), T(4))
+ r, c = TB.apply(f, g)
+ @test r isa Ball
+ @test r ≈ Ball(cart(2, 4, 6), T(8))
+ @test TB.revert(f, r, c) ≈ g
+
+ # -------
+ # SPHERE
+ # -------
+
+ f = Scale(T(1), T(2))
+ g = Sphere(cart(1, 2), T(3))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ f = Scale(T(1), T(2), T(3))
+ g = Sphere(cart(1, 2, 3), T(4))
+ r, c = TB.apply(f, g)
+ @test r isa Ellipsoid
+ @test r ≈ Ellipsoid(T.((4, 8, 12)), cart(1, 4, 9))
+ @test discretize(TB.revert(f, r, c)) ≈ discretize(g)
+
+ f = Scale(T(2))
+ g = Sphere(cart(1, 2), T(3))
+ r, c = TB.apply(f, g)
+ @test r isa Sphere
+ @test r ≈ Sphere(cart(2, 4), T(6))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Scale(T(2))
+ g = Sphere(cart(1, 2, 3), T(4))
+ r, c = TB.apply(f, g)
+ @test r isa Sphere
+ @test r ≈ Sphere(cart(2, 4, 6), T(8))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----------
+ # ELLIPSOID
+ # ----------
+
+ f = Scale(T(1), T(2), T(3))
+ g = Ellipsoid(T.((1, 2, 3)))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ f = Scale(T(2))
+ g = Ellipsoid(T.((4, 5, 6)), cart(1, 2, 3))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test r isa Ellipsoid
+ @test r ≈ Ellipsoid(T.((8, 10, 12)), cart(2, 4, 6))
+ @test TB.revert(f, r, c) ≈ g
+
+ # -----
+ # DISK
+ # -----
+
+ f = Scale(T(1), T(2), T(3))
+ g = Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # -------
+ # CIRCLE
+ # -------
+
+ f = Scale(T(1), T(2), T(3))
+ g = Circle(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ----------------
+ # CYLINDERSURFACE
+ # ----------------
+
+ f = Scale(T(1), T(2), T(3))
+ g = CylinderSurface(T(1))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ------------------
+ # PARABOLOIDSURFACE
+ # ------------------
+
+ f = Scale(T(1), T(2), T(3))
+ g = ParaboloidSurface(cart(0, 0, 0), T(1), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ------
+ # TORUS
+ # ------
+
+ f = Scale(T(1), T(2), T(3))
+ g = Torus(cart(1, 1, 1), vector(1, 0, 0), T(2), T(1))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+ @test centroid(r) ≈ f(centroid(g))
+ @test discretize(TB.revert(f, r, c)) ≈ m
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Scale(T(1), T(2), T(3))
+ g = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 2, 3))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Scale(T(1), T(2))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ g
+
+ # ------
+ # PLANE
+ # ------
+
+ f = Scale(T(1), T(1), T(2))
+ g = Plane(cart(1, 1, 1), vector(0, 0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Plane(cart(1, 1, 2), vector(0, 0, 1))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Scale(T(2), T(1), T(1))
+ g = Plane(cart(1, 1, 1), vector(0, 0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ g
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Scale(T(1), T(2))
+ d = PointSet([cart(0, 0), cart(1, 0), cart(1, 1)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([cart(0, 0), cart(1, 0), cart(1, 2)])
+ @test TB.revert(f, r, c) ≈ d
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Scale(T(1), T(2))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ d
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+ @test all(TB.revert(f, r, c) .≈ d)
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Scale(T(1), T(2))
+ d = CartesianGrid(cart(1, 1), cart(11, 11), dims=(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa CartesianGrid
+ @test r ≈ CartesianGrid(cart(1, 2), cart(11, 22), dims=(10, 10))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ------------
+ # REGULARGRID
+ # ------------
+
+ f = Scale(T(1), T(2))
+ d = RegularGrid(merc(1, 1), merc(11, 11), dims=(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ RegularGrid(merc(1, 2), merc(11, 22), dims=(10, 10))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Scale(T(1), T(2))
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa RectilinearGrid
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Scale(T(1), T(2))
+ d = convert(StructuredGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa StructuredGrid
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = Scale(T(1), T(2))
+ p = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+end
+
+@testitem "Stretch" setup = [Setup] begin
+ @test !isaffine(Stretch)
+ @test TB.isrevertible(Stretch)
+ @test TB.isinvertible(Stretch)
+ @test TB.inverse(Stretch(T(1), T(2))) == Stretch(T(1), T(1 / 2))
+ factors = (T(1), T(2))
+ f = Stretch(factors)
+ @test TB.parameters(f) == (; factors)
+
+ # ----
+ # VEC
+ # ----
+
+ f = Stretch(T(1), T(2))
+ v = vector(1, 1)
+ r, c = TB.apply(f, v)
+ @test r ≈ vector(1, 2)
+ @test TB.revert(f, r, c) ≈ v
+
+ # ------
+ # POINT
+ # ------
+
+ f = Stretch(T(1), T(2))
+ g = cart(1, 1)
+ r, c = TB.apply(f, g)
+ @test r ≈ cart(1, 1)
+ @test TB.revert(f, r, c) ≈ g
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Stretch(T(1), T(2))
+ g = Segment(cart(0, 0), cart(1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(0, 0), cart(1, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Stretch(T(2), T(1))
+ g = Segment(cart(0, 0), cart(1, 0))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(-0.5, 0), cart(1.5, 0))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----
+ # BOX
+ # ----
+
+ f = Stretch(T(1), T(2))
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r isa Box
+ @test r ≈ Box(cart(0, -0.5), cart(1, 1.5))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Stretch(T(1), T(2), T(2))
+ g = Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(0, 1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(cart(0, -1 / 3, -1 / 3), cart(1, -1 / 3, -1 / 3), cart(0, 10 / 6, 10 / 6))
+ @test TB.revert(f, r, c) ≈ g
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Stretch(T(1), T(2))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ g
+
+ # ------
+ # PLANE
+ # ------
+
+ f = Stretch(T(1), T(1), T(2))
+ g = Plane(cart(1, 1, 1), vector(0, 0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ g
+ @test TB.revert(f, r, c) ≈ g
+
+ f = Stretch(T(2), T(1), T(1))
+ g = Plane(cart(1, 1, 1), vector(0, 0, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ g
+ @test TB.revert(f, r, c) ≈ g
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Stretch(T(1), T(2))
+ d = PointSet([cart(0, 0), cart(1, 0), cart(1, 1)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([cart(0, -1 / 3), cart(1, -1 / 3), cart(1, 10 / 6)])
+ @test TB.revert(f, r, c) ≈ d
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Stretch(T(1), T(2))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ @test TB.revert(f, r, c) ≈ d
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+ @test all(TB.revert(f, r, c) .≈ d)
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Stretch(T(1), T(2))
+ d = CartesianGrid(cart(1, 1), cart(11, 11), dims=(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa CartesianGrid
+ @test r ≈ CartesianGrid(cart(1, -4), cart(11, 16), dims=(10, 10))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Stretch(T(1), T(2))
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa RectilinearGrid
+ @test r ≈ SimpleMesh(f(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Stretch(T(1), T(2))
+ d = convert(StructuredGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa StructuredGrid
+ @test r ≈ SimpleMesh(f(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = Stretch(T(1), T(2))
+ p = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f(vertices(d)), topology(d))
+ @test TB.revert(f, r, c) ≈ d
+end
+
+@testitem "StdCoords" setup = [Setup] begin
+ @test !isaffine(StdCoords)
+ @test TB.isrevertible(StdCoords)
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = StdCoords()
+ d = view(PointSet(randpoint2(100)), 1:50)
+ r, c = TB.apply(f, d)
+ @test all(sides(boundingbox(r)) .≤ oneunit(ℳ))
+ @test TB.revert(f, r, c) ≈ d
+ r2 = TB.reapply(f, d, c)
+ @test r == r2
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = StdCoords()
+ d = CartesianGrid(cart(1, 1), cart(11, 11), dims=(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa CartesianGrid
+ @test r ≈ CartesianGrid(cart(-0.5, -0.5), cart(0.5, 0.5), dims=(10, 10))
+ @test TB.revert(f, r, c) ≈ d
+
+ f = StdCoords()
+ d = cartgrid(10, 20)
+ r, c = TB.apply(f, d)
+ @test r ≈ CartesianGrid(cart(-0.5, -0.5), cart(0.5, 0.5), dims=(10, 20))
+ @test TB.revert(f, r, c) ≈ d
+ r2 = TB.reapply(f, d, c)
+ @test r == r2
+end
+
+@testitem "Proj" setup = [Setup] begin
+ @test !isaffine(Proj(Polar))
+ @test !TB.isrevertible(Proj(Polar))
+ @test !TB.isinvertible(Proj(Polar))
+ @test TB.parameters(Proj(Polar)) == (; CRS=Polar)
+ @test TB.parameters(Proj(EPSG{3395})) == (; CRS=Mercator{WGS84Latest})
+ @test TB.parameters(Proj(ESRI{54017})) == (; CRS=Behrmann{WGS84Latest})
+ f = Proj(Mercator)
+ @test sprint(show, f) == "Proj(CRS: CoordRefSystems.Mercator)"
+ @test sprint(show, MIME"text/plain"(), f) == """
+ Proj transform
+ └─ CRS: CoordRefSystems.Mercator"""
+
+ # ----
+ # VEC
+ # ----
+
+ f = Proj(Polar)
+ v = vector(1, 0)
+ r, c = TB.apply(f, v)
+ @test r == v
+
+ # ------
+ # POINT
+ # ------
+
+ f = Proj(Polar)
+ g = cart(1, 1)
+ r, c = TB.apply(f, g)
+ @test r ≈ Point(Polar(T(√2), T(π / 4)))
+
+ # change the manifold
+ f = Proj(Mercator)
+ g = latlon(0, 0)
+ r, c = TB.apply(f, g)
+ @test manifold(r) === 𝔼{2}
+ @test r ≈ merc(0, 0)
+
+ # preserve the manifold
+ f = Proj(Cartesian)
+ g = latlon(0, 0)
+ r, c = TB.apply(f, g)
+ @test manifold(r) === 🌐
+ @test r ≈ Point(Cartesian{WGS84Latest}(T(6378137.0), T(0), T(0)))
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Proj(Polar)
+ g = Segment(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(Point(Polar(T(0), T(0))), Point(Polar(T(√2), T(π / 4))))
+
+ # ----
+ # BOX
+ # ----
+
+ f = Proj(Polar)
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Box(Point(Polar(T(0), T(0))), Point(Polar(T(√2), T(π / 4))))
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Proj(Polar)
+ g = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(Point(Polar(T(0), T(0))), Point(Polar(T(1), T(0))), Point(Polar(T(√2), T(π / 4))))
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Proj(Polar)
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+
+ # ---------
+ # CYLINDER
+ # ---------
+
+ f = Proj(Cylindrical)
+ g = Cylinder(cart(0, 0, 0), cart(1, 1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Cylinder(Point(Cylindrical(T(0), T(0), T(0))), Point(Cylindrical(T(√2), T(π / 4), T(1))))
+
+ # --------------------
+ # TRANSFORMEDGEOMETRY
+ # --------------------
+
+ f = Proj(Mercator)
+ b = Box(latlon(0, 0), latlon(45, 45))
+ g = TransformedGeometry(b, Identity())
+ r, c = TB.apply(f, g)
+ @test r ≈ TransformedGeometry(b, f)
+
+ f = Proj(LatLon)
+ b = Box(merc(0, 0), merc(1, 1))
+ g = TransformedGeometry(b, Identity())
+ r, c = TB.apply(f, g)
+ @test r ≈ TransformedGeometry(b, f)
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Proj(Polar)
+ d = PointSet([cart(0, 0), cart(1, 0), cart(1, 1)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([Point(Polar(T(0), T(0))), Point(Polar(T(1), T(0))), Point(Polar(T(√2), T(π / 4)))])
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Proj(Polar)
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Proj(Polar)
+ d = CartesianGrid((10, 10), cart(1, 1), T.((1, 1)))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Proj(Polar)
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Proj(Polar)
+ d = convert(StructuredGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = Proj(Polar)
+ p = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # --------------
+ # SPECIAL CASES
+ # --------------
+
+ f = Proj(Mercator)
+ g = Box(latlon(0, 180), latlon(45, 90))
+ r, c = TB.apply(f, g)
+ @test manifold(r) === 𝔼{2}
+
+ f = Proj(LatLon)
+ g = Box(merc(0, 0), merc(1, 1))
+ r, c = TB.apply(f, g)
+ @test manifold(r) === 🌐
+
+ # --------------
+ # NO CONVERSION
+ # --------------
+
+ f = Proj(Cartesian)
+ g = cart(1, 1)
+ r, c = TB.apply(f, g)
+ @test r === g
+ f = Proj(crs(cart(0, 0)))
+ r, c = TB.apply(f, g)
+ @test r === g
+
+ f = Proj(LatLon)
+ g = Ring(latlon(0, 0), latlon(0, 1), latlon(1, 0))
+ r, c = TB.apply(f, g)
+ @test r === g
+ f = Proj(crs(latlon(0, 0)))
+ r, c = TB.apply(f, g)
+ @test r === g
+
+ f = Proj(Mercator)
+ p = merc.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r === d
+ f = Proj(crs(merc(0, 0)))
+ r, c = TB.apply(f, d)
+ @test r === d
+end
+
+@testitem "Morphological" setup = [Setup] begin
+ f = Morphological(c -> c)
+ @test !isaffine(f)
+ @test !TB.isrevertible(f)
+ @test !TB.isinvertible(f)
+ @test TB.parameters(f) == (; fun=f.fun)
+
+ # ----
+ # VEC
+ # ----
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ v = vector(1, 0)
+ r, c = TB.apply(f, v)
+ @test r == v
+
+ # ------
+ # POINT
+ # ------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ g = cart(1, 1)
+ r, c = TB.apply(f, g)
+ @test r == cart(1, 1, 0)
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ g = Segment(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(cart(0, 0, 0), cart(1, 1, 0))
+
+ # ----
+ # BOX
+ # ----
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Quadrangle(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0))
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ g = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0))
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+
+ # --------------------
+ # TRANSFORMEDGEOMETRY
+ # --------------------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ b = Box(cart(0, 0), cart(1, 1))
+ g = TransformedGeometry(b, Identity())
+ r, c = TB.apply(f, g)
+ @test r ≈ Quadrangle(cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0), cart(0, 1, 0))
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ d = PointSet([cart(0, 0), cart(1, 0), cart(1, 1)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([cart(0, 0, 0), cart(1, 0, 0), cart(1, 1, 0)])
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ d = CartesianGrid((10, 10), cart(1, 1), T.((1, 1)))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ d = convert(StructuredGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = Morphological(c -> Cartesian(c.x, c.y, zero(c.x)))
+ p = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+end
+
+@testitem "LengthUnit" setup = [Setup] begin
+ @test !isaffine(LengthUnit(u"km"))
+ @test !TB.isrevertible(LengthUnit(u"cm"))
+ @test !TB.isinvertible(LengthUnit(u"km"))
+ @test TB.parameters(LengthUnit(u"cm")) == (; unit=u"cm")
+
+ # ----
+ # VEC
+ # ----
+
+ f = LengthUnit(u"km")
+ v = vector(1000, 0)
+ r, c = TB.apply(f, v)
+ @test r ≈ Vec(T(1) * u"km", T(0) * u"km")
+
+ # ------
+ # POINT
+ # ------
+
+ f = LengthUnit(u"cm")
+ g = cart(1, 1)
+ r, c = TB.apply(f, g)
+ @test r ≈ Point(T(100) * u"cm", T(100) * u"cm")
+
+ f = LengthUnit(u"km")
+ g = Point(Polar(T(1000), T(π / 4)))
+ r, c = TB.apply(f, g)
+ @test r ≈ Point(Polar(T(1) * u"km", T(π / 4) * u"rad"))
+
+ f = LengthUnit(u"cm")
+ g = Point(Cylindrical(T(1), T(π / 4), T(1)))
+ r, c = TB.apply(f, g)
+ @test r ≈ Point(Cylindrical(T(100) * u"cm", T(π / 4) * u"rad", T(100) * u"cm"))
+
+ f = LengthUnit(u"km")
+ g = Point(Spherical(T(1000), T(π / 4), T(π / 4)))
+ r, c = TB.apply(f, g)
+ @test r ≈ Point(Spherical(T(1) * u"km", T(π / 4) * u"rad", T(π / 4) * u"rad"))
+
+ f = LengthUnit(u"cm")
+ g = Point(Mercator(T(1), T(1)))
+ @test_throws ArgumentError TB.apply(f, g)
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = LengthUnit(u"km")
+ g = Segment(cart(0, 0), cart(1000, 1000))
+ r, c = TB.apply(f, g)
+ @test r ≈ Segment(Point(T(0) * u"km", T(0) * u"km"), Point(T(1) * u"km", T(1) * u"km"))
+
+ # ----
+ # BOX
+ # ----
+
+ f = LengthUnit(u"cm")
+ g = Box(cart(0, 0), cart(1, 1))
+ r, c = TB.apply(f, g)
+ @test r isa Box
+ @test r ≈ Box(Point(T(0) * u"cm", T(0) * u"cm"), Point(T(100) * u"cm", T(100) * u"cm"))
+
+ # -------
+ # SPHERE
+ # -------
+
+ f = LengthUnit(u"km")
+ g = Sphere(cart(0, 0), T(1000))
+ r, c = TB.apply(f, g)
+ @test r isa Sphere
+ @test r ≈ Sphere(Point(T(0) * u"km", T(0) * u"km"), T(1) * u"km")
+
+ # ----------
+ # ELLIPSOID
+ # ----------
+
+ f = LengthUnit(u"cm")
+ g = Ellipsoid(T.((1, 1, 1)), cart(0, 0, 0))
+ r, c = TB.apply(f, g)
+ @test r isa Ellipsoid
+ @test r ≈ Ellipsoid((T(100) * u"cm", T(100) * u"cm", T(100) * u"cm"), Point(T(0) * u"cm", T(0) * u"cm", T(0) * u"cm"))
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = LengthUnit(u"km")
+ g = Triangle(cart(0, 0), cart(1000, 0), cart(1000, 1000))
+ r, c = TB.apply(f, g)
+ @test r ≈ Triangle(
+ Point(T(0) * u"km", T(0) * u"km"),
+ Point(T(1) * u"km", T(0) * u"km"),
+ Point(T(1) * u"km", T(1) * u"km")
+ )
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = LengthUnit(u"cm")
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r ≈ Multi([f(t), f(t)])
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = LengthUnit(u"km")
+ d = PointSet([cart(0, 0), cart(1000, 0), cart(1000, 1000)])
+ r, c = TB.apply(f, d)
+ @test r ≈ PointSet([
+ Point(T(0) * u"km", T(0) * u"km"),
+ Point(T(1) * u"km", T(0) * u"km"),
+ Point(T(1) * u"km", T(1) * u"km")
+ ])
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = LengthUnit(u"cm")
+ t = Triangle(cart(0, 0), cart(1, 0), cart(1, 1))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r ≈ GeometrySet([f(t), f(t)])
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .≈ [f(t), f(t)])
+
+ # ------------
+ # REGULARGRID
+ # ------------
+
+ f = LengthUnit(u"cm")
+ d = RegularGrid((8, 8), Point(Polar(T(1), T(0))), (T(1), T(π / 4)))
+ r, c = TB.apply(f, d)
+ @test r ≈ RegularGrid((8, 8), Point(Polar(T(100) * u"cm", T(0) * u"rad")), (T(100) * u"cm", T(π / 4) * u"rad"))
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = LengthUnit(u"km")
+ d = CartesianGrid((10, 10), cart(1000, 1000), T.((1000, 1000)))
+ r, c = TB.apply(f, d)
+ @test r isa CartesianGrid
+ @test r ≈ CartesianGrid((10, 10), Point(T(1) * u"km", T(1) * u"km"), (T(1) * u"km", T(1) * u"km"))
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = LengthUnit(u"cm")
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = LengthUnit(u"km")
+ d = convert(StructuredGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = LengthUnit(u"cm")
+ p = cart.([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r ≈ SimpleMesh(f.(vertices(d)), topology(d))
+end
+
+@testitem "Shadow" setup = [Setup] begin
+ @test !isaffine(Shadow(:xy))
+ @test !TB.isrevertible(Shadow("xy"))
+ @test !TB.isinvertible(Shadow(:xy))
+ @test TB.parameters(Shadow("xy")) == (; dims=(1, 2))
+ @test TB.parameters(Shadow(:yx)) == (; dims=(2, 1))
+ @test TB.parameters(Shadow("xz")) == (; dims=(1, 3))
+ @test TB.parameters(Shadow(:yz)) == (; dims=(2, 3))
+ @test_throws ArgumentError Shadow(:xk)
+
+ # ----
+ # VEC
+ # ----
+
+ f = Shadow(:xy)
+ v = vector(1, 2, 3)
+ r, c = TB.apply(f, v)
+ @test r == vector(1, 2)
+
+ # ------
+ # POINT
+ # ------
+
+ f = Shadow(:xz)
+ g = cart(1, 2, 3)
+ r, c = TB.apply(f, g)
+ @test r == cart(1, 3)
+
+ # --------
+ # SEGMENT
+ # --------
+
+ f = Shadow(:yz)
+ g = Segment(cart(1, 2, 3), cart(4, 5, 6))
+ r, c = TB.apply(f, g)
+ @test r == Segment(cart(2, 3), cart(5, 6))
+
+ # ----
+ # BOX
+ # ----
+
+ f = Shadow(:xy)
+ g = Box(cart(1, 2, 3), cart(4, 5, 6))
+ r, c = TB.apply(f, g)
+ @test r isa Box
+ @test r == Box(cart(1, 2), cart(4, 5))
+
+ # ------
+ # PLANE
+ # ------
+
+ f = Shadow(:xz)
+ g = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ @test_throws ArgumentError TB.apply(f, g)
+
+ # ----------
+ # ELLIPSOID
+ # ----------
+
+ f = Shadow(:yz)
+ g = Ellipsoid(T.((1, 2, 3)))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # -----
+ # DISK
+ # -----
+
+ f = Shadow(:xy)
+ g = Disk(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # -------
+ # CIRCLE
+ # -------
+
+ f = Shadow(:xz)
+ g = Circle(Plane(cart(0, 0, 0), vector(0, 0, 1)), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # ----------------
+ # CYLINDERSURFACE
+ # ----------------
+
+ f = Shadow(:yz)
+ g = CylinderSurface(T(1))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # ------------
+ # CONESURFACE
+ # ------------
+
+ f = Shadow(:xy)
+ p = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ g = ConeSurface(Disk(p, T(2)), cart(0, 0, 1))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # ---------------
+ # FRUSTUMSURFACE
+ # ---------------
+
+ f = Shadow(:xz)
+ pb = Plane(cart(0, 0, 0), vector(0, 0, 1))
+ pt = Plane(cart(0, 0, 10), vector(0, 0, 1))
+ g = FrustumSurface(Disk(pb, T(1)), Disk(pt, T(2)))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # ------------------
+ # PARABOLOIDSURFACE
+ # ------------------
+
+ f = Shadow(:yz)
+ g = ParaboloidSurface(cart(0, 0, 0), T(1), T(2))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # ------
+ # TORUS
+ # ------
+
+ f = Shadow(:xy)
+ g = Torus(cart(1, 1, 1), vector(1, 0, 0), T(2), T(1))
+ m = discretize(g)
+ r, c = TB.apply(f, g)
+ @test discretize(r) ≈ f(m)
+
+ # ---------
+ # TRIANGLE
+ # ---------
+
+ f = Shadow(:xz)
+ g = Triangle(cart(1, 2, 3), cart(4, 5, 6), cart(7, 8, 9))
+ r, c = TB.apply(f, g)
+ @test r == Triangle(cart(1, 3), cart(4, 6), cart(7, 9))
+
+ # ----------
+ # MULTIGEOM
+ # ----------
+
+ f = Shadow(:yz)
+ t = Triangle(cart(1, 2, 3), cart(4, 5, 6), cart(7, 8, 9))
+ g = Multi([t, t])
+ r, c = TB.apply(f, g)
+ @test r == Multi([f(t), f(t)])
+
+ # ---------
+ # POINTSET
+ # ---------
+
+ f = Shadow(:xy)
+ d = PointSet([cart(1, 2, 3), cart(4, 5, 6), cart(7, 8, 9)])
+ r, c = TB.apply(f, d)
+ @test r == PointSet([cart(1, 2), cart(4, 5), cart(7, 8)])
+
+ # ------------
+ # GEOMETRYSET
+ # ------------
+
+ f = Shadow(:xz)
+ t = Triangle(cart(1, 2, 3), cart(4, 5, 6), cart(7, 8, 9))
+ d = GeometrySet([t, t])
+ r, c = TB.apply(f, d)
+ @test r == GeometrySet([f(t), f(t)])
+ d = [t, t]
+ r, c = TB.apply(f, d)
+ @test all(r .== [f(t), f(t)])
+
+ # ------------
+ # REGULARGRID
+ # ------------
+
+ f = Shadow(:yz)
+ d = RegularGrid((8, 8, 8), Point(Cylindrical(T(0), T(0), T(0))), (T(1), T(π / 4), T(1)))
+ r, c = TB.apply(f, d)
+ @test r == SimpleMesh(f.(vertices(d)), topology(d))
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Shadow(:yz)
+ d = CartesianGrid((10, 11, 12), cart(1, 2, 3), T.((1.0, 1.1, 1.2)))
+ r, c = TB.apply(f, d)
+ @test r isa CartesianGrid
+ @test r == CartesianGrid((11, 12), cart(2, 3), T.((1.1, 1.2)))
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Shadow(:xy)
+ d = convert(RectilinearGrid, cartgrid(10, 11, 12))
+ r, c = TB.apply(f, d)
+ @test r == SimpleMesh(f.(vertices(d)), topology(d))
+
+ # ---------------
+ # STRUCTUREDGRID
+ # ---------------
+
+ f = Shadow(:xz)
+ d = convert(StructuredGrid, cartgrid(10, 11, 12))
+ r, c = TB.apply(f, d)
+ @test r == SimpleMesh(f.(vertices(d)), topology(d))
+
+ # -----------
+ # SIMPLEMESH
+ # -----------
+
+ f = Shadow(:yz)
+ p = cart.([(0, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 1), (0, 0.5, 0.5)])
+ c = connect.([(1, 2, 5), (2, 4, 5), (4, 3, 5), (3, 1, 5)], Triangle)
+ d = SimpleMesh(p, c)
+ r, c = TB.apply(f, d)
+ @test r == SimpleMesh(f.(vertices(d)), topology(d))
+end
+
+@testitem "Slice" setup = [Setup] begin
+ @test !isaffine(Slice(x=(T(2), T(4))))
+ @test !TB.isrevertible(Slice(x=(T(2), T(4))))
+ @test !TB.isinvertible(Slice(x=(T(2), T(4))))
+ @test TB.parameters(Slice(x=(T(2), T(4)))) == (; limits=(; x=(T(2), T(4))))
+ @test TB.parameters(Slice(y=(T(2) * u"km", T(4) * u"km"))) == (; limits=(; y=(T(2) * u"km", T(4) * u"km")))
+ @test TB.parameters(Slice(z=(2, 4))) == (; limits=(; z=(2, 4)))
+ @test TB.parameters(Slice(lat=(30, 60))) == (; limits=(; lat=(30, 60)))
+ @test TB.parameters(Slice(lon=(45u"°", 90u"°"))) == (; limits=(; lon=(45u"°", 90u"°")))
+
+ # --------------
+ # CARTESIANGRID
+ # --------------
+
+ f = Slice(z=(T(1.5), T(4.5)))
+ d = cartgrid(10, 10, 10)
+ r, c = TB.apply(f, d)
+ @test r isa CartesianGrid
+ @test r == CartesianGrid((10, 10, 4), cart(0, 0, 1), T.((1, 1, 1)))
+
+ # ----------------
+ # RECTILINEARGRID
+ # ----------------
+
+ f = Slice(y=(T(3.5), T(6.5)))
+ d = convert(RectilinearGrid, cartgrid(10, 10))
+ r, c = TB.apply(f, d)
+ @test r isa RectilinearGrid
+ @test r == convert(RectilinearGrid, CartesianGrid((10, 4), cart(0, 3), T.((1, 1))))
+end
+
+@testitem "Repair(0)" setup = [Setup] begin
+ @test !isaffine(Repair)
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 0), (1, 1), (0, 1), (0, 1)]))
+ rpoly = poly |> Repair(0)
+ @test nvertices(rpoly) == 4
+ @test vertices(rpoly) == cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+
+ repair = Repair(0)
+ @test sprint(show, repair) == "Repair(K: 0)"
+ @test sprint(show, MIME"text/plain"(), repair) == """
+ Repair transform
+ └─ K: 0"""
+end
+
+@testitem "Repair(1)" setup = [Setup] begin
+ # a tetrahedron with an unused vertex
+ points = cart.([(0, 0, 0), (0, 0, 1), (5, 5, 5), (0, 1, 0), (1, 0, 0)])
+ connec = connect.([(1, 2, 4), (1, 2, 5), (1, 4, 5), (2, 4, 5)])
+ mesh = SimpleMesh(points, connec)
+ rmesh = mesh |> Repair(1)
+ @test nvertices(rmesh) == nvertices(mesh) - 1
+ @test nelements(rmesh) == nelements(mesh)
+ @test cart(5, 5, 5) ∉ vertices(rmesh)
+end
+
+@testitem "Repair(2)" setup = [Setup] begin end
+
+@testitem "Repair(3)" setup = [Setup] begin end
+
+@testitem "Repair(4)" setup = [Setup] begin end
+
+@testitem "Repair(5)" setup = [Setup] begin end
+
+@testitem "Repair(6)" setup = [Setup] begin end
+
+@testitem "Repair(7)" setup = [Setup] begin
+ # mesh with inconsistent orientation
+ points = randpoint3(6)
+ connec = connect.([(1, 2, 3), (3, 4, 2), (4, 3, 5), (6, 3, 1)])
+ mesh = SimpleMesh(points, connec)
+ rmesh = mesh |> Repair(7)
+ topo = topology(mesh)
+ rtopo = topology(rmesh)
+ e = collect(elements(topo))
+ n = collect(elements(rtopo))
+ @test n[1] == e[1]
+ @test n[2] != e[2]
+ @test n[3] != e[3]
+ @test n[4] != e[4]
+end
+
+@testitem "Repair(8)" setup = [Setup] begin
+ poly =
+ PolyArea(cart.([(0.0, 0.0), (0.5, -0.5), (1.0, 0.0), (1.5, 0.5), (1.0, 1.0), (0.5, 1.5), (0.0, 1.0), (-0.5, 0.5)]))
+ rpoly = poly |> Repair(8)
+ @test nvertices(rpoly) == 4
+ @test vertices(rpoly) == cart.([(0.5, -0.5), (1.5, 0.5), (0.5, 1.5), (-0.5, 0.5)])
+
+ # degenerate triangle with repeated vertices
+ poly = PolyArea(cart.([(0, 0), (1, 1), (1, 1)]))
+ rpoly = poly |> Repair(8)
+ @test !hasholes(rpoly)
+ @test rings(rpoly) == [Ring(cart(0, 0))]
+ @test vertices(rpoly) == [cart(0, 0)]
+end
+
+@testitem "Repair(9)" setup = [Setup] begin
+ quad = Quadrangle(cart(0, 1, 0), cart(1, 1, 0), cart(1, 0, 0), cart(0, 0, 0))
+ repair = Repair(9)
+ rquad, cache = TB.apply(repair, quad)
+ @test rquad isa Quadrangle
+ @test rquad == quad
+
+ outer = Ring(cart(6, 4), cart(6, 7), cart(1, 6), cart(1, 1), cart(5, 2))
+ inner1 = Ring(cart(3, 3), cart(3, 4), cart(4, 3))
+ inner2 = Ring(cart(2, 5), cart(2, 6), cart(3, 5))
+ poly = PolyArea([outer, inner1, inner2])
+ repair = Repair(9)
+ rpoly, cache = TB.apply(repair, poly)
+ @test rpoly == PolyArea([outer, inner2, inner1])
+end
+
+@testitem "Repair(10)" setup = [Setup] begin
+ outer = Ring(cart.([(0, 0), (0, 3), (2, 3), (2, 2), (3, 2), (3, 0)]))
+ inner = Ring(cart.([(1, 1), (1, 2), (2, 2), (2, 1)]))
+ poly = PolyArea(outer, inner)
+ repair = Repair(10)
+ rpoly, cache = TB.apply(repair, poly)
+ @test nvertices(rpoly) == nvertices(poly)
+ @test length(first(rings(rpoly))) > length(first(rings(poly)))
+ opoly = TB.revert(repair, rpoly, cache)
+ @test opoly == poly
+end
+
+@testitem "Repair(11)" setup = [Setup] begin
+ outer = cart.([(0, 0), (0, 2), (2, 2), (2, 0)])
+ inner = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+ poly = PolyArea(outer, inner)
+ repair = Repair(11)
+ rpoly, cache = TB.apply(repair, poly)
+ router, rinner = rings(rpoly)
+ @test router == Ring(cart.([(0, 0), (2, 0), (2, 2), (0, 2)]))
+ @test rinner == Ring(cart.([(0, 0), (0, 1), (1, 1), (1, 0)]))
+end
+
+@testitem "Repair(12)" setup = [Setup] begin
+ poly = PolyArea(cart.([(0, 0), (1, 0)]))
+ repair = Repair(12)
+ rpoly, cache = TB.apply(repair, poly)
+ @test rpoly == PolyArea(cart.([(0, 0), (0.5, 0.0), (1, 0)]))
+
+ outer = cart.([(0, 0), (1, 0), (1, 1), (0, 1)])
+ inner = cart.([(1, 2), (2, 3)])
+ poly = PolyArea(outer, inner)
+ repair = Repair(12)
+ rpoly, cache = TB.apply(repair, poly)
+ @test rpoly == PolyArea(outer)
+end
+
+@testitem "Repair fallbacks" setup = [Setup] begin
+ quad = Quadrangle(cart(0, 1, 0), cart(1, 1, 0), cart(1, 0, 0), cart(0, 0, 0))
+ repair = Repair(10)
+ rquad, cache = TB.apply(repair, quad)
+ @test rquad isa Quadrangle
+ @test rquad == quad
+
+ poly1 = PolyArea(cart.([(0, 0), (0, 2), (2, 2), (2, 0)]))
+ poly2 = PolyArea(cart.([(0, 0), (0, 1), (1, 1), (1, 0)]))
+ multi = Multi([poly1, poly2])
+ repair = Repair(11)
+ rmulti, cache = TB.apply(repair, multi)
+ @test rmulti == Multi([repair(poly1), repair(poly2)])
+
+ poly1 = PolyArea(cart.([(0, 0), (0, 2), (2, 2), (2, 0)]))
+ poly2 = PolyArea(cart.([(0, 0), (0, 1), (1, 1), (1, 0)]))
+ gset = GeometrySet([poly1, poly2])
+ repair = Repair(11)
+ rgset, cache = TB.apply(repair, gset)
+ @test rgset == GeometrySet([repair(poly1), repair(poly2)])
+end
+
+@testitem "Bridge" setup = [Setup] begin
+ @test !isaffine(Bridge)
+ δ = T(0.01) * u"m"
+ f = Bridge(δ)
+ @test TB.parameters(f) == (; δ)
+ f = Bridge(T(0.01))
+ @test TB.parameters(f) == (; δ)
+
+ # https://github.com/JuliaGeometry/Meshes.jl/issues/566
+ outer = Ring(cart(6, 4), cart(6, 7), cart(1, 6), cart(1, 1), cart(5, 2))
+ inner₁ = Ring(cart(3, 3), cart(3, 4), cart(4, 3))
+ inner₂ = Ring(cart(2, 5), cart(2, 6), cart(3, 5))
+ poly = PolyArea([outer, inner₁, inner₂])
+ bpoly = poly |> Bridge(T(0.1))
+ @test !hasholes(bpoly)
+ @test nvertices(bpoly) == 15
+
+ # make sure that result is inferred
+ @inferred poly |> Bridge(T(0.1))
+
+ # unique and bridges
+ poly = PolyArea(cart.([(0, 0), (1, 0), (1, 0), (1, 1), (1, 2), (0, 2), (0, 1), (0, 1)]))
+ cpoly = poly |> Repair(0) |> Bridge()
+ @test cpoly == PolyArea(cart.([(0, 0), (1, 0), (1, 1), (1, 2), (0, 2), (0, 1)]))
+
+ # basic ngon tests
+ t = Triangle(cart(0, 0), cart(1, 0), cart(0, 1))
+ @test (t |> Bridge() |> boundary) == boundary(t)
+ q = Quadrangle(cart(0, 0), cart(1, 0), cart(1, 1), cart(0, 1))
+ @test (q |> Bridge() |> boundary) == boundary(q)
+
+ # bridges between holes
+ outer = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
+ hole1 = Ring(cart.([(0.2, 0.2), (0.4, 0.2), (0.4, 0.4), (0.2, 0.4)]))
+ hole2 = Ring(cart.([(0.6, 0.2), (0.8, 0.2), (0.8, 0.4), (0.6, 0.4)]))
+ poly = PolyArea([outer, reverse(hole1), reverse(hole2)])
+ @test vertices(poly) ==
+ cart.([
+ (0, 0),
+ (1, 0),
+ (1, 1),
+ (0, 1),
+ (0.2, 0.2),
+ (0.2, 0.4),
+ (0.4, 0.4),
+ (0.4, 0.2),
+ (0.6, 0.2),
+ (0.6, 0.4),
+ (0.8, 0.4),
+ (0.8, 0.2)
+ ])
+ bpoly = poly |> Bridge(T(0.01))
+ target =
+ cart.([
(-0.0035355339059327372, 0.0035355339059327372),
(0.19646446609406729, 0.20353553390593274),
(0.2, 0.4),
@@ -1019,37 +2242,44 @@
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0)
- ]
- @test all(vertices(bpoly) .≈ target)
-
- poly = Quadrangle(P3(0, 1, 0), P3(1, 1, 0), P3(1, 0, 0), P3(0, 0, 0))
- bpoly = poly |> Bridge()
- @test bpoly isa Quadrangle
- @test bpoly == poly
- end
-
- @testset "Smoothing" begin
- @test !isaffine(LambdaMuSmoothing)
- n, λ, μ = 30, T(0.5), T(0)
- f = LambdaMuSmoothing(n, λ, μ)
- @test TB.parameters(f) == (; n, λ, μ)
-
- # smoothing doesn't change the topology
- trans = LaplaceSmoothing(30)
- @test TB.isrevertible(trans)
- mesh = readply(T, joinpath(datadir, "beethoven.ply"))
- smesh = trans(mesh)
- @test nvertices(smesh) == nvertices(mesh)
- @test nelements(smesh) == nelements(mesh)
- @test topology(smesh) == topology(mesh)
-
- # smoothing doesn't change the topology
- trans = TaubinSmoothing(30)
- @test TB.isrevertible(trans)
- mesh = readply(T, joinpath(datadir, "beethoven.ply"))
- smesh = trans(mesh)
- @test nvertices(smesh) == nvertices(mesh)
- @test nelements(smesh) == nelements(mesh)
- @test topology(smesh) == topology(mesh)
- end
+ ])
+ @test all(vertices(bpoly) .≈ target)
+
+ poly = Quadrangle(cart(0, 1, 0), cart(1, 1, 0), cart(1, 0, 0), cart(0, 0, 0))
+ bpoly = poly |> Bridge()
+ @test bpoly isa Quadrangle
+ @test bpoly == poly
+
+ # bridge with latlon coords
+ outer = latlon.([(0, 0), (0, 90), (90, 90), (90, 0)])
+ hole1 = latlon.([(10, 10), (10, 20), (20, 20), (20, 10)])
+ hole2 = latlon.([(10, 80), (10, 90), (20, 90), (20, 80)])
+ poly = PolyArea([outer, hole1, hole2])
+ bpoly = poly |> Bridge()
+ @test nvertices(bpoly) == 16
+end
+
+@testitem "Smoothing" setup = [Setup] begin
+ @test !isaffine(LambdaMuSmoothing)
+ n, λ, μ = 30, T(0.5), T(0)
+ f = LambdaMuSmoothing(n, λ, μ)
+ @test TB.parameters(f) == (; n, λ, μ)
+
+ # smoothing doesn't change the topology
+ trans = LaplaceSmoothing(30)
+ @test TB.isrevertible(trans)
+ mesh = readply(T, joinpath(datadir, "beethoven.ply"))
+ smesh = trans(mesh)
+ @test nvertices(smesh) == nvertices(mesh)
+ @test nelements(smesh) == nelements(mesh)
+ @test topology(smesh) == topology(mesh)
+
+ # smoothing doesn't change the topology
+ trans = TaubinSmoothing(30)
+ @test TB.isrevertible(trans)
+ mesh = readply(T, joinpath(datadir, "beethoven.ply"))
+ smesh = trans(mesh)
+ @test nvertices(smesh) == nvertices(mesh)
+ @test nelements(smesh) == nelements(mesh)
+ @test topology(smesh) == topology(mesh)
end
diff --git a/test/traversing.jl b/test/traversing.jl
index 121910c34..a79a33ddf 100644
--- a/test/traversing.jl
+++ b/test/traversing.jl
@@ -1,96 +1,77 @@
-@testset "Paths" begin
- grid = CartesianGrid{T}(100, 100)
-
+@testitem "Traversing" setup = [Setup] begin
+ grid = cartgrid(100, 100)
for path in [LinearPath(), RandomPath(), ShiftedPath(LinearPath(), 0), SourcePath(1:3)]
p = traverse(grid, path)
@test length(p) == 100 * 100
end
- @testset "LinearPath" begin
- p = traverse(grid, LinearPath())
- @test p == 1:(100 * 100)
+ grid = cartgrid(100, 100)
+ p = traverse(grid, LinearPath())
+ @test p == 1:(100 * 100)
+
+ grid = cartgrid(100, 100)
+ p = traverse(grid, RandomPath())
+ @test all(1 .≤ collect(p) .≤ 100 * 100)
+ path = RandomPath(StableRNG(123))
+ grid = cartgrid(3, 3)
+ @test traverse(grid, path) == [4, 7, 2, 1, 3, 8, 5, 6, 9]
+
+ grid = cartgrid(3, 3)
+ pset = PointSet(centroid.(grid))
+ for sdomain in [grid, pset]
+ t = traverse(sdomain, SourcePath([1, 9]))
+ @test collect(t) == [1, 9, 2, 4, 6, 8, 5, 3, 7]
+
+ t = traverse(sdomain, SourcePath([1]))
+ @test collect(t) == [1, 2, 4, 5, 3, 7, 6, 8, 9]
end
- @testset "RandomPath" begin
- p = traverse(grid, RandomPath())
- @test all(1 .≤ collect(p) .≤ 100 * 100)
-
- path = RandomPath(MersenneTwister(123))
- grid = CartesianGrid{T}(3, 3)
- @test traverse(grid, path) == [8, 7, 5, 3, 4, 1, 6, 9, 2]
+ grid = cartgrid(3, 3)
+ path = LinearPath()
+ for offset in [0, 1, -1]
+ spath = ShiftedPath(path, offset)
+ t = traverse(grid, path)
+ st = traverse(grid, spath)
+ @test length(st) == 9
+ @test collect(st) == circshift(t, -offset)
end
- @testset "SourcePath" begin
- grid = CartesianGrid{T}(3, 3)
- pset = PointSet(centroid.(grid))
+ path = MultiGridPath()
- for sdomain in [grid, pset]
- p = traverse(sdomain, SourcePath([1, 9]))
- @test collect(p) == [1, 9, 2, 4, 6, 8, 5, 3, 7]
+ grid = cartgrid(3, 3)
+ @test traverse(grid, path) == [1, 3, 7, 9, 2, 4, 5, 6, 8]
- p = traverse(sdomain, SourcePath([1]))
- @test collect(p) == [1, 2, 4, 5, 3, 7, 6, 8, 9]
- end
- end
+ grid = cartgrid(3, 4)
+ @test traverse(grid, path) == [1, 3, 10, 12, 2, 7, 8, 9, 4, 5, 6, 11]
- @testset "ShiftedPath" begin
- grid = CartesianGrid{T}(3, 3)
- path = LinearPath()
- for offset in [0, 1, -1]
- spath = ShiftedPath(path, offset)
- p = traverse(grid, path)
- sp = traverse(grid, spath)
- @test length(sp) == 9
- @test collect(sp) == circshift(p, -offset)
- end
- end
-
- @testset "MultiGridPath" begin
- path = MultiGridPath()
+ grid = CartesianGrid(3, 3, 2)
+ @test traverse(grid, path) == [1, 3, 7, 9, 10, 12, 16, 18, 2, 4, 5, 6, 8, 11, 13, 14, 15, 17]
- grid = CartesianGrid{T}(3, 3)
- @test traverse(grid, path) == [1, 3, 7, 9, 2, 4, 5, 6, 8]
+ grid = RectilinearGrid(T.(0:3), T.(0:3))
+ @test traverse(grid, path) == [1, 3, 7, 9, 2, 4, 5, 6, 8]
- grid = CartesianGrid{T}(3, 4)
- @test traverse(grid, path) == [1, 3, 10, 12, 2, 7, 8, 9, 4, 5, 6, 11]
+ grid = RectilinearGrid(T.(0:0.5:2), T.(0:0.5:2))
+ @test traverse(grid, path) == [1, 4, 13, 16, 3, 9, 11, 2, 5, 6, 7, 8, 10, 12, 14, 15]
- grid = CartesianGrid(3, 3, 2)
- @test traverse(grid, path) == [1, 3, 7, 9, 10, 12, 16, 18, 2, 4, 5, 6, 8, 11, 13, 14, 15, 17]
+ cgrid = cartgrid(4, 4)
+ rgrid = RectilinearGrid(T.(0:4), T.(0:4))
+ @test traverse(cgrid, path) == traverse(rgrid, path)
- grid = RectilinearGrid(T.(0:3), T.(0:3))
- @test traverse(grid, path) == [1, 3, 7, 9, 2, 4, 5, 6, 8]
+ grid = cartgrid(3, 4)
+ vgrid = view(grid, 3:10)
+ @test traverse(vgrid, path) == [3, 10, 7, 8, 9, 4, 5, 6]
- grid = RectilinearGrid(T.(0:0.5:2), T.(0:0.5:2))
- @test traverse(grid, path) == [1, 4, 13, 16, 3, 9, 11, 2, 5, 6, 7, 8, 10, 12, 14, 15]
+ if visualtests
+ paths = [LinearPath(), RandomPath(StableRNG(123)), ShiftedPath(LinearPath(), 10), SourcePath(1:3), MultiGridPath()]
- cgrid = CartesianGrid{T}(4, 4)
- rgrid = RectilinearGrid(T.(0:4), T.(0:4))
- @test traverse(cgrid, path) == traverse(rgrid, path)
-
- grid = CartesianGrid{T}(3, 4)
- vgrid = view(grid, 3:10)
- @test traverse(vgrid, path) == [3, 10, 7, 8, 9, 4, 5, 6]
- end
+ fnames = ["linear-path", "random-path", "shifted-path", "source-path", "multi-grid-path"]
- @testset "Miscellaneous" begin
- if visualtests
- paths = [
- LinearPath(),
- RandomPath(MersenneTwister(123)),
- ShiftedPath(LinearPath(), 10),
- SourcePath(1:3),
- MultiGridPath()
- ]
-
- fnames = ["linear-path", "random-path", "shifted-path", "source-path", "multi-grid-path"]
-
- for (path, fname) in zip(paths, fnames)
- for d in (6, 7)
- grid = CartesianGrid{T}(d, d)
- elems = [grid[i] for i in traverse(grid, path)]
- fig = viz(elems, color=1:length(elems))
- @test_reference "data/$fname-$(d)x$(d).png" fig
- end
+ for (path, fname) in zip(paths, fnames)
+ for d in (6, 7)
+ agrid = cartgrid(d, d)
+ elems = [agrid[i] for i in traverse(agrid, path)]
+ fig = viz(elems, color=1:length(elems))
+ @test_reference "data/$fname-$(d)x$(d).png" fig
end
end
end
diff --git a/test/utils.jl b/test/utils.jl
index a72655b9a..825897847 100644
--- a/test/utils.jl
+++ b/test/utils.jl
@@ -1,31 +1,57 @@
-@testset "Utilities" begin
- a, b, c = P2(0, 0), P2(1, 0), P2(0, 1)
- @test signarea(a, b, c) == T(0.5)
- a, b, c = P2(0, 0), P2(0, 1), P2(1, 0)
- @test signarea(a, b, c) == T(-0.5)
+@testitem "Utilities" setup = [Setup] begin
+ a, b, c = cart(0, 0), cart(1, 0), cart(0, 1)
+ @test signarea(a, b, c) == T(0.5) * u"m^2"
+ a, b, c = cart(0, 0), cart(0, 1), cart(1, 0)
+ @test signarea(a, b, c) == T(-0.5) * u"m^2"
- normals = [V3(1, 0, 0), V3(0, 1, 0), V3(0, 0, 1), V3(-1, 0, 0), V3(0, -1, 0), V3(0, 0, -1), V3(rand(3) .- 0.5)]
+ normals = [
+ vector(1, 0, 0),
+ vector(0, 1, 0),
+ vector(0, 0, 1),
+ vector(-1, 0, 0),
+ vector(0, -1, 0),
+ vector(0, 0, -1),
+ vector(ntuple(i -> rand() - 0.5, 3))
+ ]
for n in normals
u, v = householderbasis(n)
- @test u isa V3
- @test v isa V3
- @test u × v ≈ n ./ norm(n)
+ @test u isa Vec{3}
+ @test v isa Vec{3}
+ @test ustrip.(u × v) ≈ n ./ norm(n)
end
+ n = Vec(T(1) * u"cm", T(1) * u"cm", T(1) * u"cm")
+ u, v = householderbasis(n)
+ @test unit(eltype(u)) == u"cm"
+ @test unit(eltype(v)) == u"cm"
+ n = Vec(T(1) * u"km", T(1) * u"km", T(1) * u"km")
+ u, v = householderbasis(n)
+ @test unit(eltype(u)) == u"km"
+ @test unit(eltype(v)) == u"km"
@test Meshes.mayberound(1.1, 1.0, 0.2) ≈ 1.0
@test Meshes.mayberound(1.1, 1.0, 0.10000000000000001) ≈ 1.1
@test Meshes.mayberound(1.1, 1.0, 0.05) ≈ 1.1
# intersect parameters
- p1, p2 = P2(0, 0), P2(1, 1)
- p3, p4 = P2(1, 0), P2(0, 1)
+ p1, p2 = cart(0, 0), cart(1, 1)
+ p3, p4 = cart(1, 0), cart(0, 1)
@inferred Meshes.intersectparameters(p1, p2, p3, p4)
@inferred Meshes.intersectparameters(p1, p3, p2, p4)
@inferred Meshes.intersectparameters(p1, p2, p1, p2)
- p1, p2 = P3(0, 0, 0), P3(1, 1, 1)
- p3, p4 = P3(1, 0, 0), P3(0, 1, 1)
+ p1, p2 = cart(0, 0, 0), cart(1, 1, 1)
+ p3, p4 = cart(1, 0, 0), cart(0, 1, 1)
@inferred Meshes.intersectparameters(p1, p2, p3, p4)
@inferred Meshes.intersectparameters(p1, p3, p2, p4)
@inferred Meshes.intersectparameters(p1, p2, p1, p2)
+
+ # withcrs
+ c = (T(1), T(1))
+ p = merc(c)
+ v = to(p)
+ @inferred Meshes.withcrs(p, v)
+ @inferred Meshes.withcrs(p, c)
+ c = (T(30), T(60))
+ p = latlon(c) |> Proj(Cartesian)
+ @inferred Meshes.withcrs(p, c, LatLon)
end
diff --git a/test/vectors.jl b/test/vectors.jl
index bf2714b73..3022e76b5 100644
--- a/test/vectors.jl
+++ b/test/vectors.jl
@@ -1,94 +1,71 @@
-@testset "Vectors" begin
+@testitem "Vectors" setup = [Setup] begin
# vararg constructors
- @test eltype(Vec(1, 1)) == Float64
- @test eltype(Vec(1.0, 1.0)) == Float64
- @test eltype(Vec(1.0f0, 1.0f0)) == Float32
- @test eltype(Vec1(1)) == Float64
- @test eltype(Vec2(1, 1)) == Float64
- @test eltype(Vec3(1, 1, 1)) == Float64
- @test eltype(Vec1f(1)) == Float32
- @test eltype(Vec2f(1, 1)) == Float32
- @test eltype(Vec3f(1, 1, 1)) == Float32
+ @test eltype(Vec(1, 1)) == Meshes.Met{Float64}
+ @test eltype(Vec(1.0, 1.0)) == Meshes.Met{Float64}
+ @test eltype(Vec(1.0f0, 1.0f0)) == Meshes.Met{Float32}
# tuple constructors
- @test eltype(Vec((1, 1))) == Float64
- @test eltype(Vec((1.0, 1.0))) == Float64
- @test eltype(Vec((1.0f0, 1.0f0))) == Float32
- @test eltype(Vec1((1,))) == Float64
- @test eltype(Vec2((1, 1))) == Float64
- @test eltype(Vec3((1, 1, 1))) == Float64
- @test eltype(Vec1f((1,))) == Float32
- @test eltype(Vec2f((1, 1))) == Float32
- @test eltype(Vec3f((1, 1, 1))) == Float32
-
- # parametric constructors
- @test eltype(Vec{2,T}(1, 1)) == T
- @test eltype(Vec{2,T}((1, 1))) == T
+ @test eltype(Vec((1, 1))) == Meshes.Met{Float64}
+ @test eltype(Vec((1.0, 1.0))) == Meshes.Met{Float64}
+ @test eltype(Vec((1.0f0, 1.0f0))) == Meshes.Met{Float32}
# check all 1D Vec constructors, because those tend to make trouble
- @test Vec(1) == Vec((1,))
- @test Vec{1,T}(0) == Vec{1,T}((0,))
- @test Vec{1,T}(-2) == Vec{1,T}((-2,))
+ @test Vec(T(1)) == Vec((T(1),))
+ @test Vec(T(0)) == Vec((T(0),))
+ @test Vec(T(-2)) == Vec((T(-2),))
# check that input of mixed coordinate types is allowed and works as expected
- @test Vec(1, 0.2) == Vec{2,Float64}(1.0, 0.2)
- @test Vec((3.0, 4)) == Vec{2,Float64}(3.0, 4.0)
- @test Vec((5.0, 6.0, 7)) == Vec{3,Float64}(5.0, 6.0, 7.0)
- @test Vec{2,T}(8, 9.0) == Vec{2,T}((8.0, 9.0))
- @test Vec{2,T}((-1.0, -2)) == Vec{2,T}((-1, -2.0))
- @test Vec{4,T}((0, -1.0, +2, -4.0)) == Vec{4,T}((0.0f0, -1.0f0, +2.0f0, -4.0f0))
+ @test Vec(1, 0.2) == Vec(1.0, 0.2)
+ @test Vec((3.0, 4)) == Vec(3.0, 4.0)
+ @test Vec((5.0, 6.0, 7)) == Vec(5.0, 6.0, 7.0)
+ @test Vec(8, T(9.0)) == Vec((T(8.0), T(9.0)))
+ @test Vec((T(-1.0), -2)) == Vec((T(-1.0), T(-2.0)))
+ @test Vec((0, T(-1.0), +2, T(-4.0))) == Vec((T(0.0), T(-1.0), T(+2.0), T(-4.0)))
# integer coordinates are converted to float
- @test eltype(Vec(1)) == Float64
- @test eltype(Vec(1, 2)) == Float64
- @test eltype(Vec(1, 2, 3)) == Float64
- @test Tuple(Vec(1)) == (1.0,)
- @test Tuple(Vec(1, 2)) == (1.0, 2.0)
- @test Tuple(Vec(1, 2, 3)) == (1.0, 2.0, 3.0)
+ @test eltype(Vec(1)) == Meshes.Met{Float64}
+ @test eltype(Vec(1, 2)) == Meshes.Met{Float64}
+ @test eltype(Vec(1, 2, 3)) == Meshes.Met{Float64}
+ @test Tuple(Vec(1)) == (1.0u"m",)
+ @test Tuple(Vec(1, 2)) == (1.0u"m", 2.0u"m")
+ @test Tuple(Vec(1, 2, 3)) == (1.0u"m", 2.0u"m", 3.0u"m")
# Unitful coordinates
- vector = Vec(1u"m", 1u"m")
- @test unit(eltype(vector)) == u"m"
- @test Unitful.numtype(eltype(vector)) === Float64
- vector = Vec(1.0u"m", 1.0u"m")
- @test unit(eltype(vector)) == u"m"
- @test Unitful.numtype(eltype(vector)) === Float64
- vector = Vec(1.0f0u"m", 1.0f0u"m")
- @test unit(eltype(vector)) == u"m"
- @test Unitful.numtype(eltype(vector)) === Float32
-
- # throws
- @test_throws DimensionMismatch Vec{2,T}(1)
- @test_throws DimensionMismatch Vec{3,T}((2, 3))
- @test_throws DimensionMismatch Vec{3,T}([2, 3])
- @test_throws DimensionMismatch Vec{-3,T}((4, 5, 6))
- @test_throws DimensionMismatch Vec{-3,T}([4, 5, 6])
+ v = Vec(1u"m", 1u"m")
+ @test unit(eltype(v)) == u"m"
+ @test Unitful.numtype(eltype(v)) === Float64
+ v = Vec(1.0u"m", 1.0u"m")
+ @test unit(eltype(v)) == u"m"
+ @test Unitful.numtype(eltype(v)) === Float64
+ v = Vec(1.0f0u"m", 1.0f0u"m")
+ @test unit(eltype(v)) == u"m"
+ @test Unitful.numtype(eltype(v)) === Float32
# angles between 2D vectors
- @test ∠(V2(1, 0), V2(0, 1)) ≈ T(π / 2)
- @test ∠(V2(1, 0), V2(0, -1)) ≈ T(-π / 2)
- @test ∠(V2(1, 0), V2(-1, 0)) ≈ T(π)
- @test ∠(V2(0, 1), V2(-1, 0)) ≈ T(π / 2)
- @test ∠(V2(0, 1), V2(0, -1)) ≈ T(π)
- @test ∠(V2(0, 1), V2(1, 1)) ≈ T(-π / 4)
- @test ∠(V2(0, -1), V2(1, 1)) ≈ T(π * 3 / 4)
- @test ∠(V2(-1, -1), V2(1, 1)) ≈ T(π)
- @test ∠(V2(-2, 0), V2(2, 0)) ≈ T(π)
+ @test ∠(vector(1, 0), vector(0, 1)) ≈ T(π / 2)
+ @test ∠(vector(1, 0), vector(0, -1)) ≈ T(-π / 2)
+ @test ∠(vector(1, 0), vector(-1, 0)) ≈ T(π)
+ @test ∠(vector(0, 1), vector(-1, 0)) ≈ T(π / 2)
+ @test ∠(vector(0, 1), vector(0, -1)) ≈ T(π)
+ @test ∠(vector(0, 1), vector(1, 1)) ≈ T(-π / 4)
+ @test ∠(vector(0, -1), vector(1, 1)) ≈ T(π * 3 / 4)
+ @test ∠(vector(-1, -1), vector(1, 1)) ≈ T(π)
+ @test ∠(vector(-2, 0), vector(2, 0)) ≈ T(π)
# angles between 3D vectors
- @test ∠(V3(0, 0, 1), V3(1, 1, 0)) ≈ T(π / 2)
- @test ∠(V3(1, 0, 1), V3(1, 1, 0)) ≈ T(π / 3)
- @test ∠(V3(-1, -1, 0), V3(1, 1, 0)) ≈ T(π)
- @test ∠(V3(0, -1, -1), V3(0, 1, 1)) ≈ T(π)
- @test ∠(V3(0, -1, -1), V3(0, 1, 0)) ≈ T(π * 3 / 4)
- @test ∠(V3(0, 1, 1), V3(1, 1, 0)) ≈ T(π / 3)
+ @test ∠(vector(0, 0, 1), vector(1, 1, 0)) ≈ T(π / 2)
+ @test ∠(vector(1, 0, 1), vector(1, 1, 0)) ≈ T(π / 3)
+ @test ∠(vector(-1, -1, 0), vector(1, 1, 0)) ≈ T(π)
+ @test ∠(vector(0, -1, -1), vector(0, 1, 1)) ≈ T(π)
+ @test ∠(vector(0, -1, -1), vector(0, 1, 0)) ≈ T(π * 3 / 4)
+ @test ∠(vector(0, 1, 1), vector(1, 1, 0)) ≈ T(π / 3)
- v = V2(0, 1)
- @test sprint(show, v, context=:compact => true) == "(0.0, 1.0)"
+ v = vector(0, 1)
+ @test sprint(show, v, context=:compact => true) == "(0.0 m, 1.0 m)"
if T === Float32
- @test sprint(show, v) == "Vec(0.0f0, 1.0f0)"
+ @test sprint(show, v) == "Vec(0.0f0 m, 1.0f0 m)"
else
- @test sprint(show, v) == "Vec(0.0, 1.0)"
+ @test sprint(show, v) == "Vec(0.0 m, 1.0 m)"
end
@test sprint(show, MIME("text/plain"), v) == sprint(show, v)
end
diff --git a/test/viewing.jl b/test/viewing.jl
deleted file mode 100644
index 38f340527..000000000
--- a/test/viewing.jl
+++ /dev/null
@@ -1,129 +0,0 @@
-@testset "Viewing" begin
- g = CartesianGrid{T}(10, 10)
- v = view(g, 1:3)
- @test parent(v) == g
- @test parentindices(v) == 1:3
- @test parent(g) == g
- @test parentindices(g) == 1:100
-
- g = CartesianGrid{T}(10, 10)
- b = Box(P2(1, 1), P2(5, 5))
- v = view(g, b)
- @test v == CartesianGrid(P2(0, 0), P2(6, 6), dims=(6, 6))
-
- p = PointSet(collect(vertices(g)))
- v = view(p, b)
- @test centroid(v, 1) == P2(1, 1)
- @test centroid(v, nelements(v)) == P2(5, 5)
-
- g = CartesianGrid{T}(10, 10)
- p = PointSet(collect(vertices(g)))
- b = Ball(P2(0, 0), T(2))
- v = view(g, b)
- @test nelements(v) == 4
- @test v[1] == g[1]
- v = view(p, b)
- @test nelements(v) == 6
- @test coordinates.(v) == V2[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (0, 2)]
-
- # convex polygons
- tri = Triangle(P2(5, 7), P2(10, 12), P2(15, 7))
- pent = Pentagon(P2(6, 1), P2(2, 10), P2(10, 16), P2(18, 10), P2(14, 1))
-
- grid = CartesianGrid{T}(20, 20)
- linds = LinearIndices(size(grid))
- @test linds[10, 10] ∈ indices(grid, tri)
- @test linds[10, 6] ∈ indices(grid, pent)
-
- grid = CartesianGrid(P2(-2, -2), P2(20, 20), T.((0.5, 1.5)))
- linds = LinearIndices(size(grid))
- @test linds[21, 7] ∈ indices(grid, tri)
- @test linds[21, 4] ∈ indices(grid, pent)
-
- grid = CartesianGrid(P2(-100, -100), P2(20, 20), T.((2, 2)))
- linds = LinearIndices(size(grid))
- @test linds[57, 54] ∈ indices(grid, tri)
- @test linds[55, 53] ∈ indices(grid, pent)
-
- # non-convex polygons
- poly1 = PolyArea(P2[(3, 3), (9, 9), (3, 15), (17, 15), (17, 3)])
- poly2 = PolyArea([pointify(pent), pointify(tri)])
-
- grid = CartesianGrid{T}(20, 20)
- linds = LinearIndices(size(grid))
- @test linds[12, 6] ∈ indices(grid, poly1)
- @test linds[10, 3] ∈ indices(grid, poly2)
-
- grid = CartesianGrid(P2(-2, -2), P2(20, 20), T.((0.5, 1.5)))
- linds = LinearIndices(size(grid))
- @test linds[22, 6] ∈ indices(grid, poly1)
- @test linds[17, 4] ∈ indices(grid, poly2)
-
- grid = CartesianGrid(P2(-100, -100), P2(20, 20), T.((2, 2)))
- linds = LinearIndices(size(grid))
- @test linds[57, 54] ∈ indices(grid, poly1)
- @test linds[55, 53] ∈ indices(grid, poly2)
-
- # rotate
- poly1 = poly1 |> Rotate(Angle2d(π / 2))
- poly2 = poly2 |> Rotate(Angle2d(π / 2))
-
- grid = CartesianGrid(P2(-20, 0), P2(0, 20), T.((1, 1)))
- linds = LinearIndices(size(grid))
- @test linds[12, 12] ∈ indices(grid, poly1)
- @test linds[16, 11] ∈ indices(grid, poly2)
-
- grid = CartesianGrid(P2(-22, -2), P2(0, 20), T.((0.5, 1.5)))
- linds = LinearIndices(size(grid))
- @test linds[26, 8] ∈ indices(grid, poly1)
- @test linds[36, 9] ∈ indices(grid, poly2)
-
- grid = CartesianGrid(P2(-100, -100), P2(20, 20), T.((2, 2)))
- linds = LinearIndices(size(grid))
- @test linds[46, 57] ∈ indices(grid, poly1)
- @test linds[48, 55] ∈ indices(grid, poly2)
-
- # multi
- multi = Multi([tri, pent])
- grid = CartesianGrid{T}(20, 20)
- linds = LinearIndices(size(grid))
- @test linds[10, 10] ∈ indices(grid, multi)
- @test linds[10, 6] ∈ indices(grid, multi)
-
- # clipping
- tri = Triangle(P2(-4, 10), P2(5, 19), P2(5, 1))
- grid = CartesianGrid{T}(20, 20)
- linds = LinearIndices(size(grid))
- @test linds[3, 10] ∈ indices(grid, tri)
-
- # out of grid
- tri = Triangle(P2(-12, 8), P2(-8, 14), P2(-4, 8))
- grid = CartesianGrid{T}(20, 20)
- @test isempty(indices(grid, tri))
-
- # chain
- seg = Segment(P2(2, 12), P2(16, 18))
- rope = Rope(P2(8, 1), P2(5, 9), P2(9, 13), P2(17, 10))
- ring = Ring(P2(8, 1), P2(5, 9), P2(9, 13), P2(17, 10))
- grid = CartesianGrid{T}(20, 20)
- linds = LinearIndices(size(grid))
- @test linds[9, 15] ∈ indices(grid, seg)
- @test linds[7, 11] ∈ indices(grid, rope)
- @test linds[12, 5] ∈ indices(grid, ring)
-
- # points
- p1 = P2(0, 0)
- p2 = P2(0.5, 0.5)
- p3 = P2(1, 1)
- p4 = P2(2, 2)
- p5 = P2(10, 10)
- p6 = P2(11, 11)
- grid = CartesianGrid{T}(10, 10)
- linds = LinearIndices(size(grid))
- @test linds[1, 1] == only(indices(grid, p1))
- @test linds[1, 1] == only(indices(grid, p2))
- @test linds[1, 1] == only(indices(grid, p3))
- @test linds[2, 2] == only(indices(grid, p4))
- @test linds[10, 10] == only(indices(grid, p5))
- @test isempty(indices(grid, p6))
-end
diff --git a/test/winding.jl b/test/winding.jl
index 95235a759..936473faa 100644
--- a/test/winding.jl
+++ b/test/winding.jl
@@ -1,18 +1,31 @@
-@testset "winding" begin
- p = P2(0.5, 0.5)
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1)])
+@testitem "Winding numbers" setup = [Setup] begin
+ p = cart(0.5, 0.5)
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1)]))
@test winding(p, c) ≈ T(1)
@test winding(p, reverse(c)) ≈ T(-1)
@test winding([p, p], c) ≈ T[1, 1]
- p = P2(0.5, 0.5)
- c = Ring(P2[(0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1)])
+ p = cart(0.5, 0.5)
+ c = Ring(cart.([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1)]))
@test winding(p, c) ≈ T(2)
@test winding(p, reverse(c)) ≈ T(-2)
@test winding([p, p], c) ≈ T[2, 2]
+ # record allocations for cartesian
+ alloccart = @allocated winding(p, c)
- m = boundary(Box(P3(0, 0, 0), P3(2, 2, 2)))
+ p = merc(0.5, 0.5)
+ c = Ring([merc(0, 0), merc(1, 0), merc(1, 1), merc(0, 1)])
+ @test winding(p, c) ≈ T(1)
+ @test winding(p, reverse(c)) ≈ T(-1)
+ @test winding([p, p], c) ≈ T[1, 1]
+ # record allocations for merc
+ allocmerc = @allocated winding(p, c)
+
+ # exact same memory allocations
+ @test alloccart == allocmerc
+
+ m = boundary(Box(cart(0, 0, 0), cart(2, 2, 2)))
@test all(>(0), winding(vertices(m), m))
- @test isapprox(winding(P3(1, 1, 1), m), T(1), atol=atol(T))
- @test isapprox(winding(P3(3, 3, 3), m), T(0), atol=atol(T))
+ @test isapprox(winding(cart(1, 1, 1), m), T(1), atol=atol(T))
+ @test isapprox(winding(cart(3, 3, 3), m), T(0), atol=atol(T))
end