@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.
- package/.eslintrc.json +4 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/dimensions/alignedDimension.js +5 -4
- package/core/dimensions/angularDimension.js +6 -5
- package/core/dimensions/baseCircularDimension.js +3 -2
- package/core/dimensions/baseDimension.js +59 -93
- package/core/dimensions/baseLinearDimension.js +6 -5
- package/core/dimensions/diametricDimension.js +4 -3
- package/core/dimensions/dimension.js +21 -14
- package/core/dimensions/radialDimension.js +4 -3
- package/core/dimensions/rotatedDimension.js +12 -11
- package/core/entities/arc.js +112 -145
- package/core/entities/arctext.js +212 -174
- package/core/entities/basePolyline.js +30 -282
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +322 -249
- package/core/entities/insert.js +66 -81
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/polyline.js +4 -4
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +188 -199
- package/core/lib/auxiliary/rubberBand.js +32 -0
- package/core/lib/auxiliary/selectionWindow.js +63 -0
- package/core/lib/auxiliary/snapPoint.js +113 -0
- package/core/lib/auxiliary/trackingLine.js +76 -0
- package/core/lib/canvas.js +228 -166
- package/core/lib/clipboard.js +7 -1
- package/core/lib/colour.js +16 -0
- package/core/lib/commandLine.js +1 -6
- package/core/lib/commandManager.js +18 -8
- package/core/lib/constants.js +28 -0
- package/core/lib/dxf/dxf.js +4 -3
- package/core/lib/entityManager.js +18 -11
- package/core/lib/inputManager.js +35 -11
- package/core/lib/intersect.js +25 -0
- package/core/lib/patterns.js +8 -1
- package/core/lib/plotOptions.js +67 -0
- package/core/lib/polylineUtils.js +75 -0
- package/core/lib/renderers/cairoRenderer.js +371 -0
- package/core/lib/renderers/canvasRenderer.js +315 -0
- package/core/lib/renderers/pdfRenderer.js +366 -0
- package/core/lib/renderers/rendererBase.js +284 -0
- package/core/lib/renderers/svgRenderer.js +343 -0
- package/core/lib/scene.js +27 -5
- package/core/lib/selectionManager.js +2 -2
- package/core/lib/settings.js +4 -2
- package/core/lib/snapping.js +4 -191
- package/core/lib/stateManager.js +30 -1
- package/core/lib/utils.js +21 -2
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +29 -13
- package/core/tables/block.js +4 -37
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tools/chamfer.js +111 -137
- package/core/tools/chamferFilletBase.js +135 -34
- package/core/tools/copy.js +3 -3
- package/core/tools/explode.js +2 -2
- package/core/tools/extend.js +132 -19
- package/core/tools/fillet.js +86 -183
- package/core/tools/matchProp.js +2 -11
- package/core/tools/mirror.js +7 -6
- package/core/tools/move.js +3 -3
- package/core/tools/offset.js +3 -5
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/rotate.js +6 -3
- package/core/tools/scale.js +1 -1
- package/core/tools/trim.js +265 -23
- package/core/tools/zoom.js +1 -1
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +14 -7
- package/test/dimensions/angularDimension.test.js +6 -6
- package/test/dimensions/baseDimension.test.js +69 -8
- package/test/dimensions/baseLinearDimension.test.js +62 -0
- package/test/dimensions/diametricDimension.test.js +6 -6
- package/test/dimensions/radialDimension.test.js +6 -6
- package/test/dimensions/rotatedDimension.test.js +14 -7
- package/test/entities/arc.test.js +80 -83
- package/test/entities/arctext.test.js +180 -53
- package/test/entities/basePolyline.test.js +68 -2
- package/test/entities/circle.test.js +57 -85
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +543 -70
- package/test/entities/insert.test.js +96 -1
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +294 -69
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +2 -2
- package/test/integration/intersectionIntegration.test.js +4 -5
- package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
- package/test/integration/pdfRendererIntegration.test.js +61 -0
- package/test/integration/svgRendererIntegration.reference.svg +3343 -0
- package/test/integration/svgRendererIntegration.test.js +61 -0
- package/test/integration/userInputIntegration.reference.dxf +1165 -105
- package/test/integration/userInputIntegration.test.js +239 -132
- package/test/lib/canvas.test.js +742 -123
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +80 -19
- package/test/lib/intersect.test.js +83 -0
- package/test/lib/plotOptions.test.js +179 -0
- package/test/lib/rubberBand.test.js +84 -0
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +71 -129
- package/test/lib/stateManager.test.js +7 -7
- package/test/lib/utils.test.js +36 -2
- package/test/properties/entityProperties.test.js +132 -0
- package/test/properties/property.test.js +53 -0
- package/test/properties/propertyManager.test.js +3 -3
- package/test/renderers/canvasRenderer.test.js +317 -0
- package/test/renderers/pdfRenderer.test.js +271 -0
- package/test/renderers/rendererBase.test.js +213 -0
- package/test/renderers/svgRenderer.test.js +326 -0
- package/test/tables/block.test.js +31 -1
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +152 -0
- package/test/tools/chamfer.test.js +227 -5
- package/test/tools/chamferFilletBase.test.js +98 -9
- package/test/tools/copy.test.js +2 -2
- package/test/tools/explode.test.js +3 -3
- package/test/tools/extend.test.js +331 -0
- package/test/tools/fillet.test.js +181 -0
- package/test/tools/matchProp.test.js +5 -5
- package/test/tools/mirror.test.js +13 -13
- package/test/tools/offset.test.js +170 -3
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/rotate.test.js +32 -2
- package/test/tools/scale.test.js +5 -5
- package/test/tools/trim.test.js +526 -0
- package/test/tools/zoom.test.js +7 -7
- 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
|
|
544
|
+
const commitEntitySpy = jest.spyOn(core.scene, 'commitEntity').mockReturnValue(1);
|
|
513
545
|
const line = new Line();
|
|
514
546
|
inputManager.executeCommand(line);
|
|
515
|
-
expect(
|
|
547
|
+
expect(commitEntitySpy).toHaveBeenCalled();
|
|
516
548
|
expect(inputManager.activeCommand).toBeUndefined();
|
|
517
|
-
|
|
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
|
|
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
|
-
|
|
770
|
+
commitEntitySpy.mockRestore();
|
|
736
771
|
inputManager.reset();
|
|
737
772
|
});
|
|
738
773
|
|
|
739
|
-
test('actionCommand passes index parameter to
|
|
774
|
+
test('actionCommand passes index parameter to commitEntity', () => {
|
|
740
775
|
inputManager.reset();
|
|
741
776
|
inputManager.onCommand('Line');
|
|
742
|
-
const
|
|
777
|
+
const commitEntitySpy = jest.spyOn(core.scene, 'commitEntity').mockReturnValue(5);
|
|
743
778
|
const line = new Line();
|
|
744
779
|
inputManager.actionCommand(line, 5);
|
|
745
|
-
expect(
|
|
746
|
-
|
|
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
|
|
823
|
+
test('mouseMoved calls preview even when entity is being highlighted', () => {
|
|
789
824
|
inputManager.reset();
|
|
790
|
-
//
|
|
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).
|
|
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
|
|
1006
|
-
// pre-populate
|
|
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.
|
|
1046
|
+
DesignCore.Scene.previewEntities.add({ draw: () => {} });
|
|
1047
|
+
DesignCore.Scene.hoverEntities.add({ draw: () => {} });
|
|
1010
1048
|
inputManager.mouseMoved();
|
|
1011
|
-
//
|
|
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.
|
|
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('
|
|
86
|
-
expect(settings.
|
|
87
|
-
settings.setSetting('
|
|
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('
|
|
118
|
+
const colour = settings.getSetting('accentcolour');
|
|
120
119
|
expect(colour).toHaveProperty('r');
|
|
121
120
|
expect(colour).toHaveProperty('g');
|
|
122
121
|
expect(colour).toHaveProperty('b');
|