brepjs 18.109.0 → 18.111.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
@@ -1175,6 +1175,156 @@ function withKernelDir(v, fn) {
1175
1175
  }
1176
1176
  }
1177
1177
  //#endregion
1178
+ //#region src/topology/shapeRef/edgeRefFns.ts
1179
+ /** Midpoint of an edge's endpoint vertices (a closed edge collapses to one). */
1180
+ function endpointMidpoint(verts) {
1181
+ if (verts.length === 0) return void 0;
1182
+ let x = 0;
1183
+ let y = 0;
1184
+ let z = 0;
1185
+ for (const v of verts) {
1186
+ const p = require_topologyQueryFns.vertexPosition(v);
1187
+ x += p[0];
1188
+ y += p[1];
1189
+ z += p[2];
1190
+ }
1191
+ const n = verts.length;
1192
+ return [
1193
+ x / n,
1194
+ y / n,
1195
+ z / n
1196
+ ];
1197
+ }
1198
+ function distance(a, b) {
1199
+ const dx = a[0] - b[0];
1200
+ const dy = a[1] - b[1];
1201
+ const dz = a[2] - b[2];
1202
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
1203
+ }
1204
+ function captureEdgeHint(edge) {
1205
+ const lengthResult = require_measureFns.measureLength(edge);
1206
+ return {
1207
+ entityType: "edge",
1208
+ length: lengthResult.ok ? lengthResult.value : void 0,
1209
+ midpoint: endpointMidpoint(require_healingFns.verticesOfEdge(edge))
1210
+ };
1211
+ }
1212
+ /** Reverse the role table: the role whose tracked hashes include this face. */
1213
+ function roleOfFace(face, origin, roles) {
1214
+ const originRoles = roles.get(origin);
1215
+ if (!originRoles) return void 0;
1216
+ const hash = require_shapeFns.getHashCode(face);
1217
+ for (const [role, hashes] of originRoles) if (hashes.includes(hash)) return role;
1218
+ }
1219
+ /** Current faces a role resolves to (its tracked successors present in `shape`). */
1220
+ function facesForRole(shape, origin, role, roles) {
1221
+ const hashes = roles.get(origin)?.get(role);
1222
+ if (hashes === void 0 || hashes.length === 0) return [];
1223
+ return require_topologyQueryFns.getFaces(shape).filter((f) => hashes.includes(require_shapeFns.getHashCode(f)));
1224
+ }
1225
+ function dedupeEdges(edges) {
1226
+ const seen = /* @__PURE__ */ new Set();
1227
+ const out = [];
1228
+ for (const e of edges) {
1229
+ const h = require_shapeFns.getHashCode(e);
1230
+ if (!seen.has(h)) {
1231
+ seen.add(h);
1232
+ out.push(e);
1233
+ }
1234
+ }
1235
+ return out;
1236
+ }
1237
+ /** Hint scores closer than this are treated as indistinguishable (→ ambiguous). */
1238
+ var HINT_MARGIN = 1e-6;
1239
+ /**
1240
+ * Pick the candidate edge closest to the hint (length + endpoint midpoint).
1241
+ * Returns undefined when it genuinely can't discriminate — the hint carries no
1242
+ * signal, or the two best candidates score within {@link HINT_MARGIN} — so the
1243
+ * caller can report `ambiguous` rather than committing to an arbitrary edge.
1244
+ */
1245
+ function bestByHint(candidates, hint) {
1246
+ if (hint.length === void 0 && hint.midpoint === void 0) return void 0;
1247
+ let best;
1248
+ let bestScore = Infinity;
1249
+ let secondScore = Infinity;
1250
+ for (const edge of candidates) {
1251
+ let score = 0;
1252
+ if (hint.length !== void 0) {
1253
+ const len = require_measureFns.measureLength(edge);
1254
+ if (len.ok) score += Math.abs(len.value - hint.length);
1255
+ }
1256
+ if (hint.midpoint !== void 0) {
1257
+ const mid = endpointMidpoint(require_healingFns.verticesOfEdge(edge));
1258
+ if (mid !== void 0) score += distance(hint.midpoint, mid);
1259
+ }
1260
+ if (score < bestScore) {
1261
+ secondScore = bestScore;
1262
+ bestScore = score;
1263
+ best = edge;
1264
+ } else if (score < secondScore) secondScore = score;
1265
+ }
1266
+ if (best === void 0 || secondScore - bestScore < HINT_MARGIN) return void 0;
1267
+ return best;
1268
+ }
1269
+ /**
1270
+ * Capture a lineage-based reference to `edge`: its two adjacent faces' roles
1271
+ * plus a geometric hint. Returns undefined when the edge doesn't bound two faces
1272
+ * (a boundary/degenerate edge) or when either bounding face has no role yet.
1273
+ */
1274
+ function createEdgeRef(origin, edge, shape, roles) {
1275
+ const [faceA, faceB] = require_healingFns.facesOfEdge(shape, edge);
1276
+ if (faceA === void 0 || faceB === void 0) return void 0;
1277
+ const roleA = roleOfFace(faceA, origin, roles);
1278
+ const roleB = roleOfFace(faceB, origin, roles);
1279
+ if (roleA === void 0 || roleB === void 0) return void 0;
1280
+ return {
1281
+ origin,
1282
+ faceRoles: [roleA, roleB],
1283
+ hint: captureEdgeHint(edge)
1284
+ };
1285
+ }
1286
+ /**
1287
+ * Resolve an EdgeRef in `shape`: resolve its two face-roles to current faces,
1288
+ * then return the edge they share. One shared edge → exact; several (the two
1289
+ * faces meet along more than one edge) → disambiguate by hint; none, or a
1290
+ * missing bounding face → broken.
1291
+ */
1292
+ function resolveEdgeRef(ref, roles, shape) {
1293
+ const [roleA, roleB] = ref.faceRoles;
1294
+ const facesA = facesForRole(shape, ref.origin, roleA, roles);
1295
+ const facesB = facesForRole(shape, ref.origin, roleB, roles);
1296
+ if (facesA.length === 0 || facesB.length === 0) return {
1297
+ ref,
1298
+ reason: "not-found"
1299
+ };
1300
+ const candidates = [];
1301
+ for (const a of facesA) for (const b of facesB) candidates.push(...require_healingFns.sharedEdges(a, b));
1302
+ const unique = dedupeEdges(candidates);
1303
+ if (unique.length === 1) {
1304
+ const [only] = unique;
1305
+ if (only !== void 0) return {
1306
+ edge: only,
1307
+ confidence: "exact"
1308
+ };
1309
+ }
1310
+ if (unique.length > 1) {
1311
+ const best = bestByHint(unique, ref.hint);
1312
+ if (best !== void 0) return {
1313
+ edge: best,
1314
+ confidence: "geometric-fallback"
1315
+ };
1316
+ return {
1317
+ ref,
1318
+ reason: "ambiguous",
1319
+ candidates: unique
1320
+ };
1321
+ }
1322
+ return {
1323
+ ref,
1324
+ reason: "not-found"
1325
+ };
1326
+ }
1327
+ //#endregion
1178
1328
  //#region src/topology/surfaceFns.ts
