@design-core/core 0.0.6 → 50.1.0

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 (173) hide show
  1. package/.eslintrc.json +4 -0
  2. package/.github/workflows/DesignCoreQA.yml +3 -0
  3. package/TODO.md +49 -6
  4. package/core/core/core.js +0 -3
  5. package/core/designCore.js +1 -1
  6. package/core/dimensions/alignedDimension.js +12 -11
  7. package/core/dimensions/angularDimension.js +38 -37
  8. package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
  9. package/core/dimensions/diametricDimension.js +14 -13
  10. package/core/dimensions/dimension.js +62 -55
  11. package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
  12. package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
  13. package/core/dimensions/radialDimension.js +12 -11
  14. package/core/dimensions/rotatedDimension.js +20 -19
  15. package/core/entities/arc.js +123 -156
  16. package/core/entities/arctext.js +222 -184
  17. package/core/entities/circle.js +77 -99
  18. package/core/entities/entity.js +114 -93
  19. package/core/entities/hatch.js +346 -273
  20. package/core/entities/insert.js +68 -83
  21. package/core/entities/line.js +21 -142
  22. package/core/entities/lwpolyline.js +16 -11
  23. package/core/entities/point.js +19 -19
  24. package/core/entities/polyline.js +7 -7
  25. package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
  26. package/core/entities/rectangle.js +3 -1
  27. package/core/entities/solid.js +6 -15
  28. package/core/entities/text.js +190 -201
  29. package/core/lib/auxiliary/rubberBand.js +32 -0
  30. package/core/lib/auxiliary/selectionWindow.js +63 -0
  31. package/core/lib/auxiliary/snapPoint.js +113 -0
  32. package/core/lib/auxiliary/trackingLine.js +76 -0
  33. package/core/lib/boundingBox.js +11 -11
  34. package/core/lib/canvas.js +232 -168
  35. package/core/lib/clipboard.js +9 -3
  36. package/core/lib/colour.js +16 -0
  37. package/core/lib/commandLine.js +1 -6
  38. package/core/lib/commandManager.js +18 -8
  39. package/core/lib/constants.js +28 -0
  40. package/core/lib/dxf/dxf.js +15 -14
  41. package/core/lib/dxf/dxfIterator.js +2 -2
  42. package/core/lib/entityManager.js +33 -26
  43. package/core/lib/inputManager.js +36 -12
  44. package/core/lib/intersect.js +25 -0
  45. package/core/lib/matrix.js +1 -1
  46. package/core/lib/patterns.js +9 -2
  47. package/core/lib/plotOptions.js +67 -0
  48. package/core/lib/polylineUtils.js +75 -0
  49. package/core/lib/renderers/cairoRenderer.js +371 -0
  50. package/core/lib/renderers/canvasRenderer.js +315 -0
  51. package/core/lib/renderers/pdfRenderer.js +366 -0
  52. package/core/lib/renderers/rendererBase.js +284 -0
  53. package/core/lib/renderers/svgRenderer.js +343 -0
  54. package/core/lib/scene.js +28 -6
  55. package/core/lib/selectionManager.js +20 -20
  56. package/core/lib/settings.js +4 -2
  57. package/core/lib/snapping.js +4 -191
  58. package/core/lib/stateManager.js +30 -1
  59. package/core/lib/utils.js +26 -7
  60. package/core/properties/entityProperties.js +104 -0
  61. package/core/properties/property.js +106 -2
  62. package/core/properties/propertyManager.js +60 -44
  63. package/core/tables/block.js +50 -81
  64. package/core/tables/dimStyle.js +5 -5
  65. package/core/tables/endblk.js +1 -1
  66. package/core/tables/layer.js +8 -0
  67. package/core/tables/ltypeManager.js +1 -1
  68. package/core/tables/styleManagerBase.js +3 -2
  69. package/core/tables/tableManagerBase.js +8 -3
  70. package/core/tables/vport.js +2 -2
  71. package/core/tools/chamfer.js +117 -143
  72. package/core/tools/chamferFilletBase.js +138 -37
  73. package/core/tools/copy.js +11 -11
  74. package/core/tools/copybase.js +3 -3
  75. package/core/tools/copyclip.js +3 -3
  76. package/core/tools/cornerEntity.js +2 -2
  77. package/core/tools/cutclip.js +3 -3
  78. package/core/tools/explode.js +7 -7
  79. package/core/tools/extend.js +138 -25
  80. package/core/tools/fillet.js +92 -189
  81. package/core/tools/matchProp.js +3 -12
  82. package/core/tools/mirror.js +15 -14
  83. package/core/tools/move.js +8 -8
  84. package/core/tools/offset.js +31 -33
  85. package/core/tools/pasteclip.js +1 -1
  86. package/core/tools/purge.js +10 -10
  87. package/core/tools/rotate.js +19 -16
  88. package/core/tools/scale.js +7 -7
  89. package/core/tools/trim.js +271 -29
  90. package/core/tools/zoom.js +1 -1
  91. package/cspell.json +321 -0
  92. package/jest.config.js +6 -0
  93. package/package.json +5 -3
  94. package/test/dimensions/alignedDimension.test.js +17 -10
  95. package/test/dimensions/angularDimension.test.js +13 -13
  96. package/test/dimensions/diametricDimension.test.js +15 -15
  97. package/test/dimensions/dimension.test.js +12 -12
  98. package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
  99. package/test/dimensions/linearDimensionBase.test.js +62 -0
  100. package/test/dimensions/radialDimension.test.js +15 -15
  101. package/test/dimensions/rotatedDimension.test.js +18 -11
  102. package/test/entities/arc.test.js +83 -86
  103. package/test/entities/arctext.test.js +184 -57
  104. package/test/entities/circle.test.js +68 -96
  105. package/test/entities/entity.test.js +42 -8
  106. package/test/entities/hatch.test.js +569 -96
  107. package/test/entities/insert.test.js +99 -4
  108. package/test/entities/line.test.js +15 -41
  109. package/test/entities/lwpolyline.test.js +104 -0
  110. package/test/entities/point.test.js +3 -3
  111. package/test/entities/polyline.test.js +11 -0
  112. package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
  113. package/test/entities/rectangle.test.js +11 -0
  114. package/test/entities/solid.test.js +1 -1
  115. package/test/entities/text.test.js +302 -77
  116. package/test/integration/exportIntegration.reference.dxf +9 -9
  117. package/test/integration/exportIntegration.test.js +5 -5
  118. package/test/integration/intersectionIntegration.test.js +4 -5
  119. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  120. package/test/integration/pdfRendererIntegration.test.js +61 -0
  121. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  122. package/test/integration/svgRendererIntegration.test.js +61 -0
  123. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  124. package/test/integration/userInputIntegration.test.js +239 -132
  125. package/test/lib/boundingBox.test.js +3 -3
  126. package/test/lib/canvas.test.js +744 -125
  127. package/test/lib/constants.test.js +27 -0
  128. package/test/lib/entityManager.test.js +4 -4
  129. package/test/lib/inputManager.test.js +81 -20
  130. package/test/lib/intersect.test.js +83 -0
  131. package/test/lib/plotOptions.test.js +179 -0
  132. package/test/lib/rubberBand.test.js +84 -0
  133. package/test/lib/scene.test.js +9 -9
  134. package/test/lib/selectionManager.test.js +3 -3
  135. package/test/lib/settings.test.js +5 -6
  136. package/test/lib/snapping.test.js +72 -130
  137. package/test/lib/stateManager.test.js +7 -7
  138. package/test/lib/utils.test.js +36 -2
  139. package/test/properties/entityProperties.test.js +132 -0
  140. package/test/properties/property.test.js +53 -0
  141. package/test/properties/propertyManager.test.js +25 -25
  142. package/test/renderers/canvasRenderer.test.js +317 -0
  143. package/test/renderers/pdfRenderer.test.js +271 -0
  144. package/test/renderers/rendererBase.test.js +213 -0
  145. package/test/renderers/svgRenderer.test.js +326 -0
  146. package/test/tables/block.test.js +92 -13
  147. package/test/tables/endblk.test.js +2 -2
  148. package/test/tables/layer.test.js +30 -0
  149. package/test/tables/layerManager.test.js +2 -2
  150. package/test/test-helpers/hatchGridRenderer.js +180 -0
  151. package/test/test-helpers/test-helpers.js +158 -6
  152. package/test/tools/chamfer.test.js +273 -51
  153. package/test/tools/chamferFilletBase.test.js +125 -36
  154. package/test/tools/copy.test.js +8 -8
  155. package/test/tools/copybase.test.js +2 -2
  156. package/test/tools/copyclip.test.js +2 -2
  157. package/test/tools/cornerEntity.test.js +8 -8
  158. package/test/tools/cutclip.test.js +2 -2
  159. package/test/tools/erase.test.js +6 -6
  160. package/test/tools/explode.test.js +5 -5
  161. package/test/tools/extend.test.js +373 -42
  162. package/test/tools/fillet.test.js +230 -49
  163. package/test/tools/matchProp.test.js +11 -11
  164. package/test/tools/mirror.test.js +39 -39
  165. package/test/tools/move.test.js +7 -7
  166. package/test/tools/offset.test.js +222 -55
  167. package/test/tools/pasteclip.test.js +5 -5
  168. package/test/tools/purge.test.js +4 -4
  169. package/test/tools/rotate.test.js +39 -9
  170. package/test/tools/scale.test.js +33 -33
  171. package/test/tools/trim.test.js +579 -53
  172. package/test/tools/zoom.test.js +12 -12
  173. package/core/lib/selectionWindow.js +0 -83
