@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
@@ -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();
@@ -72,7 +75,7 @@ export class Scene {
72
75
  * @param {number} index - integer of item to replace
73
76
  * @return {number} - index of created item
74
77
  */
75
- addItem(type, data, index) {
78
+ addEntity(type, data, index) {
76
79
  // TODO: validate data is valid for type
77
80
  if (!data) {
78
81
  throw Error('Input data missing');
@@ -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 */
@@ -13,7 +13,7 @@ export class SingleSelection {
13
13
  * @param {Point} point
14
14
  */
15
15
  constructor(index, point) {
16
- this.selectedItemIndex = index;
16
+ this.selectedEntityIndex = index;
17
17
  this.selectedPoint = point;
18
18
  }
19
19
  }
@@ -41,24 +41,24 @@ export class SelectionSet {
41
41
  export class SelectionManager {
42
42
  /** Create a SelectionManager */
43
43
  constructor() {
44
- this.selectionSet = new SelectionSet(); // store a list of selected items indices
45
- this.selectedItems = []; // store a copy of selected items
44
+ this.selectionSet = new SelectionSet(); // store a list of selected entities indices
45
+ this.selectedEntities = []; // store a copy of selected entities
46
46
  }
47
47
 
48
48
  /**
49
49
  * Reset the selection state
50
50
  */
51
51
  reset() {
52
- this.selectedItems = [];
52
+ this.selectedEntities = [];
53
53
  this.selectionSet = new SelectionSet();
54
54
  this.selectionSetChanged();
55
55
  }
56
56
 
57
57
  /**
58
- * Remove the last selection from the selected items
58
+ * Remove the last selection from the selected entities
59
59
  */
60
60
  removeLastSelection() {
61
- this.selectedItems.pop();
61
+ this.selectedEntities.pop();
62
62
  this.selectionSet.removeLastSelection();
63
63
  this.selectionSetChanged();
64
64
  }
@@ -157,19 +157,19 @@ export class SelectionManager {
157
157
  }
158
158
 
159
159
  /**
160
- * Remove the item at index from the selectionSet and selectedItems
160
+ * Remove the item at index from the selectionSet and selectedEntities
161
161
  * @param {number} index - index of the item
162
162
  */
163
163
  removeFromSelectionSet(index) {
164
164
  const itemIndex = this.selectionSet.selectionSet.indexOf(index);
165
165
  if (itemIndex !== -1) {
166
166
  this.selectionSet.selectionSet.splice(itemIndex, 1);
167
- this.selectedItems.splice(itemIndex, 1);
167
+ this.selectedEntities.splice(itemIndex, 1);
168
168
  }
169
169
  }
170
170
 
171
171
  /**
172
- * Add the item at index to the selectionSet and selectedItems
172
+ * Add the item at index to the selectionSet and selectedEntities
173
173
  * @param {number} index
174
174
  */
175
175
  addToSelectionSet(index) {
@@ -179,28 +179,28 @@ export class SelectionManager {
179
179
  // only store selections once
180
180
  if (this.selectionSet.selectionSet.indexOf(index) === -1) {
181
181
  this.selectionSet.selectionSet.push(index);
182
- this.addToSelectedItems(index);
182
+ this.addToSelectedEntities(index);
183
183
  }
184
184
  }
185
185
 
186
186
  /**
187
- * Duplicate the item at index and add to selectedItems
187
+ * Duplicate the item at index and add to selectedEntities
188
188
  * @param {number} index
189
189
  */
190
- addToSelectedItems(index) {
191
- const copyofitem = Utils.cloneObject(DesignCore.Scene.entities.get(index));
192
- this.selectedItems.push(copyofitem);
190
+ addToSelectedEntities(index) {
191
+ const copyOfItem = Utils.cloneObject(DesignCore.Scene.entities.get(index));
192
+ this.selectedEntities.push(copyOfItem);
193
193
  }
194
194
 
195
195
  /**
196
- * Reload the selectedItems
196
+ * Reload the selectedEntities
197
197
  * This is required following changes to selected item properties
198
198
  */
199
- reloadSelectedItems() {
200
- this.selectedItems = [];
199
+ reloadSelectedEntities() {
200
+ this.selectedEntities = [];
201
201
 
202
202
  for (let i = 0; i < this.selectionSet.selectionSet.length; i++) {
203
- this.addToSelectedItems(this.selectionSet.selectionSet[i]);
203
+ this.addToSelectedEntities(this.selectionSet.selectionSet[i]);
204
204
  }
205
205
 
206
206
  DesignCore.Core.canvas.requestPaint();
@@ -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 };