@design-core/core 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.eslintrc.json +4 -0
  2. package/TODO.md +49 -6
  3. package/core/core/core.js +0 -3
  4. package/core/dimensions/alignedDimension.js +5 -4
  5. package/core/dimensions/angularDimension.js +6 -5
  6. package/core/dimensions/baseCircularDimension.js +3 -2
  7. package/core/dimensions/baseDimension.js +59 -93
  8. package/core/dimensions/baseLinearDimension.js +6 -5
  9. package/core/dimensions/diametricDimension.js +4 -3
  10. package/core/dimensions/dimension.js +21 -14
  11. package/core/dimensions/radialDimension.js +4 -3
  12. package/core/dimensions/rotatedDimension.js +12 -11
  13. package/core/entities/arc.js +112 -145
  14. package/core/entities/arctext.js +212 -174
  15. package/core/entities/basePolyline.js +33 -284
  16. package/core/entities/circle.js +77 -99
  17. package/core/entities/entity.js +114 -93
  18. package/core/entities/hatch.js +322 -249
  19. package/core/entities/insert.js +66 -81
  20. package/core/entities/line.js +21 -142
  21. package/core/entities/lwpolyline.js +16 -11
  22. package/core/entities/polyline.js +4 -4
  23. package/core/entities/rectangle.js +3 -1
  24. package/core/entities/solid.js +6 -15
  25. package/core/entities/text.js +188 -199
  26. package/core/lib/auxiliary/rubberBand.js +32 -0
  27. package/core/lib/auxiliary/selectionWindow.js +63 -0
  28. package/core/lib/auxiliary/snapPoint.js +113 -0
  29. package/core/lib/auxiliary/trackingLine.js +76 -0
  30. package/core/lib/canvas.js +228 -166
  31. package/core/lib/clipboard.js +7 -1
  32. package/core/lib/colour.js +16 -0
  33. package/core/lib/commandLine.js +1 -6
  34. package/core/lib/commandManager.js +18 -8
  35. package/core/lib/constants.js +28 -0
  36. package/core/lib/dxf/dxf.js +4 -3
  37. package/core/lib/entityManager.js +18 -11
  38. package/core/lib/inputManager.js +36 -13
  39. package/core/lib/intersect.js +25 -0
  40. package/core/lib/patterns.js +8 -1
  41. package/core/lib/plotOptions.js +67 -0
  42. package/core/lib/polylineUtils.js +75 -0
  43. package/core/lib/renderers/cairoRenderer.js +371 -0
  44. package/core/lib/renderers/canvasRenderer.js +315 -0
  45. package/core/lib/renderers/pdfRenderer.js +366 -0
  46. package/core/lib/renderers/rendererBase.js +284 -0
  47. package/core/lib/renderers/svgRenderer.js +343 -0
  48. package/core/lib/scene.js +27 -5
  49. package/core/lib/selectionManager.js +2 -2
  50. package/core/lib/settings.js +4 -2
  51. package/core/lib/snapping.js +4 -191
  52. package/core/lib/stateManager.js +30 -1
  53. package/core/lib/utils.js +21 -2
  54. package/core/properties/entityProperties.js +104 -0
  55. package/core/properties/property.js +106 -2
  56. package/core/properties/propertyManager.js +29 -13
  57. package/core/tables/block.js +4 -37
  58. package/core/tables/endblk.js +1 -1
  59. package/core/tables/layer.js +8 -0
  60. package/core/tables/styleManagerBase.js +3 -2
  61. package/core/tables/tableManagerBase.js +8 -3
  62. package/core/tools/chamfer.js +111 -137
  63. package/core/tools/chamferFilletBase.js +135 -34
  64. package/core/tools/copy.js +3 -3
  65. package/core/tools/explode.js +2 -2
  66. package/core/tools/extend.js +132 -19
  67. package/core/tools/fillet.js +86 -183
  68. package/core/tools/matchProp.js +2 -11
  69. package/core/tools/mirror.js +7 -6
  70. package/core/tools/move.js +3 -3
  71. package/core/tools/offset.js +3 -5
  72. package/core/tools/pasteclip.js +1 -1
  73. package/core/tools/rotate.js +6 -3
  74. package/core/tools/scale.js +1 -1
  75. package/core/tools/trim.js +265 -23
  76. package/core/tools/zoom.js +1 -1
  77. package/jest.config.js +6 -0
  78. package/package.json +5 -3
  79. package/test/dimensions/alignedDimension.test.js +14 -7
  80. package/test/dimensions/angularDimension.test.js +6 -6
  81. package/test/dimensions/baseDimension.test.js +69 -8
  82. package/test/dimensions/baseLinearDimension.test.js +62 -0
  83. package/test/dimensions/diametricDimension.test.js +6 -6
  84. package/test/dimensions/radialDimension.test.js +6 -6
  85. package/test/dimensions/rotatedDimension.test.js +14 -7
  86. package/test/entities/arc.test.js +80 -83
  87. package/test/entities/arctext.test.js +180 -53
  88. package/test/entities/basePolyline.test.js +129 -2
  89. package/test/entities/circle.test.js +57 -85
  90. package/test/entities/entity.test.js +42 -8
  91. package/test/entities/hatch.test.js +543 -70
  92. package/test/entities/insert.test.js +96 -1
  93. package/test/entities/line.test.js +15 -41
  94. package/test/entities/lwpolyline.test.js +104 -0
  95. package/test/entities/polyline.test.js +11 -0
  96. package/test/entities/rectangle.test.js +11 -0
  97. package/test/entities/solid.test.js +1 -1
  98. package/test/entities/text.test.js +294 -69
  99. package/test/integration/exportIntegration.reference.dxf +9 -9
  100. package/test/integration/exportIntegration.test.js +2 -2
  101. package/test/integration/intersectionIntegration.test.js +4 -5
  102. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  103. package/test/integration/pdfRendererIntegration.test.js +61 -0
  104. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  105. package/test/integration/svgRendererIntegration.test.js +61 -0
  106. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  107. package/test/integration/userInputIntegration.test.js +239 -132
  108. package/test/lib/canvas.test.js +742 -123
  109. package/test/lib/constants.test.js +27 -0
  110. package/test/lib/entityManager.test.js +4 -4
  111. package/test/lib/inputManager.test.js +80 -19
  112. package/test/lib/intersect.test.js +83 -0
  113. package/test/lib/plotOptions.test.js +179 -0
  114. package/test/lib/rubberBand.test.js +84 -0
  115. package/test/lib/settings.test.js +5 -6
  116. package/test/lib/snapping.test.js +71 -129
  117. package/test/lib/stateManager.test.js +7 -7
  118. package/test/lib/utils.test.js +36 -2
  119. package/test/properties/entityProperties.test.js +132 -0
  120. package/test/properties/property.test.js +53 -0
  121. package/test/properties/propertyManager.test.js +3 -3
  122. package/test/renderers/canvasRenderer.test.js +317 -0
  123. package/test/renderers/pdfRenderer.test.js +271 -0
  124. package/test/renderers/rendererBase.test.js +213 -0
  125. package/test/renderers/svgRenderer.test.js +326 -0
  126. package/test/tables/block.test.js +31 -1
  127. package/test/tables/endblk.test.js +2 -2
  128. package/test/tables/layer.test.js +30 -0
  129. package/test/test-helpers/hatchGridRenderer.js +180 -0
  130. package/test/test-helpers/test-helpers.js +152 -0
  131. package/test/tools/chamfer.test.js +227 -5
  132. package/test/tools/chamferFilletBase.test.js +98 -9
  133. package/test/tools/copy.test.js +2 -2
  134. package/test/tools/explode.test.js +3 -3
  135. package/test/tools/extend.test.js +331 -0
  136. package/test/tools/fillet.test.js +296 -0
  137. package/test/tools/matchProp.test.js +5 -5
  138. package/test/tools/mirror.test.js +13 -13
  139. package/test/tools/offset.test.js +170 -3
  140. package/test/tools/pasteclip.test.js +5 -5
  141. package/test/tools/rotate.test.js +32 -2
  142. package/test/tools/scale.test.js +5 -5
  143. package/test/tools/trim.test.js +526 -0
  144. package/test/tools/zoom.test.js +7 -7
  145. package/core/lib/selectionWindow.js +0 -83
