@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
@@ -0,0 +1,343 @@
1
+ import { RendererBase } from './rendererBase.js';
2
+ import { Text } from '../../entities/text.js';
3
+
4
+ /**
5
+ * Pure-JS SVG renderer.
6
+ * Implements the RendererBase interface by accumulating SVG elements in memory.
7
+ * Call getOutput() to retrieve a complete SVG document string.
8
+ *
9
+ * Design notes:
10
+ * - Zero dependencies: uses only standard SVG elements and string concatenation.
11
+ * - Y-flip: a root <g transform="translate(0,H) scale(1,-1)"> converts scene
12
+ * Y-up coordinates to SVG Y-down. All geometry is drawn in scene Y-up space.
13
+ * - Arc segments use the native SVG A command (not chord approximation).
14
+ * - Text characters receive a local scale(1,-1) to un-flip the glyph.
15
+ * - setTransform() is a direct passthrough: the caller must supply a matrix
16
+ * with d = +scale (no Y-flip), matching the PDF renderer convention.
17
+ * - setHighlight() is a no-op — SVG export shows final geometry only.
18
+ */
19
+ export class SvgRenderer extends RendererBase {
20
+ #pageWidth;
21
+ #pageHeight;
22
+
23
+ /** @type {string[]} */
24
+ #elements = [];
25
+
26
+ // Current drawing state
27
+ #currentColour = { r: 0, g: 0, b: 0 };
28
+ #currentLineWidth = 1;
29
+ #currentDash = [];
30
+ #currentDashOffset = 0;
31
+
32
+ // Low-level path accumulation (used by beginPath / tracePath / stroke / closePath)
33
+ #currentPath = null;
34
+
35
+
36
+ /**
37
+ * @param {number} pageWidth - page width in user units (points)
38
+ * @param {number} pageHeight - page height in user units (points)
39
+ */
40
+ constructor(pageWidth, pageHeight) {
41
+ super();
42
+ this.#pageWidth = pageWidth;
43
+ this.#pageHeight = pageHeight;
44
+ }
45
+
46
+ // --- Frame setup ---
47
+
48
+ /** @inheritdoc */
49
+ setTransform(matrix) {
50
+ const { a, d, e, f } = matrix;
51
+ this.#emit(`<g transform="matrix(${this.#fmt(a)} 0 0 ${this.#fmt(d)} ${this.#fmt(e)} ${this.#fmt(f)})">`);
52
+ }
53
+
54
+ /** @inheritdoc */
55
+ fillBackground() {
56
+ // No-op: background colour is set via setBackgroundColour() and emitted in getOutput()
57
+ }
58
+
59
+ // --- High-level drawing ---
60
+
61
+ /** @inheritdoc */
62
+ drawShape(points, options = {}) {
63
+ if (!points.length) return;
64
+ const pathD = this.#buildPathD(points, options.closed);
65
+
66
+ const doStroke = options.stroke !== false;
67
+ const doFill = options.fill === true;
68
+ const doClip = options.clip === true;
69
+
70
+ if (doClip) return; // clip is a screen-only operation; no-op in SVG export
71
+
72
+ const strokeAttrs = doStroke ?
73
+ `stroke="${this.#colourStr()}" stroke-width="${this.#fmt(this.#currentLineWidth)}"${this.#dashAttrs()}` :
74
+ 'stroke="none"';
75
+
76
+ let fillAttrs = doFill ? `fill="${this.#colourStr()}"` : 'fill="none"';
77
+ if (doFill && options.fillRule) fillAttrs += ` fill-rule="${options.fillRule}"`;
78
+ if (doFill && options.alpha !== undefined && options.alpha !== 1) {
79
+ fillAttrs += ` fill-opacity="${options.alpha}"`;
80
+ }
81
+
82
+ this.#emit(`<path d="${pathD}" ${strokeAttrs} ${fillAttrs}/>`);
83
+ }
84
+
85
+ /** @inheritdoc */
86
+ drawText(characters, fontName, height) {
87
+ if (!characters || !characters.length) return;
88
+ const colour = this.#colourStr();
89
+ for (const ch of characters) {
90
+ const deg = (ch.rotation ?? 0) * 180 / Math.PI;
91
+ // translate to position in scene Y-up, then scale(1,-1) to un-flip the glyph
92
+ // (the parent Y-flip group inverts it), then rotate (negated: scale(1,-1) inverts
93
+ // rotation direction so -deg in this context = +ch.rotation CCW in scene).
94
+ // TODO: handle ch.upsideDownOffset and ch.backwardsOffset for mirrored text variants.
95
+ const transform = `translate(${this.#fmt(ch.x)},${this.#fmt(ch.y)}) scale(1,-1) rotate(${this.#fmt(-deg)})`;
96
+ this.#emit(`<text transform="${transform}" font-family="${fontName}" font-size="${this.#fmt(height)}" fill="${colour}" stroke="none">${this.#escapeXml(ch.char)}</text>`);
97
+ }
98
+ }
99
+
100
+ /** @inheritdoc */
101
+ tracePath(points) {
102
+ const d = this.#buildPathD(points, false);
103
+ this.#currentPath = this.#currentPath !== null ? `${this.#currentPath} ${d}` : d;
104
+ }
105
+
106
+ /** @inheritdoc */
107
+ drawSegments(segments, dashes = []) {
108
+ if (!segments.length) return;
109
+ const colour = this.#colourStr();
110
+ const sw = this.#fmt(this.#currentLineWidth);
111
+
112
+ if (!dashes.length) {
113
+ // Solid: batch all segments into one path element
114
+ let d = '';
115
+ for (const seg of segments) {
116
+ d += `M ${this.#fmt(seg.x1)} ${this.#fmt(seg.y1)} L ${this.#fmt(seg.x2)} ${this.#fmt(seg.y2)} `;
117
+ }
118
+ this.#emit(`<path d="${d.trim()}" stroke="${colour}" stroke-width="${sw}" fill="none"/>`);
119
+ } else {
120
+ // Dashed: one path per segment with its own dashPhase to preserve dash continuity
121
+ const dashStr = dashes.map((v) => this.#fmt(v)).join(',');
122
+ for (const seg of segments) {
123
+ const phase = this.#fmt(seg.dashPhase ?? 0);
124
+ this.#emit(`<path d="M ${this.#fmt(seg.x1)} ${this.#fmt(seg.y1)} L ${this.#fmt(seg.x2)} ${this.#fmt(seg.y2)}" stroke="${colour}" stroke-width="${sw}" stroke-dasharray="${dashStr}" stroke-dashoffset="${phase}" fill="none"/>`);
125
+ }
126
+ }
127
+ }
128
+
129
+ // --- Low-level path ---
130
+
131
+ /** @inheritdoc */
132
+ beginPath() {
133
+ this.#currentPath = null;
134
+ }
135
+
136
+ /** @inheritdoc */
137
+ closePath() {
138
+ if (this.#currentPath !== null) this.#currentPath += ' Z';
139
+ }
140
+
141
+ /** @inheritdoc */
142
+ stroke() {
143
+ if (this.#currentPath === null) return;
144
+ const colour = this.#colourStr();
145
+ const sw = this.#fmt(this.#currentLineWidth);
146
+ this.#emit(`<path d="${this.#currentPath}" stroke="${colour}" stroke-width="${sw}"${this.#dashAttrs()} fill="none"/>`);
147
+ this.#currentPath = null;
148
+ }
149
+
150
+ /** @inheritdoc */
151
+ applyPath(options = {}) {
152
+ if (this.#currentPath === null) return;
153
+ const { stroke = true, fill = false, fillRule, alpha = 1 } = options;
154
+ const strokeAttrs = stroke ?
155
+ `stroke="${this.#colourStr()}" stroke-width="${this.#fmt(this.#currentLineWidth)}"${this.#dashAttrs()}` :
156
+ 'stroke="none"';
157
+ let fillAttrs = fill ? `fill="${this.#colourStr()}"` : 'fill="none"';
158
+ if (fill && fillRule) fillAttrs += ` fill-rule="${fillRule}"`;
159
+ if (fill && alpha !== 1) fillAttrs += ` fill-opacity="${this.#fmt(alpha)}"`;
160
+ this.#emit(`<path d="${this.#currentPath}" ${strokeAttrs} ${fillAttrs}/>`);
161
+ this.#currentPath = null;
162
+ }
163
+
164
+ // --- State ---
165
+
166
+ /** @inheritdoc */
167
+ setColour(rgb) {
168
+ this.#currentColour = this.applyStyle(rgb);
169
+ }
170
+
171
+ /** @inheritdoc */
172
+ setLineWidth(width) {
173
+ this.#currentLineWidth = width;
174
+ }
175
+
176
+ /** @inheritdoc */
177
+ setDash(array, offset = 0) {
178
+ this.#currentDash = array;
179
+ this.#currentDashOffset = offset;
180
+ }
181
+
182
+ /** @inheritdoc */
183
+ setHighlight() {
184
+ // No-op: SVG export does not render hover/selection highlights.
185
+ }
186
+
187
+ // --- Transform ---
188
+
189
+ /** @inheritdoc */
190
+ applyTransform({ x = 0, y = 0, rotation = 0 } = {}) {
191
+ const deg = rotation * 180 / Math.PI;
192
+ const transform = `translate(${this.#fmt(x)},${this.#fmt(y)}) rotate(${this.#fmt(deg)})`;
193
+ // Retroactively apply the transform to the innermost plain <g> (emitted by save()).
194
+ // This mirrors canvas/cairo semantics where applyTransform modifies the current
195
+ // graphics state rather than opening a new save/restore scope.
196
+ for (let i = this.#elements.length - 1; i >= 0; i--) {
197
+ if (this.#elements[i] === '<g>') {
198
+ this.#elements[i] = `<g transform="${transform}">`;
199
+ return;
200
+ }
201
+ }
202
+ // Fallback: no plain <g> found — open a new one (shouldn't happen in normal usage)
203
+ this.#emit(`<g transform="${transform}">`);
204
+ }
205
+
206
+ /** @inheritdoc */
207
+ save() {
208
+ this.#emit('<g>');
209
+ }
210
+
211
+ /** @inheritdoc */
212
+ restore() {
213
+ this.#emit('</g>');
214
+ }
215
+
216
+ // --- Measurement ---
217
+
218
+ /** @inheritdoc */
219
+ measureText(str, fontName, height) {
220
+ return { width: Text.getApproximateWidth(str, height) };
221
+ }
222
+
223
+ /** @inheritdoc */
224
+ measureCharWidth(character) {
225
+ return Text.getApproximateWidth(character, 1);
226
+ }
227
+
228
+ // --- SVG output ---
229
+
230
+ /**
231
+ * Serialise the accumulated elements into a complete SVG document string.
232
+ * Write to disk via `fs.writeFileSync(path, output, 'utf8')` (Node) or
233
+ * `Gio.File.replace_contents(new TextEncoder().encode(output), ...)` (GJS).
234
+ * @return {string}
235
+ */
236
+ getOutput() {
237
+ // Count unclosed <g> groups in #elements so getOutput() can close them.
238
+ let openGroups = 0;
239
+ for (const el of this.#elements) {
240
+ if (/^<g[\s>]/.test(el)) openGroups++;
241
+ else if (el === '</g>') openGroups--;
242
+ }
243
+
244
+ const parts = [
245
+ '<?xml version="1.0" encoding="UTF-8"?>',
246
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${this.#pageWidth}" height="${this.#pageHeight}" viewBox="0 0 ${this.#pageWidth} ${this.#pageHeight}">`,
247
+ ];
248
+
249
+ // Background rect emitted before Y-flip group (already in SVG Y-down coords)
250
+ const bg = this.getBackgroundColour();
251
+ if (bg) {
252
+ const { r, g, b } = bg;
253
+ parts.push(`<rect x="0" y="0" width="${this.#pageWidth}" height="${this.#pageHeight}" fill="rgb(${r},${g},${b})" stroke="none"/>`);
254
+ }
255
+
256
+ // Y-flip group: converts Y-up scene coordinates to SVG Y-down
257
+ parts.push(`<g transform="translate(0,${this.#pageHeight}) scale(1,-1)">`);
258
+ parts.push(...this.#elements);
259
+
260
+ // Close any groups that were opened during drawing but not explicitly closed
261
+ for (let i = 0; i < openGroups; i++) parts.push('</g>');
262
+
263
+ parts.push('</g>'); // close Y-flip group
264
+ parts.push('</svg>');
265
+
266
+ return parts.join('\n');
267
+ }
268
+
269
+ // --- Private helpers ---
270
+
271
+ /**
272
+ * Build an SVG path `d` string from bulge-encoded points.
273
+ * Straight segments use L; arc segments use the native A command.
274
+ * @param {Array} points
275
+ * @param {boolean} [closed=false]
276
+ * @return {string}
277
+ */
278
+ #buildPathD(points, closed = false) {
279
+ if (!points.length) return '';
280
+ let d = `M ${this.#fmt(points[0].x)} ${this.#fmt(points[0].y)}`;
281
+ for (let i = 0; i < points.length - 1; i++) {
282
+ const p = points[i];
283
+ const next = points[i + 1];
284
+ if (!p.bulge) {
285
+ d += ` L ${this.#fmt(next.x)} ${this.#fmt(next.y)}`;
286
+ } else {
287
+ const radius = p.bulgeRadius(next);
288
+ const largeArc = Math.abs(p.bulge) > 1 ? 1 : 0;
289
+ // Inside the Y-up local coordinate system (Y-flip group):
290
+ // sweep=1 (positive-angle direction = CCW in Y-up) corresponds to bulge > 0.
291
+ const sweep = p.bulge > 0 ? 1 : 0;
292
+ d += ` A ${this.#fmt(radius)} ${this.#fmt(radius)} 0 ${largeArc} ${sweep} ${this.#fmt(next.x)} ${this.#fmt(next.y)}`;
293
+ }
294
+ }
295
+ if (closed) d += ' Z';
296
+ return d;
297
+ }
298
+
299
+ /** @return {string} Current colour as an SVG rgb() string. */
300
+ #colourStr() {
301
+ const { r, g, b } = this.#currentColour;
302
+ return `rgb(${r},${g},${b})`;
303
+ }
304
+
305
+ /** @return {string} Current dash state as SVG stroke-dasharray/offset attributes, or ''. */
306
+ #dashAttrs() {
307
+ if (!this.#currentDash.length) return '';
308
+ const dashStr = this.#currentDash.map((v) => this.#fmt(v)).join(',');
309
+ return ` stroke-dasharray="${dashStr}" stroke-dashoffset="${this.#fmt(this.#currentDashOffset)}"`;
310
+ }
311
+
312
+ /**
313
+ * Format a number for SVG: integers emit as-is; floats use up to 4 decimal
314
+ * places with trailing zeros stripped.
315
+ * @param {number} n
316
+ * @return {string}
317
+ */
318
+ #fmt(n) {
319
+ if (Number.isInteger(n)) return String(n);
320
+ return n.toFixed(4).replace(/\.?0+$/, '');
321
+ }
322
+
323
+ /**
324
+ * Escape a string for safe use as SVG text content.
325
+ * @param {string|number} value
326
+ * @return {string}
327
+ */
328
+ #escapeXml(value) {
329
+ return String(value)
330
+ .replace(/&/g, '&amp;')
331
+ .replace(/</g, '&lt;')
332
+ .replace(/>/g, '&gt;')
333
+ .replace(/"/g, '&quot;');
334
+ }
335
+
336
+ /**
337
+ * Append a raw string to the element list.
338
+ * @param {string} str
339
+ */
340
+ #emit(str) {
341
+ this.#elements.push(str);
342
+ }
343
+ }
package/core/lib/scene.js CHANGED
@@ -8,7 +8,7 @@ import { Headers } from './headers.js';
8
8
 
9
9
  import { DesignCore } from '../designCore.js';
10
10
  import { BlockManager } from '../tables/blockManager.js';
11
- import { AddState, StateManager, UpdateState } from './stateManager.js';
11
+ import { AddState, StateManager, UpdateState, ReplaceState } from './stateManager.js';
12
12
 
13
13
  /**
14
14
  * Scene Class
@@ -25,7 +25,8 @@ export class Scene {
25
25
  this.blockManager = new BlockManager();
26
26
 
27
27
  this.entities = new EntityManager();
28
- this.tempEntities = new EntityManager(false);
28
+ this.previewEntities = new EntityManager(false);
29
+ this.hoverEntities = new EntityManager(false);
29
30
  this.auxiliaryEntities = new EntityManager(false);
30
31
 
31
32
  this.stateManager = new StateManager();
@@ -44,13 +45,15 @@ export class Scene {
44
45
  /** Clear the scene of all items */
45
46
  clear() {
46
47
  this.entities.clear();
47
- this.tempEntities.clear();
48
+ this.previewEntities.clear();
49
+ this.hoverEntities.clear();
48
50
  this.auxiliaryEntities.clear();
49
51
  }
50
52
 
51
53
  /** Reset the scene */
52
54
  reset() {
53
- this.tempEntities.clear();
55
+ this.previewEntities.clear();
56
+ this.hoverEntities.clear();
54
57
  this.auxiliaryEntities.clear();
55
58
  this.selectionManager.reset();
56
59
  DesignCore.Canvas.requestPaint();
@@ -84,7 +87,7 @@ export class Scene {
84
87
  this.reset();
85
88
  return;
86
89
  }
87
- // Create a new item, send it the points array
90
+ // Create a new item from plain data
88
91
  const item = DesignCore.CommandManager.createNew(type, data);
89
92
 
90
93
  if (typeof index === 'undefined') {
@@ -103,6 +106,25 @@ export class Scene {
103
106
  return index;
104
107
  }
105
108
 
109
+ /**
110
+ * Add an already-constructed entity to the scene.
111
+ * Used by the input manager where the entity is built interactively;
112
+ * skips createNew so EntityProperties values are preserved.
113
+ * @param {Entity} entity - constructed entity instance
114
+ * @param {number} index - if provided, replaces the entity at that index
115
+ * @return {number} index of the added or replaced entity
116
+ */
117
+ commitEntity(entity, index = undefined) {
118
+ if (typeof index === 'undefined') {
119
+ this.commit([new AddState(entity)]);
120
+ return this.entities.count() - 1;
121
+ } else {
122
+ const existingItem = this.entities.get(index);
123
+ this.commit([new ReplaceState(existingItem, entity)]);
124
+ return index;
125
+ }
126
+ }
127
+
106
128
  /**
107
129
  * Commit state changes to the scene
108
130
  * @param {Array} stateChanges
@@ -1,6 +1,6 @@
1
1
  import { Utils } from './utils.js';
2
2
  import { Point } from '../entities/point.js';
3
- import { SelectionWindow } from './selectionWindow.js';
3
+ import { SelectionWindow } from './auxiliary/selectionWindow.js';
4
4
  import { DesignCore } from '../designCore.js';
5
5
 
6
6
  /** SingleSelection Class */
@@ -215,7 +215,7 @@ export class SelectionManager {
215
215
 
216
216
  // Add all selectable items to the selection
217
217
  for (let i = 0; i < DesignCore.Scene.entities.count(); i++) {
218
- const layer = DesignCore.LayerManager.getItemByName(DesignCore.Scene.entities.get(i).layer);
218
+ const layer = DesignCore.LayerManager.getItemByName(DesignCore.Scene.entities.get(i).getProperty('layer'));
219
219
 
220
220
  // Only select items on selectable layers
221
221
  if (layer?.isSelectable) {
@@ -6,8 +6,10 @@ export class Settings {
6
6
  constructor() {
7
7
  this.canvasbackgroundcolour = { r: 30, g: 30, b: 30 };
8
8
  this.gridcolour = { r: 119, g: 118, b: 123 };
9
- this.selecteditemscolour = { r: 0, g: 255, b: 0 };
10
- this.snaptrackingcolour = { r: 55, g: 180, b: 75 };
9
+ this.accentcolour = { r: 55, g: 180, b: 75 };
10
+ this.rubberbandcolour = { r: 225, g: 180, b: 30 };
11
+ // previewSettings
12
+ this.previewBlendFactor = 0.8; // how much to blend preview entities with the background (0-1)
11
13
  // selectionWindow and crossingWindow colours
12
14
  this.selectionWindow = { r: 0, g: 255, b: 0 };
13
15
  this.crossingWindow = { r: 0, g: 0, b: 255 };
@@ -1,198 +1,11 @@
1
1
 
2
2
  import { Point } from '../entities/point.js';
3
- import { Colours } from './colours.js';
4
3
  import { Utils } from './utils.js';
5
4
 
5
+ import { SnapPoint } from './auxiliary/snapPoint.js';
6
+ import { TrackingLine } from './auxiliary/trackingLine.js';
6
7
  import { DesignCore } from '../designCore.js';
7
-
8
- /** SnapPoint Class */
9
- export class SnapPoint {
10
- static Type = Object.freeze({
11
- NONE: 'none',
12
- END: 'end',
13
- MID: 'mid',
14
- CENTRE: 'centre',
15
- QUADRANT: 'quadrant',
16
- NEAREST: 'nearest',
17
- TANGENT: 'tangent',
18
- NODE: 'node',
19
- PERPENDICULAR: 'perpendicular',
20
- });
21
-
22
- /**
23
- * Create SnapPoint
24
- * @param {Point} snapPoint
25
- * @param {string} type - snap type from SnapPoint.Type
26
- */
27
- constructor(snapPoint, type = SnapPoint.Type.END) {
28
- this.snapPoint = snapPoint;
29
- this.type = type;
30
- }
31
-
32
- /**
33
- * Draw the entity
34
- * @param {Object} ctx - context
35
- * @param {number} scale
36
- */
37
- draw(ctx, scale) {
38
- const snapColour = DesignCore.Settings.snaptrackingcolour;
39
- const size = 6 / scale;
40
- const x = this.snapPoint.x;
41
- const y = this.snapPoint.y;
42
- const lineWidth = 2.5 / scale;
43
-
44
- try { // HTML Canvas
45
- ctx.strokeStyle = Colours.rgbToString(snapColour);
46
- ctx.lineWidth = lineWidth;
47
- ctx.setLineDash([]);
48
- ctx.beginPath();
49
- } catch { // Cairo
50
- const rgbColour = Colours.rgbToScaledRGB(snapColour);
51
- ctx.setSourceRGB(rgbColour.r, rgbColour.g, rgbColour.b);
52
- ctx.setLineWidth(lineWidth);
53
- }
54
-
55
- switch (this.type) {
56
- case SnapPoint.Type.END: // square
57
- ctx.moveTo(x - size, y - size);
58
- ctx.lineTo(x + size, y - size);
59
- ctx.lineTo(x + size, y + size);
60
- ctx.lineTo(x - size, y + size);
61
- ctx.closePath();
62
- break;
63
- case SnapPoint.Type.MID: // triangle
64
- ctx.moveTo(x, y + size);
65
- ctx.lineTo(x - size, y - size);
66
- ctx.lineTo(x + size, y - size);
67
- ctx.closePath();
68
- break;
69
- case SnapPoint.Type.QUADRANT: // diamond
70
- ctx.moveTo(x, y - size);
71
- ctx.lineTo(x + size, y);
72
- ctx.lineTo(x, y + size);
73
- ctx.lineTo(x - size, y);
74
- ctx.closePath();
75
- break;
76
- case SnapPoint.Type.NEAREST: // hourglass (X with horizontal lines at top and bottom)
77
- ctx.moveTo(x - size, y - size);
78
- ctx.lineTo(x + size, y - size);
79
- ctx.lineTo(x - size, y + size);
80
- ctx.lineTo(x + size, y + size);
81
- ctx.closePath();
82
- break;
83
- case SnapPoint.Type.TANGENT: // circle with a horizontal line over the top
84
- ctx.arc(x, y, size, 0, 6.283);
85
- ctx.moveTo(x - size, y + size * 1.5);
86
- ctx.lineTo(x + size, y + size * 1.5);
87
- break;
88
- case SnapPoint.Type.NODE: // circle with an X inside
89
- ctx.arc(x, y, size, 0, 6.283);
90
- ctx.moveTo(x - size * 0.7, y - size * 0.7);
91
- ctx.lineTo(x + size * 0.7, y + size * 0.7);
92
- ctx.moveTo(x + size * 0.7, y - size * 0.7);
93
- ctx.lineTo(x - size * 0.7, y + size * 0.7);
94
- break;
95
- case SnapPoint.Type.PERPENDICULAR: // right-angle corner (L-shape)
96
- ctx.moveTo(x - size, y + size);
97
- ctx.lineTo(x - size, y - size);
98
- ctx.lineTo(x + size, y - size);
99
- break;
100
- default: // CENTRE - circle
101
- ctx.arc(x, y, size, 0, 6.283);
102
- break;
103
- }
104
-
105
- ctx.stroke();
106
- }
107
- }
108
-
109
- /** TrackingLine Class - dotted tracking line for polar/ortho snap */
110
- class TrackingLine {
111
- /**
112
- * Create TrackingLine
113
- * @param {Point} inputPoint - the previous input point (line passes through this)
114
- * @param {Point} snapPoint - the current snapped mouse point (direction reference)
115
- */
116
- constructor(inputPoint, snapPoint) {
117
- this.inputPoint = inputPoint;
118
- this.snapPoint = snapPoint;
119
- }
120
-
121
- /**
122
- * Draw the tracking line, extended to the visible canvas edges
123
- * @param {Object} ctx - context
124
- * @param {number} scale
125
- */
126
- draw(ctx, scale) {
127
- const from = this.inputPoint;
128
- const dir = this.snapPoint.subtract(from);
129
- const lineWidth = 2 / scale;
130
-
131
- if (dir.x === 0 && dir.y === 0) {
132
- return;
133
- }
134
-
135
- // Compute visible canvas bounds in scene space
136
- const w = DesignCore.Canvas.width;
137
- const h = DesignCore.Canvas.height;
138
- const corner0 = DesignCore.Mouse.transformToScene(new Point(0, 0));
139
- const corner1 = DesignCore.Mouse.transformToScene(new Point(w, h));
140
- const boundsMin = corner0.min(corner1);
141
- const boundsMax = corner0.max(corner1);
142
-
143
- // Clip the infinite line (through 'from' in direction dir) to canvas bounds
144
- let tMin = -Infinity;
145
- let tMax = Infinity;
146
-
147
- if (dir.x !== 0) {
148
- const tx1 = (boundsMin.x - from.x) / dir.x;
149
- const tx2 = (boundsMax.x - from.x) / dir.x;
150
- tMin = Math.max(tMin, Math.min(tx1, tx2));
151
- tMax = Math.min(tMax, Math.max(tx1, tx2));
152
- } else if (from.x < boundsMin.x || from.x > boundsMax.x) {
153
- return;
154
- }
155
-
156
- if (dir.y !== 0) {
157
- const ty1 = (boundsMin.y - from.y) / dir.y;
158
- const ty2 = (boundsMax.y - from.y) / dir.y;
159
- tMin = Math.max(tMin, Math.min(ty1, ty2));
160
- tMax = Math.min(tMax, Math.max(ty1, ty2));
161
- } else if (from.y < boundsMin.y || from.y > boundsMax.y) {
162
- return;
163
- }
164
-
165
- if (tMax < tMin) {
166
- return;
167
- }
168
-
169
- const start = from.add(dir.scale(tMin));
170
- const end = from.add(dir.scale(tMax));
171
-
172
- const lineColour = DesignCore.Settings.snaptrackingcolour;
173
- const dashSize = 4 / scale;
174
-
175
- ctx.save();
176
-
177
- try { // HTML Canvas
178
- ctx.beginPath();
179
- ctx.strokeStyle = Colours.rgbToString(lineColour);
180
- ctx.lineWidth = lineWidth;
181
- ctx.setLineDash([dashSize, dashSize]);
182
- } catch { // Cairo
183
- const rgbColour = Colours.rgbToScaledRGB(lineColour);
184
- ctx.setSourceRGB(rgbColour.r, rgbColour.g, rgbColour.b);
185
- ctx.setLineWidth(lineWidth);
186
- ctx.setDash([dashSize, dashSize], 0);
187
- }
188
-
189
- ctx.moveTo(start.x, start.y);
190
- ctx.lineTo(end.x, end.y);
191
- ctx.stroke();
192
-
193
- ctx.restore();
194
- }
195
- }
8
+ import { Property } from '../properties/property.js';
196
9
 
197
10
  /** Snapping Class */
198
11
  export class Snapping {
@@ -271,7 +84,7 @@ export class Snapping {
271
84
  let delta = snapAperture / DesignCore.Canvas.getScale(); // convert to scene space
272
85
 
273
86
  for (let i = 0; i < DesignCore.Scene.entities.count(); i++) {
274
- const layer = DesignCore.LayerManager.getItemByName(DesignCore.Scene.entities.get(i).layer);
87
+ const layer = DesignCore.LayerManager.getItemByName(DesignCore.Scene.entities.get(i).getProperty(Property.Names.LAYER));
275
88
 
276
89
  if (!layer?.isVisible) {
277
90
  continue;