@design-core/core 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.eslintrc.json +4 -0
  2. package/TODO.md +49 -6
  3. package/core/core/core.js +0 -3
  4. package/core/dimensions/alignedDimension.js +5 -4
  5. package/core/dimensions/angularDimension.js +6 -5
  6. package/core/dimensions/baseCircularDimension.js +3 -2
  7. package/core/dimensions/baseDimension.js +59 -93
  8. package/core/dimensions/baseLinearDimension.js +6 -5
  9. package/core/dimensions/diametricDimension.js +4 -3
  10. package/core/dimensions/dimension.js +21 -14
  11. package/core/dimensions/radialDimension.js +4 -3
  12. package/core/dimensions/rotatedDimension.js +12 -11
  13. package/core/entities/arc.js +112 -145
  14. package/core/entities/arctext.js +212 -174
  15. package/core/entities/basePolyline.js +30 -282
  16. package/core/entities/circle.js +77 -99
  17. package/core/entities/entity.js +114 -93
  18. package/core/entities/hatch.js +322 -249
  19. package/core/entities/insert.js +66 -81
  20. package/core/entities/line.js +21 -142
  21. package/core/entities/lwpolyline.js +16 -11
  22. package/core/entities/polyline.js +4 -4
  23. package/core/entities/rectangle.js +3 -1
  24. package/core/entities/solid.js +6 -15
  25. package/core/entities/text.js +188 -199
  26. package/core/lib/auxiliary/rubberBand.js +32 -0
  27. package/core/lib/auxiliary/selectionWindow.js +63 -0
  28. package/core/lib/auxiliary/snapPoint.js +113 -0
  29. package/core/lib/auxiliary/trackingLine.js +76 -0
  30. package/core/lib/canvas.js +228 -166
  31. package/core/lib/clipboard.js +7 -1
  32. package/core/lib/colour.js +16 -0
  33. package/core/lib/commandLine.js +1 -6
  34. package/core/lib/commandManager.js +18 -8
  35. package/core/lib/constants.js +28 -0
  36. package/core/lib/dxf/dxf.js +4 -3
  37. package/core/lib/entityManager.js +18 -11
  38. package/core/lib/inputManager.js +35 -11
  39. package/core/lib/intersect.js +25 -0
  40. package/core/lib/patterns.js +8 -1
  41. package/core/lib/plotOptions.js +67 -0
  42. package/core/lib/polylineUtils.js +75 -0
  43. package/core/lib/renderers/cairoRenderer.js +371 -0
  44. package/core/lib/renderers/canvasRenderer.js +315 -0
  45. package/core/lib/renderers/pdfRenderer.js +366 -0
  46. package/core/lib/renderers/rendererBase.js +284 -0
  47. package/core/lib/renderers/svgRenderer.js +343 -0
  48. package/core/lib/scene.js +27 -5
  49. package/core/lib/selectionManager.js +2 -2
  50. package/core/lib/settings.js +4 -2
  51. package/core/lib/snapping.js +4 -191
  52. package/core/lib/stateManager.js +30 -1
  53. package/core/lib/utils.js +21 -2
  54. package/core/properties/entityProperties.js +104 -0
  55. package/core/properties/property.js +106 -2
  56. package/core/properties/propertyManager.js +29 -13
  57. package/core/tables/block.js +4 -37
  58. package/core/tables/endblk.js +1 -1
  59. package/core/tables/layer.js +8 -0
  60. package/core/tables/styleManagerBase.js +3 -2
  61. package/core/tables/tableManagerBase.js +8 -3
  62. package/core/tools/chamfer.js +111 -137
  63. package/core/tools/chamferFilletBase.js +135 -34
  64. package/core/tools/copy.js +3 -3
  65. package/core/tools/explode.js +2 -2
  66. package/core/tools/extend.js +132 -19
  67. package/core/tools/fillet.js +86 -183
  68. package/core/tools/matchProp.js +2 -11
  69. package/core/tools/mirror.js +7 -6
  70. package/core/tools/move.js +3 -3
  71. package/core/tools/offset.js +3 -5
  72. package/core/tools/pasteclip.js +1 -1
  73. package/core/tools/rotate.js +6 -3
  74. package/core/tools/scale.js +1 -1
  75. package/core/tools/trim.js +265 -23
  76. package/core/tools/zoom.js +1 -1
  77. package/jest.config.js +6 -0
  78. package/package.json +5 -3
  79. package/test/dimensions/alignedDimension.test.js +14 -7
  80. package/test/dimensions/angularDimension.test.js +6 -6
  81. package/test/dimensions/baseDimension.test.js +69 -8
  82. package/test/dimensions/baseLinearDimension.test.js +62 -0
  83. package/test/dimensions/diametricDimension.test.js +6 -6
  84. package/test/dimensions/radialDimension.test.js +6 -6
  85. package/test/dimensions/rotatedDimension.test.js +14 -7
  86. package/test/entities/arc.test.js +80 -83
  87. package/test/entities/arctext.test.js +180 -53
  88. package/test/entities/basePolyline.test.js +68 -2
  89. package/test/entities/circle.test.js +57 -85
  90. package/test/entities/entity.test.js +42 -8
  91. package/test/entities/hatch.test.js +543 -70
  92. package/test/entities/insert.test.js +96 -1
  93. package/test/entities/line.test.js +15 -41
  94. package/test/entities/lwpolyline.test.js +104 -0
  95. package/test/entities/polyline.test.js +11 -0
  96. package/test/entities/rectangle.test.js +11 -0
  97. package/test/entities/solid.test.js +1 -1
  98. package/test/entities/text.test.js +294 -69
  99. package/test/integration/exportIntegration.reference.dxf +9 -9
  100. package/test/integration/exportIntegration.test.js +2 -2
  101. package/test/integration/intersectionIntegration.test.js +4 -5
  102. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  103. package/test/integration/pdfRendererIntegration.test.js +61 -0
  104. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  105. package/test/integration/svgRendererIntegration.test.js +61 -0
  106. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  107. package/test/integration/userInputIntegration.test.js +239 -132
  108. package/test/lib/canvas.test.js +742 -123
  109. package/test/lib/constants.test.js +27 -0
  110. package/test/lib/entityManager.test.js +4 -4
  111. package/test/lib/inputManager.test.js +80 -19
  112. package/test/lib/intersect.test.js +83 -0
  113. package/test/lib/plotOptions.test.js +179 -0
  114. package/test/lib/rubberBand.test.js +84 -0
  115. package/test/lib/settings.test.js +5 -6
  116. package/test/lib/snapping.test.js +71 -129
  117. package/test/lib/stateManager.test.js +7 -7
  118. package/test/lib/utils.test.js +36 -2
  119. package/test/properties/entityProperties.test.js +132 -0
  120. package/test/properties/property.test.js +53 -0
  121. package/test/properties/propertyManager.test.js +3 -3
  122. package/test/renderers/canvasRenderer.test.js +317 -0
  123. package/test/renderers/pdfRenderer.test.js +271 -0
  124. package/test/renderers/rendererBase.test.js +213 -0
  125. package/test/renderers/svgRenderer.test.js +326 -0
  126. package/test/tables/block.test.js +31 -1
  127. package/test/tables/endblk.test.js +2 -2
  128. package/test/tables/layer.test.js +30 -0
  129. package/test/test-helpers/hatchGridRenderer.js +180 -0
  130. package/test/test-helpers/test-helpers.js +152 -0
  131. package/test/tools/chamfer.test.js +227 -5
  132. package/test/tools/chamferFilletBase.test.js +98 -9
  133. package/test/tools/copy.test.js +2 -2
  134. package/test/tools/explode.test.js +3 -3
  135. package/test/tools/extend.test.js +331 -0
  136. package/test/tools/fillet.test.js +181 -0
  137. package/test/tools/matchProp.test.js +5 -5
  138. package/test/tools/mirror.test.js +13 -13
  139. package/test/tools/offset.test.js +170 -3
  140. package/test/tools/pasteclip.test.js +5 -5
  141. package/test/tools/rotate.test.js +32 -2
  142. package/test/tools/scale.test.js +5 -5
  143. package/test/tools/trim.test.js +526 -0
  144. package/test/tools/zoom.test.js +7 -7
  145. package/core/lib/selectionWindow.js +0 -83
