@faicad/faijs 0.17.0 → 0.18.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.
Files changed (72) hide show
  1. package/dist/api/api-namespace.js +2 -2
  2. package/dist/api/api-namespace.js.map +1 -1
  3. package/dist/api/boolean.d.ts.map +1 -1
  4. package/dist/api/boolean.js +64 -26
  5. package/dist/api/boolean.js.map +1 -1
  6. package/dist/api/brep-mirror/booleanFns.d.ts.map +1 -1
  7. package/dist/api/brep-mirror/booleanFns.js +11 -6
  8. package/dist/api/brep-mirror/booleanFns.js.map +1 -1
  9. package/dist/api/brep-mirror/topologyFns.d.ts.map +1 -1
  10. package/dist/api/brep-mirror/topologyFns.js +4 -1
  11. package/dist/api/brep-mirror/topologyFns.js.map +1 -1
  12. package/dist/api/chamfer.d.ts +12 -8
  13. package/dist/api/chamfer.d.ts.map +1 -1
  14. package/dist/api/chamfer.js +52 -23
  15. package/dist/api/chamfer.js.map +1 -1
  16. package/dist/api/extrude.d.ts.map +1 -1
  17. package/dist/api/extrude.js +17 -2
  18. package/dist/api/extrude.js.map +1 -1
  19. package/dist/api/generated/operations.js +5 -5
  20. package/dist/api/generated/operations.js.map +1 -1
  21. package/dist/api/generated/script-face-manifest.js +12 -12
  22. package/dist/api/generated/script-face-manifest.js.map +1 -1
  23. package/dist/api/generated/sketching.js +1 -1
  24. package/dist/api/generated/sketching.js.map +1 -1
  25. package/dist/api/generated/topology.js +7 -7
  26. package/dist/api/generated/topology.js.map +1 -1
  27. package/dist/api/index.d.ts +2 -1
  28. package/dist/api/index.d.ts.map +1 -1
  29. package/dist/api/index.js +1 -1
  30. package/dist/api/index.js.map +1 -1
  31. package/dist/api/internal/profile-wire.d.ts +3 -3
  32. package/dist/api/internal/profile-wire.js +3 -3
  33. package/dist/api/internal/profile-wire.js.map +1 -1
  34. package/dist/api/loft.d.ts +3 -3
  35. package/dist/api/loft.js +3 -3
  36. package/dist/api/loft.js.map +1 -1
  37. package/dist/api/profile.d.ts +89 -0
  38. package/dist/api/profile.d.ts.map +1 -0
  39. package/dist/api/profile.js +194 -0
  40. package/dist/api/profile.js.map +1 -0
  41. package/dist/api/replicate.d.ts.map +1 -1
  42. package/dist/api/replicate.js +15 -14
  43. package/dist/api/replicate.js.map +1 -1
  44. package/dist/api/surface/arg-spec.d.ts.map +1 -1
  45. package/dist/api/surface/arg-spec.js +20 -15
  46. package/dist/api/surface/arg-spec.js.map +1 -1
  47. package/dist/api/sweep.d.ts +2 -2
  48. package/dist/api/sweep.js +2 -2
  49. package/dist/api/sweep.js.map +1 -1
  50. package/dist/api/thicken.d.ts +3 -3
  51. package/dist/api/thicken.js +3 -3
  52. package/dist/api/thicken.js.map +1 -1
  53. package/dist/brep/engine/adapters/brepkit.d.ts.map +1 -1
  54. package/dist/brep/engine/adapters/brepkit.js +28 -1
  55. package/dist/brep/engine/adapters/brepkit.js.map +1 -1
  56. package/dist/brep/engine/adapters/occt.d.ts.map +1 -1
  57. package/dist/brep/engine/adapters/occt.js +12 -0
  58. package/dist/brep/engine/adapters/occt.js.map +1 -1
  59. package/dist/brep/engine/types.d.ts +1 -1
  60. package/dist/brep/engine/types.d.ts.map +1 -1
  61. package/dist/brep/engine/types.js.map +1 -1
  62. package/dist/brepkit-kernel/brepkitKernel.d.ts.map +1 -1
  63. package/dist/brepkit-kernel/brepkitKernel.js +463 -91
  64. package/dist/brepkit-kernel/brepkitKernel.js.map +1 -1
  65. package/dist/brepkit-kernel/multi-version-test.d.ts +40 -0
  66. package/dist/brepkit-kernel/multi-version-test.d.ts.map +1 -0
  67. package/dist/brepkit-kernel/multi-version-test.js +89 -0
  68. package/dist/brepkit-kernel/multi-version-test.js.map +1 -0
  69. package/dist/lang/symbol-table.generated.d.ts +1 -1
  70. package/dist/lang/symbol-table.generated.js +1 -1
  71. package/dist/lang/symbol-table.generated.js.map +1 -1
  72. package/package.json +1 -1
