brepjs 18.109.0 → 18.110.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/brepjs.cjs CHANGED
@@ -29,7 +29,7 @@ const require_importFns = require("./importFns-CgMQIi0z.cjs");
29
29
  const require_loftFns = require("./loftFns-fpTmrIdd.cjs");
30
30
  const require_cameraFns = require("./cameraFns-uo1sZbBT.cjs");
31
31
  const require_textMetrics = require("./textMetrics-IC_IoI7l.cjs");
32
- const require_shapeRefFns = require("./shapeRefFns--2FPCL7P.cjs");
32
+ const require_shapeRefFns = require("./shapeRefFns-CuepnFOm.cjs");
33
33
  const require_workerPool = require("./workerPool-D30NsiIW.cjs");
34
34
  const require_primitiveFns = require("./primitiveFns-DLIWtQWA.cjs");
35
35
  //#region src/topology/shapeBooleans.ts
package/dist/brepjs.js CHANGED
@@ -28,7 +28,7 @@ import { a as revolve$1, c as multiSectionSweep, d as twistExtrude, i as extrude
28
28
  import { a as Sketch, c as compoundSketchLoft, d as sketchFace, f as sketchLoft, h as sketchWires, i as Sketches, l as compoundSketchRevolve, m as sketchSweep, n as getFont, o as compoundSketchExtrude, p as sketchRevolve, r as loadFont, s as compoundSketchFace, t as textBlueprints, u as sketchExtrude, v as CompoundSketch } from "./textBlueprints-BnkWAqwS.js";
29
29
  import { a as makeProjectedEdges, i as projectEdges, n as cameraLookAt, r as createCamera, s as isProjectionPlane, t as cameraFromPlane } from "./cameraFns-bk9e0F-T.js";
30
30
  import { n as textMetrics, r as sketchText, t as fontMetrics } from "./textMetrics-COGYk0LE.js";
31
- import { a as updateRoles, i as resolveRef, n as captureHint, o as defaultScorer, r as createRef, t as assignRoles } from "./shapeRefFns-C1DefNzr.js";
31
+ import { a as updateRoles, i as resolveRef, n as captureHint, o as defaultScorer, r as createRef, t as assignRoles } from "./shapeRefFns-ChMnsOAF.js";
32
32
  import { _ as isSuccessResponse, a as createWorkerClient, c as enqueueTask, d as rejectAll, f as isBatchRequest, g as isOperationRequest, h as isInitRequest, i as registerHandler, l as isEmpty$1, m as isErrorResponse, n as createOperationRegistry, o as createTaskQueue, p as isDisposeRequest, r as createWorkerHandler, s as dequeueTask, t as createWorkerPool, u as pendingCount } from "./workerPool-XcYX2UZ0.js";
33
33
  import { C as threePointArc, D as wireLoop, E as wire, S as tangentArc, T as vertex, _ as polygon, a as circle, b as sphere$1, c as cylinder, d as ellipsoid, f as face, g as offsetFace, h as line, i as bsplineApprox, l as ellipse, m as helix, n as bezier, o as compound, p as filledFace, r as box, s as cone, t as addHoles, u as ellipseArc, v as sewShells, w as torus$1, x as subFace, y as solid } from "./primitiveFns-CBrOyk5S.js";
34
34
  //#region \0rolldown/runtime.js
package/dist/shapeRef.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_shapeRefFns = require("./shapeRefFns--2FPCL7P.cjs");
2
+ const require_shapeRefFns = require("./shapeRefFns-CuepnFOm.cjs");
3
3
  exports.assignRoles = require_shapeRefFns.assignRoles;
4
4
  exports.captureHint = require_shapeRefFns.captureHint;
5
5
  exports.createRef = require_shapeRefFns.createRef;
package/dist/shapeRef.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as updateRoles, i as resolveRef, n as captureHint, o as defaultScorer, r as createRef, t as assignRoles } from "./shapeRefFns-C1DefNzr.js";
1
+ import { a as updateRoles, i as resolveRef, n as captureHint, o as defaultScorer, r as createRef, t as assignRoles } from "./shapeRefFns-ChMnsOAF.js";
2
2
  export { assignRoles, captureHint, createRef, defaultScorer, resolveRef, updateRoles };
