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