altium-toolkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +20 -0
  3. package/CONTRIBUTING.md +19 -0
  4. package/LICENSE +22 -0
  5. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  6. package/LICENSES/GPL-3.0-or-later.txt +232 -0
  7. package/NOTICE.md +32 -0
  8. package/README.md +116 -0
  9. package/docs/api.md +73 -0
  10. package/docs/model-format.md +36 -0
  11. package/docs/testing.md +25 -0
  12. package/examples/README.md +47 -0
  13. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
  14. package/examples/arduino-uno/SvgViewportController.mjs +306 -0
  15. package/examples/arduino-uno/example.mjs +480 -0
  16. package/examples/arduino-uno/index.html +163 -0
  17. package/examples/arduino-uno/styles.css +552 -0
  18. package/examples/server.mjs +212 -0
  19. package/package.json +53 -0
  20. package/spec/library-scope.md +32 -0
  21. package/src/core/BinaryReader.mjs +127 -0
  22. package/src/core/altium/AltiumLayoutParser.mjs +485 -0
  23. package/src/core/altium/AltiumParser.mjs +1007 -0
  24. package/src/core/altium/AsciiRecordParser.mjs +151 -0
  25. package/src/core/altium/ParserUtils.mjs +173 -0
  26. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
  27. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
  28. package/src/core/altium/PcbModelParser.mjs +336 -0
  29. package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
  30. package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
  31. package/src/core/altium/PcbStreamExtractor.mjs +210 -0
  32. package/src/core/altium/PrintableTextDecoder.mjs +156 -0
  33. package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
  34. package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
  35. package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
  36. package/src/core/altium/SchematicImageParser.mjs +173 -0
  37. package/src/core/altium/SchematicJunctionParser.mjs +43 -0
  38. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
  39. package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
  40. package/src/core/altium/SchematicPinParser.mjs +767 -0
  41. package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
  42. package/src/core/altium/SchematicSheetParser.mjs +241 -0
  43. package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
  44. package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
  45. package/src/core/altium/SchematicTextParser.mjs +708 -0
  46. package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
  47. package/src/core/ole/OleCompoundDocument.mjs +439 -0
  48. package/src/core/ole/OleConstants.mjs +64 -0
  49. package/src/core/ole/OleDirectoryEntry.mjs +95 -0
  50. package/src/index.mjs +7 -0
  51. package/src/parser.mjs +21 -0
  52. package/src/renderers.mjs +15 -0
  53. package/src/scene3d.mjs +9 -0
  54. package/src/styles/altium-renderers.css +358 -0
  55. package/src/ui/BomTableRenderer.mjs +46 -0
  56. package/src/ui/PcbArcUtils.mjs +189 -0
  57. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
  58. package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
  59. package/src/ui/PcbScene3dBuilder.mjs +742 -0
  60. package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
  61. package/src/ui/PcbScene3dPackages.mjs +137 -0
  62. package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
  63. package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
  64. package/src/ui/PcbSvgRenderer.mjs +906 -0
  65. package/src/ui/SchematicColorResolver.mjs +132 -0
  66. package/src/ui/SchematicContentLayout.mjs +661 -0
  67. package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
  68. package/src/ui/SchematicImageRenderer.mjs +135 -0
  69. package/src/ui/SchematicJunctionRenderer.mjs +381 -0
  70. package/src/ui/SchematicNoteRenderer.mjs +427 -0
  71. package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
  72. package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
  73. package/src/ui/SchematicPortRenderer.mjs +558 -0
  74. package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
  75. package/src/ui/SchematicRegionRenderer.mjs +94 -0
  76. package/src/ui/SchematicShapeRenderer.mjs +398 -0
  77. package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
  78. package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +756 -0
  80. package/src/ui/SchematicSvgUtils.mjs +182 -0
  81. package/src/ui/SchematicTypography.mjs +204 -0
  82. package/src/workers/altium-parser.worker.mjs +29 -0
