brepjs 20.12.5 → 20.12.6

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 (94) hide show
  1. package/dist/2d/lib/offset.d.ts +1 -1
  2. package/dist/2d.cjs +6 -6
  3. package/dist/2d.js +6 -6
  4. package/dist/{adjacencyFns-B2Po1gPs.js → adjacencyFns--KmBIKYJ.js} +3 -3
  5. package/dist/{adjacencyFns-Cho9v3Q3.cjs → adjacencyFns-BFJfaUwX.cjs} +3 -3
  6. package/dist/{blueprint-Zmt5-07y.js → blueprint-BhIk9Pvm.js} +7 -8
  7. package/dist/{blueprint-ERmwk6zG.cjs → blueprint-D83DDDPb.cjs} +7 -8
  8. package/dist/{blueprintFns-B_sxtIo7.cjs → blueprintFns-C4TQn-x_.cjs} +2 -2
  9. package/dist/{blueprintFns-Dtm-XukP.js → blueprintFns-eULVgkq9.js} +2 -2
  10. package/dist/{blueprintSketcher-CwTAtrkY.js → blueprintSketcher-D_Faf0GO.js} +32 -14
  11. package/dist/{blueprintSketcher-CZyokLGJ.cjs → blueprintSketcher-Di376t-0.cjs} +32 -14
  12. package/dist/{boolean2D-CRuNKgXs.cjs → boolean2D-CNZFI_y8.cjs} +4 -4
  13. package/dist/{boolean2D-yUpS8onH.js → boolean2D-DqpCF72W.js} +4 -4
  14. package/dist/brepjs.cjs +30 -30
  15. package/dist/brepjs.js +30 -30
  16. package/dist/{cameraFns-DRok4UxZ.js → cameraFns-CrClIkmV.js} +3 -3
  17. package/dist/{cameraFns-DB1IyMmS.cjs → cameraFns-RqraCXIc.cjs} +3 -3
  18. package/dist/core.cjs +2 -2
  19. package/dist/core.js +2 -2
  20. package/dist/{cornerFinder-D5qnDbtZ.js → cornerFinder-BZuQkRp2.js} +1 -1
  21. package/dist/{cornerFinder-DfYBHHZG.cjs → cornerFinder-CdOe1Wrb.cjs} +1 -1
  22. package/dist/{curveFns-BGPSFUWB.js → curveFns-BP3srCfK.js} +1 -1
  23. package/dist/{curveFns-BKT4Sg8q.cjs → curveFns-CyIvQNYT.cjs} +1 -1
  24. package/dist/{drawFns-D8e5RiRe.cjs → drawFns-71cOhWD-.cjs} +18 -13
  25. package/dist/{drawFns-aGufr-pr.js → drawFns-DxDwJ5zZ.js} +18 -13
  26. package/dist/{faceFns-Cx7cwUMx.cjs → faceFns-B7MiDYj1.cjs} +2 -2
  27. package/dist/{faceFns-lE2OvBfq.js → faceFns-iVPbTxki.js} +2 -2
  28. package/dist/{healingFns-B94zSk1-.js → healingFns-BhP8zfp1.js} +5 -5
  29. package/dist/{healingFns-CcgyrL6h.cjs → healingFns-DWljGTNR.cjs} +5 -5
  30. package/dist/{helpers-CNgipe6l.js → helpers-D2hANLRu.js} +6 -6
  31. package/dist/{helpers-C4f2MV-r.cjs → helpers-DKEVm6Hu.cjs} +6 -6
  32. package/dist/{importFns-gB1X3ti2.js → importFns-DUzIkns7.js} +2 -2
  33. package/dist/{importFns-fIdcaMqt.cjs → importFns-KEPAgbjj.cjs} +2 -2
  34. package/dist/io.cjs +2 -2
  35. package/dist/io.js +2 -2
  36. package/dist/kernel/bspline2dFit.d.ts +42 -0
  37. package/dist/kernel/geometry2d.d.ts +45 -0
  38. package/dist/kernel/occt/kernel2dOps.d.ts +1 -1
  39. package/dist/kernel/occtWasm/kernel2dOps.d.ts +7 -4
  40. package/dist/kernel/occtWasm/occtWasmAdapter.cjs +1 -1
  41. package/dist/kernel/occtWasm/occtWasmAdapter.d.ts +2 -2
  42. package/dist/kernel/occtWasm/occtWasmAdapter.js +1 -1
  43. package/dist/kernel/occtWasm/occtWasmTypes.d.ts +2 -0
  44. package/dist/{loftFns-AHaYxJyS.cjs → loftFns-D5AvfCGG.cjs} +1 -1
  45. package/dist/{loftFns-mbyBvJ8Y.js → loftFns-D5NJpHLZ.js} +1 -1
  46. package/dist/{measureFns-CuabRDRL.cjs → measureFns-CZHNWkiA.cjs} +3 -3
  47. package/dist/{measureFns-C94G0fH8.js → measureFns-mxBdPiWa.js} +3 -3
  48. package/dist/measurement.cjs +2 -2
  49. package/dist/measurement.js +2 -2
  50. package/dist/{meshFns-GXF5gPZL.cjs → meshFns-Czlt-GBJ.cjs} +3 -3
  51. package/dist/{meshFns-jarDZKw1.js → meshFns-DmjMzNbH.js} +3 -3
  52. package/dist/{occtWasmAdapter-DlNmVNUn.js → occtWasmAdapter-B9d0Q0Vl.js} +1153 -257
  53. package/dist/{occtWasmAdapter-DeoeK2iJ.cjs → occtWasmAdapter-DVxLkjlJ.cjs} +1164 -256
  54. package/dist/operations.cjs +2 -2
  55. package/dist/operations.js +2 -2
  56. package/dist/{originTrackingFns-BxeUD2NM.js → originTrackingFns-CBnyKLm7.js} +2 -2
  57. package/dist/{originTrackingFns-C-yuMV3m.cjs → originTrackingFns-CHymOLLm.cjs} +2 -2
  58. package/dist/{primitiveFns-BVAkYHVf.cjs → primitiveFns-BVeBEaa4.cjs} +4 -4
  59. package/dist/{primitiveFns-CualoFAk.js → primitiveFns-DeNl60Rw.js} +4 -4
  60. package/dist/{principalBoxFns-DtI7tVuS.js → principalBoxFns-3mW_hA6z.js} +3 -3
  61. package/dist/{principalBoxFns-DHh5_HCw.cjs → principalBoxFns-CG6kG7vT.cjs} +3 -3
  62. package/dist/projection.cjs +1 -1
  63. package/dist/projection.js +1 -1
  64. package/dist/query.cjs +2 -2
  65. package/dist/query.js +2 -2
  66. package/dist/{refResolveFns-CaAw-ya3.cjs → refResolveFns-BAs4p9z7.cjs} +6 -6
  67. package/dist/{refResolveFns--SRyN2qa.js → refResolveFns-DHx1Y3GG.js} +6 -6
  68. package/dist/{shapeFns-CwjjUOr0.cjs → shapeFns-C0w8vvfb.cjs} +2 -2
  69. package/dist/{shapeFns-CdrDroOM.js → shapeFns-CgqKICF_.js} +2 -2
  70. package/dist/shapeRef.cjs +1 -1
  71. package/dist/shapeRef.js +1 -1
  72. package/dist/{shapeTypes-D938X2mg.cjs → shapeTypes-Dc7j-MYg.cjs} +10 -16
  73. package/dist/{shapeTypes-PNVEv6ga.js → shapeTypes-DwvQUp9M.js} +10 -16
  74. package/dist/sketching.cjs +3 -3
  75. package/dist/sketching.js +3 -3
  76. package/dist/{solidBuilders-Hft8Cr6a.js → solidBuilders-DT1gpVWM.js} +5 -5
  77. package/dist/{solidBuilders-CAkBQKWb.cjs → solidBuilders-iS5DDwg8.cjs} +5 -5
  78. package/dist/{surfaceBuilders-BKCiVf7T.cjs → surfaceBuilders-BvwSgD3C.cjs} +3 -3
  79. package/dist/{surfaceBuilders-CrO_BBKM.js → surfaceBuilders-CPLe0bbk.js} +3 -3
  80. package/dist/text.cjs +2 -2
  81. package/dist/text.js +2 -2
  82. package/dist/{textBlueprints-B8q2a-nN.js → textBlueprints-BU0Rhk54.js} +7 -7
  83. package/dist/{textBlueprints-C9HeYc5a.cjs → textBlueprints-CNPCPJix.cjs} +7 -7
  84. package/dist/{textMetrics-1fhCSAGF.cjs → textMetrics-C71g4TmO.cjs} +1 -1
  85. package/dist/{textMetrics-DJfTSy6w.js → textMetrics-Df9aLYK2.js} +1 -1
  86. package/dist/{threadFns-CVIEbOW3.cjs → threadFns-CzXEnnmo.cjs} +7 -7
  87. package/dist/{threadFns-f2KwibLR.js → threadFns-DESH7lcs.js} +7 -7
  88. package/dist/topology.cjs +9 -9
  89. package/dist/topology.js +9 -9
  90. package/dist/{topologyQueryFns-4WYqtSFF.cjs → topologyQueryFns-5Frx8Y-0.cjs} +1 -1
  91. package/dist/{topologyQueryFns-BZUY10TD.js → topologyQueryFns-LXV1nTaS.js} +1 -1
  92. package/dist/{validityTypes-tWSx_zAH.cjs → validityTypes-C6rROGTP.cjs} +1 -1
  93. package/dist/{validityTypes-DAXIqwZM.js → validityTypes-DM-Iwe8N.js} +1 -1
  94. 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
