brepjs 20.12.5 → 20.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/2d/blueprints/booleanHelpers.d.ts +0 -5
  2. package/dist/2d/blueprints/intersectionSegments.d.ts +6 -5
  3. package/dist/2d/lib/offset.d.ts +1 -1
  4. package/dist/2d.cjs +6 -6
  5. package/dist/2d.js +6 -6
  6. package/dist/{adjacencyFns-B2Po1gPs.js → adjacencyFns-hMihivxC.js} +3 -3
  7. package/dist/{adjacencyFns-Cho9v3Q3.cjs → adjacencyFns-mMmTBUBY.cjs} +3 -3
  8. package/dist/{blueprint-ERmwk6zG.cjs → blueprint-17RUHsoP.cjs} +7 -8
  9. package/dist/{blueprint-Zmt5-07y.js → blueprint-BS50D3J_.js} +7 -8
  10. package/dist/{blueprintFns-B_sxtIo7.cjs → blueprintFns-Dw5_4_Bk.cjs} +2 -2
  11. package/dist/{blueprintFns-Dtm-XukP.js → blueprintFns-zy5lo2IF.js} +2 -2
  12. package/dist/{blueprintSketcher-CZyokLGJ.cjs → blueprintSketcher-BG--DxWB.cjs} +32 -14
  13. package/dist/{blueprintSketcher-CwTAtrkY.js → blueprintSketcher-C1WCI6E0.js} +32 -14
  14. package/dist/{boolean2D-CRuNKgXs.cjs → boolean2D-7SQ3B0pC.cjs} +42 -50
  15. package/dist/{boolean2D-yUpS8onH.js → boolean2D-CsdyfjFE.js} +42 -50
  16. package/dist/brepjs.cjs +30 -30
  17. package/dist/brepjs.js +30 -30
  18. package/dist/{cameraFns-DB1IyMmS.cjs → cameraFns-BAtzhCYM.cjs} +3 -3
  19. package/dist/{cameraFns-DRok4UxZ.js → cameraFns-BVWyAscu.js} +3 -3
  20. package/dist/core.cjs +2 -2
  21. package/dist/core.js +2 -2
  22. package/dist/{cornerFinder-D5qnDbtZ.js → cornerFinder-Dv_2rmaY.js} +1 -1
  23. package/dist/{cornerFinder-DfYBHHZG.cjs → cornerFinder-e4Wc7_yp.cjs} +1 -1
  24. package/dist/{curveFns-BKT4Sg8q.cjs → curveFns-BAM7QHrP.cjs} +1 -1
  25. package/dist/{curveFns-BGPSFUWB.js → curveFns-CEI-rouh.js} +1 -1
  26. package/dist/{drawFns-aGufr-pr.js → drawFns-Cl1i8B_B.js} +18 -13
  27. package/dist/{drawFns-D8e5RiRe.cjs → drawFns-d-RvC6u6.cjs} +18 -13
  28. package/dist/{faceFns-Cx7cwUMx.cjs → faceFns-C-2ttg8X.cjs} +2 -2
  29. package/dist/{faceFns-lE2OvBfq.js → faceFns-Q87HCJrp.js} +2 -2
  30. package/dist/{healingFns-B94zSk1-.js → healingFns-Cm4NkKD8.js} +5 -5
  31. package/dist/{healingFns-CcgyrL6h.cjs → healingFns-DZkO5okn.cjs} +5 -5
  32. package/dist/{helpers-C4f2MV-r.cjs → helpers-BdGGaLhF.cjs} +6 -6
  33. package/dist/{helpers-CNgipe6l.js → helpers-DpGcM2qZ.js} +6 -6
  34. package/dist/{importFns-gB1X3ti2.js → importFns-Bp-p0jiP.js} +2 -2
  35. package/dist/{importFns-fIdcaMqt.cjs → importFns-DLiP26NW.cjs} +2 -2
  36. package/dist/io.cjs +2 -2
  37. package/dist/io.js +2 -2
  38. package/dist/kernel/bspline2dFit.d.ts +42 -0
  39. package/dist/kernel/geometry2d.d.ts +45 -0
  40. package/dist/kernel/occt/kernel2dOps.d.ts +1 -1
  41. package/dist/kernel/occtWasm/kernel2dOps.d.ts +7 -4
  42. package/dist/kernel/occtWasm/occtWasmAdapter.cjs +1 -1
  43. package/dist/kernel/occtWasm/occtWasmAdapter.d.ts +2 -2
  44. package/dist/kernel/occtWasm/occtWasmAdapter.js +1 -1
  45. package/dist/kernel/occtWasm/occtWasmTypes.d.ts +2 -0
  46. package/dist/{loftFns-AHaYxJyS.cjs → loftFns-DGV1z2NJ.cjs} +1 -1
  47. package/dist/{loftFns-mbyBvJ8Y.js → loftFns-DQ7Z29n3.js} +1 -1
  48. package/dist/{measureFns-CuabRDRL.cjs → measureFns-BdSjw5fr.cjs} +3 -3
  49. package/dist/{measureFns-C94G0fH8.js → measureFns-BrdCH4cH.js} +3 -3
  50. package/dist/measurement.cjs +2 -2
  51. package/dist/measurement.js +2 -2
  52. package/dist/{meshFns-GXF5gPZL.cjs → meshFns-BeubBqEI.cjs} +3 -3
  53. package/dist/{meshFns-jarDZKw1.js → meshFns-UE8tByyw.js} +3 -3
  54. package/dist/{occtWasmAdapter-DeoeK2iJ.cjs → occtWasmAdapter-D3qerQTq.cjs} +1260 -291
  55. package/dist/{occtWasmAdapter-DlNmVNUn.js → occtWasmAdapter-DA7oMgcQ.js} +1249 -292
  56. package/dist/operations.cjs +2 -2
  57. package/dist/operations.js +2 -2
  58. package/dist/{originTrackingFns-BxeUD2NM.js → originTrackingFns-c2qgRZ_9.js} +2 -2
  59. package/dist/{originTrackingFns-C-yuMV3m.cjs → originTrackingFns-rEKOAP65.cjs} +2 -2
  60. package/dist/{primitiveFns-CualoFAk.js → primitiveFns-3-gs-FUX.js} +4 -4
  61. package/dist/{primitiveFns-BVAkYHVf.cjs → primitiveFns-CLGVLwG8.cjs} +4 -4
  62. package/dist/{principalBoxFns-DHh5_HCw.cjs → principalBoxFns-CiXzjOkr.cjs} +3 -3
  63. package/dist/{principalBoxFns-DtI7tVuS.js → principalBoxFns-RMZItjfR.js} +3 -3
  64. package/dist/projection.cjs +1 -1
  65. package/dist/projection.js +1 -1
  66. package/dist/query.cjs +2 -2
  67. package/dist/query.js +2 -2
  68. package/dist/{refResolveFns-CaAw-ya3.cjs → refResolveFns-DNX0bCrP.cjs} +6 -6
  69. package/dist/{refResolveFns--SRyN2qa.js → refResolveFns-kJg2iCad.js} +6 -6
  70. package/dist/{shapeFns-CdrDroOM.js → shapeFns-CZ6VF1yd.js} +2 -2
  71. package/dist/{shapeFns-CwjjUOr0.cjs → shapeFns-SgIrOjN4.cjs} +2 -2
  72. package/dist/shapeRef.cjs +1 -1
  73. package/dist/shapeRef.js +1 -1
  74. package/dist/{shapeTypes-PNVEv6ga.js → shapeTypes-BNGu103j.js} +10 -16
  75. package/dist/{shapeTypes-D938X2mg.cjs → shapeTypes-MUEH4XuH.cjs} +10 -16
  76. package/dist/sketching.cjs +3 -3
  77. package/dist/sketching.js +3 -3
  78. package/dist/{solidBuilders-Hft8Cr6a.js → solidBuilders-Bmr59cGp.js} +5 -5
  79. package/dist/{solidBuilders-CAkBQKWb.cjs → solidBuilders-DdG4NPHM.cjs} +5 -5
  80. package/dist/{surfaceBuilders-CrO_BBKM.js → surfaceBuilders-DI376ai0.js} +3 -3
  81. package/dist/{surfaceBuilders-BKCiVf7T.cjs → surfaceBuilders-DOmDVuMs.cjs} +3 -3
  82. package/dist/text.cjs +2 -2
  83. package/dist/text.js +2 -2
  84. package/dist/{textBlueprints-C9HeYc5a.cjs → textBlueprints-B7Vhr0xu.cjs} +7 -7
  85. package/dist/{textBlueprints-B8q2a-nN.js → textBlueprints-CE0oxB43.js} +7 -7
  86. package/dist/{textMetrics-1fhCSAGF.cjs → textMetrics-C6_kSht6.cjs} +1 -1
  87. package/dist/{textMetrics-DJfTSy6w.js → textMetrics-_VGqdewQ.js} +1 -1
  88. package/dist/{threadFns-CVIEbOW3.cjs → threadFns-1H60LrFX.cjs} +7 -7
  89. package/dist/{threadFns-f2KwibLR.js → threadFns-CHdY-4Cz.js} +7 -7
  90. package/dist/topology.cjs +9 -9
  91. package/dist/topology.js +9 -9
  92. package/dist/{topologyQueryFns-4WYqtSFF.cjs → topologyQueryFns-C_fXO6xL.cjs} +1 -1
  93. package/dist/{topologyQueryFns-BZUY10TD.js → topologyQueryFns-CltEv7Pi.js} +1 -1
  94. package/dist/{validityTypes-tWSx_zAH.cjs → validityTypes-CGX2ES81.cjs} +1 -1
  95. package/dist/{validityTypes-DAXIqwZM.js → validityTypes-GanLrmvY.js} +1 -1
  96. package/package.json +1 -1
@@ -296,9 +296,393 @@ function quickHull(inputPoints, tolerance) {
296
296
  points
297
297
  };
298
298
  }