@@ -1,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
  }
@@ -256,6 +259,77 @@ export class Canvas {
256
259
  }
257
260
  }
258
261
 
262
+ /**
263
+ * Build a renderer transform matrix that fits a scene area onto a page.
264
+ * Both PdfRenderer and SvgRenderer expect d = +scale (no Y-flip).
265
+ * @param {object} options
266
+ * @param {BoundingBox} options.area - scene area to map onto the page
267
+ * @param {number} options.pageWidth - page width in renderer units (points)
268
+ * @param {number} options.pageHeight - page height in renderer units (points)
269
+ * @param {number|null} [options.plotScale=null] - numeric scale factor, or null to fit to page
270
+ * @param {number} [options.margin=40] - margin on each side in renderer units
271
+ * @return {{a: number, d: number, e: number, f: number}|null} matrix object, or null if area is empty
272
+ */
273
+ buildExportMatrix({ area, pageWidth, pageHeight, plotScale = null, margin = 40 }) {
274
+ const sceneWidth = area.xMax - area.xMin;
275
+ const sceneHeight = area.yMax - area.yMin;
276
+ if (sceneWidth === 0 || sceneHeight === 0) return null;
277
+
278
+ const usableWidth = pageWidth - 2 * margin;
279
+ const usableHeight = pageHeight - 2 * margin;
280
+
281
+ const scale = plotScale === null ?
282
+ Math.min(usableWidth / sceneWidth, usableHeight / sceneHeight) :
283
+ plotScale;
284
+
285
+ const translateX = margin + (usableWidth - sceneWidth * scale) / 2 - area.xMin * scale;
286
+ const translateY = margin + (usableHeight - sceneHeight * scale) / 2 - area.yMin * scale;
287
+
288
+ return { a: scale, d: scale, e: translateX, f: translateY };
289
+ }
290
+
291
+ /**
292
+ * Export the visible scene entities to a renderer.
293
+ * Unlike paint(), this skips background, grid, preview, selection, and auxiliary passes.
294
+ * Builds and applies the export matrix from plotOptions before painting.
295
+ * @param {RendererBase} renderer
296
+ * @param {PlotOptions} plotOptions
297
+ * @return {boolean} false if no exportable area was found
298
+ */
299
+ exportTo(renderer, plotOptions) {
300
+ let area;
301
+ if (plotOptions.plotArea === PlotOptions.Area.DISPLAY) {
302
+ const viewport = this.getSceneOffset();
303
+ area = new BoundingBox(new Point(viewport.xmin, viewport.ymin), new Point(viewport.xmax, viewport.ymax));
304
+ } else {
305
+ area = DesignCore.Scene.boundingBox();
306
+ }
307
+ if (!area) return false;
308
+
309
+ const matrix = this.buildExportMatrix({
310
+ area,
311
+ pageWidth: plotOptions.pageWidth,
312
+ pageHeight: plotOptions.pageHeight,
313
+ plotScale: plotOptions.plotScale,
314
+ });
315
+ if (!matrix) return false;
316
+
317
+ renderer.setTransform(matrix);
318
+
319
+ renderer.setBackgroundColour({ r: 255, g: 255, b: 255 });
320
+ renderer.setStyle(plotOptions.style);
321
+
322
+ const plotScale = matrix.a;
323
+ this.#paintEntities(
324
+ DesignCore.Scene.entities, renderer, null,
325
+ (entity) => DesignCore.LayerManager.getItemByName(entity.getProperty(Property.Names.LAYER))?.isPlottable,
326
+ null, null,
327
+ plotScale,
328
+ );
329
+
330
+ return true;
331
+ }
332
+
259
333
  /**
260
334
  * Paint the canvas
261
335
  * @param {object} context
@@ -278,151 +352,161 @@ export class Canvas {
278
352
  this.flipped = true;
279
353
  }
280
354
 
355
+ const renderer = new this.#renderer(context);
356
+
281
357
  // set the transform for the context
282
358
  // 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
- }
359
+ renderer.setTransform(this.matrix);
360
+ renderer.setBackgroundColour(DesignCore.Settings.canvasbackgroundcolour);
290
361
 
291
362
  const pos = new Point();
292
363
  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
364
  const scale = this.getScale();
316
365
 
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
- }
366
+ // Paint the scene background
367
+ renderer.fillBackground(origin, width, height, scale);
368
+ // Paint the grid
369
+ this.paintGrid(renderer);
370
+
371
+ const bg = renderer.getBackgroundColour();
372
+ const hoverHaloColour = Colour.blend(DesignCore.Core.settings.accentcolour, bg, 0.5);
373
+ const selectionHaloColour = Colour.blend(DesignCore.Core.settings.accentcolour, bg, 0.25);
374
+ const selectionLineWidthDelta = 5;
375
+
376
+ // Paint the primary scene items (layer-visibility filtered).
377
+ // Hovered entity detected inline: renderer draws glow + entity in a single drawShape call.
378
+ this.#paintEntities(
379
+ DesignCore.Scene.entities, renderer, null,
380
+ (entity) => DesignCore.LayerManager.getItemByName(entity.getProperty(Property.Names.LAYER))?.isVisible,
381
+ DesignCore.Scene.hoverEntities,
382
+ { colour: hoverHaloColour, lineWidthDelta: selectionLineWidthDelta },
383
+ );
332
384
 
333
385
  // 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
- }
386
+ this.#paintEntities(DesignCore.Scene.previewEntities, renderer);
341
387
 
342
- // Paint the selected scene items
343
- this.paintState = this.paintStates.SELECTED;
388
+ // Paint the selected scene items: single pass — renderer handles glow + entity internally.
344
389
  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
+ this.#paintEntities(selectedItems, renderer, { colour: selectionHaloColour, lineWidthDelta: selectionLineWidthDelta });
350
391
 
351
392
  // Paint the auxiliary scene items
352
393
  // auxiliary items include things like the selection window, snap points etc
353
394
  // these items have their own draw routine
354
- this.paintState = this.paintStates.AUXILLARY;
395
+ renderer.setHighlight(false);
355
396
  const auxCount = DesignCore.Scene.auxiliaryEntities.count();
356
397
  for (let l = 0; l < auxCount; l++) {
357
- DesignCore.Scene.auxiliaryEntities.get(l).draw(context, scale);
398
+ renderer.save();
399
+ DesignCore.Scene.auxiliaryEntities.get(l).draw(renderer, scale);
400
+ renderer.restore();
358
401
  }
402
+ }
359
403
 
360
- this.paintState = undefined;
404
+ /**
405
+ * Recursively paint a single entity and any children it returns.
406
+ * Container entities (Insert, BaseDimension) return an array of child items from draw().
407
+ * Leaf entities return undefined. Renderer state is saved/restored around each entity.
408
+ * @param {Object} entity - entity to paint
409
+ * @param {Object} renderer - renderer instance
410
+ * @param {Object|undefined} parent - enclosing insert/dimension for ByBlock colour resolution
411
+ * @param {Object} overrides - rendering overrides passed to setContext
412
+ * @param {number} [scale] - forwarded to setContext
413
+ */
414
+ #paintEntity(entity, renderer, parent, overrides, scale) {
415
+ renderer.save();
416
+ try {
417
+ this.setContext(entity, renderer, parent, overrides, scale);
418
+ const children = entity.draw(renderer);
419
+ if (children) {
420
+ for (const item of children) {
421
+ this.#paintEntity(item, renderer, entity, overrides, scale);
422
+ }
423
+ }
424
+ } catch (err) {
425
+ Logging.instance.warn(`draw failed for entity type '${entity.type}': ${err}`);
426
+ } finally {
427
+ renderer.restore();
428
+ }
361
429
  }
