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.
- package/dist/adjacencyFns-BYnyXXqt.js +247 -0
- package/dist/adjacencyFns-d8ZabkvX.cjs +294 -0
- package/dist/brepjs.cjs +31 -406
- package/dist/brepjs.js +7 -390
- package/dist/{healingFns-D00TPE9p.js → healingFns-2TJHHDg5.js} +3 -246
- package/dist/{healingFns-x1sHV-HO.cjs → healingFns-xLJ6Cp_O.cjs} +0 -291
- package/dist/index.d.ts +2 -2
- package/dist/refResolveFns-hEOEcFZ8.js +761 -0
- package/dist/refResolveFns-pRVyjy35.cjs +880 -0
- package/dist/shapeRef.cjs +21 -7
- package/dist/shapeRef.d.ts +10 -2
- package/dist/shapeRef.js +2 -2
- package/dist/topology/shapeRef/index.d.ts +1 -0
- package/dist/topology/shapeRef/refResolveFns.d.ts +53 -0
- package/dist/topology/shapeRef/shapeRefFns.d.ts +9 -7
- package/dist/topology.cjs +8 -7
- package/dist/topology.js +2 -1
- package/package.json +1 -1
- package/dist/shapeRefFns-ChMnsOAF.js +0 -248
- package/dist/shapeRefFns-CuepnFOm.cjs +0 -283
|
@@ -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 };
|