In this section, we discuss the mathematical details behind the triangulation of curve-bounded domains. The algorithm we implement for this is from the PhD thesis Delaunay Refinement Mesh Generation of Curve-bounded Domains by Serge Gosselin (2009). A basic description of the algorithm is as follows:
The first step of the algorithm is to obtain a piecewise linear approximation to the domain so that we obtain an initial triangulation. This approximation must have no intersecting edges so that we can compute a triangulation of the approximate domain. For this approximtaion, we need to also be careful about the resolution of the approximation: Too many points will greatly increase the number of triangles in the final mesh, but too few points could result in a poor approximation and cause difficulty when triangulating with overlapping curves.
The approach we take for obtaining this approximation is based on the total variation of the curve, as defined in the last section. Recall that this total variation helps describe how much a curve varies around an interval, and so we can use the total variation to determine how many points are needed to approximate the curve by straight edges. Using the total variation as a guide so that any subcurve has total variation less than
One difficulty with this approach is that finding all vertices inside a given edge's diametral circle is no longer as easy a task as it is for a triangulation, since we do not have any triangulation data structure to work with during this enrichment phase. To overcome this, we instead use an R-Tree
Let us consider the problem of testing visibility between a vertex
The actual algorithm we use for testing this visibility is as follows, assuming that we have a point
- First, we check if there is a hole between
$p_k$ and$e_{ij}$ that would occlude visibility. To check this, let$C$ be the curve associated with$e_{ij}$ , and compute the certificates$s_c$ and$s_e$ that give the positions of$p_k$ relative to$C$ and$e_{ij}$ , respectively. If$p_k$ is directly on$e_{ij}$ , then this means that an endpoint of a boundary edge is directly on$e_{ij}$ , and so we declare that$p_k$ is visible from$e_{ij}$ ; if$s_c \neq s_e$ , then$e_{ij}$ intersects another boundary edge with$p_k$ as a vertex and so we declare that$p_k$ is visible from$e_{ij}$ ; if$p_k$ is right of$C$ , then this means that$c$ bounds a hole and so we declare that$p_k$ is not visible from$e_{ij}$ . Otherwise, we proceed onto the next step. - We now need to determine if
$p_k$ is visible from either of the endpoints of$e_{ij}$ . We initialise the variablesint1 = false
andint2 = false
. To efficiently compute the intersections, we compute the intersection of the bounding box$B$ of$T_{ijk}$ with the bounding boxes in the R-Tree$\tau$ (described in the algorithm for boundary enrichment later). Then, for each edge$e_{uv}$ disjoint from$e_{ij}$ in the set of intersections, we setint1 = true
if$e_{uv}$ insteads$e_{ik}$ andint2 = true
if$e_{uv}$ intersects$e_{jk}$ . If everint1
andint2
are both true, then we declare that$p_k$ is not visible from$e_{ij}$ . Otherwise, if after processing all intersections eitherint1
orint2
is false, then we declare that$p_k$ is visible from$e_{ij}$ .
Once we have determined that an edge
For discussing boundary enrichment, we also need to handle small angles between curves. We that an input angle between two curves is small if it is less than
using DelaunayTriangulation
using CairoMakie
using StableRNGs
A, B, C, D, E, F, G, H, J, K, L, M, N, O, I, P, Q, R, S = (0.0, 0.0), (1.0, 1.0),
(4.0, 2.0), (1.0, -1.0), (3.0, -1.0), (4.0, -2.0),
(-1.5, 1.0), (-0.5, 0.5), (-0.5, -0.5), (-1.5, -0.5), (-2.0, -1.5), (-0.6, 0.2),
(-1.0, 0.2), (-1.8, 0.0), (0.5, -1.0), (1.0, -2.0), (0.0, -3.0), (6.0, 1.5), (-3.0, 1.5)
c1 = CatmullRomSpline([A, B, C, R])
c2 = CatmullRomSpline([A, D, E, F])
c3 = CatmullRomSpline([A, H, G, S])
c4 = CatmullRomSpline([A, M, N, O])
c5 = CatmullRomSpline([A, J, K, L])
c6 = CatmullRomSpline([A, I, P, Q])
t = LinRange(0, 1, 2500)
fig = Figure(fontsize = 24)
ax = Axis(fig[1, 1], width = 300, height = 300)
lines!(ax, c1.(t), color = :black)
lines!(ax, c2.(t), color = :black)
lines!(ax, c3.(t), color = :black)
lines!(ax, c4.(t), color = :black)
lines!(ax, c5.(t), color = :black)
lines!(ax, c6.(t), color = :black)
xlims!(ax, -1.5, 1.5)
ylims!(ax, -2.5, 2.0)
text!(ax, [A .- (-0.1, 0.07)], text = [L"a"])
text!(ax, [(0.8, 0.99)], text = [L"s_1"])
text!(ax, [(0.8, -0.85)], text = [L"s_6"])
text!(ax, [(0.0, -0.7)], text = [L"s_5"])
text!(ax, [(-1.0, -0.9)], text = [L"s_4"])
text!(ax, [(-1.0, -0.15)], text = [L"s_3"])
text!(ax, [(-1.0, 0.7)], text = [L"s_2"])
scatter!(ax, [A], color = :black, markersize = 14)
hidedecorations!(ax)
resize_to_layout!(fig)
fig # hide
In the figure above, we show an example of a set of subcurves
In addition to handling small angle complexes, we also want to handle small angles that are locally acute, meaning that the angle between two segments is small. These are local checks since the curves associated with these edges might not define actually small angle at this point. We include these checks though to ensure that the enrichment phase cannot get stuck by any missed small angles.
Now let's discuss how we split subcurves near small angles. Let's first discuss the case where the subcurve is not part of a small angle complex. If there are no neighbouring acute angles to the edge
The main difficulty to work through is the splitting of a subcurve belonging to a small angle complex. Suppose
using LinearAlgebra
fig = Figure(fontsize = 24)
ax = Axis(fig[1, 1], width = 300, height = 300)
lines!(ax, c1.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c2.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c3.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c4.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c5.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c6.(t), color = (:black, 0.3), linewidth = 3)
xlims!(ax, -1.5, 0.8)
ylims!(ax, -1.0, 1.0)
text!(ax, [A .- (-0.1, 0.07)], text = [L"a"])
text!(ax, [(-1.0, -0.5)], text = [L"s_4"])
text!(ax, [(-1.0, 0)], text = [L"s_3"])
text!(ax, [(-1.0, 0.55)], text = [L"s_2"])
scatterlines!(ax, [A, H, G, S], color = :blue, linestyle = :dashdot)
scatterlines!(ax, [A, M, N, O], color = :blue, linestyle = :dashdot)
scatterlines!(ax, [A, J, K, L], color = :blue, linestyle = :dashdot)
scatter!(ax, [A], color = :black, markersize = 14)
rmax = 2min(norm(A .- H), norm(A .- M), norm(A .- J))/3
arc!(ax, A, rmax, 0.0, 2pi, color = :red)
rshell = 2.0^floor(Int, log2(rmax))
arc!(ax, A, rshell, 0.0, 2pi, color = :green)
t1, q1 = DelaunayTriangulation.get_circle_intersection(c4, 0.0, 1.0, rshell)
t2, q2 = DelaunayTriangulation.get_circle_intersection(c5, 0.0, 1.0, rshell)
t3, q3 = DelaunayTriangulation.get_circle_intersection(c3, 0.0, 1.0, rshell)
scatter!(ax, [q1, q2, q3], color = :green, markersize = 14)
hidedecorations!(ax)
ax = Axis(fig[1, 2], width = 300, height = 300)
lines!(ax, c1.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c2.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c3.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c4.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c5.(t), color = (:black, 0.3), linewidth = 3)
lines!(ax, c6.(t), color = (:black, 0.3), linewidth = 3)
xlims!(ax, -1.5, 0.8)
ylims!(ax, -1.0, 1.0)
text!(ax, [A .- (-0.1, 0.07)], text = [L"a"])
text!(ax, [(-1.0, -0.5)], text = [L"s_4"])
text!(ax, [(-1.0, 0)], text = [L"s_3"])
text!(ax, [(-1.0, 0.55)], text = [L"s_2"])
scatterlines!(ax, [A, q3, H, G, S], color = :blue, linestyle = :dashdot)
scatterlines!(ax, [A, q1, M, N, O], color = :blue, linestyle = :dashdot)
scatterlines!(ax, [A, q2, J, K, L], color = :blue, linestyle = :dashdot)
hidedecorations!(ax)
resize_to_layout!(fig)
fig # hide
In the above figure, the subcurves of interest are the subcures defined by the curves from
Now we can finally discuss the full algorithm for enriching the boundary to prepare for triangulation. We describe these steps one at a time. We assume that all the small angle complexes have already been computed. Moreover, while our discussion thus far has not included the possibility of interior segments, simple modifications to the steps below and the details above could be made to handle them. We do not discuss interior segments here.
The first step is to obtain a coarse discretisation of the boundary. For each curve, we compute a coarse discretisation by, starting with the two endpoints, computing equivariation curves between each pair of points until the total variation across any subcurve is less than
We then compute the R-Tree used for computing intersections. The R-Tree is initialised by inserting into it the axis-aligned bounding box of each segment's diametral circle in the coarse discretisation.
To determine the order in which to process vertices in the provided vertex set
We are now ready to actually being enriching. The idea is to process each vertex in the queue, adding new vertices as needed, and then stop once the queue is empty. So, let us suppose that
For this edge
The above iterative procedure continues until
Once we have obtained the initial triangulation of the boundary, we can then refine the mesh to obtain a higher-quality mesh. The algorithm for this is reasonably straightforward. We use the existing algorithm for mesh refinement as discussed previously, but when splitting the encroached segments we use the same method for splitting a subcurve as was used during enrichment.