@@ -438,7 +438,7 @@ test('Scale.preview - no points set, adds no temp entities', () => {
438
438
  // No points — guard condition prevents any work
439
439
  scale.preview();
440
440
 
441
- expect(core.scene.tempEntities.count()).toBe(0);
441
+ expect(core.scene.previewEntities.count()).toBe(0);
442
442
  });
443
443
 
444
444
  test('Scale.preview - referencePoint set, suppresses preview', () => {
@@ -454,7 +454,7 @@ test('Scale.preview - referencePoint set, suppresses preview', () => {
454
454
 
455
455
  scale.preview();
456
456
 
457
- expect(core.scene.tempEntities.count()).toBe(0);
457
+ expect(core.scene.previewEntities.count()).toBe(0);
458
458
  });
459
459
 
460
460
  test('Scale.preview - mouse on base point (distance 0), draws line but skips scaling', () => {
@@ -472,7 +472,7 @@ test('Scale.preview - mouse on base point (distance 0), draws line but skips sca
472
472
  scale.preview();
473
473
 
474
474
  // Temp line is still created
475
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
475
+ expect(core.scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
476
476
 
477
477
  // Selected item points should be unchanged (scaling skipped)
478
478
  const previewItem = core.scene.selectionManager.selectedItems[0];
@@ -494,7 +494,7 @@ test('Scale.preview - one point set, draws temp line and scales selected items',
494
494
  scale.preview();
495
495
 
496
496
  // Temp line should be created
497
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
497
+ expect(core.scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
498
498
 
499
499
  // Preview item should have been scaled (x differs from original 1.0)
500
500
  const previewItem = core.scene.selectionManager.selectedItems[0];
@@ -520,7 +520,7 @@ test('Scale.preview - reference mode uses referenceLength to compute factor', ()
520
520
  // before and after with a known referenceLength to confirm scaling occurred.
521
521
  scale.preview();
522
522
 
523
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
523
+ expect(core.scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
524
524
 
525
525
  // The preview item's first point should differ from the original (10,0)
526
526
  // because the factor (distance/10) is applied
@@ -3,6 +3,7 @@ import { Point } from '../../core/entities/point.js';
3
3
  import { Trim } from '../../core/tools/trim.js';
4
4
  import { SingleSelection } from '../../core/lib/selectionManager.js';
5
5
  import { Strings } from '../../core/lib/strings.js';
6
+ import { DesignCore } from '../../core/designCore.js';
6
7
  import { expect, jest } from '@jest/globals';
7
8
  import { withMockInput } from '../test-helpers/test-helpers.js';
8
9
 
@@ -431,6 +432,102 @@ test('Trim.preview does not throw', () => {
431
432
  expect(() => trim.preview()).not.toThrow();
432
433
  });
433
434
 
435
+ test('Trim.preview returns early when no boundary items set', () => {
436
+ core.scene.clear();
437
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] });
438
+ DesignCore.Scene.previewEntities.clear();
439
+
440
+ const trim = new Trim();
441
+ // selectedBoundaryItems is empty by default
442
+ trim.preview();
443
+
444
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
445
+ });
446
+
447
+ test('Trim.preview returns early when findClosestItem returns undefined', () => {
448
+ core.scene.clear();
449
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] });
450
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
451
+ DesignCore.Scene.previewEntities.clear();
452
+
453
+ const trim = new Trim();
454
+ trim.selectedBoundaryItems = [core.scene.entities.get(0)];
455
+
456
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
457
+ trim.preview();
458
+ findSpy.mockRestore();
459
+
460
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
461
+ });
462
+
463
+ test('Trim.preview returns early when hovered entity does not implement trim', () => {
464
+ // Text does not override Entity.trim() — preview must not emit a notification.
465
+ core.scene.clear();
466
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
467
+ core.scene.addItem('Text', { points: [new Point(5, 0)], string: 'hello' }); // unsupported
468
+ DesignCore.Scene.previewEntities.clear();
469
+
470
+ const notifySpy = jest.spyOn(core, 'notify');
471
+ const trim = new Trim();
472
+ trim.selectedBoundaryItems = [core.scene.entities.get(0)];
473
+
474
+ const savedPointOnScene1 = DesignCore.Mouse.pointOnScene;
475
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
476
+ DesignCore.Mouse.pointOnScene = () => new Point(5, 0);
477
+ trim.preview();
478
+ findSpy.mockRestore();
479
+ notifySpy.mockRestore();
480
+ DesignCore.Mouse.pointOnScene = savedPointOnScene1;
481
+
482
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
483
+ expect(notifySpy).not.toHaveBeenCalled();
484
+ });
485
+
486
+ test('Trim.preview returns early when hovered entity has no intersections with boundary', () => {
487
+ // Two parallel horizontal lines — no intersection with the vertical boundary
488
+ core.scene.clear();
489
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary (vertical)
490
+ core.scene.addItem('Line', { points: [new Point(-10, 5), new Point(10, 5)] }); // hovered (horizontal, no intersection with boundary)
491
+ DesignCore.Scene.previewEntities.clear();
492
+
493
+ // Boundary is entity 0; hovered is entity 1 — they are parallel on the y-axis, so no intersection
494
+ core.scene.addItem('Line', { points: [new Point(-10, 20), new Point(10, 20)] }); // extra line, no intersection
495
+
496
+ const trim = new Trim();
497
+ trim.selectedBoundaryItems = [core.scene.entities.get(2)]; // horizontal boundary — parallel to hovered
498
+
499
+ const savedPointOnScene2 = DesignCore.Mouse.pointOnScene;
500
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
501
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
502
+ trim.preview();
503
+ findSpy.mockRestore();
504
+ DesignCore.Mouse.pointOnScene = savedPointOnScene2;
505
+
506
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
507
+ });
508
+
509
+ test('Trim.preview populates previewEntities when entity can be trimmed', () => {
510
+ // Vertical boundary at x=0; horizontal line from (-10,0) to (10,0) is the hovered entity.
511
+ // Mouse at (5,0) → trims the right half, leaving the left half as a preview.
512
+ core.scene.clear();
513
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
514
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] }); // hovered
515
+ DesignCore.Scene.previewEntities.clear();
516
+
517
+ const trim = new Trim();
518
+ trim.selectedBoundaryItems = [core.scene.entities.get(0)];
519
+
520
+ const savedPointOnScene3 = DesignCore.Mouse.pointOnScene;
521
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
522
+ DesignCore.Mouse.pointOnScene = () => new Point(5, 0);
523
+ trim.preview();
524
+ findSpy.mockRestore();
525
+ DesignCore.Mouse.pointOnScene = savedPointOnScene3;
526
+
527
+ // dulled original + at least one trimmed survivor
528
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(2);
529
+ });
530
+
434
531
  test('Trim.execute returns early when boundary input is undefined', async () => {
435
532
  core.scene.clear();
436
533
  core.scene.selectionManager.reset();
@@ -503,6 +600,58 @@ test('Trim.action does nothing when selectedBoundaryItems is empty', () => {
503
600
  expect(trim.selectedItem).toBeNull();
504
601
  });
505
602
 
603
+ test('Trim.action notifies NOTRIM when selected entity lacks fromPolylinePoints', () => {
604
+ // Use a stub entity that has toPolylinePoints but no fromPolylinePoints.
605
+ // action() must notify and not throw.
606
+ core.scene.clear();
607
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
608
+
609
+ const trim = new Trim();
610
+ trim.selectedBoundaryItems = [core.scene.entities.get(0)];
611
+ trim.selectedItem = {
612
+ type: 'FakeEntity',
613
+ toPolylinePoints: () => [new Point(-5, 0), new Point(5, 0)],
614
+ // deliberately no fromPolylinePoints
615
+ };
616
+
617
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
618
+ trim.action();
619
+
620
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOTRIM));
621
+ expect(trim.selectedItem).toBeNull();
622
+ notifySpy.mockRestore();
623
+ });
624
+
625
+ test('Trim.preview silently skips entity that lacks fromPolylinePoints', () => {
626
+ // Stub entity implements toPolylinePoints but not fromPolylinePoints.
627
+ // preview() must return early without calling notify.
628
+ core.scene.clear();
629
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
630
+ DesignCore.Scene.previewEntities.clear();
631
+
632
+ const stubEntity = {
633
+ type: 'FakeEntity',
634
+ toPolylinePoints: () => [new Point(-5, 0), new Point(5, 0)],
635
+ // deliberately no fromPolylinePoints
636
+ };
637
+
638
+ const notifySpy = jest.spyOn(core, 'notify');
639
+ const trim = new Trim();
640
+ trim.selectedBoundaryItems = [core.scene.entities.get(0)];
641
+
642
+ const savedPointOnScene = DesignCore.Mouse.pointOnScene;
643
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
644
+ jest.spyOn(core.scene.entities, 'get').mockReturnValue(stubEntity);
645
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 0);
646
+ trim.preview();
647
+ findSpy.mockRestore();
648
+ jest.restoreAllMocks();
649
+ DesignCore.Mouse.pointOnScene = savedPointOnScene;
650
+
651
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
652
+ expect(notifySpy).not.toHaveBeenCalled();
653
+ });
654
+
506
655
  test('Trim.action notifies when no intersection found (parallel lines)', () => {
507
656
  core.scene.clear();
508
657
  // Two parallel vertical lines - no intersection
@@ -536,3 +685,380 @@ test('Trim.action notifies when boundary item equals selected item', () => {
536
685
  expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOTRIM));
537
686
  notifySpy.mockRestore();
538
687
  });
