@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,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,22 @@ 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';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a mock requestInput function that returns values from the provided array in order.
|
|
14
|
+
* @param {Array} inputs
|
|
15
|
+
* @return {Function}
|
|
16
|
+
*/
|
|
17
|
+
function mockRequestInput(inputs) {
|
|
18
|
+
let i = 0;
|
|
19
|
+
return async () => {
|
|
20
|
+
if (i < inputs.length) return inputs[i++];
|
|
21
|
+
throw new Error('cancelled');
|
|
22
|
+
};
|
|
23
|
+
}
|
|
8
24
|
|
|
9
25
|
new Core();
|
|
10
26
|
|
|
@@ -12,19 +28,19 @@ test('Test Block', () => {
|
|
|
12
28
|
const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
|
|
13
29
|
const flags = new Flags();
|
|
14
30
|
flags.addValue(1);
|
|
15
|
-
const block = new Block({
|
|
31
|
+
const block = new Block({ entities: [line], flags: flags });
|
|
16
32
|
|
|
17
|
-
expect(block.
|
|
33
|
+
expect(block.entities.length).toBe(1);
|
|
18
34
|
expect(block.flags.getFlagValue()).toBe(1);
|
|
19
35
|
});
|
|
20
36
|
|
|
21
37
|
test('Test Block.clearItems', () => {
|
|
22
38
|
const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
|
|
23
|
-
const block = new Block({
|
|
24
|
-
expect(block.
|
|
39
|
+
const block = new Block({ entities: [line] });
|
|
40
|
+
expect(block.entities.length).toBe(1);
|
|
25
41
|
|
|
26
|
-
block.
|
|
27
|
-
expect(block.
|
|
42
|
+
block.clearEntities();
|
|
43
|
+
expect(block.entities.length).toBe(0);
|
|
28
44
|
});
|
|
29
45
|
|
|
30
46
|
test('Test Block.snaps', () => {
|
|
@@ -33,7 +49,7 @@ test('Test Block.snaps', () => {
|
|
|
33
49
|
expect(block.snaps()).toHaveLength(0);
|
|
34
50
|
|
|
35
51
|
const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
|
|
36
|
-
block.
|
|
52
|
+
block.addEntity(line);
|
|
37
53
|
expect(block.snaps()).toBeInstanceOf(Array);
|
|
38
54
|
expect(block.snaps()).not.toHaveLength(0);
|
|
39
55
|
});
|
|
@@ -42,9 +58,9 @@ test('Test Block', () => {
|
|
|
42
58
|
const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
|
|
43
59
|
const flags = new Flags();
|
|
44
60
|
flags.addValue(1);
|
|
45
|
-
const block = new Block({
|
|
61
|
+
const block = new Block({ entities: [line], flags: flags });
|
|
46
62
|
|
|
47
|
-
expect(block.
|
|
63
|
+
expect(block.entities.length).toBe(1);
|
|
48
64
|
expect(block.flags.getFlagValue()).toBe(1);
|
|
49
65
|
});
|
|
50
66
|
|
|
@@ -60,7 +76,7 @@ test('Test Block.closestPoint', () => {
|
|
|
60
76
|
expect(block.closestPoint(point)[1]).toBe(Infinity);
|
|
61
77
|
|
|
62
78
|
const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
|
|
63
|
-
block.
|
|
79
|
+
block.addEntity(line);
|
|
64
80
|
expect(block.closestPoint(point)).toBeInstanceOf(Array);
|
|
65
81
|
expect(block.closestPoint(point)).toHaveLength(2);
|
|
66
82
|
expect(block.closestPoint(point)[0]).toBeInstanceOf(Point);
|
|
@@ -72,7 +88,7 @@ test('Test Block.closestPoint', () => {
|
|
|
72
88
|
test('Test Block.dxf', () => {
|
|
73
89
|
const block = new Block({ handle: '1', endblkHandle: '1' });
|
|
74
90
|
const line = new Line({ handle: '1', points: [new Point(101, 102), new Point(201, 202)] });
|
|
75
|
-
block.
|
|
91
|
+
block.addEntity(line);
|
|
76
92
|
|
|
77
93
|
const file = new File();
|
|
78
94
|
block.dxf(file);
|
|
@@ -127,7 +143,7 @@ ByLayer
|
|
|
127
143
|
31
|
|
128
144
|
0.0
|
|
129
145
|
39
|
|
130
|
-
|
|
146
|
+
1
|
|
131
147
|
0
|
|
132
148
|
ENDBLK
|
|
133
149
|
5
|
|
@@ -151,7 +167,70 @@ test('Test Block.touched', () => {
|
|
|
151
167
|
expect(block.touched(selectionExtremesTrue)).toBe(false);
|
|
152
168
|
|
|
153
169
|
const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
|
|
154
|
-
block.
|
|
170
|
+
block.addEntity(line);
|
|
155
171
|
expect(block.touched(selectionExtremesTrue)).toBe(true);
|
|
156
172
|
expect(block.touched(selectionExtremesFalse)).toBe(false);
|
|
157
173
|
});
|
|
174
|
+
|
|
175
|
+
test('Test Block.dxf throws when a block item has an undefined handle', () => {
|
|
176
|
+
const block = new Block({ handle: '1', endblkHandle: '2' });
|
|
177
|
+
// cloneObject clears the handle, simulating an item added without a handle assignment
|
|
178
|
+
const line = new Line({ handle: '3', points: [new Point(0, 0), new Point(1, 1)] });
|
|
179
|
+
const clone = Utils.cloneObject(line);
|
|
180
|
+
expect(clone.getProperty('handle')).toBeUndefined();
|
|
181
|
+
block.addEntity(clone);
|
|
182
|
+
|
|
183
|
+
// Use a real DXFFile (R2000+) so the version-gated handle validation fires
|
|
184
|
+
const file = new DXFFile('R2000');
|
|
185
|
+
expect(() => block.dxf(file)).toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('Test Block.dxf succeeds when block items have valid handles', () => {
|
|
189
|
+
const block = new Block({ handle: '1', endblkHandle: '2' });
|
|
190
|
+
const line = new Line({ handle: '3', points: [new Point(0, 0), new Point(1, 1)] });
|
|
191
|
+
const clone = Utils.cloneObject(line);
|
|
192
|
+
// simulate the handle assignment done in Block.execute()
|
|
193
|
+
clone.setProperty('handle', DesignCore.HandleManager.next());
|
|
194
|
+
expect(clone.getProperty('handle')).toMatch(/^[0-9A-F]+$/i);
|
|
195
|
+
block.addEntity(clone);
|
|
196
|
+
|
|
197
|
+
const file = new DXFFile('R2000');
|
|
198
|
+
expect(() => block.dxf(file)).not.toThrow();
|
|
199
|
+
expect(file.contents).toContain(clone.getProperty('handle'));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('Test Block.execute offsets entity points by the insert point', async () => {
|
|
203
|
+
new Core();
|
|
204
|
+
|
|
205
|
+
// Add a line to the scene at world coordinates (100,200) → (300,400)
|
|
206
|
+
const line = new Line({ points: [new Point(100, 200), new Point(300, 400)] });
|
|
207
|
+
DesignCore.Scene.entities.add(line);
|
|
208
|
+
const lineIndex = DesignCore.Scene.entities.count() - 1;
|
|
209
|
+
|
|
210
|
+
// Pre-select the line so Block.execute() skips the selection-set prompt
|
|
211
|
+
DesignCore.Scene.selectionManager.reset();
|
|
212
|
+
DesignCore.Scene.selectionManager.addToSelectionSet(lineIndex);
|
|
213
|
+
|
|
214
|
+
// Feed: block name, then insert point (50, 75)
|
|
215
|
+
const insertPoint = new Point(50, 75);
|
|
216
|
+
const origRequestInput = DesignCore.Scene.inputManager.requestInput.bind(DesignCore.Scene.inputManager);
|
|
217
|
+
DesignCore.Scene.inputManager.requestInput = mockRequestInput(['TestBlock', insertPoint]);
|
|
218
|
+
|
|
219
|
+
await new Block().execute();
|
|
220
|
+
|
|
221
|
+
DesignCore.Scene.inputManager.requestInput = origRequestInput;
|
|
222
|
+
|
|
223
|
+
// The block should exist in the block manager
|
|
224
|
+
const block = DesignCore.Scene.blockManager.getItemByName('TestBlock');
|
|
225
|
+
expect(block).toBeDefined();
|
|
226
|
+
expect(block.entities).toHaveLength(1);
|
|
227
|
+
|
|
228
|
+
// Points inside the block must be offset by -insertPoint so that
|
|
229
|
+
// when Insert renders them via applyTransform(+insertPoint) they land
|
|
230
|
+
// back at their original world coordinates.
|
|
231
|
+
const blockLine = block.entities[0];
|
|
232
|
+
expect(blockLine.points[0].x).toBeCloseTo(100 - insertPoint.x);
|
|
233
|
+
expect(blockLine.points[0].y).toBeCloseTo(200 - insertPoint.y);
|
|
234
|
+
expect(blockLine.points[1].x).toBeCloseTo(300 - insertPoint.x);
|
|
235
|
+
expect(blockLine.points[1].y).toBeCloseTo(400 - insertPoint.y);
|
|
236
|
+
});
|
|
@@ -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();
|
|
@@ -91,7 +91,7 @@ test('Test LayerManager.checkStyles', () => {
|
|
|
91
91
|
layer: newItemName,
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
-
core.scene.
|
|
94
|
+
core.scene.addEntity('Line', data);
|
|
95
95
|
|
|
96
96
|
layerManager.checkStyles();
|
|
97
97
|
const layers = layerManager.getItems();
|
|
@@ -115,7 +115,7 @@ test('Test LayerManager.checkItems', () => {
|
|
|
115
115
|
layer: newItemName,
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
core.scene.
|
|
118
|
+
core.scene.addEntity('Line', data);
|
|
119
119
|
|
|
120
120
|
layerManager.checkItems();
|
|
121
121
|
const layers = layerManager.getItems();
|