1179
1329
  /**
1180
1330
  * Surface creation functions — generate faces from height-map grids.
@@ -6880,6 +7030,7 @@ exports.createCompound = require_shapeTypes.createCompound;
6880
7030
  exports.createCompoundBlueprint = require_blueprintFns.createCompoundBlueprint;
6881
7031
  exports.createDistanceQuery = require_measureFns.createDistanceQuery;
6882
7032
  exports.createEdge = require_shapeTypes.createEdge;
7033
+ exports.createEdgeRef = createEdgeRef;
6883
7034
  exports.createFace = require_shapeTypes.createFace;
6884
7035
  exports.createHandle = require_shapeTypes.createHandle;
6885
7036
  exports.createHistory = require_threadFns.createHistory;
@@ -7267,6 +7418,7 @@ exports.resize = require_shapeFns.resize;
7267
7418
  exports.resolve = resolve;
7268
7419
  exports.resolve3D = resolve3D;
7269
7420
  exports.resolveDirection = require_types.resolveDirection;
7421
+ exports.resolveEdgeRef = resolveEdgeRef;
7270
7422
  exports.resolvePlane = require_planeOps.resolvePlane;
7271
7423
  exports.resolveRef = require_shapeRefFns.resolveRef;
7272
7424
  exports.reverseCurve = require_blueprintFns.reverseCurve;
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
@@ -1186,6 +1186,156 @@ function withKernelDir(v, fn) {
1186
1186
  }
1187
1187
  }
1188
1188
  //#endregion
1189
+ //#region src/topology/shapeRef/edgeRefFns.ts
1190
+ /** Midpoint of an edge's endpoint vertices (a closed edge collapses to one). */
1191
+ function endpointMidpoint(verts) {
1192
+ if (verts.length === 0) return void 0;
1193
+ let x = 0;
1194
+ let y = 0;
1195
+ let z = 0;
1196
+ for (const v of verts) {
1197
+ const p = vertexPosition(v);
1198
+ x += p[0];
1199
+ y += p[1];
1200
+ z += p[2];
1201
+ }
1202
+ const n = verts.length;
1203
+ return [
1204
+ x / n,
1205
+ y / n,
1206
+ z / n
1207
+ ];
1208
+ }
1209
+ function distance(a, b) {
1210
+ const dx = a[0] - b[0];
1211
+ const dy = a[1] - b[1];
1212
+ const dz = a[2] - b[2];
1213
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
1214
+ }
1215
+ function captureEdgeHint(edge) {
1216
+ const lengthResult = measureLength(edge);
1217
+ return {
1218
+ entityType: "edge",
1219
+ length: lengthResult.ok ? lengthResult.value : void 0,
1220
+ midpoint: endpointMidpoint(verticesOfEdge(edge))
1221
+ };
1222
+ }
1223
+ /** Reverse the role table: the role whose tracked hashes include this face. */
1224
+ function roleOfFace(face, origin, roles) {
1225
+ const originRoles = roles.get(origin);
1226
+ if (!originRoles) return void 0;
1227
+ const hash = getHashCode(face);
1228
+ for (const [role, hashes] of originRoles) if (hashes.includes(hash)) return role;
1229
+ }
1230
+ /** Current faces a role resolves to (its tracked successors present in `shape`). */
1231
+ function facesForRole(shape, origin, role, roles) {
1232
+ const hashes = roles.get(origin)?.get(role);
1233
+ if (hashes === void 0 || hashes.length === 0) return [];
1234
+ return getFaces(shape).filter((f) => hashes.includes(getHashCode(f)));
1235
+ }
1236
+ function dedupeEdges(edges) {
1237
+ const seen = /* @__PURE__ */ new Set();
1238
+ const out = [];
1239
+ for (const e of edges) {
1240
+ const h = getHashCode(e);
1241
+ if (!seen.has(h)) {
1242
+ seen.add(h);
1243
+ out.push(e);
1244
+ }
1245
+ }
1246
+ return out;
1247
+ }
1248
+ /** Hint scores closer than this are treated as indistinguishable (→ ambiguous). */
1249
+ var HINT_MARGIN = 1e-6;
1250
+ /**
1251
+ * Pick the candidate edge closest to the hint (length + endpoint midpoint).
1252
+ * Returns undefined when it genuinely can't discriminate — the hint carries no
1253
+ * signal, or the two best candidates score within {@link HINT_MARGIN} — so the
1254
+ * caller can report `ambiguous` rather than committing to an arbitrary edge.
1255
+ */
1256
+ function bestByHint(candidates, hint) {
1257
+ if (hint.length === void 0 && hint.midpoint === void 0) return void 0;
1258
+ let best;
1259
+ let bestScore = Infinity;
1260
+ let secondScore = Infinity;
1261
+ for (const edge of candidates) {
1262
+ let score = 0;
1263
+ if (hint.length !== void 0) {
1264
+ const len = measureLength(edge);
1265
+ if (len.ok) score += Math.abs(len.value - hint.length);
1266
+ }
1267
+ if (hint.midpoint !== void 0) {
1268
+ const mid = endpointMidpoint(verticesOfEdge(edge));
1269
+ if (mid !== void 0) score += distance(hint.midpoint, mid);
1270
+ }
1271
+ if (score < bestScore) {
1272
+ secondScore = bestScore;
1273
+ bestScore = score;
1274
+ best = edge;
1275
+ } else if (score < secondScore) secondScore = score;
1276
+ }
1277
+ if (best === void 0 || secondScore - bestScore < HINT_MARGIN) return void 0;
1278
+ return best;
1279
+ }
1280
+ /**
1281
+ * Capture a lineage-based reference to `edge`: its two adjacent faces' roles
1282
+ * plus a geometric hint. Returns undefined when the edge doesn't bound two faces
1283
+ * (a boundary/degenerate edge) or when either bounding face has no role yet.
1284
+ */
1285
+ function createEdgeRef(origin, edge, shape, roles) {
1286
+ const [faceA, faceB] = facesOfEdge(shape, edge);
1287
+ if (faceA === void 0 || faceB === void 0) return void 0;
1288
+ const roleA = roleOfFace(faceA, origin, roles);
1289
+ const roleB = roleOfFace(faceB, origin, roles);
1290
+ if (roleA === void 0 || roleB === void 0) return void 0;
1291
+ return {
1292
+ origin,
1293
+ faceRoles: [roleA, roleB],
1294
+ hint: captureEdgeHint(edge)
1295
+ };
1296
+ }
1297
+ /**
1298
+ * Resolve an EdgeRef in `shape`: resolve its two face-roles to current faces,
1299
+ * then return the edge they share. One shared edge → exact; several (the two
1300
+ * faces meet along more than one edge) → disambiguate by hint; none, or a
1301
+ * missing bounding face → broken.
1302
+ */
1303
+ function resolveEdgeRef(ref, roles, shape) {
1304
+ const [roleA, roleB] = ref.faceRoles;
1305
+ const facesA = facesForRole(shape, ref.origin, roleA, roles);
1306
+ const facesB = facesForRole(shape, ref.origin, roleB, roles);
1307
+ if (facesA.length === 0 || facesB.length === 0) return {
1308
+ ref,
1309
+ reason: "not-found"
1310
+ };
1311
+ const candidates = [];
1312
+ for (const a of facesA) for (const b of facesB) candidates.push(...sharedEdges(a, b));
1313
+ const unique = dedupeEdges(candidates);
1314
+ if (unique.length === 1) {
1315
+ const [only] = unique;
1316
+ if (only !== void 0) return {
1317
+ edge: only,
1318
+ confidence: "exact"
1319
+ };
1320
+ }
1321
+ if (unique.length > 1) {
1322
+ const best = bestByHint(unique, ref.hint);
1323
+ if (best !== void 0) return {
1324
+ edge: best,
1325
+ confidence: "geometric-fallback"
1326
+ };
1327
+ return {
1328
+ ref,
1329
+ reason: "ambiguous",
1330
+ candidates: unique
1331
+ };
1332
+ }
1333
+ return {
1334
+ ref,
1335
+ reason: "not-found"
1336
+ };
1337
+ }
1338
+ //#endregion
1189
1339
  //#region src/topology/surfaceFns.ts