@@ -1046,61 +1847,136 @@ function createBBox2d() {
1046
1847
  yMax: -Infinity
1047
1848
  };
1048
1849
  }
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);
1850
+ /** The first parameter congruent to `t` (mod 2*pi) inside [lo, hi], if any. */
1851
+ function periodicParamInRange(t, lo, hi) {
1852
+ const period = 2 * Math.PI;
1853
+ const tt = t + period * Math.ceil((lo - t) / period - 1e-12);
1854
+ return tt <= hi + 1e-12 ? tt : null;
1855
+ }
1856
+ /**
1857
+ * Basis parameters where a conic reaches its axis-aligned extremes. For
1858
+ * C + a*cos(psi)*U + b*sin(psi)*V (U at angle phi), dx/dpsi = 0 at
1859
+ * tan(psi) = -b*sin(phi) / (a*cos(phi)) and dy/dpsi = 0 at
1860
+ * tan(psi) = b*cos(phi) / (a*sin(phi)), each plus pi.
1861
+ */
1862
+ function conicExtremeParams(c) {
1863
+ const [a, b, phi] = c.__bk2d === "circle" ? [
1864
+ c.radius,
1865
+ c.radius,
1866
+ 0
1867
+ ] : [
1868
+ c.majorRadius,
1869
+ c.minorRadius,
1870
+ c.xDirAngle
1871
+ ];
1872
+ const psiX = Math.atan2(-b * Math.sin(phi), a * Math.cos(phi));
1873
+ const psiY = Math.atan2(b * Math.cos(phi), a * Math.sin(phi));
1874
+ return [
1875
+ psiX,
1876
+ psiX + Math.PI,
1877
+ psiY,
1878
+ psiY + Math.PI
1879
+ ].map((psi) => c.sense ? psi : -psi);
1880
+ }
1881
+ function splitBezier1d(vals) {
1882
+ const work = [...vals];
1883
+ const n = work.length;
1884
+ const left = [];
1885
+ const right = [];
1886
+ left.push(work[0]);
1887
+ right.push(work[n - 1]);
1888
+ for (let r = 1; r < n; r++) {
1889
+ for (let i = 0; i < n - r; i++) work[i] = (work[i] + work[i + 1]) / 2;
1890
+ left.push(work[0]);
1891
+ right.push(work[n - 1 - r]);
1892
+ }
1893
+ return [left, right.reverse()];
1894
+ }
1895
+ /**
1896
+ * Widen `range` to the values a 1D Bezier takes. Its coefficients bound it
1897
+ * (convex hull), so a piece whose coefficients stay inside the range is done;
1898
+ * otherwise the piece is halved, adding the curve value at the split.
1899
+ */
1900
+ function widenToBezier(vals, range, depth = 0) {
1901
+ const lo = Math.min(...vals);
1902
+ const hi = Math.max(...vals);
1903
+ const tol = 1e-12 * (1 + Math.abs(range.min) + Math.abs(range.max));
1904
+ if (lo >= range.min - tol && hi <= range.max + tol) return;
1905
+ if (depth >= 60) {
1906
+ range.min = Math.min(range.min, lo);
1907
+ range.max = Math.max(range.max, hi);
1092
1908
  return;
1093
1909
  }
1910
+ const [left, right] = splitBezier1d(vals);
1911
+ const mid = right[0] ?? lo;
1912
+ range.min = Math.min(range.min, mid);
1913
+ range.max = Math.max(range.max, mid);
1914
+ widenToBezier(left, range, depth + 1);
1915
+ widenToBezier(right, range, depth + 1);
1916
+ }
1917
+ /** Add the exact extent of a Bezier or B-spline over [lo, hi], span by span. */
1918
+ function addPoleCurveExtent(bbox, c, lo, hi) {
1919
+ const bs = asBSpline2d(c);
1920
+ const first = bs.knots[0] ?? 0;
1921
+ const last = bs.knots[bs.knots.length - 1] ?? 1;
1922
+ const a = Math.max(first, lo);
1923
+ const b = Math.min(last, hi);
1924
+ if (b <= a) return;
1925
+ const piece = a > first || b < last ? subBSpline2d(bs, a, b) : bs;
1926
+ const x = {
1927
+ min: bbox.xMin,
1928
+ max: bbox.xMax
1929
+ };
1930
+ const y = {
1931
+ min: bbox.yMin,
1932
+ max: bbox.yMax
1933
+ };
1934
+ for (const seg of bezierSegments2d(piece)) for (const [range, axis] of [[x, 0], [y, 1]]) {
1935
+ const vals = seg.map((pt) => pt[axis]);
1936
+ const ends = [vals[0] ?? range.min, vals[vals.length - 1] ?? range.min];
1937
+ range.min = Math.min(range.min, ...ends);
1938
+ range.max = Math.max(range.max, ...ends);
1939
+ widenToBezier(vals, range);
1940
+ }
1941
+ bbox.xMin = x.min;
1942
+ bbox.xMax = x.max;
1943
+ bbox.yMin = y.min;
1944
+ bbox.yMax = y.max;
1945
+ }
1946
+ function addCurveToBBox(bbox, c, _tol) {
1094
1947
  const bounds = curveBounds(c);
1095
1948
  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;
1949
+ const addPoint = ([x, y]) => {
1950
+ bbox.xMin = Math.min(bbox.xMin, x);
1951
+ bbox.yMin = Math.min(bbox.yMin, y);
1952
+ bbox.xMax = Math.max(bbox.xMax, x);
1953
+ bbox.yMax = Math.max(bbox.yMax, y);
1954
+ };
1955
+ addPoint(evaluateCurve2d$1(c, bounds.first));
1956
+ addPoint(evaluateCurve2d$1(c, bounds.last));
1957
+ const flat = flattenTrimmed(c);
1958
+ const basis = flat.__bk2d === "trimmed" ? flat.basis : flat;
1959
+ const basisBounds = curveBounds(basis);
1960
+ const [from, to] = flat.__bk2d === "trimmed" ? [flat.tStart, flat.tEnd] : [basisBounds.first, basisBounds.last];
1961
+ const lo = Math.min(from, to);
1962
+ const hi = Math.max(from, to);
1963
+ const add = (t) => {
1964
+ addPoint(evaluateCurve2d$1(basis, t));
1965
+ };
1966
+ switch (basis.__bk2d) {
1967
+ case "line": return;
1968
+ case "circle":
1969
+ case "ellipse":
1970
+ for (const t of conicExtremeParams(basis)) {
1971
+ const tt = periodicParamInRange(t, lo, hi);
1972
+ if (tt !== null) add(tt);
1973
+ }
1974
+ return;
1975
+ case "bezier":
1976
+ case "bspline":
1977
+ addPoleCurveExtent(bbox, basis, lo, hi);
1978
+ return;
1979
+ case "trimmed": return;
1104
1980
  }
1105
1981
  }