362
430
 
363
431
  /**
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
432
+ * Draw a collection of entities, calling setContext before each draw.
433
+ * Supports EntityManager instances (with .count()/.get()) and plain arrays.
434
+ * @param {EntityManager|Array} entities
435
+ * @param {Object} renderer - renderer instance
436
+ * @param {Object|null} [overrides] - optional rendering overrides passed to setContext for all entities
437
+ * @param {Object} [overrides.colour] - highlight glow colour (entity always draws in its natural colour)
438
+ * @param {number} [overrides.lineWidthDelta] - glow extension in pixels (divided by scale); added to renderer highlight delta
439
+ * @param {Function} [filter] - optional predicate; entity is skipped when it returns falsy
440
+ * @param {EntityManager|null} [hoverCollection] - when provided, entities found in this set receive hoverOverrides
441
+ * @param {Object|null} [hoverOverrides] - overrides applied to entities matched in hoverCollection
442
+ * @param {number} [scale] - forwarded to #paintEntity / setContext; defaults to screen zoom when omitted
368
443
  */
369
- setContext(item, context, block = undefined) {
370
- const scale = this.getScale();
444
+ #paintEntities(entities, renderer, overrides = null, filter = undefined, hoverCollection = null, hoverOverrides = null, scale = undefined) {
445
+ const isManager = typeof entities.count === 'function';
446
+ const count = isManager ? entities.count() : entities.length;
447
+ for (let i = 0; i < count; i++) {
448
+ const entity = isManager ? entities.get(i) : entities[i];
449
+ if (filter && !filter(entity)) continue;
450
+ const isHovered = hoverCollection !== null && hoverCollection.indexOf(entity) !== -1;
451
+ this.#paintEntity(entity, renderer, undefined, isHovered ? hoverOverrides : (overrides ?? {}), scale);
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Set the scene context on the renderer before drawing an entity.
457
+ * @param {Object} item - entity to draw
458
+ * @param {Object} renderer - renderer instance
459
+ * @param {Object} [block] - insert or dimension element for the current block, required for colour ByBlock
460
+ * @param {Object} [overrides] - optional rendering overrides
461
+ * @param {Object} [overrides.colour] - overrides all colour resolution (e.g. for halo/glow passes)
462
+ * @param {number} [overrides.lineWidthDelta] - additional width in pixels (divided by scale) added to the entity's line width
463
+ * @param {number} [scale] - scale divisor for line width and dash pattern; defaults to the current screen zoom
464
+ */
465
+ setContext(item, renderer, block = undefined, overrides = {}, scale = this.getScale()) {
371
466
  let colour = item.getDrawColour();
372
467
  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
- }
468
+ let lineWidth = (item.getProperty(Property.Names.LINEWIDTH) ?? 1) / scale;
469
+ const bg = renderer.getBackgroundColour() ?? { r: 0, g: 0, b: 0 };
383
470
 
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
- }
471
+ if (block && item.entityColour.aci === 0) {
472
+ colour = block.getDrawColour();
389
473
  }
390
-
391
474
  // 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;
475
+ if (this.#isAci7(item, block)) {
394
476
  colour = (bg.r + bg.g + bg.b) / 3 < 128 ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 };
395
477
  }
