brepjs 12.2.4 → 12.2.5

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
@@ -74,6 +74,4211 @@ const worker = require("./worker.cjs");
74
74
  function supportsProjection(kernel) {
75
75
  return "projectShape" in kernel;
76
76
  }
77
+ function evaluateCurve2d(c, t) {
78
+ switch (c.__bk2d) {
79
+ case "line":
80
+ return [c.ox + c.dx * t, c.oy + c.dy * t];
81
+ case "circle": {
82
+ const angle = c.sense ? t : -t;
83
+ return [c.cx + c.radius * Math.cos(angle), c.cy + c.radius * Math.sin(angle)];
84
+ }
85
+ case "ellipse": {
86
+ const angle = c.sense ? t : -t;
87
+ const cos = Math.cos(c.xDirAngle);
88
+ const sin = Math.sin(c.xDirAngle);
89
+ const x = c.majorRadius * Math.cos(angle);
90
+ const y = c.minorRadius * Math.sin(angle);
91
+ return [c.cx + x * cos - y * sin, c.cy + x * sin + y * cos];
92
+ }
93
+ case "bezier":
94
+ return evaluateBezier(c.poles, t);
95
+ case "bspline":
96
+ return evaluateBSpline2d(c, t);
97
+ case "trimmed": {
98
+ const mapped = c.tStart + t * (c.tEnd - c.tStart);
99
+ return evaluateCurve2d(c.basis, mapped);
100
+ }
101
+ }
102
+ }
103
+ function tangentCurve2d(c, t) {
104
+ switch (c.__bk2d) {
105
+ case "line":
106
+ return [c.dx, c.dy];
107
+ case "circle": {
108
+ const angle = c.sense ? t : -t;
109
+ const sign = c.sense ? 1 : -1;
110
+ return [-c.radius * Math.sin(angle) * sign, c.radius * Math.cos(angle) * sign];
111
+ }
112
+ case "ellipse": {
113
+ const angle = c.sense ? t : -t;
114
+ const sign = c.sense ? 1 : -1;
115
+ const cos = Math.cos(c.xDirAngle);
116
+ const sin = Math.sin(c.xDirAngle);
117
+ const dx = -c.majorRadius * Math.sin(angle) * sign;
118
+ const dy = c.minorRadius * Math.cos(angle) * sign;
119
+ return [dx * cos - dy * sin, dx * sin + dy * cos];
120
+ }
121
+ case "bezier": {
122
+ const h = 1e-8;
123
+ const p0 = evaluateBezier(c.poles, Math.max(0, t - h));
124
+ const p1 = evaluateBezier(c.poles, Math.min(1, t + h));
125
+ const dt = Math.min(1, t + h) - Math.max(0, t - h);
126
+ return [(p1[0] - p0[0]) / dt, (p1[1] - p0[1]) / dt];
127
+ }
128
+ case "bspline": {
129
+ const h = 1e-8;
130
+ const kFirst = c.knots[0];
131
+ const kLast = c.knots[c.knots.length - 1];
132
+ const p0 = evaluateBSpline2d(c, Math.max(kFirst, t - h));
133
+ const p1 = evaluateBSpline2d(c, Math.min(kLast, t + h));
134
+ const dt = Math.min(kLast, t + h) - Math.max(kFirst, t - h);
135
+ return [(p1[0] - p0[0]) / dt, (p1[1] - p0[1]) / dt];
136
+ }
137
+ case "trimmed": {
138
+ const mapped = c.tStart + t * (c.tEnd - c.tStart);
139
+ const tan = tangentCurve2d(c.basis, mapped);
140
+ const scale2 = c.tEnd - c.tStart;
141
+ return [tan[0] * scale2, tan[1] * scale2];
142
+ }
143
+ }
144
+ }
145
+ function curveBounds(c) {
146
+ switch (c.__bk2d) {
147
+ case "line":
148
+ return { first: 0, last: c.len };
149
+ case "circle":
150
+ case "ellipse":
151
+ return { first: 0, last: 2 * Math.PI };
152
+ case "bezier":
153
+ return { first: 0, last: 1 };
154
+ case "bspline":
155
+ return { first: c.knots[0], last: c.knots[c.knots.length - 1] };
156
+ case "trimmed":
157
+ return { first: 0, last: 1 };
158
+ }
159
+ }
160
+ function curveTypeName(c) {
161
+ switch (c.__bk2d) {
162
+ case "line":
163
+ return "LINE";
164
+ case "circle":
165
+ return "CIRCLE";
166
+ case "ellipse":
167
+ return "ELLIPSE";
168
+ case "bezier":
169
+ return "BEZIER_CURVE";
170
+ case "bspline":
171
+ return "BSPLINE_CURVE";
172
+ case "trimmed":
173
+ return "TRIMMED_" + curveTypeName(c.basis);
174
+ }
175
+ }
176
+ function makeLine2d(x1, y1, x2, y2) {
177
+ const dx = x2 - x1;
178
+ const dy = y2 - y1;
179
+ const len = Math.sqrt(dx * dx + dy * dy);
180
+ return {
181
+ __bk2d: "line",
182
+ ox: x1,
183
+ oy: y1,
184
+ dx: len > 0 ? dx / len : 1,
185
+ dy: len > 0 ? dy / len : 0,
186
+ len
187
+ };
188
+ }
189
+ function makeCircle2d(cx, cy, radius, sense = true) {
190
+ return { __bk2d: "circle", cx, cy, radius, sense };
191
+ }
192
+ function makeEllipse2d(cx, cy, majorRadius, minorRadius, xDirX = 1, xDirY = 0, sense = true) {
193
+ return {
194
+ __bk2d: "ellipse",
195
+ cx,
196
+ cy,
197
+ majorRadius,
198
+ minorRadius,
199
+ xDirAngle: Math.atan2(xDirY, xDirX),
200
+ sense
201
+ };
202
+ }
203
+ function makeBezier2d(poles) {
204
+ return { __bk2d: "bezier", poles: [...poles] };
205
+ }
206
+ function translateCurve2d(c, dx, dy) {
207
+ switch (c.__bk2d) {
208
+ case "line":
209
+ return { ...c, ox: c.ox + dx, oy: c.oy + dy };
210
+ case "circle":
211
+ return { ...c, cx: c.cx + dx, cy: c.cy + dy };
212
+ case "ellipse":
213
+ return { ...c, cx: c.cx + dx, cy: c.cy + dy };
214
+ case "bezier":
215
+ return { ...c, poles: c.poles.map(([x, y]) => [x + dx, y + dy]) };
216
+ case "bspline":
217
+ return { ...c, poles: c.poles.map(([x, y]) => [x + dx, y + dy]) };
218
+ case "trimmed":
219
+ return { ...c, basis: translateCurve2d(c.basis, dx, dy) };
220
+ }
221
+ }
222
+ function rotateCurve2d(c, angle, cx, cy) {
223
+ const cos = Math.cos(angle);
224
+ const sin = Math.sin(angle);
225
+ const rotatePoint = (x, y) => {
226
+ const rx = x - cx;
227
+ const ry = y - cy;
228
+ return [cx + rx * cos - ry * sin, cy + rx * sin + ry * cos];
229
+ };
230
+ switch (c.__bk2d) {
231
+ case "line": {
232
+ const [ox, oy] = rotatePoint(c.ox, c.oy);
233
+ const ndx = c.dx * cos - c.dy * sin;
234
+ const ndy = c.dx * sin + c.dy * cos;
235
+ return { ...c, ox, oy, dx: ndx, dy: ndy };
236
+ }
237
+ case "circle": {
238
+ const [ncx, ncy] = rotatePoint(c.cx, c.cy);
239
+ return { ...c, cx: ncx, cy: ncy };
240
+ }
241
+ case "ellipse": {
242
+ const [ncx, ncy] = rotatePoint(c.cx, c.cy);
243
+ return { ...c, cx: ncx, cy: ncy, xDirAngle: c.xDirAngle + angle };
244
+ }
245
+ case "bezier":
246
+ return { ...c, poles: c.poles.map(([x, y]) => rotatePoint(x, y)) };
247
+ case "bspline":
248
+ return { ...c, poles: c.poles.map(([x, y]) => rotatePoint(x, y)) };
249
+ case "trimmed":
250
+ return { ...c, basis: rotateCurve2d(c.basis, angle, cx, cy) };
251
+ }
252
+ }
253
+ function scaleCurve2d(c, factor, cx, cy) {
254
+ const scalePoint = (x, y) => [
255
+ cx + (x - cx) * factor,
256
+ cy + (y - cy) * factor
257
+ ];
258
+ switch (c.__bk2d) {
259
+ case "line": {
260
+ const [ox, oy] = scalePoint(c.ox, c.oy);
261
+ return { ...c, ox, oy };
262
+ }
263
+ case "circle": {
264
+ const [ncx, ncy] = scalePoint(c.cx, c.cy);
265
+ return { ...c, cx: ncx, cy: ncy, radius: c.radius * Math.abs(factor) };
266
+ }
267
+ case "ellipse": {
268
+ const [ncx, ncy] = scalePoint(c.cx, c.cy);
269
+ return {
270
+ ...c,
271
+ cx: ncx,
272
+ cy: ncy,
273
+ majorRadius: c.majorRadius * Math.abs(factor),
274
+ minorRadius: c.minorRadius * Math.abs(factor)
275
+ };
276
+ }
277
+ case "bezier":
278
+ return { ...c, poles: c.poles.map(([x, y]) => scalePoint(x, y)) };
279
+ case "bspline":
280
+ return { ...c, poles: c.poles.map(([x, y]) => scalePoint(x, y)) };
281
+ case "trimmed":
282
+ return { ...c, basis: scaleCurve2d(c.basis, factor, cx, cy) };
283
+ }
284
+ }
285
+ function mirrorAtPoint(c, cx, cy) {
286
+ return scaleCurve2d(c, -1, cx, cy);
287
+ }
288
+ function mirrorAcrossAxis(c, ox, oy, dx, dy) {
289
+ const len = Math.sqrt(dx * dx + dy * dy);
290
+ const nx = dx / len;
291
+ const ny = dy / len;
292
+ const reflectPoint = (x, y) => {
293
+ const rx = x - ox;
294
+ const ry = y - oy;
295
+ const dot = rx * nx + ry * ny;
296
+ return [ox + 2 * dot * nx - rx, oy + 2 * dot * ny - ry];
297
+ };
298
+ switch (c.__bk2d) {
299
+ case "line": {
300
+ const [nox, noy] = reflectPoint(c.ox, c.oy);
301
+ const ndx = 2 * (c.dx * nx + c.dy * ny) * nx - c.dx;
302
+ const ndy = 2 * (c.dx * nx + c.dy * ny) * ny - c.dy;
303
+ return { ...c, ox: nox, oy: noy, dx: ndx, dy: ndy };
304
+ }
305
+ case "circle": {
306
+ const [ncx, ncy] = reflectPoint(c.cx, c.cy);
307
+ return { ...c, cx: ncx, cy: ncy, sense: !c.sense };
308
+ }
309
+ case "ellipse": {
310
+ const [ncx, ncy] = reflectPoint(c.cx, c.cy);
311
+ const cos2 = nx * nx - ny * ny;
312
+ const sin2 = 2 * nx * ny;
313
+ const newAngle = Math.atan2(
314
+ sin2 * Math.cos(c.xDirAngle) - cos2 * Math.sin(c.xDirAngle),
315
+ cos2 * Math.cos(c.xDirAngle) + sin2 * Math.sin(c.xDirAngle)
316
+ );
317
+ return { ...c, cx: ncx, cy: ncy, xDirAngle: newAngle, sense: !c.sense };
318
+ }
319
+ case "bezier":
320
+ return { ...c, poles: c.poles.map(([x, y]) => reflectPoint(x, y)) };
321
+ case "bspline":
322
+ return { ...c, poles: c.poles.map(([x, y]) => reflectPoint(x, y)) };
323
+ case "trimmed":
324
+ return { ...c, basis: mirrorAcrossAxis(c.basis, ox, oy, dx, dy) };
325
+ }
326
+ }
327
+ function intersectCurves2dFn(c1, c2, tolerance) {
328
+ const b1 = unwrapCurve(c1);
329
+ const b2 = unwrapCurve(c2);
330
+ if (b1.__bk2d === "line" && b2.__bk2d === "line") {
331
+ return intersectLineLine(c1, b1, c2, b2, tolerance);
332
+ }
333
+ if (b1.__bk2d === "line" && b2.__bk2d === "circle") {
334
+ return { points: intersectLineCircle(c1, b1, c2, b2, tolerance), segments: [] };
335
+ }
336
+ if (b1.__bk2d === "circle" && b2.__bk2d === "line") {
337
+ return { points: intersectLineCircle(c2, b2, c1, b1, tolerance), segments: [] };
338
+ }
339
+ if (b1.__bk2d === "circle" && b2.__bk2d === "circle") {
340
+ return { points: intersectCircleCircle(c1, b1, c2, b2, tolerance), segments: [] };
341
+ }
342
+ const isSelf = c1 === c2;
343
+ return numericalIntersect(c1, c2, tolerance, isSelf);
344
+ }
345
+ function unwrapCurve(c) {
346
+ let cur = c;
347
+ while (cur.__bk2d === "trimmed") cur = cur.basis;
348
+ return cur;
349
+ }
350
+ function inDomain(c, t, tol) {
351
+ const b = curveBounds(c);
352
+ return t >= b.first - tol && t <= b.last + tol;
353
+ }
354
+ function refineParam(c, px, py) {
355
+ const bounds = curveBounds(c);
356
+ if (!isFinite(bounds.first) || !isFinite(bounds.last)) return null;
357
+ const N = 80;
358
+ const dt = (bounds.last - bounds.first) / N;
359
+ let bestT = bounds.first;
360
+ let bestD = Infinity;
361
+ for (let i = 0; i <= N; i++) {
362
+ const t = bounds.first + i * dt;
363
+ const [ex2, ey2] = evaluateCurve2d(c, t);
364
+ const d = (ex2 - px) ** 2 + (ey2 - py) ** 2;
365
+ if (d < bestD) {
366
+ bestD = d;
367
+ bestT = t;
368
+ }
369
+ }
370
+ const [sx, sy] = evaluateCurve2d(c, bounds.first);
371
+ const [ex, ey] = evaluateCurve2d(c, bounds.last);
372
+ const [mx, my] = evaluateCurve2d(c, (bounds.first + bounds.last) / 2);
373
+ const geomExtent = Math.max(
374
+ Math.sqrt((ex - sx) ** 2 + (ey - sy) ** 2),
375
+ Math.sqrt((mx - sx) ** 2 + (my - sy) ** 2),
376
+ 1e-6
377
+ );
378
+ const maxDist = geomExtent * 0.1;
379
+ return bestD < maxDist * maxDist ? bestT : null;
380
+ }
381
+ function intersectLineLine(c1, l1, c2, l2, tol) {
382
+ const det = l1.dx * l2.dy - l1.dy * l2.dx;
383
+ if (Math.abs(det) >= 1e-14) {
384
+ const ex3 = l2.ox - l1.ox;
385
+ const ey3 = l2.oy - l1.oy;
386
+ const t1 = (ex3 * l2.dy - ey3 * l2.dx) / det;
387
+ const t2 = (ex3 * l1.dy - ey3 * l1.dx) / det;
388
+ if (!inDomain(c1, t1, tol) || !inDomain(c2, t2, tol)) return { points: [], segments: [] };
389
+ return { points: [[l1.ox + t1 * l1.dx, l1.oy + t1 * l1.dy]], segments: [] };
390
+ }
391
+ const ex = l2.ox - l1.ox;
392
+ const ey = l2.oy - l1.oy;
393
+ const cross = ex * l1.dy - ey * l1.dx;
394
+ if (Math.abs(cross) > tol) return { points: [], segments: [] };
395
+ const b1 = curveBounds(c1);
396
+ const b2 = curveBounds(c2);
397
+ const p2s = evaluateCurve2d(c2, b2.first);
398
+ const p2e = evaluateCurve2d(c2, b2.last);
399
+ const t2sOn1 = (p2s[0] - l1.ox) * l1.dx + (p2s[1] - l1.oy) * l1.dy;
400
+ const t2eOn1 = (p2e[0] - l1.ox) * l1.dx + (p2e[1] - l1.oy) * l1.dy;
401
+ const overlapStart = Math.max(b1.first, Math.min(t2sOn1, t2eOn1));
402
+ const overlapEnd = Math.min(b1.last, Math.max(t2sOn1, t2eOn1));
403
+ if (overlapEnd - overlapStart < tol) return { points: [], segments: [] };
404
+ const sx = l1.ox + overlapStart * l1.dx;
405
+ const sy = l1.oy + overlapStart * l1.dy;
406
+ const ex2 = l1.ox + overlapEnd * l1.dx;
407
+ const ey2 = l1.oy + overlapEnd * l1.dy;
408
+ const seg = makeLine2d(sx, sy, ex2, ey2);
409
+ return { points: [], segments: [seg] };
410
+ }
411
+ function intersectLineCircle(cLine, line, cCirc, circ, tol) {
412
+ const fx = line.ox - circ.cx;
413
+ const fy = line.oy - circ.cy;
414
+ const a = line.dx * line.dx + line.dy * line.dy;
415
+ const b = 2 * (fx * line.dx + fy * line.dy);
416
+ const c = fx * fx + fy * fy - circ.radius * circ.radius;
417
+ const disc = b * b - 4 * a * c;
418
+ if (disc < -tol) return [];
419
+ const results = [];
420
+ const sqrtDisc = Math.sqrt(Math.max(0, disc));
421
+ const t1 = (-b - sqrtDisc) / (2 * a);
422
+ const t2 = (-b + sqrtDisc) / (2 * a);
423
+ for (const tLine of disc < tol * tol ? [t1] : [t1, t2]) {
424
+ if (!inDomain(cLine, tLine, tol)) continue;
425
+ const px = line.ox + tLine * line.dx;
426
+ const py = line.oy + tLine * line.dy;
427
+ const tCirc = refineParam(cCirc, px, py);
428
+ if (tCirc === null) continue;
429
+ const [cx2, cy2] = evaluateCurve2d(cCirc, tCirc);
430
+ if ((cx2 - px) ** 2 + (cy2 - py) ** 2 > tol * tol * 1e6) continue;
431
+ results.push([px, py]);
432
+ }
433
+ return results;
434
+ }
435
+ function intersectCircleCircle(c1, circ1, c2, circ2, tol) {
436
+ const dx = circ2.cx - circ1.cx;
437
+ const dy = circ2.cy - circ1.cy;
438
+ const d = Math.sqrt(dx * dx + dy * dy);
439
+ if (d > circ1.radius + circ2.radius + tol) return [];
440
+ if (d < Math.abs(circ1.radius - circ2.radius) - tol) return [];
441
+ if (d < 1e-14) return [];
442
+ const a = (circ1.radius * circ1.radius - circ2.radius * circ2.radius + d * d) / (2 * d);
443
+ const h2 = circ1.radius * circ1.radius - a * a;
444
+ const h = Math.sqrt(Math.max(0, h2));
445
+ const mx = circ1.cx + a * dx / d;
446
+ const my = circ1.cy + a * dy / d;
447
+ const candidates = h < tol ? [[mx, my]] : [
448
+ [mx + h * dy / d, my - h * dx / d],
449
+ [mx - h * dy / d, my + h * dx / d]
450
+ ];
451
+ const results = [];
452
+ for (const [px, py] of candidates) {
453
+ const t1 = refineParam(c1, px, py);
454
+ const t2 = refineParam(c2, px, py);
455
+ if (t1 === null || t2 === null) continue;
456
+ const [x1, y1] = evaluateCurve2d(c1, t1);
457
+ const [x2, y2] = evaluateCurve2d(c2, t2);
458
+ const tolSq = (tol * 10) ** 2;
459
+ if ((x1 - px) ** 2 + (y1 - py) ** 2 > tolSq) continue;
460
+ if ((x2 - px) ** 2 + (y2 - py) ** 2 > tolSq) continue;
461
+ results.push([px, py]);
462
+ }
463
+ return results;
464
+ }
465
+ function numericalIntersect(c1, c2, tolerance, isSelf = false) {
466
+ const b1 = curveBounds(c1);
467
+ const b2 = curveBounds(c2);
468
+ if (!isFinite(b1.first) || !isFinite(b1.last) || !isFinite(b2.first) || !isFinite(b2.last)) {
469
+ return { points: [], segments: [] };
470
+ }
471
+ const N = 100;
472
+ const pts1 = [];
473
+ const pts2 = [];
474
+ for (let i = 0; i <= N; i++) {
475
+ const t1 = b1.first + (b1.last - b1.first) * i / N;
476
+ const [x1, y1] = evaluateCurve2d(c1, t1);
477
+ pts1.push({ t: t1, x: x1, y: y1 });
478
+ const t2 = b2.first + (b2.last - b2.first) * i / N;
479
+ const [x2, y2] = evaluateCurve2d(c2, t2);
480
+ pts2.push({ t: t2, x: x2, y: y2 });
481
+ }
482
+ const crossTol = Math.max(tolerance * 100, 0.5);
483
+ const candidates = [];
484
+ for (let i = 0; i < N; i++) {
485
+ const p1a = pts1[i];
486
+ const p1b = pts1[i + 1];
487
+ for (let j = 0; j < N; j++) {
488
+ const p2a = pts2[j];
489
+ const p2b = pts2[j + 1];
490
+ const x1min = Math.min(p1a.x, p1b.x) - crossTol;
491
+ const x1max = Math.max(p1a.x, p1b.x) + crossTol;
492
+ const y1min = Math.min(p1a.y, p1b.y) - crossTol;
493
+ const y1max = Math.max(p1a.y, p1b.y) + crossTol;
494
+ const x2min = Math.min(p2a.x, p2b.x);
495
+ const x2max = Math.max(p2a.x, p2b.x);
496
+ const y2min = Math.min(p2a.y, p2b.y);
497
+ const y2max = Math.max(p2a.y, p2b.y);
498
+ if (x1max < x2min || x2max < x1min || y1max < y2min || y2max < y1min) continue;
499
+ const t1mid = (p1a.t + p1b.t) / 2;
500
+ const t2mid = (p2a.t + p2b.t) / 2;
501
+ if (isSelf && Math.abs(t1mid - t2mid) < (b1.last - b1.first) / 5) continue;
502
+ candidates.push({ t1: t1mid, t2: t2mid });
503
+ }
504
+ }
505
+ const tol2 = tolerance * tolerance;
506
+ const found = [];
507
+ for (const { t1: t1Init, t2: t2Init } of candidates) {
508
+ let t1 = t1Init;
509
+ let t2 = t2Init;
510
+ for (let iter = 0; iter < 20; iter++) {
511
+ const [x12, y12] = evaluateCurve2d(c1, t1);
512
+ const [x22, y22] = evaluateCurve2d(c2, t2);
513
+ const dx = x12 - x22;
514
+ const dy = y12 - y22;
515
+ if (dx * dx + dy * dy < tol2) break;
516
+ const d1 = tangentCurve2d(c1, t1);
517
+ const d2 = tangentCurve2d(c2, t2);
518
+ const det = d1[0] * -d2[1] - -d2[0] * d1[1];
519
+ if (Math.abs(det) < 1e-14) break;
520
+ const dt1 = (-dx * -d2[1] - -dy * -d2[0]) / det;
521
+ const dt2 = (d1[0] * -dy - d1[1] * -dx) / det;
522
+ t1 += dt1;
523
+ t2 += dt2;
524
+ t1 = Math.max(b1.first, Math.min(b1.last, t1));
525
+ t2 = Math.max(b2.first, Math.min(b2.last, t2));
526
+ }
527
+ const [x1, y1] = evaluateCurve2d(c1, t1);
528
+ const [x2, y2] = evaluateCurve2d(c2, t2);
529
+ if (isSelf && Math.abs(t1 - t2) < (b1.last - b1.first) * 0.05) continue;
530
+ if ((x1 - x2) ** 2 + (y1 - y2) ** 2 < tolerance * tolerance * 1e6) {
531
+ const px = (x1 + x2) / 2;
532
+ const py = (y1 + y2) / 2;
533
+ let dup = false;
534
+ for (const [fx, fy] of found) {
535
+ if ((fx - px) ** 2 + (fy - py) ** 2 < tolerance * tolerance * 1e4) {
536
+ dup = true;
537
+ break;
538
+ }
539
+ }
540
+ if (!dup) found.push([px, py]);
541
+ }
542
+ }
543
+ return { points: found, segments: [] };
544
+ }
545
+ function serializeCurve2d(c) {
546
+ return JSON.stringify(c);
547
+ }
548
+ function deserializeCurve2d(data) {
549
+ return JSON.parse(data);
550
+ }
551
+ function createBBox2d() {
552
+ return { __bk2d_bbox: true, xMin: Infinity, yMin: Infinity, xMax: -Infinity, yMax: -Infinity };
553
+ }
554
+ function addCurveToBBox(bbox, c, _tol) {
555
+ const bounds = curveBounds(c);
556
+ if (!isFinite(bounds.first) || !isFinite(bounds.last)) return;
557
+ const nSamples = 20;
558
+ const dt = (bounds.last - bounds.first) / nSamples;
559
+ for (let i = 0; i <= nSamples; i++) {
560
+ const t = bounds.first + i * dt;
561
+ const [x, y] = evaluateCurve2d(c, t);
562
+ if (x < bbox.xMin) bbox.xMin = x;
563
+ if (y < bbox.yMin) bbox.yMin = y;
564
+ if (x > bbox.xMax) bbox.xMax = x;
565
+ if (y > bbox.yMax) bbox.yMax = y;
566
+ }
567
+ }
568
+ function evaluateBezier(poles, t) {
569
+ const n = poles.length;
570
+ const work = poles.map(([x, y]) => [x, y]);
571
+ for (let r = 1; r < n; r++) {
572
+ for (let i = 0; i < n - r; i++) {
573
+ const wi = work[i];
574
+ const wi1 = work[i + 1];
575
+ wi[0] = (1 - t) * wi[0] + t * wi1[0];
576
+ wi[1] = (1 - t) * wi[1] + t * wi1[1];
577
+ }
578
+ }
579
+ return work[0];
580
+ }
581
+ function evaluateBSpline2d(c, t) {
582
+ const fullKnots = [];
583
+ for (let i = 0; i < c.knots.length; i++) {
584
+ const mult = c.multiplicities[i] ?? 1;
585
+ for (let j = 0; j < mult; j++) {
586
+ fullKnots.push(c.knots[i]);
587
+ }
588
+ }
589
+ const p = c.degree;
590
+ const n = c.poles.length;
591
+ const k = fullKnots.length;
592
+ const tClamped = Math.max(fullKnots[p], Math.min(fullKnots[k - p - 1], t));
593
+ let span = p;
594
+ for (let i = p; i < k - p - 1; i++) {
595
+ if (tClamped >= fullKnots[i] && tClamped < fullKnots[i + 1]) {
596
+ span = i;
597
+ break;
598
+ }
599
+ }
600
+ if (tClamped >= fullKnots[k - p - 1]) span = k - p - 2;
601
+ const d = [];
602
+ for (let j = 0; j <= p; j++) {
603
+ const idx = Math.min(span - p + j, n - 1);
604
+ const pole = c.poles[Math.max(0, idx)];
605
+ d.push([pole[0], pole[1]]);
606
+ }
607
+ for (let r = 1; r <= p; r++) {
608
+ for (let j = p; j >= r; j--) {
609
+ const i = span - p + j;
610
+ const left = fullKnots[i] ?? 0;
611
+ const right = fullKnots[i + p - r + 1] ?? 1;
612
+ const denom = right - left;
613
+ const alpha = denom > 1e-15 ? (tClamped - left) / denom : 0;
614
+ const dj = d[j];
615
+ const djPrev = d[j - 1];
616
+ dj[0] = (1 - alpha) * djPrev[0] + alpha * dj[0];
617
+ dj[1] = (1 - alpha) * djPrev[1] + alpha * dj[1];
618
+ }
619
+ }
620
+ return d[p];
621
+ }
622
+ function isBrepkitHandle(shape2) {
623
+ return shape2 !== null && shape2 !== void 0 && typeof shape2 === "object" && shape2.__brepkit;
624
+ }
625
+ const noop = () => {
626
+ };
627
+ function handle(type, id) {
628
+ return {
629
+ __brepkit: true,
630
+ type,
631
+ id,
632
+ delete: noop,
633
+ HashCode(upperBound) {
634
+ return id % upperBound;
635
+ },
636
+ IsNull() {
637
+ return false;
638
+ }
639
+ };
640
+ }
641
+ function solidHandle(id) {
642
+ return handle("solid", id);
643
+ }
644
+ function faceHandle(id) {
645
+ return handle("face", id);
646
+ }
647
+ function edgeHandle(id) {
648
+ return handle("edge", id);
649
+ }
650
+ function wireHandle(id) {
651
+ return handle("wire", id);
652
+ }
653
+ function shellHandle(id) {
654
+ return handle("shell", id);
655
+ }
656
+ function compoundHandle(id) {
657
+ const h = handle("compound", id);
658
+ if (syntheticCompounds.has(id)) {
659
+ return { ...h, delete: () => syntheticCompounds.delete(id) };
660
+ }
661
+ return h;
662
+ }
663
+ function vertexHandle(id) {
664
+ return handle("vertex", id);
665
+ }
666
+ function unwrap(shape2, expected) {
667
+ if (!isBrepkitHandle(shape2)) {
668
+ throw new Error("brepkit: expected a BrepkitHandle, got " + typeof shape2);
669
+ }
670
+ if (expected && shape2.type !== expected) {
671
+ throw new Error(`brepkit: expected ${expected} handle, got ${shape2.type}`);
672
+ }
673
+ return shape2.id;
674
+ }
675
+ function toArray(ids) {
676
+ return Array.from(ids);
677
+ }
678
+ function unwrapSolidOrThrow(shape2, methodName) {
679
+ if (!isBrepkitHandle(shape2)) {
680
+ throw new Error("brepkit: expected a BrepkitHandle, got " + typeof shape2);
681
+ }
682
+ if (shape2.type !== "solid") {
683
+ throw new Error(
684
+ `brepkit: ${methodName} requires a solid, got ${shape2.type}. Consider using makeCompound() to combine shapes first.`
685
+ );
686
+ }
687
+ return shape2.id;
688
+ }
689
+ function unwrapSolidsForExport(bk, shape2, methodName) {
690
+ if (!isBrepkitHandle(shape2)) {
691
+ throw new Error("brepkit: expected a BrepkitHandle, got " + typeof shape2);
692
+ }
693
+ if (shape2.type === "solid") {
694
+ return [shape2.id];
695
+ }
696
+ if (shape2.type === "compound") {
697
+ const ids = toArray(bk.getCompoundSolids(shape2.id));
698
+ if (ids.length > 0) return ids;
699
+ throw new Error(`brepkit: ${methodName} received a compound with no solids.`);
700
+ }
701
+ throw new Error(
702
+ `brepkit: ${methodName} requires a solid or compound of solids, got ${shape2.type}.`
703
+ );
704
+ }
705
+ function dist3(x1, y1, z1, x2, y2, z2) {
706
+ const dx = x1 - x2, dy = y1 - y2, dz = z1 - z2;
707
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
708
+ }
709
+ function translationMatrix(x, y, z) {
710
+ return [
711
+ 1,
712
+ 0,
713
+ 0,
714
+ x,
715
+ 0,
716
+ 1,
717
+ 0,
718
+ y,
719
+ 0,
720
+ 0,
721
+ 1,
722
+ z,
723
+ 0,
724
+ 0,
725
+ 0,
726
+ 1
727
+ ];
728
+ }
729
+ function rotationMatrix(angleDeg, axis = [0, 0, 1], center = [0, 0, 0]) {
730
+ const rad = angleDeg * Math.PI / 180;
731
+ const c = Math.cos(rad);
732
+ const s = Math.sin(rad);
733
+ const t = 1 - c;
734
+ const len = Math.sqrt(axis[0] ** 2 + axis[1] ** 2 + axis[2] ** 2);
735
+ const [ux, uy, uz] = [axis[0] / len, axis[1] / len, axis[2] / len];
736
+ const r00 = t * ux * ux + c;
737
+ const r01 = t * ux * uy - s * uz;
738
+ const r02 = t * ux * uz + s * uy;
739
+ const r10 = t * uy * ux + s * uz;
740
+ const r11 = t * uy * uy + c;
741
+ const r12 = t * uy * uz - s * ux;
742
+ const r20 = t * uz * ux - s * uy;
743
+ const r21 = t * uz * uy + s * ux;
744
+ const r22 = t * uz * uz + c;
745
+ const [cx, cy, cz] = center;
746
+ const tx = cx - (r00 * cx + r01 * cy + r02 * cz);
747
+ const ty = cy - (r10 * cx + r11 * cy + r12 * cz);
748
+ const tz = cz - (r20 * cx + r21 * cy + r22 * cz);
749
+ return [
750
+ r00,
751
+ r01,
752
+ r02,
753
+ tx,
754
+ r10,
755
+ r11,
756
+ r12,
757
+ ty,
758
+ r20,
759
+ r21,
760
+ r22,
761
+ tz,
762
+ 0,
763
+ 0,
764
+ 0,
765
+ 1
766
+ ];
767
+ }
768
+ function scaleMatrix(center, factor) {
769
+ const [cx, cy, cz] = center;
770
+ const tx = cx * (1 - factor);
771
+ const ty = cy * (1 - factor);
772
+ const tz = cz * (1 - factor);
773
+ return [
774
+ factor,
775
+ 0,
776
+ 0,
777
+ tx,
778
+ 0,
779
+ factor,
780
+ 0,
781
+ ty,
782
+ 0,
783
+ 0,
784
+ factor,
785
+ tz,
786
+ 0,
787
+ 0,
788
+ 0,
789
+ 1
790
+ ];
791
+ }
792
+ function affineMatrix(linear, translation) {
793
+ return [
794
+ linear[0],
795
+ linear[1],
796
+ linear[2],
797
+ translation[0],
798
+ linear[3],
799
+ linear[4],
800
+ linear[5],
801
+ translation[1],
802
+ linear[6],
803
+ linear[7],
804
+ linear[8],
805
+ translation[2],
806
+ 0,
807
+ 0,
808
+ 0,
809
+ 1
810
+ ];
811
+ }
812
+ function mirrorMatrix(origin, normal) {
813
+ const [ox, oy, oz] = origin;
814
+ const len = Math.sqrt(normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2);
815
+ const nx = normal[0] / len;
816
+ const ny = normal[1] / len;
817
+ const nz = normal[2] / len;
818
+ const d = 2 * (ox * nx + oy * ny + oz * nz);
819
+ return [
820
+ 1 - 2 * nx * nx,
821
+ -2 * nx * ny,
822
+ -2 * nx * nz,
823
+ d * nx,
824
+ -2 * ny * nx,
825
+ 1 - 2 * ny * ny,
826
+ -2 * ny * nz,
827
+ d * ny,
828
+ -2 * nz * nx,
829
+ -2 * nz * ny,
830
+ 1 - 2 * nz * nz,
831
+ d * nz,
832
+ 0,
833
+ 0,
834
+ 0,
835
+ 1
836
+ ];
837
+ }
838
+ const DEFAULT_DEFLECTION = 0.01;
839
+ const DEFAULT_SEGMENTS = 32;
840
+ let syntheticCompoundCounter = 9e5;
841
+ const syntheticCompounds = /* @__PURE__ */ new Map();
842
+ const _warned = /* @__PURE__ */ new Set();
843
+ function warnOnce(key, message) {
844
+ if (_warned.has(key)) return;
845
+ _warned.add(key);
846
+ console.warn(`brepkit: ${message}`);
847
+ }
848
+ function hasBooleanOptions(opts) {
849
+ return opts.optimisation !== void 0 || opts.simplify !== void 0 || opts.strategy !== void 0 || opts.fuzzyValue !== void 0;
850
+ }
851
+ class BrepkitAdapter {
852
+ oc;
853
+ kernelId = "brepkit";
854
+ /** The underlying brepkit WASM kernel instance (typed). */
855
+ bk;
856
+ constructor(brepkitKernel) {
857
+ this.bk = brepkitKernel;
858
+ this.oc = brepkitKernel;
859
+ }
860
+ // ═══════════════════════════════════════════════════════════════════════
861
+ // Boolean operations
862
+ // ═══════════════════════════════════════════════════════════════════════
863
+ fuse(shape2, tool, _options) {
864
+ if (_options && hasBooleanOptions(_options)) {
865
+ warnOnce(
866
+ "boolean-options",
867
+ "BooleanOptions (optimisation, simplify, strategy, fuzzyValue) not supported; ignored."
868
+ );
869
+ }
870
+ const baseId = unwrapSolidOrThrow(shape2, "fuse");
871
+ const toolHandle = tool;
872
+ if (toolHandle.type === "compound") {
873
+ const toolSolidIds = toArray(this.bk.getCompoundSolids(toolHandle.id));
874
+ let currentId = baseId;
875
+ for (const toolSolidId of toolSolidIds) {
876
+ currentId = this.bk.fuse(currentId, toolSolidId);
877
+ }
878
+ return solidHandle(currentId);
879
+ }
880
+ const result2 = this.bk.fuse(baseId, unwrapSolidOrThrow(tool, "fuse"));
881
+ return solidHandle(result2);
882
+ }
883
+ cut(shape2, tool, _options) {
884
+ if (_options && hasBooleanOptions(_options)) {
885
+ warnOnce(
886
+ "boolean-options",
887
+ "BooleanOptions (optimisation, simplify, strategy, fuzzyValue) not supported; ignored."
888
+ );
889
+ }
890
+ const baseId = unwrapSolidOrThrow(shape2, "cut");
891
+ const toolHandle = tool;
892
+ if (toolHandle.type === "compound") {
893
+ const toolSolidIds = toArray(this.bk.getCompoundSolids(toolHandle.id));
894
+ let currentId = baseId;
895
+ for (const toolSolidId of toolSolidIds) {
896
+ currentId = this.bk.cut(currentId, toolSolidId);
897
+ }
898
+ return solidHandle(currentId);
899
+ }
900
+ const result2 = this.bk.cut(baseId, unwrapSolidOrThrow(tool, "cut"));
901
+ return solidHandle(result2);
902
+ }
903
+ intersect(shape2, tool, _options) {
904
+ if (_options && hasBooleanOptions(_options)) {
905
+ warnOnce(
906
+ "boolean-options",
907
+ "BooleanOptions (optimisation, simplify, strategy, fuzzyValue) not supported; ignored."
908
+ );
909
+ }
910
+ const result2 = this.bk.intersect(
911
+ unwrapSolidOrThrow(shape2, "intersect"),
912
+ unwrapSolidOrThrow(tool, "intersect")
913
+ );
914
+ return solidHandle(result2);
915
+ }
916
+ section(shape2, plane, _approximation) {
917
+ const { point, normal } = this.extractPlaneFromFace(plane);
918
+ const solidId = isBrepkitHandle(shape2) && shape2.type === "solid" ? shape2.id : unwrap(shape2, "solid");
919
+ const faceIds = toArray(
920
+ this.bk.section(solidId, point[0], point[1], point[2], normal[0], normal[1], normal[2])
921
+ );
922
+ if (faceIds.length === 0) {
923
+ return compoundHandle(this.bk.makeCompound([]));
924
+ }
925
+ const firstWireId = this.bk.getFaceOuterWire(faceIds[0]);
926
+ return wireHandle(firstWireId);
927
+ }
928
+ fuseAll(shapes, options) {
929
+ if (shapes.length === 0) throw new Error("brepkit: fuseAll requires at least one shape");
930
+ if (shapes.length === 1) return shapes[0];
931
+ let current = [...shapes];
932
+ while (current.length > 1) {
933
+ const next = [];
934
+ for (let i = 0; i < current.length; i += 2) {
935
+ if (i + 1 < current.length) {
936
+ next.push(this.fuse(current[i], current[i + 1], options));
937
+ } else {
938
+ next.push(current[i]);
939
+ }
940
+ }
941
+ current = next;
942
+ }
943
+ return current[0];
944
+ }
945
+ cutAll(shape2, tools, options) {
946
+ let result2 = shape2;
947
+ for (const tool of tools) {
948
+ result2 = this.cut(result2, tool, options);
949
+ }
950
+ return result2;
951
+ }
952
+ split(shape2, tools) {
953
+ if (tools.length === 0) throw new Error("brepkit: split requires at least one tool");
954
+ const { point, normal } = this.extractPlaneFromFace(tools[0]);
955
+ const result2 = toArray(
956
+ this.bk.split(
957
+ unwrap(shape2, "solid"),
958
+ point[0],
959
+ point[1],
960
+ point[2],
961
+ normal[0],
962
+ normal[1],
963
+ normal[2]
964
+ )
965
+ );
966
+ return compoundHandle(this.bk.makeCompound(result2));
967
+ }
968
+ // ═══════════════════════════════════════════════════════════════════════
969
+ // Convex hull (not yet implemented)
970
+ // ═══════════════════════════════════════════════════════════════════════
971
+ hull(shapes, _tolerance) {
972
+ const coords = [];
973
+ for (const shape2 of shapes) {
974
+ const h = shape2;
975
+ if (h.type === "solid") {
976
+ const vertIds = toArray(this.bk.getSolidVertices(h.id));
977
+ for (const vid of vertIds) {
978
+ const pos = this.bk.getVertexPosition(vid);
979
+ coords.push(pos[0], pos[1], pos[2]);
980
+ }
981
+ } else if (h.type === "vertex") {
982
+ const pos = this.bk.getVertexPosition(h.id);
983
+ coords.push(pos[0], pos[1], pos[2]);
984
+ }
985
+ }
986
+ if (coords.length < 12) throw new Error("brepkit: hull requires enough points");
987
+ const id = this.bk.convexHull(coords);
988
+ return solidHandle(id);
989
+ }
990
+ hullFromPoints(points, _tolerance) {
991
+ if (points.length < 4) throw new Error("brepkit: hull needs at least 4 points");
992
+ const coords = [];
993
+ for (const p of points) {
994
+ coords.push(p.x, p.y, p.z);
995
+ }
996
+ const id = this.bk.convexHull(coords);
997
+ return solidHandle(id);
998
+ }
999
+ buildSolidFromFaces(points, faces, _tolerance) {
1000
+ const positions = new Float64Array(points.length * 3);
1001
+ for (let i = 0; i < points.length; i++) {
1002
+ const p = points[i];
1003
+ positions[i * 3] = p.x;
1004
+ positions[i * 3 + 1] = p.y;
1005
+ positions[i * 3 + 2] = p.z;
1006
+ }
1007
+ const indices = new Uint32Array(faces.length * 3);
1008
+ for (let i = 0; i < faces.length; i++) {
1009
+ const f = faces[i];
1010
+ indices[i * 3] = f[0];
1011
+ indices[i * 3 + 1] = f[1];
1012
+ indices[i * 3 + 2] = f[2];
1013
+ }
1014
+ const id = this.bk.importIndexedMesh(positions, indices);
1015
+ return solidHandle(id);
1016
+ }
1017
+ // ═══════════════════════════════════════════════════════════════════════
1018
+ // Shape construction
1019
+ // ═══════════════════════════════════════════════════════════════════════
1020
+ makeVertex(x, y, z) {
1021
+ const id = this.bk.makeVertex(x, y, z);
1022
+ return vertexHandle(id);
1023
+ }
1024
+ makeEdge(curve, start, end) {
1025
+ if (curve && typeof curve === "object" && "origin" in curve && "direction" in curve) {
1026
+ const { origin, direction } = curve;
1027
+ const t0 = start ?? 0;
1028
+ const t1 = end ?? 1;
1029
+ return this.makeLineEdge(
1030
+ [
1031
+ origin[0] + direction[0] * t0,
1032
+ origin[1] + direction[1] * t0,
1033
+ origin[2] + direction[2] * t0
1034
+ ],
1035
+ [
1036
+ origin[0] + direction[0] * t1,
1037
+ origin[1] + direction[1] * t1,
1038
+ origin[2] + direction[2] * t1
1039
+ ]
1040
+ );
1041
+ }
1042
+ if (isBrepkitHandle(curve) && curve.type === "edge") {
1043
+ return curve;
1044
+ }
1045
+ throw new Error("brepkit: makeEdge requires a curve with origin/direction, or an edge handle");
1046
+ }
1047
+ makeWire(edges) {
1048
+ const edgeIds = [];
1049
+ for (const e of edges) {
1050
+ const h = e;
1051
+ if (h.type === "wire") {
1052
+ for (const childEdgeId of toArray(this.bk.getWireEdges(h.id))) {
1053
+ edgeIds.push(childEdgeId);
1054
+ }
1055
+ } else {
1056
+ edgeIds.push(unwrap(e, "edge"));
1057
+ }
1058
+ }
1059
+ const id = this.bk.makeWire(edgeIds, true);
1060
+ return wireHandle(id);
1061
+ }
1062
+ makeFace(wire, _planar) {
1063
+ const h = wire;
1064
+ if (h.type === "edge") {
1065
+ const wireId = this.bk.makeWire([h.id], true);
1066
+ const id2 = this.bk.makeFaceFromWire(wireId);
1067
+ return faceHandle(id2);
1068
+ }
1069
+ const id = this.bk.makeFaceFromWire(unwrap(wire, "wire"));
1070
+ return faceHandle(id);
1071
+ }
1072
+ makeBox(width, height, depth) {
1073
+ const id = this.bk.makeBox(width, height, depth);
1074
+ return solidHandle(id);
1075
+ }
1076
+ makeRectangle(width, height) {
1077
+ const id = this.bk.makeRectangle(width, height);
1078
+ return faceHandle(id);
1079
+ }
1080
+ makeCylinder(radius, height, center, direction) {
1081
+ const id = this.bk.makeCylinder(radius, height);
1082
+ const sh = solidHandle(id);
1083
+ if (this.needsTransform(center, direction)) {
1084
+ return this.transformToPlacement(sh, center, direction);
1085
+ }
1086
+ return sh;
1087
+ }
1088
+ makeSphere(radius, center) {
1089
+ const id = this.bk.makeSphere(radius, DEFAULT_SEGMENTS);
1090
+ const sh = solidHandle(id);
1091
+ if (center && (center[0] !== 0 || center[1] !== 0 || center[2] !== 0)) {
1092
+ return this.translate(sh, center[0], center[1], center[2]);
1093
+ }
1094
+ return sh;
1095
+ }
1096
+ makeCone(radius1, radius2, height, center, direction) {
1097
+ const id = this.bk.makeCone(radius1, radius2, height);
1098
+ const sh = solidHandle(id);
1099
+ if (this.needsTransform(center, direction)) {
1100
+ return this.transformToPlacement(sh, center, direction);
1101
+ }
1102
+ return sh;
1103
+ }
1104
+ makeTorus(majorRadius, minorRadius, center, direction) {
1105
+ const id = this.bk.makeTorus(majorRadius, minorRadius, DEFAULT_SEGMENTS);
1106
+ const sh = solidHandle(id);
1107
+ if (this.needsTransform(center, direction)) {
1108
+ return this.transformToPlacement(sh, center, direction);
1109
+ }
1110
+ return sh;
1111
+ }
1112
+ makeEllipsoid(aLength, bLength, cLength) {
1113
+ const maxR = Math.max(aLength, bLength, cLength);
1114
+ const sphere = this.makeSphere(maxR);
1115
+ const scaleX = aLength / maxR;
1116
+ const scaleY = bLength / maxR;
1117
+ const scaleZ = cLength / maxR;
1118
+ return this.generalTransform(
1119
+ sphere,
1120
+ [scaleX, 0, 0, 0, scaleY, 0, 0, 0, scaleZ],
1121
+ [0, 0, 0],
1122
+ false
1123
+ );
1124
+ }
1125
+ // --- Extended construction ---
1126
+ makeLineEdge(p1, p2) {
1127
+ const id = this.bk.makeLineEdge(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]);
1128
+ return edgeHandle(id);
1129
+ }
1130
+ makeCircleEdge(center, normal, radius) {
1131
+ return this.makeCircleNurbs(center, normal, radius, 0, 2 * Math.PI);
1132
+ }
1133
+ makeCircleArc(center, normal, radius, startAngle, endAngle) {
1134
+ return this.makeCircleNurbs(center, normal, radius, startAngle, endAngle);
1135
+ }
1136
+ makeArcEdge(p1, p2, p3) {
1137
+ const ab = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
1138
+ const ac = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
1139
+ const normal = [
1140
+ ab[1] * ac[2] - ab[2] * ac[1],
1141
+ ab[2] * ac[0] - ab[0] * ac[2],
1142
+ ab[0] * ac[1] - ab[1] * ac[0]
1143
+ ];
1144
+ const nLen = Math.sqrt(normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2);
1145
+ if (nLen < 1e-12) {
1146
+ return this.makeLineEdge(p1, p3);
1147
+ }
1148
+ const nz = [normal[0] / nLen, normal[1] / nLen, normal[2] / nLen];
1149
+ const abLen = Math.sqrt(ab[0] ** 2 + ab[1] ** 2 + ab[2] ** 2);
1150
+ const ux = [ab[0] / abLen, ab[1] / abLen, ab[2] / abLen];
1151
+ const uy = [
1152
+ nz[1] * ux[2] - nz[2] * ux[1],
1153
+ nz[2] * ux[0] - nz[0] * ux[2],
1154
+ nz[0] * ux[1] - nz[1] * ux[0]
1155
+ ];
1156
+ const proj = (p) => {
1157
+ const dx = p[0] - p1[0], dy = p[1] - p1[1], dz = p[2] - p1[2];
1158
+ return [dx * ux[0] + dy * ux[1] + dz * ux[2], dx * uy[0] + dy * uy[1] + dz * uy[2]];
1159
+ };
1160
+ const [ax2, ay2] = proj(p1);
1161
+ const [bx2, by2] = proj(p2);
1162
+ const [cx2, cy2] = proj(p3);
1163
+ const d = 2 * (ax2 * (by2 - cy2) + bx2 * (cy2 - ay2) + cx2 * (ay2 - by2));
1164
+ if (Math.abs(d) < 1e-12) {
1165
+ return this.makeLineEdge(p1, p3);
1166
+ }
1167
+ const ccx = ((ax2 ** 2 + ay2 ** 2) * (by2 - cy2) + (bx2 ** 2 + by2 ** 2) * (cy2 - ay2) + (cx2 ** 2 + cy2 ** 2) * (ay2 - by2)) / d;
1168
+ const ccy = ((ax2 ** 2 + ay2 ** 2) * (cx2 - bx2) + (bx2 ** 2 + by2 ** 2) * (ax2 - cx2) + (cx2 ** 2 + cy2 ** 2) * (bx2 - ax2)) / d;
1169
+ const center = [
1170
+ p1[0] + ccx * ux[0] + ccy * uy[0],
1171
+ p1[1] + ccx * ux[1] + ccy * uy[1],
1172
+ p1[2] + ccx * ux[2] + ccy * uy[2]
1173
+ ];
1174
+ const radius = Math.sqrt(
1175
+ (p1[0] - center[0]) ** 2 + (p1[1] - center[1]) ** 2 + (p1[2] - center[2]) ** 2
1176
+ );
1177
+ const lx = [p1[0] - center[0], p1[1] - center[1], p1[2] - center[2]];
1178
+ const lxLen = Math.sqrt(lx[0] ** 2 + lx[1] ** 2 + lx[2] ** 2);
1179
+ const uxA = [lx[0] / lxLen, lx[1] / lxLen, lx[2] / lxLen];
1180
+ const uyA = [
1181
+ nz[1] * uxA[2] - nz[2] * uxA[1],
1182
+ nz[2] * uxA[0] - nz[0] * uxA[2],
1183
+ nz[0] * uxA[1] - nz[1] * uxA[0]
1184
+ ];
1185
+ const v3 = [p3[0] - center[0], p3[1] - center[1], p3[2] - center[2]];
1186
+ const dotX = v3[0] * uxA[0] + v3[1] * uxA[1] + v3[2] * uxA[2];
1187
+ const dotY = v3[0] * uyA[0] + v3[1] * uyA[1] + v3[2] * uyA[2];
1188
+ let endAngle = Math.atan2(dotY, dotX);
1189
+ if (endAngle <= 0) endAngle += 2 * Math.PI;
1190
+ return this.makeCircleNurbs(center, normal, radius, 0, endAngle);
1191
+ }
1192
+ makeEllipseEdge(center, normal, majorRadius, minorRadius, xDir) {
1193
+ return this.makeEllipseNurbs(center, normal, majorRadius, minorRadius, 0, 2 * Math.PI, xDir);
1194
+ }
1195
+ makeEllipseArc(center, normal, majorRadius, minorRadius, startAngle, endAngle, xDir) {
1196
+ return this.makeEllipseNurbs(
1197
+ center,
1198
+ normal,
1199
+ majorRadius,
1200
+ minorRadius,
1201
+ startAngle,
1202
+ endAngle,
1203
+ xDir
1204
+ );
1205
+ }
1206
+ makeBezierEdge(points) {
1207
+ if (points.length < 2) throw new Error("brepkit: bezier requires at least 2 points");
1208
+ const degree = points.length - 1;
1209
+ const n = points.length;
1210
+ const knots = [...Array(degree + 1).fill(0), ...Array(degree + 1).fill(1)];
1211
+ const weights = Array(n).fill(1);
1212
+ const flatCp = points.flatMap(([x, y, z]) => [x, y, z]);
1213
+ const startPt = points[0];
1214
+ const endPt = points[n - 1];
1215
+ const id = this.bk.makeNurbsEdge(
1216
+ startPt[0],
1217
+ startPt[1],
1218
+ startPt[2],
1219
+ endPt[0],
1220
+ endPt[1],
1221
+ endPt[2],
1222
+ degree,
1223
+ knots,
1224
+ flatCp,
1225
+ weights
1226
+ );
1227
+ return edgeHandle(id);
1228
+ }
1229
+ makeTangentArc(startPoint, startTangent, endPoint) {
1230
+ const cp1 = [
1231
+ startPoint[0] + startTangent[0] / 3,
1232
+ startPoint[1] + startTangent[1] / 3,
1233
+ startPoint[2] + startTangent[2] / 3
1234
+ ];
1235
+ const dx = endPoint[0] - cp1[0];
1236
+ const dy = endPoint[1] - cp1[1];
1237
+ const dz = endPoint[2] - cp1[2];
1238
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
1239
+ const cp2 = [
1240
+ endPoint[0] - dx / (3 * Math.max(len, 1e-10)),
1241
+ endPoint[1] - dy / (3 * Math.max(len, 1e-10)),
1242
+ endPoint[2] - dz / (3 * Math.max(len, 1e-10))
1243
+ ];
1244
+ return this.makeBezierEdge([startPoint, cp1, cp2, endPoint]);
1245
+ }
1246
+ makeHelixWire(pitch, height, radius, center, _direction, leftHanded) {
1247
+ const turns = height / pitch;
1248
+ const nSamplesPerTurn = 16;
1249
+ const nSamples = Math.max(4, Math.ceil(turns * nSamplesPerTurn));
1250
+ const cx = center?.[0] ?? 0;
1251
+ const cy = center?.[1] ?? 0;
1252
+ const cz = center?.[2] ?? 0;
1253
+ const sign = leftHanded ? -1 : 1;
1254
+ const points = [];
1255
+ for (let i = 0; i <= nSamples; i++) {
1256
+ const t = i / nSamples;
1257
+ const angle = sign * 2 * Math.PI * turns * t;
1258
+ points.push([cx + radius * Math.cos(angle), cy + radius * Math.sin(angle), cz + height * t]);
1259
+ }
1260
+ const edge = this.interpolatePoints(points);
1261
+ return this.makeWire([edge]);
1262
+ }
1263
+ makeWireFromMixed(items) {
1264
+ const edgeIds = [];
1265
+ for (const item of items) {
1266
+ const h = item;
1267
+ if (h.type === "edge") {
1268
+ edgeIds.push(h.id);
1269
+ } else if (h.type === "wire") {
1270
+ for (const childEdgeId of toArray(this.bk.getWireEdges(h.id))) {
1271
+ edgeIds.push(childEdgeId);
1272
+ }
1273
+ }
1274
+ }
1275
+ if (edgeIds.length === 0)
1276
+ throw new Error("brepkit: makeWireFromMixed requires at least one edge");
1277
+ const id = this.bk.makeWire(edgeIds, false);
1278
+ return wireHandle(id);
1279
+ }
1280
+ makeCompound(shapes) {
1281
+ const handles = shapes.filter(isBrepkitHandle);
1282
+ if (handles.length === 0) {
1283
+ throw new Error("brepkit: makeCompound requires at least one shape");
1284
+ }
1285
+ const allSolids = handles.every((h) => h.type === "solid");
1286
+ if (allSolids) {
1287
+ const id2 = this.bk.makeCompound(handles.map((h) => h.id));
1288
+ return compoundHandle(id2);
1289
+ }
1290
+ const id = syntheticCompoundCounter++;
1291
+ syntheticCompounds.set(id, handles);
1292
+ return compoundHandle(id);
1293
+ }
1294
+ makeBoxFromCorners(p1, p2) {
1295
+ const w = Math.abs(p2[0] - p1[0]);
1296
+ const h = Math.abs(p2[1] - p1[1]);
1297
+ const d = Math.abs(p2[2] - p1[2]);
1298
+ const box = this.makeBox(w, h, d);
1299
+ const minX = Math.min(p1[0], p2[0]);
1300
+ const minY = Math.min(p1[1], p2[1]);
1301
+ const minZ = Math.min(p1[2], p2[2]);
1302
+ if (minX !== 0 || minY !== 0 || minZ !== 0) {
1303
+ return this.translate(box, minX, minY, minZ);
1304
+ }
1305
+ return box;
1306
+ }
1307
+ solidFromShell(shell2) {
1308
+ const h = shell2;
1309
+ if (h.type === "solid") return shell2;
1310
+ if (h.type === "shell") {
1311
+ try {
1312
+ this.bk.getSolidFaces(h.id);
1313
+ return solidHandle(h.id);
1314
+ } catch {
1315
+ }
1316
+ const id2 = this.bk.solidFromShell(h.id);
1317
+ return solidHandle(id2);
1318
+ }
1319
+ const id = this.bk.solidFromShell(unwrap(shell2, "shell"));
1320
+ return solidHandle(id);
1321
+ }
1322
+ // ═══════════════════════════════════════════════════════════════════════
1323
+ // Extrusion / sweep / loft / revolution
1324
+ // ═══════════════════════════════════════════════════════════════════════
1325
+ extrude(face, direction, length) {
1326
+ const id = this.bk.extrude(
1327
+ unwrap(face, "face"),
1328
+ direction[0],
1329
+ direction[1],
1330
+ direction[2],
1331
+ length
1332
+ );
1333
+ return solidHandle(id);
1334
+ }
1335
+ revolve(shape2, axis, angle) {
1336
+ if (axis && typeof axis === "object" && "origin" in axis && "direction" in axis) {
1337
+ const { origin, direction } = axis;
1338
+ let angleDeg = angle * (180 / Math.PI);
1339
+ if (angleDeg > 360) angleDeg = 360;
1340
+ const id = this.bk.revolve(
1341
+ unwrap(shape2, "face"),
1342
+ origin[0],
1343
+ origin[1],
1344
+ origin[2],
1345
+ direction[0],
1346
+ direction[1],
1347
+ direction[2],
1348
+ angleDeg
1349
+ );
1350
+ return solidHandle(id);
1351
+ }
1352
+ throw new Error("brepkit: revolve requires axis with origin and direction");
1353
+ }
1354
+ revolveVec(shape2, center, direction, angle) {
1355
+ let angleDeg = angle * (180 / Math.PI);
1356
+ if (angleDeg > 360) angleDeg = 360;
1357
+ const id = this.bk.revolve(
1358
+ unwrap(shape2, "face"),
1359
+ center[0],
1360
+ center[1],
1361
+ center[2],
1362
+ direction[0],
1363
+ direction[1],
1364
+ direction[2],
1365
+ angleDeg
1366
+ );
1367
+ return solidHandle(id);
1368
+ }
1369
+ loft(wires, _ruled, _startShape, _endShape) {
1370
+ if (_ruled !== void 0 || _startShape !== void 0 || _endShape !== void 0) {
1371
+ warnOnce(
1372
+ "loft-options",
1373
+ "Loft options (ruled, startShape, endShape) not supported; ignored."
1374
+ );
1375
+ }
1376
+ const faceIds = wires.map((w) => {
1377
+ const h = w;
1378
+ if (h.type === "wire") {
1379
+ return this.bk.makeFaceFromWire(h.id);
1380
+ }
1381
+ return unwrap(w, "face");
1382
+ });
1383
+ const id = this.bk.loft(faceIds);
1384
+ return solidHandle(id);
1385
+ }
1386
+ sweep(wire, spine, _options) {
1387
+ if (_options?.transitionMode !== void 0) {
1388
+ warnOnce("sweep-transition", "Sweep transition mode not supported; ignored.");
1389
+ }
1390
+ const spineHandle = spine;
1391
+ if (spineHandle.type === "wire") {
1392
+ const edges = this.iterShapes(spine, "edge");
1393
+ const edgeIds = edges.map((e) => unwrap(e, "edge"));
1394
+ const id2 = this.bk.sweepAlongEdges(unwrap(wire, "face"), edgeIds);
1395
+ return solidHandle(id2);
1396
+ }
1397
+ const nurbsData = this.extractNurbsFromEdge(spine);
1398
+ if (!nurbsData) {
1399
+ throw new Error("brepkit: sweep spine must be an edge or wire");
1400
+ }
1401
+ const id = this.bk.sweep(
1402
+ unwrap(wire, "face"),
1403
+ nurbsData.degree,
1404
+ nurbsData.knots,
1405
+ nurbsData.controlPoints,
1406
+ nurbsData.weights
1407
+ );
1408
+ return solidHandle(id);
1409
+ }
1410
+ simplePipe(profile, spine) {
1411
+ const profileHandle = profile;
1412
+ const faceId = profileHandle.type === "wire" ? this.bk.makeFaceFromWire(profileHandle.id) : unwrap(profile, "face");
1413
+ const spineHandle = spine;
1414
+ if (spineHandle.type === "wire") {
1415
+ const edges = this.iterShapes(spine, "edge");
1416
+ const edgeIds = edges.map((e) => unwrap(e, "edge"));
1417
+ const id2 = this.bk.sweepAlongEdges(faceId, edgeIds);
1418
+ return solidHandle(id2);
1419
+ }
1420
+ const nurbsData = this.extractNurbsFromEdge(spine);
1421
+ if (!nurbsData) {
1422
+ throw new Error("brepkit: pipe spine must be an edge or wire");
1423
+ }
1424
+ const id = this.bk.pipe(
1425
+ faceId,
1426
+ nurbsData.degree,
1427
+ nurbsData.knots,
1428
+ nurbsData.controlPoints,
1429
+ nurbsData.weights
1430
+ );
1431
+ return solidHandle(id);
1432
+ }
1433
+ // ═══════════════════════════════════════════════════════════════════════
1434
+ // Modification
1435
+ // ═══════════════════════════════════════════════════════════════════════
1436
+ fillet(shape2, edges, radius) {
1437
+ const r = typeof radius === "number" ? radius : Array.isArray(radius) ? radius[0] : 1;
1438
+ if (typeof radius !== "number") {
1439
+ warnOnce(
1440
+ "fillet-variable",
1441
+ typeof radius === "function" ? "Per-edge fillet radius function not supported; falling back to radius=1." : "Variable-radius fillet not supported; using first radius only."
1442
+ );
1443
+ }
1444
+ const edgeIds = edges.map((e) => unwrap(e, "edge"));
1445
+ const id = this.bk.fillet(unwrapSolidOrThrow(shape2, "fillet"), edgeIds, r);
1446
+ return solidHandle(id);
1447
+ }
1448
+ chamfer(shape2, edges, distance) {
1449
+ const d = typeof distance === "number" ? distance : Array.isArray(distance) ? distance[0] : 1;
1450
+ if (typeof distance !== "number") {
1451
+ warnOnce(
1452
+ "chamfer-asymmetric",
1453
+ typeof distance === "function" ? "Per-edge chamfer distance function not supported; falling back to distance=1." : "Asymmetric chamfer not supported; using first distance only."
1454
+ );
1455
+ }
1456
+ const edgeIds = edges.map((e) => unwrap(e, "edge"));
1457
+ const id = this.bk.chamfer(unwrapSolidOrThrow(shape2, "chamfer"), edgeIds, d);
1458
+ return solidHandle(id);
1459
+ }
1460
+ chamferDistAngle(shape2, edges, distance, angleDeg) {
1461
+ warnOnce("chamfer-dist-angle", "Distance-angle chamfer approximated as uniform chamfer.");
1462
+ const d2 = distance * Math.tan(angleDeg * Math.PI / 180);
1463
+ const avgDist = (distance + d2) / 2;
1464
+ return this.chamfer(shape2, edges, avgDist);
1465
+ }
1466
+ shell(shape2, faces, thickness, tolerance) {
1467
+ if (tolerance !== void 0) {
1468
+ warnOnce(
1469
+ "shell-tolerance",
1470
+ "shell() tolerance parameter is not supported; brepkit uses its own internal tolerance."
1471
+ );
1472
+ }
1473
+ const solidId = unwrapSolidOrThrow(shape2, "shell");
1474
+ const solidFaces = toArray(this.bk.getSolidFaces(solidId));
1475
+ const solidFaceSet = new Set(solidFaces);
1476
+ const resolvedFaceIds = faces.map((f) => {
1477
+ const fid = unwrap(f, "face");
1478
+ if (solidFaceSet.has(fid)) return fid;
1479
+ try {
1480
+ const origNormal = this.bk.getFaceNormal(fid);
1481
+ let bestMatch = -1;
1482
+ let bestDot = -2;
1483
+ for (const sf of solidFaces) {
1484
+ try {
1485
+ const sn = this.bk.getFaceNormal(sf);
1486
+ const dot = (origNormal[0] ?? 0) * (sn[0] ?? 0) + (origNormal[1] ?? 0) * (sn[1] ?? 0) + (origNormal[2] ?? 0) * (sn[2] ?? 0);
1487
+ if (dot > bestDot) {
1488
+ bestDot = dot;
1489
+ bestMatch = sf;
1490
+ }
1491
+ } catch {
1492
+ }
1493
+ }
1494
+ if (bestMatch >= 0 && bestDot > 0.99) return bestMatch;
1495
+ } catch {
1496
+ }
1497
+ return fid;
1498
+ });
1499
+ const id = this.bk.shell(solidId, thickness, resolvedFaceIds);
1500
+ return solidHandle(id);
1501
+ }
1502
+ thicken(shape2, thickness) {
1503
+ const h = shape2;
1504
+ if (h.type === "face") {
1505
+ const id = this.bk.thicken(h.id, thickness);
1506
+ return solidHandle(id);
1507
+ }
1508
+ throw new Error("brepkit: thicken() requires a face");
1509
+ }
1510
+ offset(shape2, distance, tolerance) {
1511
+ if (tolerance !== void 0) {
1512
+ warnOnce(
1513
+ "offset-tolerance",
1514
+ "offset() tolerance parameter is not supported; brepkit uses its own internal tolerance."
1515
+ );
1516
+ }
1517
+ const h = shape2;
1518
+ if (h.type === "face") {
1519
+ const id2 = this.bk.thicken(h.id, distance);
1520
+ return solidHandle(id2);
1521
+ }
1522
+ const id = this.bk.offsetSolid(unwrapSolidOrThrow(shape2, "offset"), distance);
1523
+ return solidHandle(id);
1524
+ }
1525
+ // ═══════════════════════════════════════════════════════════════════════
1526
+ // Transforms
1527
+ // ═══════════════════════════════════════════════════════════════════════
1528
+ transform(shape2, trsf) {
1529
+ if (Array.isArray(trsf) && trsf.length === 16) {
1530
+ return this.applyMatrix(shape2, trsf);
1531
+ }
1532
+ throw new Error("brepkit: transform expects a 16-element matrix array");
1533
+ }
1534
+ translate(shape2, x, y, z) {
1535
+ return this.applyMatrix(shape2, translationMatrix(x, y, z));
1536
+ }
1537
+ rotate(shape2, angle, axis, center) {
1538
+ return this.applyMatrix(shape2, rotationMatrix(angle, axis, center));
1539
+ }
1540
+ mirror(shape2, origin, normal) {
1541
+ const h = shape2;
1542
+ if (h.type === "solid") {
1543
+ const id = this.bk.mirror(
1544
+ h.id,
1545
+ origin[0],
1546
+ origin[1],
1547
+ origin[2],
1548
+ normal[0],
1549
+ normal[1],
1550
+ normal[2]
1551
+ );
1552
+ return solidHandle(id);
1553
+ }
1554
+ return this.applyMatrix(shape2, mirrorMatrix(origin, normal));
1555
+ }
1556
+ scale(shape2, center, factor) {
1557
+ return this.applyMatrix(shape2, scaleMatrix(center, factor));
1558
+ }
1559
+ generalTransform(shape2, linear, translation, _isOrthogonal) {
1560
+ return this.applyMatrix(shape2, affineMatrix(linear, translation));
1561
+ }
1562
+ generalTransformNonOrthogonal(shape2, linear, translation) {
1563
+ return this.applyMatrix(shape2, affineMatrix(linear, translation));
1564
+ }
1565
+ // ═══════════════════════════════════════════════════════════════════════
1566
+ // Operations with shape evolution tracking
1567
+ // ═══════════════════════════════════════════════════════════════════════
1568
+ /**
1569
+ * Parse native brepkit evolution JSON and convert face IDs to hash-based
1570
+ * evolution that the brepjs propagation system expects.
1571
+ *
1572
+ * The native API returns:
1573
+ * `{"solid": u32, "evolution": {"modified": {inputFaceId: [outputFaceIds]}, "generated": {}, "deleted": [faceIds]}}`
1574
+ *
1575
+ * We convert face IDs → hashes via `id % hashUpperBound`.
1576
+ */
1577
+ parseNativeEvolution(json, hashUpperBound) {
1578
+ const parsed = JSON.parse(json);
1579
+ const evo = parsed.evolution;
1580
+ if (!evo || typeof evo.modified !== "object" || typeof evo.generated !== "object") {
1581
+ throw new Error("brepkit: invalid evolution JSON structure");
1582
+ }
1583
+ const resultShape = solidHandle(parsed.solid);
1584
+ const collectHashes = (entries) => {
1585
+ const map = /* @__PURE__ */ new Map();
1586
+ for (const [inputId, outputIds] of Object.entries(entries)) {
1587
+ const inputHash = Number(inputId) % hashUpperBound;
1588
+ const outputHashes = outputIds.map((id) => id % hashUpperBound);
1589
+ const existing = map.get(inputHash);
1590
+ if (existing) {
1591
+ existing.push(...outputHashes);
1592
+ } else {
1593
+ map.set(inputHash, outputHashes);
1594
+ }
1595
+ }
1596
+ return map;
1597
+ };
1598
+ const modified = collectHashes(evo.modified);
1599
+ const generated = collectHashes(evo.generated);
1600
+ const deleted = /* @__PURE__ */ new Set();
1601
+ for (const id of evo.deleted) {
1602
+ deleted.add(id % hashUpperBound);
1603
+ }
1604
+ return { shape: resultShape, evolution: { modified, generated, deleted } };
1605
+ }
1606
+ /**
1607
+ * Build a ShapeEvolution by comparing input face hashes to output face hashes.
1608
+ *
1609
+ * For transforms: 1:1 mapping (modified = identity, no generated/deleted).
1610
+ * For booleans/modifiers: compare sets to detect changes, with geometric
1611
+ * fallback when hash matching fails (brepkit always creates new face IDs).
1612
+ */
1613
+ buildEvolution(resultShape, inputFaceHashes, hashUpperBound, isTransform, originalShape) {
1614
+ const h = resultShape;
1615
+ const modified = /* @__PURE__ */ new Map();
1616
+ const generated = /* @__PURE__ */ new Map();
1617
+ const deleted = /* @__PURE__ */ new Set();
1618
+ if (h.type === "solid") {
1619
+ const outputFaces = toArray(this.bk.getSolidFaces(h.id));
1620
+ const outputHashes = outputFaces.map((fid) => fid % hashUpperBound);
1621
+ if (isTransform) {
1622
+ for (let i = 0; i < inputFaceHashes.length && i < outputHashes.length; i++) {
1623
+ modified.set(inputFaceHashes[i], [outputHashes[i]]);
1624
+ }
1625
+ } else {
1626
+ const inputSet = new Set(inputFaceHashes);
1627
+ let hasOverlap = false;
1628
+ for (const hash of outputHashes) {
1629
+ if (inputSet.has(hash)) {
1630
+ hasOverlap = true;
1631
+ break;
1632
+ }
1633
+ }
1634
+ if (hasOverlap) {
1635
+ const outputSet = new Set(outputHashes);
1636
+ for (const hash of outputHashes) {
1637
+ if (inputSet.has(hash)) {
1638
+ modified.set(hash, [hash]);
1639
+ }
1640
+ }
1641
+ const newFaces = outputHashes.filter((fh) => !inputSet.has(fh));
1642
+ if (newFaces.length > 0 && inputFaceHashes.length > 0) {
1643
+ generated.set(inputFaceHashes[0], newFaces);
1644
+ }
1645
+ for (const hash of inputFaceHashes) {
1646
+ if (!outputSet.has(hash)) {
1647
+ deleted.add(hash);
1648
+ }
1649
+ }
1650
+ } else if (originalShape) {
1651
+ this.matchFacesGeometrically(
1652
+ originalShape,
1653
+ inputFaceHashes,
1654
+ outputFaces,
1655
+ hashUpperBound,
1656
+ modified,
1657
+ generated,
1658
+ deleted
1659
+ );
1660
+ } else {
1661
+ for (let i = 0; i < inputFaceHashes.length && i < outputHashes.length; i++) {
1662
+ modified.set(inputFaceHashes[i], [outputHashes[i]]);
1663
+ }
1664
+ if (outputHashes.length > inputFaceHashes.length && inputFaceHashes.length > 0) {
1665
+ generated.set(inputFaceHashes[0], outputHashes.slice(inputFaceHashes.length));
1666
+ }
1667
+ }
1668
+ }
1669
+ }
1670
+ return { shape: resultShape, evolution: { modified, generated, deleted } };
1671
+ }
1672
+ /**
1673
+ * Chain an evolution map (modified or generated) through one step of a multi-step
1674
+ * boolean. For each entry, each previous output hash is resolved against this
1675
+ * step's evolution: if it was further modified, follow to the new outputs; if
1676
+ * deleted, drop it; otherwise keep it unchanged.
1677
+ *
1678
+ * Mutates `map` in-place and records each resolved prevOut in `intermediateOutputs`.
1679
+ * When `deleteOnEmpty` is provided, entries that reduce to no outputs are added to it.
1680
+ */
1681
+ static chainEvolutionMap(map, stepModified, stepDeleted, intermediateOutputs, deleteOnEmpty) {
1682
+ for (const [origKey, prevOutputs] of map) {
1683
+ const chainedOutputs = [];
1684
+ for (const prevOut of prevOutputs) {
1685
+ intermediateOutputs.add(prevOut);
1686
+ const nextOutputs = stepModified.get(prevOut);
1687
+ if (nextOutputs) {
1688
+ chainedOutputs.push(...nextOutputs);
1689
+ } else if (!stepDeleted.has(prevOut)) {
1690
+ chainedOutputs.push(prevOut);
1691
+ }
1692
+ }
1693
+ if (chainedOutputs.length > 0) {
1694
+ map.set(origKey, chainedOutputs);
1695
+ } else {
1696
+ map.delete(origKey);
1697
+ deleteOnEmpty?.add(origKey);
1698
+ }
1699
+ }
1700
+ }
1701
+ /** Squared Euclidean distance between two 3-component centroids. */
1702
+ static centroidDistSq(a, b) {
1703
+ const dx = a[0] - b[0];
1704
+ const dy = a[1] - b[1];
1705
+ const dz = a[2] - b[2];
1706
+ return dx * dx + dy * dy + dz * dz;
1707
+ }
1708
+ /** Compute face centroid as the average of tessellation vertices. */
1709
+ faceCentroidById(faceId) {
1710
+ try {
1711
+ const pos = this.bk.tessellateFace(faceId, 1).positions;
1712
+ if (pos.length < 3) return [0, 0, 0];
1713
+ let cx = 0;
1714
+ let cy = 0;
1715
+ let cz = 0;
1716
+ const nVerts = pos.length / 3;
1717
+ for (let i = 0; i < pos.length; i += 3) {
1718
+ cx += pos[i];
1719
+ cy += pos[i + 1];
1720
+ cz += pos[i + 2];
1721
+ }
1722
+ return [cx / nVerts, cy / nVerts, cz / nVerts];
1723
+ } catch {
1724
+ return [0, 0, 0];
1725
+ }
1726
+ }
1727
+ /**
1728
+ * Match input→output faces geometrically using normal dot product and centroid distance.
1729
+ * Mirrors the algorithm in brepkit's `boolean_with_evolution`.
1730
+ */
1731
+ matchFacesGeometrically(originalShape, inputFaceHashes, outputFaceIds, hashUpperBound, modified, generated, deleted) {
1732
+ const orig = originalShape;
1733
+ if (orig.type !== "solid") return;
1734
+ const inputFaceIds = toArray(this.bk.getSolidFaces(orig.id));
1735
+ const hashCount = Math.min(inputFaceIds.length, inputFaceHashes.length);
1736
+ const inputSigs = [];
1737
+ for (let i = 0; i < hashCount; i++) {
1738
+ const fid = inputFaceIds[i];
1739
+ try {
1740
+ const normal = this.bk.getFaceNormal(fid);
1741
+ const centroid = this.faceCentroidById(fid);
1742
+ inputSigs.push({ hash: inputFaceHashes[i] ?? fid % hashUpperBound, normal, centroid });
1743
+ } catch {
1744
+ inputSigs.push({
1745
+ hash: inputFaceHashes[i] ?? fid % hashUpperBound,
1746
+ normal: [0, 0, 0],
1747
+ centroid: this.faceCentroidById(fid)
1748
+ });
1749
+ }
1750
+ }
1751
+ const outputSigs = [];
1752
+ for (const fid of outputFaceIds) {
1753
+ try {
1754
+ const normal = this.bk.getFaceNormal(fid);
1755
+ const centroid = this.faceCentroidById(fid);
1756
+ outputSigs.push({ hash: fid % hashUpperBound, normal, centroid });
1757
+ } catch {
1758
+ outputSigs.push({
1759
+ hash: fid % hashUpperBound,
1760
+ normal: [0, 0, 0],
1761
+ centroid: this.faceCentroidById(fid)
1762
+ });
1763
+ }
1764
+ }
1765
+ const NORMAL_THRESHOLD = 0.707;
1766
+ const CENTROID_DIST_SQ_MAX = 100;
1767
+ const matchedInputIndices = /* @__PURE__ */ new Set();
1768
+ for (const out of outputSigs) {
1769
+ let bestScore = -Infinity;
1770
+ let bestIdx = -1;
1771
+ for (let i = 0; i < inputSigs.length; i++) {
1772
+ const inp = inputSigs[i];
1773
+ const dot = (out.normal[0] ?? 0) * (inp.normal[0] ?? 0) + (out.normal[1] ?? 0) * (inp.normal[1] ?? 0) + (out.normal[2] ?? 0) * (inp.normal[2] ?? 0);
1774
+ if (dot < NORMAL_THRESHOLD) continue;
1775
+ const distSq = BrepkitAdapter.centroidDistSq(out.centroid, inp.centroid);
1776
+ if (distSq > CENTROID_DIST_SQ_MAX) continue;
1777
+ const score = dot - distSq / CENTROID_DIST_SQ_MAX;
1778
+ if (score > bestScore) {
1779
+ bestScore = score;
1780
+ bestIdx = i;
1781
+ }
1782
+ }
1783
+ if (bestIdx >= 0) {
1784
+ const bestInput = inputSigs[bestIdx];
1785
+ const existing = modified.get(bestInput.hash) ?? [];
1786
+ existing.push(out.hash);
1787
+ modified.set(bestInput.hash, existing);
1788
+ matchedInputIndices.add(bestIdx);
1789
+ } else {
1790
+ let bestDistSq = Infinity;
1791
+ let nearestInput;
1792
+ for (const inp of inputSigs) {
1793
+ const distSq = BrepkitAdapter.centroidDistSq(out.centroid, inp.centroid);
1794
+ if (distSq < bestDistSq) {
1795
+ bestDistSq = distSq;
1796
+ nearestInput = inp;
1797
+ }
1798
+ }
1799
+ if (nearestInput) {
1800
+ const existing = generated.get(nearestInput.hash) ?? [];
1801
+ existing.push(out.hash);
1802
+ generated.set(nearestInput.hash, existing);
1803
+ }
1804
+ }
1805
+ }
1806
+ for (let i = 0; i < inputSigs.length; i++) {
1807
+ if (!matchedInputIndices.has(i)) {
1808
+ deleted.add(inputSigs[i].hash);
1809
+ }
1810
+ }
1811
+ }
1812
+ translateWithHistory(shape2, x, y, z, inputFaceHashes, hashUpperBound) {
1813
+ return this.buildEvolution(
1814
+ this.translate(shape2, x, y, z),
1815
+ inputFaceHashes,
1816
+ hashUpperBound,
1817
+ true
1818
+ );
1819
+ }
1820
+ rotateWithHistory(shape2, angle, inputFaceHashes, hashUpperBound, axis, center) {
1821
+ const angleDeg = angle * 180 / Math.PI;
1822
+ return this.buildEvolution(
1823
+ this.rotate(shape2, angleDeg, axis, center),
1824
+ inputFaceHashes,
1825
+ hashUpperBound,
1826
+ true
1827
+ );
1828
+ }
1829
+ mirrorWithHistory(shape2, origin, normal, inputFaceHashes, hashUpperBound) {
1830
+ return this.buildEvolution(
1831
+ this.mirror(shape2, origin, normal),
1832
+ inputFaceHashes,
1833
+ hashUpperBound,
1834
+ true
1835
+ );
1836
+ }
1837
+ scaleWithHistory(shape2, center, factor, inputFaceHashes, hashUpperBound) {
1838
+ return this.buildEvolution(
1839
+ this.scale(shape2, center, factor),
1840
+ inputFaceHashes,
1841
+ hashUpperBound,
1842
+ true
1843
+ );
1844
+ }
1845
+ generalTransformWithHistory(shape2, linear, translation, isOrthogonal, inputFaceHashes, hashUpperBound) {
1846
+ return this.buildEvolution(
1847
+ this.generalTransform(shape2, linear, translation, isOrthogonal),
1848
+ inputFaceHashes,
1849
+ hashUpperBound,
1850
+ true
1851
+ );
1852
+ }
1853
+ booleanWithHistoryImpl(shape2, tool, inputFaceHashes, hashUpperBound, options, nativeFn, fallbackFn, _label) {
1854
+ const sh = shape2;
1855
+ const th = tool;
1856
+ if (inputFaceHashes.length > 0 && sh.type === "solid") {
1857
+ if (th.type === "solid") {
1858
+ const json = nativeFn(sh.id, th.id);
1859
+ return this.parseNativeEvolution(json, hashUpperBound);
1860
+ }
1861
+ if (th.type === "compound") {
1862
+ const childSolidIds = toArray(this.bk.getCompoundSolids(th.id));
1863
+ let currentShape = shape2;
1864
+ const combinedModified = /* @__PURE__ */ new Map();
1865
+ const combinedGenerated = /* @__PURE__ */ new Map();
1866
+ const combinedDeleted = /* @__PURE__ */ new Set();
1867
+ const inputFaceHashSet = new Set(inputFaceHashes);
1868
+ for (const childId of childSolidIds) {
1869
+ const ch = currentShape;
1870
+ if (ch.type !== "solid") break;
1871
+ const json = nativeFn(ch.id, childId);
1872
+ const result2 = this.parseNativeEvolution(json, hashUpperBound);
1873
+ currentShape = result2.shape;
1874
+ const intermediateOutputs = /* @__PURE__ */ new Set();
1875
+ BrepkitAdapter.chainEvolutionMap(
1876
+ combinedModified,
1877
+ result2.evolution.modified,
1878
+ result2.evolution.deleted,
1879
+ intermediateOutputs,
1880
+ combinedDeleted
1881
+ );
1882
+ BrepkitAdapter.chainEvolutionMap(
1883
+ combinedGenerated,
1884
+ result2.evolution.modified,
1885
+ result2.evolution.deleted,
1886
+ intermediateOutputs
1887
+ );
1888
+ for (const [k, v] of result2.evolution.modified) {
1889
+ if (!combinedModified.has(k) && !intermediateOutputs.has(k)) {
1890
+ combinedModified.set(k, [...v]);
1891
+ }
1892
+ }
1893
+ for (const [k, v] of result2.evolution.generated) {
1894
+ if (!intermediateOutputs.has(k)) {
1895
+ const existing = combinedGenerated.get(k) ?? [];
1896
+ combinedGenerated.set(k, [...existing, ...v]);
1897
+ }
1898
+ }
1899
+ for (const d of result2.evolution.deleted) {
1900
+ if (inputFaceHashSet.has(d)) {
1901
+ combinedDeleted.add(d);
1902
+ }
1903
+ }
1904
+ }
1905
+ return {
1906
+ shape: currentShape,
1907
+ evolution: {
1908
+ modified: combinedModified,
1909
+ generated: combinedGenerated,
1910
+ deleted: combinedDeleted
1911
+ }
1912
+ };
1913
+ }
1914
+ }
1915
+ const fallbackResult = fallbackFn(shape2, tool, options);
1916
+ return this.buildEvolution(fallbackResult, inputFaceHashes, hashUpperBound, false, shape2);
1917
+ }
1918
+ fuseWithHistory(shape2, tool, inputFaceHashes, hashUpperBound, options) {
1919
+ return this.booleanWithHistoryImpl(
1920
+ shape2,
1921
+ tool,
1922
+ inputFaceHashes,
1923
+ hashUpperBound,
1924
+ options,
1925
+ (a, b) => this.bk.fuseWithEvolution(a, b),
1926
+ (s, t, o) => this.fuse(s, t, o),
1927
+ "fuseWithHistory"
1928
+ );
1929
+ }
1930
+ cutWithHistory(shape2, tool, inputFaceHashes, hashUpperBound, options) {
1931
+ return this.booleanWithHistoryImpl(
1932
+ shape2,
1933
+ tool,
1934
+ inputFaceHashes,
1935
+ hashUpperBound,
1936
+ options,
1937
+ (a, b) => this.bk.cutWithEvolution(a, b),
1938
+ (s, t, o) => this.cut(s, t, o),
1939
+ "cutWithHistory"
1940
+ );
1941
+ }
1942
+ intersectWithHistory(shape2, tool, inputFaceHashes, hashUpperBound, options) {
1943
+ return this.booleanWithHistoryImpl(
1944
+ shape2,
1945
+ tool,
1946
+ inputFaceHashes,
1947
+ hashUpperBound,
1948
+ options,
1949
+ (a, b) => this.bk.intersectWithEvolution(a, b),
1950
+ (s, t, o) => this.intersect(s, t, o),
1951
+ "intersectWithHistory"
1952
+ );
1953
+ }
1954
+ filletWithHistory(shape2, edges, radius, inputFaceHashes, hashUpperBound) {
1955
+ return this.buildEvolution(
1956
+ this.fillet(shape2, edges, radius),
1957
+ inputFaceHashes,
1958
+ hashUpperBound,
1959
+ false,
1960
+ shape2
1961
+ );
1962
+ }
1963
+ chamferWithHistory(shape2, edges, distance, inputFaceHashes, hashUpperBound) {
1964
+ return this.buildEvolution(
1965
+ this.chamfer(shape2, edges, distance),
1966
+ inputFaceHashes,
1967
+ hashUpperBound,
1968
+ false,
1969
+ shape2
1970
+ );
1971
+ }
1972
+ shellWithHistory(shape2, faces, thickness, inputFaceHashes, hashUpperBound, tolerance) {
1973
+ return this.buildEvolution(
1974
+ this.shell(shape2, faces, thickness, tolerance),
1975
+ inputFaceHashes,
1976
+ hashUpperBound,
1977
+ false,
1978
+ shape2
1979
+ );
1980
+ }
1981
+ thickenWithHistory(shape2, thickness, inputFaceHashes, hashUpperBound) {
1982
+ return this.buildEvolution(
1983
+ this.thicken(shape2, thickness),
1984
+ inputFaceHashes,
1985
+ hashUpperBound,
1986
+ false,
1987
+ shape2
1988
+ );
1989
+ }
1990
+ offsetWithHistory(shape2, distance, inputFaceHashes, hashUpperBound, tolerance) {
1991
+ return this.buildEvolution(
1992
+ this.offset(shape2, distance, tolerance),
1993
+ inputFaceHashes,
1994
+ hashUpperBound,
1995
+ false,
1996
+ shape2
1997
+ );
1998
+ }
1999
+ // ═══════════════════════════════════════════════════════════════════════
2000
+ // Meshing
2001
+ // ═══════════════════════════════════════════════════════════════════════
2002
+ mesh(shape2, options) {
2003
+ if (options.angularTolerance > 0) {
2004
+ warnOnce(
2005
+ "mesh-angular",
2006
+ "mesh angularTolerance is not supported; only linear deflection is used."
2007
+ );
2008
+ }
2009
+ const h = unwrap(shape2);
2010
+ const bkHandle = shape2;
2011
+ const deflection = options.tolerance || DEFAULT_DEFLECTION;
2012
+ let result2;
2013
+ if (bkHandle.type === "solid") {
2014
+ result2 = this.meshSolid(h, deflection);
2015
+ } else if (bkHandle.type === "face") {
2016
+ result2 = this.meshSingleFace(h, deflection, 0);
2017
+ } else {
2018
+ throw new Error(`brepkit: cannot mesh shape of type '${bkHandle.type}'`);
2019
+ }
2020
+ if (options.skipNormals) {
2021
+ result2.normals = new Float32Array(0);
2022
+ }
2023
+ if (!options.includeUVs) {
2024
+ result2.uvs = new Float32Array(0);
2025
+ }
2026
+ return result2;
2027
+ }
2028
+ meshEdges(shape2, tolerance, angularTolerance) {
2029
+ if (angularTolerance > 0) {
2030
+ warnOnce(
2031
+ "mesh-edges-angular",
2032
+ "meshEdges angularTolerance is not supported; only linear deflection is used."
2033
+ );
2034
+ }
2035
+ const bkHandle = shape2;
2036
+ if (bkHandle.type !== "solid") {
2037
+ return { lines: new Float32Array(0), edgeGroups: [] };
2038
+ }
2039
+ const edgeLines = this.bk.meshEdges(bkHandle.id, tolerance);
2040
+ const positions = edgeLines.positions;
2041
+ const offsets = edgeLines.offsets;
2042
+ const edgeCount = edgeLines.edgeCount;
2043
+ const edgeGroups = [];
2044
+ for (let i = 0; i < edgeCount; i++) {
2045
+ const startIdx = offsets[i];
2046
+ const endIdx = i + 1 < edgeCount ? offsets[i + 1] : positions.length;
2047
+ const pointCount = (endIdx - startIdx) / 3;
2048
+ edgeGroups.push({ start: startIdx / 3, count: pointCount, edgeHash: i });
2049
+ }
2050
+ return {
2051
+ lines: new Float32Array(positions),
2052
+ edgeGroups
2053
+ };
2054
+ }
2055
+ // ═══════════════════════════════════════════════════════════════════════
2056
+ // File I/O
2057
+ // ═══════════════════════════════════════════════════════════════════════
2058
+ exportSTEP(shapes) {
2059
+ if (shapes.length === 0) return "";
2060
+ const parts = [];
2061
+ for (const shape2 of shapes) {
2062
+ const solidIds = unwrapSolidsForExport(this.bk, shape2, "exportSTEP");
2063
+ for (const sid of solidIds) {
2064
+ const bytes = this.bk.exportStep(sid);
2065
+ parts.push(new TextDecoder().decode(bytes));
2066
+ }
2067
+ }
2068
+ return parts.join("\n");
2069
+ }
2070
+ exportSTL(shape2, binary) {
2071
+ const solidIds = unwrapSolidsForExport(this.bk, shape2, "exportSTL");
2072
+ if (binary) {
2073
+ const bytes2 = this.bk.exportStl(solidIds[0], DEFAULT_DEFLECTION);
2074
+ return bytes2.buffer;
2075
+ }
2076
+ const bytes = this.bk.exportStlAscii(solidIds[0], DEFAULT_DEFLECTION);
2077
+ return new TextDecoder().decode(bytes);
2078
+ }
2079
+ importSTEP(data) {
2080
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
2081
+ return toArray(this.bk.importStep(bytes)).map(solidHandle);
2082
+ }
2083
+ importSTL(data) {
2084
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
2085
+ const id = this.bk.importStl(bytes);
2086
+ return solidHandle(id);
2087
+ }
2088
+ exportIGES(shapes) {
2089
+ if (shapes.length === 0) return "";
2090
+ const parts = [];
2091
+ for (const shape2 of shapes) {
2092
+ const solidIds = unwrapSolidsForExport(this.bk, shape2, "exportIGES");
2093
+ for (const sid of solidIds) {
2094
+ const bytes = this.bk.exportIges(sid);
2095
+ parts.push(new TextDecoder().decode(bytes));
2096
+ }
2097
+ }
2098
+ return parts.join("\n");
2099
+ }
2100
+ importIGES(data) {
2101
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
2102
+ return toArray(this.bk.importIges(bytes)).map(solidHandle);
2103
+ }
2104
+ exportSTEPAssembly(parts, _options) {
2105
+ if (parts.length === 0) return "";
2106
+ const shapes = parts.map((p) => p.shape);
2107
+ return this.exportSTEP(shapes);
2108
+ }
2109
+ // ═══════════════════════════════════════════════════════════════════════
2110
+ // Measurement
2111
+ // ═══════════════════════════════════════════════════════════════════════
2112
+ volume(shape2) {
2113
+ const h = shape2;
2114
+ if (h.type !== "solid") return 0;
2115
+ return this.bk.volume(unwrap(shape2), DEFAULT_DEFLECTION);
2116
+ }
2117
+ area(shape2) {
2118
+ const h = shape2;
2119
+ if (h.type === "face") {
2120
+ return this.bk.faceArea(unwrap(shape2), DEFAULT_DEFLECTION);
2121
+ }
2122
+ if (h.type === "solid") {
2123
+ return this.bk.surfaceArea(unwrap(shape2), DEFAULT_DEFLECTION);
2124
+ }
2125
+ if (h.type === "compound") {
2126
+ const faces = this.iterShapes(shape2, "face");
2127
+ let total = 0;
2128
+ for (const face of faces) {
2129
+ total += this.bk.faceArea(unwrap(face), DEFAULT_DEFLECTION);
2130
+ }
2131
+ return total;
2132
+ }
2133
+ return 0;
2134
+ }
2135
+ length(shape2) {
2136
+ const h = shape2;
2137
+ if (h.type === "edge") {
2138
+ return this.bk.edgeLength(unwrap(shape2));
2139
+ }
2140
+ if (h.type === "face") {
2141
+ return this.bk.facePerimeter(unwrap(shape2));
2142
+ }
2143
+ if (h.type === "wire") {
2144
+ return this.bk.wireLength(h.id);
2145
+ }
2146
+ throw new Error("brepkit: length() requires an edge, wire, or face");
2147
+ }
2148
+ centerOfMass(shape2) {
2149
+ const h = shape2;
2150
+ if (h.type === "solid") {
2151
+ const result2 = this.bk.centerOfMass(unwrap(shape2), DEFAULT_DEFLECTION);
2152
+ return [result2[0], result2[1], result2[2]];
2153
+ }
2154
+ if (h.type === "face") {
2155
+ const domain = this.uvBounds(shape2);
2156
+ const uMid = (domain.uMin + domain.uMax) / 2;
2157
+ const vMid = (domain.vMin + domain.vMax) / 2;
2158
+ return this.pointOnSurface(shape2, uMid, vMid);
2159
+ }
2160
+ if (h.type === "edge") {
2161
+ const verts = this.bk.getEdgeVertices(h.id);
2162
+ return [
2163
+ (verts[0] + verts[3]) / 2,
2164
+ (verts[1] + verts[4]) / 2,
2165
+ (verts[2] + verts[5]) / 2
2166
+ ];
2167
+ }
2168
+ if (h.type === "vertex") {
2169
+ return this.vertexPosition(shape2);
2170
+ }
2171
+ const vertices = this.iterShapes(shape2, "vertex");
2172
+ if (vertices.length > 0) {
2173
+ let sx = 0, sy = 0, sz = 0;
2174
+ for (const v of vertices) {
2175
+ const p = this.vertexPosition(v);
2176
+ sx += p[0];
2177
+ sy += p[1];
2178
+ sz += p[2];
2179
+ }
2180
+ return [sx / vertices.length, sy / vertices.length, sz / vertices.length];
2181
+ }
2182
+ return [0, 0, 0];
2183
+ }
2184
+ linearCenterOfMass(shape2) {
2185
+ const h = shape2;
2186
+ if (h.type === "edge") {
2187
+ const verts = this.bk.getEdgeVertices(h.id);
2188
+ return [
2189
+ (verts[0] + verts[3]) / 2,
2190
+ (verts[1] + verts[4]) / 2,
2191
+ (verts[2] + verts[5]) / 2
2192
+ ];
2193
+ }
2194
+ return this.centerOfMass(shape2);
2195
+ }
2196
+ boundingBox(shape2) {
2197
+ const h = shape2;
2198
+ if (h.type === "solid") {
2199
+ const bb = this.bk.boundingBox(unwrap(shape2));
2200
+ return {
2201
+ min: [bb[0], bb[1], bb[2]],
2202
+ max: [bb[3], bb[4], bb[5]]
2203
+ };
2204
+ }
2205
+ if (h.type === "vertex") {
2206
+ const pos = this.vertexPosition(shape2);
2207
+ return { min: [...pos], max: [...pos] };
2208
+ }
2209
+ const vertices = this.iterShapes(shape2, "vertex");
2210
+ if (vertices.length === 0) {
2211
+ return { min: [0, 0, 0], max: [0, 0, 0] };
2212
+ }
2213
+ const first = this.vertexPosition(vertices[0]);
2214
+ let minX = first[0], minY = first[1], minZ = first[2];
2215
+ let maxX = first[0], maxY = first[1], maxZ = first[2];
2216
+ for (let i = 1; i < vertices.length; i++) {
2217
+ const p = this.vertexPosition(vertices[i]);
2218
+ if (p[0] < minX) minX = p[0];
2219
+ if (p[0] > maxX) maxX = p[0];
2220
+ if (p[1] < minY) minY = p[1];
2221
+ if (p[1] > maxY) maxY = p[1];
2222
+ if (p[2] < minZ) minZ = p[2];
2223
+ if (p[2] > maxZ) maxZ = p[2];
2224
+ }
2225
+ return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
2226
+ }
2227
+ // ═══════════════════════════════════════════════════════════════════════
2228
+ // Topology introspection
2229
+ // ═══════════════════════════════════════════════════════════════════════
2230
+ iterShapes(shape2, type) {
2231
+ const h = unwrap(shape2);
2232
+ const bkHandle = shape2;
2233
+ switch (bkHandle.type) {
2234
+ case "compound": {
2235
+ const children = syntheticCompounds.get(h);
2236
+ if (children) {
2237
+ const results = [];
2238
+ for (const child of children) {
2239
+ if (child.type === type) {
2240
+ results.push(child);
2241
+ } else {
2242
+ results.push(...this.iterShapes(child, type));
2243
+ }
2244
+ }
2245
+ return results;
2246
+ }
2247
+ if (type === "solid") {
2248
+ return toArray(this.bk.getCompoundSolids(h)).map(solidHandle);
2249
+ }
2250
+ if (type === "face" || type === "edge" || type === "vertex" || type === "wire") {
2251
+ const solids = toArray(this.bk.getCompoundSolids(h)).map(solidHandle);
2252
+ return solids.flatMap((s) => this.iterShapes(s, type));
2253
+ }
2254
+ return [];
2255
+ }
2256
+ case "solid": {
2257
+ switch (type) {
2258
+ case "face":
2259
+ return toArray(this.bk.getSolidFaces(h)).map(faceHandle);
2260
+ case "edge":
2261
+ return toArray(this.bk.getSolidEdges(h)).map(edgeHandle);
2262
+ case "vertex":
2263
+ return toArray(this.bk.getSolidVertices(h)).map(vertexHandle);
2264
+ case "wire":
2265
+ return toArray(this.bk.getSolidFaces(h)).flatMap(
2266
+ (faceId) => toArray(this.bk.getFaceWires(faceId)).map(wireHandle)
2267
+ );
2268
+ default:
2269
+ return [];
2270
+ }
2271
+ }
2272
+ case "shell": {
2273
+ if (type === "face") {
2274
+ return toArray(this.bk.getShellFaces(h)).map(faceHandle);
2275
+ }
2276
+ if (type === "edge" || type === "vertex") {
2277
+ const faces = toArray(this.bk.getShellFaces(h)).map(faceHandle);
2278
+ const seen = /* @__PURE__ */ new Set();
2279
+ const results = [];
2280
+ for (const face of faces) {
2281
+ for (const child of this.iterShapes(face, type)) {
2282
+ const childId = unwrap(child);
2283
+ if (!seen.has(childId)) {
2284
+ seen.add(childId);
2285
+ results.push(child);
2286
+ }
2287
+ }
2288
+ }
2289
+ return results;
2290
+ }
2291
+ return [];
2292
+ }
2293
+ case "face": {
2294
+ if (type === "face") {
2295
+ return [shape2];
2296
+ }
2297
+ if (type === "edge") {
2298
+ return toArray(this.bk.getFaceEdges(h)).map(edgeHandle);
2299
+ }
2300
+ if (type === "vertex") {
2301
+ return toArray(this.bk.getFaceVertices(h)).map(vertexHandle);
2302
+ }
2303
+ if (type === "wire") {
2304
+ return toArray(this.bk.getFaceWires(h)).map(wireHandle);
2305
+ }
2306
+ return [];
2307
+ }
2308
+ case "wire": {
2309
+ if (type === "wire") {
2310
+ return [shape2];
2311
+ }
2312
+ if (type === "edge") {
2313
+ return toArray(this.bk.getWireEdges(h)).map(edgeHandle);
2314
+ }
2315
+ if (type === "vertex") {
2316
+ const edgeIds = toArray(this.bk.getWireEdges(h));
2317
+ const seen = /* @__PURE__ */ new Set();
2318
+ const results = [];
2319
+ for (const eid of edgeIds) {
2320
+ const verts = this.bk.getEdgeVertices(eid);
2321
+ const coords = [
2322
+ [verts[0], verts[1], verts[2]],
2323
+ [verts[3], verts[4], verts[5]]
2324
+ ];
2325
+ for (const [x, y, z] of coords) {
2326
+ const key = `${x},${y},${z}`;
2327
+ if (!seen.has(key)) {
2328
+ seen.add(key);
2329
+ results.push(vertexHandle(this.bk.makeVertex(x, y, z)));
2330
+ }
2331
+ }
2332
+ }
2333
+ return results;
2334
+ }
2335
+ return [];
2336
+ }
2337
+ case "edge": {
2338
+ if (type === "edge") {
2339
+ return [shape2];
2340
+ }
2341
+ if (type === "vertex") {
2342
+ const verts = this.bk.getEdgeVertices(h);
2343
+ const v1 = this.bk.makeVertex(verts[0], verts[1], verts[2]);
2344
+ const v2 = this.bk.makeVertex(verts[3], verts[4], verts[5]);
2345
+ return [vertexHandle(v1), vertexHandle(v2)];
2346
+ }
2347
+ return [];
2348
+ }
2349
+ default:
2350
+ return [];
2351
+ }
2352
+ }
2353
+ iterShapeList(list, callback) {
2354
+ if (Array.isArray(list)) {
2355
+ for (const item of list) callback(item);
2356
+ }
2357
+ }
2358
+ shapeType(shape2) {
2359
+ if (isBrepkitHandle(shape2)) return shape2.type;
2360
+ throw new Error("brepkit: cannot determine shape type of non-brepkit handle");
2361
+ }
2362
+ isSame(a, b) {
2363
+ return isBrepkitHandle(a) && isBrepkitHandle(b) && a.id === b.id && a.type === b.type;
2364
+ }
2365
+ isEqual(a, b) {
2366
+ return this.isSame(a, b);
2367
+ }
2368
+ downcast(shape2, _type) {
2369
+ return shape2;
2370
+ }
2371
+ hashCode(shape2, upperBound) {
2372
+ if (!isBrepkitHandle(shape2)) return 0;
2373
+ return shape2.id % upperBound;
2374
+ }
2375
+ isNull(shape2) {
2376
+ return !shape2 || !isBrepkitHandle(shape2);
2377
+ }
2378
+ shapeOrientation(shape2) {
2379
+ const h = unwrap(shape2);
2380
+ const orient = this.bk.getShapeOrientation(h);
2381
+ return orient;
2382
+ }
2383
+ // ═══════════════════════════════════════════════════════════════════════
2384
+ // Geometry queries: vertex
2385
+ // ═══════════════════════════════════════════════════════════════════════
2386
+ vertexPosition(vertex) {
2387
+ const pos = this.bk.getVertexPosition(unwrap(vertex, "vertex"));
2388
+ return [pos[0], pos[1], pos[2]];
2389
+ }
2390
+ // ═══════════════════════════════════════════════════════════════════════
2391
+ // Geometry queries: face / surface
2392
+ // ═══════════════════════════════════════════════════════════════════════
2393
+ surfaceType(face) {
2394
+ const typeStr = this.bk.getSurfaceType(unwrap(face, "face"));
2395
+ return typeStr;
2396
+ }
2397
+ uvBounds(face) {
2398
+ const domain = this.bk.getSurfaceDomain(unwrap(face, "face"));
2399
+ return { uMin: domain[0], uMax: domain[1], vMin: domain[2], vMax: domain[3] };
2400
+ }
2401
+ outerWire(face) {
2402
+ const id = this.bk.getFaceOuterWire(unwrap(face, "face"));
2403
+ return wireHandle(id);
2404
+ }
2405
+ surfaceNormal(face, u, v) {
2406
+ const n = this.bk.evaluateSurfaceNormal(unwrap(face, "face"), u, v);
2407
+ return [n[0], n[1], n[2]];
2408
+ }
2409
+ pointOnSurface(face, u, v) {
2410
+ const p = this.bk.evaluateSurface(unwrap(face, "face"), u, v);
2411
+ return [p[0], p[1], p[2]];
2412
+ }
2413
+ uvFromPoint(face, point) {
2414
+ try {
2415
+ const result2 = this.bk.projectPointOnSurface(
2416
+ unwrap(face, "face"),
2417
+ point[0],
2418
+ point[1],
2419
+ point[2]
2420
+ );
2421
+ return [result2[0], result2[1]];
2422
+ } catch (e) {
2423
+ console.warn("brepkit: uvFromPoint failed:", e);
2424
+ return null;
2425
+ }
2426
+ }
2427
+ projectPointOnFace(face, point) {
2428
+ const result2 = this.bk.projectPointOnSurface(
2429
+ unwrap(face, "face"),
2430
+ point[0],
2431
+ point[1],
2432
+ point[2]
2433
+ );
2434
+ return [result2[2], result2[3], result2[4]];
2435
+ }
2436
+ // ═══════════════════════════════════════════════════════════════════════
2437
+ // Geometry queries: edge / curve
2438
+ // ═══════════════════════════════════════════════════════════════════════
2439
+ curveTangent(shape2, param) {
2440
+ const h = shape2;
2441
+ let edgeId;
2442
+ let evalParam = param;
2443
+ if (h.type === "wire") {
2444
+ const edgeIds = toArray(this.bk.getWireEdges(h.id));
2445
+ edgeId = edgeIds[edgeIds.length - 1];
2446
+ let cumulative = 0;
2447
+ for (const eid of edgeIds) {
2448
+ const p = this.bk.getEdgeCurveParameters(eid);
2449
+ const span = p[1] - p[0];
2450
+ if (param <= cumulative + span || eid === edgeId) {
2451
+ edgeId = eid;
2452
+ evalParam = Math.min(p[0] + (param - cumulative), p[1]);
2453
+ break;
2454
+ }
2455
+ cumulative += span;
2456
+ }
2457
+ } else {
2458
+ edgeId = unwrap(shape2, "edge");
2459
+ }
2460
+ const result2 = this.bk.evaluateEdgeCurveD1(edgeId, evalParam);
2461
+ return {
2462
+ point: [result2[0], result2[1], result2[2]],
2463
+ tangent: [result2[3], result2[4], result2[5]]
2464
+ };
2465
+ }
2466
+ curveParameters(shape2) {
2467
+ const h = shape2;
2468
+ if (h.type === "wire") {
2469
+ const edgeIds = toArray(this.bk.getWireEdges(h.id));
2470
+ if (edgeIds.length === 0) return [0, 0];
2471
+ let total = 0;
2472
+ for (const eid of edgeIds) {
2473
+ const p = this.bk.getEdgeCurveParameters(eid);
2474
+ total += p[1] - p[0];
2475
+ }
2476
+ return [0, total];
2477
+ }
2478
+ const edgeId = unwrap(shape2, "edge");
2479
+ const params = this.bk.getEdgeCurveParameters(edgeId);
2480
+ return [params[0], params[1]];
2481
+ }
2482
+ curvePointAtParam(shape2, param) {
2483
+ const h = shape2;
2484
+ if (h.type === "wire") {
2485
+ const edgeIds = toArray(this.bk.getWireEdges(h.id));
2486
+ let cumulative = 0;
2487
+ for (const eid of edgeIds) {
2488
+ const p2 = this.bk.getEdgeCurveParameters(eid);
2489
+ const span = p2[1] - p2[0];
2490
+ if (param <= cumulative + span || eid === edgeIds[edgeIds.length - 1]) {
2491
+ const localParam = p2[0] + (param - cumulative);
2492
+ const pt2 = this.bk.evaluateEdgeCurve(eid, Math.min(localParam, p2[1]));
2493
+ return [pt2[0], pt2[1], pt2[2]];
2494
+ }
2495
+ cumulative += span;
2496
+ }
2497
+ const pt = this.bk.evaluateEdgeCurve(edgeIds[0], param);
2498
+ return [pt[0], pt[1], pt[2]];
2499
+ }
2500
+ const edgeId = unwrap(shape2, "edge");
2501
+ const p = this.bk.evaluateEdgeCurve(edgeId, param);
2502
+ return [p[0], p[1], p[2]];
2503
+ }
2504
+ curveIsClosed(shape2) {
2505
+ const h = shape2;
2506
+ if (h.type === "wire") {
2507
+ const edgeIds = toArray(this.bk.getWireEdges(h.id));
2508
+ if (edgeIds.length === 0) return false;
2509
+ if (edgeIds.length === 1) {
2510
+ const verts2 = this.bk.getEdgeVertices(edgeIds[0]);
2511
+ return dist3(verts2[0], verts2[1], verts2[2], verts2[3], verts2[4], verts2[5]) < 1e-7;
2512
+ }
2513
+ const endpoints = [];
2514
+ for (const eid of edgeIds) {
2515
+ const verts2 = this.bk.getEdgeVertices(eid);
2516
+ endpoints.push([verts2[0], verts2[1], verts2[2]]);
2517
+ endpoints.push([verts2[3], verts2[4], verts2[5]]);
2518
+ }
2519
+ const unmatched = [];
2520
+ for (const pt of endpoints) {
2521
+ const matchIdx = unmatched.findIndex(
2522
+ (u) => dist3(u[0], u[1], u[2], pt[0], pt[1], pt[2]) < 1e-7
2523
+ );
2524
+ if (matchIdx >= 0) {
2525
+ unmatched.splice(matchIdx, 1);
2526
+ } else {
2527
+ unmatched.push(pt);
2528
+ }
2529
+ }
2530
+ return unmatched.length === 0;
2531
+ }
2532
+ const verts = this.bk.getEdgeVertices(unwrap(shape2, "edge"));
2533
+ return dist3(verts[0], verts[1], verts[2], verts[3], verts[4], verts[5]) < 1e-7;
2534
+ }
2535
+ curveIsPeriodic(shape2) {
2536
+ const h = shape2;
2537
+ try {
2538
+ if (h.type === "edge") return this.curveIsClosed(shape2);
2539
+ if (h.type === "wire") {
2540
+ const edgeIds = toArray(this.bk.getWireEdges(h.id));
2541
+ if (edgeIds.length === 1) return this.curveIsClosed(shape2);
2542
+ }
2543
+ } catch {
2544
+ }
2545
+ return false;
2546
+ }
2547
+ curvePeriod(shape2) {
2548
+ try {
2549
+ if (this.curveIsPeriodic(shape2)) {
2550
+ const bounds = this.curveParameters(shape2);
2551
+ return bounds[1] - bounds[0];
2552
+ }
2553
+ } catch {
2554
+ }
2555
+ return 0;
2556
+ }
2557
+ curveType(shape2) {
2558
+ const h = shape2;
2559
+ if (h.type === "wire") {
2560
+ const edges = this.iterShapes(shape2, "edge");
2561
+ const first = edges[0];
2562
+ if (first) return this.bk.getEdgeCurveType(unwrap(first, "edge"));
2563
+ return "LINE";
2564
+ }
2565
+ return this.bk.getEdgeCurveType(unwrap(shape2, "edge"));
2566
+ }
2567
+ // ═══════════════════════════════════════════════════════════════════════
2568
+ // Simplification & repair
2569
+ // ═══════════════════════════════════════════════════════════════════════
2570
+ simplify(shape2) {
2571
+ if (shape2.type === "solid") {
2572
+ try {
2573
+ this.bk.healSolid(unwrap(shape2));
2574
+ } catch (e) {
2575
+ console.warn("brepkit: healing failed in simplify:", e);
2576
+ }
2577
+ }
2578
+ return shape2;
2579
+ }
2580
+ isValid(shape2) {
2581
+ if (!isBrepkitHandle(shape2)) return false;
2582
+ if (shape2.type !== "solid") return true;
2583
+ try {
2584
+ const errors2 = this.bk.validateSolidRelaxed(shape2.id);
2585
+ return errors2 === 0;
2586
+ } catch (e) {
2587
+ console.warn("brepkit: isValid check failed:", e);
2588
+ return false;
2589
+ }
2590
+ }
2591
+ isValidStrict(shape2) {
2592
+ if (!isBrepkitHandle(shape2)) return false;
2593
+ if (shape2.type !== "solid") return true;
2594
+ try {
2595
+ const errors2 = this.bk.validateSolid(shape2.id);
2596
+ return errors2 === 0;
2597
+ } catch (e) {
2598
+ console.warn("brepkit: isValidStrict check failed:", e);
2599
+ return false;
2600
+ }
2601
+ }
2602
+ sew(shapes, tolerance) {
2603
+ const faceIds = [];
2604
+ for (const s of shapes) {
2605
+ const h = s;
2606
+ if (h.type === "face") {
2607
+ faceIds.push(h.id);
2608
+ } else if (h.type === "solid") {
2609
+ for (const fid of toArray(this.bk.getSolidFaces(h.id))) {
2610
+ faceIds.push(fid);
2611
+ }
2612
+ } else if (h.type === "shell") {
2613
+ for (const fid of toArray(this.bk.getShellFaces(h.id))) {
2614
+ faceIds.push(fid);
2615
+ }
2616
+ }
2617
+ }
2618
+ const tol = tolerance ?? 1e-7;
2619
+ try {
2620
+ const id2 = this.bk.weldShellsAndFaces(faceIds, tol);
2621
+ return shellHandle(id2);
2622
+ } catch (e) {
2623
+ console.warn("brepkit: weldShellsAndFaces failed, falling back to sewFaces:", e);
2624
+ }
2625
+ const id = this.bk.sewFaces(faceIds, tol);
2626
+ return shellHandle(id);
2627
+ }
2628
+ healSolid(shape2) {
2629
+ const h = shape2;
2630
+ if (h.type !== "solid") {
2631
+ throw new Error(
2632
+ `brepkit: healSolid requires a solid, got ${h.type}. Consider using makeCompound() to combine shapes first.`
2633
+ );
2634
+ }
2635
+ try {
2636
+ const remaining = this.bk.repairSolid(unwrap(shape2));
2637
+ if (remaining > 0) {
2638
+ console.warn(`brepkit: repairSolid left ${remaining} error(s) on solid.`);
2639
+ }
2640
+ return shape2;
2641
+ } catch (e) {
2642
+ try {
2643
+ this.bk.healSolid(unwrap(shape2));
2644
+ return shape2;
2645
+ } catch (healErr) {
2646
+ console.warn(
2647
+ "brepkit: healSolid failed (repairSolid error:",
2648
+ e,
2649
+ ", healSolid error:",
2650
+ healErr,
2651
+ ")"
2652
+ );
2653
+ return null;
2654
+ }
2655
+ }
2656
+ }
2657
+ healFace(shape2) {
2658
+ return shape2;
2659
+ }
2660
+ healWire(wire, _face) {
2661
+ return wire;
2662
+ }
2663
+ // ═══════════════════════════════════════════════════════════════════════
2664
+ // 2D offset
2665
+ // ═══════════════════════════════════════════════════════════════════════
2666
+ offsetWire2D(wire, offset2, _joinType) {
2667
+ const edges = this.iterShapes(wire, "edge");
2668
+ if (edges.length === 0) return wire;
2669
+ const coords2d = [];
2670
+ for (const edge of edges) {
2671
+ const verts = this.bk.getEdgeVertices(unwrap(edge, "edge"));
2672
+ coords2d.push(verts[0], verts[1]);
2673
+ }
2674
+ if (coords2d.length < 6) return wire;
2675
+ const result2 = this.bk.offsetPolygon2d(coords2d, offset2, 1e-10);
2676
+ const coords3d = [];
2677
+ for (let i = 0; i < result2.length; i += 2) {
2678
+ coords3d.push(result2[i], result2[i + 1], 0);
2679
+ }
2680
+ const wireId = this.bk.makePolygonWire(coords3d);
2681
+ return wireHandle(wireId);
2682
+ }
2683
+ // ═══════════════════════════════════════════════════════════════════════
2684
+ // Distance
2685
+ // ═══════════════════════════════════════════════════════════════════════
2686
+ distance(shape1, shape2) {
2687
+ const h1 = shape1;
2688
+ const h2 = shape2;
2689
+ if (h1.type === "solid" && h2.type === "solid") {
2690
+ const d = this.bk.solidToSolidDistance(h1.id, h2.id);
2691
+ return { value: d, point1: [0, 0, 0], point2: [0, 0, 0] };
2692
+ }
2693
+ if (h1.type === "vertex" && h2.type === "solid") {
2694
+ const pos = this.bk.getVertexPosition(h1.id);
2695
+ const result2 = this.bk.pointToSolidDistance(pos[0], pos[1], pos[2], h2.id);
2696
+ return {
2697
+ value: result2[0],
2698
+ point1: [pos[0], pos[1], pos[2]],
2699
+ point2: [result2[1], result2[2], result2[3]]
2700
+ };
2701
+ }
2702
+ if (h1.type === "vertex" && h2.type === "face") {
2703
+ const pos = this.bk.getVertexPosition(h1.id);
2704
+ const result2 = this.bk.pointToFaceDistance(pos[0], pos[1], pos[2], h2.id);
2705
+ return {
2706
+ value: result2[0],
2707
+ point1: [pos[0], pos[1], pos[2]],
2708
+ point2: [result2[1], result2[2], result2[3]]
2709
+ };
2710
+ }
2711
+ if (h1.type === "vertex" && h2.type === "edge") {
2712
+ const pos = this.bk.getVertexPosition(h1.id);
2713
+ const result2 = this.bk.pointToEdgeDistance(pos[0], pos[1], pos[2], h2.id);
2714
+ return {
2715
+ value: result2[0],
2716
+ point1: [pos[0], pos[1], pos[2]],
2717
+ point2: [result2[1], result2[2], result2[3]]
2718
+ };
2719
+ }
2720
+ const getPos = (s) => {
2721
+ if (s.type === "vertex") {
2722
+ const p = this.bk.getVertexPosition(s.id);
2723
+ return [p[0], p[1], p[2]];
2724
+ }
2725
+ if (s.type === "solid") {
2726
+ const bb = this.bk.boundingBox(s.id);
2727
+ return [(bb[0] + bb[3]) / 2, (bb[1] + bb[4]) / 2, (bb[2] + bb[5]) / 2];
2728
+ }
2729
+ return [0, 0, 0];
2730
+ };
2731
+ const p1 = getPos(h1);
2732
+ const p2 = getPos(h2);
2733
+ const dx = p2[0] - p1[0];
2734
+ const dy = p2[1] - p1[1];
2735
+ const dz = p2[2] - p1[2];
2736
+ return { value: Math.sqrt(dx * dx + dy * dy + dz * dz), point1: p1, point2: p2 };
2737
+ }
2738
+ // ═══════════════════════════════════════════════════════════════════════
2739
+ // Classification
2740
+ // ═══════════════════════════════════════════════════════════════════════
2741
+ classifyPointOnFace(face, u, v, tolerance) {
2742
+ if (tolerance !== void 0) {
2743
+ warnOnce(
2744
+ "classify-tolerance",
2745
+ "classifyPointOnFace() tolerance parameter is not supported; brepkit uses domain-based classification."
2746
+ );
2747
+ }
2748
+ const faceId = unwrap(face, "face");
2749
+ const domain = this.bk.getSurfaceDomain(faceId);
2750
+ if (u < domain[0] || u > domain[1] || v < domain[2] || v > domain[3]) {
2751
+ return "out";
2752
+ }
2753
+ return "in";
2754
+ }
2755
+ // ═══════════════════════════════════════════════════════════════════════
2756
+ // Curve construction
2757
+ // ═══════════════════════════════════════════════════════════════════════
2758
+ interpolatePoints(points, options) {
2759
+ if (options?.tolerance !== void 0) {
2760
+ warnOnce(
2761
+ "interpolate-tolerance",
2762
+ "interpolatePoints() tolerance parameter is not supported; brepkit uses chord-length parameterisation."
2763
+ );
2764
+ }
2765
+ if (points.length < 2) throw new Error("brepkit: need at least 2 points");
2766
+ if (points.length === 2) {
2767
+ return this.makeLineEdge(points[0], points[1]);
2768
+ }
2769
+ const degree = Math.min(3, points.length - 1);
2770
+ const coords = points.flatMap(([x, y, z]) => [x, y, z]);
2771
+ const id = this.bk.interpolatePoints(coords, degree);
2772
+ return edgeHandle(id);
2773
+ }
2774
+ approximatePoints(points, options) {
2775
+ const degree = options?.degMax ?? 3;
2776
+ const tol = options?.tolerance ?? 1e-6;
2777
+ const coords = [];
2778
+ for (const p of points) coords.push(p[0], p[1], p[2]);
2779
+ const numCps = Math.max(degree + 1, Math.min(points.length, Math.ceil(points.length * 0.7)));
2780
+ const id = this.bk.approximateCurveLspia(coords, degree, numCps, tol, 100);
2781
+ return edgeHandle(id);
2782
+ }
2783
+ // ═══════════════════════════════════════════════════════════════════════
2784
+ // Serialization
2785
+ // ═══════════════════════════════════════════════════════════════════════
2786
+ toBREP(shape2) {
2787
+ warnOnce(
2788
+ "brep-format",
2789
+ "toBREP/fromBREP uses STEP format (not OCCT BREP). Cross-kernel BREP round-trips are not supported."
2790
+ );
2791
+ return this.exportSTEP([shape2]);
2792
+ }
2793
+ fromBREP(data) {
2794
+ warnOnce(
2795
+ "brep-format",
2796
+ "toBREP/fromBREP uses STEP format (not OCCT BREP). Cross-kernel BREP round-trips are not supported."
2797
+ );
2798
+ const shapes = this.importSTEP(data);
2799
+ if (shapes.length === 0) throw new Error("brepkit: fromBREP produced no shapes");
2800
+ return shapes[0];
2801
+ }
2802
+ // ═══════════════════════════════════════════════════════════════════════
2803
+ // Mesh preparation
2804
+ // ═══════════════════════════════════════════════════════════════════════
2805
+ hasTriangulation(_shape) {
2806
+ return false;
2807
+ }
2808
+ meshShape(_shape, _tolerance, _angularTolerance) {
2809
+ }
2810
+ // ═══════════════════════════════════════════════════════════════════════
2811
+ // Composed transforms
2812
+ // ═══════════════════════════════════════════════════════════════════════
2813
+ composeTransform(ops) {
2814
+ let matrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
2815
+ for (const op of ops) {
2816
+ const m = op.type === "translate" ? translationMatrix(op.x, op.y, op.z) : rotationMatrix(op.angle, op.axis, op.center);
2817
+ matrix = multiplyMatrices(m, matrix);
2818
+ }
2819
+ return { handle: matrix, dispose: () => {
2820
+ } };
2821
+ }
2822
+ applyComposedTransformWithHistory(shape2, transformHandle, inputFaceHashes, hashUpperBound) {
2823
+ const result2 = this.applyMatrix(shape2, transformHandle);
2824
+ return this.buildEvolution(result2, inputFaceHashes, hashUpperBound, true);
2825
+ }
2826
+ // ═══════════════════════════════════════════════════════════════════════
2827
+ // Advanced sweep/loft
2828
+ // ═══════════════════════════════════════════════════════════════════════
2829
+ sweepPipeShell(profile, spine, options) {
2830
+ const profileHandle = profile;
2831
+ const faceId = profileHandle.type === "wire" ? this.bk.makeFaceFromWire(profileHandle.id) : unwrap(profile, "face");
2832
+ const shellMode = !!(options && options["shellMode"]);
2833
+ const nurbsData = this.extractNurbsFromEdge(spine);
2834
+ if (nurbsData && nurbsData.degree > 1) {
2835
+ try {
2836
+ const id = this.bk.sweepSmooth(
2837
+ faceId,
2838
+ nurbsData.degree,
2839
+ nurbsData.knots,
2840
+ nurbsData.controlPoints,
2841
+ nurbsData.weights
2842
+ );
2843
+ const shape22 = solidHandle(id);
2844
+ if (shellMode) return { shape: shape22, firstShape: profile, lastShape: profile };
2845
+ return shape22;
2846
+ } catch (e) {
2847
+ console.warn("brepkit: sweepSmooth failed, falling back to simplePipe:", e);
2848
+ }
2849
+ }
2850
+ const shape2 = this.simplePipe(profile, spine);
2851
+ if (shellMode) return { shape: shape2, firstShape: profile, lastShape: profile };
2852
+ return shape2;
2853
+ }
2854
+ loftAdvanced(wires, options) {
2855
+ const faceIds = wires.map((w) => {
2856
+ const h = w;
2857
+ if (h.type === "wire") return this.bk.makeFaceFromWire(h.id);
2858
+ return unwrap(w, "face");
2859
+ });
2860
+ try {
2861
+ const opts = {};
2862
+ if (options?.ruled !== void 0) opts["ruled"] = options.ruled;
2863
+ if (options?.solid !== void 0) opts["solid"] = options.solid;
2864
+ if (options?.tolerance !== void 0) opts["tolerance"] = options.tolerance;
2865
+ if (options?.startVertex) {
2866
+ const pos = this.bk.getVertexPosition(unwrap(options.startVertex, "vertex"));
2867
+ opts["startPoint"] = [pos[0], pos[1], pos[2]];
2868
+ }
2869
+ if (options?.endVertex) {
2870
+ const pos = this.bk.getVertexPosition(unwrap(options.endVertex, "vertex"));
2871
+ opts["endPoint"] = [pos[0], pos[1], pos[2]];
2872
+ }
2873
+ const id = this.bk.loftWithOptions(faceIds, JSON.stringify(opts));
2874
+ return solidHandle(id);
2875
+ } catch (e) {
2876
+ console.warn("brepkit: loftWithOptions failed, falling back to smooth/basic loft:", e);
2877
+ }
2878
+ if (!options?.ruled) {
2879
+ try {
2880
+ const id = this.bk.loftSmooth(faceIds);
2881
+ return solidHandle(id);
2882
+ } catch (e) {
2883
+ console.warn("brepkit: loftSmooth failed, falling back to basic loft:", e);
2884
+ }
2885
+ }
2886
+ return this.loft(wires);
2887
+ }
2888
+ buildExtrusionLaw(profile, length, endFactor) {
2889
+ const law = {
2890
+ type: "extrusionLaw",
2891
+ profile,
2892
+ length,
2893
+ endFactor,
2894
+ Trim(_first, _last, _tol) {
2895
+ return law;
2896
+ },
2897
+ delete: noop
2898
+ };
2899
+ return law;
2900
+ }
2901
+ // ═══════════════════════════════════════════════════════════════════════
2902
+ // Curve positioning & patterns
2903
+ // ═══════════════════════════════════════════════════════════════════════
2904
+ positionOnCurve(shape2, spine, param) {
2905
+ const { point, tangent } = this.curveTangent(spine, param);
2906
+ const [tx, ty, tz] = tangent;
2907
+ const len = Math.sqrt(tx * tx + ty * ty + tz * tz);
2908
+ if (len < 1e-12) return this.translate(shape2, point[0], point[1], point[2]);
2909
+ const nx = tx / len, ny = ty / len, nz = tz / len;
2910
+ const dot = nz;
2911
+ let result2 = shape2;
2912
+ if (Math.abs(dot + 1) < 1e-10) {
2913
+ result2 = this.rotate(result2, 180, [1, 0, 0]);
2914
+ } else if (Math.abs(dot - 1) > 1e-10) {
2915
+ const axis = [-ny, nx, 0];
2916
+ const angleDeg = Math.acos(Math.max(-1, Math.min(1, dot))) * (180 / Math.PI);
2917
+ result2 = this.rotate(result2, angleDeg, axis);
2918
+ }
2919
+ return this.translate(result2, point[0], point[1], point[2]);
2920
+ }
2921
+ linearPattern(shape2, direction, spacing, count) {
2922
+ const results = [shape2];
2923
+ for (let i = 1; i < count; i++) {
2924
+ const offset2 = spacing * i;
2925
+ results.push(
2926
+ this.translate(shape2, direction[0] * offset2, direction[1] * offset2, direction[2] * offset2)
2927
+ );
2928
+ }
2929
+ return results;
2930
+ }
2931
+ circularPattern(shape2, center, axis, angleStep, count) {
2932
+ const results = [shape2];
2933
+ for (let i = 1; i < count; i++) {
2934
+ results.push(this.rotate(shape2, angleStep * i, axis, center));
2935
+ }
2936
+ return results;
2937
+ }
2938
+ gridPattern(shape2, directionX, directionY, spacingX, spacingY, countX, countY) {
2939
+ const id = this.bk.gridPattern(
2940
+ unwrapSolidOrThrow(shape2, "gridPattern"),
2941
+ directionX[0],
2942
+ directionX[1],
2943
+ directionX[2],
2944
+ directionY[0],
2945
+ directionY[1],
2946
+ directionY[2],
2947
+ spacingX,
2948
+ spacingY,
2949
+ countX,
2950
+ countY
2951
+ );
2952
+ return compoundHandle(id);
2953
+ }
2954
+ // ═══════════════════════════════════════════════════════════════════════
2955
+ // Surface construction
2956
+ // ═══════════════════════════════════════════════════════════════════════
2957
+ makeNonPlanarFace(wire) {
2958
+ return this.makeFace(wire, true);
2959
+ }
2960
+ addHolesInFace(face, holeWires) {
2961
+ const wireIds = holeWires.map((w) => unwrap(w, "wire"));
2962
+ const id = this.bk.addHolesToFace(unwrap(face, "face"), wireIds);
2963
+ return faceHandle(id);
2964
+ }
2965
+ // TODO: Expose bk.removeHolesFromFace() — inverse of addHolesInFace.
2966
+ // Useful for defeaturing workflows. See brepkitWasmTypes.ts.
2967
+ makeFaceOnSurface(_surface, wire) {
2968
+ return this.makeFace(wire, true);
2969
+ }
2970
+ bsplineSurface(points, rows, cols) {
2971
+ const coords = [];
2972
+ for (const [x, y, z] of points) {
2973
+ coords.push(x, y, z);
2974
+ }
2975
+ const degreeU = Math.min(3, rows - 1);
2976
+ const degreeV = Math.min(3, cols - 1);
2977
+ try {
2978
+ const faceId = this.bk.interpolateSurface(coords, rows, cols, degreeU, degreeV);
2979
+ return faceHandle(faceId);
2980
+ } catch {
2981
+ return this.triangulatedSurface(points, rows, cols);
2982
+ }
2983
+ }
2984
+ triangulatedSurface(points, rows, cols) {
2985
+ const faces = [];
2986
+ for (let r = 0; r < rows - 1; r++) {
2987
+ for (let c = 0; c < cols - 1; c++) {
2988
+ const i00 = r * cols + c;
2989
+ const i10 = (r + 1) * cols + c;
2990
+ const i01 = r * cols + (c + 1);
2991
+ const i11 = (r + 1) * cols + (c + 1);
2992
+ const f1 = this.buildTriFace(points[i00], points[i10], points[i01]);
2993
+ if (f1) faces.push(f1);
2994
+ const f2 = this.buildTriFace(points[i10], points[i11], points[i01]);
2995
+ if (f2) faces.push(f2);
2996
+ }
2997
+ }
2998
+ if (faces.length === 0) throw new Error("brepkit: no valid faces in surface grid");
2999
+ return this.sew(faces, 1e-6);
3000
+ }
3001
+ // ═══════════════════════════════════════════════════════════════════════
3002
+ // Mesh sewing -> solid
3003
+ // ═══════════════════════════════════════════════════════════════════════
3004
+ buildTriFace(a, b, c) {
3005
+ const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
3006
+ const ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
3007
+ const cross = [
3008
+ ab[1] * ac[2] - ab[2] * ac[1],
3009
+ ab[2] * ac[0] - ab[0] * ac[2],
3010
+ ab[0] * ac[1] - ab[1] * ac[0]
3011
+ ];
3012
+ const area = Math.sqrt(cross[0] ** 2 + cross[1] ** 2 + cross[2] ** 2);
3013
+ if (area < 1e-12) return null;
3014
+ try {
3015
+ const e1 = this.makeLineEdge(a, b);
3016
+ const e2 = this.makeLineEdge(b, c);
3017
+ const e3 = this.makeLineEdge(c, a);
3018
+ const wire = this.makeWire([e1, e2, e3]);
3019
+ return this.makeFace(wire);
3020
+ } catch (e) {
3021
+ console.warn("brepkit: makeNonPlanarFace failed:", e);
3022
+ return null;
3023
+ }
3024
+ }
3025
+ sewAndSolidify(faces, tolerance) {
3026
+ const faceIds = faces.map((s) => unwrap(s, "face"));
3027
+ const solidId = this.bk.sewFaces(faceIds, tolerance);
3028
+ return solidHandle(solidId);
3029
+ }
3030
+ // ═══════════════════════════════════════════════════════════════════════
3031
+ // Repair
3032
+ // ═══════════════════════════════════════════════════════════════════════
3033
+ fixShape(shape2) {
3034
+ const h = shape2;
3035
+ if (h.type === "solid") {
3036
+ this.bk.healSolid(h.id);
3037
+ }
3038
+ return shape2;
3039
+ }
3040
+ fixSelfIntersection(wire) {
3041
+ return wire;
3042
+ }
3043
+ // ═══════════════════════════════════════════════════════════════════════
3044
+ // Measurement (advanced)
3045
+ // ═══════════════════════════════════════════════════════════════════════
3046
+ surfaceCurvature(face, u, v) {
3047
+ const fid = unwrap(face, "face");
3048
+ const data = this.bk.measureCurvatureAtSurface(fid, u, v);
3049
+ if (data.length < 8) {
3050
+ throw new Error(
3051
+ `brepkit: measureCurvatureAtSurface returned ${data.length} values, expected 8`
3052
+ );
3053
+ }
3054
+ const k1 = data[0];
3055
+ const k2 = data[1];
3056
+ const gaussian = k1 * k2;
3057
+ const mean = (k1 + k2) / 2;
3058
+ return {
3059
+ gaussian,
3060
+ mean,
3061
+ max: Math.max(k1, k2),
3062
+ min: Math.min(k1, k2),
3063
+ maxDirection: [data[2], data[3], data[4]],
3064
+ minDirection: [data[5], data[6], data[7]]
3065
+ };
3066
+ }
3067
+ surfaceCenterOfMass(face) {
3068
+ const mesh2 = this.bk.tessellateFace(unwrap(face, "face"), 0.1);
3069
+ const pos = mesh2.positions;
3070
+ const idx = mesh2.indices;
3071
+ let cx = 0, cy = 0, cz = 0, totalArea = 0;
3072
+ for (let t = 0; t < idx.length; t += 3) {
3073
+ const i0 = idx[t] * 3, i1 = idx[t + 1] * 3, i2 = idx[t + 2] * 3;
3074
+ const tcx = (pos[i0] + pos[i1] + pos[i2]) / 3;
3075
+ const tcy = (pos[i0 + 1] + pos[i1 + 1] + pos[i2 + 1]) / 3;
3076
+ const tcz = (pos[i0 + 2] + pos[i1 + 2] + pos[i2 + 2]) / 3;
3077
+ const ux = pos[i1] - pos[i0], uy = pos[i1 + 1] - pos[i0 + 1], uz = pos[i1 + 2] - pos[i0 + 2];
3078
+ const vx = pos[i2] - pos[i0], vy = pos[i2 + 1] - pos[i0 + 1], vz = pos[i2 + 2] - pos[i0 + 2];
3079
+ const area = 0.5 * Math.sqrt((uy * vz - uz * vy) ** 2 + (uz * vx - ux * vz) ** 2 + (ux * vy - uy * vx) ** 2);
3080
+ cx += tcx * area;
3081
+ cy += tcy * area;
3082
+ cz += tcz * area;
3083
+ totalArea += area;
3084
+ }
3085
+ if (totalArea < 1e-30) return [0, 0, 0];
3086
+ return [cx / totalArea, cy / totalArea, cz / totalArea];
3087
+ }
3088
+ createDistanceQuery(referenceShape) {
3089
+ const distanceFn = (shape2) => this.distance(referenceShape, shape2);
3090
+ return {
3091
+ distanceTo(shape2) {
3092
+ return distanceFn(shape2);
3093
+ },
3094
+ dispose() {
3095
+ }
3096
+ };
3097
+ }
3098
+ // ═══════════════════════════════════════════════════════════════════════
3099
+ // Projection
3100
+ // ═══════════════════════════════════════════════════════════════════════
3101
+ projectEdges(shape2, _cameraOrigin, _cameraDirection, _cameraXAxis) {
3102
+ const edges = this.iterShapes(shape2, "edge");
3103
+ const emptyCompound = edges.length > 0 ? edges[0] : shape2;
3104
+ return {
3105
+ visible: { outline: emptyCompound, smooth: emptyCompound, sharp: emptyCompound },
3106
+ hidden: { outline: emptyCompound, smooth: emptyCompound, sharp: emptyCompound }
3107
+ };
3108
+ }
3109
+ // ═══════════════════════════════════════════════════════════════════════
3110
+ // Draft
3111
+ // ═══════════════════════════════════════════════════════════════════════
3112
+ draftPrism(shape2, face, _baseFace, height, _angleDeg, fuse2) {
3113
+ if (height !== null) {
3114
+ const normal = this.surfaceNormal(face, 0, 0);
3115
+ const extruded = this.extrude(face, normal, height);
3116
+ if (fuse2) {
3117
+ return this.fuse(shape2, extruded);
3118
+ }
3119
+ return extruded;
3120
+ }
3121
+ return shape2;
3122
+ }
3123
+ // ═══════════════════════════════════════════════════════════════════════
3124
+ // XCAF / configured export
3125
+ // ═══════════════════════════════════════════════════════════════════════
3126
+ createXCAFDocument(shapes) {
3127
+ return { __brepkit_xcaf: true, shapes, delete: noop };
3128
+ }
3129
+ writeXCAFToSTEP(doc, _options) {
3130
+ if (doc && doc.__brepkit_xcaf && Array.isArray(doc.shapes)) {
3131
+ return this.exportSTEP(doc.shapes.map((s) => s.shape));
3132
+ }
3133
+ return "";
3134
+ }
3135
+ exportSTEPConfigured(shapes, _options) {
3136
+ return this.exportSTEP(shapes.map((s) => s.shape));
3137
+ }
3138
+ // ═══════════════════════════════════════════════════════════════════════
3139
+ // Export helpers
3140
+ // ═══════════════════════════════════════════════════════════════════════
3141
+ wrapString(str) {
3142
+ return str;
3143
+ }
3144
+ wrapColor(red, green, blue, alpha) {
3145
+ return [red, green, blue, alpha];
3146
+ }
3147
+ configureStepUnits(_unit, _modelUnit) {
3148
+ }
3149
+ configureStepWriter(_writer) {
3150
+ }
3151
+ // ═══════════════════════════════════════════════════════════════════════
3152
+ // Curve adaptor
3153
+ // ═══════════════════════════════════════════════════════════════════════
3154
+ createCurveAdaptor(shape2) {
3155
+ return shape2;
3156
+ }
3157
+ // ═══════════════════════════════════════════════════════════════════════
3158
+ // Bezier pole extraction
3159
+ // ═══════════════════════════════════════════════════════════════════════
3160
+ getBezierPenultimatePole(edge) {
3161
+ const nurbsData = this.extractNurbsFromEdge(edge);
3162
+ if (!nurbsData || nurbsData.controlPoints.length < 6) return null;
3163
+ const n = nurbsData.controlPoints.length;
3164
+ return [
3165
+ nurbsData.controlPoints[n - 6],
3166
+ nurbsData.controlPoints[n - 5],
3167
+ nurbsData.controlPoints[n - 4]
3168
+ ];
3169
+ }
3170
+ // ═══════════════════════════════════════════════════════════════════════
3171
+ // Surface geometry extraction
3172
+ // ═══════════════════════════════════════════════════════════════════════
3173
+ getSurfaceCylinderData(surface) {
3174
+ if (isBrepkitHandle(surface) && surface.type === "face") {
3175
+ const faceId = surface.id;
3176
+ const params = JSON.parse(this.bk.getAnalyticSurfaceParams(faceId));
3177
+ if (params.type === "cylinder") {
3178
+ return { radius: params.radius, isDirect: true };
3179
+ }
3180
+ }
3181
+ return null;
3182
+ }
3183
+ reverseSurfaceU(surface) {
3184
+ return surface;
3185
+ }
3186
+ // ═══════════════════════════════════════════════════════════════════════
3187
+ // 3D geometry primitive factories
3188
+ // ═══════════════════════════════════════════════════════════════════════
3189
+ createPoint3d(x, y, z) {
3190
+ return { x, y, z };
3191
+ }
3192
+ createDirection3d(x, y, z) {
3193
+ const len = Math.sqrt(x * x + y * y + z * z);
3194
+ return { x: x / len, y: y / len, z: z / len };
3195
+ }
3196
+ createVector3d(x, y, z) {
3197
+ return { x, y, z };
3198
+ }
3199
+ createAxis1(cx, cy, cz, dx, dy, dz) {
3200
+ return { origin: [cx, cy, cz], direction: [dx, dy, dz] };
3201
+ }
3202
+ createAxis2(ox, oy, oz, zx, zy, zz, xx, xy, xz) {
3203
+ return {
3204
+ origin: [ox, oy, oz],
3205
+ z: [zx, zy, zz],
3206
+ x: xx !== void 0 ? [xx, xy, xz] : void 0
3207
+ };
3208
+ }
3209
+ createAxis3(ox, oy, oz, zx, zy, zz, xx, xy, xz) {
3210
+ return {
3211
+ origin: [ox, oy, oz],
3212
+ z: [zx, zy, zz],
3213
+ x: xx !== void 0 ? [xx, xy, xz] : void 0
3214
+ };
3215
+ }
3216
+ // ═══════════════════════════════════════════════════════════════════════
3217
+ // Shape reversal
3218
+ // ═══════════════════════════════════════════════════════════════════════
3219
+ reverseShape(shape2) {
3220
+ const h = shape2;
3221
+ const newId = this.bk.reverseShape(h.id);
3222
+ return handle(h.type, newId);
3223
+ }
3224
+ // ═══════════════════════════════════════════════════════════════════════
3225
+ // Dispose
3226
+ // ═══════════════════════════════════════════════════════════════════════
3227
+ // TODO: Expose bk.checkpoint() / bk.restore() / bk.discardCheckpoint() for
3228
+ // transactional arena management. Could back a future undo/redo or speculative
3229
+ // operation API (try operation → restore on failure). See brepkitWasmTypes.ts.
3230
+ dispose(_handle) {
3231
+ }
3232
+ // ═══════════════════════════════════════════════════════════════════════
3233
+ // Kernel2DCapability (stubs — returns false from supportsKernel2D)
3234
+ // ═══════════════════════════════════════════════════════════════════════
3235
+ // ═══════════════════════════════════════════════════════════════════════
3236
+ // Kernel2DCapability — pure TypeScript implementation
3237
+ // ═══════════════════════════════════════════════════════════════════════
3238
+ c2d(h) {
3239
+ return h;
3240
+ }
3241
+ /** Unwrap any trimmed wrappers to get the underlying geometry. */
3242
+ c2dBasis(h) {
3243
+ let c = this.c2d(h);
3244
+ while (c.__bk2d === "trimmed") c = c.basis;
3245
+ return c;
3246
+ }
3247
+ bb2d(h) {
3248
+ return h;
3249
+ }
3250
+ createPoint2d(x, y) {
3251
+ return { x, y };
3252
+ }
3253
+ createDirection2d(x, y) {
3254
+ const l = Math.sqrt(x * x + y * y);
3255
+ return { x: x / l, y: y / l };
3256
+ }
3257
+ createVector2d(x, y) {
3258
+ return { x, y };
3259
+ }
3260
+ createAxis2d(px, py, dx, dy) {
3261
+ return { px, py, dx, dy };
3262
+ }
3263
+ wrapCurve2dHandle(handle2) {
3264
+ return handle2;
3265
+ }
3266
+ createCurve2dAdaptor(handle2) {
3267
+ return handle2;
3268
+ }
3269
+ makeLine2d(x1, y1, x2, y2) {
3270
+ return makeLine2d(x1, y1, x2, y2);
3271
+ }
3272
+ makeCircle2d(cx, cy, radius, sense) {
3273
+ return makeCircle2d(cx, cy, radius, sense);
3274
+ }
3275
+ makeArc2dThreePoints(x1, y1, xm, ym, x2, y2) {
3276
+ const d = 2 * (x1 * (ym - y2) + xm * (y2 - y1) + x2 * (y1 - ym));
3277
+ if (Math.abs(d) < 1e-12) {
3278
+ return makeLine2d(x1, y1, x2, y2);
3279
+ }
3280
+ const cx = ((x1 * x1 + y1 * y1) * (ym - y2) + (xm * xm + ym * ym) * (y2 - y1) + (x2 * x2 + y2 * y2) * (y1 - ym)) / d;
3281
+ const cy = ((x1 * x1 + y1 * y1) * (x2 - xm) + (xm * xm + ym * ym) * (x1 - x2) + (x2 * x2 + y2 * y2) * (xm - x1)) / d;
3282
+ const radius = Math.sqrt((x1 - cx) ** 2 + (y1 - cy) ** 2);
3283
+ const a1 = Math.atan2(y1 - cy, x1 - cx);
3284
+ const am = Math.atan2(ym - cy, xm - cx);
3285
+ const a2 = Math.atan2(y2 - cy, x2 - cx);
3286
+ let da1m = am - a1;
3287
+ if (da1m < 0) da1m += 2 * Math.PI;
3288
+ let da12 = a2 - a1;
3289
+ if (da12 < 0) da12 += 2 * Math.PI;
3290
+ const sense = da1m < da12;
3291
+ const circle = makeCircle2d(cx, cy, radius, sense);
3292
+ if (!sense) {
3293
+ return { __bk2d: "trimmed", basis: circle, tStart: -a1, tEnd: -a2 };
3294
+ }
3295
+ return { __bk2d: "trimmed", basis: circle, tStart: a1, tEnd: a2 };
3296
+ }
3297
+ makeArc2dTangent(sx, sy, tx, ty, ex, ey) {
3298
+ const len = Math.sqrt(tx * tx + ty * ty);
3299
+ const ntx = len > 0 ? tx / len : 0;
3300
+ const nty = len > 0 ? ty / len : 0;
3301
+ const chord = Math.sqrt((ex - sx) ** 2 + (ey - sy) ** 2);
3302
+ const offset2 = chord * 0.25;
3303
+ return this.makeArc2dThreePoints(
3304
+ sx,
3305
+ sy,
3306
+ (sx + ex) / 2 + nty * offset2,
3307
+ (sy + ey) / 2 - ntx * offset2,
3308
+ ex,
3309
+ ey
3310
+ );
3311
+ }
3312
+ makeEllipse2d(cx, cy, major, minor, xDirX, xDirY, sense) {
3313
+ return makeEllipse2d(cx, cy, major, minor, xDirX, xDirY, sense);
3314
+ }
3315
+ makeEllipseArc2d(cx, cy, major, minor, start, end, xDirX, xDirY, sense) {
3316
+ const ellipse = makeEllipse2d(cx, cy, major, minor, xDirX, xDirY, sense);
3317
+ return { __bk2d: "trimmed", basis: ellipse, tStart: start, tEnd: end };
3318
+ }
3319
+ makeBezier2d(points) {
3320
+ return makeBezier2d(points);
3321
+ }
3322
+ makeBSpline2d(points, _options) {
3323
+ const n = points.length;
3324
+ const degree = Math.min(3, n - 1);
3325
+ const knots = [];
3326
+ const mults = [];
3327
+ knots.push(0);
3328
+ mults.push(degree + 1);
3329
+ const nInternal = n - degree - 1;
3330
+ for (let i = 1; i <= nInternal; i++) {
3331
+ knots.push(i / (nInternal + 1));
3332
+ mults.push(1);
3333
+ }
3334
+ knots.push(1);
3335
+ mults.push(degree + 1);
3336
+ return {
3337
+ __bk2d: "bspline",
3338
+ poles: [...points],
3339
+ knots,
3340
+ multiplicities: mults,
3341
+ degree,
3342
+ isPeriodic: false
3343
+ };
3344
+ }
3345
+ evaluateCurve2d(curve, param) {
3346
+ return evaluateCurve2d(this.c2d(curve), param);
3347
+ }
3348
+ evaluateCurve2dD1(curve, param) {
3349
+ return {
3350
+ point: evaluateCurve2d(this.c2d(curve), param),
3351
+ tangent: tangentCurve2d(this.c2d(curve), param)
3352
+ };
3353
+ }
3354
+ getCurve2dBounds(curve) {
3355
+ return curveBounds(this.c2d(curve));
3356
+ }
3357
+ getCurve2dType(curve) {
3358
+ return curveTypeName(this.c2dBasis(curve));
3359
+ }
3360
+ trimCurve2d(curve, start, end) {
3361
+ return { __bk2d: "trimmed", basis: this.c2d(curve), tStart: start, tEnd: end };
3362
+ }
3363
+ reverseCurve2d(_curve) {
3364
+ }
3365
+ copyCurve2d(curve) {
3366
+ return JSON.parse(JSON.stringify(curve));
3367
+ }
3368
+ offsetCurve2d(curve, offset2) {
3369
+ const c = this.c2d(curve);
3370
+ const bounds = curveBounds(c);
3371
+ const N = 30;
3372
+ const poles = [];
3373
+ for (let i = 0; i <= N; i++) {
3374
+ const t = bounds.first + (bounds.last - bounds.first) * i / N;
3375
+ const [px, py] = evaluateCurve2d(c, t);
3376
+ const [tx, ty] = tangentCurve2d(c, t);
3377
+ const tLen = Math.sqrt(tx * tx + ty * ty);
3378
+ if (tLen > 1e-12) {
3379
+ poles.push([px - ty / tLen * offset2, py + tx / tLen * offset2]);
3380
+ } else {
3381
+ poles.push([px, py]);
3382
+ }
3383
+ }
3384
+ return this.makeBSpline2d(poles);
3385
+ }
3386
+ translateCurve2d(curve, dx, dy) {
3387
+ return translateCurve2d(this.c2d(curve), dx, dy);
3388
+ }
3389
+ rotateCurve2d(curve, angle, cx, cy) {
3390
+ return rotateCurve2d(this.c2d(curve), angle, cx, cy);
3391
+ }
3392
+ scaleCurve2d(curve, factor, cx, cy) {
3393
+ return scaleCurve2d(this.c2d(curve), factor, cx, cy);
3394
+ }
3395
+ mirrorCurve2dAtPoint(curve, cx, cy) {
3396
+ return mirrorAtPoint(this.c2d(curve), cx, cy);
3397
+ }
3398
+ mirrorCurve2dAcrossAxis(curve, ox, oy, dx, dy) {
3399
+ return mirrorAcrossAxis(this.c2d(curve), ox, oy, dx, dy);
3400
+ }
3401
+ affinityTransform2d(curve, ox, oy, dx, dy, ratio) {
3402
+ const len = Math.sqrt(dx * dx + dy * dy);
3403
+ if (len < 1e-15) return this.c2d(curve);
3404
+ const ax = dx / len, ay = dy / len;
3405
+ const px = -ay, py = ax;
3406
+ const k = ratio - 1;
3407
+ const m00 = 1 + k * px * px, m01 = k * px * py;
3408
+ const m10 = k * py * px, m11 = 1 + k * py * py;
3409
+ const txOff = ox - m00 * ox - m01 * oy;
3410
+ const tyOff = oy - m10 * ox - m11 * oy;
3411
+ const gtrsf = { m: [m00, m01, 0, m10, m11, 0, 0, 0, 1], tx: txOff, ty: tyOff };
3412
+ return this.transformCurve2dGeneral(curve, gtrsf);
3413
+ }
3414
+ // --- General 2D transforms (stored as 3×3 matrices) ---
3415
+ createIdentityGTrsf2d() {
3416
+ return { m: [1, 0, 0, 0, 1, 0, 0, 0, 1], tx: 0, ty: 0 };
3417
+ }
3418
+ createAffinityGTrsf2d(ox, oy, dx, dy, ratio) {
3419
+ const len = Math.sqrt(dx * dx + dy * dy);
3420
+ if (len < 1e-15) return this.createIdentityGTrsf2d();
3421
+ const px = -dy / len, py = dx / len;
3422
+ const k = ratio - 1;
3423
+ const m = [1 + k * px * px, k * px * py, 0, k * py * px, 1 + k * py * py, 0, 0, 0, 1];
3424
+ const txv = ox - m[0] * ox - m[1] * oy;
3425
+ const tyv = oy - m[3] * ox - m[4] * oy;
3426
+ return { m, tx: txv, ty: tyv };
3427
+ }
3428
+ createTranslationGTrsf2d(dx, dy) {
3429
+ return { m: [1, 0, 0, 0, 1, 0, 0, 0, 1], tx: dx, ty: dy };
3430
+ }
3431
+ createMirrorGTrsf2d(cx, cy, mode, ox, oy, dx, dy) {
3432
+ if (mode === "axis" && dx !== void 0 && dy !== void 0) {
3433
+ const len = Math.sqrt(dx * dx + dy * dy);
3434
+ const nx = dx / len, ny = dy / len;
3435
+ const m = [2 * nx * nx - 1, 2 * nx * ny, 0, 2 * nx * ny, 2 * ny * ny - 1, 0, 0, 0, 1];
3436
+ const px = ox ?? cx, py = oy ?? cy;
3437
+ const txv = px - m[0] * px - m[1] * py;
3438
+ const tyv = py - m[3] * px - m[4] * py;
3439
+ return { m, tx: txv, ty: tyv };
3440
+ }
3441
+ return { m: [-1, 0, 0, 0, -1, 0, 0, 0, 1], tx: 2 * cx, ty: 2 * cy };
3442
+ }
3443
+ createRotationGTrsf2d(angle, cx, cy) {
3444
+ const c = Math.cos(angle), s = Math.sin(angle);
3445
+ return { m: [c, -s, 0, s, c, 0, 0, 0, 1], tx: cx - c * cx + s * cy, ty: cy - s * cx - c * cy };
3446
+ }
3447
+ createScaleGTrsf2d(factor, cx, cy) {
3448
+ return {
3449
+ m: [factor, 0, 0, 0, factor, 0, 0, 0, 1],
3450
+ tx: cx * (1 - factor),
3451
+ ty: cy * (1 - factor)
3452
+ };
3453
+ }
3454
+ setGTrsf2dTranslationPart(gtrsf, dx, dy) {
3455
+ gtrsf.tx = dx;
3456
+ gtrsf.ty = dy;
3457
+ }
3458
+ multiplyGTrsf2d(base, other) {
3459
+ const a = base.m, b = other.m;
3460
+ const r = [
3461
+ a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
3462
+ a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
3463
+ a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
3464
+ a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
3465
+ a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
3466
+ a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
3467
+ a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
3468
+ a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
3469
+ a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
3470
+ ];
3471
+ base.m = r;
3472
+ const oldTx = base.tx, oldTy = base.ty;
3473
+ const otx = Number(other.tx) || 0, oty = Number(other.ty) || 0;
3474
+ base.tx = a[0] * otx + a[1] * oty + oldTx;
3475
+ base.ty = a[3] * otx + a[4] * oty + oldTy;
3476
+ }
3477
+ transformCurve2dGeneral(curve, gtrsf) {
3478
+ const c = this.c2d(curve);
3479
+ const m = gtrsf.m ?? [1, 0, 0, 0, 1, 0, 0, 0, 1];
3480
+ const tx = Number(gtrsf.tx) || 0, ty = Number(gtrsf.ty) || 0;
3481
+ const isIdentityMatrix = Math.abs(m[0] - 1) < 1e-12 && Math.abs(m[4] - 1) < 1e-12 && Math.abs(m[1]) < 1e-12 && Math.abs(m[3]) < 1e-12;
3482
+ if (isIdentityMatrix) {
3483
+ return translateCurve2d(c, tx, ty);
3484
+ }
3485
+ const bounds = curveBounds(c);
3486
+ const N = 20;
3487
+ const pts = [];
3488
+ for (let i = 0; i <= N; i++) {
3489
+ const t = bounds.first + (bounds.last - bounds.first) * i / N;
3490
+ const [px, py] = evaluateCurve2d(c, t);
3491
+ pts.push([m[0] * px + m[1] * py + tx, m[3] * px + m[4] * py + ty]);
3492
+ }
3493
+ return makeBezier2d(pts);
3494
+ }
3495
+ // --- 2D intersection & distance ---
3496
+ intersectCurves2d(c1, c2, tolerance) {
3497
+ const result2 = intersectCurves2dFn(this.c2d(c1), this.c2d(c2), tolerance);
3498
+ const segments = result2.segments.map(
3499
+ (s) => Object.assign(s, {
3500
+ delete() {
3501
+ }
3502
+ })
3503
+ );
3504
+ return { points: result2.points, segments };
3505
+ }
3506
+ projectPointOnCurve2d(curve, x, y) {
3507
+ const c = this.c2d(curve);
3508
+ const bounds = curveBounds(c);
3509
+ if (c.__bk2d === "line") {
3510
+ const dx = x - c.ox;
3511
+ const dy = y - c.oy;
3512
+ const t = Math.max(bounds.first, Math.min(bounds.last, dx * c.dx + dy * c.dy));
3513
+ const [px, py] = evaluateCurve2d(c, t);
3514
+ return { param: t, distance: Math.sqrt((px - x) ** 2 + (py - y) ** 2) };
3515
+ }
3516
+ if (c.__bk2d === "circle") {
3517
+ const angle = Math.atan2(y - c.cy, x - c.cx);
3518
+ let t = c.sense ? angle : -angle;
3519
+ while (t < 0) t += 2 * Math.PI;
3520
+ while (t > 2 * Math.PI) t -= 2 * Math.PI;
3521
+ const [px, py] = evaluateCurve2d(c, t);
3522
+ return { param: t, distance: Math.sqrt((px - x) ** 2 + (py - y) ** 2) };
3523
+ }
3524
+ if (!isFinite(bounds.first) || !isFinite(bounds.last)) return null;
3525
+ let bestT = bounds.first;
3526
+ let bestDist = Infinity;
3527
+ const N = 200;
3528
+ const dt = (bounds.last - bounds.first) / N;
3529
+ for (let i = 0; i <= N; i++) {
3530
+ const t = bounds.first + i * dt;
3531
+ const [px, py] = evaluateCurve2d(c, t);
3532
+ const d = (px - x) ** 2 + (py - y) ** 2;
3533
+ if (d < bestDist) {
3534
+ bestDist = d;
3535
+ bestT = t;
3536
+ }
3537
+ }
3538
+ for (let iter = 0; iter < 10; iter++) {
3539
+ const [px, py] = evaluateCurve2d(c, bestT);
3540
+ const [tx, ty] = tangentCurve2d(c, bestT);
3541
+ const dot = (px - x) * tx + (py - y) * ty;
3542
+ const denom = tx * tx + ty * ty;
3543
+ if (denom < 1e-20) break;
3544
+ const step = dot / denom;
3545
+ const newT = Math.max(bounds.first, Math.min(bounds.last, bestT - step));
3546
+ if (Math.abs(newT - bestT) < 1e-14) break;
3547
+ bestT = newT;
3548
+ }
3549
+ const [fx, fy] = evaluateCurve2d(c, bestT);
3550
+ return { param: bestT, distance: Math.sqrt((fx - x) ** 2 + (fy - y) ** 2) };
3551
+ }
3552
+ distanceBetweenCurves2d(c1, c2, p1s, p1e, p2s, p2e) {
3553
+ const curve1 = this.c2d(c1);
3554
+ const curve2 = this.c2d(c2);
3555
+ let bestT1 = p1s;
3556
+ let bestT2 = p2s;
3557
+ let minDistSq = Infinity;
3558
+ const N = 50;
3559
+ for (let i = 0; i <= N; i++) {
3560
+ const t12 = p1s + (p1e - p1s) * i / N;
3561
+ const [x1, y1] = evaluateCurve2d(curve1, t12);
3562
+ for (let j = 0; j <= N; j++) {
3563
+ const t22 = p2s + (p2e - p2s) * j / N;
3564
+ const [x2, y2] = evaluateCurve2d(curve2, t22);
3565
+ const d = (x2 - x1) ** 2 + (y2 - y1) ** 2;
3566
+ if (d < minDistSq) {
3567
+ minDistSq = d;
3568
+ bestT1 = t12;
3569
+ bestT2 = t22;
3570
+ }
3571
+ }
3572
+ }
3573
+ let t1 = bestT1;
3574
+ let t2 = bestT2;
3575
+ for (let iter = 0; iter < 20; iter++) {
3576
+ const [x2, y2] = evaluateCurve2d(curve2, t2);
3577
+ const proj1 = this.projectPointOnCurve2d(c1, x2, y2);
3578
+ if (proj1) {
3579
+ const newT1 = Math.max(p1s, Math.min(p1e, proj1.param));
3580
+ const converged1 = Math.abs(newT1 - t1) < 1e-12;
3581
+ t1 = newT1;
3582
+ if (converged1) break;
3583
+ }
3584
+ const [x1, y1] = evaluateCurve2d(curve1, t1);
3585
+ const proj2 = this.projectPointOnCurve2d(c2, x1, y1);
3586
+ if (proj2) {
3587
+ const newT2 = Math.max(p2s, Math.min(p2e, proj2.param));
3588
+ const converged2 = Math.abs(newT2 - t2) < 1e-12;
3589
+ t2 = newT2;
3590
+ if (converged2) break;
3591
+ }
3592
+ }
3593
+ const [fx1, fy1] = evaluateCurve2d(curve1, t1);
3594
+ const [fx2, fy2] = evaluateCurve2d(curve2, t2);
3595
+ return Math.sqrt((fx2 - fx1) ** 2 + (fy2 - fy1) ** 2);
3596
+ }
3597
+ approximateCurve2dAsBSpline(curve, tol, cont, maxSeg) {
3598
+ const c = this.c2d(curve);
3599
+ const bounds = curveBounds(c);
3600
+ const contDeg = cont === "C0" ? 1 : cont === "C1" ? 2 : cont === "C2" ? 3 : 4;
3601
+ const degree = Math.max(3, contDeg);
3602
+ let N = Math.max(100, maxSeg * 10);
3603
+ let poles = [];
3604
+ let maxErr = Infinity;
3605
+ for (let attempt = 0; attempt < 3 && maxErr > tol; attempt++) {
3606
+ poles = [];
3607
+ for (let i = 0; i <= N; i++) {
3608
+ const t = bounds.first + (bounds.last - bounds.first) * i / N;
3609
+ poles.push(evaluateCurve2d(c, t));
3610
+ }
3611
+ maxErr = 0;
3612
+ for (let i = 0; i < N; i++) {
3613
+ const tMid = bounds.first + (bounds.last - bounds.first) * (i + 0.5) / N;
3614
+ const [ex, ey] = evaluateCurve2d(c, tMid);
3615
+ const p0 = poles[i];
3616
+ const p1 = poles[i + 1];
3617
+ const mx = (p0[0] + p1[0]) / 2;
3618
+ const my = (p0[1] + p1[1]) / 2;
3619
+ const err = Math.sqrt((ex - mx) ** 2 + (ey - my) ** 2);
3620
+ if (err > maxErr) maxErr = err;
3621
+ }
3622
+ if (maxErr > tol) N = Math.min(N * 2, 500);
3623
+ }
3624
+ return this.makeBSpline2d(poles, { degMax: degree });
3625
+ }
3626
+ decomposeBSpline2dToBeziers(curve) {
3627
+ const c = this.c2dBasis(curve);
3628
+ if (c.__bk2d === "bezier") return [curve];
3629
+ if (c.__bk2d !== "bspline") {
3630
+ const approx = this.approximateCurve2dAsBSpline(curve, 1e-6, "C2", 10);
3631
+ return this.decomposeBSpline2dToBeziers(approx);
3632
+ }
3633
+ const trimBounds = curveBounds(this.c2d(curve));
3634
+ const first = trimBounds.first;
3635
+ const last = trimBounds.last;
3636
+ const internalKnots = [];
3637
+ for (const k of c.knots) {
3638
+ if (k > first + 1e-12 && k < last - 1e-12) internalKnots.push(k);
3639
+ }
3640
+ const breakpoints = [first, ...internalKnots, last];
3641
+ const result2 = [];
3642
+ for (let i = 0; i < breakpoints.length - 1; i++) {
3643
+ const t0 = breakpoints[i];
3644
+ const t1 = breakpoints[i + 1];
3645
+ const span = t1 - t0;
3646
+ if (span < 1e-15) continue;
3647
+ const p0 = evaluateCurve2d(c, t0);
3648
+ const p3 = evaluateCurve2d(c, t1);
3649
+ const tan0 = tangentCurve2d(c, t0);
3650
+ const tan3 = tangentCurve2d(c, t1);
3651
+ const s = span / 3;
3652
+ const bezier = {
3653
+ __bk2d: "bezier",
3654
+ poles: [
3655
+ p0,
3656
+ [p0[0] + tan0[0] * s, p0[1] + tan0[1] * s],
3657
+ [p3[0] - tan3[0] * s, p3[1] - tan3[1] * s],
3658
+ p3
3659
+ ]
3660
+ };
3661
+ result2.push(bezier);
3662
+ }
3663
+ return result2.length > 0 ? result2 : [curve];
3664
+ }
3665
+ // --- 2D bounding boxes ---
3666
+ createBoundingBox2d() {
3667
+ return createBBox2d();
3668
+ }
3669
+ addCurveToBBox2d(bbox, curve, tol) {
3670
+ addCurveToBBox(this.bb2d(bbox), this.c2d(curve));
3671
+ }
3672
+ getBBox2dBounds(bbox) {
3673
+ const b = this.bb2d(bbox);
3674
+ return { xMin: b.xMin, yMin: b.yMin, xMax: b.xMax, yMax: b.yMax };
3675
+ }
3676
+ mergeBBox2d(target, other) {
3677
+ const t = this.bb2d(target), o = this.bb2d(other);
3678
+ t.xMin = Math.min(t.xMin, o.xMin);
3679
+ t.yMin = Math.min(t.yMin, o.yMin);
3680
+ t.xMax = Math.max(t.xMax, o.xMax);
3681
+ t.yMax = Math.max(t.yMax, o.yMax);
3682
+ }
3683
+ isBBox2dOut(a, b) {
3684
+ const ba = this.bb2d(a), bb = this.bb2d(b);
3685
+ return ba.xMax < bb.xMin || bb.xMax < ba.xMin || ba.yMax < bb.yMin || bb.yMax < ba.yMin;
3686
+ }
3687
+ isBBox2dOutPoint(bbox, x, y) {
3688
+ const b = this.bb2d(bbox);
3689
+ return x < b.xMin || x > b.xMax || y < b.yMin || y > b.yMax;
3690
+ }
3691
+ // --- 2D type extraction ---
3692
+ getCurve2dCircleData(curve) {
3693
+ const c = this.c2dBasis(curve);
3694
+ if (c.__bk2d === "circle") return { cx: c.cx, cy: c.cy, radius: c.radius, isDirect: c.sense };
3695
+ return null;
3696
+ }
3697
+ getCurve2dEllipseData(curve) {
3698
+ const c = this.c2dBasis(curve);
3699
+ if (c.__bk2d === "ellipse")
3700
+ return {
3701
+ majorRadius: c.majorRadius,
3702
+ minorRadius: c.minorRadius,
3703
+ xAxisAngle: c.xDirAngle,
3704
+ isDirect: c.sense
3705
+ };
3706
+ return null;
3707
+ }
3708
+ getCurve2dBezierPoles(curve) {
3709
+ const c = this.c2dBasis(curve);
3710
+ if (c.__bk2d === "bezier") return [...c.poles];
3711
+ return null;
3712
+ }
3713
+ getCurve2dBezierDegree(curve) {
3714
+ const c = this.c2dBasis(curve);
3715
+ if (c.__bk2d === "bezier") return c.poles.length - 1;
3716
+ return null;
3717
+ }
3718
+ getCurve2dBSplineData(curve) {
3719
+ const c = this.c2dBasis(curve);
3720
+ if (c.__bk2d === "bspline")
3721
+ return {
3722
+ poles: [...c.poles],
3723
+ knots: [...c.knots],
3724
+ multiplicities: [...c.multiplicities],
3725
+ degree: c.degree,
3726
+ isPeriodic: c.isPeriodic
3727
+ };
3728
+ return null;
3729
+ }
3730
+ // --- 2D serialization ---
3731
+ serializeCurve2d(curve) {
3732
+ return serializeCurve2d(this.c2d(curve));
3733
+ }
3734
+ deserializeCurve2d(data) {
3735
+ return deserializeCurve2d(data);
3736
+ }
3737
+ // --- 2D curve splitting ---
3738
+ splitCurve2d(curve, params) {
3739
+ const c = this.c2d(curve);
3740
+ const bounds = curveBounds(c);
3741
+ const sortedParams = [bounds.first, ...params.sort((a, b) => a - b), bounds.last];
3742
+ const result2 = [];
3743
+ for (let i = 0; i < sortedParams.length - 1; i++) {
3744
+ result2.push({
3745
+ __bk2d: "trimmed",
3746
+ basis: c,
3747
+ tStart: sortedParams[i],
3748
+ tEnd: sortedParams[i + 1]
3749
+ });
3750
+ }
3751
+ return result2;
3752
+ }
3753
+ // --- 2D → 3D projection ---
3754
+ liftCurve2dToPlane(curve, origin, planeZ, planeX) {
3755
+ const c = this.c2d(curve);
3756
+ const y = [
3757
+ planeZ[1] * planeX[2] - planeZ[2] * planeX[1],
3758
+ planeZ[2] * planeX[0] - planeZ[0] * planeX[2],
3759
+ planeZ[0] * planeX[1] - planeZ[1] * planeX[0]
3760
+ ];
3761
+ const lift = (u, v) => [
3762
+ origin[0] + u * planeX[0] + v * y[0],
3763
+ origin[1] + u * planeX[1] + v * y[1],
3764
+ origin[2] + u * planeX[2] + v * y[2]
3765
+ ];
3766
+ if (c.__bk2d === "line") {
3767
+ const p1 = lift(c.ox, c.oy);
3768
+ const p2 = lift(c.ox + c.dx * c.len, c.oy + c.dy * c.len);
3769
+ return this.makeLineEdge(p1, p2);
3770
+ }
3771
+ if (c.__bk2d === "circle" || c.__bk2d === "trimmed") {
3772
+ const bounds2 = curveBounds(c);
3773
+ let angularSpan;
3774
+ if (c.__bk2d === "trimmed") {
3775
+ angularSpan = Math.abs(c.tEnd - c.tStart);
3776
+ } else {
3777
+ angularSpan = 2 * Math.PI;
3778
+ }
3779
+ const nSegments = angularSpan > Math.PI ? 4 : angularSpan > Math.PI / 2 ? 2 : 1;
3780
+ const segmentSpan = (bounds2.last - bounds2.first) / nSegments;
3781
+ const samplesPerSegment = Math.max(12, Math.ceil(angularSpan / nSegments / (Math.PI / 45)));
3782
+ if (nSegments === 1) {
3783
+ const points2 = [];
3784
+ for (let i = 0; i <= samplesPerSegment; i++) {
3785
+ const t = bounds2.first + (bounds2.last - bounds2.first) * i / samplesPerSegment;
3786
+ const [u, v] = evaluateCurve2d(c, t);
3787
+ points2.push(lift(u, v));
3788
+ }
3789
+ return this.interpolatePoints(points2);
3790
+ }
3791
+ const edgeIds = [];
3792
+ for (let seg = 0; seg < nSegments; seg++) {
3793
+ const segStart = bounds2.first + seg * segmentSpan;
3794
+ const segEnd = bounds2.first + (seg + 1) * segmentSpan;
3795
+ const points2 = [];
3796
+ for (let i = 0; i <= samplesPerSegment; i++) {
3797
+ const t = segStart + (segEnd - segStart) * i / samplesPerSegment;
3798
+ const [u, v] = evaluateCurve2d(c, t);
3799
+ points2.push(lift(u, v));
3800
+ }
3801
+ const coords = points2.flatMap(([px, py, pz]) => [px, py, pz]);
3802
+ const degree = Math.min(3, points2.length - 1);
3803
+ edgeIds.push(this.bk.interpolatePoints(coords, degree));
3804
+ }
3805
+ const wireId = this.bk.makeWire(edgeIds, false);
3806
+ return wireHandle(wireId);
3807
+ }
3808
+ if (c.__bk2d === "bezier") {
3809
+ const points3d = c.poles.map(([u, v]) => lift(u, v));
3810
+ if (points3d.length === 2) return this.makeLineEdge(points3d[0], points3d[1]);
3811
+ const degree = Math.min(3, points3d.length - 1);
3812
+ const coords = points3d.flatMap(([px, py, pz]) => [px, py, pz]);
3813
+ const id = this.bk.interpolatePoints(coords, degree);
3814
+ return edgeHandle(id);
3815
+ }
3816
+ if (c.__bk2d === "bspline") {
3817
+ const points3d = c.poles.map(([u, v]) => lift(u, v));
3818
+ if (points3d.length === 2) return this.makeLineEdge(points3d[0], points3d[1]);
3819
+ const degree = Math.min(3, points3d.length - 1);
3820
+ const coords = points3d.flatMap(([px, py, pz]) => [px, py, pz]);
3821
+ const id = this.bk.interpolatePoints(coords, degree);
3822
+ return edgeHandle(id);
3823
+ }
3824
+ const bounds = curveBounds(c);
3825
+ const nSamples = 100;
3826
+ const points = [];
3827
+ for (let i = 0; i <= nSamples; i++) {
3828
+ const t = bounds.first + (bounds.last - bounds.first) * i / nSamples;
3829
+ const [u, v] = evaluateCurve2d(c, t);
3830
+ points.push(lift(u, v));
3831
+ }
3832
+ return this.interpolatePoints(points);
3833
+ }
3834
+ buildEdgeOnSurface(curve, surface) {
3835
+ if (!isBrepkitHandle(surface))
3836
+ throw new Error("brepkit: buildEdgeOnSurface requires a face handle as surface");
3837
+ const fid = unwrap(surface, "face");
3838
+ const c = this.c2d(curve);
3839
+ const bounds = curveBounds(c);
3840
+ const surfType = this.bk.getSurfaceType(fid);
3841
+ const N = surfType === "plane" ? 50 : 100;
3842
+ const points = [];
3843
+ for (let i = 0; i <= N; i++) {
3844
+ const t = bounds.first + (bounds.last - bounds.first) * i / N;
3845
+ const [u, v] = evaluateCurve2d(c, t);
3846
+ const p = this.bk.evaluateSurface(fid, u, v);
3847
+ points.push([p[0], p[1], p[2]]);
3848
+ }
3849
+ return this.interpolatePoints(points);
3850
+ }
3851
+ extractSurfaceFromFace(face) {
3852
+ return face;
3853
+ }
3854
+ extractCurve2dFromEdge(edge, face) {
3855
+ const eid = unwrap(edge, "edge");
3856
+ unwrap(face, "face");
3857
+ const params = this.bk.getEdgeCurveParameters(eid);
3858
+ const tMin = params[0] ?? 0;
3859
+ const tMax = params[1] ?? 1;
3860
+ const N = 40;
3861
+ const uvPoints = [];
3862
+ for (let i = 0; i <= N; i++) {
3863
+ const t = tMin + (tMax - tMin) * i / N;
3864
+ const pt = this.bk.evaluateEdgeCurve(eid, t);
3865
+ uvPoints.push([pt[0], pt[1]]);
3866
+ }
3867
+ if (uvPoints.length >= 2) {
3868
+ return this.makeBSpline2d(uvPoints);
3869
+ }
3870
+ const verts = this.bk.getEdgeVertices(eid);
3871
+ return makeLine2d(verts[0], verts[1], verts[3], verts[4]);
3872
+ }
3873
+ buildCurves3d(_wire) {
3874
+ }
3875
+ fixWireOnFace(wire, _face, _tolerance) {
3876
+ return wire;
3877
+ }
3878
+ fillSurface(wires, _options) {
3879
+ if (wires.length >= 1) {
3880
+ const wireEdges = this.iterShapes(wires[0], "edge");
3881
+ if (wireEdges.length === 4) {
3882
+ const allCoords = [];
3883
+ const curveLengths = [];
3884
+ for (const edge of wireEdges) {
3885
+ const edgeId = unwrap(edge, "edge");
3886
+ const params = this.bk.getEdgeCurveParameters(edgeId);
3887
+ const tMin = params[0], tMax = params[1];
3888
+ const N = 10;
3889
+ const pts = [];
3890
+ for (let i = 0; i <= N; i++) {
3891
+ const t = tMin + (tMax - tMin) * i / N;
3892
+ const p = this.bk.evaluateEdgeCurve(edgeId, t);
3893
+ pts.push(p[0], p[1], p[2]);
3894
+ }
3895
+ allCoords.push(...pts);
3896
+ curveLengths.push(N + 1);
3897
+ }
3898
+ try {
3899
+ const faceId = this.bk.fillCoonsPatch(allCoords, curveLengths);
3900
+ return faceHandle(faceId);
3901
+ } catch (e) {
3902
+ console.warn("brepkit: Coons patch failed, falling back:", e);
3903
+ }
3904
+ }
3905
+ }
3906
+ const outerWire = wires[0];
3907
+ if (!outerWire) throw new Error("fillSurface: no wires provided");
3908
+ return this.makeNonPlanarFace(outerWire);
3909
+ }
3910
+ // ═══════════════════════════════════════════════════════════════════════
3911
+ // Private helpers
3912
+ // ═══════════════════════════════════════════════════════════════════════
3913
+ applyMatrix(shape2, matrix) {
3914
+ const h = shape2;
3915
+ if (!isBrepkitHandle(shape2)) {
3916
+ throw new Error("brepkit: applyMatrix requires a BrepkitHandle");
3917
+ }
3918
+ switch (h.type) {
3919
+ case "solid": {
3920
+ const copy = this.bk.copySolid(h.id);
3921
+ this.bk.transformSolid(copy, matrix);
3922
+ return solidHandle(copy);
3923
+ }
3924
+ case "face": {
3925
+ if (typeof this.bk.copyFace !== "function" || typeof this.bk.transformFace !== "function") {
3926
+ throw new Error(
3927
+ "brepkit: applyMatrix for faces requires copyFace/transformFace WASM exports"
3928
+ );
3929
+ }
3930
+ const copy = this.bk.copyFace(h.id);
3931
+ this.bk.transformFace(copy, matrix);
3932
+ return faceHandle(copy);
3933
+ }
3934
+ case "wire": {
3935
+ if (typeof this.bk.copyWire !== "function" || typeof this.bk.transformWire !== "function") {
3936
+ throw new Error(
3937
+ "brepkit: applyMatrix for wires requires copyWire/transformWire WASM exports"
3938
+ );
3939
+ }
3940
+ const copy = this.bk.copyWire(h.id);
3941
+ this.bk.transformWire(copy, matrix);
3942
+ return wireHandle(copy);
3943
+ }
3944
+ case "edge": {
3945
+ if (typeof this.bk.copyEdge !== "function" || typeof this.bk.transformEdge !== "function") {
3946
+ throw new Error(
3947
+ "brepkit: applyMatrix for edges requires copyEdge/transformEdge WASM exports"
3948
+ );
3949
+ }
3950
+ const copy = this.bk.copyEdge(h.id);
3951
+ this.bk.transformEdge(copy, matrix);
3952
+ return edgeHandle(copy);
3953
+ }
3954
+ default:
3955
+ throw new Error(`brepkit: applyMatrix does not support '${h.type}' shapes`);
3956
+ }
3957
+ }
3958
+ /** Check if we need to transform from default placement (origin, +Z). */
3959
+ needsTransform(center, direction) {
3960
+ if (center && (center[0] !== 0 || center[1] !== 0 || center[2] !== 0)) return true;
3961
+ if (direction && (direction[0] !== 0 || direction[1] !== 0 || direction[2] !== 1)) return true;
3962
+ return false;
3963
+ }
3964
+ /** Transform a shape from default placement (origin, +Z) to the given center and direction. */
3965
+ transformToPlacement(shape2, center, direction) {
3966
+ let result2 = shape2;
3967
+ if (direction && (direction[0] !== 0 || direction[1] !== 0 || direction[2] !== 1)) {
3968
+ const [dx, dy, dz] = direction;
3969
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
3970
+ const nx = dx / len;
3971
+ const ny = dy / len;
3972
+ const nz = dz / len;
3973
+ const dot = nz;
3974
+ if (Math.abs(dot + 1) < 1e-10) {
3975
+ result2 = this.rotate(result2, 180, [1, 0, 0]);
3976
+ } else if (Math.abs(dot - 1) > 1e-10) {
3977
+ const axis = [-ny, nx, 0];
3978
+ const angleDeg = Math.acos(Math.max(-1, Math.min(1, dot))) * (180 / Math.PI);
3979
+ result2 = this.rotate(result2, angleDeg, axis);
3980
+ }
3981
+ }
3982
+ if (center && (center[0] !== 0 || center[1] !== 0 || center[2] !== 0)) {
3983
+ result2 = this.translate(result2, center[0], center[1], center[2]);
3984
+ }
3985
+ return result2;
3986
+ }
3987
+ /** Tessellate a solid with per-face groups for brepjs mesh format. */
3988
+ // TODO: Replace per-face tessellation loop with bk.tessellateSolidGrouped()
3989
+ // for a single WASM call instead of N. Requires aligning the grouped output
3990
+ // format with KernelMeshResult (faceGroups, index offsets). See brepkitWasmTypes.ts.
3991
+ meshSolid(solidId, deflection) {
3992
+ const faceIds = toArray(this.bk.getSolidFaces(solidId));
3993
+ const allVertices = [];
3994
+ const allNormals = [];
3995
+ const allTriangles = [];
3996
+ const allUVs = [];
3997
+ const faceGroups = [];
3998
+ let vertexOffset = 0;
3999
+ for (const faceId of faceIds) {
4000
+ try {
4001
+ const faceMesh = this.bk.tessellateFace(faceId, deflection);
4002
+ const positions = faceMesh.positions;
4003
+ const normals = faceMesh.normals;
4004
+ const indices = faceMesh.indices;
4005
+ const vertCount = positions.length / 3;
4006
+ if (vertCount === 0) continue;
4007
+ const triStart = allTriangles.length;
4008
+ for (const v of positions) allVertices.push(v);
4009
+ for (const n of normals) allNormals.push(n);
4010
+ for (const idx of indices) {
4011
+ allTriangles.push(idx + vertexOffset);
4012
+ }
4013
+ for (let i = 0; i < vertCount; i++) {
4014
+ allUVs.push(0, 0);
4015
+ }
4016
+ faceGroups.push({
4017
+ start: triStart,
4018
+ count: indices.length,
4019
+ faceHash: faceId
4020
+ });
4021
+ vertexOffset += vertCount;
4022
+ } catch (e) {
4023
+ console.warn(`brepkit: face tessellation failed (faceId=${faceId}):`, e);
4024
+ }
4025
+ }
4026
+ return {
4027
+ vertices: new Float32Array(allVertices),
4028
+ normals: new Float32Array(allNormals),
4029
+ triangles: new Uint32Array(allTriangles),
4030
+ uvs: new Float32Array(allUVs),
4031
+ faceGroups
4032
+ };
4033
+ }
4034
+ /** Tessellate a single face and return brepjs mesh format. */
4035
+ meshSingleFace(faceId, deflection, faceHash) {
4036
+ const faceMesh = this.bk.tessellateFace(faceId, deflection);
4037
+ const positions = faceMesh.positions;
4038
+ const normals = faceMesh.normals;
4039
+ const indices = faceMesh.indices;
4040
+ const vertCount = positions.length / 3;
4041
+ const uvs = [];
4042
+ for (let i = 0; i < vertCount; i++) {
4043
+ uvs.push(0, 0);
4044
+ }
4045
+ return {
4046
+ vertices: new Float32Array(positions),
4047
+ normals: new Float32Array(normals),
4048
+ triangles: new Uint32Array(indices),
4049
+ uvs: new Float32Array(uvs),
4050
+ faceGroups: [{ start: 0, count: indices.length, faceHash }]
4051
+ };
4052
+ }
4053
+ /**
4054
+ * Create a NURBS circle/arc edge in 3D.
4055
+ *
4056
+ * Uses the rational quadratic B-spline circle representation:
4057
+ * 9-point circle for full 2π, fewer arcs for partial.
4058
+ */
4059
+ makeCircleNurbs(center, normal, radius, startAngle, endAngle) {
4060
+ const len = Math.sqrt(normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2);
4061
+ const nz = [normal[0] / len, normal[1] / len, normal[2] / len];
4062
+ const ref = Math.abs(nz[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
4063
+ const xAxis = [
4064
+ nz[1] * ref[2] - nz[2] * ref[1],
4065
+ nz[2] * ref[0] - nz[0] * ref[2],
4066
+ nz[0] * ref[1] - nz[1] * ref[0]
4067
+ ];
4068
+ const xLen = Math.sqrt(xAxis[0] ** 2 + xAxis[1] ** 2 + xAxis[2] ** 2);
4069
+ xAxis[0] /= xLen;
4070
+ xAxis[1] /= xLen;
4071
+ xAxis[2] /= xLen;
4072
+ const yAxis = [
4073
+ nz[1] * xAxis[2] - nz[2] * xAxis[1],
4074
+ nz[2] * xAxis[0] - nz[0] * xAxis[2],
4075
+ nz[0] * xAxis[1] - nz[1] * xAxis[0]
4076
+ ];
4077
+ const nSegments = Math.ceil(Math.abs(endAngle - startAngle) / (Math.PI / 2));
4078
+ const dAngle = (endAngle - startAngle) / nSegments;
4079
+ const controlPoints = [];
4080
+ const weights = [];
4081
+ for (let i = 0; i <= nSegments; i++) {
4082
+ const angle = startAngle + i * dAngle;
4083
+ const cos = Math.cos(angle);
4084
+ const sin = Math.sin(angle);
4085
+ const px = center[0] + radius * (cos * xAxis[0] + sin * yAxis[0]);
4086
+ const py = center[1] + radius * (cos * xAxis[1] + sin * yAxis[1]);
4087
+ const pz = center[2] + radius * (cos * xAxis[2] + sin * yAxis[2]);
4088
+ if (i > 0) {
4089
+ const midAngle = startAngle + (i - 0.5) * dAngle;
4090
+ const midCos = Math.cos(midAngle);
4091
+ const midSin = Math.sin(midAngle);
4092
+ const midR = radius / Math.cos(dAngle / 2);
4093
+ const mx = center[0] + midR * (midCos * xAxis[0] + midSin * yAxis[0]);
4094
+ const my = center[1] + midR * (midCos * xAxis[1] + midSin * yAxis[1]);
4095
+ const mz = center[2] + midR * (midCos * xAxis[2] + midSin * yAxis[2]);
4096
+ controlPoints.push(mx, my, mz);
4097
+ weights.push(Math.cos(dAngle / 2));
4098
+ }
4099
+ controlPoints.push(px, py, pz);
4100
+ weights.push(1);
4101
+ }
4102
+ const degree = 2;
4103
+ const knots = Array(degree + 1).fill(0);
4104
+ for (let i = 1; i < nSegments; i++) {
4105
+ knots.push(i, i);
4106
+ }
4107
+ knots.push(...Array(degree + 1).fill(nSegments));
4108
+ const kMax = knots[knots.length - 1];
4109
+ for (let i = 0; i < knots.length; i++) {
4110
+ knots[i] = knots[i] / kMax;
4111
+ }
4112
+ const startPt = controlPoints.slice(0, 3);
4113
+ const endPt = controlPoints.slice(-3);
4114
+ const id = this.bk.makeNurbsEdge(
4115
+ startPt[0],
4116
+ startPt[1],
4117
+ startPt[2],
4118
+ endPt[0],
4119
+ endPt[1],
4120
+ endPt[2],
4121
+ degree,
4122
+ knots,
4123
+ controlPoints,
4124
+ weights
4125
+ );
4126
+ return edgeHandle(id);
4127
+ }
4128
+ /**
4129
+ * Extract NURBS curve data from an edge handle.
4130
+ * Returns null for line edges (caller can build a linear NURBS).
4131
+ * Returns {degree, knots, controlPoints, weights} for NURBS edges.
4132
+ */
4133
+ extractNurbsFromEdge(shape2) {
4134
+ const h = shape2;
4135
+ if (h.type !== "edge") return null;
4136
+ const nurbsJson = this.bk.getEdgeNurbsData(h.id);
4137
+ if (nurbsJson) {
4138
+ const data = JSON.parse(nurbsJson);
4139
+ return {
4140
+ degree: data.degree,
4141
+ knots: data.knots,
4142
+ controlPoints: data.controlPoints,
4143
+ weights: data.weights
4144
+ };
4145
+ }
4146
+ const verts = this.bk.getEdgeVertices(h.id);
4147
+ return {
4148
+ degree: 1,
4149
+ knots: [0, 0, 1, 1],
4150
+ controlPoints: [verts[0], verts[1], verts[2], verts[3], verts[4], verts[5]],
4151
+ weights: [1, 1]
4152
+ };
4153
+ }
4154
+ /**
4155
+ * Create a NURBS ellipse/ellipse-arc edge in 3D.
4156
+ */
4157
+ makeEllipseNurbs(center, normal, majorRadius, minorRadius, startAngle, endAngle, xDir) {
4158
+ const len = Math.sqrt(normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2);
4159
+ const nz = [normal[0] / len, normal[1] / len, normal[2] / len];
4160
+ let xAxis;
4161
+ if (xDir) {
4162
+ const xl = Math.sqrt(xDir[0] ** 2 + xDir[1] ** 2 + xDir[2] ** 2);
4163
+ xAxis = [xDir[0] / xl, xDir[1] / xl, xDir[2] / xl];
4164
+ } else {
4165
+ const ref = Math.abs(nz[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
4166
+ xAxis = [
4167
+ nz[1] * ref[2] - nz[2] * ref[1],
4168
+ nz[2] * ref[0] - nz[0] * ref[2],
4169
+ nz[0] * ref[1] - nz[1] * ref[0]
4170
+ ];
4171
+ const xLen2 = Math.sqrt(xAxis[0] ** 2 + xAxis[1] ** 2 + xAxis[2] ** 2);
4172
+ xAxis[0] /= xLen2;
4173
+ xAxis[1] /= xLen2;
4174
+ xAxis[2] /= xLen2;
4175
+ }
4176
+ const yAxis = [
4177
+ nz[1] * xAxis[2] - nz[2] * xAxis[1],
4178
+ nz[2] * xAxis[0] - nz[0] * xAxis[2],
4179
+ nz[0] * xAxis[1] - nz[1] * xAxis[0]
4180
+ ];
4181
+ const nSegments = Math.ceil(Math.abs(endAngle - startAngle) / (Math.PI / 2));
4182
+ const dAngle = (endAngle - startAngle) / nSegments;
4183
+ const controlPoints = [];
4184
+ const weights = [];
4185
+ for (let i = 0; i <= nSegments; i++) {
4186
+ const angle = startAngle + i * dAngle;
4187
+ const cos = Math.cos(angle);
4188
+ const sin = Math.sin(angle);
4189
+ const px = center[0] + majorRadius * cos * xAxis[0] + minorRadius * sin * yAxis[0];
4190
+ const py = center[1] + majorRadius * cos * xAxis[1] + minorRadius * sin * yAxis[1];
4191
+ const pz = center[2] + majorRadius * cos * xAxis[2] + minorRadius * sin * yAxis[2];
4192
+ if (i > 0) {
4193
+ const midAngle = startAngle + (i - 0.5) * dAngle;
4194
+ const midCos = Math.cos(midAngle);
4195
+ const midSin = Math.sin(midAngle);
4196
+ const scale2 = 1 / Math.cos(dAngle / 2);
4197
+ const mx = center[0] + majorRadius * scale2 * midCos * xAxis[0] + minorRadius * scale2 * midSin * yAxis[0];
4198
+ const my = center[1] + majorRadius * scale2 * midCos * xAxis[1] + minorRadius * scale2 * midSin * yAxis[1];
4199
+ const mz = center[2] + majorRadius * scale2 * midCos * xAxis[2] + minorRadius * scale2 * midSin * yAxis[2];
4200
+ controlPoints.push(mx, my, mz);
4201
+ weights.push(Math.cos(dAngle / 2));
4202
+ }
4203
+ controlPoints.push(px, py, pz);
4204
+ weights.push(1);
4205
+ }
4206
+ const degree = 2;
4207
+ const knots = Array(degree + 1).fill(0);
4208
+ for (let i = 1; i < nSegments; i++) {
4209
+ knots.push(i, i);
4210
+ }
4211
+ knots.push(...Array(degree + 1).fill(nSegments));
4212
+ const kMax = knots[knots.length - 1];
4213
+ for (let i = 0; i < knots.length; i++) {
4214
+ knots[i] = knots[i] / kMax;
4215
+ }
4216
+ const startPt = controlPoints.slice(0, 3);
4217
+ const endPt = controlPoints.slice(-3);
4218
+ const id = this.bk.makeNurbsEdge(
4219
+ startPt[0],
4220
+ startPt[1],
4221
+ startPt[2],
4222
+ endPt[0],
4223
+ endPt[1],
4224
+ endPt[2],
4225
+ degree,
4226
+ knots,
4227
+ controlPoints,
4228
+ weights
4229
+ );
4230
+ return edgeHandle(id);
4231
+ }
4232
+ /**
4233
+ * Extract a plane definition (point + normal) from a face handle.
4234
+ * Uses tessellation to find a concrete point on the face.
4235
+ */
4236
+ extractPlaneFromFace(faceShape) {
4237
+ let faceId;
4238
+ const h = faceShape;
4239
+ if (h.type === "solid" || h.type === "compound") {
4240
+ const faces = this.iterShapes(faceShape, "face");
4241
+ if (faces.length === 0) throw new Error("brepkit: extractPlaneFromFace: no faces found");
4242
+ const firstFace = faces[0];
4243
+ if (!firstFace) throw new Error("brepkit: extractPlaneFromFace: no faces found");
4244
+ let bestId = unwrap(firstFace, "face");
4245
+ let bestArea = 0;
4246
+ for (const f of faces) {
4247
+ const id = unwrap(f, "face");
4248
+ try {
4249
+ const a = this.bk.faceArea(id, DEFAULT_DEFLECTION);
4250
+ if (a > bestArea) {
4251
+ bestArea = a;
4252
+ bestId = id;
4253
+ }
4254
+ } catch {
4255
+ }
4256
+ }
4257
+ faceId = bestId;
4258
+ } else {
4259
+ faceId = unwrap(faceShape, "face");
4260
+ }
4261
+ const n = this.bk.getFaceNormal(faceId);
4262
+ const normal = [n[0], n[1], n[2]];
4263
+ const mesh2 = this.bk.tessellateFace(faceId, 1);
4264
+ const positions = mesh2.positions;
4265
+ if (positions.length >= 3) {
4266
+ return { point: [positions[0], positions[1], positions[2]], normal };
4267
+ }
4268
+ return { point: [0, 0, 0], normal };
4269
+ }
4270
+ }
4271
+ function multiplyMatrices(a, b) {
4272
+ const result2 = new Array(16).fill(0);
4273
+ for (let i = 0; i < 4; i++) {
4274
+ for (let j = 0; j < 4; j++) {
4275
+ for (let k = 0; k < 4; k++) {
4276
+ result2[i * 4 + j] = result2[i * 4 + j] + a[i * 4 + k] * b[k * 4 + j];
4277
+ }
4278
+ }
4279
+ }
4280
+ return result2;
4281
+ }
77
4282
  const errorFactories = {
78
4283
  KERNEL_OPERATION: (code, message, cause) => ({ kind: "KERNEL_OPERATION", code, message, cause }),
79
4284
  VALIDATION: (code, message, cause) => ({ kind: "VALIDATION", code, message, cause }),
@@ -2987,6 +7192,7 @@ exports.pendingCount = worker.pendingCount;
2987
7192
  exports.registerHandler = worker.registerHandler;
2988
7193
  exports.rejectAll = worker.rejectAll;
2989
7194
  exports.BrepWrapperError = BrepWrapperError;
7195
+ exports.BrepkitAdapter = BrepkitAdapter;
2990
7196
  exports.addMate = addMate;
2991
7197
  exports.applyGlue = applyGlue;
2992
7198
  exports.applyMatrix = applyMatrix;