@@ -102,21 +102,30 @@ function mapEvolution(kernel, raw, resultHandle, inputFaceHashes, hashUpperBound
102
102
  // raw: {solid, evolution:{modified:{oldHandle:[newHandles]}, generated:{oldHandle:[new]}, deleted:[oldHandles]}}
103
103
  const parsed = JSON.parse(raw);
104
104
  const evo = parsed.evolution ?? { modified: {}, generated: {}, deleted: [] };
105
- const modifiedKeys = new Set(Object.keys(evo.modified ?? {}).map(Number));
106
105
  const deletedKeys = new Set((evo.deleted ?? []).map(Number));
107
- // 输入面 hash → 是否被改/删:通过该输入面在输入 hash 快照中的位置无法直接回溯句柄,
108
- // 诚实映射:modified/deleted 用"发生变化的输入句柄数 == 输入面数"的对齐近似——
109
- // v1 精确溯源依赖 per-shape hash 注册表(subShapeHashes 时登记 hash↔handle)。
106
+ // registry 登记的是 hash→handle(输入面)。下游 decodeEvolution/decodeHashEvolution 期望
107
+ // modified 为 OCCT 分段格式 [inHash, count, outHash1, outHash2, ...](见 face-evolution.ts),
108
+ // 不是扁平 hash 列表——GOTCHA:曾误返回扁平列表,把 modified[1](一个 hash ~1e9)当成
109
+ // count,导致 decodeHashEvolution 空转 17 亿次 push 抛 "Invalid array length"(2026-09-26 实证)。
110
+ // 故这里反向建 handle→hash 表,逐条目拼出 [inHash, outCount, ...outHashes]。
110
111
  const registry = hashRegistry.get(kernel);
112
+ const handleToHash = new Map();
113
+ if (registry)
114
+ for (const [h, handle] of registry)
115
+ handleToHash.set(handle, h);
111
116
  const modified = [];
112
117
  const deleted = [];
113
- if (registry) {
114
- for (const [hash, handle] of registry) {
115
- if (modifiedKeys.has(handle))
116
- modified.push(hash);
117
- if (deletedKeys.has(handle))
118
- deleted.push(hash);
119
- }
118
+ for (const oldHandle of deletedKeys) {
119
+ const h = handleToHash.get(oldHandle);
120
+ if (h !== undefined)
121
+ deleted.push(h);
122
+ }
123
+ for (const [oldHandleStr, newHandles] of Object.entries(evo.modified ?? {})) {
124
+ const inHash = handleToHash.get(Number(oldHandleStr));
125
+ if (inHash === undefined)
126
+ continue;
127
+ const outHashes = newHandles.map((nh) => faceFingerprint(kernel, nh, hashUpperBound));
128
+ modified.push(inHash, outHashes.length, ...outHashes);
120
129
  }
121
130
  // generated:结果实体的新面 hash(数量按 generated 表展开)
122
131
  const generated = [];
@@ -187,6 +196,41 @@ export async function createBrepkitPrimitives() {
187
196
  // 返回的面在此登记;extrude/revolveVec 的 face 输入断言、getSubShapes 的
188
197
  // edge/vertex 分发都据此判定(brepkit 无 shape-type 分类 API,见下方注释)。
189
198
  const knownFaces = new Set();
199
+ // GOTCHA(2026-09-26 非实体句柄修复):brepkit 的 tessellateSolidGrouped / getSolidFaces
200
+ // 等 *Solid API 只吃 solid 句柄,传 face/wire/edge/compound 即抛
201
+ // "invalid solid handle: index N out of bounds"。而 brepkit **没有 shape-type 分类 API**
202
+ // (getFaceEdges(solid) 不抛错、会返回某一面的边,不能 try/catch 探测)。故 meshShape
203
+ // 必须靠这组「已知句柄集合」在执行前静态分发,而不是运行时探测:
204
+ // knownFaces → tessellateFace(JsMesh)
205
+ // knownWires → getWireEdges + tessellateEdge 逐边采样成线段
206
+ // knownEdges → tessellateEdge 直接采样成线段(wire.ts smooth 路径 interpolatePoints 返回 edge)
207
+ // knownCompounds→ 逐子句柄按其类型 tessellate 后合并(sectionByPlane 面组 / importStep 多实体)
208
+ // 不在任何集合里的句柄一律按 solid 走 tessellateSolidGrouped(实体创建不入集合,天然落此路径)。
209
+ const knownWires = new Set();
210
+ const knownEdges = new Set();
211
+ // GOTCHA: getSolidFaces returns GLOBAL face indices (0..5, 6..11, ...) that collide
212
+ // with solid handles (per-type namespace). Enumerated sub-faces must NOT pollute
213
+ // knownFaces, else measuring a later solid whose handle number equals an old
214
+ // enumerated face gets mis-tessellated as a face. derivedFaces is consulted ONLY by
215
+ // getSubShapes edge/vertex drilling, never by meshShape/getBoundingBox.
216
+ const derivedFaces = new Set();
217
+ // 虚拟 compound 句柄计数器(非实体子句柄不进内核 makeCompound,见 makeCompound GOTCHA)。
218
+ const knownCompounds = new Map();
219
+ let virtualCompoundCounter = 0;
220
+ // GOTCHA(2026-09-26):brepkit 句柄按**类型分命名空间**——solid/face/wire/edge 各自从 0 计数
221
+ // (实测 edge=wire=face=solid=0 可共存)。因此「某编号 N 在 knownFaces」与「某编号 N 是 solid」
222
+ // 并不互斥:新 solid 恰好复用了早先某 face 的编号时,knownFaces.has(N) 会把 solid 误判成 face
223
+ // (实测 box makeBox 返回 solid 2,撞 profile 早先的 face 2 → cloneShape 走 copyFace 崩溃)。
224
+ // 建 solid 时必须从 knownFaces/knownWires/knownEdges/derivedFaces 抹掉该编号,让默认 solid 路径生效。
225
+ // derivedFaces 也必须清:getSubShapes(solid,'edge') 会查 derivedFaces.has(s) 误走 getFaceEdges。
226
+ const markSolid = (h) => {
227
+ knownFaces.delete(h);
228
+ knownWires.delete(h);
229
+ knownEdges.delete(h);
230
+ knownCompounds.delete(h);
231
+ derivedFaces.delete(h);
232
+ return h;
233
+ };
190
234
  const readFallback = () => {
191
235
  try {
192
236
  return typeof kernel.meshFallbackCount === 'function' ? Number(kernel.meshFallbackCount()) : 0;
@@ -205,6 +249,162 @@ export async function createBrepkitPrimitives() {
205
249
  const v = ((d[2] ?? 0) + (d[3] ?? 1)) / 2;
206
250
  return vec3Of(toNumArray(kernel.evaluateSurface(asNum(face), u, v)));
207
251
  };
252
+ /** 单个 face → BrepMeshResult(tessellateFace)。 */
253
+ const faceMesh = (f, deflection, angular) => {
254
+ const jm = kernel.tessellateFace(f, deflection, angular);
255
+ const positions = arr(jm.positions);
256
+ const normals = arr(jm.normals);
257
+ const indices = arr(jm.indices);
258
+ const triCount = jm.triangleCount > 0 ? jm.triangleCount : indices.length / 3;
259
+ return {
260
+ positions: new Float32Array(positions),
261
+ normals: new Float32Array(normals),
262
+ indices: new Uint32Array(indices),
263
+ vertexCount: positions.length / 3,
264
+ triangleCount: triCount,
265
+ faceGroups: new Int32Array([0, triCount, faceFingerprint(kernel, f, 1e9)]),
266
+ faceCount: 1,
267
+ };
268
+ };
269
+ /** 一组 edge 句柄 → 线段 mesh(positions 为采样点、indices 为端点对)。 */
270
+ // GOTCHA:tessellateEdge(edge, N) 返回扁平 [x,y,z,...] 采样点(NURBS 才采样 N 点,
271
+ // 直线边只回端点),不是三角网格;wire/curve 显示吃「端点对」索引(与 mesh 路径 wireMesh 同构)。
272
+ const edgesToLineMesh = (edgeHandles, deflection) => {
273
+ const positions = [];
274
+ const indices = [];
275
+ // 采样密度:按 deflection 粗采样即可(1D 线框显示,非精确几何)。
276
+ const nPts = Math.max(12, Math.round(4 / Math.max(deflection, 0.01)));
277
+ for (const e of edgeHandles) {
278
+ const pts = arr(kernel.tessellateEdge(e, nPts));
279
+ const base = positions.length / 3;
280
+ for (let i = 0; i < pts.length; i++)
281
+ positions.push(pts[i]);
282
+ const verts = pts.length / 3;
283
+ for (let i = 0; i < verts - 1; i++)
284
+ indices.push(base + i, base + i + 1);
285
+ }
286
+ return {
287
+ positions: new Float32Array(positions),
288
+ normals: new Float32Array(positions.length),
289
+ indices: new Uint32Array(indices),
290
+ vertexCount: positions.length / 3,
291
+ triangleCount: 0,
292
+ faceGroups: new Int32Array(0),
293
+ faceCount: 0,
294
+ };
295
+ };
296
+ /** wire → 线段 mesh(getWireEdges 取边集后逐边采样)。 */
297
+ const wireMesh = (wire, deflection) => {
298
+ const edges = arr(kernel.getWireEdges(wire));
299
+ return edgesToLineMesh(edges, deflection);
300
+ };
301
+ /** solid → BrepMeshResult(tessellateSolidGrouped,原 meshShape 实体路径抽出)。 */
302
+ const solidMesh = (solid, deflection, angular) => {
303
+ const raw = kernel.tessellateSolidGrouped(solid, deflection, angular);
304
+ const m = JSON.parse(raw);
305
+ // faceOffsets 形如 [start0, start1, ..., totalEnd]:以「索引数组单位」计的每面偏移(2026-09-19 实测),
306
+ // 除以 3 换算为 faijs 契约的三角形单位 [triStart, triCount, faceHash]
307
+ const fo = m.faceOffsets;
308
+ const faceCount = Math.max(0, fo.length - 1);
309
+ const faceHashes = [];
310
+ try {
311
+ const faces = arr(kernel.getSolidFaces(solid));
312
+ for (let i = 0; i < faceCount && i < faces.length; i++)
313
+ faceHashes.push(faceFingerprint(kernel, faces[i], 1e9));
314
+ }
315
+ catch { /* 非实体:faceHash 置 0 */ }
316
+ const faceGroups = new Int32Array(faceCount * 3);
317
+ for (let i = 0; i < faceCount; i++) {
318
+ faceGroups[i * 3] = fo[i] / 3;
319
+ faceGroups[i * 3 + 1] = (fo[i + 1] - fo[i]) / 3;
320
+ faceGroups[i * 3 + 2] = faceHashes[i] ?? 0;
321
+ }
322
+ return {
323
+ positions: new Float32Array(m.positions),
324
+ normals: new Float32Array(m.normals),
325
+ indices: new Uint32Array(m.indices),
326
+ vertexCount: m.positions.length / 3,
327
+ triangleCount: m.indices.length / 3,
328
+ faceGroups,
329
+ faceCount,
330
+ };
331
+ };
332
+ /** compound → 逐子句柄 tessellate 后合并(sectionByPlane 面组 / importStep 多实体)。 */
333
+ const compoundMesh = (children, deflection, angular) => {
334
+ const positions = [];
335
+ const normals = [];
336
+ const indices = [];
337
+ const faceGroups = [];
338
+ let triOffset = 0;
339
+ for (const c of children) {
340
+ let sub;
341
+ if (knownFaces.has(c)) {
342
+ sub = faceMesh(c, deflection, angular);
343
+ }
344
+ else if (knownWires.has(c)) {
345
+ sub = wireMesh(c, deflection);
346
+ }
347
+ else if (knownEdges.has(c)) {
348
+ sub = edgesToLineMesh([c], deflection);
349
+ }
350
+ else {
351
+ // 子句柄按 solid 处理(importStep 多实体 / makeCompound(solids))。
352
+ sub = solidMesh(c, deflection, angular);
353
+ }
354
+ const vOffset = positions.length / 3;
355
+ for (let i = 0; i < sub.positions.length; i++)
356
+ positions.push(sub.positions[i]);
357
+ for (let i = 0; i < sub.normals.length; i++)
358
+ normals.push(sub.normals[i] ?? 0);
359
+ for (let i = 0; i < sub.indices.length; i++)
360
+ indices.push(sub.indices[i] + vOffset);
361
+ if (sub.faceGroups) {
362
+ for (let i = 0; i < sub.faceGroups.length; i += 3) {
363
+ faceGroups.push((sub.faceGroups[i] ?? 0) + triOffset, sub.faceGroups[i + 1] ?? 0, sub.faceGroups[i + 2] ?? 0);
364
+ }
365
+ }
366
+ triOffset += sub.triangleCount;
367
+ }
368
+ return {
369
+ positions: new Float32Array(positions),
370
+ normals: new Float32Array(normals),
371
+ indices: new Uint32Array(indices),
372
+ vertexCount: positions.length / 3,
373
+ triangleCount: indices.length / 3,
374
+ faceGroups: new Int32Array(faceGroups),
375
+ faceCount: faceGroups.length / 3,
376
+ };
377
+ };
378
+ // ── 按句柄类型深拷贝(transform/copy 族分发用) ──
379
+ // GOTCHA:brepkit copySolid/copyFace/copyWire 三者入参严格对应 solid/face/wire 句柄,
380
+ // 传错即崩溃;transformSolid/transformFace/transformWire 均为「原地修改」void。
381
+ // place/translate 等会作用在 profile 产出的 face 上(sew 用例 place 两张 face),
382
+ // 故必须按 knownFaces/knownWires 分发,不能一律 copySolid。
383
+ const cloneShape = (shape) => {
384
+ if (knownFaces.has(shape)) {
385
+ const c = kernel.copyFace(shape);
386
+ knownFaces.add(c);
387
+ return c;
388
+ }
389
+ if (knownWires.has(shape)) {
390
+ const c = kernel.copyWire(shape);
391
+ knownWires.add(c);
392
+ return c;
393
+ }
394
+ return markSolid(kernel.copySolid(shape));
395
+ };
396
+ /** 深拷贝 + 原地仿射变换(matrix 已经 toKernelMatrix 归一为 16 元素 4×4)。 */
397
+ const cloneAndTransform = (shape, matrix16) => {
398
+ const c = cloneShape(shape);
399
+ const m = Float64Array.from(matrix16);
400
+ if (knownFaces.has(c))
401
+ kernel.transformFace(c, m);
402
+ else if (knownWires.has(c))
403
+ kernel.transformWire(c, m);
404
+ else
405
+ kernel.transformSolid(c, m);
406
+ return c;
407
+ };
208
408
  const api = {
209
409
  // ── 生命周期 ──
210
410
  release(_shape) { },
@@ -217,26 +417,26 @@ export async function createBrepkitPrimitives() {
217
417
  return delta;
218
418
  },
219
419
  // ── 实体图元 ──
220
- makeBox(dx, dy, dz) { return asHandle(kernel.makeBox(dx, dy, dz)); },
420
+ makeBox(dx, dy, dz) { return asHandle(markSolid(kernel.makeBox(dx, dy, dz))); },
221
421
  makeBoxFromCorners(corner1, corner2) {
222
422
  const dx = Math.abs(corner2.x - corner1.x), dy = Math.abs(corner2.y - corner1.y), dz = Math.abs(corner2.z - corner1.z);
223
- const h = kernel.makeBox(dx, dy, dz);
423
+ const h = markSolid(kernel.makeBox(dx, dy, dz));
224
424
  // ⚠️ transformSolid 是「原地修改、返回 undefined」——makeBox 的句柄本身即新实体,
225
425
  // 直接原地变换后返回 h;若写 `asHandle(transformSolid(...))` 会拿到 undefined 句柄
226
426
  // (2026-09-25 实证:导致 splitBrep 基于半空间盒的 common/cut 全部失效)。
227
427
  kernel.transformSolid(h, toKernelMatrix(translationMatrix(Math.min(corner1.x, corner2.x), Math.min(corner1.y, corner2.y), Math.min(corner1.z, corner2.z))));
228
428
  return asHandle(h);
229
429
  },
230
- makeCylinder(radius, height) { return asHandle(kernel.makeCylinder(radius, height)); },
231
- makeSphere(radius) { return asHandle(kernel.makeSphere(radius, 32)); },
232
- makeCone(r1, r2, height) { return asHandle(kernel.makeCone(r1, r2, height)); },
430
+ makeCylinder(radius, height) { return asHandle(markSolid(kernel.makeCylinder(radius, height))); },
431
+ makeSphere(radius) { return asHandle(markSolid(kernel.makeSphere(radius, 32))); },
432
+ makeCone(r1, r2, height) { return asHandle(markSolid(kernel.makeCone(r1, r2, height))); },
233
433
  makeRectangle(width, height) {
234
434
  const f = kernel.makeRectangle(width, height);
235
435
  knownFaces.add(f);
236
436
  return asHandle(f);
237
437
  },
238
- makeEllipsoid(rx, ry, rz) { return asHandle(kernel.makeEllipsoid(rx, ry, rz)); },
239
- makeTorus(majorRadius, minorRadius) { return asHandle(kernel.makeTorus(majorRadius, minorRadius, 64)); },
438
+ makeEllipsoid(rx, ry, rz) { return asHandle(markSolid(kernel.makeEllipsoid(rx, ry, rz))); },
439
+ makeTorus(majorRadius, minorRadius) { return asHandle(markSolid(kernel.makeTorus(majorRadius, minorRadius, 64))); },
240
440
  makeVertex(x, y, z) { return asHandle(kernel.makeVertex(x, y, z)); },
241
441
  // ── 造型运算 ──
242
442
  extrude(shape, dx, dy, dz) {
@@ -248,7 +448,7 @@ export async function createBrepkitPrimitives() {
248
448
  const len = Math.hypot(dx, dy, dz);
249
449
  if (len === 0)
250
450
  fail('extrude: zero-length extrusion vector');
251
- return asHandle(kernel.extrude(s, dx / len, dy / len, dz / len, len));
451
+ return asHandle(markSolid(kernel.extrude(s, dx / len, dy / len, dz / len, len)));
252
452
  },
253
453
  revolveVec(shape, center, direction, angleDeg) {
254
454
  // 方言归一:brepkit revolve 只吃 face(与 extrude 同族断言)。
@@ -256,14 +456,14 @@ export async function createBrepkitPrimitives() {
256
456
  if (!knownFaces.has(s)) {
257
457
  fail('revolveVec requires a face input; got a handle that is not a known face');
258
458
  }
259
- return asHandle(kernel.revolve(s, center.x, center.y, center.z, direction.x, direction.y, direction.z, angleDeg));
459
+ return asHandle(markSolid(kernel.revolve(s, center.x, center.y, center.z, direction.x, direction.y, direction.z, angleDeg)));
260
460
  },
261
461
  sew(shapesList, tolerance) {
262
462
  return asHandle(kernel.sewFaces(Uint32Array.from(shapesList.map(asNum)), tolerance ?? 1e-6));
263
463
  },
264
464
  sewAndSolidify(faces, _tolerance) {
265
465
  // brepkit 的 makeSolid 即「缝合并固化成实体」(sewFaces + 建 solid)。
266
- return asHandle(kernel.makeSolid(Uint32Array.from(faces.map(asNum))));
466
+ return asHandle(markSolid(kernel.makeSolid(Uint32Array.from(faces.map(asNum)))));
267
467
  },
268
468
  shell(solid, facesToRemove, thickness, _tolerance) {
269
469
  // brepkit 参数序 (solid, thickness, open_faces);tolerance 内核固定。
@@ -273,19 +473,25 @@ export async function createBrepkitPrimitives() {
273
473
  return asHandle(kernel.convexHull(flattenPoints(points)));
274
474
  },
275
475
  // ── 布尔与分割 ──
276
- fuse(a, b) { return asHandle(trackFallback(kernel.fuse(asNum(a), asNum(b)))); },
277
- cut(a, b) { return asHandle(trackFallback(kernel.cut(asNum(a), asNum(b)))); },
476
+ fuse(a, b) { return asHandle(markSolid(trackFallback(kernel.fuse(asNum(a), asNum(b))))); },
477
+ cut(a, b) { return asHandle(markSolid(trackFallback(kernel.cut(asNum(a), asNum(b))))); },
278
478
  common(a, b) {
279
479
  // brepkit 无 `common` 原生名;交集语义由 `intersect` 承担(method-map: dialect)。
280
- return asHandle(trackFallback(kernel.intersect(asNum(a), asNum(b))));
480
+ return asHandle(markSolid(trackFallback(kernel.intersect(asNum(a), asNum(b)))));
281
481
  },
282
482
  intersect(a, b) { return asHandle(trackFallback(kernel.intersect(asNum(a), asNum(b)))); },
283
483
  fuseAll(shapesList) {
284
- return asHandle(trackFallback(kernel.fuseAll(Int32Array.from(shapesList.map(asNum)))));
484
+ return asHandle(markSolid(trackFallback(kernel.fuseAll(Int32Array.from(shapesList.map(asNum))))));
285
485
  },
286
486
  sectionByPlane(shape, point, normal) {
287
487
  // brepkit 原生 section(solid, plane) 返回剖面 face 句柄组(§3.6-2 新中立名)。
288
- return arr(kernel.section(asNum(shape), point.x, point.y, point.z, normal.x, normal.y, normal.z)).map(asHandle);
488
+ // GOTCHA:brepkit section 返回的是 **face** 句柄(不是 edge/wire),必须登记进 knownFaces,
489
+ // 否则 op 层 makeCompound(这些 face) 后送 meshShape 会按 solid 路径崩溃。
490
+ const s = asNum(shape);
491
+ const faces = arr(kernel.section(s, point.x, point.y, point.z, normal.x, normal.y, normal.z));
492
+ for (const f of faces)
493
+ knownFaces.add(f);
494
+ return faces.map(asHandle);
289
495
  },
290
496
  splitByPlane(shape, point, normal) {
291
497
  const parts = arr(kernel.split(asNum(shape), point.x, point.y, point.z, normal.x, normal.y, normal.z));
@@ -299,6 +505,8 @@ export async function createBrepkitPrimitives() {
299
505
  return (c[0] ?? 0) * normal.x + (c[1] ?? 0) * normal.y + (c[2] ?? 0) * normal.z - originDot;
300
506
  };
301
507
  const [a, b] = parts;
508
+ markSolid(a);
509
+ markSolid(b);
302
510
  return side(a) >= side(b)
303
511
  ? { positive: asHandle(a), negative: asHandle(b) }
304
512
  : { positive: asHandle(b), negative: asHandle(a) };
@@ -335,37 +543,25 @@ export async function createBrepkitPrimitives() {
335
543
  // brepkit transformSolid 吃 4×4 行主序 16 元素。所有 transformSolid 调用一律经
336
544
  // toKernelMatrix 归一(下方唯一转换点),禁止直出 16 元素旁路。
337
545
  translate(shape, dx, dy, dz) {
338
- const c = kernel.copySolid(asNum(shape));
339
- kernel.transformSolid(c, toKernelMatrix(translationMatrix(dx, dy, dz)));
340
- return asHandle(c);
546
+ return asHandle(cloneAndTransform(asNum(shape), toKernelMatrix(translationMatrix(dx, dy, dz))));
341
547
  },
342
548
  scale(shape, center, factor) {
343
- const c = kernel.copySolid(asNum(shape));
344
- kernel.transformSolid(c, toKernelMatrix(scaleMatrix(center, factor)));
345
- return asHandle(c);
549
+ return asHandle(cloneAndTransform(asNum(shape), toKernelMatrix(scaleMatrix(center, factor))));
346
550
  },
347
551
  transform(shape, matrix) {
348
- const c = kernel.copySolid(asNum(shape));
349
- kernel.transformSolid(c, toKernelMatrix(matrix));
350
- return asHandle(c);
552
+ return asHandle(cloneAndTransform(asNum(shape), toKernelMatrix(matrix)));
351
553
  },
352
554
  located(shape, matrix) {
353
- const c = kernel.copySolid(asNum(shape));
354
- kernel.transformSolid(c, toKernelMatrix(matrix));
355
- return asHandle(c);
555
+ return asHandle(cloneAndTransform(asNum(shape), toKernelMatrix(matrix)));
356
556
  },
357
557
  locate(shape, matrix) {
358
- const c = kernel.copySolid(asNum(shape));
359
- kernel.transformSolid(c, toKernelMatrix(matrix));
360
- return asHandle(c);
558
+ return asHandle(cloneAndTransform(asNum(shape), toKernelMatrix(matrix)));
361
559
  },
362
560
  generalTransform(shape, matrix) {
363
- const c = kernel.copySolid(asNum(shape));
364
- kernel.transformSolid(c, toKernelMatrix(matrix));
365
- return asHandle(c);
561
+ return asHandle(cloneAndTransform(asNum(shape), toKernelMatrix(matrix)));
366
562
  },
367
- copy(shape) { return asHandle(kernel.copySolid(asNum(shape))); },
368
- copyShape(shape) { return asHandle(kernel.copySolid(asNum(shape))); },
563
+ copy(shape) { return asHandle(cloneShape(asNum(shape))); },
564
+ copyShape(shape) { return asHandle(cloneShape(asNum(shape))); },
369
565
  composeTransform(m1, m2) {
370
566
  // 方言:L1 是 3×4 行主序 12 元素;brepkit composeTransforms 吃/回 4×4 16 元素。
371
567
  const r = kernel.composeTransforms(Float64Array.from(toKernelMatrix(m1)), Float64Array.from(toKernelMatrix(m2)));
@@ -392,12 +588,16 @@ export async function createBrepkitPrimitives() {
392
588
  },
393
589
  // ── 曲线构造 ──
394
590
  makeLineEdge(start, end) {
395
- return asHandle(kernel.makeLineEdge(start.x, start.y, start.z, end.x, end.y, end.z));
591
+ const e = kernel.makeLineEdge(start.x, start.y, start.z, end.x, end.y, end.z);
592
+ knownEdges.add(e);
593
+ return asHandle(e);
396
594
  },
397
595
  makeArcEdge(start, mid, end) {
398
596
  // 方言消化:occt 吃 3 点;brepkit makeCircleArc3d 吃 (start,end,center,axis)。
399
597
  const { center, axis } = circumcircle(start, mid, end);
400
- return asHandle(kernel.makeCircleArc3d(start.x, start.y, start.z, end.x, end.y, end.z, center.x, center.y, center.z, axis.x, axis.y, axis.z));
598
+ const e = kernel.makeCircleArc3d(start.x, start.y, start.z, end.x, end.y, end.z, center.x, center.y, center.z, axis.x, axis.y, axis.z);
599
+ knownEdges.add(e);
600
+ return asHandle(e);
401
601
  },
402
602
  makeBezierEdge(controlPoints) {
403
603
  // 方言消化:L1 吃控制点(Bezier);brepkit 原生 makeNurbsEdge 吃
@@ -413,14 +613,20 @@ export async function createBrepkitPrimitives() {
413
613
  const weights = new Float64Array(n).fill(1);
414
614
  const first = controlPoints[0];
415
615
  const last = controlPoints[n - 1];
416
- return asHandle(kernel.makeNurbsEdge(first.x, first.y, first.z, last.x, last.y, last.z, degree, knots, flattenPoints(controlPoints), weights));
616
+ const e = kernel.makeNurbsEdge(first.x, first.y, first.z, last.x, last.y, last.z, degree, knots, flattenPoints(controlPoints), weights);
617
+ knownEdges.add(e);
618
+ return asHandle(e);
417
619
  },
418
620
  makeCircleEdge(center, normal, radius) {
419
- return asHandle(kernel.makeCircleEdge(center.x, center.y, center.z, normal.x, normal.y, normal.z, radius));
621
+ const e = kernel.makeCircleEdge(center.x, center.y, center.z, normal.x, normal.y, normal.z, radius);
622
+ knownEdges.add(e);
623
+ return asHandle(e);
420
624
  },
421
625
  // ── 拓扑构造 ──
422
626
  makeWire(edges) {
423
- return asHandle(kernel.makeWire(Int32Array.from(edges.map(asNum)), false));
627
+ const w = kernel.makeWire(Int32Array.from(edges.map(asNum)), false);
628
+ knownWires.add(w);
629
+ return asHandle(w);
424
630
  },
425
631
  makeFace(wire) {
426
632
  const f = kernel.makeFaceFromWire(asNum(wire));
@@ -428,10 +634,28 @@ export async function createBrepkitPrimitives() {
428
634
  return asHandle(f);
429
635
  },
430
636
  makeCompound(shapesList) {
431
- return asHandle(kernel.makeCompound(Int32Array.from(shapesList.map(asNum))));
637
+ const kids = arr(shapesList.map(asNum));
638
+ // GOTCHA(2026-09-26):brepkit makeCompound 只接受 **solid** 句柄——把 face/wire
639
+ // (sectionByPlane 截面面组)塞进去即抛 "invalid solid handle"。非实体子句柄时
640
+ // 不调内核,改用高段虚拟句柄(0x80000000+),仅靠 knownCompounds 表分发
641
+ // meshShape/wireframe/getSubShapes/getBoundingBox,不触达内核。
642
+ const hasNonSolid = kids.some((k) => knownFaces.has(k) || knownWires.has(k) || knownEdges.has(k) || knownCompounds.has(k));
643
+ if (hasNonSolid) {
644
+ virtualCompoundCounter++;
645
+ const v = 0x80000000 + virtualCompoundCounter;
646
+ knownCompounds.set(v, kids);
647
+ return asHandle(v);
648
+ }
649
+ const c = kernel.makeCompound(Int32Array.from(kids));
650
+ knownCompounds.set(c, kids);
651
+ return asHandle(c);
432
652
  },
433
653
  addHolesInFace(face, holeWires) {
434
- return asHandle(kernel.addHolesToFace(asNum(face), Int32Array.from(holeWires.map(asNum))));
654
+ // addHolesToFace 返回**新 face 句柄**(同曲面 + 内孔 wire),必须登记进 knownFaces,
655
+ // 否则 profile(op) 带孔面送 meshShape 会落到 solid 路径崩溃。
656
+ const f = kernel.addHolesToFace(asNum(face), Int32Array.from(holeWires.map(asNum)));
657
+ knownFaces.add(f);
658
+ return asHandle(f);
435
659
  },
436
660
  buildTriFace(a, b, c) {
437
661
  const f = kernel.makePolygon(flattenPoints([a, b, c]));
@@ -442,39 +666,67 @@ export async function createBrepkitPrimitives() {
442
666
  meshShape(shape, options) {
443
667
  const deflection = options?.linearDeflection ?? 0.1;
444
668
  const angular = options?.angularDeflection ?? 0.5;
445
- const raw = kernel.tessellateSolidGrouped(asNum(shape), deflection, angular);
446
- const m = JSON.parse(raw);
447
- // faceOffsets 形如 [start0, start1, ..., totalEnd]:以「索引数组单位」计的每面偏移(2026-09-19 实测),
448
- // 除以 3 换算为 faijs 契约的三角形单位 [triStart, triCount, faceHash]
449
- const fo = m.faceOffsets;
450
- const faceCount = Math.max(0, fo.length - 1);
451
- const faceHashes = [];
452
- try {
453
- const faces = arr(kernel.getSolidFaces(asNum(shape)));
454
- for (let i = 0; i < faceCount && i < faces.length; i++)
455
- faceHashes.push(faceFingerprint(kernel, faces[i], 1e9));
456
- }
457
- catch { /* 非实体:faceHash 置 0 */ }
458
- const faceGroups = new Int32Array(faceCount * 3);
459
- for (let i = 0; i < faceCount; i++) {
460
- faceGroups[i * 3] = fo[i] / 3;
461
- faceGroups[i * 3 + 1] = (fo[i + 1] - fo[i]) / 3;
462
- faceGroups[i * 3 + 2] = faceHashes[i] ?? 0;
463
- }
464
- return {
465
- positions: new Float32Array(m.positions),
466
- normals: new Float32Array(m.normals),
467
- indices: new Uint32Array(m.indices),
468
- vertexCount: m.positions.length / 3,
469
- triangleCount: m.indices.length / 3,
470
- faceGroups,
471
- faceCount,
472
- };
669
+ const s = asNum(shape);
670
+ // 非实体句柄按已知集合静态分发(见文件头 GOTCHA:brepkit 无 shape-type 分类 API)。
671
+ // 顺序:face → wire → edge → compound → solid。实体创建不入任何集合,天然落 solid 路径。
672
+ if (knownFaces.has(s))
673
+ return faceMesh(s, deflection, angular);
674
+ if (knownWires.has(s))
675
+ return wireMesh(s, deflection);
676
+ if (knownEdges.has(s))
677
+ return edgesToLineMesh([s], deflection);
678
+ const compoundChildren = knownCompounds.get(s);
679
+ if (compoundChildren)
680
+ return compoundMesh(compoundChildren, deflection, angular);
681
+ // solid(或未登记句柄):走原 tessellateSolidGrouped 路径。
682
+ return solidMesh(s, deflection, angular);
473
683
  },
474
684
  wireframe(shape, deflection) {
475
685
  const d = deflection ?? 0.1;
686
+ const s = asNum(shape);
687
+ // GOTCHA:meshEdgesAll 是 solid-only API——buildTopologyFromMesh 会对 face/wire/edge
688
+ // 产物调 wireframe(...),传入非实体句柄即抛 "invalid solid handle"。按已知集合分发:
689
+ // face -> getFaceEdges;wire -> getWireEdges;edge -> [self];否则 solid 走 meshEdgesAll。
690
+ let edgeHandles = null;
691
+ if (knownFaces.has(s))
692
+ edgeHandles = arr(kernel.getFaceEdges(s));
693
+ else if (knownWires.has(s))
694
+ edgeHandles = arr(kernel.getWireEdges(s));
695
+ else if (knownEdges.has(s))
696
+ edgeHandles = [s];
697
+ else if (knownCompounds.has(s)) {
698
+ // compound(sectionByPlane 面组):逐子面收集边。
699
+ edgeHandles = [];
700
+ for (const k of knownCompounds.get(s)) {
701
+ if (knownFaces.has(k))
702
+ edgeHandles.push(...arr(kernel.getFaceEdges(k)));
703
+ else if (knownWires.has(k))
704
+ edgeHandles.push(...arr(kernel.getWireEdges(k)));
705
+ else if (knownEdges.has(k))
706
+ edgeHandles.push(k);
707
+ }
708
+ }
709
+ if (edgeHandles) {
710
+ const nPts = Math.max(12, Math.round(4 / Math.max(d, 0.01)));
711
+ const positions = [];
712
+ const offsets = [0];
713
+ for (const e of edgeHandles) {
714
+ const pts = arr(kernel.tessellateEdge(e, nPts));
715
+ for (const p of pts)
716
+ positions.push(p);
717
+ offsets.push(positions.length / 3);
718
+ }
719
+ const edgeGroups = new Int32Array(edgeHandles.length * 3);
720
+ for (let i = 0; i < edgeHandles.length; i++) {
721
+ edgeGroups[i * 3] = offsets[i] ?? 0;
722
+ edgeGroups[i * 3 + 1] = (offsets[i + 1] ?? positions.length / 3) - (offsets[i] ?? 0);
723
+ edgeGroups[i * 3 + 2] = i;
724
+ }
725
+ return { points: new Float32Array(positions), edgeGroups, pointCount: positions.length / 3, edgeCount: edgeHandles.length };
726
+ }
727
+ // solid(或未登记句柄):走原 meshEdgesAll 路径。
476
728
  // JsEdgeLines:edgeCount/offsets/positions 均为属性(offsets/positions 为 TypedArray,2026-09-19 实测)
477
- const me = kernel.meshEdgesAll(asNum(shape), d, 0.5);
729
+ const me = kernel.meshEdgesAll(s, d, 0.5);
478
730
  const edgeCount = Number(me.edgeCount);
479
731
  const offsets = arr(me.offsets);
480
732
  const positions = arr(me.positions);
@@ -497,9 +749,52 @@ export async function createBrepkitPrimitives() {
497
749
  // edge/vertex 查询时按句柄是否在 knownFaces 里分发到 face 级或 solid 级 API。
498
750
  getSubShapes(shape, type) {
499
751
  const s = asNum(shape);
752
+ // GOTCHA(2026-09-26 非实体句柄修复):buildTopologyFromMesh 会对**面产物**
753
+ // 调 getSubShapes(face,'face')。brepkit 的 getSolidFaces/getShellFaces 是 solid-only,
754
+ // 对面句柄一个抛 "invalid solid handle"、catch 里再抛 "invalid shell handle"——
755
+ // 两跳都不被捕获即崩溃。brepkit 无 shape-type 分类 API,必须靠已知集合先分发:
756
+ // 输入本身就是 knownFaces 里的一张面时,'face' 返回 [s],edge/vertex 走 face 级 API。
757
+ if (knownFaces.has(s)) {
758
+ if (type === 'face')
759
+ return [asHandle(s)];
760
+ if (type === 'edge') {
761
+ const es = arr(kernel.getFaceEdges(s));
762
+ es.forEach((e) => knownEdges.add(e));
763
+ return es.map(asHandle);
764
+ }
765
+ if (type === 'vertex')
766
+ return arr(kernel.getFaceVertices(s)).map(asHandle);
767
+ return [];
768
+ }
769
+ if (knownWires.has(s)) {
770
+ if (type === 'edge') {
771
+ const es = arr(kernel.getWireEdges(s));
772
+ es.forEach((e) => knownEdges.add(e));
773
+ return es.map(asHandle);
774
+ }
775
+ return [];
776
+ }
777
+ if (knownEdges.has(s)) {
778
+ // input is itself an edge (wire.ts smooth path: interpolatePoints returns edge)
779
+ if (type === 'edge')
780
+ return [asHandle(s)];
781
+ return [];
782
+ }
783
+ const compKids = knownCompounds.get(s);
784
+ if (compKids) {
785
+ // compound(sectionByPlane 面组 / makeCompound 多实体 / importStep 多实体):
786
+ // 'face' 返回子面,'solid' 返回子实体,其余空。
787
+ if (type === 'face')
788
+ return compKids.filter((k) => knownFaces.has(k)).map(asHandle);
789
+ if (type === 'solid')
790
+ return compKids.filter((k) => !knownFaces.has(k) && !knownWires.has(k) && !knownEdges.has(k)).map(asHandle);
791
+ return [];
792
+ }
500
793
  let list;
794
+ const isFaceLike = knownFaces.has(s) || derivedFaces.has(s);
501
795
  if (type === 'face') {
502
- // 输入可能是 solid 或 shell
796
+ // 输入可能是 solid 或 shell。枚举到的子面登记到 derivedFaces(不进 knownFaces,
797
+ // 避免与 solid 句柄撞号——见 derivedFaces 注释)。
503
798
  try {
504
799
  list = arr(kernel.getSolidFaces(s));
505
800
  }
@@ -507,15 +802,15 @@ export async function createBrepkitPrimitives() {
507
802
  list = arr(kernel.getShellFaces?.(s) ?? []);
508
803
  }
509
804
  for (const f of list)
510
- knownFaces.add(f);
805
+ derivedFaces.add(f);
511
806
  }
512
807
  else if (type === 'edge') {
513
- list = knownFaces.has(s)
808
+ list = isFaceLike
514
809
  ? arr(kernel.getFaceEdges(s))
515
810
  : arr(kernel.getSolidEdges(s));
516
811
  }
517
812
  else if (type === 'vertex') {
518
- list = knownFaces.has(s)
813
+ list = isFaceLike
519
814
  ? arr(kernel.getFaceVertices(s))
520
815
  : arr(kernel.getSolidVertices(s));
521
816
  }
@@ -539,7 +834,20 @@ export async function createBrepkitPrimitives() {
539
834
  subShapeHashes(shape, type, hashUpperBound) {
540
835
  if (type !== 'face')
541
836
  fail(`subShapeHashes(${type}): only 'face' is supported by brepkit`);
542
- const faces = arr(kernel.getSolidFaces(asNum(shape)));
837
+ const s = asNum(shape);
838
+ // GOTCHA:buildTopologyFromMesh 会对面产物调 subShapeHashes(face,'face');
839
+ // getSolidFaces 是 solid-only,对面句柄崩溃。输入本身是已知面时,哈希即其自身指纹。
840
+ if (knownFaces.has(s))
841
+ return [faceFingerprint(kernel, s, hashUpperBound)];
842
+ // wire/edge 无面;compound(sectionByPlane 面组)的子句柄即面。
843
+ if (knownWires.has(s) || knownEdges.has(s))
844
+ return [];
845
+ const compKids = knownCompounds.get(s);
846
+ if (compKids) {
847
+ const faces = compKids.filter((k) => knownFaces.has(k));
848
+ return faces.map((f) => faceFingerprint(kernel, f, hashUpperBound));
849
+ }
850
+ const faces = arr(kernel.getSolidFaces(s));
543
851
  const hashes = faces.map(f => faceFingerprint(kernel, f, hashUpperBound));
544
852
  // 登记 hash↔handle(fuseWithHistory 溯源对齐依赖此表)
545
853
  const reg = hashRegistry.get(kernel) ?? new Map();
@@ -665,7 +973,11 @@ export async function createBrepkitPrimitives() {
665
973
  return null;
666
974
  },
667
975
  interpolatePoints(points, degree) {
668
- return asHandle(kernel.interpolatePoints(flattenPoints(points), degree));
976
+ // GOTCHA:interpolatePoints 返回 **edge** 句柄(不是 wire)——wire.ts smooth 路径把它
977
+ // 直接当 wire 送 solidToShape。登记进 knownEdges,meshShape 才能走 tessellateEdge 路径。
978
+ const e = kernel.interpolatePoints(flattenPoints(points), degree);
979
+ knownEdges.add(e);
980
+ return asHandle(e);
669
981
  },
670
982
  defeature(shape, faces) {
671
983
  return asHandle(kernel.defeature(asNum(shape), Uint32Array.from(faces.map(asNum))));
@@ -674,7 +986,10 @@ export async function createBrepkitPrimitives() {
674
986
  return asHandle(kernel.draft(asNum(shape), Uint32Array.from(faces.map(asNum)), pull.x, pull.y, pull.z, neutral.x, neutral.y, neutral.z, angleDeg));
675
987
  },
676
988
  removeHolesFromFace(face) {
677
- return asHandle(kernel.removeHolesFromFace(asNum(face)));
989
+ // removeHolesFromFace 返回**新 face 句柄**(去掉内孔的同曲面 face),必须登记进 knownFaces。
990
+ const f = kernel.removeHolesFromFace(asNum(face));
991
+ knownFaces.add(f);
992
+ return asHandle(f);
678
993
  },
679
994
  reverseShape(shape) {
680
995
  return asHandle(kernel.reverseShape(asNum(shape)));
@@ -685,8 +1000,65 @@ export async function createBrepkitPrimitives() {
685
1000
  },
686
1001
  // ── 测量 ──
687
1002
  getBoundingBox(shape, _useTriangulation) {
688
- const b = kernel.boundingBox(asNum(shape));
689
- return { xmin: b[0], ymin: b[1], zmin: b[2], xmax: b[3], ymax: b[4], zmax: b[5] };
1003
+ const s = asNum(shape);
1004
+ const bboxFromPoints = (pts) => {
1005
+ let xmin = Infinity, ymin = Infinity, zmin = Infinity, xmax = -Infinity, ymax = -Infinity, zmax = -Infinity;
1006
+ for (let i = 0; i + 2 < pts.length; i += 3) {
1007
+ xmin = Math.min(xmin, pts[i]);
1008
+ ymin = Math.min(ymin, pts[i + 1]);
1009
+ zmin = Math.min(zmin, pts[i + 2]);
1010
+ xmax = Math.max(xmax, pts[i]);
1011
+ ymax = Math.max(ymax, pts[i + 1]);
1012
+ zmax = Math.max(zmax, pts[i + 2]);
1013
+ }
1014
+ if (!Number.isFinite(xmin)) {
1015
+ xmin = ymin = zmin = 0;
1016
+ xmax = ymax = zmax = 0;
1017
+ }
1018
+ return { xmin, ymin, zmin, xmax, ymax, zmax };
1019
+ };
1020
+ const solidBbox = (h) => {
1021
+ const b = kernel.boundingBox(h);
1022
+ return { xmin: b[0], ymin: b[1], zmin: b[2], xmax: b[3], ymax: b[4], zmax: b[5] };
1023
+ };
1024
+ const kids = knownCompounds.get(s);
1025
+ if (kids) {
1026
+ let xmin = Infinity, ymin = Infinity, zmin = Infinity, xmax = -Infinity, ymax = -Infinity, zmax = -Infinity;
1027
+ for (const k of kids) {
1028
+ try {
1029
+ let b;
1030
+ if (knownFaces.has(k))
1031
+ b = bboxFromPoints(arr(kernel.tessellateFace(k, 0.1, 1).positions));
1032
+ else if (knownWires.has(k))
1033
+ b = bboxFromPoints(arr(kernel.getWireEdges(k)).flatMap((e) => arr(kernel.tessellateEdge(e, 16))));
1034
+ else if (knownEdges.has(k))
1035
+ b = bboxFromPoints(arr(kernel.tessellateEdge(k, 16)));
1036
+ else
1037
+ b = solidBbox(k);
1038
+ xmin = Math.min(xmin, b.xmin);
1039
+ ymin = Math.min(ymin, b.ymin);
1040
+ zmin = Math.min(zmin, b.zmin);
1041
+ xmax = Math.max(xmax, b.xmax);
1042
+ ymax = Math.max(ymax, b.ymax);
1043
+ zmax = Math.max(zmax, b.zmax);
1044
+ }
1045
+ catch { /* 子项 bbox 查询失败忽略 */ }
1046
+ }
1047
+ if (!Number.isFinite(xmin)) {
1048
+ xmin = ymin = zmin = 0;
1049
+ xmax = ymax = zmax = 0;
1050
+ }
1051
+ return { xmin, ymin, zmin, xmax, ymax, zmax };
1052
+ }
1053
+ // GOTCHA:kernel.boundingBox 是 solid-only。face/wire/edge 句柄必须走 tessellation 采样,
1054
+ // 否则 measure() 对 wire/profile 调 getBoundingBox 即崩 "invalid solid handle"。
1055
+ if (knownFaces.has(s))
1056
+ return bboxFromPoints(arr(kernel.tessellateFace(s, 0.1, 1).positions));
1057
+ if (knownWires.has(s))
1058
+ return bboxFromPoints(arr(kernel.getWireEdges(s)).flatMap((e) => arr(kernel.tessellateEdge(e, 16))));
1059
+ if (knownEdges.has(s))
1060
+ return bboxFromPoints(arr(kernel.tessellateEdge(s, 16)));
1061
+ return solidBbox(s);
690
1062
  },
691
1063
  getVolume(shape) { return Number(kernel.volume(asNum(shape), 0.05)); },
692
1064
  getCenterOfMass(shape) {