1106
1982
  function evaluateBezier(poles, t) {
@@ -4255,26 +5131,13 @@ function makeBezier2d(points) {
4255
5131
  return c2dWrap(makeBezier2d$1(points));
4256
5132
  }
4257
5133
  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
- });
5134
+ const pts = [];
5135
+ for (const p of points) {
5136
+ const prev = pts[pts.length - 1];
5137
+ if (!prev || Math.hypot(p[0] - prev[0], p[1] - prev[1]) > 1e-12) pts.push([p[0], p[1]]);
5138
+ }
5139
+ if (pts.length < 2) throw new Error("occt-wasm: makeBSpline2d needs at least 2 distinct points");
5140
+ return c2dWrap(interpolateBSpline2d(pts, options?.degMax ?? 3));
4278
5141
  }
4279
5142
  function evaluateCurve2d(curve, param) {
4280
5143
  return evaluateCurve2d$1(c2d(curve), param);
@@ -4296,26 +5159,20 @@ function getCurve2dType(curve) {
4296
5159
  function trimCurve2d(curve, start, end) {
4297
5160
  return c2dWrap({
4298
5161
  __bk2d: "trimmed",
4299
- basis: c2d(curve),
5162
+ basis: structuredClone(c2d(curve)),
4300
5163
  tStart: start,
4301
5164
  tEnd: end
4302
5165
  });
4303
5166
  }
5167
+ function reverseCurve2d(curve) {
5168
+ const c = c2d(curve);
5169
+ Object.assign(c, reverseCurve2d$1(c));
5170
+ }
4304
5171
  function copyCurve2d(curve) {
4305
5172
  return c2dWrap(JSON.parse(JSON.stringify(c2d(curve))));
4306
5173
  }
4307
5174
  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)));
