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