@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.
- package/.eslintrc.json +4 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/dimensions/alignedDimension.js +5 -4
- package/core/dimensions/angularDimension.js +6 -5
- package/core/dimensions/baseCircularDimension.js +3 -2
- package/core/dimensions/baseDimension.js +59 -93
- package/core/dimensions/baseLinearDimension.js +6 -5
- package/core/dimensions/diametricDimension.js +4 -3
- package/core/dimensions/dimension.js +21 -14
- package/core/dimensions/radialDimension.js +4 -3
- package/core/dimensions/rotatedDimension.js +12 -11
- package/core/entities/arc.js +112 -145
- package/core/entities/arctext.js +212 -174
- package/core/entities/basePolyline.js +30 -282
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +322 -249
- package/core/entities/insert.js +66 -81
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/polyline.js +4 -4
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +188 -199
- 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/canvas.js +228 -166
- package/core/lib/clipboard.js +7 -1
- 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 +4 -3
- package/core/lib/entityManager.js +18 -11
- package/core/lib/inputManager.js +35 -11
- package/core/lib/intersect.js +25 -0
- package/core/lib/patterns.js +8 -1
- 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 +27 -5
- package/core/lib/selectionManager.js +2 -2
- 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 +21 -2
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +29 -13
- package/core/tables/block.js +4 -37
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tools/chamfer.js +111 -137
- package/core/tools/chamferFilletBase.js +135 -34
- package/core/tools/copy.js +3 -3
- package/core/tools/explode.js +2 -2
- package/core/tools/extend.js +132 -19
- package/core/tools/fillet.js +86 -183
- package/core/tools/matchProp.js +2 -11
- package/core/tools/mirror.js +7 -6
- package/core/tools/move.js +3 -3
- package/core/tools/offset.js +3 -5
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/rotate.js +6 -3
- package/core/tools/scale.js +1 -1
- package/core/tools/trim.js +265 -23
- package/core/tools/zoom.js +1 -1
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +14 -7
- package/test/dimensions/angularDimension.test.js +6 -6
- package/test/dimensions/baseDimension.test.js +69 -8
- package/test/dimensions/baseLinearDimension.test.js +62 -0
- package/test/dimensions/diametricDimension.test.js +6 -6
- package/test/dimensions/radialDimension.test.js +6 -6
- package/test/dimensions/rotatedDimension.test.js +14 -7
- package/test/entities/arc.test.js +80 -83
- package/test/entities/arctext.test.js +180 -53
- package/test/entities/basePolyline.test.js +68 -2
- package/test/entities/circle.test.js +57 -85
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +543 -70
- package/test/entities/insert.test.js +96 -1
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +294 -69
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +2 -2
- 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/canvas.test.js +742 -123
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +80 -19
- 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/settings.test.js +5 -6
- package/test/lib/snapping.test.js +71 -129
- 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 +3 -3
- 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 +31 -1
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +152 -0
- package/test/tools/chamfer.test.js +227 -5
- package/test/tools/chamferFilletBase.test.js +98 -9
- package/test/tools/copy.test.js +2 -2
- package/test/tools/explode.test.js +3 -3
- package/test/tools/extend.test.js +331 -0
- package/test/tools/fillet.test.js +181 -0
- package/test/tools/matchProp.test.js +5 -5
- package/test/tools/mirror.test.js +13 -13
- package/test/tools/offset.test.js +170 -3
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/rotate.test.js +32 -2
- package/test/tools/scale.test.js +5 -5
- package/test/tools/trim.test.js +526 -0
- package/test/tools/zoom.test.js +7 -7
- 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();
|
|
@@ -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 */
|
|
@@ -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 };
|
package/core/lib/snapping.js
CHANGED
|
@@ -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).
|
|
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;
|