@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
@@ -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.applySharpTrim();
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.applySharpTrim();
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.applySharpTrim();
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.applySharpTrim();
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.applySharpTrim();
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.applySharpTrim();
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.applySharpTrim();
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.applySharpTrim();
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.applySharpTrim();
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
+ });
@@ -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
+ });