@bitbybit-dev/base 0.20.2 → 0.20.3

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 (47) hide show
  1. package/LICENSE +1 -1
  2. package/lib/api/inputs/base-inputs.d.ts +8 -0
  3. package/lib/api/inputs/index.d.ts +3 -0
  4. package/lib/api/inputs/index.js +3 -0
  5. package/lib/api/inputs/inputs.d.ts +3 -0
  6. package/lib/api/inputs/inputs.js +3 -0
  7. package/lib/api/inputs/line-inputs.d.ts +240 -0
  8. package/lib/api/inputs/line-inputs.js +247 -0
  9. package/lib/api/inputs/mesh-inputs.d.ts +82 -0
  10. package/lib/api/inputs/mesh-inputs.js +83 -0
  11. package/lib/api/inputs/point-inputs.d.ts +153 -0
  12. package/lib/api/inputs/point-inputs.js +188 -0
  13. package/lib/api/inputs/polyline-inputs.d.ts +206 -0
  14. package/lib/api/inputs/polyline-inputs.js +229 -0
  15. package/lib/api/inputs/text-inputs.d.ts +1 -1
  16. package/lib/api/inputs/transforms-inputs.d.ts +18 -0
  17. package/lib/api/inputs/transforms-inputs.js +29 -0
  18. package/lib/api/inputs/vector-inputs.d.ts +8 -0
  19. package/lib/api/inputs/vector-inputs.js +8 -0
  20. package/lib/api/models/index.d.ts +1 -0
  21. package/lib/api/models/index.js +1 -0
  22. package/lib/api/models/point/bucket.d.ts +1 -0
  23. package/lib/api/models/point/bucket.js +1 -0
  24. package/lib/api/models/point/hex-grid-data.d.ts +8 -0
  25. package/lib/api/models/point/hex-grid-data.js +2 -0
  26. package/lib/api/models/point/index.d.ts +1 -0
  27. package/lib/api/models/point/index.js +1 -0
  28. package/lib/api/services/dates.js +45 -15
  29. package/lib/api/services/index.d.ts +3 -0
  30. package/lib/api/services/index.js +3 -0
  31. package/lib/api/services/line.d.ts +149 -0
  32. package/lib/api/services/line.js +320 -0
  33. package/lib/api/services/lists.d.ts +1 -1
  34. package/lib/api/services/lists.js +1 -2
  35. package/lib/api/services/mesh.d.ts +66 -0
  36. package/lib/api/services/mesh.js +235 -0
  37. package/lib/api/services/point.d.ts +96 -1
  38. package/lib/api/services/point.js +540 -1
  39. package/lib/api/services/polyline.d.ts +149 -0
  40. package/lib/api/services/polyline.js +444 -0
  41. package/lib/api/services/transforms.d.ts +26 -1
  42. package/lib/api/services/transforms.js +66 -3
  43. package/lib/api/services/vector.d.ts +18 -0
  44. package/lib/api/services/vector.js +27 -0
  45. package/lib/api/unit-test-helper.d.ts +20 -0
  46. package/lib/api/unit-test-helper.js +130 -0
  47. package/package.json +2 -2
@@ -4,10 +4,11 @@
4
4
  * When creating 2D points, z coordinate is simply set to 0 - [x, y, 0].
5
5
  */