299
+ // src/kernel/bspline2dFit.ts
300
+ /* v8 ignore file -- pure-math hot path for 2D curve ops; excluded from coverage for the
301
+ same reason as geometry2d.ts (instrumentation overhead times out 2D-heavy tests). */
302
+ function findSpan(U, p, n, u) {
303
+ if (u >= U[n + 1]) return n;
304
+ if (u <= U[p]) return p;
305
+ let low = p;
306
+ let high = n + 1;
307
+ let mid = low + high >> 1;
308
+ while (u < U[mid] || u >= U[mid + 1]) {
309
+ if (u < U[mid]) high = mid;
310
+ else low = mid;
311
+ mid = low + high >> 1;
312
+ }
313
+ return mid;
314
+ }
315
+ function basisFuns(U, span, u, p) {
316
+ const N = new Array(p + 1).fill(0);
317
+ const left = new Array(p + 1).fill(0);
318
+ const right = new Array(p + 1).fill(0);
319
+ N[0] = 1;
320
+ for (let j = 1; j <= p; j++) {
321
+ left[j] = u - U[span + 1 - j];
322
+ right[j] = U[span + j] - u;
323
+ let saved = 0;
324
+ for (let r = 0; r < j; r++) {
325
+ const denom = right[r + 1] + left[j - r];
326
+ const temp = denom === 0 ? 0 : N[r] / denom;
327
+ N[r] = saved + right[r + 1] * temp;
328
+ saved = left[j - r] * temp;
329
+ }
330
+ N[j] = saved;
331
+ }
332
+ return N;
333
+ }
334
+ function evaluateFull(U, poles, p, u) {
335
+ const span = findSpan(U, p, poles.length - 1, u);
336
+ const N = basisFuns(U, span, u, p);
337
+ let x = 0;
338
+ let y = 0;
339
+ for (let j = 0; j <= p; j++) {
340
+ const pole = poles[span - p + j];
341
+ x += N[j] * pole[0];
342
+ y += N[j] * pole[1];
343
+ }
344
+ return [x, y];
345
+ }
346
+ /**
347
+ * Gaussian elimination without pivoting on a banded system. B-spline collocation
348
+ * matrices are totally positive, so elimination in natural order is stable.
349
+ */
350
+ function solveBanded(rows, rhs) {
351
+ const n = rows.length;
352
+ let kl = 0;
353
+ let ku = 0;
354
+ rows.forEach((row, k) => {
355
+ kl = Math.max(kl, k - row.start);
356
+ ku = Math.max(ku, row.start + row.vals.length - 1 - k);
357
+ });
358
+ const width = kl + ku + 1;
359
+ const band = rows.map((row, k) => {
360
+ const r = new Float64Array(width);
361
+ row.vals.forEach((v, j) => {
362
+ r[row.start + j - k + kl] = v;
363
+ });
364
+ return r;
365
+ });
366
+ const bx = rhs.map((q) => q[0]);
367
+ const by = rhs.map((q) => q[1]);
368
+ const at = (r, c) => band[r][c - r + kl];
369
+ for (let i = 0; i < n; i++) {
370
+ const pivot = at(i, i);
371
+ if (Math.abs(pivot) < 1e-300) throw new Error("interpolateBSpline2d: singular system");
372
+ for (let r = i + 1; r <= Math.min(n - 1, i + kl); r++) {
373
+ const f = at(r, i) / pivot;
374
+ if (f === 0) continue;
375
+ const rowR = band[r];
376
+ for (let c = i; c <= Math.min(n - 1, i + ku); c++) rowR[c - r + kl] = rowR[c - r + kl] - f * at(i, c);
377
+ bx[r] = bx[r] - f * bx[i];
378
+ by[r] = by[r] - f * by[i];
379
+ }
380
+ }
381
+ const out = new Array(n);
382
+ for (let i = n - 1; i >= 0; i--) {
383
+ let sx = bx[i];
384
+ let sy = by[i];
385
+ for (let c = i + 1; c <= Math.min(n - 1, i + ku); c++) {
386
+ const a = at(i, c);
387
+ sx -= a * out[c][0];
388
+ sy -= a * out[c][1];
389
+ }
390
+ const d = at(i, i);
391
+ out[i] = [sx / d, sy / d];
392
+ }
393
+ return out;
394
+ }
395
+ function chordLengthParams(points) {
396
+ const n = points.length - 1;
397
+ const d = [0];
398
+ for (let k = 1; k <= n; k++) {
399
+ const a = points[k - 1];
400
+ const b = points[k];
401
+ d.push(d[k - 1] + Math.hypot(b[0] - a[0], b[1] - a[1]));
402
+ }
403
+ const total = d[n];
404
+ if (total < 1e-300) return d.map((_, k) => k / n);
405
+ return d.map((v) => v / total);
406
+ }
407
+ function compressKnots(U) {
408
+ const knots = [];
409
+ const multiplicities = [];
410
+ for (const u of U) {
411
+ const last = knots.length - 1;
412
+ if (last >= 0 && u === knots[last]) multiplicities[last] = multiplicities[last] + 1;
413
+ else {
414
+ knots.push(u);
415
+ multiplicities.push(1);
416
+ }
417
+ }
418
+ return {
419
+ knots,
420
+ multiplicities
421
+ };
422
+ }
423
+ /**
424
+ * Interpolate `points` with a clamped B-spline of `degree` (capped at
425
+ * `points.length - 1`). `params` are the increasing curve parameters assigned to
426
+ * the points, normalised to [0, 1]; chord length when omitted.
427
+ */
428
+ function interpolateBSpline2d(points, degree = 3, params) {
429
+ const n = points.length - 1;
430
+ if (n < 1) throw new Error("interpolateBSpline2d: need at least 2 points");
431
+ const p = Math.max(1, Math.min(degree, n));
432
+ const ub = params ? [...params] : chordLengthParams(points);
433
+ const U = [];
434
+ for (let i = 0; i <= p; i++) U.push(0);
435
+ for (let j = 1; j <= n - p; j++) {
436
+ let sum = 0;
437
+ for (let i = j; i < j + p; i++) sum += ub[i];
438
+ U.push(sum / p);
439
+ }
440
+ for (let i = 0; i <= p; i++) U.push(1);
441
+ const poles = solveBanded(ub.map((u) => {
442
+ const span = findSpan(U, p, n, u);
443
+ return {
444
+ start: span - p,
445
+ vals: basisFuns(U, span, u, p)
446
+ };
447
+ }), points.map((q) => [q[0], q[1]]));
448
+ poles[0] = [points[0][0], points[0][1]];
449
+ poles[n] = [points[n][0], points[n][1]];
450
+ const { knots, multiplicities } = compressKnots(U);
451
+ return {
452
+ __bk2d: "bspline",
453
+ poles,
454
+ knots,
455
+ multiplicities,
456
+ degree: p,
457
+ isPeriodic: false
458
+ };
459
+ }
460
+ function fitPiece(evaluate, s0, s1, tolerance, degree, minSegments, maxSegments) {
461
+ let segments = Math.min(maxSegments, Math.max(2, minSegments));
462
+ for (;;) {
463
+ const params = [];
464
+ const points = [];
465
+ for (let i = 0; i <= segments; i++) {
466
+ const u = i / segments;
467
+ params.push(u);
468
+ points.push(evaluate(s0 + (s1 - s0) * u));
469
+ }
470
+ const curve = interpolateBSpline2d(points, degree, params);
471
+ if (segments >= maxSegments) return curve;
472
+ const U = [];
473
+ curve.knots.forEach((k, i) => {
474
+ for (let m = 0; m < (curve.multiplicities[i] ?? 1); m++) U.push(k);
475
+ });
476
+ let maxErr = 0;
477
+ for (let i = 0; i < segments && maxErr <= tolerance; i++) {
478
+ const u = (i + .5) / segments;
479
+ const [fx, fy] = evaluateFull(U, curve.poles, curve.degree, u);
480
+ const [tx, ty] = evaluate(s0 + (s1 - s0) * u);
481
+ maxErr = Math.max(maxErr, Math.hypot(fx - tx, fy - ty));
482
+ }
483
+ if (maxErr <= tolerance) return curve;
484
+ segments = Math.min(maxSegments, segments * 2);
485
+ }
486
+ }
487
+ /**
488
+ * Concatenate clamped B-splines of one degree, each running over [0, 1], onto
489
+ * the consecutive intervals of `cuts`. Joints are C0 (knot multiplicity =
490
+ * degree); each piece starts on the pole the previous one ended on.
491
+ */
492
+ function joinPieces(pieces, cuts) {
493
+ const p = pieces[0].degree;
494
+ const knots = [];
495
+ const multiplicities = [];
496
+ const poles = [];
497
+ pieces.forEach((piece, i) => {
498
+ const a = cuts[i];
499
+ const b = cuts[i + 1];
500
+ if (i === 0) {
501
+ knots.push(a);
502
+ multiplicities.push(p + 1);
503
+ poles.push(...piece.poles);
504
+ } else poles.push(...piece.poles.slice(1));
505
+ for (let j = 1; j < piece.knots.length - 1; j++) {
506
+ knots.push(a + piece.knots[j] * (b - a));
507
+ multiplicities.push(piece.multiplicities[j]);
508
+ }
509
+ knots.push(b);
510
+ multiplicities.push(i === pieces.length - 1 ? p + 1 : p);
511
+ });
512
+ return {
513
+ __bk2d: "bspline",
514
+ poles,
515
+ knots,
516
+ multiplicities,
517
+ degree: p,
518
+ isPeriodic: false
519
+ };
520
+ }
521
+ /**
522
+ * Approximate the parametric curve `evaluate` over [s0, s1] by interpolating
523
+ * uniformly spaced samples, doubling the sample count until the interpolant is
524
+ * within `tolerance` of the curve at every mid-sample parameter (or the
525
+ * segment cap is reached). With `breaks`, each piece between them is fitted
526
+ * separately and the pieces are joined. The B-spline runs over [0, 1] in step
527
+ * with s.
528
+ */
529
+ function fitBSpline2d(evaluate, s0, s1, tolerance, options = {}) {
530
+ const degree = options.degree ?? 3;
531
+ const maxSegments = Math.max(2, options.maxSegments ?? 1024);
532
+ const lo = Math.min(s0, s1);
533
+ const hi = Math.max(s0, s1);
534
+ const margin = 1e-12 * (hi - lo);
535
+ const inner = [...new Set(options.breaks ?? [])].filter((b) => b > lo + margin && b < hi - margin).sort((a, b) => s0 <= s1 ? a - b : b - a);
536
+ if (!inner.length) return fitPiece(evaluate, s0, s1, tolerance, degree, options.minSegments ?? 8, maxSegments);
537
+ const cuts = [
538
+ s0,
539
+ ...inner,
540
+ s1
541
+ ];
542
+ return joinPieces(cuts.slice(1).map((end, i) => fitPiece(evaluate, cuts[i], end, tolerance, degree, Math.max(degree, 2), maxSegments)), cuts.map((c) => (c - s0) / (s1 - s0)));
543
+ }
544
+ function fullKnotVector(c) {
545
+ const U = [];
546
+ c.knots.forEach((k, i) => {
547
+ for (let m = 0; m < (c.multiplicities[i] ?? 1); m++) U.push(k);
548
+ });
549
+ return U;
550
+ }
551
+ /** The B-spline form of a Bezier; a B-spline is returned as is. */
552
+ function asBSpline2d(c) {
553
+ if (c.__bk2d === "bspline") return c;
554
+ const degree = c.poles.length - 1;
555
+ return {
556
+ __bk2d: "bspline",
557
+ poles: c.poles.map(([x, y]) => [x, y]),
558
+ knots: [0, 1],
559
+ multiplicities: [degree + 1, degree + 1],
560
+ degree,
561
+ isPeriodic: false
562
+ };
563
+ }
564
+ /** Insert `u` once (Boehm, The NURBS Book A5.1 with r = 1). */
565
+ function insertKnot(U, P, p, u) {
566
+ const n = P.length - 1;
567
+ const k = findSpan(U, p, n, u);
568
+ const Q = [];
569
+ for (let i = 0; i <= n + 1; i++) if (i <= k - p) Q.push(P[i]);
570
+ else if (i > k) Q.push(P[i - 1]);
571
+ else {
572
+ const alpha = (u - U[i]) / (U[i + p] - U[i]);
573
+ const a = P[i];
574
+ const b = P[i - 1];
575
+ Q.push([alpha * a[0] + (1 - alpha) * b[0], alpha * a[1] + (1 - alpha) * b[1]]);
576
+ }
577
+ return {
578
+ U: [
579
+ ...U.slice(0, k + 1),
580
+ u,
581
+ ...U.slice(k + 1)
582
+ ],
583
+ P: Q
584
+ };
585
+ }
586
+ /**
587
+ * The part of a clamped B-spline over [lo, hi] (inside its knot range, lo < hi)
588
+ * as a clamped B-spline of its own, exact by knot insertion.
589
+ */
590
+ function subBSpline2d(c, lo, hi) {
591
+ const p = c.degree;
592
+ let U = fullKnotVector(c);
593
+ let P = c.poles.map(([x, y]) => [x, y]);
594
+ const first = U[p];
595
+ const last = U[U.length - 1 - p];
596
+ const a = Math.min(last, Math.max(first, lo));
597
+ const b = Math.min(last, Math.max(first, hi));
598
+ for (const u of [a, b]) {
599
+ if (u <= first || u >= last) continue;
600
+ while (U.filter((k) => k === u).length < p) ({U, P} = insertKnot(U, P, p, u));
601
+ }
602
+ const kLo = a <= first ? p : U.lastIndexOf(a);
603
+ const kHi = b >= last ? U.length - 1 - p : U.indexOf(b);
604
+ const sub = [];
605
+ for (let i = 0; i <= p; i++) sub.push(a);
606
+ sub.push(...U.slice(kLo + 1, kHi));
607
+ for (let i = 0; i <= p; i++) sub.push(b);
608
+ const { knots, multiplicities } = compressKnots(sub);
609
+ return {
610
+ __bk2d: "bspline",
611
+ poles: P.slice(kLo - p, kHi),
612
+ knots,
613
+ multiplicities,
614
+ degree: p,
615
+ isPeriodic: false
616
+ };
617
+ }
618
+ /**
619
+ * Bezier poles of every non-empty knot span (The NURBS Book A5.6). A malformed
620
+ * knot vector yields the whole control polygon as one segment, which still
621
+ * bounds the curve.
622
+ */
623
+ function bezierSegments2d(c) {
624
+ const poles = c.poles;
625
+ const p = c.degree;
626
+ const U = fullKnotVector(c);
627
+ const m = poles.length - 1 + p + 1;
628
+ const clone = (pt) => [pt[0], pt[1]];
629
+ if (p < 1 || U.length !== m + 1) return [poles.map(clone)];
630
+ const segments = [];
631
+ let a = p;
632
+ let b = p + 1;
633
+ let segPoles = [];
634
+ for (let i = 0; i <= p; i++) segPoles[i] = clone(poles[i]);
635
+ let nextPoles = [];
636
+ while (b < m) {
637
+ const i0 = b;
638
+ while (b < m && U[b + 1] === U[b]) b++;
639
+ const mult = b - i0 + 1;
640
+ if (mult > p && b < m) return [poles.map(clone)];
641
+ if (mult < p) {
642
+ const numer = U[b] - U[a];
643
+ const alphas = [];
644
+ for (let j = p; j > mult; j--) alphas[j - mult - 1] = numer / (U[a + j] - U[a]);
645
+ const r = p - mult;
646
+ for (let j = 1; j <= r; j++) {
647
+ const save = r - j;
648
+ const s = mult + j;
649
+ for (let k = p; k >= s; k--) {
650
+ const alpha = alphas[k - s];
651
+ const cur = segPoles[k];
652
+ const prev = segPoles[k - 1];
653
+ segPoles[k] = [alpha * cur[0] + (1 - alpha) * prev[0], alpha * cur[1] + (1 - alpha) * prev[1]];
654
+ }
655
+ if (b < m) nextPoles[save] = clone(segPoles[p]);
656
+ }
657
+ }
658
+ segments.push(segPoles);
659
+ if (b < m) {
660
+ for (let i = p - mult; i <= p; i++) nextPoles[i] = clone(poles[b - p + i]);
661
+ segPoles = nextPoles;
662
+ nextPoles = [];
663
+ a = b;
664
+ b++;
665
+ }
666
+ }
667
+ return segments;
668
+ }
299
669
  // src/kernel/geometry2d.ts