@@ -0,0 +1,1025 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
+ import { SchematicColorResolver } from './SchematicColorResolver.mjs'
7
+
8
+ const {
9
+ basename,
10
+ buildCurrentDateValue,
11
+ createSvgText,
12
+ formatNumber,
13
+ projectSchematicY
14
+ } = SchematicSvgUtils
15
+
16
+ /**
17
+ * Renders synthesized sheet border, zones, and title-block chrome.
18
+ */
19
+ export class SchematicSheetChromeRenderer {
20
+ /**
21
+ * Builds page border and title-block chrome from sheet metadata.
22
+ * @param {number} width
23
+ * @param {number} height
24
+ * @param {{ borderOn?: boolean, titleBlockOn?: boolean, marginWidth?: number, paperSize?: string, sourceWidth?: number, xZones?: number, yZones?: number, titleBlock?: { title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string, footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>> } }} sheet
25
+ * @param {string | undefined} fileName
26
+ * @returns {string}
27
+ */
28
+ static buildMarkup(width, height, sheet, fileName) {
29
+ const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
30
+ let markup = SchematicSheetChromeRenderer.#buildSheetZoneMarkup(
31
+ width,
32
+ height,
33
+ margin,
34
+ sheet
35
+ )
36
+
37
+ if (sheet?.borderOn) {
38
+ markup +=
39
+ '<rect class="sheet-frame" x="' +
40
+ formatNumber(margin) +
41
+ '" y="' +
42
+ formatNumber(margin) +
43
+ '" width="' +
44
+ formatNumber(Math.max(width - margin * 2, 10)) +
45
+ '" height="' +
46
+ formatNumber(Math.max(height - margin * 2, 10)) +
47
+ '" />'
48
+ }
49
+
50
+ if (!sheet?.titleBlockOn) {
51
+ return markup
52
+ }
53
+
54
+ const titleBlock = sheet?.titleBlock || {}
55
+ const resolvedTitleBlock =
56
+ SchematicSheetChromeRenderer.#resolveRenderedTitleBlock(
57
+ width,
58
+ sheet,
59
+ titleBlock
60
+ )
61
+ const titleBlockLayout =
62
+ SchematicSheetChromeRenderer.#resolveSheetTitleBlockLayout(
63
+ width,
64
+ height,
65
+ margin,
66
+ resolvedTitleBlock
67
+ )
68
+ const renderedFileName = basename(fileName)
69
+ const renderedDate = resolvedTitleBlock.date || buildCurrentDateValue()
70
+
71
+ const titleBlockMarkup =
72
+ SchematicSheetChromeRenderer.#shouldUseHintedStandardLayout(
73
+ sheet,
74
+ resolvedTitleBlock
75
+ )
76
+ ? SchematicSheetChromeRenderer.#buildHintedStandardTitleBlockMarkup(
77
+ titleBlockLayout,
78
+ sheet,
79
+ height,
80
+ resolvedTitleBlock,
81
+ renderedDate,
82
+ renderedFileName
83
+ )
84
+ : SchematicSheetChromeRenderer.#buildGenericTitleBlockMarkup(
85
+ titleBlockLayout,
86
+ sheet,
87
+ height,
88
+ resolvedTitleBlock,
89
+ renderedDate,
90
+ renderedFileName
91
+ )
92
+
93
+ return markup + titleBlockMarkup
94
+ }
95
+
96
+ /**
97
+ * Resolves the synthesized title-block bounds, using recovered footer
98
+ * value hints when the source file exposes them.
99
+ * @param {number} width
100
+ * @param {{ sourceWidth?: number }} sheet
101
+ * @param {{ title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string, footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>> }} titleBlock
102
+ * @returns {{ title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string, footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>> }}
103
+ */
104
+ static #resolveRenderedTitleBlock(width, sheet, titleBlock) {
105
+ const footerHints = titleBlock?.footerHints
106
+ const sourceWidth = Number(sheet?.sourceWidth || 0)
107
+
108
+ if (!footerHints || !sourceWidth || width <= sourceWidth) {
109
+ return titleBlock
110
+ }
111
+
112
+ const maxHintX = Math.max(
113
+ ...Object.values(footerHints).map((hint) => Number(hint?.x || 0))
114
+ )
115
+
116
+ if (maxHintX > sourceWidth) {
117
+ return titleBlock
118
+ }
119
+
120
+ const footerOffsetX = width - sourceWidth
121
+
122
+ return {
123
+ ...titleBlock,
124
+ footerHints: Object.fromEntries(
125
+ Object.entries(footerHints).map(([key, hint]) => [
126
+ key,
127
+ hint
128
+ ? {
129
+ ...hint,
130
+ x: Number(hint.x || 0) + footerOffsetX
131
+ }
132
+ : hint
133
+ ])
134
+ )
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Resolves the synthesized title-block bounds, using recovered footer
140
+ * value hints when the source file exposes them.
141
+ * @param {number} width
142
+ * @param {number} height
143
+ * @param {number} margin
144
+ * @param {{ footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number }>> }} titleBlock
145
+ * @returns {{ x: number, y: number, width: number, height: number }}
146
+ */
147
+ static #resolveSheetTitleBlockLayout(width, height, margin, titleBlock) {
148
+ const defaultWidth = Math.min(
149
+ Math.max(width - margin * 2, 100),
150
+ Math.max(Math.min(480, width * 0.34), 140)
151
+ )
152
+ const defaultHeight = Math.min(
153
+ Math.max(height - margin * 2, 100),
154
+ Math.max(Math.min(138, height * 0.18), 102)
155
+ )
156
+ const footerHints = Object.values(titleBlock?.footerHints || {})
157
+
158
+ if (footerHints.length < 3) {
159
+ return {
160
+ x: width - margin - defaultWidth,
161
+ y: height - margin - defaultHeight,
162
+ width: defaultWidth,
163
+ height: defaultHeight
164
+ }
165
+ }
166
+
167
+ const minX = Math.min(...footerHints.map((hint) => Number(hint.x || 0)))
168
+ const maxX = Math.max(...footerHints.map((hint) => Number(hint.x || 0)))
169
+ const maxY = Math.max(...footerHints.map((hint) => Number(hint.y || 0)))
170
+ const x = Math.max(minX - 120, margin)
171
+ const titleBlockWidth = Math.max(width - margin - x, 280)
172
+ const topDocY = Math.max(maxY + 18, margin + 52)
173
+ const titleBlockHeight = Math.max(topDocY - margin, 72)
174
+
175
+ return {
176
+ x,
177
+ y: projectSchematicY(height, topDocY),
178
+ width: titleBlockWidth,
179
+ height: titleBlockHeight
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Returns true when the recovered footer hints expose enough of the
185
+ * standard footer to rebuild the corrected compact footer chrome.
186
+ * @param {{ paperSize?: string } | undefined} sheet
187
+ * @param {{ footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number }>> }} titleBlock
188
+ * @returns {boolean}
189
+ */
190
+ static #shouldUseHintedStandardLayout(sheet, titleBlock) {
191
+ const footerHints = titleBlock?.footerHints || {}
192
+
193
+ return Boolean(
194
+ footerHints.title &&
195
+ footerHints.revision &&
196
+ (footerHints.documentNumber ||
197
+ footerHints.sheetNumber ||
198
+ footerHints.sheetTotal)
199
+ )
200
+ }
201
+
202
+ /**
203
+ * Builds the corrected compact title-block chrome from recovered footer
204
+ * hints.
205
+ * @param {{ x: number, y: number, width: number, height: number }} layout
206
+ * @param {{ paperSize?: string }} sheet
207
+ * @param {number} sheetHeight
208
+ * @param {{ title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>> }} titleBlock
209
+ * @param {string} renderedDate
210
+ * @param {string} renderedFileName
211
+ * @returns {string}
212
+ */
213
+ static #buildHintedStandardTitleBlockMarkup(
214
+ layout,
215
+ sheet,
216
+ sheetHeight,
217
+ titleBlock,
218
+ renderedDate,
219
+ renderedFileName
220
+ ) {
221
+ const x = layout.x
222
+ const y = layout.y
223
+ const titleBlockWidth = layout.width
224
+ const titleBlockHeight = layout.height
225
+ const titleLabelY = y + titleBlockHeight * 0.16
226
+ const titleValueY = y + titleBlockHeight * 0.48
227
+ const topRowBottomY = y + titleBlockHeight * 0.5
228
+ const middleRowBottomY = y + titleBlockHeight * 0.75
229
+ const footerRowDividerY = y + titleBlockHeight * 0.875
230
+ const secondRowHeight = middleRowBottomY - topRowBottomY
231
+ const footerTopRowHeight = footerRowDividerY - middleRowBottomY
232
+ const footerBottomRowHeight = y + titleBlockHeight - footerRowDividerY
233
+ const sectionLabelY = topRowBottomY + secondRowHeight * 0.25
234
+ const sectionValueY = topRowBottomY + secondRowHeight * 0.625
235
+ const paperSizeY = topRowBottomY + secondRowHeight * 0.75
236
+ const footerDateY = middleRowBottomY + footerTopRowHeight * 0.65
237
+ const footerFileY = footerRowDividerY + footerBottomRowHeight * 0.65
238
+ const topDividerX = x + titleBlockWidth * 0.67
239
+ const sizeDividerX = x + titleBlockWidth * 0.16
240
+ const revisionDividerX = x + titleBlockWidth * 0.72
241
+ const bottomRightDividerX = x + titleBlockWidth * 0.57
242
+ const labelOptions =
243
+ SchematicSheetChromeRenderer.#resolveTitleBlockLabelOptions(
244
+ titleBlock
245
+ )
246
+ const footerValueOptions =
247
+ SchematicSheetChromeRenderer.#resolveTitleBlockFooterValueOptions(
248
+ titleBlock
249
+ )
250
+ const useLiteralSheetHints =
251
+ String(sheet?.paperSize || '')
252
+ .trim()
253
+ .toUpperCase() === 'A3'
254
+ const sheetTotalHint = useLiteralSheetHints
255
+ ? titleBlock?.footerHints?.sheetTotal
256
+ : undefined
257
+
258
+ return (
259
+ '<g class="sheet-title-block">' +
260
+ '<rect x="' +
261
+ formatNumber(x) +
262
+ '" y="' +
263
+ formatNumber(y) +
264
+ '" width="' +
265
+ formatNumber(titleBlockWidth) +
266
+ '" height="' +
267
+ formatNumber(titleBlockHeight) +
268
+ '" />' +
269
+ '<line x1="' +
270
+ formatNumber(x) +
271
+ '" y1="' +
272
+ formatNumber(topRowBottomY) +
273
+ '" x2="' +
274
+ formatNumber(x + titleBlockWidth) +
275
+ '" y2="' +
276
+ formatNumber(topRowBottomY) +
277
+ '" />' +
278
+ '<line x1="' +
279
+ formatNumber(x) +
280
+ '" y1="' +
281
+ formatNumber(middleRowBottomY) +
282
+ '" x2="' +
283
+ formatNumber(x + titleBlockWidth) +
284
+ '" y2="' +
285
+ formatNumber(middleRowBottomY) +
286
+ '" />' +
287
+ '<line x1="' +
288
+ formatNumber(x) +
289
+ '" y1="' +
290
+ formatNumber(footerRowDividerY) +
291
+ '" x2="' +
292
+ formatNumber(x + titleBlockWidth) +
293
+ '" y2="' +
294
+ formatNumber(footerRowDividerY) +
295
+ '" />' +
296
+ '<line x1="' +
297
+ formatNumber(topDividerX) +
298
+ '" y1="' +
299
+ formatNumber(y) +
300
+ '" x2="' +
301
+ formatNumber(topDividerX) +
302
+ '" y2="' +
303
+ formatNumber(topRowBottomY) +
304
+ '" />' +
305
+ '<line x1="' +
306
+ formatNumber(sizeDividerX) +
307
+ '" y1="' +
308
+ formatNumber(topRowBottomY) +
309
+ '" x2="' +
310
+ formatNumber(sizeDividerX) +
311
+ '" y2="' +
312
+ formatNumber(middleRowBottomY) +
313
+ '" />' +
314
+ '<line x1="' +
315
+ formatNumber(revisionDividerX) +
316
+ '" y1="' +
317
+ formatNumber(topRowBottomY) +
318
+ '" x2="' +
319
+ formatNumber(revisionDividerX) +
320
+ '" y2="' +
321
+ formatNumber(middleRowBottomY) +
322
+ '" />' +
323
+ '<line x1="' +
324
+ formatNumber(bottomRightDividerX) +
325
+ '" y1="' +
326
+ formatNumber(middleRowBottomY) +
327
+ '" x2="' +
328
+ formatNumber(bottomRightDividerX) +
329
+ '" y2="' +
330
+ formatNumber(y + titleBlockHeight) +
331
+ '" />' +
332
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
333
+ x + titleBlockWidth * 0.03,
334
+ titleLabelY,
335
+ 'Title',
336
+ labelOptions
337
+ ) +
338
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
339
+ x + titleBlockWidth * 0.05,
340
+ sectionLabelY,
341
+ 'Size',
342
+ labelOptions
343
+ ) +
344
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
345
+ sizeDividerX + 12,
346
+ sectionLabelY,
347
+ 'Number',
348
+ labelOptions
349
+ ) +
350
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
351
+ revisionDividerX + 8,
352
+ sectionLabelY,
353
+ 'Revision',
354
+ labelOptions
355
+ ) +
356
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
357
+ x + 8,
358
+ footerDateY,
359
+ 'Date:',
360
+ labelOptions
361
+ ) +
362
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
363
+ x + 8,
364
+ footerFileY,
365
+ 'File:',
366
+ labelOptions
367
+ ) +
368
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
369
+ bottomRightDividerX + 8,
370
+ footerDateY,
371
+ 'Sheet',
372
+ labelOptions
373
+ ) +
374
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
375
+ sheetTotalHint
376
+ ? sheetTotalHint.x - 20
377
+ : bottomRightDividerX + titleBlockWidth * 0.28,
378
+ footerDateY,
379
+ 'of',
380
+ labelOptions
381
+ ) +
382
+ SchematicSheetChromeRenderer.#buildTitleBlockLabelMarkup(
383
+ bottomRightDividerX + 8,
384
+ footerFileY,
385
+ 'Drawn By:',
386
+ labelOptions
387
+ ) +
388
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkup(
389
+ x + titleBlockWidth * 0.31,
390
+ titleValueY,
391
+ titleBlock.title || '',
392
+ 'var(--schematic-default-ink-color)',
393
+ titleBlock.footerHints?.title,
394
+ sheetHeight,
395
+ useLiteralSheetHints
396
+ ) +
397
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkup(
398
+ x + titleBlockWidth * 0.84,
399
+ titleValueY,
400
+ titleBlock.documentNumber || '',
401
+ 'var(--schematic-text-color)',
402
+ titleBlock.footerHints?.documentNumber,
403
+ sheetHeight,
404
+ useLiteralSheetHints
405
+ ) +
406
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkupWithResolvedY(
407
+ x + titleBlockWidth * 0.93,
408
+ sectionValueY,
409
+ titleBlock.revision || '',
410
+ 'var(--schematic-default-ink-color)',
411
+ titleBlock.footerHints?.revision,
412
+ sheetHeight,
413
+ useLiteralSheetHints
414
+ ) +
415
+ createSvgText(
416
+ 'sheet-title-value',
417
+ x + titleBlockWidth * 0.08,
418
+ paperSizeY,
419
+ sheet?.paperSize || 'A4',
420
+ 'var(--schematic-text-color)',
421
+ 'middle',
422
+ footerValueOptions
423
+ ) +
424
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkupWithResolvedY(
425
+ x + titleBlockWidth * 0.8,
426
+ footerDateY,
427
+ titleBlock.sheetNumber || '',
428
+ 'var(--schematic-default-ink-color)',
429
+ useLiteralSheetHints
430
+ ? titleBlock.footerHints?.sheetNumber
431
+ : undefined,
432
+ sheetHeight
433
+ ) +
434
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkupWithResolvedY(
435
+ x + titleBlockWidth * 0.88,
436
+ footerDateY,
437
+ titleBlock.sheetTotal || '',
438
+ 'var(--schematic-default-ink-color)',
439
+ useLiteralSheetHints
440
+ ? titleBlock.footerHints?.sheetTotal
441
+ : undefined,
442
+ sheetHeight
443
+ ) +
444
+ createSvgText(
445
+ 'sheet-title-value',
446
+ x + titleBlockWidth * 0.24,
447
+ footerDateY,
448
+ renderedDate,
449
+ 'var(--schematic-text-color)',
450
+ 'start',
451
+ footerValueOptions
452
+ ) +
453
+ createSvgText(
454
+ 'sheet-title-value',
455
+ x + titleBlockWidth * 0.24,
456
+ footerFileY,
457
+ renderedFileName,
458
+ 'var(--schematic-text-color)',
459
+ 'start',
460
+ footerValueOptions
461
+ ) +
462
+ createSvgText(
463
+ 'sheet-title-value',
464
+ x + titleBlockWidth * 0.93,
465
+ footerFileY,
466
+ titleBlock.drawnBy || '',
467
+ 'var(--schematic-default-ink-color)',
468
+ 'middle',
469
+ footerValueOptions
470
+ ) +
471
+ '</g>'
472
+ )
473
+ }
474
+
475
+ /**
476
+ * Builds the generic fallback title-block chrome used when no corrected
477
+ * footer-hint layout is available.
478
+ * @param {{ x: number, y: number, width: number, height: number }} layout
479
+ * @param {{ paperSize?: string }} sheet
480
+ * @param {number} sheetHeight
481
+ * @param {{ title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>> }} titleBlock
482
+ * @param {string} renderedDate
483
+ * @param {string} renderedFileName
484
+ * @returns {string}
485
+ */
486
+ static #buildGenericTitleBlockMarkup(
487
+ layout,
488
+ sheet,
489
+ sheetHeight,
490
+ titleBlock,
491
+ renderedDate,
492
+ renderedFileName
493
+ ) {
494
+ const titleBlockWidth = layout.width
495
+ const titleBlockHeight = layout.height
496
+ const x = layout.x
497
+ const y = layout.y
498
+ const headerY = y + titleBlockHeight * 0.16
499
+ const titleRowY = y + titleBlockHeight * 0.48
500
+ const labelRowY = y + titleBlockHeight * 0.62
501
+ const valueRowY = y + titleBlockHeight * 0.78
502
+ const footerDateY = y + titleBlockHeight * 0.9
503
+ const footerFileY = y + titleBlockHeight * 0.98
504
+ const line1Y = y + titleBlockHeight * 0.18
505
+ const line2Y = y + titleBlockHeight * 0.5
506
+ const line3Y = y + titleBlockHeight * 0.66
507
+ const line4Y = y + titleBlockHeight * 0.82
508
+ const numberX = x + titleBlockWidth * 0.64
509
+ const revisionX = x + titleBlockWidth * 0.84
510
+ const sizeX = x + titleBlockWidth * 0.16
511
+ const sheetX = x + titleBlockWidth * 0.67
512
+ const drawnByX = x + titleBlockWidth * 0.82
513
+ const sheetValue =
514
+ SchematicSheetChromeRenderer.#buildSheetValue(titleBlock)
515
+ const sheetValueHint =
516
+ SchematicSheetChromeRenderer.#buildSheetValueFooterHint(titleBlock)
517
+
518
+ return (
519
+ '<g class="sheet-title-block">' +
520
+ '<rect x="' +
521
+ formatNumber(x) +
522
+ '" y="' +
523
+ formatNumber(y) +
524
+ '" width="' +
525
+ formatNumber(titleBlockWidth) +
526
+ '" height="' +
527
+ formatNumber(titleBlockHeight) +
528
+ '" />' +
529
+ '<line x1="' +
530
+ formatNumber(x) +
531
+ '" y1="' +
532
+ formatNumber(line1Y) +
533
+ '" x2="' +
534
+ formatNumber(x + titleBlockWidth) +
535
+ '" y2="' +
536
+ formatNumber(line1Y) +
537
+ '" />' +
538
+ '<line x1="' +
539
+ formatNumber(x) +
540
+ '" y1="' +
541
+ formatNumber(line2Y) +
542
+ '" x2="' +
543
+ formatNumber(x + titleBlockWidth) +
544
+ '" y2="' +
545
+ formatNumber(line2Y) +
546
+ '" />' +
547
+ '<line x1="' +
548
+ formatNumber(x) +
549
+ '" y1="' +
550
+ formatNumber(line3Y) +
551
+ '" x2="' +
552
+ formatNumber(x + titleBlockWidth) +
553
+ '" y2="' +
554
+ formatNumber(line3Y) +
555
+ '" />' +
556
+ '<line x1="' +
557
+ formatNumber(x) +
558
+ '" y1="' +
559
+ formatNumber(line4Y) +
560
+ '" x2="' +
561
+ formatNumber(x + titleBlockWidth) +
562
+ '" y2="' +
563
+ formatNumber(line4Y) +
564
+ '" />' +
565
+ '<line x1="' +
566
+ formatNumber(numberX) +
567
+ '" y1="' +
568
+ formatNumber(y) +
569
+ '" x2="' +
570
+ formatNumber(numberX) +
571
+ '" y2="' +
572
+ formatNumber(line2Y) +
573
+ '" />' +
574
+ '<line x1="' +
575
+ formatNumber(revisionX) +
576
+ '" y1="' +
577
+ formatNumber(y) +
578
+ '" x2="' +
579
+ formatNumber(revisionX) +
580
+ '" y2="' +
581
+ formatNumber(line2Y) +
582
+ '" />' +
583
+ '<line x1="' +
584
+ formatNumber(sizeX) +
585
+ '" y1="' +
586
+ formatNumber(line2Y) +
587
+ '" x2="' +
588
+ formatNumber(sizeX) +
589
+ '" y2="' +
590
+ formatNumber(y + titleBlockHeight) +
591
+ '" />' +
592
+ '<line x1="' +
593
+ formatNumber(sheetX) +
594
+ '" y1="' +
595
+ formatNumber(line2Y) +
596
+ '" x2="' +
597
+ formatNumber(sheetX) +
598
+ '" y2="' +
599
+ formatNumber(line4Y) +
600
+ '" />' +
601
+ '<line x1="' +
602
+ formatNumber(drawnByX) +
603
+ '" y1="' +
604
+ formatNumber(line4Y) +
605
+ '" x2="' +
606
+ formatNumber(drawnByX) +
607
+ '" y2="' +
608
+ formatNumber(y + titleBlockHeight) +
609
+ '" />' +
610
+ createSvgText(
611
+ 'sheet-title-label',
612
+ x + titleBlockWidth * 0.03,
613
+ headerY,
614
+ 'Title',
615
+ 'var(--schematic-sheet-label-color)',
616
+ 'start'
617
+ ) +
618
+ createSvgText(
619
+ 'sheet-title-label',
620
+ numberX + titleBlockWidth * 0.03,
621
+ headerY,
622
+ 'Number',
623
+ 'var(--schematic-sheet-label-color)',
624
+ 'start'
625
+ ) +
626
+ createSvgText(
627
+ 'sheet-title-label',
628
+ revisionX + titleBlockWidth * 0.02,
629
+ headerY,
630
+ 'Revision',
631
+ 'var(--schematic-sheet-label-color)',
632
+ 'start'
633
+ ) +
634
+ createSvgText(
635
+ 'sheet-title-label',
636
+ x + titleBlockWidth * 0.05,
637
+ labelRowY,
638
+ 'Size',
639
+ 'var(--schematic-sheet-label-color)',
640
+ 'start'
641
+ ) +
642
+ createSvgText(
643
+ 'sheet-title-label',
644
+ sizeX + titleBlockWidth * 0.05,
645
+ labelRowY,
646
+ 'Sheet',
647
+ 'var(--schematic-sheet-label-color)',
648
+ 'start'
649
+ ) +
650
+ createSvgText(
651
+ 'sheet-title-label',
652
+ sizeX + 8,
653
+ footerDateY,
654
+ 'Date:',
655
+ 'var(--schematic-sheet-label-color)',
656
+ 'start'
657
+ ) +
658
+ createSvgText(
659
+ 'sheet-title-label',
660
+ sizeX + 8,
661
+ footerFileY,
662
+ 'File:',
663
+ 'var(--schematic-sheet-label-color)',
664
+ 'start'
665
+ ) +
666
+ createSvgText(
667
+ 'sheet-title-label',
668
+ drawnByX + 8,
669
+ footerFileY,
670
+ 'Drawn By:',
671
+ 'var(--schematic-sheet-label-color)',
672
+ 'start'
673
+ ) +
674
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkup(
675
+ x + titleBlockWidth * 0.31,
676
+ titleRowY,
677
+ titleBlock.title || '',
678
+ 'var(--schematic-default-ink-color)',
679
+ titleBlock.footerHints?.title,
680
+ sheetHeight
681
+ ) +
682
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkup(
683
+ x + titleBlockWidth * 0.74,
684
+ titleRowY,
685
+ titleBlock.documentNumber || '',
686
+ 'var(--schematic-text-color)',
687
+ titleBlock.footerHints?.documentNumber,
688
+ sheetHeight
689
+ ) +
690
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkup(
691
+ x + titleBlockWidth * 0.92,
692
+ titleRowY,
693
+ titleBlock.revision || '',
694
+ 'var(--schematic-default-ink-color)',
695
+ titleBlock.footerHints?.revision,
696
+ sheetHeight
697
+ ) +
698
+ createSvgText(
699
+ 'sheet-title-value',
700
+ x + titleBlockWidth * 0.08,
701
+ valueRowY,
702
+ sheet?.paperSize || 'A4',
703
+ 'var(--schematic-text-color)',
704
+ 'middle'
705
+ ) +
706
+ SchematicSheetChromeRenderer.#buildTitleBlockValueMarkup(
707
+ x + titleBlockWidth * 0.415,
708
+ valueRowY,
709
+ sheetValue,
710
+ 'var(--schematic-default-ink-color)',
711
+ sheetValueHint,
712
+ sheetHeight
713
+ ) +
714
+ createSvgText(
715
+ 'sheet-title-value',
716
+ sizeX + titleBlockWidth * 0.08,
717
+ footerDateY,
718
+ renderedDate,
719
+ 'var(--schematic-text-color)',
720
+ 'start'
721
+ ) +
722
+ createSvgText(
723
+ 'sheet-title-value',
724
+ sizeX + titleBlockWidth * 0.08,
725
+ footerFileY,
726
+ renderedFileName,
727
+ 'var(--schematic-text-color)',
728
+ 'start'
729
+ ) +
730
+ createSvgText(
731
+ 'sheet-title-value',
732
+ x + titleBlockWidth * 0.93,
733
+ footerFileY,
734
+ titleBlock.drawnBy || '',
735
+ 'var(--schematic-default-ink-color)',
736
+ 'middle'
737
+ ) +
738
+ '</g>'
739
+ )
740
+ }
741
+
742
+ /**
743
+ * Resolves serif label typography for corrected title-block labels.
744
+ * @param {{ footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision', { fontFamily: string }>> }} titleBlock
745
+ * @returns {{ fontSize: number, fontFamily: string, fontWeight: number }}
746
+ */
747
+ static #resolveTitleBlockLabelOptions(titleBlock) {
748
+ return {
749
+ fontSize: 10,
750
+ fontFamily:
751
+ titleBlock?.footerHints?.revision?.fontFamily ||
752
+ titleBlock?.footerHints?.title?.fontFamily ||
753
+ 'Times New Roman',
754
+ fontWeight: 400
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Resolves default serif typography for synthesized footer values that do
760
+ * not have their own recovered hint styling.
761
+ * @param {{ footerHints?: Partial<Record<'title' | 'documentNumber' | 'revision', { fontFamily: string }>> }} titleBlock
762
+ * @returns {{ fontSize: number, fontFamily: string, fontWeight: number }}
763
+ */
764
+ static #resolveTitleBlockFooterValueOptions(titleBlock) {
765
+ return {
766
+ fontSize: 10,
767
+ fontFamily:
768
+ titleBlock?.footerHints?.revision?.fontFamily ||
769
+ titleBlock?.footerHints?.title?.fontFamily ||
770
+ 'Times New Roman',
771
+ fontWeight: 400
772
+ }
773
+ }
774
+
775
+ /**
776
+ * Builds one title-block label with explicit typography so the SVG does
777
+ * not inherit the viewer stylesheet sans-serif fallback.
778
+ * @param {number} x
779
+ * @param {number} y
780
+ * @param {string} text
781
+ * @param {{ fontSize: number, fontFamily: string, fontWeight: number }} options
782
+ * @returns {string}
783
+ */
784
+ static #buildTitleBlockLabelMarkup(x, y, text, options) {
785
+ return createSvgText(
786
+ 'sheet-title-label',
787
+ x,
788
+ y,
789
+ text,
790
+ 'var(--schematic-sheet-label-color)',
791
+ 'start',
792
+ options
793
+ )
794
+ }
795
+ /**
796
+ * Builds one title-block value while preserving recovered X/color/font
797
+ * hints and overriding the baseline to match the synthesized cell layout.
798
+ * @param {number} fallbackX
799
+ * @param {number} resolvedY
800
+ * @param {string} text
801
+ * @param {string} fallbackColor
802
+ * @param {{ x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number } | undefined} footerHint
803
+ * @param {number} sheetHeight
804
+ * @param {boolean} [preserveHintX=true]
805
+ * @returns {string}
806
+ */
807
+ static #buildTitleBlockValueMarkupWithResolvedY(
808
+ fallbackX,
809
+ resolvedY,
810
+ text,
811
+ fallbackColor,
812
+ footerHint,
813
+ sheetHeight,
814
+ preserveHintX = true
815
+ ) {
816
+ if (!footerHint) {
817
+ return createSvgText(
818
+ 'sheet-title-value',
819
+ fallbackX,
820
+ resolvedY,
821
+ text,
822
+ fallbackColor,
823
+ 'middle'
824
+ )
825
+ }
826
+
827
+ return createSvgText(
828
+ 'sheet-title-value',
829
+ preserveHintX ? footerHint.x : fallbackX,
830
+ resolvedY,
831
+ text,
832
+ SchematicColorResolver.resolveColor(
833
+ footerHint.color,
834
+ fallbackColor.replace(/^var\((.+)\)$/, '$1')
835
+ ),
836
+ 'middle',
837
+ {
838
+ fontSize: footerHint.fontSize,
839
+ fontFamily: footerHint.fontFamily,
840
+ fontWeight: footerHint.fontWeight
841
+ }
842
+ )
843
+ }
844
+
845
+ /**
846
+ * Builds one title-block value, preferring recovered footer hint
847
+ * placement, color, and typography when available.
848
+ * @param {number} fallbackX
849
+ * @param {number} fallbackY
850
+ * @param {string} text
851
+ * @param {string} fallbackColor
852
+ * @param {{ x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number } | undefined} footerHint
853
+ * @param {number} sheetHeight
854
+ * @param {boolean} [preserveHintX=true]
855
+ * @returns {string}
856
+ */
857
+ static #buildTitleBlockValueMarkup(
858
+ fallbackX,
859
+ fallbackY,
860
+ text,
861
+ fallbackColor,
862
+ footerHint,
863
+ sheetHeight,
864
+ preserveHintX = true
865
+ ) {
866
+ if (!footerHint) {
867
+ return createSvgText(
868
+ 'sheet-title-value',
869
+ fallbackX,
870
+ fallbackY,
871
+ text,
872
+ fallbackColor,
873
+ 'middle'
874
+ )
875
+ }
876
+
877
+ return createSvgText(
878
+ 'sheet-title-value',
879
+ preserveHintX ? footerHint.x : fallbackX,
880
+ projectSchematicY(sheetHeight, footerHint.y),
881
+ text,
882
+ SchematicColorResolver.resolveColor(
883
+ footerHint.color,
884
+ fallbackColor.replace(/^var\((.+)\)$/, '$1')
885
+ ),
886
+ 'middle',
887
+ {
888
+ fontSize: footerHint.fontSize,
889
+ fontFamily: footerHint.fontFamily,
890
+ fontWeight: footerHint.fontWeight
891
+ }
892
+ )
893
+ }
894
+
895
+ /**
896
+ * Builds one combined sheet-value hint from the recovered sheet-number row.
897
+ * @param {{ footerHints?: Partial<Record<'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>> }} titleBlock
898
+ * @returns {{ x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number } | undefined}
899
+ */
900
+ static #buildSheetValueFooterHint(titleBlock) {
901
+ const sheetNumberHint = titleBlock?.footerHints?.sheetNumber
902
+ const sheetTotalHint = titleBlock?.footerHints?.sheetTotal
903
+
904
+ if (!sheetNumberHint || !sheetTotalHint) {
905
+ return undefined
906
+ }
907
+
908
+ return {
909
+ x: (sheetNumberHint.x + sheetTotalHint.x) / 2,
910
+ y: Math.max(sheetNumberHint.y, sheetTotalHint.y),
911
+ color: sheetNumberHint.color,
912
+ fontSize: sheetNumberHint.fontSize,
913
+ fontFamily: sheetNumberHint.fontFamily,
914
+ fontWeight: sheetNumberHint.fontWeight
915
+ }
916
+ }
917
+
918
+ /**
919
+ * Builds the border zone labels around the sheet frame.
920
+ * @param {number} width
921
+ * @param {number} height
922
+ * @param {number} margin
923
+ * @param {{ borderOn?: boolean, xZones?: number, yZones?: number }} sheet
924
+ * @returns {string}
925
+ */
926
+ static #buildSheetZoneMarkup(width, height, margin, sheet) {
927
+ if (!sheet?.borderOn) return ''
928
+
929
+ const xZones = Math.max(Number(sheet?.xZones || 0), 1)
930
+ const yZones = Math.max(Number(sheet?.yZones || 0), 1)
931
+ const innerWidth = Math.max(width - margin * 2, 10)
932
+ const innerHeight = Math.max(height - margin * 2, 10)
933
+ const separator = (x1, y1, x2, y2) =>
934
+ '<line class="sheet-zone-separator" x1="' +
935
+ formatNumber(x1) +
936
+ '" y1="' +
937
+ formatNumber(y1) +
938
+ '" x2="' +
939
+ formatNumber(x2) +
940
+ '" y2="' +
941
+ formatNumber(y2) +
942
+ '" />'
943
+ let markup = ''
944
+
945
+ for (let index = 1; index < xZones; index += 1) {
946
+ const x = margin + (innerWidth * index) / xZones
947
+
948
+ markup +=
949
+ separator(x, 0, x, margin) +
950
+ separator(x, height - margin, x, height)
951
+ }
952
+
953
+ for (let index = 0; index < xZones; index += 1) {
954
+ const label = String(index + 1)
955
+ const x = margin + (innerWidth * (index + 0.5)) / xZones
956
+
957
+ markup +=
958
+ createSvgText(
959
+ 'sheet-zone-label',
960
+ x,
961
+ margin - 6,
962
+ label,
963
+ 'var(--schematic-text-color)',
964
+ 'middle'
965
+ ) +
966
+ createSvgText(
967
+ 'sheet-zone-label',
968
+ x,
969
+ height - 4,
970
+ label,
971
+ 'var(--schematic-text-color)',
972
+ 'middle'
973
+ )
974
+ }
975
+
976
+ for (let index = 1; index < yZones; index += 1) {
977
+ const y = margin + (innerHeight * index) / yZones
978
+
979
+ markup +=
980
+ separator(0, y, margin, y) +
981
+ separator(width - margin, y, width, y)
982
+ }
983
+
984
+ for (let index = 0; index < yZones; index += 1) {
985
+ const label = String.fromCharCode(65 + index)
986
+ const y = margin + (innerHeight * (index + 0.5)) / yZones
987
+
988
+ markup +=
989
+ createSvgText(
990
+ 'sheet-zone-label',
991
+ 8,
992
+ y + 2,
993
+ label,
994
+ 'var(--schematic-text-color)',
995
+ 'middle'
996
+ ) +
997
+ createSvgText(
998
+ 'sheet-zone-label',
999
+ width - 8,
1000
+ y + 2,
1001
+ label,
1002
+ 'var(--schematic-text-color)',
1003
+ 'middle'
1004
+ )
1005
+ }
1006
+
1007
+ return markup
1008
+ }
1009
+
1010
+ /**
1011
+ * Formats the sheet numbering shown in the title block.
1012
+ * @param {{ sheetNumber?: string, sheetTotal?: string }} titleBlock
1013
+ * @returns {string}
1014
+ */
1015
+ static #buildSheetValue(titleBlock) {
1016
+ const sheetNumber = String(titleBlock?.sheetNumber || '').trim()
1017
+ const sheetTotal = String(titleBlock?.sheetTotal || '').trim()
1018
+
1019
+ if (sheetNumber && sheetTotal) {
1020
+ return 'Sheet ' + sheetNumber + ' of ' + sheetTotal
1021
+ }
1022
+
1023
+ return sheetNumber || sheetTotal || ''
1024
+ }
1025
+ }