Identify the Vertices and Lines around the edges of a free surface - Mesh/DiscreteGraphics
The presence of a seam in both @flinty's and @kglr's answer suggest that an invalid mesh (for FEM purposes) is being created by the OP's DiscretizeGraphics
approach. Rather than troubleshooting the meshing approach, I will present a structured meshing approach that eliminates the spurious edge artifact.
First, we will use a BSplineFunction
to map a structured UV map to the curved surface.
curv1 = {{3, 0, 0}, {1, 1, 0}, {0, 2, 0}, {-2, 0, 0}, {0, -2, 0}, {3,
0, 0}};
curv2 = {{2, 0, 5}, {1, 1, 2}, {0, 2, 2}, {-1, 0, 5}, {0, -2, 5}, {3,
0, 5}};
bsf1 = BSplineFunction[{curv1, curv2}, SplineClosed -> {False, True},
SplineDegree -> 3];
The following workflow will create structure MeshRegion
:
(* Import Required Package *)
Needs["NDSolve`FEM`"]
(* Create a UV Tensor Product Grid *)
pointsToMesh[data_] :=
MeshRegion[Transpose[{data}],
Line@Table[{i, i + 1}, {i, Length[data] - 1}]];
seg = pointsToMesh@Subdivide[0, 1, 36]
rp = RegionProduct[seg, seg]
(* Extract Coords from RegionProduct *)
crd = MeshCoordinates[rp];
(* Map coordinates to BSPlineFunction *)
crd2 = crd /. {{x_, y_} -> Chop[bsf1[x, y], 1*^-7]};
(*grab incidents RegionProduct mesh*)
inc = Delete[0] /@ MeshCells[rp, 2];
(* Convert Quads to Triangles *)
inc2 = Partition[
Flatten[ inc /. {{i_, j_, k_, l_} -> {{i, j, k}, {i, k, l}}}], 3];
mrkrs = ConstantArray[1, First@Dimensions@inc2];
(* FEM Create BoundaryMesh *)
bm = ToBoundaryMesh["Coordinates" -> crd2,
"BoundaryElements" -> {TriangleElement[inc2, mrkrs]}];
(* Convert BoundaryMesh to MeshRegion *)
mr = MeshRegion[bm];
HighlightMesh[mr, Style[1, Orange]]
The resulting mesh looks pretty good.
Now, we can apply @kglr's approach to see that we removed the spurious edge:
(* Apply kglr's Edge Extraction Method *)
boundaryedgeindices =
Flatten@Position[
Length /@ mr["ConnectivityMatrix"[1, 2]]["AdjacencyLists"], 1];
HighlightMesh[mr, Style[{1, boundaryedgeindices}, Thick, Red]]
Update: Top Surface 2D Mesh
In the comments, the OP had a question about capping the ends of the mesh. Because the projected curve is not convex, a simple capping is not generally possible. One possibility, is to create a minimal surface.
The following workflow shows how to create a 2D mesh with nodes that are equivalenced with the 3D mesh by setting the Mesh Order to 1 and the SteinerPoints
option to False
.
(* Extract Coords from segment *)
crd2d = MeshCoordinates[seg];
(* Map coordinates to BSPlineFunction *)
crd2d2 = Flatten[
crd2d /. {{x_} :> Chop[{bsf1[1, x][[1 ;; 2]]}, 1*^-7]}, 1];
(* grab incidents segmentr mesh *)
inc2d = Delete[0] /@ MeshCells[seg, 1];
(* Create Boundary Mesh *)
bm2d = ToBoundaryMesh["Coordinates" -> crd2d2,
"BoundaryElements" -> {LineElement[inc2d]}];
bm2d["Wireframe"]
(* Create 2D element mesh *)
m2d = ToElementMesh[bm2d, "MeshOrder" -> 1, "SteinerPoints" -> False];
m2d["Wireframe"]
To use the Mathematica example to create a minimal surface will require a little thought to specify the DirichletCondition
because the curve is specified parametrically. Since the OP has routines for calculating minimal surfaces, I will not go into it here.
Update: Minimal Surface
The following workflow will solve for the minimal surface using NDSolveValue
.
(* convert bsf1 to x,y,z components *)
ztop0[v_?NumericQ] := Module[{val}, val = bsf1[1, v]; Last@val];
zmin = First@NMinimize[ztop0[t], {t, 0, 1}];
zmax = First@NMaximize[ztop0[t], {t, 0, 1}];
zmid = Mean[{zmin, zmax}];
xtop[v_?NumericQ] := Module[{val}, val = bsf1[1, v]; First@val];
ytop[v_?NumericQ] := Module[{val}, val = bsf1[1, v]; val[[2]]];
ztop[v_?NumericQ] := Module[{val}, val = bsf1[1, v]; Last@val - zmid];
(* Use Nearest to find v given x,y *)
nf = Nearest[
Table[{xtop[t], ytop[t]}, {t, 0, 1, .0001}] ->
Table[t, {t, 0, 1, .0001}]];
(* calculate z given x,y for DirichletCondition *)
fz[x_?NumericQ, y_?NumericQ] := ztop[First@nf[{x, y}]]
(* Minimal Surface https://wolfram.com/xid/0bdpx7hg6-hvook1 *)
ufun = NDSolveValue[{-Inactive[Div][(1/Sqrt[1 + \!\(
\*SubscriptBox[\(∇\), \({x, y}\)]\(u[x, y]\)\).\!\(
\*SubscriptBox[\(∇\), \({x, y}\)]\(u[x, y]\)\)]) Inactive[Grad][
u[x, y], {x, y}], {x, y}] == 0,
DirichletCondition[u[x, y] == fz[x, y], True]},
u, {x, y} ∈ m2d];
Now, we can convert the 2D mesh to a 3D boundary mesh using the minimal surface solution for the z coordinates:
(* create and display minimal surface boundary mesh *)
c3d = Join[m2d["Coordinates"], List /@ (ufun["ValuesOnGrid"] + zmid),
2];
bmtop = ToBoundaryMesh["Coordinates" -> c3d,
"BoundaryElements" -> m2d["MeshElements"]];
Show[bm["Wireframe"["MeshElementStyle" -> {FaceForm[Green]}]],
bmtop["Wireframe"["MeshElementStyle" -> {FaceForm[Red]}]]]
You can see that the free surface nodes align well with the base mesh.
Find the lines on the mesh polygons that are not shared with other polygons:
curv1 = {{3, 0, 0}, {1, 1, 0}, {0, 2, 0}, {-2, 0, 0}, {0, -2, 0}, {3, 0, 0}};
curv2 = {{2, 0, 5}, {1, 1, 2}, {0, 2, 2}, {-1, 0, 5}, {0, -2, 5}, {3, 0, 5}};
sur1 = BSplineSurface[{curv1, curv2}, SplineClosed -> {False, True}, SplineDegree -> 3];
mesh = DiscretizeGraphics[sur1];
cells = MeshCells[mesh, 2][[All, 1]];
lines = Join @@ ((Sort /@ Subsets[#, {2}]) & /@ cells);
edgecells = Select[Tally[lines], Last[#] == 1 &][[All, 1]];
coords = MeshCoordinates[mesh];
edgelines = Line[{coords[[First[#]]], coords[[Last[#]]]}] & /@ edgecells;
Graphics3D[{{EdgeForm[None], Opacity[.5], mesh}, Red, Thick,
edgelines}, Boxed -> False]
Note there is a seam in the mesh because the mesh is not connected there.
Update: Two alternative, more direct, ways to get the boundary edges:
1. Use the property "EdgeFaceConnectivityRules"
and select the edges connected to 0
:
be1 = Keys @ Select[#[[1]] == 0 &] @ Association[ mr["EdgeFaceConnectivityRules"]]
be1 == boundaryedges
True
2. Use the property "ConnectivityMatrix"[1, 2]"
(which gives a SparseArray
where entry $ij$ is 1 iff 1-dimensional element $i$ is connected to 2-dimensional element $j$) and select the rows that contain a single element:
be2 = Flatten @
Position[Length /@ mr["ConnectivityMatrix"[1, 2]]["AdjacencyLists"], 1];
be2 == boundaryedges
True
Original answer:
You can process mr["FaceEdgeConnectivityRules"]
to identify edges connected to a single face:
mr = DiscretizeGraphics[sur1];
boundaryedges = Keys @ Select[EqualTo @ 1] @
Counts @ Flatten @ Values @ mr["FaceEdgeConnectivityRules"];
HighlightMesh[mr, {1, boundaryedges}, PlotTheme -> "Lines"]
Then we can use mr["EdgeVertexConnectivityRules"]
to identify the vertices incident to boundaryedges
:
boundaryvertices = Union @@ (boundaryedges /. mr["EdgeVertexConnectivityRules"]);
HighlightMesh[mr,
Style[{0, boundaryvertices}, PointSize[Medium], Red],
PlotTheme -> "Lines"]