@@ -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();
@@ -13,7 +14,7 @@ const inputScenarios = [
13
14
  desc: 'offset line to the left by distance 5',
14
15
  entityType: 'Line',
15
16
  entityData: { points: [new Point(0, 0), new Point(10, 0)] },
16
- inputs: [5, { selectedItemIndex: 0 }, new Point(5, 5)],
17
+ inputs: [5, { selectedEntityIndex: 0 }, new Point(5, 5)],
17
18
  checkIndex: 1,
18
19
  expectedPoints: [new Point(0, 5), new Point(10, 5)],
19
20
  },
@@ -21,7 +22,7 @@ const inputScenarios = [
21
22
  desc: 'offset line to the right by distance 5',
22
23
  entityType: 'Line',
23
24
  entityData: { points: [new Point(0, 0), new Point(10, 0)] },
24
- inputs: [5, { selectedItemIndex: 0 }, new Point(5, -5)],
25
+ inputs: [5, { selectedEntityIndex: 0 }, new Point(5, -5)],
25
26
  checkIndex: 1,
26
27
  expectedPoints: [new Point(0, -5), new Point(10, -5)],
27
28
  },
@@ -29,7 +30,7 @@ const inputScenarios = [
29
30
  desc: 'offset circle outward by distance 5',
30
31
  entityType: 'Circle',
31
32
  entityData: { points: [new Point(0, 0), new Point(10, 0)] },
32
- inputs: [5, { selectedItemIndex: 0 }, new Point(20, 0)],
33
+ inputs: [5, { selectedEntityIndex: 0 }, new Point(20, 0)],
33
34
  checkIndex: 1,
34
35
  checkRadius: 15,
35
36
  },