@@ -1,4 +1,3 @@
1
- import { n as wasmIndex } from "./vec3-Dpha8d5k.js";
2
1
  import { c as getFaces } from "./topologyQueryFns-BeKaWpNm.js";
3
2
  import { i as faceGeomType, l as normalAt, r as faceCenter } from "./faceFns-B86WGqDg.js";
4
3
  import { n as getHashCode } from "./shapeFns-DrZOVwHX.js";
@@ -81,7 +80,8 @@ function boxRoleFromNormal(n) {
81
80
  *
82
81
  * For other types: sequential naming ('opType:face_0', 'opType:face_1', ...).
83
82
  *
84
- * @returns Map from role name to face hash code
83
+ * @returns Map from role name to its face hash codes (one at assignment time;
84
+ * a role accrues more hashes only later, when `updateRoles` tracks a split).
85
85
  */
86
86
  function assignRoles(shape, operationType) {
87
87
  const faces = getFaces(shape);
@@ -89,13 +89,13 @@ function assignRoles(shape, operationType) {
89
89
  if (operationType === "box") {
90
90
  for (const face of faces) {
91
91
  const role = boxRoleFromNormal(normalAt(face));
92
- if (role !== void 0 && !roles.has(role)) roles.set(role, getHashCode(face));
92
+ if (role !== void 0 && !roles.has(role)) roles.set(role, [getHashCode(face)]);
93
93
  }
94
94
  return roles;
95
95
  }
96
96
  let index = 0;
97
97
  for (const face of faces) {
98
- roles.set(`${operationType}:face_${index}`, getHashCode(face));
98
+ roles.set(`${operationType}:face_${index}`, [getHashCode(face)]);
99
99
  index++;
100
100
  }
101
101
  return roles;
@@ -109,31 +109,47 @@ function createRef(origin, role, face) {
109
109
  };
110
110
  }
111
111
  /**
112
+ * Advance a role's face hashes through one evolution: drop deleted faces,
113
+ * replace a modified face with *all* its successors (a 1→many split keeps every
114
+ * fragment), and keep unchanged faces. Deduped so a shared successor isn't
115
+ * doubled.
116
+ */
117
+ function nextHashes(hashes, evolution) {
118
+ const successors = [];
119
+ for (const hash of hashes) {
120
+ if (evolution.deleted.has(hash)) continue;
121
+ const modified = evolution.modified.get(hash);
122
+ const targets = modified && modified.length > 0 ? modified : [hash];
123
+ for (const h of targets) if (!successors.includes(h)) successors.push(h);
124
+ }
125
+ return successors;
126
+ }
127
+ /**
112
128
  * Propagate a role table through a ShapeEvolution record.
113
129
  * Returns a new RoleTable with hashes updated according to the evolution.
114
130
  *
115
- * - Deleted faces: role removed
116
- * - Modified faces: hash updated to first result hash
117
- * - Unchanged faces: hash preserved
131
+ * - Deleted faces: hash dropped (role removed once all its hashes are gone).
132
+ * - Modified faces: hash replaced by **all** successor hashes — so a 1→many
133
+ * split keeps every fragment, and `resolveRef` disambiguates among them.
134
+ * - Unchanged faces: hash preserved.
118
135
  *
119
- * **Limitation:** When a face splits (1→many in `evolution.modified`), only the
120
- * first successor hash is tracked. The geometric fallback in `resolveRef` handles
121
- * cases where this picks the "wrong" successor. A future version may return
122
- * multi-hash mappings for split-aware resolution.
136
+ * Note: `evolution.generated` is intentionally not consumed here on the OCCT
137
+ * kernels its hashes refer to an intermediate shape, not the final result, so
138
+ * naming generated faces produces roles that never resolve (verified: 0 live
139
+ * generated hashes across cut/fuse on occt-wasm). Stable names for generated
140
+ * geometry (fillet rounds, boolean seams) need history-fidelity work tracked
141
+ * separately.
123
142
  */
124
143
  function updateRoles(roles, origin, evolution) {
125
144
  const originRoles = roles.get(origin);
126
145
  if (!originRoles) return roles;
127
146
  const updatedOriginRoles = /* @__PURE__ */ new Map();
128
- for (const [role, hash] of originRoles) {
129
- if (evolution.deleted.has(hash)) continue;
130
- const modifiedHashes = evolution.modified.get(hash);
131
- if (modifiedHashes && modifiedHashes.length > 0) updatedOriginRoles.set(role, wasmIndex(modifiedHashes, 0));
132
- else updatedOriginRoles.set(role, hash);
147
+ for (const [role, hashes] of originRoles) {
148
+ const successors = nextHashes(hashes, evolution);
149
+ if (successors.length > 0) updatedOriginRoles.set(role, successors);
133
150
  }
134
151
  const newRoles = /* @__PURE__ */ new Map();
135
- for (const [key, value] of roles) if (key === origin) newRoles.set(key, updatedOriginRoles);
136
- else newRoles.set(key, value);
152
+ for (const [key, value] of roles) newRoles.set(key, key === origin ? updatedOriginRoles : value);
137
153
  return newRoles;
138
154
  }
139
155
  /** Ambiguity threshold: if two scores are within this range, it's ambiguous. */
@@ -141,34 +157,19 @@ var AMBIGUITY_THRESHOLD = .1;
141
157
  /** Minimum score for geometric fallback to accept a match. */
142
158
  var MIN_SCORE = .5;
143
159
  /**
144
- * Resolve a ShapeRef to a face in the current shape.
145
- *
146
- * Resolution strategy:
147
- * 1. Exact lookup via role table hash match
148
- * 2. Geometric fallback using scorer against all faces
149
- * 3. Ambiguous if multiple faces score within threshold
150
- * 4. Not-found if no match above minimum score
160
+ * Score `candidates` against `hint`, returning the best match, a tie within
161
+ * {@link AMBIGUITY_THRESHOLD}, or nothing above {@link MIN_SCORE}. Scoping the
162
+ * candidates to a role's tracked successors (rather than every face) is what
163
+ * makes split-face disambiguation reliable the fragments compete only with
164
+ * each other, not with unrelated geometry.
151
165
  */
152
- function resolveRef(ref, roles, currentShape, scorer) {
153
- const faces = getFaces(currentShape);
154
- const scoreFn = scorer ?? defaultScorer;
155
- const targetHash = roles.get(ref.origin)?.get(ref.role);
156
- if (targetHash !== void 0) {
157
- for (const face of faces) if (getHashCode(face) === targetHash) return {
158
- face,
159
- confidence: "exact"
160
- };
161
- return {
162
- ref,
163
- reason: "deleted"
164
- };
165
- }
166
+ function scoreFaces(hint, candidates, scoreFn) {
166
167
  let bestScore = -Infinity;
167
168
  let bestFace;
168
169
  let secondBestScore = -Infinity;
169
170
  const scored = [];
170
- for (const face of faces) {
171
- const score = scoreFn(ref.hint, face);
171
+ for (const face of candidates) {
172
+ const score = scoreFn(hint, face);
172
173
  if (score > MIN_SCORE) scored.push([face, score]);
173
174
  if (score > bestScore) {
174
175
  secondBestScore = bestScore;
@@ -178,15 +179,66 @@ function resolveRef(ref, roles, currentShape, scorer) {
178
179
  }
179
180
  if (bestFace !== void 0 && bestScore > MIN_SCORE) {
180
181
  if (bestScore - secondBestScore < AMBIGUITY_THRESHOLD && scored.length > 1) return {
181
- ref,
182
- reason: "ambiguous",
182
+ kind: "ambiguous",
183
183
  candidates: scored.filter(([, s]) => s >= bestScore - AMBIGUITY_THRESHOLD).map(([f]) => f)
184
184
  };
185
185
  return {
186
- face: bestFace,
187
- confidence: "geometric-fallback"
186
+ kind: "match",
187
+ face: bestFace
188
+ };
189
+ }
190
+ return { kind: "none" };
191
+ }
192
+ /**
193
+ * Resolve a ShapeRef to a face in the current shape.
194
+ *
195
+ * Resolution strategy:
196
+ * 1. Exact: the role's tracked successor hashes. One survivor → exact match;
197
+ * several survivors (a face that split) → disambiguate among *only those*
198
+ * fragments; none survive → deleted.
199
+ * 2. Geometric fallback over the whole shape when the role isn't tracked (or a
200
+ * scoped score turned up nothing): best-scoring face, else ambiguous /
201
+ * not-found.
202
+ */
203
+ function resolveRef(ref, roles, currentShape, scorer) {
204
+ const faces = getFaces(currentShape);
205
+ const scoreFn = scorer ?? defaultScorer;
206
+ const targetHashes = roles.get(ref.origin)?.get(ref.role);
207
+ if (targetHashes !== void 0 && targetHashes.length > 0) {
208
+ const survivors = faces.filter((f) => targetHashes.includes(getHashCode(f)));
209
+ if (survivors.length === 1) {
210
+ const [only] = survivors;
211
+ if (only !== void 0) return {
212
+ face: only,
213
+ confidence: "exact"
214
+ };
215
+ } else if (survivors.length === 0) return {
216
+ ref,
217
+ reason: "deleted"
188
218
  };
219
+ else {
220
+ const outcome = scoreFaces(ref.hint, survivors, scoreFn);
221
+ if (outcome.kind === "match") return {
222
+ face: outcome.face,
223
+ confidence: "geometric-fallback"
224
+ };
225
+ if (outcome.kind === "ambiguous") return {
226
+ ref,
227
+ reason: "ambiguous",
228
+ candidates: outcome.candidates
229
+ };
230
+ }
189
231
  }
232
+ const outcome = scoreFaces(ref.hint, faces, scoreFn);
233
+ if (outcome.kind === "match") return {
234
+ face: outcome.face,
235
+ confidence: "geometric-fallback"
236
+ };
237
+ if (outcome.kind === "ambiguous") return {
238
+ ref,
239
+ reason: "ambiguous",
240
+ candidates: outcome.candidates
241
+ };
190
242
  return {
191
243
  ref,
192
244
  reason: "not-found"
@@ -1,4 +1,3 @@
1
- const require_vec3 = require("./vec3-CFwOI0ZI.cjs");
2
1
  const require_topologyQueryFns = require("./topologyQueryFns-CoXVHuN7.cjs");
3
2
  const require_faceFns = require("./faceFns-BV0GnTlt.cjs");
4
3
  const require_shapeFns = require("./shapeFns-ybo0INGL.cjs");
@@ -81,7 +80,8 @@ function boxRoleFromNormal(n) {
81
80
  *
82
81
  * For other types: sequential naming ('opType:face_0', 'opType:face_1', ...).
83
82
  *
84
- * @returns Map from role name to face hash code
83
+ * @returns Map from role name to its face hash codes (one at assignment time;
84
+ * a role accrues more hashes only later, when `updateRoles` tracks a split).
85
85
  */
86
86
  function assignRoles(shape, operationType) {
87
87
  const faces = require_topologyQueryFns.getFaces(shape);
@@ -89,13 +89,13 @@ function assignRoles(shape, operationType) {
89
89
  if (operationType === "box") {
90
90
  for (const face of faces) {
91
91
  const role = boxRoleFromNormal(require_faceFns.normalAt(face));
92
- if (role !== void 0 && !roles.has(role)) roles.set(role, require_shapeFns.getHashCode(face));
92
+ if (role !== void 0 && !roles.has(role)) roles.set(role, [require_shapeFns.getHashCode(face)]);
93
93
  }
94
94
  return roles;
95
95
  }
96
96
  let index = 0;
97
97
  for (const face of faces) {
98
- roles.set(`${operationType}:face_${index}`, require_shapeFns.getHashCode(face));
98
+ roles.set(`${operationType}:face_${index}`, [require_shapeFns.getHashCode(face)]);
99
99
  index++;
100
100
  }
101
101
  return roles;
@@ -109,31 +109,47 @@ function createRef(origin, role, face) {
109
109
  };
110
110
  }
111
111
  /**
112
+ * Advance a role's face hashes through one evolution: drop deleted faces,
113
+ * replace a modified face with *all* its successors (a 1→many split keeps every
114
+ * fragment), and keep unchanged faces. Deduped so a shared successor isn't
115
+ * doubled.
116
+ */
117
+ function nextHashes(hashes, evolution) {
118
+ const successors = [];
119
+ for (const hash of hashes) {
120
+ if (evolution.deleted.has(hash)) continue;
121
+ const modified = evolution.modified.get(hash);
122
+ const targets = modified && modified.length > 0 ? modified : [hash];
123
+ for (const h of targets) if (!successors.includes(h)) successors.push(h);
124
+ }
125
+ return successors;
126
+ }
127
+ /**
112
128
  * Propagate a role table through a ShapeEvolution record.
113
129
  * Returns a new RoleTable with hashes updated according to the evolution.
114
130
  *
115
- * - Deleted faces: role removed
116
- * - Modified faces: hash updated to first result hash
117
- * - Unchanged faces: hash preserved
131
+ * - Deleted faces: hash dropped (role removed once all its hashes are gone).
132
+ * - Modified faces: hash replaced by **all** successor hashes — so a 1→many
133
+ * split keeps every fragment, and `resolveRef` disambiguates among them.
134
+ * - Unchanged faces: hash preserved.
118
135
  *
119
- * **Limitation:** When a face splits (1→many in `evolution.modified`), only the
120
- * first successor hash is tracked. The geometric fallback in `resolveRef` handles
121
- * cases where this picks the "wrong" successor. A future version may return
122
- * multi-hash mappings for split-aware resolution.
136
+ * Note: `evolution.generated` is intentionally not consumed here on the OCCT
137
+ * kernels its hashes refer to an intermediate shape, not the final result, so
138
+ * naming generated faces produces roles that never resolve (verified: 0 live
139
+ * generated hashes across cut/fuse on occt-wasm). Stable names for generated
140
+ * geometry (fillet rounds, boolean seams) need history-fidelity work tracked
141
+ * separately.
123
142
  */
124
143
  function updateRoles(roles, origin, evolution) {
125
144
  const originRoles = roles.get(origin);
126
145
  if (!originRoles) return roles;
127
146
  const updatedOriginRoles = /* @__PURE__ */ new Map();
128
- for (const [role, hash] of originRoles) {
129
- if (evolution.deleted.has(hash)) continue;
130
- const modifiedHashes = evolution.modified.get(hash);
131
- if (modifiedHashes && modifiedHashes.length > 0) updatedOriginRoles.set(role, require_vec3.wasmIndex(modifiedHashes, 0));
132
- else updatedOriginRoles.set(role, hash);
147
+ for (const [role, hashes] of originRoles) {
148
+ const successors = nextHashes(hashes, evolution);
149
+ if (successors.length > 0) updatedOriginRoles.set(role, successors);
133
150
  }
134
151
  const newRoles = /* @__PURE__ */ new Map();
135
- for (const [key, value] of roles) if (key === origin) newRoles.set(key, updatedOriginRoles);
136
- else newRoles.set(key, value);
152
+ for (const [key, value] of roles) newRoles.set(key, key === origin ? updatedOriginRoles : value);
137
153
  return newRoles;
138
154
  }
139
155
  /** Ambiguity threshold: if two scores are within this range, it's ambiguous. */
@@ -141,34 +157,19 @@ var AMBIGUITY_THRESHOLD = .1;
141
157
  /** Minimum score for geometric fallback to accept a match. */
142
158
  var MIN_SCORE = .5;
143
159
  /**
144
- * Resolve a ShapeRef to a face in the current shape.
145
- *
146
- * Resolution strategy:
147
- * 1. Exact lookup via role table hash match
148
- * 2. Geometric fallback using scorer against all faces
149
- * 3. Ambiguous if multiple faces score within threshold
150
- * 4. Not-found if no match above minimum score
160
+ * Score `candidates` against `hint`, returning the best match, a tie within
161
+ * {@link AMBIGUITY_THRESHOLD}, or nothing above {@link MIN_SCORE}. Scoping the
162
+ * candidates to a role's tracked successors (rather than every face) is what
163
+ * makes split-face disambiguation reliable the fragments compete only with
164
+ * each other, not with unrelated geometry.
151
165
  */
152
- function resolveRef(ref, roles, currentShape, scorer) {
153
- const faces = require_topologyQueryFns.getFaces(currentShape);
154
- const scoreFn = scorer ?? defaultScorer;
155
- const targetHash = roles.get(ref.origin)?.get(ref.role);
156
- if (targetHash !== void 0) {
157
- for (const face of faces) if (require_shapeFns.getHashCode(face) === targetHash) return {
158
- face,
159
- confidence: "exact"
160
- };
161
- return {
162
- ref,
163
- reason: "deleted"
164
- };
165
- }
166
+ function scoreFaces(hint, candidates, scoreFn) {
166
167
  let bestScore = -Infinity;
167
168
  let bestFace;
168
169
  let secondBestScore = -Infinity;
169
170
  const scored = [];
170
- for (const face of faces) {
171
- const score = scoreFn(ref.hint, face);
171
+ for (const face of candidates) {
172
+ const score = scoreFn(hint, face);
172
173
  if (score > MIN_SCORE) scored.push([face, score]);
173
174
  if (score > bestScore) {
174
175
  secondBestScore = bestScore;
@@ -178,15 +179,66 @@ function resolveRef(ref, roles, currentShape, scorer) {
178
179
  }
179
180
  if (bestFace !== void 0 && bestScore > MIN_SCORE) {
180
181
  if (bestScore - secondBestScore < AMBIGUITY_THRESHOLD && scored.length > 1) return {
181
- ref,
182
- reason: "ambiguous",
182
+ kind: "ambiguous",
183
183
  candidates: scored.filter(([, s]) => s >= bestScore - AMBIGUITY_THRESHOLD).map(([f]) => f)
184
184
  };
185
185
  return {
186
- face: bestFace,
187
- confidence: "geometric-fallback"
186
+ kind: "match",
187
+ face: bestFace
188
+ };
189
+ }
190
+ return { kind: "none" };
191
+ }
192
+ /**
193
+ * Resolve a ShapeRef to a face in the current shape.
194
+ *
195
+ * Resolution strategy:
196
+ * 1. Exact: the role's tracked successor hashes. One survivor → exact match;
197
+ * several survivors (a face that split) → disambiguate among *only those*
198
+ * fragments; none survive → deleted.
199
+ * 2. Geometric fallback over the whole shape when the role isn't tracked (or a
200
+ * scoped score turned up nothing): best-scoring face, else ambiguous /
201
+ * not-found.
202
+ */
203
+ function resolveRef(ref, roles, currentShape, scorer) {
204
+ const faces = require_topologyQueryFns.getFaces(currentShape);
205
+ const scoreFn = scorer ?? defaultScorer;
206
+ const targetHashes = roles.get(ref.origin)?.get(ref.role);
207
+ if (targetHashes !== void 0 && targetHashes.length > 0) {
208
+ const survivors = faces.filter((f) => targetHashes.includes(require_shapeFns.getHashCode(f)));
209
+ if (survivors.length === 1) {
210
+ const [only] = survivors;
211
+ if (only !== void 0) return {
212
+ face: only,
213
+ confidence: "exact"
214
+ };
215
+ } else if (survivors.length === 0) return {
216
+ ref,
217
+ reason: "deleted"
188
218
  };
219
+ else {
220
+ const outcome = scoreFaces(ref.hint, survivors, scoreFn);
221
+ if (outcome.kind === "match") return {
222
+ face: outcome.face,
223
+ confidence: "geometric-fallback"
224
+ };
225
+ if (outcome.kind === "ambiguous") return {
226
+ ref,
227
+ reason: "ambiguous",
228
+ candidates: outcome.candidates
229
+ };
230
+ }
189
231
  }
232
+ const outcome = scoreFaces(ref.hint, faces, scoreFn);
233
+ if (outcome.kind === "match") return {
234
+ face: outcome.face,
235
+ confidence: "geometric-fallback"
236
+ };
237
+ if (outcome.kind === "ambiguous") return {
238
+ ref,
239
+ reason: "ambiguous",
240
+ candidates: outcome.candidates
241
+ };
190
242
  return {
191
243
  ref,
192
244
  reason: "not-found"
@@ -15,32 +15,38 @@ export declare function captureHint(face: Face): GeometricHint;
15
15
  *
16
16
  * For other types: sequential naming ('opType:face_0', 'opType:face_1', ...).
17
17
  *
18
- * @returns Map from role name to face hash code
18
+ * @returns Map from role name to its face hash codes (one at assignment time;
19
+ * a role accrues more hashes only later, when `updateRoles` tracks a split).
19
20
  */
20
- export declare function assignRoles(shape: Shape3D, operationType: string): Map<string, number>;
21
+ export declare function assignRoles(shape: Shape3D, operationType: string): Map<string, number[]>;
21
22
  /** Create a ShapeRef from an origin ID, role name, and face. */
22
23
  export declare function createRef(origin: string, role: string, face: Face): ShapeRef;
23
24
  /**
24
25
  * Propagate a role table through a ShapeEvolution record.
25
26
  * Returns a new RoleTable with hashes updated according to the evolution.
26
27
  *
27
- * - Deleted faces: role removed
28
- * - Modified faces: hash updated to first result hash
29
- * - Unchanged faces: hash preserved
28
+ * - Deleted faces: hash dropped (role removed once all its hashes are gone).
29
+ * - Modified faces: hash replaced by **all** successor hashes — so a 1→many
30
+ * split keeps every fragment, and `resolveRef` disambiguates among them.
31
+ * - Unchanged faces: hash preserved.
30
32
  *
31
- * **Limitation:** When a face splits (1→many in `evolution.modified`), only the
32
- * first successor hash is tracked. The geometric fallback in `resolveRef` handles
33
- * cases where this picks the "wrong" successor. A future version may return
34
- * multi-hash mappings for split-aware resolution.
33
+ * Note: `evolution.generated` is intentionally not consumed here on the OCCT
34
+ * kernels its hashes refer to an intermediate shape, not the final result, so
35
+ * naming generated faces produces roles that never resolve (verified: 0 live
36
+ * generated hashes across cut/fuse on occt-wasm). Stable names for generated
37
+ * geometry (fillet rounds, boolean seams) need history-fidelity work tracked
38
+ * separately.
35
39
  */
36
40
  export declare function updateRoles(roles: RoleTable, origin: string, evolution: ShapeEvolution): RoleTable;
37
41
  /**
38
42
  * Resolve a ShapeRef to a face in the current shape.
39
43
  *
40
44
  * Resolution strategy:
41
- * 1. Exact lookup via role table hash match
42
- * 2. Geometric fallback using scorer against all faces
43
- * 3. Ambiguous if multiple faces score within threshold
44
- * 4. Not-found if no match above minimum score
45
+ * 1. Exact: the role's tracked successor hashes. One survivor → exact match;
46
+ * several survivors (a face that split) disambiguate among *only those*
47
+ * fragments; none survive deleted.
48
+ * 2. Geometric fallback over the whole shape when the role isn't tracked (or a
49
+ * scoped score turned up nothing): best-scoring face, else ambiguous /
50
+ * not-found.
45
51
  */
46
52
  export declare function resolveRef(ref: ShapeRef, roles: RoleTable, currentShape: Shape3D, scorer?: FaceScorer): ResolvedRef | BrokenRef;
@@ -22,10 +22,14 @@ export interface ShapeRef {
22
22
  readonly hint: GeometricHint;
23
23
  }
24
24
  /**
25
- * Immutable table mapping `origin -> role -> faceHash`.
25
+ * Immutable table mapping `origin -> role -> faceHashes`.
26
26
  * Updated through evolution records when the model is rebuilt.
27
+ *
28
+ * A role usually maps to a single hash, but maps to several after its face
29
+ * splits (a 1→many `modified` evolution); resolution then disambiguates among
30
+ * the surviving successors rather than competing against the whole shape.
27
31
  */
28
- export type RoleTable = ReadonlyMap<string, ReadonlyMap<string, number>>;
32
+ export type RoleTable = ReadonlyMap<string, ReadonlyMap<string, readonly number[]>>;
29
33
  /** A successfully resolved face reference. */
30
34
  export interface ResolvedRef {
31
35
  readonly face: Face;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs",
3
- "version": "18.109.0",
3
+ "version": "18.110.0",
4
4
  "description": "Web CAD library with pluggable geometry kernel",
5
5
  "keywords": [
6
6
  "cad",