5175
+ return c2dWrap(offsetCurve2d$1(c2d(curve), offset));
4319
5176
  }
4320
5177
  function translateCurve2d(curve, dx, dy) {
4321
5178
  return c2dWrap(translateCurve2d$1(c2d(curve), dx, dy));
@@ -4335,8 +5192,7 @@ function mirrorCurve2dAcrossAxis(curve, originX, originY, dirX, dirY) {
4335
5192
  /**
4336
5193
  * Directional affinity matching OCCT `gp_GTrsf2d::SetAffinity`: scales the
4337
5194
  * 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.
5195
+ * leaving the parallel component fixed.
4340
5196
  */
4341
5197
  function affinityCurve2d(curve, ox, oy, dx, dy, ratio) {
4342
5198
  const len = Math.sqrt(dx * dx + dy * dy);
@@ -4346,15 +5202,14 @@ function affinityCurve2d(curve, ox, oy, dx, dy, ratio) {
4346
5202
  const a = 1 + k * px * px, b = k * px * py, d = k * py * px, e = 1 + k * py * py;
4347
5203
  const tx = ox - a * ox - b * oy;
4348
5204
  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));
5205
+ return c2dWrap(affineCurve2d(c2d(curve), {
5206
+ a,
5207
+ b,
5208
+ c: d,
5209
+ d: e,
5210
+ tx,
5211
+ ty
5212
+ }));
4358
5213
  }
4359
5214
  function affinityTransform2d(curve, ox, oy, dx, dy, ratio) {
4360
5215
  return affinityCurve2d(curve, ox, oy, dx, dy, ratio);
@@ -4499,103 +5354,46 @@ function projectPointOnCurve2d(curve, x, y) {
4499
5354
  return projectOnBasis(c, bounds.first, bounds.last);
4500
5355
  }
4501
5356
  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;
5357
+ return distanceBetweenCurves2d$1(c2d(c1), c2d(c2), p1Start, p1End, p2Start, p2End);
4515
5358
  }
4516
- function approximateCurve2dAsBSpline(curve, maxSegments) {
4517
- const cu = c2d(curve);
5359
+ /**
5360
+ * The exact B-spline of a Bezier or B-spline basis over its trim range, traversed
5361
+ * from `from` to `to`; null for an empty range.
5362
+ */
5363
+ function trimmedPoleCurve(basis, from, to) {
5364
+ if (from === to) return null;
5365
+ const sub = subBSpline2d(asBSpline2d(basis), Math.min(from, to), Math.max(from, to));
5366
+ if (from < to) return sub;
5367
+ const reversed = reverseCurve2d$1(sub);
5368
+ return reversed.__bk2d === "bspline" ? reversed : null;
5369
+ }
5370
+ /**
5371
+ * Degree <= 3 B-spline within `tolerance` of the curve (Geom2dConvert_ApproxCurve
5372
+ * with MaxDegree 3, as brepjs calls it); exact where the curve already is one.
5373
+ */
5374
+ function approximateCurve2dAsBSpline(curve, tolerance, maxSegments) {
5375
+ const cu = flattenTrimmed(c2d(curve));
5376
+ const basis = cu.__bk2d === "trimmed" ? cu.basis : cu;
5377
+ const poleCurve = basis.__bk2d === "bspline" || basis.__bk2d === "bezier" ? basis : null;
5378
+ if (poleCurve && asBSpline2d(poleCurve).degree <= 3) {
5379
+ if (cu.__bk2d !== "trimmed") return copyCurve2d(asBSpline2d(poleCurve));
5380
+ const exact = trimmedPoleCurve(poleCurve, cu.tStart, cu.tEnd);
5381
+ if (exact) return c2dWrap(exact);
5382
+ }
5383
+ if (cu.__bk2d === "line") return c2dWrap(asBSpline2d(makeBezier2d$1([evaluateCurve2d$1(cu, 0), evaluateCurve2d$1(cu, cu.len)])));
4518
5384
  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
- });
5385
+ return c2dWrap(fitBSpline2d((t) => evaluateCurve2d$1(cu, t), bounds.first, bounds.last, tolerance, {
5386
+ maxSegments: Math.max(8, maxSegments),
5387
+ breaks: curveKnotParams(cu)
5388
+ }));
4546
5389
  }
