brepjs 18.112.0 → 18.114.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.
@@ -0,0 +1,857 @@
1
+ const require_topologyQueryFns = require("./topologyQueryFns-CoXVHuN7.cjs");
2
+ const require_faceFns = require("./faceFns-BV0GnTlt.cjs");
3
+ const require_shapeFns = require("./shapeFns-ybo0INGL.cjs");
4
+ const require_measureFns = require("./measureFns-DblG0Yaz.cjs");
5
+ const require_adjacencyFns = require("./adjacencyFns-d8ZabkvX.cjs");
6
+ //#region src/topology/shapeRef/scoring.ts
7
+ /**
8
+ * Default face scorer combining surface type, normal alignment, centroid proximity,
9
+ * and area similarity.
10
+ *
11
+ * Scoring breakdown:
12
+ * - Surface type match: +1.0 (mismatch when both defined: -Infinity)
13
+ * - Normal dot product: weighted contribution (rejected if < 0.707)
14
+ * - Centroid distance: quadratic penalty (rejected if distSq > 100)
15
+ * - Area ratio: penalized if |log(hintArea / faceArea)| > 1.0
16
+ */
17
+ function defaultScorer(hint, face) {
18
+ let score = 0;
19
+ const faceType = require_faceFns.faceGeomType(face);
20
+ if (hint.surfaceType !== void 0) if (faceType === hint.surfaceType) score += 1;
21
+ else return -Infinity;
22
+ if (hint.normal !== void 0) {
23
+ const faceNormal = require_faceFns.normalAt(face);
24
+ const dot = hint.normal[0] * faceNormal[0] + hint.normal[1] * faceNormal[1] + hint.normal[2] * faceNormal[2];
25
+ if (dot < .707) return -Infinity;
26
+ score += dot;
27
+ }
28
+ if (hint.centroid !== void 0) {
29
+ const faceCentroid = require_faceFns.faceCenter(face);
30
+ const dx = hint.centroid[0] - faceCentroid[0];
31
+ const dy = hint.centroid[1] - faceCentroid[1];
32
+ const dz = hint.centroid[2] - faceCentroid[2];
33
+ const distSq = dx * dx + dy * dy + dz * dz;
34
+ if (distSq > 100) return -Infinity;
35
+ score -= distSq / 100;
36
+ }
37
+ if (hint.area !== void 0 && hint.area > 0) {
38
+ const areaResult = require_measureFns.measureArea(face);
39
+ if (areaResult.ok && areaResult.value > 0) {
40
+ const logRatio = Math.abs(Math.log(hint.area / areaResult.value));
41
+ if (logRatio > 1) score -= logRatio;
42
+ }
43
+ }
44
+ return score;
45
+ }
46
+ //#endregion
47
+ //#region src/topology/shapeRef/shapeRefFns.ts
48
+ /** Snapshot the geometric properties of a face for later matching. */
49
+ function captureHint(face) {
50
+ const surfaceType = require_faceFns.faceGeomType(face);
51
+ const normal = require_faceFns.normalAt(face);
52
+ const centroid = require_faceFns.faceCenter(face);
53
+ const areaResult = require_measureFns.measureArea(face);
54
+ return {
55
+ entityType: "face",
56
+ surfaceType,
57
+ normal,
58
+ centroid,
59
+ area: areaResult.ok ? areaResult.value : void 0
60
+ };
61
+ }
62
+ /** Threshold for dominant-axis detection (abs(component) > 0.9). */
63
+ var AXIS_THRESHOLD = .9;
64
+ /** Determine the cardinal role name for a box face from its outward normal. */
65
+ function boxRoleFromNormal(n) {
66
+ if (n[2] > AXIS_THRESHOLD) return "box:top";
67
+ if (n[2] < -.9) return "box:bottom";
68
+ if (n[1] > AXIS_THRESHOLD) return "box:back";
69
+ if (n[1] < -.9) return "box:front";
70
+ if (n[0] > AXIS_THRESHOLD) return "box:right";
71
+ if (n[0] < -.9) return "box:left";
72
+ }
73
+ /**
74
+ * Auto-assign role names to the faces of a shape based on operation type.
75
+ *
76
+ * For 'box': uses face normals to assign cardinal names
77
+ * ('box:top', 'box:bottom', 'box:front', 'box:back', 'box:left', 'box:right').
78
+ * **Note:** Box role detection assumes axis-aligned faces (normal within 0.9 of
79
+ * a cardinal axis). Rotated boxes may receive fewer than 6 named roles; remaining
80
+ * faces fall through to sequential naming.
81
+ *
82
+ * For other types: sequential naming ('opType:face_0', 'opType:face_1', ...).
83
+ *
84
+ * @returns Map from role name to its face hash codes (one at assignment time;
85
+ * a role accrues more hashes only later, when `updateRoles` tracks a split).
86
+ */
87
+ function assignRoles(shape, operationType) {
88
+ const faces = require_topologyQueryFns.getFaces(shape);
89
+ const roles = /* @__PURE__ */ new Map();
90
+ if (operationType === "box") {
91
+ for (const face of faces) {
92
+ const role = boxRoleFromNormal(require_faceFns.normalAt(face));
93
+ if (role !== void 0 && !roles.has(role)) roles.set(role, [require_shapeFns.getHashCode(face)]);
94
+ }
95
+ return roles;
96
+ }
97
+ let index = 0;
98
+ for (const face of faces) {
99
+ roles.set(`${operationType}:face_${index}`, [require_shapeFns.getHashCode(face)]);
100
+ index++;
101
+ }
102
+ return roles;
103
+ }
104
+ /** Create a ShapeRef from an origin ID, role name, and face. */
105
+ function createRef(origin, role, face) {
106
+ return {
107
+ origin,
108
+ role,
109
+ hint: captureHint(face)
110
+ };
111
+ }
112
+ /**
113
+ * Advance a role's face hashes through one evolution: drop deleted faces,
114
+ * replace a modified face with *all* its successors (a 1→many split keeps every
115
+ * fragment), and keep unchanged faces. Deduped so a shared successor isn't
116
+ * doubled.
117
+ */
118
+ function nextHashes(hashes, evolution) {
119
+ const successors = [];
120
+ for (const hash of hashes) {
121
+ if (evolution.deleted.has(hash)) continue;
122
+ const modified = evolution.modified.get(hash);
123
+ const targets = modified && modified.length > 0 ? modified : [hash];
124
+ for (const h of targets) if (!successors.includes(h)) successors.push(h);
125
+ }
126
+ return successors;
127
+ }
128
+ /**
129
+ * Propagate a role table through a ShapeEvolution record.
130
+ * Returns a new RoleTable with hashes updated according to the evolution.
131
+ *
132
+ * - Deleted faces: hash dropped (role removed once all its hashes are gone).
133
+ * - Modified faces: hash replaced by **all** successor hashes — so a 1→many
134
+ * split keeps every fragment, and `resolveRef` disambiguates among them.
135
+ * - Unchanged faces: hash preserved.
136
+ *
137
+ * Note: `evolution.generated` is intentionally not consumed here — on the OCCT
138
+ * kernels its hashes refer to an intermediate shape, not the final result, so
139
+ * naming generated faces produces roles that never resolve (verified: 0 live
140
+ * generated hashes across cut/fuse on occt-wasm). Stable names for generated
141
+ * geometry (fillet rounds, boolean seams) need history-fidelity work tracked
142
+ * separately.
143
+ */
144
+ function updateRoles(roles, origin, evolution) {
145
+ const originRoles = roles.get(origin);
146
+ if (!originRoles) return roles;
147
+ const updatedOriginRoles = /* @__PURE__ */ new Map();
148
+ for (const [role, hashes] of originRoles) {
149
+ const successors = nextHashes(hashes, evolution);
150
+ if (successors.length > 0) updatedOriginRoles.set(role, successors);
151
+ }
152
+ const newRoles = /* @__PURE__ */ new Map();
153
+ for (const [key, value] of roles) newRoles.set(key, key === origin ? updatedOriginRoles : value);
154
+ return newRoles;
155
+ }
156
+ /** Ambiguity threshold: if two scores are within this range, it's ambiguous. */
157
+ var AMBIGUITY_THRESHOLD = .1;
158
+ /** Minimum score for geometric fallback to accept a match. */
159
+ var MIN_SCORE = .5;
160
+ /**
161
+ * Score `candidates` against `hint`, returning the best match, a tie within
162
+ * {@link AMBIGUITY_THRESHOLD}, or nothing above {@link MIN_SCORE}. Scoping the
163
+ * candidates to a role's tracked successors (rather than every face) is what
164
+ * makes split-face disambiguation reliable — the fragments compete only with
165
+ * each other, not with unrelated geometry.
166
+ */
167
+ function scoreFaces(hint, candidates, scoreFn) {
168
+ let bestScore = -Infinity;
169
+ let bestFace;
170
+ let secondBestScore = -Infinity;
171
+ const scored = [];
172
+ for (const face of candidates) {
173
+ const score = scoreFn(hint, face);
174
+ if (score > MIN_SCORE) scored.push([face, score]);
175
+ if (score > bestScore) {
176
+ secondBestScore = bestScore;
177
+ bestScore = score;
178
+ bestFace = face;
179
+ } else if (score > secondBestScore) secondBestScore = score;
180
+ }
181
+ if (bestFace !== void 0 && bestScore > MIN_SCORE) {
182
+ if (bestScore - secondBestScore < AMBIGUITY_THRESHOLD && scored.length > 1) return {
183
+ kind: "ambiguous",
184
+ candidates: scored.filter(([, s]) => s >= bestScore - AMBIGUITY_THRESHOLD).map(([f]) => f)
185
+ };
186
+ return {
187
+ kind: "match",
188
+ face: bestFace
189
+ };
190
+ }
191
+ return { kind: "none" };
192
+ }
193
+ /**
194
+ * Resolve a ShapeRef to a face in the current shape.
195
+ *
196
+ * Resolution strategy:
197
+ * 1. Exact: the role's tracked successor hashes. One survivor → exact match;
198
+ * several survivors (a face that split) → disambiguate among *only those*
199
+ * fragments; none survive → deleted.
200
+ * 2. Geometric fallback over the whole shape when the role isn't tracked (or a
201
+ * scoped score turned up nothing): best-scoring face, else ambiguous /
202
+ * not-found.
203
+ */
204
+ function resolveRef(ref, roles, currentShape, scorer) {
205
+ const faces = require_topologyQueryFns.getFaces(currentShape);
206
+ const scoreFn = scorer ?? defaultScorer;
207
+ const targetHashes = roles.get(ref.origin)?.get(ref.role);
208
+ if (targetHashes !== void 0 && targetHashes.length > 0) {
209
+ const survivors = faces.filter((f) => targetHashes.includes(require_shapeFns.getHashCode(f)));
210
+ if (survivors.length === 1) {
211
+ const [only] = survivors;
212
+ if (only !== void 0) return {
213
+ face: only,
214
+ confidence: "exact"
215
+ };
216
+ } else if (survivors.length === 0) return {
217
+ ref,
218
+ reason: "deleted"
219
+ };
220
+ else {
221
+ const outcome = scoreFaces(ref.hint, survivors, scoreFn);
222
+ if (outcome.kind === "match") return {
223
+ face: outcome.face,
224
+ confidence: "geometric-fallback"
225
+ };
226
+ if (outcome.kind === "ambiguous") return {
227
+ ref,
228
+ reason: "ambiguous",
229
+ candidates: outcome.candidates
230
+ };
231
+ }
232
+ }
233
+ const outcome = scoreFaces(ref.hint, faces, scoreFn);
234
+ if (outcome.kind === "match") return {
235
+ face: outcome.face,
236
+ confidence: "geometric-fallback"
237
+ };
238
+ if (outcome.kind === "ambiguous") return {
239
+ ref,
240
+ reason: "ambiguous",
241
+ candidates: outcome.candidates
242
+ };
243
+ return {
244
+ ref,
245
+ reason: "not-found"
246
+ };
247
+ }
248
+ //#endregion
249
+ //#region src/topology/shapeRef/roleLookup.ts
250
+ /** Euclidean distance between two points. */
251
+ function distance(a, b) {
252
+ const dx = a[0] - b[0];
253
+ const dy = a[1] - b[1];
254
+ const dz = a[2] - b[2];
255
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
256
+ }
257
+ /** Mean of a set of vertices' positions (e.g. an edge's endpoints). */
258
+ function vertexCentroid(verts) {
259
+ if (verts.length === 0) return void 0;
260
+ let x = 0;
261
+ let y = 0;
262
+ let z = 0;
263
+ for (const v of verts) {
264
+ const p = require_topologyQueryFns.vertexPosition(v);
265
+ x += p[0];
266
+ y += p[1];
267
+ z += p[2];
268
+ }
269
+ const n = verts.length;
270
+ return [
271
+ x / n,
272
+ y / n,
273
+ z / n
274
+ ];
275
+ }
276
+ /** The role whose tracked hashes include this face (reverse lookup). */
277
+ function roleOfFace(face, origin, roles) {
278
+ const originRoles = roles.get(origin);
279
+ if (!originRoles) return void 0;
280
+ const hash = require_shapeFns.getHashCode(face);
281
+ for (const [role, hashes] of originRoles) if (hashes.includes(hash)) return role;
282
+ }
283
+ /** Current faces a role resolves to — its tracked successors present in `shape`. */
284
+ function facesForRole(shape, origin, role, roles) {
285
+ const hashes = roles.get(origin)?.get(role);
286
+ if (hashes === void 0 || hashes.length === 0) return [];
287
+ return require_topologyQueryFns.getFaces(shape).filter((f) => hashes.includes(require_shapeFns.getHashCode(f)));
288
+ }
289
+ //#endregion
290
+ //#region src/topology/shapeRef/edgeRefFns.ts
291
+ function captureEdgeHint(edge) {
292
+ const lengthResult = require_measureFns.measureLength(edge);
293
+ return {
294
+ entityType: "edge",
295
+ length: lengthResult.ok ? lengthResult.value : void 0,
296
+ midpoint: vertexCentroid(require_adjacencyFns.verticesOfEdge(edge))
297
+ };
298
+ }
299
+ function dedupeEdges(edges) {
300
+ const seen = /* @__PURE__ */ new Set();
301
+ const out = [];
302
+ for (const e of edges) {
303
+ const h = require_shapeFns.getHashCode(e);
304
+ if (!seen.has(h)) {
305
+ seen.add(h);
306
+ out.push(e);
307
+ }
308
+ }
309
+ return out;
310
+ }
311
+ /** Hint scores closer than this are treated as indistinguishable (→ ambiguous). */
312
+ var HINT_MARGIN$2 = 1e-6;
313
+ /**
314
+ * Pick the candidate edge closest to the hint (length + endpoint midpoint).
315
+ * Returns undefined when it genuinely can't discriminate — the hint carries no
316
+ * signal, or the two best candidates score within {@link HINT_MARGIN} — so the
317
+ * caller can report `ambiguous` rather than committing to an arbitrary edge.
318
+ */
319
+ function bestByHint(candidates, hint) {
320
+ if (hint.length === void 0 && hint.midpoint === void 0) return void 0;
321
+ let best;
322
+ let bestScore = Infinity;
323
+ let secondScore = Infinity;
324
+ for (const edge of candidates) {
325
+ let score = 0;
326
+ if (hint.length !== void 0) {
327
+ const len = require_measureFns.measureLength(edge);
328
+ if (len.ok) score += Math.abs(len.value - hint.length);
329
+ }
330
+ if (hint.midpoint !== void 0) {
331
+ const mid = vertexCentroid(require_adjacencyFns.verticesOfEdge(edge));
332
+ if (mid !== void 0) score += distance(hint.midpoint, mid);
333
+ }
334
+ if (score < bestScore) {
335
+ secondScore = bestScore;
336
+ bestScore = score;
337
+ best = edge;
338
+ } else if (score < secondScore) secondScore = score;
339
+ }
340
+ if (best === void 0 || secondScore - bestScore < HINT_MARGIN$2) return void 0;
341
+ return best;
342
+ }
343
+ /**
344
+ * Capture a lineage-based reference to `edge`: its two adjacent faces' roles
345
+ * plus a geometric hint. Returns undefined when the edge doesn't bound two faces
346
+ * (a boundary/degenerate edge) or when either bounding face has no role yet.
347
+ */
348
+ function createEdgeRef(origin, edge, shape, roles) {
349
+ const [faceA, faceB] = require_adjacencyFns.facesOfEdge(shape, edge);
350
+ if (faceA === void 0 || faceB === void 0) return void 0;
351
+ const roleA = roleOfFace(faceA, origin, roles);
352
+ const roleB = roleOfFace(faceB, origin, roles);
353
+ if (roleA === void 0 || roleB === void 0) return void 0;
354
+ return {
355
+ origin,
356
+ faceRoles: [roleA, roleB],
357
+ hint: captureEdgeHint(edge)
358
+ };
359
+ }
360
+ /**
361
+ * Resolve an EdgeRef in `shape`: resolve its two face-roles to current faces,
362
+ * then return the edge they share. One shared edge → exact; several (the two
363
+ * faces meet along more than one edge) → disambiguate by hint; none, or a
364
+ * missing bounding face → broken.
365
+ */
366
+ function resolveEdgeRef(ref, roles, shape) {
367
+ const [roleA, roleB] = ref.faceRoles;
368
+ const facesA = facesForRole(shape, ref.origin, roleA, roles);
369
+ const facesB = facesForRole(shape, ref.origin, roleB, roles);
370
+ if (facesA.length === 0 || facesB.length === 0) return {
371
+ ref,
372
+ reason: "not-found"
373
+ };
374
+ const candidates = [];
375
+ for (const a of facesA) for (const b of facesB) candidates.push(...require_adjacencyFns.sharedEdges(a, b));
376
+ const unique = dedupeEdges(candidates);
377
+ if (unique.length === 1) {
378
+ const [only] = unique;
379
+ if (only !== void 0) return {
380
+ edge: only,
381
+ confidence: "exact"
382
+ };
383
+ }
384
+ if (unique.length > 1) {
385
+ const best = bestByHint(unique, ref.hint);
386
+ if (best !== void 0) return {
387
+ edge: best,
388
+ confidence: "geometric-fallback"
389
+ };
390
+ return {
391
+ ref,
392
+ reason: "ambiguous",
393
+ candidates: unique
394
+ };
395
+ }
396
+ return {
397
+ ref,
398
+ reason: "not-found"
399
+ };
400
+ }
401
+ //#endregion
402
+ //#region src/topology/shapeRef/vertexRefFns.ts
403
+ /** A corner needs at least this many faces to pin a unique point. */
404
+ var MIN_VERTEX_FACES = 3;
405
+ /** Hint distances closer than this are indistinguishable (→ ambiguous). */
406
+ var HINT_MARGIN$1 = 1e-6;
407
+ /** Hashes of all vertices across `faces`, recording one handle per hash. */
408
+ function vertexHashes(faces, handles) {
409
+ const hashes = /* @__PURE__ */ new Set();
410
+ for (const f of faces) for (const v of require_adjacencyFns.verticesOfFace(f)) {
411
+ const h = require_shapeFns.getHashCode(v);
412
+ hashes.add(h);
413
+ if (!handles.has(h)) handles.set(h, v);
414
+ }
415
+ return hashes;
416
+ }
417
+ function intersect(a, b) {
418
+ const out = /* @__PURE__ */ new Set();
419
+ for (const h of a) if (b.has(h)) out.add(h);
420
+ return out;
421
+ }
422
+ /** Vertices present in (a face of) every face-set — the shared corner(s). */
423
+ function commonVertices(faceSets) {
424
+ const handles = /* @__PURE__ */ new Map();
425
+ let common;
426
+ for (const faces of faceSets) {
427
+ const hashes = vertexHashes(faces, handles);
428
+ common = common === void 0 ? hashes : intersect(common, hashes);
429
+ }
430
+ if (common === void 0) return [];
431
+ const result = [];
432
+ for (const h of common) {
433
+ const v = handles.get(h);
434
+ if (v !== void 0) result.push(v);
435
+ }
436
+ return result;
437
+ }
438
+ /** Nearest candidate to the hint position; undefined if no signal or a tie. */
439
+ function nearestToHint(vertices, hint) {
440
+ if (hint.position === void 0) return void 0;
441
+ let best;
442
+ let bestDist = Infinity;
443
+ let secondDist = Infinity;
444
+ for (const v of vertices) {
445
+ const d = distance(require_topologyQueryFns.vertexPosition(v), hint.position);
446
+ if (d < bestDist) {
447
+ secondDist = bestDist;
448
+ bestDist = d;
449
+ best = v;
450
+ } else if (d < secondDist) secondDist = d;
451
+ }
452
+ if (best === void 0 || secondDist - bestDist < HINT_MARGIN$1) return void 0;
453
+ return best;
454
+ }
455
+ /**
456
+ * Capture a lineage-based reference to `vertex`: the roles of the ≥3 faces
457
+ * meeting at it, plus a position hint. Returns undefined when fewer than three
458
+ * named faces meet there (a 2-face "vertex" is ambiguous — an edge has two).
459
+ */
460
+ function createVertexRef(origin, vertex, shape, roles) {
461
+ const faces = require_adjacencyFns.facesOfVertex(shape, vertex);
462
+ if (faces.length < MIN_VERTEX_FACES) return void 0;
463
+ const roleSet = /* @__PURE__ */ new Set();
464
+ for (const f of faces) {
465
+ const role = roleOfFace(f, origin, roles);
466
+ if (role !== void 0) roleSet.add(role);
467
+ }
468
+ if (roleSet.size < MIN_VERTEX_FACES) return void 0;
469
+ return {
470
+ origin,
471
+ faceRoles: [...roleSet].sort(),
472
+ hint: {
473
+ entityType: "vertex",
474
+ position: require_topologyQueryFns.vertexPosition(vertex)
475
+ }
476
+ };
477
+ }
478
+ /**
479
+ * Resolve a VertexRef in `shape`: gather the current faces of each role, then
480
+ * return the vertex common to all of them. One common vertex → exact; several →
481
+ * disambiguate by hint position; none, or a missing role → broken.
482
+ */
483
+ function resolveVertexRef(ref, roles, shape) {
484
+ const faceSets = [];
485
+ for (const role of ref.faceRoles) {
486
+ const faces = facesForRole(shape, ref.origin, role, roles);
487
+ if (faces.length === 0) return {
488
+ ref,
489
+ reason: "not-found"
490
+ };
491
+ faceSets.push(faces);
492
+ }
493
+ const common = commonVertices(faceSets);
494
+ if (common.length === 1) {
495
+ const [only] = common;
496
+ if (only !== void 0) return {
497
+ vertex: only,
498
+ confidence: "exact"
499
+ };
500
+ }
501
+ if (common.length > 1) {
502
+ const best = nearestToHint(common, ref.hint);
503
+ if (best !== void 0) return {
504
+ vertex: best,
505
+ confidence: "geometric-fallback"
506
+ };
507
+ return {
508
+ ref,
509
+ reason: "ambiguous",
510
+ candidates: common
511
+ };
512
+ }
513
+ return {
514
+ ref,
515
+ reason: "not-found"
516
+ };
517
+ }
518
+ //#endregion
519
+ //#region src/topology/shapeRef/derivedFaceRefFns.ts
520
+ /** A face re-derives a bridged face when its normal is nearly identical. */
521
+ var NORMAL_MATCH = .99;
522
+ /** A generated face's normal has a positive component along BOTH bridged normals. */
523
+ var BLEND_THRESHOLD = .1;
524
+ /** Tiebreak distances closer than this are indistinguishable (→ ambiguous). */
525
+ var HINT_MARGIN = 1e-6;
526
+ function normalDot(a, b) {
527
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
528
+ }
529
+ /** Faces whose outward normal nearly matches `normal` (re-derives a bridged face). */
530
+ function facesByNormal(shape, normal) {
531
+ return require_topologyQueryFns.getFaces(shape).filter((f) => normalDot(require_faceFns.normalAt(f), normal) > NORMAL_MATCH);
532
+ }
533
+ /**
534
+ * Faces adjacent to BOTH face sets, excluding the sets themselves. Uses the
535
+ * cached edge→faces adjacency (`adjacentFaces`) rather than an O(F·N) per-pair
536
+ * `sharedEdges` scan.
537
+ */
538
+ function betweenFaces(shape, aFaces, bFaces) {
539
+ const exclude = new Set([...aFaces, ...bFaces].map(require_shapeFns.getHashCode));
540
+ const adjacentToA = /* @__PURE__ */ new Set();
541
+ for (const a of aFaces) for (const f of require_adjacencyFns.adjacentFaces(shape, a)) adjacentToA.add(require_shapeFns.getHashCode(f));
542
+ const result = [];
543
+ const seen = /* @__PURE__ */ new Set();
544
+ for (const b of bFaces) for (const f of require_adjacencyFns.adjacentFaces(shape, b)) {
545
+ const h = require_shapeFns.getHashCode(f);
546
+ if (exclude.has(h) || seen.has(h) || !adjacentToA.has(h)) continue;
547
+ seen.add(h);
548
+ result.push(f);
549
+ }
550
+ return result;
551
+ }
552
+ /** Face whose center is nearest `point`; undefined on a tie (→ ambiguous). */
553
+ function nearestFace(faces, point) {
554
+ let best;
555
+ let bestDist = Infinity;
556
+ let secondDist = Infinity;
557
+ for (const f of faces) {
558
+ const d = distance(require_faceFns.faceCenter(f), point);
559
+ if (d < bestDist) {
560
+ secondDist = bestDist;
561
+ bestDist = d;
562
+ best = f;
563
+ } else if (d < secondDist) secondDist = d;
564
+ }
565
+ if (best === void 0 || secondDist - bestDist < HINT_MARGIN) return void 0;
566
+ return best;
567
+ }
568
+ /**
569
+ * Capture a reference to the face an `op` (fillet/chamfer) will generate across
570
+ * `edge`: the roles of the edge's two faces, their outward normals, and the edge
571
+ * midpoint. Call this on the PRE-op shape. Returns undefined when the edge
572
+ * doesn't bound two named faces.
573
+ */
574
+ function createDerivedFaceRef(origin, op, edge, preShape, roles) {
575
+ const [faceA, faceB] = require_adjacencyFns.facesOfEdge(preShape, edge);
576
+ if (faceA === void 0 || faceB === void 0) return void 0;
577
+ const roleA = roleOfFace(faceA, origin, roles);
578
+ const roleB = roleOfFace(faceB, origin, roles);
579
+ if (roleA === void 0 || roleB === void 0) return void 0;
580
+ return {
581
+ origin,
582
+ op,
583
+ betweenRoles: [roleA, roleB],
584
+ hint: {
585
+ entityType: "derived-face",
586
+ normalA: require_faceFns.normalAt(faceA),
587
+ normalB: require_faceFns.normalAt(faceB),
588
+ edgeMidpoint: vertexCentroid(require_adjacencyFns.verticesOfEdge(edge))
589
+ }
590
+ };
591
+ }
592
+ /**
593
+ * Resolve a DerivedFaceRef in the post-op `shape`: re-derive the two bridged
594
+ * faces (role table, else by captured normal), then return the face adjacent to
595
+ * both whose normal blends both. One survivor → resolved; several →
596
+ * nearest-to-edge-midpoint; none → broken.
597
+ */
598
+ function resolveDerivedFaceRef(ref, roles, shape) {
599
+ let aFaces = facesForRole(shape, ref.origin, ref.betweenRoles[0], roles);
600
+ if (aFaces.length === 0) aFaces = facesByNormal(shape, ref.hint.normalA);
601
+ let bFaces = facesForRole(shape, ref.origin, ref.betweenRoles[1], roles);
602
+ if (bFaces.length === 0) bFaces = facesByNormal(shape, ref.hint.normalB);
603
+ if (aFaces.length === 0 || bFaces.length === 0) return {
604
+ ref,
605
+ reason: "not-found"
606
+ };
607
+ const blended = betweenFaces(shape, aFaces, bFaces).filter((f) => normalDot(require_faceFns.normalAt(f), ref.hint.normalA) > BLEND_THRESHOLD && normalDot(require_faceFns.normalAt(f), ref.hint.normalB) > BLEND_THRESHOLD);
608
+ if (blended.length === 1) {
609
+ const [only] = blended;
610
+ if (only !== void 0) return {
611
+ face: only,
612
+ confidence: "geometric-fallback"
613
+ };
614
+ }
615
+ if (blended.length > 1) {
616
+ const best = ref.hint.edgeMidpoint && nearestFace(blended, ref.hint.edgeMidpoint);
617
+ if (best) return {
618
+ face: best,
619
+ confidence: "geometric-fallback"
620
+ };
621
+ return {
622
+ ref,
623
+ reason: "ambiguous",
624
+ candidates: blended
625
+ };
626
+ }
627
+ return {
628
+ ref,
629
+ reason: "not-found"
630
+ };
631
+ }
632
+ //#endregion
633
+ //#region src/topology/shapeRef/refResolveFns.ts
634
+ function isRefObject(v) {
635
+ return typeof v === "object" && v !== null && "origin" in v;
636
+ }
637
+ /** A generated-face ref: carries the bridged roles + the op that made it. */
638
+ function isDerivedFaceRef(v) {
639
+ return isRefObject(v) && "betweenRoles" in v && "op" in v;
640
+ }
641
+ /** An edge ref: exactly two adjacent face-roles. */
642
+ function isEdgeRef(v) {
643
+ return isRefObject(v) && Array.isArray(v["faceRoles"]) && v["faceRoles"].length === 2;
644
+ }
645
+ /** A vertex ref: three or more adjacent face-roles. */
646
+ function isVertexRef(v) {
647
+ return isRefObject(v) && Array.isArray(v["faceRoles"]) && v["faceRoles"].length >= 3;
648
+ }
649
+ /** A face ref: a single role name. */
650
+ function isFaceRef(v) {
651
+ return isRefObject(v) && typeof v["role"] === "string";
652
+ }
653
+ /** True for any of the four lineage reference kinds. */
654
+ function isLineageRef(v) {
655
+ return isFaceRef(v) || isEdgeRef(v) || isVertexRef(v) || isDerivedFaceRef(v);
656
+ }
657
+ function brokenResolution(reason, candidates) {
658
+ return candidates === void 0 ? {
659
+ ok: false,
660
+ reason
661
+ } : {
662
+ ok: false,
663
+ reason,
664
+ candidates
665
+ };
666
+ }
667
+ /**
668
+ * Resolve a lineage ref against `shape` using a prepared role table (the robust
669
+ * path — the table is maintained across edits via `updateRoles`). Returns the
670
+ * live entity on success, or the failure reason (and tied candidates) on failure.
671
+ */
672
+ function resolveLineageRef(ref, roles, shape) {
673
+ if (isDerivedFaceRef(ref)) {
674
+ const r = resolveDerivedFaceRef(ref, roles, shape);
675
+ return "face" in r ? {
676
+ ok: true,
677
+ entity: r.face
678
+ } : brokenResolution(r.reason, r.candidates);
679
+ }
680
+ if (isEdgeRef(ref)) {
681
+ const r = resolveEdgeRef(ref, roles, shape);
682
+ return "edge" in r ? {
683
+ ok: true,
684
+ entity: r.edge
685
+ } : brokenResolution(r.reason, r.candidates);
686
+ }
687
+ if (isVertexRef(ref)) {
688
+ const r = resolveVertexRef(ref, roles, shape);
689
+ return "vertex" in r ? {
690
+ ok: true,
691
+ entity: r.vertex
692
+ } : brokenResolution(r.reason, r.candidates);
693
+ }
694
+ const r = resolveRef(ref, roles, shape);
695
+ return "face" in r ? {
696
+ ok: true,
697
+ entity: r.face
698
+ } : brokenResolution(r.reason, r.candidates);
699
+ }
700
+ /** Role table for a from-scratch rebuild, reusing a per-origin cache. */
701
+ function rolesFor(ref, shape, cache) {
702
+ const cached = cache.get(ref.origin);
703
+ if (cached !== void 0) return cached;
704
+ const roles = new Map([[ref.origin, assignRoles(shape, ref.origin)]]);
705
+ cache.set(ref.origin, roles);
706
+ return roles;
707
+ }
708
+ /**
709
+ * Resolve a lineage ref against a freshly rebuilt `shape` with no maintained
710
+ * role table, re-deriving roles via `assignRoles(shape, ref.origin)`. The ref's
711
+ * `origin` must therefore be the role-assignment scheme (e.g. `'box'`), and
712
+ * stability is bounded by that scheme — `'box'` names faces semantically
713
+ * (rebuild-stable); other schemes fall back to positional `face_N`.
714
+ */
715
+ function resolveRefIn(ref, shape) {
716
+ return resolveLineageRef(ref, rolesFor(ref, shape, /* @__PURE__ */ new Map()), shape);
717
+ }
718
+ /**
719
+ * Replace every lineage ref in an operation's params with the live entity it
720
+ * resolves to in `shape`, recursing into arrays (multi-entity selections like a
721
+ * fillet's edge list). Refs that can't resolve are left as-is. Role tables are
722
+ * re-derived once per `origin` and reused across the whole params map. Lets a
723
+ * replay engine pass stable entity selections that survive upstream edits.
724
+ */
725
+ function resolveRefParams(params, shape) {
726
+ const roleCache = /* @__PURE__ */ new Map();
727
+ const resolveValue = (value) => {
728
+ if (Array.isArray(value)) return value.map(resolveValue);
729
+ if (!isLineageRef(value)) return value;
730
+ const res = resolveLineageRef(value, rolesFor(value, shape, roleCache), shape);
731
+ return res.ok ? res.entity : value;
732
+ };
733
+ const out = { ...params };
734
+ for (const [key, value] of Object.entries(params)) out[key] = resolveValue(value);
735
+ return out;
736
+ }
737
+ //#endregion
738
+ Object.defineProperty(exports, "assignRoles", {
739
+ enumerable: true,
740
+ get: function() {
741
+ return assignRoles;
742
+ }
743
+ });
744
+ Object.defineProperty(exports, "captureHint", {
745
+ enumerable: true,
746
+ get: function() {
747
+ return captureHint;
748
+ }
749
+ });
750
+ Object.defineProperty(exports, "createDerivedFaceRef", {
751
+ enumerable: true,
752
+ get: function() {
753
+ return createDerivedFaceRef;
754
+ }
755
+ });
756
+ Object.defineProperty(exports, "createEdgeRef", {
757
+ enumerable: true,
758
+ get: function() {
759
+ return createEdgeRef;
760
+ }
761
+ });
762
+ Object.defineProperty(exports, "createRef", {
763
+ enumerable: true,
764
+ get: function() {
765
+ return createRef;
766
+ }
767
+ });
768
+ Object.defineProperty(exports, "createVertexRef", {
769
+ enumerable: true,
770
+ get: function() {
771
+ return createVertexRef;
772
+ }
773
+ });
774
+ Object.defineProperty(exports, "defaultScorer", {
775
+ enumerable: true,
776
+ get: function() {
777
+ return defaultScorer;
778
+ }
779
+ });
780
+ Object.defineProperty(exports, "isDerivedFaceRef", {
781
+ enumerable: true,
782
+ get: function() {
783
+ return isDerivedFaceRef;
784
+ }
785
+ });
786
+ Object.defineProperty(exports, "isEdgeRef", {
787
+ enumerable: true,
788
+ get: function() {
789
+ return isEdgeRef;
790
+ }
791
+ });
792
+ Object.defineProperty(exports, "isFaceRef", {
793
+ enumerable: true,
794
+ get: function() {
795
+ return isFaceRef;
796
+ }
797
+ });
798
+ Object.defineProperty(exports, "isLineageRef", {
799
+ enumerable: true,
800
+ get: function() {
801
+ return isLineageRef;
802
+ }
803
+ });
804
+ Object.defineProperty(exports, "isVertexRef", {
805
+ enumerable: true,
806
+ get: function() {
807
+ return isVertexRef;
808
+ }
809
+ });
810
+ Object.defineProperty(exports, "resolveDerivedFaceRef", {
811
+ enumerable: true,
812
+ get: function() {
813
+ return resolveDerivedFaceRef;
814
+ }
815
+ });
816
+ Object.defineProperty(exports, "resolveEdgeRef", {
817
+ enumerable: true,
818
+ get: function() {
819
+ return resolveEdgeRef;
820
+ }
821
+ });
822
+ Object.defineProperty(exports, "resolveLineageRef", {
823
+ enumerable: true,
824
+ get: function() {
825
+ return resolveLineageRef;
826
+ }
827
+ });
828
+ Object.defineProperty(exports, "resolveRef", {
829
+ enumerable: true,
830
+ get: function() {
831
+ return resolveRef;
832
+ }
833
+ });
834
+ Object.defineProperty(exports, "resolveRefIn", {
835
+ enumerable: true,
836
+ get: function() {
837
+ return resolveRefIn;
838
+ }
839
+ });
840
+ Object.defineProperty(exports, "resolveRefParams", {
841
+ enumerable: true,
842
+ get: function() {
843
+ return resolveRefParams;
844
+ }
845
+ });
846
+ Object.defineProperty(exports, "resolveVertexRef", {
847
+ enumerable: true,
848
+ get: function() {
849
+ return resolveVertexRef;
850
+ }
851
+ });
852
+ Object.defineProperty(exports, "updateRoles", {
853
+ enumerable: true,
854
+ get: function() {
855
+ return updateRoles;
856
+ }
857
+ });