6
6
  export class Point {
7
- constructor(geometryHelper, transforms, vector) {
7
+ constructor(geometryHelper, transforms, vector, lists) {
8
8
  this.geometryHelper = geometryHelper;
9
9
  this.transforms = transforms;
10
10
  this.vector = vector;
11
+ this.lists = lists;
11
12
  }
12
13
  /**
13
14
  * Transforms the single point
@@ -103,6 +104,18 @@ export class Point {
103
104
  const scaleTransforms = this.transforms.scaleCenterXYZ({ center: inputs.center, scaleXyz: inputs.scaleXyz });
104
105
  return this.geometryHelper.transformControlPoints(scaleTransforms, inputs.points);
105
106
  }
107
+ /**
108
+ * Stretch multiple points by providing center point, direction and uniform scale factor
109
+ * @param inputs Contains points, center point, direction and scale factor
110
+ * @returns Stretched points
111
+ * @group transforms
112
+ * @shortname stretch points dir from center
113
+ * @drawable true
114
+ */
115
+ stretchPointsDirFromCenter(inputs) {
116
+ const stretchTransforms = this.transforms.stretchDirFromCenter({ center: inputs.center, scale: inputs.scale, direction: inputs.direction });
117
+ return this.geometryHelper.transformControlPoints(stretchTransforms, inputs.points);
118
+ }
106
119
  /**
107
120
  * Rotate multiple points by providing center point, axis and degrees of rotation
108
121
  * @param inputs Contains points, axis, center point and angle of rotation
@@ -359,6 +372,479 @@ export class Point {
359
372
  }
360
373
  return points;
361
374
  }
375
+ /**
376
+ * Creates a pointy-top or flat-top hexagon grid, scaling hexagons to fit specified dimensions exactly.
377
+ * Returns both center points and the vertices of each (potentially scaled) hexagon.
378
+ * Hexagons are ordered column-first, then row-first.
379
+ * @param inputs Information about the desired grid dimensions and hexagon counts.
380
+ * @returns An object containing the array of center points and an array of hexagon vertex arrays.
381
+ * @group create
382
+ * @shortname scaled hex grid to fit
383
+ * @drawable false
384
+ */
385
+ hexGridScaledToFit(inputs) {
386
+ var _a, _b, _c, _d;
387
+ let width = inputs.width;
388
+ let height = inputs.height;
389
+ let nrHexagonsInHeight = inputs.nrHexagonsInHeight;
390
+ let nrHexagonsInWidth = inputs.nrHexagonsInWidth;
391
+ let extendTop = (_a = inputs.extendTop) !== null && _a !== void 0 ? _a : false;
392
+ let extendBottom = (_b = inputs.extendBottom) !== null && _b !== void 0 ? _b : false;
393
+ let extendLeft = (_c = inputs.extendLeft) !== null && _c !== void 0 ? _c : false;
394
+ let extendRight = (_d = inputs.extendRight) !== null && _d !== void 0 ? _d : false;
395
+ const { flatTop = false, centerGrid = false, pointsOnGround = false } = inputs;
396
+ // we flip the width and height if the hexagons are flat-topped and will then rotate resuls afterwards as default
397
+ // computes pointy-top hexagons
398
+ if (flatTop) {
399
+ const oldWidth = width;
400
+ width = inputs.height;
401
+ height = oldWidth;
402
+ const oldNrHexagonsInWidth = nrHexagonsInWidth;
403
+ nrHexagonsInWidth = nrHexagonsInHeight;
404
+ nrHexagonsInHeight = oldNrHexagonsInWidth;
405
+ const extendTopOld = extendTop;
406
+ const extendBottomOld = extendBottom;
407
+ const extendLeftOld = extendLeft;
408
+ const extendRightOld = extendRight;
409
+ extendTop = extendLeftOld;
410
+ extendBottom = extendRightOld;
411
+ extendLeft = extendBottomOld;
412
+ extendRight = extendTopOld;
413
+ }
414
+ // --- Input Validation ---
415
+ if (width <= 0 || height <= 0 || nrHexagonsInWidth < 1 || nrHexagonsInHeight < 1) {
416
+ console.warn("Hex grid dimensions and counts must be positive.");
417
+ return { centers: [], hexagons: [], shortestDistEdge: undefined, longestDistEdge: undefined, maxFilletRadius: undefined };
418
+ }
419
+ // --- Generate Unscaled Regular Grid Centers (Radius = 1) ---
420
+ // Use the *existing* hexGrid function, ensuring it doesn't center or project yet.
421
+ const BASE_RADIUS = 1.0;
422
+ const unscaledCenters = this.hexGrid({
423
+ radiusHexagon: BASE_RADIUS,
424
+ nrHexagonsX: nrHexagonsInWidth,
425
+ nrHexagonsY: nrHexagonsInHeight,
426
+ orientOnCenter: false,
427
+ pointsOnGround: false // Keep on XY plane for now
428
+ });
429
+ if (unscaledCenters.length === 0) {
430
+ return { centers: [], hexagons: [], shortestDistEdge: undefined, longestDistEdge: undefined, maxFilletRadius: undefined }; // Return empty if base grid failed
431
+ }
432
+ // --- Generate Unscaled Regular Hexagon Vertices (Radius = 1) ---
433
+ const unscaledHexagons = unscaledCenters.map(center => this.getRegularHexagonVertices(center, BASE_RADIUS));
434
+ // --- Determine Dimensions of the Unscaled Grid Bounding Box ---
435
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
436
+ for (const hex of unscaledHexagons) {
437
+ for (const vertex of hex) {
438
+ if (vertex[0] < minX)
439
+ minX = vertex[0];
440
+ if (vertex[0] > maxX)
441
+ maxX = vertex[0];
442
+ if (vertex[1] < minY)
443
+ minY = vertex[1];
444
+ if (vertex[1] > maxY)
445
+ maxY = vertex[1];
446
+ }
447
+ }
448
+ const unscaledWidth = maxX - minX;
449
+ const unscaledHeight = maxY - minY;
450
+ // --- Step 4: Calculate Scaling Factors ---
451
+ // Handle potential zero dimensions if only 1 hex (W/H would be based on hex size)
452
+ const scaleX = (unscaledWidth > 1e-9) ? width / unscaledWidth : 1;
453
+ const scaleY = (unscaledHeight > 1e-9) ? height / unscaledHeight : 1;
454
+ // If unscaled W/H is 0 (e.g., 1x1 grid), scale=1 means the final hex will have
455
+ // width/height derived from its regular R=1 shape, not fitting totalW/H.
456
+ // This might need adjustment if a single hex *must* fill the total W/H.
457
+ // For now, assume nrU/nrV > 1 or accept R=1 size for single hex.
458
+ // --- Scale Centers and Vertices ---
459
+ // Scale relative to the min corner of the unscaled grid (minX, minY)
460
+ let scaledCenters = unscaledCenters.map(p => [
461
+ (p[0] - minX) * scaleX,
462
+ (p[1] - minY) * scaleY,
463
+ 0 // Keep Z=0 for now
464
+ ]);
465
+ let scaledHexagons = unscaledHexagons.map(hex => hex.map(v => [
466
+ (v[0] - minX) * scaleX,
467
+ (v[1] - minY) * scaleY,
468
+ 0 // Keep Z=0 for now
469
+ ]));
470
+ let shortestDistEdge = Infinity;
471
+ let longestDistEdge = -Infinity;
472
+ let maxFilletRadius = 0;
473
+ // --- Calculate Shortes/Longest & Extensions ---
474
+ if (scaledHexagons.length !== 0) {
475
+ const firstHex = scaledHexagons[0];
476
+ maxFilletRadius = this.safestPointsMaxFilletHalfLine({
477
+ points: firstHex,
478
+ checkLastWithFirst: true,
479
+ tolerance: 1e-7
480
+ });
481
+ // Calculate the shortest and longest edge distances
482
+ firstHex.forEach((pt, index) => {
483
+ const nextPt = firstHex[(index + 1) % firstHex.length];
484
+ const dist = this.distance({ startPoint: pt, endPoint: nextPt });
485
+ if (dist < shortestDistEdge) {
486
+ shortestDistEdge = dist;
487
+ }
488
+ if (dist > longestDistEdge) {
489
+ longestDistEdge = dist;
490
+ }
491
+ });
492
+ if (extendTop || extendBottom || extendLeft || extendRight) {
493
+ const pt1Pointy = firstHex[0];
494
+ const pt2Pointy = firstHex[1];
495
+ const cellHeight = pt1Pointy[1] - pt2Pointy[1];
496
+ const cellWidth = pt2Pointy[0] - pt1Pointy[0];
497
+ if (extendTop && !extendBottom) {
498
+ const transform = {
499
+ center: [0, 0, 0],
500
+ direction: [0, 1, 0],
501
+ scale: height / (height - cellHeight),
502
+ };
503
+ scaledHexagons = scaledHexagons.map(hex => {
504
+ transform.points = hex;
505
+ return this.stretchPointsDirFromCenter(transform);
506
+ });
507
+ transform.points = scaledCenters;
508
+ scaledCenters = this.stretchPointsDirFromCenter(transform);
509
+ }
510
+ if (extendBottom && !extendTop) {
511
+ const transform = {
512
+ center: [0, height, 0],
513
+ direction: [0, -1, 0],
514
+ scale: height / (height - cellHeight),
515
+ };
516
+ scaledHexagons = scaledHexagons.map(hex => {
517
+ transform.points = hex;
518
+ return this.stretchPointsDirFromCenter(transform);
519
+ });
520
+ transform.points = scaledCenters;
521
+ scaledCenters = this.stretchPointsDirFromCenter(transform);
522
+ }
523
+ if (extendTop && extendBottom) {
524
+ const transform = {
525
+ center: [0, height / 2, 0],
526
+ direction: [0, 1, 0],
527
+ scale: height / (height - cellHeight * 2),
528
+ };
529
+ scaledHexagons = scaledHexagons.map(hex => {
530
+ transform.points = hex;
531
+ return this.stretchPointsDirFromCenter(transform);
532
+ });
533
+ transform.points = scaledCenters;
534
+ scaledCenters = this.stretchPointsDirFromCenter(transform);
535
+ }
536
+ if (extendLeft && !extendRight) {
537
+ const transform = {
538
+ center: [width, 0, 0],
539
+ direction: [1, 0, 0],
540
+ scale: width / (width - cellWidth),
541
+ };
542
+ scaledHexagons = scaledHexagons.map(hex => {
543
+ transform.points = hex;
544
+ return this.stretchPointsDirFromCenter(transform);
545
+ });
546
+ transform.points = scaledCenters;
547
+ scaledCenters = this.stretchPointsDirFromCenter(transform);
548
+ }
549
+ if (extendRight && !extendLeft) {
550
+ const transform = {
551
+ center: [0, 0, 0],
552
+ direction: [1, 0, 0],
553
+ scale: width / (width - cellWidth),
554
+ };
555
+ scaledHexagons = scaledHexagons.map(hex => {
556
+ transform.points = hex;
557
+ return this.stretchPointsDirFromCenter(transform);
558
+ });
559
+ transform.points = scaledCenters;
560
+ scaledCenters = this.stretchPointsDirFromCenter(transform);
561
+ }
562
+ if (extendLeft && extendRight) {
563
+ const transform = {
564
+ center: [width / 2, 0, 0],
565
+ direction: [1, 0, 0],
566
+ scale: width / (width - cellWidth * 2),
567
+ };
568
+ scaledHexagons = scaledHexagons.map(hex => {
569
+ transform.points = hex;
570
+ return this.stretchPointsDirFromCenter(transform);
571
+ });
572
+ transform.points = scaledCenters;
573
+ scaledCenters = this.stretchPointsDirFromCenter(transform);
574
+ }
575
+ }
576
+ }
577
+ if (flatTop) {
578
+ // width and height are swapped
579
+ scaledCenters = this.rotatePointsCenterAxis({
580
+ points: scaledCenters,
581
+ center: [width / 2, height / 2, 0],
582
+ axis: [0, 0, 1],
583
+ angle: 90
584
+ });
585
+ scaledHexagons = scaledHexagons.map(hex => {
586
+ return this.rotatePointsCenterAxis({
587
+ points: hex,
588
+ center: [width / 2, height / 2, 0],
589
+ axis: [0, 0, 1],
590
+ angle: 90
591
+ });
592
+ });
593
+ // translate to new center
594
+ const vecTranslation = this.vector.sub({
595
+ first: [height / 2, width / 2, 0],
596
+ second: [width / 2, height / 2, 0]
597
+ });
598
+ scaledCenters = this.translatePoints({
599
+ points: scaledCenters,
600
+ translation: vecTranslation
601
+ });
602
+ scaledHexagons = scaledHexagons.map(hex => {
603
+ return this.translatePoints({
604
+ points: hex,
605
+ translation: vecTranslation
606
+ });
607
+ });
608
+ }
609
+ // --- Apply Optional Centering ---
610
+ // Center the scaled grid (currently starting at [0,0]) around [0,0]
611
+ if (centerGrid) {
612
+ let shiftX = width / 2;
613
+ let shiftY = height / 2;
614
+ if (flatTop) {
615
+ shiftX = height / 2;
616
+ shiftY = width / 2;
617
+ }
618
+ for (let i = 0; i < scaledCenters.length; i++) {
619
+ scaledCenters[i][0] -= shiftX;
620
+ scaledCenters[i][1] -= shiftY;
621
+ }
622
+ for (let i = 0; i < scaledHexagons.length; i++) {
623
+ for (let j = 0; j < scaledHexagons[i].length; j++) {
624
+ scaledHexagons[i][j][0] -= shiftX;
625
+ scaledHexagons[i][j][1] -= shiftY;
626
+ }
627
+ }
628
+ }
629
+ // --- Apply Optional Ground Projection ---
630
+ if (pointsOnGround) {
631
+ for (let i = 0; i < scaledCenters.length; i++) {
632
+ scaledCenters[i] = [scaledCenters[i][0], 0, scaledCenters[i][1]];
633
+ }
634
+ for (let i = 0; i < scaledHexagons.length; i++) {
635
+ for (let j = 0; j < scaledHexagons[i].length; j++) {
636
+ scaledHexagons[i][j] = [scaledHexagons[i][j][0], 0, scaledHexagons[i][j][1]];
637
+ }
638
+ }
639
+ }
640
+ // We need to adjust orders to be column first and then row first if we choose flat top
641
+ if (flatTop) {
642
+ const grouped = this.lists.groupNth({
643
+ list: scaledHexagons.reverse(),
644
+ nrElements: inputs.nrHexagonsInWidth,
645
+ keepRemainder: true,
646
+ });
647
+ const res = this.lists.flipLists({
648
+ list: grouped
649
+ });
650
+ res.forEach(s => s.reverse());
651
+ scaledHexagons = res.flat();
652
+ const groupedCenters = this.lists.groupNth({
653
+ list: scaledCenters.reverse(),
654
+ nrElements: inputs.nrHexagonsInWidth,
655
+ keepRemainder: true,
656
+ });
657
+ const resCenters = this.lists.flipLists({
658
+ list: groupedCenters
659
+ });
660
+ resCenters.forEach(s => s.reverse());
661
+ scaledCenters = resCenters.flat();
662
+ }
663
+ // --- Return Result ---
664
+ return {
665
+ centers: scaledCenters,
666
+ hexagons: scaledHexagons,
667
+ shortestDistEdge,
668
+ longestDistEdge,
669
+ maxFilletRadius
670
+ };
671
+ }
672
+ /**
673
+ * Calculates the maximum possible fillet radius at a corner formed by two line segments
674
+ * sharing an endpoint (C), such that the fillet arc is tangent to both segments
675
+ * and lies entirely within them.
676
+ * @param inputs three points and the tolerance
677
+ * @returns the maximum fillet radius
678
+ * @group fillet
679
+ * @shortname max fillet radius
680
+ * @drawable false
681
+ */
682
+ maxFilletRadius(inputs) {
683
+ const { start: p1, center: p2, end: c, tolerance = 1e-7 } = inputs;
684
+ const v1 = this.vector.sub({ first: p1, second: c });
685
+ const v2 = this.vector.sub({ first: p2, second: c });
686
+ const len1 = this.vector.length({ vector: v1 });
687
+ const len2 = this.vector.length({ vector: v2 });
688
+ if (len1 < tolerance || len2 < tolerance) {
689
+ return 0;
690
+ }
691
+ const normV1 = this.vector.normalized({ vector: v1 });
692
+ const normV2 = this.vector.normalized({ vector: v2 });
693
+ if (!normV1 || !normV2) {
694
+ return 0;
695
+ }
696
+ // Calculate the cosine of the angle between the vectors
697
+ // Clamp to [-1, 1] to avoid potential domain errors with acos due to floating point inaccuracies
698
+ const cosAlpha = Math.max(-1.0, Math.min(1.0, this.vector.dot({ first: normV1, second: normV2 })));
699
+ // Check for collinearity
700
+ // If vectors point in the same direction (angle ~ 0), no fillet
701
+ if (cosAlpha > 1.0 - tolerance) {
702
+ return 0;
703
+ }
704
+ // If vectors point in opposite directions (angle ~ 180 deg), no corner for a fillet
705
+ if (cosAlpha < -1.0 + tolerance) {
706
+ return 0;
707
+ }
708
+ // Calculate the angle alpha (0 < alpha < PI)
709
+ const alpha = Math.acos(cosAlpha);
710
+ // Calculate tan(alpha / 2)
711
+ // alpha/2 is between 0 and PI/2, so tan is positive and non-zero
712
+ const tanHalfAlpha = Math.tan(alpha / 2.0);
713
+ // If tanHalfAlpha is extremely small (alpha near 0, shouldn't happen due to collinearity check), return 0
714
+ if (tanHalfAlpha < tolerance) {
715
+ return 0;
716
+ }
717
+ // The distance 'd' from corner C to the tangent point must be less than or equal to the segment lengths.
718
+ // d = r / tan(alpha/2) <= min(len1, len2)
719
+ // r <= min(len1, len2) * tan(alpha/2)
720
+ const maxRadius = Math.min(len1, len2) * tanHalfAlpha;
721
+ return maxRadius;
722
+ }
723
+ /**
724
+ * Calculates the maximum possible fillet radius at a corner C, such that the fillet arc
725
+ * is tangent to both segments (P1-C, P2-C) and the tangent points lie within
726
+ * the first half of each segment (measured from C).
727
+ * @param inputs three points and the tolerance
728
+ * @returns the maximum fillet radius
729
+ * @group fillet
730
+ * @shortname max fillet radius half line
731
+ * @drawable false
732
+ */
733
+ maxFilletRadiusHalfLine(inputs) {
734
+ const { start: p1, center: p2, end: c, tolerance = 1e-7 } = inputs;
735
+ const v1 = this.vector.sub({ first: p1, second: c });
736
+ const v2 = this.vector.sub({ first: p2, second: c });
737
+ const len1 = this.vector.length({ vector: v1 });
738
+ const len2 = this.vector.length({ vector: v2 });
739
+ if (len1 < tolerance || len2 < tolerance) {
740
+ return 0;
741
+ }
742
+ const normV1 = this.vector.normalized({ vector: v1 });
743
+ const normV2 = this.vector.normalized({ vector: v2 });
744
+ if (!normV1 || !normV2) {
745
+ return 0;
746
+ }
747
+ const cosAlpha = Math.max(-1.0, Math.min(1.0, this.vector.dot({ first: normV1, second: normV2 })));
748
+ if (cosAlpha > 1.0 - tolerance || cosAlpha < -1.0 + tolerance) {
749
+ return 0; // Collinear
750
+ }
751
+ const alpha = Math.acos(cosAlpha);
752
+ const tanHalfAlpha = Math.tan(alpha / 2.0);
753
+ if (tanHalfAlpha < tolerance) {
754
+ return 0;
755
+ }
756
+ // The distance 'd' from corner C to the tangent point must be less than or equal
757
+ // to HALF the length of each segment.
758
+ // d = r / tan(alpha/2) <= min(len1 / 2, len2 / 2)
759
+ // r <= min(len1 / 2, len2 / 2) * tan(alpha/2)
760
+ const maxRadius = Math.min(len1 / 2.0, len2 / 2.0) * tanHalfAlpha;
761
+ return maxRadius;
762
+ }
763
+ /**
764
+ * Calculates the maximum possible fillet radius at each corner of a polyline formed by
765
+ * formed by a series of points. The fillet radius is calculated for each internal
766
+ * corner and optionally for the closing corners if the polyline is closed.
767
+ * @param inputs Points, checkLastWithFirst flag, and tolerance
768
+ * @returns Array of maximum fillet radii for each corner
769
+ * @group fillet
770
+ * @shortname max fillets half line
771
+ * @drawable false
772
+ */
773
+ maxFilletsHalfLine(inputs) {
774
+ const { points, checkLastWithFirst = false, tolerance = 1e-7 } = inputs;
775
+ const n = points.length;
776
+ const results = [];
777
+ // Need at least 3 points to form a corner
778
+ if (n < 3) {
779
+ return results;
780
+ }
781
+ // 1. Calculate fillets for internal corners (P[1] to P[n-2])
782
+ for (let i = 1; i < n - 1; i++) {
783
+ const p_prev = points[i - 1];
784
+ const p_corner = points[i];
785
+ const p_next = points[i + 1];
786
+ // Map geometric points to the DTO structure used by calculateMaxFilletRadiusHalfLine
787
+ // DTO: { start: P_prev, center: P_next, end: P_corner, tolerance }
788
+ const cornerInput = {
789
+ start: p_prev,
790
+ center: p_next,
791
+ end: p_corner,
792
+ tolerance: tolerance
793
+ };
794
+ results.push(this.maxFilletRadiusHalfLine(cornerInput));
795
+ }
796
+ // 2. Calculate fillets for closing corners if it's a closed polyline
797
+ if (checkLastWithFirst && n >= 3) {
798
+ // Corner at P[0] (formed by P[n-1]-P[0] and P[1]-P[0])
799
+ const p_prev_start = points[n - 1]; // Previous point is the last point
800
+ const p_corner_start = points[0];
801
+ const p_next_start = points[1];
802
+ const startCornerInput = {
803
+ start: p_prev_start,
804
+ center: p_next_start,
805
+ end: p_corner_start,
806
+ tolerance: tolerance
807
+ };
808
+ results.push(this.maxFilletRadiusHalfLine(startCornerInput));
809
+ // Corner at P[n-1] (formed by P[n-2]-P[n-1] and P[0]-P[n-1])
810
+ const p_prev_end = points[n - 2];
811
+ const p_corner_end = points[n - 1];
812
+ const p_next_end = points[0]; // Next point wraps around to the first point
813
+ const endCornerInput = {
814
+ start: p_prev_end,
815
+ center: p_next_end,
816
+ end: p_corner_end,
817
+ tolerance: tolerance
818
+ };
819
+ results.push(this.maxFilletRadiusHalfLine(endCornerInput));
820
+ }
821
+ return results;
822
+ }
823
+ /**
824
+ * Calculates the single safest maximum fillet radius that can be applied
825
+ * uniformly to all corners of collection of points, based on the 'half-line' constraint.
826
+ * This is determined by finding the minimum of the maximum possible fillet
827
+ * radii calculated for each individual corner.
828
+ * @param inputs Defines the points, whether it's closed, and an optional tolerance.
829
+ * @returns The smallest value from the results of pointsMaxFilletsHalfLine.
830
+ * Returns 0 if the polyline has fewer than 3 points or if any
831
+ * calculated maximum radius is 0.
832
+ * @group fillet
833
+ * @shortname safest fillet radii points
834
+ * @drawable false
835
+ */
836
+ safestPointsMaxFilletHalfLine(inputs) {
837
+ const allMaxRadii = this.maxFilletsHalfLine(inputs);
838
+ if (allMaxRadii.length === 0) {
839
+ // No corners, or fewer than 3 points. No fillet possible.
840
+ return 0;
841
+ }
842
+ // Find the minimum radius among all calculated maximums.
843
+ // If any corner calculation resulted in 0, the safest radius is 0.
844
+ const safestRadius = Math.min(...allMaxRadii);
845
+ // Ensure we don't return a negative radius if Math.min had weird input (shouldn't happen here)
846
+ return Math.max(0, safestRadius);
847
+ }
362
848
  /**
363
849
  * Removes consecutive duplicates from the point array with tolerance
364
850
  * @param inputs points, tolerance and check first and last
@@ -426,4 +912,57 @@ export class Point {
426
912
  }
427
913
  return { index: closestPointIndex + 1, distance, point };
428
914
  }
915
+ /**
916
+ * Checks if two points are almost equal
917
+ * @param inputs Two points and the tolerance
918
+ * @returns true if the points are almost equal
919
+ * @group measure
920
+ * @shortname two points almost equal
921
+ * @drawable false
922
+ */
923
+ twoPointsAlmostEqual(inputs) {
924
+ const p1 = inputs.point1;
925
+ const p2 = inputs.point2;
926
+ const dist = this.distance({ startPoint: p1, endPoint: p2 });
927
+ return dist < inputs.tolerance;
928
+ }
929
+ /**
930
+ * Sorts points lexicographically (X, then Y, then Z)
931
+ * @param inputs points
932
+ * @returns sorted points
933
+ * @group sort
934
+ * @shortname sort points
935
+ * @drawable true
936
+ */
937
+ sortPoints(inputs) {
938
+ return [...inputs.points].sort((a, b) => {
939
+ if (a[0] !== b[0])
940
+ return a[0] - b[0];
941
+ if (a[1] !== b[1])
942
+ return a[1] - b[1];
943
+ return a[2] - b[2];
944
+ });
945
+ }
946
+ /**
947
+ * Calculates the 6 vertices of a regular flat-top hexagon.
948
+ * @param center The center point [x, y, z].
949
+ * @param radius The radius (distance from center to vertex).
950
+ * @returns An array of 6 Point3 vertices in counter-clockwise order.
951
+ */
952
+ getRegularHexagonVertices(center, radius) {
953
+ const vertices = [];
954
+ const cx = center[0];
955
+ const cy = center[1];
956
+ const cz = center[2];
957
+ const angleStep = Math.PI / 3;
958
+ for (let i = 0; i < 6; i++) {
959
+ const angle = angleStep * i;
960
+ vertices.push([
961
+ cx + radius * Math.sin(angle),
962
+ cy + radius * Math.cos(angle),
963
+ cz // Maintain original Z
964
+ ]);
965
+ }
966
+ return vertices;
967
+ }
429
968
  }