1190
1340
  /**
1191
1341
  * Surface creation functions — generate faces from height-map grids.
@@ -6786,4 +6936,4 @@ var csg_exports = /* @__PURE__ */ __exportAll({
6786
6936
  withEvaluator: () => withEvaluator
6787
6937
  });
6788
6938
  //#endregion
6789
- export { BaseSketcher2d, BlueprintSketcher, BrepBugError, BrepErrorCode, BrepWrapperError, BrepkitAdapter, CompoundSketch, DEFAULT_CAPABILITIES, DEG2RAD, DisposalScope, EXACT_BREP_CAPABILITIES, FaceSketcher, HASH_CODE_MAX, OK, OcctWasmAdapter, RAD2DEG, Sketch, Sketcher, Sketches, addChild, addHoles, addJoint, addMate, addStep, adjacentFaces, all, andThen, applyGlue, applyMatrix, approximateCurve, as2D, as3D, asTopo, assignRoles, autoHeal, bezier, blueprintToDXF, booleanPipeline, booleans_exports as booleans, boss, box, bsplineApprox, bug, cameraFromPlane, cameraLookAt, captureHint, cast, castShape, castShape3D, chamfer, chamferDistAngle as chamferDistAngleShape, chamferWithEvolution, checkAllInterferences, checkBoolean, checkInterference, circle, circularPattern, classifyPointOnFace, clearMeshCache, clone, closedWire, collect, collectShapes, colorFaces, colorShape, complexExtrude, composeTransforms, compound, compoundSketchExtrude, compoundSketchFace, compoundSketchLoft, compoundSketchRevolve, computationError, computeStraightSkeleton, cone, construction_exports as construction, convexHull, cornerFinder, countNodes, createAssembly, createAssemblyNode, createBlueprint, createCamera, createCompound, createCompoundBlueprint, createDistanceQuery, createEdge, createFace, createHandle, createHistory, createKernelHandle, createMeshCache, createNamedPlane, createOperationRegistry, createPlane, createRef, createRegistry, createShell, createSolid, createTaskQueue, createVertex, createWire, createWorkerClient, createWorkerHandler, createWorkerPool, csg_exports as csg, currentQuality, curve2dBoundingBox, curve2dDistanceFrom, curve2dFirstPoint, curve2dIsOnCurve, curve2dLastPoint, curve2dParameter, curve2dSplitAt, curve2dTangentAt, curveAxis, curveEndPoint, curveIsClosed, curveIsPeriodic, curveLength, curvePeriod, curvePointAt, curveStartPoint, curveTangentAt, cut, cut2D, cutAll, cutAllBisect, cutBlueprints, cutWithEvolution, cylinder, cylindricalJoint, defaultScorer, dequeueTask, describe, deserializeDrawing, deserializeHistory, fromBREP as deserializeShape, downcast, draft, draw, drawCircle, drawEllipse, drawFaceOutline, drawParametricFunction, drawPointsInterpolation, drawPolysides, drawProjection, drawRectangle, drawRoundedRectangle, drawSingleCircle, drawSingleEllipse, drawText, drawingChamfer, drawingCut, drawingFillet, drawingFuse, drawingIntersect, drawingToSketchOnPlane, drill, edgeFinder, edgesOfFace, ellipse, ellipseArc, ellipsoid, enqueueTask, err, exportAssemblySTEP, exportDXF, exportGlb, exportGltf, exportIGES, exportOBJ, exportSTEP, exportSTEPConfigured, exportSTL, exportThreeMF, exportURDF, extrude, extrudeAll, face, faceAxis, faceCenter, faceFinder, faceGeomType, faceOrientation, facesOfEdge, fieldBoolean, fieldContour, fieldOffset, fieldReinit, fieldShell, fill, filledFace, fillet, filletWithEvolution, findFacesByTag, findNode, findStep, fixSelfIntersection, fixShape, flatMap, flatten, flipFaceOrientation, flipOrientation, fontMetrics, forwardKinematics, fromBREP$1 as fromBREP, fromKernelDir, fromKernelPnt, fromKernelVec, fromNullable, fuse, fuse2D, fuseAll, fuseAllBisect, fuseBlueprints, fuseWithEvolution, gearGeometry, getActiveVoxelId, getBounds, getBounds2D, getCompSolids, getCurveType, getDisposalStats, getEdges, getFaceColor, getFaceOrigins, getFaceTags, getFaces, getFont, getHashCode, getShape as getHistoryShape, getKernel, getKernelCapabilities, getKernelTier, getNurbsCurveData, getNurbsSurfaceData, getOrientation, getOrientation2D, getPerformanceStats, getShapeColor, getShapeKind, getShells, getSingleFace, getSolids, getSurfaceType, getTagMetadata, getVertices, getVoxel, getWires, gridPattern, guidedSweep, heal, healFace, healSolid, healWire, helix, hull, importDXF, importGLB, importIGES, importOBJ, importSTEP, importSTL, importSVG, importSVGPathD, importThreeMF, importURDF, init, initFromManifold, initFromOC, initVoxel, innerWires, instance, instanceCount, instanceGrid, instancedMesh, interpolateCurve, intersect, intersect2D, intersectBlueprints, intersectWithEvolution, invalidateShapeCache, inverseKinematics, ioNs_exports as io, ioError, is2D, is3D, isBatchRequest, isChamferRadius, isClosedWire, isCompSolid, isCompound, isDisposeRequest, isEdge, isEmpty, isEqualShape, isErr, isErrorResponse, isFace, isFilletRadius, isInitRequest, isInside2D, isInstanced, isLive, isManifoldShell, isNumber, isOk, isOperationRequest, isOrientedFace, isPlanarFace, isPlanarWire, isProjectionPlane, isEmpty$1 as isQueueEmpty, isSameShape, isShape1D, isShape3D, isShell, isSolid, isSuccessResponse, isValid, isValidSolid, isVertex, isWire, iterCompSolids, iterEdges, iterFaces, iterShells, iterSolids, iterTopo, iterVertices, iterWires, jointTrajectory, jointTransform, jointsFromDH, kernelCall, kernelCallRaw, kernelCallScoped, kernelError, latticeInfill, latticeInfillShape, line, linearPattern, loadFont, loft, loftAll, makeBaseBox, makeExternalGear, makeInternalGear, makePlane, makePlanetaryGear, makeProjectedEdges, manifoldShell, map, mapBoth, mapErr, match, materialize, measureArea, measureCurvatureAt, measureCurvatureAtMid, measureDistance, measureDistanceProps, measureLength, measureLinearProps, measureSurfaceProps, measureVolume, measureVolumeProps, measurement_exports as measurement, mechanismDOF, mesh, meshEdges, meshLODs, meshLODsProgressive, meshMultiLOD, minkowski, mirror, mirror2D, mirrorDrawing, mirrorJoin, modifiers_exports as modifiers, modifyStep, moduleInitError, multiSectionSweep, normalAt, offset, offsetFace, offsetMesh, offsetShape, offsetWire2D, ok, or, orElse, organiseBlueprints, orientedFace, outerWire, patterns_exports as patterns, pendingCount, pipeline, pivotPlane, planarFace, planarJoint, planarWire, planetPlacements, pocket, pointOnSurface, pointsInside, polygon, polyhedron, polysideInnerRadius, polysidesBlueprint, positionOnCurve, prewarm, primitives_exports as primitives, prismaticJoint, projectEdges, projectPointOnFace, query_exports as query, queryError, rectangularPattern, registerHandler, registerKernel, registerKernelTier, registerOperation, registerShape, registerVoxel, rejectAll, removeChild, removeHolesFromFace, repairMesh, replayFrom, replayHistory, resetDisposalStats, resetPerformanceStats, resize, resolve, resolve3D, resolveDirection, resolvePlane, resolveRef, reverseCurve, revoluteJoint, revolve, roof, rotate, rotate2D, rotateDrawing, roundedRectangleBlueprint, scale, scale2D, scaleDrawing, box$1 as sdfBox, capsule as sdfCapsule, cone$1 as sdfCone, cylinder$1 as sdfCylinder, fieldAxialRamp as sdfFieldAxialRamp, fieldClamp as sdfFieldClamp, fieldConst as sdfFieldConst, fieldFromSdf as sdfFieldFromSdf, fieldRadialRamp as sdfFieldRadialRamp, lattice as sdfLattice, plane as sdfPlane, roundedBox as sdfRoundedBox, sphere as sdfSphere, strutLattice as sdfStrutLattice, sweep as sdfSweep, torus as sdfTorus, section, sectionToFace, serializeHistory, setJointValue, setJointValues, setShapeOrigin, setTagMetadata, sewShells, shape, shapeToMeshInput, shapeType, sharedEdges, shell, shellMesh, shellShape, shellWithEvolution, simplify, sketchCircle, sketchEllipse, sketchExtrude, sketchFace, sketchFaceOffset, sketchHelix, sketchLoft, sketchOnFace2D, sketchOnPlane2D, sketchParametricFunction, sketchPolysides, sketchRectangle, sketchRevolve, sketchRoundedRectangle, sketchSweep, sketchText, sketchWires, sketcherStateError, slice, solid, solidFromShell, solveAssembly, sphere$1 as sphere, sphericalJoint, split, stepCount, stepsFrom, stretch2D, subFace, supportExtrude, supportsConstraintSketch, supportsProjection, surfaceFromGrid, surfaceFromImage, sweep$1 as sweep, tagFaces, tangentArc, tap, tapErr, textBlueprints, textMetrics, thicken, thread, threePointArc, toBREP, toBufferGeometryData, toGroupedBufferGeometryData, toKernelVec, toLODGeometryData, toLODGeometryLevels, toLineGeometryData, toSVGPathD, toVec2, toVec3, torus$1 as torus, tpmsLattice, transformCopy, transforms_exports as transforms, translate, translate2D, translateDrawing, translatePlane, tryCatch, tryCatchAsync, twistExtrude, typeCastError, undoLast, unsupportedError, unwrap, unwrapErr, unwrapOr, unwrapOrElse, updateNode, updateRoles, uvBounds, uvCoordinates, validSolid, validatePlanetary, validationError, variableFillet, vecAdd, vecAngle, vecCross, vecDistance, vecDot, vecEquals, vecIsZero, vecLength, vecLengthSq, vecNegate, vecNormalize, vecProjectToPlane, vecRepr, vecRotate, vecScale, vecSub, vertex, vertexFinder, vertexPosition, verticesOfEdge, voxelBoolean, voxelBooleanField, voxelBooleanFieldShapes, voxelBooleanShapes, voxelField, voxelFieldFromShape, walkAssembly, windingNumbers, wire, wireFinder, wireLoop, wiresOfFace, withKernel, withKernelDir, withKernelPnt, withKernelVec, withQuality, withScope, withScopeResult, withScopeResultAsync, withTier, zip as zipResults };
6939
+ export { BaseSketcher2d, BlueprintSketcher, BrepBugError, BrepErrorCode, BrepWrapperError, BrepkitAdapter, CompoundSketch, DEFAULT_CAPABILITIES, DEG2RAD, DisposalScope, EXACT_BREP_CAPABILITIES, FaceSketcher, HASH_CODE_MAX, OK, OcctWasmAdapter, RAD2DEG, Sketch, Sketcher, Sketches, addChild, addHoles, addJoint, addMate, addStep, adjacentFaces, all, andThen, applyGlue, applyMatrix, approximateCurve, as2D, as3D, asTopo, assignRoles, autoHeal, bezier, blueprintToDXF, booleanPipeline, booleans_exports as booleans, boss, box, bsplineApprox, bug, cameraFromPlane, cameraLookAt, captureHint, cast, castShape, castShape3D, chamfer, chamferDistAngle as chamferDistAngleShape, chamferWithEvolution, checkAllInterferences, checkBoolean, checkInterference, circle, circularPattern, classifyPointOnFace, clearMeshCache, clone, closedWire, collect, collectShapes, colorFaces, colorShape, complexExtrude, composeTransforms, compound, compoundSketchExtrude, compoundSketchFace, compoundSketchLoft, compoundSketchRevolve, computationError, computeStraightSkeleton, cone, construction_exports as construction, convexHull, cornerFinder, countNodes, createAssembly, createAssemblyNode, createBlueprint, createCamera, createCompound, createCompoundBlueprint, createDistanceQuery, createEdge, createEdgeRef, createFace, createHandle, createHistory, createKernelHandle, createMeshCache, createNamedPlane, createOperationRegistry, createPlane, createRef, createRegistry, createShell, createSolid, createTaskQueue, createVertex, createWire, createWorkerClient, createWorkerHandler, createWorkerPool, csg_exports as csg, currentQuality, curve2dBoundingBox, curve2dDistanceFrom, curve2dFirstPoint, curve2dIsOnCurve, curve2dLastPoint, curve2dParameter, curve2dSplitAt, curve2dTangentAt, curveAxis, curveEndPoint, curveIsClosed, curveIsPeriodic, curveLength, curvePeriod, curvePointAt, curveStartPoint, curveTangentAt, cut, cut2D, cutAll, cutAllBisect, cutBlueprints, cutWithEvolution, cylinder, cylindricalJoint, defaultScorer, dequeueTask, describe, deserializeDrawing, deserializeHistory, fromBREP as deserializeShape, downcast, draft, draw, drawCircle, drawEllipse, drawFaceOutline, drawParametricFunction, drawPointsInterpolation, drawPolysides, drawProjection, drawRectangle, drawRoundedRectangle, drawSingleCircle, drawSingleEllipse, drawText, drawingChamfer, drawingCut, drawingFillet, drawingFuse, drawingIntersect, drawingToSketchOnPlane, drill, edgeFinder, edgesOfFace, ellipse, ellipseArc, ellipsoid, enqueueTask, err, exportAssemblySTEP, exportDXF, exportGlb, exportGltf, exportIGES, exportOBJ, exportSTEP, exportSTEPConfigured, exportSTL, exportThreeMF, exportURDF, extrude, extrudeAll, face, faceAxis, faceCenter, faceFinder, faceGeomType, faceOrientation, facesOfEdge, fieldBoolean, fieldContour, fieldOffset, fieldReinit, fieldShell, fill, filledFace, fillet, filletWithEvolution, findFacesByTag, findNode, findStep, fixSelfIntersection, fixShape, flatMap, flatten, flipFaceOrientation, flipOrientation, fontMetrics, forwardKinematics, fromBREP$1 as fromBREP, fromKernelDir, fromKernelPnt, fromKernelVec, fromNullable, fuse, fuse2D, fuseAll, fuseAllBisect, fuseBlueprints, fuseWithEvolution, gearGeometry, getActiveVoxelId, getBounds, getBounds2D, getCompSolids, getCurveType, getDisposalStats, getEdges, getFaceColor, getFaceOrigins, getFaceTags, getFaces, getFont, getHashCode, getShape as getHistoryShape, getKernel, getKernelCapabilities, getKernelTier, getNurbsCurveData, getNurbsSurfaceData, getOrientation, getOrientation2D, getPerformanceStats, getShapeColor, getShapeKind, getShells, getSingleFace, getSolids, getSurfaceType, getTagMetadata, getVertices, getVoxel, getWires, gridPattern, guidedSweep, heal, healFace, healSolid, healWire, helix, hull, importDXF, importGLB, importIGES, importOBJ, importSTEP, importSTL, importSVG, importSVGPathD, importThreeMF, importURDF, init, initFromManifold, initFromOC, initVoxel, innerWires, instance, instanceCount, instanceGrid, instancedMesh, interpolateCurve, intersect, intersect2D, intersectBlueprints, intersectWithEvolution, invalidateShapeCache, inverseKinematics, ioNs_exports as io, ioError, is2D, is3D, isBatchRequest, isChamferRadius, isClosedWire, isCompSolid, isCompound, isDisposeRequest, isEdge, isEmpty, isEqualShape, isErr, isErrorResponse, isFace, isFilletRadius, isInitRequest, isInside2D, isInstanced, isLive, isManifoldShell, isNumber, isOk, isOperationRequest, isOrientedFace, isPlanarFace, isPlanarWire, isProjectionPlane, isEmpty$1 as isQueueEmpty, isSameShape, isShape1D, isShape3D, isShell, isSolid, isSuccessResponse, isValid, isValidSolid, isVertex, isWire, iterCompSolids, iterEdges, iterFaces, iterShells, iterSolids, iterTopo, iterVertices, iterWires, jointTrajectory, jointTransform, jointsFromDH, kernelCall, kernelCallRaw, kernelCallScoped, kernelError, latticeInfill, latticeInfillShape, line, linearPattern, loadFont, loft, loftAll, makeBaseBox, makeExternalGear, makeInternalGear, makePlane, makePlanetaryGear, makeProjectedEdges, manifoldShell, map, mapBoth, mapErr, match, materialize, measureArea, measureCurvatureAt, measureCurvatureAtMid, measureDistance, measureDistanceProps, measureLength, measureLinearProps, measureSurfaceProps, measureVolume, measureVolumeProps, measurement_exports as measurement, mechanismDOF, mesh, meshEdges, meshLODs, meshLODsProgressive, meshMultiLOD, minkowski, mirror, mirror2D, mirrorDrawing, mirrorJoin, modifiers_exports as modifiers, modifyStep, moduleInitError, multiSectionSweep, normalAt, offset, offsetFace, offsetMesh, offsetShape, offsetWire2D, ok, or, orElse, organiseBlueprints, orientedFace, outerWire, patterns_exports as patterns, pendingCount, pipeline, pivotPlane, planarFace, planarJoint, planarWire, planetPlacements, pocket, pointOnSurface, pointsInside, polygon, polyhedron, polysideInnerRadius, polysidesBlueprint, positionOnCurve, prewarm, primitives_exports as primitives, prismaticJoint, projectEdges, projectPointOnFace, query_exports as query, queryError, rectangularPattern, registerHandler, registerKernel, registerKernelTier, registerOperation, registerShape, registerVoxel, rejectAll, removeChild, removeHolesFromFace, repairMesh, replayFrom, replayHistory, resetDisposalStats, resetPerformanceStats, resize, resolve, resolve3D, resolveDirection, resolveEdgeRef, resolvePlane, resolveRef, reverseCurve, revoluteJoint, revolve, roof, rotate, rotate2D, rotateDrawing, roundedRectangleBlueprint, scale, scale2D, scaleDrawing, box$1 as sdfBox, capsule as sdfCapsule, cone$1 as sdfCone, cylinder$1 as sdfCylinder, fieldAxialRamp as sdfFieldAxialRamp, fieldClamp as sdfFieldClamp, fieldConst as sdfFieldConst, fieldFromSdf as sdfFieldFromSdf, fieldRadialRamp as sdfFieldRadialRamp, lattice as sdfLattice, plane as sdfPlane, roundedBox as sdfRoundedBox, sphere as sdfSphere, strutLattice as sdfStrutLattice, sweep as sdfSweep, torus as sdfTorus, section, sectionToFace, serializeHistory, setJointValue, setJointValues, setShapeOrigin, setTagMetadata, sewShells, shape, shapeToMeshInput, shapeType, sharedEdges, shell, shellMesh, shellShape, shellWithEvolution, simplify, sketchCircle, sketchEllipse, sketchExtrude, sketchFace, sketchFaceOffset, sketchHelix, sketchLoft, sketchOnFace2D, sketchOnPlane2D, sketchParametricFunction, sketchPolysides, sketchRectangle, sketchRevolve, sketchRoundedRectangle, sketchSweep, sketchText, sketchWires, sketcherStateError, slice, solid, solidFromShell, solveAssembly, sphere$1 as sphere, sphericalJoint, split, stepCount, stepsFrom, stretch2D, subFace, supportExtrude, supportsConstraintSketch, supportsProjection, surfaceFromGrid, surfaceFromImage, sweep$1 as sweep, tagFaces, tangentArc, tap, tapErr, textBlueprints, textMetrics, thicken, thread, threePointArc, toBREP, toBufferGeometryData, toGroupedBufferGeometryData, toKernelVec, toLODGeometryData, toLODGeometryLevels, toLineGeometryData, toSVGPathD, toVec2, toVec3, torus$1 as torus, tpmsLattice, transformCopy, transforms_exports as transforms, translate, translate2D, translateDrawing, translatePlane, tryCatch, tryCatchAsync, twistExtrude, typeCastError, undoLast, unsupportedError, unwrap, unwrapErr, unwrapOr, unwrapOrElse, updateNode, updateRoles, uvBounds, uvCoordinates, validSolid, validatePlanetary, validationError, variableFillet, vecAdd, vecAngle, vecCross, vecDistance, vecDot, vecEquals, vecIsZero, vecLength, vecLengthSq, vecNegate, vecNormalize, vecProjectToPlane, vecRepr, vecRotate, vecScale, vecSub, vertex, vertexFinder, vertexPosition, verticesOfEdge, voxelBoolean, voxelBooleanField, voxelBooleanFieldShapes, voxelBooleanShapes, voxelField, voxelFieldFromShape, walkAssembly, windingNumbers, wire, wireFinder, wireLoop, wiresOfFace, withKernel, withKernelDir, withKernelPnt, withKernelVec, withQuality, withScope, withScopeResult, withScopeResultAsync, withTier, zip as zipResults };
package/dist/index.d.ts CHANGED
@@ -102,8 +102,8 @@ export { checkBoolean } from './topology/booleanDiagnosticFns.js';
102
102
  export type { CheckBooleanResult, BooleanIssue, BooleanOpType, BooleanDiagnostics, } from './kernel/types.js';
