@design-core/core 0.0.5 → 0.0.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.
@@ -398,9 +398,10 @@ export class BasePolyline extends Entity {
398
398
  let distance = Infinity;
399
399
  let minPnt = P;
400
400
 
401
- for (let i = 1; i < this.points.length; i++) {
401
+ const segmentCount = this.flags.hasFlag(1) ? this.points.length : this.points.length - 1;
402
+ for (let i = 1; i <= segmentCount; i++) {
402
403
  const A = this.points[i - 1];
403
- const B = this.points[i];
404
+ const B = this.points[i % this.points.length];
404
405
 
405
406
  let pnt;
406
407
  if (A.bulge !== 0) {
@@ -293,8 +293,7 @@ export class InputManager {
293
293
  * Handle single selection
294
294
  */
295
295
  singleSelect() {
296
- this.inputPoint = DesignCore.Mouse.pointOnScene();
297
- this.onLeftClick(this.inputPoint);
296
+ this.onLeftClick(DesignCore.Mouse.pointOnScene());
298
297
  }
299
298
 
300
299
  /**
@@ -257,9 +257,9 @@ export class Fillet extends ChamferFilletBase {
257
257
  const isClosingWrap = (seg1 === closeSegIdx && seg2 === 1) || (seg2 === closeSegIdx && seg1 === 1);
258
258
  const isFirstLower = seg1 < seg2;
259
259
  const cornerIdx = isClosingWrap ? 0 : Math.min(seg1, seg2);
260
- const lowerTangent = (isClosingWrap ? seg1 === closeSegIdx : !isFirstLower) ? secondTangentPoint : firstTangentPoint;
261
- const upperTangent = (isClosingWrap ? seg1 === closeSegIdx : !isFirstLower) ? firstTangentPoint : secondTangentPoint;
262
- const polyArcDir = (isClosingWrap ? seg1 === closeSegIdx : !isFirstLower) ? -arcDirection : arcDirection;
260
+ const lowerTangent = (isClosingWrap ? seg1 !== closeSegIdx : !isFirstLower) ? secondTangentPoint : firstTangentPoint;
261
+ const upperTangent = (isClosingWrap ? seg1 !== closeSegIdx : !isFirstLower) ? firstTangentPoint : secondTangentPoint;
262
+ const polyArcDir = (isClosingWrap ? seg1 !== closeSegIdx : !isFirstLower) ? -arcDirection : arcDirection;
263
263
  const startAngle = arcCentre.angle(lowerTangent);
264
264
  const endAngle = arcCentre.angle(upperTangent);
265
265
  const includedAngle = ((endAngle - startAngle) * polyArcDir + 4 * Math.PI) % (2 * Math.PI);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design-core/core",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
@@ -626,3 +626,64 @@ test('BasePolyline.fromPolylinePoints round-trip strips closure point for closed
626
626
  expect(rebuilt.points[3].x).toBe(0);
627
627
  expect(rebuilt.points[3].y).toBe(10);
628
628
  });
629
+
630
+ // ─── closestPoint: closed polyline closing segment ───────────────────────────
631
+
632
+ test('BasePolyline.closestPoint finds closest point on closing segment of closed polyline', () => {
633
+ // Square: P0=(0,0), P1=(10,0), P2=(10,10), P3=(0,10), closed flag set.
634
+ // Closing segment is P3=(0,10) → P0=(0,0).
635
+ // A test point on the left edge (0, 5) must resolve to (0, 5) on the closing segment.
636
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
637
+ const poly = new BasePolyline({ points });
638
+ poly.flags.setFlagValue(1);
639
+
640
+ const [closest, dist] = poly.closestPoint(new Point(0, 5));
641
+ expect(closest.x).toBeCloseTo(0);
642
+ expect(closest.y).toBeCloseTo(5);
643
+ expect(dist).toBeCloseTo(0);
644
+ });
645
+
646
+ test('BasePolyline.closestPoint closing segment is preferred over other segments', () => {
647
+ // The test point (-1, 5) is clearly nearest the closing segment (left edge),
648
+ // not the bottom, right, or top edges.
649
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
650
+ const poly = new BasePolyline({ points });
651
+ poly.flags.setFlagValue(1);
652
+
653
+ const [closest, dist] = poly.closestPoint(new Point(-1, 5));
654
+ expect(closest.x).toBeCloseTo(0);
655
+ expect(closest.y).toBeCloseTo(5);
656
+ expect(dist).toBeCloseTo(1);
657
+ });
658
+
659
+ test('BasePolyline.closestPoint for open polyline does not include closing segment', () => {
660
+ // For point (-1, 1) on the open square:
661
+ // – Nearest on seg P0→P1 (y=0): foot (-1,0) clips to (0,0), dist = √2 ≈ 1.41
662
+ // – Nearest on seg P2→P3 (y=10): foot (-1,10) clips to (0,10), dist ≈ 9.06
663
+ // – If the closing segment existed (x=0, y∈[0,10]): foot (0,1), dist = 1 ← closer!
664
+ // An open polyline must NOT check the closing segment, so it returns (0,0).
665
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
666
+ const poly = new BasePolyline({ points });
667
+ // flag NOT set — open polyline
668
+
669
+ const [closest] = poly.closestPoint(new Point(-1, 1));
670
+ expect(closest.x).toBeCloseTo(0);
671
+ expect(closest.y).toBeCloseTo(0);
672
+ });
673
+
674
+ test('BasePolyline.closestPoint is consistent with getClosestSegmentIndex for closed polyline', () => {
675
+ // For a closed square, both methods should agree that the closing segment
676
+ // is nearest for a point on the left edge.
677
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
678
+ const poly = new BasePolyline({ points });
679
+ poly.flags.setFlagValue(1);
680
+
681
+ const testPoint = new Point(-2, 5);
682
+ const [closest] = poly.closestPoint(testPoint);
683
+ const segIdx = poly.getClosestSegmentIndex(testPoint);
684
+
685
+ // Closing segment index == points.length == 4
686
+ expect(segIdx).toBe(points.length);
687
+ expect(closest.x).toBeCloseTo(0);
688
+ expect(closest.y).toBeCloseTo(5);
689
+ });
@@ -829,3 +829,118 @@ test('Fillet.action radius>0 trimMode=false Line + Lwpolyline: standalone arc ad
829
829
  expect(core.scene.entities.get(0).points.length).toBe(2);
830
830
  expect(core.scene.entities.get(1).points.length).toBe(4);
831
831
  });
832
+
833
+ // ─── action: closed polyline closing-wrap arc direction (regression) ──────────
834
+ //
835
+ // Square: P0=(0,0), P1=(10,0), P2=(10,10), P3=(0,10), closed flag=1.
836
+ // Closing segment (idx=4): P3=(0,10)→P0=(0,0) — i.e. the left edge, travelling downward.
837
+ // Segment 1: P0=(0,0)→P1=(10,0) — i.e. the bottom edge, travelling rightward.
838
+ // The shared corner is P0=(0,0).
839
+ //
840
+ // With radius 2 the fillet must sit inside the square (bottom-left interior corner).
841
+ // The arc centre is at (2,2).
842
+ // – lowerTangent = (0,2) on the closing segment (left edge, y=2)
843
+ // – upperTangent = (2,0) on segment 1 (bottom edge, x=2)
844
+ // – bulge on lowerTangent must be POSITIVE (CCW arc from (0,2) to (2,0) through ≈(0.59,0.59))
845
+ // (BasePolyline convention: –ve bulge = CW, +ve bulge = CCW)
846
+ //
847
+ // Before the fix the lowerTangent/upperTangent assignment was inverted, producing
848
+ // an arc that traversed the exterior of the square.
849
+
850
+ test('Fillet.action closing-wrap: lowerTangent is on closing segment (left edge)', () => {
851
+ core.scene.clear();
852
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
853
+ const polyEntity = core.scene.entities.get(0);
854
+ polyEntity.flags.setFlagValue(1);
855
+
856
+ core.scene.headers.filletRadius = 2;
857
+ core.scene.headers.trimMode = true;
858
+
859
+ // firstPick = closing segment (idx=4, left edge), secondPick = segment 1 (bottom edge)
860
+ const fillet = new Fillet();
861
+ fillet.firstPick.entity = polyEntity;
862
+ fillet.secondPick.entity = polyEntity;
863
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
864
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
865
+ fillet.firstPick.clickPoint = new Point(0, 5);
866
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
867
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
868
+ fillet.secondPick.clickPoint = new Point(5, 0);
869
+ fillet.action();
870
+
871
+ expect(core.scene.entities.count()).toBe(1);
872
+ const poly = core.scene.entities.get(0);
873
+ expect(poly.points.length).toBe(5);
874
+
875
+ // After the splice at cornerIdx=0:
876
+ // points[0] = lowerTangent (arcStartPoint), points[1] = upperTangent
877
+ // lowerTangent must be on the closing segment (left edge, x≈0)
878
+ expect(poly.points[0].x).toBeCloseTo(0);
879
+ expect(poly.points[0].y).toBeCloseTo(2);
880
+ // upperTangent must be on segment 1 (bottom edge, y≈0)
881
+ expect(poly.points[1].x).toBeCloseTo(2);
882
+ expect(poly.points[1].y).toBeCloseTo(0);
883
+ });
884
+
885
+ test('Fillet.action closing-wrap: arc bulge sign is positive (CCW, fillet inside square)', () => {
886
+ // Same setup. The CCW arc from (0,2) to (2,0) sweeps through (≈0.59, ≈0.59)
887
+ // staying inside the square — a positive bulge value.
888
+ // Before the fix the lowerTangent/upperTangent were swapped, producing an arc
889
+ // that traversed the exterior of the square.
890
+ core.scene.clear();
891
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
892
+ const polyEntity = core.scene.entities.get(0);
893
+ polyEntity.flags.setFlagValue(1);
894
+
895
+ core.scene.headers.filletRadius = 2;
896
+ core.scene.headers.trimMode = true;
897
+
898
+ const fillet = new Fillet();
899
+ fillet.firstPick.entity = polyEntity;
900
+ fillet.secondPick.entity = polyEntity;
901
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
902
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
903
+ fillet.firstPick.clickPoint = new Point(0, 5);
904
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
905
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
906
+ fillet.secondPick.clickPoint = new Point(5, 0);
907
+ fillet.action();
908
+
909
+ const poly = core.scene.entities.get(0);
910
+ // points[0] carries the bulge for the arc from lowerTangent to upperTangent
911
+ expect(poly.points[0].bulge).toBeGreaterThan(0);
912
+ });
913
+
914
+ test('Fillet.action closing-wrap reversed pick order: arc bulge sign and tangent positions are correct', () => {
915
+ // Swap firstPick/secondPick — should produce the same geometry.
916
+ core.scene.clear();
917
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
918
+ const polyEntity = core.scene.entities.get(0);
919
+ polyEntity.flags.setFlagValue(1);
920
+
921
+ core.scene.headers.filletRadius = 2;
922
+ core.scene.headers.trimMode = true;
923
+
924
+ // firstPick = segment 1 (bottom edge), secondPick = closing segment (left edge, idx=4)
925
+ const fillet = new Fillet();
926
+ fillet.firstPick.entity = polyEntity;
927
+ fillet.secondPick.entity = polyEntity;
928
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
929
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
930
+ fillet.firstPick.clickPoint = new Point(5, 0);
931
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
932
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
933
+ fillet.secondPick.clickPoint = new Point(0, 5);
934
+ fillet.action();
935
+
936
+ expect(core.scene.entities.count()).toBe(1);
937
+ const poly = core.scene.entities.get(0);
938
+ expect(poly.points.length).toBe(5);
939
+
940
+ // Regardless of pick order: lowerTangent at (0,2), upperTangent at (2,0), bulge > 0 (CCW)
941
+ expect(poly.points[0].x).toBeCloseTo(0);
942
+ expect(poly.points[0].y).toBeCloseTo(2);
943
+ expect(poly.points[1].x).toBeCloseTo(2);
944
+ expect(poly.points[1].y).toBeCloseTo(0);
945
+ expect(poly.points[0].bulge).toBeGreaterThan(0);
946
+ });