4547
5390
  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)));
5391
+ const cu = flattenTrimmed(c2d(curve));
5392
+ const basis = cu.__bk2d === "trimmed" ? cu.basis : cu;
5393
+ if (basis.__bk2d !== "bspline") return [curve];
5394
+ const piece = cu.__bk2d === "trimmed" ? trimmedPoleCurve(basis, cu.tStart, cu.tEnd) : basis;
5395
+ if (!piece) return [curve];
5396
+ return bezierSegments2d(piece).map((poles) => c2dWrap(makeBezier2d$1(poles)));
4599
5397
  }
4600
5398
  function createBoundingBox2d() {
4601
5399
  return createBBox2d();
@@ -4678,78 +5476,174 @@ function splitCurve2d(curve, params) {
4678
5476
  }
4679
5477
  return result;
4680
5478
  }
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;
5479
+ function planeFrame(origin, z, x) {
5480
+ const [ox, oy, oz] = origin;
5481
+ const [zx, zy, zz] = z;
5482
+ const [xx, xy, xz] = x;
4686
5483
  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
5484
+ return {
5485
+ point: (u, v) => [
5486
+ ox + u * xx + v * yx,
5487
+ oy + u * xy + v * yy,
5488
+ oz + u * xz + v * yz
5489
+ ],
5490
+ dir: (u, v) => [
5491
+ u * xx + v * yx,
5492
+ u * xy + v * yy,
5493
+ u * xz + v * yz
5494
+ ],
5495
+ normal: z
5496
+ };
5497
+ }
5498
+ /** Lift an ellipse (or an arc of it over `range`) as an exact OCCT ellipse edge. */
5499
+ function liftEllipse(k, frame, basis, range) {
5500
+ let e = basis;
5501
+ let r = range;
5502
+ if (r && r[1] < r[0]) {
5503
+ e = {
5504
+ ...e,
5505
+ sense: !e.sense
5506
+ };
5507
+ r = [-r[0], -r[1]];
5508
+ }
5509
+ let major = e.majorRadius;
5510
+ let minor = e.minorRadius;
5511
+ let angle = e.xDirAngle;
5512
+ let shift = 0;
5513
+ if (major < minor) {
5514
+ [major, minor] = [minor, major];
5515
+ angle += Math.PI / 2;
5516
+ shift = e.sense ? -Math.PI / 2 : Math.PI / 2;
5517
+ }
5518
+ const n = frame.normal;
5519
+ const normal = e.sense ? n : [
5520
+ -n[0],
5521
+ -n[1],
5522
+ -n[2]
4691
5523
  ];
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]));
5524
+ const center = frame.point(e.cx, e.cy);
5525
+ const xDir = frame.dir(Math.cos(angle), Math.sin(angle));
5526
+ if (!r || r[1] - r[0] >= 2 * Math.PI - 1e-12) return makeEllipseEdge(k, center, normal, major, minor, xDir);
5527
+ return makeEllipseArc(k, center, normal, major, minor, r[0] + shift, r[1] + shift, xDir);
5528
+ }
5529
+ function splitBezier(poles, t) {
5530
+ const work = poles.map(([x, y]) => [x, y]);
5531
+ const n = work.length;
5532
+ const left = [];
5533
+ const right = [];
5534
+ left.push([...work[0]]);
5535
+ right.push([...work[n - 1]]);
5536
+ for (let r = 1; r < n; r++) {
5537
+ for (let i = 0; i < n - r; i++) {
5538
+ const a = work[i];
5539
+ const b = work[i + 1];
5540
+ work[i] = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
4719
5541
  }
5542
+ left.push([...work[0]]);
5543
+ right.push([...work[n - 1 - r]]);
4720
5544
  }
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]));
5545
+ return [left, right.reverse()];
5546
+ }
5547
+ /** Poles of the Bezier restricted to [from, to] (either order), traversed from -> to. */
5548
+ function bezierSegmentPoles(poles, from, to) {
5549
+ const lo = Math.min(from, to);
5550
+ const hi = Math.max(from, to);
5551
+ let seg = poles;
5552
+ if (hi < 1) seg = splitBezier(seg, hi)[0];
5553
+ if (lo > 0) seg = splitBezier(seg, hi > 0 ? lo / hi : 0)[1];
5554
+ return from <= to ? seg : [...seg].reverse();
5555
+ }
5556
+ function liftBezier(k, Module, frame, poles) {
5557
+ const vec = new Module.VectorDouble();
5558
+ try {
5559
+ for (const [u, v] of poles) for (const x of frame.point(u, v)) vec.push_back(x);
5560
+ return handle("edge", k.makeBezierEdge(vec));
5561
+ } finally {
5562
+ vec.delete();
4735
5563
  }
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);
5564
+ }
5565
+ /** Lift a non-periodic B-spline exactly (an affine lift maps poles to poles). */
5566
+ function liftBSpline(k, Module, frame, c) {
5567
+ if (!k.makeBSplineEdge || c.isPeriodic) return null;
5568
+ const poles = new Module.VectorDouble();
5569
+ const weights = new Module.VectorDouble();
5570
+ const knots = new Module.VectorDouble();
5571
+ const mults = new Module.VectorInt();
5572
+ try {
5573
+ for (const [u, v] of c.poles) for (const x of frame.point(u, v)) poles.push_back(x);
5574
+ for (const knot of c.knots) knots.push_back(knot);
5575
+ for (const m of c.multiplicities) mults.push_back(m);
5576
+ return handle("edge", k.makeBSplineEdge(poles, weights, knots, mults, c.degree, false));
5577
+ } finally {
5578
+ poles.delete();
5579
+ weights.delete();
5580
+ knots.delete();
5581
+ mults.delete();
4744
5582
  }
5583
+ }
5584
+ function liftFitted(k, Module, frame, cu) {
5585
+ const bounds = curveBounds(cu);
5586
+ const bbox = createBBox2d();
5587
+ addCurveToBBox(bbox, cu, 0);
5588
+ const extent = Math.hypot(bbox.xMax - bbox.xMin, bbox.yMax - bbox.yMin);
5589
+ const knots = curveKnotParams(cu);
5590
+ const fit = fitBSpline2d((t) => evaluateCurve2d$1(cu, t), bounds.first, bounds.last, 1e-9 * Math.max(1, extent), { breaks: knots });
5591
+ const exact = liftBSpline(k, Module, frame, fit);
5592
+ if (exact) return exact;
5593
+ const nSamples = Math.max(24, fit.poles.length);
5594
+ const span = bounds.last - bounds.first;
5595
+ const params = [];
5596
+ for (let i = 0; i <= nSamples; i++) params.push(bounds.first + span * i / nSamples);
5597
+ for (const t of knots) {
5598
+ const nearest = bounds.first + span * Math.round((t - bounds.first) / span * nSamples) / nSamples;
5599
+ if (Math.abs(t - nearest) > 1e-9 * span) params.push(t);
5600
+ }
5601
+ params.sort((a, b) => a - b);
5602
+ const distinct = params.filter((t, i) => i === 0 || t - (params[i - 1] ?? t) > 1e-9 * span);
4745
5603
  const vec = new Module.VectorDouble();
4746
- for (const p of pts) vec.push_back(p);
4747
5604
  try {
5605
+ for (const t of distinct) {
5606
+ const [u, v] = evaluateCurve2d$1(cu, t);
5607
+ for (const x of frame.point(u, v)) vec.push_back(x);
5608
+ }
4748
5609
  return handle("edge", k.interpolatePoints(vec, false));
4749
5610
  } finally {
4750
5611
  vec.delete();
4751
5612
  }
4752
5613
  }
