@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,317 @@
|
|
|
1
|
+
import { CanvasRenderer } from '../../core/lib/renderers/canvasRenderer.js';
|
|
2
|
+
import { Point } from '../../core/entities/point.js';
|
|
3
|
+
|
|
4
|
+
// ─── Mock context factory ─────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build a minimal mock CanvasRenderingContext2D that records every call.
|
|
8
|
+
* @return {Object} mock context with a `calls` array
|
|
9
|
+
*/
|
|
10
|
+
function makeMockCtx() {
|
|
11
|
+
const calls = [];
|
|
12
|
+
const record = (name) => (...args) => calls.push({ name, args });
|
|
13
|
+
return {
|
|
14
|
+
calls,
|
|
15
|
+
save: record('save'),
|
|
16
|
+
restore: record('restore'),
|
|
17
|
+
beginPath: record('beginPath'),
|
|
18
|
+
closePath: record('closePath'),
|
|
19
|
+
moveTo: record('moveTo'),
|
|
20
|
+
lineTo: record('lineTo'),
|
|
21
|
+
arc: record('arc'),
|
|
22
|
+
stroke: record('stroke'),
|
|
23
|
+
fill: record('fill'),
|
|
24
|
+
clip: record('clip'),
|
|
25
|
+
fillText: record('fillText'),
|
|
26
|
+
strokeText: record('strokeText'),
|
|
27
|
+
translate: record('translate'),
|
|
28
|
+
rotate: record('rotate'),
|
|
29
|
+
scale: record('scale'),
|
|
30
|
+
setLineDash: record('setLineDash'),
|
|
31
|
+
setTransform: record('setTransform'),
|
|
32
|
+
fillRect: record('fillRect'),
|
|
33
|
+
measureText: (s) => ({ width: s.length * 8 }),
|
|
34
|
+
strokeStyle: '', fillStyle: '', lineWidth: 0, lineDashOffset: 0, globalAlpha: 1, font: '',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── drawShape ────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe('CanvasRenderer — drawShape', () => {
|
|
41
|
+
test('returns early and emits nothing for empty points', () => {
|
|
42
|
+
const ctx = makeMockCtx();
|
|
43
|
+
new CanvasRenderer(ctx).drawShape([]);
|
|
44
|
+
expect(ctx.calls).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('calls beginPath, moveTo, lineTo, stroke for a basic shape', () => {
|
|
48
|
+
const ctx = makeMockCtx();
|
|
49
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 20 }]);
|
|
50
|
+
const names = ctx.calls.map((c) => c.name);
|
|
51
|
+
expect(names).toContain('beginPath');
|
|
52
|
+
expect(names).toContain('moveTo');
|
|
53
|
+
expect(names).toContain('lineTo');
|
|
54
|
+
expect(names).toContain('stroke');
|
|
55
|
+
expect(names).not.toContain('fill');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('calls closePath before stroke when closed=true', () => {
|
|
59
|
+
const ctx = makeMockCtx();
|
|
60
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { closed: true });
|
|
61
|
+
const names = ctx.calls.map((c) => c.name);
|
|
62
|
+
const closeIdx = names.lastIndexOf('closePath');
|
|
63
|
+
const strokeIdx = names.lastIndexOf('stroke');
|
|
64
|
+
expect(closeIdx).toBeGreaterThan(-1);
|
|
65
|
+
expect(strokeIdx).toBeGreaterThan(closeIdx);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('calls fill with nonzero when fill=true', () => {
|
|
69
|
+
const ctx = makeMockCtx();
|
|
70
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false });
|
|
71
|
+
const fillCall = ctx.calls.find((c) => c.name === 'fill');
|
|
72
|
+
expect(fillCall).toBeDefined();
|
|
73
|
+
expect(fillCall.args[0]).toBe('nonzero');
|
|
74
|
+
expect(ctx.calls.map((c) => c.name)).not.toContain('stroke');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('calls fill with evenodd when fillRule=evenodd', () => {
|
|
78
|
+
const ctx = makeMockCtx();
|
|
79
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false, fillRule: 'evenodd' });
|
|
80
|
+
const fillCall = ctx.calls.find((c) => c.name === 'fill');
|
|
81
|
+
expect(fillCall.args[0]).toBe('evenodd');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('calls both fill and stroke when fill=true and stroke=true', () => {
|
|
85
|
+
const ctx = makeMockCtx();
|
|
86
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: true });
|
|
87
|
+
const names = ctx.calls.map((c) => c.name);
|
|
88
|
+
expect(names).toContain('fill');
|
|
89
|
+
expect(names).toContain('stroke');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('calls clip with nonzero when clip=true', () => {
|
|
93
|
+
const ctx = makeMockCtx();
|
|
94
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { clip: true });
|
|
95
|
+
const clipCall = ctx.calls.find((c) => c.name === 'clip');
|
|
96
|
+
expect(clipCall).toBeDefined();
|
|
97
|
+
expect(clipCall.args[0]).toBe('nonzero');
|
|
98
|
+
expect(ctx.calls.map((c) => c.name)).not.toContain('stroke');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('calls clip with evenodd when clip=true and fillRule=evenodd', () => {
|
|
102
|
+
const ctx = makeMockCtx();
|
|
103
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { clip: true, fillRule: 'evenodd' });
|
|
104
|
+
const clipCall = ctx.calls.find((c) => c.name === 'clip');
|
|
105
|
+
expect(clipCall.args[0]).toBe('evenodd');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('wraps fill in save/restore when alpha < 1', () => {
|
|
109
|
+
const ctx = makeMockCtx();
|
|
110
|
+
new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false, alpha: 0.5 });
|
|
111
|
+
const names = ctx.calls.map((c) => c.name);
|
|
112
|
+
const saveIdx = names.lastIndexOf('save');
|
|
113
|
+
const fillIdx = names.lastIndexOf('fill');
|
|
114
|
+
const restoreIdx = names.lastIndexOf('restore');
|
|
115
|
+
expect(saveIdx).toBeLessThan(fillIdx);
|
|
116
|
+
expect(fillIdx).toBeLessThan(restoreIdx);
|
|
117
|
+
expect(ctx.globalAlpha).toBe(0.5);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('draws highlight halo before normal path when highlighted', () => {
|
|
121
|
+
const ctx = makeMockCtx();
|
|
122
|
+
const renderer = new CanvasRenderer(ctx);
|
|
123
|
+
renderer.setHighlight(true, { r: 255, g: 0, b: 0 }, 3);
|
|
124
|
+
renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }]);
|
|
125
|
+
// Two save/restore pairs: one for the highlight pass, none extra for normal
|
|
126
|
+
const saves = ctx.calls.filter((c) => c.name === 'save');
|
|
127
|
+
const strokes = ctx.calls.filter((c) => c.name === 'stroke');
|
|
128
|
+
expect(saves.length).toBeGreaterThanOrEqual(1);
|
|
129
|
+
expect(strokes.length).toBe(2); // halo stroke + normal stroke
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ─── tracePath ────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
describe('CanvasRenderer — tracePath', () => {
|
|
136
|
+
test('returns early for empty points', () => {
|
|
137
|
+
const ctx = makeMockCtx();
|
|
138
|
+
new CanvasRenderer(ctx).tracePath([]);
|
|
139
|
+
expect(ctx.calls).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('calls moveTo for first point and lineTo for each subsequent straight segment', () => {
|
|
143
|
+
const ctx = makeMockCtx();
|
|
144
|
+
new CanvasRenderer(ctx).tracePath([{ x: 0, y: 0, bulge: 0 }, { x: 5, y: 0, bulge: 0 }, { x: 10, y: 5, bulge: 0 }]);
|
|
145
|
+
expect(ctx.calls[0]).toEqual({ name: 'moveTo', args: [0, 0] });
|
|
146
|
+
expect(ctx.calls[1]).toEqual({ name: 'lineTo', args: [5, 0] });
|
|
147
|
+
expect(ctx.calls[2]).toEqual({ name: 'lineTo', args: [10, 5] });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('calls arc for a bulge segment', () => {
|
|
151
|
+
const ctx = makeMockCtx();
|
|
152
|
+
const p1 = new Point(0, 0, 1); // bulge=1 → semicircle CCW
|
|
153
|
+
const p2 = new Point(10, 0);
|
|
154
|
+
new CanvasRenderer(ctx).tracePath([p1, p2]);
|
|
155
|
+
const arcCall = ctx.calls.find((c) => c.name === 'arc');
|
|
156
|
+
expect(arcCall).toBeDefined();
|
|
157
|
+
expect(arcCall.args).toHaveLength(6);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('passes anticlockwise=true to arc for negative bulge', () => {
|
|
161
|
+
const ctx = makeMockCtx();
|
|
162
|
+
const p1 = new Point(0, 0, -1); // negative bulge → CW in DXF = anticlockwise=true in canvas
|
|
163
|
+
const p2 = new Point(10, 0);
|
|
164
|
+
new CanvasRenderer(ctx).tracePath([p1, p2]);
|
|
165
|
+
const arcCall = ctx.calls.find((c) => c.name === 'arc');
|
|
166
|
+
expect(arcCall.args[5]).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('passes anticlockwise=false to arc for positive bulge', () => {
|
|
170
|
+
const ctx = makeMockCtx();
|
|
171
|
+
const p1 = new Point(0, 0, 1);
|
|
172
|
+
const p2 = new Point(10, 0);
|
|
173
|
+
new CanvasRenderer(ctx).tracePath([p1, p2]);
|
|
174
|
+
const arcCall = ctx.calls.find((c) => c.name === 'arc');
|
|
175
|
+
expect(arcCall.args[5]).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ─── drawSegments ─────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
describe('CanvasRenderer — drawSegments', () => {
|
|
182
|
+
test('returns early for empty segments', () => {
|
|
183
|
+
const ctx = makeMockCtx();
|
|
184
|
+
new CanvasRenderer(ctx).drawSegments([]);
|
|
185
|
+
expect(ctx.calls).toHaveLength(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('calls stroke without setLineDash for solid segments', () => {
|
|
189
|
+
const ctx = makeMockCtx();
|
|
190
|
+
new CanvasRenderer(ctx).drawSegments([{ x1: 0, y1: 0, x2: 10, y2: 0 }]);
|
|
191
|
+
expect(ctx.calls.map((c) => c.name)).not.toContain('setLineDash');
|
|
192
|
+
expect(ctx.calls.map((c) => c.name)).toContain('stroke');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('calls setLineDash per segment when dashes provided', () => {
|
|
196
|
+
const ctx = makeMockCtx();
|
|
197
|
+
new CanvasRenderer(ctx).drawSegments(
|
|
198
|
+
[{ x1: 0, y1: 0, x2: 10, y2: 0, dashPhase: 2 }],
|
|
199
|
+
[5, 3],
|
|
200
|
+
);
|
|
201
|
+
const dashCall = ctx.calls.find((c) => c.name === 'setLineDash');
|
|
202
|
+
expect(dashCall).toBeDefined();
|
|
203
|
+
expect(dashCall.args[0]).toEqual([5, 3]);
|
|
204
|
+
expect(ctx.lineDashOffset).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('draws highlight halo before normal pass when highlighted', () => {
|
|
208
|
+
const ctx = makeMockCtx();
|
|
209
|
+
const renderer = new CanvasRenderer(ctx);
|
|
210
|
+
renderer.setHighlight(true, { r: 0, g: 255, b: 0 }, 2);
|
|
211
|
+
renderer.drawSegments([{ x1: 0, y1: 0, x2: 10, y2: 0 }]);
|
|
212
|
+
const strokes = ctx.calls.filter((c) => c.name === 'stroke');
|
|
213
|
+
expect(strokes.length).toBe(2);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ─── drawText ─────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe('CanvasRenderer — drawText', () => {
|
|
220
|
+
test('returns early for null characters', () => {
|
|
221
|
+
const ctx = makeMockCtx();
|
|
222
|
+
new CanvasRenderer(ctx).drawText(null, 'Arial', 12);
|
|
223
|
+
expect(ctx.calls).toHaveLength(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('returns early for empty characters array', () => {
|
|
227
|
+
const ctx = makeMockCtx();
|
|
228
|
+
new CanvasRenderer(ctx).drawText([], 'Arial', 12);
|
|
229
|
+
expect(ctx.calls).toHaveLength(0);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('sets ctx.font from height and fontName', () => {
|
|
233
|
+
const ctx = makeMockCtx();
|
|
234
|
+
new CanvasRenderer(ctx).drawText([{ x: 0, y: 0, char: 'A' }], 'Courier', 14);
|
|
235
|
+
expect(ctx.font).toBe('14pt Courier');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('calls translate, scale(1,-1), fillText for each character', () => {
|
|
239
|
+
const ctx = makeMockCtx();
|
|
240
|
+
new CanvasRenderer(ctx).drawText([{ x: 5, y: 10, char: 'X' }], 'Arial', 12);
|
|
241
|
+
const translateCall = ctx.calls.find((c) => c.name === 'translate');
|
|
242
|
+
const scaleCall = ctx.calls.find((c) => c.name === 'scale');
|
|
243
|
+
const fillTextCall = ctx.calls.find((c) => c.name === 'fillText');
|
|
244
|
+
expect(translateCall.args).toEqual([5, 10]);
|
|
245
|
+
expect(scaleCall.args).toEqual([1, -1]);
|
|
246
|
+
expect(fillTextCall.args).toEqual(['X', 0, 0]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('calls rotate with negated rotation when rotation is set', () => {
|
|
250
|
+
const ctx = makeMockCtx();
|
|
251
|
+
new CanvasRenderer(ctx).drawText([{ x: 0, y: 0, char: 'A', rotation: Math.PI / 4 }], 'Arial', 12);
|
|
252
|
+
const rotateCall = ctx.calls.find((c) => c.name === 'rotate');
|
|
253
|
+
expect(rotateCall).toBeDefined();
|
|
254
|
+
expect(rotateCall.args[0]).toBeCloseTo(-Math.PI / 4);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('uses strokeText for highlight pass', () => {
|
|
258
|
+
const ctx = makeMockCtx();
|
|
259
|
+
const renderer = new CanvasRenderer(ctx);
|
|
260
|
+
renderer.setHighlight(true, { r: 255, g: 0, b: 0 }, 2);
|
|
261
|
+
renderer.drawText([{ x: 0, y: 0, char: 'A' }], 'Arial', 12);
|
|
262
|
+
expect(ctx.calls.map((c) => c.name)).toContain('strokeText');
|
|
263
|
+
expect(ctx.calls.map((c) => c.name)).toContain('fillText');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ─── state setters ────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
describe('CanvasRenderer — state setters', () => {
|
|
270
|
+
test('setColour sets strokeStyle and fillStyle', () => {
|
|
271
|
+
const ctx = makeMockCtx();
|
|
272
|
+
const renderer = new CanvasRenderer(ctx);
|
|
273
|
+
renderer.setColour({ r: 255, g: 0, b: 0 });
|
|
274
|
+
expect(ctx.strokeStyle).toBeTruthy();
|
|
275
|
+
expect(ctx.fillStyle).toBeTruthy();
|
|
276
|
+
expect(ctx.strokeStyle).toBe(ctx.fillStyle);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('setLineWidth sets ctx.lineWidth and is reflected in next drawShape', () => {
|
|
280
|
+
const ctx = makeMockCtx();
|
|
281
|
+
const renderer = new CanvasRenderer(ctx);
|
|
282
|
+
renderer.setLineWidth(4);
|
|
283
|
+
expect(ctx.lineWidth).toBe(4);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('setDash sets ctx.setLineDash and lineDashOffset', () => {
|
|
287
|
+
const ctx = makeMockCtx();
|
|
288
|
+
const renderer = new CanvasRenderer(ctx);
|
|
289
|
+
renderer.setDash([4, 2], 1);
|
|
290
|
+
const dashCall = ctx.calls.find((c) => c.name === 'setLineDash');
|
|
291
|
+
expect(dashCall.args[0]).toEqual([4, 2]);
|
|
292
|
+
expect(ctx.lineDashOffset).toBe(1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('save and restore delegate to ctx', () => {
|
|
296
|
+
const ctx = makeMockCtx();
|
|
297
|
+
const renderer = new CanvasRenderer(ctx);
|
|
298
|
+
renderer.save();
|
|
299
|
+
renderer.restore();
|
|
300
|
+
expect(ctx.calls.map((c) => c.name)).toEqual(['save', 'restore']);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ─── measureText / measureCharWidth ──────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
describe('CanvasRenderer — measurement', () => {
|
|
307
|
+
test('measureText delegates to ctx.measureText', () => {
|
|
308
|
+
const ctx = makeMockCtx();
|
|
309
|
+
const result = new CanvasRenderer(ctx).measureText('hello', 'Arial', 12);
|
|
310
|
+
expect(result).toEqual({ width: 40 }); // 5 chars × 8
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('measureCharWidth returns width from ctx.measureText', () => {
|
|
314
|
+
const ctx = makeMockCtx();
|
|
315
|
+
expect(new CanvasRenderer(ctx).measureCharWidth('A')).toBe(8);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -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), center=(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 center=(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
|
+
});
|