@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
@@ -40,8 +40,7 @@ test('polar and ortho are mutually exclusive', () => {
40
40
  test('Settings constructor - colour defaults', () => {
41
41
  const fresh = new (settings.constructor)();
42
42
  expect(fresh.canvasbackgroundcolour).toEqual({ r: 30, g: 30, b: 30 });
43
- expect(fresh.selecteditemscolour).toEqual({ r: 0, g: 255, b: 0 });
44
- expect(fresh.snaptrackingcolour).toEqual({ r: 55, g: 180, b: 75 });
43
+ expect(fresh.accentcolour).toEqual({ r: 55, g: 180, b: 75 });
45
44
  expect(fresh.gridcolour).toEqual({ r: 119, g: 118, b: 123 });
46
45
  expect(fresh.selectionWindow).toEqual({ r: 0, g: 255, b: 0 });
47
46
  expect(fresh.crossingWindow).toEqual({ r: 0, g: 0, b: 255 });
@@ -82,9 +81,9 @@ test('setSetting with false value does not trigger polar/ortho exclusion', () =>
82
81
  });
83
82
 
84
83
  test('setSetting updates arbitrary colour settings', () => {
85
- settings.setSetting('snaptrackingcolour', { r: 1, g: 2, b: 3 });
86
- expect(settings.snaptrackingcolour).toEqual({ r: 1, g: 2, b: 3 });
87
- settings.setSetting('snaptrackingcolour', { r: 55, g: 180, b: 75 });
84
+ settings.setSetting('accentcolour', { r: 1, g: 2, b: 3 });
85
+ expect(settings.accentcolour).toEqual({ r: 1, g: 2, b: 3 });
86
+ settings.setSetting('accentcolour', { r: 55, g: 180, b: 75 });
88
87
  });
89
88
 
90
89
  test('setSetting updates polarangle', () => {
@@ -116,7 +115,7 @@ test('getSetting returns current value after setSetting', () => {
116
115
  });
117
116
 
118
117
  test('getSetting returns correct type for colour objects', () => {
119
- const colour = settings.getSetting('snaptrackingcolour');
118
+ const colour = settings.getSetting('accentcolour');
120
119
  expect(colour).toHaveProperty('r');
121
120
  expect(colour).toHaveProperty('g');
122
121
  expect(colour).toHaveProperty('b');
@@ -1,6 +1,7 @@
1
1
  import { jest } from '@jest/globals';
2
2
  import { Core } from '../../core/core/core.js';
3
- import { Snapping, SnapPoint } from '../../core/lib/snapping.js';
3
+ import { Snapping } from '../../core/lib/snapping.js';
4
+ import { SnapPoint } from '../../core/lib/auxiliary/snapPoint.js';
4
5
  import { Point } from '../../core/entities/point.js';
5
6
  import { DesignCore } from '../../core/designCore.js';
6
7
 
@@ -8,7 +9,7 @@ const core = new Core();
8
9
  const snapping = new Snapping();
9
10
 
10
11
  // Add a valid item to scene
11
- core.scene.addItem('Line', { points: [new Point(10, 10), new Point(100, 10)] });
12
+ core.scene.addEntity('Line', { points: [new Point(10, 10), new Point(100, 10)] });
12
13
 
13
14
  afterEach(() => {
14
15
  // Reset all mouse button states so tests don't affect each other via
@@ -69,21 +70,10 @@ test('Test SnapPoint.draw', () => {
69
70
  DesignCore.Scene.auxiliaryEntities.clear();
70
71
  snapping.addSnapPoint(new SnapPoint(new Point(0.5, 0.5), SnapPoint.Type.CENTRE));
71
72
  const snapPointEntity = DesignCore.Scene.auxiliaryEntities.get(0);
72
- const ctx = {
73
- strokeStyle: '',
74
- lineWidth: 0,
75
- setLineDash: jest.fn(),
76
- beginPath: jest.fn(),
77
- moveTo: jest.fn(),
78
- lineTo: jest.fn(),
79
- closePath: jest.fn(),
80
- arc: jest.fn(),
81
- stroke: jest.fn(),
82
- };
83
- snapPointEntity.draw(ctx, 1);
84
- expect(ctx.beginPath).toHaveBeenCalled();
85
- expect(ctx.arc).toHaveBeenCalledWith(0.5, 0.5, expect.any(Number), 0, 6.283);
86
- expect(ctx.stroke).toHaveBeenCalled();
73
+ const renderer = makeSnapRenderer();
74
+ snapPointEntity.draw(renderer, 1);
75
+ expect(renderer.setColour).toHaveBeenCalled();
76
+ expect(renderer.drawShape).toHaveBeenCalled();
87
77
  });
88
78
 
89
79
  test('Test Snapping.addTrackingLine', () => {
@@ -98,23 +88,9 @@ test('Test TrackingLine.draw', () => {
98
88
  // note: the default matrix has d=-1, so scene y is in [-1, 0] for a 1x1 canvas
99
89
  snapping.addTrackingLine(new Point(0.2, -0.5), new Point(0.8, -0.5));
100
90
  const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
101
- const ctx = {
102
- save: jest.fn(),
103
- restore: jest.fn(),
104
- beginPath: jest.fn(),
105
- strokeStyle: '',
106
- lineWidth: 0,
107
- setLineDash: jest.fn(),
108
- moveTo: jest.fn(),
109
- lineTo: jest.fn(),
110
- stroke: jest.fn(),
111
- };
112
- trackingLine.draw(ctx, 1);
113
- expect(ctx.save).toHaveBeenCalled();
114
- expect(ctx.moveTo).toHaveBeenCalled();
115
- expect(ctx.lineTo).toHaveBeenCalled();
116
- expect(ctx.stroke).toHaveBeenCalled();
117
- expect(ctx.restore).toHaveBeenCalled();
91
+ const renderer = makeTrackingRenderer();
92
+ trackingLine.draw(renderer, 1);
93
+ expect(renderer.drawShape).toHaveBeenCalled();
118
94
  });
119
95
 
120
96
  test('Test TrackingLine.draw degenerate zero direction', () => {
@@ -122,20 +98,10 @@ test('Test TrackingLine.draw degenerate zero direction', () => {
122
98
  // inputPoint equals snapPoint → direction is zero vector → draw returns early
123
99
  snapping.addTrackingLine(new Point(0.5, 0.5), new Point(0.5, 0.5));
124
100
  const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
125
- const ctx = {
126
- save: jest.fn(),
127
- restore: jest.fn(),
128
- beginPath: jest.fn(),
129
- strokeStyle: '',
130
- lineWidth: 0,
131
- setLineDash: jest.fn(),
132
- moveTo: jest.fn(),
133
- lineTo: jest.fn(),
134
- stroke: jest.fn(),
135
- };
136
- trackingLine.draw(ctx, 1);
137
- expect(ctx.moveTo).not.toHaveBeenCalled();
138
- expect(ctx.stroke).not.toHaveBeenCalled();
101
+ const renderer = makeTrackingRenderer();
102
+ trackingLine.draw(renderer, 1);
103
+ expect(renderer.drawShape).not.toHaveBeenCalled();
104
+ expect(renderer.restore).not.toHaveBeenCalled();
139
105
  });
140
106
 
141
107
  test('Test Snapping.polarSnap', () => {
@@ -199,35 +165,27 @@ test('Test Snapping.orthoSnap', () => {
199
165
  expect(snapping.orthoSnap(new Point(0, 0))).toBeUndefined();
200
166
  });
201
167
 
202
- // ─── Context mock helpers ────────────────────────────────────────────────────
168
+ // ─── Renderer mock helpers ──────────────────────────────────────────────────
203
169
 
204
- /** @return {object} mock HTML canvas context for SnapPoint.draw tests */
205
- function makeSnapCtx() {
170
+ /** @return {object} mock renderer for SnapPoint.draw tests */
171
+ function makeSnapRenderer() {
206
172
  return {
207
- strokeStyle: '',
208
- lineWidth: 0,
209
- setLineDash: jest.fn(),
210
- beginPath: jest.fn(),
211
- moveTo: jest.fn(),
212
- lineTo: jest.fn(),
213
- closePath: jest.fn(),
214
- arc: jest.fn(),
215
- stroke: jest.fn(),
173
+ setColour: jest.fn(),
174
+ setLineWidth: jest.fn(),
175
+ setDash: jest.fn(),
176
+ drawShape: jest.fn(),
216
177
  };
217
178
  }
218
179
 
219
- /** @return {object} mock HTML canvas context for TrackingLine.draw tests */
220
- function makeTrackingCtx() {
180
+ /** @return {object} mock renderer for TrackingLine.draw tests */
181
+ function makeTrackingRenderer() {
221
182
  return {
222
183
  save: jest.fn(),
223
184
  restore: jest.fn(),
224
- beginPath: jest.fn(),
225
- strokeStyle: '',
226
- lineWidth: 0,
227
- setLineDash: jest.fn(),
228
- moveTo: jest.fn(),
229
- lineTo: jest.fn(),
230
- stroke: jest.fn(),
185
+ setColour: jest.fn(),
186
+ setLineWidth: jest.fn(),
187
+ setDash: jest.fn(),
188
+ drawShape: jest.fn(),
231
189
  };
232
190
  }
233
191
 
@@ -294,74 +252,58 @@ test('Test Snapping.getSnapPoint with non-matching override returns undefined',
294
252
 
295
253
  // ─── SnapPoint.draw – all snap types ────────────────────────────────────────
296
254
 
297
- test('Test SnapPoint.draw END type draws closed polygon without arc', () => {
255
+ test('Test SnapPoint.draw END type draws closed polygon', () => {
298
256
  const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.END);
299
- const ctx = makeSnapCtx();
300
- sp.draw(ctx, 1);
301
- expect(ctx.beginPath).toHaveBeenCalled();
302
- expect(ctx.closePath).toHaveBeenCalled();
303
- expect(ctx.stroke).toHaveBeenCalled();
304
- expect(ctx.arc).not.toHaveBeenCalled();
257
+ const renderer = makeSnapRenderer();
258
+ sp.draw(renderer, 1);
259
+ expect(renderer.drawShape).toHaveBeenCalledTimes(1);
260
+ expect(renderer.drawShape).toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({ closed: true }));
305
261
  });
306
262
 
307
- test('Test SnapPoint.draw MID type draws closed polygon without arc', () => {
263
+ test('Test SnapPoint.draw MID type draws closed polygon', () => {
308
264
  const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.MID);
309
- const ctx = makeSnapCtx();
310
- sp.draw(ctx, 1);
311
- expect(ctx.beginPath).toHaveBeenCalled();
312
- expect(ctx.closePath).toHaveBeenCalled();
313
- expect(ctx.stroke).toHaveBeenCalled();
314
- expect(ctx.arc).not.toHaveBeenCalled();
265
+ const renderer = makeSnapRenderer();
266
+ sp.draw(renderer, 1);
267
+ expect(renderer.drawShape).toHaveBeenCalledTimes(1);
268
+ expect(renderer.drawShape).toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({ closed: true }));
315
269
  });
316
270
 
317
- test('Test SnapPoint.draw QUADRANT type draws closed polygon without arc', () => {
271
+ test('Test SnapPoint.draw QUADRANT type draws closed polygon', () => {
318
272
  const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.QUADRANT);
319
- const ctx = makeSnapCtx();
320
- sp.draw(ctx, 1);
321
- expect(ctx.beginPath).toHaveBeenCalled();
322
- expect(ctx.closePath).toHaveBeenCalled();
323
- expect(ctx.stroke).toHaveBeenCalled();
324
- expect(ctx.arc).not.toHaveBeenCalled();
273
+ const renderer = makeSnapRenderer();
274
+ sp.draw(renderer, 1);
275
+ expect(renderer.drawShape).toHaveBeenCalledTimes(1);
276
+ expect(renderer.drawShape).toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({ closed: true }));
325
277
  });
326
278
 
327
- test('Test SnapPoint.draw NEAREST type draws closed polygon without arc', () => {
279
+ test('Test SnapPoint.draw NEAREST type draws closed polygon', () => {
328
280
  const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.NEAREST);
329
- const ctx = makeSnapCtx();
330
- sp.draw(ctx, 1);
331
- expect(ctx.beginPath).toHaveBeenCalled();
332
- expect(ctx.closePath).toHaveBeenCalled();
333
- expect(ctx.stroke).toHaveBeenCalled();
334
- expect(ctx.arc).not.toHaveBeenCalled();
281
+ const renderer = makeSnapRenderer();
282
+ sp.draw(renderer, 1);
283
+ expect(renderer.drawShape).toHaveBeenCalledTimes(1);
284
+ expect(renderer.drawShape).toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({ closed: true }));
335
285
  });
336
286
 
337
- test('Test SnapPoint.draw TANGENT type draws arc with tangent line', () => {
287
+ test('Test SnapPoint.draw TANGENT type draws circle and tangent line', () => {
338
288
  const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.TANGENT);
339
- const ctx = makeSnapCtx();
340
- sp.draw(ctx, 1);
341
- expect(ctx.beginPath).toHaveBeenCalled();
342
- expect(ctx.arc).toHaveBeenCalledWith(5, 5, expect.any(Number), 0, 6.283);
343
- expect(ctx.stroke).toHaveBeenCalled();
344
- expect(ctx.closePath).not.toHaveBeenCalled();
289
+ const renderer = makeSnapRenderer();
290
+ sp.draw(renderer, 1);
291
+ expect(renderer.drawShape).toHaveBeenCalledTimes(2);
345
292
  });
346
293
 
347
- test('Test SnapPoint.draw NODE type draws arc with X inside', () => {
294
+ test('Test SnapPoint.draw NODE type draws circle and X lines', () => {
348
295
  const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.NODE);
349
- const ctx = makeSnapCtx();
350
- sp.draw(ctx, 1);
351
- expect(ctx.beginPath).toHaveBeenCalled();
352
- expect(ctx.arc).toHaveBeenCalledWith(5, 5, expect.any(Number), 0, 6.283);
353
- expect(ctx.stroke).toHaveBeenCalled();
354
- expect(ctx.closePath).not.toHaveBeenCalled();
296
+ const renderer = makeSnapRenderer();
297
+ sp.draw(renderer, 1);
298
+ expect(renderer.drawShape).toHaveBeenCalledTimes(3);
355
299
  });
356
300
 
357
- test('Test SnapPoint.draw PERPENDICULAR type draws L-shape without arc', () => {
301
+ test('Test SnapPoint.draw PERPENDICULAR type draws L-shape', () => {
358
302
  const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.PERPENDICULAR);
359
- const ctx = makeSnapCtx();
360
- sp.draw(ctx, 1);
361
- expect(ctx.beginPath).toHaveBeenCalled();
362
- expect(ctx.stroke).toHaveBeenCalled();
363
- expect(ctx.closePath).not.toHaveBeenCalled();
364
- expect(ctx.arc).not.toHaveBeenCalled();
303
+ const renderer = makeSnapRenderer();
304
+ sp.draw(renderer, 1);
305
+ expect(renderer.drawShape).toHaveBeenCalledTimes(1);
306
+ expect(renderer.drawShape).not.toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({ closed: true }));
365
307
  });
366
308
 
367
309
  // ─── TrackingLine.draw – out-of-bounds cases ─────────────────────────────────
@@ -371,10 +313,10 @@ test('Test TrackingLine.draw vertical line outside x bounds', () => {
371
313
  // dir.x === 0 and from.x = 2 > boundsMax.x = 1 → draw returns early
372
314
  snapping.addTrackingLine(new Point(2, -0.5), new Point(2, 0.5));
373
315
  const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
374
- const ctx = makeTrackingCtx();
375
- trackingLine.draw(ctx, 1);
376
- expect(ctx.moveTo).not.toHaveBeenCalled();
377
- expect(ctx.stroke).not.toHaveBeenCalled();
316
+ const renderer = makeTrackingRenderer();
317
+ trackingLine.draw(renderer, 1);
318
+ expect(renderer.drawShape).not.toHaveBeenCalled();
319
+ expect(renderer.restore).not.toHaveBeenCalled();
378
320
  });
379
321
 
380
322
  test('Test TrackingLine.draw horizontal line outside y bounds', () => {
@@ -382,10 +324,10 @@ test('Test TrackingLine.draw horizontal line outside y bounds', () => {
382
324
  // dir.y === 0 and from.y = -5 < boundsMin.y = -1 → draw returns early
383
325
  snapping.addTrackingLine(new Point(0.5, -5), new Point(0.8, -5));
384
326
  const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
385
- const ctx = makeTrackingCtx();
386
- trackingLine.draw(ctx, 1);
387
- expect(ctx.moveTo).not.toHaveBeenCalled();
388
- expect(ctx.stroke).not.toHaveBeenCalled();
327
+ const renderer = makeTrackingRenderer();
328
+ trackingLine.draw(renderer, 1);
329
+ expect(renderer.drawShape).not.toHaveBeenCalled();
330
+ expect(renderer.restore).not.toHaveBeenCalled();
389
331
  });
390
332
 
391
333
  test('Test TrackingLine.draw line does not intersect canvas bounds', () => {
@@ -394,8 +336,8 @@ test('Test TrackingLine.draw line does not intersect canvas bounds', () => {
394
336
  // tMax (-1) < tMin (-0.5) → draw returns early
395
337
  snapping.addTrackingLine(new Point(2, -0.5), new Point(3, 0.5));
396
338
  const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
397
- const ctx = makeTrackingCtx();
398
- trackingLine.draw(ctx, 1);
399
- expect(ctx.moveTo).not.toHaveBeenCalled();
400
- expect(ctx.stroke).not.toHaveBeenCalled();
339
+ const renderer = makeTrackingRenderer();
340
+ trackingLine.draw(renderer, 1);
341
+ expect(renderer.drawShape).not.toHaveBeenCalled();
342
+ expect(renderer.restore).not.toHaveBeenCalled();
401
343
  });
@@ -58,24 +58,24 @@ describe('StateManager', () => {
58
58
  }
59
59
  const target = em.get(2);
60
60
  sm.commit(em, [new UpdateState(target, { layer: 'newLayer' })]);
61
- expect(em.get(2).layer).toBe('newLayer');
61
+ expect(em.get(2).getProperty('layer')).toBe('newLayer');
62
62
  sm.undo();
63
- expect(em.get(2).layer).toBe('testLayer');
63
+ expect(em.get(2).getProperty('layer')).toBe('testLayer');
64
64
  sm.redo();
65
- expect(em.get(2).layer).toBe('newLayer');
65
+ expect(em.get(2).getProperty('layer')).toBe('newLayer');
66
66
  });
67
67
 
68
68
  test('addState removed future states', () => {
69
69
  const em = new EntityManager();
70
70
  const line = new Line({ layer: 'testLayer', points: [new Point(), new Point(10, 11)] });
71
71
  sm.commit(em, [new AddState(line)]);
72
- expect(em.get(0).layer).toBe('testLayer');
72
+ expect(em.get(0).getProperty('layer')).toBe('testLayer');
73
73
  sm.commit(em, [new UpdateState(line, { layer: 'differentLayer', points: [new Point(1, 2), new Point(101, 110)] })]);
74
- expect(em.get(0).layer).toBe('differentLayer');
74
+ expect(em.get(0).getProperty('layer')).toBe('differentLayer');
75
75
  sm.undo();
76
- expect(em.get(0).layer).toBe('testLayer');
76
+ expect(em.get(0).getProperty('layer')).toBe('testLayer');
77
77
  sm.redo();
78
- expect(em.get(0).layer).toBe('differentLayer');
78
+ expect(em.get(0).getProperty('layer')).toBe('differentLayer');
79
79
  });
80
80
 
81
81
  test('clearHistory resets history and indices', () => {
@@ -1,4 +1,5 @@
1
1
  import { Arc } from '../../core/entities/arc.js';
2
+ import { Hatch } from '../../core/entities/hatch.js';
2
3
  import { Utils } from '../../core/lib/utils.js';
3
4
  import { Point } from '../../core/entities/point.js';
4
5
 
@@ -74,7 +75,7 @@ test('Test Utils.sortPointsOnArc mutates array ordered by angular position', ()
74
75
  ];
75
76
 
76
77
  const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)], direction: 1 });
77
- Utils.sortPointsOnArc(pts, arc.points[1], arc.points[2], arc.points[0], arc.direction);
78
+ Utils.sortPointsOnArc(pts, arc.points[1], arc.points[2], arc.points[0], arc.getProperty('direction'));
78
79
  // Expect order: (10,0), (7.1, 7.1), (10,10), (-7.1, 7.1), (-10,0)
79
80
  expect(pts[0].x).toBe(10);
80
81
  expect(pts[0].y).toBe(0);
@@ -92,7 +93,7 @@ test('Test Utils.sortPointsOnArc mutates array ordered by angular position', ()
92
93
  expect(pts[4].y).toBe(0);
93
94
 
94
95
  const cwArc = new Arc({ points: [new Point(0, 0), new Point(-10, 0), new Point(10, 0)], direction: -1 });
95
- Utils.sortPointsOnArc(pts, cwArc.points[1], cwArc.points[2], cwArc.points[0], cwArc.direction);
96
+ Utils.sortPointsOnArc(pts, cwArc.points[1], cwArc.points[2], cwArc.points[0], cwArc.getProperty('direction'));
96
97
  // Expect order: (-10,0), (-7.1, 7.1), (10,10), (7.1, 7.1), (10,0)
97
98
  expect(pts[0].x).toBe(-10);
98
99
  expect(pts[0].y).toBe(0);
@@ -110,6 +111,39 @@ test('Test Utils.sortPointsOnArc mutates array ordered by angular position', ()
110
111
  expect(pts[4].y).toBe(0);
111
112
  });
112
113
 
114
+ test('Test Utils.cloneProperties copies default properties from entity', () => {
115
+ const arc = new Arc({ layer: 'walls', lineWidth: 5, lineType: 'DASHED' });
116
+ const props = Utils.cloneProperties(arc);
117
+ expect(props.layer).toBe('walls');
118
+ expect(props.lineWidth).toBe(5);
119
+ expect(props.lineType).toBe('DASHED');
120
+ expect(Object.keys(props)).toEqual(expect.arrayContaining(['layer', 'colour', 'lineWidth', 'lineType']));
121
+ });
122
+
123
+ test('Test Utils.cloneProperties respects custom properties list', () => {
124
+ const arc = new Arc({ layer: 'walls', lineWidth: 5, lineType: 'DASHED' });
125
+ const props = Utils.cloneProperties(arc, ['layer', 'lineWidth']);
126
+ expect(props.layer).toBe('walls');
127
+ expect(props.lineWidth).toBe(5);
128
+ expect(props.lineType).toBeUndefined();
129
+ expect(props.colour).toBeUndefined();
130
+ });
131
+
132
+ test('Test Utils.cloneProperties copies patternName from Hatch', () => {
133
+ const hatch = new Hatch({ patternName: 'ANSI37' });
134
+ const props = Utils.cloneProperties(hatch);
135
+ expect(props.patternName).toBe('ANSI37');
136
+ // lineType is hidden on Hatch
137
+ expect(props.lineType).toBeUndefined();
138
+ expect(props.lineWidth).toBeDefined();
139
+ });
140
+
141
+ test('Test Utils.cloneProperties does not include patternName for non-hatch entities', () => {
142
+ const arc = new Arc({ layer: 'walls' });
143
+ const props = Utils.cloneProperties(arc);
144
+ expect(props.patternName).toBeUndefined();
145
+ });
146
+
113
147
  test('Test Utils.areaOfTriangle', () => {
114
148
  expect(Utils.areaOfTriangle(new Point(0, 0), new Point(10, 0), new Point(0, 10))).toBe(50);
115
149
  expect(Utils.areaOfTriangle(new Point(-5, -5), new Point(5, -5), new Point(0, 5))).toBe(50);
@@ -0,0 +1,132 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { EntityProperties } from '../../core/properties/entityProperties.js';
3
+ import { Property } from '../../core/properties/property.js';
4
+ import { Entity } from '../../core/entities/entity.js';
5
+
6
+ // initialise core
7
+ new Core();
8
+
9
+ /**
10
+ * Minimal entity-like object for isolated EntityProperties tests.
11
+ * @return {Object} plain object with layer and lineWidth fields
12
+ */
13
+ function makeTarget() {
14
+ return { layer: '0', lineWidth: 1 };
15
+ }
16
+
17
+ describe('EntityProperties.add / get', () => {
18
+ test('get returns value after add', () => {
19
+ const ep = new EntityProperties(makeTarget());
20
+ ep.add('layer', { type: Property.Type.LIST, value: '0' });
21
+ expect(ep.get('layer')).toBe('0');
22
+ });
23
+
24
+ test('get returns undefined for unknown property', () => {
25
+ const ep = new EntityProperties(makeTarget());
26
+ expect(ep.get('unknown')).toBeUndefined();
27
+ });
28
+ });
29
+
30
+ describe('EntityProperties.set', () => {
31
+ test('updates value in _store, readable via get', () => {
32
+ const target = makeTarget();
33
+ const ep = new EntityProperties(target);
34
+ ep.add('layer', { type: Property.Type.LIST, value: target.layer });
35
+ ep.set('layer', 'WALLS');
36
+ expect(ep.get('layer')).toBe('WALLS');
37
+ });
38
+
39
+ test('no-op for unknown property', () => {
40
+ const target = makeTarget();
41
+ const ep = new EntityProperties(target);
42
+ expect(() => ep.set('unknown', 'X')).not.toThrow();
43
+ });
44
+
45
+ test('no-op when property is readOnly', () => {
46
+ const target = makeTarget();
47
+ const ep = new EntityProperties(target);
48
+ ep.add('type', { type: Property.Type.LABEL, value: 'Line', readOnly: true });
49
+ ep.set('type', 'Circle');
50
+ expect(ep.get('type')).toBe('Line');
51
+ });
52
+ });
53
+
54
+ describe('EntityProperties.has', () => {
55
+ test('returns true for registered property', () => {
56
+ const ep = new EntityProperties(makeTarget());
57
+ ep.add('layer', { type: Property.Type.LIST, value: '0' });
58
+ expect(ep.has('layer')).toBe(true);
59
+ });
60
+
61
+ test('returns false for unregistered property', () => {
62
+ const ep = new EntityProperties(makeTarget());
63
+ expect(ep.has('layer')).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('EntityProperties.remove', () => {
68
+ test('removes a registered property', () => {
69
+ const ep = new EntityProperties(makeTarget());
70
+ ep.add('layer', { type: Property.Type.LIST, value: '0' });
71
+ ep.remove('layer');
72
+ expect(ep.has('layer')).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('EntityProperties.list', () => {
77
+ test('returns names of visible properties in registration order', () => {
78
+ const ep = new EntityProperties(makeTarget());
79
+ ep.add('layer', { type: Property.Type.LIST, value: '0' });
80
+ ep.add('lineWidth', { type: Property.Type.NUMBER, value: 1 });
81
+ ep.add('hidden', { type: Property.Type.LABEL, value: 'x', visible: false });
82
+ expect(ep.list()).toEqual(['layer', 'lineWidth']);
83
+ });
84
+ });
85
+
86
+ describe('EntityProperties.definition', () => {
87
+ test('returns the Property descriptor', () => {
88
+ const ep = new EntityProperties(makeTarget());
89
+ ep.add('layer', { type: Property.Type.LIST, value: '0', dxfCode: 8 });
90
+ const def = ep.definition('layer');
91
+ expect(def).toBeInstanceOf(Property);
92
+ expect(def.type).toBe(Property.Type.LIST);
93
+ expect(def.dxfCode).toBe(8);
94
+ });
95
+
96
+ test('returns undefined for unknown property', () => {
97
+ const ep = new EntityProperties(makeTarget());
98
+ expect(ep.definition('unknown')).toBeUndefined();
99
+ });
100
+ });
101
+
102
+ describe('Entity.properties integration', () => {
103
+ test('entity has properties registered for layer, lineType, lineWidth, colour', () => {
104
+ const e = new Entity();
105
+ expect(e.properties.has('layer')).toBe(true);
106
+ expect(e.properties.has('lineType')).toBe(true);
107
+ expect(e.properties.has('lineWidth')).toBe(true);
108
+ expect(e.properties.has('colour')).toBe(true);
109
+ });
110
+
111
+ test('properties values match entity fields at construction', () => {
112
+ const e = new Entity({ layer: 'TEST', lineWidth: 5 });
113
+ expect(e.properties.get('layer')).toBe('TEST');
114
+ expect(e.properties.get('lineWidth')).toBe(5);
115
+ });
116
+
117
+ test('set updates _store value, readable via get', () => {
118
+ const e = new Entity();
119
+ e.properties.set('layer', 'DOORS');
120
+ expect(e.properties.get('layer')).toBe('DOORS');
121
+ });
122
+
123
+ test('list returns the 4 registered property names', () => {
124
+ const e = new Entity();
125
+ expect(e.properties.list()).toEqual(['layer', 'lineType', 'lineWidth', 'colour']);
126
+ });
127
+
128
+ test('properties is non-enumerable on entity', () => {
129
+ const e = new Entity();
130
+ expect(Object.keys(e)).not.toContain('properties');
131
+ });
132
+ });
@@ -11,3 +11,56 @@ test('Test property.loadValue', () => {
11
11
  flags.setFlagValue(4);
12
12
  expect(Property.loadValue([flags], 3)).toBe(4);
13
13
  });
14
+
15
+ describe('Property.Type', () => {
16
+ test('has all expected members', () => {
17
+ expect(Property.Type.NUMBER).toBe('NUMBER');
18
+ expect(Property.Type.STRING).toBe('STRING');
19
+ expect(Property.Type.BOOLEAN).toBe('BOOLEAN');
20
+ expect(Property.Type.LIST).toBe('LIST');
21
+ expect(Property.Type.COLOUR).toBe('COLOUR');
22
+ expect(Property.Type.LABEL).toBe('LABEL');
23
+ expect(Property.Type.ENTITIES).toBe('ENTITIES');
24
+ });
25
+
26
+ test('has exactly 7 members', () => {
27
+ expect(Object.keys(Property.Type)).toHaveLength(7);
28
+ });
29
+ });
30
+
31
+ describe('Property instance', () => {
32
+ test('stores type and value', () => {
33
+ const p = new Property({ type: Property.Type.NUMBER, value: 42 });
34
+ expect(p.type).toBe(Property.Type.NUMBER);
35
+ expect(p.value).toBe(42);
36
+ });
37
+
38
+ test('defaults readOnly to false and visible to true', () => {
39
+ const p = new Property({ type: Property.Type.STRING, value: 'hello' });
40
+ expect(p.readOnly).toBe(false);
41
+ expect(p.visible).toBe(true);
42
+ });
43
+
44
+ test('set value updates when not readOnly', () => {
45
+ const p = new Property({ type: Property.Type.NUMBER, value: 1 });
46
+ p.value = 99;
47
+ expect(p.value).toBe(99);
48
+ });
49
+
50
+ test('set value is ignored when readOnly', () => {
51
+ const p = new Property({ type: Property.Type.LABEL, value: 'fixed', readOnly: true });
52
+ p.value = 'changed';
53
+ expect(p.value).toBe('fixed');
54
+ });
55
+
56
+ test('stores dxfCode', () => {
57
+ const p = new Property({ type: Property.Type.LIST, value: '0', dxfCode: 8 });
58
+ expect(p.dxfCode).toBe(8);
59
+ });
60
+
61
+ test('no-arg constructor produces undefined fields without throwing', () => {
62
+ const p = new Property();
63
+ expect(p.type).toBeUndefined();
64
+ expect(p.value).toBeUndefined();
65
+ });
66
+ });