396
478
 
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);
479
+ if (overrides.colour !== undefined) {
480
+ // Highlight pass: overrides.colour is the glow colour only — the entity
481
+ // always draws in its natural resolved colour on the normal pass.
482
+ // lineWidthDelta goes to the highlight delta so the glow extends beyond
483
+ // the entity line; the entity's own lineWidth is unchanged.
484
+ renderer.setHighlight(true, overrides.colour, (overrides.lineWidthDelta ?? 0) / scale);
485
+ } else {
486
+ renderer.setHighlight(false);
487
+ lineWidth += (overrides.lineWidthDelta ?? 0) / scale;
408
488
  }
489
+
490
+ renderer.setColour(colour);
491
+ renderer.setLineWidth(lineWidth);
492
+ renderer.setDash(lineType.getPattern(scale), 0);
409
493
  }
410
494
 
411
495
  /**
412
496
  * Resolve whether the effective draw colour for an item is ACI 7.
413
497
  * Follows ByLayer and ByBlock indirection.
414
- * @param {entity} item
415
- * @param {entity} block - enclosing insert/dimension, or undefined
498
+ * @param {Object} item - entity to check
499
+ * @param {Object} [block] - enclosing insert/dimension, or undefined
416
500
  * @return {boolean}
417
501
  */