688
+
689
+ test('Trim.action trims an arc', () => {
690
+ // Arc: center (0,0), start (10,0), end (-10,0) — CCW upper semicircle through (0,10)
691
+ // Boundary: vertical line at x=0, intersects arc at (0,10)
692
+ // Mouse at (7,7) — in the right half, selects (10,0)→(0,10) segment for removal
693
+ // Expected: original arc removed, shorter arc from (0,10) to (-10,0) added
694
+
695
+ const trim = new Trim();
696
+ core.scene.clear();
697
+
698
+ core.scene.addItem('Arc', { points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)] });
699
+ core.scene.addItem('Line', { points: [new Point(0, -20), new Point(0, 20)] });
700
+
701
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
702
+ trim.selectedItem = core.scene.entities.get(0);
703
+ core.mouse.setPosFromScenePoint(new Point(7, 7));
704
+ trim.action();
705
+
706
+ expect(core.scene.entities.count()).toBe(2);
707
+
708
+ let arc;
709
+ for (let i = 0; i < core.scene.entities.count(); i++) {
710
+ if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
711
+ }
712
+ expect(arc).toBeDefined();
713
+ // Remaining arc runs from the intersection (0,10) to the original end (-10,0)
714
+ expect(arc.points[1].x).toBeCloseTo(0);
715
+ expect(arc.points[1].y).toBeCloseTo(10);
716
+ expect(arc.points[2].x).toBeCloseTo(-10);
717
+ expect(arc.points[2].y).toBeCloseTo(0);
718
+ });
719
+
720
+ test('Trim.action trims a circle', () => {
721
+ // Circle: center (0,0), radius 10
722
+ // Boundary: vertical line at x=0, intersects circle at (0,10) and (0,-10)
723
+ // Mouse at (8,0) — on the right side
724
+ // Expected: original circle removed, arc covering the right side added
725
+
726
+ const trim = new Trim();
727
+ core.scene.clear();
728
+
729
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
730
+ core.scene.addItem('Line', { points: [new Point(0, -20), new Point(0, 20)] });
731
+
732
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
733
+ trim.selectedItem = core.scene.entities.get(0);
734
+ core.mouse.setPosFromScenePoint(new Point(8, 0));
735
+ trim.action();
736
+
737
+ // circle removed, arc added, boundary line remains
738
+ expect(core.scene.entities.count()).toBe(2);
739
+
740
+ let arc;
741
+ for (let i = 0; i < core.scene.entities.count(); i++) {
742
+ if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
743
+ }
744
+ expect(arc).toBeDefined();
745
+ // Arc center should match the original circle center
746
+ expect(arc.points[0].x).toBeCloseTo(0, 10);
747
+ expect(arc.points[0].y).toBeCloseTo(0, 10);
748
+ });
749
+
750
+ test('Trim.action trims a closed polyline - produces open polyline with closed flag cleared', () => {
751
+ // Closed square: (0,0)→(100,0)→(100,100)→(0,100) with flag bit 1
752
+ // Boundary: vertical line at x=50
753
+ // Mouse at (75,50) — right of centre, selects the right side for removal
754
+ // Expected: one open polyline (50,100)→(0,100)→(0,0)→(50,0), flag bit 1 cleared
755
+
756
+ const trim = new Trim();
757
+ core.scene.clear();
758
+
759
+ // Five points where first === last → constructor pops last and sets flag=1
760
+ core.scene.addItem('Lwpolyline', {
761
+ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100), new Point(0, 0)],
762
+ });
763
+ core.scene.addItem('Line', { points: [new Point(50, -10), new Point(50, 110)] });
764
+
765
+ const polyline = core.scene.entities.get(0);
766
+ expect(polyline.flags.hasFlag(1)).toBe(true); // confirm closed before trim
767
+
768
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
769
+ trim.selectedItem = polyline;
770
+ core.mouse.setPosFromScenePoint(new Point(75, 50));
771
+ trim.action();
772
+
773
+ // Original closed polyline removed, one open polyline added, boundary line remains
774
+ expect(core.scene.entities.count()).toBe(2);
775
+
776
+ let trimmed;
777
+ for (let i = 0; i < core.scene.entities.count(); i++) {
778
+ if (core.scene.entities.get(i).type === 'Polyline') trimmed = core.scene.entities.get(i);
779
+ }
780
+
781
+ expect(trimmed).toBeDefined();
782
+ expect(trimmed.flags.hasFlag(1)).toBe(false); // closed flag removed
783
+ expect(trimmed.points.length).toBe(4);
784
+ // Joined path runs from one intersection, around the left side, to the other
785
+ expect(trimmed.points[0].x).toBeCloseTo(50);
786
+ expect(trimmed.points[3].x).toBeCloseTo(50);
787
+ });
788
+
789
+ test('Trim.action trims middle of a line - circle boundary creates two intersections on the same segment', () => {
790
+ // A circle boundary intersects a single-segment line at two points, both on
791
+ // segment index 1. The sort comparator falls through to the
792
+ // positionAlongSegment comparison (line 179 of trim.js) because the segment
793
+ // indices are equal.
794
+ // Circle: center (50,0) radius 20 → intersects the x-axis at (30,0) and (70,0)
795
+ // Line: (0,0) → (100,0), mouse at (50,0) — inside the circle
796
+ // Expected: original line removed, two shorter lines (0→30) and (70→100) added
797
+
798
+ const trim = new Trim();
799
+ core.scene.clear();
800
+
801
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(100, 0)] });
802
+ core.scene.addItem('Circle', { points: [new Point(50, 0), new Point(70, 0)] }); // radius 20
803
+
804
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
805
+ trim.selectedItem = core.scene.entities.get(0);
806
+ core.mouse.setPosFromScenePoint(new Point(50, 0));
807
+ trim.action();
808
+
809
+ // original line removed; two shorter lines added; circle remains
810
+ expect(core.scene.entities.count()).toBe(3);
811
+
812
+ const lines = [];
813
+ for (let i = 0; i < core.scene.entities.count(); i++) {
814
+ if (core.scene.entities.get(i).type === 'Line') lines.push(core.scene.entities.get(i));
815
+ }
816
+ expect(lines.length).toBe(2);
817
+ lines.sort((a, b) => a.points[0].x - b.points[0].x);
818
+
819
+ expect(lines[0].points[0].x).toBeCloseTo(0);
820
+ expect(lines[0].points[1].x).toBeCloseTo(30);
821
+ expect(lines[1].points[0].x).toBeCloseTo(70);
822
+ expect(lines[1].points[1].x).toBeCloseTo(100);
823
+ });
824
+
825
+ test('Trim.execute catches and logs errors thrown during input', async () => {
826
+ // Force an error inside execute() to cover the catch block (line 69 of trim.js)
827
+ core.scene.clear();
828
+ core.scene.selectionManager.reset();
829
+
830
+ const requestInputSpy = jest.spyOn(DesignCore.Scene.inputManager, 'requestInput')
831
+ .mockRejectedValue(new Error('mocked error'));
832
+
833
+ const trim = new Trim();
834
+ // execute() should resolve without re-throwing because the catch block handles it
835
+ await expect(trim.execute()).resolves.toBeUndefined();
836
+
837
+ requestInputSpy.mockRestore();
838
+ });
839
+
840
+ test('Trim.action trims a circle near the seam (right side, mouse at angle 0)', () => {
841
+ // Circle: center (0,0), radius 10.
842
+ // Circle.toPolylinePoints() always places the seam at angle 0 → vertex (10,0).
843
+ // The old code failed when the mouse was near that seam because both intersections
844
+ // appeared "after" the mouse in linear polyline order, leaving trimBefore=null.
845
+ //
846
+ // Boundary: vertical line at x=0, intersects at (0,10) [seg 2] and (0,-10) [seg 1].
847
+ // Mouse at (8,0) — on the RIGHT side, which is the forward arc from (0,-10) → (10,0) → (0,10)
848
+ // (crosses the seam vertex in the polyline chain but is a valid arc region).
849
+ // Expected: arc from (10,0) → (0,-10) covering the left half, trimming the seam-crossing right arc.
850
+
851
+ const trim = new Trim();
852
+ core.scene.clear();
853
+
854
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
855
+ core.scene.addItem('Line', { points: [new Point(0, -20), new Point(0, 20)] });
856
+
857
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
858
+ trim.selectedItem = core.scene.entities.get(0);
859
+ core.mouse.setPosFromScenePoint(new Point(8, 0));
860
+ trim.action();
861
+
862
+ expect(core.scene.entities.count()).toBe(2);
863
+
864
+ let arc;
865
+ for (let i = 0; i < core.scene.entities.count(); i++) {
866
+ if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
867
+ }
868
+ expect(arc).toBeDefined();
869
+ // Remaining arc is the left half: from (0,10) CCW through (-10,0) to (0,-10)
870
+ expect(arc.points[0].x).toBeCloseTo(0); // center
871
+ expect(arc.points[0].y).toBeCloseTo(0);
872
+ expect(arc.points[1].x).toBeCloseTo(0); // start at (0,10)
873
+ expect(arc.points[1].y).toBeCloseTo(10);
874
+ expect(arc.points[2].x).toBeCloseTo(0); // end at (0,-10)
875
+ expect(arc.points[2].y).toBeCloseTo(-10);
876
+ });
877
+
878
+ test('Trim.action trims a closed polyline near the seam vertex', () => {
879
+ // Closed square drawn starting from (0,0): seam is at vertex (0,0).
880
+ // Boundary: horizontal line at y=50, intersects:
881
+ // left edge (closing segment (0,100)→(0,0)→...) at (0,50) — segment 4 (closing seg)
882
+ // right edge (100,0)→(100,100) at (100,50) — segment 2
883
+ //
884
+ // Mouse at (50,25) — in the BOTTOM half, which is the forward arc from intA=(100,50)
885
+ // through the bottom vertices to intB=(0,50). This forward arc crosses the seam vertex.
886
+ //
887
+ // Expected: open polyline (100,50)→(100,0)→(0,0)→(0,50), flag bit 1 cleared.
888
+
889
+ const trim = new Trim();
890
+ core.scene.clear();
891
+
892
+ core.scene.addItem('Lwpolyline', {
893
+ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100), new Point(0, 0)],
894
+ });
895
+ core.scene.addItem('Line', { points: [new Point(-10, 50), new Point(110, 50)] });
896
+
897
+ const polyline = core.scene.entities.get(0);
898
+ expect(polyline.flags.hasFlag(1)).toBe(true);
899
+
900
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
901
+ trim.selectedItem = polyline;
902
+ // Mouse in the BOTTOM half — forward arc between the two intersections, crosses the seam
903
+ core.mouse.setPosFromScenePoint(new Point(50, 25));
904
+ trim.action();
905
+
906
+ expect(core.scene.entities.count()).toBe(2);
907
+
908
+ let trimmed;
909
+ for (let i = 0; i < core.scene.entities.count(); i++) {
910
+ if (core.scene.entities.get(i).type === 'Polyline') trimmed = core.scene.entities.get(i);
911
+ }
912
+
913
+ expect(trimmed).toBeDefined();
914
+ expect(trimmed.flags.hasFlag(1)).toBe(false);
915
+ expect(trimmed.points.length).toBe(4);
916
+ // Forward arc: right edge from y=50 down to corner, across bottom, left edge up to y=50
917
+ expect(trimmed.points[0].x).toBeCloseTo(100);
918
+ expect(trimmed.points[0].y).toBeCloseTo(50);
919
+ expect(trimmed.points[3].x).toBeCloseTo(0);
920
+ expect(trimmed.points[3].y).toBeCloseTo(50);
921
+ });
922
+
923
+ test('Trim.action trims an open polyline with two consecutive arc segments', () => {
924
+ // Polyline: (0,0,b=1) -> arc1 -> (100,0,b=1) -> arc2 -> (200,0)
925
+ // arc1 center (50,0) radius 50, through (50,-50)
926
+ // arc2 center (150,0) radius 50, through (150,-50)
927
+ // Boundary: vertical line at x=150, intersection at (150,-50) on arc2.
928
+ // Mouse at (120,-40): on arc2 BEFORE the intersection (in arc-travel order).
929
+ // Expected: keep right portion = (150,-50) -> (200,0); discard left side.
930
+
931
+ const trim = new Trim();
932
+ core.scene.clear();
933
+
934
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0, 1), new Point(100, 0, 1), new Point(200, 0)] });
935
+ core.scene.addItem('Line', { points: [new Point(150, -100), new Point(150, 100)] });
936
+
937
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
938
+ trim.selectedItem = core.scene.entities.get(0);
939
+ core.mouse.setPosFromScenePoint(new Point(120, -40));
940
+ trim.action();
941
+
942
+ expect(core.scene.entities.count()).toBe(2);
943
+
944
+ let result;
945
+ for (let i = 0; i < core.scene.entities.count(); i++) {
946
+ if (core.scene.entities.get(i).type === 'Polyline') result = core.scene.entities.get(i);
947
+ }
948
+
949
+ expect(result).toBeDefined();
950
+ expect(result.points.length).toBe(2);
951
+ expect(result.points[0].x).toBeCloseTo(150);
952
+ expect(result.points[0].y).toBeCloseTo(-50);
953
+ expect(result.points[1].x).toBeCloseTo(200);
954
+ expect(result.points[1].y).toBeCloseTo(0);
955
+ });
956
+
957
+ test('Trim.action trims an arc from the start (portionOne same-segment bulge)', () => {
958
+ // Arc: center (0,0), start (10,0), end (-10,0) — CCW upper semicircle through (0,10).
959
+ // Boundary: vertical line at x=0, intersection at (0,10).
960
+ // Mouse at (-7,7) — LEFT of intersection, so trimBefore=(0,10), trimAfter=null.
961
+ // portionOne = startLoc(seg1,(10,0)) → trimBefore(seg1,(0,10)) — same-segment arc split.
962
+ // Expected: arc from (10,0) to (0,10), covering the right quarter.
963
+
964
+ const trim = new Trim();
965
+ core.scene.clear();
966
+
967
+ core.scene.addItem('Arc', { points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)] });
968
+ core.scene.addItem('Line', { points: [new Point(0, -20), new Point(0, 20)] });
969
+
970
+ trim.selectedBoundaryItems = [core.scene.entities.get(1)];
971
+ trim.selectedItem = core.scene.entities.get(0);
972
+ core.mouse.setPosFromScenePoint(new Point(-7, 7));
973
+ trim.action();
974
+
975
+ expect(core.scene.entities.count()).toBe(2);
976
+
977
+ let arc;
978
+ for (let i = 0; i < core.scene.entities.count(); i++) {
979
+ if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
980
+ }
981
+ expect(arc).toBeDefined();
982
+ // Kept portion runs from original start (10,0) to intersection (0,10)
983
+ expect(arc.points[1].x).toBeCloseTo(10);
984
+ expect(arc.points[1].y).toBeCloseTo(0);
985
+ expect(arc.points[2].x).toBeCloseTo(0);
986
+ expect(arc.points[2].y).toBeCloseTo(10);
987
+ });
988
+
989
+ test('Trim.action trims the middle of an arc (two intersections on same arc segment)', () => {
990
+ // Arc: center (0,0), start (10,0), end (-10,0) — CCW upper semicircle through (0,10).
991
+ // Two boundaries: vertical line at x=-5 and x=5.
992
+ // Intersections at (5,√75)≈(5,8.66) and (-5,√75)≈(-5,8.66), both on segment 1.
993
+ // Mouse at (0,9) — between both intersections at the top.
994
+ // Expected: two arcs: (10,0)→(5,8.66) and (-5,8.66)→(-10,0).
995
+
996
+ const trim = new Trim();
997
+ core.scene.clear();
998
+
999
+ core.scene.addItem('Arc', { points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)] });
1000
+ core.scene.addItem('Line', { points: [new Point(5, -20), new Point(5, 20)] });
1001
+ core.scene.addItem('Line', { points: [new Point(-5, -20), new Point(-5, 20)] });
1002
+
1003
+ trim.selectedBoundaryItems = [core.scene.entities.get(1), core.scene.entities.get(2)];
1004
+ trim.selectedItem = core.scene.entities.get(0);
1005
+ core.mouse.setPosFromScenePoint(new Point(0, 9));
1006
+ trim.action();
1007
+
1008
+ // Original arc removed, two new arcs added, two boundary lines remain
1009
+ expect(core.scene.entities.count()).toBe(4);
1010
+
1011
+ const arcs = [];
1012
+ for (let i = 0; i < core.scene.entities.count(); i++) {
1013
+ if (core.scene.entities.get(i).type === 'Arc') arcs.push(core.scene.entities.get(i));
1014
+ }
1015
+ expect(arcs.length).toBe(2);
1016
+ arcs.sort((a, b) => a.points[1].x - b.points[1].x); // sort by start x
1017
+
1018
+ // Left arc: start near (-5,8.66), end at (-10,0)
1019
+ expect(arcs[0].points[1].x).toBeCloseTo(-5);
1020
+ expect(arcs[0].points[1].y).toBeCloseTo(Math.sqrt(75));
1021
+ expect(arcs[0].points[2].x).toBeCloseTo(-10);
1022
+ expect(arcs[0].points[2].y).toBeCloseTo(0);
1023
+
1024
+ // Right arc: start at (10,0), end near (5,8.66)
1025
+ expect(arcs[1].points[1].x).toBeCloseTo(10);
1026
+ expect(arcs[1].points[1].y).toBeCloseTo(0);
1027
+ expect(arcs[1].points[2].x).toBeCloseTo(5);
1028
+ expect(arcs[1].points[2].y).toBeCloseTo(Math.sqrt(75));
1029
+ });
1030
+
1031
+ test('Trim.action trims with multiple boundary items', () => {
1032
+ // Line: (0,0)→(200,0). Two separate boundary lines at x=50 and x=150.
1033
+ // Mouse at (100,0) — between both boundaries.
1034
+ // Expected: two lines: (0,0)→(50,0) and (150,0)→(200,0).
1035
+
1036
+ const trim = new Trim();
1037
+ core.scene.clear();
1038
+
1039
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(200, 0)] });
1040
+ core.scene.addItem('Line', { points: [new Point(50, -20), new Point(50, 20)] });
1041
+ core.scene.addItem('Line', { points: [new Point(150, -20), new Point(150, 20)] });
1042
+
1043
+ trim.selectedBoundaryItems = [core.scene.entities.get(1), core.scene.entities.get(2)];
1044
+ trim.selectedItem = core.scene.entities.get(0);
1045
+ core.mouse.setPosFromScenePoint(new Point(100, 0));
1046
+ trim.action();
1047
+
1048
+ // Original line removed, two new lines added, two boundary lines remain
1049
+ expect(core.scene.entities.count()).toBe(4);
1050
+
1051
+ const lines = [];
1052
+ for (let i = 0; i < core.scene.entities.count(); i++) {
1053
+ const e = core.scene.entities.get(i);
1054
+ // Identify result lines by both points being at y=0 (boundary lines are vertical)
1055
+ if (e.type === 'Line' && e.points[0].y === 0 && e.points[1].y === 0) lines.push(e);
1056
+ }
1057
+ expect(lines.length).toBe(2);
1058
+ lines.sort((a, b) => a.points[0].x - b.points[0].x);
1059
+
1060
+ expect(lines[0].points[0].x).toBeCloseTo(0);
1061
+ expect(lines[0].points[1].x).toBeCloseTo(50);
1062
+ expect(lines[1].points[0].x).toBeCloseTo(150);
1063
+ expect(lines[1].points[1].x).toBeCloseTo(200);
1064
+ });
@@ -109,15 +109,15 @@ test('Test Zoom.preview - Window mode with one point', () => {
109
109
  // Mock mouse position
110
110
  core.mouse.mouseMoved(50, 50);
111
111
 
112
- const initialTempItemsCount = DesignCore.Scene.tempEntities.count();
112
+ const initialTempItemsCount = DesignCore.Scene.previewEntities.count();
113
113
 
114
114
  zoom.preview();
115
115
 
116
116
  // Should have added a polyline (rectangle preview)
117
- expect(DesignCore.Scene.tempEntities.count()).toBe(initialTempItemsCount + 1);
117
+ expect(DesignCore.Scene.previewEntities.count()).toBe(initialTempItemsCount + 1);
118
118
 
119
119
  // Verify it's a polyline
120
- const tempItem = DesignCore.Scene.tempEntities.get(DesignCore.Scene.tempEntities.count() - 1);
120
+ const tempItem = DesignCore.Scene.previewEntities.get(DesignCore.Scene.previewEntities.count() - 1);
121
121
  expect(tempItem.type).toBe('Polyline');
122
122
  expect(tempItem.points.length).toBe(4); // Rectangle has 4 points (closed flag set)
123
123
  });
