altium-toolkit 0.1.1 → 0.1.16

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 (54) hide show
  1. package/README.md +24 -6
  2. package/docs/api.md +42 -4
  3. package/docs/model-format.md +95 -5
  4. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
  5. package/docs/testing.md +7 -2
  6. package/package.json +6 -2
  7. package/spec/library-scope.md +7 -1
  8. package/src/core/altium/AltiumParser.mjs +22 -325
  9. package/src/core/altium/NormalizedModelSchema.mjs +28 -0
  10. package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
  11. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
  12. package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
  13. package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
  14. package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
  15. package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
  16. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
  17. package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
  18. package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
  19. package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
  20. package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
  21. package/src/core/altium/PcbLibModelParser.mjs +202 -0
  22. package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
  23. package/src/core/altium/PcbModelParser.mjs +618 -66
  24. package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
  25. package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
  26. package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
  27. package/src/core/altium/PcbPadStackParser.mjs +903 -0
  28. package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
  29. package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
  30. package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
  31. package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
  32. package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
  33. package/src/core/altium/PcbRuleParser.mjs +587 -0
  34. package/src/core/altium/PcbStreamExtractor.mjs +127 -4
  35. package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
  36. package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
  37. package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
  38. package/src/core/altium/PcbViaStackParser.mjs +548 -0
  39. package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
  40. package/src/core/altium/PrjPcbModelParser.mjs +797 -0
  41. package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
  42. package/src/parser.mjs +13 -0
  43. package/src/renderers.mjs +5 -0
  44. package/src/styles/altium-renderers.css +11 -6
  45. package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
  46. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
  47. package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
  48. package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
  49. package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
  50. package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
  51. package/src/ui/PcbSvgRenderer.mjs +101 -109
  52. package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
  53. package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
  54. package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