418
502
  #isAci7(item, block) {
419
503
  let colour;
420
504
  if (item.entityColour.byBlock && block) {
421
505
  colour = block.entityColour.byLayer ?
422
- DesignCore.LayerManager.getItemByName(block.layer)?.layerColour :
506
+ DesignCore.LayerManager.getItemByName(block.getProperty(Property.Names.LAYER))?.layerColour :
423
507
  block.entityColour;
424
508
  } else if (item.entityColour.byLayer) {
425
- colour = DesignCore.LayerManager.getItemByName(item.layer)?.layerColour;
509
+ colour = DesignCore.LayerManager.getItemByName(item.getProperty(Property.Names.LAYER))?.layerColour;
426
510
  } else {
427
511
  colour = item.entityColour;
428
512
  }
@@ -431,23 +515,18 @@ export class Canvas {
431
515
 
432
516
  /**
433
517
  * Paint the background grid
434
- * @param {object} context
518
+ * @param {object} renderer
435
519
  */
436
- paintGrid(context) {
520
+ paintGrid(renderer) {
437
521
  const scale = this.getScale();
438
522
 
439
523
  // TODO: Move grid linewidth to settings?
440
524
  let lineWidth = 0.75;
441
525
 
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
- }
526
+ renderer.setColour(DesignCore.Settings.gridcolour);
527
+ renderer.setLineWidth(lineWidth / scale);
528
+ renderer.setDash([], 0);
529
+ renderer.setHighlight(false);
451
530
 
452
531
  const extents = this.getSceneOffset();
453
532
 
@@ -457,13 +536,11 @@ export class Canvas {
457
536
  const ygridmax = extents.ymax;
458
537
 
459
538
  // 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();
539
+ renderer.drawSegments([
540
+ { x1: xgridmin, y1: 0, x2: xgridmax, y2: 0 },
541
+ { x1: 0, y1: 0, x2: 0, y2: ygridmax },
542
+ { x1: 0, y1: 0, x2: 0, y2: ygridmin },
543
+ ], []);
467
544
 
468
545
  // only draw the grid if within scale limits
469
546
  if (scale < this.minScaleFactor || scale > this.maxScaleFactor) {
@@ -473,50 +550,35 @@ export class Canvas {
473
550
  if (DesignCore.Settings['drawgrid']) {
474
551
  // set a feint linewidth for the grid
475
552
  lineWidth = lineWidth * 0.25;
553
+ renderer.setLineWidth(lineWidth / scale);
476
554
 
477
- try { // HTML Canvas
478
- context.lineWidth = lineWidth / scale;
479
- context.beginPath();
480
- } catch { // Cairo
481
- context.setLineWidth(lineWidth / scale);
482
- }
555
+ // Target ~60px between grid lines; snap to nearest 1/2/5 × 10^n interval
556
+ const targetPx = 60;
557
+ const rawInterval = targetPx / scale;
558
+ const mag = Math.pow(10, Math.floor(Math.log10(rawInterval)));
559
+ const norm = rawInterval / mag;
560
+ const gridInterval = mag * (norm < 2 ? 1 : norm < 5 ? 2 : 5);
483
561
 
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
- }
562
+ const gridSegments = [];
497
563
 
498
564
  // Draw positive minor X gridlines
499
565
  for (let i = 0; i < xgridmax; i = i + gridInterval) {
500
- context.moveTo(i, ygridmin);
501
- context.lineTo(i, ygridmax);
566
+ gridSegments.push({ x1: i, y1: ygridmin, x2: i, y2: ygridmax });
502
567
  }
503
568
  // Draw negative minor X gridlines
504
569
  for (let i = 0; i > xgridmin; i = i - gridInterval) {
505
- context.moveTo(i, ygridmin);
506
- context.lineTo(i, ygridmax);
570
+ gridSegments.push({ x1: i, y1: ygridmin, x2: i, y2: ygridmax });
507
571
  }
508
572
  // Draw positive minor Y gridlines
509
573
  for (let i = 0; i < ygridmax; i = i + gridInterval) {
510
- context.moveTo(xgridmin, i);
511
- context.lineTo(xgridmax, i);
574
+ gridSegments.push({ x1: xgridmin, y1: i, x2: xgridmax, y2: i });
512
575
  }
513
576
  // Draw negative minor Y gridlines
514
577
  for (let i = 0; i > ygridmin; i = i - gridInterval) {
515
- context.moveTo(xgridmin, i);
516
- context.lineTo(xgridmax, i);
578
+ gridSegments.push({ x1: xgridmin, y1: i, x2: xgridmax, y2: i });
517
579
  }
518
580
 
519
- context.stroke();
581
+ renderer.drawSegments(gridSegments, []);
520
582
  }
521
583
  }
522
584
 
@@ -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
  /**