extractEdges method
The mesh's unique undirected edges as disconnected line segments.
With a null creaseAngleDegrees every edge is kept (a full
wireframe). With a value, an edge is kept only when its two adjacent
faces meet at more than that angle (a feature-edge wireframe);
boundary edges (a single adjacent face) are always kept.
The output carries per-endpoint source normals when this mesh has normals, so callers can offset the segments off the surface.
Implementation
LineSegmentData extractEdges({double? creaseAngleDegrees}) {
_requireTriangles('extractEdges');
final count = triangleCount;
// Unique undirected edges, keyed lo * vertexCount + hi (safe well past
// any practical vertex count). Each edge tracks up to two face normals
// for the crease filter.
final edgeSlot = <int, int>{};
final edgeA = <int>[];
final edgeB = <int>[];
final faceNx = <double>[];
final faceNy = <double>[];
final faceNz = <double>[];
// Per edge: first face index, second face index (or -1).
final firstFace = <int>[];
final secondFace = <int>[];
for (var t = 0; t < count; t++) {
final i0 = _cornerIndex(t * 3);
final i1 = _cornerIndex(t * 3 + 1);
final i2 = _cornerIndex(t * 3 + 2);
if (creaseAngleDegrees != null) {
final ax = positions[i0 * 3],
ay = positions[i0 * 3 + 1],
az = positions[i0 * 3 + 2];
final e1x = positions[i1 * 3] - ax,
e1y = positions[i1 * 3 + 1] - ay,
e1z = positions[i1 * 3 + 2] - az;
final e2x = positions[i2 * 3] - ax,
e2y = positions[i2 * 3 + 1] - ay,
e2z = positions[i2 * 3 + 2] - az;
var nx = e1y * e2z - e1z * e2y;
var ny = e1z * e2x - e1x * e2z;
var nz = e1x * e2y - e1y * e2x;
final len = math.sqrt(nx * nx + ny * ny + nz * nz);
if (len > 0) {
nx /= len;
ny /= len;
nz /= len;
}
faceNx.add(nx);
faceNy.add(ny);
faceNz.add(nz);
}
void addEdge(int a, int b) {
final lo = math.min(a, b);
final hi = math.max(a, b);
final key = lo * vertexCount + hi;
final slot = edgeSlot[key];
if (slot == null) {
edgeSlot[key] = edgeA.length;
edgeA.add(lo);
edgeB.add(hi);
firstFace.add(t);
secondFace.add(-1);
} else if (secondFace[slot] == -1) {
secondFace[slot] = t;
}
}
addEdge(i0, i1);
addEdge(i1, i2);
addEdge(i2, i0);
}
// cos of the crease angle; adjacent faces whose normals agree more than
// this are coplanar enough to drop.
final creaseCos = creaseAngleDegrees == null
? null
: math.cos(creaseAngleDegrees * math.pi / 180.0);
final srcNormals = normals;
final outPositions = <double>[];
final outNormals = srcNormals == null ? null : <double>[];
for (var e = 0; e < edgeA.length; e++) {
if (creaseCos != null && secondFace[e] != -1) {
final f0 = firstFace[e];
final f1 = secondFace[e];
final dot =
faceNx[f0] * faceNx[f1] +
faceNy[f0] * faceNy[f1] +
faceNz[f0] * faceNz[f1];
if (dot > creaseCos) continue;
}
for (final v in [edgeA[e], edgeB[e]]) {
outPositions
..add(positions[v * 3])
..add(positions[v * 3 + 1])
..add(positions[v * 3 + 2]);
outNormals
?..add(srcNormals![v * 3])
..add(srcNormals[v * 3 + 1])
..add(srcNormals[v * 3 + 2]);
}
}
return LineSegmentData(
positions: Float32List.fromList(outPositions),
normals: outNormals == null ? null : Float32List.fromList(outNormals),
);
}