103
103
  export { fuseWithEvolution, cutWithEvolution, intersectWithEvolution, filletWithEvolution, chamferWithEvolution, shellWithEvolution, type EvolutionResult, } from './topology/evolutionFns.js';
104
104
  export type { ShapeEvolution } from './kernel/types.js';
105
- export { captureHint, assignRoles, createRef, updateRoles, resolveRef, defaultScorer, } from './topology/shapeRef/index.js';
106
- export type { GeometricHint, ShapeRef, RoleTable, ResolvedRef, BrokenRef, FaceScorer, } from './topology/shapeRef/index.js';
105
+ export { captureHint, assignRoles, createRef, updateRoles, resolveRef, defaultScorer, createEdgeRef, resolveEdgeRef, } from './topology/shapeRef/index.js';
106
+ export type { GeometricHint, ShapeRef, RoleTable, ResolvedRef, BrokenRef, FaceScorer, EdgeHint, EdgeRef, ResolvedEdgeRef, BrokenEdgeRef, } from './topology/shapeRef/index.js';
107
107
  export { surfaceFromGrid, surfaceFromImage, type SurfaceFromGridOptions, type SurfaceFromImageOptions, } from './topology/surfaceFns.js';
108
108
  export { hull, type HullOptions } from './topology/hullFns.js';
