@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.
Files changed (145) hide show
  1. package/.eslintrc.json +4 -0
  2. package/TODO.md +49 -6
  3. package/core/core/core.js +0 -3
  4. package/core/dimensions/alignedDimension.js +5 -4
  5. package/core/dimensions/angularDimension.js +6 -5
  6. package/core/dimensions/baseCircularDimension.js +3 -2
  7. package/core/dimensions/baseDimension.js +59 -93
  8. package/core/dimensions/baseLinearDimension.js +6 -5
  9. package/core/dimensions/diametricDimension.js +4 -3
  10. package/core/dimensions/dimension.js +21 -14
  11. package/core/dimensions/radialDimension.js +4 -3
  12. package/core/dimensions/rotatedDimension.js +12 -11
  13. package/core/entities/arc.js +112 -145
  14. package/core/entities/arctext.js +212 -174
  15. package/core/entities/basePolyline.js +30 -282
  16. package/core/entities/circle.js +77 -99
  17. package/core/entities/entity.js +114 -93
  18. package/core/entities/hatch.js +322 -249
  19. package/core/entities/insert.js +66 -81
  20. package/core/entities/line.js +21 -142
  21. package/core/entities/lwpolyline.js +16 -11
  22. package/core/entities/polyline.js +4 -4
  23. package/core/entities/rectangle.js +3 -1
  24. package/core/entities/solid.js +6 -15
  25. package/core/entities/text.js +188 -199
  26. package/core/lib/auxiliary/rubberBand.js +32 -0
  27. package/core/lib/auxiliary/selectionWindow.js +63 -0
  28. package/core/lib/auxiliary/snapPoint.js +113 -0
  29. package/core/lib/auxiliary/trackingLine.js +76 -0
  30. package/core/lib/canvas.js +228 -166
  31. package/core/lib/clipboard.js +7 -1
  32. package/core/lib/colour.js +16 -0
  33. package/core/lib/commandLine.js +1 -6
  34. package/core/lib/commandManager.js +18 -8
  35. package/core/lib/constants.js +28 -0
  36. package/core/lib/dxf/dxf.js +4 -3
  37. package/core/lib/entityManager.js +18 -11
  38. package/core/lib/inputManager.js +35 -11
  39. package/core/lib/intersect.js +25 -0
  40. package/core/lib/patterns.js +8 -1
  41. package/core/lib/plotOptions.js +67 -0
  42. package/core/lib/polylineUtils.js +75 -0
  43. package/core/lib/renderers/cairoRenderer.js +371 -0
  44. package/core/lib/renderers/canvasRenderer.js +315 -0
  45. package/core/lib/renderers/pdfRenderer.js +366 -0
  46. package/core/lib/renderers/rendererBase.js +284 -0
  47. package/core/lib/renderers/svgRenderer.js +343 -0
  48. package/core/lib/scene.js +27 -5
  49. package/core/lib/selectionManager.js +2 -2
  50. package/core/lib/settings.js +4 -2
  51. package/core/lib/snapping.js +4 -191
  52. package/core/lib/stateManager.js +30 -1
  53. package/core/lib/utils.js +21 -2
  54. package/core/properties/entityProperties.js +104 -0
  55. package/core/properties/property.js +106 -2
  56. package/core/properties/propertyManager.js +29 -13
  57. package/core/tables/block.js +4 -37
  58. package/core/tables/endblk.js +1 -1
  59. package/core/tables/layer.js +8 -0
  60. package/core/tables/styleManagerBase.js +3 -2
  61. package/core/tables/tableManagerBase.js +8 -3
  62. package/core/tools/chamfer.js +111 -137
  63. package/core/tools/chamferFilletBase.js +135 -34
  64. package/core/tools/copy.js +3 -3
  65. package/core/tools/explode.js +2 -2
  66. package/core/tools/extend.js +132 -19
  67. package/core/tools/fillet.js +86 -183
  68. package/core/tools/matchProp.js +2 -11
  69. package/core/tools/mirror.js +7 -6
  70. package/core/tools/move.js +3 -3
  71. package/core/tools/offset.js +3 -5
  72. package/core/tools/pasteclip.js +1 -1
  73. package/core/tools/rotate.js +6 -3
  74. package/core/tools/scale.js +1 -1
  75. package/core/tools/trim.js +265 -23
  76. package/core/tools/zoom.js +1 -1
  77. package/jest.config.js +6 -0
  78. package/package.json +5 -3
  79. package/test/dimensions/alignedDimension.test.js +14 -7
  80. package/test/dimensions/angularDimension.test.js +6 -6
  81. package/test/dimensions/baseDimension.test.js +69 -8
  82. package/test/dimensions/baseLinearDimension.test.js +62 -0
  83. package/test/dimensions/diametricDimension.test.js +6 -6
  84. package/test/dimensions/radialDimension.test.js +6 -6
  85. package/test/dimensions/rotatedDimension.test.js +14 -7
  86. package/test/entities/arc.test.js +80 -83
  87. package/test/entities/arctext.test.js +180 -53
  88. package/test/entities/basePolyline.test.js +68 -2
  89. package/test/entities/circle.test.js +57 -85
  90. package/test/entities/entity.test.js +42 -8
  91. package/test/entities/hatch.test.js +543 -70
  92. package/test/entities/insert.test.js +96 -1
  93. package/test/entities/line.test.js +15 -41
  94. package/test/entities/lwpolyline.test.js +104 -0
  95. package/test/entities/polyline.test.js +11 -0
  96. package/test/entities/rectangle.test.js +11 -0
  97. package/test/entities/solid.test.js +1 -1
  98. package/test/entities/text.test.js +294 -69
  99. package/test/integration/exportIntegration.reference.dxf +9 -9
  100. package/test/integration/exportIntegration.test.js +2 -2
  101. package/test/integration/intersectionIntegration.test.js +4 -5
  102. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  103. package/test/integration/pdfRendererIntegration.test.js +61 -0
  104. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  105. package/test/integration/svgRendererIntegration.test.js +61 -0
  106. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  107. package/test/integration/userInputIntegration.test.js +239 -132
  108. package/test/lib/canvas.test.js +742 -123
  109. package/test/lib/constants.test.js +27 -0
  110. package/test/lib/entityManager.test.js +4 -4
  111. package/test/lib/inputManager.test.js +80 -19
  112. package/test/lib/intersect.test.js +83 -0
  113. package/test/lib/plotOptions.test.js +179 -0
  114. package/test/lib/rubberBand.test.js +84 -0
  115. package/test/lib/settings.test.js +5 -6
  116. package/test/lib/snapping.test.js +71 -129
  117. package/test/lib/stateManager.test.js +7 -7
  118. package/test/lib/utils.test.js +36 -2
  119. package/test/properties/entityProperties.test.js +132 -0
  120. package/test/properties/property.test.js +53 -0
  121. package/test/properties/propertyManager.test.js +3 -3
  122. package/test/renderers/canvasRenderer.test.js +317 -0
  123. package/test/renderers/pdfRenderer.test.js +271 -0
  124. package/test/renderers/rendererBase.test.js +213 -0
  125. package/test/renderers/svgRenderer.test.js +326 -0
  126. package/test/tables/block.test.js +31 -1
  127. package/test/tables/endblk.test.js +2 -2
  128. package/test/tables/layer.test.js +30 -0
  129. package/test/test-helpers/hatchGridRenderer.js +180 -0
  130. package/test/test-helpers/test-helpers.js +152 -0
  131. package/test/tools/chamfer.test.js +227 -5
  132. package/test/tools/chamferFilletBase.test.js +98 -9
  133. package/test/tools/copy.test.js +2 -2
  134. package/test/tools/explode.test.js +3 -3
  135. package/test/tools/extend.test.js +331 -0
  136. package/test/tools/fillet.test.js +181 -0
  137. package/test/tools/matchProp.test.js +5 -5
  138. package/test/tools/mirror.test.js +13 -13
  139. package/test/tools/offset.test.js +170 -3
  140. package/test/tools/pasteclip.test.js +5 -5
  141. package/test/tools/rotate.test.js +32 -2
  142. package/test/tools/scale.test.js +5 -5
  143. package/test/tools/trim.test.js +526 -0
  144. package/test/tools/zoom.test.js +7 -7
  145. 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
+ }