@design-core/core 0.0.5 → 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 +33 -284
- 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 +36 -13
- 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 +129 -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 +296 -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,213 @@
|
|
|
1
|
+
import { RendererBase } from '../../core/lib/renderers/rendererBase.js';
|
|
2
|
+
|
|
3
|
+
/** Concrete subclass used throughout — only implements what the tests need. */
|
|
4
|
+
class TestRenderer extends RendererBase {}
|
|
5
|
+
|
|
6
|
+
// ─── RendererBase.Styles ──────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe('RendererBase.Styles', () => {
|
|
9
|
+
// Use lowercase aliases to satisfy the new-cap lint rule
|
|
10
|
+
const applyNone = RendererBase.Styles.NONE;
|
|
11
|
+
const applyMono = RendererBase.Styles.MONOCHROME;
|
|
12
|
+
const applyGrey = RendererBase.Styles.GREYSCALE;
|
|
13
|
+
|
|
14
|
+
test('NONE returns colour unchanged', () => {
|
|
15
|
+
expect(applyNone({ r: 100, g: 150, b: 200 })).toEqual({ r: 100, g: 150, b: 200 });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('MONOCHROME returns black for any colour', () => {
|
|
19
|
+
expect(applyMono({ r: 255, g: 128, b: 0 })).toEqual({ r: 0, g: 0, b: 0 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('MONOCHROME returns black even for white', () => {
|
|
23
|
+
expect(applyMono({ r: 255, g: 255, b: 255 })).toEqual({ r: 0, g: 0, b: 0 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('GREYSCALE produces equal r/g/b channels', () => {
|
|
27
|
+
const result = applyGrey({ r: 255, g: 0, b: 0 });
|
|
28
|
+
expect(result.r).toBe(result.g);
|
|
29
|
+
expect(result.g).toBe(result.b);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('GREYSCALE pure red uses BT.601 red coefficient (~76)', () => {
|
|
33
|
+
// 0.299 * 255 = 76.245 → rounds to 76
|
|
34
|
+
const result = applyGrey({ r: 255, g: 0, b: 0 });
|
|
35
|
+
expect(result.r).toBe(76);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('GREYSCALE pure green uses BT.601 green coefficient (~150)', () => {
|
|
39
|
+
// 0.587 * 255 = 149.685 → rounds to 150
|
|
40
|
+
const result = applyGrey({ r: 0, g: 255, b: 0 });
|
|
41
|
+
expect(result.r).toBe(150);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('GREYSCALE pure blue uses BT.601 blue coefficient (~29)', () => {
|
|
45
|
+
// 0.114 * 255 = 29.07 → rounds to 29
|
|
46
|
+
const result = applyGrey({ r: 0, g: 0, b: 255 });
|
|
47
|
+
expect(result.r).toBe(29);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('GREYSCALE white maps to white (255, 255, 255)', () => {
|
|
51
|
+
expect(applyGrey({ r: 255, g: 255, b: 255 })).toEqual({ r: 255, g: 255, b: 255 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('GREYSCALE black maps to black (0, 0, 0)', () => {
|
|
55
|
+
expect(applyGrey({ r: 0, g: 0, b: 0 })).toEqual({ r: 0, g: 0, b: 0 });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ─── setBackgroundColour / getBackgroundColour ────────────────────────────────
|
|
60
|
+
|
|
61
|
+
test('getBackgroundColour returns null before any set', () => {
|
|
62
|
+
const r = new TestRenderer();
|
|
63
|
+
expect(r.getBackgroundColour()).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('setBackgroundColour stores and getBackgroundColour returns the colour', () => {
|
|
67
|
+
const r = new TestRenderer();
|
|
68
|
+
const colour = { r: 255, g: 255, b: 255 };
|
|
69
|
+
r.setBackgroundColour(colour);
|
|
70
|
+
expect(r.getBackgroundColour()).toBe(colour);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('setBackgroundColour can be updated', () => {
|
|
74
|
+
const r = new TestRenderer();
|
|
75
|
+
r.setBackgroundColour({ r: 0, g: 0, b: 0 });
|
|
76
|
+
const updated = { r: 255, g: 0, b: 0 };
|
|
77
|
+
r.setBackgroundColour(updated);
|
|
78
|
+
expect(r.getBackgroundColour()).toBe(updated);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('setBackgroundColour accepts null to clear', () => {
|
|
82
|
+
const r = new TestRenderer();
|
|
83
|
+
r.setBackgroundColour({ r: 0, g: 0, b: 0 });
|
|
84
|
+
r.setBackgroundColour(null);
|
|
85
|
+
expect(r.getBackgroundColour()).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ─── setStyle / applyStyle ────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
test('applyStyle defaults to NONE (colour passes through unchanged)', () => {
|
|
91
|
+
const r = new TestRenderer();
|
|
92
|
+
expect(r.applyStyle({ r: 10, g: 20, b: 30 })).toEqual({ r: 10, g: 20, b: 30 });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('setStyle to MONOCHROME causes applyStyle to return black', () => {
|
|
96
|
+
const r = new TestRenderer();
|
|
97
|
+
r.setStyle(RendererBase.Styles.MONOCHROME);
|
|
98
|
+
expect(r.applyStyle({ r: 100, g: 150, b: 200 })).toEqual({ r: 0, g: 0, b: 0 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('setStyle to GREYSCALE causes applyStyle to return grey', () => {
|
|
102
|
+
const r = new TestRenderer();
|
|
103
|
+
r.setStyle(RendererBase.Styles.GREYSCALE);
|
|
104
|
+
const result = r.applyStyle({ r: 255, g: 0, b: 0 });
|
|
105
|
+
expect(result.r).toBe(result.g);
|
|
106
|
+
expect(result.g).toBe(result.b);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('setStyle falls back to NONE when passed a falsy value', () => {
|
|
110
|
+
const r = new TestRenderer();
|
|
111
|
+
r.setStyle(RendererBase.Styles.MONOCHROME);
|
|
112
|
+
r.setStyle(null);
|
|
113
|
+
expect(r.applyStyle({ r: 10, g: 20, b: 30 })).toEqual({ r: 10, g: 20, b: 30 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('setStyle can be changed multiple times', () => {
|
|
117
|
+
const r = new TestRenderer();
|
|
118
|
+
r.setStyle(RendererBase.Styles.MONOCHROME);
|
|
119
|
+
r.setStyle(RendererBase.Styles.NONE);
|
|
120
|
+
expect(r.applyStyle({ r: 50, g: 100, b: 150 })).toEqual({ r: 50, g: 100, b: 150 });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── Abstract methods throw ───────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test('drawShape throws with renderer name in message', () => {
|
|
126
|
+
const r = new TestRenderer();
|
|
127
|
+
expect(() => r.drawShape([])).toThrow('TestRenderer: drawShape() not implemented');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('drawText throws with renderer name in message', () => {
|
|
131
|
+
const r = new TestRenderer();
|
|
132
|
+
expect(() => r.drawText([], 'Arial', 12)).toThrow('TestRenderer: drawText() not implemented');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('tracePath throws with renderer name in message', () => {
|
|
136
|
+
const r = new TestRenderer();
|
|
137
|
+
expect(() => r.tracePath([])).toThrow('TestRenderer: tracePath() not implemented');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('drawSegments throws with renderer name in message', () => {
|
|
141
|
+
const r = new TestRenderer();
|
|
142
|
+
expect(() => r.drawSegments([])).toThrow('TestRenderer: drawSegments() not implemented');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('beginPath throws with renderer name in message', () => {
|
|
146
|
+
const r = new TestRenderer();
|
|
147
|
+
expect(() => r.beginPath()).toThrow('TestRenderer: beginPath() not implemented');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('stroke throws with renderer name in message', () => {
|
|
151
|
+
const r = new TestRenderer();
|
|
152
|
+
expect(() => r.stroke()).toThrow('TestRenderer: stroke() not implemented');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('closePath throws with renderer name in message', () => {
|
|
156
|
+
const r = new TestRenderer();
|
|
157
|
+
expect(() => r.closePath()).toThrow('TestRenderer: closePath() not implemented');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('setColour throws with renderer name in message', () => {
|
|
161
|
+
const r = new TestRenderer();
|
|
162
|
+
expect(() => r.setColour({ r: 0, g: 0, b: 0 })).toThrow('TestRenderer: setColour() not implemented');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('setLineWidth throws with renderer name in message', () => {
|
|
166
|
+
const r = new TestRenderer();
|
|
167
|
+
expect(() => r.setLineWidth(1)).toThrow('TestRenderer: setLineWidth() not implemented');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('setDash throws with renderer name in message', () => {
|
|
171
|
+
const r = new TestRenderer();
|
|
172
|
+
expect(() => r.setDash([])).toThrow('TestRenderer: setDash() not implemented');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('setHighlight throws with renderer name in message', () => {
|
|
176
|
+
const r = new TestRenderer();
|
|
177
|
+
expect(() => r.setHighlight(true)).toThrow('TestRenderer: setHighlight() not implemented');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('setTransform throws with renderer name in message', () => {
|
|
181
|
+
const r = new TestRenderer();
|
|
182
|
+
expect(() => r.setTransform({})).toThrow('TestRenderer: setTransform() not implemented');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('fillBackground throws with renderer name in message', () => {
|
|
186
|
+
const r = new TestRenderer();
|
|
187
|
+
expect(() => r.fillBackground({ x: 0, y: 0 }, 800, 600, 1)).toThrow('TestRenderer: fillBackground() not implemented');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('applyTransform throws with renderer name in message', () => {
|
|
191
|
+
const r = new TestRenderer();
|
|
192
|
+
expect(() => r.applyTransform({})).toThrow('TestRenderer: applyTransform() not implemented');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('save throws with renderer name in message', () => {
|
|
196
|
+
const r = new TestRenderer();
|
|
197
|
+
expect(() => r.save()).toThrow('TestRenderer: save() not implemented');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('restore throws with renderer name in message', () => {
|
|
201
|
+
const r = new TestRenderer();
|
|
202
|
+
expect(() => r.restore()).toThrow('TestRenderer: restore() not implemented');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('measureText throws with renderer name in message', () => {
|
|
206
|
+
const r = new TestRenderer();
|
|
207
|
+
expect(() => r.measureText('hello')).toThrow('TestRenderer: measureText() not implemented');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('measureCharWidth throws with renderer name in message', () => {
|
|
211
|
+
const r = new TestRenderer();
|
|
212
|
+
expect(() => r.measureCharWidth('A')).toThrow('TestRenderer: measureCharWidth() not implemented');
|
|
213
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { SvgRenderer } from '../../core/lib/renderers/svgRenderer.js';
|
|
2
|
+
import { RendererBase } from '../../core/lib/renderers/rendererBase.js';
|
|
3
|
+
import { Point } from '../../core/entities/point.js';
|
|
4
|
+
import { Text } from '../../core/entities/text.js';
|
|
5
|
+
|
|
6
|
+
describe('SvgRenderer — getOutput()', () => {
|
|
7
|
+
test('produces valid SVG with XML declaration', () => {
|
|
8
|
+
const renderer = new SvgRenderer(100, 100);
|
|
9
|
+
expect(renderer.getOutput().startsWith('<?xml version="1.0" encoding="UTF-8"?>')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('contains svg element with correct dimensions', () => {
|
|
13
|
+
const renderer = new SvgRenderer(595, 842);
|
|
14
|
+
const output = renderer.getOutput();
|
|
15
|
+
expect(output).toContain('<svg xmlns="http://www.w3.org/2000/svg" width="595" height="842"');
|
|
16
|
+
expect(output).toContain('viewBox="0 0 595 842"');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('contains Y-flip group for scene coordinate conversion', () => {
|
|
20
|
+
const renderer = new SvgRenderer(100, 200);
|
|
21
|
+
expect(renderer.getOutput()).toContain('translate(0,200) scale(1,-1)');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('ends with </svg>', () => {
|
|
25
|
+
const renderer = new SvgRenderer(100, 100);
|
|
26
|
+
expect(renderer.getOutput().trim().endsWith('</svg>')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('fillBackground emits rect before Y-flip group', () => {
|
|
30
|
+
const renderer = new SvgRenderer(200, 100);
|
|
31
|
+
renderer.setBackgroundColour({ r: 30, g: 30, b: 30 });
|
|
32
|
+
renderer.fillBackground();
|
|
33
|
+
const output = renderer.getOutput();
|
|
34
|
+
const rectPos = output.indexOf('<rect');
|
|
35
|
+
const flipPos = output.indexOf('scale(1,-1)');
|
|
36
|
+
expect(rectPos).toBeGreaterThan(-1);
|
|
37
|
+
expect(flipPos).toBeGreaterThan(rectPos);
|
|
38
|
+
expect(output).toContain('fill="rgb(30,30,30)"');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('SvgRenderer — drawing', () => {
|
|
43
|
+
test('drawShape emits path element with M and L commands', () => {
|
|
44
|
+
const renderer = new SvgRenderer(100, 100);
|
|
45
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
46
|
+
renderer.setLineWidth(1);
|
|
47
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 20 }], {});
|
|
48
|
+
const output = renderer.getOutput();
|
|
49
|
+
expect(output).toContain('<path');
|
|
50
|
+
expect(output).toContain('M 0 0');
|
|
51
|
+
expect(output).toContain('L 10 20');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('drawShape with closed=true emits Z in path d', () => {
|
|
55
|
+
const renderer = new SvgRenderer(100, 100);
|
|
56
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
57
|
+
renderer.setLineWidth(1);
|
|
58
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 5, y: 10 }], { closed: true });
|
|
59
|
+
expect(renderer.getOutput()).toContain('Z');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('drawShape defaults to stroke=true with fill="none"', () => {
|
|
63
|
+
const renderer = new SvgRenderer(100, 100);
|
|
64
|
+
renderer.setColour({ r: 255, g: 0, b: 0 });
|
|
65
|
+
renderer.setLineWidth(1);
|
|
66
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], {});
|
|
67
|
+
const output = renderer.getOutput();
|
|
68
|
+
expect(output).toContain('stroke="rgb(255,0,0)"');
|
|
69
|
+
expect(output).toContain('fill="none"');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('drawShape with fill=true, stroke=false emits fill colour and stroke="none"', () => {
|
|
73
|
+
const renderer = new SvgRenderer(100, 100);
|
|
74
|
+
renderer.setColour({ r: 0, g: 128, b: 0 });
|
|
75
|
+
renderer.setLineWidth(1);
|
|
76
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false });
|
|
77
|
+
const output = renderer.getOutput();
|
|
78
|
+
expect(output).toContain('fill="rgb(0,128,0)"');
|
|
79
|
+
expect(output).toContain('stroke="none"');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('drawShape with bulge emits A arc command', () => {
|
|
83
|
+
const renderer = new SvgRenderer(100, 100);
|
|
84
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
85
|
+
renderer.setLineWidth(1);
|
|
86
|
+
// bulge = 1 → semicircle CCW
|
|
87
|
+
const p1 = new Point(100, 50, 1);
|
|
88
|
+
const p2 = new Point(-100, 50);
|
|
89
|
+
renderer.drawShape([p1, p2], {});
|
|
90
|
+
expect(renderer.getOutput()).toContain(' A ');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('drawShape arc: large-arc-flag is 0 for |bulge| <= 1', () => {
|
|
94
|
+
const renderer = new SvgRenderer(100, 100);
|
|
95
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
96
|
+
renderer.setLineWidth(1);
|
|
97
|
+
const p1 = new Point(10, 0, 1); // bulge=1 → semicircle, |bulge| not > 1
|
|
98
|
+
const p2 = new Point(-10, 0);
|
|
99
|
+
renderer.drawShape([p1, p2], {});
|
|
100
|
+
// A rx ry 0 0 sweep x y — large-arc-flag = 0
|
|
101
|
+
expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 0 /);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('drawShape arc: large-arc-flag is 1 for |bulge| > 1', () => {
|
|
105
|
+
const renderer = new SvgRenderer(100, 100);
|
|
106
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
107
|
+
renderer.setLineWidth(1);
|
|
108
|
+
const p1 = new Point(10, 0, 2); // |bulge| > 1 → major arc
|
|
109
|
+
const p2 = new Point(-10, 0);
|
|
110
|
+
renderer.drawShape([p1, p2], {});
|
|
111
|
+
// A rx ry 0 1 sweep x y — large-arc-flag = 1
|
|
112
|
+
expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 1 /);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('drawShape arc: sweep-flag is 1 for bulge > 0 (CCW in Y-up)', () => {
|
|
116
|
+
const renderer = new SvgRenderer(100, 100);
|
|
117
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
118
|
+
renderer.setLineWidth(1);
|
|
119
|
+
const p1 = new Point(10, 0, 1); // CCW
|
|
120
|
+
const p2 = new Point(-10, 0);
|
|
121
|
+
renderer.drawShape([p1, p2], {});
|
|
122
|
+
// A rx ry 0 large-arc 1 x y
|
|
123
|
+
expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 \d 1 /);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('drawShape arc: sweep-flag is 0 for bulge < 0 (CW in Y-up)', () => {
|
|
127
|
+
const renderer = new SvgRenderer(100, 100);
|
|
128
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
129
|
+
renderer.setLineWidth(1);
|
|
130
|
+
const p1 = new Point(10, 0, -1); // CW
|
|
131
|
+
const p2 = new Point(-10, 0);
|
|
132
|
+
renderer.drawShape([p1, p2], {});
|
|
133
|
+
// A rx ry 0 large-arc 0 x y
|
|
134
|
+
expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 \d 0 /);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('drawSegments solid emits one path element', () => {
|
|
138
|
+
const renderer = new SvgRenderer(100, 100);
|
|
139
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
140
|
+
renderer.setLineWidth(1);
|
|
141
|
+
renderer.drawSegments([
|
|
142
|
+
{ x1: 0, y1: 0, x2: 10, y2: 0 },
|
|
143
|
+
{ x1: 20, y1: 0, x2: 30, y2: 0 },
|
|
144
|
+
], []);
|
|
145
|
+
const output = renderer.getOutput();
|
|
146
|
+
expect(output).toContain('M 0 0 L 10 0');
|
|
147
|
+
expect(output).toContain('M 20 0 L 30 0');
|
|
148
|
+
// Both segments in one <path>
|
|
149
|
+
expect((output.match(/<path/g) ?? []).length).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('drawSegments dashed emits one path per segment with stroke-dasharray', () => {
|
|
153
|
+
const renderer = new SvgRenderer(100, 100);
|
|
154
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
155
|
+
renderer.setLineWidth(1);
|
|
156
|
+
renderer.drawSegments([
|
|
157
|
+
{ x1: 0, y1: 0, x2: 10, y2: 0, dashPhase: 0 },
|
|
158
|
+
{ x1: 20, y1: 0, x2: 30, y2: 0, dashPhase: 5 },
|
|
159
|
+
], [5, 3]);
|
|
160
|
+
const output = renderer.getOutput();
|
|
161
|
+
expect(output).toContain('stroke-dasharray="5,3"');
|
|
162
|
+
expect(output).toContain('stroke-dashoffset="0"');
|
|
163
|
+
expect(output).toContain('stroke-dashoffset="5"');
|
|
164
|
+
expect((output.match(/<path/g) ?? []).length).toBe(2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('drawText emits text element with character and transform', () => {
|
|
168
|
+
const renderer = new SvgRenderer(100, 100);
|
|
169
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
170
|
+
renderer.drawText([{ char: 'A', x: 10, y: 20, rotation: 0 }], 'Helvetica', 5);
|
|
171
|
+
const output = renderer.getOutput();
|
|
172
|
+
expect(output).toContain('<text');
|
|
173
|
+
expect(output).toContain('>A<');
|
|
174
|
+
expect(output).toContain('font-family="Helvetica"');
|
|
175
|
+
expect(output).toContain('font-size="5"');
|
|
176
|
+
expect(output).toContain('translate(10,20)');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('drawText with rotation includes rotate in transform', () => {
|
|
180
|
+
const renderer = new SvgRenderer(100, 100);
|
|
181
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
182
|
+
const r = Math.PI / 4; // 45 degrees
|
|
183
|
+
renderer.drawText([{ char: 'X', x: 0, y: 0, rotation: r }], 'Helvetica', 5);
|
|
184
|
+
// -45 degrees in transform (negated for Y-flip compensation)
|
|
185
|
+
expect(renderer.getOutput()).toContain('rotate(-45)');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('drawText escapes XML special characters', () => {
|
|
189
|
+
const renderer = new SvgRenderer(100, 100);
|
|
190
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
191
|
+
renderer.drawText([{ char: '<', x: 0, y: 0, rotation: 0 }], 'Helvetica', 5);
|
|
192
|
+
expect(renderer.getOutput()).toContain('<');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('SvgRenderer — state methods', () => {
|
|
197
|
+
test('setColour affects stroke and fill on subsequent drawShape', () => {
|
|
198
|
+
const renderer = new SvgRenderer(100, 100);
|
|
199
|
+
renderer.setColour({ r: 100, g: 150, b: 200 });
|
|
200
|
+
renderer.setLineWidth(1);
|
|
201
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 1, y: 0 }], {});
|
|
202
|
+
expect(renderer.getOutput()).toContain('rgb(100,150,200)');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('setLineWidth affects stroke-width on drawShape', () => {
|
|
206
|
+
const renderer = new SvgRenderer(100, 100);
|
|
207
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
208
|
+
renderer.setLineWidth(3.5);
|
|
209
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 1, y: 0 }], {});
|
|
210
|
+
expect(renderer.getOutput()).toContain('stroke-width="3.5"');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('setDash with array emits stroke-dasharray on drawShape', () => {
|
|
214
|
+
const renderer = new SvgRenderer(100, 100);
|
|
215
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
216
|
+
renderer.setLineWidth(1);
|
|
217
|
+
renderer.setDash([5, 3], 1);
|
|
218
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], {});
|
|
219
|
+
const output = renderer.getOutput();
|
|
220
|
+
expect(output).toContain('stroke-dasharray="5,3"');
|
|
221
|
+
expect(output).toContain('stroke-dashoffset="1"');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('setHighlight is a no-op (does not add content)', () => {
|
|
225
|
+
const renderer = new SvgRenderer(100, 100);
|
|
226
|
+
renderer.setHighlight(true, { r: 255, g: 0, b: 0 }, 5);
|
|
227
|
+
const output = renderer.getOutput();
|
|
228
|
+
// Only the structural SVG tags — no extra elements
|
|
229
|
+
expect((output.match(/<path/g) ?? []).length).toBe(0);
|
|
230
|
+
expect((output.match(/<text/g) ?? []).length).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('SvgRenderer — transform methods', () => {
|
|
235
|
+
test('setTransform emits matrix group', () => {
|
|
236
|
+
const renderer = new SvgRenderer(100, 100);
|
|
237
|
+
renderer.setTransform({ a: 2, d: 2, e: 10, f: 20 });
|
|
238
|
+
expect(renderer.getOutput()).toContain('matrix(2 0 0 2 10 20)');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('setTransform group is automatically closed in getOutput()', () => {
|
|
242
|
+
const renderer = new SvgRenderer(100, 100);
|
|
243
|
+
renderer.setTransform({ a: 1, d: 1, e: 0, f: 0 });
|
|
244
|
+
const output = renderer.getOutput();
|
|
245
|
+
// Every <g must have a matching </g>
|
|
246
|
+
const opens = (output.match(/<g[\s>]/g) ?? []).length;
|
|
247
|
+
const closes = (output.match(/<\/g>/g) ?? []).length;
|
|
248
|
+
expect(opens).toBe(closes);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('save and restore emit balanced <g> and </g>', () => {
|
|
252
|
+
const renderer = new SvgRenderer(100, 100);
|
|
253
|
+
renderer.save();
|
|
254
|
+
renderer.setColour({ r: 0, g: 0, b: 0 });
|
|
255
|
+
renderer.setLineWidth(1);
|
|
256
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 1, y: 0 }], {});
|
|
257
|
+
renderer.restore();
|
|
258
|
+
const output = renderer.getOutput();
|
|
259
|
+
const opens = (output.match(/<g[\s>]/g) ?? []).length;
|
|
260
|
+
const closes = (output.match(/<\/g>/g) ?? []).length;
|
|
261
|
+
expect(opens).toBe(closes);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('applyTransform updates innermost plain <g> with transform attribute', () => {
|
|
265
|
+
const renderer = new SvgRenderer(100, 100);
|
|
266
|
+
renderer.save();
|
|
267
|
+
renderer.applyTransform({ x: 5, y: 10, rotation: 0 });
|
|
268
|
+
renderer.restore();
|
|
269
|
+
expect(renderer.getOutput()).toContain('translate(5,10)');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('SvgRenderer — measurement', () => {
|
|
274
|
+
test('measureText returns width matching Text.getApproximateWidth', () => {
|
|
275
|
+
const renderer = new SvgRenderer(100, 100);
|
|
276
|
+
const result = renderer.measureText('Hello', 'Helvetica', 10);
|
|
277
|
+
expect(result.width).toBeCloseTo(Text.getApproximateWidth('Hello', 10), 5);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('measureCharWidth returns per-character width', () => {
|
|
281
|
+
const renderer = new SvgRenderer(100, 100);
|
|
282
|
+
expect(renderer.measureCharWidth('M')).toBeCloseTo(Text.getApproximateWidth('M', 1), 5);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('SvgRenderer — plot styles', () => {
|
|
287
|
+
/**
|
|
288
|
+
* @param {SvgRenderer} renderer
|
|
289
|
+
*/
|
|
290
|
+
function drawLine(renderer) {
|
|
291
|
+
renderer.setLineWidth(1);
|
|
292
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], {});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
test('default style NONE renders colour unchanged', () => {
|
|
296
|
+
const renderer = new SvgRenderer(100, 100);
|
|
297
|
+
renderer.setColour({ r: 255, g: 0, b: 0 });
|
|
298
|
+
drawLine(renderer);
|
|
299
|
+
expect(renderer.getOutput()).toContain('stroke="rgb(255,0,0)"');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('style MONOCHROME maps all colours to black', () => {
|
|
303
|
+
const renderer = new SvgRenderer(100, 100);
|
|
304
|
+
renderer.setStyle(RendererBase.Styles.MONOCHROME);
|
|
305
|
+
renderer.setColour({ r: 255, g: 0, b: 0 });
|
|
306
|
+
drawLine(renderer);
|
|
307
|
+
expect(renderer.getOutput()).toContain('stroke="rgb(0,0,0)"');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('style GREYSCALE maps colour to grey', () => {
|
|
311
|
+
const renderer = new SvgRenderer(100, 100);
|
|
312
|
+
renderer.setStyle(RendererBase.Styles.GREYSCALE);
|
|
313
|
+
renderer.setColour({ r: 255, g: 0, b: 0 }); // 0.299*255 ≈ 76
|
|
314
|
+
drawLine(renderer);
|
|
315
|
+
expect(renderer.getOutput()).toContain('stroke="rgb(76,76,76)"');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('setStyle with null falls back to NONE', () => {
|
|
319
|
+
const renderer = new SvgRenderer(100, 100);
|
|
320
|
+
renderer.setStyle(RendererBase.Styles.MONOCHROME);
|
|
321
|
+
renderer.setStyle(null); // reset to NONE
|
|
322
|
+
renderer.setColour({ r: 0, g: 128, b: 255 });
|
|
323
|
+
drawLine(renderer);
|
|
324
|
+
expect(renderer.getOutput()).toContain('stroke="rgb(0,128,255)"');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -5,6 +5,9 @@ import { Point } from '../../core/entities/point.js';
|
|
|
5
5
|
|
|
6
6
|
import { File } from '../test-helpers/test-helpers.js';
|
|
7
7
|
import { Flags } from '../../core/properties/flags.js';
|
|
8
|
+
import { DesignCore } from '../../core/designCore.js';
|
|
9
|
+
import { Utils } from '../../core/lib/utils.js';
|
|
10
|
+
import { DXFFile } from '../../core/lib/dxf/dxfFile.js';
|
|
8
11
|
|
|
9
12
|
new Core();
|
|
10
13
|
|
|
@@ -127,7 +130,7 @@ ByLayer
|
|
|
127
130
|
31
|
|
128
131
|
0.0
|
|
129
132
|
39
|
|
130
|
-
|
|
133
|
+
1
|
|
131
134
|
0
|
|
132
135
|
ENDBLK
|
|
133
136
|
5
|
|
@@ -155,3 +158,30 @@ test('Test Block.touched', () => {
|
|
|
155
158
|
expect(block.touched(selectionExtremesTrue)).toBe(true);
|
|
156
159
|
expect(block.touched(selectionExtremesFalse)).toBe(false);
|
|
157
160
|
});
|
|
161
|
+
|
|
162
|
+
test('Test Block.dxf throws when a block item has an undefined handle', () => {
|
|
163
|
+
const block = new Block({ handle: '1', endblkHandle: '2' });
|
|
164
|
+
// cloneObject clears the handle, simulating an item added without a handle assignment
|
|
165
|
+
const line = new Line({ handle: '3', points: [new Point(0, 0), new Point(1, 1)] });
|
|
166
|
+
const clone = Utils.cloneObject(line);
|
|
167
|
+
expect(clone.getProperty('handle')).toBeUndefined();
|
|
168
|
+
block.addItem(clone);
|
|
169
|
+
|
|
170
|
+
// Use a real DXFFile (R2000+) so the version-gated handle validation fires
|
|
171
|
+
const file = new DXFFile('R2000');
|
|
172
|
+
expect(() => block.dxf(file)).toThrow();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Test Block.dxf succeeds when block items have valid handles', () => {
|
|
176
|
+
const block = new Block({ handle: '1', endblkHandle: '2' });
|
|
177
|
+
const line = new Line({ handle: '3', points: [new Point(0, 0), new Point(1, 1)] });
|
|
178
|
+
const clone = Utils.cloneObject(line);
|
|
179
|
+
// simulate the handle assignment done in Block.execute()
|
|
180
|
+
clone.setProperty('handle', DesignCore.HandleManager.next());
|
|
181
|
+
expect(clone.getProperty('handle')).toMatch(/^[0-9A-F]+$/i);
|
|
182
|
+
block.addItem(clone);
|
|
183
|
+
|
|
184
|
+
const file = new DXFFile('R2000');
|
|
185
|
+
expect(() => block.dxf(file)).not.toThrow();
|
|
186
|
+
expect(file.contents).toContain(clone.getProperty('handle'));
|
|
187
|
+
});
|
|
@@ -3,12 +3,12 @@ import { File } from '../test-helpers/test-helpers.js';
|
|
|
3
3
|
|
|
4
4
|
test('Test EndBlock constructor defaults', () => {
|
|
5
5
|
const endblk = new EndBlock();
|
|
6
|
-
expect(endblk.layer).toBe('0');
|
|
6
|
+
expect(endblk.getProperty('layer')).toBe('0');
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
test('Test EndBlock constructor with layer', () => {
|
|
10
10
|
const endblk = new EndBlock({ 8: 'TestLayer' });
|
|
11
|
-
expect(endblk.layer).toBe('TestLayer');
|
|
11
|
+
expect(endblk.getProperty('layer')).toBe('TestLayer');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
test('Test EndBlock.dxf', () => {
|
|
@@ -62,6 +62,36 @@ test('Test Layer.isSelectable', () => {
|
|
|
62
62
|
expect(layer.isSelectable).toBe(true);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test('Test Layer.isPlottable - defaults to true', () => {
|
|
66
|
+
const layer = new Layer();
|
|
67
|
+
expect(layer.isPlottable).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('Test Layer.isPlottable - false when plotting is false', () => {
|
|
71
|
+
const layer = new Layer();
|
|
72
|
+
layer.plotting = false;
|
|
73
|
+
expect(layer.isPlottable).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('Test Layer.isPlottable - false when layer is frozen', () => {
|
|
77
|
+
const layer = new Layer();
|
|
78
|
+
layer.frozen = true;
|
|
79
|
+
expect(layer.isPlottable).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('Test Layer.isPlottable - false when layer is off', () => {
|
|
83
|
+
const layer = new Layer();
|
|
84
|
+
layer.on = false;
|
|
85
|
+
expect(layer.isPlottable).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('Test Layer.isPlottable - false when both plotting false and frozen', () => {
|
|
89
|
+
const layer = new Layer();
|
|
90
|
+
layer.plotting = false;
|
|
91
|
+
layer.frozen = true;
|
|
92
|
+
expect(layer.isPlottable).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
65
95
|
test('Test Layer.dxf', () => {
|
|
66
96
|
const layer = new Layer({ handle: 'A', name: 'TestLayer' });
|
|
67
97
|
const file = new File();
|