@@ -0,0 +1,252 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
+
7
+ /**
8
+ * Renders recovered PCB text primitives.
9
+ */
10
+ export class PcbTextPrimitiveRenderer {
11
+ /**
12
+ * Selects texts that belong to the requested board-side composite.
13
+ * @param {{ layerId: number, name: string }[]} primitiveLayers
14
+ * @param {{ text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, visible?: boolean }[]} texts
15
+ * @param {'top' | 'bottom'} [side]
16
+ * @returns {{ text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, visible?: boolean }[]}
17
+ */
18
+ static select(primitiveLayers, texts, side = 'top') {
19
+ const layerIds = PcbTextPrimitiveRenderer.#resolveLayerIds(
20
+ primitiveLayers || [],
21
+ side
22
+ )
23
+
24
+ return (texts || []).filter((text) => {
25
+ const layerId = Number(text?.layerId)
26
+ return (
27
+ text?.visible !== false &&
28
+ String(text?.text || '').trim() &&
29
+ !PcbTextPrimitiveRenderer.#isPlaceholderText(text) &&
30
+ Number.isInteger(layerId) &&
31
+ layerIds.has(layerId)
32
+ )
33
+ })
34
+ }
35
+
36
+ /**
37
+ * Renders selected PCB texts into SVG markup.
38
+ * @param {{ text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string }[]} texts
39
+ * @returns {string}
40
+ */
41
+ static render(texts) {
42
+ return (texts || [])
43
+ .map((text) => PcbTextPrimitiveRenderer.#renderText(text))
44
+ .join('')
45
+ }
46
+
47
+ /**
48
+ * Renders one PCB text primitive.
49
+ * @param {{ text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string }} text
50
+ * @returns {string}
51
+ */
52
+ static #renderText(text) {
53
+ const fontSize = Math.max(Number(text.height || 0), 8)
54
+ const rotation = Number(text.rotation || 0)
55
+ const lines = String(text.text || '')
56
+ .replace(/\r\n?/gu, '\n')
57
+ .split('\n')
58
+ .filter((line) => line.length > 0)
59
+ const content = lines.length
60
+ ? PcbTextPrimitiveRenderer.#renderTextLines(lines, fontSize)
61
+ : SchematicSvgUtils.escapeHtml(String(text.text || ''))
62
+
63
+ return (
64
+ '<text class="pcb-text pcb-text--layer-' +
65
+ SchematicSvgUtils.escapeHtml(String(Number(text.layerId || 0))) +
66
+ '" transform="translate(' +
67
+ SchematicSvgUtils.formatNumber(Number(text.x || 0)) +
68
+ ' ' +
69
+ SchematicSvgUtils.formatNumber(Number(text.y || 0)) +
70
+ ') rotate(' +
71
+ SchematicSvgUtils.formatNumber(rotation) +
72
+ ')" font-size="' +
73
+ SchematicSvgUtils.formatNumber(fontSize) +
74
+ '"' +
75
+ PcbTextPrimitiveRenderer.#renderFontAttributes(text) +
76
+ ' text-anchor="start" dominant-baseline="alphabetic">' +
77
+ content +
78
+ '</text>'
79
+ )
80
+ }
81
+
82
+ /**
83
+ * Renders optional SVG font attributes for TrueType text primitives.
84
+ * @param {{ fontFamily?: string, fontWeight?: number, fontStyle?: string }} text
85
+ * @returns {string}
86
+ */
87
+ static #renderFontAttributes(text) {
88
+ let attributes = ''
89
+
90
+ if (text.fontFamily && text.fontFamily !== 'Stroke') {
91
+ attributes +=
92
+ ' font-family="' +
93
+ SchematicSvgUtils.escapeHtml(text.fontFamily) +
94
+ '"'
95
+ }
96
+
97
+ if (text.fontWeight) {
98
+ attributes +=
99
+ ' font-weight="' +
100
+ SchematicSvgUtils.escapeHtml(String(text.fontWeight)) +
101
+ '"'
102
+ }
103
+
104
+ if (text.fontStyle && text.fontStyle !== 'normal') {
105
+ attributes +=
106
+ ' font-style="' +
107
+ SchematicSvgUtils.escapeHtml(text.fontStyle) +
108
+ '"'
109
+ }
110
+
111
+ return attributes
112
+ }
113
+
114
+ /**
115
+ * Renders one or more text lines with SVG tspans.
116
+ * @param {string[]} lines
117
+ * @param {number} fontSize
118
+ * @returns {string}
119
+ */
120
+ static #renderTextLines(lines, fontSize) {
121
+ if (lines.length === 1) {
122
+ return SchematicSvgUtils.escapeHtml(lines[0])
123
+ }
124
+
125
+ return lines
126
+ .map(
127
+ (line, index) =>
128
+ '<tspan x="0" dy="' +
129
+ SchematicSvgUtils.formatNumber(index === 0 ? 0 : fontSize) +
130
+ '">' +
131
+ SchematicSvgUtils.escapeHtml(line) +
132
+ '</tspan>'
133
+ )
134
+ .join('')
135
+ }
136
+
137
+ /**
138
+ * Resolves candidate text layer ids from layer names, falling back to
139
+ * standard Altium layer ids when legacy layer metadata is absent.
140
+ * @param {{ layerId: number, name: string }[]} primitiveLayers
141
+ * @param {'top' | 'bottom'} side
142
+ * @returns {Set<number>}
143
+ */
144
+ static #resolveLayerIds(primitiveLayers, side) {
145
+ const matchers = PcbTextPrimitiveRenderer.#resolveLayerMatchers(side)
146
+ const layerIds = new Set(
147
+ primitiveLayers
148
+ .filter((layer) =>
149
+ matchers.some((matchesLayerName) =>
150
+ matchesLayerName(layer.name)
151
+ )
152
+ )
153
+ .map((layer) => Number(layer.layerId))
154
+ .filter((layerId) => Number.isInteger(layerId))
155
+ )
156
+
157
+ if (layerIds.size) {
158
+ return layerIds
159
+ }
160
+
161
+ return new Set(
162
+ side === 'bottom' ? [32, 34, 36, 38, 73] : [1, 33, 35, 37, 73]
163
+ )
164
+ }
165
+
166
+ /**
167
+ * Resolves side-specific layer-name matchers.
168
+ * @param {'top' | 'bottom'} side
169
+ * @returns {((layerName: string) => boolean)[]}
170
+ */
171
+ static #resolveLayerMatchers(side) {
172
+ if (side === 'bottom') {
173
+ return [
174
+ (layerName) =>
175
+ PcbTextPrimitiveRenderer.#includesLayerName(
176
+ layerName,
177
+ 'BOTTOM OVERLAY'
178
+ ),
179
+ (layerName) =>
180
+ PcbTextPrimitiveRenderer.#includesLayerName(
181
+ layerName,
182
+ 'BOTTOM SOLDER'
183
+ ),
184
+ (layerName) =>
185
+ PcbTextPrimitiveRenderer.#includesLayerName(
186
+ layerName,
187
+ 'BOTTOM PASTE'
188
+ ),
189
+ (layerName) =>
190
+ PcbTextPrimitiveRenderer.#includesLayerName(
191
+ layerName,
192
+ 'L4_BOT'
193
+ ),
194
+ (layerName) =>
195
+ PcbTextPrimitiveRenderer.#includesLayerName(
196
+ layerName,
197
+ 'DRILL DRAWING'
198
+ )
199
+ ]
200
+ }
201
+
202
+ return [
203
+ (layerName) =>
204
+ PcbTextPrimitiveRenderer.#includesLayerName(
205
+ layerName,
206
+ 'TOP OVERLAY'
207
+ ),
208
+ (layerName) =>
209
+ PcbTextPrimitiveRenderer.#includesLayerName(
210
+ layerName,
211
+ 'TOP SOLDER'
212
+ ),
213
+ (layerName) =>
214
+ PcbTextPrimitiveRenderer.#includesLayerName(
215
+ layerName,
216
+ 'TOP PASTE'
217
+ ),
218
+ (layerName) =>
219
+ PcbTextPrimitiveRenderer.#includesLayerName(
220
+ layerName,
221
+ 'L1_TOP'
222
+ ),
223
+ (layerName) =>
224
+ PcbTextPrimitiveRenderer.#includesLayerName(
225
+ layerName,
226
+ 'DRILL DRAWING'
227
+ )
228
+ ]
229
+ }
230
+
231
+ /**
232
+ * Returns true when a layer name contains the target token.
233
+ * @param {string} layerName
234
+ * @param {string} needle
235
+ * @returns {boolean}
236
+ */
237
+ static #includesLayerName(layerName, needle) {
238
+ return String(layerName || '')
239
+ .trim()
240
+ .toUpperCase()
241
+ .includes(needle)
242
+ }
243
+
244
+ /**
245
+ * Returns true for unresolved Altium component annotation placeholders.
246
+ * @param {{ isPlaceholder?: boolean }} text
247
+ * @returns {boolean}
248
+ */
249
+ static #isPlaceholderText(text) {
250
+ return text?.isPlaceholder === true
251
+ }
252
+ }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
6
  import { SchematicColorResolver } from './SchematicColorResolver.mjs'
