diff --git a/NEWS.md b/NEWS.md index 875e5d5e1..b3d2621ea 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,12 @@ # Changelog +## 1.6.0 +- Feature: Define `reverse` for `AbstractParametricCurve`s, making it easier to reverse the orientation of a curve. See [#195](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/pull/195). +- Bugfix: Fixed an issue with `LineSegment` not returning the exact endpoints at `t=1`, which can be problematic when joining boundary nodes. This has been fixed. See [#195](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/pull/195). +- Bugfix: Introduced `is_linear` to fix issues with boundary enrichment of domains with `LineSegment`s. In particular, `LineSegment`s are no longer enriched. See [#195](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/pull/195). +- Bugfix: `orientation_markers` now uses `uniquetol` instead of `unique` for the final set of markers (it already did it for the intermediate markers). See [#195](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/pull/195). +- Bugfix: For large `Tuple`s, functions like `eval_fnc_at_het_tuple_two_elements` are problematic and allocate more than their non-type-stable counterparts. To get around this, for `Tuple`s of length `N > 32`, the non-type-stable version is used. See [#195](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/pull/195). + ## 1.5.0 - Introduced the ability to reconstruct unconstrained triangulations from an existing set of points and triangles using `Triangulation(points, triangles)`. See [#192](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/pull/192) diff --git a/Project.toml b/Project.toml index a08afb6e0..6772382a7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DelaunayTriangulation" uuid = "927a84f5-c5f4-47a5-9785-b46e178433df" authors = ["Daniel VandenHeuvel "] -version = "1.5.1" +version = "1.6.0" [deps] AdaptivePredicates = "35492f91-a3bd-45ad-95db-fcad7dcfedb7" diff --git a/docs/src/extended/data_structures.md b/docs/src/extended/data_structures.md index a50eafec1..911b547e1 100644 --- a/docs/src/extended/data_structures.md +++ b/docs/src/extended/data_structures.md @@ -130,9 +130,45 @@ CatmullRomSpline ```@autodocs Modules = [DelaunayTriangulation] -Pages = ["data_structures/mesh_refinement/curves.jl"] +Pages = ["data_structures/mesh_refinement/curves/abstract.jl"] ``` +```@autodocs +Modules = [DelaunayTriangulation] +Pages = ["data_structures/mesh_refinement/curves/beziercurve.jl"] +``` + +```@autodocs +Modules = [DelaunayTriangulation] +Pages = ["data_structures/mesh_refinement/curves/bspline.jl"] +``` + +```@autodocs +Modules = [DelaunayTriangulation] +Pages = ["data_structures/mesh_refinement/curves/catmullromspline.jl"] +``` + +```@autodocs +Modules = [DelaunayTriangulation] +Pages = ["data_structures/mesh_refinement/curves/circulararc.jl"] +``` + +```@autodocs +Modules = [DelaunayTriangulation] +Pages = ["data_structures/mesh_refinement/curves/ellipticalarc.jl"] +``` + +```@autodocs +Modules = [DelaunayTriangulation] +Pages = ["data_structures/mesh_refinement/curves/linesegment.jl"] +``` + +```@autodocs +Modules = [DelaunayTriangulation] +Pages = ["data_structures/mesh_refinement/curves/piecewiselinear.jl"] +``` + + ## RepresentativeCoordinates We use a `RepresentativeCoordinates` struct for storing the representative coordinates used for interpreting ghost vertices. diff --git a/src/algorithms/triangulation/check_args.jl b/src/algorithms/triangulation/check_args.jl index d2ea0086b..45dfc49bf 100644 --- a/src/algorithms/triangulation/check_args.jl +++ b/src/algorithms/triangulation/check_args.jl @@ -96,9 +96,10 @@ function Base.showerror(io::IO, err::InconsistentOrientationError) # Only show this longer message if this part of the boundary could be defined by an AbstractParametricCurve. # It's hard to detect if the curve is indeed defined by an AbstractParametricCurve since the curve could be defined # by a combination of multiple AbstractParametricCurves and possibly a PiecewiseLinear part. Thus, the above advice - # might nto be wrong. + # might not be wrong. str2 = "\nIf this curve is defined by an AbstractParametricCurve, you may instead need to reverse the order of the control points defining" * - " the sections of the curve; the `positive` keyword may also be of interest for CircularArcs and EllipticalArcs." + " the sections of the curve; the `positive` keyword may also be of interest for CircularArcs and EllipticalArcs. Alternatively, for individual" * + " AbstractParametricCurves, note that `reverse` can be used to reverse the orientation of the curve directly instead of the control points." str *= str2 end sign = err.should_be_positive ? "positive" : "negative" diff --git a/src/algorithms/triangulation/main.jl b/src/algorithms/triangulation/main.jl index d7d124a46..6850ca042 100644 --- a/src/algorithms/triangulation/main.jl +++ b/src/algorithms/triangulation/main.jl @@ -48,7 +48,7 @@ For the keyword arguments below, you may like to review the extended help as som - `recompute_representative_points=true`: Whether to recompute the representative points after the triangulation is computed. This is done using [`compute_representative_points!`](@ref). - `delete_holes=true`: Whether to delete holes after the triangulation is computed. This is done using [`delete_holes!`](@ref). - `check_arguments=true`: Whether to check the arguments `points` and `boundary_nodes` are valid. This is done using [`check_args`](@ref). -- `polygonise_n=4096`: Number of points to use for polygonising the boundary when considering the poylgon hierarchy for a curve-bounded domain using [`polygonise`](@ref). See [`triangulate_curve_bounded`](@ref). +- `polygonise_n=4096`: Number of points to use for polygonising the boundary when considering the polygon hierarchy for a curve-bounded domain using [`polygonise`](@ref). See [`triangulate_curve_bounded`](@ref). - `coarse_n=0`: Number of points to use for initialising a curve-bounded domain. See [`triangulate_curve_bounded`](@ref). (A value of `0` means the number of points is chosen automatically until the diametral circles of all edges are empty.) # Outputs diff --git a/src/algorithms/triangulation/triangulate_curve_bounded.jl b/src/algorithms/triangulation/triangulate_curve_bounded.jl index e6b9cbac0..5e2a8d665 100644 --- a/src/algorithms/triangulation/triangulate_curve_bounded.jl +++ b/src/algorithms/triangulation/triangulate_curve_bounded.jl @@ -155,7 +155,7 @@ function _coarse_discretisation_multiple_sections!(points, boundary_nodes, bound return nothing end function _coarse_discretisation_contiguous!(points, boundary_nodes, boundary_curves, curve_index, m) - is_piecewise_linear(boundary_curves, curve_index) && return nothing + (is_piecewise_linear(boundary_curves, curve_index) || is_linear(boundary_curves, curve_index)) && return nothing is_while = m == M_INF for _ in 1:m nn = num_boundary_edges(boundary_nodes) diff --git a/src/data_structures.jl b/src/data_structures.jl index c1fb869e9..b9719c129 100644 --- a/src/data_structures.jl +++ b/src/data_structures.jl @@ -6,7 +6,14 @@ include("data_structures/trees/polygon_hierarchy.jl") include("data_structures/triangulation/adjacent.jl") include("data_structures/triangulation/adjacent2vertex.jl") include("data_structures/triangulation/graph.jl") -include("data_structures/mesh_refinement/curves.jl") +include("data_structures/mesh_refinement/curves/abstract.jl") +include("data_structures/mesh_refinement/curves/linesegment.jl") +include("data_structures/mesh_refinement/curves/piecewiselinear.jl") +include("data_structures/mesh_refinement/curves/circulararc.jl") +include("data_structures/mesh_refinement/curves/ellipticalarc.jl") +include("data_structures/mesh_refinement/curves/beziercurve.jl") +include("data_structures/mesh_refinement/curves/bspline.jl") +include("data_structures/mesh_refinement/curves/catmullromspline.jl") include("data_structures/representative_coordinates/representative_coordinates.jl") include("data_structures/representative_coordinates/cell.jl") include("data_structures/representative_coordinates/cell_queue.jl") diff --git a/src/data_structures/mesh_refinement/boundary_enricher.jl b/src/data_structures/mesh_refinement/boundary_enricher.jl index ee2475ec4..ca13df746 100644 --- a/src/data_structures/mesh_refinement/boundary_enricher.jl +++ b/src/data_structures/mesh_refinement/boundary_enricher.jl @@ -730,6 +730,20 @@ end return eval_fnc_at_het_tuple_element(is_piecewise_linear, boundary_curves, curve_index) end +""" + is_linear(enricher::BoundaryEnricher, curve_index) -> Bool + +Returns `true` if the `curve_index`th curve in `enricher` is a [`LineSegment`](@ref), and `false` otherwise. +""" +@inline function is_linear(enricher::BoundaryEnricher, curve_index) + boundary_curves = get_boundary_curves(enricher) + return is_linear(boundary_curves, curve_index) +end +@inline function is_linear(boundary_curves::C, curve_index) where {C <: Tuple} + isempty(boundary_curves) && return true + return eval_fnc_at_het_tuple_element(is_linear, boundary_curves, curve_index) +end + """ get_inverse(enricher::BoundaryEnricher, curve_index, q) -> Float64 diff --git a/src/data_structures/mesh_refinement/curves.jl b/src/data_structures/mesh_refinement/curves.jl deleted file mode 100644 index 879bd151c..000000000 --- a/src/data_structures/mesh_refinement/curves.jl +++ /dev/null @@ -1,2332 +0,0 @@ -const GL_NW = permutedims( - [ - -0.999953919435642 0.00011825670068171721 - -0.9997572122115891 0.00027526103865831744 - -0.9994033532930161 0.00043245460060038196 - -0.9988923197258543 0.0005896003421932499 - -0.998224182256913 0.0007466574055662421 - -0.9973990436722557 0.0009035982480626941 - -0.9964170329848294 0.0010603974326420873 - -0.9952783043339365 0.0012170300415936897 - -0.9939830366739069 0.0013734713364273322 - -0.992531433650476 0.0015296966649272486 - -0.9909237235316215 0.0016856814323170032 - -0.9891601591554368 0.0018414010924821323 - -0.9872410178826077 0.0019968311464057843 - -0.9851666015488034 0.002151947143493035 - -0.9829372364150317 0.002306724684154767 - -0.9805532731150799 0.0024611394229790977 - -0.9780150865996246 0.002615167072191434 - -0.9753230760767991 0.0027687834052614944 - -0.9724776649491127 0.002921964260586262 - -0.9694793007466641 0.00307468554521151 - -0.9663284550566241 0.0032269232385712526 - -0.963025623448975 0.0033786533962332863 - -0.959571325398504 0.0035298521536436056 - -0.9559661042030546 0.0036804957298652166 - -0.9522105268980449 0.003830560431308329 - -0.9483051841672582 0.003980022655449804 - -0.944250690249923 0.0041288588945403645 - -0.94004768284409 0.004277045739298241 - -0.9356968230063247 0.004424559882588365 - -0.931198795047727 0.004571378123086171 - -0.9265543064262952 0.004717477368925247 - -0.9217640876356519 0.0048628346413281485 - -0.9168288920901461 0.005007427078219678 - -0.911749496006352 0.0051512319378220145 - -0.9065266982809823 0.005294226602231066 - -0.901161320365234 0.005436388580973459 - -0.8956542061355872 0.0055776955145435516 - -0.890006221761078 0.0057181251779199384 - -0.8842182555670642 0.00585765548406083 - -0.8782912178955077 0.005996264487377793 - -0.8722260409617927 0.006133930387187249 - -0.866023678708105 0.006270631531139229 - -0.8596851066533939 0.006406346418622805 - -0.85321132173994 0.00654105370414767 - -0.8466033421765535 0.006674732200701343 - -0.8398622072784298 0.006807360883081451 - -0.8329889773036814 0.006938918891202568 - -0.8259847332865807 0.007069385533377089 - -0.818850576867531 0.007198740289569644 - -0.8115876301197994 0.007326962814624488 - -0.804197035373034 0.007454032941465406 - -0.796679955033597 0.007579930684267617 - -0.7890375714017384 0.007704636241601166 - -0.7812710864856423 0.007828129999545307 - -0.7733817218123715 0.007950392534773417 - -0.7653707182357452 0.00807140461760791 - -0.7572393357411735 0.008191147215044737 - -0.7489888532474854 0.008309601493746885 - -0.740620568405778 0.008426748823006541 - -0.732135797395319 0.008542570777675352 - -0.7235358747165359 0.008657049141062358 - -0.7148221529811238 0.008770165907799143 - -0.705996002699303 0.008881903286671764 - -0.6970588120642633 0.008992243703418954 - -0.6880119867338267 0.009101169803496257 - -0.6788569496093618 0.00920866445480557 - -0.6695951406119885 0.00931471075038971 - -0.6602280164561035 0.009419292011091571 - -0.6507570504202663 0.009522391788177445 - -0.641183732115479 0.00962399386592412 - -0.6315095672508971 0.0097240822641693 - -0.6217360773970098 0.009822641240825014 - -0.6118647997463229 0.009919655294353544 - -0.6018972868715882 0.010015109166205537 - -0.5918351064816105 0.010108987843219902 - -0.5816798411746766 0.010201276559985107 - -0.5714330881896406 0.010291960801161504 - -0.5610964591547072 0.010381026303764331 - -0.550671579833952 0.010468459059407015 - -0.5401600898716189 0.010554245316504453 - -0.529563642534233 0.010638371582435857 - -0.5188839044505712 0.010720824625666934 - -0.5081225553495348 0.01080159147783093 - -0.49728128779595404 0.010880659435768353 - -0.48636180692438263 0.01095801606352491 - -0.4753658301709075 0.011033649194307498 - -0.4642950870030309 0.011107546932397785 - -0.4531513186476536 0.011179697655023212 - -0.4419362778172123 0.011250090014185036 - -0.43065172843401034 0.011318712938443162 - -0.4192994453527847 0.011385555634657481 - -0.4078812140815536 0.011450607589685441 - -0.39639883050078745 0.011513858572035563 - -0.38485410058095026 0.011575298633476675 - -0.37324884009845394 0.011634918110602589 - -0.3615848743500686 0.01169270762635197 - -0.34986403786583664 0.011748658091483198 - -0.33808817412053493 0.011802760706003907 - -0.32625913524372974 0.011855006960555099 - -0.3143787817284691 0.011905388637749498 - -0.30244898213866317 0.011953897813463982 - -0.29047161281519085 0.012000526858085933 - -0.27844855758078807 0.012045268437713197 - -0.2663817074437531 0.012088115515307618 - -0.2542729603005287 0.012129061351801793 - -0.24212422063719571 0.012168099507159044 - -0.2299373992299318 0.012205223841386304 - -0.21771441284448234 0.012240428515499802 - -0.20545718393468665 0.012273707992443457 - -0.19316764034011163 0.012305057037959772 - -0.1808477149828374 0.012334470721413042 - -0.16849934556344323 0.012361944416564878 - -0.15612447425624337 0.012387473802301837 - -0.14372504740381872 0.012411054863315053 - -0.1313030152108904 0.01243268389073175 - -0.11886033143758944 0.01245235748269861 - -0.10639895309216538 0.012470072544916794 - -0.09392084012318516 0.01248582629112865 - -0.08142795511126816 0.01249961624355592 - -0.0689222629604074 0.012511440233289444 - -0.05640573058892591 0.012521296400630325 - -0.043880326620117996 0.01252918319538237 - -0.031348021072617915 0.01253509937709596 - -0.01881078505055355 0.012539044015263127 - -0.0062705904335270835 0.012541016489463895 - 0.0062705904335270835 0.012541016489463895 - 0.01881078505055355 0.012539044015263127 - 0.031348021072617915 0.01253509937709596 - 0.043880326620117996 0.01252918319538237 - 0.05640573058892591 0.012521296400630325 - 0.0689222629604074 0.012511440233289444 - 0.08142795511126816 0.01249961624355592 - 0.09392084012318516 0.01248582629112865 - 0.10639895309216538 0.012470072544916794 - 0.11886033143758944 0.01245235748269861 - 0.1313030152108904 0.01243268389073175 - 0.14372504740381872 0.012411054863315053 - 0.15612447425624337 0.012387473802301837 - 0.16849934556344323 0.012361944416564878 - 0.1808477149828374 0.012334470721413042 - 0.19316764034011163 0.012305057037959772 - 0.20545718393468665 0.012273707992443457 - 0.21771441284448234 0.012240428515499802 - 0.2299373992299318 0.012205223841386304 - 0.24212422063719571 0.012168099507159044 - 0.2542729603005287 0.012129061351801793 - 0.2663817074437531 0.012088115515307618 - 0.27844855758078807 0.012045268437713197 - 0.29047161281519085 0.012000526858085933 - 0.30244898213866317 0.011953897813463982 - 0.3143787817284691 0.011905388637749498 - 0.32625913524372974 0.011855006960555099 - 0.33808817412053493 0.011802760706003907 - 0.34986403786583664 0.011748658091483198 - 0.3615848743500686 0.01169270762635197 - 0.37324884009845394 0.011634918110602589 - 0.38485410058095026 0.011575298633476675 - 0.39639883050078745 0.011513858572035563 - 0.4078812140815536 0.011450607589685441 - 0.4192994453527847 0.011385555634657481 - 0.43065172843401034 0.011318712938443162 - 0.4419362778172123 0.011250090014185036 - 0.4531513186476536 0.011179697655023212 - 0.4642950870030309 0.011107546932397785 - 0.4753658301709075 0.011033649194307498 - 0.48636180692438263 0.01095801606352491 - 0.49728128779595404 0.010880659435768353 - 0.5081225553495348 0.01080159147783093 - 0.5188839044505712 0.010720824625666934 - 0.529563642534233 0.010638371582435857 - 0.5401600898716189 0.010554245316504453 - 0.550671579833952 0.010468459059407015 - 0.5610964591547072 0.010381026303764331 - 0.5714330881896406 0.010291960801161504 - 0.5816798411746766 0.010201276559985107 - 0.5918351064816105 0.010108987843219902 - 0.6018972868715882 0.010015109166205537 - 0.6118647997463229 0.009919655294353544 - 0.6217360773970098 0.009822641240825014 - 0.6315095672508971 0.0097240822641693 - 0.641183732115479 0.00962399386592412 - 0.6507570504202663 0.009522391788177445 - 0.6602280164561035 0.009419292011091571 - 0.6695951406119885 0.00931471075038971 - 0.6788569496093618 0.00920866445480557 - 0.6880119867338267 0.009101169803496257 - 0.6970588120642633 0.008992243703418954 - 0.705996002699303 0.008881903286671764 - 0.7148221529811238 0.008770165907799143 - 0.7235358747165359 0.008657049141062358 - 0.732135797395319 0.008542570777675352 - 0.740620568405778 0.008426748823006541 - 0.7489888532474854 0.008309601493746885 - 0.7572393357411735 0.008191147215044737 - 0.7653707182357452 0.00807140461760791 - 0.7733817218123715 0.007950392534773417 - 0.7812710864856423 0.007828129999545307 - 0.7890375714017384 0.007704636241601166 - 0.796679955033597 0.007579930684267617 - 0.804197035373034 0.007454032941465406 - 0.8115876301197994 0.007326962814624488 - 0.818850576867531 0.007198740289569644 - 0.8259847332865807 0.007069385533377089 - 0.8329889773036814 0.006938918891202568 - 0.8398622072784298 0.006807360883081451 - 0.8466033421765535 0.006674732200701343 - 0.85321132173994 0.00654105370414767 - 0.8596851066533939 0.006406346418622805 - 0.866023678708105 0.006270631531139229 - 0.8722260409617927 0.006133930387187249 - 0.8782912178955077 0.005996264487377793 - 0.8842182555670642 0.00585765548406083 - 0.890006221761078 0.0057181251779199384 - 0.8956542061355872 0.0055776955145435516 - 0.901161320365234 0.005436388580973459 - 0.9065266982809823 0.005294226602231066 - 0.911749496006352 0.0051512319378220145 - 0.9168288920901461 0.005007427078219678 - 0.9217640876356519 0.0048628346413281485 - 0.9265543064262952 0.004717477368925247 - 0.931198795047727 0.004571378123086171 - 0.9356968230063247 0.004424559882588365 - 0.94004768284409 0.004277045739298241 - 0.944250690249923 0.0041288588945403645 - 0.9483051841672582 0.003980022655449804 - 0.9522105268980449 0.003830560431308329 - 0.9559661042030546 0.0036804957298652166 - 0.959571325398504 0.0035298521536436056 - 0.963025623448975 0.0033786533962332863 - 0.9663284550566241 0.0032269232385712526 - 0.9694793007466641 0.00307468554521151 - 0.9724776649491127 0.002921964260586262 - 0.9753230760767991 0.0027687834052614944 - 0.9780150865996246 0.002615167072191434 - 0.9805532731150799 0.0024611394229790977 - 0.9829372364150317 0.002306724684154767 - 0.9851666015488034 0.002151947143493035 - 0.9872410178826077 0.0019968311464057843 - 0.9891601591554368 0.0018414010924821323 - 0.9909237235316215 0.0016856814323170032 - 0.992531433650476 0.0015296966649272486 - 0.9939830366739069 0.0013734713364273322 - 0.9952783043339365 0.0012170300415936897 - 0.9964170329848294 0.0010603974326420873 - 0.9973990436722557 0.0009035982480626941 - 0.998224182256913 0.0007466574055662421 - 0.9988923197258543 0.0005896003421932499 - 0.9994033532930161 0.00043245460060038196 - 0.9997572122115891 0.00027526103865831744 - 0.999953919435642 0.00011825670068171721 - ], -) - -""" - abstract type AbstractParametricCurve <: Function end - -Abstract type for representing a parametric curve parametrised over `0 ≤ t ≤ 1`. The curves represented by this -abstract type should not be self-intersecting, with the exception of allowing for closed curves. - -The structs that subtype this abstract type must implement are: -- [`differentiate`](@ref). -- [`twice_differentiate`](@ref). -- [`thrice_differentiate`](@ref) (only if you have not manually defined [`total_variation`](@ref)). -- The struct must be callable so that `c(t)`, where `c` an instance of the struct, returns the associated value of the curve at `t`. -- If the struct does not implement [`point_position_relative_to_curve`](@ref), then the struct must implement [`get_closest_point`](@ref). Alternatively, - rather than implementing [`get_closest_point`](@ref), the struct should have a `lookup_table` field as a `Vector{NTuple{2,Float64}}`, which returns values on the curve at a set of points, - where `lookup_table[i]` is the value of the curve at `t = (i - 1) / (length(lookup_table) - 1)`. - -Functions that are defined for all [`AbstractParametricCurve`](@ref) subtypes are: -- [`arc_length`](@ref) -- [`curvature`](@ref) -- [`total_variation`](@ref) - -!!! note "Efficiently computing the total variation" - - The curves in this package evaluate the total variation not by evaluating the integral itself, but by taking care of the changes in orientation in the curve - to efficiently compute it. This is done by using the orientation markers of the curves, obtained using [`orientation_markers`](@ref), that stored in the field - `orientation_markers` of these curves. The function [`marked_total_variation`](@ref) is then used to evaluate it. You may like to consider using these functions for - any curve you wish to implement yourself, using e.g. the [`BezierCurve`](@ref) struct's implementation as a reference. -""" -abstract type AbstractParametricCurve <: Function end # defined in t ∈ [0, 1] - -Base.show(io::IO, c::C) where {C <: AbstractParametricCurve} = print(io, string(C)) - -""" - is_curve_bounded(c::AbstractParametricCurve) -> Bool - -Returns `true` if `c` is not a [`PiecewiseLinear`](@ref) curve. This is equivalent to `!is_piecewise_linear(c)`. -""" -is_curve_bounded(c::AbstractParametricCurve) = !is_piecewise_linear(c) - -""" - is_piecewise_linear(c::AbstractParametricCurve) -> Bool - -Returns `true` if `c` is [`PiecewiseLinear`](@ref), and `false` otherwise. -""" -is_piecewise_linear(::AbstractParametricCurve) = false - -""" - is_interpolating(c::AbstractParametricCurve) -> Bool - -Returns `true` if `c` goes through all its control points, and `false` otherwise. -""" -is_interpolating(::AbstractParametricCurve) = false - -""" - has_lookup_table(c::AbstractParametricCurve) -> Bool - -Returns `true` if `c` has a lookup table, and `false` otherwise. -""" -has_lookup_table(::AbstractParametricCurve) = false - -@doc """ - arc_length(c::AbstractParametricCurve) -> Float64 - arc_length(c::AbstractParametricCurve, t₁, t₂) -> Float64 - -Returns the arc length of the [`AbstractParametricCurve`] `c`. The second method returns the arc length in the interval `[t₁, t₂]`, where `0 ≤ t₁ ≤ t₂ ≤ 1`. -""" -arc_length - -arc_length(c::AbstractParametricCurve) = arc_length(c, 0.0, 1.0) -function arc_length(c::AbstractParametricCurve, t₁, t₂) - # The integral to evaluate is ∫ √(x′(t)² + y′(t)²) dt - scale = (t₂ - t₁) / 2 - shift = (t₂ + t₁) / 2 - s = 0.0 - for (x, w) in eachcol(GL_NW) - t = scale * x + shift - c′ = differentiate(c, t) - s += w * norm(c′) - end - return scale * s -end - -@doc """ - differentiate(c::AbstractParametricCurve, t) -> NTuple{2, Float64} - -Evaluates the derivative of `c` at `t`. -""" -differentiate - -@doc """ - twice_differentiate(c::AbstractParametricCurve, t) -> NTuple{2, Float64} - -Evaluates the second derivative of `c` at `t`. -""" -twice_differentiate - -@doc """ - thrice_differentiate(c::AbstractParametricCurve, t) -> NTuple{2, Float64} - -Evaluates the third derivative of `c` at `t`. -""" -thrice_differentiate - -""" - curvature(c::AbstractParametricCurve, t) -> Float64 - -Returns the curvature of the [`AbstractParametricCurve`] `c` at `t`. -""" -function curvature(c::AbstractParametricCurve, t) - x′, y′ = getxy(differentiate(c, t)) - x′′, y′′ = getxy(twice_differentiate(c, t)) - return (x′ * y′′ - y′ * x′′) / (x′^2 + y′^2)^(3 / 2) -end - -@doc """ - total_variation(c::AbstractParametricCurve) -> Float64 - total_variation(c::AbstractParametricCurve, t₁, t₂) -> Float64 - -Returns the total variation of a curve `c`, or the subcurve over `[t₁, t₂]` with `0 ≤ t₁ ≤ t₂ ≤ 1`, -defined as the integral of the absolute curvature over this interval. (This is also known as the total absolute curvature.) -""" -total_variation - -total_variation(c::AbstractParametricCurve) = total_variation(c, 0.0, 1.0) -function total_variation(c::AbstractParametricCurve, t₁, t₂) - scale = (t₂ - t₁) / 2 - shift = (t₂ + t₁) / 2 - s = 0.0 - for (x, w) in eachcol(GL_NW) - t = scale * x + shift - κ = abs(curvature(c, t)) - c′ = getxy(differentiate(c, t)) - ds = norm(c′) - s += w * κ * ds - end - return scale * s -end - -""" - marked_total_variation(b::AbstractParametricCurve, t₁, t₂) - -Returns the total variation of the curve `b` over the interval `[t₁, t₂]` using the orientation markers of `b`. -""" -function marked_total_variation(b::AbstractParametricCurve, t₁, t₂) - i₁ = min(lastindex(b.orientation_markers) - 1, searchsortedlast(b.orientation_markers, t₁)) # avoid issues at t = 1 - i₂ = min(lastindex(b.orientation_markers) - 1, searchsortedlast(b.orientation_markers, t₂)) - if i₁ == i₂ - T₁ = differentiate(b, t₁) - T₂ = differentiate(b, t₂) - θ = angle_between(T₁, T₂) - θ > π && (θ = 2π - θ) - return θ - elseif i₁ == i₂ - 1 - T₁ = differentiate(b, t₁) - T₂ = differentiate(b, b.orientation_markers[i₂]) - T₃ = differentiate(b, t₂) - θ₁ = angle_between(T₁, T₂) - θ₁ > π && (θ₁ = 2π - θ₁) - θ₂ = angle_between(T₂, T₃) - θ₂ > π && (θ₂ = 2π - θ₂) - return θ₁ + θ₂ - else - T₁ = differentiate(b, t₁) - T₂ = differentiate(b, b.orientation_markers[i₁ + 1]) - θ = angle_between(T₁, T₂) - θ > π && (θ = 2π - θ) - Δθ = θ - for i in (i₁ + 1):(i₂ - 1) - T₁ = T₂ - T₂ = differentiate(b, b.orientation_markers[i + 1]) - θ = angle_between(T₁, T₂) - θ > π && (θ = 2π - θ) - Δθ += θ - end - T₁ = T₂ - T₂ = differentiate(b, t₂) - θ = angle_between(T₁, T₂) - θ > π && (θ = 2π - θ) - Δθ += θ - return Δθ - end -end - -""" - point_position_relative_to_curve([kernel::AbstractPredicateKernel=AdaptiveKernel(),] e::AbstractParametricCurve, p) -> Certificate - -Returns the position of the point `p` relative to the curve `c`. This function returns a [`Certificate`]: - -- `Left`: `p` is to the left of `c`. -- `Right`: `p` is to the right of `c`. -- `On`: `p` is on `c`. - -The `kernel` argument determines how this result is computed, and should be -one of [`ExactKernel`](@ref), [`FastKernel`](@ref), and [`AdaptiveKernel`](@ref) (the default). -See the documentation for more information about these choices. -""" -function point_position_relative_to_curve(kernel::AbstractPredicateKernel, b::AbstractParametricCurve, p) - t, q = get_closest_point(b, p) - qx, qy = getxy(q) - q′ = differentiate(b, t) - q′x, q′y = getxy(q′) - τx, τy = qx + q′x, qy + q′y - return point_position_relative_to_curve(kernel, LineSegment(q, (τx, τy)), p) -end -point_position_relative_to_curve(b::AbstractParametricCurve, p) = point_position_relative_to_curve(AdaptiveKernel(), b, p) - -""" - convert_lookup_idx(b::AbstractParametricCurve, i) -> Float64 - -Converts the index `i` of the lookup table of the curve `b` to the corresponding `t`-value. -""" -function convert_lookup_idx(b::AbstractParametricCurve, i) - n = length(b.lookup_table) - return (i - 1) / (n - 1) -end - -const PROJECTION_INTERVAL_TOL = 1.0e-12 -""" - get_closest_point(b::AbstractParametricCurve p) -> (Float64, NTuple{2,Float64}) - -Returns the `t`-value and the associated point `q` on the curve `b` that is nearest to `p` using a binary search. The search is done until the -binary search interval is smaller than `1e-12`. This function will only work if the curve `b` has a lookup table. - -!!! danger "Loops" - - This function is only tested on loop-free curves. It is not guaranteed to work on curves with loops. Moreover, for this function to be accurate, - you want the lookup table in `b` to be sufficiently dense. -""" -function get_closest_point(b::AbstractParametricCurve, p) - has_ctrl_points = hasfield(typeof(b), :control_points) - left_flag = has_ctrl_points ? (p == b.control_points[begin]) : (b(0.0) == p) - right_flag = has_ctrl_points ? (p == b.control_points[end]) : (b(1.0) == p) - if left_flag - return 0.0, p - elseif right_flag - return 1.0, p - end - i, δ = _get_closest_point_lookup_table(b, p) - if i == 1 - t, q = _get_closest_point_left_search(b, p, δ) - elseif i == length(b.lookup_table) - t, q = _get_closest_point_right_search(b, p, δ) - else - tmid, pmid = convert_lookup_idx(b, i), b.lookup_table[i] - tleft, pleft = convert_lookup_idx(b, i - 1), b.lookup_table[i - 1] - tright, pright = convert_lookup_idx(b, i + 1), b.lookup_table[i + 1] - t, q = _get_closest_point_interior_search(b, p, tmid, pmid, tleft, pleft, tright, pright, δ) - end - return t, q -end - -function _get_closest_point_lookup_table(b::AbstractParametricCurve, p) - δ = Inf - i = 0 - for (j, q) in enumerate(b.lookup_table) - δ′ = dist_sqr(p, q) - if δ′ < δ - δ = δ′ - i = j - end - end - return i, δ -end -function _get_closest_point_interior_search(b::AbstractParametricCurve, p, tmid, pmid, tleft, pleft, tright, pright, δ) - δleft, δright, δmid = dist_sqr(p, pleft), dist_sqr(p, pright), δ - w = tright - tleft - while w > PROJECTION_INTERVAL_TOL # Keep middle as closest - tleftmid, trightmid = midpoint(tleft, tmid), midpoint(tmid, tright) - pleftmid, prightmid = b(tleftmid), b(trightmid) - δleftmid, δrightmid = dist_sqr(p, pleftmid), dist_sqr(p, prightmid) - if δleftmid < δrightmid - # Choose the left-middle as the new center - tleft, tright, tmid = tleft, tmid, tleftmid - pleft, pright, pmid = pleft, pmid, pleftmid - δleft, δright, δmid = δleft, δmid, δleftmid - else - # Choose the right-middle as the new center - tleft, tright, tmid = tmid, tright, trightmid - pleft, pright, pmid = pmid, pright, prightmid - δleft, δright, δmid = δmid, δright, δrightmid - end - w = tright - tleft - end - return tmid, pmid -end -function _get_closest_point_left_search(b::AbstractParametricCurve, p, δ) - tleft, pleft = 0.0, b.lookup_table[begin] - tright, pright = convert_lookup_idx(b, 2), b.lookup_table[2] - δleft, δright = δ, dist_sqr(p, pright) - tmid = midpoint(tleft, tright) - pmid = b(tmid) - δmid = dist_sqr(p, pmid) - w = tmid - tleft - while δmid > δleft && w > PROJECTION_INTERVAL_TOL - tright, pright, δright = tmid, pmid, δmid - tmid = midpoint(tleft, tmid) - pmid = b(tmid) - δmid = dist_sqr(p, pmid) - end - if δmid < δleft - return _get_closest_point_interior_search(b, p, tmid, pmid, tleft, pleft, tright, pright, δmid) - else - return tleft, pleft - end -end -function _get_closest_point_right_search(b::AbstractParametricCurve, p, δ) - tleft, pleft = convert_lookup_idx(b, length(b.lookup_table) - 1), b.lookup_table[end - 1] - tright, pright = 1.0, b.lookup_table[end] - δleft, δright = dist_sqr(p, pleft), δ - tmid = midpoint(tleft, tright) - pmid = b(tmid) - δmid = dist_sqr(p, pmid) - w = tright - tmid - while δmid > δright && w > PROJECTION_INTERVAL_TOL - tleft, pleft, δleft = tmid, pmid, δmid - tmid = midpoint(tmid, tright) - pmid = b(tmid) - δmid = dist_sqr(p, pmid) - end - if δmid < δright - return _get_closest_point_interior_search(b, p, tmid, pmid, tleft, pleft, tright, pright, δmid) - else - return tright, pright - end -end - -""" - process_roots_and_residuals!(roots, residuals, tol) -> Vector{Float64} - -Processes the roots and residuals of a root-finding algorithm. This function removes all `NaN` values from `roots` and `residuals`, sorts the roots in ascending order, and removes all roots with residuals greater than `tol`. The -returned vector is the vector of roots with duplicates (i.e. roots that are within `tol` of each other) removed. -""" -function process_roots_and_residuals!(roots, residuals, tol) - nan_idx = findall(isnan, roots) - deleteat!(roots, nan_idx) - deleteat!(residuals, nan_idx) - sort_idx = sortperm(roots) - permute!(roots, sort_idx) - permute!(residuals, sort_idx) - bad_idx = Int[] - for (i, resid) in enumerate(residuals) - if resid > tol - push!(bad_idx, i) - end - end - deleteat!(roots, bad_idx) - return uniquetol(roots; tol) -end - -""" - protect_against_bad_division!(roots, residuals, val, i) -> Bool - -Protects against bad division in root-finding algorithms. This function checks if `val` is close to `0` or if `roots[i]` is outside of `[0, 1]`. If either of these conditions are true, then `roots[i]` and `residuals[i]` are set to `NaN` and `true` is returned. Otherwise, `false` is returned. -""" -function protect_against_bad_division!(roots, residuals, val, i) - if abs(val) < 1.0e-8 || roots[i] < 0 || roots[i] > 1 - roots[i] = NaN - residuals[i] = NaN - return true - end - return false -end - -""" - horizontal_turning_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} - -Returns points `t` such that `x'(t) = 0` and `0 ≤ t ≤ 1`, where `x'` is the derivative of the `x`-coordinate of `c`. This function uses Newton's method to find the roots of `x'`. - -!!! danger "High-degree curves" - - For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all - turning points. - -# Arguments -- `c::AbstractParametricCurve`: The curve to find the horizontal turning points of. - -# Keyword Arguments -- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. -- `iters=50`: The number of iterations to run Newton's method for. -- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(x'(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. - -# Output -- `t`: All turning points, given in sorted order. -""" -function horizontal_turning_points(c::AbstractParametricCurve; steps = 200, iters = 50, tol = 1.0e-5) - roots = collect(LinRange(0, 1, steps)) - residuals = fill(Inf, steps) - for i in eachindex(roots) - x′ = getx(differentiate(c, roots[i])) - x′′ = getx(twice_differentiate(c, roots[i])) - roots[i] -= x′ / x′′ - residuals[i] = abs(x′) - for _ in 1:iters - protect_against_bad_division!(roots, residuals, x′′, i) && break - x′ = getx(differentiate(c, roots[i])) - x′′ = getx(twice_differentiate(c, roots[i])) - roots[i] -= x′ / x′′ - residuals[i] = abs(x′) - end - end - return process_roots_and_residuals!(roots, residuals, tol) -end - -""" - vertical_turning_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} - -Returns points `t` such that `y'(t) = 0` and `0 ≤ t ≤ 1`, where `y'` is the derivative of the `y`-coordinate of `c`. This function uses Newton's method to find the roots of `y'`. - -!!! danger "High-degree curves" - - For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all - turning points. - -# Arguments -- `c::AbstractParametricCurve`: The curve to find the vertical turning points of. - -# Keyword Arguments -- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. -- `iters=50`: The number of iterations to run Newton's method for. -- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(y'(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. - -# Output -- `t`: All turning points, given in sorted order. -""" -function vertical_turning_points(c::AbstractParametricCurve; steps = 200, iters = 50, tol = 1.0e-5) - roots = collect(LinRange(0, 1, steps)) - residuals = fill(Inf, steps) - for i in eachindex(roots) - y′ = gety(differentiate(c, roots[i])) - y′′ = gety(twice_differentiate(c, roots[i])) - roots[i] -= y′ / y′′ - residuals[i] = abs(y′) - for _ in 1:iters - protect_against_bad_division!(roots, residuals, y′′, i) && break - y′ = gety(differentiate(c, roots[i])) - y′′ = gety(twice_differentiate(c, roots[i])) - roots[i] -= y′ / y′′ - residuals[i] = abs(y′) - end - end - return process_roots_and_residuals!(roots, residuals, tol) -end - -""" - horizontal_inflection_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} - -Returns points `t` such that `x''(t) = 0` and `0 ≤ t ≤ 1`, where `x''` is the second derivative of the `x`-coordinate of `c`. This function uses Newton's method to find the roots of `x''`. -Note that these are only technically inflection points if `x'''(t) ≠ 0` at these points, but this is not checked. - -!!! danger "High-degree curves" - - For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all - inflection points. - -# Arguments -- `c::AbstractParametricCurve`: The curve to find the horizontal inflection points of. - -# Keyword Arguments -- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. -- `iters=50`: The number of iterations to run Newton's method for. -- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(x''(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. - -# Output -- `t`: All inflection points, given in sorted order. -""" -function horizontal_inflection_points(c::AbstractParametricCurve; steps = 200, iters = 50, tol = 1.0e-5) - roots = collect(LinRange(0, 1, steps)) - residuals = fill(Inf, steps) - for i in eachindex(roots) - x′′ = getx(twice_differentiate(c, roots[i])) - x′′′ = getx(thrice_differentiate(c, roots[i])) - roots[i] -= x′′ / x′′′ - residuals[i] = abs(x′′) - for _ in 1:iters - protect_against_bad_division!(roots, residuals, x′′′, i) && break - x′′ = getx(twice_differentiate(c, roots[i])) - x′′′ = getx(thrice_differentiate(c, roots[i])) - roots[i] -= x′′ / x′′′ - residuals[i] = abs(x′′) - end - end - return process_roots_and_residuals!(roots, residuals, tol) -end - -""" - vertical_inflection_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} - -Returns points `t` such that `y''(t) = 0` and `0 ≤ t ≤ 1`, where `y''` is the second derivative of the `y`-coordinate of `c`. This function uses Newton's method to find the roots of `y''`. -Note that these are only technically inflection points if `y'''(t) ≠ 0` at these points, but this is not checked. - -!!! danger "High-degree curves" - - For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all - inflection points. - -# Arguments -- `c::AbstractParametricCurve`: The curve to find the vertical inflection points of. - -# Keyword Arguments -- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. -- `iters=50`: The number of iterations to run Newton's method for. -- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(y''(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. - -# Output -- `t`: All inflection points, given in sorted order. -""" -function vertical_inflection_points(c::AbstractParametricCurve; steps = 200, iters = 50, tol = 1.0e-5) - roots = collect(LinRange(0, 1, steps)) - residuals = fill(Inf, steps) - for i in eachindex(roots) - y′′ = gety(twice_differentiate(c, roots[i])) - y′′′ = gety(thrice_differentiate(c, roots[i])) - roots[i] -= y′′ / y′′′ - residuals[i] = abs(y′′) - for _ in 1:iters - protect_against_bad_division!(roots, residuals, y′′′, i) && break - y′′ = gety(twice_differentiate(c, roots[i])) - y′′′ = gety(thrice_differentiate(c, roots[i])) - roots[i] -= y′′ / y′′′ - residuals[i] = abs(y′′) - end - end - return process_roots_and_residuals!(roots, residuals, tol) -end - -""" - inflection_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} - -Returns points `t` such that `κ(t) = 0` and `0 ≤ t ≤ 1`, where `κ` is the curvature of `c`. This function uses Newton's method to find the roots of `κ`. - -!!! danger "High-degree curves" - - For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all - inflection points. - -# Arguments -- `c::AbstractParametricCurve`: The curve to find the inflection points of. - -# Keyword Arguments -- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. -- `iters=50`: The number of iterations to run Newton's method for. -- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(κ(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. -""" -function inflection_points(c::AbstractParametricCurve; steps = 200, iters = 50, tol = 1.0e-5) - roots = collect(LinRange(0, 1, steps)) - residuals = fill(Inf, steps) - for i in eachindex(roots) - x′, y′ = getxy(differentiate(c, roots[i])) - x′′, y′′ = getxy(twice_differentiate(c, roots[i])) - x′′′, y′′′ = getxy(thrice_differentiate(c, roots[i])) - κ = x′ * y′′ - y′ * x′′ # don't need to divide by (x′^2 + y′^2)^(3 / 2) since we're only checking if it's zero - κ′ = x′ * y′′′ - y′ * x′′′ - roots[i] -= κ / κ′ - residuals[i] = abs(κ) - for _ in 1:iters - protect_against_bad_division!(roots, residuals, κ′, i) && break - x′, y′ = getxy(differentiate(c, roots[i])) - x′′, y′′ = getxy(twice_differentiate(c, roots[i])) - x′′′, y′′′ = getxy(thrice_differentiate(c, roots[i])) - κ = x′ * y′′ - y′ * x′′ - κ′ = x′ * y′′′ - y′ * x′′′ - roots[i] -= κ / κ′ - residuals[i] = abs(κ) - end - end - return process_roots_and_residuals!(roots, residuals, tol) -end - -""" - orientation_markers(c::AbstractParametricCurve; steps=200, iters=50, tol=1e-5) -> Vector{Float64} - -Finds all orientation markers of the [`AbstractParametricCurve`](@ref) `c`. These are points `t` where any of the following -conditions hold (not necessarily simultaneously), letting `c(t) = (x(t), y(t))`: - -- `x'(t) = 0` -- `y'(t) = 0` -- `κ(t; x) = 0`, where `κ(t; x)` is the curvature of the component function `x(t)` -- `κ(t; y) = 0`, where `κ(t; y)` is the curvature of the component function `y(t)` -- `κ(t) = 0`, where `κ` is the curvature of `c(t)` - -Note that the third and fourth conditions give all the inflection points of the component functions, and similarly for the fifth condition. - -See also [`horizontal_turning_points`](@ref), [`vertical_turning_points`](@ref), [`horizontal_inflection_points`](@ref), [`vertical_inflection_points`](@ref), and [`inflection_points`](@ref). - -!!! danger "High-degree curves" - - For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all - inflection points. - -# Arguments -- `c::AbstractParametricCurve`: The [`AbstractParametricCurve`](@ref). - -# Keyword Arguments -- `steps=200`: The number of equally spaced points to use for initialising Newton's method. -- `iters=50`: How many iterations to use for Newton's method. -- `tol=1e-5`: The tolerance used for determining if two `t`-values are the same. - -# Output -- `markers::Vector{Float64}`: The `t`-values of the orientation markers of `b`. The returned vector is sorted, and also includes the - endpoints `0` and `1`; any `t`-values outside of `[0, 1]` are discarded, and multiplicity - of any `t` is not considered (so the `t`-values in the returned vector are unique). These values can be used to split the curve into monotone pieces, meaning - the orientation is monotone. These markers also guarantee that, over any monotone piece, the orientation changes by an angle of at most `π/2`. -""" -function orientation_markers(c::AbstractParametricCurve; steps = 200, iters = 50, tol = 1.0e-5) - t₁ = horizontal_turning_points(c, steps = steps, iters = iters, tol = tol) - t₂ = vertical_turning_points(c, steps = steps, iters = iters, tol = tol) - t₃ = horizontal_inflection_points(c, steps = steps, iters = iters, tol = tol) - t₄ = vertical_inflection_points(c, steps = steps, iters = iters, tol = tol) - t₅ = inflection_points(c, steps = steps, iters = iters, tol = tol) - all_t = vcat(t₁, t₂, t₃, t₄, t₅) - isempty(all_t) && return [0.0, 1.0] - sort!(all_t) - all_t[1] ≠ 0.0 && pushfirst!(all_t, 0.0) - all_t[end] ≠ 1.0 && push!(all_t, 1.0) - unique!(all_t) - return all_t -end - -""" - get_equidistant_split(c::AbstractParametricCurve, t₁, t₂) -> Float64 - -Returns a value of `t` such that the arc length along `c` from `t₁` to `t` is equal to the arc length along `c` from `t` to `t₂`. -Uses the bisection method to compute the `t`-value. -""" -function get_equidistant_split(c::AbstractParametricCurve, t₁, t₂) - a = t₁ - s₁₂ = arc_length(c, a, t₂) - s = s₁₂ / 2 - t = midpoint(a, t₂) - for _ in 1:100 # limit iterations to 100 - s₁t = arc_length(c, a, t) - if abs(s₁t - s) < 1.0e-3 || abs(t₁ - t₂) < 2.0e-8 - return t - end - s₁t > s ? (t₂ = t) : (t₁ = t) - t = midpoint(t₁, t₂) - end - return t -end - -""" - get_equivariation_split(c::AbstractParametricCurve, t₁, t₂) -> Float64, Float64 - -Returns a value of `t` such that the total variation of `c` from `t₁` to `t` is equal to the total variation of `c` from `t` to `t₂`. -Uses the bisection method to compute the `t`-value. Also returns the new total variation of the two pieces. -""" -function get_equivariation_split(c::AbstractParametricCurve, t₁, t₂) - a = t₁ - Δθ₁₂ = total_variation(c, a, t₂) - Δθ = Δθ₁₂ / 2 - Δθ₁t = Δθ - t = get_equidistant_split(c, a, t₂) - for _ in 1:100 # limit iterations to 100 - Δθ₁t = total_variation(c, a, t) - if abs(Δθ₁t - Δθ) < 1.0e-4 || abs(t₁ - t₂) < 2.0e-8 - return t, Δθ₁t - end - Δθ₁t > Δθ ? (t₂ = t) : (t₁ = t) - t = midpoint(t₁, t₂) - end - return t, Δθ₁t -end - -""" - get_inverse(c::AbstractParametricCurve, p) -> Float64 - -Given a point `p` on `c`, returns the `t`-value such that `c(t) ≈ p`. -""" -function get_inverse(c::AbstractParametricCurve, p) - t, _ = get_closest_point(c, p) - return t -end - -""" - angle_between(c₁::AbstractParametricCurve, c₂::AbstractParametricCurve) -> Float64 - -Given two curves `c₁` and `c₂` such that `c₁(1) == c₂(0)`, returns the angle between the two curves, treating the interior of the -curves as being left of both. -""" -function angle_between(c₁::AbstractParametricCurve, c₂::AbstractParametricCurve) - T₁x, T₁y = getxy(differentiate(c₁, 1.0)) - T₂x, T₂y = getxy(differentiate(c₂, 0.0)) - qx, qy = getxy(c₁(1.0)) - px, py = qx - T₁x, qy - T₁y - rx, ry = qx + T₂x, qy + T₂y - L₁ = LineSegment((px, py), (qx, qy), 0.0) - L₂ = LineSegment((qx, qy), (rx, ry), 0.0) - return angle_between(L₁, L₂) -end - -""" - get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) -> (Float64, NTuple{2,Float64}) - -Given a circle centered at `c(t₁)` with radius `r`, finds the first intersection of the circle with -the curve after `t₁` and less than `t₂`. It is assumed that such an intersection exists. The returned value -is `(t, q)`, where `t` is the parameter value of the intersection and `q` is the point of intersection. -""" -function get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) - tᵢ, tⱼ, p = _get_interval_for_get_circle_intersection(c, t₁, t₂, r) - t = midpoint(tᵢ, tⱼ) - for _ in 1:100 - q = c(t) - δ = dist(p, q) - if abs(δ - r) < 1.0e-3 || abs(tᵢ - tⱼ) < 2.0e-8 - return t, q - end - (δ > r) ? (tⱼ = t) : (tᵢ = t) - t = midpoint(tᵢ, tⱼ) - end - return t, c(t) -end - -""" - _get_interval_for_get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) -> (Float64, Float64, NTuple{2, Float64}) - -Given a circle centered at `c(t₁)` with radius `r`, finds an initial interval for [`get_circle_intersection`](@ref) -to perform bisection on to find a point of intersection. The returned interval is `(tᵢ, tⱼ)`, -where `tᵢ` is the parameter value of the first point in the interval and `tⱼ` -is the parameter value of the last point in the interval. (The interval does not have to be sorted.) The third returned value is `p = c(t₁)`. -""" -function _get_interval_for_get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) - if has_lookup_table(c) - return _get_interval_for_get_circle_intersection_lookup(c, t₁, t₂, r) - else - return _get_interval_for_get_circle_intersection_direct(c, t₁, t₂, r) - end -end -function _get_interval_for_get_circle_intersection_direct(c::AbstractParametricCurve, t₁, t₂, r) - t = LinRange(t₁, t₂, 500) - tᵢ, tⱼ = t₁, t₂ - p = c(t₁) - for τ in t - q = c(τ) - δ = dist(p, q) - if δ > r - tⱼ = τ - break - else - tᵢ = τ - end - end - return tᵢ, tⱼ, p -end -function _get_interval_for_get_circle_intersection_lookup(c::AbstractParametricCurve, t₁, t₂, r) - n = length(c.lookup_table) - p = c(t₁) # the center - i₁ = floor(Int, t₁ * (n - 1)) + 1 - i₂ = ceil(Int, t₂ * (n - 1)) + 1 - i = i₁ - itr = t₁ < t₂ ? (i₁:1:i₂) : (i₁:-1:i₂) # explicit :1 step so that bothr anges are a StepRange - for outer i in itr - t = convert_lookup_idx(c, i) - q = c(t) - δ = dist(p, q) - δ > r && break - end - i, j = i - 1, i # δ = r somewhere inside here - tᵢ, tⱼ = convert_lookup_idx(c, i), convert_lookup_idx(c, j) - return tᵢ, tⱼ, p -end - -""" - LineSegment <: AbstractParametricCurve - -Curve for representing a line segment, parametrised over `0 ≤ t ≤ 1`. This curve can be using -`line_segment(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. - -# Fields -- `first::NTuple{2,Float64}`: The first point of the line segment. -- `last::NTuple{2,Float64}`: The last point of the line segment. -- `length::Float64`: The length of the line segment. - -# Constructor -You can construct a `LineSegment` using - - LineSegment(first, last) -""" -struct LineSegment <: AbstractParametricCurve # line segment - first::NTuple{2, Float64} - last::NTuple{2, Float64} - length::Float64 -end -Base.:(==)(L₁::LineSegment, L₂::LineSegment) = L₁.first == L₂.first && L₁.last == L₂.last - -function LineSegment(p₀, p₁) - return LineSegment(p₀, p₁, dist(p₀, p₁)) -end -function (L::LineSegment)(t) - x₀, y₀ = getxy(L.first) - x₁, y₁ = getxy(L.last) - x = x₀ + t * (x₁ - x₀) - y = y₀ + t * (y₁ - y₀) - return (x, y) -end - -function differentiate(L::LineSegment, t) - x₀, y₀ = getxy(L.first) - x₁, y₁ = getxy(L.last) - return (x₁ - x₀, y₁ - y₀) -end - -twice_differentiate(::LineSegment, t) = (0.0, 0.0) - -curvature(::LineSegment, t) = 0.0 - -total_variation(::LineSegment) = 0.0 -total_variation(::LineSegment, t₁, t₂) = 0.0 - -""" - point_position_relative_to_curve([kernel::AbstractPredicateKernel=AdaptiveKernel(),] L::LineSegment, p) -> Certificate - -Returns the position of `p` relative to `L`, returning a [`Certificate`](@ref): - -- `Left`: `p` is to the left of `L`. -- `Right`: `p` is to the right of `L`. -- `On`: `p` is on `L`. - -See also [`point_position_relative_to_line`](@ref). - -The `kernel` argument determines how this result is computed, and should be -one of [`ExactKernel`](@ref), [`FastKernel`](@ref), and [`AdaptiveKernel`](@ref) (the default). -See the documentation for more information about these choices. -""" -function point_position_relative_to_curve(kernel::AbstractPredicateKernel, L::LineSegment, p) - cert = point_position_relative_to_line(kernel, L.first, L.last, p) - if is_collinear(cert) - return Cert.On - else - return cert - end -end - -arc_length(L::LineSegment) = L.length -arc_length(L::LineSegment, t₁, t₂) = L.length * (t₂ - t₁) - -get_equidistant_split(L::LineSegment, t₁, t₂) = midpoint(t₁, t₂) -get_equivariation_split(L::LineSegment, t₁, t₂) = (midpoint(t₁, t₂), 0.0) - -function get_inverse(L::LineSegment, p) - if p == L.first - return 0.0 - elseif p == L.last - return 1.0 - end - px, py = getxy(p) - x₀, y₀ = getxy(L.first) - x₁, y₁ = getxy(L.last) - if iszero(x₁ - x₀) - return (py - y₀) / (y₁ - y₀) - else - return (px - x₀) / (x₁ - x₀) - end -end - -""" - angle_between(L₁::LineSegment, L₂::LineSegment) -> Float64 - -Returns the angle between `L₁` and `L₂`, assuming that `L₁.last == L₂.first` (this is not checked). For consistency with -If the segments are part of some domain, then the line segments should be oriented so that the interior is to the left of both segments. -""" -function angle_between(L₁::LineSegment, L₂::LineSegment) - T₁ = differentiate(L₁, 1.0) - T₂ = differentiate(L₂, 0.0) - T₁x, T₁y = getxy(T₁) - T₁′ = (-T₁x, -T₁y) - θ = angle_between(T₁′, T₂) - return θ -end - -function get_circle_intersection(L::LineSegment, t₁, t₂, r) - ℓ = L.length - if iszero(t₁) - t = r / ℓ - elseif isone(t₁) - t = 1 - r / ℓ - else - p, q = L(t₁), L(t₂) - Ls = LineSegment(p, q) - return get_circle_intersection(Ls, 0.0, 1.0, r) - end - return t, L(t) -end - -""" - PiecewiseLinear <: AbstractParametricCurve - -Struct for representing a piecewise linear curve. This curve should not be -interacted with or constructed directly. It only exists so that it can be -an [`AbstractParametricCurve`](@ref). Instead, triangulations use this curve to -know that its `boundary_nodes` field should be used instead. - -!!! danger "Existing methods" - - This struct does have fields, namely `points` and `boundary_nodes` (and boundary_nodes should be a contiguous section). These are only used so that - we can use this struct in [`angle_between`](@ref) easily. In particular, we need to allow - for evaluating this curve at `t=0` and at `t=1`, and similarly for differentiating the curve at `t=0` - and at `t=1`. For this, we have defined, letting `L` be a `PiecewiseLinear` curve, `L(0)` to return the first point - on the curve, and the last point otherwise (meaning `L(h)` is constant for `h > 0`), and similarly for differentiation. - Do NOT rely on the implementation of these methods. -""" -struct PiecewiseLinear{P, V} <: AbstractParametricCurve - points::P - boundary_nodes::V -end -Base.show(io::IO, ::PiecewiseLinear) = print(io, "PiecewiseLinear()") -Base.show(io::IO, ::MIME"text/plain", L::PiecewiseLinear) = Base.show(io, L) -Base.:(==)(L1::PiecewiseLinear, L2::PiecewiseLinear) = get_points(L1) == get_points(L2) && get_boundary_nodes(L1) == get_boundary_nodes(L2) -is_piecewise_linear(::PiecewiseLinear) = true -get_points(pl::PiecewiseLinear) = pl.points -get_boundary_nodes(pl::PiecewiseLinear) = pl.boundary_nodes -function (L::PiecewiseLinear)(t) # ONLY FOR EVALUATING AT THE ENDPOINTS. - points = get_points(L) - boundary_nodes = get_boundary_nodes(L) - if iszero(t) - u = get_boundary_nodes(boundary_nodes, 1) - else - n = num_boundary_edges(boundary_nodes) - u = get_boundary_nodes(boundary_nodes, n + 1) - end - p = get_point(points, u) - return p -end -function differentiate(L::PiecewiseLinear, t) - points = get_points(L) - boundary_nodes = get_boundary_nodes(L) - if iszero(t) - u = get_boundary_nodes(boundary_nodes, 1) - v = get_boundary_nodes(boundary_nodes, 2) - else - n = num_boundary_edges(boundary_nodes) - u = get_boundary_nodes(boundary_nodes, n) - v = get_boundary_nodes(boundary_nodes, n + 1) - end - p, q = get_point(points, u), get_point(points, v) - px, py = getxy(p) - qx, qy = getxy(q) - return (qx - px, qy - py) -end - -function get_circle_intersection(L::PiecewiseLinear, t₁, t₂, r) - points = get_points(L) - boundary_nodes = get_boundary_nodes(L) - if iszero(t₁) - u, v = get_boundary_nodes(boundary_nodes, 1), get_boundary_nodes(boundary_nodes, 2) - else - n = num_boundary_edges(boundary_nodes) - u, v = get_boundary_nodes(boundary_nodes, n + 1), get_boundary_nodes(boundary_nodes, n) - end - p, q = get_point(points, u, v) - Ls = LineSegment(p, q) - return get_circle_intersection(Ls, 0.0, 1.0, r) -end - -""" - CircularArc <: AbstractParametricCurve - -Curve for representing a circular arc, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated -using `circular_arc(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. - -# Fields -- `center::NTuple{2,Float64}`: The center of the arc. -- `radius::Float64`: The radius of the arc. -- `start_angle::Float64`: The angle of the initial point of the arc, in radians. -- `sector_angle::Float64`: The angle of the sector of the arc, in radians. This is given by `end_angle - start_angle`, where `end_angle` is the angle at `last`, and so might be negative for negatively oriented arcs. -- `first::NTuple{2,Float64}`: The first point of the arc. -- `last::NTuple{2,Float64}`: The last point of the arc. -- `pqr::NTuple{3, NTuple{2, Float64}}`: Three points on the circle through the arc. This is needed for [`point_position_relative_to_curve`](@ref). - -!!! warning "Orientation" - - The angles `start_angle` and `end_angle` should be setup such that `start_angle > end_angle` implies a positively oriented arc, - and `start_angle < end_angle` implies a negatively oriented arc. Moreover, they must be in `[0°, 2π°)`. - -# Constructor -You can construct a `CircularArc` using - - CircularArc(first, last, center; positive=true) - -It is up to you to ensure that `first` and `last` are equidistant from `center` - the radius used will be the -distance between `center` and `first`. The `positive` keyword argument is used to determine if the -arc is positively oriented or negatively oriented. -""" -struct CircularArc <: AbstractParametricCurve - center::NTuple{2, Float64} - radius::Float64 - start_angle::Float64 - sector_angle::Float64 - first::NTuple{2, Float64} - last::NTuple{2, Float64} - pqr::NTuple{3, NTuple{2, Float64}} -end -function Base.:(==)(c₁::CircularArc, c₂::CircularArc) - c₁.center ≠ c₂.center && return false - c₁.radius ≠ c₂.radius && return false - c₁.start_angle ≠ c₂.start_angle && return false - c₁.sector_angle ≠ c₂.sector_angle && return false - return true -end - -function CircularArc(p, q, c; positive = true) - px, py = getxy(p) - qx, qy = getxy(q) - cx, cy = getxy(c) - r = dist((px, py), (cx, cy)) - θ₀ = mod(atan(py - cy, px - cx), 2π) - if p == q - θ₁ = θ₀ - else - θ₁ = mod(atan(qy - cy, qx - cx), 2π) - end - θ₀, θ₁ = adjust_θ(θ₀, θ₁, positive) - sector_angle = θ₁ - θ₀ - p′ = (cx + r, cy) - q′ = (cx, cy + r) - r′ = (cx - r, cy) - if positive - pqr = (p′, q′, r′) - else - pqr = (r′, q′, p′) - end - return CircularArc(c, r, θ₀, sector_angle, p, q, pqr) -end -function (c::CircularArc)(t) - if iszero(t) - return c.first - elseif isone(t) - return c.last - else - θ₀, Δθ = c.start_angle, c.sector_angle - θ = Δθ * t + θ₀ - sθ, cθ = sincos(θ) - cx, cy = getxy(c.center) - x = cx + c.radius * cθ - y = cy + c.radius * sθ - return (x, y) - end -end - -function differentiate(c::CircularArc, t) - θ₀, Δθ = c.start_angle, c.sector_angle - θ = Δθ * t + θ₀ - sθ, cθ = sincos(θ) - x = -c.radius * sθ - y = c.radius * cθ - return (x * Δθ, y * Δθ) -end - -function twice_differentiate(c::CircularArc, t) - θ₀, Δθ = c.start_angle, c.sector_angle - θ = Δθ * t + θ₀ - sθ, cθ = sincos(θ) - x = -c.radius * cθ - y = -c.radius * sθ - return (x * Δθ^2, y * Δθ^2) -end - -function point_position_relative_to_curve(kernel::AbstractPredicateKernel, c::CircularArc, p) - a, b, c = c.pqr - cert = point_position_relative_to_circle(kernel, a, b, c, p) - if is_outside(cert) - return Cert.Right - elseif is_inside(cert) - return Cert.Left - else - return Cert.On - end -end - -arc_length(c::CircularArc) = c.radius * abs(c.sector_angle) -arc_length(c::CircularArc, t₁, t₂) = c.radius * abs(c.sector_angle) * (t₂ - t₁) - -curvature(c::CircularArc, t) = sign(c.sector_angle) / c.radius - -total_variation(c::CircularArc) = abs(c.sector_angle) -function total_variation(c::CircularArc, t₁, t₂) - Δθ = c.sector_angle - return abs(Δθ) * (t₂ - t₁) -end - -get_equidistant_split(c::CircularArc, t₁, t₂) = midpoint(t₁, t₂) -get_equivariation_split(c::CircularArc, t₁, t₂) = - let t = midpoint(t₁, t₂) - (t, total_variation(c, t₁, t)) -end - -function get_inverse(c::CircularArc, p) - if p == c.first - return 0.0 - elseif p == c.last - return 1.0 - end - px, py = getxy(p) - cx, cy = getxy(c.center) - r = c.radius - cθ = (px - cx) / r - sθ = (py - cy) / r - θ = atan(sθ, cθ) - Δθ, θ₀ = c.sector_angle, c.start_angle - t = (θ - θ₀) / Δθ - while t < 0 - t += 2π / abs(Δθ) - end - while t > 1 - t -= 2π / abs(Δθ) - end - return t -end - -""" - EllipticalArc <: AbstractParametricCurve - -Curve for representing an elliptical arc, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated -using `elliptical_arc(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. - -# Fields -- `center::NTuple{2,Float64}`: The center of the ellipse. -- `horz_radius::Float64`: The horizontal radius of the ellipse. -- `vert_radius::Float64`: The vertical radius of the ellipse. -- `rotation_scales::NTuple{2,Float64}`: If `θ` is the angle of rotation of the ellipse, then this is `(sin(θ), cos(θ))`. -- `start_angle::Float64`: The angle of the initial point of the arc measured from `center`, in radians. This angle is measured from the center prior to rotating the ellipse. -- `sector_angle::Float64`: The angle of the sector of the arc, in radians. This is given by `end_angle - start_angle`, where `end_angle` is the angle at `last`, and so might be negative for negatively oriented arcs. -- `first::NTuple{2,Float64}`: The first point of the arc. -- `last::NTuple{2,Float64}`: The last point of the arc. - -# Constructor -You can construct an `EllipticalArc` using - - EllipticalArc(first, last, center, major_radius, minor_radius, rotation; positive=true) - -where `rotation` is the angle of rotation of the ellipse, in degrees. The `positive` keyword argument is used to determine if the -arc is positively oriented or negatively oriented. -""" -struct EllipticalArc <: AbstractParametricCurve - center::NTuple{2, Float64} - horz_radius::Float64 - vert_radius::Float64 - rotation_scales::NTuple{2, Float64} - start_angle::Float64 - sector_angle::Float64 - first::NTuple{2, Float64} - last::NTuple{2, Float64} -end -function Base.:(==)(e₁::EllipticalArc, e₂::EllipticalArc) - e₁.center ≠ e₂.center && return false - e₁.horz_radius ≠ e₂.horz_radius && return false - e₁.vert_radius ≠ e₂.vert_radius && return false - e₁.rotation_scales ≠ e₂.rotation_scales && return false - e₁.start_angle ≠ e₂.start_angle && return false - e₁.sector_angle ≠ e₂.sector_angle && return false - return true -end - -function EllipticalArc(p, q, c, α, β, θ°; positive = true) - px, py = getxy(p) - qx, qy = getxy(q) - cx, cy = getxy(c) - θ = deg2rad(θ°) - sθ, cθ = sincos(θ) - start_cost = inv(α) * (cθ * (px - cx) + sθ * (py - cy)) - start_sint = inv(β) * (-sθ * (px - cx) + cθ * (py - cy)) - start_angle = mod(atan(start_sint, start_cost), 2π) - if p == q - end_angle = start_angle - else - end_cost = inv(α) * (cθ * (qx - cx) + sθ * (qy - cy)) - end_sint = inv(β) * (-sθ * (qx - cx) + cθ * (qy - cy)) - end_angle = mod(atan(end_sint, end_cost), 2π) - end - start_angle, end_angle = adjust_θ(start_angle, end_angle, positive) - sector_angle = end_angle - start_angle - return EllipticalArc(c, α, β, (sθ, cθ), start_angle, sector_angle, p, q) -end -function (e::EllipticalArc)(t) - if iszero(t) - return e.first - elseif isone(t) - return e.last - else - c, α, β, (sinθ, cosθ), θ₀, Δθ = e.center, e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle - t′ = Δθ * t + θ₀ - st, ct = sincos(t′) - cx, cy = getxy(c) - x = cx + α * ct * cosθ - β * st * sinθ - y = cy + α * ct * sinθ + β * st * cosθ - return (x, y) - end -end - -function differentiate(e::EllipticalArc, t) - α, β, (sinθ, cosθ), θ₀, Δθ = e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle - t′ = Δθ * t + θ₀ - st, ct = sincos(t′) - x = -α * st * cosθ - β * ct * sinθ - y = -α * st * sinθ + β * ct * cosθ - return (x * Δθ, y * Δθ) -end - -function twice_differentiate(e::EllipticalArc, t) - α, β, (sinθ, cosθ), θ₀, Δθ = e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle - t′ = Δθ * t + θ₀ - st, ct = sincos(t′) - x = -α * ct * cosθ + β * st * sinθ - y = -α * ct * sinθ - β * st * cosθ - return (x * Δθ^2, y * Δθ^2) -end - -function curvature(e::EllipticalArc, t) - α, β, θ₀, Δθ = e.horz_radius, e.vert_radius, e.start_angle, e.sector_angle - t′ = Δθ * t + θ₀ - st, ct = sincos(t′) - return sign(Δθ) * α * β / (α^2 * st^2 + β^2 * ct^2)^(3 / 2) -end - -function total_variation(e::EllipticalArc, t₁, t₂) - if e.first == e.last && (t₁ == 0 && t₂ == 1) - return 2π - end - T₁ = differentiate(e, t₁) - T₂ = differentiate(e, t₂) - if e.sector_angle > 0 - θ = angle_between(T₂, T₁) - else - θ = angle_between(T₁, T₂) - end - return θ -end - -function point_position_relative_to_curve(kernel::AbstractPredicateKernel, e::EllipticalArc, p) - x, y = getxy(p) - c, α, β, (sinθ, cosθ), Δθ = e.center, e.horz_radius, e.vert_radius, e.rotation_scales, e.sector_angle - cx, cy = getxy(c) - x′ = x - cx - y′ = y - cy - x′′ = x′ * cosθ + y′ * sinθ - y′′ = -x′ * sinθ + y′ * cosθ - x′′′ = x′′ / α - y′′′ = y′′ / β - positive = Δθ > 0 - if positive - a, b, c = (1.0, 0.0), (0.0, 1.0), (-1.0, 0.0) - else - a, b, c = (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0) - end - cert = point_position_relative_to_circle(kernel, a, b, c, (x′′′, y′′′)) - if is_outside(cert) - return Cert.Right - elseif is_inside(cert) - return Cert.Left - else - return Cert.On - end -end - -function get_inverse(e::EllipticalArc, p) - if p == e.first - return 0.0 - elseif p == e.last - return 1.0 - end - px, py = getxy(p) - c, α, β, (sinθ, cosθ), θ₀, Δθ = e.center, e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle - cx, cy = getxy(c) - px′ = px - cx - py′ = py - cy - ct′ = inv(α) * (px′ * cosθ + py′ * sinθ) - st′ = inv(β) * (-px′ * sinθ + py′ * cosθ) - t′ = mod(atan(st′, ct′), 2π) - t = (t′ - θ₀) / Δθ - while t < 0 - t += 2π / abs(Δθ) - end - while t > 1 - t -= 2π / abs(Δθ) - end - return t -end - -""" - BezierCurve <: AbstractParametricCurve - -Curve for representing a Bezier curve, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated -using `bezier_curve(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. - -A good reference on Bezier curves is [this](https://pomax.github.io/bezierinfo/). - -See also [`BSpline`](@ref) and [`CatmullRomSpline`](@ref). - -!!! danger "Loops" - - This curve is only tested on loop-free curves (and closed curves that otherwise have no self-intersections). It is not guaranteed to work on curves with loops, especially for finding the nearest point on the curve to a given point. - -!!! danger "Interpolation" - - Remember that Bezier curves are not interpolation curves. They only go through the first and last control points, but not the intermediate ones. If you want an interpolation curve, use [`CatmullRomSpline`](@ref). - -# Fields -- `control_points::Vector{NTuple{2,Float64}}`: The control points of the Bezier curve. The curve goes through the first and last control points, but not the intermediate ones. -- `cache::Vector{NTuple{2,Float64}}`: A cache of the points on the curve. This is used to speed up evaluation of the curve using de Casteljau's algorithm. -- `lookup_table::Vector{NTuple{2,Float64}}`: A lookup table for the Bezier curve, used for finding the point on the curve closest to a given point. The `i`th entry of the lookup table - corresponds to the `t`-value `i / (length(lookup_table) - 1)`. -- `orientation_markers::Vector{Float64}`: The orientation markers of the curve. These are defined so that the orientation of the curve is monotone between any two consecutive markers. The first and last markers are always `0` and `1`, respectively. See [`orientation_markers`](@ref). - -!!! warning "Concurrency" - - The cache is not thread-safe, and so you should not evaluate this curve in parallel. - -# Constructor -You can construct a `BezierCurve` using - - BezierCurve(control_points::Vector{NTuple{2,Float64}}; lookup_steps=5000, kwargs...) - -The keyword argument `lookup_steps=100` controls how many time points in `[0, 1]` are used for the lookup table. The `kwargs...` are keyword arguments passed to [`orientation_markers`](@ref). -""" -struct BezierCurve <: AbstractParametricCurve - control_points::Vector{NTuple{2, Float64}} - cache::Vector{NTuple{2, Float64}} - lookup_table::Vector{NTuple{2, Float64}} - orientation_markers::Vector{Float64} -end -function Base.:(==)(b₁::BezierCurve, b₂::BezierCurve) - b₁.control_points ≠ b₂.control_points && return false - return true -end - -function BezierCurve(control_points::Vector{NTuple{2, Float64}}; lookup_steps = 5000, kwargs...) - cache = similar(control_points) # will be copyto! later - lookup_table = similar(control_points, lookup_steps) - markers = Float64[] - spl = BezierCurve(control_points, cache, lookup_table, markers) - for i in 1:lookup_steps - t = (i - 1) / (lookup_steps - 1) - spl.lookup_table[i] = spl(t) - end - markers = orientation_markers(spl; kwargs...) - resize!(spl.orientation_markers, length(markers)) - copyto!(spl.orientation_markers, markers) - return spl -end - -function (b::BezierCurve)(t)::NTuple{2, Float64} - return _eval_bezier_curve(b.control_points, b.cache, t) -end -function de_casteljau!(control_points, t) - if iszero(t) - return control_points[begin] - elseif isone(t) - return control_points[end] - else - n = length(control_points) - 1 - for j in 1:n - for k in 1:(n - j + 1) - xₖ, yₖ = getxy(control_points[k]) - xₖ₊₁, yₖ₊₁ = getxy(control_points[k + 1]) - x = (one(t) - t) * xₖ + t * xₖ₊₁ - y = (one(t) - t) * yₖ + t * yₖ₊₁ - control_points[k] = (x, y) - end - end - return control_points[begin] - end -end -function _eval_bezier_curve(control_points, cache, t) # de Casteljau's algorithm - copyto!(cache, control_points) - return de_casteljau!(cache, t) -end - -function differentiate(b::BezierCurve, t) - copyto!(b.cache, b.control_points) - n = length(b.control_points) - 1 - for i in 1:n - xᵢ, yᵢ = getxy(b.control_points[i]) - xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i + 1]) - b.cache[i] = (n * (xᵢ₊₁ - xᵢ), n * (yᵢ₊₁ - yᵢ)) - end - return @views de_casteljau!(b.cache[begin:(end - 1)], t) -end - -function twice_differentiate(b::BezierCurve, t) - copyto!(b.cache, b.control_points) - n = length(b.control_points) - 1 - if n == 1 - return (0.0, 0.0) - end - for i in 1:(n - 1) - xᵢ, yᵢ = getxy(b.control_points[i]) - xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i + 1]) - xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i + 2]) - # To compute the second derivative control points, we note that e.g. - # for points [A, B, C, D], the first derivative gives [3(B - A), 3(C - B), 3(D - C)]. - # Let these new points be [A', B', C']. Thus, differentiating again, we obtain - # [2(B' - A'), 2(C - B')]. - # So, the ith point is given by - # Qᵢ′′ = (p-1) * (Qᵢ₊₁′ - Qᵢ′), - # where - # Qᵢ′ = p * (Pᵢ₊₁ - Pᵢ). - # Thus, Qᵢ′′ = p(p-1) * (Pᵢ₊₂ - 2Pᵢ₊₁ + Pᵢ). - scale = n * (n - 1) - b.cache[i] = (scale * (xᵢ₊₂ - 2xᵢ₊₁ + xᵢ), scale * (yᵢ₊₂ - 2yᵢ₊₁ + yᵢ)) - end - return @views de_casteljau!(b.cache[begin:(end - 2)], t) -end - -function thrice_differentiate(b::BezierCurve, t) - copyto!(b.cache, b.control_points) - n = length(b.control_points) - 1 - if n ≤ 2 - return (0.0, 0.0) - end - for i in 1:(n - 2) - xᵢ, yᵢ = getxy(b.control_points[i]) - xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i + 1]) - xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i + 2]) - xᵢ₊₃, yᵢ₊₃ = getxy(b.control_points[i + 3]) - # We know that Qᵢ′ = p(Pᵢ₊₁ - Pᵢ), where Qᵢ′ is the ith control point for the first derivative, - # p is the degree of b, and Pᵢ is the ith control point of b. Thus, - # Qᵢ′′ = (p-1)(Qᵢ₊₁′ - Qᵢ′) = p(p-1)(Pᵢ₊₂ - 2Pᵢ₊₁ + Pᵢ), and then - # Qᵢ′′′ = (p-2)(Qᵢ₊₁′′ - Qᵢ′′) = p(p-1)(p-2)(Pᵢ₊₃ - 3Pᵢ₊₂ + 3Pᵢ₊₁ - Pᵢ). - scale = n * (n - 1) * (n - 2) - b.cache[i] = (scale * (xᵢ₊₃ - 3xᵢ₊₂ + 3xᵢ₊₁ - xᵢ), scale * (yᵢ₊₃ - 3yᵢ₊₂ + 3yᵢ₊₁ - yᵢ)) - end - return @views de_casteljau!(b.cache[begin:(end - 3)], t) -end - -total_variation(b::BezierCurve, t₁, t₂) = marked_total_variation(b, t₁, t₂) - -has_lookup_table(b::BezierCurve) = true - -""" - BSpline <: AbstractParametricCurve - -Curve for representing a BSpline, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated -using `b_spline(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. - -See also [`BezierCurve`](@ref) and [`CatmullRomSpline`](@ref). - -Our implementation of a BSpline is based on https://github.com/thibauts/b-spline. - -!!! danger "Loops" - - This curve is only tested on loop-free curves (and closed curves that otherwise have no self-intersections). It is not guaranteed to work on curves with loops, especially for finding the nearest point on the curve to a given point. - -!!! danger "Interpolation" - - Remember that B-spline curves are not interpolation curves. They only go through the first and last control points, but not the intermediate ones. For an interpolating spline, see [`CatmullRomSpline`](@ref). - -# Fields -- `control_points::Vector{NTuple{2,Float64}}`: The control points of the BSpline. The curve goes through the first and last control points, but not the intermediate ones. -- `knots::Vector{Int}`: The knots of the BSpline. You should not modify or set this field directly (in particular, do not expect any support for non-uniform B-splines). -- `cache::Vector{NTuple{2,Float64}}`: A cache of the points on the curve. This is used to speed up evaluation of the curve using de Boor's algorithm. -- `lookup_table::Vector{NTuple{2,Float64}}`: A lookup table for the B-spline curve, used for finding the point on the curve closest to a given point. The `i`th entry of the lookup table - corresponds to the `t`-value `i / (length(lookup_table) - 1)`. -- `orientation_markers::Vector{Float64}`: The orientation markers of the curve. These are defined so that the orientation of the curve is monotone between any two consecutive markers. The first and last markers are always `0` and `1`, respectively. See [`orientation_markers`](@ref). - -# Constructor -You can construct a `BSpline` using - - BSpline(control_points::Vector{NTuple{2,Float64}}; degree=3, lookup_steps=5000, kwargs...) - -The keyword argument `lookup_steps` is used to build the lookup table for the curve. Note that the default -`degree=3` corresponds to a cubic B-spline curve. The `kwargs...` are keyword arguments passed to [`orientation_markers`](@ref). -""" -struct BSpline <: AbstractParametricCurve - control_points::Vector{NTuple{2, Float64}} - knots::Vector{Int} - cache::Vector{NTuple{2, Float64}} - lookup_table::Vector{NTuple{2, Float64}} - orientation_markers::Vector{Float64} -end -function Base.:(==)(b₁::BSpline, b₂::BSpline) - b₁.control_points ≠ b₂.control_points && return false - b₁.knots ≠ b₂.knots && return false - return true -end - -function BSpline(control_points::Vector{NTuple{2, Float64}}; degree = 3, lookup_steps = 5000, kwargs...) - nc = length(control_points) - @assert degree ≥ 1 "Degree must be at least 1, got $degree." - @assert degree ≤ nc - 1 "Degree must be at most n - 1 = $(nc - 1), where n is the number of control points, got $degree." - order = degree + 1 - cache = similar(control_points) - knots = zeros(nc + order) - for i in eachindex(knots) - if i ≤ order - knots[i] = 0 - elseif i < nc + 1 - knots[i] = knots[i - 1] + 1 - else - knots[i] = knots[nc] + 1 - end - end - lookup_table = similar(control_points, lookup_steps) - markers = Float64[] - spl = BSpline(control_points, knots, cache, lookup_table, markers) - for i in 1:lookup_steps - t = (i - 1) / (lookup_steps - 1) - spl.lookup_table[i] = spl(t) - end - markers = orientation_markers(spl; kwargs...) - resize!(spl.orientation_markers, length(markers)) - copyto!(spl.orientation_markers, markers) - return spl -end - -function (b::BSpline)(t)::NTuple{2, Float64} - return _eval_bspline(b.control_points, b.knots, b.cache, t) -end - -function de_boor!(control_points, knots, t) - if iszero(t) - return control_points[begin] - elseif isone(t) - return control_points[end] - end - nc = length(control_points) - nk = length(knots) - order = nk - nc - domain = (order, nc + 1) # nc + 1 = nk - degree (order = degree + 1) - a, b = knots[domain[1]], knots[domain[2]] - t = a + t * (b - a) - s = @views searchsortedfirst(knots[domain[1]:domain[2]], t) + domain[1] - 2 - for L in 1:order - for i in s:-1:(s - order + L + 1) - numerator = t - knots[i] - denominator = knots[i + order - L] - knots[i] - α = numerator / denominator - α′ = 1 - α - xᵢ₋₁, yᵢ₋₁ = getxy(control_points[i - 1]) - xᵢ, yᵢ = getxy(control_points[i]) - control_points[i] = (α′ * xᵢ₋₁ + α * xᵢ, α′ * yᵢ₋₁ + α * yᵢ) - end - end - return control_points[s] -end -function _eval_bspline(control_points, knots, cache, t) # de Boor's algorithm - if iszero(t) - return control_points[begin] - elseif isone(t) - return control_points[end] - end - copyto!(cache, control_points) - return de_boor!(cache, knots, t) -end - -function differentiate(b::BSpline, t) - copyto!(b.cache, b.control_points) - nc = length(b.control_points) - nk = length(b.knots) - degree = nk - nc - 1 - for i in 1:(nc - 1) - xᵢ, yᵢ = getxy(b.control_points[i]) - xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i + 1]) - scale = degree / (b.knots[i + degree + 1] - b.knots[i + 1]) - b.cache[i] = (scale * (xᵢ₊₁ - xᵢ), scale * (yᵢ₊₁ - yᵢ)) - end - deriv = @views de_boor!(b.cache[begin:(end - 1)], b.knots[(begin + 1):(end - 1)], t) - # Need to scale, since the formula used assumes that the knots are all in [0, 1] - range = b.knots[end] - b.knots[begin] - return (deriv[1] * range, deriv[2] * range) -end - -function twice_differentiate(b::BSpline, t) - copyto!(b.cache, b.control_points) - nc = length(b.control_points) - nk = length(b.knots) - degree = nk - nc - 1 - if degree == 1 - return (0.0, 0.0) - end - for i in 1:(nc - 2) - xᵢ, yᵢ = getxy(b.control_points[i]) - xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i + 1]) - xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i + 2]) - scale1 = degree / (b.knots[i + degree + 1] - b.knots[i + 1]) - scale2 = degree / (b.knots[i + degree + 2] - b.knots[i + 2]) - scale3 = (degree - 1) / (b.knots[i + degree + 1] - b.knots[i + 2]) # different shifts between the knots are knots[begin+1:end-1] - Qᵢ′x, Qᵢ′y = (scale1 * (xᵢ₊₁ - xᵢ), scale1 * (yᵢ₊₁ - yᵢ)) - Qᵢ₊₁′x, Qᵢ₊₁′y = (scale2 * (xᵢ₊₂ - xᵢ₊₁), scale2 * (yᵢ₊₂ - yᵢ₊₁)) - b.cache[i] = (scale3 * (Qᵢ₊₁′x - Qᵢ′x), scale3 * (Qᵢ₊₁′y - Qᵢ′y)) - end - deriv = @views de_boor!(b.cache[begin:(end - 2)], b.knots[(begin + 2):(end - 2)], t) - range = (b.knots[end] - b.knots[begin])^2 - return (deriv[1] * range, deriv[2] * range) -end - -function thrice_differentiate(b::BSpline, t) # yes there is a way to evaluate (B, B', B'', B''') all in one pass. just haven't implemented it - copyto!(b.cache, b.control_points) - nc = length(b.control_points) - nk = length(b.knots) - degree = nk - nc - 1 - if degree ≤ 2 - return (0.0, 0.0) - end - for i in 1:(nc - 3) - xᵢ, yᵢ = getxy(b.control_points[i]) - xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i + 1]) - xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i + 2]) - xᵢ₊₃, yᵢ₊₃ = getxy(b.control_points[i + 3]) - scale1 = degree / (b.knots[i + degree + 1] - b.knots[i + 1]) - scale2 = degree / (b.knots[i + degree + 2] - b.knots[i + 2]) - scale3 = degree / (b.knots[i + degree + 3] - b.knots[i + 3]) - scale4 = (degree - 1) / (b.knots[i + degree + 1] - b.knots[i + 2]) - scale5 = (degree - 1) / (b.knots[i + degree + 2] - b.knots[i + 3]) - scale6 = (degree - 2) / (b.knots[i + degree + 1] - b.knots[i + 3]) - Qᵢ′x, Qᵢ′y = (scale1 * (xᵢ₊₁ - xᵢ), scale1 * (yᵢ₊₁ - yᵢ)) - Qᵢ₊₁′x, Qᵢ₊₁′y = (scale2 * (xᵢ₊₂ - xᵢ₊₁), scale2 * (yᵢ₊₂ - yᵢ₊₁)) - Qᵢ₊₂′x, Qᵢ₊₂′y = (scale3 * (xᵢ₊₃ - xᵢ₊₂), scale3 * (yᵢ₊₃ - yᵢ₊₂)) - Qᵢ′′x, Qᵢ′′y = (scale4 * (Qᵢ₊₁′x - Qᵢ′x), scale4 * (Qᵢ₊₁′y - Qᵢ′y)) - Qᵢ₊₁′′x, Qᵢ₊₁′′y = (scale5 * (Qᵢ₊₂′x - Qᵢ₊₁′x), scale5 * (Qᵢ₊₂′y - Qᵢ₊₁′y)) - b.cache[i] = (scale6 * (Qᵢ₊₁′′x - Qᵢ′′x), scale6 * (Qᵢ₊₁′′y - Qᵢ′′y)) - end - deriv = @views de_boor!(b.cache[begin:(end - 3)], b.knots[(begin + 3):(end - 3)], t) - range = (b.knots[end] - b.knots[begin])^3 - return (deriv[1] * range, deriv[2] * range) -end - -total_variation(b::BSpline, t₁, t₂) = marked_total_variation(b, t₁, t₂) - -has_lookup_table(b::BSpline) = true - -""" - CatmullRomSplineSegment <: AbstractParametricCurve - -A single segment of a Camtull-Rom spline, representing by a cubic polynomial. Note that evaluating this curve will only -draw within the two interior control points of the spline. - -Based on [this article](https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html). - -# Fields -- `a::NTuple{2,Float64}`: The coefficient on `t³`. -- `b::NTuple{2,Float64}`: The coefficient on `t²`. -- `c::NTuple{2,Float64}`: The coefficient on `t`. -- `d::NTuple{2,Float64}`: The constant in the polynomial. -- `p₁::NTuple{2,Float64}`: The second control point of the segment. -- `p₂::NTuple{2,Float64}`: The third control point of the segment. - -With these fields, the segment is parametrised over `0 ≤ t ≤ 1` by `q(t)`, where - - q(t) = at³ + bt² + ct + d, - -and `q(0) = p₁` and `q(1) = p₂`, where the segment is defined by four control points `p₀`, `p₁`, `p₂`, and `p₃`. - -This struct is callable, returning the interpolated point `(x, y)` at `t` as a `NTuple{2,Float64}`. - -# Constructor -To construct this segment, use - - catmull_rom_spline_segment(p₀, p₁, p₂, p₃, α, τ) - -Here, `p₀`, `p₁`, `p₂`, and `p₃` are the four points of the segment (not `a`, `b`, `c`, and `d`), and `α` and `τ` are the parameters of the spline. The parameter `α` -controls the type of the parametrisation, where - -- `α = 0`: Uniform parametrisation. -- `α = 1/2`: Centripetal parametrisation. -- `α = 1`: Chordal parametrisation. - -The parameter `τ` is the tension, and controls the tightness of the segment. `τ = 0` is the least tight, while `τ = 1` leads to straight lines between the -control points. Both `α` and `τ` must be in `[0, 1]`. -""" -struct CatmullRomSplineSegment <: AbstractParametricCurve - a::NTuple{2, Float64} - b::NTuple{2, Float64} - c::NTuple{2, Float64} - d::NTuple{2, Float64} - p₁::NTuple{2, Float64} - p₂::NTuple{2, Float64} -end -function (c::CatmullRomSplineSegment)(t) - if iszero(t) - return c.p₁ - elseif isone(t) - return c.p₂ - else - ax, ay = getxy(c.a) - bx, by = getxy(c.b) - cx, cy = getxy(c.c) - dx, dy = getxy(c.d) - cx = evalpoly(t, (dx, cx, bx, ax)) - cy = evalpoly(t, (dy, cy, by, ay)) - return (cx, cy) - end -end - -function catmull_rom_spline_segment(p₀, p₁, p₂, p₃, α, τ) - x₀, y₀ = getxy(p₀) - x₁, y₁ = getxy(p₁) - x₂, y₂ = getxy(p₂) - x₃, y₃ = getxy(p₃) - t₀₁ = dist(p₀, p₁)^α - t₁₂ = dist(p₁, p₂)^α - t₂₃ = dist(p₂, p₃)^α - τ′ = one(τ) - τ - if iszero(τ′) - m₁x, m₁y, m₂x, m₂y = zero(τ), zero(τ), zero(τ), zero(τ) - else - m₁x = τ′ * (x₂ - x₁ + t₁₂ * ((x₁ - x₀) / t₀₁ - (x₂ - x₀) / (t₀₁ + t₁₂))) - m₁y = τ′ * (y₂ - y₁ + t₁₂ * ((y₁ - y₀) / t₀₁ - (y₂ - y₀) / (t₀₁ + t₁₂))) - m₂x = τ′ * (x₂ - x₁ + t₁₂ * ((x₃ - x₂) / t₂₃ - (x₃ - x₁) / (t₁₂ + t₂₃))) - m₂y = τ′ * (y₂ - y₁ + t₁₂ * ((y₃ - y₂) / t₂₃ - (y₃ - y₁) / (t₁₂ + t₂₃))) - end - ax = 2(x₁ - x₂) + m₁x + m₂x - ay = 2(y₁ - y₂) + m₁y + m₂y - bx = -3(x₁ - x₂) - 2m₁x - m₂x - by = -3(y₁ - y₂) - 2m₁y - m₂y - cx = m₁x - cy = m₁y - dx = x₁ - dy = y₁ - a = (ax, ay) - b = (bx, by) - c = (cx, cy) - d = (dx, dy) - return CatmullRomSplineSegment(a, b, c, d, p₁, p₂) -end - -function differentiate(c::CatmullRomSplineSegment, t) - ax, ay = getxy(c.a) - bx, by = getxy(c.b) - cx, cy = getxy(c.c) - a′x, a′y = 3ax, 3ay - b′x, b′y = 2bx, 2by - x = evalpoly(t, (cx, b′x, a′x)) - y = evalpoly(t, (cy, b′y, a′y)) - return (x, y) -end - -function twice_differentiate(c::CatmullRomSplineSegment, t) - ax, ay = getxy(c.a) - bx, by = getxy(c.b) - a′′x, a′′y = 6ax, 6ay - b′′x, b′′y = 2bx, 2by - x = evalpoly(t, (b′′x, a′′x)) - y = evalpoly(t, (b′′y, a′′y)) - return (x, y) -end - -function thrice_differentiate(c::CatmullRomSplineSegment, t) - ax, ay = getxy(c.a) - a′′′x, a′′′y = 6ax, 6ay - return (a′′′x, a′′′y) -end - -""" - CatmullRomSpline <: AbstractParametricCurve - -Curve for representing a Catmull-Rom spline, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated -using `catmull_rom_spline(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. - -For information on these splines, see e.g. [this article](https://people.engr.tamu.edu/schaefer/research/cr_cad.pdf) and [this article](https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html). -Additionally, [this article](https://splines.readthedocs.io/en/latest/euclidean/catmull-rom-properties.html) lists some nice properties of these splines. - -!!! danger "Loops" - - This curve is only tested on loop-free curves (and closed curves that otherwise have no self-intersections). It is not guaranteed to work on curves with loops, especially for finding the nearest point on the curve to a given point. - -!!! note "Extension" - - Typically, Catmull-Rom splines are defined on segments of four control points, and drawn between the two interior control points. - This creates an issue in that the first and last control points will not be joined to the spline. To overcome this, we extend the spline to the left - and right during the evaluation of a spline, using the fields `left` and `right` defined below. The rules used for extending these points come from [CatmullRom.jl](https://github.com/JeffreySarnoff/CatmullRom.jl), - which extrapolates based on a Thiele-like cubic polynomial. - -# Fields -- `control_points::Vector{NTuple{2,Float64}}`: The control points of the Catmull-Rom spline. The curve goes through each point. -- `knots::Vector{Float64}`: The parameter values of the Catmull-Rom spline. The `i`th entry of this vector corresponds to the `t`-value associated with the `i`th control point. - With an alpha parameter `α`, these values are given by `knots[i+1] = knots[i] + dist(control_points[i], control_points[i+1])^α`, where `knots[1] = 0`, - and the vector is the normalised by dividing by `knots[end]`. -- `lookup_table::Vector{NTuple{2,Float64}}`: A lookup table for the Catmull-Rom spline, used for finding the point on the curve closest to a given point. The `i`th entry of the lookup table - corresponds to the `t`-value `i / (length(lookup_table) - 1)`. -- `alpha::Float64`: The alpha parameter of the Catmull-Rom spline. This controls the type of the parametrisation, where `alpha = 0` corresponds to uniform parametrisation, - `alpha = 1/2` corresponds to centripetal parametrisation, and `alpha = 1` corresponds to chordal parametrisation. Must be in `[0, 1]`. For reasons similar to what we describe for `tension` below, we only support - `alpha = 1/2` for now. (If you do really want to change it, use the `_alpha` keyword argument in the constructor.) -- `tension::Float64`: The tension parameter of the Catmull-Rom spline. This controls the tightness of the spline, with `tension = 0` being the least tight, and `tension = 1` leading to straight lines between the control points. Must be in `[0, 1]`. - You can not currently set this to anything except `0.0` due to numerical issues with boundary refinement. (For example, equivariation splits are not possible if `tension=1` since - the curve is piecewise linear in that case, and for `tension` very close to `1`, the equivariation split is not always between the provided times. If you _really_ want to change it, then you - can use the `_tension` keyword argument in the constructor - but be warned that this may lead to numerical issues and potentially infinite loops.) -- `left::NTuple{2,Float64}`: The left extension of the spline. This is used to evaluate the spline on the first segment. -- `right::NTuple{2,Float64}`: The right extension of the spline. This is used to evaluate the spline on the last segment. -- `lengths::Vector{Float64}`: The lengths of the individual segments of the spline. -- `segments::Vector{CatmullRomSplineSegment}`: The individual segments of the spline. -- `orientation_markers::Vector{Float64}`: The orientation markers of the curve. These are defined so that the orientation of the curve is monotone between any two consecutive markers. The first and last markers are always `0` and `1`, respectively. See [`orientation_markers`](@ref). - -# Constructor -To construct a `CatmullRomSpline`, use - - CatmullRomSpline(control_points::Vector{NTuple{2,Float64}}; lookup_steps=5000, kwargs...) - -The keyword argument `lookup_steps` is used to build the lookup table for the curve, with `lookup_steps` giving the number of time points in `[0, 1]` used for the lookup table. -The `kwargs...` are keyword arguments passed to [`orientation_markers`](@ref). -""" -struct CatmullRomSpline <: AbstractParametricCurve - control_points::Vector{NTuple{2, Float64}} - knots::Vector{Float64} - lookup_table::Vector{NTuple{2, Float64}} - alpha::Float64 - tension::Float64 - left::NTuple{2, Float64} - right::NTuple{2, Float64} - lengths::Vector{Float64} - segments::Vector{CatmullRomSplineSegment} - orientation_markers::Vector{Float64} -end -function Base.:(==)(spl1::CatmullRomSpline, spl2::CatmullRomSpline) - spl1.control_points ≠ spl2.control_points && return false - spl1.knots ≠ spl2.knots && return false - spl1.alpha ≠ spl2.alpha && return false - spl1.tension ≠ spl2.tension && return false - return true -end - -is_interpolating(spl::CatmullRomSpline) = true - -function CatmullRomSpline(control_points; _alpha = 1 / 2, _tension = 0.0, lookup_steps = 5000, kwargs...) - alpha = _alpha - tension = _tension - @assert length(control_points) ≥ 4 "Catmull-Rom splines require at least 4 control points, got $(length(control_points))." - nc = length(control_points) - @assert 0 ≤ alpha ≤ 1 "Alpha must be in [0, 1], got $alpha." - @assert 0 ≤ tension ≤ 1 "Tension must be in [0, 1], got $tension." - knots = zeros(nc) - for i in 2:nc - knots[i] = knots[i - 1] + dist(control_points[i - 1], control_points[i])^alpha - end - left = extend_left_control_point(control_points) - right = extend_right_control_point(control_points) - scale = knots[end] - knots ./= scale - knots[end] = 1.0 - lookup_table = similar(control_points, lookup_steps) - lengths = zeros(nc - 1) - markers = Float64[] - segments = Vector{CatmullRomSplineSegment}(undef, nc - 1) - spl = CatmullRomSpline(control_points, knots, lookup_table, alpha, tension, left, right, lengths, segments, markers) - for i in 1:(nc - 1) - spl.segments[i] = _get_segment(spl, i) - end - for i in 1:lookup_steps - t = (i - 1) / (lookup_steps - 1) - spl.lookup_table[i] = spl(t) - end - for i in 1:(nc - 1) - segment = get_segment(spl, i) - spl.lengths[i] = arc_length(segment, 0.0, 1.0) - end - markers = orientation_markers(spl; kwargs...) - resize!(spl.orientation_markers, length(markers)) - copyto!(spl.orientation_markers, markers) - return spl -end - -function extend_left_control_point(control_points) - is_closed = control_points[begin] == control_points[end] - if is_closed - return control_points[end - 1] - else - c₁, c₂, c₃, c₄ = control_points[begin], control_points[begin + 1], control_points[begin + 2], control_points[begin + 3] - x₁, x₂ = getx(c₁), getx(c₂) - reverse_flag = x₁ == x₂ - if reverse_flag - c₁, c₂, c₃, c₄ = reverse(getxy(c₁)), reverse(getxy(c₂)), reverse(getxy(c₃)), reverse(getxy(c₄)) - x₁, x₂ = getx(c₁), getx(c₂) - end - x = 2x₁ - x₂ # reflection - y = thiele4(c₁, c₂, c₃, c₄, x) - if !reverse_flag - return (x, y) - else - return (y, x) - end - end -end -function extend_right_control_point(control_points) - is_closed = control_points[begin] == control_points[end] - if is_closed - return control_points[begin + 1] - else - cₙ₋₃, cₙ₋₂, cₙ₋₁, cₙ = control_points[end - 3], control_points[end - 2], control_points[end - 1], control_points[end] - xₙ₋₁, xₙ = getx(cₙ₋₁), getx(cₙ) - reverse_flag = xₙ₋₁ == xₙ - if reverse_flag - cₙ₋₃, cₙ₋₂, cₙ₋₁, cₙ = reverse(getxy(cₙ₋₃)), reverse(getxy(cₙ₋₂)), reverse(getxy(cₙ₋₁)), reverse(getxy(cₙ)) - xₙ₋₁, xₙ = getx(cₙ₋₁), getx(cₙ) - end - x = 2xₙ - xₙ₋₁ - y = thiele4(cₙ₋₃, cₙ₋₂, cₙ₋₁, cₙ, x) - if !reverse_flag - return (x, y) - else - return (y, x) - end - end -end - -# See https://github.com/JeffreySarnoff/CatmullRom.jl/tree/49e6536c184dfc4200b980f89aa55bc5cf357b82/src/fewpoints -function thiele4(p₁, p₂, p₃, p₄, x) - x₁, y₁ = getxy(p₁) - x₂, y₂ = getxy(p₂) - x₃, y₃ = getxy(p₃) - x₄, y₄ = getxy(p₄) - y = thiele4(x₁, x₂, x₃, x₄, y₁, y₂, y₃, y₄, x) - if !isfinite(y) - if x ≤ x₃ - y = thiele3(p₁, p₂, p₃, x) - else - y = thiele3(p₂, p₃, p₄, x) - end - end - return y -end -function thiele4(x₁, x₂, x₃, x₄, y₁, y₂, y₃, y₄, x) - t₂ = (x₂ - x₃) / (y₂ - y₃) - t₁ = (x₁ - x₂) / (y₁ - y₂) - t₄ = -(x₃ - x₄) / (y₃ - y₄) + t₂ - t₃ = (x₁ - x₃) / (t₁ - t₂) - t₄ = -(x₂ - x₄) / t₄ + y₂ - y₃ + t₃ - t₂ = -(x₁ - x₄) / t₄ + t₁ - t₂ - t₂ = (x₃ - x) / t₂ - y₁ + y₂ + t₃ - t₁ = -(x₂ - x) / t₂ + t₁ - y = -(x₁ - x) / t₁ + y₁ - return y -end -function thiele3(p₁, p₂, p₃, x) - x₁, y₁ = getxy(p₁) - x₂, y₂ = getxy(p₂) - x₃, y₃ = getxy(p₃) - y = thiele3(x₁, x₂, x₃, y₁, y₂, y₃, x) - if !isfinite(y) - y = quadratic_interp(p₁, p₂, p₃, x) - end - return y -end -function thiele3(x₁, x₂, x₃, y₁, y₂, y₃, x) - t₁ = (x₁ - x₂) / (y₁ - y₂) - t₂ = -(x₂ - x₃) / (y₂ - y₃) + t₁ - t₂ = (x₁ - x₃) / t₂ - y₁ + y₂ - t₁ = -(x₂ - x) / t₂ + t₁ - y = -(x₁ - x) / t₁ + y₁ - return y -end -function quadratic_interp(p₁, p₂, p₃, x) - x₁, y₁ = getxy(p₁) - x₂, y₂ = getxy(p₂) - x₃, y₃ = getxy(p₃) - y = quadratic_interp(x₁, x₂, x₃, y₁, y₂, y₃, x) - return y -end -function quadratic_interp(x₁, x₂, x₃, y₁, y₂, y₃, x) - t₁ = x₂ - x₁ - t₂ = x₁ - x₃ - t₃ = x₃ - x₂ - t₄ = t₂ * y₂ - t₅ = x₃^2 - t₆ = x₂^2 - t₇ = x₁^2 - s = -inv(t₁ * t₂ * t₃) - a = (y₁ * t₃ + y₃ * t₁ + t₄) * x - b = t₅ * (y₁ - y₂) - c = t₆ * (y₃ - y₁) - d = t₇ * (y₂ - y₃) - q = t₆ * (y₁ * x₃ - y₃ * x₁) - r = t₄ * x₁ * x₃ - n = (-y₁ * t₅ + y₃ * t₇) * x₂ - α = a - b - c - d - β = r - n - q - y = s * evalpoly(x, (β, α)) - return y -end - -function (c::CatmullRomSpline)(t) - if iszero(t) - return c.control_points[begin] - elseif isone(t) - return c.control_points[end] - else - segment, i = get_segment(c, t) - t′ = map_t_to_segment(c, i, t) - return segment(t′) - end -end - -function map_t_to_segment(c::CatmullRomSpline, i, t) - tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i + 1] - t′ = (t - tᵢ) / (tᵢ₊₁ - tᵢ) - return t′ -end - -function differentiate(c::CatmullRomSpline, t) - segment, i = get_segment(c, t) - tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i + 1] - t′ = map_t_to_segment(c, i, t) - ∂x, ∂y = getxy(differentiate(segment, t′)) - scale = inv(tᵢ₊₁ - tᵢ) - return (scale * ∂x, scale * ∂y) -end - -function twice_differentiate(c::CatmullRomSpline, t) - segment, i = get_segment(c, t) - tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i + 1] - t′ = map_t_to_segment(c, i, t) - ∂x, ∂y = getxy(twice_differentiate(segment, t′)) - scale = inv(tᵢ₊₁ - tᵢ) - return (scale^2 * ∂x, scale^2 * ∂y) -end - -function thrice_differentiate(c::CatmullRomSpline, t) - segment, i = get_segment(c, t) - tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i + 1] - t′ = map_t_to_segment(c, i, t) - ∂x, ∂y = getxy(thrice_differentiate(segment, t′)) - scale = inv(tᵢ₊₁ - tᵢ) - return (scale^3 * ∂x, scale^3 * ∂y) -end - -""" - get_segment(c::CatmullRomSpline, t) -> (CatmullRomSplineSegment, Int) - -Returns the [`CatmullRomSplineSegment`](@ref) of the [`CatmullRomSpline`](@ref) `c` that contains the point at `t`. Also -returns the segment index. -""" -function get_segment(c::CatmullRomSpline, t) - i = min(lastindex(c.knots) - 1, searchsortedlast(c.knots, t)) # avoid issues at t = 1 - return get_segment(c, i), i -end -function _get_segment(c::CatmullRomSpline, i::Int) - if i == firstindex(c.control_points) - pᵢ₋₁ = c.left - else - pᵢ₋₁ = c.control_points[i - 1] - end - if i == lastindex(c.control_points) - 1 - pᵢ₊₂ = c.right - else - pᵢ₊₂ = c.control_points[i + 2] - end - pᵢ, pᵢ₊₁ = c.control_points[i], c.control_points[i + 1] - segment = catmull_rom_spline_segment(pᵢ₋₁, pᵢ, pᵢ₊₁, pᵢ₊₂, c.alpha, c.tension) - return segment -end -function get_segment(c::CatmullRomSpline, i::Int) - return c.segments[i] -end - -function arc_length(c::CatmullRomSpline) - return sum(c.lengths) -end -function arc_length(c::CatmullRomSpline, t₁, t₂) - segment₁, i₁ = get_segment(c, t₁) - segment₂, i₂ = get_segment(c, t₂) - if i₁ == i₂ # same segment - just integrate directly - t₁′ = map_t_to_segment(c, i₁, t₁) - t₂′ = map_t_to_segment(c, i₂, t₂) - return arc_length(segment₁, t₁′, t₂′) - elseif i₁ == i₂ - 1 # adjacent segments - integrate on each segment - t₁′ = map_t_to_segment(c, i₁, t₁) - t₂′ = map_t_to_segment(c, i₂, t₂) - s₁ = arc_length(segment₁, t₁′, 1.0) - s₂ = arc_length(segment₂, 0.0, t₂′) - return s₁ + s₂ - else # at least one complete segment separates the outer segments - s = 0.0 - for i in (i₁ + 1):(i₂ - 1) - s += c.lengths[i] - end - t₁′ = map_t_to_segment(c, i₁, t₁) - t₂′ = map_t_to_segment(c, i₂, t₂) - s₁ = arc_length(segment₁, t₁′, 1.0) - s₂ = arc_length(segment₂, 0.0, t₂′) - return s + s₁ + s₂ - end -end - -total_variation(c::CatmullRomSpline, t₁, t₂) = marked_total_variation(c, t₁, t₂) - -has_lookup_table(c::CatmullRomSpline) = true - -#function is_piecewise_linear(c::CatmullRomSpline) -# tension = c.tension -# return isone(tension) -#end diff --git a/src/data_structures/mesh_refinement/curves/abstract.jl b/src/data_structures/mesh_refinement/curves/abstract.jl new file mode 100644 index 000000000..0d9ba0a9c --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/abstract.jl @@ -0,0 +1,1030 @@ +const GL_NW = permutedims( + [ + -0.999953919435642 0.00011825670068171721 + -0.9997572122115891 0.00027526103865831744 + -0.9994033532930161 0.00043245460060038196 + -0.9988923197258543 0.0005896003421932499 + -0.998224182256913 0.0007466574055662421 + -0.9973990436722557 0.0009035982480626941 + -0.9964170329848294 0.0010603974326420873 + -0.9952783043339365 0.0012170300415936897 + -0.9939830366739069 0.0013734713364273322 + -0.992531433650476 0.0015296966649272486 + -0.9909237235316215 0.0016856814323170032 + -0.9891601591554368 0.0018414010924821323 + -0.9872410178826077 0.0019968311464057843 + -0.9851666015488034 0.002151947143493035 + -0.9829372364150317 0.002306724684154767 + -0.9805532731150799 0.0024611394229790977 + -0.9780150865996246 0.002615167072191434 + -0.9753230760767991 0.0027687834052614944 + -0.9724776649491127 0.002921964260586262 + -0.9694793007466641 0.00307468554521151 + -0.9663284550566241 0.0032269232385712526 + -0.963025623448975 0.0033786533962332863 + -0.959571325398504 0.0035298521536436056 + -0.9559661042030546 0.0036804957298652166 + -0.9522105268980449 0.003830560431308329 + -0.9483051841672582 0.003980022655449804 + -0.944250690249923 0.0041288588945403645 + -0.94004768284409 0.004277045739298241 + -0.9356968230063247 0.004424559882588365 + -0.931198795047727 0.004571378123086171 + -0.9265543064262952 0.004717477368925247 + -0.9217640876356519 0.0048628346413281485 + -0.9168288920901461 0.005007427078219678 + -0.911749496006352 0.0051512319378220145 + -0.9065266982809823 0.005294226602231066 + -0.901161320365234 0.005436388580973459 + -0.8956542061355872 0.0055776955145435516 + -0.890006221761078 0.0057181251779199384 + -0.8842182555670642 0.00585765548406083 + -0.8782912178955077 0.005996264487377793 + -0.8722260409617927 0.006133930387187249 + -0.866023678708105 0.006270631531139229 + -0.8596851066533939 0.006406346418622805 + -0.85321132173994 0.00654105370414767 + -0.8466033421765535 0.006674732200701343 + -0.8398622072784298 0.006807360883081451 + -0.8329889773036814 0.006938918891202568 + -0.8259847332865807 0.007069385533377089 + -0.818850576867531 0.007198740289569644 + -0.8115876301197994 0.007326962814624488 + -0.804197035373034 0.007454032941465406 + -0.796679955033597 0.007579930684267617 + -0.7890375714017384 0.007704636241601166 + -0.7812710864856423 0.007828129999545307 + -0.7733817218123715 0.007950392534773417 + -0.7653707182357452 0.00807140461760791 + -0.7572393357411735 0.008191147215044737 + -0.7489888532474854 0.008309601493746885 + -0.740620568405778 0.008426748823006541 + -0.732135797395319 0.008542570777675352 + -0.7235358747165359 0.008657049141062358 + -0.7148221529811238 0.008770165907799143 + -0.705996002699303 0.008881903286671764 + -0.6970588120642633 0.008992243703418954 + -0.6880119867338267 0.009101169803496257 + -0.6788569496093618 0.00920866445480557 + -0.6695951406119885 0.00931471075038971 + -0.6602280164561035 0.009419292011091571 + -0.6507570504202663 0.009522391788177445 + -0.641183732115479 0.00962399386592412 + -0.6315095672508971 0.0097240822641693 + -0.6217360773970098 0.009822641240825014 + -0.6118647997463229 0.009919655294353544 + -0.6018972868715882 0.010015109166205537 + -0.5918351064816105 0.010108987843219902 + -0.5816798411746766 0.010201276559985107 + -0.5714330881896406 0.010291960801161504 + -0.5610964591547072 0.010381026303764331 + -0.550671579833952 0.010468459059407015 + -0.5401600898716189 0.010554245316504453 + -0.529563642534233 0.010638371582435857 + -0.5188839044505712 0.010720824625666934 + -0.5081225553495348 0.01080159147783093 + -0.49728128779595404 0.010880659435768353 + -0.48636180692438263 0.01095801606352491 + -0.4753658301709075 0.011033649194307498 + -0.4642950870030309 0.011107546932397785 + -0.4531513186476536 0.011179697655023212 + -0.4419362778172123 0.011250090014185036 + -0.43065172843401034 0.011318712938443162 + -0.4192994453527847 0.011385555634657481 + -0.4078812140815536 0.011450607589685441 + -0.39639883050078745 0.011513858572035563 + -0.38485410058095026 0.011575298633476675 + -0.37324884009845394 0.011634918110602589 + -0.3615848743500686 0.01169270762635197 + -0.34986403786583664 0.011748658091483198 + -0.33808817412053493 0.011802760706003907 + -0.32625913524372974 0.011855006960555099 + -0.3143787817284691 0.011905388637749498 + -0.30244898213866317 0.011953897813463982 + -0.29047161281519085 0.012000526858085933 + -0.27844855758078807 0.012045268437713197 + -0.2663817074437531 0.012088115515307618 + -0.2542729603005287 0.012129061351801793 + -0.24212422063719571 0.012168099507159044 + -0.2299373992299318 0.012205223841386304 + -0.21771441284448234 0.012240428515499802 + -0.20545718393468665 0.012273707992443457 + -0.19316764034011163 0.012305057037959772 + -0.1808477149828374 0.012334470721413042 + -0.16849934556344323 0.012361944416564878 + -0.15612447425624337 0.012387473802301837 + -0.14372504740381872 0.012411054863315053 + -0.1313030152108904 0.01243268389073175 + -0.11886033143758944 0.01245235748269861 + -0.10639895309216538 0.012470072544916794 + -0.09392084012318516 0.01248582629112865 + -0.08142795511126816 0.01249961624355592 + -0.0689222629604074 0.012511440233289444 + -0.05640573058892591 0.012521296400630325 + -0.043880326620117996 0.01252918319538237 + -0.031348021072617915 0.01253509937709596 + -0.01881078505055355 0.012539044015263127 + -0.0062705904335270835 0.012541016489463895 + 0.0062705904335270835 0.012541016489463895 + 0.01881078505055355 0.012539044015263127 + 0.031348021072617915 0.01253509937709596 + 0.043880326620117996 0.01252918319538237 + 0.05640573058892591 0.012521296400630325 + 0.0689222629604074 0.012511440233289444 + 0.08142795511126816 0.01249961624355592 + 0.09392084012318516 0.01248582629112865 + 0.10639895309216538 0.012470072544916794 + 0.11886033143758944 0.01245235748269861 + 0.1313030152108904 0.01243268389073175 + 0.14372504740381872 0.012411054863315053 + 0.15612447425624337 0.012387473802301837 + 0.16849934556344323 0.012361944416564878 + 0.1808477149828374 0.012334470721413042 + 0.19316764034011163 0.012305057037959772 + 0.20545718393468665 0.012273707992443457 + 0.21771441284448234 0.012240428515499802 + 0.2299373992299318 0.012205223841386304 + 0.24212422063719571 0.012168099507159044 + 0.2542729603005287 0.012129061351801793 + 0.2663817074437531 0.012088115515307618 + 0.27844855758078807 0.012045268437713197 + 0.29047161281519085 0.012000526858085933 + 0.30244898213866317 0.011953897813463982 + 0.3143787817284691 0.011905388637749498 + 0.32625913524372974 0.011855006960555099 + 0.33808817412053493 0.011802760706003907 + 0.34986403786583664 0.011748658091483198 + 0.3615848743500686 0.01169270762635197 + 0.37324884009845394 0.011634918110602589 + 0.38485410058095026 0.011575298633476675 + 0.39639883050078745 0.011513858572035563 + 0.4078812140815536 0.011450607589685441 + 0.4192994453527847 0.011385555634657481 + 0.43065172843401034 0.011318712938443162 + 0.4419362778172123 0.011250090014185036 + 0.4531513186476536 0.011179697655023212 + 0.4642950870030309 0.011107546932397785 + 0.4753658301709075 0.011033649194307498 + 0.48636180692438263 0.01095801606352491 + 0.49728128779595404 0.010880659435768353 + 0.5081225553495348 0.01080159147783093 + 0.5188839044505712 0.010720824625666934 + 0.529563642534233 0.010638371582435857 + 0.5401600898716189 0.010554245316504453 + 0.550671579833952 0.010468459059407015 + 0.5610964591547072 0.010381026303764331 + 0.5714330881896406 0.010291960801161504 + 0.5816798411746766 0.010201276559985107 + 0.5918351064816105 0.010108987843219902 + 0.6018972868715882 0.010015109166205537 + 0.6118647997463229 0.009919655294353544 + 0.6217360773970098 0.009822641240825014 + 0.6315095672508971 0.0097240822641693 + 0.641183732115479 0.00962399386592412 + 0.6507570504202663 0.009522391788177445 + 0.6602280164561035 0.009419292011091571 + 0.6695951406119885 0.00931471075038971 + 0.6788569496093618 0.00920866445480557 + 0.6880119867338267 0.009101169803496257 + 0.6970588120642633 0.008992243703418954 + 0.705996002699303 0.008881903286671764 + 0.7148221529811238 0.008770165907799143 + 0.7235358747165359 0.008657049141062358 + 0.732135797395319 0.008542570777675352 + 0.740620568405778 0.008426748823006541 + 0.7489888532474854 0.008309601493746885 + 0.7572393357411735 0.008191147215044737 + 0.7653707182357452 0.00807140461760791 + 0.7733817218123715 0.007950392534773417 + 0.7812710864856423 0.007828129999545307 + 0.7890375714017384 0.007704636241601166 + 0.796679955033597 0.007579930684267617 + 0.804197035373034 0.007454032941465406 + 0.8115876301197994 0.007326962814624488 + 0.818850576867531 0.007198740289569644 + 0.8259847332865807 0.007069385533377089 + 0.8329889773036814 0.006938918891202568 + 0.8398622072784298 0.006807360883081451 + 0.8466033421765535 0.006674732200701343 + 0.85321132173994 0.00654105370414767 + 0.8596851066533939 0.006406346418622805 + 0.866023678708105 0.006270631531139229 + 0.8722260409617927 0.006133930387187249 + 0.8782912178955077 0.005996264487377793 + 0.8842182555670642 0.00585765548406083 + 0.890006221761078 0.0057181251779199384 + 0.8956542061355872 0.0055776955145435516 + 0.901161320365234 0.005436388580973459 + 0.9065266982809823 0.005294226602231066 + 0.911749496006352 0.0051512319378220145 + 0.9168288920901461 0.005007427078219678 + 0.9217640876356519 0.0048628346413281485 + 0.9265543064262952 0.004717477368925247 + 0.931198795047727 0.004571378123086171 + 0.9356968230063247 0.004424559882588365 + 0.94004768284409 0.004277045739298241 + 0.944250690249923 0.0041288588945403645 + 0.9483051841672582 0.003980022655449804 + 0.9522105268980449 0.003830560431308329 + 0.9559661042030546 0.0036804957298652166 + 0.959571325398504 0.0035298521536436056 + 0.963025623448975 0.0033786533962332863 + 0.9663284550566241 0.0032269232385712526 + 0.9694793007466641 0.00307468554521151 + 0.9724776649491127 0.002921964260586262 + 0.9753230760767991 0.0027687834052614944 + 0.9780150865996246 0.002615167072191434 + 0.9805532731150799 0.0024611394229790977 + 0.9829372364150317 0.002306724684154767 + 0.9851666015488034 0.002151947143493035 + 0.9872410178826077 0.0019968311464057843 + 0.9891601591554368 0.0018414010924821323 + 0.9909237235316215 0.0016856814323170032 + 0.992531433650476 0.0015296966649272486 + 0.9939830366739069 0.0013734713364273322 + 0.9952783043339365 0.0012170300415936897 + 0.9964170329848294 0.0010603974326420873 + 0.9973990436722557 0.0009035982480626941 + 0.998224182256913 0.0007466574055662421 + 0.9988923197258543 0.0005896003421932499 + 0.9994033532930161 0.00043245460060038196 + 0.9997572122115891 0.00027526103865831744 + 0.999953919435642 0.00011825670068171721 + ], +) + +""" + abstract type AbstractParametricCurve <: Function end + +Abstract type for representing a parametric curve parametrised over `0 ≤ t ≤ 1`. The curves represented by this +abstract type should not be self-intersecting, with the exception of allowing for closed curves. + +The structs that subtype this abstract type must implement are: +- [`differentiate`](@ref). +- [`twice_differentiate`](@ref). +- [`thrice_differentiate`](@ref) (only if you have not manually defined [`total_variation`](@ref)). +- The struct must be callable so that `c(t)`, where `c` an instance of the struct, returns the associated value of the curve at `t`. +- If the struct does not implement [`point_position_relative_to_curve`](@ref), then the struct must implement [`get_closest_point`](@ref). Alternatively, + rather than implementing [`get_closest_point`](@ref), the struct should have a `lookup_table` field as a `Vector{NTuple{2,Float64}}`, which returns values on the curve at a set of points, + where `lookup_table[i]` is the value of the curve at `t = (i - 1) / (length(lookup_table) - 1)`. + +Functions that are defined for all [`AbstractParametricCurve`](@ref) subtypes are: +- [`arc_length`](@ref) +- [`curvature`](@ref) +- [`total_variation`](@ref) + +!!! note "Efficiently computing the total variation" + + The curves in this package evaluate the total variation not by evaluating the integral itself, but by taking care of the changes in orientation in the curve + to efficiently compute it. This is done by using the orientation markers of the curves, obtained using [`orientation_markers`](@ref), that stored in the field + `orientation_markers` of these curves. The function [`marked_total_variation`](@ref) is then used to evaluate it. You may like to consider using these functions for + any curve you wish to implement yourself, using e.g. the [`BezierCurve`](@ref) struct's implementation as a reference. +""" +abstract type AbstractParametricCurve <: Function end # defined in t ∈ [0, 1] + +Base.show(io::IO, c::C) where {C<:AbstractParametricCurve} = print(io, string(C)) + +""" + is_curve_bounded(c::AbstractParametricCurve) -> Bool + +Returns `true` if `c` is not a [`PiecewiseLinear`](@ref) curve. This is equivalent to `!is_piecewise_linear(c)`. +""" +is_curve_bounded(c::AbstractParametricCurve) = !is_piecewise_linear(c) + +""" + is_piecewise_linear(c::AbstractParametricCurve) -> Bool + +Returns `true` if `c` is [`PiecewiseLinear`](@ref), and `false` otherwise. +""" +is_piecewise_linear(::AbstractParametricCurve) = false + +""" + is_linear(c::AbstractParametricCurve) -> Bool + +Returns `true` if `c` is [`LineSegment`](@ref), and `false` otherwise. +""" +is_linear(::AbstractParametricCurve) = false + +""" + is_interpolating(c::AbstractParametricCurve) -> Bool + +Returns `true` if `c` goes through all its control points, and `false` otherwise. +""" +is_interpolating(::AbstractParametricCurve) = false + +""" + has_lookup_table(c::AbstractParametricCurve) -> Bool + +Returns `true` if `c` has a lookup table, and `false` otherwise. +""" +has_lookup_table(::AbstractParametricCurve) = false + +@doc """ + arc_length(c::AbstractParametricCurve) -> Float64 + arc_length(c::AbstractParametricCurve, t₁, t₂) -> Float64 + +Returns the arc length of the [`AbstractParametricCurve`] `c`. The second method returns the arc length in the interval `[t₁, t₂]`, where `0 ≤ t₁ ≤ t₂ ≤ 1`. +""" +arc_length + +arc_length(c::AbstractParametricCurve) = arc_length(c, 0.0, 1.0) +function arc_length(c::AbstractParametricCurve, t₁, t₂) + # The integral to evaluate is ∫ √(x′(t)² + y′(t)²) dt + scale = (t₂ - t₁) / 2 + shift = (t₂ + t₁) / 2 + s = 0.0 + for (x, w) in eachcol(GL_NW) + t = scale * x + shift + c′ = differentiate(c, t) + s += w * norm(c′) + end + return scale * s +end + +@doc """ + differentiate(c::AbstractParametricCurve, t) -> NTuple{2, Float64} + +Evaluates the derivative of `c` at `t`. +""" +differentiate + +@doc """ + twice_differentiate(c::AbstractParametricCurve, t) -> NTuple{2, Float64} + +Evaluates the second derivative of `c` at `t`. +""" +twice_differentiate + +@doc """ + thrice_differentiate(c::AbstractParametricCurve, t) -> NTuple{2, Float64} + +Evaluates the third derivative of `c` at `t`. +""" +thrice_differentiate + +""" + curvature(c::AbstractParametricCurve, t) -> Float64 + +Returns the curvature of the [`AbstractParametricCurve`] `c` at `t`. +""" +function curvature(c::AbstractParametricCurve, t) + x′, y′ = getxy(differentiate(c, t)) + x′′, y′′ = getxy(twice_differentiate(c, t)) + return (x′ * y′′ - y′ * x′′) / (x′^2 + y′^2)^(3 / 2) +end + +@doc """ + total_variation(c::AbstractParametricCurve) -> Float64 + total_variation(c::AbstractParametricCurve, t₁, t₂) -> Float64 + +Returns the total variation of a curve `c`, or the subcurve over `[t₁, t₂]` with `0 ≤ t₁ ≤ t₂ ≤ 1`, +defined as the integral of the absolute curvature over this interval. (This is also known as the total absolute curvature.) +""" +total_variation + +total_variation(c::AbstractParametricCurve) = total_variation(c, 0.0, 1.0) +function total_variation(c::AbstractParametricCurve, t₁, t₂) + scale = (t₂ - t₁) / 2 + shift = (t₂ + t₁) / 2 + s = 0.0 + for (x, w) in eachcol(GL_NW) + t = scale * x + shift + κ = abs(curvature(c, t)) + c′ = getxy(differentiate(c, t)) + ds = norm(c′) + s += w * κ * ds + end + return scale * s +end + +""" + marked_total_variation(b::AbstractParametricCurve, t₁, t₂) + +Returns the total variation of the curve `b` over the interval `[t₁, t₂]` using the orientation markers of `b`. +""" +function marked_total_variation(b::AbstractParametricCurve, t₁, t₂) + i₁ = min(lastindex(b.orientation_markers) - 1, searchsortedlast(b.orientation_markers, t₁)) # avoid issues at t = 1 + i₂ = min(lastindex(b.orientation_markers) - 1, searchsortedlast(b.orientation_markers, t₂)) + if i₁ == i₂ + T₁ = differentiate(b, t₁) + T₂ = differentiate(b, t₂) + θ = angle_between(T₁, T₂) + θ > π && (θ = 2π - θ) + return θ + elseif i₁ == i₂ - 1 + T₁ = differentiate(b, t₁) + T₂ = differentiate(b, b.orientation_markers[i₂]) + T₃ = differentiate(b, t₂) + θ₁ = angle_between(T₁, T₂) + θ₁ > π && (θ₁ = 2π - θ₁) + θ₂ = angle_between(T₂, T₃) + θ₂ > π && (θ₂ = 2π - θ₂) + return θ₁ + θ₂ + else + T₁ = differentiate(b, t₁) + T₂ = differentiate(b, b.orientation_markers[i₁+1]) + θ = angle_between(T₁, T₂) + θ > π && (θ = 2π - θ) + Δθ = θ + for i in (i₁+1):(i₂-1) + T₁ = T₂ + T₂ = differentiate(b, b.orientation_markers[i+1]) + θ = angle_between(T₁, T₂) + θ > π && (θ = 2π - θ) + Δθ += θ + end + T₁ = T₂ + T₂ = differentiate(b, t₂) + θ = angle_between(T₁, T₂) + θ > π && (θ = 2π - θ) + Δθ += θ + return Δθ + end +end + +""" + point_position_relative_to_curve([kernel::AbstractPredicateKernel=AdaptiveKernel(),] e::AbstractParametricCurve, p) -> Certificate + +Returns the position of the point `p` relative to the curve `c`. This function returns a [`Certificate`]: + +- `Left`: `p` is to the left of `c`. +- `Right`: `p` is to the right of `c`. +- `On`: `p` is on `c`. + +The `kernel` argument determines how this result is computed, and should be +one of [`ExactKernel`](@ref), [`FastKernel`](@ref), and [`AdaptiveKernel`](@ref) (the default). +See the documentation for more information about these choices. +""" +function point_position_relative_to_curve(kernel::AbstractPredicateKernel, b::AbstractParametricCurve, p) + t, q = get_closest_point(b, p) + qx, qy = getxy(q) + q′ = differentiate(b, t) + q′x, q′y = getxy(q′) + τx, τy = qx + q′x, qy + q′y + return point_position_relative_to_curve(kernel, LineSegment(q, (τx, τy)), p) +end +point_position_relative_to_curve(b::AbstractParametricCurve, p) = point_position_relative_to_curve(AdaptiveKernel(), b, p) + +""" + convert_lookup_idx(b::AbstractParametricCurve, i) -> Float64 + +Converts the index `i` of the lookup table of the curve `b` to the corresponding `t`-value. +""" +function convert_lookup_idx(b::AbstractParametricCurve, i) + n = length(b.lookup_table) + return (i - 1) / (n - 1) +end + +const PROJECTION_INTERVAL_TOL = 1.0e-12 +""" + get_closest_point(b::AbstractParametricCurve p) -> (Float64, NTuple{2,Float64}) + +Returns the `t`-value and the associated point `q` on the curve `b` that is nearest to `p` using a binary search. The search is done until the +binary search interval is smaller than `1e-12`. This function will only work if the curve `b` has a lookup table. + +!!! danger "Loops" + + This function is only tested on loop-free curves. It is not guaranteed to work on curves with loops. Moreover, for this function to be accurate, + you want the lookup table in `b` to be sufficiently dense. +""" +function get_closest_point(b::AbstractParametricCurve, p) + has_ctrl_points = hasfield(typeof(b), :control_points) + left_flag = has_ctrl_points ? (p == b.control_points[begin]) : (b(0.0) == p) + right_flag = has_ctrl_points ? (p == b.control_points[end]) : (b(1.0) == p) + if left_flag + return 0.0, p + elseif right_flag + return 1.0, p + end + i, δ = _get_closest_point_lookup_table(b, p) + if i == 1 + t, q = _get_closest_point_left_search(b, p, δ) + elseif i == length(b.lookup_table) + t, q = _get_closest_point_right_search(b, p, δ) + else + tmid, pmid = convert_lookup_idx(b, i), b.lookup_table[i] + tleft, pleft = convert_lookup_idx(b, i - 1), b.lookup_table[i-1] + tright, pright = convert_lookup_idx(b, i + 1), b.lookup_table[i+1] + t, q = _get_closest_point_interior_search(b, p, tmid, pmid, tleft, pleft, tright, pright, δ) + end + return t, q +end + +function _get_closest_point_lookup_table(b::AbstractParametricCurve, p) + δ = Inf + i = 0 + for (j, q) in enumerate(b.lookup_table) + δ′ = dist_sqr(p, q) + if δ′ < δ + δ = δ′ + i = j + end + end + return i, δ +end +function _get_closest_point_interior_search(b::AbstractParametricCurve, p, tmid, pmid, tleft, pleft, tright, pright, δ) + δleft, δright, δmid = dist_sqr(p, pleft), dist_sqr(p, pright), δ + w = tright - tleft + while w > PROJECTION_INTERVAL_TOL # Keep middle as closest + tleftmid, trightmid = midpoint(tleft, tmid), midpoint(tmid, tright) + pleftmid, prightmid = b(tleftmid), b(trightmid) + δleftmid, δrightmid = dist_sqr(p, pleftmid), dist_sqr(p, prightmid) + if δleftmid < δrightmid + # Choose the left-middle as the new center + tleft, tright, tmid = tleft, tmid, tleftmid + pleft, pright, pmid = pleft, pmid, pleftmid + δleft, δright, δmid = δleft, δmid, δleftmid + else + # Choose the right-middle as the new center + tleft, tright, tmid = tmid, tright, trightmid + pleft, pright, pmid = pmid, pright, prightmid + δleft, δright, δmid = δmid, δright, δrightmid + end + w = tright - tleft + end + return tmid, pmid +end +function _get_closest_point_left_search(b::AbstractParametricCurve, p, δ) + tleft, pleft = 0.0, b.lookup_table[begin] + tright, pright = convert_lookup_idx(b, 2), b.lookup_table[2] + δleft, δright = δ, dist_sqr(p, pright) + tmid = midpoint(tleft, tright) + pmid = b(tmid) + δmid = dist_sqr(p, pmid) + w = tmid - tleft + while δmid > δleft && w > PROJECTION_INTERVAL_TOL + tright, pright, δright = tmid, pmid, δmid + tmid = midpoint(tleft, tmid) + pmid = b(tmid) + δmid = dist_sqr(p, pmid) + end + if δmid < δleft + return _get_closest_point_interior_search(b, p, tmid, pmid, tleft, pleft, tright, pright, δmid) + else + return tleft, pleft + end +end +function _get_closest_point_right_search(b::AbstractParametricCurve, p, δ) + tleft, pleft = convert_lookup_idx(b, length(b.lookup_table) - 1), b.lookup_table[end-1] + tright, pright = 1.0, b.lookup_table[end] + δleft, δright = dist_sqr(p, pleft), δ + tmid = midpoint(tleft, tright) + pmid = b(tmid) + δmid = dist_sqr(p, pmid) + w = tright - tmid + while δmid > δright && w > PROJECTION_INTERVAL_TOL + tleft, pleft, δleft = tmid, pmid, δmid + tmid = midpoint(tmid, tright) + pmid = b(tmid) + δmid = dist_sqr(p, pmid) + end + if δmid < δright + return _get_closest_point_interior_search(b, p, tmid, pmid, tleft, pleft, tright, pright, δmid) + else + return tright, pright + end +end + +""" + process_roots_and_residuals!(roots, residuals, tol) -> Vector{Float64} + +Processes the roots and residuals of a root-finding algorithm. This function removes all `NaN` values from `roots` and `residuals`, sorts the roots in ascending order, and removes all roots with residuals greater than `tol`. The +returned vector is the vector of roots with duplicates (i.e. roots that are within `tol` of each other) removed. +""" +function process_roots_and_residuals!(roots, residuals, tol) + nan_idx = findall(isnan, roots) + deleteat!(roots, nan_idx) + deleteat!(residuals, nan_idx) + sort_idx = sortperm(roots) + permute!(roots, sort_idx) + permute!(residuals, sort_idx) + bad_idx = Int[] + for (i, resid) in enumerate(residuals) + if resid > tol + push!(bad_idx, i) + end + end + deleteat!(roots, bad_idx) + return uniquetol(roots; tol) +end + +""" + protect_against_bad_division!(roots, residuals, val, i) -> Bool + +Protects against bad division in root-finding algorithms. This function checks if `val` is close to `0` or if `roots[i]` is outside of `[0, 1]`. If either of these conditions are true, then `roots[i]` and `residuals[i]` are set to `NaN` and `true` is returned. Otherwise, `false` is returned. +""" +function protect_against_bad_division!(roots, residuals, val, i) + if abs(val) < 1.0e-8 || roots[i] < 0 || roots[i] > 1 + roots[i] = NaN + residuals[i] = NaN + return true + end + return false +end + +""" + horizontal_turning_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} + +Returns points `t` such that `x'(t) = 0` and `0 ≤ t ≤ 1`, where `x'` is the derivative of the `x`-coordinate of `c`. This function uses Newton's method to find the roots of `x'`. + +!!! danger "High-degree curves" + + For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all + turning points. + +# Arguments +- `c::AbstractParametricCurve`: The curve to find the horizontal turning points of. + +# Keyword Arguments +- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. +- `iters=50`: The number of iterations to run Newton's method for. +- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(x'(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. + +# Output +- `t`: All turning points, given in sorted order. +""" +function horizontal_turning_points(c::AbstractParametricCurve; steps=200, iters=50, tol=1.0e-5) + roots = collect(LinRange(0, 1, steps)) + residuals = fill(Inf, steps) + for i in eachindex(roots) + x′ = getx(differentiate(c, roots[i])) + x′′ = getx(twice_differentiate(c, roots[i])) + roots[i] -= x′ / x′′ + residuals[i] = abs(x′) + for _ in 1:iters + protect_against_bad_division!(roots, residuals, x′′, i) && break + x′ = getx(differentiate(c, roots[i])) + x′′ = getx(twice_differentiate(c, roots[i])) + roots[i] -= x′ / x′′ + residuals[i] = abs(x′) + end + end + return process_roots_and_residuals!(roots, residuals, tol) +end + +""" + vertical_turning_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} + +Returns points `t` such that `y'(t) = 0` and `0 ≤ t ≤ 1`, where `y'` is the derivative of the `y`-coordinate of `c`. This function uses Newton's method to find the roots of `y'`. + +!!! danger "High-degree curves" + + For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all + turning points. + +# Arguments +- `c::AbstractParametricCurve`: The curve to find the vertical turning points of. + +# Keyword Arguments +- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. +- `iters=50`: The number of iterations to run Newton's method for. +- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(y'(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. + +# Output +- `t`: All turning points, given in sorted order. +""" +function vertical_turning_points(c::AbstractParametricCurve; steps=200, iters=50, tol=1.0e-5) + roots = collect(LinRange(0, 1, steps)) + residuals = fill(Inf, steps) + for i in eachindex(roots) + y′ = gety(differentiate(c, roots[i])) + y′′ = gety(twice_differentiate(c, roots[i])) + roots[i] -= y′ / y′′ + residuals[i] = abs(y′) + for _ in 1:iters + protect_against_bad_division!(roots, residuals, y′′, i) && break + y′ = gety(differentiate(c, roots[i])) + y′′ = gety(twice_differentiate(c, roots[i])) + roots[i] -= y′ / y′′ + residuals[i] = abs(y′) + end + end + return process_roots_and_residuals!(roots, residuals, tol) +end + +""" + horizontal_inflection_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} + +Returns points `t` such that `x''(t) = 0` and `0 ≤ t ≤ 1`, where `x''` is the second derivative of the `x`-coordinate of `c`. This function uses Newton's method to find the roots of `x''`. +Note that these are only technically inflection points if `x'''(t) ≠ 0` at these points, but this is not checked. + +!!! danger "High-degree curves" + + For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all + inflection points. + +# Arguments +- `c::AbstractParametricCurve`: The curve to find the horizontal inflection points of. + +# Keyword Arguments +- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. +- `iters=50`: The number of iterations to run Newton's method for. +- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(x''(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. + +# Output +- `t`: All inflection points, given in sorted order. +""" +function horizontal_inflection_points(c::AbstractParametricCurve; steps=200, iters=50, tol=1.0e-5) + roots = collect(LinRange(0, 1, steps)) + residuals = fill(Inf, steps) + for i in eachindex(roots) + x′′ = getx(twice_differentiate(c, roots[i])) + x′′′ = getx(thrice_differentiate(c, roots[i])) + roots[i] -= x′′ / x′′′ + residuals[i] = abs(x′′) + for _ in 1:iters + protect_against_bad_division!(roots, residuals, x′′′, i) && break + x′′ = getx(twice_differentiate(c, roots[i])) + x′′′ = getx(thrice_differentiate(c, roots[i])) + roots[i] -= x′′ / x′′′ + residuals[i] = abs(x′′) + end + end + return process_roots_and_residuals!(roots, residuals, tol) +end + +""" + vertical_inflection_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} + +Returns points `t` such that `y''(t) = 0` and `0 ≤ t ≤ 1`, where `y''` is the second derivative of the `y`-coordinate of `c`. This function uses Newton's method to find the roots of `y''`. +Note that these are only technically inflection points if `y'''(t) ≠ 0` at these points, but this is not checked. + +!!! danger "High-degree curves" + + For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all + inflection points. + +# Arguments +- `c::AbstractParametricCurve`: The curve to find the vertical inflection points of. + +# Keyword Arguments +- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. +- `iters=50`: The number of iterations to run Newton's method for. +- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(y''(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. + +# Output +- `t`: All inflection points, given in sorted order. +""" +function vertical_inflection_points(c::AbstractParametricCurve; steps=200, iters=50, tol=1.0e-5) + roots = collect(LinRange(0, 1, steps)) + residuals = fill(Inf, steps) + for i in eachindex(roots) + y′′ = gety(twice_differentiate(c, roots[i])) + y′′′ = gety(thrice_differentiate(c, roots[i])) + roots[i] -= y′′ / y′′′ + residuals[i] = abs(y′′) + for _ in 1:iters + protect_against_bad_division!(roots, residuals, y′′′, i) && break + y′′ = gety(twice_differentiate(c, roots[i])) + y′′′ = gety(thrice_differentiate(c, roots[i])) + roots[i] -= y′′ / y′′′ + residuals[i] = abs(y′′) + end + end + return process_roots_and_residuals!(roots, residuals, tol) +end + +""" + inflection_points(c::AbstractParametricCurve; steps=200, iters = 50, tol = 1e-5) -> Vector{Float64} + +Returns points `t` such that `κ(t) = 0` and `0 ≤ t ≤ 1`, where `κ` is the curvature of `c`. This function uses Newton's method to find the roots of `κ`. + +!!! danger "High-degree curves" + + For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all + inflection points. + +# Arguments +- `c::AbstractParametricCurve`: The curve to find the inflection points of. + +# Keyword Arguments +- `steps=200`: The number of `t`-values to use for seeding Newton's method. In particular, Newton's method is run for each initial value in `LinRange(0, 1, steps)`. +- `iters=50`: The number of iterations to run Newton's method for. +- `tol=1e-5`: The tolerance to use for [`uniquetol`](@ref). Also used for deciding whether a root is a valid root, i.e. if `abs(κ(t)) > tol` for a found root `t`, then `t` is not a valid root and is rejected. +""" +function inflection_points(c::AbstractParametricCurve; steps=200, iters=50, tol=1.0e-5) + roots = collect(LinRange(0, 1, steps)) + residuals = fill(Inf, steps) + for i in eachindex(roots) + x′, y′ = getxy(differentiate(c, roots[i])) + x′′, y′′ = getxy(twice_differentiate(c, roots[i])) + x′′′, y′′′ = getxy(thrice_differentiate(c, roots[i])) + κ = x′ * y′′ - y′ * x′′ # don't need to divide by (x′^2 + y′^2)^(3 / 2) since we're only checking if it's zero + κ′ = x′ * y′′′ - y′ * x′′′ + roots[i] -= κ / κ′ + residuals[i] = abs(κ) + for _ in 1:iters + protect_against_bad_division!(roots, residuals, κ′, i) && break + x′, y′ = getxy(differentiate(c, roots[i])) + x′′, y′′ = getxy(twice_differentiate(c, roots[i])) + x′′′, y′′′ = getxy(thrice_differentiate(c, roots[i])) + κ = x′ * y′′ - y′ * x′′ + κ′ = x′ * y′′′ - y′ * x′′′ + roots[i] -= κ / κ′ + residuals[i] = abs(κ) + end + end + return process_roots_and_residuals!(roots, residuals, tol) +end + +""" + orientation_markers(c::AbstractParametricCurve; steps=200, iters=50, tol=1e-5) -> Vector{Float64} + +Finds all orientation markers of the [`AbstractParametricCurve`](@ref) `c`. These are points `t` where any of the following +conditions hold (not necessarily simultaneously), letting `c(t) = (x(t), y(t))`: + +- `x'(t) = 0` +- `y'(t) = 0` +- `κ(t; x) = 0`, where `κ(t; x)` is the curvature of the component function `x(t)` +- `κ(t; y) = 0`, where `κ(t; y)` is the curvature of the component function `y(t)` +- `κ(t) = 0`, where `κ` is the curvature of `c(t)` + +Note that the third and fourth conditions give all the inflection points of the component functions, and similarly for the fifth condition. + +See also [`horizontal_turning_points`](@ref), [`vertical_turning_points`](@ref), [`horizontal_inflection_points`](@ref), [`vertical_inflection_points`](@ref), and [`inflection_points`](@ref). + +!!! danger "High-degree curves" + + For curves of very high degree, such as Bezier curves with `steps` control points or greater, this function might fail to return all + inflection points. + +# Arguments +- `c::AbstractParametricCurve`: The [`AbstractParametricCurve`](@ref). + +# Keyword Arguments +- `steps=200`: The number of equally spaced points to use for initialising Newton's method. +- `iters=50`: How many iterations to use for Newton's method. +- `tol=1e-5`: The tolerance used for determining if two `t`-values are the same. + +# Output +- `markers::Vector{Float64}`: The `t`-values of the orientation markers of `b`. The returned vector is sorted, and also includes the + endpoints `0` and `1`; any `t`-values outside of `[0, 1]` are discarded, and multiplicity + of any `t` is not considered (so the `t`-values in the returned vector are unique). These values can be used to split the curve into monotone pieces, meaning + the orientation is monotone. These markers also guarantee that, over any monotone piece, the orientation changes by an angle of at most `π/2`. +""" +function orientation_markers(c::AbstractParametricCurve; steps=200, iters=50, tol=1.0e-5) + t₁ = horizontal_turning_points(c, steps=steps, iters=iters, tol=tol) + t₂ = vertical_turning_points(c, steps=steps, iters=iters, tol=tol) + t₃ = horizontal_inflection_points(c, steps=steps, iters=iters, tol=tol) + t₄ = vertical_inflection_points(c, steps=steps, iters=iters, tol=tol) + t₅ = inflection_points(c, steps=steps, iters=iters, tol=tol) + all_t = vcat(t₁, t₂, t₃, t₄, t₅) + isempty(all_t) && return [0.0, 1.0] + sort!(all_t) + all_t[1] ≠ 0.0 && pushfirst!(all_t, 0.0) + all_t[end] ≠ 1.0 && push!(all_t, 1.0) + return uniquetol(all_t; tol) +end + +""" + get_equidistant_split(c::AbstractParametricCurve, t₁, t₂) -> Float64 + +Returns a value of `t` such that the arc length along `c` from `t₁` to `t` is equal to the arc length along `c` from `t` to `t₂`. +Uses the bisection method to compute the `t`-value. +""" +function get_equidistant_split(c::AbstractParametricCurve, t₁, t₂) + a = t₁ + s₁₂ = arc_length(c, a, t₂) + s = s₁₂ / 2 + t = midpoint(a, t₂) + for _ in 1:100 # limit iterations to 100 + s₁t = arc_length(c, a, t) + if abs(s₁t - s) < 1.0e-3 || abs(t₁ - t₂) < 2.0e-8 + return t + end + s₁t > s ? (t₂ = t) : (t₁ = t) + t = midpoint(t₁, t₂) + end + return t +end + +""" + get_equivariation_split(c::AbstractParametricCurve, t₁, t₂) -> Float64, Float64 + +Returns a value of `t` such that the total variation of `c` from `t₁` to `t` is equal to the total variation of `c` from `t` to `t₂`. +Uses the bisection method to compute the `t`-value. Also returns the new total variation of the two pieces. +""" +function get_equivariation_split(c::AbstractParametricCurve, t₁, t₂) + a = t₁ + Δθ₁₂ = total_variation(c, a, t₂) + Δθ = Δθ₁₂ / 2 + Δθ₁t = Δθ + t = get_equidistant_split(c, a, t₂) + for _ in 1:100 # limit iterations to 100 + Δθ₁t = total_variation(c, a, t) + if abs(Δθ₁t - Δθ) < 1.0e-4 || abs(t₁ - t₂) < 2.0e-8 + return t, Δθ₁t + end + Δθ₁t > Δθ ? (t₂ = t) : (t₁ = t) + t = midpoint(t₁, t₂) + end + return t, Δθ₁t +end + +""" + get_inverse(c::AbstractParametricCurve, p) -> Float64 + +Given a point `p` on `c`, returns the `t`-value such that `c(t) ≈ p`. +""" +function get_inverse(c::AbstractParametricCurve, p) + t, _ = get_closest_point(c, p) + return t +end + +""" + angle_between(c₁::AbstractParametricCurve, c₂::AbstractParametricCurve) -> Float64 + +Given two curves `c₁` and `c₂` such that `c₁(1) == c₂(0)`, returns the angle between the two curves, treating the interior of the +curves as being left of both. +""" +function angle_between(c₁::AbstractParametricCurve, c₂::AbstractParametricCurve) + T₁x, T₁y = getxy(differentiate(c₁, 1.0)) + T₂x, T₂y = getxy(differentiate(c₂, 0.0)) + qx, qy = getxy(c₁(1.0)) + px, py = qx - T₁x, qy - T₁y + rx, ry = qx + T₂x, qy + T₂y + L₁ = LineSegment((px, py), (qx, qy), 0.0) + L₂ = LineSegment((qx, qy), (rx, ry), 0.0) + return angle_between(L₁, L₂) +end + +""" + get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) -> (Float64, NTuple{2,Float64}) + +Given a circle centered at `c(t₁)` with radius `r`, finds the first intersection of the circle with +the curve after `t₁` and less than `t₂`. It is assumed that such an intersection exists. The returned value +is `(t, q)`, where `t` is the parameter value of the intersection and `q` is the point of intersection. +""" +function get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) + tᵢ, tⱼ, p = _get_interval_for_get_circle_intersection(c, t₁, t₂, r) + t = midpoint(tᵢ, tⱼ) + for _ in 1:100 + q = c(t) + δ = dist(p, q) + if abs(δ - r) < 1.0e-3 || abs(tᵢ - tⱼ) < 2.0e-8 + return t, q + end + (δ > r) ? (tⱼ = t) : (tᵢ = t) + t = midpoint(tᵢ, tⱼ) + end + return t, c(t) +end + +""" + _get_interval_for_get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) -> (Float64, Float64, NTuple{2, Float64}) + +Given a circle centered at `c(t₁)` with radius `r`, finds an initial interval for [`get_circle_intersection`](@ref) +to perform bisection on to find a point of intersection. The returned interval is `(tᵢ, tⱼ)`, +where `tᵢ` is the parameter value of the first point in the interval and `tⱼ` +is the parameter value of the last point in the interval. (The interval does not have to be sorted.) The third returned value is `p = c(t₁)`. +""" +function _get_interval_for_get_circle_intersection(c::AbstractParametricCurve, t₁, t₂, r) + if has_lookup_table(c) + return _get_interval_for_get_circle_intersection_lookup(c, t₁, t₂, r) + else + return _get_interval_for_get_circle_intersection_direct(c, t₁, t₂, r) + end +end +function _get_interval_for_get_circle_intersection_direct(c::AbstractParametricCurve, t₁, t₂, r) + t = LinRange(t₁, t₂, 500) + tᵢ, tⱼ = t₁, t₂ + p = c(t₁) + for τ in t + q = c(τ) + δ = dist(p, q) + if δ > r + tⱼ = τ + break + else + tᵢ = τ + end + end + return tᵢ, tⱼ, p +end +function _get_interval_for_get_circle_intersection_lookup(c::AbstractParametricCurve, t₁, t₂, r) + n = length(c.lookup_table) + p = c(t₁) # the center + i₁ = floor(Int, t₁ * (n - 1)) + 1 + i₂ = ceil(Int, t₂ * (n - 1)) + 1 + i = i₁ + itr = t₁ < t₂ ? (i₁:1:i₂) : (i₁:-1:i₂) # explicit :1 step so that bothr anges are a StepRange + for outer i in itr + t = convert_lookup_idx(c, i) + q = c(t) + δ = dist(p, q) + δ > r && break + end + i, j = i - 1, i # δ = r somewhere inside here + tᵢ, tⱼ = convert_lookup_idx(c, i), convert_lookup_idx(c, j) + return tᵢ, tⱼ, p +end + +""" + reverse(curve::AbstractParametricCurve) -> AbstractParametricCurve + +Returns an [`AbstractParametricCurve`](@ref) that reverses the orientation of `curve`. In particular, +`c(t) = c̄(1-t)` for all `t` in `[0, 1]`, where `c` is the original curve and `c̄` is the reversed curve. +""" +function Base.reverse(c::AbstractParametricCurve) + return _reverse(c) +end diff --git a/src/data_structures/mesh_refinement/curves/beziercurve.jl b/src/data_structures/mesh_refinement/curves/beziercurve.jl new file mode 100644 index 000000000..d8dffa6be --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/beziercurve.jl @@ -0,0 +1,158 @@ +""" + BezierCurve <: AbstractParametricCurve + +Curve for representing a Bezier curve, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated +using `bezier_curve(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. + +A good reference on Bezier curves is [this](https://pomax.github.io/bezierinfo/). + +See also [`BSpline`](@ref) and [`CatmullRomSpline`](@ref). + +!!! danger "Loops" + + This curve is only tested on loop-free curves (and closed curves that otherwise have no self-intersections). It is not guaranteed to work on curves with loops, especially for finding the nearest point on the curve to a given point. + +!!! danger "Interpolation" + + Remember that Bezier curves are not interpolation curves. They only go through the first and last control points, but not the intermediate ones. If you want an interpolation curve, use [`CatmullRomSpline`](@ref). + +# Fields +- `control_points::Vector{NTuple{2,Float64}}`: The control points of the Bezier curve. The curve goes through the first and last control points, but not the intermediate ones. +- `cache::Vector{NTuple{2,Float64}}`: A cache of the points on the curve. This is used to speed up evaluation of the curve using de Casteljau's algorithm. +- `lookup_table::Vector{NTuple{2,Float64}}`: A lookup table for the Bezier curve, used for finding the point on the curve closest to a given point. The `i`th entry of the lookup table + corresponds to the `t`-value `i / (length(lookup_table) - 1)`. +- `orientation_markers::Vector{Float64}`: The orientation markers of the curve. These are defined so that the orientation of the curve is monotone between any two consecutive markers. The first and last markers are always `0` and `1`, respectively. See [`orientation_markers`](@ref). + +!!! warning "Concurrency" + + The cache is not thread-safe, and so you should not evaluate this curve in parallel. + +# Constructor +You can construct a `BezierCurve` using + + BezierCurve(control_points::Vector{NTuple{2,Float64}}; lookup_steps=5000, kwargs...) + +The keyword argument `lookup_steps=100` controls how many time points in `[0, 1]` are used for the lookup table. The `kwargs...` are keyword arguments passed to [`orientation_markers`](@ref). +""" +struct BezierCurve <: AbstractParametricCurve + control_points::Vector{NTuple{2,Float64}} + cache::Vector{NTuple{2,Float64}} + lookup_table::Vector{NTuple{2,Float64}} + orientation_markers::Vector{Float64} +end +function Base.:(==)(b₁::BezierCurve, b₂::BezierCurve) + b₁.control_points ≠ b₂.control_points && return false + return true +end + +function BezierCurve(control_points::Vector{NTuple{2,Float64}}; lookup_steps=5000, kwargs...) + cache = similar(control_points) # will be copyto! later + lookup_table = similar(control_points, lookup_steps) + markers = Float64[] + spl = BezierCurve(control_points, cache, lookup_table, markers) + for i in 1:lookup_steps + t = (i - 1) / (lookup_steps - 1) + spl.lookup_table[i] = spl(t) + end + markers = orientation_markers(spl; kwargs...) + resize!(spl.orientation_markers, length(markers)) + copyto!(spl.orientation_markers, markers) + return spl +end + +function (b::BezierCurve)(t)::NTuple{2,Float64} + return _eval_bezier_curve(b.control_points, b.cache, t) +end +function de_casteljau!(control_points, t) + if iszero(t) + return control_points[begin] + elseif isone(t) + return control_points[end] + else + n = length(control_points) - 1 + for j in 1:n + for k in 1:(n-j+1) + xₖ, yₖ = getxy(control_points[k]) + xₖ₊₁, yₖ₊₁ = getxy(control_points[k+1]) + x = (one(t) - t) * xₖ + t * xₖ₊₁ + y = (one(t) - t) * yₖ + t * yₖ₊₁ + control_points[k] = (x, y) + end + end + return control_points[begin] + end +end +function _eval_bezier_curve(control_points, cache, t) # de Casteljau's algorithm + copyto!(cache, control_points) + return de_casteljau!(cache, t) +end + +function differentiate(b::BezierCurve, t) + copyto!(b.cache, b.control_points) + n = length(b.control_points) - 1 + for i in 1:n + xᵢ, yᵢ = getxy(b.control_points[i]) + xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i+1]) + b.cache[i] = (n * (xᵢ₊₁ - xᵢ), n * (yᵢ₊₁ - yᵢ)) + end + return @views de_casteljau!(b.cache[begin:(end-1)], t) +end + +function twice_differentiate(b::BezierCurve, t) + copyto!(b.cache, b.control_points) + n = length(b.control_points) - 1 + if n == 1 + return (0.0, 0.0) + end + for i in 1:(n-1) + xᵢ, yᵢ = getxy(b.control_points[i]) + xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i+1]) + xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i+2]) + # To compute the second derivative control points, we note that e.g. + # for points [A, B, C, D], the first derivative gives [3(B - A), 3(C - B), 3(D - C)]. + # Let these new points be [A', B', C']. Thus, differentiating again, we obtain + # [2(B' - A'), 2(C - B')]. + # So, the ith point is given by + # Qᵢ′′ = (p-1) * (Qᵢ₊₁′ - Qᵢ′), + # where + # Qᵢ′ = p * (Pᵢ₊₁ - Pᵢ). + # Thus, Qᵢ′′ = p(p-1) * (Pᵢ₊₂ - 2Pᵢ₊₁ + Pᵢ). + scale = n * (n - 1) + b.cache[i] = (scale * (xᵢ₊₂ - 2xᵢ₊₁ + xᵢ), scale * (yᵢ₊₂ - 2yᵢ₊₁ + yᵢ)) + end + return @views de_casteljau!(b.cache[begin:(end-2)], t) +end + +function thrice_differentiate(b::BezierCurve, t) + copyto!(b.cache, b.control_points) + n = length(b.control_points) - 1 + if n ≤ 2 + return (0.0, 0.0) + end + for i in 1:(n-2) + xᵢ, yᵢ = getxy(b.control_points[i]) + xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i+1]) + xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i+2]) + xᵢ₊₃, yᵢ₊₃ = getxy(b.control_points[i+3]) + # We know that Qᵢ′ = p(Pᵢ₊₁ - Pᵢ), where Qᵢ′ is the ith control point for the first derivative, + # p is the degree of b, and Pᵢ is the ith control point of b. Thus, + # Qᵢ′′ = (p-1)(Qᵢ₊₁′ - Qᵢ′) = p(p-1)(Pᵢ₊₂ - 2Pᵢ₊₁ + Pᵢ), and then + # Qᵢ′′′ = (p-2)(Qᵢ₊₁′′ - Qᵢ′′) = p(p-1)(p-2)(Pᵢ₊₃ - 3Pᵢ₊₂ + 3Pᵢ₊₁ - Pᵢ). + scale = n * (n - 1) * (n - 2) + b.cache[i] = (scale * (xᵢ₊₃ - 3xᵢ₊₂ + 3xᵢ₊₁ - xᵢ), scale * (yᵢ₊₃ - 3yᵢ₊₂ + 3yᵢ₊₁ - yᵢ)) + end + return @views de_casteljau!(b.cache[begin:(end-3)], t) +end + +total_variation(b::BezierCurve, t₁, t₂) = marked_total_variation(b, t₁, t₂) + +has_lookup_table(b::BezierCurve) = true + +function _reverse(c::BezierCurve) + return BezierCurve( + reverse(c.control_points), + reverse(c.cache), + reverse(c.lookup_table), + 1 .- reverse(c.orientation_markers) + ) +end \ No newline at end of file diff --git a/src/data_structures/mesh_refinement/curves/bspline.jl b/src/data_structures/mesh_refinement/curves/bspline.jl new file mode 100644 index 000000000..a1e7fa9fe --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/bspline.jl @@ -0,0 +1,201 @@ +""" + BSpline <: AbstractParametricCurve + +Curve for representing a BSpline, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated +using `b_spline(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. + +See also [`BezierCurve`](@ref) and [`CatmullRomSpline`](@ref). + +Our implementation of a BSpline is based on https://github.com/thibauts/b-spline. + +!!! danger "Loops" + + This curve is only tested on loop-free curves (and closed curves that otherwise have no self-intersections). It is not guaranteed to work on curves with loops, especially for finding the nearest point on the curve to a given point. + +!!! danger "Interpolation" + + Remember that B-spline curves are not interpolation curves. They only go through the first and last control points, but not the intermediate ones. For an interpolating spline, see [`CatmullRomSpline`](@ref). + +# Fields +- `control_points::Vector{NTuple{2,Float64}}`: The control points of the BSpline. The curve goes through the first and last control points, but not the intermediate ones. +- `knots::Vector{Int}`: The knots of the BSpline. You should not modify or set this field directly (in particular, do not expect any support for non-uniform B-splines). +- `cache::Vector{NTuple{2,Float64}}`: A cache of the points on the curve. This is used to speed up evaluation of the curve using de Boor's algorithm. +- `lookup_table::Vector{NTuple{2,Float64}}`: A lookup table for the B-spline curve, used for finding the point on the curve closest to a given point. The `i`th entry of the lookup table + corresponds to the `t`-value `i / (length(lookup_table) - 1)`. +- `orientation_markers::Vector{Float64}`: The orientation markers of the curve. These are defined so that the orientation of the curve is monotone between any two consecutive markers. The first and last markers are always `0` and `1`, respectively. See [`orientation_markers`](@ref). + +# Constructor +You can construct a `BSpline` using + + BSpline(control_points::Vector{NTuple{2,Float64}}; degree=3, lookup_steps=5000, kwargs...) + +The keyword argument `lookup_steps` is used to build the lookup table for the curve. Note that the default +`degree=3` corresponds to a cubic B-spline curve. The `kwargs...` are keyword arguments passed to [`orientation_markers`](@ref). +""" +struct BSpline <: AbstractParametricCurve + control_points::Vector{NTuple{2,Float64}} + knots::Vector{Int} + cache::Vector{NTuple{2,Float64}} + lookup_table::Vector{NTuple{2,Float64}} + orientation_markers::Vector{Float64} +end +function Base.:(==)(b₁::BSpline, b₂::BSpline) + b₁.control_points ≠ b₂.control_points && return false + b₁.knots ≠ b₂.knots && return false + return true +end + +function BSpline(control_points::Vector{NTuple{2,Float64}}; degree=3, lookup_steps=5000, kwargs...) + nc = length(control_points) + @assert degree ≥ 1 "Degree must be at least 1, got $degree." + @assert degree ≤ nc - 1 "Degree must be at most n - 1 = $(nc - 1), where n is the number of control points, got $degree." + order = degree + 1 + cache = similar(control_points) + knots = zeros(nc + order) + for i in eachindex(knots) + if i ≤ order + knots[i] = 0 + elseif i < nc + 1 + knots[i] = knots[i-1] + 1 + else + knots[i] = knots[nc] + 1 + end + end + lookup_table = similar(control_points, lookup_steps) + markers = Float64[] + spl = BSpline(control_points, knots, cache, lookup_table, markers) + for i in 1:lookup_steps + t = (i - 1) / (lookup_steps - 1) + spl.lookup_table[i] = spl(t) + end + markers = orientation_markers(spl; kwargs...) + resize!(spl.orientation_markers, length(markers)) + copyto!(spl.orientation_markers, markers) + return spl +end + +function (b::BSpline)(t)::NTuple{2,Float64} + return _eval_bspline(b.control_points, b.knots, b.cache, t) +end + +function de_boor!(control_points, knots, t) + if iszero(t) + return control_points[begin] + elseif isone(t) + return control_points[end] + end + nc = length(control_points) + nk = length(knots) + order = nk - nc + domain = (order, nc + 1) # nc + 1 = nk - degree (order = degree + 1) + a, b = knots[domain[1]], knots[domain[2]] + t = a + t * (b - a) + s = @views searchsortedfirst(knots[domain[1]:domain[2]], t) + domain[1] - 2 + for L in 1:order + for i in s:-1:(s-order+L+1) + numerator = t - knots[i] + denominator = knots[i+order-L] - knots[i] + α = numerator / denominator + α′ = 1 - α + xᵢ₋₁, yᵢ₋₁ = getxy(control_points[i-1]) + xᵢ, yᵢ = getxy(control_points[i]) + control_points[i] = (α′ * xᵢ₋₁ + α * xᵢ, α′ * yᵢ₋₁ + α * yᵢ) + end + end + return control_points[s] +end +function _eval_bspline(control_points, knots, cache, t) # de Boor's algorithm + if iszero(t) + return control_points[begin] + elseif isone(t) + return control_points[end] + end + copyto!(cache, control_points) + return de_boor!(cache, knots, t) +end + +function differentiate(b::BSpline, t) + copyto!(b.cache, b.control_points) + nc = length(b.control_points) + nk = length(b.knots) + degree = nk - nc - 1 + for i in 1:(nc-1) + xᵢ, yᵢ = getxy(b.control_points[i]) + xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i+1]) + scale = degree / (b.knots[i+degree+1] - b.knots[i+1]) + b.cache[i] = (scale * (xᵢ₊₁ - xᵢ), scale * (yᵢ₊₁ - yᵢ)) + end + deriv = @views de_boor!(b.cache[begin:(end-1)], b.knots[(begin+1):(end-1)], t) + # Need to scale, since the formula used assumes that the knots are all in [0, 1] + range = b.knots[end] - b.knots[begin] + return (deriv[1] * range, deriv[2] * range) +end + +function twice_differentiate(b::BSpline, t) + copyto!(b.cache, b.control_points) + nc = length(b.control_points) + nk = length(b.knots) + degree = nk - nc - 1 + if degree == 1 + return (0.0, 0.0) + end + for i in 1:(nc-2) + xᵢ, yᵢ = getxy(b.control_points[i]) + xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i+1]) + xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i+2]) + scale1 = degree / (b.knots[i+degree+1] - b.knots[i+1]) + scale2 = degree / (b.knots[i+degree+2] - b.knots[i+2]) + scale3 = (degree - 1) / (b.knots[i+degree+1] - b.knots[i+2]) # different shifts between the knots are knots[begin+1:end-1] + Qᵢ′x, Qᵢ′y = (scale1 * (xᵢ₊₁ - xᵢ), scale1 * (yᵢ₊₁ - yᵢ)) + Qᵢ₊₁′x, Qᵢ₊₁′y = (scale2 * (xᵢ₊₂ - xᵢ₊₁), scale2 * (yᵢ₊₂ - yᵢ₊₁)) + b.cache[i] = (scale3 * (Qᵢ₊₁′x - Qᵢ′x), scale3 * (Qᵢ₊₁′y - Qᵢ′y)) + end + deriv = @views de_boor!(b.cache[begin:(end-2)], b.knots[(begin+2):(end-2)], t) + range = (b.knots[end] - b.knots[begin])^2 + return (deriv[1] * range, deriv[2] * range) +end + +function thrice_differentiate(b::BSpline, t) # yes there is a way to evaluate (B, B', B'', B''') all in one pass. just haven't implemented it + copyto!(b.cache, b.control_points) + nc = length(b.control_points) + nk = length(b.knots) + degree = nk - nc - 1 + if degree ≤ 2 + return (0.0, 0.0) + end + for i in 1:(nc-3) + xᵢ, yᵢ = getxy(b.control_points[i]) + xᵢ₊₁, yᵢ₊₁ = getxy(b.control_points[i+1]) + xᵢ₊₂, yᵢ₊₂ = getxy(b.control_points[i+2]) + xᵢ₊₃, yᵢ₊₃ = getxy(b.control_points[i+3]) + scale1 = degree / (b.knots[i+degree+1] - b.knots[i+1]) + scale2 = degree / (b.knots[i+degree+2] - b.knots[i+2]) + scale3 = degree / (b.knots[i+degree+3] - b.knots[i+3]) + scale4 = (degree - 1) / (b.knots[i+degree+1] - b.knots[i+2]) + scale5 = (degree - 1) / (b.knots[i+degree+2] - b.knots[i+3]) + scale6 = (degree - 2) / (b.knots[i+degree+1] - b.knots[i+3]) + Qᵢ′x, Qᵢ′y = (scale1 * (xᵢ₊₁ - xᵢ), scale1 * (yᵢ₊₁ - yᵢ)) + Qᵢ₊₁′x, Qᵢ₊₁′y = (scale2 * (xᵢ₊₂ - xᵢ₊₁), scale2 * (yᵢ₊₂ - yᵢ₊₁)) + Qᵢ₊₂′x, Qᵢ₊₂′y = (scale3 * (xᵢ₊₃ - xᵢ₊₂), scale3 * (yᵢ₊₃ - yᵢ₊₂)) + Qᵢ′′x, Qᵢ′′y = (scale4 * (Qᵢ₊₁′x - Qᵢ′x), scale4 * (Qᵢ₊₁′y - Qᵢ′y)) + Qᵢ₊₁′′x, Qᵢ₊₁′′y = (scale5 * (Qᵢ₊₂′x - Qᵢ₊₁′x), scale5 * (Qᵢ₊₂′y - Qᵢ₊₁′y)) + b.cache[i] = (scale6 * (Qᵢ₊₁′′x - Qᵢ′′x), scale6 * (Qᵢ₊₁′′y - Qᵢ′′y)) + end + deriv = @views de_boor!(b.cache[begin:(end-3)], b.knots[(begin+3):(end-3)], t) + range = (b.knots[end] - b.knots[begin])^3 + return (deriv[1] * range, deriv[2] * range) +end + +total_variation(b::BSpline, t₁, t₂) = marked_total_variation(b, t₁, t₂) + +has_lookup_table(b::BSpline) = true + +function _reverse(c::BSpline) + return BSpline( + reverse(c.control_points), + c.knots, + reverse(c.cache), + reverse(c.lookup_table), + 1 .- reverse(c.orientation_markers) + ) +end \ No newline at end of file diff --git a/src/data_structures/mesh_refinement/curves/catmullromspline.jl b/src/data_structures/mesh_refinement/curves/catmullromspline.jl new file mode 100644 index 000000000..d37927b19 --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/catmullromspline.jl @@ -0,0 +1,497 @@ +""" + CatmullRomSplineSegment <: AbstractParametricCurve + +A single segment of a Camtull-Rom spline, representing by a cubic polynomial. Note that evaluating this curve will only +draw within the two interior control points of the spline. + +Based on [this article](https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html). + +# Fields +- `a::NTuple{2,Float64}`: The coefficient on `t³`. +- `b::NTuple{2,Float64}`: The coefficient on `t²`. +- `c::NTuple{2,Float64}`: The coefficient on `t`. +- `d::NTuple{2,Float64}`: The constant in the polynomial. +- `p₁::NTuple{2,Float64}`: The second control point of the segment. +- `p₂::NTuple{2,Float64}`: The third control point of the segment. + +With these fields, the segment is parametrised over `0 ≤ t ≤ 1` by `q(t)`, where + + q(t) = at³ + bt² + ct + d, + +and `q(0) = p₁` and `q(1) = p₂`, where the segment is defined by four control points `p₀`, `p₁`, `p₂`, and `p₃`. + +This struct is callable, returning the interpolated point `(x, y)` at `t` as a `NTuple{2,Float64}`. + +# Constructor +To construct this segment, use + + catmull_rom_spline_segment(p₀, p₁, p₂, p₃, α, τ) + +Here, `p₀`, `p₁`, `p₂`, and `p₃` are the four points of the segment (not `a`, `b`, `c`, and `d`), and `α` and `τ` are the parameters of the spline. The parameter `α` +controls the type of the parametrisation, where + +- `α = 0`: Uniform parametrisation. +- `α = 1/2`: Centripetal parametrisation. +- `α = 1`: Chordal parametrisation. + +The parameter `τ` is the tension, and controls the tightness of the segment. `τ = 0` is the least tight, while `τ = 1` leads to straight lines between the +control points. Both `α` and `τ` must be in `[0, 1]`. +""" +struct CatmullRomSplineSegment <: AbstractParametricCurve + a::NTuple{2,Float64} + b::NTuple{2,Float64} + c::NTuple{2,Float64} + d::NTuple{2,Float64} + p₁::NTuple{2,Float64} + p₂::NTuple{2,Float64} +end + +function (c::CatmullRomSplineSegment)(t) + if iszero(t) + return c.p₁ + elseif isone(t) + return c.p₂ + else + ax, ay = getxy(c.a) + bx, by = getxy(c.b) + cx, cy = getxy(c.c) + dx, dy = getxy(c.d) + cx = evalpoly(t, (dx, cx, bx, ax)) + cy = evalpoly(t, (dy, cy, by, ay)) + return (cx, cy) + end +end + +function catmull_rom_spline_segment(p₀, p₁, p₂, p₃, α, τ) + x₀, y₀ = getxy(p₀) + x₁, y₁ = getxy(p₁) + x₂, y₂ = getxy(p₂) + x₃, y₃ = getxy(p₃) + t₀₁ = dist(p₀, p₁)^α + t₁₂ = dist(p₁, p₂)^α + t₂₃ = dist(p₂, p₃)^α + τ′ = one(τ) - τ + if iszero(τ′) + m₁x, m₁y, m₂x, m₂y = zero(τ), zero(τ), zero(τ), zero(τ) + else + m₁x = τ′ * (x₂ - x₁ + t₁₂ * ((x₁ - x₀) / t₀₁ - (x₂ - x₀) / (t₀₁ + t₁₂))) + m₁y = τ′ * (y₂ - y₁ + t₁₂ * ((y₁ - y₀) / t₀₁ - (y₂ - y₀) / (t₀₁ + t₁₂))) + m₂x = τ′ * (x₂ - x₁ + t₁₂ * ((x₃ - x₂) / t₂₃ - (x₃ - x₁) / (t₁₂ + t₂₃))) + m₂y = τ′ * (y₂ - y₁ + t₁₂ * ((y₃ - y₂) / t₂₃ - (y₃ - y₁) / (t₁₂ + t₂₃))) + end + ax = 2(x₁ - x₂) + m₁x + m₂x + ay = 2(y₁ - y₂) + m₁y + m₂y + bx = -3(x₁ - x₂) - 2m₁x - m₂x + by = -3(y₁ - y₂) - 2m₁y - m₂y + cx = m₁x + cy = m₁y + dx = x₁ + dy = y₁ + a = (ax, ay) + b = (bx, by) + c = (cx, cy) + d = (dx, dy) + return CatmullRomSplineSegment(a, b, c, d, p₁, p₂) +end + +function differentiate(c::CatmullRomSplineSegment, t) + ax, ay = getxy(c.a) + bx, by = getxy(c.b) + cx, cy = getxy(c.c) + a′x, a′y = 3ax, 3ay + b′x, b′y = 2bx, 2by + x = evalpoly(t, (cx, b′x, a′x)) + y = evalpoly(t, (cy, b′y, a′y)) + return (x, y) +end + +function twice_differentiate(c::CatmullRomSplineSegment, t) + ax, ay = getxy(c.a) + bx, by = getxy(c.b) + a′′x, a′′y = 6ax, 6ay + b′′x, b′′y = 2bx, 2by + x = evalpoly(t, (b′′x, a′′x)) + y = evalpoly(t, (b′′y, a′′y)) + return (x, y) +end + +function thrice_differentiate(c::CatmullRomSplineSegment, t) + ax, ay = getxy(c.a) + a′′′x, a′′′y = 6ax, 6ay + return (a′′′x, a′′′y) +end + +function _reverse(c::CatmullRomSplineSegment) + #= + If q(t) = at³ + bt² + ct + d, then + q(1-t) = a′t³ + b′t² + c′t + d, + where + a′ = -a + b′ = 3a + b + c′ = -3a - 2b - c + d′ = a + b + c + d. + =# + a, b, c, d, p₁, p₂ = c.a, c.b, c.c, c.d, c.p₁, c.p₂ + return CatmullRomSplineSegment( + .-a, + 3 .* a .+ b, + -3 .* a .- 2 .* b .- c, + a .+ b .+ c .+ d, + p₂, + p₁ + ) +end + +""" + CatmullRomSpline <: AbstractParametricCurve + +Curve for representing a Catmull-Rom spline, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated +using `catmull_rom_spline(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. + +For information on these splines, see e.g. [this article](https://people.engr.tamu.edu/schaefer/research/cr_cad.pdf) and [this article](https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html). +Additionally, [this article](https://splines.readthedocs.io/en/latest/euclidean/catmull-rom-properties.html) lists some nice properties of these splines. + +!!! danger "Loops" + + This curve is only tested on loop-free curves (and closed curves that otherwise have no self-intersections). It is not guaranteed to work on curves with loops, especially for finding the nearest point on the curve to a given point. + +!!! note "Extension" + + Typically, Catmull-Rom splines are defined on segments of four control points, and drawn between the two interior control points. + This creates an issue in that the first and last control points will not be joined to the spline. To overcome this, we extend the spline to the left + and right during the evaluation of a spline, using the fields `left` and `right` defined below. The rules used for extending these points come from [CatmullRom.jl](https://github.com/JeffreySarnoff/CatmullRom.jl), + which extrapolates based on a Thiele-like cubic polynomial. + +# Fields +- `control_points::Vector{NTuple{2,Float64}}`: The control points of the Catmull-Rom spline. The curve goes through each point. +- `knots::Vector{Float64}`: The parameter values of the Catmull-Rom spline. The `i`th entry of this vector corresponds to the `t`-value associated with the `i`th control point. + With an alpha parameter `α`, these values are given by `knots[i+1] = knots[i] + dist(control_points[i], control_points[i+1])^α`, where `knots[1] = 0`, + and the vector is the normalised by dividing by `knots[end]`. +- `lookup_table::Vector{NTuple{2,Float64}}`: A lookup table for the Catmull-Rom spline, used for finding the point on the curve closest to a given point. The `i`th entry of the lookup table + corresponds to the `t`-value `i / (length(lookup_table) - 1)`. +- `alpha::Float64`: The alpha parameter of the Catmull-Rom spline. This controls the type of the parametrisation, where `alpha = 0` corresponds to uniform parametrisation, + `alpha = 1/2` corresponds to centripetal parametrisation, and `alpha = 1` corresponds to chordal parametrisation. Must be in `[0, 1]`. For reasons similar to what we describe for `tension` below, we only support + `alpha = 1/2` for now. (If you do really want to change it, use the `_alpha` keyword argument in the constructor.) +- `tension::Float64`: The tension parameter of the Catmull-Rom spline. This controls the tightness of the spline, with `tension = 0` being the least tight, and `tension = 1` leading to straight lines between the control points. Must be in `[0, 1]`. + You can not currently set this to anything except `0.0` due to numerical issues with boundary refinement. (For example, equivariation splits are not possible if `tension=1` since + the curve is piecewise linear in that case, and for `tension` very close to `1`, the equivariation split is not always between the provided times. If you _really_ want to change it, then you + can use the `_tension` keyword argument in the constructor - but be warned that this may lead to numerical issues and potentially infinite loops.) +- `left::NTuple{2,Float64}`: The left extension of the spline. This is used to evaluate the spline on the first segment. +- `right::NTuple{2,Float64}`: The right extension of the spline. This is used to evaluate the spline on the last segment. +- `lengths::Vector{Float64}`: The lengths of the individual segments of the spline. +- `segments::Vector{CatmullRomSplineSegment}`: The individual segments of the spline. +- `orientation_markers::Vector{Float64}`: The orientation markers of the curve. These are defined so that the orientation of the curve is monotone between any two consecutive markers. The first and last markers are always `0` and `1`, respectively. See [`orientation_markers`](@ref). + +# Constructor +To construct a `CatmullRomSpline`, use + + CatmullRomSpline(control_points::Vector{NTuple{2,Float64}}; lookup_steps=5000, kwargs...) + +The keyword argument `lookup_steps` is used to build the lookup table for the curve, with `lookup_steps` giving the number of time points in `[0, 1]` used for the lookup table. +The `kwargs...` are keyword arguments passed to [`orientation_markers`](@ref). +""" +struct CatmullRomSpline <: AbstractParametricCurve + control_points::Vector{NTuple{2,Float64}} + knots::Vector{Float64} + lookup_table::Vector{NTuple{2,Float64}} + alpha::Float64 + tension::Float64 + left::NTuple{2,Float64} + right::NTuple{2,Float64} + lengths::Vector{Float64} + segments::Vector{CatmullRomSplineSegment} + orientation_markers::Vector{Float64} +end + +function _reverse(c::CatmullRomSpline) + return CatmullRomSpline( + reverse(c.control_points), + 1 .- reverse(c.knots), + reverse(c.lookup_table), + c.alpha, + c.tension, + c.right, + c.left, + reverse(c.lengths), + reverse(reverse.(c.segments)), + 1 .- reverse(c.orientation_markers) + ) +end + +function Base.:(==)(spl1::CatmullRomSpline, spl2::CatmullRomSpline) + spl1.control_points ≠ spl2.control_points && return false + spl1.knots ≠ spl2.knots && return false + spl1.alpha ≠ spl2.alpha && return false + spl1.tension ≠ spl2.tension && return false + return true +end + +is_interpolating(spl::CatmullRomSpline) = true + +function CatmullRomSpline(control_points; _alpha=1 / 2, _tension=0.0, lookup_steps=5000, kwargs...) + alpha = _alpha + tension = _tension + @assert length(control_points) ≥ 4 "Catmull-Rom splines require at least 4 control points, got $(length(control_points))." + nc = length(control_points) + @assert 0 ≤ alpha ≤ 1 "Alpha must be in [0, 1], got $alpha." + @assert 0 ≤ tension ≤ 1 "Tension must be in [0, 1], got $tension." + knots = zeros(nc) + for i in 2:nc + knots[i] = knots[i-1] + dist(control_points[i-1], control_points[i])^alpha + end + left = extend_left_control_point(control_points) + right = extend_right_control_point(control_points) + scale = knots[end] + knots ./= scale + knots[end] = 1.0 + lookup_table = similar(control_points, lookup_steps) + lengths = zeros(nc - 1) + markers = Float64[] + segments = Vector{CatmullRomSplineSegment}(undef, nc - 1) + spl = CatmullRomSpline(control_points, knots, lookup_table, alpha, tension, left, right, lengths, segments, markers) + for i in 1:(nc-1) + spl.segments[i] = _get_segment(spl, i) + end + for i in 1:lookup_steps + t = (i - 1) / (lookup_steps - 1) + spl.lookup_table[i] = spl(t) + end + for i in 1:(nc-1) + segment = get_segment(spl, i) + spl.lengths[i] = arc_length(segment, 0.0, 1.0) + end + markers = orientation_markers(spl; kwargs...) + resize!(spl.orientation_markers, length(markers)) + copyto!(spl.orientation_markers, markers) + return spl +end + +function extend_left_control_point(control_points) + is_closed = control_points[begin] == control_points[end] + if is_closed + return control_points[end-1] + else + c₁, c₂, c₃, c₄ = control_points[begin], control_points[begin+1], control_points[begin+2], control_points[begin+3] + x₁, x₂ = getx(c₁), getx(c₂) + reverse_flag = x₁ == x₂ + if reverse_flag + c₁, c₂, c₃, c₄ = reverse(getxy(c₁)), reverse(getxy(c₂)), reverse(getxy(c₃)), reverse(getxy(c₄)) + x₁, x₂ = getx(c₁), getx(c₂) + end + x = 2x₁ - x₂ # reflection + y = thiele4(c₁, c₂, c₃, c₄, x) + if !reverse_flag + return (x, y) + else + return (y, x) + end + end +end +function extend_right_control_point(control_points) + is_closed = control_points[begin] == control_points[end] + if is_closed + return control_points[begin+1] + else + cₙ₋₃, cₙ₋₂, cₙ₋₁, cₙ = control_points[end-3], control_points[end-2], control_points[end-1], control_points[end] + xₙ₋₁, xₙ = getx(cₙ₋₁), getx(cₙ) + reverse_flag = xₙ₋₁ == xₙ + if reverse_flag + cₙ₋₃, cₙ₋₂, cₙ₋₁, cₙ = reverse(getxy(cₙ₋₃)), reverse(getxy(cₙ₋₂)), reverse(getxy(cₙ₋₁)), reverse(getxy(cₙ)) + xₙ₋₁, xₙ = getx(cₙ₋₁), getx(cₙ) + end + x = 2xₙ - xₙ₋₁ + y = thiele4(cₙ₋₃, cₙ₋₂, cₙ₋₁, cₙ, x) + if !reverse_flag + return (x, y) + else + return (y, x) + end + end +end + +# See https://github.com/JeffreySarnoff/CatmullRom.jl/tree/49e6536c184dfc4200b980f89aa55bc5cf357b82/src/fewpoints +function thiele4(p₁, p₂, p₃, p₄, x) + x₁, y₁ = getxy(p₁) + x₂, y₂ = getxy(p₂) + x₃, y₃ = getxy(p₃) + x₄, y₄ = getxy(p₄) + y = thiele4(x₁, x₂, x₃, x₄, y₁, y₂, y₃, y₄, x) + if !isfinite(y) + if x ≤ x₃ + y = thiele3(p₁, p₂, p₃, x) + else + y = thiele3(p₂, p₃, p₄, x) + end + end + return y +end +function thiele4(x₁, x₂, x₃, x₄, y₁, y₂, y₃, y₄, x) + t₂ = (x₂ - x₃) / (y₂ - y₃) + t₁ = (x₁ - x₂) / (y₁ - y₂) + t₄ = -(x₃ - x₄) / (y₃ - y₄) + t₂ + t₃ = (x₁ - x₃) / (t₁ - t₂) + t₄ = -(x₂ - x₄) / t₄ + y₂ - y₃ + t₃ + t₂ = -(x₁ - x₄) / t₄ + t₁ - t₂ + t₂ = (x₃ - x) / t₂ - y₁ + y₂ + t₃ + t₁ = -(x₂ - x) / t₂ + t₁ + y = -(x₁ - x) / t₁ + y₁ + return y +end +function thiele3(p₁, p₂, p₃, x) + x₁, y₁ = getxy(p₁) + x₂, y₂ = getxy(p₂) + x₃, y₃ = getxy(p₃) + y = thiele3(x₁, x₂, x₃, y₁, y₂, y₃, x) + if !isfinite(y) + y = quadratic_interp(p₁, p₂, p₃, x) + end + return y +end +function thiele3(x₁, x₂, x₃, y₁, y₂, y₃, x) + t₁ = (x₁ - x₂) / (y₁ - y₂) + t₂ = -(x₂ - x₃) / (y₂ - y₃) + t₁ + t₂ = (x₁ - x₃) / t₂ - y₁ + y₂ + t₁ = -(x₂ - x) / t₂ + t₁ + y = -(x₁ - x) / t₁ + y₁ + return y +end +function quadratic_interp(p₁, p₂, p₃, x) + x₁, y₁ = getxy(p₁) + x₂, y₂ = getxy(p₂) + x₃, y₃ = getxy(p₃) + y = quadratic_interp(x₁, x₂, x₃, y₁, y₂, y₃, x) + return y +end +function quadratic_interp(x₁, x₂, x₃, y₁, y₂, y₃, x) + t₁ = x₂ - x₁ + t₂ = x₁ - x₃ + t₃ = x₃ - x₂ + t₄ = t₂ * y₂ + t₅ = x₃^2 + t₆ = x₂^2 + t₇ = x₁^2 + s = -inv(t₁ * t₂ * t₃) + a = (y₁ * t₃ + y₃ * t₁ + t₄) * x + b = t₅ * (y₁ - y₂) + c = t₆ * (y₃ - y₁) + d = t₇ * (y₂ - y₃) + q = t₆ * (y₁ * x₃ - y₃ * x₁) + r = t₄ * x₁ * x₃ + n = (-y₁ * t₅ + y₃ * t₇) * x₂ + α = a - b - c - d + β = r - n - q + y = s * evalpoly(x, (β, α)) + return y +end + +function (c::CatmullRomSpline)(t) + if iszero(t) + return c.control_points[begin] + elseif isone(t) + return c.control_points[end] + else + segment, i = get_segment(c, t) + t′ = map_t_to_segment(c, i, t) + return segment(t′) + end +end + +function map_t_to_segment(c::CatmullRomSpline, i, t) + tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i+1] + t′ = (t - tᵢ) / (tᵢ₊₁ - tᵢ) + return t′ +end + +function differentiate(c::CatmullRomSpline, t) + segment, i = get_segment(c, t) + tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i+1] + t′ = map_t_to_segment(c, i, t) + ∂x, ∂y = getxy(differentiate(segment, t′)) + scale = inv(tᵢ₊₁ - tᵢ) + return (scale * ∂x, scale * ∂y) +end + +function twice_differentiate(c::CatmullRomSpline, t) + segment, i = get_segment(c, t) + tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i+1] + t′ = map_t_to_segment(c, i, t) + ∂x, ∂y = getxy(twice_differentiate(segment, t′)) + scale = inv(tᵢ₊₁ - tᵢ) + return (scale^2 * ∂x, scale^2 * ∂y) +end + +function thrice_differentiate(c::CatmullRomSpline, t) + segment, i = get_segment(c, t) + tᵢ, tᵢ₊₁ = c.knots[i], c.knots[i+1] + t′ = map_t_to_segment(c, i, t) + ∂x, ∂y = getxy(thrice_differentiate(segment, t′)) + scale = inv(tᵢ₊₁ - tᵢ) + return (scale^3 * ∂x, scale^3 * ∂y) +end + +""" + get_segment(c::CatmullRomSpline, t) -> (CatmullRomSplineSegment, Int) + +Returns the [`CatmullRomSplineSegment`](@ref) of the [`CatmullRomSpline`](@ref) `c` that contains the point at `t`. Also +returns the segment index. +""" +function get_segment(c::CatmullRomSpline, t) + i = min(lastindex(c.knots) - 1, searchsortedlast(c.knots, t)) # avoid issues at t = 1 + return get_segment(c, i), i +end +function _get_segment(c::CatmullRomSpline, i::Int) + if i == firstindex(c.control_points) + pᵢ₋₁ = c.left + else + pᵢ₋₁ = c.control_points[i-1] + end + if i == lastindex(c.control_points) - 1 + pᵢ₊₂ = c.right + else + pᵢ₊₂ = c.control_points[i+2] + end + pᵢ, pᵢ₊₁ = c.control_points[i], c.control_points[i+1] + segment = catmull_rom_spline_segment(pᵢ₋₁, pᵢ, pᵢ₊₁, pᵢ₊₂, c.alpha, c.tension) + return segment +end +function get_segment(c::CatmullRomSpline, i::Int) + return c.segments[i] +end + +function arc_length(c::CatmullRomSpline) + return sum(c.lengths) +end +function arc_length(c::CatmullRomSpline, t₁, t₂) + segment₁, i₁ = get_segment(c, t₁) + segment₂, i₂ = get_segment(c, t₂) + if i₁ == i₂ # same segment - just integrate directly + t₁′ = map_t_to_segment(c, i₁, t₁) + t₂′ = map_t_to_segment(c, i₂, t₂) + return arc_length(segment₁, t₁′, t₂′) + elseif i₁ == i₂ - 1 # adjacent segments - integrate on each segment + t₁′ = map_t_to_segment(c, i₁, t₁) + t₂′ = map_t_to_segment(c, i₂, t₂) + s₁ = arc_length(segment₁, t₁′, 1.0) + s₂ = arc_length(segment₂, 0.0, t₂′) + return s₁ + s₂ + else # at least one complete segment separates the outer segments + s = 0.0 + for i in (i₁+1):(i₂-1) + s += c.lengths[i] + end + t₁′ = map_t_to_segment(c, i₁, t₁) + t₂′ = map_t_to_segment(c, i₂, t₂) + s₁ = arc_length(segment₁, t₁′, 1.0) + s₂ = arc_length(segment₂, 0.0, t₂′) + return s + s₁ + s₂ + end +end + +total_variation(c::CatmullRomSpline, t₁, t₂) = marked_total_variation(c, t₁, t₂) + +has_lookup_table(c::CatmullRomSpline) = true + +#function is_piecewise_linear(c::CatmullRomSpline) +# tension = c.tension +# return isone(tension) +#end diff --git a/src/data_structures/mesh_refinement/curves/circulararc.jl b/src/data_structures/mesh_refinement/curves/circulararc.jl new file mode 100644 index 000000000..cf2c3b447 --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/circulararc.jl @@ -0,0 +1,166 @@ +""" + CircularArc <: AbstractParametricCurve + +Curve for representing a circular arc, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated +using `circular_arc(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. + +# Fields +- `center::NTuple{2,Float64}`: The center of the arc. +- `radius::Float64`: The radius of the arc. +- `start_angle::Float64`: The angle of the initial point of the arc, in radians. +- `sector_angle::Float64`: The angle of the sector of the arc, in radians. This is given by `end_angle - start_angle`, where `end_angle` is the angle at `last`, and so might be negative for negatively oriented arcs. +- `first::NTuple{2,Float64}`: The first point of the arc. +- `last::NTuple{2,Float64}`: The last point of the arc. +- `pqr::NTuple{3, NTuple{2, Float64}}`: Three points on the circle through the arc. This is needed for [`point_position_relative_to_curve`](@ref). + +!!! warning "Orientation" + + The angles `start_angle` and `end_angle` should be setup such that `start_angle > end_angle` implies a positively oriented arc, + and `start_angle < end_angle` implies a negatively oriented arc. Moreover, they must be in `[0°, 2π°)`. + +# Constructor +You can construct a `CircularArc` using + + CircularArc(first, last, center; positive=true) + +It is up to you to ensure that `first` and `last` are equidistant from `center` - the radius used will be the +distance between `center` and `first`. The `positive` keyword argument is used to determine if the +arc is positively oriented or negatively oriented. +""" +struct CircularArc <: AbstractParametricCurve + center::NTuple{2,Float64} + radius::Float64 + start_angle::Float64 + sector_angle::Float64 + first::NTuple{2,Float64} + last::NTuple{2,Float64} + pqr::NTuple{3,NTuple{2,Float64}} +end +function Base.:(==)(c₁::CircularArc, c₂::CircularArc) + c₁.center ≠ c₂.center && return false + c₁.radius ≠ c₂.radius && return false + c₁.start_angle ≠ c₂.start_angle && return false + c₁.sector_angle ≠ c₂.sector_angle && return false + return true +end + +function CircularArc(p, q, c; positive=true) + px, py = getxy(p) + qx, qy = getxy(q) + cx, cy = getxy(c) + r = dist((px, py), (cx, cy)) + θ₀ = mod(atan(py - cy, px - cx), 2π) + if p == q + θ₁ = θ₀ + else + θ₁ = mod(atan(qy - cy, qx - cx), 2π) + end + θ₀, θ₁ = adjust_θ(θ₀, θ₁, positive) + sector_angle = θ₁ - θ₀ + p′ = (cx + r, cy) + q′ = (cx, cy + r) + r′ = (cx - r, cy) + if positive + pqr = (p′, q′, r′) + else + pqr = (r′, q′, p′) + end + return CircularArc(c, r, θ₀, sector_angle, p, q, pqr) +end +function (c::CircularArc)(t) + if iszero(t) + return c.first + elseif isone(t) + return c.last + else + θ₀, Δθ = c.start_angle, c.sector_angle + θ = Δθ * t + θ₀ + sθ, cθ = sincos(θ) + cx, cy = getxy(c.center) + x = cx + c.radius * cθ + y = cy + c.radius * sθ + return (x, y) + end +end + +function differentiate(c::CircularArc, t) + θ₀, Δθ = c.start_angle, c.sector_angle + θ = Δθ * t + θ₀ + sθ, cθ = sincos(θ) + x = -c.radius * sθ + y = c.radius * cθ + return (x * Δθ, y * Δθ) +end + +function twice_differentiate(c::CircularArc, t) + θ₀, Δθ = c.start_angle, c.sector_angle + θ = Δθ * t + θ₀ + sθ, cθ = sincos(θ) + x = -c.radius * cθ + y = -c.radius * sθ + return (x * Δθ^2, y * Δθ^2) +end + +function point_position_relative_to_curve(kernel::AbstractPredicateKernel, c::CircularArc, p) + a, b, c = c.pqr + cert = point_position_relative_to_circle(kernel, a, b, c, p) + if is_outside(cert) + return Cert.Right + elseif is_inside(cert) + return Cert.Left + else + return Cert.On + end +end + +arc_length(c::CircularArc) = c.radius * abs(c.sector_angle) +arc_length(c::CircularArc, t₁, t₂) = c.radius * abs(c.sector_angle) * (t₂ - t₁) + +curvature(c::CircularArc, t) = sign(c.sector_angle) / c.radius + +total_variation(c::CircularArc) = abs(c.sector_angle) +function total_variation(c::CircularArc, t₁, t₂) + Δθ = c.sector_angle + return abs(Δθ) * (t₂ - t₁) +end + +get_equidistant_split(c::CircularArc, t₁, t₂) = midpoint(t₁, t₂) +get_equivariation_split(c::CircularArc, t₁, t₂) = + let t = midpoint(t₁, t₂) + (t, total_variation(c, t₁, t)) + end + +function get_inverse(c::CircularArc, p) + if p == c.first + return 0.0 + elseif p == c.last + return 1.0 + end + px, py = getxy(p) + cx, cy = getxy(c.center) + r = c.radius + cθ = (px - cx) / r + sθ = (py - cy) / r + θ = atan(sθ, cθ) + Δθ, θ₀ = c.sector_angle, c.start_angle + t = (θ - θ₀) / Δθ + while t < 0 + t += 2π / abs(Δθ) + end + while t > 1 + t -= 2π / abs(Δθ) + end + return t +end + +function _reverse(c::CircularArc) + return CircularArc( + c.center, + c.radius, + c.start_angle + c.sector_angle, + -c.sector_angle, + c.last, + c.first, + reverse(c.pqr) + ) +end \ No newline at end of file diff --git a/src/data_structures/mesh_refinement/curves/ellipticalarc.jl b/src/data_structures/mesh_refinement/curves/ellipticalarc.jl new file mode 100644 index 000000000..45fc7dc89 --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/ellipticalarc.jl @@ -0,0 +1,181 @@ +""" + EllipticalArc <: AbstractParametricCurve + +Curve for representing an elliptical arc, parametrised over `0 ≤ t ≤ 1`. This curve can be evaluated +using `elliptical_arc(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. + +# Fields +- `center::NTuple{2,Float64}`: The center of the ellipse. +- `horz_radius::Float64`: The horizontal radius of the ellipse. +- `vert_radius::Float64`: The vertical radius of the ellipse. +- `rotation_scales::NTuple{2,Float64}`: If `θ` is the angle of rotation of the ellipse, then this is `(sin(θ), cos(θ))`. +- `start_angle::Float64`: The angle of the initial point of the arc measured from `center`, in radians. This angle is measured from the center prior to rotating the ellipse. +- `sector_angle::Float64`: The angle of the sector of the arc, in radians. This is given by `end_angle - start_angle`, where `end_angle` is the angle at `last`, and so might be negative for negatively oriented arcs. +- `first::NTuple{2,Float64}`: The first point of the arc. +- `last::NTuple{2,Float64}`: The last point of the arc. + +# Constructor +You can construct an `EllipticalArc` using + + EllipticalArc(first, last, center, major_radius, minor_radius, rotation; positive=true) + +where `rotation` is the angle of rotation of the ellipse, in degrees. The `positive` keyword argument is used to determine if the +arc is positively oriented or negatively oriented. +""" +struct EllipticalArc <: AbstractParametricCurve + center::NTuple{2,Float64} + horz_radius::Float64 + vert_radius::Float64 + rotation_scales::NTuple{2,Float64} + start_angle::Float64 + sector_angle::Float64 + first::NTuple{2,Float64} + last::NTuple{2,Float64} +end +function Base.:(==)(e₁::EllipticalArc, e₂::EllipticalArc) + e₁.center ≠ e₂.center && return false + e₁.horz_radius ≠ e₂.horz_radius && return false + e₁.vert_radius ≠ e₂.vert_radius && return false + e₁.rotation_scales ≠ e₂.rotation_scales && return false + e₁.start_angle ≠ e₂.start_angle && return false + e₁.sector_angle ≠ e₂.sector_angle && return false + return true +end + +function EllipticalArc(p, q, c, α, β, θ°; positive=true) + px, py = getxy(p) + qx, qy = getxy(q) + cx, cy = getxy(c) + θ = deg2rad(θ°) + sθ, cθ = sincos(θ) + start_cost = inv(α) * (cθ * (px - cx) + sθ * (py - cy)) + start_sint = inv(β) * (-sθ * (px - cx) + cθ * (py - cy)) + start_angle = mod(atan(start_sint, start_cost), 2π) + if p == q + end_angle = start_angle + else + end_cost = inv(α) * (cθ * (qx - cx) + sθ * (qy - cy)) + end_sint = inv(β) * (-sθ * (qx - cx) + cθ * (qy - cy)) + end_angle = mod(atan(end_sint, end_cost), 2π) + end + start_angle, end_angle = adjust_θ(start_angle, end_angle, positive) + sector_angle = end_angle - start_angle + return EllipticalArc(c, α, β, (sθ, cθ), start_angle, sector_angle, p, q) +end +function (e::EllipticalArc)(t) + if iszero(t) + return e.first + elseif isone(t) + return e.last + else + c, α, β, (sinθ, cosθ), θ₀, Δθ = e.center, e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle + t′ = Δθ * t + θ₀ + st, ct = sincos(t′) + cx, cy = getxy(c) + x = cx + α * ct * cosθ - β * st * sinθ + y = cy + α * ct * sinθ + β * st * cosθ + return (x, y) + end +end + +function differentiate(e::EllipticalArc, t) + α, β, (sinθ, cosθ), θ₀, Δθ = e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle + t′ = Δθ * t + θ₀ + st, ct = sincos(t′) + x = -α * st * cosθ - β * ct * sinθ + y = -α * st * sinθ + β * ct * cosθ + return (x * Δθ, y * Δθ) +end + +function twice_differentiate(e::EllipticalArc, t) + α, β, (sinθ, cosθ), θ₀, Δθ = e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle + t′ = Δθ * t + θ₀ + st, ct = sincos(t′) + x = -α * ct * cosθ + β * st * sinθ + y = -α * ct * sinθ - β * st * cosθ + return (x * Δθ^2, y * Δθ^2) +end + +function curvature(e::EllipticalArc, t) + α, β, θ₀, Δθ = e.horz_radius, e.vert_radius, e.start_angle, e.sector_angle + t′ = Δθ * t + θ₀ + st, ct = sincos(t′) + return sign(Δθ) * α * β / (α^2 * st^2 + β^2 * ct^2)^(3 / 2) +end + +function total_variation(e::EllipticalArc, t₁, t₂) + if e.first == e.last && (t₁ == 0 && t₂ == 1) + return 2π + end + T₁ = differentiate(e, t₁) + T₂ = differentiate(e, t₂) + if e.sector_angle > 0 + θ = angle_between(T₂, T₁) + else + θ = angle_between(T₁, T₂) + end + return θ +end + +function point_position_relative_to_curve(kernel::AbstractPredicateKernel, e::EllipticalArc, p) + x, y = getxy(p) + c, α, β, (sinθ, cosθ), Δθ = e.center, e.horz_radius, e.vert_radius, e.rotation_scales, e.sector_angle + cx, cy = getxy(c) + x′ = x - cx + y′ = y - cy + x′′ = x′ * cosθ + y′ * sinθ + y′′ = -x′ * sinθ + y′ * cosθ + x′′′ = x′′ / α + y′′′ = y′′ / β + positive = Δθ > 0 + if positive + a, b, c = (1.0, 0.0), (0.0, 1.0), (-1.0, 0.0) + else + a, b, c = (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0) + end + cert = point_position_relative_to_circle(kernel, a, b, c, (x′′′, y′′′)) + if is_outside(cert) + return Cert.Right + elseif is_inside(cert) + return Cert.Left + else + return Cert.On + end +end + +function get_inverse(e::EllipticalArc, p) + if p == e.first + return 0.0 + elseif p == e.last + return 1.0 + end + px, py = getxy(p) + c, α, β, (sinθ, cosθ), θ₀, Δθ = e.center, e.horz_radius, e.vert_radius, e.rotation_scales, e.start_angle, e.sector_angle + cx, cy = getxy(c) + px′ = px - cx + py′ = py - cy + ct′ = inv(α) * (px′ * cosθ + py′ * sinθ) + st′ = inv(β) * (-px′ * sinθ + py′ * cosθ) + t′ = mod(atan(st′, ct′), 2π) + t = (t′ - θ₀) / Δθ + while t < 0 + t += 2π / abs(Δθ) + end + while t > 1 + t -= 2π / abs(Δθ) + end + return t +end + +function _reverse(c::EllipticalArc) + return EllipticalArc( + c.center, + c.horz_radius, + c.vert_radius, + c.rotation_scales, + c.start_angle + c.sector_angle, + -c.sector_angle, + c.last, + c.first + ) +end \ No newline at end of file diff --git a/src/data_structures/mesh_refinement/curves/linesegment.jl b/src/data_structures/mesh_refinement/curves/linesegment.jl new file mode 100644 index 000000000..cd68b996a --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/linesegment.jl @@ -0,0 +1,133 @@ +""" + LineSegment <: AbstractParametricCurve + +Curve for representing a line segment, parametrised over `0 ≤ t ≤ 1`. This curve can be using +`line_segment(t)` and returns a tuple `(x, y)` of the coordinates of the point on the curve at `t`. + +# Fields +- `first::NTuple{2,Float64}`: The first point of the line segment. +- `last::NTuple{2,Float64}`: The last point of the line segment. +- `length::Float64`: The length of the line segment. + +# Constructor +You can construct a `LineSegment` using + + LineSegment(first, last) +""" +struct LineSegment <: AbstractParametricCurve # line segment + first::NTuple{2,Float64} + last::NTuple{2,Float64} + length::Float64 +end +Base.:(==)(L₁::LineSegment, L₂::LineSegment) = L₁.first == L₂.first && L₁.last == L₂.last + +function LineSegment(p₀, p₁) + return LineSegment(p₀, p₁, dist(p₀, p₁)) +end +function (L::LineSegment)(t) + if iszero(t) + return L.first + elseif isone(t) + return L.last + else + x₀, y₀ = getxy(L.first) + x₁, y₁ = getxy(L.last) + x = x₀ + t * (x₁ - x₀) + y = y₀ + t * (y₁ - y₀) + return (x, y) + end +end + +function differentiate(L::LineSegment, t) + x₀, y₀ = getxy(L.first) + x₁, y₁ = getxy(L.last) + return (x₁ - x₀, y₁ - y₀) +end + +twice_differentiate(::LineSegment, t) = (0.0, 0.0) + +curvature(::LineSegment, t) = 0.0 + +total_variation(::LineSegment) = 0.0 +total_variation(::LineSegment, t₁, t₂) = 0.0 + +""" + point_position_relative_to_curve([kernel::AbstractPredicateKernel=AdaptiveKernel(),] L::LineSegment, p) -> Certificate + +Returns the position of `p` relative to `L`, returning a [`Certificate`](@ref): + +- `Left`: `p` is to the left of `L`. +- `Right`: `p` is to the right of `L`. +- `On`: `p` is on `L`. + +See also [`point_position_relative_to_line`](@ref). + +The `kernel` argument determines how this result is computed, and should be +one of [`ExactKernel`](@ref), [`FastKernel`](@ref), and [`AdaptiveKernel`](@ref) (the default). +See the documentation for more information about these choices. +""" +function point_position_relative_to_curve(kernel::AbstractPredicateKernel, L::LineSegment, p) + cert = point_position_relative_to_line(kernel, L.first, L.last, p) + if is_collinear(cert) + return Cert.On + else + return cert + end +end + +arc_length(L::LineSegment) = L.length +arc_length(L::LineSegment, t₁, t₂) = L.length * (t₂ - t₁) + +get_equidistant_split(L::LineSegment, t₁, t₂) = midpoint(t₁, t₂) +get_equivariation_split(L::LineSegment, t₁, t₂) = (midpoint(t₁, t₂), 0.0) + +function get_inverse(L::LineSegment, p) + if p == L.first + return 0.0 + elseif p == L.last + return 1.0 + end + px, py = getxy(p) + x₀, y₀ = getxy(L.first) + x₁, y₁ = getxy(L.last) + if iszero(x₁ - x₀) + return (py - y₀) / (y₁ - y₀) + else + return (px - x₀) / (x₁ - x₀) + end +end + +""" + angle_between(L₁::LineSegment, L₂::LineSegment) -> Float64 + +Returns the angle between `L₁` and `L₂`, assuming that `L₁.last == L₂.first` (this is not checked). For consistency with +If the segments are part of some domain, then the line segments should be oriented so that the interior is to the left of both segments. +""" +function angle_between(L₁::LineSegment, L₂::LineSegment) + T₁ = differentiate(L₁, 1.0) + T₂ = differentiate(L₂, 0.0) + T₁x, T₁y = getxy(T₁) + T₁′ = (-T₁x, -T₁y) + θ = angle_between(T₁′, T₂) + return θ +end + +function get_circle_intersection(L::LineSegment, t₁, t₂, r) + ℓ = L.length + if iszero(t₁) + t = r / ℓ + elseif isone(t₁) + t = 1 - r / ℓ + else + p, q = L(t₁), L(t₂) + Ls = LineSegment(p, q) + return get_circle_intersection(Ls, 0.0, 1.0, r) + end + return t, L(t) +end + +function _reverse(L::LineSegment) + return LineSegment(L.last, L.first, L.length) +end + +is_linear(::LineSegment) = true \ No newline at end of file diff --git a/src/data_structures/mesh_refinement/curves/piecewiselinear.jl b/src/data_structures/mesh_refinement/curves/piecewiselinear.jl new file mode 100644 index 000000000..2684c35a7 --- /dev/null +++ b/src/data_structures/mesh_refinement/curves/piecewiselinear.jl @@ -0,0 +1,69 @@ +""" + PiecewiseLinear <: AbstractParametricCurve + +Struct for representing a piecewise linear curve. This curve should not be +interacted with or constructed directly. It only exists so that it can be +an [`AbstractParametricCurve`](@ref). Instead, triangulations use this curve to +know that its `boundary_nodes` field should be used instead. + +!!! danger "Existing methods" + + This struct does have fields, namely `points` and `boundary_nodes` (and boundary_nodes should be a contiguous section). These are only used so that + we can use this struct in [`angle_between`](@ref) easily. In particular, we need to allow + for evaluating this curve at `t=0` and at `t=1`, and similarly for differentiating the curve at `t=0` + and at `t=1`. For this, we have defined, letting `L` be a `PiecewiseLinear` curve, `L(0)` to return the first point + on the curve, and the last point otherwise (meaning `L(h)` is constant for `h > 0`), and similarly for differentiation. + Do NOT rely on the implementation of these methods. +""" +struct PiecewiseLinear{P,V} <: AbstractParametricCurve + points::P + boundary_nodes::V +end +Base.show(io::IO, ::PiecewiseLinear) = print(io, "PiecewiseLinear()") +Base.show(io::IO, ::MIME"text/plain", L::PiecewiseLinear) = Base.show(io, L) +Base.:(==)(L1::PiecewiseLinear, L2::PiecewiseLinear) = get_points(L1) == get_points(L2) && get_boundary_nodes(L1) == get_boundary_nodes(L2) +is_piecewise_linear(::PiecewiseLinear) = true +get_points(pl::PiecewiseLinear) = pl.points +get_boundary_nodes(pl::PiecewiseLinear) = pl.boundary_nodes +function (L::PiecewiseLinear)(t) # ONLY FOR EVALUATING AT THE ENDPOINTS. + points = get_points(L) + boundary_nodes = get_boundary_nodes(L) + if iszero(t) + u = get_boundary_nodes(boundary_nodes, 1) + else + n = num_boundary_edges(boundary_nodes) + u = get_boundary_nodes(boundary_nodes, n + 1) + end + p = get_point(points, u) + return p +end +function differentiate(L::PiecewiseLinear, t) + points = get_points(L) + boundary_nodes = get_boundary_nodes(L) + if iszero(t) + u = get_boundary_nodes(boundary_nodes, 1) + v = get_boundary_nodes(boundary_nodes, 2) + else + n = num_boundary_edges(boundary_nodes) + u = get_boundary_nodes(boundary_nodes, n) + v = get_boundary_nodes(boundary_nodes, n + 1) + end + p, q = get_point(points, u), get_point(points, v) + px, py = getxy(p) + qx, qy = getxy(q) + return (qx - px, qy - py) +end + +function get_circle_intersection(L::PiecewiseLinear, t₁, t₂, r) + points = get_points(L) + boundary_nodes = get_boundary_nodes(L) + if iszero(t₁) + u, v = get_boundary_nodes(boundary_nodes, 1), get_boundary_nodes(boundary_nodes, 2) + else + n = num_boundary_edges(boundary_nodes) + u, v = get_boundary_nodes(boundary_nodes, n + 1), get_boundary_nodes(boundary_nodes, n) + end + p, q = get_point(points, u, v) + Ls = LineSegment(p, q) + return get_circle_intersection(Ls, 0.0, 1.0, r) +end \ No newline at end of file diff --git a/src/data_structures/trees/polygon_hierarchy.jl b/src/data_structures/trees/polygon_hierarchy.jl index b67baa616..3ccf11e01 100644 --- a/src/data_structures/trees/polygon_hierarchy.jl +++ b/src/data_structures/trees/polygon_hierarchy.jl @@ -162,7 +162,7 @@ Struct used to define a polygon hierarchy. The hierarchy is represented as a for !!! note "One-based indexing" - Note that the vector definitions for `poylgon_orientations` and `bounding_boxes` are treating the curves with the assumption that they are + Note that the vector definitions for `polygon_orientations` and `bounding_boxes` are treating the curves with the assumption that they are enumerated in the order 1, 2, 3, .... # Constructor diff --git a/src/utils/utils.jl b/src/utils/utils.jl index 73f16c960..3574039fe 100644 --- a/src/utils/utils.jl +++ b/src/utils/utils.jl @@ -520,10 +520,17 @@ function uniquetol(A::Vector{Float64}; tol = 1.0e-12) return Auniq end +const ANY32{N} = Tuple{Any,Any,Any,Any,Any,Any,Any,Any, + Any,Any,Any,Any,Any,Any,Any,Any, + Any,Any,Any,Any,Any,Any,Any,Any, + Any,Any,Any,Any,Any,Any,Any,Any, + Any, Vararg{Any,N}} + """ eval_fnc_at_het_tuple_element(f, tup, idx) Evaluates `f(tup[idx])` in a type-stable way. If `idx > length(tup)`, then `f` is evaluated on the last element of `tup`. +If `length(tup) > 32`, then the function is not type-stable; note that, in this case, `idx > length(tup)` leads to a `BoundsError`. """ @inline function eval_fnc_at_het_tuple_element(f::F, tup::T, idx) where {F, T} return _eval_fnc_at_het_tuple_element(f, idx, tup...) @@ -536,10 +543,14 @@ end return f(el) end +function eval_fnc_at_het_tuple_element(f::F, tup::ANY32{N}, idx) where {F, N} + return f(tup[idx]) +end + """ eval_fnc_at_het_tuple_two_elements(f, tup, idx1, idx2) -Evaluates `f(tup[idx1], tup[idx2])` in a type-stable way. +Evaluates `f(tup[idx1], tup[idx2])` in a type-stable way. If `length(tup) > 32`, then the function is not type-stable. """ @inline function eval_fnc_at_het_tuple_two_elements(f::F, tup::T, idx1, idx2) where {F, T <: Tuple} return _eval_fnc_at_het_tuple_two_elements(f, idx2, tup, idx1, tup...) @@ -559,10 +570,15 @@ end return f(el, el2) end +function eval_fnc_at_het_tuple_two_elements(f::F, tup::ANY32{N}, idx1, idx2) where {F, N} + return f(tup[idx1], tup[idx2]) +end + """ eval_fnc_at_het_tuple_element_with_arg(f, tup, arg, idx) Evaluates `f(tup[idx], arg...)` in a type-stable way. If `idx > length(tup)`, then `f` is evaluated on the last element of `tup`. +If `length(tup) > 32`, then the function is not type-stable; note that, in this case, `idx > length(tup)` leads to a `BoundsError`. """ @inline function eval_fnc_at_het_tuple_element_with_arg(f::F, tup::T, arg, idx) where {F, T} return _eval_fnc_at_het_tuple_element_with_arg(f, idx, arg, tup...) @@ -575,10 +591,15 @@ end return f(el, arg...) end +function eval_fnc_at_het_tuple_element_with_arg(f::F, tup::ANY32{N}, arg, idx) where {F, N} + return f(tup[idx], arg...) +end + """ eval_fnc_at_het_tuple_element_with_arg_and_prearg(f, tup, prearg, arg, idx) Evaluates `f(prearg, tup[idx], arg...)` in a type-stable way. If `idx > length(tup)`, then `f` is evaluated on the last element of `tup`. +If `length(tup) > 32`, then the function is not type-stable; note that, in this case, `idx > length(tup)` leads to a `BoundsError`. """ @inline function eval_fnc_at_het_tuple_element_with_arg_and_prearg(f::F, tup::T, prearg, arg, idx) where {F, T} return _eval_fnc_at_het_tuple_element_with_arg_and_prearg(f, idx, prearg, arg, tup...) @@ -591,6 +612,10 @@ end return f(prearg, el, arg...) end +function eval_fnc_at_het_tuple_element_with_arg_and_prearg(f::F, tup::ANY32{N}, prearg, arg, idx) where {F, N} + return f(prearg, tup[idx], arg...) +end + """ eval_fnc_in_het_tuple(tup, arg, idx) diff --git a/test/data_structures/curves.jl b/test/data_structures/curves.jl index b547d6c19..967b06ee6 100644 --- a/test/data_structures/curves.jl +++ b/test/data_structures/curves.jl @@ -21,6 +21,7 @@ using ReferenceTests @test L.first == p @test L.last == q @test L.length ≈ sqrt(sum((p .- q) .^ 2)) + @test DT.is_linear(L) # Evaluation @test collect(L(0.0)) ≈ collect(p) @@ -120,7 +121,7 @@ using ReferenceTests t′, q′ = DT.get_circle_intersection(L, 1.0, 0.0, r) @test t′ ≈ 0.6464466094067263 && q′ ⪧ (0.6464466094067263, 0.6464466094067263) - # == + ## == p = rand(2) |> Tuple q = rand(2) |> Tuple L1 = LineSegment(p, q) @@ -132,6 +133,23 @@ using ReferenceTests @test L1 ≠ L2 L2 = LineSegment(q, q) @test L1 ≠ L2 + + ## Evaluating at the endpoints + (p, q) = ((-0.21174248248640498, -0.3224838363216223), (0.02115793761243195, 1.0376631964860346)) + L = LineSegment(p, q) + @test L(0.0) == p + @test L(1.0) == q + + ## Reverse + for _ in 1:100 + p, q = rand(2) |> Tuple, rand(2) |> Tuple + L = LineSegment(p, q) + LR = reverse(L) + @test LR.first == q && LR.last == p + for t in LinRange(0, 1, 100) + @test L(t) ⪧ LR(1 - t) + end + end end @testset "PiecewiseLinear" begin @@ -186,13 +204,13 @@ end @test !DT.is_piecewise_linear(arc) @test !DT.is_interpolating(arc) @inferred CircularArc(p1, q1, center1) - negarc = CircularArc(p1, q1, center1, positive = false) + negarc = CircularArc(p1, q1, center1, positive=false) revarc = CircularArc(q1, p1, center1) - revnegarc = CircularArc(q1, p1, center1, positive = false) + revnegarc = CircularArc(q1, p1, center1, positive=false) center2 = (3.0, -5.0) p2 = (6.0, 5.0) circ = CircularArc(p2, p2, center2) - revcirc = CircularArc(p2, p2, center2, positive = false) + revcirc = CircularArc(p2, p2, center2, positive=false) @test arc.center == negarc.center == revarc.center == revnegarc.center == center1 @test circ.center == revcirc.center == center2 @test arc.radius == negarc.radius == revarc.radius == revnegarc.radius ≈ sqrt(41) @@ -400,6 +418,21 @@ end @test arc ≠ revnegarc @test circ ≠ revcirc @test circ ≠ arc + + ## reverse + p, q, ct = (5.0, 5.0), (-4.0, -4.0), (1.0, 0.0) + c = CircularArc(p, q, ct) + cr = CircularArc(q, p, ct, positive=false) + c1 = CircularArc(p, q, ct, positive=false) + c1r = CircularArc(q, p, ct) + _cr = reverse(c) + _c1r = reverse(c1) + for t in LinRange(0, 1, 100) + @test c(t) ⪧ _cr(1 - t) + @test cr(t) ⪧ _cr(t) + @test c1(t) ⪧ _c1r(1 - t) + @test c1r(t) ⪧ _c1r(t) + end end @testset "EllipticalArc" begin @@ -409,11 +442,12 @@ end arc = EllipticalArc(A, B, (Cx, Cy), Rx, Ry, s) @test DT.is_curve_bounded(arc) @test !DT.is_piecewise_linear(arc) + @test !DT.is_linear(arc) @test !DT.is_interpolating(arc) @inferred EllipticalArc(A, B, (Cx, Cy), Rx, Ry, s) - negarc = EllipticalArc(A, B, (Cx, Cy), Rx, Ry, s, positive = false) + negarc = EllipticalArc(A, B, (Cx, Cy), Rx, Ry, s, positive=false) revarc = EllipticalArc(B, A, (Cx, Cy), Rx, Ry, s) - revnegarc = EllipticalArc(B, A, (Cx, Cy), Rx, Ry, s, positive = false) + revnegarc = EllipticalArc(B, A, (Cx, Cy), Rx, Ry, s, positive=false) @test arc.center == negarc.center == revarc.center == revnegarc.center == (Cx, Cy) @test arc.first == negarc.first == revarc.last == revnegarc.last == A @test arc.last == negarc.last == revarc.first == revnegarc.first == B @@ -429,7 +463,7 @@ end @test revarc.sector_angle > 0 @test revnegarc.sector_angle < 0 closed_arc = EllipticalArc(A, A, (Cx, Cy), Rx, Ry, s) - revclosed_arc = EllipticalArc(A, A, (Cx, Cy), Rx, Ry, s, positive = false) + revclosed_arc = EllipticalArc(A, A, (Cx, Cy), Rx, Ry, s, positive=false) @test closed_arc.sector_angle ≈ 2π @test revclosed_arc.sector_angle ≈ -2π @test closed_arc.center == revclosed_arc.center == (Cx, Cy) @@ -668,6 +702,22 @@ end @test arc ≠ revnegarc @test closed_arc ≠ revclosed_arc @test closed_arc ≠ arc + + ## reverse + Rx, Ry, Cx, Cy, s = 2.2, 2.75, 0.6, 0.65, 60.0 + A, B = (-1.9827767129992, 1.4963893939715), (2.5063071261053, -1.2608915244961) + arc = EllipticalArc(A, B, (Cx, Cy), Rx, Ry, s) + revarc = EllipticalArc(B, A, (Cx, Cy), Rx, Ry, s, positive=false) + arc2 = EllipticalArc(A, B, (Cx, Cy), Rx, Ry, s, positive=false) + revarc2 = EllipticalArc(B, A, (Cx, Cy), Rx, Ry, s) + _revarc = reverse(arc) + _revarc2 = reverse(arc2) + for t in LinRange(0, 1, 100) + @test arc(t) ⪧ _revarc(1 - t) + @test revarc(t) ⪧ _revarc(t) + @test arc2(t) ⪧ _revarc2(1 - t) + @test revarc2(t) ⪧ _revarc2(t) + end end @testset "BezierCurve" begin @@ -676,9 +726,10 @@ end bezier = BezierCurve(control_points) @test DT.is_curve_bounded(bezier) @test !DT.is_piecewise_linear(bezier) + @test !DT.is_linear(bezier) @test !DT.is_interpolating(bezier) @test bezier.control_points == control_points - @test typeof(bezier.cache) == Vector{NTuple{2, Float64}} && length(bezier.cache) == 5 + @test typeof(bezier.cache) == Vector{NTuple{2,Float64}} && length(bezier.cache) == 5 @test length(bezier.lookup_table) == 5000 for i in 1:5000 t = (i - 1) / 4999 @@ -708,8 +759,8 @@ end ## Closest point invert(points) = let y = maximum(last.(points)) - return [(x, y - y′) for (x, y′) in points] - end # just to match my reference figure + return [(x, y - y′) for (x, y′) in points] + end # just to match my reference figure control_points = invert([(207.0, 65.0), (84.0, 97.0), (58.0, 196.0), (261.0, 130.0), (216.0, 217.0), (54.0, 122.0), (52.0, 276.0), (85.0, 330.0), (258.0, 334.0), (209.0, 286.0)]) for lookup_steps in (100, 500, 1000) if lookup_steps ≠ 500 @@ -1079,6 +1130,28 @@ end ctrl2 = control_points @test BezierCurve(ctrl1) == BezierCurve(ctrl1) @test BezierCurve(ctrl1) ≠ BezierCurve(ctrl2) + + ## Reverse + control_points = [ + (-12.0, 5.0), (-9.77, 6.29), + (-8.11, 4.55), (-7.47, 1.49), + (-7.61, -1.29), (-9.63, -3.69), + (-13.65, -4.37), (-15.65, -1.25), + (-15.39, 0.93), (-14.17, 1.63), + (-12.37, -0.93), (-13.51, -1.17), + (-12.59, -2.39), (-10.6, -2.47), + (-9.19, 0.11), (-9.95, 2.79), + ] + spl = BezierCurve(control_points) + revspl = BezierCurve(reverse(control_points)) + _revspl = reverse(spl) + @test revspl.control_points == _revspl.control_points + for t in LinRange(0, 1, 1000) + @test spl(t) ⪧ _revspl(1 - t) + @test revspl(t) ⪧ _revspl(t) + end + @test revspl.lookup_table ⪧ _revspl.lookup_table + @test revspl.orientation_markers ≈ _revspl.orientation_markers end @testset "BSpline" begin @@ -1087,9 +1160,10 @@ end spline = BSpline(control_points) @test DT.is_curve_bounded(spline) @test !DT.is_piecewise_linear(spline) + @test !DT.is_linear(spline) @test !DT.is_interpolating(spline) @test spline.control_points ⪧ control_points - @test typeof(spline.cache) == Vector{NTuple{2, Float64}} && length(spline.cache) == 4 + @test typeof(spline.cache) == Vector{NTuple{2,Float64}} && length(spline.cache) == 4 @test spline.knots == [0, 0, 0, 0, 1, 1, 1, 1] @test length(spline.lookup_table) == 5000 for i in 1:5000 @@ -1099,7 +1173,7 @@ end periodic_control_points = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (1 / 2, 1 / 2), (0.0, 0.0)] periodic_spline = BSpline(periodic_control_points) @test periodic_spline.control_points ⪧ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (1 / 2, 1 / 2), (0.0, 0.0)] - @test typeof(periodic_spline.cache) == Vector{NTuple{2, Float64}} && length(periodic_spline.cache) == 6 + @test typeof(periodic_spline.cache) == Vector{NTuple{2,Float64}} && length(periodic_spline.cache) == 6 @test periodic_spline.knots == [0, 0, 0, 0, 1, 2, 3, 3, 3, 3] @test length(periodic_spline.lookup_table) == 5000 for i in 1:5000 @@ -1107,9 +1181,9 @@ end end longer_control_points = [(0.3, 0.3), (0.5, -1.0), (2.0, 0.0), (2.5, 3.2), (-10.0, 10.0)] - longer_spline = BSpline(longer_control_points; lookup_steps = 2500) + longer_spline = BSpline(longer_control_points; lookup_steps=2500) @test longer_spline.control_points ⪧ longer_control_points - @test typeof(longer_spline.cache) == Vector{NTuple{2, Float64}} && length(longer_spline.cache) == 5 + @test typeof(longer_spline.cache) == Vector{NTuple{2,Float64}} && length(longer_spline.cache) == 5 @test longer_spline.knots == [0, 0, 0, 0, 1, 2, 2, 2, 2] @test length(longer_spline.lookup_table) == 2500 for i in 1:2500 @@ -1117,9 +1191,9 @@ end end quadratic_control_points = [(0.1, 0.1), (0.2, 0.2), (0.5, 0.8), (1.0, 2.0), (0.0, 15.0), (-5.0, 10.0), (-10.0, 0.0)] - quadratic_spline = BSpline(quadratic_control_points; degree = 2) + quadratic_spline = BSpline(quadratic_control_points; degree=2) @test quadratic_spline.control_points ⪧ quadratic_control_points - @test typeof(quadratic_spline.cache) == Vector{NTuple{2, Float64}} && length(quadratic_spline.cache) == 7 + @test typeof(quadratic_spline.cache) == Vector{NTuple{2,Float64}} && length(quadratic_spline.cache) == 7 @test quadratic_spline.knots == [0, 0, 0, 1, 2, 3, 4, 5, 5, 5] @test length(quadratic_spline.lookup_table) == 5000 for i in 1:5000 @@ -1127,9 +1201,9 @@ end end sextic_control_points = [(cos(t), sin(t)) for t in LinRange(0, π - π / 3, 10)] - sextic_spline = BSpline(sextic_control_points; degree = 6) + sextic_spline = BSpline(sextic_control_points; degree=6) @test sextic_spline.control_points ⪧ sextic_control_points - @test typeof(sextic_spline.cache) == Vector{NTuple{2, Float64}} && length(sextic_spline.cache) == 10 + @test typeof(sextic_spline.cache) == Vector{NTuple{2,Float64}} && length(sextic_spline.cache) == 10 @test sextic_spline.knots == [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 4, 4, 4, 4] @test length(sextic_spline.lookup_table) == 5000 for i in 1:5000 @@ -1138,9 +1212,9 @@ end quadratic_periodic_control_points = [(cos(t), sin(t)) for t in LinRange(0, 2π, 5)] quadratic_periodic_control_points[end] = quadratic_periodic_control_points[1] - quadratic_periodic_spline = BSpline(quadratic_periodic_control_points; degree = 2) + quadratic_periodic_spline = BSpline(quadratic_periodic_control_points; degree=2) @test quadratic_periodic_spline.control_points ⪧ quadratic_periodic_control_points - @test typeof(quadratic_periodic_spline.cache) == Vector{NTuple{2, Float64}} && length(quadratic_periodic_spline.cache) == 5 + @test typeof(quadratic_periodic_spline.cache) == Vector{NTuple{2,Float64}} && length(quadratic_periodic_spline.cache) == 5 @test quadratic_periodic_spline.knots == [0, 0, 0, 1, 2, 3, 3, 3] @test length(quadratic_periodic_spline.lookup_table) == 5000 for i in 1:5000 @@ -1276,10 +1350,10 @@ end t = LinRange(0, 1, 25000) f = tuple.(sin.(π .* t), cos.(π .* t)) spl = BSpline(f) - for t in t[3:(end - 2)] + for t in t[3:(end-2)] @test spl(t) ⪧ (sin(π * t), cos(π * t)) rtol = 1.0e-2 der = DT.differentiate(spl, t) - @test ⪧(der, (π * cos(π * t), -π * sin(π * t)); atol = 1.0e-2) + @test ⪧(der, (π * cos(π * t), -π * sin(π * t)); atol=1.0e-2) end t = LinRange(0, 1, 25000) @@ -1287,10 +1361,10 @@ end f[end] = f[begin] spl = BSpline(f) ctr = 0 - for t in t[3:(end - 2)] + for t in t[3:(end-2)] @test spl(t) ⪧ (sin(2π * t), cos(2π * t)) rtol = 1.0e-2 der = DT.differentiate(spl, t) - @test ⪧(der, (2π * cos(2π * t), -2π * sin(2π * t)); atol = 1.0e-2) + @test ⪧(der, (2π * cos(2π * t), -2π * sin(2π * t)); atol=1.0e-2) end t = LinRange(0, 1, 1500) @@ -1307,8 +1381,8 @@ end else der3 = (spl(t) .- spl(t - h)) ./ h end - flag1 = ⪧(der1, der2, rtol = 1.0e-6, atol = 1.0e-6) - flag2 = ⪧(der2, der3, rtol = 1.0e-3, atol = 1.0e-6) + flag1 = ⪧(der1, der2, rtol=1.0e-6, atol=1.0e-6) + flag2 = ⪧(der2, der3, rtol=1.0e-3, atol=1.0e-6) @test flag1 || flag2 end end @@ -1335,9 +1409,9 @@ end t = LinRange(0, 1, 25000) f = tuple.(sin.(π .* t), cos.(π .* t)) spl = BSpline(f) - for t in t[4:(end - 4)] + for t in t[4:(end-4)] der = DT.twice_differentiate(spl, t) - @test ⪧(der, (-π^2 * sin(π * t), -π^2 * cos(π * t)); atol = 1.0e-2) + @test ⪧(der, (-π^2 * sin(π * t), -π^2 * cos(π * t)); atol=1.0e-2) end ## Thrice differentiate @@ -1356,8 +1430,8 @@ end else der3 = (DT.twice_differentiate(spl, t) .- DT.twice_differentiate(spl, t - h)) ./ h end - flag1 = ⪧(der1, der2, rtol = 1.0e-6, atol = 1.0e-6) - flag2 = ⪧(der2, der3, rtol = 1.0e-3, atol = 1.0e-6) + flag1 = ⪧(der1, der2, rtol=1.0e-6, atol=1.0e-6) + flag2 = ⪧(der2, der3, rtol=1.0e-3, atol=1.0e-6) @test flag1 || flag2 end end @@ -1365,9 +1439,9 @@ end t = LinRange(0, 1, 2500) f = tuple.(sin.(π .* t), cos.(π .* t)) spl = BSpline(f) - for t in t[4:(end - 4)] + for t in t[4:(end-4)] der = DT.thrice_differentiate(spl, t) - @test ⪧(der, (-π^3 * cos(π * t), π^3 * sin(π * t)); atol = 1.0e-2, rtol = 1.0e-2) + @test ⪧(der, (-π^3 * cos(π * t), π^3 * sin(π * t)); atol=1.0e-2, rtol=1.0e-2) end ## Closest point @@ -1665,8 +1739,29 @@ end ctrl1 = [(0.0, 1.3), (17.3, 5.0), (-1.0, 2.0), (50.0, 23.0), (17.3, -2.0), (27.3, 50.1)] ctrl2 = [(5.3, 1.3), (17.5, 23.0), (17.3, 200.0), (173.0, 1.3), (0.0, 0.0)] @test BSpline(ctrl1) == BSpline(ctrl1) - @test BSpline(ctrl1) ≠ BSpline(ctrl1, degree = 2) + @test BSpline(ctrl1) ≠ BSpline(ctrl1, degree=2) @test BSpline(ctrl1) ≠ BSpline(ctrl2) + + ## reverse + for c in (spline, periodic_spline, longer_spline, quadratic_spline, sextic_spline, quadratic_periodic_spline) + rev = reverse(c) + for t in LinRange(0, 1, 1500) + @test c(t) ⪧ rev(1 - t) + end + end + control_points = [(cos(t), sin(t)) for t in LinRange(0, π - π / 3, 10)] + spl = BSpline(control_points) + revspl = BSpline(reverse(control_points)) + _revspl = reverse(spl) + for t in LinRange(0, 1, 1500) + @test spl(t) ⪧ _revspl(1 - t) + @test revspl(t) ⪧ _revspl(t) + end + @test _revspl.control_points == revspl.control_points + @test _revspl.knots == revspl.knots + @test _revspl.cache == revspl.cache + @test _revspl.lookup_table ⪧ revspl.lookup_table + @test _revspl.orientation_markers ≈ revspl.orientation_markers end @testset "CatmullRomSpline" begin @@ -1683,6 +1778,7 @@ end @test DT.is_curve_bounded(_spl) @test !DT.is_interpolating(_spl) @test !DT.is_piecewise_linear(_spl) + @test !DT.is_linear(_spl) @test _spl(0.0) == p₁ @test _spl(1.0) == p₂ @inferred _spl(rand()) @@ -1692,12 +1788,12 @@ end end end t = LinRange(0, 1, 1500) - fig = Figure(fontsize = 44) + fig = Figure(fontsize=44) for i in 1:3 for j in 1:3 - ax = Axis(fig[i, j], xlabel = L"x", ylabel = L"y", title = L"α = %$(α[i]), τ = %$(τ[j])", titlealign = :left, width = 400, height = 400) - lines!(ax, spl[i, j].(t), color = :blue, linewidth = 6) - scatter!(ax, [p₀, p₁, p₂, p₃], color = [:blue, :black, :red, :green], markersize = 16) + ax = Axis(fig[i, j], xlabel=L"x", ylabel=L"y", title=L"α = %$(α[i]), τ = %$(τ[j])", titlealign=:left, width=400, height=400) + lines!(ax, spl[i, j].(t), color=:blue, linewidth=6) + scatter!(ax, [p₀, p₁, p₂, p₃], color=[:blue, :black, :red, :green], markersize=16) end end resize_to_layout!(fig) @@ -1716,67 +1812,83 @@ end @inferred DT.thrice_differentiate(_spl, t) end end + + ## reverse + p₀ = (0.0, 0.0) + p₁ = (1.0, 1.0) + p₂ = (2.0, -1.0) + p₃ = (3.0, 0.0) + spl = DT.catmull_rom_spline_segment(p₀, p₁, p₂, p₃, 0.0, 1.0) + revspl = DT.catmull_rom_spline_segment(p₃, p₂, p₁, p₀, 0.0, 1.0) + _revspl = reverse(spl) + for t in LinRange(0, 1, 1500) + @test spl(t) ⪧ _revspl(1 - t) + @test revspl(t) ⪧ _revspl(t) + @test DT.differentiate(spl, t) ⪧ .-DT.differentiate(revspl, 1 - t) + @test DT.twice_differentiate(spl, t) ⪧ DT.twice_differentiate(revspl, 1 - t) + @test DT.thrice_differentiate(spl, t) ⪧ .-DT.thrice_differentiate(revspl, 1 - t) + end end ## Extrapolations p₁, p₂, p₃, p₄, x = (2.571, 4.812), (2.05, 17.81), (17.3, -25.3), (0.5, 0.3), 10.0 perms_1234 = [ - 4 3 2 1 - 4 3 1 2 - 4 2 3 1 - 4 2 1 3 - 4 1 3 2 - 4 1 2 3 - 3 4 2 1 - 3 4 1 2 - 3 2 4 1 - 3 2 1 4 - 3 1 4 2 - 3 1 2 4 - 2 4 3 1 - 2 4 1 3 - 2 3 4 1 - 2 3 1 4 - 2 1 4 3 - 2 1 3 4 - 1 4 3 2 - 1 4 2 3 - 1 3 4 2 - 1 3 2 4 - 1 2 4 3 - 1 2 3 4 - ] + 4 3 2 1 + 4 3 1 2 + 4 2 3 1 + 4 2 1 3 + 4 1 3 2 + 4 1 2 3 + 3 4 2 1 + 3 4 1 2 + 3 2 4 1 + 3 2 1 4 + 3 1 4 2 + 3 1 2 4 + 2 4 3 1 + 2 4 1 3 + 2 3 4 1 + 2 3 1 4 + 2 1 4 3 + 2 1 3 4 + 1 4 3 2 + 1 4 2 3 + 1 3 4 2 + 1 3 2 4 + 1 2 4 3 + 1 2 3 4 + ] th4 = [DT.thiele4((p₁, p₂, p₃, p₄)[[i, j, k, ℓ]]..., x)[1] for (i, j, k, ℓ) in eachrow(perms_1234)] th3 = [DT.thiele3((p₁, p₂, p₃, p₄)[[i, j, k]]..., x)[1] for (i, j, k, ℓ) in eachrow(perms_1234)] quad = [DT.quadratic_interp((p₁, p₂, p₃, p₄)[[i, j, k]]..., x)[1] for (i, j, k, ℓ) in eachrow(perms_1234)] th4th3quad = [ - -12.5908 -31.3945 44.1259 - -12.5908 -91.4788 3.2565 - -12.5908 -31.3945 44.1259 - -12.5908 1.95457 -1214.16 - -12.5908 -91.4788 3.2565 - -12.5908 1.95457 -1214.16 - -12.5908 -31.3945 44.1259 - -12.5908 -91.4788 3.2565 - -12.5908 -31.3945 44.1259 - -12.5908 -22.2832 -91.8257 - -12.5908 -91.4788 3.2565 - -12.5908 -22.2832 -91.8257 - -12.5908 -31.3945 44.1259 - -12.5908 1.95457 -1214.16 - -12.5908 -31.3945 44.1259 - -12.5908 -22.2832 -91.8257 - -12.5908 1.95457 -1214.16 - -12.5908 -22.2832 -91.8257 - -12.5908 -91.4788 3.2565 - -12.5908 1.95457 -1214.16 - -12.5908 -91.4788 3.2565 - -12.5908 -22.2832 -91.8257 - -12.5908 1.95457 -1214.16 - -12.5908 -22.2832 -91.8257 - ] + -12.5908 -31.3945 44.1259 + -12.5908 -91.4788 3.2565 + -12.5908 -31.3945 44.1259 + -12.5908 1.95457 -1214.16 + -12.5908 -91.4788 3.2565 + -12.5908 1.95457 -1214.16 + -12.5908 -31.3945 44.1259 + -12.5908 -91.4788 3.2565 + -12.5908 -31.3945 44.1259 + -12.5908 -22.2832 -91.8257 + -12.5908 -91.4788 3.2565 + -12.5908 -22.2832 -91.8257 + -12.5908 -31.3945 44.1259 + -12.5908 1.95457 -1214.16 + -12.5908 -31.3945 44.1259 + -12.5908 -22.2832 -91.8257 + -12.5908 1.95457 -1214.16 + -12.5908 -22.2832 -91.8257 + -12.5908 -91.4788 3.2565 + -12.5908 1.95457 -1214.16 + -12.5908 -91.4788 3.2565 + -12.5908 -22.2832 -91.8257 + -12.5908 1.95457 -1214.16 + -12.5908 -22.2832 -91.8257 + ] @test [th4 th3 quad] ≈ th4th3quad rtol = 1.0e-5 control_points = [(-9.0, 5.0), (-7.0, 6.0), (-6.0, 4.0), (-3.0, 5.0)] @@ -1784,6 +1896,7 @@ end spl = CatmullRomSpline(control_points) pspl = CatmullRomSpline(periodic_control_points) @test !DT.is_piecewise_linear(spl) + @test !DT.is_linear(spl) @test DT.is_interpolating(spl) @test spl.left ⪧ (-11.0, 4.7894736842105265) @test spl.right ⪧ (0.0, 5.243243243243242) @@ -1816,16 +1929,16 @@ end alpha = 1 / 2 tension = 0.0 else - spl = CatmullRomSpline(control_points; lookup_steps, _alpha = alpha, _tension = tension) - pspl = CatmullRomSpline(periodic_control_points; lookup_steps, _alpha = alpha, _tension = tension) + spl = CatmullRomSpline(control_points; lookup_steps, _alpha=alpha, _tension=tension) + pspl = CatmullRomSpline(periodic_control_points; lookup_steps, _alpha=alpha, _tension=tension) end knots = zeros(4) pknots = zeros(6) for i in 2:4 - knots[i] = knots[i - 1] + norm(control_points[i] .- control_points[i - 1])^alpha + knots[i] = knots[i-1] + norm(control_points[i] .- control_points[i-1])^alpha end for i in 2:6 - pknots[i] = pknots[i - 1] + norm(periodic_control_points[i] .- periodic_control_points[i - 1])^alpha + pknots[i] = pknots[i-1] + norm(periodic_control_points[i] .- periodic_control_points[i-1])^alpha end left = DT.extend_left_control_point(control_points) right = DT.extend_right_control_point(control_points) @@ -1833,8 +1946,8 @@ end pright = DT.extend_right_control_point(periodic_control_points) knots ./= knots[end] pknots ./= pknots[end] - lookup_table = Vector{NTuple{2, Float64}}(undef, lookup_steps) - plookup_table = Vector{NTuple{2, Float64}}(undef, lookup_steps) + lookup_table = Vector{NTuple{2,Float64}}(undef, lookup_steps) + plookup_table = Vector{NTuple{2,Float64}}(undef, lookup_steps) for i in 1:lookup_steps t = (i - 1) / (lookup_steps - 1) lookup_table[i] = spl(t) @@ -1995,7 +2108,7 @@ end _t, _q = closest_point_on_curve(spl, p) if !(spl == pspl) @test _t ≈ t′ rtol = 1.0e-1 atol = 1.0e-1 - elseif !isapprox(_t, 0, atol = 1.0e-3) && !isapprox(_t, 1, atol = 1.0e-3) && !isapprox(t′, 0, atol = 1.0e-3) && !isapprox(t′, 1, atol = 1.0e-3) + elseif !isapprox(_t, 0, atol=1.0e-3) && !isapprox(_t, 1, atol=1.0e-3) && !isapprox(t′, 0, atol=1.0e-3) && !isapprox(t′, 1, atol=1.0e-3) @test _t ≈ t′ rtol = 1.0e-1 atol = 1.0e-1 end @test q ⪧ _q rtol = 1.0e-1 atol = 1.0e-1 @@ -2007,7 +2120,7 @@ end @test q ⪧ spl(t′) if !(spl == pspl) @test t ≈ t′ rtol = 1.0e-1 atol = 1.0e-1 - elseif !isapprox(t, 0, atol = 1.0e-3) && !isapprox(t, 1, atol = 1.0e-3) + elseif !isapprox(t, 0, atol=1.0e-3) && !isapprox(t, 1, atol=1.0e-3) @test t ≈ t′ rtol = 1.0e-1 atol = 1.0e-1 end @test p ⪧ q rtol = 1.0e-1 atol = 1.0e-1 @@ -2213,8 +2326,30 @@ end @test CatmullRomSpline(ctrl1) == CatmullRomSpline(ctrl1) @test CatmullRomSpline(ctrl1) ≠ CatmullRomSpline(ctrl2) @test CatmullRomSpline(ctrl2) == CatmullRomSpline(ctrl2) - @test CatmullRomSpline(ctrl1, _alpha = 0.3) ≠ CatmullRomSpline(ctrl1) - @test CatmullRomSpline(ctrl1, _tension = 0.2) ≠ CatmullRomSpline(ctrl1) + @test CatmullRomSpline(ctrl1, _alpha=0.3) ≠ CatmullRomSpline(ctrl1) + @test CatmullRomSpline(ctrl1, _tension=0.2) ≠ CatmullRomSpline(ctrl1) + + ## reverse + control_points = [(-9.0, 5.0), (-7.0, 6.0), (-6.0, 4.0), (-3.0, 5.0), (0.0, 0.0), (2.0, -5.0), (2.0, 10.0), (5.0, 12.0), (-9.0, 10.0)] + spl = CatmullRomSpline(control_points) + revspl = CatmullRomSpline(reverse(control_points)) + _revspl = reverse(spl) + @test revspl.control_points == _revspl.control_points + @test revspl.knots ≈ _revspl.knots + @test revspl.lookup_table ⪧ _revspl.lookup_table + @test revspl.alpha == _revspl.alpha + @test revspl.tension == _revspl.tension + @test revspl.left == _revspl.left + @test revspl.right == _revspl.right + @test revspl.lengths ≈ _revspl.lengths + @test revspl.orientation_markers ≈ _revspl.orientation_markers + for t in LinRange(0, 1, 15000) + @test revspl(t) ⪧ _revspl(t) + @test _revspl(t) ⪧ spl(1 - t) + @test DT.differentiate(revspl, t) ⪧ DT.differentiate(_revspl, t) + @test DT.twice_differentiate(revspl, t) ⪧ DT.twice_differentiate(_revspl, t) + @test DT.thrice_differentiate(revspl, t) ⪧ DT.thrice_differentiate(_revspl, t) + end end @testset "angle_between" begin @@ -2292,10 +2427,10 @@ end @inferred DT.angle_between(L5, L1) A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V = (14.0, -2.0), (12.0, 2.0), - (0.0, 6.0), (12.0, -2.0), (0.0, -2.0), (2.0, -6.0), (0.0, -8.0), - (4.0, -8.0), (2.0, -4.0), (6.0, -4.0), (6.0, -14.0), (14.0, -14.0), - (8.0, -12.0), (14.0, -12.0), (8.0, -8.0), (16.0, -8.0), (8.0, -4.0), - (16.0, -4.0), (16.0, 6.0), (2.0, 6.0), (8.0, 4.0), (14.0, 4.0) + (0.0, 6.0), (12.0, -2.0), (0.0, -2.0), (2.0, -6.0), (0.0, -8.0), + (4.0, -8.0), (2.0, -4.0), (6.0, -4.0), (6.0, -14.0), (14.0, -14.0), + (8.0, -12.0), (14.0, -12.0), (8.0, -8.0), (16.0, -8.0), (8.0, -4.0), + (16.0, -4.0), (16.0, 6.0), (2.0, 6.0), (8.0, 4.0), (14.0, 4.0) LAB = LineSegment(A, B) LBC = LineSegment(B, C) LCD = LineSegment(C, D) @@ -2409,7 +2544,7 @@ end c₃ = CatmullRomSpline([(12.0, -3.0), (12.0, -4.0), (5.0, -10.0), (0.0, -10.0)]) c₄ = DT.PiecewiseLinear([(0.0, -10.0), (-10.0, -3.0), (-7.0, -3.0), (-5.0, -4.0), (-5.0, 2.0)], [1, 2, 3, 4, 5]) c₅ = BSpline([(-5.0, 2.0), (0.0, 8.0), (0.0, 10.0), (5.0, 1.0), (10.0, 0.0)]) - c₆ = CircularArc((10.0, 0.0), (0.0, 0.0), (5.0, 0.0), positive = false) + c₆ = CircularArc((10.0, 0.0), (0.0, 0.0), (5.0, 0.0), positive=false) θ12 = DT.angle_between(c₁, c₂) |> rad2deg θ23 = DT.angle_between(c₂, c₃) |> rad2deg θ34 = DT.angle_between(c₃, c₄) |> rad2deg diff --git a/test/refinement/curve_bounded.jl b/test/refinement/curve_bounded.jl index 29031bc3a..30609ee25 100644 --- a/test/refinement/curve_bounded.jl +++ b/test/refinement/curve_bounded.jl @@ -90,7 +90,7 @@ segments_III = Set([(n + 2, n + 1)]) fpoints_III_extra_segments = flatten_boundary_nodes(points_III_extra_segments, curve_III, segments_III) curve_IV = [CircularArc((1.0, 0.0), (1.0, 0.0), (0.0, 0.0))] -points_IV = NTuple{2, Float64}[] +points_IV = NTuple{2,Float64}[] fpoints_IV = flatten_boundary_nodes(points_IV, curve_IV) points_IV_extra = copy(points_IV) push!(points_IV_extra, (0.99, 0.14), (0.0, 0.99), (-0.99, 0.0), (0.99, 0.0), (0.0, -0.99), (0.0, 0.0), (0.5, -0.5)) @@ -130,7 +130,7 @@ curve_VIII = [ [1, 2, 3, 4, 5], [DT.EllipticalArc((0.0, 0.0), (2.0, -2.0), (1.0, -1.0), sqrt(2), sqrt(2), 45.0)], [6, 7, 8, 9, 10], - [CatmullRomSpline([(10.0, -3.0), (20.0, 0.0), (18.0, 0.0), (10.0, 0.0)], lookup_steps = 5000)], + [CatmullRomSpline([(10.0, -3.0), (20.0, 0.0), (18.0, 0.0), (10.0, 0.0)], lookup_steps=5000)], ] points_VIII = [ (10.0, 0.0), (8.0, 0.0), (4.0, 0.0), (2.0, 2.0), (0.0, 0.0), (2.0, -2.0), @@ -142,13 +142,13 @@ push!(points_VIII_extra, (5.0, -0.01), (10.0, -1.0), (15.0, -1.95), (0.0, -1.0), curve_IX = [ - [ - [1, 2, 3, 4, 5, 6, 7, 1], - ], - [ - [CircularArc((0.6, 0.5), (0.6, 0.5), (0.5, 0.5), positive = false)], - ], -] + [ + [1, 2, 3, 4, 5, 6, 7, 1], + ], + [ + [CircularArc((0.6, 0.5), (0.6, 0.5), (0.5, 0.5), positive=false)], + ], + ] points_IX = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.5, 1.5), (0.0, 1.0), (0.0, 0.5), (0.0, 0.2)] fpoints_IX = flatten_boundary_nodes(points_IX, curve_IX) points_IX_extra = copy(points_IX) @@ -186,7 +186,7 @@ curve_XI = [ [12, 11, 10, 12], ], [ - [CircularArc((1.1, -3.0), (1.1, -3.0), (0.0, -3.0), positive = false)], + [CircularArc((1.1, -3.0), (1.1, -3.0), (0.0, -3.0), positive=false)], ], ] points_XI = [(-2.0, 0.0), (0.0, 0.0), (2.0, 0.0), (-2.0, -5.0), (2.0, -5.0), (2.0, -1 / 10), (-2.0, -1 / 10), (-1.0, -3.0), (0.0, -4.0), (0.0, -2.3), (-0.5, -3.5), (0.9, -3.0)] @@ -206,7 +206,7 @@ points_XII = [ (0.0, -0.1), (-0.1, 0.0), (0.4, 0.2), (0.2, 0.4), (0.0, 0.6), ] -curve_XII = [[[BSpline(ctrl, lookup_steps = 25000)]], [[1, 8, 7, 6, 5, 4, 3, 2, 1]]] +curve_XII = [[[BSpline(ctrl, lookup_steps=25000)]], [[1, 8, 7, 6, 5, 4, 3, 2, 1]]] fpoints_XII = flatten_boundary_nodes(points_XII, curve_XII) points_XII_extra = copy(points_XII) push!(points_XII_extra, (0.5, -0.15), (1.0, -0.1), (1.0, 0.25), (0.0, 0.75), (-0.01, 0.0)) @@ -364,53 +364,53 @@ end boundary_nodes = deepcopy(curve_VI) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) @test boundary_curves == DT.to_boundary_curves(points, boundary_nodes) && - new_boundary_nodes == [[10, 11], [11, 5], [5, 6, 10]] && - points == [(0.1, 0.1), (0.15, 0.15), (0.23, 0.23), (0.009, 0.11), (0.0, -2.0), (0.2, -1.7), (0.000591, 0.00019), (0.111, -0.005), (-0.0001, -0.00991), (1.0, 0.0), (0.0, 1.0)] + new_boundary_nodes == [[10, 11], [11, 5], [5, 6, 10]] && + points == [(0.1, 0.1), (0.15, 0.15), (0.23, 0.23), (0.009, 0.11), (0.0, -2.0), (0.2, -1.7), (0.000591, 0.00019), (0.111, -0.005), (-0.0001, -0.00991), (1.0, 0.0), (0.0, 1.0)] points = deepcopy(points_VII) boundary_nodes = deepcopy(curve_VII) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) @test boundary_curves == DT.to_boundary_curves(points, boundary_nodes) && - new_boundary_nodes == [[1, 3], [3, 1]] && - points == [(2.0, 0.0), (0.0, 0.5), (-2.0, 0.0)] + new_boundary_nodes == [[1, 3], [3, 1]] && + points == [(2.0, 0.0), (0.0, 0.5), (-2.0, 0.0)] points = deepcopy(points_VIII) boundary_nodes = deepcopy(curve_VIII) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) @test boundary_curves == DT.to_boundary_curves(points, boundary_nodes) && - new_boundary_nodes == [[1, 2, 3, 4, 5], [5, 6], [6, 7, 8, 9, 10], [10, 1]] && - points == [ - (10.0, 0.0), (8.0, 0.0), (4.0, 0.0), (2.0, 2.0), (0.0, 0.0), (2.0, -2.0), - (2.5, -2.0), (3.5, -2.0), (4.5, -3.0), (10.0, -3.0), (10.0, -0.2), (14.0, -0.05), - ] + new_boundary_nodes == [[1, 2, 3, 4, 5], [5, 6], [6, 7, 8, 9, 10], [10, 1]] && + points == [ + (10.0, 0.0), (8.0, 0.0), (4.0, 0.0), (2.0, 2.0), (0.0, 0.0), (2.0, -2.0), + (2.5, -2.0), (3.5, -2.0), (4.5, -3.0), (10.0, -3.0), (10.0, -0.2), (14.0, -0.05), + ] points = deepcopy(points_IX) boundary_nodes = deepcopy(curve_IX) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) @test boundary_curves == DT.to_boundary_curves(points, boundary_nodes) && - new_boundary_nodes == [[[1, 2, 3, 4, 5, 6, 7, 1]], [[8, 8]]] && - points == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.5, 1.5), (0.0, 1.0), (0.0, 0.5), (0.0, 0.2), (0.6, 0.5)] + new_boundary_nodes == [[[1, 2, 3, 4, 5, 6, 7, 1]], [[8, 8]]] && + points == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.5, 1.5), (0.0, 1.0), (0.0, 0.5), (0.0, 0.2), (0.6, 0.5)] points = deepcopy(points_X) boundary_nodes = deepcopy(curve_X) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) @test boundary_curves == DT.to_boundary_curves(points, boundary_nodes) && - new_boundary_nodes == [[[1, 2, 3], [3, 1]], [[4, 8], [8, 7, 6, 5, 4]]] && - points == [(-2.0, 0.0), (0.0, 0.0), (2.0, 0.0), (-1.0, 0.2), (-1.0, 0.1), (0.0, 0.1), (1.0, 0.1), (1.0, 0.2)] + new_boundary_nodes == [[[1, 2, 3], [3, 1]], [[4, 8], [8, 7, 6, 5, 4]]] && + points == [(-2.0, 0.0), (0.0, 0.0), (2.0, 0.0), (-1.0, 0.2), (-1.0, 0.1), (0.0, 0.1), (1.0, 0.1), (1.0, 0.2)] points = deepcopy(points_XI) boundary_nodes = deepcopy(curve_XI) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) @test boundary_curves == DT.to_boundary_curves(points, boundary_nodes) && - new_boundary_nodes == [[[1, 2, 3], [3, 1]], [[13, 13]], [[4, 5, 6, 7, 4]], [[14, 8], [8, 14]], [[12, 11, 10, 12]], [[15, 15]]] && - points == [(-2.0, 0.0), (0.0, 0.0), (2.0, 0.0), (-2.0, -5.0), (2.0, -5.0), (2.0, -1 / 10), (-2.0, -1 / 10), (-1.0, -3.0), (0.0, -4.0), (0.0, -2.3), (-0.5, -3.5), (0.9, -3.0), (0.0, 0.4), (0.0, -2.0), (1.1, -3.0)] + new_boundary_nodes == [[[1, 2, 3], [3, 1]], [[13, 13]], [[4, 5, 6, 7, 4]], [[14, 8], [8, 14]], [[12, 11, 10, 12]], [[15, 15]]] && + points == [(-2.0, 0.0), (0.0, 0.0), (2.0, 0.0), (-2.0, -5.0), (2.0, -5.0), (2.0, -1 / 10), (-2.0, -1 / 10), (-1.0, -3.0), (0.0, -4.0), (0.0, -2.3), (-0.5, -3.5), (0.9, -3.0), (0.0, 0.4), (0.0, -2.0), (1.1, -3.0)] end @testset "BoundaryEnricher" begin all_points = deepcopy.((points_I, points_II, points_III, points_IV, points_V, points_VI, points_VII, points_VIII, points_IX, points_X, points_XI)) all_boundary_nodes = deepcopy.((curve_I, curve_II, curve_III, curve_IV, curve_V, curve_VI, curve_VII, curve_VIII, curve_IX, curve_X, curve_XI)) for (points, boundary_nodes) in zip(all_points, all_boundary_nodes) - enricher = DT.BoundaryEnricher(points, boundary_nodes; IntegerType = Int) + enricher = DT.BoundaryEnricher(points, boundary_nodes; IntegerType=Int) @test get_points(enricher) == enricher.points == points @test get_boundary_nodes(enricher) == enricher.boundary_nodes @test DT.get_boundary_curves(enricher) == enricher.boundary_curves @@ -428,7 +428,7 @@ end @test DT.get_queue(enricher) ⊢ DT.Queue{Int}() @test DT.get_boundary_edge_map(enricher) == enricher.boundary_edge_map == DT.construct_boundary_edge_map(DT.get_boundary_nodes(enricher)) @test !DT.has_segments(enricher) - @test DT.get_segments(enricher) == Set{NTuple{2, Int}}() + @test DT.get_segments(enricher) == Set{NTuple{2,Int}}() @test !DT.is_segment(enricher, 1, 2) end points, boundary_nodes, segments = deepcopy(points_I_extra_segments), deepcopy(curve_I), deepcopy(segments_I) @@ -558,14 +558,14 @@ end points = deepcopy(points_V) boundary_nodes = deepcopy(curve_V) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) - DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n = 5) # 5 gets mapped to 8 + DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n=5) # 5 gets mapped to 8 @test length(points) == 9 all_t = DT.get_inverse.(Ref(curve_V[1]), get_point(points, new_boundary_nodes...)) |> collect all_t[end] = 1.0 @test issorted(all_t) boundary_nodes = new_boundary_nodes @test DT.num_boundary_edges(boundary_nodes) == 8 - for i in 1:(num_boundary_edges(boundary_nodes) - 1) + for i in 1:(num_boundary_edges(boundary_nodes)-1) j = i + 1 k = j + 1 u, v, w = get_boundary_nodes(boundary_nodes, i), get_boundary_nodes(boundary_nodes, j), get_boundary_nodes(boundary_nodes, k) @@ -587,7 +587,7 @@ end points = deepcopy(points_VI) boundary_nodes = deepcopy(curve_VI) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) - DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n = 64) + DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n=64) @test length(points) == 137 all_t = DT.get_inverse.(Ref(curve_VI[1][1]), get_point(points, get_boundary_nodes(new_boundary_nodes, 1)...)) |> collect @test issorted(all_t) @@ -599,7 +599,7 @@ end @test DT.num_boundary_edges(boundary_nodes[2]) == 64 for idx in 1:2 section_nodes = get_boundary_nodes(boundary_nodes, idx) - for i in 1:(num_boundary_edges(section_nodes) - 1) + for i in 1:(num_boundary_edges(section_nodes)-1) j = i + 1 k = j + 1 u, v, w = get_boundary_nodes(section_nodes, i), get_boundary_nodes(section_nodes, j), get_boundary_nodes(section_nodes, k) @@ -615,7 +615,7 @@ end points = deepcopy(points_VII) boundary_nodes = deepcopy(curve_VII) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) - DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n = 512) + DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n=512) @test length(points) == 1025 all_t = DT.get_inverse.(Ref(curve_VII[1][1]), get_point(points, get_boundary_nodes(new_boundary_nodes, 1)...)) |> collect @test issorted(all_t) @@ -626,7 +626,7 @@ end @test DT.num_boundary_edges(boundary_nodes[2]) == 512 for idx in 1:2 section_nodes = get_boundary_nodes(boundary_nodes, idx) - for i in 1:(num_boundary_edges(section_nodes) - 1) + for i in 1:(num_boundary_edges(section_nodes)-1) j = i + 1 k = j + 1 u, v, w = get_boundary_nodes(section_nodes, i), get_boundary_nodes(section_nodes, j), get_boundary_nodes(section_nodes, k) @@ -657,7 +657,7 @@ end @test boundary_nodes[3] == [6, 7, 8, 9, 10] for idx in [2, 4] section_nodes = get_boundary_nodes(boundary_nodes, idx) - for i in 1:(num_boundary_edges(section_nodes) - 1) + for i in 1:(num_boundary_edges(section_nodes)-1) j = i + 1 k = j + 1 u, v, w = get_boundary_nodes(section_nodes, i), get_boundary_nodes(section_nodes, j), get_boundary_nodes(section_nodes, k) @@ -673,7 +673,7 @@ end points = deepcopy(points_IX) boundary_nodes = deepcopy(curve_IX) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) - DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves, n = 1) # check that 1 becomes 4 + DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves, n=1) # check that 1 becomes 4 @test length(points) == 11 # 1 gets ignored because it's a piecewise linear curve all_t = DT.get_inverse.(Ref(curve_IX[2][1][1]), get_point(points, new_boundary_nodes[2][1]...)) |> collect @@ -685,7 +685,7 @@ end for idx in (2,) curve_nodes = get_boundary_nodes(boundary_nodes, idx) section_nodes = get_boundary_nodes(curve_nodes, 1) - for i in 1:(num_boundary_edges(section_nodes) - 1) + for i in 1:(num_boundary_edges(section_nodes)-1) j = i + 1 k = j + 1 u, v, w = get_boundary_nodes(section_nodes, i), get_boundary_nodes(section_nodes, j), get_boundary_nodes(section_nodes, k) @@ -708,7 +708,7 @@ end points = deepcopy(points_X) boundary_nodes = deepcopy(curve_X) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int) - DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n = 32) + DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n=32) @test length(points) == 70 # 1 and 4 are piecewise linear curves all_t = DT.get_inverse.(Ref(curve_X[1][2][1]), get_point(points, new_boundary_nodes[1][2]...)) |> collect @@ -723,7 +723,7 @@ end for (idx, idx2) in zip((1, 2), (2, 1)) curve_nodes = get_boundary_nodes(boundary_nodes, idx) section_nodes = get_boundary_nodes(curve_nodes, idx2) - for i in 1:(num_boundary_edges(section_nodes) - 1) + for i in 1:(num_boundary_edges(section_nodes)-1) j = i + 1 k = j + 1 u, v, w = get_boundary_nodes(section_nodes, i), get_boundary_nodes(section_nodes, j), get_boundary_nodes(section_nodes, k) @@ -739,7 +739,7 @@ end points = deepcopy(points_XI) boundary_nodes = deepcopy(curve_XI) boundary_curves, new_boundary_nodes = DT.convert_boundary_curves!(points, boundary_nodes, Int32) - DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n = 128) + DT.coarse_discretisation!(points, new_boundary_nodes, boundary_curves; n=128) @test length(points) == 650 # (1, 1), (3, 1), and (5, 1) are piecewise linear # (2, 1) and (6, 1) are periodic @@ -764,7 +764,7 @@ end for (idx, idx2) in zip((1, 2, 4, 4, 6), (2, 1, 1, 2, 1)) curve_nodes = get_boundary_nodes(boundary_nodes, idx) section_nodes = get_boundary_nodes(curve_nodes, idx2) - for i in 1:(num_boundary_edges(section_nodes) - 1) + for i in 1:(num_boundary_edges(section_nodes)-1) j = i + 1 k = j + 1 u, v, w = get_boundary_nodes(section_nodes, i), get_boundary_nodes(section_nodes, j), get_boundary_nodes(section_nodes, k) @@ -802,8 +802,8 @@ end @test DT.is_piecewise_linear(enricher_III, 1) @test !DT.is_piecewise_linear(enricher_IV, 1) @test DT.is_piecewise_linear(enricher_XI, 1) && !DT.is_piecewise_linear(enricher_XI, 2) && - !DT.is_piecewise_linear(enricher_XI, 3) && DT.is_piecewise_linear(enricher_XI, 4) && !DT.is_piecewise_linear(enricher_XI, 5) && - !DT.is_piecewise_linear(enricher_XI, 6) && DT.is_piecewise_linear(enricher_XI, 7) && !DT.is_piecewise_linear(enricher_XI, 8) + !DT.is_piecewise_linear(enricher_XI, 3) && DT.is_piecewise_linear(enricher_XI, 4) && !DT.is_piecewise_linear(enricher_XI, 5) && + !DT.is_piecewise_linear(enricher_XI, 6) && DT.is_piecewise_linear(enricher_XI, 7) && !DT.is_piecewise_linear(enricher_XI, 8) @inferred DT.is_piecewise_linear(enricher_I, 1) @inferred DT.is_piecewise_linear(enricher_II, 3) @inferred DT.is_piecewise_linear(enricher_III, 3) @@ -936,7 +936,7 @@ end (3.4, -0.2), (3.4, 0.0) geo1 = [[g, m, h, ℓ, a, ii, b, jj, c, kk, d, p, e, o, f, n, g]] geo2 = [[a1, z, w, v, u, t, s, a1]] - boundary_nodes, points = convert_boundary_points_to_indices([geo1, geo2]; existing_points = [r, c1, b1, d1, e1, f1, g1]) + boundary_nodes, points = convert_boundary_points_to_indices([geo1, geo2]; existing_points=[r, c1, b1, d1, e1, f1, g1]) enricher = DT.BoundaryEnricher(points, boundary_nodes) i, j, k = findfirst(==(ii), points), findfirst(==(b), points), findfirst(==(r), points) @test DT.is_invisible(DT.test_visibility(PT(), enricher, i, j, k)) @@ -1008,7 +1008,7 @@ end DT.split_edge!(enricher, 1, 2, length(enricher.points)) vis = DT.test_visibility(PT(), enricher, length(enricher.points), 2, length(enricher.points) - 1) @test DT.is_invisible(vis) - enricher.points[end - 1] = (0.5, 0.49) + enricher.points[end-1] = (0.5, 0.49) vis = DT.test_visibility(PT(), enricher, length(enricher.points), 2, length(enricher.points) - 1) @test DT.is_visible(vis) end @@ -1060,9 +1060,9 @@ end @test DT.get_small_angle_complexes(enricher) == _complexes A, B, C, D, E, F, G, H, I, J, K = (0.0, 0.0), (0.2, 1.4), (0.6, 1.2), - (1.2, 0.2), (1.2, -0.2), (-1.4, -0.2), - (-1.0, -0.6), (0.6, 1.0), (0.8, 0.6), - (0.6, 0.4), (0.6, 0.2) + (1.2, 0.2), (1.2, -0.2), (-1.4, -0.2), + (-1.0, -0.6), (0.6, 1.0), (0.8, 0.6), + (0.6, 0.4), (0.6, 0.2) points = [A, B, C, D, E, F, G, H, I, J, K] boundary_nodes = [[[1, 3, 2, 1]], [[1, 9, 8, 1]], [[1, 11, 10, 1]], [[1, 5, 4, 1]], [[1, 6, 7, 1]]] enricher = DT.BoundaryEnricher(points, boundary_nodes) @@ -1231,9 +1231,9 @@ end ctr = 1 for i in eachindex(enricher_III.boundary_nodes) for j in eachindex(enricher_III.boundary_nodes[i]) - for k in 1:(length(enricher_III.boundary_nodes[i]) - 1) + for k in 1:(length(enricher_III.boundary_nodes[i])-1) u = enricher_III.boundary_nodes[i][j][k] - v = enricher_III.boundary_nodes[i][j][k + 1] + v = enricher_III.boundary_nodes[i][j][k+1] p = enricher_III.parent_map[(u, v)] @test p == ctr end @@ -1243,7 +1243,7 @@ end rects, els = get_dt_rectangles(enricher_III.spatial_tree.tree) all_edges = Set(DT.get_edge(el) for el in els) @test (ii, jj) ∉ all_edges && (jj, ii) ∉ all_edges && - (ii, rr) ∈ all_edges && (jj, rr) ∈ all_edges + (ii, rr) ∈ all_edges && (jj, rr) ∈ all_edges end @testset "split_subcurve! (standard)" begin @@ -1321,9 +1321,9 @@ end @test DT.get_small_angle_complexes(enricher) == _complexes A, B, C, D, E, F, G, H, I, J, K = (0.0, 0.0), (0.2, 1.4), (0.6, 1.2), - (1.2, 0.2), (1.2, -0.2), (-1.4, -0.2), - (-1.0, -0.6), (0.6, 1.0), (0.8, 0.6), - (0.6, 0.4), (0.6, 0.2) + (1.2, 0.2), (1.2, -0.2), (-1.4, -0.2), + (-1.0, -0.6), (0.6, 1.0), (0.8, 0.6), + (0.6, 0.4), (0.6, 0.2) points = [A, B, C, D, E, F, G, H, I, J, K] boundary_nodes = [[[1, 3, 2, 1]], [[1, 9, 8, 1]], [[1, 11, 10, 1]], [[1, 5, 4, 1]], [[1, 6, 7, 1]]] enricher = DT.BoundaryEnricher(points, boundary_nodes) @@ -1351,8 +1351,8 @@ end @testset "has_acute_neighbouring_angles and splitting small angles" begin A, B, C, D, E, F, G, H, I, J, K = (0.0, 0.0), (7.0, 0.0), (-0.2, 1.0), (1.0, 0.0), - (2.0, 0.0), (3.0, 0.0), (4.0, 0.0), (4.5, 0.5), (3.0, 0.8), - (1.6, 0.8), (0.4, 0.2) + (2.0, 0.0), (3.0, 0.0), (4.0, 0.0), (4.5, 0.5), (3.0, 0.8), + (1.6, 0.8), (0.4, 0.2) points = [E, F, G, B, H, I, C, A, D, E] boundary_nodes, points = convert_boundary_points_to_indices(points) enricher = DT.BoundaryEnricher(points, boundary_nodes) @@ -1416,19 +1416,19 @@ end curve_sets = [curve_I, curve_II, curve_III, curve_IV, curve_V, curve_VI, curve_VII, curve_VIII, curve_IX, curve_X, curve_XI, curve_XII] for (points, curve) in zip(point_sets, curve_sets) enricher = DT.BoundaryEnricher(deepcopy(points), deepcopy(curve)) - DT.enrich_boundary!(enricher; predicates = rt()) + DT.enrich_boundary!(enricher; predicates=rt()) @test all_diametral_circles_are_empty(enricher) == 1 @test allunique(get_points(enricher)) @test all_points_are_inside(enricher, points, curve) end A, B, C, D, E, F, G, H, I, J, K = (0.0, 0.0), (7.0, 0.0), (2.0, 1.0), (1.0, 0.0), - (2.0, 0.0), (3.0, 0.0), (4.0, 0.0), (4.5, 0.5), (3.0, 0.8), - (1.6, 0.8), (0.4, 0.2) + (2.0, 0.0), (3.0, 0.0), (4.0, 0.0), (4.5, 0.5), (3.0, 0.8), + (1.6, 0.8), (0.4, 0.2) points = [E, F, G, B, H, I, C, J, K, A, D, E] boundary_nodes, points = convert_boundary_points_to_indices(points) enricher = DT.BoundaryEnricher(points, boundary_nodes) - DT.enrich_boundary!(enricher; predicates = rt()) + DT.enrich_boundary!(enricher; predicates=rt()) @test all_diametral_circles_are_empty(enricher) == 1 @test all_points_are_inside(enricher, points, boundary_nodes) end @@ -1438,7 +1438,7 @@ end curve_sets = [curve_I, curve_II, curve_III, curve_IV, curve_V, curve_VI, curve_VII, curve_VIII, curve_IX, curve_X, curve_XI, curve_XII] for (points, curve) in zip(point_sets, curve_sets) enricher = DT.BoundaryEnricher(deepcopy(points), deepcopy(curve)) - DT.enrich_boundary!(enricher; predicates = rt()) + DT.enrich_boundary!(enricher; predicates=rt()) @test all_diametral_circles_are_empty(enricher) == 1 @test allunique(get_points(enricher)) @test all_points_are_inside(enricher, points, curve) @@ -1452,7 +1452,7 @@ end segment_sets = [segments_I, segments_II, segments_III, segments_IV] for i in eachindex(point_sets) enricher = DT.BoundaryEnricher(deepcopy(point_sets[i]), deepcopy(curve_sets[i]), deepcopy(segment_sets[i])) - DT.enrich_boundary!(enricher; predicates = PT()) + DT.enrich_boundary!(enricher; predicates=PT()) @test all_diametral_circles_are_empty(enricher) == 1 @test allunique(get_points(enricher)) @test all_points_are_inside(enricher, point_sets[i], curve_sets[i]) @@ -1468,10 +1468,10 @@ end curve_sets = deepcopy.([curve_I, curve_II, curve_III, curve_IV, curve_V, curve_VI, curve_VII, curve_VIII, curve_IX, curve_X, curve_XI, curve_XII]) for i in eachindex(point_sets, curve_sets) points, curve = deepcopy(point_sets[i]), deepcopy(curve_sets[i]) - tri = triangulate(points; boundary_nodes = curve, enrich = i ≤ 3, predicates = PT()) + tri = triangulate(points; boundary_nodes=curve, enrich=i ≤ 3, predicates=PT()) @test validate_triangulation(tri) - @test is_conformal(tri; predicates = PT()) - @test DT.get_boundary_enricher(tri) == DT.enrich_boundary!(DT.BoundaryEnricher(deepcopy(point_sets[i]), deepcopy(curve_sets[i])), predicates = PT()) + @test is_conformal(tri; predicates=PT()) + @test DT.get_boundary_enricher(tri) == DT.enrich_boundary!(DT.BoundaryEnricher(deepcopy(point_sets[i]), deepcopy(curve_sets[i])), predicates=PT()) i > 3 && @test DT.is_curve_bounded(tri) i ≤ 3 && @test !DT.is_curve_bounded(tri) end @@ -1484,10 +1484,10 @@ end curve_sets = deepcopy.([curve_I, curve_II, curve_III, curve_IV, curve_V, curve_VI, curve_VII, curve_VIII, curve_IX, curve_X, curve_XI, curve_XII]) for i in eachindex(point_sets, curve_sets) points, curve = deepcopy(point_sets[i]), deepcopy(curve_sets[i]) - tri = triangulate(points; boundary_nodes = curve, enrich = i ≤ 3, predicates = PT()) + tri = triangulate(points; boundary_nodes=curve, enrich=i ≤ 3, predicates=PT()) @test validate_triangulation(tri) - @test is_conformal(tri; predicates = PT()) - i ∉ (2, 11) && @test DT.get_boundary_enricher(tri) == DT.enrich_boundary!(DT.BoundaryEnricher(deepcopy(point_sets[i]), deepcopy(curve_sets[i])), predicates = PT()) # i ≠ 2 since we deliberately included some boundary points in the extra points, which triangulate then sees and mutates + @test is_conformal(tri; predicates=PT()) + i ∉ (2, 11) && @test DT.get_boundary_enricher(tri) == DT.enrich_boundary!(DT.BoundaryEnricher(deepcopy(point_sets[i]), deepcopy(curve_sets[i])), predicates=PT()) # i ≠ 2 since we deliberately included some boundary points in the extra points, which triangulate then sees and mutates i > 3 && @test DT.is_curve_bounded(tri) i ≤ 3 && @test !DT.is_curve_bounded(tri) end @@ -1501,10 +1501,10 @@ end segment_sets = deepcopy.([segments_I, segments_II, segments_III, segments_IV]) for i in eachindex(point_sets, curve_sets, segment_sets) points, curve, segments = deepcopy(point_sets[i]), deepcopy(curve_sets[i]), deepcopy(segment_sets[i]) - tri = triangulate(points; boundary_nodes = curve, segments = segments, enrich = i ≤ 3, predicates = PT()) + tri = triangulate(points; boundary_nodes=curve, segments=segments, enrich=i ≤ 3, predicates=PT()) @test validate_triangulation(tri) - @test is_conformal(tri; predicates = PT()) - i ≠ 2 && @test DT.get_boundary_enricher(tri) == DT.enrich_boundary!(DT.BoundaryEnricher(deepcopy(point_sets[i]), deepcopy(curve_sets[i]), deepcopy(segment_sets[i])), predicates = PT()) + @test is_conformal(tri; predicates=PT()) + i ≠ 2 && @test DT.get_boundary_enricher(tri) == DT.enrich_boundary!(DT.BoundaryEnricher(deepcopy(point_sets[i]), deepcopy(curve_sets[i]), deepcopy(segment_sets[i])), predicates=PT()) i > 3 && @test DT.is_curve_bounded(tri) i ≤ 3 && @test !DT.is_curve_bounded(tri) end @@ -1553,21 +1553,21 @@ end rng = StableRNG(abs(_rng_num(idx1, idx2, idx3, idx4, idx5, curve_idx, point_idx))) points, curve = deepcopy(point_sets[point_idx][curve_idx]), deepcopy(curve_sets[curve_idx]) if point_idx ≤ 2 - tri = triangulate(points; boundary_nodes = curve, enrich = curve_idx ≤ 3, rng, predicates = PT()) + tri = triangulate(points; boundary_nodes=curve, enrich=curve_idx ≤ 3, rng, predicates=PT()) else segments = deepcopy(segment_sets[curve_idx]) - tri = triangulate(points; boundary_nodes = curve, segments = segments, enrich = curve_idx ≤ 3, rng, predicates = PT()) + tri = triangulate(points; boundary_nodes=curve, segments=segments, enrich=curve_idx ≤ 3, rng, predicates=PT()) end custom_constraint = (_tri, T) -> curve_idx ≠ 5 ? false : begin - i, j, k = triangle_vertices(T) - p, q, r = get_point(_tri, i, j, k) - c = (p .+ q .+ r) ./ 3 - x, y = getxy(c) - return (x + y^2 < 1 / 4) && DT.triangle_area(p, q, r) > 1.0e-4 / 2 - end - refine!(tri; min_angle, min_area, max_area, custom_constraint, seditious_angle, use_circumcenter = true, use_lens, rng, predicates = PT()) - args = DT.RefinementArguments(tri; min_angle, min_area, max_area, seditious_angle, custom_constraint, use_circumcenter = true, use_lens, predicates = PT()) - @test validate_refinement(tri, args, warn = false) + i, j, k = triangle_vertices(T) + p, q, r = get_point(_tri, i, j, k) + c = (p .+ q .+ r) ./ 3 + x, y = getxy(c) + return (x + y^2 < 1 / 4) && DT.triangle_area(p, q, r) > 1.0e-4 / 2 + end + refine!(tri; min_angle, min_area, max_area, custom_constraint, seditious_angle, use_circumcenter=true, use_lens, rng, predicates=PT()) + args = DT.RefinementArguments(tri; min_angle, min_area, max_area, seditious_angle, custom_constraint, use_circumcenter=true, use_lens, predicates=PT()) + @test validate_refinement(tri, args, warn=false) if _rng_num(idx1, idx2, idx3, idx4, idx5, curve_idx, point_idx) == _rng_num(1, 3, 1, 2, 2, curve_idx, point_idx) fig, ax, sc = triplot(tri) @test_reference "refine_curve_bounded_example_$(curve_idx)_$(names[curve_idx])_$(point_names[point_idx])_$(abs(_rng_num(1, 3, 1, 2, 2, curve_idx, point_idx))).png" fig by = psnr_equality(7) @@ -1591,11 +1591,11 @@ end curve_idx = 12 point_idx = 2 points, curve = deepcopy(point_sets[point_idx][curve_idx]), deepcopy(curve_sets[curve_idx]) - tri = triangulate(points; boundary_nodes = curve, predicates = PT()) - refine!(tri; max_area = 1.0e-3, max_points = 500, use_circumcenter = true, predicates = PT()) + tri = triangulate(points; boundary_nodes=curve, predicates=PT()) + refine!(tri; max_area=1.0e-3, max_points=500, use_circumcenter=true, predicates=PT()) @test DT.num_solid_vertices(tri) == 500 @test validate_triangulation(tri) - @test !validate_refinement(tri; max_area = 1.0e-3, max_points = 500, use_circumcenter = true, warn = false) + @test !validate_refinement(tri; max_area=1.0e-3, max_points=500, use_circumcenter=true, warn=false) end end end @@ -1619,20 +1619,20 @@ end [12, 11, 10, 12], ], [ - [CircularArc((1.1, -3.0), (1.1, -3.0), (0.0, -3.0), positive = false)], + [CircularArc((1.1, -3.0), (1.1, -3.0), (0.0, -3.0), positive=false)], ], ] points = [(-2.0, 0.0), (0.0, 0.0), (2.0, 0.0), (-2.0, -5.0), (2.0, -5.0), (2.0, -1 / 10), (-2.0, -1 / 10), (-1.0, -3.0), (0.0, -4.0), (0.0, -2.3), (-0.5, -3.5), (0.9, -3.0)] rng = StableRNG(123) - tri = triangulate(points; boundary_nodes = curve, rng, predicates = PT()) - refine!(tri; max_area = 1.0e-2, predicates = PT()) - @test validate_triangulation(tri, predicates = PT()) + tri = triangulate(points; boundary_nodes=curve, rng, predicates=PT()) + refine!(tri; max_area=1.0e-2, predicates=PT()) + @test validate_triangulation(tri, predicates=PT()) r = DT.num_points(tri) - add_point!(tri, -3 / 2, -4.0, concavity_protection = true, predicates = PT()) - add_point!(tri, -3 / 2, -1.0, concavity_protection = true, predicates = PT()) - @test validate_triangulation(tri, predicates = PT()) - add_segment!(tri, r + 1, r + 2, predicates = PT()) - @test validate_triangulation(tri, predicates = PT()) + add_point!(tri, -3 / 2, -4.0, concavity_protection=true, predicates=PT()) + add_point!(tri, -3 / 2, -1.0, concavity_protection=true, predicates=PT()) + @test validate_triangulation(tri, predicates=PT()) + add_segment!(tri, r + 1, r + 2, predicates=PT()) + @test validate_triangulation(tri, predicates=PT()) @test tri.boundary_enricher.segments ∈ (Set(((r + 1, r + 2),)), Set(((r + 2, r + 1),))) end end @@ -1641,13 +1641,13 @@ end # Used to be a broken example DT = DelaunayTriangulation struct Custom2Points - points::Vector{NTuple{2, Float64}} + points::Vector{NTuple{2,Float64}} end struct Custom2Segments - segments::Set{NTuple{2, Int}} + segments::Set{NTuple{2,Int}} end struct Custom2Triangles - triangles::Set{NTuple{3, Int}} + triangles::Set{NTuple{3,Int}} end DT.is_planar(::Custom2Points) = true Base.eachindex(points::Custom2Points) = Base.eachindex(points.points) @@ -1657,30 +1657,43 @@ end DT.number_type(::Type{Custom2Points}) = Float64 Base.iterate(triangles::Custom2Triangles, state...) = Base.iterate(triangles.triangles, state...) Base.sizehint!(triangles::Custom2Triangles, n) = sizehint!(triangles.triangles, n) - Base.eltype(::Type{Custom2Triangles}) = NTuple{3, Int} + Base.eltype(::Type{Custom2Triangles}) = NTuple{3,Int} Base.length(triangles::Custom2Triangles) = length(triangles.triangles) - Custom2Triangles() = Custom2Triangles(Set{NTuple{3, Int}}()) + Custom2Triangles() = Custom2Triangles(Set{NTuple{3,Int}}()) Base.iterate(segments::Custom2Segments, state...) = Base.iterate(segments.segments, state...) - Base.eltype(::Type{Custom2Segments}) = NTuple{2, Int} + Base.eltype(::Type{Custom2Segments}) = NTuple{2,Int} Base.rand(rng::AbstractRNG, segments::Custom2Segments) = rand(rng, segments.segments) Base.length(segments::Custom2Segments) = length(segments.segments) - Custom2Segments() = Custom2Segments(Set{NTuple{2, Int}}()) - DT.contains_edge(e::NTuple{2, Int}, Es::Custom2Segments) = e ∈ Es.segments + Custom2Segments() = Custom2Segments(Set{NTuple{2,Int}}()) + DT.contains_edge(e::NTuple{2,Int}, Es::Custom2Segments) = e ∈ Es.segments Base.empty!(triangles::Custom2Triangles) = empty!(triangles.triangles) Base.empty!(segments::Custom2Segments) = empty!(segments.segments) - Base.push!(segments::Custom2Segments, e::NTuple{2, Int}) = push!(segments.segments, e) - Base.delete!(segments::Custom2Segments, e::NTuple{2, Int}) = delete!(segments.segments, e) + Base.push!(segments::Custom2Segments, e::NTuple{2,Int}) = push!(segments.segments, e) + Base.delete!(segments::Custom2Segments, e::NTuple{2,Int}) = delete!(segments.segments, e) Base.pop!(points::Custom2Points) = pop!(points.points) DT.push_point!(points::Custom2Points, x, y) = push!(points.points, (x, y)) - Base.delete!(triangles::Custom2Triangles, T::NTuple{3, Int}) = delete!(triangles.triangles, T) + Base.delete!(triangles::Custom2Triangles, T::NTuple{3,Int}) = delete!(triangles.triangles, T) DT.set_point!(points::Custom2Segments, i, x, y) = points.points[i] = (x, y) - Base.push!(triangles::Custom2Triangles, T::NTuple{3, Int}) = push!(triangles.triangles, T) + Base.push!(triangles::Custom2Triangles, T::NTuple{3,Int}) = push!(triangles.triangles, T) points = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] append!(points, [(clamp(0.5 + randn(), -2, 2), clamp(0.5 + randn(), -2, 2)) for _ in 1:100]) unique!(points) - inner_circle = CircularArc((0.5, 0.25), (0.5, 0.25), (0.5, 0.5), positive = false) + inner_circle = CircularArc((0.5, 0.25), (0.5, 0.25), (0.5, 0.5), positive=false) boundary_nodes = [[[1, 2, 3, 4, 1]], [[inner_circle]]] points = Custom2Points(points) - tri = triangulate(points; boundary_nodes, TrianglesType = Custom2Triangles, EdgesType = Custom2Segments) + tri = triangulate(points; boundary_nodes, TrianglesType=Custom2Triangles, EdgesType=Custom2Segments) @test DT.validate_triangulation(tri) end + +@testset "LineSegment domain" begin + a, b, c, d = (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0) + L1, L2, L3, L4 = LineSegment(a, b), LineSegment(b, c), LineSegment(c, d), LineSegment(d, a) + boundary_nodes = [[L1], [L2], [L3], [L4]] + tri = triangulate(NTuple{2,Float64}[]; boundary_nodes) + @test get_points(tri) == [a, b, c, d] # test that we didn't do any more discretisation than is needed + @test validate_triangulation(tri) + @test DT.is_curve_bounded(tri) + refine!(tri; max_area = 1e-3) + @test validate_triangulation(tri) + @test validate_refinement(tri; max_area = 1e-3) +end \ No newline at end of file diff --git a/test/utils.jl b/test/utils.jl index e630296ab..44a0cbd2e 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -1571,6 +1571,26 @@ end @test DT.eval_fnc_at_het_tuple_element(f, tup, i) == basic_def(f, tup, i) @inferred DT.eval_fnc_at_het_tuple_element(f, tup, i) end + + ## large tuples + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 32) + @test DT.eval_fnc_at_het_tuple_element(f, tup, 14) == basic_def(f, tup, 14) + b = @allocated DT.eval_fnc_at_het_tuple_element(f, tup, 14) + b = @allocated DT.eval_fnc_at_het_tuple_element(f, tup, 14) + b2 = @allocated basic_def(f, tup, 14) + b2 = @allocated basic_def(f, tup, 14) + @inferred DT.eval_fnc_at_het_tuple_element(f, tup, 14) + @test iszero(b) || iszero(b .- 16) + @test b < b2 + @test !(tup isa DT.ANY32) + + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 33) + @test DT.eval_fnc_at_het_tuple_element(f, tup, 33) == basic_def(f, tup, 33) + b = @allocated DT.eval_fnc_at_het_tuple_element(f, tup, 33) + b = @allocated DT.eval_fnc_at_het_tuple_element(f, tup, 33) + b2 = @allocated basic_def(f, tup, 33) + b2 = @allocated basic_def(f, tup, 33) + @test b == b2 end @testset "eval_fnc_in_het_tuple" begin @@ -1612,6 +1632,24 @@ end @test DT.eval_fnc_in_het_tuple(tup, arg, i) == basic_def2(tup, arg, i) @inferred DT.eval_fnc_in_het_tuple(tup, arg, i) end + + ## large tuples + tup = ntuple(_ -> rand((gg1, gg2, gg3, gg4, gg5, gg6, gg7)), 32) + @test DT.eval_fnc_in_het_tuple(tup, arg, 14) == basic_def2(tup, arg, 14) + b = @allocated DT.eval_fnc_in_het_tuple(tup, arg, 14) + b = @allocated DT.eval_fnc_in_het_tuple(tup, arg, 14) + b2 = @allocated basic_def2(tup, arg, 14) + b2 = @allocated basic_def2(tup, arg, 14) + @inferred DT.eval_fnc_in_het_tuple(tup, arg, 14) + @test iszero(b) || iszero(b .- 16) + + tup = ntuple(_ -> rand((gg1, gg2, gg3, gg4, gg5, gg6, gg7)), 33) + @test DT.eval_fnc_in_het_tuple(tup, arg, 33) == basic_def2(tup, arg, 33) + b = @allocated DT.eval_fnc_in_het_tuple(tup, arg, 33) + b = @allocated DT.eval_fnc_in_het_tuple(tup, arg, 33) + b2 = @allocated basic_def2(tup, arg, 33) + b2 = @allocated basic_def2(tup, arg, 33) + @test b == b2 end @testset "eval_fnc_at_het_tuple_two_elements" begin @@ -1639,6 +1677,24 @@ end end end @test all(iszero, a .- 16) || all(iszero, a) + + ## large tuples + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 32) + @test DT.eval_fnc_at_het_tuple_two_elements(fft, tup, 14, 15) == basic_defft(fft, tup, 14, 15) + b = @allocated DT.eval_fnc_at_het_tuple_two_elements(fft, tup, 14, 15) + b = @allocated DT.eval_fnc_at_het_tuple_two_elements(fft, tup, 14, 15) + b2 = @allocated basic_defft(fft, tup, 14, 15) + b2 = @allocated basic_defft(fft, tup, 14, 15) + @inferred DT.eval_fnc_at_het_tuple_two_elements(fft, tup, 14, 15) + @test iszero(b) || iszero(b .- 16) + + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 33) + @test DT.eval_fnc_at_het_tuple_two_elements(fft, tup, 33, 32) == basic_defft(fft, tup, 33, 32) + b = @allocated DT.eval_fnc_at_het_tuple_two_elements(fft, tup, 33, 32) + b = @allocated DT.eval_fnc_at_het_tuple_two_elements(fft, tup, 33, 32) + b2 = @allocated basic_defft(fft, tup, 33, 32) + b2 = @allocated basic_defft(fft, tup, 33, 32) + @test b == b2 end @testset "eval_fnc_at_het_tuple_element_with_arg" begin @@ -1667,6 +1723,20 @@ end @inferred DT.eval_fnc_at_het_tuple_element_with_arg(fft, tup, arg, i) end @test all(iszero, a .- 16) || all(iszero, a) + + ## large tuples + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 32) + @test DT.eval_fnc_at_het_tuple_element_with_arg(fft, tup, ((2.0, 3.0), -1.0, "5"), 14) == basic_defft(fft, tup, ((2.0, 3.0), -1.0, "5"), 14) + arg = ((2.0, 3.0), -1.0, "5") + b = @allocated DT.eval_fnc_at_het_tuple_element_with_arg(fft, tup, arg, 14) + b = @allocated DT.eval_fnc_at_het_tuple_element_with_arg(fft, tup, arg, 14) + b2 = @allocated basic_defft(fft, tup, ((2.0, 3.0), -1.0, "5"), 14) + b2 = @allocated basic_defft(fft, tup, ((2.0, 3.0), -1.0, "5"), 14) + @inferred DT.eval_fnc_at_het_tuple_element_with_arg(fft, tup, ((2.0, 3.0), -1.0, "5"), 14) + @test iszero(b) || iszero(b .- 16) + + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 33) + @test DT.eval_fnc_at_het_tuple_element_with_arg(fft, tup, ((2.0, 3.0), -1.0, "5"), 33) == basic_defft(fft, tup, ((2.0, 3.0), -1.0, "5"), 33) end @testset "eval_fnc_at_het_tuple_element_with_arg_and_prearg" begin @@ -1696,6 +1766,20 @@ end @inferred DT.eval_fnc_at_het_tuple_element_with_arg_and_prearg(fft, tup, prearg, arg, i) end @test all(iszero, a .- 16) || all(iszero, a) + + ## large tuples + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 32) + @test DT.eval_fnc_at_het_tuple_element_with_arg_and_prearg(fft, tup, -3.0, ((2.0, 3.0), "5"), 14) == basic_defft(fft, tup, -3.0, ((2.0, 3.0), "5"), 14) + arg = ((2.0, 3.0), "5") + b = @allocated DT.eval_fnc_at_het_tuple_element_with_arg_and_prearg(fft, tup, -3.0, arg, 14) + b = @allocated DT.eval_fnc_at_het_tuple_element_with_arg_and_prearg(fft, tup, -3.0, arg, 14) + b2 = @allocated basic_defft(fft, tup, -3.0, ((2.0, 3.0), "5"), 14) + b2 = @allocated basic_defft(fft, tup, -3.0, ((2.0, 3.0), "5"), 14) + @inferred DT.eval_fnc_at_het_tuple_element_with_arg_and_prearg(fft, tup, -3.0, ((2.0, 3.0), "5"), 14) + @test iszero(b) || iszero(b .- 16) + + tup = ntuple(_ -> rand((1, 2.0, "string", [1 2 3], [5, 7, 9], 0x00, 'A')), 33) + @test DT.eval_fnc_at_het_tuple_element_with_arg_and_prearg(fft, tup, -3.0, ((2.0, 3.0), "5"), 33) == basic_defft(fft, tup, -3.0, ((2.0, 3.0), "5"), 33) end end