300
670
  /* v8 ignore file -- excluded from coverage reports; prevents V8 instrumentation
301
671
  overhead that causes 2D-heavy tests to timeout under coverage mode. */
672
+ /**
673
+ * Pure-TypeScript 2D geometry implementation for Kernel2DCapability.
674
+ *
675
+ * All 2D curves are represented as plain objects with a `type` discriminant
676
+ * and `evaluate(t)` method. No WASM boundary crossing required.
677
+ *
678
+ * ADR-0006: Inline math here (distance, rotation, normalization, dot/cross
679
+ * products) operates on curve struct fields, not Point2D tuples. The canonical
680
+ * tuple-based functions live in src/utils/vec2d.ts. The struct-field patterns
681
+ * are not worth converting to tuple form — the temporary array allocations
682
+ * would degrade both readability and performance.
683
+ *
684
+ * @module
685
+ */
302
686
  function evaluateCurve2d$1(c, t) {
303
687
  switch (c.__bk2d) {
304
688
  case "line": return [c.ox + c.dx * t, c.oy + c.dy * t];
@@ -398,6 +782,79 @@ function curveTypeName(c) {
398
782
  case "trimmed": return "TRIMMED_" + curveTypeName(c.basis);
399
783
  }
400
784
  }
785
+ /**
786
+ * Collapse nested trimmed wrappers into a single trimmed curve over a
787
+ * non-trimmed basis (or return the curve itself when it is not trimmed).
788
+ * Evaluation is unchanged: both forms map t in [0, 1] to the same basis point.
789
+ */
790
+ function flattenTrimmed(c) {
791
+ if (c.__bk2d !== "trimmed") return c;
792
+ const inner = flattenTrimmed(c.basis);
793
+ if (inner.__bk2d !== "trimmed") return c;
794
+ const span = inner.tEnd - inner.tStart;
795
+ return {
796
+ __bk2d: "trimmed",
797
+ basis: inner.basis,
798
+ tStart: inner.tStart + c.tStart * span,
799
+ tEnd: inner.tStart + c.tEnd * span
800
+ };
801
+ }
802
+ /** Basis parameter of the reversed basis that lands on the point at `u` of the original. */
803
+ function reversedBasisParam(c, u) {
804
+ switch (c.__bk2d) {
805
+ case "circle":
806
+ case "ellipse": return -u;
807
+ default: {
808
+ const { first, last } = curveBounds(c);
809
+ return first + last - u;
810
+ }
811
+ }
812
+ }
813
+ /**
814
+ * The same geometry traversed in the opposite direction (OCCT `Geom2d_Curve::Reversed`).
815
+ * Trimmed curves keep an increasing trim range: the basis is reversed and the
816
+ * range is remapped onto it.
817
+ */
818
+ function reverseCurve2d$1(c) {
819
+ switch (c.__bk2d) {
820
+ case "line": return {
821
+ ...c,
822
+ ox: c.ox + c.dx * c.len,
823
+ oy: c.oy + c.dy * c.len,
824
+ dx: -c.dx,
825
+ dy: -c.dy
826
+ };
827
+ case "circle":
828
+ case "ellipse": return {
829
+ ...c,
830
+ sense: !c.sense
831
+ };
832
+ case "bezier": return {
833
+ ...c,
834
+ poles: [...c.poles].reverse()
835
+ };
836
+ case "bspline": {
837
+ const lo = c.knots[0] ?? 0;
838
+ const hi = c.knots[c.knots.length - 1] ?? 1;
839
+ return {
840
+ ...c,
841
+ poles: [...c.poles].reverse(),
842
+ knots: c.knots.map((k) => lo + hi - k).reverse(),
843
+ multiplicities: [...c.multiplicities].reverse()
844
+ };
845
+ }
846
+ case "trimmed": {
847
+ const flat = flattenTrimmed(c);
848
+ if (flat.__bk2d !== "trimmed") return reverseCurve2d$1(flat);
849
+ return {
850
+ __bk2d: "trimmed",
851
+ basis: reverseCurve2d$1(flat.basis),
852
+ tStart: reversedBasisParam(flat.basis, flat.tEnd),
853
+ tEnd: reversedBasisParam(flat.basis, flat.tStart)
854
+ };
855
+ }
856
+ }
857
+ }
401
858
  function makeLine2d$1(x1, y1, x2, y2) {
402
859
  const dx = x2 - x1;
403
860
  const dy = y2 - y1;
@@ -630,7 +1087,8 @@ function scaleCurve2d$1(c, factor, cx, cy) {
630
1087
  cx: ncx,
631
1088
  cy: ncy,
632
1089
  majorRadius: c.majorRadius * Math.abs(factor),
633
- minorRadius: c.minorRadius * Math.abs(factor)
1090
+ minorRadius: c.minorRadius * Math.abs(factor),
1091
+ xDirAngle: factor < 0 ? c.xDirAngle + Math.PI : c.xDirAngle
634
1092
  };
635
1093
  }
636
1094
  case "bezier": return {
@@ -641,10 +1099,31 @@ function scaleCurve2d$1(c, factor, cx, cy) {
641
1099
  ...c,
642
1100
  poles: c.poles.map(([x, y]) => scalePoint(x, y))
643
1101
  };
644
- case "trimmed": return {
645
- ...c,
646
- basis: scaleCurve2d$1(c.basis, factor, cx, cy)
647
- };
1102
+ case "trimmed": {
1103
+ const basis = scaleCurve2d$1(c.basis, factor, cx, cy);
1104
+ if (basis.__bk2d === "line") {
1105
+ const s = Math.abs(factor);
1106
+ return {
1107
+ ...c,
1108
+ basis,
1109
+ tStart: c.tStart * s,
1110
+ tEnd: c.tEnd * s
1111
+ };
1112
+ }
1113
+ if (basis.__bk2d === "circle" && factor < 0) {
1114
+ const shift = basis.sense ? Math.PI : -Math.PI;
1115
+ return {
1116
+ ...c,
1117
+ basis,
1118
+ tStart: c.tStart + shift,
1119
+ tEnd: c.tEnd + shift
1120
+ };
1121
+ }
1122
+ return {
1123
+ ...c,
1124
+ basis
1125
+ };
1126
+ }
648
1127
  }
649
1128
  }
650
1129
  function mirrorAtPoint(c, cx, cy) {
@@ -723,6 +1202,328 @@ function mirrorAcrossAxis(c, ox, oy, dx, dy) {
723
1202
  }
724
1203
  }
725
1204
  }