7
+ import { SchematicSheetZoneRenderer } from './SchematicSheetZoneRenderer.mjs'
7
8
 
8
9
  const {
9
10
  basename,
@@ -27,7 +28,7 @@ export class SchematicSheetChromeRenderer {
27
28
  */
28
29
  static buildMarkup(width, height, sheet, fileName) {
29
30
  const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
30
- let markup = SchematicSheetChromeRenderer.#buildSheetZoneMarkup(
31
+ let markup = SchematicSheetZoneRenderer.buildMarkup(
31
32
  width,
32
33
  height,
33
34
  margin,
@@ -915,98 +916,6 @@ export class SchematicSheetChromeRenderer {
915
916
  }
916
917
  }
917
918
 
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
919
  /**
1011
920
  * Formats the sheet numbering shown in the title block.
1012
921
  * @param {{ sheetNumber?: string, sheetTotal?: string }} titleBlock
@@ -0,0 +1,104 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
+
7
+ const { createSvgText, formatNumber } = SchematicSvgUtils
8
+
9
+ /**
10
+ * Renders schematic border zone labels and separators.
11
+ */
12
+ export class SchematicSheetZoneRenderer {
13
+ /**
14
+ * Builds the border zone labels around the sheet frame.
15
+ * @param {number} width
16
+ * @param {number} height
17
+ * @param {number} margin
18
+ * @param {{ borderOn?: boolean, xZones?: number, yZones?: number }} sheet
19
+ * @returns {string}
20
+ */
21
+ static buildMarkup(width, height, margin, sheet) {
22
+ if (!sheet?.borderOn) return ''
23
+
24
+ const xZones = Math.max(Number(sheet?.xZones || 0), 1)
25
+ const yZones = Math.max(Number(sheet?.yZones || 0), 1)
26
+ const innerWidth = Math.max(width - margin * 2, 10)
27
+ const innerHeight = Math.max(height - margin * 2, 10)
28
+ const separator = (x1, y1, x2, y2) =>
29
+ '<line class="sheet-zone-separator" x1="' +
30
+ formatNumber(x1) +
31
+ '" y1="' +
32
+ formatNumber(y1) +
33
+ '" x2="' +
34
+ formatNumber(x2) +
35
+ '" y2="' +
36
+ formatNumber(y2) +
37
+ '" />'
38
+ let markup = ''
39
+
40
+ for (let index = 1; index < xZones; index += 1) {
41
+ const x = margin + (innerWidth * index) / xZones
42
+
43
+ markup +=
44
+ separator(x, 0, x, margin) +
45
+ separator(x, height - margin, x, height)
46
+ }
47
+
48
+ for (let index = 0; index < xZones; index += 1) {
49
+ const label = String(index + 1)
50
+ const x = margin + (innerWidth * (index + 0.5)) / xZones
51
+
52
+ markup +=
53
+ createSvgText(
54
+ 'sheet-zone-label',
55
+ x,
56
+ margin - 6,
57
+ label,
58
+ 'var(--schematic-text-color)',
59
+ 'middle'
60
+ ) +
61
+ createSvgText(
62
+ 'sheet-zone-label',
63
+ x,
64
+ height - 4,
65
+ label,
66
+ 'var(--schematic-text-color)',
67
+ 'middle'
68
+ )
69
+ }
70
+
71
+ for (let index = 1; index < yZones; index += 1) {
72
+ const y = margin + (innerHeight * index) / yZones
73
+
74
+ markup +=
75
+ separator(0, y, margin, y) +
76
+ separator(width - margin, y, width, y)
77
+ }
78
+
79
+ for (let index = 0; index < yZones; index += 1) {
80
+ const label = String.fromCharCode(65 + index)
81
+ const y = margin + (innerHeight * (index + 0.5)) / yZones
82
+
83
+ markup +=
84
+ createSvgText(
85
+ 'sheet-zone-label',
86
+ 8,
87
+ y + 2,
88
+ label,
89
+ 'var(--schematic-text-color)',
90
+ 'middle'
91
+ ) +
92
+ createSvgText(
93
+ 'sheet-zone-label',
94
+ width - 8,
95
+ y + 2,
96
+ label,
97
+ 'var(--schematic-text-color)',
98
+ 'middle'
99
+ )
100
+ }
101
+
102
+ return markup
103
+ }
104
+ }