109
109
  export { convexHull } from './operations/convexHullFns.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"
@@ -0,0 +1,15 @@
1
+ import { Edge, Shape3D } from '../../core/shapeTypes.js';
2
+ import { EdgeRef, ResolvedEdgeRef, BrokenEdgeRef, RoleTable } from './shapeRefTypes.js';
3
+ /**
4
+ * Capture a lineage-based reference to `edge`: its two adjacent faces' roles
5
+ * plus a geometric hint. Returns undefined when the edge doesn't bound two faces
6
+ * (a boundary/degenerate edge) or when either bounding face has no role yet.
7
+ */
8
+ export declare function createEdgeRef(origin: string, edge: Edge, shape: Shape3D, roles: RoleTable): EdgeRef | undefined;
9
+ /**
10
+ * Resolve an EdgeRef in `shape`: resolve its two face-roles to current faces,
11
+ * then return the edge they share. One shared edge → exact; several (the two
12
+ * faces meet along more than one edge) → disambiguate by hint; none, or a
13
+ * missing bounding face → broken.
14
+ */
15
+ export declare function resolveEdgeRef(ref: EdgeRef, roles: RoleTable, shape: Shape3D): ResolvedEdgeRef | BrokenEdgeRef;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * ShapeRef — stable, serializable face references for parametric replay.
3
3
  */
