@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,271 @@
|
|
|
1
|
+
import { PdfRenderer } from '../../core/lib/renderers/pdfRenderer.js';
|
|
2
|
+
import { Text } from '../../core/entities/text.js';
|
|
3
|
+
import { Point } from '../../core/entities/point.js';
|
|
4
|
+
|
|
5
|
+
describe('PdfRenderer — getOutput()', () => {
|
|
6
|
+
test('produces a valid PDF header', () => {
|
|
7
|
+
const renderer = new PdfRenderer(100, 100);
|
|
8
|
+
const output = renderer.getOutput();
|
|
9
|
+
expect(output.startsWith('%PDF-1.4')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('ends with %%EOF', () => {
|
|
13
|
+
const renderer = new PdfRenderer(100, 100);
|
|
14
|
+
const output = renderer.getOutput();
|
|
15
|
+
expect(output.endsWith('%%EOF')).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('contains required PDF structural elements', () => {
|
|
19
|
+
const renderer = new PdfRenderer(595, 842);
|
|
20
|
+
const output = renderer.getOutput();
|
|
21
|
+
expect(output).toContain('/Catalog');
|
|
22
|
+
expect(output).toContain('/Pages');
|
|
23
|
+
expect(output).toContain('/Page');
|
|
24
|
+
expect(output).toContain('/MediaBox [0 0 595 842]');
|
|
25
|
+
expect(output).toContain('/Helvetica');
|
|
26
|
+
expect(output).toContain('xref');
|
|
27
|
+
expect(output).toContain('trailer');
|
|
28
|
+
expect(output).toContain('startxref');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('PdfRenderer — drawing', () => {
|
|
33
|
+
test('drawShape emits m, l, and S operators', () => {
|
|
34
|
+
const renderer = new PdfRenderer(100, 100);
|
|
35
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 20 }], {});
|
|
36
|
+
const output = renderer.getOutput();
|
|
37
|
+
expect(output).toContain('0 0 m');
|
|
38
|
+
expect(output).toContain('10 20 l');
|
|
39
|
+
expect(output).toContain('S');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('drawShape with closed=true emits h before S', () => {
|
|
43
|
+
const renderer = new PdfRenderer(100, 100);
|
|
44
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 5, y: 10 }], { closed: true });
|
|
45
|
+
const output = renderer.getOutput();
|
|
46
|
+
const hPos = output.indexOf('\nh\n');
|
|
47
|
+
const sPos = output.indexOf('\nS\n');
|
|
48
|
+
expect(hPos).toBeGreaterThan(-1);
|
|
49
|
+
expect(sPos).toBeGreaterThan(hPos);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('drawShape with fill+stroke emits B operator', () => {
|
|
53
|
+
const renderer = new PdfRenderer(100, 100);
|
|
54
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: true });
|
|
55
|
+
const output = renderer.getOutput();
|
|
56
|
+
expect(output).toContain('\nB\n');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('drawShape with fill only emits f operator', () => {
|
|
60
|
+
const renderer = new PdfRenderer(100, 100);
|
|
61
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false });
|
|
62
|
+
const output = renderer.getOutput();
|
|
63
|
+
expect(output).toContain('\nf\n');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('drawShape with fillRule evenodd emits f* operator', () => {
|
|
67
|
+
const renderer = new PdfRenderer(100, 100);
|
|
68
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false, fillRule: 'evenodd' });
|
|
69
|
+
const output = renderer.getOutput();
|
|
70
|
+
expect(output).toContain('\nf*\n');
|
|
71
|
+
expect(output).not.toContain('\nf\n');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('drawShape with fill+stroke and fillRule evenodd emits B* operator', () => {
|
|
75
|
+
const renderer = new PdfRenderer(100, 100);
|
|
76
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: true, fillRule: 'evenodd' });
|
|
77
|
+
const output = renderer.getOutput();
|
|
78
|
+
expect(output).toContain('\nB*\n');
|
|
79
|
+
expect(output).not.toContain('\nB\n');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('straight segment (no bulge) emits l operator', () => {
|
|
83
|
+
const renderer = new PdfRenderer(100, 100);
|
|
84
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], {});
|
|
85
|
+
const output = renderer.getOutput();
|
|
86
|
+
expect(output).toContain('10 0 l');
|
|
87
|
+
expect(output).not.toContain(' c\n');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('bulge segment emits cubic Bézier c operator, not chord l', () => {
|
|
91
|
+
const renderer = new PdfRenderer(100, 100);
|
|
92
|
+
// bulge=1 → 180° CCW arc from (0,0) to (10,0)
|
|
93
|
+
const start = new Point(0, 0, 1);
|
|
94
|
+
const end = new Point(10, 0);
|
|
95
|
+
renderer.drawShape([start, end], {});
|
|
96
|
+
const output = renderer.getOutput();
|
|
97
|
+
expect(output).toContain(' c');
|
|
98
|
+
expect(output).not.toContain('10 0 l');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('180° arc (bulge=1) splits into 2 Bézier curves', () => {
|
|
102
|
+
const renderer = new PdfRenderer(100, 100);
|
|
103
|
+
const start = new Point(0, 0, 1);
|
|
104
|
+
const end = new Point(10, 0);
|
|
105
|
+
renderer.drawShape([start, end], {});
|
|
106
|
+
const streamContent = renderer.getOutput().match(/stream\n([\s\S]*?)\nendstream/)[1];
|
|
107
|
+
const cCount = (streamContent.match(/ c\n/g) || []).length;
|
|
108
|
+
expect(cCount).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('90° arc splits into 1 Bézier curve', () => {
|
|
112
|
+
const renderer = new PdfRenderer(100, 100);
|
|
113
|
+
// start=(100,0) bulge=tan(π/8)≈0.4142 → 90° arc, end=(150,50), centre=(100,50), r=50
|
|
114
|
+
const start = new Point(100, 0, Math.tan(Math.PI / 8));
|
|
115
|
+
const end = new Point(150, 50);
|
|
116
|
+
renderer.drawShape([start, end], {});
|
|
117
|
+
const streamContent = renderer.getOutput().match(/stream\n([\s\S]*?)\nendstream/)[1];
|
|
118
|
+
const cCount = (streamContent.match(/ c\n/g) || []).length;
|
|
119
|
+
expect(cCount).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('full circle (two 180° arcs) emits 4 Bézier curves', () => {
|
|
123
|
+
const renderer = new PdfRenderer(100, 100);
|
|
124
|
+
// Circle centre=(50,50) r=50 — mirrors Circle.toPolylinePoints() output
|
|
125
|
+
const p1 = new Point(100, 50, 1);
|
|
126
|
+
const p2 = new Point(0, 50, 1);
|
|
127
|
+
const p3 = new Point(100, 50);
|
|
128
|
+
renderer.drawShape([p1, p2, p3], {});
|
|
129
|
+
const streamContent = renderer.getOutput().match(/stream\n([\s\S]*?)\nendstream/)[1];
|
|
130
|
+
const cCount = (streamContent.match(/ c\n/g) || []).length;
|
|
131
|
+
expect(cCount).toBe(4);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('drawSegments solid batches all segments with one S', () => {
|
|
135
|
+
const renderer = new PdfRenderer(100, 100);
|
|
136
|
+
renderer.drawSegments([
|
|
137
|
+
{ x1: 0, y1: 0, x2: 10, y2: 0 },
|
|
138
|
+
{ x1: 20, y1: 0, x2: 30, y2: 0 },
|
|
139
|
+
], []);
|
|
140
|
+
const output = renderer.getOutput();
|
|
141
|
+
expect(output).toContain('0 0 m');
|
|
142
|
+
expect(output).toContain('10 0 l');
|
|
143
|
+
expect(output).toContain('20 0 m');
|
|
144
|
+
expect(output).toContain('30 0 l');
|
|
145
|
+
// Only one S for the entire batch
|
|
146
|
+
const streamStart = output.indexOf('stream\n') + 'stream\n'.length;
|
|
147
|
+
const streamEnd = output.indexOf('\nendstream');
|
|
148
|
+
const streamContent = output.slice(streamStart, streamEnd);
|
|
149
|
+
expect(streamContent.split('\nS').length - 1).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('drawSegments dashed emits d operator per segment', () => {
|
|
153
|
+
const renderer = new PdfRenderer(100, 100);
|
|
154
|
+
renderer.drawSegments([
|
|
155
|
+
{ x1: 0, y1: 0, x2: 10, y2: 0, dashPhase: 0 },
|
|
156
|
+
{ x1: 20, y1: 0, x2: 30, y2: 0, dashPhase: 5 },
|
|
157
|
+
], [5, 3]);
|
|
158
|
+
const output = renderer.getOutput();
|
|
159
|
+
expect(output).toContain('[5 3] 0 d');
|
|
160
|
+
expect(output).toContain('[5 3] 5 d');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('drawText emits BT block with character and position transform', () => {
|
|
164
|
+
const renderer = new PdfRenderer(100, 100);
|
|
165
|
+
renderer.drawText([{ char: 'A', x: 10, y: 20, rotation: 0 }], 'Helvetica', 5);
|
|
166
|
+
const output = renderer.getOutput();
|
|
167
|
+
expect(output).toContain('BT');
|
|
168
|
+
expect(output).toContain('ET');
|
|
169
|
+
expect(output).toContain('(A)');
|
|
170
|
+
expect(output).toContain('10 20 cm');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('drawText with rotation applies sin/cos transform', () => {
|
|
174
|
+
const renderer = new PdfRenderer(100, 100);
|
|
175
|
+
const r = Math.PI / 4; // 45 degrees
|
|
176
|
+
renderer.drawText([{ char: 'X', x: 0, y: 0, rotation: r }], 'Helvetica', 5);
|
|
177
|
+
const output = renderer.getOutput();
|
|
178
|
+
// cos(π/4) ≈ 0.7071, sin(π/4) ≈ 0.7071
|
|
179
|
+
expect(output).toContain('0.7071');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('PdfRenderer — state methods', () => {
|
|
184
|
+
test('setColour emits RG and rg operators', () => {
|
|
185
|
+
const renderer = new PdfRenderer(100, 100);
|
|
186
|
+
renderer.setColour({ r: 255, g: 0, b: 0 });
|
|
187
|
+
const output = renderer.getOutput();
|
|
188
|
+
expect(output).toContain('1 0 0 RG');
|
|
189
|
+
expect(output).toContain('1 0 0 rg');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('setLineWidth emits w operator', () => {
|
|
193
|
+
const renderer = new PdfRenderer(100, 100);
|
|
194
|
+
renderer.setLineWidth(2.5);
|
|
195
|
+
const output = renderer.getOutput();
|
|
196
|
+
expect(output).toContain('2.5 w');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('setDash with array emits d operator', () => {
|
|
200
|
+
const renderer = new PdfRenderer(100, 100);
|
|
201
|
+
renderer.setDash([5, 3], 1);
|
|
202
|
+
const output = renderer.getOutput();
|
|
203
|
+
expect(output).toContain('[5 3] 1 d');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('setDash with empty array emits solid line operator', () => {
|
|
207
|
+
const renderer = new PdfRenderer(100, 100);
|
|
208
|
+
renderer.setDash([], 0);
|
|
209
|
+
const output = renderer.getOutput();
|
|
210
|
+
expect(output).toContain('[] 0 d');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('setHighlight is a no-op (does not add content)', () => {
|
|
214
|
+
const renderer = new PdfRenderer(100, 100);
|
|
215
|
+
renderer.setHighlight(true, { r: 255, g: 0, b: 0 }, 5);
|
|
216
|
+
const streamContent = renderer.getOutput().match(/stream\n([\s\S]*?)\nendstream/)[1];
|
|
217
|
+
expect(streamContent).toBe('');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('fillBackground is a no-op (does not add content)', () => {
|
|
221
|
+
const renderer = new PdfRenderer(100, 100);
|
|
222
|
+
renderer.setBackgroundColour({ r: 0, g: 0, b: 0 });
|
|
223
|
+
renderer.fillBackground();
|
|
224
|
+
const streamContent = renderer.getOutput().match(/stream\n([\s\S]*?)\nendstream/)[1];
|
|
225
|
+
expect(streamContent).toBe('');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('PdfRenderer — transform methods', () => {
|
|
230
|
+
test('setTransform emits cm operator with matrix values', () => {
|
|
231
|
+
const renderer = new PdfRenderer(100, 100);
|
|
232
|
+
renderer.setTransform({ a: 2, d: 2, e: 10, f: 20 });
|
|
233
|
+
const output = renderer.getOutput();
|
|
234
|
+
expect(output).toContain('2 0 0 2 10 20 cm');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('applyTransform with no rotation emits identity-rotate cm', () => {
|
|
238
|
+
const renderer = new PdfRenderer(100, 100);
|
|
239
|
+
renderer.applyTransform({ x: 5, y: 10, rotation: 0 });
|
|
240
|
+
const output = renderer.getOutput();
|
|
241
|
+
expect(output).toContain('1 0 0 1 5 10 cm');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('save emits q operator', () => {
|
|
245
|
+
const renderer = new PdfRenderer(100, 100);
|
|
246
|
+
renderer.save();
|
|
247
|
+
const output = renderer.getOutput();
|
|
248
|
+
expect(output).toContain('\nq\n');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('restore emits Q operator', () => {
|
|
252
|
+
const renderer = new PdfRenderer(100, 100);
|
|
253
|
+
renderer.restore();
|
|
254
|
+
const output = renderer.getOutput();
|
|
255
|
+
expect(output).toContain('\nQ\n');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('PdfRenderer — measurement', () => {
|
|
260
|
+
test('measureText returns width matching Text.getApproximateWidth', () => {
|
|
261
|
+
const renderer = new PdfRenderer(100, 100);
|
|
262
|
+
const result = renderer.measureText('Hello', 'Helvetica', 10);
|
|
263
|
+
expect(result).toEqual({ width: Text.getApproximateWidth('Hello', 10) });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('measureCharWidth returns value matching Text.getApproximateWidth', () => {
|
|
267
|
+
const renderer = new PdfRenderer(100, 100);
|
|
268
|
+
const result = renderer.measureCharWidth('A');
|
|
269
|
+
expect(result).toBe(Text.getApproximateWidth('A', 1));
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -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
|
+
});
|