@design-core/core 0.0.6 → 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 +30 -282
  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 +35 -11
  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 +68 -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 +181 -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
@@ -7,3 +7,30 @@ test('Constants.Tolerance.EPSILON is 1e-10', () => {
7
7
  test('Constants.Precision.DECIMALPLACES is 5', () => {
8
8
  expect(Constants.Precision.DECIMALPLACES).toBe(5);
9
9
  });
10
+
11
+ describe('Constants.PageSizes', () => {
12
+ test('A4 portrait is 595 × 842', () => {
13
+ expect(Constants.PageSizes['A4']).toEqual({ width: 595, height: 842 });
14
+ });
15
+
16
+ test('A3 portrait is 842 × 1191', () => {
17
+ expect(Constants.PageSizes['A3']).toEqual({ width: 842, height: 1191 });
18
+ });
19
+
20
+ test('Letter portrait is 612 × 792', () => {
21
+ expect(Constants.PageSizes['Letter']).toEqual({ width: 612, height: 792 });
22
+ });
23
+
24
+ test('all sizes have portrait orientation (width < height)', () => {
25
+ for (const [, size] of Object.entries(Constants.PageSizes)) {
26
+ expect(size.width).toBeLessThan(size.height);
27
+ }
28
+ });
29
+
30
+ test('contains all expected ISO and ANSI sizes', () => {
31
+ const expected = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter', 'Legal', 'Tabloid', 'ANSI C', 'ANSI D', 'ANSI E'];
32
+ for (const name of expected) {
33
+ expect(Constants.PageSizes).toHaveProperty(name);
34
+ }
35
+ });
36
+ });
@@ -23,7 +23,7 @@ describe('EntityManager', () => {
23
23
  expect(mgr.count()).toBe(1);
24
24
  const ent = mgr.get(0);
25
25
  expect(ent).toBeInstanceOf(Line);
26
- expect(ent.layer).toBe(DesignCore.LayerManager.getCstyle());
26
+ expect(ent.getProperty('layer')).toBe(DesignCore.LayerManager.getCstyle());
27
27
  });
28
28
 
29
29
  test('add/get/count manage entities correctly', () => {
@@ -62,7 +62,7 @@ describe('EntityManager', () => {
62
62
  const mgr = DesignCore.Scene.entities;
63
63
  const line = new Line({ points: [new Point(0, 0), new Point(1, 0)] });
64
64
  mgr.add(line);
65
- const handle = line.handle;
65
+ const handle = line.getProperty('handle');
66
66
  expect(DesignCore.HandleManager.usedHandles.has(handle)).toBe(true);
67
67
  mgr.remove(0);
68
68
  expect(DesignCore.HandleManager.usedHandles.has(handle)).toBe(false);
@@ -72,7 +72,7 @@ describe('EntityManager', () => {
72
72
  const mgr = DesignCore.Scene.entities;
73
73
  const line = new Line({ points: [new Point(0, 0), new Point(1, 0)] });
74
74
  mgr.add(line);
75
- const handle = line.handle;
75
+ const handle = line.getProperty('handle');
76
76
  const sizeAfterAdd = DesignCore.HandleManager.usedHandles.size;
77
77
 
78
78
  mgr.remove(0);
@@ -82,7 +82,7 @@ describe('EntityManager', () => {
82
82
 
83
83
  // Re-add the same entity (simulates undo of a delete)
84
84
  mgr.add(line);
85
- expect(line.handle).toBe(handle);
85
+ expect(line.getProperty('handle')).toBe(handle);
86
86
  expect(DesignCore.HandleManager.usedHandles.has(handle)).toBe(true);
87
87
  // Size must return to what it was after the first add, not grow by one.
88
88
  // If checkHandle silently re-inserted without the prior releaseHandle, the
@@ -237,6 +237,38 @@ test('onCommandButton with no active command - starts command', () => {
237
237
  inputManager.reset();
238
238
  });
239
239
 
240
+ // ─── startCommand — selection clearing ───────────────────────────────────────
241
+
242
+ test('starting an entity command from idle clears the selection', () => {
243
+ inputManager.reset();
244
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
245
+ core.scene.selectionManager.addToSelectionSet(0);
246
+ expect(core.scene.selectionManager.selectionSet.selectionSet.length).toBe(1);
247
+
248
+ inputManager.onCommand('L');
249
+ expect(core.scene.selectionManager.selectionSet.selectionSet.length).toBe(0);
250
+ inputManager.reset();
251
+ });
252
+
253
+ test('starting a tool command from idle preserves the selection', () => {
254
+ inputManager.reset();
255
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
256
+ core.scene.selectionManager.addToSelectionSet(0);
257
+ expect(core.scene.selectionManager.selectionSet.selectionSet.length).toBe(1);
258
+
259
+ // Prevent Erase.execute() from running so we can isolate startCommand's own behaviour.
260
+ // Without this, Erase synchronously calls executeCommand() → reset() in the
261
+ // preselection branch before our assertion can fire.
262
+ const Erase = DesignCore.CommandManager.createNew('Erase').constructor;
263
+ jest.spyOn(Erase.prototype, 'execute').mockImplementation(() => {});
264
+
265
+ inputManager.onCommand('E');
266
+ expect(core.scene.selectionManager.selectionSet.selectionSet.length).toBe(1);
267
+
268
+ jest.restoreAllMocks();
269
+ inputManager.reset();
270
+ });
271
+
240
272
  // ─── onCommand - command shortcut while non-accepting prompt is active ─────────
241
273
 
242
274
  test('onCommand with valid shortcut while POINT prompt active - switches command without notifying', () => {
@@ -509,12 +541,12 @@ test('actionCommand with a Tool as activeCommand - calls its action method', ()
509
541
  test('executeCommand with an entity item - adds item to scene then resets', () => {
510
542
  inputManager.reset();
511
543
  inputManager.onCommand('Line');
512
- const addItemSpy = jest.spyOn(core.scene, 'addItem').mockReturnValue(1);
544
+ const commitEntitySpy = jest.spyOn(core.scene, 'commitEntity').mockReturnValue(1);
513
545
  const line = new Line();
514
546
  inputManager.executeCommand(line);
515
- expect(addItemSpy).toHaveBeenCalled();
547
+ expect(commitEntitySpy).toHaveBeenCalled();
516
548
  expect(inputManager.activeCommand).toBeUndefined();
517
- addItemSpy.mockRestore();
549
+ commitEntitySpy.mockRestore();
518
550
  });
519
551
 
520
552
  // ─── requestInput validation ──────────────────────────────────────────────────
@@ -670,6 +702,9 @@ test('highlightEntityUnderMouse returns true when entity found and no active com
670
702
  const mockLine = new Line();
671
703
  jest.spyOn(core.scene.entities, 'get').mockReturnValue(mockLine);
672
704
  expect(inputManager.highlightEntityUnderMouse()).toBe(true);
705
+ // entity should go into hoverEntities, not previewEntities
706
+ expect(core.scene.hoverEntities.count()).toBe(1);
707
+ expect(core.scene.previewEntities.count()).toBe(0);
673
708
  jest.restoreAllMocks();
674
709
  });
675
710
 
@@ -728,22 +763,22 @@ test('actionCommand with undefined item and non-Tool activeCommand - does nothin
728
763
  test('actionCommand returns the item index when adding an entity', () => {
729
764
  inputManager.reset();
730
765
  inputManager.onCommand('Line');
731
- const addItemSpy = jest.spyOn(core.scene, 'addItem').mockReturnValue(42);
766
+ const commitEntitySpy = jest.spyOn(core.scene, 'commitEntity').mockReturnValue(42);
732
767
  const line = new Line();
733
768
  const index = inputManager.actionCommand(line);
734
769
  expect(index).toBe(42);
735
- addItemSpy.mockRestore();
770
+ commitEntitySpy.mockRestore();
736
771
  inputManager.reset();
737
772
  });
738
773
 
739
- test('actionCommand passes index parameter to addItem', () => {
774
+ test('actionCommand passes index parameter to commitEntity', () => {
740
775
  inputManager.reset();
741
776
  inputManager.onCommand('Line');
742
- const addItemSpy = jest.spyOn(core.scene, 'addItem').mockReturnValue(5);
777
+ const commitEntitySpy = jest.spyOn(core.scene, 'commitEntity').mockReturnValue(5);
743
778
  const line = new Line();
744
779
  inputManager.actionCommand(line, 5);
745
- expect(addItemSpy).toHaveBeenCalledWith(line.type, expect.any(Object), 5);
746
- addItemSpy.mockRestore();
780
+ expect(commitEntitySpy).toHaveBeenCalledWith(line, 5);
781
+ commitEntitySpy.mockRestore();
747
782
  inputManager.reset();
748
783
  });
749
784
 
@@ -785,9 +820,9 @@ test('mouseMoved calls activeCommand.preview when command is active and no entit
785
820
  inputManager.reset();
786
821
  });
787
822
 
788
- test('mouseMoved does not call preview when entity is being selected', () => {
823
+ test('mouseMoved calls preview even when entity is being highlighted', () => {
789
824
  inputManager.reset();
790
- // with SINGLESELECTION prompt, highlightEntityUnderMouse will run and return true (selecting = true)
825
+ // preview() is always called for the active command, including when an entity is highlighted
791
826
  inputManager.activeCommand = new Line();
792
827
  const po = new PromptOptions('', [Input.Type.SINGLESELECTION]);
793
828
  inputManager.requestInput(po);
@@ -796,7 +831,7 @@ test('mouseMoved does not call preview when entity is being selected', () => {
796
831
  jest.spyOn(core.scene.entities, 'get').mockReturnValue(new Line());
797
832
  core.mouse.buttonOneDown = false;
798
833
  inputManager.mouseMoved();
799
- expect(previewSpy).not.toHaveBeenCalled();
834
+ expect(previewSpy).toHaveBeenCalled();
800
835
  jest.restoreAllMocks();
801
836
  po.cancel();
802
837
  inputManager.reset();
@@ -904,6 +939,7 @@ test('highlightEntityUnderMouse returns true with SINGLESELECTION prompt and ent
904
939
  jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
905
940
  jest.spyOn(core.scene.entities, 'get').mockReturnValue(new Line());
906
941
  expect(inputManager.highlightEntityUnderMouse()).toBe(true);
942
+ expect(core.scene.hoverEntities.count()).toBe(1);
907
943
  jest.restoreAllMocks();
908
944
  po.cancel();
909
945
  inputManager.reset();
@@ -917,6 +953,7 @@ test('highlightEntityUnderMouse returns true with SELECTIONSET prompt and entity
917
953
  jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
918
954
  jest.spyOn(core.scene.entities, 'get').mockReturnValue(new Line());
919
955
  expect(inputManager.highlightEntityUnderMouse()).toBe(true);
956
+ expect(core.scene.hoverEntities.count()).toBe(1);
920
957
  jest.restoreAllMocks();
921
958
  po.cancel();
922
959
  inputManager.reset();
@@ -1002,16 +1039,14 @@ test('reset sets inputPoint to null', () => {
1002
1039
 
1003
1040
  // ─── mouseMoved – auxiliaryEntities cleared at start ─────────────────────────
1004
1041
 
1005
- test('mouseMoved clears auxiliaryEntities and tempEntities at the start', () => {
1006
- // pre-populate both collections
1042
+ test('mouseMoved clears auxiliaryEntities, previewEntities and hoverEntities at the start', () => {
1043
+ // pre-populate all three collections
1007
1044
  const addTrackLineSpy = jest.spyOn(inputManager.snapping, 'addTrackingLine').mockImplementation(() => {});
1008
1045
  DesignCore.Scene.auxiliaryEntities.add({ draw: () => {} });
1009
- DesignCore.Scene.tempEntities.add({ draw: () => {} });
1046
+ DesignCore.Scene.previewEntities.add({ draw: () => {} });
1047
+ DesignCore.Scene.hoverEntities.add({ draw: () => {} });
1010
1048
  inputManager.mouseMoved();
1011
- // both should have been cleared (count re-started from 0 before any new adds)
1012
- // After mouseMoved the tracking line may or may not have been re-added, but the
1013
- // clear happened — verify by checking they were empty after the initial clear
1014
- // (we can confirm this indirectly: no crash and spies didn't see stale data)
1049
+ // all should have been cleared — confirm no crash and stale data is gone
1015
1050
  expect(() => inputManager.mouseMoved()).not.toThrow();
1016
1051
  addTrackLineSpy.mockRestore();
1017
1052
  });
@@ -1145,3 +1180,29 @@ test('highlightEntityUnderMouse returns false when buttonThreeDown is true', ()
1145
1180
  expect(inputManager.highlightEntityUnderMouse()).toBe(false);
1146
1181
  core.mouse.buttonThreeDown = false;
1147
1182
  });
1183
+
1184
+ test('refreshPreview clears hoverEntities in addition to previewEntities and auxiliaryEntities', () => {
1185
+ core.scene.clear();
1186
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
1187
+
1188
+ // Simulate a stale hover glow left by the previous mouseMoved()
1189
+ DesignCore.Scene.hoverEntities.add(core.scene.entities.get(0));
1190
+ expect(DesignCore.Scene.hoverEntities.count()).toBe(1);
1191
+
1192
+ // Attach a minimal active command so refreshPreview doesn't bail early
1193
+ inputManager.activeCommand = { preview: () => {} };
1194
+ inputManager.refreshPreview();
1195
+
1196
+ expect(DesignCore.Scene.hoverEntities.count()).toBe(0);
1197
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
1198
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
1199
+ });
1200
+
1201
+ test('refreshPreview returns early without clearing when there is no active command', () => {
1202
+ DesignCore.Scene.hoverEntities.add({ draw: () => {} });
1203
+ inputManager.activeCommand = undefined;
1204
+ inputManager.refreshPreview();
1205
+ // hoverEntities should be untouched — no active command means no refresh
1206
+ expect(DesignCore.Scene.hoverEntities.count()).toBe(1);
1207
+ DesignCore.Scene.hoverEntities.clear();
1208
+ });
@@ -539,3 +539,86 @@ test('intersectPolylinePolyline - two full circles outside each other, no inters
539
539
  expect(result.status).toBe('None');
540
540
  expect(result.points.length).toBe(0);
541
541
  });
542
+
543
+ // --- isInsidePolyline ---
544
+ // Helper: axis-aligned rectangle as a closed polyline
545
+ /**
546
+ * @param {number} x1
547
+ * @param {number} y1
548
+ * @param {number} x2
549
+ * @param {number} y2
550
+ * @return {Array}
551
+ */
552
+ function makeRect(x1, y1, x2, y2) {
553
+ return [
554
+ new Point(x1, y1), new Point(x2, y1),
555
+ new Point(x2, y2), new Point(x1, y2),
556
+ new Point(x1, y1),
557
+ ];
558
+ }
559
+
560
+ // Helper: full circle as two bulge-encoded semicircles, centre (cx,cy) radius r
561
+ /**
562
+ * @param {number} cx
563
+ * @param {number} cy
564
+ * @param {number} r
565
+ * @return {Array}
566
+ */
567
+ function makeCircle(cx, cy, r) {
568
+ return [
569
+ new Point(cx, cy + r, 1),
570
+ new Point(cx, cy - r, 1),
571
+ new Point(cx, cy + r),
572
+ ];
573
+ }
574
+
575
+ test('isInsidePolyline - point inside a rectangle', () => {
576
+ const rect = makeRect(0, 0, 10, 10);
577
+ expect(Intersection.isInsidePolyline(5, 5, [rect], 20)).toBe(true);
578
+ });
579
+
580
+ test('isInsidePolyline - point outside a rectangle', () => {
581
+ const rect = makeRect(0, 0, 10, 10);
582
+ expect(Intersection.isInsidePolyline(15, 5, [rect], 20)).toBe(false);
583
+ });
584
+
585
+ test('isInsidePolyline - point on the boundary edge does not throw', () => {
586
+ const rect = makeRect(0, 0, 10, 10);
587
+ expect(() => Intersection.isInsidePolyline(0, 5, [rect], 20)).not.toThrow();
588
+ });
589
+
590
+ test('isInsidePolyline - point inside a circle boundary', () => {
591
+ const circle = makeCircle(0, 0, 10);
592
+ expect(Intersection.isInsidePolyline(0, 0, [circle], 25)).toBe(true);
593
+ });
594
+
595
+ test('isInsidePolyline - point outside a circle boundary', () => {
596
+ const circle = makeCircle(0, 0, 10);
597
+ expect(Intersection.isInsidePolyline(15, 0, [circle], 25)).toBe(false);
598
+ });
599
+
600
+ test('isInsidePolyline - ray at exact top of circle is handled correctly by rayEps', () => {
601
+ // Without the rayEps nudge a horizontal ray fired from y=10 would be tangent
602
+ // to the top of the circle and produce a wrong crossing count.
603
+ // The point (0,10) is on the boundary; the method must not return true.
604
+ const circle = makeCircle(0, 0, 10);
605
+ expect(Intersection.isInsidePolyline(0, 10, [circle], 25)).toBe(false);
606
+ });
607
+
608
+ test('isInsidePolyline - multiple boundaries, point inside inner boundary is outside under even-odd', () => {
609
+ // Outer 20×20 rect + inner 6×6 rect acting as a hole.
610
+ // A point inside the inner rect crosses both boundaries (count=2, even) → outside.
611
+ const outer = makeRect(0, 0, 20, 20);
612
+ const inner = makeRect(7, 7, 13, 13);
613
+ expect(Intersection.isInsidePolyline(10, 10, [outer, inner], 30)).toBe(false);
614
+ });
615
+
616
+ test('isInsidePolyline - multiple boundaries, point between outer and inner is inside', () => {
617
+ const outer = makeRect(0, 0, 20, 20);
618
+ const inner = makeRect(7, 7, 13, 13);
619
+ expect(Intersection.isInsidePolyline(2, 2, [outer, inner], 30)).toBe(true);
620
+ });
621
+
622
+ test('isInsidePolyline - empty boundaries returns false', () => {
623
+ expect(Intersection.isInsidePolyline(5, 5, [], 20)).toBe(false);
624
+ });
@@ -0,0 +1,179 @@
1
+ import { PlotOptions } from '../../core/lib/plotOptions.js';
2
+ import { RendererBase } from '../../core/lib/renderers/rendererBase.js';
3
+
4
+ // ─── Constructor ─────────────────────────────────────────────────────────────
5
+
6
+ test('constructor stores pageWidth and pageHeight', () => {
7
+ const po = new PlotOptions(595, 842);
8
+ expect(po.pageWidth).toBe(595);
9
+ expect(po.pageHeight).toBe(842);
10
+ });
11
+
12
+ test('constructor sets default plotScale to null', () => {
13
+ const po = new PlotOptions(595, 842);
14
+ expect(po.plotScale).toBeNull();
15
+ });
16
+
17
+ test('constructor sets default plotArea to PlotOptions.Area.EXTENTS', () => {
18
+ const po = new PlotOptions(595, 842);
19
+ expect(po.plotArea).toBe(PlotOptions.Area.EXTENTS);
20
+ });
21
+
22
+ test('constructor sets default style to RendererBase.Styles.NONE', () => {
23
+ const po = new PlotOptions(595, 842);
24
+ expect(po.style).toBe(RendererBase.Styles.NONE);
25
+ });
26
+
27
+ test('constructor sets default fileType to pdf', () => {
28
+ const po = new PlotOptions(595, 842);
29
+ expect(po.fileType).toBe('pdf');
30
+ });
31
+
32
+ test('constructor throws when pageWidth is missing', () => {
33
+ expect(() => new PlotOptions(undefined, 842)).toThrow('pageWidth');
34
+ });
35
+
36
+ test('constructor throws when pageHeight is missing', () => {
37
+ expect(() => new PlotOptions(595, undefined)).toThrow('pageHeight');
38
+ });
39
+
40
+ test('constructor throws when pageWidth is zero', () => {
41
+ expect(() => new PlotOptions(0, 842)).toThrow('pageWidth');
42
+ });
43
+
44
+ test('constructor throws when pageWidth is negative', () => {
45
+ expect(() => new PlotOptions(-1, 842)).toThrow('pageWidth');
46
+ });
47
+
48
+ test('constructor throws when pageHeight is zero', () => {
49
+ expect(() => new PlotOptions(595, 0)).toThrow('pageHeight');
50
+ });
51
+
52
+ test('constructor throws when pageWidth is a string', () => {
53
+ expect(() => new PlotOptions('595', 842)).toThrow('pageWidth');
54
+ });
55
+
56
+ // ─── static Area ─────────────────────────────────────────────────────────────
57
+
58
+ test('PlotOptions.Area.EXTENTS is defined', () => {
59
+ expect(PlotOptions.Area.EXTENTS).toBeDefined();
60
+ });
61
+
62
+ test('PlotOptions.Area.DISPLAY is defined', () => {
63
+ expect(PlotOptions.Area.DISPLAY).toBeDefined();
64
+ });
65
+
66
+ test('PlotOptions.Area.EXTENTS and DISPLAY are distinct values', () => {
67
+ expect(PlotOptions.Area.EXTENTS).not.toBe(PlotOptions.Area.DISPLAY);
68
+ });
69
+
70
+ // ─── setOption — pageWidth / pageHeight ──────────────────────────────────────
71
+
72
+ test('setOption updates pageWidth to a valid value', () => {
73
+ const po = new PlotOptions(595, 842);
74
+ po.setOption('pageWidth', 420);
75
+ expect(po.pageWidth).toBe(420);
76
+ });
77
+
78
+ test('setOption throws for non-positive pageWidth', () => {
79
+ const po = new PlotOptions(595, 842);
80
+ expect(() => po.setOption('pageWidth', 0)).toThrow('pageWidth');
81
+ });
82
+
83
+ test('setOption throws for non-numeric pageHeight', () => {
84
+ const po = new PlotOptions(595, 842);
85
+ expect(() => po.setOption('pageHeight', 'tall')).toThrow('pageHeight');
86
+ });
87
+
88
+ // ─── setOption — plotScale ────────────────────────────────────────────────────
89
+
90
+ test('setOption accepts null for plotScale', () => {
91
+ const po = new PlotOptions(595, 842);
92
+ po.setOption('plotScale', null);
93
+ expect(po.plotScale).toBeNull();
94
+ });
95
+
96
+ test('setOption accepts a positive number for plotScale', () => {
97
+ const po = new PlotOptions(595, 842);
98
+ po.setOption('plotScale', 0.5);
99
+ expect(po.plotScale).toBe(0.5);
100
+ });
101
+
102
+ test('setOption throws for zero plotScale', () => {
103
+ const po = new PlotOptions(595, 842);
104
+ expect(() => po.setOption('plotScale', 0)).toThrow('plotScale');
105
+ });
106
+
107
+ test('setOption throws for negative plotScale', () => {
108
+ const po = new PlotOptions(595, 842);
109
+ expect(() => po.setOption('plotScale', -1)).toThrow('plotScale');
110
+ });
111
+
112
+ test('setOption throws for non-null non-number plotScale', () => {
113
+ const po = new PlotOptions(595, 842);
114
+ expect(() => po.setOption('plotScale', 'fit')).toThrow('plotScale');
115
+ });
116
+
117
+ // ─── setOption — plotArea ─────────────────────────────────────────────────────
118
+
119
+ test('setOption accepts PlotOptions.Area.EXTENTS for plotArea', () => {
120
+ const po = new PlotOptions(595, 842);
121
+ po.setOption('plotArea', PlotOptions.Area.EXTENTS);
122
+ expect(po.plotArea).toBe(PlotOptions.Area.EXTENTS);
123
+ });
124
+
125
+ test('setOption accepts PlotOptions.Area.DISPLAY for plotArea', () => {
126
+ const po = new PlotOptions(595, 842);
127
+ po.setOption('plotArea', PlotOptions.Area.DISPLAY);
128
+ expect(po.plotArea).toBe(PlotOptions.Area.DISPLAY);
129
+ });
130
+
131
+ test('setOption throws for an unknown plotArea value', () => {
132
+ const po = new PlotOptions(595, 842);
133
+ expect(() => po.setOption('plotArea', 'Window')).toThrow('plotArea');
134
+ });
135
+
136
+ // ─── setOption — style ────────────────────────────────────────────────────────
137
+
138
+ test('setOption accepts RendererBase.Styles.NONE for style', () => {
139
+ const po = new PlotOptions(595, 842);
140
+ po.setOption('style', RendererBase.Styles.NONE);
141
+ expect(po.style).toBe(RendererBase.Styles.NONE);
142
+ });
143
+
144
+ test('setOption accepts RendererBase.Styles.MONOCHROME for style', () => {
145
+ const po = new PlotOptions(595, 842);
146
+ po.setOption('style', RendererBase.Styles.MONOCHROME);
147
+ expect(po.style).toBe(RendererBase.Styles.MONOCHROME);
148
+ });
149
+
150
+ test('setOption throws when style is not a function', () => {
151
+ const po = new PlotOptions(595, 842);
152
+ expect(() => po.setOption('style', 'monochrome')).toThrow('style');
153
+ });
154
+
155
+ // ─── setOption — fileType ─────────────────────────────────────────────────────
156
+
157
+ test('setOption accepts pdf for fileType', () => {
158
+ const po = new PlotOptions(595, 842);
159
+ po.setOption('fileType', 'pdf');
160
+ expect(po.fileType).toBe('pdf');
161
+ });
162
+
163
+ test('setOption accepts svg for fileType', () => {
164
+ const po = new PlotOptions(595, 842);
165
+ po.setOption('fileType', 'svg');
166
+ expect(po.fileType).toBe('svg');
167
+ });
168
+
169
+ test('setOption throws for an unknown fileType', () => {
170
+ const po = new PlotOptions(595, 842);
171
+ expect(() => po.setOption('fileType', 'png')).toThrow('fileType');
172
+ });
173
+
174
+ // ─── setOption — unknown key ──────────────────────────────────────────────────
175
+
176
+ test('setOption throws for an unknown key', () => {
177
+ const po = new PlotOptions(595, 842);
178
+ expect(() => po.setOption('colour', 'red')).toThrow('unknown option \'colour\'');
179
+ });
@@ -0,0 +1,84 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { RubberBand } from '../../core/lib/auxiliary/rubberBand.js';
4
+ import { jest } from '@jest/globals';
5
+
6
+ new Core();
7
+
8
+ // ─── helpers ─────────────────────────────────────────────────────────────────
9
+
10
+ /**
11
+ * Build a mock renderer that records calls to setColour, setLineWidth, setDash, drawShape.
12
+ * @return {object} mock renderer
13
+ */
14
+ function makeRenderer() {
15
+ return {
16
+ setColour: jest.fn(),
17
+ setLineWidth: jest.fn(),
18
+ setDash: jest.fn(),
19
+ drawShape: jest.fn(),
20
+ };
21
+ }
22
+
23
+ // ─── constructor ─────────────────────────────────────────────────────────────
24
+
25
+ test('RubberBand constructor stores points and defaults', () => {
26
+ const pts = [new Point(0, 0), new Point(10, 0)];
27
+ const rb = new RubberBand(pts);
28
+
29
+ expect(rb.points).toBe(pts);
30
+ expect(rb.lineWidth).toBe(1.5);
31
+ expect(rb.dashPattern).toEqual([12, 6]);
32
+ });
33
+
34
+ // ─── draw – guard ─────────────────────────────────────────────────────────────
35
+
36
+ test('RubberBand.draw does nothing when points.length < 2', () => {
37
+ const rb = new RubberBand([new Point(0, 0)]);
38
+ const renderer = makeRenderer();
39
+ expect(() => rb.draw(renderer, 1)).not.toThrow();
40
+ expect(renderer.drawShape).not.toHaveBeenCalled();
41
+ });
42
+
43
+ test('RubberBand.draw does nothing when points is empty', () => {
44
+ const rb = new RubberBand([]);
45
+ const renderer = makeRenderer();
46
+ expect(() => rb.draw(renderer, 1)).not.toThrow();
47
+ expect(renderer.drawShape).not.toHaveBeenCalled();
48
+ });
49
+
50
+ // ─── draw ─────────────────────────────────────────────────────────────────────
51
+
52
+ test('RubberBand.draw with 2 points calls drawShape with the points array', () => {
53
+ const pts = [new Point(0, 0), new Point(10, 5)];
54
+ const rb = new RubberBand(pts);
55
+ const renderer = makeRenderer();
56
+
57
+ rb.draw(renderer, 1);
58
+
59
+ expect(renderer.setColour).toHaveBeenCalledTimes(1);
60
+ expect(renderer.setDash).toHaveBeenCalledTimes(1);
61
+ expect(renderer.drawShape).toHaveBeenCalledWith(pts);
62
+ expect(renderer.drawShape).toHaveBeenCalledTimes(1);
63
+ });
64
+
65
+ test('RubberBand.draw scales lineWidth and dashPattern by 1/scale', () => {
66
+ const scale = 2;
67
+ const rb = new RubberBand([new Point(0, 0), new Point(10, 0)]);
68
+ const renderer = makeRenderer();
69
+
70
+ rb.draw(renderer, scale);
71
+
72
+ expect(renderer.setLineWidth).toHaveBeenCalledWith(rb.lineWidth / scale);
73
+ expect(renderer.setDash).toHaveBeenCalledWith([12 / scale, 6 / scale], 0);
74
+ });
75
+
76
+ test('RubberBand.draw with 3 points passes all 3 points to drawShape', () => {
77
+ const pts = [new Point(0, 0), new Point(5, 5), new Point(10, 0)];
78
+ const rb = new RubberBand(pts);
79
+ const renderer = makeRenderer();
80
+
81
+ rb.draw(renderer, 1);
82
+
83
+ expect(renderer.drawShape).toHaveBeenCalledWith(pts);
84
+ });
@@ -40,8 +40,7 @@ test('polar and ortho are mutually exclusive', () => {
40
40
  test('Settings constructor - colour defaults', () => {
41
41
  const fresh = new (settings.constructor)();
42
42
  expect(fresh.canvasbackgroundcolour).toEqual({ r: 30, g: 30, b: 30 });
43
- expect(fresh.selecteditemscolour).toEqual({ r: 0, g: 255, b: 0 });
44
- expect(fresh.snaptrackingcolour).toEqual({ r: 55, g: 180, b: 75 });
43
+ expect(fresh.accentcolour).toEqual({ r: 55, g: 180, b: 75 });
45
44
  expect(fresh.gridcolour).toEqual({ r: 119, g: 118, b: 123 });
46
45
  expect(fresh.selectionWindow).toEqual({ r: 0, g: 255, b: 0 });
47
46
  expect(fresh.crossingWindow).toEqual({ r: 0, g: 0, b: 255 });
@@ -82,9 +81,9 @@ test('setSetting with false value does not trigger polar/ortho exclusion', () =>
82
81
  });
83
82
 
84
83
  test('setSetting updates arbitrary colour settings', () => {
85
- settings.setSetting('snaptrackingcolour', { r: 1, g: 2, b: 3 });
86
- expect(settings.snaptrackingcolour).toEqual({ r: 1, g: 2, b: 3 });
87
- settings.setSetting('snaptrackingcolour', { r: 55, g: 180, b: 75 });
84
+ settings.setSetting('accentcolour', { r: 1, g: 2, b: 3 });
85
+ expect(settings.accentcolour).toEqual({ r: 1, g: 2, b: 3 });
86
+ settings.setSetting('accentcolour', { r: 55, g: 180, b: 75 });
88
87
  });
89
88
 
90
89
  test('setSetting updates polarangle', () => {
@@ -116,7 +115,7 @@ test('getSetting returns current value after setSetting', () => {
116
115
  });
117
116
 
118
117
  test('getSetting returns correct type for colour objects', () => {
119
- const colour = settings.getSetting('snaptrackingcolour');
118
+ const colour = settings.getSetting('accentcolour');
120
119
  expect(colour).toHaveProperty('r');
121
120
  expect(colour).toHaveProperty('g');
122
121
  expect(colour).toHaveProperty('b');