altium-toolkit 1.0.8 → 1.0.10

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 (102) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,366 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { unzlibSync } from 'fflate'
6
+ import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
7
+ import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
8
+ import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
9
+
10
+ /**
11
+ * Extracts read-only metadata and bundled source entries from integrated
12
+ * library compound documents.
13
+ */
14
+ export class IntLibStreamExtractor {
15
+ /**
16
+ * Extracts integrated-library data directly from one OLE buffer.
17
+ * @param {ArrayBuffer} arrayBuffer
18
+ * @returns {ReturnType<typeof IntLibStreamExtractor.extractFromStreams> | null}
19
+ */
20
+ static extractFromArrayBuffer(arrayBuffer) {
21
+ const compoundDocument =
22
+ OleCompoundDocument.fromArrayBuffer(arrayBuffer)
23
+ const streams = new Map()
24
+
25
+ for (const name of compoundDocument.listStreams()) {
26
+ streams.set(name, compoundDocument.getStream(name))
27
+ }
28
+
29
+ return IntLibStreamExtractor.extractFromStreams(streams)
30
+ }
31
+
32
+ /**
33
+ * Extracts integrated-library metadata and source payloads from streams.
34
+ * @param {Map<string, Uint8Array>} streams
35
+ * @returns {{ version: string, crossReferences: object[], parameters: Record<string, string>, parameterRecords: object[], sources: object[], streamNames: string[], diagnostics: Record<string, number> }}
36
+ */
37
+ static extractFromStreams(streams) {
38
+ const version = IntLibStreamExtractor.#decodeText(
39
+ streams.get('Version.Txt')
40
+ ).trim()
41
+ const crossReferenceParse = IntLibStreamExtractor.#parseCrossReferences(
42
+ streams.get('LibCrossRef.Txt')
43
+ )
44
+ const crossReferences = crossReferenceParse.records
45
+ const parameterRecords = IntLibStreamExtractor.#parseParameterRecords(
46
+ streams.get('Parameters .bin')
47
+ )
48
+ const parameters =
49
+ IntLibStreamExtractor.#buildParameterMap(parameterRecords)
50
+ const sources = IntLibStreamExtractor.#extractSources(streams)
51
+ const streamNames = IntLibStreamExtractor.#collectUsedStreamNames(
52
+ streams,
53
+ sources
54
+ )
55
+
56
+ return {
57
+ version,
58
+ crossReferences,
59
+ parameters,
60
+ parameterRecords,
61
+ sources,
62
+ streamNames,
63
+ diagnostics: {
64
+ crossReferenceCount: crossReferences.length,
65
+ parameterCount: Object.keys(parameters).length,
66
+ sourceCount: sources.length,
67
+ issues: crossReferenceParse.issues
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Parses LibCrossRef.Txt into field records.
74
+ * @param {Uint8Array | undefined} bytes
75
+ * @returns {{ records: object[], issues: object[] }}
76
+ */
77
+ static #parseCrossReferences(bytes) {
78
+ const records = []
79
+ const issues = []
80
+
81
+ IntLibStreamExtractor.#decodeText(bytes)
82
+ .split(/\r?\n/u)
83
+ .map((line) => line.trim())
84
+ .filter(Boolean)
85
+ .forEach((line, index) => {
86
+ const fields = PcbSidecarRecordParser.parseRecordFields(
87
+ new TextEncoder().encode(line)
88
+ )
89
+ const component = fields.COMPONENT || fields.COMPONENTNAME || ''
90
+ const model = fields.MODEL || fields.MODELNAME || ''
91
+
92
+ if (!Object.keys(fields).length || !component || !model) {
93
+ issues.push(
94
+ IntLibStreamExtractor.#issue(
95
+ 'intlib.crossref.malformed-row',
96
+ 'LibCrossRef.Txt',
97
+ index + 1,
98
+ 'Skipped malformed integrated-library cross-reference row.'
99
+ )
100
+ )
101
+ return
102
+ }
103
+
104
+ records.push({
105
+ component,
106
+ model,
107
+ kind: fields.KIND || fields.TYPE || '',
108
+ fields
109
+ })
110
+ })
111
+
112
+ return { records, issues }
113
+ }
114
+
115
+ /**
116
+ * Builds a structured parser issue.
117
+ * @param {string} code Stable diagnostic code.
118
+ * @param {string} stream Source stream name.
119
+ * @param {number} line One-based line number.
120
+ * @param {string} message User-facing summary.
121
+ * @returns {object}
122
+ */
123
+ static #issue(code, stream, line, message) {
124
+ return {
125
+ code,
126
+ severity: 'warning',
127
+ stream,
128
+ line,
129
+ message
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Parses integrated-library parameter records.
135
+ * @param {Uint8Array | undefined} bytes
136
+ * @returns {object[]}
137
+ */
138
+ static #parseParameterRecords(bytes) {
139
+ const lengthPrefixed =
140
+ PcbSidecarRecordParser.parseLengthPrefixedRecords(
141
+ bytes,
142
+ 'Parameters .bin'
143
+ )
144
+
145
+ if (lengthPrefixed.length) {
146
+ return lengthPrefixed.map((record) => record.fields)
147
+ }
148
+
149
+ return IntLibStreamExtractor.#decodeText(bytes)
150
+ .split(/\r?\n/u)
151
+ .map((line) => line.trim())
152
+ .filter(Boolean)
153
+ .map((line) =>
154
+ PcbSidecarRecordParser.parseRecordFields(
155
+ new TextEncoder().encode(line)
156
+ )
157
+ )
158
+ .filter((fields) => Object.keys(fields).length)
159
+ }
160
+
161
+ /**
162
+ * Builds a parameter lookup from parameter records.
163
+ * @param {Record<string, string>[]} records
164
+ * @returns {Record<string, string>}
165
+ */
166
+ static #buildParameterMap(records) {
167
+ const parameters = {}
168
+
169
+ for (const record of records) {
170
+ const name = record.NAME || record.PARAMETER || ''
171
+ if (!name) {
172
+ continue
173
+ }
174
+
175
+ parameters[name] = record.VALUE || ''
176
+ }
177
+
178
+ return parameters
179
+ }
180
+
181
+ /**
182
+ * Extracts bundled source entries from recognized source-library folders.
183
+ * @param {Map<string, Uint8Array>} streams
184
+ * @returns {object[]}
185
+ */
186
+ static #extractSources(streams) {
187
+ const sources = []
188
+
189
+ for (const [path, bytes] of streams.entries()) {
190
+ const sourceKind = IntLibStreamExtractor.#sourceKind(path)
191
+ if (!sourceKind) {
192
+ continue
193
+ }
194
+
195
+ const payload = IntLibStreamExtractor.#decodePayload(bytes)
196
+ sources.push({
197
+ path,
198
+ fileName: IntLibStreamExtractor.#basename(path),
199
+ fileType: IntLibStreamExtractor.#sourceFileType(path),
200
+ libraryKind: sourceKind,
201
+ compression: payload.compression,
202
+ byteLength: payload.bytes.byteLength,
203
+ payloadBase64: IntLibStreamExtractor.#bytesToBase64(
204
+ payload.bytes
205
+ ),
206
+ payloadText: IntLibStreamExtractor.#decodePrintablePayload(
207
+ payload.bytes
208
+ )
209
+ })
210
+ }
211
+
212
+ return sources.sort((left, right) =>
213
+ left.path.localeCompare(right.path)
214
+ )
215
+ }
216
+
217
+ /**
218
+ * Decodes a possibly wrapped zlib source payload.
219
+ * @param {Uint8Array} bytes
220
+ * @returns {{ bytes: Uint8Array, compression: string }}
221
+ */
222
+ static #decodePayload(bytes) {
223
+ const normalized = PcbSidecarRecordParser.toUint8Array(bytes)
224
+
225
+ if (normalized[0] === 0x02 && normalized[1] === 0x78) {
226
+ return {
227
+ bytes: Uint8Array.from(unzlibSync(normalized.subarray(1))),
228
+ compression: 'zlib-wrapper'
229
+ }
230
+ }
231
+
232
+ if (normalized[0] === 0x78) {
233
+ return {
234
+ bytes: Uint8Array.from(unzlibSync(normalized)),
235
+ compression: 'zlib'
236
+ }
237
+ }
238
+
239
+ return {
240
+ bytes: normalized,
241
+ compression: 'none'
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Decodes text bytes with toolkit printable-text fallbacks.
247
+ * @param {Uint8Array | undefined} bytes
248
+ * @returns {string}
249
+ */
250
+ static #decodeText(bytes) {
251
+ if (!bytes) {
252
+ return ''
253
+ }
254
+
255
+ return PrintableTextDecoder.decodeBytes(
256
+ PcbSidecarRecordParser.toUint8Array(bytes)
257
+ ).replace(/^\uFEFF/u, '')
258
+ }
259
+
260
+ /**
261
+ * Decodes source payload text only when it looks printable.
262
+ * @param {Uint8Array} bytes
263
+ * @returns {string}
264
+ */
265
+ static #decodePrintablePayload(bytes) {
266
+ const text = IntLibStreamExtractor.#decodeText(bytes)
267
+ .replace(/\u0000/gu, '')
268
+ .trim()
269
+ if (!text || /[\u0001-\u0008\u000b\u000c\u000e-\u001f]/u.test(text)) {
270
+ return ''
271
+ }
272
+
273
+ return text
274
+ }
275
+
276
+ /**
277
+ * Resolves the source-library kind for one stream path.
278
+ * @param {string} path
279
+ * @returns {string}
280
+ */
281
+ static #sourceKind(path) {
282
+ const normalized = String(path || '').replace(/\\/gu, '/')
283
+
284
+ if (/^SchLib\//iu.test(normalized)) {
285
+ return 'schematic-symbols'
286
+ }
287
+ if (/^PCBLib\//iu.test(normalized)) {
288
+ return 'pcb-footprints'
289
+ }
290
+ if (/^PCB3DLib\//iu.test(normalized)) {
291
+ return 'pcb-3d-models'
292
+ }
293
+
294
+ return ''
295
+ }
296
+
297
+ /**
298
+ * Resolves the source file type from one stream path.
299
+ * @param {string} path
300
+ * @returns {string}
301
+ */
302
+ static #sourceFileType(path) {
303
+ const extension = IntLibStreamExtractor.#basename(path).split('.').pop()
304
+
305
+ if (/^SchLib$/iu.test(extension)) return 'SchLib'
306
+ if (/^PcbLib$/iu.test(extension)) return 'PcbLib'
307
+ if (/^PCB3DLib$/iu.test(extension)) return 'PCB3DLib'
308
+
309
+ return extension || 'unknown'
310
+ }
311
+
312
+ /**
313
+ * Returns the final path segment.
314
+ * @param {string} path
315
+ * @returns {string}
316
+ */
317
+ static #basename(path) {
318
+ return String(path || '')
319
+ .replace(/\\/gu, '/')
320
+ .split('/')
321
+ .pop()
322
+ }
323
+
324
+ /**
325
+ * Collects streams that contributed to extraction.
326
+ * @param {Map<string, Uint8Array>} streams
327
+ * @param {object[]} sources
328
+ * @returns {string[]}
329
+ */
330
+ static #collectUsedStreamNames(streams, sources) {
331
+ return [
332
+ 'Version.Txt',
333
+ 'LibCrossRef.Txt',
334
+ 'Parameters .bin',
335
+ ...sources.map((source) => source.path)
336
+ ]
337
+ .filter((streamName) => streams.has(streamName))
338
+ .sort((left, right) => left.localeCompare(right))
339
+ }
340
+
341
+ /**
342
+ * Encodes bytes as base64 in browser and Node runtimes.
343
+ * @param {Uint8Array} bytes
344
+ * @returns {string}
345
+ */
346
+ static #bytesToBase64(bytes) {
347
+ if (typeof btoa === 'function') {
348
+ let binary = ''
349
+ const chunkSize = 0x8000
350
+
351
+ for (
352
+ let offset = 0;
353
+ offset < bytes.byteLength;
354
+ offset += chunkSize
355
+ ) {
356
+ binary += String.fromCharCode(
357
+ ...bytes.subarray(offset, offset + chunkSize)
358
+ )
359
+ }
360
+
361
+ return btoa(binary)
362
+ }
363
+
364
+ return Buffer.from(bytes).toString('base64')
365
+ }
366
+ }