@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
@@ -1,6 +1,7 @@
1
1
  import { Core } from '../../core/core/core.js';
2
2
  import { Point } from '../../core/entities/point.js';
3
3
  import { Fillet } from '../../core/tools/fillet.js';
4
+ import { DesignCore } from '../../core/designCore.js';
4
5
  import { Strings } from '../../core/lib/strings.js';
5
6
  import { SingleSelection } from '../../core/lib/selectionManager.js';
6
7
  import { expect, jest } from '@jest/globals';
@@ -944,3 +945,183 @@ test('Fillet.action closing-wrap reversed pick order: arc bulge sign and tangent
944
945
  expect(poly.points[1].y).toBeCloseTo(0);
945
946
  expect(poly.points[0].bulge).toBeGreaterThan(0);
946
947
  });
948
+
949
+ // ─── action: entity property inheritance ─────────────────────────────────────
950
+
951
+ test('Fillet.action arc inherits layer, lineWidth, lineType from first entity', () => {
952
+ // Perpendicular L-corner at origin; first entity on a non-default layer.
953
+ core.scene.clear();
954
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)], layer: 'walls', lineWidth: 5, lineType: 'DASHED' });
955
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
956
+
957
+ core.scene.headers.filletRadius = 2;
958
+ core.scene.headers.trimMode = false;
959
+
960
+ const fillet = new Fillet();
961
+ fillet.firstPick.entity = core.scene.entities.get(0);
962
+ fillet.secondPick.entity = core.scene.entities.get(1);
963
+ fillet.firstPick.clickPoint = new Point(-5, 0);
964
+ fillet.secondPick.clickPoint = new Point(0, 5);
965
+ fillet.action();
966
+
967
+ expect(core.scene.entities.count()).toBe(3);
968
+ const arc = core.scene.entities.get(2);
969
+ expect(arc.type).toBe('Arc');
970
+ expect(arc.getProperty('layer')).toBe('walls');
971
+ expect(arc.getProperty('lineWidth')).toBe(5);
972
+ expect(arc.getProperty('lineType')).toBe('DASHED');
973
+ });
974
+
975
+ test('Fillet.preview does not throw', () => {
976
+ const fillet = new Fillet();
977
+ expect(() => fillet.preview()).not.toThrow();
978
+ });
979
+
980
+ test('Fillet.preview returns early when findClosestItem returns undefined', () => {
981
+ core.scene.clear();
982
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
983
+ DesignCore.Scene.previewEntities.clear();
984
+
985
+ const fillet = new Fillet();
986
+ fillet.firstPick.entity = core.scene.entities.get(0);
987
+ fillet.firstPick.clickPoint = new Point(-5, 0);
988
+
989
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
990
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
991
+ fillet.preview();
992
+ findSpy.mockRestore();
993
+
994
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
995
+ });
996
+
997
+ test('Fillet.preview adds fillet arc to previewEntities in no-trim mode', () => {
998
+ core.scene.clear();
999
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1000
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1001
+ DesignCore.Scene.previewEntities.clear();
1002
+
1003
+ core.scene.headers.filletRadius = 2;
1004
+ core.scene.headers.trimMode = false;
1005
+
1006
+ const fillet = new Fillet();
1007
+ fillet.firstPick.entity = core.scene.entities.get(0);
1008
+ fillet.firstPick.clickPoint = new Point(-5, 0);
1009
+
1010
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1011
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1012
+ fillet.preview();
1013
+ findSpy.mockRestore();
1014
+
1015
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
1016
+ });
1017
+
1018
+ test('Fillet.preview adds dulled segments and arc to previewEntities in trim mode', () => {
1019
+ core.scene.clear();
1020
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1021
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1022
+ DesignCore.Scene.previewEntities.clear();
1023
+
1024
+ core.scene.headers.filletRadius = 2;
1025
+ core.scene.headers.trimMode = true;
1026
+
1027
+ const fillet = new Fillet();
1028
+ fillet.firstPick.entity = core.scene.entities.get(0);
1029
+ fillet.firstPick.clickPoint = new Point(-5, 0);
1030
+
1031
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1032
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1033
+ fillet.preview();
1034
+ findSpy.mockRestore();
1035
+
1036
+ // dulled firstSeg + dulled secondSeg + firstTrimLine + secondTrimLine + arc = 5
1037
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(5);
1038
+ });
1039
+
1040
+ test('Fillet.execute returns early when second-entity input is undefined (inner loop)', async () => {
1041
+ core.scene.clear();
1042
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1043
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1044
+
1045
+ const executeCommandSpy = jest.fn();
1046
+ // Only one input: the first selection. Second requestInput returns undefined → inner loop returns.
1047
+ await withMockInput(
1048
+ core.scene,
1049
+ [new SingleSelection(0, new Point(-5, 0))],
1050
+ async () => {
1051
+ const fillet = new Fillet();
1052
+ await fillet.execute();
1053
+ expect(executeCommandSpy).not.toHaveBeenCalled();
1054
+ },
1055
+ { extraMethods: { executeCommand: executeCommandSpy } },
1056
+ );
1057
+ });
1058
+
1059
+ test('Fillet.execute notifies NONCONSECUTIVESEGMENTS for non-adjacent polyline segments', async () => {
1060
+ // 5-point polyline → 4 segments. Pick segment 1 (near -15,0) then segment 3 (near 5,0).
1061
+ // Neither consecutive nor open-ends → NONCONSECUTIVESEGMENTS, then undefined to exit.
1062
+ core.scene.clear();
1063
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0), new Point(20, 0)] });
1064
+
1065
+ const notifySpy = jest.spyOn(core, 'notify');
1066
+ await withMockInput(
1067
+ core.scene,
1068
+ [new SingleSelection(0, new Point(-15, 0)), new SingleSelection(0, new Point(5, 0)), undefined],
1069
+ async () => {
1070
+ const fillet = new Fillet();
1071
+ await fillet.execute();
1072
+ },
1073
+ );
1074
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NONCONSECUTIVESEGMENTS));
1075
+ notifySpy.mockRestore();
1076
+ });
1077
+
1078
+ test('Fillet.execute Trim→Trim keeps trimMode=true', async () => {
1079
+ core.scene.headers.trimMode = true;
1080
+
1081
+ await withMockInput(
1082
+ core.scene,
1083
+ ['Trim', 'Trim', undefined],
1084
+ async () => {
1085
+ const fillet = new Fillet();
1086
+ await fillet.execute();
1087
+ },
1088
+ );
1089
+
1090
+ expect(core.scene.headers.trimMode).toBe(true);
1091
+ });
1092
+
1093
+ // ─── regression: bulge must not leak onto Line endpoints ─────────────────────
1094
+
1095
+ test('Fillet.action two fillets on the same line: trimmed endpoints have bulge = 0', () => {
1096
+ // Line A: (-10, 0) → (10, 0) horizontal
1097
+ // Line B: (-10, 2) → (-10, -2) vertical at left end
1098
+ // Line C: ( 10, 2) → ( 10, -2) vertical at right end
1099
+ // Fillet A–B at the left corner, then A–C at the right corner.
1100
+ // arc.toPolylinePoints() sets bulge on the start point;
1101
+ // that value must not bleed onto the Line endpoints.
1102
+ core.scene.clear();
1103
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
1104
+ core.scene.addItem('Line', { points: [new Point(-10, 2), new Point(-10, -2)] });
1105
+ core.scene.addItem('Line', { points: [new Point(10, 2), new Point(10, -2)] });
1106
+
1107
+ core.scene.headers.filletRadius = 1;
1108
+ core.scene.headers.trimMode = true;
1109
+
1110
+ const fillet1 = new Fillet();
1111
+ fillet1.firstPick.entity = core.scene.entities.get(0);
1112
+ fillet1.secondPick.entity = core.scene.entities.get(1);
1113
+ fillet1.firstPick.clickPoint = new Point(-5, 0);
1114
+ fillet1.secondPick.clickPoint = new Point(-10, 1);
1115
+ fillet1.action();
1116
+
1117
+ const fillet2 = new Fillet();
1118
+ fillet2.firstPick.entity = core.scene.entities.get(0);
1119
+ fillet2.secondPick.entity = core.scene.entities.get(2);
1120
+ fillet2.firstPick.clickPoint = new Point(5, 0);
1121
+ fillet2.secondPick.clickPoint = new Point(10, 1);
1122
+ fillet2.action();
1123
+
1124
+ const line = core.scene.entities.get(0);
1125
+ expect(line.points[0].bulge).toBe(0);
1126
+ expect(line.points[1].bulge).toBe(0);
1127
+ });
@@ -69,16 +69,16 @@ test('Test MatchProp.action matches properties from source to targets', () => {
69
69
  // Validate that layer property was matched to targets
70
70
  for (let i = 0; i < match.destinationSetIndices.length; i++) {
71
71
  const tgt = core.scene.entities.get(match.destinationSetIndices[i]);
72
- expect(tgt.layer).toBe('test');
72
+ expect(tgt.getProperty('layer')).toBe('test');
73
73
  }
74
74
 
75
75
  const circle = core.scene.entities.get(1);
76
- expect(circle.radius).toBe(10); // original radius
76
+ expect(circle.getProperty('radius')).toBe(10); // original radius
77
77
 
78
78
  const arc = core.scene.entities.get(3);
79
- expect(arc.radius).toBe(10); // original radius
79
+ expect(arc.getProperty('radius')).toBe(10); // original radius
80
80
 
81
81
  const text = core.scene.entities.get(5);
82
- expect(text.height).toBe(10); // original height
83
- expect(text.string).toBe('text test'); // original string
82
+ expect(text.getProperty('height')).toBe(10); // original height
83
+ expect(text.getProperty('string')).toBe('text test'); // original string
84
84
  });
