@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.
- package/.eslintrc.json +4 -0
- package/.github/workflows/DesignCoreQA.yml +3 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/designCore.js +1 -1
- package/core/dimensions/alignedDimension.js +12 -11
- package/core/dimensions/angularDimension.js +38 -37
- package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
- package/core/dimensions/diametricDimension.js +14 -13
- package/core/dimensions/dimension.js +62 -55
- package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
- package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
- package/core/dimensions/radialDimension.js +12 -11
- package/core/dimensions/rotatedDimension.js +20 -19
- package/core/entities/arc.js +123 -156
- package/core/entities/arctext.js +222 -184
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +346 -273
- package/core/entities/insert.js +68 -83
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/point.js +19 -19
- package/core/entities/polyline.js +7 -7
- package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +190 -201
- package/core/lib/auxiliary/rubberBand.js +32 -0
- package/core/lib/auxiliary/selectionWindow.js +63 -0
- package/core/lib/auxiliary/snapPoint.js +113 -0
- package/core/lib/auxiliary/trackingLine.js +76 -0
- package/core/lib/boundingBox.js +11 -11
- package/core/lib/canvas.js +232 -168
- package/core/lib/clipboard.js +9 -3
- package/core/lib/colour.js +16 -0
- package/core/lib/commandLine.js +1 -6
- package/core/lib/commandManager.js +18 -8
- package/core/lib/constants.js +28 -0
- package/core/lib/dxf/dxf.js +15 -14
- package/core/lib/dxf/dxfIterator.js +2 -2
- package/core/lib/entityManager.js +33 -26
- package/core/lib/inputManager.js +36 -12
- package/core/lib/intersect.js +25 -0
- package/core/lib/matrix.js +1 -1
- package/core/lib/patterns.js +9 -2
- package/core/lib/plotOptions.js +67 -0
- package/core/lib/polylineUtils.js +75 -0
- package/core/lib/renderers/cairoRenderer.js +371 -0
- package/core/lib/renderers/canvasRenderer.js +315 -0
- package/core/lib/renderers/pdfRenderer.js +366 -0
- package/core/lib/renderers/rendererBase.js +284 -0
- package/core/lib/renderers/svgRenderer.js +343 -0
- package/core/lib/scene.js +28 -6
- package/core/lib/selectionManager.js +20 -20
- package/core/lib/settings.js +4 -2
- package/core/lib/snapping.js +4 -191
- package/core/lib/stateManager.js +30 -1
- package/core/lib/utils.js +26 -7
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +60 -44
- package/core/tables/block.js +50 -81
- package/core/tables/dimStyle.js +5 -5
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/ltypeManager.js +1 -1
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tables/vport.js +2 -2
- package/core/tools/chamfer.js +117 -143
- package/core/tools/chamferFilletBase.js +138 -37
- package/core/tools/copy.js +11 -11
- package/core/tools/copybase.js +3 -3
- package/core/tools/copyclip.js +3 -3
- package/core/tools/cornerEntity.js +2 -2
- package/core/tools/cutclip.js +3 -3
- package/core/tools/explode.js +7 -7
- package/core/tools/extend.js +138 -25
- package/core/tools/fillet.js +92 -189
- package/core/tools/matchProp.js +3 -12
- package/core/tools/mirror.js +15 -14
- package/core/tools/move.js +8 -8
- package/core/tools/offset.js +31 -33
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/purge.js +10 -10
- package/core/tools/rotate.js +19 -16
- package/core/tools/scale.js +7 -7
- package/core/tools/trim.js +271 -29
- package/core/tools/zoom.js +1 -1
- package/cspell.json +321 -0
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +17 -10
- package/test/dimensions/angularDimension.test.js +13 -13
- package/test/dimensions/diametricDimension.test.js +15 -15
- package/test/dimensions/dimension.test.js +12 -12
- package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
- package/test/dimensions/linearDimensionBase.test.js +62 -0
- package/test/dimensions/radialDimension.test.js +15 -15
- package/test/dimensions/rotatedDimension.test.js +18 -11
- package/test/entities/arc.test.js +83 -86
- package/test/entities/arctext.test.js +184 -57
- package/test/entities/circle.test.js +68 -96
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +569 -96
- package/test/entities/insert.test.js +99 -4
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/point.test.js +3 -3
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +302 -77
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +5 -5
- package/test/integration/intersectionIntegration.test.js +4 -5
- package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
- package/test/integration/pdfRendererIntegration.test.js +61 -0
- package/test/integration/svgRendererIntegration.reference.svg +3343 -0
- package/test/integration/svgRendererIntegration.test.js +61 -0
- package/test/integration/userInputIntegration.reference.dxf +1165 -105
- package/test/integration/userInputIntegration.test.js +239 -132
- package/test/lib/boundingBox.test.js +3 -3
- package/test/lib/canvas.test.js +744 -125
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +81 -20
- package/test/lib/intersect.test.js +83 -0
- package/test/lib/plotOptions.test.js +179 -0
- package/test/lib/rubberBand.test.js +84 -0
- package/test/lib/scene.test.js +9 -9
- package/test/lib/selectionManager.test.js +3 -3
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +72 -130
- package/test/lib/stateManager.test.js +7 -7
- package/test/lib/utils.test.js +36 -2
- package/test/properties/entityProperties.test.js +132 -0
- package/test/properties/property.test.js +53 -0
- package/test/properties/propertyManager.test.js +25 -25
- package/test/renderers/canvasRenderer.test.js +317 -0
- package/test/renderers/pdfRenderer.test.js +271 -0
- package/test/renderers/rendererBase.test.js +213 -0
- package/test/renderers/svgRenderer.test.js +326 -0
- package/test/tables/block.test.js +92 -13
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/tables/layerManager.test.js +2 -2
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +158 -6
- package/test/tools/chamfer.test.js +273 -51
- package/test/tools/chamferFilletBase.test.js +125 -36
- package/test/tools/copy.test.js +8 -8
- package/test/tools/copybase.test.js +2 -2
- package/test/tools/copyclip.test.js +2 -2
- package/test/tools/cornerEntity.test.js +8 -8
- package/test/tools/cutclip.test.js +2 -2
- package/test/tools/erase.test.js +6 -6
- package/test/tools/explode.test.js +5 -5
- package/test/tools/extend.test.js +373 -42
- package/test/tools/fillet.test.js +230 -49
- package/test/tools/matchProp.test.js +11 -11
- package/test/tools/mirror.test.js +39 -39
- package/test/tools/move.test.js +7 -7
- package/test/tools/offset.test.js +222 -55
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/purge.test.js +4 -4
- package/test/tools/rotate.test.js +39 -9
- package/test/tools/scale.test.js +33 -33
- package/test/tools/trim.test.js +579 -53
- package/test/tools/zoom.test.js +12 -12
- 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, '&')
|
|
331
|
+
.replace(/</g, '<')
|
|
332
|
+
.replace(/>/g, '>')
|
|
333
|
+
.replace(/"/g, '"');
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
45
|
-
this.
|
|
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.
|
|
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
|
|
58
|
+
* Remove the last selection from the selected entities
|
|
59
59
|
*/
|
|
60
60
|
removeLastSelection() {
|
|
61
|
-
this.
|
|
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
|
|
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.
|
|
167
|
+
this.selectedEntities.splice(itemIndex, 1);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
|
-
* Add the item at index to the selectionSet and
|
|
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.
|
|
182
|
+
this.addToSelectedEntities(index);
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
|
-
* Duplicate the item at index and add to
|
|
187
|
+
* Duplicate the item at index and add to selectedEntities
|
|
188
188
|
* @param {number} index
|
|
189
189
|
*/
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
this.
|
|
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
|
|
196
|
+
* Reload the selectedEntities
|
|
197
197
|
* This is required following changes to selected item properties
|
|
198
198
|
*/
|
|
199
|
-
|
|
200
|
-
this.
|
|
199
|
+
reloadSelectedEntities() {
|
|
200
|
+
this.selectedEntities = [];
|
|
201
201
|
|
|
202
202
|
for (let i = 0; i < this.selectionSet.selectionSet.length; i++) {
|
|
203
|
-
this.
|
|
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) {
|
package/core/lib/settings.js
CHANGED
|
@@ -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.
|
|
10
|
-
this.
|
|
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 };
|