5614
+ function liftCurve2dToPlane(k, Module, curve, planeOrigin, planeZ, planeX) {
5615
+ const cu = flattenTrimmed(c2d(curve));
5616
+ const frame = planeFrame(planeOrigin, planeZ, planeX);
5617
+ const trimmed = cu.__bk2d === "trimmed" ? cu : null;
5618
+ const basis = trimmed ? trimmed.basis : cu;
5619
+ const range = trimmed ? [trimmed.tStart, trimmed.tEnd] : null;
5620
+ const bounds = curveBounds(cu);
5621
+ switch (basis.__bk2d) {
5622
+ case "line": {
5623
+ const p1 = frame.point(...evaluateCurve2d$1(cu, bounds.first));
5624
+ const p2 = frame.point(...evaluateCurve2d$1(cu, bounds.last));
5625
+ return handle("edge", k.makeLineEdge(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]));
5626
+ }
5627
+ case "circle": {
5628
+ if (!range || Math.abs(range[1] - range[0]) >= 2 * Math.PI - 1e-12) {
5629
+ const [pcx, pcy, pcz] = frame.point(basis.cx, basis.cy);
5630
+ const [zx, zy, zz] = planeZ;
5631
+ return handle("edge", k.makeCircleEdge(pcx, pcy, pcz, zx, zy, zz, basis.radius));
5632
+ }
5633
+ const p1 = frame.point(...evaluateCurve2d$1(cu, bounds.first));
5634
+ const pm = frame.point(...evaluateCurve2d$1(cu, (bounds.first + bounds.last) / 2));
5635
+ const p2 = frame.point(...evaluateCurve2d$1(cu, bounds.last));
5636
+ return handle("edge", k.makeArcEdge(p1[0], p1[1], p1[2], pm[0], pm[1], pm[2], p2[0], p2[1], p2[2]));
5637
+ }
5638
+ case "ellipse": return liftEllipse(k, frame, basis, range);
5639
+ case "bezier": return liftBezier(k, Module, frame, range ? bezierSegmentPoles(basis.poles, range[0], range[1]) : basis.poles);
5640
+ case "bspline": {
5641
+ const piece = range ? trimmedPoleCurve(basis, range[0], range[1]) : basis;
5642
+ return piece && liftBSpline(k, Module, frame, piece) || liftFitted(k, Module, frame, cu);
5643
+ }
5644
+ case "trimmed": return liftFitted(k, Module, frame, cu);
5645
+ }
5646
+ }
4753
5647
  function buildEdgeOnSurface(k, Module, curve, surface) {
4754
5648
  const cu = c2d(curve);
4755
5649
  const bounds = curveBounds(cu);
@@ -5831,8 +6725,8 @@ var OcctWasmAdapter = class OcctWasmAdapter {
5831
6725
  makeBezier2d(points) {
5832
6726
  return makeBezier2d(points);
5833
6727
  }
5834
- makeBSpline2d(points, _options) {
5835
- return makeBSpline2d(points);
6728
+ makeBSpline2d(points, options) {
6729
+ return makeBSpline2d(points, options);
5836
6730
  }
5837
6731
  evaluateCurve2d(curve, param) {
5838
6732
  return evaluateCurve2d(curve, param);
@@ -5849,7 +6743,9 @@ var OcctWasmAdapter = class OcctWasmAdapter {
5849
6743
  trimCurve2d(curve, start, end) {
5850
6744
  return trimCurve2d(curve, start, end);
5851
6745
  }
5852
- reverseCurve2d(curve) {}
6746
+ reverseCurve2d(curve) {
6747
+ reverseCurve2d(curve);
6748
+ }
5853
6749
  copyCurve2d(curve) {
5854
6750
  return copyCurve2d(curve);
5855
6751
  }
@@ -5910,8 +6806,8 @@ var OcctWasmAdapter = class OcctWasmAdapter {
5910
6806
  distanceBetweenCurves2d(c1, c2, p1Start, p1End, p2Start, p2End) {
5911
6807
  return distanceBetweenCurves2d(c1, c2, p1Start, p1End, p2Start, p2End);
5912
6808
  }
5913
- approximateCurve2dAsBSpline(curve, _tolerance, _continuity, maxSegments) {
5914
- return approximateCurve2dAsBSpline(curve, maxSegments);
6809
+ approximateCurve2dAsBSpline(curve, tolerance, _continuity, maxSegments) {
6810
+ return approximateCurve2dAsBSpline(curve, tolerance, maxSegments);
5915
6811
  }
5916
6812
  decomposeBSpline2dToBeziers(curve) {
5917
6813
  return decomposeBSpline2dToBeziers(curve);
@@ -5983,4 +6879,4 @@ var OcctWasmAdapter = class OcctWasmAdapter {
5983
6879
  return getNurbsCurveData(this.k, edge);
5984
6880
  }
5985
6881
  };
5986
- export { DEFAULT_CAPABILITIES as A, scaleCurve2d$1 as C, quickHull as D, translateCurve2d$1 as E, OCCT_CAPABILITIES as M, UnsupportedKernelOperationError as O, rotateCurve2d$1 as S, tangentCurve2d as T, makeCircle2d$1 as _, buildAsciiSTL as a, mirrorAcrossAxis as b, createBBox2d as c, deserializeCurve2d$1 as d, evaluateCurve2d$1 as f, makeBezier2d$1 as g, makeArc2dThreePoints$1 as h, DEFAULT_STL_TOLERANCE as i, EXACT_BREP_CAPABILITIES as j, isUnsupportedKernelOperationError as k, curveBounds as l, makeArc2dTangent$1 as m, faultyEdgeIndexesOf as n, buildBinarySTL as o, intersectCurves2dFn as p, DEFAULT_STL_ANGULAR_TOLERANCE as r, addCurveToBBox as s, OcctWasmAdapter as t, curveTypeName as u, makeEllipse2d$1 as v, serializeCurve2d$1 as w, mirrorAtPoint as x, makeLine2d$1 as y };
6882
+ export { UnsupportedKernelOperationError as A, reverseCurve2d$1 as C, tangentCurve2d as D, serializeCurve2d$1 as E, DEFAULT_CAPABILITIES as M, EXACT_BREP_CAPABILITIES as N, translateCurve2d$1 as O, OCCT_CAPABILITIES as P, offsetCurve2d$1 as S, scaleCurve2d$1 as T, makeCircle2d$1 as _, buildAsciiSTL as a, mirrorAcrossAxis as b, createBBox2d as c, deserializeCurve2d$1 as d, evaluateCurve2d$1 as f, makeBezier2d$1 as g, makeArc2dThreePoints$1 as h, DEFAULT_STL_TOLERANCE as i, isUnsupportedKernelOperationError as j, quickHull as k, curveBounds as l, makeArc2dTangent$1 as m, faultyEdgeIndexesOf as n, buildBinarySTL as o, intersectCurves2dFn as p, DEFAULT_STL_ANGULAR_TOLERANCE as r, addCurveToBBox as s, OcctWasmAdapter as t, curveTypeName as u, makeEllipse2d$1 as v, rotateCurve2d$1 as w, mirrorAtPoint as x, makeLine2d$1 as y };