@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
|
-
|
|
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) {
|
package/core/lib/inputManager.js
CHANGED
|
@@ -293,8 +293,7 @@ export class InputManager {
|
|
|
293
293
|
* Handle single selection
|
|
294
294
|
*/
|
|
295
295
|
singleSelect() {
|
|
296
|
-
this.
|
|
297
|
-
this.onLeftClick(this.inputPoint);
|
|
296
|
+
this.onLeftClick(DesignCore.Mouse.pointOnScene());
|
|
298
297
|
}
|
|
299
298
|
|
|
300
299
|
/**
|
package/core/tools/fillet.js
CHANGED
|
@@ -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
|
|
261
|
-
const upperTangent = (isClosingWrap ? seg1
|
|
262
|
-
const polyArcDir = (isClosingWrap ? seg1
|
|
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
|
@@ -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
|
+
});
|