4
- export type { GeometricHint, ShapeRef, RoleTable, ResolvedRef, BrokenRef, } from './shapeRefTypes.js';
4
+ export type { GeometricHint, ShapeRef, RoleTable, ResolvedRef, BrokenRef, EdgeHint, EdgeRef, ResolvedEdgeRef, BrokenEdgeRef, } from './shapeRefTypes.js';
5
5
  export { type FaceScorer, defaultScorer } from './scoring.js';
6
6
  export { captureHint, assignRoles, createRef, updateRoles, resolveRef } from './shapeRefFns.js';
7
+ export { createEdgeRef, resolveEdgeRef } from './edgeRefFns.js';
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import { Vec3 } from '../../core/types.js';
2
2
  import { SurfaceType } from '../faceFns.js';
3
- import { Face } from '../../core/shapeTypes.js';
3
+ import { Face, Edge } from '../../core/shapeTypes.js';
4
4
  /** Geometric snapshot of a face, used for fallback matching when hashes change. */
5
5
  export interface GeometricHint {
6
6
  readonly entityType: 'face';
@@ -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;
@@ -37,3 +41,38 @@ export interface BrokenRef {
37
41
  readonly reason: 'deleted' | 'ambiguous' | 'not-found';
38
42
  readonly candidates?: readonly Face[];
39
43
  }
44
+ /**
45
+ * Geometric snapshot of an edge — a tiebreaker for the rare case where an edge's
46
+ * two faces share more than one edge.
47
+ */
48
+ export interface EdgeHint {
49
+ readonly entityType: 'edge';
50
+ readonly length?: number | undefined;
51
+ /** Midpoint of the edge's endpoint vertices. */
52
+ readonly midpoint?: Vec3 | undefined;
53
+ }
54
+ /**
55
+ * A stable reference to an edge, identified by the roles of its two adjacent
56
+ * faces — its lineage. An edge *is* the intersection of its two faces, so this
57
+ * resolves by finding the edge shared by the current faces of those roles
58
+ * (`sharedEdges`). Identity rides on the already-stable face roles rather than
59
+ * the edge's own hash, so it survives edits that re-hash the edge — and it
60
+ * sidesteps the kernel's unreliable `generated`-face hashes entirely.
61
+ */
62
+ export interface EdgeRef {
63
+ readonly origin: string;
64
+ /** Roles of the two faces this edge bounds. */
65
+ readonly faceRoles: readonly [string, string];
66
+ readonly hint: EdgeHint;
67
+ }
68
+ /** A successfully resolved edge reference. */
69
+ export interface ResolvedEdgeRef {
70
+ readonly edge: Edge;
71
+ readonly confidence: 'exact' | 'geometric-fallback';
72
+ }
73
+ /** An edge reference that could not be resolved. */
74
+ export interface BrokenEdgeRef {
75
+ readonly ref: EdgeRef;
76
+ readonly reason: 'ambiguous' | 'not-found';
77
+ readonly candidates?: readonly Edge[];
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs",
3
- "version": "18.109.0",
3
+ "version": "18.111.0",
4
4
  "description": "Web CAD library with pluggable geometry kernel",
5
5
  "keywords": [
6
6
  "cad",
@@ -268,7 +268,7 @@
268
268
  "knip": "6.16.1",
269
269
  "lint-staged": "17.0.7",
270
270
  "lz-string": "1.5.0",
271
- "occt-wasm": "3.4.0",
271
+ "occt-wasm": "3.6.0",
272
272
  "prettier": "3.8.4",
273
273
  "puppeteer": "25.1.0",
274
274
  "size-limit": "12.1.0",
@@ -284,7 +284,7 @@
284
284
  "brepjs-manifold": "^0.1.0",
285
285
  "brepjs-opencascade": "^0.5.1 || ^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0 || ^0.16.0",
286
286
  "brepkit-wasm": "^0.10.1 || ^1.0.0 || ^2.0.0",
287
- "occt-wasm": "^3.4.0"
287
+ "occt-wasm": "^3.6.0"
288
288
  },
289
289
  "peerDependenciesMeta": {
290
290
  "brepjs-manifold": {