@metamesh/modeling 0.0.0-bootstrap.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +313 -6
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +54 -0
- package/dist/api/geometry.d.ts +2912 -0
- package/dist/api/index.d.ts +1648 -0
- package/dist/api/text.d.ts +77 -0
- package/dist/engine/construction-operations.js +189 -0
- package/dist/engine/execution.js +227 -0
- package/dist/engine/export-3mf.js +100 -0
- package/dist/engine/extrusion-finishing.js +82 -0
- package/dist/engine/finalization.js +214 -0
- package/dist/engine/index.js +72 -0
- package/dist/engine/inspection-operations.js +52 -0
- package/dist/engine/kernel/LICENSE +201 -0
- package/dist/engine/kernel/manifold.js +2 -0
- package/dist/engine/kernel/manifold.wasm +0 -0
- package/dist/engine/loft-operations.js +156 -0
- package/dist/engine/mechanical-operations.js +51 -0
- package/dist/engine/modeling-context.js +38 -0
- package/dist/engine/native/adapter.js +20 -0
- package/dist/engine/native/context.js +209 -0
- package/dist/engine/native/copied-views.js +46 -0
- package/dist/engine/native/index.js +13 -0
- package/dist/engine/native/operations.js +115 -0
- package/dist/engine/native/ownership.js +98 -0
- package/dist/engine/native/regions.js +138 -0
- package/dist/engine/native/scoped-primitives.js +127 -0
- package/dist/engine/native/scoped-threads.js +34 -0
- package/dist/engine/native/threads.js +52 -0
- package/dist/engine/native/transforms.js +111 -0
- package/dist/engine/operation-environment.js +20 -0
- package/dist/engine/pattern-operations.js +36 -0
- package/dist/engine/pattern-placements.js +176 -0
- package/dist/engine/primitive-anchors.js +70 -0
- package/dist/engine/primitive-correspondence.js +74 -0
- package/dist/engine/primitive-operations.js +151 -0
- package/dist/engine/primitive-profile-types.js +1 -0
- package/dist/engine/primitive-profiles.js +250 -0
- package/dist/engine/profile-mesh.js +279 -0
- package/dist/engine/section-operations.js +93 -0
- package/dist/engine/shape-anchors.js +88 -0
- package/dist/engine/transform-operations.js +225 -0
- package/dist/engine/types.js +1 -0
- package/dist/geometry/anchors.js +226 -0
- package/dist/geometry/curves/evaluation.js +162 -0
- package/dist/geometry/curves/sampling.js +265 -0
- package/dist/geometry/curves/types.js +1 -0
- package/dist/geometry/cylinder.js +117 -0
- package/dist/geometry/edge-finishing.js +160 -0
- package/dist/geometry/errors.js +76 -0
- package/dist/geometry/extrusion.js +150 -0
- package/dist/geometry/heightfield.js +107 -0
- package/dist/geometry/index.js +42 -0
- package/dist/geometry/loft-correspondence.js +204 -0
- package/dist/geometry/loft-geometry.js +184 -0
- package/dist/geometry/loft-options.js +56 -0
- package/dist/geometry/loft-simple-mesh.js +92 -0
- package/dist/geometry/loft-triangulation.js +64 -0
- package/dist/geometry/loft.js +80 -0
- package/dist/geometry/math/vector3.js +41 -0
- package/dist/geometry/mechanical/gear-motion.js +94 -0
- package/dist/geometry/mechanical/gears.js +484 -0
- package/dist/geometry/mechanical/index.js +13 -0
- package/dist/geometry/mechanical/standards.js +67 -0
- package/dist/geometry/mechanical/thread-helices.js +44 -0
- package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
- package/dist/geometry/mechanical/thread-mesh.js +142 -0
- package/dist/geometry/mechanical/thread-profiles.js +368 -0
- package/dist/geometry/mechanical/thread-solids.js +230 -0
- package/dist/geometry/mechanical/thread-surface.js +433 -0
- package/dist/geometry/mechanical/threads.js +138 -0
- package/dist/geometry/mesh-planar.js +408 -0
- package/dist/geometry/mesh-quality.js +396 -0
- package/dist/geometry/mesh-signature.js +137 -0
- package/dist/geometry/mesh.js +300 -0
- package/dist/geometry/model-limits.js +50 -0
- package/dist/geometry/numeric.js +11 -0
- package/dist/geometry/paths/frames.js +118 -0
- package/dist/geometry/paths/helix.js +49 -0
- package/dist/geometry/paths/path.js +30 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +124 -0
- package/dist/geometry/polygon-clipping.js +52 -0
- package/dist/geometry/primitive-options.js +1 -0
- package/dist/geometry/primitive-resolution.js +17 -0
- package/dist/geometry/primitive-resolvers.js +235 -0
- package/dist/geometry/profiles/matched.js +184 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +70 -0
- package/dist/geometry/profiles/rectangle.js +235 -0
- package/dist/geometry/profiles/shapes.js +34 -0
- package/dist/geometry/profiles/span-sampling.js +28 -0
- package/dist/geometry/profiles/types.js +1 -0
- package/dist/geometry/profiles/validation.js +20 -0
- package/dist/geometry/regions.js +371 -0
- package/dist/geometry/resolution.js +236 -0
- package/dist/geometry/revolve/revolve-mesh.js +123 -0
- package/dist/geometry/sampling-limits.js +29 -0
- package/dist/geometry/stroke-path.js +181 -0
- package/dist/geometry/svg/errors.js +23 -0
- package/dist/geometry/svg/fill.js +172 -0
- package/dist/geometry/svg/index.js +1 -0
- package/dist/geometry/svg/matrix.js +130 -0
- package/dist/geometry/svg/parser.js +536 -0
- package/dist/geometry/svg/path.js +349 -0
- package/dist/geometry/svg/types.js +1 -0
- package/dist/geometry/svg-import.js +662 -0
- package/dist/geometry/sweeps/options.js +28 -0
- package/dist/geometry/sweeps/orientation.js +106 -0
- package/dist/geometry/sweeps/stations.js +161 -0
- package/dist/geometry/sweeps/sweep-closed.js +96 -0
- package/dist/geometry/sweeps/sweep-mesh.js +20 -0
- package/dist/geometry/sweeps/sweep-open.js +76 -0
- package/dist/geometry/sweeps/validation.js +116 -0
- package/dist/geometry/text-contours.js +32 -0
- package/dist/geometry/text-layout.js +1 -0
- package/dist/geometry/tolerance.js +53 -0
- package/dist/geometry/transform-geometry.js +41 -0
- package/dist/geometry/transform3.js +340 -0
- package/dist/geometry/types.js +1 -0
- package/dist/index.js +1 -0
- package/dist/model/execution-binding.js +30 -0
- package/dist/model/operations.js +152 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +48 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +89 -8
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { GeometryError } from "./errors.js";
|
|
2
|
+
import { buildTriangleMeshQualityAnalysis, } from "./mesh-quality.js";
|
|
3
|
+
const DEFAULT_MAXIMUM_FLIP_ATTEMPTS = 1_000_000;
|
|
4
|
+
const DEFAULT_MAXIMUM_PATCH_TRIANGLE_COUNT = 50_000;
|
|
5
|
+
const DEFAULT_MAXIMUM_PROCESSED_TRIANGLE_COUNT = 200_000;
|
|
6
|
+
const DEFAULT_MAXIMUM_EDGE_REVISITS = 12;
|
|
7
|
+
const DEFAULT_MINIMUM_PATCH_TRIANGLE_COUNT = 8;
|
|
8
|
+
const DEFAULT_TRIGGER_MINIMUM_ANGLE_DEGREES = 8;
|
|
9
|
+
const DEFAULT_TRIGGER_MAXIMUM_ASPECT_RATIO = 12;
|
|
10
|
+
const DEFAULT_TRIGGER_MAXIMUM_VERTEX_TRIANGLE_INCIDENCE = 16;
|
|
11
|
+
const DEFAULT_TRIGGER_MAXIMUM_FAN_RATIO = 0.2;
|
|
12
|
+
const ATTEMPTS_PER_INITIAL_EDGE = 20;
|
|
13
|
+
const edgeKey = (first, second) => first < second ? `${first}:${second}` : `${second}:${first}`;
|
|
14
|
+
const pointAt = (positions, vertex) => [
|
|
15
|
+
positions[vertex * 3],
|
|
16
|
+
positions[vertex * 3 + 1],
|
|
17
|
+
positions[vertex * 3 + 2],
|
|
18
|
+
];
|
|
19
|
+
const subtract = (left, right) => [
|
|
20
|
+
left[0] - right[0],
|
|
21
|
+
left[1] - right[1],
|
|
22
|
+
left[2] - right[2],
|
|
23
|
+
];
|
|
24
|
+
const dot = (left, right) => left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
|
|
25
|
+
const cross = (left, right) => [
|
|
26
|
+
left[1] * right[2] - left[2] * right[1],
|
|
27
|
+
left[2] * right[0] - left[0] * right[2],
|
|
28
|
+
left[0] * right[1] - left[1] * right[0],
|
|
29
|
+
];
|
|
30
|
+
const squaredLength = (vector) => dot(vector, vector);
|
|
31
|
+
const positiveInteger = (value, label) => {
|
|
32
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
33
|
+
throw new GeometryError("INVALID_ARGUMENT", `${label} must be a positive safe integer.`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
const positiveFinite = (value, label) => {
|
|
38
|
+
if (!Number.isFinite(value) || !(value > 0)) {
|
|
39
|
+
throw new GeometryError("INVALID_ARGUMENT", `${label} must be finite and greater than zero.`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const resolveOptions = (options) => {
|
|
44
|
+
const triggerMaximumFanRatio = options.triggerMaximumFanRatio ?? DEFAULT_TRIGGER_MAXIMUM_FAN_RATIO;
|
|
45
|
+
if (!Number.isFinite(triggerMaximumFanRatio) ||
|
|
46
|
+
triggerMaximumFanRatio < 0 ||
|
|
47
|
+
triggerMaximumFanRatio > 1) {
|
|
48
|
+
throw new GeometryError("INVALID_ARGUMENT", "Maximum fan ratio trigger must be finite and between zero and one.");
|
|
49
|
+
}
|
|
50
|
+
const triggerMinimumAngleDegrees = positiveFinite(options.triggerMinimumAngleDegrees ?? DEFAULT_TRIGGER_MINIMUM_ANGLE_DEGREES, "Minimum angle trigger");
|
|
51
|
+
if (triggerMinimumAngleDegrees >= 180) {
|
|
52
|
+
throw new GeometryError("INVALID_ARGUMENT", "Minimum angle trigger must be below 180 degrees.");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
maximumEdgeRevisits: positiveInteger(options.maximumEdgeRevisits ?? DEFAULT_MAXIMUM_EDGE_REVISITS, "Maximum edge revisits"),
|
|
56
|
+
maximumFlipAttempts: options.maximumFlipAttempts === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: positiveInteger(options.maximumFlipAttempts, "Maximum flip attempts"),
|
|
59
|
+
maximumPatchTriangleCount: positiveInteger(options.maximumPatchTriangleCount ?? DEFAULT_MAXIMUM_PATCH_TRIANGLE_COUNT, "Maximum patch triangle count"),
|
|
60
|
+
maximumProcessedTriangleCount: positiveInteger(options.maximumProcessedTriangleCount ?? DEFAULT_MAXIMUM_PROCESSED_TRIANGLE_COUNT, "Maximum processed triangle count"),
|
|
61
|
+
minimumPatchTriangleCount: positiveInteger(options.minimumPatchTriangleCount ?? DEFAULT_MINIMUM_PATCH_TRIANGLE_COUNT, "Minimum patch triangle count"),
|
|
62
|
+
triggerMaximumAspectRatio: positiveFinite(options.triggerMaximumAspectRatio ?? DEFAULT_TRIGGER_MAXIMUM_ASPECT_RATIO, "Maximum aspect-ratio trigger"),
|
|
63
|
+
triggerMaximumFanRatio,
|
|
64
|
+
triggerMaximumVertexTriangleIncidence: positiveInteger(options.triggerMaximumVertexTriangleIncidence ??
|
|
65
|
+
DEFAULT_TRIGGER_MAXIMUM_VERTEX_TRIANGLE_INCIDENCE, "Maximum vertex triangle-incidence trigger"),
|
|
66
|
+
triggerMinimumAngleDegrees,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const pairQuality = (positions, first, second) => {
|
|
70
|
+
const angles = [];
|
|
71
|
+
let maximumAspectRatio = 0;
|
|
72
|
+
for (const triangle of [first, second]) {
|
|
73
|
+
const points = triangle.map((vertex) => pointAt(positions, vertex));
|
|
74
|
+
const edgeVectors = [
|
|
75
|
+
subtract(points[1], points[0]),
|
|
76
|
+
subtract(points[2], points[1]),
|
|
77
|
+
subtract(points[0], points[2]),
|
|
78
|
+
];
|
|
79
|
+
const edgeSquaredLengths = edgeVectors.map(squaredLength);
|
|
80
|
+
const doubleArea = Math.sqrt(squaredLength(cross(edgeVectors[0], subtract(points[2], points[0]))));
|
|
81
|
+
maximumAspectRatio = Math.max(maximumAspectRatio, doubleArea === 0
|
|
82
|
+
? Number.POSITIVE_INFINITY
|
|
83
|
+
: (Math.sqrt(3) * Math.max(...edgeSquaredLengths)) / (2 * doubleArea));
|
|
84
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
85
|
+
const origin = points[corner];
|
|
86
|
+
const incoming = subtract(points[(corner + 1) % 3], origin);
|
|
87
|
+
const outgoing = subtract(points[(corner + 2) % 3], origin);
|
|
88
|
+
angles.push(Math.atan2(Math.sqrt(squaredLength(cross(incoming, outgoing))), dot(incoming, outgoing)));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
angles.sort((left, right) => left - right);
|
|
92
|
+
return { angles, maximumAspectRatio };
|
|
93
|
+
};
|
|
94
|
+
const comparePairQuality = (before, after, angularTolerance) => {
|
|
95
|
+
for (let index = 0; index < before.angles.length; index += 1) {
|
|
96
|
+
const difference = after.angles[index] - before.angles[index];
|
|
97
|
+
if (difference > angularTolerance)
|
|
98
|
+
return 1;
|
|
99
|
+
if (difference < -angularTolerance)
|
|
100
|
+
return -1;
|
|
101
|
+
}
|
|
102
|
+
const aspectTolerance = Math.max(1, before.maximumAspectRatio) * 1e-12;
|
|
103
|
+
if (after.maximumAspectRatio < before.maximumAspectRatio - aspectTolerance)
|
|
104
|
+
return 1;
|
|
105
|
+
if (after.maximumAspectRatio > before.maximumAspectRatio + aspectTolerance)
|
|
106
|
+
return -1;
|
|
107
|
+
return 0;
|
|
108
|
+
};
|
|
109
|
+
const localConcentrationPotential = (vertices, incidence, valence, changes = new Map()) => {
|
|
110
|
+
let potential = 0;
|
|
111
|
+
for (const vertex of new Set(vertices)) {
|
|
112
|
+
const change = changes.get(vertex) ?? 0;
|
|
113
|
+
const nextIncidence = incidence[vertex] + change;
|
|
114
|
+
const nextValence = valence[vertex] + change;
|
|
115
|
+
potential += nextIncidence ** 2 + nextValence ** 2;
|
|
116
|
+
}
|
|
117
|
+
return potential;
|
|
118
|
+
};
|
|
119
|
+
const isTriggeredPatch = (patch, options) => {
|
|
120
|
+
const report = patch.report;
|
|
121
|
+
return (report.triangleCount >= options.minimumPatchTriangleCount &&
|
|
122
|
+
(report.minimumTriangleAngleDegrees < options.triggerMinimumAngleDegrees ||
|
|
123
|
+
report.maximumTriangleAspectRatio > options.triggerMaximumAspectRatio ||
|
|
124
|
+
report.maximumVertexTriangleIncidence > options.triggerMaximumVertexTriangleIncidence ||
|
|
125
|
+
report.fanRatio > options.triggerMaximumFanRatio));
|
|
126
|
+
};
|
|
127
|
+
const unchangedResult = (mesh, report, skippedPatchCount, budgetExhausted, fallbackReason) => ({
|
|
128
|
+
after: report,
|
|
129
|
+
before: report,
|
|
130
|
+
budgetExhausted,
|
|
131
|
+
changed: false,
|
|
132
|
+
...(fallbackReason === undefined ? {} : { fallbackReason }),
|
|
133
|
+
flipCount: 0,
|
|
134
|
+
mesh,
|
|
135
|
+
optimizedPatchCount: 0,
|
|
136
|
+
skippedPatchCount,
|
|
137
|
+
});
|
|
138
|
+
const boundsEqual = (first, second) => first.min.every((value, axis) => value === second.min[axis]) &&
|
|
139
|
+
first.max.every((value, axis) => value === second.max[axis]);
|
|
140
|
+
const fallbackReasonForCandidate = (before, after, boundaryEdgeKeys) => {
|
|
141
|
+
if (!after.report.isClosedOrientedManifold)
|
|
142
|
+
return "candidate-not-closed-oriented-manifold";
|
|
143
|
+
for (const key of boundaryEdgeKeys) {
|
|
144
|
+
if (!after.edges.has(key))
|
|
145
|
+
return "candidate-boundary-changed";
|
|
146
|
+
}
|
|
147
|
+
const surfaceTolerance = Math.max(before.tolerance.area * before.report.triangleCount * 8, Math.abs(before.report.surfaceArea) * 1e-10);
|
|
148
|
+
const volumeTolerance = Math.max(before.tolerance.linear ** 3 * before.report.triangleCount * 8, Math.max(1, before.extent ** 3) * 1e-10);
|
|
149
|
+
if (!boundsEqual(before.bounds, after.bounds) ||
|
|
150
|
+
Math.abs(before.report.surfaceArea - after.report.surfaceArea) > surfaceTolerance ||
|
|
151
|
+
Math.abs(before.report.signedVolume - after.report.signedVolume) > volumeTolerance) {
|
|
152
|
+
return "candidate-measurements-changed";
|
|
153
|
+
}
|
|
154
|
+
const angleToleranceDegrees = (before.tolerance.angular * 180) / Math.PI;
|
|
155
|
+
const aspectTolerance = Math.max(1, before.report.maximumTriangleAspectRatio) * 1e-10;
|
|
156
|
+
if (after.report.minimumTriangleAngleDegrees <
|
|
157
|
+
before.report.minimumTriangleAngleDegrees - angleToleranceDegrees ||
|
|
158
|
+
after.report.maximumTriangleAspectRatio >
|
|
159
|
+
before.report.maximumTriangleAspectRatio + aspectTolerance) {
|
|
160
|
+
return "candidate-quality-regressed";
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Improves pathological planar triangulation with bounded legal diagonal flips.
|
|
166
|
+
*
|
|
167
|
+
* The operation changes indices only: it never moves, adds, welds, or removes vertices; never
|
|
168
|
+
* crosses a coplanar-patch boundary; and never changes the face count. Each accepted flip improves
|
|
169
|
+
* a deterministic ordering of triangle angles, aspect ratio, diagonal locality, and local
|
|
170
|
+
* incidence/valence concentration. Unsupported input or a failed final integrity check returns the
|
|
171
|
+
* exact original mesh object. Malformed buffers and the configured analysis complexity limit throw.
|
|
172
|
+
*
|
|
173
|
+
* @param mesh - Closed, consistently wound indexed triangle mesh in arbitrary consistent units.
|
|
174
|
+
* @param options - Patch eligibility, numeric tolerance, and deterministic work bounds.
|
|
175
|
+
* @returns Optimized connectivity and before/after diagnostics, or an unchanged safe fallback.
|
|
176
|
+
*/
|
|
177
|
+
export function optimizePlanarPatches(mesh, options = {}) {
|
|
178
|
+
const resolved = resolveOptions(options);
|
|
179
|
+
const before = buildTriangleMeshQualityAnalysis(mesh, options);
|
|
180
|
+
if (!before.report.isClosedOrientedManifold) {
|
|
181
|
+
return unchangedResult(mesh, before.report, 0, false, "input-not-closed-oriented-manifold");
|
|
182
|
+
}
|
|
183
|
+
const selectedPatchIds = new Set();
|
|
184
|
+
const boundaryEdgeKeys = new Set();
|
|
185
|
+
let selectedTriangleCount = 0;
|
|
186
|
+
let skippedPatchCount = 0;
|
|
187
|
+
let budgetExhausted = false;
|
|
188
|
+
for (const patch of before.patches) {
|
|
189
|
+
if (!isTriggeredPatch(patch, resolved))
|
|
190
|
+
continue;
|
|
191
|
+
if (patch.report.triangleCount > resolved.maximumPatchTriangleCount ||
|
|
192
|
+
selectedTriangleCount + patch.report.triangleCount > resolved.maximumProcessedTriangleCount) {
|
|
193
|
+
skippedPatchCount += 1;
|
|
194
|
+
budgetExhausted = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
selectedPatchIds.add(patch.report.id);
|
|
198
|
+
selectedTriangleCount += patch.report.triangleCount;
|
|
199
|
+
for (const key of patch.boundaryEdgeKeys)
|
|
200
|
+
boundaryEdgeKeys.add(key);
|
|
201
|
+
}
|
|
202
|
+
if (selectedPatchIds.size === 0) {
|
|
203
|
+
return unchangedResult(mesh, before.report, skippedPatchCount, budgetExhausted);
|
|
204
|
+
}
|
|
205
|
+
const triangles = before.faces.map(({ vertices }) => [...vertices]);
|
|
206
|
+
const edges = new Map();
|
|
207
|
+
const vertexEdges = Array.from({ length: before.report.vertexCount }, () => new Set());
|
|
208
|
+
for (const edge of before.edges.values()) {
|
|
209
|
+
const mutable = { ...edge, uses: edge.uses.map((use) => ({ ...use })) };
|
|
210
|
+
edges.set(edge.key, mutable);
|
|
211
|
+
vertexEdges[edge.first].add(edge.key);
|
|
212
|
+
vertexEdges[edge.second].add(edge.key);
|
|
213
|
+
}
|
|
214
|
+
const incidence = new Uint32Array(before.vertexTriangleIncidence);
|
|
215
|
+
const valence = new Uint32Array(before.vertexValence);
|
|
216
|
+
const isCandidateEdge = (edge) => {
|
|
217
|
+
if (edge.uses.length !== 2 || boundaryEdgeKeys.has(edge.key))
|
|
218
|
+
return false;
|
|
219
|
+
const firstPatch = before.patchByFace[edge.uses[0].face];
|
|
220
|
+
return (firstPatch >= 0 &&
|
|
221
|
+
firstPatch === before.patchByFace[edge.uses[1].face] &&
|
|
222
|
+
selectedPatchIds.has(firstPatch));
|
|
223
|
+
};
|
|
224
|
+
const initialEdges = [...edges.values()]
|
|
225
|
+
.filter(isCandidateEdge)
|
|
226
|
+
.sort((left, right) => left.first - right.first || left.second - right.second);
|
|
227
|
+
const maximumFlipAttempts = resolved.maximumFlipAttempts ??
|
|
228
|
+
Math.min(DEFAULT_MAXIMUM_FLIP_ATTEMPTS, initialEdges.length * ATTEMPTS_PER_INITIAL_EDGE);
|
|
229
|
+
if (maximumFlipAttempts === 0) {
|
|
230
|
+
return unchangedResult(mesh, before.report, skippedPatchCount, budgetExhausted);
|
|
231
|
+
}
|
|
232
|
+
const queue = initialEdges.map(({ key }) => key);
|
|
233
|
+
const queued = new Set(queue);
|
|
234
|
+
const revisitCounts = new Map();
|
|
235
|
+
const changedPatchIds = new Set();
|
|
236
|
+
let queueOffset = 0;
|
|
237
|
+
let attemptCount = 0;
|
|
238
|
+
let flipCount = 0;
|
|
239
|
+
const removeFace = (faceIndex, triangle) => {
|
|
240
|
+
for (let localEdge = 0; localEdge < 3; localEdge += 1) {
|
|
241
|
+
const key = edgeKey(triangle[localEdge], triangle[(localEdge + 1) % 3]);
|
|
242
|
+
const edge = edges.get(key);
|
|
243
|
+
const useIndex = edge.uses.findIndex(({ face }) => face === faceIndex);
|
|
244
|
+
edge.uses.splice(useIndex, 1);
|
|
245
|
+
if (edge.uses.length === 0) {
|
|
246
|
+
edges.delete(key);
|
|
247
|
+
vertexEdges[edge.first].delete(key);
|
|
248
|
+
vertexEdges[edge.second].delete(key);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const addFace = (faceIndex, triangle) => {
|
|
253
|
+
for (let localEdge = 0; localEdge < 3; localEdge += 1) {
|
|
254
|
+
const start = triangle[localEdge];
|
|
255
|
+
const end = triangle[(localEdge + 1) % 3];
|
|
256
|
+
const key = edgeKey(start, end);
|
|
257
|
+
let edge = edges.get(key);
|
|
258
|
+
if (edge === undefined) {
|
|
259
|
+
edge = {
|
|
260
|
+
first: Math.min(start, end),
|
|
261
|
+
key,
|
|
262
|
+
second: Math.max(start, end),
|
|
263
|
+
uses: [],
|
|
264
|
+
};
|
|
265
|
+
edges.set(key, edge);
|
|
266
|
+
vertexEdges[edge.first].add(key);
|
|
267
|
+
vertexEdges[edge.second].add(key);
|
|
268
|
+
}
|
|
269
|
+
edge.uses.push({ end, face: faceIndex, start });
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const enqueueAround = (vertices) => {
|
|
273
|
+
const candidates = new Map();
|
|
274
|
+
for (const vertex of vertices) {
|
|
275
|
+
for (const key of vertexEdges[vertex]) {
|
|
276
|
+
const edge = edges.get(key);
|
|
277
|
+
if (edge !== undefined && isCandidateEdge(edge))
|
|
278
|
+
candidates.set(key, edge);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const sorted = [...candidates.values()].sort((left, right) => left.first - right.first || left.second - right.second);
|
|
282
|
+
for (const { key } of sorted) {
|
|
283
|
+
if (queued.has(key) || (revisitCounts.get(key) ?? 0) >= resolved.maximumEdgeRevisits) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
queued.add(key);
|
|
287
|
+
queue.push(key);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
while (queueOffset < queue.length && attemptCount < maximumFlipAttempts) {
|
|
291
|
+
const key = queue[queueOffset++];
|
|
292
|
+
queued.delete(key);
|
|
293
|
+
const revisits = (revisitCounts.get(key) ?? 0) + 1;
|
|
294
|
+
revisitCounts.set(key, revisits);
|
|
295
|
+
const edge = edges.get(key);
|
|
296
|
+
if (edge === undefined || !isCandidateEdge(edge))
|
|
297
|
+
continue;
|
|
298
|
+
attemptCount += 1;
|
|
299
|
+
const uses = [...edge.uses].sort((left, right) => left.face - right.face);
|
|
300
|
+
const firstUse = uses[0];
|
|
301
|
+
const secondUse = uses[1];
|
|
302
|
+
if (firstUse.start !== secondUse.end || firstUse.end !== secondUse.start)
|
|
303
|
+
continue;
|
|
304
|
+
const firstTriangle = triangles[firstUse.face];
|
|
305
|
+
const secondTriangle = triangles[secondUse.face];
|
|
306
|
+
const oppositeFirst = firstTriangle.find((vertex) => vertex !== firstUse.start && vertex !== firstUse.end);
|
|
307
|
+
const oppositeSecond = secondTriangle.find((vertex) => vertex !== firstUse.start && vertex !== firstUse.end);
|
|
308
|
+
if (oppositeFirst === undefined || oppositeSecond === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
const vertices = [firstUse.start, firstUse.end, oppositeFirst, oppositeSecond];
|
|
311
|
+
if (new Set(vertices).size !== 4)
|
|
312
|
+
continue;
|
|
313
|
+
const replacementKey = edgeKey(oppositeFirst, oppositeSecond);
|
|
314
|
+
if (edges.has(replacementKey))
|
|
315
|
+
continue;
|
|
316
|
+
const patchId = before.patchByFace[firstUse.face];
|
|
317
|
+
const patch = before.patches[patchId];
|
|
318
|
+
const normal = patch.report.normal;
|
|
319
|
+
const startPoint = pointAt(mesh.positions, firstUse.start);
|
|
320
|
+
const endPoint = pointAt(mesh.positions, firstUse.end);
|
|
321
|
+
const firstPoint = pointAt(mesh.positions, oppositeFirst);
|
|
322
|
+
const secondPoint = pointAt(mesh.positions, oppositeSecond);
|
|
323
|
+
const sideOfReplacementFirst = dot(cross(subtract(secondPoint, firstPoint), subtract(startPoint, firstPoint)), normal);
|
|
324
|
+
const sideOfReplacementSecond = dot(cross(subtract(secondPoint, firstPoint), subtract(endPoint, firstPoint)), normal);
|
|
325
|
+
const sideOfCurrentFirst = dot(cross(subtract(endPoint, startPoint), subtract(firstPoint, startPoint)), normal);
|
|
326
|
+
const sideOfCurrentSecond = dot(cross(subtract(endPoint, startPoint), subtract(secondPoint, startPoint)), normal);
|
|
327
|
+
const minimumDoubleArea = before.tolerance.area;
|
|
328
|
+
if (Math.abs(sideOfReplacementFirst) <= minimumDoubleArea ||
|
|
329
|
+
Math.abs(sideOfReplacementSecond) <= minimumDoubleArea ||
|
|
330
|
+
Math.sign(sideOfReplacementFirst) === Math.sign(sideOfReplacementSecond) ||
|
|
331
|
+
Math.abs(sideOfCurrentFirst) <= minimumDoubleArea ||
|
|
332
|
+
Math.abs(sideOfCurrentSecond) <= minimumDoubleArea ||
|
|
333
|
+
Math.sign(sideOfCurrentFirst) === Math.sign(sideOfCurrentSecond)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const replacementFirst = [oppositeFirst, firstUse.start, oppositeSecond];
|
|
337
|
+
const replacementSecond = [oppositeFirst, oppositeSecond, firstUse.end];
|
|
338
|
+
const replacementFirstArea = dot(cross(subtract(pointAt(mesh.positions, replacementFirst[1]), firstPoint), subtract(pointAt(mesh.positions, replacementFirst[2]), firstPoint)), normal);
|
|
339
|
+
const replacementSecondArea = dot(cross(subtract(pointAt(mesh.positions, replacementSecond[1]), firstPoint), subtract(pointAt(mesh.positions, replacementSecond[2]), firstPoint)), normal);
|
|
340
|
+
if (replacementFirstArea <= minimumDoubleArea || replacementSecondArea <= minimumDoubleArea) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const qualityComparison = comparePairQuality(pairQuality(mesh.positions, firstTriangle, secondTriangle), pairQuality(mesh.positions, replacementFirst, replacementSecond), before.tolerance.angular);
|
|
344
|
+
if (qualityComparison < 0)
|
|
345
|
+
continue;
|
|
346
|
+
if (qualityComparison === 0) {
|
|
347
|
+
const currentDiagonalSquared = squaredLength(subtract(endPoint, startPoint));
|
|
348
|
+
const replacementDiagonalSquared = squaredLength(subtract(secondPoint, firstPoint));
|
|
349
|
+
const squaredLengthTolerance = before.tolerance.linear ** 2;
|
|
350
|
+
if (replacementDiagonalSquared > currentDiagonalSquared + squaredLengthTolerance)
|
|
351
|
+
continue;
|
|
352
|
+
if (Math.abs(replacementDiagonalSquared - currentDiagonalSquared) <= squaredLengthTolerance) {
|
|
353
|
+
const changes = new Map([
|
|
354
|
+
[firstUse.start, -1],
|
|
355
|
+
[firstUse.end, -1],
|
|
356
|
+
[oppositeFirst, 1],
|
|
357
|
+
[oppositeSecond, 1],
|
|
358
|
+
]);
|
|
359
|
+
if (localConcentrationPotential(vertices, incidence, valence, changes) >=
|
|
360
|
+
localConcentrationPotential(vertices, incidence, valence)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
removeFace(firstUse.face, firstTriangle);
|
|
366
|
+
removeFace(secondUse.face, secondTriangle);
|
|
367
|
+
triangles[firstUse.face] = replacementFirst;
|
|
368
|
+
triangles[secondUse.face] = replacementSecond;
|
|
369
|
+
addFace(firstUse.face, replacementFirst);
|
|
370
|
+
addFace(secondUse.face, replacementSecond);
|
|
371
|
+
incidence[firstUse.start] -= 1;
|
|
372
|
+
incidence[firstUse.end] -= 1;
|
|
373
|
+
incidence[oppositeFirst] += 1;
|
|
374
|
+
incidence[oppositeSecond] += 1;
|
|
375
|
+
valence[firstUse.start] -= 1;
|
|
376
|
+
valence[firstUse.end] -= 1;
|
|
377
|
+
valence[oppositeFirst] += 1;
|
|
378
|
+
valence[oppositeSecond] += 1;
|
|
379
|
+
flipCount += 1;
|
|
380
|
+
changedPatchIds.add(patchId);
|
|
381
|
+
enqueueAround(vertices);
|
|
382
|
+
}
|
|
383
|
+
if (queueOffset < queue.length)
|
|
384
|
+
budgetExhausted = true;
|
|
385
|
+
if (flipCount === 0) {
|
|
386
|
+
return unchangedResult(mesh, before.report, skippedPatchCount, budgetExhausted);
|
|
387
|
+
}
|
|
388
|
+
const indices = new Uint32Array(mesh.indices.length);
|
|
389
|
+
for (let faceIndex = 0; faceIndex < triangles.length; faceIndex += 1) {
|
|
390
|
+
indices.set(triangles[faceIndex], faceIndex * 3);
|
|
391
|
+
}
|
|
392
|
+
const candidate = { indices, positions: mesh.positions };
|
|
393
|
+
const after = buildTriangleMeshQualityAnalysis(candidate, options);
|
|
394
|
+
const fallbackReason = fallbackReasonForCandidate(before, after, boundaryEdgeKeys);
|
|
395
|
+
if (fallbackReason !== undefined) {
|
|
396
|
+
return unchangedResult(mesh, before.report, skippedPatchCount, budgetExhausted, fallbackReason);
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
after: after.report,
|
|
400
|
+
before: before.report,
|
|
401
|
+
budgetExhausted,
|
|
402
|
+
changed: true,
|
|
403
|
+
flipCount,
|
|
404
|
+
mesh: candidate,
|
|
405
|
+
optimizedPatchCount: changedPatchIds.size,
|
|
406
|
+
skippedPatchCount,
|
|
407
|
+
};
|
|
408
|
+
}
|