@@ -37,7 +38,7 @@ const inputScenarios = [
37
38
  desc: 'offset circle inward by distance 3',
38
39
  entityType: 'Circle',
39
40
  entityData: { points: [new Point(0, 0), new Point(10, 0)] },
40
- inputs: [3, { selectedItemIndex: 0 }, new Point(5, 0)],
41
+ inputs: [3, { selectedEntityIndex: 0 }, new Point(5, 0)],
41
42
  checkIndex: 1,
42
43
  checkRadius: 7,
43
44
  },
@@ -45,7 +46,7 @@ const inputScenarios = [
45
46
  desc: 'offset arc outward by distance 5',
46
47
  entityType: 'Arc',
47
48
  entityData: { points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] },
48
- inputs: [5, { selectedItemIndex: 0 }, new Point(20, 0)],
49
+ inputs: [5, { selectedEntityIndex: 0 }, new Point(20, 0)],
49
50
  checkIndex: 1,
50
51
  checkRadius: 15,
51
52
  },
@@ -57,7 +58,7 @@ test.each(inputScenarios)('Offset.execute handles $desc', async (scenario) => {
57
58
  core.scene.clear();
58
59
  core.scene.selectionManager.reset();
59
60
 
60
- core.scene.addItem(entityType, entityData);
61
+ core.scene.addEntity(entityType, entityData);
61
62
 
62
63
  const origInputManager = core.scene.inputManager;
63
64
  let callCount = 0;
@@ -99,7 +100,7 @@ test('Offset.execute - cancel at distance prompt does not offset', async () => {
99
100
  core.scene.clear();
100
101
  core.scene.selectionManager.reset();
101
102
 
102
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
103
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
103
104
 
104
105
  const origInputManager = core.scene.inputManager;
105
106
  core.scene.inputManager = {
@@ -120,7 +121,7 @@ test('Offset.execute - cancel at entity selection does not offset', async () =>
120
121
  core.scene.clear();
121
122
  core.scene.selectionManager.reset();
122
123
 
123
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
124
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
124
125
 
125
126
  const origInputManager = core.scene.inputManager;
126
127
  let callCount = 0;
@@ -147,11 +148,11 @@ test('Offset.execute - cancel at side point does not offset', async () => {
147
148
  core.scene.clear();
148
149
  core.scene.selectionManager.reset();
149
150
 
150
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
151
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
151
152
 
152
153
  const origInputManager = core.scene.inputManager;
153
154
  let callCount = 0;
154
- const inputs = [5, { selectedItemIndex: 0 }]; // distance + selection, then undefined
155
+ const inputs = [5, { selectedEntityIndex: 0 }]; // distance + selection, then undefined
155
156
  core.scene.inputManager = {
156
157
  requestInput: async () => {
157
158
  if (callCount < inputs.length) {
@@ -166,8 +167,8 @@ test('Offset.execute - cancel at side point does not offset', async () => {
166
167
  await offset.execute();
167
168
 
168
169
  expect(core.scene.entities.count()).toBe(1);
169
- // selectedItem must be cleared so a stale action() call cannot create an unintended entity
170
- expect(offset.selectedItem).toBeNull();
170
+ // selectedEntity must be cleared so a stale action() call cannot create an unintended entity
171
+ expect(offset.selectedEntity).toBeNull();
171
172
 
172
173
  core.scene.inputManager = origInputManager;
173
174
  });
@@ -176,14 +177,14 @@ test('Offset.execute - entity count increases by one per offset', async () => {
176
177
  core.scene.clear();
177
178
  core.scene.selectionManager.reset();
178
179
 
179
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
180
- core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
180
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
181
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
181
182
 
182
183
  const entityCount = core.scene.entities.count();
183
184
 
184
185
  const origInputManager = core.scene.inputManager;
185
186
  let callCount = 0;
186
- const inputs = [5, { selectedItemIndex: 0 }, new Point(5, 5)];
187
+ const inputs = [5, { selectedEntityIndex: 0 }, new Point(5, 5)];
187
188
  core.scene.inputManager = {
188
189
  requestInput: async () => {
189
190
  if (callCount < inputs.length) {
@@ -209,10 +210,10 @@ test('Test Offset.action - offset line perpendicular', () => {
209
210
  core.scene.clear();
210
211
  core.scene.selectionManager.reset();
211
212
 
212
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
213
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
213
214
 
214
215
  const offset = new Offset();
215
- offset.selectedItem = core.scene.entities.get(0);
216
+ offset.selectedEntity = core.scene.entities.get(0);
216
217
  core.scene.headers.offsetDistance = 5;
217
218
  offset.points = [new Point(5, 5)]; // side point above line
218
219
 
@@ -231,10 +232,10 @@ test('Test Offset.action - offset vertical line', () => {
231
232
  core.scene.clear();
232
233
  core.scene.selectionManager.reset();
233
234
 
234
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
235
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
235
236
 
236
237
  const offset = new Offset();
237
- offset.selectedItem = core.scene.entities.get(0);
238
+ offset.selectedEntity = core.scene.entities.get(0);
238
239
  core.scene.headers.offsetDistance = 3;
239
240
  offset.points = [new Point(5, 5)]; // side point to the right
240
241
 
@@ -251,10 +252,10 @@ test('Test Offset.action - offset circle outward', () => {
251
252
  core.scene.clear();
252
253
  core.scene.selectionManager.reset();
253
254
 
254
- core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
255
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
255
256
 
256
257
  const offset = new Offset();
257
- offset.selectedItem = core.scene.entities.get(0);
258
+ offset.selectedEntity = core.scene.entities.get(0);
258
259
  core.scene.headers.offsetDistance = 5;
259
260
  offset.points = [new Point(20, 0)]; // outside the circle
260
261
 
@@ -267,17 +268,17 @@ 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', () => {
274
275
  core.scene.clear();
275
276
  core.scene.selectionManager.reset();
276
277
 
277
- core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
278
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
278
279
 
279
280
  const offset = new Offset();
280
- offset.selectedItem = core.scene.entities.get(0);
281
+ offset.selectedEntity = core.scene.entities.get(0);
281
282
  core.scene.headers.offsetDistance = 3;
282
283
  offset.points = [new Point(5, 0)]; // inside the circle
283
284
 
@@ -286,17 +287,17 @@ 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', () => {
293
294
  core.scene.clear();
294
295
  core.scene.selectionManager.reset();
295
296
 
296
- core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
297
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
297
298
 
298
299
  const offset = new Offset();
299
- offset.selectedItem = core.scene.entities.get(0);
300
+ offset.selectedEntity = core.scene.entities.get(0);
300
301
  core.scene.headers.offsetDistance = 15; // larger than radius
301
302
  offset.points = [new Point(5, 0)]; // inside
302
303
 
@@ -310,14 +311,14 @@ test('Test Offset.action - offset arc outward preserves angles', () => {
310
311
  core.scene.clear();
311
312
  core.scene.selectionManager.reset();
312
313
 
313
- const center = new Point(0, 0);
314
- const start = center.project(0, 10); // radius 10
315
- const end = center.project(Math.PI / 2, 10);
314
+ const centre = new Point(0, 0);
315
+ const start = centre.project(0, 10); // radius 10
316
+ const end = centre.project(Math.PI / 2, 10);
316
317
 
317
- core.scene.addItem('Arc', { points: [center, start, end] });
318
+ core.scene.addEntity('Arc', { points: [centre, start, end] });
318
319
 
319
320
  const offset = new Offset();
320
- offset.selectedItem = core.scene.entities.get(0);
321
+ offset.selectedEntity = core.scene.entities.get(0);
321
322
  core.scene.headers.offsetDistance = 5;
322
323
  offset.points = [new Point(20, 0)]; // outside
323
324
 
@@ -329,8 +330,8 @@ test('Test Offset.action - offset arc outward preserves angles', () => {
329
330
  expect(newRadius).toBeCloseTo(15);
330
331
 
331
332
  // Start and end angles should be preserved
332
- const originalStartAngle = center.angle(start);
333
- const originalEndAngle = center.angle(end);
333
+ const originalStartAngle = centre.angle(start);
334
+ const originalEndAngle = centre.angle(end);
334
335
  const newStartAngle = newArc.points[0].angle(newArc.points[1]);
335
336
  const newEndAngle = newArc.points[0].angle(newArc.points[2]);
336
337
  expect(newStartAngle).toBeCloseTo(originalStartAngle);
@@ -341,10 +342,10 @@ test('Test Offset.action - unsupported entity type does not create new entity',
341
342
  core.scene.clear();
342
343
  core.scene.selectionManager.reset();
343
344
 
344
- core.scene.addItem('Rectangle', { points: [new Point(0, 0), new Point(10, 10)] });
345
+ core.scene.addEntity('Rectangle', { points: [new Point(0, 0), new Point(10, 10)] });
345
346
 
346
347
  const offset = new Offset();
347
- offset.selectedItem = core.scene.entities.get(0);
348
+ offset.selectedEntity = core.scene.entities.get(0);
348
349
  core.scene.headers.offsetDistance = 5;
349
350
  offset.points = [new Point(20, 20)];
350
351
 
@@ -396,7 +397,7 @@ test('Test Offset.getThroughDistance - circle', () => {
396
397
 
397
398
  test('Test Offset.getThroughDistance - arc uses same radial distance as circle', () => {
398
399
  const offset = new Offset();
399
- // Arc: center (0,0), radius 10
400
+ // Arc: centre (0,0), radius 10
400
401
  const entity = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] });
401
402
 
402
403
  // Point inside the arc radius
@@ -408,7 +409,7 @@ test('Test Offset.getThroughDistance - arc uses same radial distance as circle',
408
409
  test('Test Offset.getThroughDistance - polyline uses closestPoint distance', () => {
409
410
  core.scene.clear();
410
411
  core.scene.selectionManager.reset();
411
- core.scene.addItem('Polyline', { points: [new Point(0, 0), new Point(10, 0)] });
412
+ core.scene.addEntity('Polyline', { points: [new Point(0, 0), new Point(10, 0)] });
412
413
  const entity = core.scene.entities.get(0);
413
414
 
414
415
  const offset = new Offset();
@@ -425,13 +426,13 @@ test('Test Offset.getThroughDistance - unsupported entity returns 0', () => {
425
426
 
426
427
  // ─── action edge cases ────────────────────────────────────────────────────────
427
428
 
428
- test('Test Offset.action - returns early when selectedItem is null', () => {
429
+ test('Test Offset.action - returns early when selectedEntity is null', () => {
429
430
  core.scene.clear();
430
431
  core.scene.selectionManager.reset();
431
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
432
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
432
433
 
433
434
  const offset = new Offset();
434
- offset.selectedItem = null;
435
+ offset.selectedEntity = null;
435
436
  core.scene.headers.offsetDistance = 5;
436
437
  offset.action();
437
438
 
@@ -441,10 +442,10 @@ test('Test Offset.action - returns early when selectedItem is null', () => {
441
442
  test('Test Offset.action - returns early when offsetDistance is 0', () => {
442
443
  core.scene.clear();
443
444
  core.scene.selectionManager.reset();
444
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
445
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
445
446
 
446
447
  const offset = new Offset();
447
- offset.selectedItem = core.scene.entities.get(0);
448
+ offset.selectedEntity = core.scene.entities.get(0);
448
449
  core.scene.headers.offsetDistance = 0;
449
450
  offset.points = [new Point(5, 5)];
450
451
  offset.action();
@@ -458,13 +459,13 @@ test('Test Offset.getOffsetPoints - arc inward offset shrinks radius', () => {
458
459
  const offset = new Offset();
459
460
  const entity = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] });
460
461
 
461
- // sidePoint inside the arc (between center and circumference)
462
+ // sidePoint inside the arc (between centre and circumference)
462
463
  const result = offset.getOffsetPoints(entity, new Point(5, 0), 3);
463
464
 
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', () => {
@@ -537,7 +538,7 @@ test('Test Offset.getOffsetPolylinePoints - closed rectangle offset outward', ()
537
538
  test('Test Offset.getOffsetPolylinePoints - arc segment (bulge) offset outward grows radius', () => {
538
539
  const offset = new Offset();
539
540
  // CW semicircle (bulge=-1) from (0,0) to (10,0): traces the TOP half through (5,5)
540
- // center=(5,0), radius=5
541
+ // centre=(5,0), radius=5
541
542
  const start = new Point(0, 0, -1); // bulge=-1 → CW, arc goes upward
542
543
  const end = new Point(10, 0);
543
544
  const points = [start, end];
@@ -576,7 +577,7 @@ test('Offset.findJunction - parallel line/line falls back to midpoint', () => {
576
577
 
577
578
  test('Offset.findJunction - arc/line returns circle-line intersection', () => {
578
579
  const offset = new Offset();
579
- // Arc: A=(-5,0) B=(5,0) bulge=1 → center (0,0), radius 5 via bulgeCentrePoint
580
+ // Arc: A=(-5,0) B=(5,0) bulge=1 → centre (0,0), radius 5 via bulgeCentrePoint
580
581
  // Line: vertical at x=3 → circle intersects at (3,4) and (3,-4)
581
582
  const seg = { A: new Point(-5, 0), B: new Point(5, 0), bulge: 1 };
582
583
  const nextSeg = { A: new Point(3, 5), B: new Point(3, 10), bulge: 0 };
@@ -600,7 +601,7 @@ test('Offset.findJunction - line/arc returns line-circle intersection', () => {
600
601
 
601
602
  test('Offset.findJunction - arc/arc returns circle-circle intersection', () => {
602
603
  const offset = new Offset();
603
- // Circle 1: A=(-4,0) B=(4,0) bulge=1 → center (0,0), radius 4 via bulgeCentrePoint
604
+ // Circle 1: A=(-4,0) B=(4,0) bulge=1 → centre (0,0), radius 4 via bulgeCentrePoint
604
605
  // Circle 2: A=(0,4) B=(6,4) bulge=1 → center (3,4), radius 3 via bulgeCentrePoint
605
606
  // Intersections: (0,4) and (96/25, 28/25)
606
607
  const seg = { A: new Point(-4, 0), B: new Point(4, 0), bulge: 1 };
@@ -628,7 +629,7 @@ test('Offset.findJunction - non-intersecting arc/arc falls back to midpoint', ()
628
629
  test('Offset.execute - single preselection skips entity selection prompt', async () => {
629
630
  core.scene.clear();
630
631
  core.scene.selectionManager.reset();
631
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
632
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
632
633
  // Preselect the entity
633
634
  core.scene.selectionManager.addToSelectionSet(0);
634
635
 
@@ -657,8 +658,8 @@ test('Offset.execute - single preselection skips entity selection prompt', async
657
658
  test('Offset.execute - multi-preselection clears selection and prompts', async () => {
658
659
  core.scene.clear();
659
660
  core.scene.selectionManager.reset();
660
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
661
- core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
661
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
662
+ core.scene.addEntity('Line', { points: [new Point(0, 5), new Point(10, 5)] });
662
663
  // Preselect two entities
663
664
  core.scene.selectionManager.addToSelectionSet(0);
664
665
  core.scene.selectionManager.addToSelectionSet(1);
@@ -666,7 +667,7 @@ test('Offset.execute - multi-preselection clears selection and prompts', async (
666
667
  const origInputManager = core.scene.inputManager;
667
668
  let callCount = 0;
668
669
  // After clearing, distance + selection + side-point
669
- const inputs = [5, { selectedItemIndex: 0 }, new Point(5, 5)];
670
+ const inputs = [5, { selectedEntityIndex: 0 }, new Point(5, 5)];
670
671
  const offset = new Offset();
671
672
  core.scene.inputManager = {
672
673
  requestInput: async () => {
@@ -690,7 +691,7 @@ test('Offset.execute - multi-preselection clears selection and prompts', async (
690
691
  test('Offset.execute - distance <= 0 notifies and does not offset', async () => {
691
692
  core.scene.clear();
692
693
  core.scene.selectionManager.reset();
693
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
694
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
694
695
 
695
696
  const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
696
697
 
@@ -714,12 +715,12 @@ test('Offset.execute - distance <= 0 notifies and does not offset', async () =>
714
715
  test('Offset.execute - Through mode offsets entity to the through point distance', async () => {
715
716
  core.scene.clear();
716
717
  core.scene.selectionManager.reset();
717
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
718
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
718
719
 
719
720
  const origInputManager = core.scene.inputManager;
720
721
  let callCount = 0;
721
722
  // 'Through' → select entity at index 0 → through point at (5, 7) → undefined exits loop
722
- const inputs = ['Through', { selectedItemIndex: 0 }, new Point(5, 7)];
723
+ const inputs = ['Through', { selectedEntityIndex: 0 }, new Point(5, 7)];
723
724
 
724
725
  const offset = new Offset();
725
726
  core.scene.inputManager = {
@@ -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 selectedEntity 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.selectedEntity = 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.addEntity('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.selectedEntity = 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.addEntity('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.selectedEntity = 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.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10)] });
815
+
816
+ const offset = new Offset();
817
+ offset.selectedEntity = 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.addEntity('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.selectedEntity = 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: centre (0,0), radius 10
887
+ core.scene.addEntity('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', { selectedEntityIndex: 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
  });
@@ -29,10 +29,10 @@ beforeEach(() => {
29
29
 
30
30
  const data = { points: [point1, point2], layer: 'layerWithItems', lineType: 'ltypeWithItems' };
31
31
 
32
- DesignCore.Scene.addItem('Insert', { blockName: 'blockWithItems' });
33
- DesignCore.Scene.addItem('Line', data);
34
- DesignCore.Scene.addItem('Circle', data);
35
- DesignCore.Scene.addItem('Text', data);
32
+ DesignCore.Scene.addEntity('Insert', { blockName: 'blockWithItems' });
33
+ DesignCore.Scene.addEntity('Line', data);
34
+ DesignCore.Scene.addEntity('Circle', data);
35
+ DesignCore.Scene.addEntity('Text', data);
36
36
  });
37
37
 
38
38
  test('Test purge.action', () => {
@@ -49,7 +49,7 @@ test.each(inputScenarios)('Rotate.execute handles $desc', async (scenario) => {
49
49
  // create line
50
50
  const pointOne = new Point(10, 0);
51
51
  const pointTwo = new Point(20, 0);
52
- core.scene.addItem('Line', { points: [pointOne, pointTwo] });
52
+ core.scene.addEntity('Line', { points: [pointOne, pointTwo] });
53
53
 
54
54
  // select line
55
55
  core.scene.selectionManager.addToSelectionSet(0);
@@ -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
 
@@ -78,12 +78,12 @@ test.each(inputScenarios)('Rotate.execute handles $desc', async (scenario) => {
78
78
 
79
79
  test('Test Rotate.action', () => {
80
80
  // Add items to scene
81
- core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
82
- core.scene.addItem('Circle', { points: [new Point(10, 0), new Point(20, 0)] });
83
- core.scene.addItem('Polyline', { points: [new Point(10, 0), new Point(20, 0)] });
84
- core.scene.addItem('Arc', { points: [new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
85
- core.scene.addItem('Rectangle', { points: [new Point(10, 0), new Point(0, 10)] });
86
- core.scene.addItem('Text', { points: [new Point(10, 0), new Point(0, 10)], height: 10, rotation: 0, string: 'text test' });
81
+ core.scene.addEntity('Line', { points: [new Point(10, 0), new Point(20, 0)] });
82
+ core.scene.addEntity('Circle', { points: [new Point(10, 0), new Point(20, 0)] });
83
+ core.scene.addEntity('Polyline', { points: [new Point(10, 0), new Point(20, 0)] });
84
+ core.scene.addEntity('Arc', { points: [new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
85
+ core.scene.addEntity('Rectangle', { points: [new Point(10, 0), new Point(0, 10)] });
86
+ core.scene.addEntity('Text', { points: [new Point(10, 0), new Point(0, 10)], height: 10, rotation: 0, string: 'text test' });
87
87
 
88
88
  // Add items to selection set
89
89
  for (let i = 0; i < core.scene.entities.count(); i++) {
@@ -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 centre', 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.addEntity('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 centre (points[0]) so the tracking line uses the correct origin.
160
+ expect(mockInputManager.inputPoint).toEqual(basePoint);
161
+
162
+ core.scene.inputManager = origInputManager;
163
+ });