@design-core/core 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +4 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/dimensions/alignedDimension.js +5 -4
- package/core/dimensions/angularDimension.js +6 -5
- package/core/dimensions/baseCircularDimension.js +3 -2
- package/core/dimensions/baseDimension.js +59 -93
- package/core/dimensions/baseLinearDimension.js +6 -5
- package/core/dimensions/diametricDimension.js +4 -3
- package/core/dimensions/dimension.js +21 -14
- package/core/dimensions/radialDimension.js +4 -3
- package/core/dimensions/rotatedDimension.js +12 -11
- package/core/entities/arc.js +112 -145
- package/core/entities/arctext.js +212 -174
- package/core/entities/basePolyline.js +30 -282
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +322 -249
- package/core/entities/insert.js +66 -81
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/polyline.js +4 -4
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +188 -199
- package/core/lib/auxiliary/rubberBand.js +32 -0
- package/core/lib/auxiliary/selectionWindow.js +63 -0
- package/core/lib/auxiliary/snapPoint.js +113 -0
- package/core/lib/auxiliary/trackingLine.js +76 -0
- package/core/lib/canvas.js +228 -166
- package/core/lib/clipboard.js +7 -1
- package/core/lib/colour.js +16 -0
- package/core/lib/commandLine.js +1 -6
- package/core/lib/commandManager.js +18 -8
- package/core/lib/constants.js +28 -0
- package/core/lib/dxf/dxf.js +4 -3
- package/core/lib/entityManager.js +18 -11
- package/core/lib/inputManager.js +35 -11
- package/core/lib/intersect.js +25 -0
- package/core/lib/patterns.js +8 -1
- package/core/lib/plotOptions.js +67 -0
- package/core/lib/polylineUtils.js +75 -0
- package/core/lib/renderers/cairoRenderer.js +371 -0
- package/core/lib/renderers/canvasRenderer.js +315 -0
- package/core/lib/renderers/pdfRenderer.js +366 -0
- package/core/lib/renderers/rendererBase.js +284 -0
- package/core/lib/renderers/svgRenderer.js +343 -0
- package/core/lib/scene.js +27 -5
- package/core/lib/selectionManager.js +2 -2
- package/core/lib/settings.js +4 -2
- package/core/lib/snapping.js +4 -191
- package/core/lib/stateManager.js +30 -1
- package/core/lib/utils.js +21 -2
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +29 -13
- package/core/tables/block.js +4 -37
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tools/chamfer.js +111 -137
- package/core/tools/chamferFilletBase.js +135 -34
- package/core/tools/copy.js +3 -3
- package/core/tools/explode.js +2 -2
- package/core/tools/extend.js +132 -19
- package/core/tools/fillet.js +86 -183
- package/core/tools/matchProp.js +2 -11
- package/core/tools/mirror.js +7 -6
- package/core/tools/move.js +3 -3
- package/core/tools/offset.js +3 -5
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/rotate.js +6 -3
- package/core/tools/scale.js +1 -1
- package/core/tools/trim.js +265 -23
- package/core/tools/zoom.js +1 -1
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +14 -7
- package/test/dimensions/angularDimension.test.js +6 -6
- package/test/dimensions/baseDimension.test.js +69 -8
- package/test/dimensions/baseLinearDimension.test.js +62 -0
- package/test/dimensions/diametricDimension.test.js +6 -6
- package/test/dimensions/radialDimension.test.js +6 -6
- package/test/dimensions/rotatedDimension.test.js +14 -7
- package/test/entities/arc.test.js +80 -83
- package/test/entities/arctext.test.js +180 -53
- package/test/entities/basePolyline.test.js +68 -2
- package/test/entities/circle.test.js +57 -85
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +543 -70
- package/test/entities/insert.test.js +96 -1
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +294 -69
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +2 -2
- package/test/integration/intersectionIntegration.test.js +4 -5
- package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
- package/test/integration/pdfRendererIntegration.test.js +61 -0
- package/test/integration/svgRendererIntegration.reference.svg +3343 -0
- package/test/integration/svgRendererIntegration.test.js +61 -0
- package/test/integration/userInputIntegration.reference.dxf +1165 -105
- package/test/integration/userInputIntegration.test.js +239 -132
- package/test/lib/canvas.test.js +742 -123
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +80 -19
- package/test/lib/intersect.test.js +83 -0
- package/test/lib/plotOptions.test.js +179 -0
- package/test/lib/rubberBand.test.js +84 -0
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +71 -129
- package/test/lib/stateManager.test.js +7 -7
- package/test/lib/utils.test.js +36 -2
- package/test/properties/entityProperties.test.js +132 -0
- package/test/properties/property.test.js +53 -0
- package/test/properties/propertyManager.test.js +3 -3
- package/test/renderers/canvasRenderer.test.js +317 -0
- package/test/renderers/pdfRenderer.test.js +271 -0
- package/test/renderers/rendererBase.test.js +213 -0
- package/test/renderers/svgRenderer.test.js +326 -0
- package/test/tables/block.test.js +31 -1
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +152 -0
- package/test/tools/chamfer.test.js +227 -5
- package/test/tools/chamferFilletBase.test.js +98 -9
- package/test/tools/copy.test.js +2 -2
- package/test/tools/explode.test.js +3 -3
- package/test/tools/extend.test.js +331 -0
- package/test/tools/fillet.test.js +181 -0
- package/test/tools/matchProp.test.js +5 -5
- package/test/tools/mirror.test.js +13 -13
- package/test/tools/offset.test.js +170 -3
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/rotate.test.js +32 -2
- package/test/tools/scale.test.js +5 -5
- package/test/tools/trim.test.js +526 -0
- package/test/tools/zoom.test.js +7 -7
- package/core/lib/selectionWindow.js +0 -83
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { RendererBase } from './rendererBase.js';
|
|
2
|
+
import { Text } from '../../entities/text.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure-JS PDF renderer.
|
|
6
|
+
* Implements the RendererBase interface by accumulating a PDF content stream in memory.
|
|
7
|
+
* Call getOutput() to retrieve the complete PDF document as a string.
|
|
8
|
+
*
|
|
9
|
+
* Design notes:
|
|
10
|
+
* - Zero dependencies: uses only standard PDF operators and string concatenation.
|
|
11
|
+
* - Uses /Helvetica (standard Type1 font) — no font embedding required.
|
|
12
|
+
* - Text measurement delegates to Text.getApproximateWidth (Helvetica AFM widths).
|
|
13
|
+
* - No highlight/selection rendering — PDF export shows final drawing geometry only.
|
|
14
|
+
* - setTransform() is a direct passthrough: the caller (export command) must supply
|
|
15
|
+
* a PDF-space Matrix with d = +scale (no Y-flip), so no sign adjustment is needed.
|
|
16
|
+
*
|
|
17
|
+
* // Draw a black line from (100, 100) to (400, 400) on an A4 page (595×842 pt):
|
|
18
|
+
* //
|
|
19
|
+
* // Content stream:
|
|
20
|
+
* // 0 0 0 RG % stroke colour: black (RGB)
|
|
21
|
+
* // 0 0 0 rg % fill colour: black (RGB)
|
|
22
|
+
* // 1 w % line width: 1pt
|
|
23
|
+
* // 1 0 0 1 0 0 cm % identity CTM (no transform)
|
|
24
|
+
* // 100 100 m % moveto start point
|
|
25
|
+
* // 400 400 l % lineto end point
|
|
26
|
+
* // S % stroke path
|
|
27
|
+
* //
|
|
28
|
+
*
|
|
29
|
+
* Example usage:
|
|
30
|
+
* const r = new PdfRenderer(595, 842);
|
|
31
|
+
* r.setColour({ r: 0, g: 0, b: 0 });
|
|
32
|
+
* r.setLineWidth(1);
|
|
33
|
+
* r.drawShape([{ x: 100, y: 100 }, { x: 400, y: 400 }]);
|
|
34
|
+
* const pdf = r.getOutput();
|
|
35
|
+
*/
|
|
36
|
+
export class PdfRenderer extends RendererBase {
|
|
37
|
+
#pageWidth;
|
|
38
|
+
#pageHeight;
|
|
39
|
+
|
|
40
|
+
/** @type {string[]} */
|
|
41
|
+
#stream = [];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {number} pageWidth - page width in PDF user units (points)
|
|
45
|
+
* @param {number} pageHeight - page height in PDF user units (points)
|
|
46
|
+
*/
|
|
47
|
+
constructor(pageWidth, pageHeight) {
|
|
48
|
+
super();
|
|
49
|
+
this.#pageWidth = pageWidth;
|
|
50
|
+
this.#pageHeight = pageHeight;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Frame setup ---
|
|
54
|
+
|
|
55
|
+
/** @inheritdoc */
|
|
56
|
+
setTransform(matrix) {
|
|
57
|
+
// Direct passthrough: the export command supplies a PDF-space matrix with
|
|
58
|
+
// d = +scale (no Y-flip), so no sign adjustment is needed here.
|
|
59
|
+
const { a, d, e, f } = matrix;
|
|
60
|
+
this.#emit(`${this.#fmt(a)} 0 0 ${this.#fmt(d)} ${this.#fmt(e)} ${this.#fmt(f)} cm`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @inheritdoc */
|
|
64
|
+
fillBackground() {
|
|
65
|
+
// No-op: PDF export does not render a background fill.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- High-level drawing ---
|
|
69
|
+
|
|
70
|
+
/** @inheritdoc */
|
|
71
|
+
drawShape(points, options = {}) {
|
|
72
|
+
if (!points.length) return;
|
|
73
|
+
this.tracePath(points);
|
|
74
|
+
this.#applyOptions(options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @inheritdoc */
|
|
78
|
+
drawText(characters, fontName, height) {
|
|
79
|
+
if (!characters || !characters.length) return;
|
|
80
|
+
for (const ch of characters) {
|
|
81
|
+
const r = ch.rotation ?? 0;
|
|
82
|
+
const cos = Math.cos(r);
|
|
83
|
+
const sin = Math.sin(r);
|
|
84
|
+
this.#emit('q');
|
|
85
|
+
// Combined rotate+translate matrix: cos sin -sin cos x y cm.
|
|
86
|
+
// ch.x/ch.y are scene-space coordinates; rotation is CCW positive
|
|
87
|
+
// (no sign compensation needed — global CTM has d = +scale, no Y-flip).
|
|
88
|
+
// TODO: handle ch.upsideDownOffset and ch.backwardsOffset for mirrored text variants.
|
|
89
|
+
this.#emit(`${this.#fmt(cos)} ${this.#fmt(sin)} ${this.#fmt(-sin)} ${this.#fmt(cos)} ${this.#fmt(ch.x)} ${this.#fmt(ch.y)} cm`);
|
|
90
|
+
this.#emit(`BT /F1 ${this.#fmt(height)} Tf 0 0 Td (${this.#escapePdfString(ch.char)}) Tj ET`);
|
|
91
|
+
this.#emit('Q');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @inheritdoc */
|
|
96
|
+
tracePath(points) {
|
|
97
|
+
if (!points.length) return;
|
|
98
|
+
this.#emit(`${this.#fmt(points[0].x)} ${this.#fmt(points[0].y)} m`);
|
|
99
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
100
|
+
const p = points[i];
|
|
101
|
+
const next = points[i + 1];
|
|
102
|
+
if (!p.bulge) {
|
|
103
|
+
this.#emit(`${this.#fmt(next.x)} ${this.#fmt(next.y)} l`);
|
|
104
|
+
} else {
|
|
105
|
+
const center = p.bulgeCentrePoint(next);
|
|
106
|
+
const radius = p.bulgeRadius(next);
|
|
107
|
+
const startAngle = center.angle(p);
|
|
108
|
+
const sweepAngle = p.bulgeAngle();
|
|
109
|
+
for (const curve of this.#arcToBezier(center, radius, startAngle, sweepAngle)) {
|
|
110
|
+
this.#emit(`${this.#fmt(curve.cp1x)} ${this.#fmt(curve.cp1y)} ${this.#fmt(curve.cp2x)} ${this.#fmt(curve.cp2y)} ${this.#fmt(curve.x)} ${this.#fmt(curve.y)} c`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @inheritdoc */
|
|
117
|
+
drawSegments(segments, dashes = []) {
|
|
118
|
+
if (!segments.length) return;
|
|
119
|
+
if (!dashes.length) {
|
|
120
|
+
// Solid: batch all segments into a single stroked path.
|
|
121
|
+
for (const seg of segments) {
|
|
122
|
+
this.tracePath([{ x: seg.x1, y: seg.y1 }, { x: seg.x2, y: seg.y2 }]);
|
|
123
|
+
}
|
|
124
|
+
this.#emit('S');
|
|
125
|
+
} else {
|
|
126
|
+
// Dashed: stroke each segment individually to preserve dash phase continuity.
|
|
127
|
+
for (const seg of segments) {
|
|
128
|
+
const dashStr = dashes.map((v) => this.#fmt(v)).join(' ');
|
|
129
|
+
this.#emit(`[${dashStr}] ${this.#fmt(seg.dashPhase ?? 0)} d`);
|
|
130
|
+
this.tracePath([{ x: seg.x1, y: seg.y1 }, { x: seg.x2, y: seg.y2 }]);
|
|
131
|
+
this.#emit('S');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Low-level path ---
|
|
137
|
+
|
|
138
|
+
/** @inheritdoc */
|
|
139
|
+
beginPath() {
|
|
140
|
+
// No-op: PDF paths start implicitly with the first `m` (moveto) operator.
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @inheritdoc */
|
|
144
|
+
closePath() {
|
|
145
|
+
this.#emit('h');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @inheritdoc */
|
|
149
|
+
stroke() {
|
|
150
|
+
this.#emit('S');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @inheritdoc */
|
|
154
|
+
applyPath(options = {}) {
|
|
155
|
+
this.#applyOptions(options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- State ---
|
|
159
|
+
|
|
160
|
+
/** @inheritdoc */
|
|
161
|
+
setColour(rgb) {
|
|
162
|
+
const { r, g, b } = this.applyStyle(rgb);
|
|
163
|
+
const rf = this.#fmt(r / 255);
|
|
164
|
+
const gf = this.#fmt(g / 255);
|
|
165
|
+
const bf = this.#fmt(b / 255);
|
|
166
|
+
this.#emit(`${rf} ${gf} ${bf} RG`);
|
|
167
|
+
this.#emit(`${rf} ${gf} ${bf} rg`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** @inheritdoc */
|
|
171
|
+
setLineWidth(width) {
|
|
172
|
+
this.#emit(`${this.#fmt(width)} w`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @inheritdoc */
|
|
176
|
+
setDash(array, offset = 0) {
|
|
177
|
+
if (!array.length) {
|
|
178
|
+
this.#emit('[] 0 d');
|
|
179
|
+
} else {
|
|
180
|
+
const vals = array.map((v) => this.#fmt(v)).join(' ');
|
|
181
|
+
this.#emit(`[${vals}] ${this.#fmt(offset)} d`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** @inheritdoc */
|
|
186
|
+
setHighlight() {
|
|
187
|
+
// No-op: PDF export does not render hover/selection highlights.
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- Transform ---
|
|
191
|
+
|
|
192
|
+
/** @inheritdoc */
|
|
193
|
+
applyTransform({ x = 0, y = 0, rotation = 0 } = {}) {
|
|
194
|
+
const cos = Math.cos(rotation);
|
|
195
|
+
const sin = Math.sin(rotation);
|
|
196
|
+
this.#emit(`${this.#fmt(cos)} ${this.#fmt(sin)} ${this.#fmt(-sin)} ${this.#fmt(cos)} ${this.#fmt(x)} ${this.#fmt(y)} cm`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @inheritdoc */
|
|
200
|
+
save() {
|
|
201
|
+
this.#emit('q');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @inheritdoc */
|
|
205
|
+
restore() {
|
|
206
|
+
this.#emit('Q');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Measurement ---
|
|
210
|
+
|
|
211
|
+
/** @inheritdoc */
|
|
212
|
+
measureText(str, fontName, height) {
|
|
213
|
+
return { width: Text.getApproximateWidth(str, height) };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** @inheritdoc */
|
|
217
|
+
measureCharWidth(character) {
|
|
218
|
+
return Text.getApproximateWidth(character, 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- PDF output ---
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Serialise the accumulated content stream into a complete PDF document string.
|
|
225
|
+
* Write to disk via `fs.writeFileSync(path, output, 'utf8')` (Node) or
|
|
226
|
+
* `Gio.File.replace_contents(new TextEncoder().encode(output), ...)` (GJS).
|
|
227
|
+
* @return {string}
|
|
228
|
+
*/
|
|
229
|
+
getOutput() {
|
|
230
|
+
const contentStream = this.#stream.join('\n');
|
|
231
|
+
const streamBytes = contentStream.length; // ASCII content: char count = byte count
|
|
232
|
+
|
|
233
|
+
const objects = [];
|
|
234
|
+
|
|
235
|
+
// 1: Catalog
|
|
236
|
+
objects.push('<< /Type /Catalog /Pages 2 0 R >>');
|
|
237
|
+
|
|
238
|
+
// 2: Pages
|
|
239
|
+
objects.push('<< /Type /Pages /Count 1 /Kids [3 0 R] >>');
|
|
240
|
+
|
|
241
|
+
// 3: Page
|
|
242
|
+
objects.push(`<< /Type /Page /Parent 2 0 R` +
|
|
243
|
+
` /MediaBox [0 0 ${this.#pageWidth} ${this.#pageHeight}]` +
|
|
244
|
+
` /Contents 4 0 R` +
|
|
245
|
+
` /Resources << /Font << /F1 5 0 R >> >> >>`);
|
|
246
|
+
|
|
247
|
+
// 4: Content stream
|
|
248
|
+
objects.push(`<< /Length ${streamBytes} >>\nstream\n${contentStream}\nendstream`);
|
|
249
|
+
|
|
250
|
+
// 5: Font (Helvetica — standard Type1, always available in PDF readers)
|
|
251
|
+
objects.push('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>');
|
|
252
|
+
|
|
253
|
+
// Assemble body and record xref byte offsets
|
|
254
|
+
let body = '%PDF-1.4\n';
|
|
255
|
+
/** @type {number[]} */
|
|
256
|
+
const offsets = [];
|
|
257
|
+
for (let i = 0; i < objects.length; i++) {
|
|
258
|
+
offsets.push(body.length);
|
|
259
|
+
body += `${i + 1} 0 obj\n${objects[i]}\nendobj\n`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Cross-reference table (each entry is exactly 20 bytes: 10+SP+5+SP+1+SP+LF)
|
|
263
|
+
const xrefOffset = body.length;
|
|
264
|
+
let xref = `xref\n0 ${objects.length + 1}\n`;
|
|
265
|
+
xref += '0000000000 65535 f \n';
|
|
266
|
+
for (const offset of offsets) {
|
|
267
|
+
xref += `${String(offset).padStart(10, '0')} 00000 n \n`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const trailer =
|
|
271
|
+
`trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\n` +
|
|
272
|
+
`startxref\n${xrefOffset}\n%%EOF`;
|
|
273
|
+
|
|
274
|
+
return body + xref + trailer;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- Private helpers ---
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Append an operator line to the content stream.
|
|
281
|
+
* @param {string} line
|
|
282
|
+
*/
|
|
283
|
+
#emit(line) {
|
|
284
|
+
this.#stream.push(line);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Apply fill/stroke/close operators to the current path based on drawShape options.
|
|
289
|
+
* Uses even-odd operators (f* / B*) when fillRule is 'evenodd' (e.g. Hatch with holes),
|
|
290
|
+
* and non-zero winding operators (f / B) otherwise.
|
|
291
|
+
* @param {Object} options
|
|
292
|
+
*/
|
|
293
|
+
#applyOptions(options) {
|
|
294
|
+
const { closed = false, stroke = true, fill = false, clip = false, fillRule } = options;
|
|
295
|
+
const evenOdd = fillRule === 'evenodd';
|
|
296
|
+
if (closed) this.#emit('h');
|
|
297
|
+
if (clip) return; // clip is a screen-only operation; no-op in PDF
|
|
298
|
+
if (fill && stroke) {
|
|
299
|
+
this.#emit(evenOdd ? 'B*' : 'B');
|
|
300
|
+
} else if (fill) {
|
|
301
|
+
this.#emit(evenOdd ? 'f*' : 'f');
|
|
302
|
+
} else if (stroke) {
|
|
303
|
+
this.#emit('S');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Format a number for PDF output.
|
|
309
|
+
* Integers are emitted without a decimal point.
|
|
310
|
+
* Floats are rounded to 4 decimal places with trailing zeros stripped.
|
|
311
|
+
* Non-finite values (NaN, Infinity) emit '0' to prevent invalid PDF.
|
|
312
|
+
* @param {number} n
|
|
313
|
+
* @return {string}
|
|
314
|
+
*/
|
|
315
|
+
#fmt(n) {
|
|
316
|
+
if (!Number.isFinite(n)) return '0';
|
|
317
|
+
if (Number.isInteger(n)) return String(n);
|
|
318
|
+
return parseFloat(n.toFixed(4)).toString();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Approximate a circular arc as one or more PDF cubic Bezier curves.
|
|
323
|
+
* Arcs larger than 90° are split into ≤90° segments for accuracy.
|
|
324
|
+
* @param {Point} center - arc centre
|
|
325
|
+
* @param {number} radius - arc radius
|
|
326
|
+
* @param {number} startAngle - angle from positive x-axis to arc start (radians)
|
|
327
|
+
* @param {number} sweepAngle - signed sweep: positive = CCW, negative = CW (radians)
|
|
328
|
+
* @return {Array<{cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number}>}
|
|
329
|
+
*/
|
|
330
|
+
#arcToBezier(center, radius, startAngle, sweepAngle) {
|
|
331
|
+
// Number of ≤90° segments needed to keep Bezier error below ~0.03%
|
|
332
|
+
const steps = Math.max(1, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 2)));
|
|
333
|
+
// Signed angle of each segment (negative for CW arcs)
|
|
334
|
+
const stepAngle = sweepAngle / steps;
|
|
335
|
+
// How far each control point is offset from its arc endpoint along the tangent.
|
|
336
|
+
// Derived from the condition that the cubic Bezier must pass through the arc midpoint
|
|
337
|
+
const tangentScale = (4 / 3) * Math.tan(stepAngle / 4);
|
|
338
|
+
const curves = [];
|
|
339
|
+
for (let i = 0; i < steps; i++) {
|
|
340
|
+
const a1 = startAngle + i * stepAngle;
|
|
341
|
+
const a2 = a1 + stepAngle;
|
|
342
|
+
// Points on the circle at the segment start and end angles
|
|
343
|
+
const p1 = center.project(a1, radius);
|
|
344
|
+
const p2 = center.project(a2, radius);
|
|
345
|
+
// Control points are offset from p1/p2 along the tangent direction by radius*tangentScale.
|
|
346
|
+
// Tangent at p1 (CCW) is a1 + π/2; tangent at p2 (CW, pointing back) is a2 - π/2.
|
|
347
|
+
// tangentScale is signed, so CW arcs (negative sweepAngle) automatically reverse the offset.
|
|
348
|
+
const cp1 = p1.project(a1 + Math.PI / 2, radius * tangentScale);
|
|
349
|
+
const cp2 = p2.project(a2 - Math.PI / 2, radius * tangentScale);
|
|
350
|
+
curves.push({ cp1x: cp1.x, cp1y: cp1.y, cp2x: cp2.x, cp2y: cp2.y, x: p2.x, y: p2.y });
|
|
351
|
+
}
|
|
352
|
+
return curves;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Escape special characters for use inside a PDF string literal ( ... ).
|
|
357
|
+
* @param {string} str
|
|
358
|
+
* @return {string}
|
|
359
|
+
*/
|
|
360
|
+
#escapePdfString(str) {
|
|
361
|
+
return str
|
|
362
|
+
.replace(/\\/g, '\\\\')
|
|
363
|
+
.replace(/\(/g, '\\(')
|
|
364
|
+
.replace(/\)/g, '\\)');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract renderer base class.
|
|
3
|
+
* Defines the interface all render targets must implement.
|
|
4
|
+
*
|
|
5
|
+
* State-setting methods (setColour, setLineWidth, setDash, setHighlight) are
|
|
6
|
+
* called by canvas.js once per entity before entity.draw(renderer) is invoked.
|
|
7
|
+
*
|
|
8
|
+
* High-level drawing methods (drawShape, drawText, tracePath) are called by
|
|
9
|
+
* entity draw() implementations. Concrete renderers handle backend-specific
|
|
10
|
+
* details such as bulge-to-arc decoding, highlight glow passes, and font size
|
|
11
|
+
* correction.
|
|
12
|
+
*
|
|
13
|
+
* Transform helpers (applyTransform, save, restore) are called by canvas.js
|
|
14
|
+
* when processing container entities (Insert, BaseDimension) that return a
|
|
15
|
+
* child list from draw().
|
|
16
|
+
*/
|
|
17
|
+
export class RendererBase {
|
|
18
|
+
#backgroundColour = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Colour-transform functions for plot styles.
|
|
22
|
+
* Each entry maps style name (uppercase) → (rgb) => rgb.
|
|
23
|
+
* Add new entries here to extend the available styles without changing any other code.
|
|
24
|
+
*/
|
|
25
|
+
static Styles = {
|
|
26
|
+
/**
|
|
27
|
+
* No transform — colours are rendered as-is.
|
|
28
|
+
* @param {{ r: number, g: number, b: number }} rgb
|
|
29
|
+
* @return {{ r: number, g: number, b: number }}
|
|
30
|
+
*/
|
|
31
|
+
NONE: (rgb) => rgb,
|
|
32
|
+
/**
|
|
33
|
+
* All colours mapped to black, matching AutoCAD's monochrome.ctb behaviour.
|
|
34
|
+
* @param {{ r: number, g: number, b: number }} _
|
|
35
|
+
* @return {{ r: number, g: number, b: number }}
|
|
36
|
+
*/
|
|
37
|
+
MONOCHROME: (_) => ({ r: 0, g: 0, b: 0 }),
|
|
38
|
+
/**
|
|
39
|
+
* Colours converted to luminance-preserving grey (ITU-R BT.601 coefficients).
|
|
40
|
+
* @param {{ r: number, g: number, b: number }} rgb
|
|
41
|
+
* @return {{ r: number, g: number, b: number }}
|
|
42
|
+
*/
|
|
43
|
+
GREYSCALE: (rgb) => {
|
|
44
|
+
const l = Math.round(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b);
|
|
45
|
+
return { r: l, g: l, b: l };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
#styleTransform = RendererBase.Styles.NONE;
|
|
50
|
+
|
|
51
|
+
// --- Background colour (concrete — used by all renderers for ACI 7 resolution) ---
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Store the background colour for this renderer.
|
|
55
|
+
* Called by canvas.js before painting so that setContext can resolve ACI 7
|
|
56
|
+
* correctly for both screen and export renderers.
|
|
57
|
+
* @param {{ r: number, g: number, b: number }|null} colour
|
|
58
|
+
*/
|
|
59
|
+
setBackgroundColour(colour) {
|
|
60
|
+
this.#backgroundColour = colour;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return the renderer's background colour, or null if not set.
|
|
65
|
+
* @return {{ r: number, g: number, b: number }|null}
|
|
66
|
+
*/
|
|
67
|
+
getBackgroundColour() {
|
|
68
|
+
return this.#backgroundColour;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Plot style (concrete — applied in setColour() by each renderer) ---
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set the active plot style.
|
|
75
|
+
* Pass one of the RendererBase.Styles values (a colour-transform function).
|
|
76
|
+
* Falls back to RendererBase.Styles.NONE if the value is falsy.
|
|
77
|
+
* @param {Function} styleTransform - a function (rgb) => rgb from RendererBase.Styles
|
|
78
|
+
*/
|
|
79
|
+
setStyle(styleTransform) {
|
|
80
|
+
this.#styleTransform = styleTransform ?? RendererBase.Styles.NONE;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Apply the active plot style transform to a resolved colour.
|
|
85
|
+
* Called by each concrete renderer at the start of setColour().
|
|
86
|
+
* @param {{ r: number, g: number, b: number }} rgb
|
|
87
|
+
* @return {{ r: number, g: number, b: number }}
|
|
88
|
+
*/
|
|
89
|
+
applyStyle(rgb) {
|
|
90
|
+
return this.#styleTransform(rgb);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- High-level drawing ---
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Draw a shape described by a sequence of bulge-encoded points.
|
|
97
|
+
* Handles the complete path lifecycle: new path, trace segments, stroke/fill.
|
|
98
|
+
* When isHighlighted is set, a glow pass is prepended automatically.
|
|
99
|
+
* @param {Array} points - Bulge-encoded Point array. Each point's .bulge
|
|
100
|
+
* describes the segment to the next point:
|
|
101
|
+
* 0 = straight line, +ve = CCW arc, -ve = CW arc.
|
|
102
|
+
* @param {Object} [options]
|
|
103
|
+
* @param {boolean} [options.closed=false] - Call closePath before fill/stroke.
|
|
104
|
+
* @param {boolean} [options.stroke=true] - Stroke the path.
|
|
105
|
+
* @param {boolean} [options.fill=false] - Fill the path.
|
|
106
|
+
* @param {string} [options.fillRule] - 'evenodd' or 'nonzero' (default).
|
|
107
|
+
* @param {boolean} [options.clip=false] - Clip to path instead of fill/stroke.
|
|
108
|
+
* @param {number} [options.alpha=1] - Fill opacity (0–1). Only applied to fill, not stroke.
|
|
109
|
+
*/
|
|
110
|
+
drawShape(points, options = {}) {
|
|
111
|
+
throw new Error(`${this.constructor.name}: drawShape() not implemented`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Draw an array of positioned characters.
|
|
116
|
+
* When isHighlighted is set, a glow pass is prepended automatically.
|
|
117
|
+
* @param {Array} characters - Array of { char, x, y, rotation } descriptors.
|
|
118
|
+
* @param {string} fontName - Font family name.
|
|
119
|
+
* @param {number} height - Desired text height in scene units.
|
|
120
|
+
*/
|
|
121
|
+
drawText(characters, fontName, height) {
|
|
122
|
+
throw new Error(`${this.constructor.name}: drawText() not implemented`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Trace a path from bulge-encoded points WITHOUT starting a new path and
|
|
127
|
+
* without stroking or filling. The caller is responsible for starting the
|
|
128
|
+
* path (e.g. via a preceding drawShape or a future beginPath call) so that
|
|
129
|
+
* multiple tracePath calls can build up a compound path — needed by Hatch.
|
|
130
|
+
* @param {Array} points - Bulge-encoded Point array.
|
|
131
|
+
*/
|
|
132
|
+
tracePath(points) {
|
|
133
|
+
throw new Error(`${this.constructor.name}: tracePath() not implemented`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Draw an array of disconnected line segments.
|
|
138
|
+
* When dashes is empty, all segments are batched into a single stroke call.
|
|
139
|
+
* When dashes is non-empty, each segment is stroked individually with its
|
|
140
|
+
* own dash phase so that dash continuity is preserved across clipped boundaries.
|
|
141
|
+
* @param {Array} segments
|
|
142
|
+
* @param {number[]} [dashes=[]]
|
|
143
|
+
*/
|
|
144
|
+
drawSegments(segments, dashes = []) {
|
|
145
|
+
throw new Error(`${this.constructor.name}: drawSegments() not implemented`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Low-level path (used by Hatch compound clip paths) ---
|
|
149
|
+
|
|
150
|
+
/** Start a new path, clearing any previous path. */
|
|
151
|
+
beginPath() {
|
|
152
|
+
throw new Error(`${this.constructor.name}: beginPath() not implemented`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Stroke the current path with the current line style. */
|
|
156
|
+
stroke() {
|
|
157
|
+
throw new Error(`${this.constructor.name}: stroke() not implemented`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Close the current sub-path by drawing a straight line back to its start point. */
|
|
161
|
+
closePath() {
|
|
162
|
+
throw new Error(`${this.constructor.name}: closePath() not implemented`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Apply fill and/or stroke to the current accumulated path without resetting it first.
|
|
167
|
+
* Use after manually building a compound path via beginPath() + tracePath() + closePath()
|
|
168
|
+
* when multiple loops are needed (e.g. solid hatch with island/hole boundaries).
|
|
169
|
+
* @param {Object} [options]
|
|
170
|
+
* @param {boolean} [options.stroke=true]
|
|
171
|
+
* @param {boolean} [options.fill=false]
|
|
172
|
+
* @param {string} [options.fillRule] - 'evenodd' or 'nonzero' (default)
|
|
173
|
+
* @param {number} [options.alpha=1] - fill opacity (0–1)
|
|
174
|
+
*/
|
|
175
|
+
applyPath(options = {}) {
|
|
176
|
+
throw new Error(`${this.constructor.name}: applyPath() not implemented`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- State (called by canvas.js setContext before each entity.draw()) ---
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Set the current drawing colour.
|
|
183
|
+
* @param {{ r: number, g: number, b: number }} rgb
|
|
184
|
+
*/
|
|
185
|
+
setColour(rgb) {
|
|
186
|
+
throw new Error(`${this.constructor.name}: setColour() not implemented`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Set the current line width (already scaled to scene units by canvas.js).
|
|
191
|
+
* @param {number} width
|
|
192
|
+
*/
|
|
193
|
+
setLineWidth(width) {
|
|
194
|
+
throw new Error(`${this.constructor.name}: setLineWidth() not implemented`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Set the current dash pattern.
|
|
199
|
+
* @param {number[]} array
|
|
200
|
+
* @param {number} [offset=0]
|
|
201
|
+
*/
|
|
202
|
+
setDash(array, offset = 0) {
|
|
203
|
+
throw new Error(`${this.constructor.name}: setDash() not implemented`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Configure the highlight (hover/selection glow) state for subsequent draws.
|
|
208
|
+
* When isHighlighted is true, drawShape and drawText prepend a wide stroke
|
|
209
|
+
* pass in highlightColour before the normal pass.
|
|
210
|
+
* @param {boolean} isHighlighted
|
|
211
|
+
* @param {{ r, g, b }|null} [colour]
|
|
212
|
+
* @param {number} [lineWidthDelta=0] - extra width in scene units (pre-scaled by canvas.js).
|
|
213
|
+
*/
|
|
214
|
+
setHighlight(isHighlighted, colour = null, lineWidthDelta = 0) {
|
|
215
|
+
throw new Error(`${this.constructor.name}: setHighlight() not implemented`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Frame setup (called once per paint by canvas.js) ---
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Set the pan/zoom transform matrix on the underlying context.
|
|
222
|
+
* Must be called once at the start of each paint, before any drawing.
|
|
223
|
+
* @param {Matrix} matrix
|
|
224
|
+
*/
|
|
225
|
+
setTransform(matrix) {
|
|
226
|
+
throw new Error(`${this.constructor.name}: setTransform() not implemented`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Paint the scene background rectangle using the colour set by setBackgroundColour().
|
|
231
|
+
* @param {{ x: number, y: number }} origin - scene-space top-left corner
|
|
232
|
+
* @param {number} width - canvas pixel width
|
|
233
|
+
* @param {number} height - canvas pixel height
|
|
234
|
+
* @param {number} scale - current scene scale
|
|
235
|
+
*/
|
|
236
|
+
fillBackground(origin, width, height, scale) {
|
|
237
|
+
throw new Error(`${this.constructor.name}: fillBackground() not implemented`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Transform (called by canvas.js for container entities) ---
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Translate and optionally rotate the current graphics state.
|
|
244
|
+
* @param {Object} transform
|
|
245
|
+
* @param {number} [transform.x=0]
|
|
246
|
+
* @param {number} [transform.y=0]
|
|
247
|
+
* @param {number} [transform.rotation=0]
|
|
248
|
+
*/
|
|
249
|
+
applyTransform(transform) {
|
|
250
|
+
throw new Error(`${this.constructor.name}: applyTransform() not implemented`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Save the current graphics state onto the renderer's stack. */
|
|
254
|
+
save() {
|
|
255
|
+
throw new Error(`${this.constructor.name}: save() not implemented`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Restore the most recently saved graphics state. */
|
|
259
|
+
restore() {
|
|
260
|
+
throw new Error(`${this.constructor.name}: restore() not implemented`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- Measurement (used by text/arctext rendering) ---
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Measure the rendered extent of a string.
|
|
267
|
+
* If fontName and height are supplied the font is set before measuring,
|
|
268
|
+
* so the result reflects the correct face and size.
|
|
269
|
+
* @param {string} str
|
|
270
|
+
* @param {string} [fontName]
|
|
271
|
+
* @param {number} [height]
|
|
272
|
+
*/
|
|
273
|
+
measureText(str, fontName, height) {
|
|
274
|
+
throw new Error(`${this.constructor.name}: measureText() not implemented`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Measure the advance width of a single character.
|
|
279
|
+
* @param {string} character
|
|
280
|
+
*/
|
|
281
|
+
measureCharWidth(character) {
|
|
282
|
+
throw new Error(`${this.constructor.name}: measureCharWidth() not implemented`);
|
|
283
|
+
}
|
|
284
|
+
}
|