@@ -126,12 +126,12 @@ test('Test Zoom.preview - Window mode with no points (should not preview)', () =
126
126
  const zoom = new Zoom();
127
127
  zoom.mode = 'Window';
128
128
 
129
- const initialTempItemsCount = DesignCore.Scene.tempEntities.count();
129
+ const initialTempItemsCount = DesignCore.Scene.previewEntities.count();
130
130
 
131
131
  zoom.preview();
132
132
 
133
133
  // Should not add anything
134
- expect(DesignCore.Scene.tempEntities.count()).toBe(initialTempItemsCount);
134
+ expect(DesignCore.Scene.previewEntities.count()).toBe(initialTempItemsCount);
135
135
  });
136
136
 
137
137
  test('Test Zoom.preview - All mode (should not preview)', () => {
@@ -139,12 +139,12 @@ test('Test Zoom.preview - All mode (should not preview)', () => {
139
139
  zoom.mode = 'All';
140
140
  zoom.points.push(new Point(10, 10));
141
141
 
142
- const initialTempItemsCount = DesignCore.Scene.tempEntities.count();
142
+ const initialTempItemsCount = DesignCore.Scene.previewEntities.count();
143
143
 
144
144
  zoom.preview();
145
145
 
146
146
  // Should not add anything for All/Extents modes
147
- expect(DesignCore.Scene.tempEntities.count()).toBe(initialTempItemsCount);
147
+ expect(DesignCore.Scene.previewEntities.count()).toBe(initialTempItemsCount);
148
148
  });
149
149
 
150
150
  test('Test Canvas.zoomToWindow - valid window', () => {