@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
@@ -1,14 +1,21 @@
1
1
  import { Matrix } from './matrix.js';
2
- import { Colours } from './colours.js';
2
+ import { Colour } from './colour.js';
3
+ import { BoundingBox } from './boundingBox.js';
3
4
  import { Point } from '../entities/point.js';
4
5
  import { Input } from './input.js';
6
+ import { Logging } from './logging.js';
7
+ import { PlotOptions } from './plotOptions.js';
8
+ // import { RendererBase } from './renderers/rendererBase.js';
5
9
 
6
10
  import { DesignCore } from '../designCore.js';
11
+ import { Property } from '../properties/property.js';
7
12
 
8
13
  /** Canvas Class */
9
14
  export class Canvas {
10
15
  #panCursorTimeout;
11
16
  #baseCursorState = 'DEFAULT';
17
+ #lastPanCanvasPoint = new Point();
18
+ #renderer = null;
12
19
 
13
20
  /** Create Canvas */
14
21
  constructor() {
@@ -21,19 +28,8 @@ export class Canvas {
21
28
  this.width = 1;
22
29
  this.height = 1;
23
30
 
24
- this.panDelta = new Point();
25
- this.lastDelta = new Point();
26
31
  this.flipped = false;
27
32
 
28
- this.paintState;
29
-
30
- this.paintStates = {
31
- ENTITIES: 'ENTITIES',
32
- TEMPORARY: 'TEMPORARY',
33
- SELECTED: 'SELECTED',
34
- AUXILLARY: 'AUXILLARY',
35
- };
36
-
37
33
  this.cursorStates = Input.Cursor;
38
34
 
39
35
  this.cursorState = Input.Cursor.DEFAULT;
@@ -62,6 +58,15 @@ export class Canvas {
62
58
  this.externalPaintCallbackFunction = callback;
63
59
  }
64
60
 
61
+ /**
62
+ * Set the renderer class to use for entity drawing.
63
+ * Must be called once during UI initialisation before the first paint.
64
+ * @param {Function} RendererClass - CanvasRenderer or CairoRenderer class
65
+ */
66
+ setRenderer(RendererClass) {
67
+ this.#renderer = RendererClass;
68
+ }
69
+
65
70
  /**
66
71
  * Set external cursor callback
67
72
  * Called when the cursor state changes.
@@ -116,8 +121,10 @@ export class Canvas {
116
121
  mouseDown(button) {
117
122
  switch (button) {
118
123
  case 0: // left button
124
+ this.#lastPanCanvasPoint = DesignCore.Mouse.pointOnCanvas();
119
125
  break;
120
126
  case 1: // middle button
127
+ this.#lastPanCanvasPoint = DesignCore.Mouse.pointOnCanvas();
121
128
  clearTimeout(this.#panCursorTimeout);
122
129
  this.#panCursorTimeout = setTimeout(() => this.#setCursor(Input.Cursor.GRABBING), 250);
123
130
  break;
@@ -138,7 +145,6 @@ export class Canvas {
138
145
  break;
139
146
  case 1: // middle button
140
147
  clearTimeout(this.#panCursorTimeout);
141
- this.lastDelta = new Point();
142
148
  this.requestPaint();
143
149
  this.#setCursor(this.#baseCursorState);
144
150
  break;
@@ -169,13 +175,10 @@ export class Canvas {
169
175
  * Pan the canvas
170
176
  */
171
177
  pan() {
172
- // pandelta: mouse drag distance in scene scale
173
- this.panDelta = DesignCore.Mouse.pointOnScene().subtract(DesignCore.Mouse.transformToScene(DesignCore.Mouse.mouseDownCanvasPoint));
174
- // delta difference between last delta calculation and current mouse position
175
- const delta = this.panDelta.subtract(this.lastDelta);
176
- // set the last delta value
177
- this.lastDelta = this.panDelta;
178
- // add translation to the matrix object
178
+ const current = DesignCore.Mouse.pointOnCanvas();
179
+ const delta = DesignCore.Mouse.transformToScene(current)
180
+ .subtract(DesignCore.Mouse.transformToScene(this.#lastPanCanvasPoint));
181
+ this.#lastPanCanvasPoint = current;
179
182
  this.matrix.translate(delta.x, delta.y);
180
183
  this.requestPaint();
181
184
  }
@@ -186,7 +189,9 @@ export class Canvas {
186
189
  */
187
190
  wheel(delta) {
188
191
  if (DesignCore.Mouse.buttonTwoDown) return;
189
- const scale = Math.pow(1 + Math.abs(delta), delta > 0 ? 1 : -1);
192
+ // Clamp to [-1, 1] so that high-resolution mice and track pads don't produce runaway jumps.
193
+ const clampedDelta = Math.max(-1, Math.min(1, delta));
194
+ const scale = Math.pow(1.2, clampedDelta);
190
195
  if (scale < 1 && this.getScale() > this.minScaleFactor || scale > 1 && this.getScale() < this.maxScaleFactor) {
191
196
  this.zoom(scale);
192
197
  }
@@ -256,6 +261,77 @@ export class Canvas {
256
261
  }
257
262
  }
258
263
 
264
+ /**
265
+ * Build a renderer transform matrix that fits a scene area onto a page.
266
+ * Both PdfRenderer and SvgRenderer expect d = +scale (no Y-flip).
267
+ * @param {object} options
268
+ * @param {BoundingBox} options.area - scene area to map onto the page
269
+ * @param {number} options.pageWidth - page width in renderer units (points)
270
+ * @param {number} options.pageHeight - page height in renderer units (points)
271
+ * @param {number|null} [options.plotScale=null] - numeric scale factor, or null to fit to page
272
+ * @param {number} [options.margin=40] - margin on each side in renderer units
273
+ * @return {{a: number, d: number, e: number, f: number}|null} matrix object, or null if area is empty
274
+ */
275
+ buildExportMatrix({ area, pageWidth, pageHeight, plotScale = null, margin = 40 }) {
276
+ const sceneWidth = area.xMax - area.xMin;
277
+ const sceneHeight = area.yMax - area.yMin;
278
+ if (sceneWidth === 0 || sceneHeight === 0) return null;
279
+
280
+ const usableWidth = pageWidth - 2 * margin;
281
+ const usableHeight = pageHeight - 2 * margin;
282
+
283
+ const scale = plotScale === null ?
284
+ Math.min(usableWidth / sceneWidth, usableHeight / sceneHeight) :
285
+ plotScale;
286
+
287
+ const translateX = margin + (usableWidth - sceneWidth * scale) / 2 - area.xMin * scale;
288
+ const translateY = margin + (usableHeight - sceneHeight * scale) / 2 - area.yMin * scale;
289
+
290
+ return { a: scale, d: scale, e: translateX, f: translateY };
291
+ }
292
+
293
+ /**
294
+ * Export the visible scene entities to a renderer.
295
+ * Unlike paint(), this skips background, grid, preview, selection, and auxiliary passes.
296
+ * Builds and applies the export matrix from plotOptions before painting.
297
+ * @param {RendererBase} renderer
298
+ * @param {PlotOptions} plotOptions
299
+ * @return {boolean} false if no exportable area was found
300
+ */
301
+ exportTo(renderer, plotOptions) {
302
+ let area;
303
+ if (plotOptions.plotArea === PlotOptions.Area.DISPLAY) {
304
+ const viewport = this.getSceneOffset();
305
+ area = new BoundingBox(new Point(viewport.xmin, viewport.ymin), new Point(viewport.xmax, viewport.ymax));
306
+ } else {
307
+ area = DesignCore.Scene.boundingBox();
308
+ }
309
+ if (!area) return false;
310
+
311
+ const matrix = this.buildExportMatrix({
312
+ area,
313
+ pageWidth: plotOptions.pageWidth,
314
+ pageHeight: plotOptions.pageHeight,
315
+ plotScale: plotOptions.plotScale,
316
+ });
317
+ if (!matrix) return false;
318
+
319
+ renderer.setTransform(matrix);
320
+
321
+ renderer.setBackgroundColour({ r: 255, g: 255, b: 255 });
322
+ renderer.setStyle(plotOptions.style);
323
+
324
+ const plotScale = matrix.a;
325
+ this.#paintEntities(
326
+ DesignCore.Scene.entities, renderer, null,
327
+ (entity) => DesignCore.LayerManager.getItemByName(entity.getProperty(Property.Names.LAYER))?.isPlottable,
328
+ null, null,
329
+ plotScale,
330
+ );
331
+
332
+ return true;
333
+ }
334
+
259
335
  /**
260
336
  * Paint the canvas
261
337
  * @param {object} context
@@ -278,151 +354,161 @@ export class Canvas {
278
354
  this.flipped = true;
279
355
  }
280
356
 
357
+ const renderer = new this.#renderer(context);
358
+
281
359
  // set the transform for the context
282
360
  // this will set all the pan and zoom actions
283
- try {
284
- context.setTransform(this.matrix);
285
- } catch {
286
- // context.setMatrix(this.matrix)
287
- context.translate(this.matrix.e, this.matrix.f);
288
- context.scale(this.matrix.a, this.matrix.d);
289
- }
361
+ renderer.setTransform(this.matrix);
362
+ renderer.setBackgroundColour(DesignCore.Settings.canvasbackgroundcolour);
290
363
 
291
364
  const pos = new Point();
292
365
  const origin = DesignCore.Mouse.transformToScene(pos);
293
-
294
- // Paint the scene background
295
- try {// HTML
296
- // this.clear()
297
- context.fillStyle = Colours.rgbToString(DesignCore.Settings.canvasbackgroundcolour);
298
- context.fillRect(origin.x, origin.y, width / this.getScale(), height / this.getScale());
299
- // context.globalAlpha = this.cvs.alpha
300
- } catch { // Cairo
301
- const rgbColour = Colours.rgbToScaledRGB(DesignCore.Settings.canvasbackgroundcolour);
302
- context.setSourceRGB(rgbColour.r, rgbColour.g, rgbColour.b);
303
- const scaled = new Point(width, height);
304
- const sc = DesignCore.Mouse.transformToScene(scaled);
305
- context.moveTo(origin.x, origin.y);
306
- context.lineTo(origin.x, sc.y);
307
- context.lineTo(sc.x, sc.y);
308
- context.lineTo(sc.x, origin.y);
309
- context.lineTo(origin.x, origin.y);
310
- context.fill();
311
- }
312
-
313
- this.paintGrid(context, width, height);
314
-
315
366
  const scale = this.getScale();
316
367
 
317
- // Paint the primary scene items
318
- this.paintState = this.paintStates.ENTITIES;
319
-
320
- const entityCount = DesignCore.Scene.entities.count();
321
- for (let i = 0; i < entityCount; i++) {
322
- const entity = DesignCore.Scene.entities.get(i);
323
- const layer = DesignCore.LayerManager.getItemByName(entity.layer);
324
-
325
- if (!layer?.isVisible) {
326
- continue;
327
- }
328
-
329
- this.setContext(entity, context, undefined, scale);
330
- entity.draw(context, scale);
331
- }
368
+ // Paint the scene background
369
+ renderer.fillBackground(origin, width, height, scale);
370
+ // Paint the grid
371
+ this.paintGrid(renderer);
372
+
373
+ const bg = renderer.getBackgroundColour();
374
+ const hoverHaloColour = Colour.blend(DesignCore.Core.settings.accentcolour, bg, 0.5);
375
+ const selectionHaloColour = Colour.blend(DesignCore.Core.settings.accentcolour, bg, 0.25);
376
+ const selectionLineWidthDelta = 5;
377
+
378
+ // Paint the primary scene items (layer-visibility filtered).
379
+ // Hovered entity detected inline: renderer draws glow + entity in a single drawShape call.
380
+ this.#paintEntities(
381
+ DesignCore.Scene.entities, renderer, null,
382
+ (entity) => DesignCore.LayerManager.getItemByName(entity.getProperty(Property.Names.LAYER))?.isVisible,
383
+ DesignCore.Scene.hoverEntities,
384
+ { colour: hoverHaloColour, lineWidthDelta: selectionLineWidthDelta },
385
+ );
332
386
 
333
387
  // Paint the temporary scene items
334
- this.paintState = this.paintStates.TEMPORARY;
335
- const tempCount = DesignCore.Scene.tempEntities.count();
336
- for (let j = 0; j < tempCount; j++) {
337
- const entity = DesignCore.Scene.tempEntities.get(j);
338
- this.setContext(entity, context, undefined, scale);
339
- entity.draw(context, scale);
340
- }
388
+ this.#paintEntities(DesignCore.Scene.previewEntities, renderer);
341
389
 
342
- // Paint the selected scene items
343
- this.paintState = this.paintStates.SELECTED;
344
- const selectedItems = DesignCore.Scene.selectionManager.selectedItems;
345
- for (let k = 0; k < selectedItems.length; k++) {
346
- const entity = selectedItems[k];
347
- this.setContext(entity, context, undefined, scale);
348
- entity.draw(context, scale);
349
- }
390
+ // Paint the selected scene items: single pass — renderer handles glow + entity internally.
391
+ const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities;
392
+ this.#paintEntities(selectedEntities, renderer, { colour: selectionHaloColour, lineWidthDelta: selectionLineWidthDelta });
350
393
 
351
394
  // Paint the auxiliary scene items
352
395
  // auxiliary items include things like the selection window, snap points etc
353
396
  // these items have their own draw routine
354
- this.paintState = this.paintStates.AUXILLARY;
397
+ renderer.setHighlight(false);
355
398
  const auxCount = DesignCore.Scene.auxiliaryEntities.count();
356
399
  for (let l = 0; l < auxCount; l++) {
357
- DesignCore.Scene.auxiliaryEntities.get(l).draw(context, scale);
400
+ renderer.save();
401
+ DesignCore.Scene.auxiliaryEntities.get(l).draw(renderer, scale);
402
+ renderer.restore();
358
403
  }
404
+ }
359
405
 
360
- this.paintState = undefined;
406
+ /**
407
+ * Recursively paint a single entity and any children it returns.
408
+ * Container entities (Insert, DimensionBase) return an array of child items from draw().
409
+ * Leaf entities return undefined. Renderer state is saved/restored around each entity.
410
+ * @param {Object} entity - entity to paint
411
+ * @param {Object} renderer - renderer instance
412
+ * @param {Object|undefined} parent - enclosing insert/dimension for ByBlock colour resolution
413
+ * @param {Object} overrides - rendering overrides passed to setContext
414
+ * @param {number} [scale] - forwarded to setContext
415
+ */
416
+ #paintEntity(entity, renderer, parent, overrides, scale) {
417
+ renderer.save();
418
+ try {
419
+ this.setContext(entity, renderer, parent, overrides, scale);
420
+ const children = entity.draw(renderer);
421
+ if (children) {
422
+ for (const item of children) {
423
+ this.#paintEntity(item, renderer, entity, overrides, scale);
424
+ }
425
+ }
426
+ } catch (err) {
427
+ Logging.instance.warn(`draw failed for entity type '${entity.type}': ${err}`);
428
+ } finally {
429
+ renderer.restore();
430
+ }
361
431
  }
362
432
 
363
433
  /**
364
- * Set the scene context
365
- * @param {entity} item
366
- * @param {Object} context - scene painting context from ui
367
- * @param {Object} block - insert or dimension element for the current block, required for colour ByBlock
434
+ * Draw a collection of entities, calling setContext before each draw.
435
+ * Supports EntityManager instances (with .count()/.get()) and plain arrays.
436
+ * @param {EntityManager|Array} entities
437
+ * @param {Object} renderer - renderer instance
438
+ * @param {Object|null} [overrides] - optional rendering overrides passed to setContext for all entities
439
+ * @param {Object} [overrides.colour] - highlight glow colour (entity always draws in its natural colour)
440
+ * @param {number} [overrides.lineWidthDelta] - glow extension in pixels (divided by scale); added to renderer highlight delta
441
+ * @param {Function} [filter] - optional predicate; entity is skipped when it returns falsy
442
+ * @param {EntityManager|null} [hoverCollection] - when provided, entities found in this set receive hoverOverrides
443
+ * @param {Object|null} [hoverOverrides] - overrides applied to entities matched in hoverCollection
444
+ * @param {number} [scale] - forwarded to #paintEntity / setContext; defaults to screen zoom when omitted
368
445
  */
369
- setContext(item, context, block = undefined) {
370
- const scale = this.getScale();
446
+ #paintEntities(entities, renderer, overrides = null, filter = undefined, hoverCollection = null, hoverOverrides = null, scale = undefined) {
447
+ const isManager = typeof entities.count === 'function';
448
+ const count = isManager ? entities.count() : entities.length;
449
+ for (let i = 0; i < count; i++) {
450
+ const entity = isManager ? entities.get(i) : entities[i];
451
+ if (filter && !filter(entity)) continue;
452
+ const isHovered = hoverCollection !== null && hoverCollection.indexOf(entity) !== -1;
453
+ this.#paintEntity(entity, renderer, undefined, isHovered ? hoverOverrides : (overrides ?? {}), scale);
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Set the scene context on the renderer before drawing an entity.
459
+ * @param {Object} item - entity to draw
460
+ * @param {Object} renderer - renderer instance
461
+ * @param {Object} [block] - insert or dimension element for the current block, required for colour ByBlock
462
+ * @param {Object} [overrides] - optional rendering overrides
463
+ * @param {Object} [overrides.colour] - overrides all colour resolution (e.g. for halo/glow passes)
464
+ * @param {number} [overrides.lineWidthDelta] - additional width in pixels (divided by scale) added to the entity's line width
465
+ * @param {number} [scale] - scale divisor for line width and dash pattern; defaults to the current screen zoom
466
+ */
467
+ setContext(item, renderer, block = undefined, overrides = {}, scale = this.getScale()) {
371
468
  let colour = item.getDrawColour();
372
469
  const lineType = item.getLineType();
373
- let lineWidth = item.lineWidth / scale;
374
-
375
- if (this.paintState === this.paintStates.SELECTED) {
376
- // Set Context for selected items
377
- colour = DesignCore.Core.settings.selecteditemscolour;
378
- lineWidth = (item.lineWidth * 2) / scale;
379
- } else if (this.paintState === this.paintStates.TEMPORARY) {
380
- // Set context for temp items
381
- lineWidth = (item.lineWidth * 2) / scale;
382
- }
470
+ let lineWidth = (item.getProperty(Property.Names.LINEWIDTH) ?? 1) / scale;
471
+ const bg = renderer.getBackgroundColour() ?? { r: 0, g: 0, b: 0 };
383
472
 
384
- if (block && this.paintState != this.paintStates.SELECTED) {
385
- // set colour for items with colour byblock
386
- if (item.entityColour.aci === 0) {
387
- colour = block.getDrawColour();
388
- }
473
+ if (block && item.entityColour.aci === 0) {
474
+ colour = block.getDrawColour();
389
475
  }
390
-
391
476
  // ACI 7: white on dark background, black on light background
392
- if (this.paintState !== this.paintStates.SELECTED && this.#isAci7(item, block)) {
393
- const bg = DesignCore.Settings.canvasbackgroundcolour;
477
+ if (this.#isAci7(item, block)) {
394
478
  colour = (bg.r + bg.g + bg.b) / 3 < 128 ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 };
395
479
  }
396
480
 
397
- try { // HTML Canvas
398
- context.strokeStyle = Colours.rgbToString(colour);
399
- context.fillStyle = Colours.rgbToString(colour);
400
- context.lineWidth = lineWidth;
401
- context.setLineDash(lineType.getPattern(scale));
402
- context.beginPath();
403
- } catch { // Cairo
404
- const rgbColour = Colours.rgbToScaledRGB(colour);
405
- context.setSourceRGB(rgbColour.r, rgbColour.g, rgbColour.b);
406
- context.setDash(lineType.getPattern(scale), 1);
407
- context.setLineWidth(lineWidth);
481
+ if (overrides.colour !== undefined) {
482
+ // Highlight pass: overrides.colour is the glow colour only — the entity
483
+ // always draws in its natural resolved colour on the normal pass.
484
+ // lineWidthDelta goes to the highlight delta so the glow extends beyond
485
+ // the entity line; the entity's own lineWidth is unchanged.
486
+ renderer.setHighlight(true, overrides.colour, (overrides.lineWidthDelta ?? 0) / scale);
487
+ } else {
488
+ renderer.setHighlight(false);
489
+ lineWidth += (overrides.lineWidthDelta ?? 0) / scale;
408
490
  }
491
+
492
+ renderer.setColour(colour);
493
+ renderer.setLineWidth(lineWidth);
494
+ renderer.setDash(lineType.getPattern(scale), 0);
409
495
  }
410
496
 
411
497
  /**
412
498
  * Resolve whether the effective draw colour for an item is ACI 7.
413
499
  * Follows ByLayer and ByBlock indirection.
414
- * @param {entity} item
415
- * @param {entity} block - enclosing insert/dimension, or undefined
500
+ * @param {Object} item - entity to check
501
+ * @param {Object} [block] - enclosing insert/dimension, or undefined
416
502
  * @return {boolean}
417
503
  */
418
504
  #isAci7(item, block) {
419
505
  let colour;
420
506
  if (item.entityColour.byBlock && block) {
421
507
  colour = block.entityColour.byLayer ?
422
- DesignCore.LayerManager.getItemByName(block.layer)?.layerColour :
508
+ DesignCore.LayerManager.getItemByName(block.getProperty(Property.Names.LAYER))?.layerColour :
423
509
  block.entityColour;
424
510
  } else if (item.entityColour.byLayer) {
425
- colour = DesignCore.LayerManager.getItemByName(item.layer)?.layerColour;
511
+ colour = DesignCore.LayerManager.getItemByName(item.getProperty(Property.Names.LAYER))?.layerColour;
426
512
  } else {
427
513
  colour = item.entityColour;
428
514
  }
@@ -431,23 +517,18 @@ export class Canvas {
431
517
 
432
518
  /**
433
519
  * Paint the background grid
434
- * @param {object} context
520
+ * @param {object} renderer
435
521
  */
436
- paintGrid(context) {
522
+ paintGrid(renderer) {
437
523
  const scale = this.getScale();
438
524
 
439
525
  // TODO: Move grid linewidth to settings?
440
526
  let lineWidth = 0.75;
441
527
 
442
- try { // HTML Canvas
443
- context.strokeStyle = Colours.rgbToString(DesignCore.Settings.gridcolour);
444
- context.lineWidth = lineWidth / scale;
445
- context.beginPath();
446
- } catch { // Cairo
447
- context.setLineWidth(lineWidth / scale);
448
- const rgbColour = Colours.rgbToScaledRGB(DesignCore.Settings.gridcolour);
449
- context.setSourceRGB(rgbColour.r, rgbColour.g, rgbColour.b);
450
- }
528
+ renderer.setColour(DesignCore.Settings.gridcolour);
529
+ renderer.setLineWidth(lineWidth / scale);
530
+ renderer.setDash([], 0);
531
+ renderer.setHighlight(false);
451
532
 
452
533
  const extents = this.getSceneOffset();
453
534
 
@@ -457,13 +538,11 @@ export class Canvas {
457
538
  const ygridmax = extents.ymax;
458
539
 
459
540
  // Draw major gridlines through origin
460
- context.moveTo(xgridmin, 0);
461
- context.lineTo(xgridmax, 0);
462
- context.moveTo(0, 0);
463
- context.lineTo(0, ygridmax);
464
- context.moveTo(0, 0);
465
- context.lineTo(0, ygridmin);
466
- context.stroke();
541
+ renderer.drawSegments([
542
+ { x1: xgridmin, y1: 0, x2: xgridmax, y2: 0 },
543
+ { x1: 0, y1: 0, x2: 0, y2: ygridmax },
544
+ { x1: 0, y1: 0, x2: 0, y2: ygridmin },
545
+ ], []);
467
546
 
468
547
  // only draw the grid if within scale limits
469
548
  if (scale < this.minScaleFactor || scale > this.maxScaleFactor) {
@@ -473,50 +552,35 @@ export class Canvas {
473
552
  if (DesignCore.Settings['drawgrid']) {
474
553
  // set a feint linewidth for the grid
475
554
  lineWidth = lineWidth * 0.25;
555
+ renderer.setLineWidth(lineWidth / scale);
476
556
 
477
- try { // HTML Canvas
478
- context.lineWidth = lineWidth / scale;
479
- context.beginPath();
480
- } catch { // Cairo
481
- context.setLineWidth(lineWidth / scale);
482
- }
557
+ // Target ~60px between grid lines; snap to nearest 1/2/5 × 10^n interval
558
+ const targetPx = 60;
559
+ const rawInterval = targetPx / scale;
560
+ const mag = Math.pow(10, Math.floor(Math.log10(rawInterval)));
561
+ const norm = rawInterval / mag;
562
+ const gridInterval = mag * (norm < 2 ? 1 : norm < 5 ? 2 : 5);
483
563
 
484
- // TODO: add setting for grid spacing
485
- let gridInterval = 100;
486
-
487
- // define the grid spacing based on zoom level
488
- if (scale > 50) {
489
- gridInterval = 1;
490
- } else if (scale > 5) {
491
- gridInterval = 10;
492
- } else if (scale < 0.6) {
493
- gridInterval = 1000;
494
- } else {
495
- gridInterval = 100;
496
- }
564
+ const gridSegments = [];
497
565
 
498
566
  // Draw positive minor X gridlines
499
567
  for (let i = 0; i < xgridmax; i = i + gridInterval) {
500
- context.moveTo(i, ygridmin);
501
- context.lineTo(i, ygridmax);
568
+ gridSegments.push({ x1: i, y1: ygridmin, x2: i, y2: ygridmax });
502
569
  }
503
570
  // Draw negative minor X gridlines
504
571
  for (let i = 0; i > xgridmin; i = i - gridInterval) {
505
- context.moveTo(i, ygridmin);
506
- context.lineTo(i, ygridmax);
572
+ gridSegments.push({ x1: i, y1: ygridmin, x2: i, y2: ygridmax });
507
573
  }
508
574
  // Draw positive minor Y gridlines
509
575
  for (let i = 0; i < ygridmax; i = i + gridInterval) {
510
- context.moveTo(xgridmin, i);
511
- context.lineTo(xgridmax, i);
576
+ gridSegments.push({ x1: xgridmin, y1: i, x2: xgridmax, y2: i });
512
577
  }
513
578
  // Draw negative minor Y gridlines
514
579
  for (let i = 0; i > ygridmin; i = i - gridInterval) {
515
- context.moveTo(xgridmin, i);
516
- context.lineTo(xgridmax, i);
580
+ gridSegments.push({ x1: xgridmin, y1: i, x2: xgridmax, y2: i });
517
581
  }
518
582
 
519
- context.stroke();
583
+ renderer.drawSegments(gridSegments, []);
520
584
  }
521
585
  }
522
586
 
@@ -69,7 +69,7 @@ export class Clipboard {
69
69
  return;
70
70
  }
71
71
 
72
- // copy the item to avoid mutation of originals
72
+ // copy the entity to avoid mutation of originals
73
73
  this.#Entities = entities.map((entity) => Utils.cloneObject(entity));
74
74
  // set basepoint to bottom left of selection bounding box
75
75
  const bbox = BoundingBox.fromEntities(entities);
@@ -152,7 +152,7 @@ export class Clipboard {
152
152
  const clipboardData = { basePoint: undefined, Entities: undefined };
153
153
  const jsonEntities = [];
154
154
 
155
- // convert each entity to a simple object - this allows non-enumberable properties (like points) to be copied
155
+ // convert each entity to a simple object - this allows non-enumerable properties (like points) to be copied
156
156
  for (const ent of this.#Entities) {
157
157
  const jsonEntity = this.#simplify(ent);
158
158
  jsonEntities.push(jsonEntity);
@@ -171,7 +171,13 @@ export class Clipboard {
171
171
  * @return {Object} - simplified object
172
172
  */
173
173
  #simplify(original) {
174
- return Object.getOwnPropertyNames(original).reduce((simplified, property) => {
174
+ const names = Object.getOwnPropertyNames(original);
175
+ // 'points' is now stored in EntityProperties and accessed via a prototype accessor,
176
+ // so it's not an own property — include it explicitly when it's an accessible array.
177
+ if (!names.includes('points') && Array.isArray(original.points)) {
178
+ names.push('points');
179
+ }
180
+ return names.reduce((simplified, property) => {
175
181
  const value = original[property];
176
182
  if (Array.isArray(value)) {
177
183
  simplified[property] = original[property].map((item) => this.#simplify(item));
@@ -63,6 +63,22 @@ export class Colour {
63
63
  this.rgb = rgb;
64
64
  }
65
65
  }
66
+
67
+ /**
68
+ * Blend two RGB colours together.
69
+ * @param {{r:number,g:number,b:number}} colourA
70
+ * @param {{r:number,g:number,b:number}} colourB
71
+ * @param {number} factor - 0 returns colourA, 1 returns colourB
72
+ * @return {{r:number,g:number,b:number}}
73
+ */
74
+ static blend(colourA, colourB, factor) {
75
+ const inverseFactor = 1 - factor;
76
+ return {
77
+ r: Math.round(colourA.r * inverseFactor + colourB.r * factor),
78
+ g: Math.round(colourA.g * inverseFactor + colourB.g * factor),
79
+ b: Math.round(colourA.b * inverseFactor + colourB.b * factor),
80
+ };
81
+ }
66
82
  }
67
83
 
68
84
  /**
@@ -56,12 +56,7 @@ export class CommandLine {
56
56
  this.updateCallbackFunction(this.cmdLine);
57
57
  }
58
58
 
59
- if (DesignCore.Scene.inputManager.activeCommand !== undefined) {
60
- // TODO: This should call a common function that is currently called mouseMove in the scene class
61
- DesignCore.Scene.tempEntities.clear();
62
- DesignCore.Scene.inputManager.activeCommand.preview();
63
- DesignCore.Canvas.requestPaint();
64
- }
59
+ DesignCore.Scene.inputManager.refreshPreview();
65
60
  }
66
61
 
67
62
  /**
@@ -1,4 +1,5 @@
1
1
  import { Line } from '../entities/line.js';
2
+ import { Entity } from '../entities/entity.js';
2
3
  import { Polyline } from '../entities/polyline.js';
3
4
  import { Lwpolyline } from '../entities/lwpolyline.js';
4
5
  import { Circle } from '../entities/circle.js';
@@ -141,21 +142,30 @@ export class CommandManager {
141
142
  }
142
143
 
143
144
  /**
144
- * Create a new instance of type using data
145
+ * Create a new instance of type using data.
146
+ * When data is a plain object (DXF load path), applies a two-pass normalisation:
147
+ * 1. Entity.fromDxf() — resolves group codes shared by all entities (e.g. colour, true colour)
148
+ * 2. CommandClass.fromDxf() — resolves group codes specific to the entity type (e.g. geometry)
149
+ * When data is absent or already a canonical object (e.g. an entity instance), it is passed through unchanged.
145
150
  * @param {string} type
146
- * @param {Array} data
151
+ * @param {Object} data
147
152
  * @return {Object} instance of type
148
153
  */
149
154
  createNew(type, data) {
150
- let newItem;
151
- if (this.isCommand(type)) {
152
- newItem = new classes[this.getCommand(type)](data);
153
- } else {
155
+ if (!this.isCommand(type)) {
154
156
  Logging.instance.warn(`${Strings.Message.UNKNOWNCOMMAND}: ${type}`);
155
157
  return;
156
158
  }
157
-
158
- return newItem;
159
+ const CommandClass = classes[this.getCommand(type)];
160
+ // Skip DXF normalisation when data is absent or is already a canonical object (e.g. an entity instance)
161
+ if (!data || Object.getPrototypeOf(data) !== Object.prototype) {
162
+ return new CommandClass(data);
163
+ }
164
+ // Resolve group codes shared by all entities (colour, true colour, etc.)
165
+ const baseNormalised = Entity.fromDxf(data);
166
+ // Resolve group codes specific to this entity type (geometry, etc.)
167
+ const classNormalised = Object.prototype.hasOwnProperty.call(CommandClass, 'fromDxf') ? CommandClass.fromDxf(baseNormalised) : baseNormalised;
168
+ return new CommandClass(classNormalised);
159
169
  };
160
170
 
161
171
  /**