@@ -231,8 +231,8 @@ test('Test Mirror.action - keep source (default)', () => {
231
231
  for (let i = 0; i < entityCount; i++) {
232
232
  const original = core.scene.entities.get(i);
233
233
  const copy = core.scene.entities.get(i + entityCount);
234
- expect(copy.handle).toBeDefined();
235
- expect(copy.handle).not.toBe(original.handle);
234
+ expect(copy.getProperty('handle')).toBeDefined();
235
+ expect(copy.getProperty('handle')).not.toBe(original.getProperty('handle'));
236
236
  }
237
237
  });
238
238
 
@@ -310,8 +310,8 @@ test('Test Mirror.action - arc direction is reversed', () => {
310
310
  const original = core.scene.entities.get(0);
311
311
  const mirrored = core.scene.entities.get(1);
312
312
 
313
- expect(original.direction).toBe(1);
314
- expect(mirrored.direction).toBe(-1);
313
+ expect(original.getProperty('direction')).toBe(1);
314
+ expect(mirrored.getProperty('direction')).toBe(-1);
315
315
  });
316
316
 
317
317
  test('Test Mirror.getMirroredPoints', () => {
@@ -395,7 +395,7 @@ test('Mirror.preview - no points set, adds no temp entities', () => {
395
395
  // No points pushed — guard condition should prevent any work
396
396
  mirror.preview();
397
397
 
398
- expect(core.scene.tempEntities.count()).toBe(0);
398
+ expect(core.scene.auxiliaryEntities.count()).toBe(0);
399
399
  });
400
400
 
401
401
  test('Mirror.preview - one point set, draws temp mirror line using mouse position', () => {
@@ -409,8 +409,8 @@ test('Mirror.preview - one point set, draws temp mirror line using mouse positio
409
409
  mirror.points.push(new Point(3, 3)); // pt1 distinct from default mouse (0,0) scene position
410
410
  mirror.preview();
411
411
 
412
- // Temp mirror line should have been created
413
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
412
+ // Rubber-band mirror line should have been created
413
+ expect(core.scene.auxiliaryEntities.count()).toBeGreaterThanOrEqual(1);
414
414
  });
415
415
 
416
416
  test('Mirror.preview - two distinct points, draws mirror line and mirrors selected items', () => {
@@ -425,8 +425,8 @@ test('Mirror.preview - two distinct points, draws mirror line and mirrors select
425
425
  mirror.points.push(new Point(1, 0)); // pt2 — mirror across X axis
426
426
  mirror.preview();
427
427
 
428
- // Temp mirror line should exist
429
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
428
+ // Rubber-band mirror line should exist
429
+ expect(core.scene.auxiliaryEntities.count()).toBeGreaterThanOrEqual(1);
430
430
 
431
431
  // The selectedItems copy should have its points mirrored (y negated)
432
432
  const previewItem = core.scene.selectionManager.selectedItems[0];
@@ -445,8 +445,8 @@ test('Mirror.preview - identical points, draws line but skips mirroring selected
445
445
  mirror.points.push(new Point(5, 5)); // pt2 — same as pt1, zero-length mirror line
446
446
  mirror.preview();
447
447
 
448
- // Temp line is still drawn
449
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
448
+ // Rubber-band line is still drawn
449
+ expect(core.scene.auxiliaryEntities.count()).toBeGreaterThanOrEqual(1);
450
450
 
451
451
  // Selected item points should be unchanged (mirroring skipped)
452
452
  const previewItem = core.scene.selectionManager.selectedItems[0];
@@ -467,9 +467,9 @@ test('Mirror.preview - reverses direction for entities with a direction property
467
467
  mirror.preview();
468
468
 
469
469
  // Original entity direction is unchanged
470
- expect(core.scene.entities.get(0).direction).toBe(1);
470
+ expect(core.scene.entities.get(0).getProperty('direction')).toBe(1);
471
471
 
472
472
  // Preview copy has direction reversed
473
473
  const previewItem = core.scene.selectionManager.selectedItems[0];
474
- expect(previewItem.direction).toBe(-1);
474
+ expect(previewItem.getProperty('direction')).toBe(-1);
475
475
  });
@@ -4,6 +4,7 @@ import { Offset } from '../../core/tools/offset.js';
4
4
  import { Line } from '../../core/entities/line.js';
5
5
  import { Arc } from '../../core/entities/arc.js';
6
6
  import { Circle } from '../../core/entities/circle.js';
7
+ import { DesignCore } from '../../core/designCore.js';
7
8
  import { expect, jest } from '@jest/globals';
8
9
 
9
10
  const core = new Core();
@@ -267,7 +268,7 @@ test('Test Offset.action - offset circle outward', () => {
267
268
  expect(newCircle.points[0].x).toBeCloseTo(0);
268
269
  expect(newCircle.points[0].y).toBeCloseTo(0);
269
270
  // New radius should be 15
270
- expect(newCircle.radius).toBeCloseTo(15);
271
+ expect(newCircle.getProperty('radius')).toBeCloseTo(15);
271
272
  });
272
273
 
273
274
  test('Test Offset.action - offset circle inward', () => {
@@ -286,7 +287,7 @@ test('Test Offset.action - offset circle inward', () => {
286
287
  const newCircle = core.scene.entities.get(1);
287
288
  expect(newCircle.points[0].x).toBeCloseTo(0);
288
289
  expect(newCircle.points[0].y).toBeCloseTo(0);
289
- expect(newCircle.radius).toBeCloseTo(7);
290
+ expect(newCircle.getProperty('radius')).toBeCloseTo(7);
290
291
  });
291
292
 
292
293
  test('Test Offset.action - offset circle inward returns null when radius would be zero', () => {
@@ -464,7 +465,7 @@ test('Test Offset.getOffsetPoints - arc inward offset shrinks radius', () => {
464
465
  expect(result).not.toBeNull();
465
466
  const rebuilt = new Arc({});
466
467
  rebuilt.fromPolylinePoints(result);
467
- expect(rebuilt.radius).toBeCloseTo(7);
468
+ expect(rebuilt.getProperty('radius')).toBeCloseTo(7);
468
469
  });
469
470
 
470
471
  test('Test Offset.getOffsetPoints - arc returns null when inward offset collapses radius', () => {
@@ -746,3 +747,169 @@ test('Offset.execute - Through mode offsets entity to the through point distance
746
747
 
747
748
  core.scene.inputManager = origInputManager;
748
749
  });
750
+
751
+ // ─── register ─────────────────────────────────────────────────────────────────
752
+
753
+ test('Offset.register returns correct metadata', () => {
754
+ const reg = Offset.register();
755
+ expect(reg.command).toBe('Offset');
756
+ expect(reg.shortcut).toBe('O');
757
+ });
758
+
759
+ // ─── preview ──────────────────────────────────────────────────────────────────
760
+
761
+ test('Offset.preview does not add preview entity when selectedItem is null', () => {
762
+ core.scene.clear();
763
+ DesignCore.Scene.previewEntities.clear();
764
+ core.scene.headers.offsetDistance = 5;
765
+
766
+ const offset = new Offset();
767
+ offset.selectedItem = null;
768
+ expect(() => offset.preview()).not.toThrow();
769
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
770
+ });
771
+
772
+ test('Offset.preview does not add preview entity when offsetDistance is 0', () => {
773
+ core.scene.clear();
774
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
775
+ DesignCore.Scene.previewEntities.clear();
776
+ core.scene.headers.offsetDistance = 0;
777
+
778
+ const savedPointOnScene = DesignCore.Mouse.pointOnScene;
779
+ DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
780
+
781
+ const offset = new Offset();
782
+ offset.selectedItem = core.scene.entities.get(0);
783
+ offset.preview();
784
+
785
+ DesignCore.Mouse.pointOnScene = savedPointOnScene;
786
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
787
+ });
788
+
789
+ test('Offset.preview adds entity to previewEntities for a valid line', () => {
790
+ core.scene.clear();
791
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
792
+ DesignCore.Scene.previewEntities.clear();
793
+ core.scene.headers.offsetDistance = 5;
794
+
795
+ const savedPointOnScene = DesignCore.Mouse.pointOnScene;
796
+ DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
797
+
798
+ const offset = new Offset();
799
+ offset.selectedItem = core.scene.entities.get(0);
800
+ offset.preview();
801
+
802
+ DesignCore.Mouse.pointOnScene = savedPointOnScene;
803
+ expect(DesignCore.Scene.previewEntities.count()).toBe(1);
804
+ expect(DesignCore.Scene.previewEntities.get(0).points[0].y).toBeCloseTo(5);
805
+ });
806
+
807
+ // ─── Lwpolyline action ─────────────────────────────────────────────────────────
808
+
809
+ test('Test Offset.action - offset open Lwpolyline', () => {
810
+ core.scene.clear();
811
+ core.scene.selectionManager.reset();
812
+
813
+ // L-shape: (0,0) → (10,0) → (10,10)
814
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10)] });
815
+
816
+ const offset = new Offset();
817
+ offset.selectedItem = core.scene.entities.get(0);
818
+ core.scene.headers.offsetDistance = 1;
819
+ offset.points = [new Point(5, 2)]; // above the bottom segment → offset upward/left
820
+
821
+ offset.action();
822
+
823
+ expect(core.scene.entities.count()).toBe(2);
824
+ const newPoly = core.scene.entities.get(1);
825
+ expect(newPoly.points[0].x).toBeCloseTo(0);
826
+ expect(newPoly.points[0].y).toBeCloseTo(1);
827
+ expect(newPoly.points[2].x).toBeCloseTo(9);
828
+ expect(newPoly.points[2].y).toBeCloseTo(10);
829
+ });
830
+
831
+ test('Test Offset.action - offset closed Lwpolyline outward', () => {
832
+ core.scene.clear();
833
+ core.scene.selectionManager.reset();
834
+
835
+ // Closed square: 5 points where first === last → constructor sets flag=1
836
+ core.scene.addItem('Lwpolyline', {
837
+ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10), new Point(0, 0)],
838
+ });
839
+
840
+ const polyline = core.scene.entities.get(0);
841
+ expect(polyline.flags.hasFlag(1)).toBe(true); // confirm closed
842
+
843
+ const offset = new Offset();
844
+ offset.selectedItem = polyline;
845
+ core.scene.headers.offsetDistance = 1;
846
+ offset.points = [new Point(-5, 5)]; // outside the left edge
847
+
848
+ offset.action();
849
+
850
+ expect(core.scene.entities.count()).toBe(2);
851
+ const newPoly = core.scene.entities.get(1);
852
+ expect(newPoly.flags.hasFlag(1)).toBe(true); // closed flag preserved
853
+ const xs = newPoly.points.map((p) => p.x);
854
+ const ys = newPoly.points.map((p) => p.y);
855
+ expect(Math.min(...xs)).toBeCloseTo(-1);
856
+ expect(Math.max(...xs)).toBeCloseTo(11);
857
+ expect(Math.min(...ys)).toBeCloseTo(-1);
858
+ expect(Math.max(...ys)).toBeCloseTo(11);
859
+ });
860
+
861
+ // ─── closed inward offset ──────────────────────────────────────────────────────
862
+
863
+ test('Test Offset.getOffsetPolylinePoints - closed rectangle offset inward', () => {
864
+ const offset = new Offset();
865
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
866
+
867
+ // sidePoint inside the rectangle
868
+ const result = offset.getOffsetPolylinePoints(points, true, new Point(5, 5), 1);
869
+
870
+ expect(result).not.toBeNull();
871
+ expect(result.length).toBe(4);
872
+ const xs = result.map((p) => p.x);
873
+ const ys = result.map((p) => p.y);
874
+ expect(Math.min(...xs)).toBeCloseTo(1);
875
+ expect(Math.max(...xs)).toBeCloseTo(9);
876
+ expect(Math.min(...ys)).toBeCloseTo(1);
877
+ expect(Math.max(...ys)).toBeCloseTo(9);
878
+ });
879
+
880
+ // ─── Through mode with circle ──────────────────────────────────────────────────
881
+
882
+ test('Offset.execute - Through mode offsets circle to the through point', async () => {
883
+ core.scene.clear();
884
+ core.scene.selectionManager.reset();
885
+
886
+ // Circle: center (0,0), radius 10
887
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
888
+
889
+ const origInputManager = core.scene.inputManager;
890
+ let callCount = 0;
891
+ // Through → select circle → through point at (15,0) → undefined exits loop
892
+ const inputs = ['Through', { selectedItemIndex: 0 }, new Point(15, 0)];
893
+
894
+ const offset = new Offset();
895
+ core.scene.inputManager = {
896
+ requestInput: async () => {
897
+ if (callCount < inputs.length) return inputs[callCount++];
898
+ },
899
+ actionCommand: () => {
900
+ offset.action();
901
+ },
902
+ executeCommand: () => {},
903
+ reset: () => {},
904
+ };
905
+
906
+ await offset.execute();
907
+
908
+ // getThroughDistance(circle at radius 10, throughPoint at r=15) = |15 - 10| = 5
909
+ expect(core.scene.headers.offsetDistance).toBeCloseTo(5);
910
+ expect(core.scene.entities.count()).toBe(2);
911
+ const newCircle = core.scene.entities.get(1);
912
+ expect(newCircle.getProperty('radius')).toBeCloseTo(15); // original 10 + outward 5
913
+
914
+ core.scene.inputManager = origInputManager;
915
+ });
@@ -12,7 +12,7 @@ describe('Pasteclip Tool', () => {
12
12
  beforeEach(() => {
13
13
  DesignCore.Scene.entities.clear();
14
14
  DesignCore.Scene.selectionManager.reset();
15
- DesignCore.Scene.tempEntities.clear();
15
+ DesignCore.Scene.previewEntities.clear();
16
16
  DesignCore.Clipboard.Entities = []; // also resets basePoint
17
17
  });
18
18
 
@@ -57,8 +57,8 @@ describe('Pasteclip Tool', () => {
57
57
 
58
58
  const tool = new Pasteclip();
59
59
  tool.preview();
60
- expect(DesignCore.Scene.tempEntities.count()).toBeGreaterThan(0);
61
- const tempLine = DesignCore.Scene.tempEntities.get(0);
60
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThan(0);
61
+ const tempLine = DesignCore.Scene.previewEntities.get(0);
62
62
  const deltaX = targetPoint.x - basePoint.x;
63
63
  const deltaY = targetPoint.y - basePoint.y;
64
64
  expect(tempLine.points[0].x).toBe(line.points[0].x + deltaX);
@@ -94,7 +94,7 @@ describe('Pasteclip Tool', () => {
94
94
  expect(pasted.points[0].y).toBe(0);
95
95
 
96
96
  // Verify pasted entity has a unique handle
97
- expect(newEntity.handle).toBeDefined();
98
- expect(newEntity.handle).not.toBe(line.handle);
97
+ expect(newEntity.getProperty('handle')).toBeDefined();
98
+ expect(newEntity.getProperty('handle')).not.toBe(line.getProperty('handle'));
99
99
  });
100
100
  });
@@ -57,9 +57,9 @@ test.each(inputScenarios)('Rotate.execute handles $desc', async (scenario) => {
57
57
  const rotate = new Rotate();
58
58
  await rotate.execute();
59
59
 
60
- expect(core.scene.tempEntities.count()).toBe(0); // no temp entities before preview
60
+ expect(core.scene.previewEntities.count()).toBe(0); // no temp entities before preview
61
61
  rotate.preview(); // to set up any preview state if needed
62
- expect(core.scene.tempEntities.count()).toBe(1); // preview should create temp entity
62
+ expect(core.scene.auxiliaryEntities.count()).toBeGreaterThanOrEqual(1); // preview should create rubber-band
63
63
 
64
64
  rotate.action();
65
65
 
@@ -131,3 +131,33 @@ test('Test Rotate.action', () => {
131
131
  expect(core.scene.entities.get(i).points[0].y).toBeCloseTo(0);
132
132
  }
133
133
  });
134
+
135
+ test('Rotate.execute with reference - resets inputPoint to rotation center', async () => {
136
+ const origInputManager = core.scene.inputManager;
137
+
138
+ const basePoint = new Point(10, 0);
139
+ // inputs: basePoint, 'r' (reference option), refBase, refEnd, then undefined to exit the while loop
140
+ const inputs = [basePoint, 'r', new Point(5, 0), new Point(5, 10), undefined];
141
+ let callCount = 0;
142
+ const mockInputManager = {
143
+ requestInput: async () => {
144
+ if (callCount < inputs.length) return inputs[callCount++];
145
+ },
146
+ executeCommand: () => {},
147
+ inputPoint: null,
148
+ };
149
+ core.scene.inputManager = mockInputManager;
150
+
151
+ core.scene.clear();
152
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
153
+ core.scene.selectionManager.addToSelectionSet(0);
154
+
155
+ const rotate = new Rotate();
156
+ await rotate.execute();
157
+
158
+ // After the reference sequence completes, inputPoint must be reset to the
159
+ // rotation center (points[0]) so the tracking line uses the correct origin.
160
+ expect(mockInputManager.inputPoint).toEqual(basePoint);
161
+
162
+ core.scene.inputManager = origInputManager;
163
+ });
@@ -438,7 +438,7 @@ test('Scale.preview - no points set, adds no temp entities', () => {
438
438
  // No points — guard condition prevents any work
439
439
  scale.preview();
440
440
 
441
- expect(core.scene.tempEntities.count()).toBe(0);
441
+ expect(core.scene.previewEntities.count()).toBe(0);
442
442
  });
443
443
 
444
444
  test('Scale.preview - referencePoint set, suppresses preview', () => {
@@ -454,7 +454,7 @@ test('Scale.preview - referencePoint set, suppresses preview', () => {
454
454
 
455
455
  scale.preview();
456
456
 
457
- expect(core.scene.tempEntities.count()).toBe(0);
457
+ expect(core.scene.previewEntities.count()).toBe(0);
458
458
  });
459
459
 
460
460
  test('Scale.preview - mouse on base point (distance 0), draws line but skips scaling', () => {
@@ -472,7 +472,7 @@ test('Scale.preview - mouse on base point (distance 0), draws line but skips sca
472
472
  scale.preview();
473
473
 
474
474
  // Temp line is still created
475
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
475
+ expect(core.scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
476
476
 
477
477
  // Selected item points should be unchanged (scaling skipped)
478
478
  const previewItem = core.scene.selectionManager.selectedItems[0];
@@ -494,7 +494,7 @@ test('Scale.preview - one point set, draws temp line and scales selected items',
494
494
  scale.preview();
495
495
 
496
496
  // Temp line should be created
497
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
497
+ expect(core.scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
498
498
 
499
499
  // Preview item should have been scaled (x differs from original 1.0)
500
500
  const previewItem = core.scene.selectionManager.selectedItems[0];
@@ -520,7 +520,7 @@ test('Scale.preview - reference mode uses referenceLength to compute factor', ()
520
520
  // before and after with a known referenceLength to confirm scaling occurred.
521
521
  scale.preview();
522
522
 
523
- expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
523
+ expect(core.scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
524
524
 
525
525
  // The preview item's first point should differ from the original (10,0)
526
526
  // because the factor (distance/10) is applied