@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
|
@@ -167,7 +167,7 @@ test('ChamferFilletBase.applySharpTrim trims two Lines to the intersection point
|
|
|
167
167
|
chamfer.secondPick.clickPoint = new Point(8, -7); // projects to (10,-7) → lineKeptEnd = (10,-10)
|
|
168
168
|
chamfer.intersectionPoint = new Point(10, 0);
|
|
169
169
|
|
|
170
|
-
const stateChanges = chamfer.
|
|
170
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
171
171
|
DesignCore.Scene.commit(stateChanges);
|
|
172
172
|
|
|
173
173
|
const updatedA = core.scene.entities.get(0);
|
|
@@ -198,7 +198,7 @@ test('ChamferFilletBase.applySharpTrim poly+poly consecutive segments: replaces
|
|
|
198
198
|
chamfer.secondPick.segmentIndex = 2;
|
|
199
199
|
chamfer.intersectionPoint = new Point(0, 0);
|
|
200
200
|
|
|
201
|
-
const stateChanges = chamfer.
|
|
201
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
202
202
|
DesignCore.Scene.commit(stateChanges);
|
|
203
203
|
|
|
204
204
|
const poly = core.scene.entities.get(0);
|
|
@@ -222,7 +222,7 @@ test('ChamferFilletBase.applySharpTrim poly+poly open ends: moves both endpoints
|
|
|
222
222
|
chamfer.secondPick.segmentIndex = 3; // open end (lastIdx = 3)
|
|
223
223
|
chamfer.intersectionPoint = new Point(3, 2);
|
|
224
224
|
|
|
225
|
-
const stateChanges = chamfer.
|
|
225
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
226
226
|
DesignCore.Scene.commit(stateChanges);
|
|
227
227
|
|
|
228
228
|
const poly = core.scene.entities.get(0);
|
|
@@ -258,7 +258,7 @@ test('ChamferFilletBase.applySharpTrim poly+poly closed polyline seg1+lastIdx: c
|
|
|
258
258
|
chamfer.secondPick.segmentIndex = polyEntity.points.length - 1; // lastIdx
|
|
259
259
|
chamfer.intersectionPoint = new Point(0, 0);
|
|
260
260
|
|
|
261
|
-
const stateChanges = chamfer.
|
|
261
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
262
262
|
DesignCore.Scene.commit(stateChanges);
|
|
263
263
|
|
|
264
264
|
// Corner-splice: replaces vertex at cornerIdx=1 with intersectionPoint — point count unchanged
|
|
@@ -286,7 +286,7 @@ test('ChamferFilletBase.applySharpTrim closed poly last-regular-seg + closing-se
|
|
|
286
286
|
chamfer.secondPick.segmentIndex = 4; // closing segment (points.length)
|
|
287
287
|
chamfer.intersectionPoint = new Point(0, 10);
|
|
288
288
|
|
|
289
|
-
const stateChanges = chamfer.
|
|
289
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
290
290
|
DesignCore.Scene.commit(stateChanges);
|
|
291
291
|
|
|
292
292
|
const poly = core.scene.entities.get(0);
|
|
@@ -314,7 +314,7 @@ test('ChamferFilletBase.applySharpTrim closed poly closing-seg + seg-1: replaces
|
|
|
314
314
|
chamfer.secondPick.segmentIndex = 1;
|
|
315
315
|
chamfer.intersectionPoint = new Point(0, 0);
|
|
316
316
|
|
|
317
|
-
const stateChanges = chamfer.
|
|
317
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
318
318
|
DesignCore.Scene.commit(stateChanges);
|
|
319
319
|
|
|
320
320
|
const poly = core.scene.entities.get(0);
|
|
@@ -354,7 +354,7 @@ test('ChamferFilletBase.applySharpTrim first=Line second=Poly keepStart: line co
|
|
|
354
354
|
chamfer.intersectionPoint = new Point(0, 0);
|
|
355
355
|
|
|
356
356
|
|
|
357
|
-
const stateChanges = chamfer.
|
|
357
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
358
358
|
DesignCore.Scene.commit(stateChanges);
|
|
359
359
|
|
|
360
360
|
expect(core.scene.entities.count()).toBe(1);
|
|
@@ -392,7 +392,7 @@ test('ChamferFilletBase.applySharpTrim first=Line second=Poly keepEnd: line cons
|
|
|
392
392
|
chamfer.intersectionPoint = new Point(0, 0);
|
|
393
393
|
|
|
394
394
|
|
|
395
|
-
const stateChanges = chamfer.
|
|
395
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
396
396
|
DesignCore.Scene.commit(stateChanges);
|
|
397
397
|
|
|
398
398
|
expect(core.scene.entities.count()).toBe(1);
|
|
@@ -431,7 +431,7 @@ test('ChamferFilletBase.applySharpTrim first=Poly second=Line: poly/line orderin
|
|
|
431
431
|
chamfer.intersectionPoint = new Point(0, 0);
|
|
432
432
|
|
|
433
433
|
|
|
434
|
-
const stateChanges = chamfer.
|
|
434
|
+
const stateChanges = chamfer.applyCornerTrim(chamfer.intersectionPoint, chamfer.intersectionPoint, null);
|
|
435
435
|
DesignCore.Scene.commit(stateChanges);
|
|
436
436
|
|
|
437
437
|
expect(core.scene.entities.count()).toBe(1);
|
|
@@ -444,3 +444,92 @@ test('ChamferFilletBase.applySharpTrim first=Poly second=Line: poly/line orderin
|
|
|
444
444
|
expect(poly.points[3].x).toBeCloseTo(0);
|
|
445
445
|
expect(poly.points[3].y).toBeCloseTo(10);
|
|
446
446
|
});
|
|
447
|
+
|
|
448
|
+
// ─── validateSelection ────────────────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
test('ChamferFilletBase.validateSelection returns null when firstPick has no entity', () => {
|
|
451
|
+
const chamfer = new Chamfer();
|
|
452
|
+
expect(chamfer.validateSelection()).toBeNull();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('ChamferFilletBase.validateSelection returns null when firstPick entity is not a Line', () => {
|
|
456
|
+
core.scene.clear();
|
|
457
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
|
|
458
|
+
|
|
459
|
+
const chamfer = new Chamfer();
|
|
460
|
+
chamfer.firstPick.entity = core.scene.entities.get(0); // Circle — resolveEndpoints fails
|
|
461
|
+
|
|
462
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
|
|
463
|
+
expect(chamfer.validateSelection()).toBeNull();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('ChamferFilletBase.validateSelection returns null when findClosestItem returns undefined', () => {
|
|
467
|
+
core.scene.clear();
|
|
468
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
469
|
+
|
|
470
|
+
const chamfer = new Chamfer();
|
|
471
|
+
chamfer.firstPick.entity = core.scene.entities.get(0);
|
|
472
|
+
chamfer.firstPick.clickPoint = new Point(-5, 0);
|
|
473
|
+
|
|
474
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
|
|
475
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
|
|
476
|
+
const result = chamfer.validateSelection();
|
|
477
|
+
findSpy.mockRestore();
|
|
478
|
+
|
|
479
|
+
expect(result).toBeNull();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('ChamferFilletBase.validateSelection returns null when hovered entity is not a Line or BasePolyline', () => {
|
|
483
|
+
core.scene.clear();
|
|
484
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
485
|
+
core.scene.addItem('Circle', { points: [new Point(5, 5), new Point(10, 5)] });
|
|
486
|
+
|
|
487
|
+
const chamfer = new Chamfer();
|
|
488
|
+
chamfer.firstPick.entity = core.scene.entities.get(0);
|
|
489
|
+
chamfer.firstPick.clickPoint = new Point(-5, 0);
|
|
490
|
+
|
|
491
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
492
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
|
|
493
|
+
const result = chamfer.validateSelection();
|
|
494
|
+
findSpy.mockRestore();
|
|
495
|
+
|
|
496
|
+
expect(result).toBeNull();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('ChamferFilletBase.validateSelection returns null when first and second segments are parallel', () => {
|
|
500
|
+
core.scene.clear();
|
|
501
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
502
|
+
core.scene.addItem('Line', { points: [new Point(0, 2), new Point(10, 2)] }); // parallel horizontal
|
|
503
|
+
|
|
504
|
+
const chamfer = new Chamfer();
|
|
505
|
+
chamfer.firstPick.entity = core.scene.entities.get(0);
|
|
506
|
+
chamfer.firstPick.clickPoint = new Point(-5, 0);
|
|
507
|
+
|
|
508
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
509
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 2);
|
|
510
|
+
const result = chamfer.validateSelection();
|
|
511
|
+
findSpy.mockRestore();
|
|
512
|
+
|
|
513
|
+
expect(result).toBeNull();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('ChamferFilletBase.validateSelection returns selection object for valid perpendicular lines', () => {
|
|
517
|
+
core.scene.clear();
|
|
518
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] }); // horizontal
|
|
519
|
+
core.scene.addItem('Line', { points: [new Point(5, -5), new Point(5, 5)] }); // vertical at x=5
|
|
520
|
+
|
|
521
|
+
const chamfer = new Chamfer();
|
|
522
|
+
chamfer.firstPick.entity = core.scene.entities.get(0);
|
|
523
|
+
chamfer.firstPick.clickPoint = new Point(-5, 0);
|
|
524
|
+
|
|
525
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
526
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 2);
|
|
527
|
+
const result = chamfer.validateSelection();
|
|
528
|
+
findSpy.mockRestore();
|
|
529
|
+
|
|
530
|
+
expect(result).not.toBeNull();
|
|
531
|
+
expect(result.candidate).toBe(core.scene.entities.get(1));
|
|
532
|
+
expect(result.intersectionPoint.x).toBeCloseTo(5);
|
|
533
|
+
expect(result.intersectionPoint.y).toBeCloseTo(0);
|
|
534
|
+
expect(result.tempSecond).toBeDefined();
|
|
535
|
+
});
|
package/test/tools/copy.test.js
CHANGED
|
@@ -44,7 +44,7 @@ test('Test Copy.action', () => {
|
|
|
44
44
|
for (let i = 0; i < 6; i++) {
|
|
45
45
|
const original = core.scene.entities.get(i);
|
|
46
46
|
const copy = core.scene.entities.get(i + 6);
|
|
47
|
-
expect(copy.handle).toBeDefined();
|
|
48
|
-
expect(copy.handle).not.toBe(original.handle);
|
|
47
|
+
expect(copy.getProperty('handle')).toBeDefined();
|
|
48
|
+
expect(copy.getProperty('handle')).not.toBe(original.getProperty('handle'));
|
|
49
49
|
}
|
|
50
50
|
});
|
|
@@ -38,8 +38,8 @@ test('Test Explode.action', () => {
|
|
|
38
38
|
expect(item.points[0].y).toBe(i * 10 + insertPoint.y);
|
|
39
39
|
|
|
40
40
|
// Verify each exploded entity has a unique handle
|
|
41
|
-
expect(item.handle).toBeDefined();
|
|
42
|
-
expect(handles.has(item.handle)).toBe(false);
|
|
43
|
-
handles.add(item.handle);
|
|
41
|
+
expect(item.getProperty('handle')).toBeDefined();
|
|
42
|
+
expect(handles.has(item.getProperty('handle'))).toBe(false);
|
|
43
|
+
handles.add(item.getProperty('handle'));
|
|
44
44
|
}
|
|
45
45
|
});
|
|
@@ -2,6 +2,8 @@ import { Core } from '../../core/core/core.js';
|
|
|
2
2
|
import { Point } from '../../core/entities/point.js';
|
|
3
3
|
import { Extend } from '../../core/tools/extend.js';
|
|
4
4
|
import { SingleSelection } from '../../core/lib/selectionManager.js';
|
|
5
|
+
import { Strings } from '../../core/lib/strings.js';
|
|
6
|
+
import { DesignCore } from '../../core/designCore.js';
|
|
5
7
|
import { expect, jest } from '@jest/globals';
|
|
6
8
|
import { withMockInput } from '../test-helpers/test-helpers.js';
|
|
7
9
|
|
|
@@ -393,3 +395,332 @@ test('Test Extend.action polyline - no intersections', () => {
|
|
|
393
395
|
expect(polyline.points[0].x).toBe(0);
|
|
394
396
|
expect(polyline.points[1].x).toBe(50);
|
|
395
397
|
});
|
|
398
|
+
|
|
399
|
+
test('Extend.register returns correct metadata', () => {
|
|
400
|
+
const reg = Extend.register();
|
|
401
|
+
expect(reg.command).toBe('Extend');
|
|
402
|
+
expect(reg.shortcut).toBe('EX');
|
|
403
|
+
expect(reg.type).toBe('Tool');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('Extend.preview does not throw', () => {
|
|
407
|
+
const extend = new Extend();
|
|
408
|
+
expect(() => extend.preview()).not.toThrow();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('Extend.preview returns early when no boundary items set', () => {
|
|
412
|
+
core.scene.clear();
|
|
413
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] });
|
|
414
|
+
DesignCore.Scene.previewEntities.clear();
|
|
415
|
+
|
|
416
|
+
const extend = new Extend();
|
|
417
|
+
// selectedBoundaryItems is empty by default
|
|
418
|
+
extend.preview();
|
|
419
|
+
|
|
420
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('Extend.preview returns early when findClosestItem returns undefined', () => {
|
|
424
|
+
core.scene.clear();
|
|
425
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] });
|
|
426
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(50, 0)] });
|
|
427
|
+
DesignCore.Scene.previewEntities.clear();
|
|
428
|
+
|
|
429
|
+
const extend = new Extend();
|
|
430
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
431
|
+
|
|
432
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
|
|
433
|
+
extend.preview();
|
|
434
|
+
findSpy.mockRestore();
|
|
435
|
+
|
|
436
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('Extend.preview returns early when hovered entity does not implement extend', () => {
|
|
440
|
+
// Circle does not override Entity.extend() — preview must not emit a notification.
|
|
441
|
+
core.scene.clear();
|
|
442
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
|
|
443
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] }); // unsupported
|
|
444
|
+
DesignCore.Scene.previewEntities.clear();
|
|
445
|
+
|
|
446
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
447
|
+
const extend = new Extend();
|
|
448
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
449
|
+
|
|
450
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
451
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 0);
|
|
452
|
+
extend.preview();
|
|
453
|
+
findSpy.mockRestore();
|
|
454
|
+
notifySpy.mockRestore();
|
|
455
|
+
|
|
456
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
457
|
+
expect(notifySpy).not.toHaveBeenCalled();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('Extend.preview returns early when hovered entity has no valid extension', () => {
|
|
461
|
+
// Hovered line is parallel to the boundary — no intersection ahead of its endpoint.
|
|
462
|
+
core.scene.clear();
|
|
463
|
+
core.scene.addItem('Line', { points: [new Point(0, 50), new Point(100, 50)] }); // boundary (horizontal)
|
|
464
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(50, 0)] }); // hovered (parallel, y=0)
|
|
465
|
+
DesignCore.Scene.previewEntities.clear();
|
|
466
|
+
|
|
467
|
+
const extend = new Extend();
|
|
468
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
469
|
+
|
|
470
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
471
|
+
DesignCore.Mouse.pointOnScene = () => new Point(45, 0);
|
|
472
|
+
extend.preview();
|
|
473
|
+
findSpy.mockRestore();
|
|
474
|
+
|
|
475
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('Extend.preview populates previewEntities when entity can be extended', () => {
|
|
479
|
+
// Vertical boundary at x=100; short horizontal line from (0,0) to (50,0).
|
|
480
|
+
// Mouse near end (45,0) → end extends to (100,0).
|
|
481
|
+
core.scene.clear();
|
|
482
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
|
|
483
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(50, 0)] }); // hovered
|
|
484
|
+
DesignCore.Scene.previewEntities.clear();
|
|
485
|
+
|
|
486
|
+
const extend = new Extend();
|
|
487
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
488
|
+
|
|
489
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
490
|
+
DesignCore.Mouse.pointOnScene = () => new Point(45, 0);
|
|
491
|
+
extend.preview();
|
|
492
|
+
findSpy.mockRestore();
|
|
493
|
+
|
|
494
|
+
// Preview of the extended line should be added
|
|
495
|
+
expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
|
|
496
|
+
// The previewed line should reach the boundary
|
|
497
|
+
const preview = DesignCore.Scene.previewEntities.get(DesignCore.Scene.previewEntities.count() - 1);
|
|
498
|
+
expect(preview.points[1].x).toBeCloseTo(100);
|
|
499
|
+
expect(preview.points[1].y).toBeCloseTo(0);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('Extend.execute returns early when boundary input is undefined', async () => {
|
|
503
|
+
core.scene.clear();
|
|
504
|
+
core.scene.selectionManager.reset();
|
|
505
|
+
core.scene.addItem('Line', { points: [new Point(), new Point(10, 0)] });
|
|
506
|
+
|
|
507
|
+
await withMockInput(core.scene, [undefined], async () => {
|
|
508
|
+
const extend = new Extend();
|
|
509
|
+
await extend.execute();
|
|
510
|
+
expect(extend.selectedBoundaryItems).toHaveLength(0);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test('Extend.execute exits selection loop when selection input is undefined', async () => {
|
|
515
|
+
core.scene.clear();
|
|
516
|
+
core.scene.selectionManager.reset();
|
|
517
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] });
|
|
518
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(50, 0)] });
|
|
519
|
+
core.scene.selectionManager.selectionSet.selectionSet.push(0);
|
|
520
|
+
|
|
521
|
+
const executeCommandSpy = jest.fn();
|
|
522
|
+
await withMockInput(core.scene, [undefined], async () => {
|
|
523
|
+
const extend = new Extend();
|
|
524
|
+
await extend.execute();
|
|
525
|
+
expect(extend.selectedBoundaryItems).toHaveLength(1);
|
|
526
|
+
expect(executeCommandSpy).toHaveBeenCalled();
|
|
527
|
+
}, { extraMethods: { executeCommand: () => executeCommandSpy() } });
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test('Extend.execute calls actionCommand once per extend selection', async () => {
|
|
531
|
+
core.scene.clear();
|
|
532
|
+
core.scene.selectionManager.reset();
|
|
533
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] });
|
|
534
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(50, 0)] });
|
|
535
|
+
core.scene.addItem('Line', { points: [new Point(0, 5), new Point(50, 5)] });
|
|
536
|
+
core.scene.selectionManager.selectionSet.selectionSet.push(0);
|
|
537
|
+
|
|
538
|
+
const actionSpy = jest.fn();
|
|
539
|
+
await withMockInput(
|
|
540
|
+
core.scene,
|
|
541
|
+
[new SingleSelection(1, new Point(45, 0)), new SingleSelection(2, new Point(45, 5)), undefined],
|
|
542
|
+
async () => {
|
|
543
|
+
const extend = new Extend();
|
|
544
|
+
await extend.execute();
|
|
545
|
+
expect(actionSpy).toHaveBeenCalledTimes(2);
|
|
546
|
+
},
|
|
547
|
+
{ extraMethods: { actionCommand: () => actionSpy() } },
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('Extend.action does nothing when selectedItem is null', () => {
|
|
552
|
+
core.scene.clear();
|
|
553
|
+
core.scene.addItem('Line', { points: [new Point(), new Point(10, 0)] });
|
|
554
|
+
const extend = new Extend();
|
|
555
|
+
extend.selectedItem = null;
|
|
556
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
557
|
+
|
|
558
|
+
expect(() => extend.action()).not.toThrow();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('Extend.action does nothing when selectedBoundaryItems is empty', () => {
|
|
562
|
+
core.scene.clear();
|
|
563
|
+
core.scene.addItem('Line', { points: [new Point(), new Point(10, 0)] });
|
|
564
|
+
const extend = new Extend();
|
|
565
|
+
extend.selectedItem = core.scene.entities.get(0);
|
|
566
|
+
extend.selectedBoundaryItems = [];
|
|
567
|
+
|
|
568
|
+
expect(() => extend.action()).not.toThrow();
|
|
569
|
+
expect(extend.selectedItem).toBeNull();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('Extend.action notifies NOEXTEND when selected entity lacks fromPolylinePoints', () => {
|
|
573
|
+
// Use a stub entity that has toPolylinePoints but no fromPolylinePoints.
|
|
574
|
+
// action() must notify and not throw.
|
|
575
|
+
core.scene.clear();
|
|
576
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
|
|
577
|
+
|
|
578
|
+
const extend = new Extend();
|
|
579
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
580
|
+
extend.selectedItem = {
|
|
581
|
+
type: 'FakeEntity',
|
|
582
|
+
toPolylinePoints: () => [new Point(0, 0), new Point(50, 0)],
|
|
583
|
+
// deliberately no fromPolylinePoints
|
|
584
|
+
};
|
|
585
|
+
core.mouse.setPosFromScenePoint(new Point(45, 0));
|
|
586
|
+
|
|
587
|
+
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
588
|
+
extend.action();
|
|
589
|
+
|
|
590
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
|
|
591
|
+
expect(extend.selectedItem).toBeNull();
|
|
592
|
+
notifySpy.mockRestore();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('Extend.action notifies NOEXTEND when selected entity is a closed polyline', () => {
|
|
596
|
+
// Closed polylines have no open endpoints — extend must notify and leave the entity unchanged.
|
|
597
|
+
core.scene.clear();
|
|
598
|
+
core.scene.addItem('Line', { points: [new Point(200, -10), new Point(200, 10)] }); // boundary
|
|
599
|
+
// Closed square: flag bit 1 set
|
|
600
|
+
core.scene.addItem('Lwpolyline', {
|
|
601
|
+
points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)],
|
|
602
|
+
flags: 1,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
const extend = new Extend();
|
|
606
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
607
|
+
extend.selectedItem = core.scene.entities.get(1);
|
|
608
|
+
core.mouse.setPosFromScenePoint(new Point(95, 0));
|
|
609
|
+
|
|
610
|
+
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
611
|
+
extend.action();
|
|
612
|
+
|
|
613
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
|
|
614
|
+
expect(extend.selectedItem).toBeNull();
|
|
615
|
+
notifySpy.mockRestore();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('Extend.preview silently skips entity that lacks fromPolylinePoints', () => {
|
|
619
|
+
// Stub entity implements toPolylinePoints but not fromPolylinePoints.
|
|
620
|
+
// preview() must return early without calling notify.
|
|
621
|
+
core.scene.clear();
|
|
622
|
+
core.scene.addItem('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
|
|
623
|
+
DesignCore.Scene.previewEntities.clear();
|
|
624
|
+
|
|
625
|
+
const stubEntity = {
|
|
626
|
+
type: 'FakeEntity',
|
|
627
|
+
toPolylinePoints: () => [new Point(0, 0), new Point(50, 0)],
|
|
628
|
+
// deliberately no fromPolylinePoints
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
632
|
+
const extend = new Extend();
|
|
633
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
634
|
+
|
|
635
|
+
const savedPointOnScene = DesignCore.Mouse.pointOnScene;
|
|
636
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
|
|
637
|
+
jest.spyOn(core.scene.entities, 'get').mockReturnValue(stubEntity);
|
|
638
|
+
DesignCore.Mouse.pointOnScene = () => new Point(45, 0);
|
|
639
|
+
extend.preview();
|
|
640
|
+
findSpy.mockRestore();
|
|
641
|
+
jest.restoreAllMocks();
|
|
642
|
+
DesignCore.Mouse.pointOnScene = savedPointOnScene;
|
|
643
|
+
|
|
644
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
645
|
+
expect(notifySpy).not.toHaveBeenCalled();
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('Extend.action notifies when no intersection found (parallel lines)', () => {
|
|
649
|
+
core.scene.clear();
|
|
650
|
+
// Two parallel horizontal lines — no intersection possible
|
|
651
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(100, 0)] });
|
|
652
|
+
core.scene.addItem('Line', { points: [new Point(0, 50), new Point(50, 50)] });
|
|
653
|
+
|
|
654
|
+
const extend = new Extend();
|
|
655
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
656
|
+
extend.selectedItem = core.scene.entities.get(1);
|
|
657
|
+
core.mouse.setPosFromScenePoint(new Point(40, 50));
|
|
658
|
+
|
|
659
|
+
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
660
|
+
extend.action();
|
|
661
|
+
|
|
662
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
|
|
663
|
+
expect(extend.selectedItem).toBeNull();
|
|
664
|
+
notifySpy.mockRestore();
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('Extend.action notifies when boundary item equals selected item', () => {
|
|
668
|
+
core.scene.clear();
|
|
669
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(50, 0)] });
|
|
670
|
+
const entity = core.scene.entities.get(0);
|
|
671
|
+
|
|
672
|
+
const extend = new Extend();
|
|
673
|
+
extend.selectedBoundaryItems = [entity];
|
|
674
|
+
extend.selectedItem = entity;
|
|
675
|
+
core.mouse.setPosFromScenePoint(new Point(40, 0));
|
|
676
|
+
|
|
677
|
+
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
678
|
+
extend.action();
|
|
679
|
+
|
|
680
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
|
|
681
|
+
notifySpy.mockRestore();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test('Extend.action extends the start of a line', () => {
|
|
685
|
+
// Line from (50,50) to (100,50) — horizontal
|
|
686
|
+
// Boundary: vertical line at x=0
|
|
687
|
+
// Mouse near the start (50,50) → extend start back to x=0
|
|
688
|
+
core.scene.clear();
|
|
689
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 100)] });
|
|
690
|
+
core.scene.addItem('Line', { points: [new Point(50, 50), new Point(100, 50)] });
|
|
691
|
+
|
|
692
|
+
const extend = new Extend();
|
|
693
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
694
|
+
extend.selectedItem = core.scene.entities.get(1);
|
|
695
|
+
core.mouse.setPosFromScenePoint(new Point(55, 50));
|
|
696
|
+
extend.action();
|
|
697
|
+
|
|
698
|
+
expect(core.scene.entities.get(1).points[0].x).toBeCloseTo(0);
|
|
699
|
+
expect(core.scene.entities.get(1).points[0].y).toBeCloseTo(50);
|
|
700
|
+
expect(core.scene.entities.get(1).points[1].x).toBe(100);
|
|
701
|
+
expect(core.scene.entities.get(1).points[1].y).toBe(50);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('Extend.action notifies NOEXTEND when end segment of an Arc is curved', () => {
|
|
705
|
+
// A standalone Arc always has a non-zero bulge on its single segment.
|
|
706
|
+
// #extendEntity must reject the extension and notify because endSegmentBulge !== 0.
|
|
707
|
+
core.scene.clear();
|
|
708
|
+
core.scene.addItem('Arc', {
|
|
709
|
+
points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)],
|
|
710
|
+
direction: 1,
|
|
711
|
+
});
|
|
712
|
+
// Vertical boundary at x=5 — intersects the CCW quarter-arc
|
|
713
|
+
core.scene.addItem('Line', { points: [new Point(5, -20), new Point(5, 20)] });
|
|
714
|
+
|
|
715
|
+
const extend = new Extend();
|
|
716
|
+
extend.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
717
|
+
extend.selectedItem = core.scene.entities.get(0);
|
|
718
|
+
core.mouse.setPosFromScenePoint(new Point(9, 1)); // near arc start (10,0)
|
|
719
|
+
|
|
720
|
+
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
721
|
+
extend.action();
|
|
722
|
+
|
|
723
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
|
|
724
|
+
expect(extend.selectedItem).toBeNull();
|
|
725
|
+
notifySpy.mockRestore();
|
|
726
|
+
});
|