1205
+ function applyAffine(m, x, y) {
1206
+ return [m.a * x + m.b * y + m.tx, m.c * x + m.d * y + m.ty];
1207
+ }
1208
+ /**
1209
+ * Image of the conic C + M0 * (cos psi, sin psi), psi = sense ? t : -t.
1210
+ * The closed-form 2x2 SVD M = R(phi) * diag(sx, sy) * R(theta) gives the new
1211
+ * frame (phi), radii (sx, |sy|) and the angular shift (theta); sy < 0 means the
1212
+ * map reflects, which flips the sense.
1213
+ */
1214
+ function affineConic(cx, cy, m0, sense, m) {
1215
+ const [ncx, ncy] = applyAffine(m, cx, cy);
1216
+ const m00 = m.a * m0[0] + m.b * m0[2];
1217
+ const m01 = m.a * m0[1] + m.b * m0[3];
1218
+ const m10 = m.c * m0[0] + m.d * m0[2];
1219
+ const m11 = m.c * m0[1] + m.d * m0[3];
1220
+ const e = (m00 + m11) / 2;
1221
+ const f = (m00 - m11) / 2;
1222
+ const g = (m10 + m01) / 2;
1223
+ const h = (m10 - m01) / 2;
1224
+ const q = Math.hypot(e, h);
1225
+ const r = Math.hypot(f, g);
1226
+ const sx = q + r;
1227
+ const sy = q - r;
1228
+ const a1 = Math.atan2(g, f);
1229
+ const a2 = Math.atan2(h, e);
1230
+ const theta = (a2 - a1) / 2;
1231
+ const phi = (a2 + a1) / 2;
1232
+ const newSense = sy < 0 ? !sense : sense;
1233
+ const shift = sense ? theta : -theta;
1234
+ if (Math.min(q, r) <= 1e-12 * Math.max(q, r)) {
1235
+ const circleShift = shift + (newSense ? phi : -phi);
1236
+ return {
1237
+ curve: {
1238
+ __bk2d: "circle",
1239
+ cx: ncx,
1240
+ cy: ncy,
1241
+ radius: sx,
1242
+ sense: newSense
1243
+ },
1244
+ mapParam: (u) => u + circleShift
1245
+ };
1246
+ }
1247
+ return {
1248
+ curve: {
1249
+ __bk2d: "ellipse",
1250
+ cx: ncx,
1251
+ cy: ncy,
1252
+ majorRadius: sx,
1253
+ minorRadius: Math.abs(sy),
1254
+ xDirAngle: phi,
1255
+ sense: newSense
1256
+ },
1257
+ mapParam: (u) => u + shift
1258
+ };
1259
+ }
1260
+ function affineBasis(c, m) {
1261
+ switch (c.__bk2d) {
1262
+ case "line": {
1263
+ const ldx = m.a * c.dx + m.b * c.dy;
1264
+ const ldy = m.c * c.dx + m.d * c.dy;
1265
+ const speed = Math.hypot(ldx, ldy);
1266
+ const [ox, oy] = applyAffine(m, c.ox, c.oy);
1267
+ if (speed < 1e-300) return {
1268
+ curve: {
1269
+ ...c,
1270
+ ox,
1271
+ oy,
1272
+ len: 0
1273
+ },
1274
+ mapParam: () => 0
1275
+ };
1276
+ return {
1277
+ curve: {
1278
+ __bk2d: "line",
1279
+ ox,
1280
+ oy,
1281
+ dx: ldx / speed,
1282
+ dy: ldy / speed,
1283
+ len: c.len * speed
1284
+ },
1285
+ mapParam: (u) => u * speed
1286
+ };
1287
+ }
1288
+ case "circle": return affineConic(c.cx, c.cy, [
1289
+ c.radius,
1290
+ 0,
1291
+ 0,
1292
+ c.radius
1293
+ ], c.sense, m);
1294
+ case "ellipse": {
1295
+ const cos = Math.cos(c.xDirAngle);
1296
+ const sin = Math.sin(c.xDirAngle);
1297
+ const m0 = [
1298
+ c.majorRadius * cos,
1299
+ -c.minorRadius * sin,
1300
+ c.majorRadius * sin,
1301
+ c.minorRadius * cos
1302
+ ];
1303
+ return affineConic(c.cx, c.cy, m0, c.sense, m);
1304
+ }
1305
+ case "bezier":
1306
+ case "bspline": return {
1307
+ curve: {
1308
+ ...c,
1309
+ poles: c.poles.map(([x, y]) => applyAffine(m, x, y))
1310
+ },
1311
+ mapParam: (u) => u
1312
+ };
1313
+ case "trimmed": return {
1314
+ curve: affineCurve2d(c, m),
1315
+ mapParam: (u) => u
1316
+ };
1317
+ }
1318
+ }
1319
+ /**
1320
+ * Exact image of a curve under an affine map (OCCT `Geom2d_Curve::Transformed`
1321
+ * with a `gp_GTrsf2d`): lines stay lines, circles and ellipses become ellipses
1322
+ * (or circles), and pole-based curves map their poles.
1323
+ */
1324
+ function affineCurve2d(c, m) {
1325
+ const flat = flattenTrimmed(c);
1326
+ if (flat.__bk2d !== "trimmed") return affineBasis(flat, m).curve;
1327
+ const { curve, mapParam } = affineBasis(flat.basis, m);
1328
+ return {
1329
+ __bk2d: "trimmed",
1330
+ basis: curve,
1331
+ tStart: mapParam(flat.tStart),
1332
+ tEnd: mapParam(flat.tEnd)
1333
+ };
1334
+ }
1335
+ function unitTangent(c, t, first, last) {
1336
+ let [tx, ty] = tangentCurve2d(c, t);
1337
+ let len = Math.hypot(tx, ty);
1338
+ for (let k = 1; len < 1e-12 && k <= 6; k++) {
1339
+ const h = (last - first) * 10 ** (-9 + k);
1340
+ [tx, ty] = tangentCurve2d(c, t + h <= Math.max(first, last) ? t + h : t - h);
1341
+ len = Math.hypot(tx, ty);
1342
+ }
1343
+ return len < 1e-300 ? [1, 0] : [tx / len, ty / len];
1344
+ }
1345
+ function curveExtent(c) {
1346
+ const box = createBBox2d();
1347
+ addCurveToBBox(box, c, 0);
1348
+ return Math.hypot(box.xMax - box.xMin, box.yMax - box.yMin);
1349
+ }
1350
+ /**
1351
+ * Parameters of `c`, in its own parameter range, at the interior knots of its
1352
+ * B-spline basis: where a fit of the curve (or of its offset) must sample.
1353
+ */
1354
+ function curveKnotParams(c) {
1355
+ const flat = flattenTrimmed(c);
1356
+ const basis = flat.__bk2d === "trimmed" ? flat.basis : flat;
1357
+ if (basis.__bk2d !== "bspline") return [];
1358
+ const inner = basis.knots.slice(1, -1);
1359
+ if (flat.__bk2d !== "trimmed") return inner;
1360
+ const { tStart, tEnd } = flat;
1361
+ return inner.filter((k) => k > Math.min(tStart, tEnd) && k < Math.max(tStart, tEnd)).map((k) => (k - tStart) / (tEnd - tStart));
1362
+ }
1363
+ /**
1364
+ * Offset `distance` to the right of the direction of travel (OCCT
1365
+ * `Geom2d_OffsetCurve`: a counter-clockwise circle grows). Lines and circles
1366
+ * are offset exactly; other curves are fitted within `tolerance` (by default
1367
+ * relative to the curve's size).
1368
+ */
1369
+ function offsetCurve2d$1(c, distance, tolerance) {
1370
+ const flat = flattenTrimmed(c);
1371
+ const trimmed = flat.__bk2d === "trimmed" ? flat : null;
1372
+ const basis = trimmed ? trimmed.basis : flat;
1373
+ const d = trimmed && trimmed.tEnd < trimmed.tStart ? -distance : distance;
1374
+ if (basis.__bk2d === "line") {
1375
+ const shifted = {
1376
+ ...basis,
1377
+ ox: basis.ox + basis.dy * d,
1378
+ oy: basis.oy - basis.dx * d
1379
+ };
1380
+ return trimmed ? {
1381
+ ...trimmed,
1382
+ basis: shifted
1383
+ } : shifted;
1384
+ }
1385
+ if (basis.__bk2d === "circle") {
1386
+ const radius = basis.radius + (basis.sense ? d : -d);
1387
+ if (Math.abs(radius) <= 1e-12 * Math.max(1, basis.radius)) throw new Error("offsetCurve2d: the offset collapses the circle to its center");
1388
+ const circle = {
1389
+ ...basis,
1390
+ radius: Math.abs(radius)
1391
+ };
1392
+ if (!trimmed) return circle;
1393
+ if (radius > 0) return {
1394
+ ...trimmed,
1395
+ basis: circle
1396
+ };
1397
+ const shift = basis.sense ? Math.PI : -Math.PI;
1398
+ return {
1399
+ ...trimmed,
1400
+ basis: circle,
1401
+ tStart: trimmed.tStart + shift,
1402
+ tEnd: trimmed.tEnd + shift
1403
+ };
1404
+ }
1405
+ const { first, last } = curveBounds(flat);
1406
+ return fitBSpline2d((t) => {
1407
+ const [px, py] = evaluateCurve2d$1(flat, t);
1408
+ const [ux, uy] = unitTangent(flat, t, first, last);
1409
+ return [px + uy * distance, py - ux * distance];
1410
+ }, first, last, tolerance ?? 1e-9 * Math.max(1, curveExtent(flat) + Math.abs(distance)), { breaks: curveKnotParams(flat) });
1411
+ }
1412
+ function sampleCurve(c, from, to, n) {
1413
+ const pts = [];
1414
+ for (let i = 0; i <= n; i++) pts.push(evaluateCurve2d$1(c, from + (to - from) * i / n));
1415
+ return pts;
1416
+ }
1417
+ /** Second derivative by central differences, kept inside [lo, hi] for clamped curves. */
1418
+ function secondDerivative(c, t, lo, hi) {
1419
+ const h = 1e-4 * Math.max(hi - lo, 1e-9);
1420
+ const mid = Math.min(hi - h, Math.max(lo + h, t));
1421
+ const [ax, ay] = evaluateCurve2d$1(c, mid - h);
1422
+ const [bx, by] = evaluateCurve2d$1(c, mid);
1423
+ const [cx, cy] = evaluateCurve2d$1(c, mid + h);
1424
+ return [(ax - 2 * bx + cx) / (h * h), (ay - 2 * by + cy) / (h * h)];
1425
+ }
1426
+ /**
1427
+ * Newton step for f(s, t) = |c1(s) - c2(t)|^2 / 2. The closest pair of two
1428
+ * disjoint curves has parallel tangents, so the curvature terms of the Hessian
1429
+ * are what make it converge; where the Hessian is not positive definite the
1430
+ * step falls back to Gauss-Newton, then to projecting c1(s) onto c2.
1431
+ */
1432
+ function closestPairStep(c1, c2, s, t, box1, box2) {
1433
+ const [px, py] = evaluateCurve2d$1(c1, s);
1434
+ const [qx, qy] = evaluateCurve2d$1(c2, t);
1435
+ const dx = px - qx;
1436
+ const dy = py - qy;
1437
+ const t1 = tangentCurve2d(c1, s);
1438
+ const t2 = tangentCurve2d(c2, t);
1439
+ const g1 = t1[0] * dx + t1[1] * dy;
1440
+ const g2 = -(t2[0] * dx + t2[1] * dy);
1441
+ const a11 = t1[0] * t1[0] + t1[1] * t1[1];
1442
+ const a22 = t2[0] * t2[0] + t2[1] * t2[1];
1443
+ const a12 = -(t1[0] * t2[0] + t1[1] * t2[1]);
1444
+ const k1 = secondDerivative(c1, s, box1[0], box1[1]);
1445
+ const k2 = secondDerivative(c2, t, box2[0], box2[1]);
1446
+ const h11 = a11 + k1[0] * dx + k1[1] * dy;
1447
+ const h22 = a22 - (k2[0] * dx + k2[1] * dy);
1448
+ const detH = h11 * h22 - a12 * a12;
1449
+ if (h11 > 0 && detH > 1e-12 * Math.abs(h11 * h22)) return [(-g1 * h22 + g2 * a12) / detH, (-g2 * h11 + g1 * a12) / detH];
1450
+ const detGN = a11 * a22 - a12 * a12;
1451
+ if (detGN > 1e-12 * a11 * a22) return [(-g1 * a22 + g2 * a12) / detGN, (-g2 * a11 + g1 * a12) / detGN];
1452
+ if (a22 > 0) return [0, -g2 / a22];
1453
+ return a11 > 0 ? [-g1 / a11, 0] : [0, 0];
1454
+ }
1455
+ /** Refine a seed pair, halving each step until it lowers the distance; returns the squared distance. */
1456
+ function refineClosestPair(c1, c2, seed, box1, box2) {
1457
+ const clamp = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v));
1458
+ const distSq = (a, b) => {
1459
+ const [px, py] = evaluateCurve2d$1(c1, a);
1460
+ const [qx, qy] = evaluateCurve2d$1(c2, b);
1461
+ return (px - qx) ** 2 + (py - qy) ** 2;
1462
+ };
1463
+ let [s, t] = seed;
1464
+ let f = distSq(s, t);
1465
+ for (let iter = 0; iter < 60 && f > 0; iter++) {
1466
+ const [ds, dt] = closestPairStep(c1, c2, s, t, box1, box2);
1467
+ let lambda = 1;
1468
+ let ns = s;
1469
+ let nt = t;
1470
+ let nf = f;
1471
+ for (; lambda > 1e-6 && nf >= f; lambda /= 2) {
1472
+ ns = clamp(s + lambda * ds, box1);
1473
+ nt = clamp(t + lambda * dt, box2);
1474
+ nf = distSq(ns, nt);
1475
+ }
1476
+ if (nf >= f || ns === s && nt === t) break;
1477
+ [s, t, f] = [
1478
+ ns,
1479
+ nt,
1480
+ nf
1481
+ ];
1482
+ }
1483
+ return f;
1484
+ }
1485
+ /** Whether grid cell (i, j) is no larger than any of its eight neighbours. */
1486
+ function isGridMinimum(at, i, j, n) {
1487
+ const d = at(i, j);
1488
+ for (let ni = Math.max(0, i - 1); ni <= Math.min(n, i + 1); ni++) for (let nj = Math.max(0, j - 1); nj <= Math.min(n, j + 1); nj++) if (at(ni, nj) < d) return false;
1489
+ return true;
1490
+ }
1491
+ /**
1492
+ * Minimum distance between c1 over [from1, to1] and c2 over [from2, to2]
1493
+ * (OCCT `Geom2dAPI_ExtremaCurveCurve`): every local minimum of a sampled
1494
+ * distance grid seeds a Gauss-Newton refinement.
1495
+ */
1496
+ function distanceBetweenCurves2d$1(c1, c2, from1, to1, from2, to2) {
1497
+ const n = 32;
1498
+ const p1 = sampleCurve(c1, from1, to1, n);
1499
+ const p2 = sampleCurve(c2, from2, to2, n);
1500
+ const grid = /* @__PURE__ */ new Float64Array(1089);
1501
+ for (let i = 0; i <= n; i++) for (let j = 0; j <= n; j++) {
1502
+ const a = p1[i];
1503
+ const b = p2[j];
1504
+ grid[i * 33 + j] = (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
1505
+ }
1506
+ const at = (i, j) => grid[i * 33 + j] ?? Infinity;
1507
+ const seeds = [];
1508
+ let best = Infinity;
1509
+ for (let i = 0; i <= n; i++) for (let j = 0; j <= n; j++) {
1510
+ const d = at(i, j);
1511
+ best = Math.min(best, d);
1512
+ if (isGridMinimum(at, i, j, n)) seeds.push({
1513
+ i,
1514
+ j,
1515
+ d
1516
+ });
1517
+ }
1518
+ seeds.sort((x, y) => x.d - y.d);
1519
+ const box1 = [Math.min(from1, to1), Math.max(from1, to1)];
1520
+ const box2 = [Math.min(from2, to2), Math.max(from2, to2)];
1521
+ for (const { i, j } of seeds.slice(0, 8)) {
1522
+ const seed = [from1 + (to1 - from1) * i / n, from2 + (to2 - from2) * j / n];
1523
+ best = Math.min(best, refineClosestPair(c1, c2, seed, box1, box2));
1524
+ }
1525
+ return Math.sqrt(best);
1526
+ }
726
1527
  /**
727
1528
  * Compute intersection points (and overlapping segments) between two 2D curves.
728
1529
  * Handles analytic cases (line-line, line-circle, circle-circle) and falls back
@@ -740,10 +1541,7 @@ function intersectCurves2dFn(c1, c2, tolerance) {
740
1541
  points: intersectLineCircle(c2, b2, c1, b1, tolerance),
741
1542
  segments: []
742
1543
  };
743
- if (b1.__bk2d === "circle" && b2.__bk2d === "circle") return {
744
- points: intersectCircleCircle(c1, b1, c2, b2, tolerance),
745
- segments: []
746
- };
1544
+ if (b1.__bk2d === "circle" && b2.__bk2d === "circle") return intersectCircleCircle(c1, b1, c2, b2, tolerance);
747
1545
  return numericalIntersect(c1, c2, tolerance, c1 === c2);
748
1546
  }
749
1547
  /** Unwrap trimmed wrappers to get the basis curve type. */
@@ -752,9 +1550,32 @@ function unwrapCurve(c) {
752
1550
  while (cur.__bk2d === "trimmed") cur = cur.basis;
753
1551
  return cur;
754
1552
  }
755
- /** Check if parameter t is within the curve's domain. */
1553
+ /**
1554
+ * The interval a curve covers on its innermost basis's parameter. A trimmed wrapper's own
1555
+ * parameter runs 0..1, but the analytic solvers work on the unwrapped basis, so their
1556
+ * parameters must be bounded by this interval rather than by `curveBounds(c)`.
1557
+ */
1558
+ function basisInterval(c) {
1559
+ if (c.__bk2d !== "trimmed") return curveBounds(c);
1560
+ const a = basisParam(c.basis, c.tStart);
1561
+ const b = basisParam(c.basis, c.tEnd);
1562
+ return {
1563
+ first: Math.min(a, b),
1564
+ last: Math.max(a, b)
1565
+ };
1566
+ }
1567
+ function basisParam(c, t) {
1568
+ let cur = c;
1569
+ let s = t;
1570
+ while (cur.__bk2d === "trimmed") {
1571
+ s = cur.tStart + s * (cur.tEnd - cur.tStart);
1572
+ cur = cur.basis;
1573
+ }
1574
+ return s;
1575
+ }
1576
+ /** Check if basis parameter t lies within the part of the basis that c covers. */
756
1577
  function inDomain(c, t, tol) {
757
- const b = curveBounds(c);
1578
+ const b = basisInterval(c);
758
1579
  return t >= b.first - tol && t <= b.last + tol;
759
1580
  }
760
1581
  /** Find the parameter on curve c closest to point (px, py), searching near tGuess. */
@@ -830,7 +1651,7 @@ function intersectLineLine(c1, l1, c2, l2, tol) {
830
1651
  points: [],
831
1652
  segments: []
832
1653
  };
833
- const b1 = curveBounds(c1);
1654
+ const b1 = basisInterval(c1);
834
1655
  const b2 = curveBounds(c2);
835
1656
  const p2s = evaluateCurve2d$1(c2, b2.first);
836
1657
  const p2e = evaluateCurve2d$1(c2, b2.last);
@@ -871,42 +1692,83 @@ function intersectLineCircle(cLine, line, cCirc, circ, tol) {
871
1692
  }
872
1693
  return results;
873
1694
  }
874
- function intersectConcentricArcs(c1, c2, tol) {
875
- const b1 = curveBounds(c1);
876
- const b2 = curveBounds(c2);
877
- const isFullCircle1 = Math.abs(b1.last - b1.first - 2 * Math.PI) < 1e-10;
878
- const isFullCircle2 = Math.abs(b2.last - b2.first - 2 * Math.PI) < 1e-10;
879
- if (isFullCircle1 && isFullCircle2) return [];
880
- if (isFullCircle1) return [evaluateCurve2d$1(c2, b2.first), evaluateCurve2d$1(c2, b2.last)];
881
- if (isFullCircle2) return [evaluateCurve2d$1(c1, b1.first), evaluateCurve2d$1(c1, b1.last)];
882
- const pts = [];
883
- const checks = [
884
- [c2, evaluateCurve2d$1(c1, b1.first)],
885
- [c2, evaluateCurve2d$1(c1, b1.last)],
886
- [c1, evaluateCurve2d$1(c2, b2.first)],
887
- [c1, evaluateCurve2d$1(c2, b2.last)]
888
- ];
889
- for (const [target, pt] of checks) {
890
- const t = refineParam(target, pt[0], pt[1]);
891
- if (t !== null) {
892
- const [ex, ey] = evaluateCurve2d$1(target, t);
893
- if ((ex - pt[0]) ** 2 + (ey - pt[1]) ** 2 < tol * tol * 100) pts.push(pt);
894
- }
1695
+ /**
1696
+ * The counter-clockwise angular range [start, start + sweep] a circular curve covers,
1697
+ * and whether the curve itself runs counter-clockwise through it.
1698
+ */
1699
+ function arcSpan(c, circ) {
1700
+ const range = basisInterval(c);
1701
+ const own = curveBounds(c);
1702
+ const forward = basisParam(c, own.last) >= basisParam(c, own.first);
1703
+ return {
1704
+ start: circ.sense ? range.first : -range.last,
1705
+ sweep: Math.min(range.last - range.first, 2 * Math.PI),
1706
+ ccw: circ.sense === forward
1707
+ };
1708
+ }
1709
+ /** The arc of `circ` from angle `from` counter-clockwise to `to`, traversed as `ccw` says. */
1710
+ function arcBetween(circ, from, to, ccw) {
1711
+ const basis = makeCircle2d$1(circ.cx, circ.cy, circ.radius, ccw);
1712
+ return ccw ? {
1713
+ __bk2d: "trimmed",
1714
+ basis,
1715
+ tStart: from,
1716
+ tEnd: to
1717
+ } : {
1718
+ __bk2d: "trimmed",
1719
+ basis,
1720
+ tStart: -to,
1721
+ tEnd: -from
1722
+ };
1723
+ }
1724
+ /**
1725
+ * Overlap of two arcs on the same circle: the shared stretches as segments oriented
1726
+ * along c1, and the places where the arcs only touch end to end as points.
1727
+ */
1728
+ function intersectCoincidentArcs(c1, circ1, c2, circ2, tol) {
1729
+ const turn = 2 * Math.PI;
1730
+ const a = arcSpan(c1, circ1);
1731
+ const b = arcSpan(c2, circ2);
1732
+ const angleTol = tol / circ1.radius;
1733
+ const isFull = (sweep) => turn - sweep <= angleTol;
1734
+ const d = ((b.start - a.start) % turn + turn) % turn;
1735
+ let windows;
1736
+ if (isFull(b.sweep)) windows = [[0, a.sweep]];
1737
+ else if (isFull(a.sweep)) windows = [[d, d + b.sweep]];
1738
+ else windows = [[Math.max(0, d), Math.min(a.sweep, d + b.sweep)], [Math.max(0, d - turn), Math.min(a.sweep, d - turn + b.sweep)]];
1739
+ const points = [];
1740
+ const segments = [];
1741
+ for (const [u, v] of windows) if (v - u > angleTol) segments.push(arcBetween(circ1, a.start + u, a.start + v, a.ccw));
1742
+ else if (v - u > -angleTol) {
1743
+ const angle = a.start + (u + v) / 2;
1744
+ points.push([circ1.cx + circ1.radius * Math.cos(angle), circ1.cy + circ1.radius * Math.sin(angle)]);
895
1745
  }
896
- const deduped = [];
897
- for (const p of pts) if (!deduped.some(([ddx, ddy]) => (ddx - p[0]) ** 2 + (ddy - p[1]) ** 2 < tol * tol * 100)) deduped.push(p);
898
- return deduped;
1746
+ return {
1747
+ points,
1748
+ segments
1749
+ };
899
1750
  }
900
1751
  function intersectCircleCircle(c1, circ1, c2, circ2, tol) {
901
1752
  const dx = circ2.cx - circ1.cx;
902
1753
  const dy = circ2.cy - circ1.cy;
903
1754
  const d = Math.sqrt(dx * dx + dy * dy);
1755
+ if (d <= tol) {
1756
+ if (Math.abs(circ1.radius - circ2.radius) > tol) return {
1757
+ points: [],
1758
+ segments: []
1759
+ };
1760
+ return intersectCoincidentArcs(c1, circ1, c2, circ2, tol);
1761
+ }
1762
+ return {
1763
+ points: intersectCrossingCircles(c1, circ1, c2, circ2, tol, d),
1764
+ segments: []
1765
+ };
1766
+ }
1767
+ function intersectCrossingCircles(c1, circ1, c2, circ2, tol, d) {
1768
+ const dx = circ2.cx - circ1.cx;
1769
+ const dy = circ2.cy - circ1.cy;
904
1770
  if (d > circ1.radius + circ2.radius + tol) return [];
905
1771
  if (d < Math.abs(circ1.radius - circ2.radius) - tol) return [];
906
- if (d < 1e-14) {
907
- if (Math.abs(circ1.radius - circ2.radius) > tol) return [];
908
- return intersectConcentricArcs(c1, c2, tol);
909
- }
910
1772
  const a = (circ1.radius * circ1.radius - circ2.radius * circ2.radius + d * d) / (2 * d);
911
1773
  const h2 = circ1.radius * circ1.radius - a * a;
912
1774
  const h = Math.sqrt(Math.max(0, h2));
@@ -1046,61 +1908,136 @@ function createBBox2d() {
1046
1908
  yMax: -Infinity
1047
1909
  };
1048
1910
  }
1049
- function addCurveToBBox(bbox, c, _tol) {
1050
- const basis = c.__bk2d === "trimmed" ? c.basis : c;
1051
- if (basis.__bk2d === "line") {
1052
- const bounds = curveBounds(c);
1053
- if (!isFinite(bounds.first) || !isFinite(bounds.last)) return;
1054
- const [x0, y0] = evaluateCurve2d$1(c, bounds.first);
1055
- const [x1, y1] = evaluateCurve2d$1(c, bounds.last);
1056
- bbox.xMin = Math.min(bbox.xMin, x0, x1);
1057
- bbox.yMin = Math.min(bbox.yMin, y0, y1);
1058
- bbox.xMax = Math.max(bbox.xMax, x0, x1);
1059
- bbox.yMax = Math.max(bbox.yMax, y0, y1);
1060
- return;
1061
- }
1062
- if (basis.__bk2d === "circle") {
1063
- const bounds = curveBounds(c);
1064
- if (!isFinite(bounds.first) || !isFinite(bounds.last)) return;
1065
- const { cx, cy, radius, sense } = basis;
1066
- const [x0, y0] = evaluateCurve2d$1(c, bounds.first);
1067
- const [x1, y1] = evaluateCurve2d$1(c, bounds.last);
1068
- let xMin = Math.min(x0, x1), xMax = Math.max(x0, x1);
1069
- let yMin = Math.min(y0, y1), yMax = Math.max(y0, y1);
1070
- const tStart = c.__bk2d === "trimmed" ? c.tStart : bounds.first;
1071
- const tEnd = c.__bk2d === "trimmed" ? c.tEnd : bounds.last;
1072
- for (let k = 0; k < 4; k++) {
1073
- const angle = k * Math.PI / 2;
1074
- const t = sense ? angle : -angle;
1075
- for (let wrap = 0; wrap <= 1; wrap++) {
1076
- const tt = t + wrap * 2 * Math.PI;
1077
- if (tt >= tStart - 1e-10 && tt <= tEnd + 1e-10) {
1078
- const ex = cx + radius * Math.cos(angle);
1079
- const ey = cy + radius * Math.sin(angle);
1080
- xMin = Math.min(xMin, ex);
1081
- xMax = Math.max(xMax, ex);
1082
- yMin = Math.min(yMin, ey);
1083
- yMax = Math.max(yMax, ey);
1084
- break;
1085
- }
1086
- }
1087
- }
1088
- bbox.xMin = Math.min(bbox.xMin, xMin);
1089
- bbox.yMin = Math.min(bbox.yMin, yMin);
1090
- bbox.xMax = Math.max(bbox.xMax, xMax);
1091
- bbox.yMax = Math.max(bbox.yMax, yMax);
1911
+ /** The first parameter congruent to `t` (mod 2*pi) inside [lo, hi], if any. */
1912
+ function periodicParamInRange(t, lo, hi) {
1913
+ const period = 2 * Math.PI;
1914
+ const tt = t + period * Math.ceil((lo - t) / period - 1e-12);
1915
+ return tt <= hi + 1e-12 ? tt : null;
1916
+ }
1917
+ /**
1918
+ * Basis parameters where a conic reaches its axis-aligned extremes. For
1919
+ * C + a*cos(psi)*U + b*sin(psi)*V (U at angle phi), dx/dpsi = 0 at
1920
+ * tan(psi) = -b*sin(phi) / (a*cos(phi)) and dy/dpsi = 0 at
1921
+ * tan(psi) = b*cos(phi) / (a*sin(phi)), each plus pi.
1922
+ */
1923
+ function conicExtremeParams(c) {
1924
+ const [a, b, phi] = c.__bk2d === "circle" ? [
1925
+ c.radius,
1926
+ c.radius,
1927
+ 0
1928
+ ] : [
1929
+ c.majorRadius,
1930
+ c.minorRadius,
1931
+ c.xDirAngle
1932
+ ];
1933
+ const psiX = Math.atan2(-b * Math.sin(phi), a * Math.cos(phi));
1934
+ const psiY = Math.atan2(b * Math.cos(phi), a * Math.sin(phi));
1935
+ return [
1936
+ psiX,
1937
+ psiX + Math.PI,
1938
+ psiY,
1939
+ psiY + Math.PI
1940
+ ].map((psi) => c.sense ? psi : -psi);
1941
+ }
1942
+ function splitBezier1d(vals) {
1943
+ const work = [...vals];
1944
+ const n = work.length;
1945
+ const left = [];
1946
+ const right = [];
1947
+ left.push(work[0]);
1948
+ right.push(work[n - 1]);
1949
+ for (let r = 1; r < n; r++) {
1950
+ for (let i = 0; i < n - r; i++) work[i] = (work[i] + work[i + 1]) / 2;
1951
+ left.push(work[0]);
1952
+ right.push(work[n - 1 - r]);
1953
+ }
1954
+ return [left, right.reverse()];
1955
+ }
1956
+ /**
1957
+ * Widen `range` to the values a 1D Bezier takes. Its coefficients bound it
1958
+ * (convex hull), so a piece whose coefficients stay inside the range is done;
1959
+ * otherwise the piece is halved, adding the curve value at the split.
1960
+ */
1961
+ function widenToBezier(vals, range, depth = 0) {
1962
+ const lo = Math.min(...vals);
1963
+ const hi = Math.max(...vals);
1964
+ const tol = 1e-12 * (1 + Math.abs(range.min) + Math.abs(range.max));
1965
+ if (lo >= range.min - tol && hi <= range.max + tol) return;
1966
+ if (depth >= 60) {
1967
+ range.min = Math.min(range.min, lo);
1968
+ range.max = Math.max(range.max, hi);
1092
1969
  return;
1093
1970
  }
1971
+ const [left, right] = splitBezier1d(vals);
1972
+ const mid = right[0] ?? lo;
1973
+ range.min = Math.min(range.min, mid);
1974
+ range.max = Math.max(range.max, mid);
1975
+ widenToBezier(left, range, depth + 1);
1976
+ widenToBezier(right, range, depth + 1);
1977
+ }
1978
+ /** Add the exact extent of a Bezier or B-spline over [lo, hi], span by span. */
1979
+ function addPoleCurveExtent(bbox, c, lo, hi) {
1980
+ const bs = asBSpline2d(c);
1981
+ const first = bs.knots[0] ?? 0;
1982
+ const last = bs.knots[bs.knots.length - 1] ?? 1;
1983
+ const a = Math.max(first, lo);
1984
+ const b = Math.min(last, hi);
1985
+ if (b <= a) return;
1986
+ const piece = a > first || b < last ? subBSpline2d(bs, a, b) : bs;
1987
+ const x = {
1988
+ min: bbox.xMin,
1989
+ max: bbox.xMax
1990
+ };
1991
+ const y = {
1992
+ min: bbox.yMin,
1993
+ max: bbox.yMax
1994
+ };
1995
+ for (const seg of bezierSegments2d(piece)) for (const [range, axis] of [[x, 0], [y, 1]]) {
1996
+ const vals = seg.map((pt) => pt[axis]);
1997
+ const ends = [vals[0] ?? range.min, vals[vals.length - 1] ?? range.min];
1998
+ range.min = Math.min(range.min, ...ends);
1999
+ range.max = Math.max(range.max, ...ends);
2000
+ widenToBezier(vals, range);
2001
+ }
2002
+ bbox.xMin = x.min;
2003
+ bbox.xMax = x.max;
2004
+ bbox.yMin = y.min;
2005
+ bbox.yMax = y.max;
2006
+ }
2007
+ function addCurveToBBox(bbox, c, _tol) {
1094
2008
  const bounds = curveBounds(c);
1095
2009
  if (!isFinite(bounds.first) || !isFinite(bounds.last)) return;
1096
- const nSamples = 20;
1097
- const dt = (bounds.last - bounds.first) / nSamples;
1098
- for (let i = 0; i <= nSamples; i++) {
1099
- const [x, y] = evaluateCurve2d$1(c, bounds.first + i * dt);
1100
- if (x < bbox.xMin) bbox.xMin = x;
1101
- if (y < bbox.yMin) bbox.yMin = y;
1102
- if (x > bbox.xMax) bbox.xMax = x;
1103
- if (y > bbox.yMax) bbox.yMax = y;
2010
+ const addPoint = ([x, y]) => {
2011
+ bbox.xMin = Math.min(bbox.xMin, x);
2012
+ bbox.yMin = Math.min(bbox.yMin, y);
2013
+ bbox.xMax = Math.max(bbox.xMax, x);
2014
+ bbox.yMax = Math.max(bbox.yMax, y);
2015
+ };
2016
+ addPoint(evaluateCurve2d$1(c, bounds.first));
2017
+ addPoint(evaluateCurve2d$1(c, bounds.last));
2018
+ const flat = flattenTrimmed(c);
2019
+ const basis = flat.__bk2d === "trimmed" ? flat.basis : flat;
2020
+ const basisBounds = curveBounds(basis);
2021
+ const [from, to] = flat.__bk2d === "trimmed" ? [flat.tStart, flat.tEnd] : [basisBounds.first, basisBounds.last];
2022
+ const lo = Math.min(from, to);
2023
+ const hi = Math.max(from, to);
2024
+ const add = (t) => {
2025
+ addPoint(evaluateCurve2d$1(basis, t));
2026
+ };
2027
+ switch (basis.__bk2d) {
2028
+ case "line": return;
2029
+ case "circle":
2030
+ case "ellipse":
2031
+ for (const t of conicExtremeParams(basis)) {
2032
+ const tt = periodicParamInRange(t, lo, hi);
2033
+ if (tt !== null) add(tt);
2034
+ }
2035
+ return;
2036
+ case "bezier":
2037
+ case "bspline":
2038
+ addPoleCurveExtent(bbox, basis, lo, hi);
2039
+ return;
2040
+ case "trimmed": return;
1104
2041
  }
1105
2042
  }
1106
2043
  function evaluateBezier(poles, t) {
@@ -4255,26 +5192,13 @@ function makeBezier2d(points) {
4255
5192
  return c2dWrap(makeBezier2d$1(points));
4256
5193
  }
4257
5194
  function makeBSpline2d(points, options) {
4258
- const poles = points;
4259
- const n = poles.length;
4260
- const degree = Math.max(1, Math.min(options?.degMax ?? 3, n - 1));
4261
- const knots = [0];
4262
- const mults = [degree + 1];
4263
- const nInternalKnots = n - degree - 1;
4264
- for (let i = 1; i <= nInternalKnots; i++) {
4265
- knots.push(i / (nInternalKnots + 1));
4266
- mults.push(1);
4267
- }
4268
- knots.push(1);
4269
- mults.push(degree + 1);
4270
- return c2dWrap({
4271
- __bk2d: "bspline",
4272
- poles,
4273
- knots,
4274
- multiplicities: mults,
4275
- degree,
4276
- isPeriodic: false
4277
- });
5195
+ const pts = [];
5196
+ for (const p of points) {
5197
+ const prev = pts[pts.length - 1];
5198
+ if (!prev || Math.hypot(p[0] - prev[0], p[1] - prev[1]) > 1e-12) pts.push([p[0], p[1]]);
5199
+ }
5200
+ if (pts.length < 2) throw new Error("occt-wasm: makeBSpline2d needs at least 2 distinct points");
5201
+ return c2dWrap(interpolateBSpline2d(pts, options?.degMax ?? 3));
4278
5202
  }
4279
5203
  function evaluateCurve2d(curve, param) {
4280
5204
  return evaluateCurve2d$1(c2d(curve), param);
@@ -4296,26 +5220,20 @@ function getCurve2dType(curve) {
4296
5220
  function trimCurve2d(curve, start, end) {
4297
5221
  return c2dWrap({
4298
5222
  __bk2d: "trimmed",
4299
- basis: c2d(curve),
5223
+ basis: structuredClone(c2d(curve)),
4300
5224
  tStart: start,
4301
5225
  tEnd: end
4302
5226
  });
4303
5227
  }
5228
+ function reverseCurve2d(curve) {
5229
+ const c = c2d(curve);
5230
+ Object.assign(c, reverseCurve2d$1(c));
5231
+ }
4304
5232
  function copyCurve2d(curve) {
4305
5233
  return c2dWrap(JSON.parse(JSON.stringify(c2d(curve))));
4306
5234
  }
4307
5235
  function offsetCurve2d(curve, offset) {
4308
- const c = c2d(curve);
4309
- const bounds = curveBounds(c);
4310
- const pts = [];
4311
- for (let i = 0; i <= 20; i++) {
4312
- const t = bounds.first + (bounds.last - bounds.first) * i / 20;
4313
- const [px, py] = evaluateCurve2d$1(c, t);
4314
- const [tx, ty] = tangentCurve2d(c, t);
4315
- const len = Math.sqrt(tx * tx + ty * ty) || 1;
4316
- pts.push([px - ty / len * offset, py + tx / len * offset]);
4317
- }
4318
- return c2dWrap(makeBezier2d$1(pts.length <= 25 ? pts : pts.filter((_, i) => i % 2 === 0 || i === pts.length - 1)));
5236
+ return c2dWrap(offsetCurve2d$1(c2d(curve), offset));
4319
5237
  }
4320
5238
  function translateCurve2d(curve, dx, dy) {
4321
5239
  return c2dWrap(translateCurve2d$1(c2d(curve), dx, dy));
@@ -4335,8 +5253,7 @@ function mirrorCurve2dAcrossAxis(curve, originX, originY, dirX, dirY) {
4335
5253
  /**
4336
5254
  * Directional affinity matching OCCT `gp_GTrsf2d::SetAffinity`: scales the
4337
5255
  * component perpendicular to the axis `(dx, dy)` through `(ox, oy)` by `ratio`,
4338
- * leaving the parallel component fixed. Applied by sampling and refitting as a
4339
- * Bezier, since occt-wasm has no native Geom2d transform.
5256
+ * leaving the parallel component fixed.
4340
5257
  */
4341
5258
  function affinityCurve2d(curve, ox, oy, dx, dy, ratio) {
4342
5259
  const len = Math.sqrt(dx * dx + dy * dy);
@@ -4346,15 +5263,14 @@ function affinityCurve2d(curve, ox, oy, dx, dy, ratio) {
4346
5263
  const a = 1 + k * px * px, b = k * px * py, d = k * py * px, e = 1 + k * py * py;
4347
5264
  const tx = ox - a * ox - b * oy;
4348
5265
  const ty = oy - d * ox - e * oy;
4349
- const c = c2d(curve);
4350
- const bounds = curveBounds(c);
4351
- const N = 20;
4352
- const pts = [];
4353
- for (let i = 0; i <= N; i++) {
4354
- const [qx, qy] = evaluateCurve2d$1(c, bounds.first + (bounds.last - bounds.first) * i / N);
4355
- pts.push([a * qx + b * qy + tx, d * qx + e * qy + ty]);
4356
- }
4357
- return c2dWrap(makeBezier2d$1(pts));
5266
+ return c2dWrap(affineCurve2d(c2d(curve), {
5267
+ a,
5268
+ b,
5269
+ c: d,
5270
+ d: e,
5271
+ tx,
5272
+ ty
5273
+ }));
4358
5274
  }
4359
5275
  function affinityTransform2d(curve, ox, oy, dx, dy, ratio) {
4360
5276
  return affinityCurve2d(curve, ox, oy, dx, dy, ratio);
@@ -4499,103 +5415,46 @@ function projectPointOnCurve2d(curve, x, y) {
4499
5415
  return projectOnBasis(c, bounds.first, bounds.last);
4500
5416
  }
4501
5417
  function distanceBetweenCurves2d(c1, c2, p1Start, p1End, p2Start, p2End) {
4502
- const n = 20;
4503
- let minDist = Infinity;
4504
- for (let i = 0; i <= n; i++) {
4505
- const t1 = p1Start + (p1End - p1Start) * (i / n);
4506
- const [x1, y1] = evaluateCurve2d$1(c2d(c1), t1);
4507
- for (let j = 0; j <= n; j++) {
4508
- const t2 = p2Start + (p2End - p2Start) * (j / n);
4509
- const [x2, y2] = evaluateCurve2d$1(c2d(c2), t2);
4510
- const d = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
4511
- if (d < minDist) minDist = d;
4512
- }
4513
- }
4514
- return minDist;
5418
+ return distanceBetweenCurves2d$1(c2d(c1), c2d(c2), p1Start, p1End, p2Start, p2End);
4515
5419
  }
4516
- function approximateCurve2dAsBSpline(curve, maxSegments) {
4517
- const cu = c2d(curve);
5420
+ /**
5421
+ * The exact B-spline of a Bezier or B-spline basis over its trim range, traversed
5422
+ * from `from` to `to`; null for an empty range.
5423
+ */
5424
+ function trimmedPoleCurve(basis, from, to) {
5425
+ if (from === to) return null;
5426
+ const sub = subBSpline2d(asBSpline2d(basis), Math.min(from, to), Math.max(from, to));
5427
+ if (from < to) return sub;
5428
+ const reversed = reverseCurve2d$1(sub);
5429
+ return reversed.__bk2d === "bspline" ? reversed : null;
5430
+ }
5431
+ /**
5432
+ * Degree <= 3 B-spline within `tolerance` of the curve (Geom2dConvert_ApproxCurve
5433
+ * with MaxDegree 3, as brepjs calls it); exact where the curve already is one.
5434
+ */
5435
+ function approximateCurve2dAsBSpline(curve, tolerance, maxSegments) {
5436
+ const cu = flattenTrimmed(c2d(curve));
5437
+ const basis = cu.__bk2d === "trimmed" ? cu.basis : cu;
5438
+ const poleCurve = basis.__bk2d === "bspline" || basis.__bk2d === "bezier" ? basis : null;
5439
+ if (poleCurve && asBSpline2d(poleCurve).degree <= 3) {
5440
+ if (cu.__bk2d !== "trimmed") return copyCurve2d(asBSpline2d(poleCurve));
5441
+ const exact = trimmedPoleCurve(poleCurve, cu.tStart, cu.tEnd);
5442
+ if (exact) return c2dWrap(exact);
5443
+ }
5444
+ if (cu.__bk2d === "line") return c2dWrap(asBSpline2d(makeBezier2d$1([evaluateCurve2d$1(cu, 0), evaluateCurve2d$1(cu, cu.len)])));
4518
5445
  const bounds = curveBounds(cu);
4519
- const nPts = Math.min(Math.max(maxSegments + 1, 10), 100);
4520
- const poles = [];
4521
- for (let i = 0; i < nPts; i++) {
4522
- const t = bounds.first + (bounds.last - bounds.first) * i / (nPts - 1);
4523
- poles.push(evaluateCurve2d$1(cu, t));
4524
- }
4525
- const degree = Math.min(3, nPts - 1);
4526
- const n = poles.length;
4527
- const knots = [];
4528
- const mults = [];
4529
- const nInternalKnots = n - degree - 1;
4530
- knots.push(0);
4531
- mults.push(degree + 1);
4532
- for (let i = 1; i <= nInternalKnots; i++) {
4533
- knots.push(i / (nInternalKnots + 1));
4534
- mults.push(1);
4535
- }
4536
- knots.push(1);
4537
- mults.push(degree + 1);
4538
- return c2dWrap({
4539
- __bk2d: "bspline",
4540
- poles,
4541
- knots,
4542
- multiplicities: mults,
4543
- degree,
4544
- isPeriodic: false
4545
- });
5446
+ return c2dWrap(fitBSpline2d((t) => evaluateCurve2d$1(cu, t), bounds.first, bounds.last, tolerance, {
5447
+ maxSegments: Math.max(8, maxSegments),
5448
+ breaks: curveKnotParams(cu)
5449
+ }));
4546
5450
  }
4547
5451
  function decomposeBSpline2dToBeziers(curve) {
4548
- let cu = c2d(curve);
4549
- while (cu.__bk2d === "trimmed") cu = cu.basis;
4550
- if (cu.__bk2d !== "bspline") return [curve];
4551
- const poles = cu.poles;
4552
- const p = cu.degree;
4553
- const U = [];
4554
- for (let i = 0; i < cu.knots.length; i++) {
4555
- const knot = cu.knots[i] ?? 0;
4556
- const mult = cu.multiplicities[i] ?? 0;
4557
- for (let j = 0; j < mult; j++) U.push(knot);
4558
- }
4559
- const m = poles.length - 1 + p + 1;
4560
- if (p < 1 || U.length !== m + 1) return [c2dWrap(makeBezier2d$1(poles))];
4561
- const clone = (pt) => [pt[0], pt[1]];
4562
- const segments = [];
4563
- let a = p;
4564
- let b = p + 1;
4565
- let segPoles = [];
4566
- for (let i = 0; i <= p; i++) segPoles[i] = clone(poles[i]);
4567
- let nextPoles = [];
4568
- while (b < m) {
4569
- const i0 = b;
4570
- while (b < m && U[b + 1] === U[b]) b++;
4571
- const mult = b - i0 + 1;
4572
- if (mult < p) {
4573
- const numer = U[b] - U[a];
4574
- const alphas = [];
4575
- for (let j = p; j > mult; j--) alphas[j - mult - 1] = numer / (U[a + j] - U[a]);
4576
- const r = p - mult;
4577
- for (let j = 1; j <= r; j++) {
4578
- const save = r - j;
4579
- const s = mult + j;
4580
- for (let k = p; k >= s; k--) {
4581
- const alpha = alphas[k - s];
4582
- const cur = segPoles[k];
4583
- const prev = segPoles[k - 1];
4584
- segPoles[k] = [alpha * cur[0] + (1 - alpha) * prev[0], alpha * cur[1] + (1 - alpha) * prev[1]];
4585
- }
4586
- if (b < m) nextPoles[save] = clone(segPoles[p]);
4587
- }
4588
- }
4589
- segments.push(segPoles);
4590
- if (b < m) {
4591
- for (let i = p - mult; i <= p; i++) nextPoles[i] = clone(poles[b - p + i]);
4592
- segPoles = nextPoles;
4593
- nextPoles = [];
4594
- a = b;
4595
- b++;
4596
- }
4597
- }
4598
- return segments.map((sp) => c2dWrap(makeBezier2d$1(sp)));
5452
+ const cu = flattenTrimmed(c2d(curve));
5453
+ const basis = cu.__bk2d === "trimmed" ? cu.basis : cu;
5454
+ if (basis.__bk2d !== "bspline") return [curve];
5455
+ const piece = cu.__bk2d === "trimmed" ? trimmedPoleCurve(basis, cu.tStart, cu.tEnd) : basis;
5456
+ if (!piece) return [curve];
5457
+ return bezierSegments2d(piece).map((poles) => c2dWrap(makeBezier2d$1(poles)));
4599
5458
  }
4600
5459
  function createBoundingBox2d() {
4601
5460
  return createBBox2d();
@@ -4678,78 +5537,174 @@ function splitCurve2d(curve, params) {
4678
5537
  }
4679
5538
  return result;
4680
5539
  }
4681
- function liftCurve2dToPlane(k, Module, curve, planeOrigin, planeZ, planeX) {
4682
- const cu = c2d(curve);
4683
- const [ox, oy, oz] = planeOrigin;
4684
- const [zx, zy, zz] = planeZ;
4685
- const [xx, xy, xz] = planeX;
5540
+ function planeFrame(origin, z, x) {
5541
+ const [ox, oy, oz] = origin;
5542
+ const [zx, zy, zz] = z;
5543
+ const [xx, xy, xz] = x;
4686
5544
  const yx = zy * xz - zz * xy, yy = zz * xx - zx * xz, yz = zx * xy - zy * xx;
4687
- const lift = (u, v) => [
4688
- ox + u * xx + v * yx,
4689
- oy + u * xy + v * yy,
4690
- oz + u * xz + v * yz
5545
+ return {
5546
+ point: (u, v) => [
5547
+ ox + u * xx + v * yx,
5548
+ oy + u * xy + v * yy,
5549
+ oz + u * xz + v * yz
5550
+ ],
5551
+ dir: (u, v) => [
5552
+ u * xx + v * yx,
5553
+ u * xy + v * yy,
5554
+ u * xz + v * yz
5555
+ ],
5556
+ normal: z
5557
+ };
5558
+ }
5559
+ /** Lift an ellipse (or an arc of it over `range`) as an exact OCCT ellipse edge. */
5560
+ function liftEllipse(k, frame, basis, range) {
5561
+ let e = basis;
5562
+ let r = range;
5563
+ if (r && r[1] < r[0]) {
5564
+ e = {
5565
+ ...e,
5566
+ sense: !e.sense
5567
+ };
5568
+ r = [-r[0], -r[1]];
5569
+ }
5570
+ let major = e.majorRadius;
5571
+ let minor = e.minorRadius;
5572
+ let angle = e.xDirAngle;
5573
+ let shift = 0;
5574
+ if (major < minor) {
5575
+ [major, minor] = [minor, major];
5576
+ angle += Math.PI / 2;
5577
+ shift = e.sense ? -Math.PI / 2 : Math.PI / 2;
5578
+ }
5579
+ const n = frame.normal;
5580
+ const normal = e.sense ? n : [
5581
+ -n[0],
5582
+ -n[1],
5583
+ -n[2]
4691
5584
  ];
4692
- const bk2dType = cu.__bk2d;
4693
- if (bk2dType === "line") {
4694
- const bounds = curveBounds(cu);
4695
- const [u1, v1] = evaluateCurve2d$1(cu, bounds.first);
4696
- const [u2, v2] = evaluateCurve2d$1(cu, bounds.last);
4697
- const p1 = lift(u1, v1);
4698
- const p2 = lift(u2, v2);
4699
- return handle("edge", k.makeLineEdge(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]));
4700
- }
4701
- if (bk2dType === "trimmed") {
4702
- const trimmed = cu;
4703
- if (trimmed.basis && trimmed.basis.__bk2d === "line") {
4704
- const [u1, v1] = evaluateCurve2d$1(cu, 0);
4705
- const [u2, v2] = evaluateCurve2d$1(cu, 1);
4706
- const p1 = lift(u1, v1);
4707
- const p2 = lift(u2, v2);
4708
- return handle("edge", k.makeLineEdge(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]));
4709
- }
4710
- if (trimmed.basis && trimmed.basis.__bk2d === "circle") {
4711
- const bounds = curveBounds(cu);
4712
- const [u1, v1] = evaluateCurve2d$1(cu, bounds.first);
4713
- const [um, vm] = evaluateCurve2d$1(cu, (bounds.first + bounds.last) / 2);
4714
- const [u2, v2] = evaluateCurve2d$1(cu, bounds.last);
4715
- const p1 = lift(u1, v1);
4716
- const pm = lift(um, vm);
4717
- const p2 = lift(u2, v2);
4718
- return handle("edge", k.makeArcEdge(p1[0], p1[1], p1[2], pm[0], pm[1], pm[2], p2[0], p2[1], p2[2]));
5585
+ const center = frame.point(e.cx, e.cy);
5586
+ const xDir = frame.dir(Math.cos(angle), Math.sin(angle));
5587
+ if (!r || r[1] - r[0] >= 2 * Math.PI - 1e-12) return makeEllipseEdge(k, center, normal, major, minor, xDir);
5588
+ return makeEllipseArc(k, center, normal, major, minor, r[0] + shift, r[1] + shift, xDir);
5589
+ }
5590
+ function splitBezier(poles, t) {
5591
+ const work = poles.map(([x, y]) => [x, y]);
5592
+ const n = work.length;
5593
+ const left = [];
5594
+ const right = [];
5595
+ left.push([...work[0]]);
5596
+ right.push([...work[n - 1]]);
5597
+ for (let r = 1; r < n; r++) {
5598
+ for (let i = 0; i < n - r; i++) {
5599
+ const a = work[i];
5600
+ const b = work[i + 1];
5601
+ work[i] = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
4719
5602
  }
5603
+ left.push([...work[0]]);
5604
+ right.push([...work[n - 1 - r]]);
4720
5605
  }
4721
- if (bk2dType === "circle") {
4722
- const circleData = cu;
4723
- if (circleData.cx !== void 0 && circleData.radius !== void 0) {
4724
- const [pcx, pcy, pcz] = lift(circleData.cx, circleData.cy);
4725
- return handle("edge", k.makeCircleEdge(pcx, pcy, pcz, zx, zy, zz, circleData.radius));
4726
- }
4727
- const bounds = curveBounds(cu);
4728
- const [u1, v1] = evaluateCurve2d$1(cu, bounds.first);
4729
- const [um, vm] = evaluateCurve2d$1(cu, (bounds.first + bounds.last) / 2);
4730
- const [u2, v2] = evaluateCurve2d$1(cu, bounds.last);
4731
- const p1 = lift(u1, v1);
4732
- const pm = lift(um, vm);
4733
- const p2 = lift(u2, v2);
4734
- return handle("edge", k.makeArcEdge(p1[0], p1[1], p1[2], pm[0], pm[1], pm[2], p2[0], p2[1], p2[2]));
5606
+ return [left, right.reverse()];
5607
+ }
5608
+ /** Poles of the Bezier restricted to [from, to] (either order), traversed from -> to. */
5609
+ function bezierSegmentPoles(poles, from, to) {
5610
+ const lo = Math.min(from, to);
5611
+ const hi = Math.max(from, to);
5612
+ let seg = poles;
5613
+ if (hi < 1) seg = splitBezier(seg, hi)[0];
5614
+ if (lo > 0) seg = splitBezier(seg, hi > 0 ? lo / hi : 0)[1];
5615
+ return from <= to ? seg : [...seg].reverse();
5616
+ }
5617
+ function liftBezier(k, Module, frame, poles) {
5618
+ const vec = new Module.VectorDouble();
5619
+ try {
5620
+ for (const [u, v] of poles) for (const x of frame.point(u, v)) vec.push_back(x);
5621
+ return handle("edge", k.makeBezierEdge(vec));
5622
+ } finally {
5623
+ vec.delete();
4735
5624
  }
4736
- const bounds = curveBounds(cu);
4737
- const nSamples = 24;
4738
- const dt = (bounds.last - bounds.first) / nSamples;
4739
- const pts = [];
4740
- for (let i = 0; i <= nSamples; i++) {
4741
- const [u, v] = evaluateCurve2d$1(cu, bounds.first + i * dt);
4742
- const [px, py, pz] = lift(u, v);
4743
- pts.push(px, py, pz);
5625
+ }
5626
+ /** Lift a non-periodic B-spline exactly (an affine lift maps poles to poles). */
5627
+ function liftBSpline(k, Module, frame, c) {
5628
+ if (!k.makeBSplineEdge || c.isPeriodic) return null;
5629
+ const poles = new Module.VectorDouble();
5630
+ const weights = new Module.VectorDouble();
5631
+ const knots = new Module.VectorDouble();
5632
+ const mults = new Module.VectorInt();
5633
+ try {
5634
+ for (const [u, v] of c.poles) for (const x of frame.point(u, v)) poles.push_back(x);
5635
+ for (const knot of c.knots) knots.push_back(knot);
5636
+ for (const m of c.multiplicities) mults.push_back(m);
5637
+ return handle("edge", k.makeBSplineEdge(poles, weights, knots, mults, c.degree, false));
5638
+ } finally {
5639
+ poles.delete();
5640
+ weights.delete();
5641
+ knots.delete();
5642
+ mults.delete();
4744
5643
  }
5644
+ }
5645
+ function liftFitted(k, Module, frame, cu) {
5646
+ const bounds = curveBounds(cu);
5647
+ const bbox = createBBox2d();
5648
+ addCurveToBBox(bbox, cu, 0);
5649
+ const extent = Math.hypot(bbox.xMax - bbox.xMin, bbox.yMax - bbox.yMin);
5650
+ const knots = curveKnotParams(cu);
5651
+ const fit = fitBSpline2d((t) => evaluateCurve2d$1(cu, t), bounds.first, bounds.last, 1e-9 * Math.max(1, extent), { breaks: knots });
5652
+ const exact = liftBSpline(k, Module, frame, fit);
5653
+ if (exact) return exact;
5654
+ const nSamples = Math.max(24, fit.poles.length);
5655
+ const span = bounds.last - bounds.first;
5656
+ const params = [];
5657
+ for (let i = 0; i <= nSamples; i++) params.push(bounds.first + span * i / nSamples);
5658
+ for (const t of knots) {
5659
+ const nearest = bounds.first + span * Math.round((t - bounds.first) / span * nSamples) / nSamples;
5660
+ if (Math.abs(t - nearest) > 1e-9 * span) params.push(t);
5661
+ }
5662
+ params.sort((a, b) => a - b);
5663
+ const distinct = params.filter((t, i) => i === 0 || t - (params[i - 1] ?? t) > 1e-9 * span);
4745
5664
  const vec = new Module.VectorDouble();
4746
- for (const p of pts) vec.push_back(p);
4747
5665
  try {
5666
+ for (const t of distinct) {
5667
+ const [u, v] = evaluateCurve2d$1(cu, t);
5668
+ for (const x of frame.point(u, v)) vec.push_back(x);
5669
+ }
4748
5670
  return handle("edge", k.interpolatePoints(vec, false));
4749
5671
  } finally {
4750
5672
  vec.delete();
4751
5673
  }
4752
5674
  }
5675
+ function liftCurve2dToPlane(k, Module, curve, planeOrigin, planeZ, planeX) {
5676
+ const cu = flattenTrimmed(c2d(curve));
5677
+ const frame = planeFrame(planeOrigin, planeZ, planeX);
5678
+ const trimmed = cu.__bk2d === "trimmed" ? cu : null;
5679
+ const basis = trimmed ? trimmed.basis : cu;
5680
+ const range = trimmed ? [trimmed.tStart, trimmed.tEnd] : null;
5681
+ const bounds = curveBounds(cu);
5682
+ switch (basis.__bk2d) {
5683
+ case "line": {
5684
+ const p1 = frame.point(...evaluateCurve2d$1(cu, bounds.first));
5685
+ const p2 = frame.point(...evaluateCurve2d$1(cu, bounds.last));
5686
+ return handle("edge", k.makeLineEdge(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]));
5687
+ }
5688
+ case "circle": {
5689
+ if (!range || Math.abs(range[1] - range[0]) >= 2 * Math.PI - 1e-12) {
5690
+ const [pcx, pcy, pcz] = frame.point(basis.cx, basis.cy);
5691
+ const [zx, zy, zz] = planeZ;
5692
+ return handle("edge", k.makeCircleEdge(pcx, pcy, pcz, zx, zy, zz, basis.radius));
5693
+ }
5694
+ const p1 = frame.point(...evaluateCurve2d$1(cu, bounds.first));
5695
+ const pm = frame.point(...evaluateCurve2d$1(cu, (bounds.first + bounds.last) / 2));
5696
+ const p2 = frame.point(...evaluateCurve2d$1(cu, bounds.last));
5697
+ return handle("edge", k.makeArcEdge(p1[0], p1[1], p1[2], pm[0], pm[1], pm[2], p2[0], p2[1], p2[2]));
5698
+ }
5699
+ case "ellipse": return liftEllipse(k, frame, basis, range);
5700
+ case "bezier": return liftBezier(k, Module, frame, range ? bezierSegmentPoles(basis.poles, range[0], range[1]) : basis.poles);
5701
+ case "bspline": {
5702
+ const piece = range ? trimmedPoleCurve(basis, range[0], range[1]) : basis;
5703
+ return piece && liftBSpline(k, Module, frame, piece) || liftFitted(k, Module, frame, cu);
5704
+ }
5705
+ case "trimmed": return liftFitted(k, Module, frame, cu);
5706
+ }
5707
+ }
4753
5708
  function buildEdgeOnSurface(k, Module, curve, surface) {
4754
5709
  const cu = c2d(curve);
4755
5710
  const bounds = curveBounds(cu);
@@ -5831,8 +6786,8 @@ var OcctWasmAdapter = class OcctWasmAdapter {
5831
6786
  makeBezier2d(points) {
5832
6787
  return makeBezier2d(points);
5833
6788
  }
5834
- makeBSpline2d(points, _options) {
5835
- return makeBSpline2d(points);
6789
+ makeBSpline2d(points, options) {
6790
+ return makeBSpline2d(points, options);
5836
6791
  }
5837
6792
  evaluateCurve2d(curve, param) {
5838
6793
  return evaluateCurve2d(curve, param);
@@ -5849,7 +6804,9 @@ var OcctWasmAdapter = class OcctWasmAdapter {
5849
6804
  trimCurve2d(curve, start, end) {
5850
6805
  return trimCurve2d(curve, start, end);
5851
6806
  }
5852
- reverseCurve2d(curve) {}
6807
+ reverseCurve2d(curve) {
6808
+ reverseCurve2d(curve);
6809
+ }
5853
6810
  copyCurve2d(curve) {
5854
6811
  return copyCurve2d(curve);
5855
6812
  }
@@ -5910,8 +6867,8 @@ var OcctWasmAdapter = class OcctWasmAdapter {
5910
6867
  distanceBetweenCurves2d(c1, c2, p1Start, p1End, p2Start, p2End) {
5911
6868
  return distanceBetweenCurves2d(c1, c2, p1Start, p1End, p2Start, p2End);
5912
6869
  }
5913
- approximateCurve2dAsBSpline(curve, _tolerance, _continuity, maxSegments) {
5914
- return approximateCurve2dAsBSpline(curve, maxSegments);
6870
+ approximateCurve2dAsBSpline(curve, tolerance, _continuity, maxSegments) {
6871
+ return approximateCurve2dAsBSpline(curve, tolerance, maxSegments);
5915
6872
  }
5916
6873
  decomposeBSpline2dToBeziers(curve) {
5917
6874
  return decomposeBSpline2dToBeziers(curve);
@@ -6139,12 +7096,24 @@ Object.defineProperty(exports, "mirrorAtPoint", {
6139
7096
  return mirrorAtPoint;
6140
7097
  }
6141
7098
  });
7099
+ Object.defineProperty(exports, "offsetCurve2d", {
7100
+ enumerable: true,
7101
+ get: function() {
7102
+ return offsetCurve2d$1;
7103
+ }
7104
+ });
6142
7105
  Object.defineProperty(exports, "quickHull", {
6143
7106
  enumerable: true,
6144
7107
  get: function() {
6145
7108
  return quickHull;
6146
7109
  }
6147
7110
  });
7111
+ Object.defineProperty(exports, "reverseCurve2d", {
7112
+ enumerable: true,
7113
+ get: function() {
7114
+ return reverseCurve2d$1;
7115
+ }
7116
+ });
6148
7117
  Object.defineProperty(exports, "rotateCurve2d", {
6149
7118
  enumerable: true,
6150
7119
  get: function() {