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
@@ -18,7 +18,7 @@ export class PcbScene3dBuilder {
18
18
  static #DENSE_OVERLAY_MIN_REGION_AREA_RATIO = 0.2
19
19
  static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
20
20
  static #DENSE_OVERLAY_KNOCKOUT_COLOR = 0x2f6a2c
21
- static #PRECISE_BODY_MATCH_TOLERANCE_MIL = 5
21
+ static #PRECISE_BODY_MATCH_TOLERANCE_MIL = 20
22
22
  static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
23
23
  static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
24
24
  static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
@@ -27,8 +27,8 @@ export class PcbScene3dBuilder {
27
27
  /**
28
28
  * Builds a scene description for host 3D renderers.
29
29
  * @param {{ pcb?: { boardOutline?: { widthMil?: number, heightMil?: number, minX?: number, minY?: number, segments?: Array<Record<string, number | string>> }, primitiveLayers?: { layerId: number, name: string }[], pads?: { x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[], tracks?: any[], arcs?: any[], fills?: any[], vias?: any[], polygons?: any[], embeddedModels?: any[], componentBodies?: { modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }[], components?: { designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, modelPath?: string }[] } }} documentModel
30
- * @param {{ modelRegistry?: { resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null, resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null, boardThicknessMil?: number }} [options]
31
- * @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
30
+ * @param {{ modelRegistry?: { resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null, resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null, resolveBoardAssemblyModel?: (documentModel: any) => { origin: string, name: string, format: string, file?: File | Blob | null, relativePath?: string } | null } | null, boardThicknessMil?: number }} [options]
31
+ * @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, boardAssemblyModel: { origin: string, name: string, format: string, file?: File | Blob | null, relativePath?: string } | null, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
32
32
  */
33
33
  static build(documentModel, options = {}) {
34
34
  const pcb = documentModel?.pcb || {}
@@ -63,6 +63,10 @@ export class PcbScene3dBuilder {
63
63
  centerY:
64
64
  Number(boardOutline.minY || 0) +
65
65
  Number(boardOutline.heightMil || 0) / 2,
66
+ surfaceColor: Number.isInteger(appearance3d.solderMaskTopColor)
67
+ ? appearance3d.solderMaskTopColor
68
+ : appearance3d.solderMaskBottomColor,
69
+ edgeColor: appearance3d.boardCoreColor,
66
70
  segments: Array.isArray(boardOutline.segments)
67
71
  ? boardOutline.segments
68
72
  : []
@@ -106,6 +110,9 @@ export class PcbScene3dBuilder {
106
110
  const sceneDescription = {
107
111
  sourceFormat: 'altium',
108
112
  board,
113
+ boardAssemblyModel:
114
+ modelRegistry?.resolveBoardAssemblyModel?.(documentModel) ||
115
+ null,
109
116
  components: components.map((component) =>
110
117
  PcbScene3dBuilder.#buildComponent(
111
118
  component,
@@ -121,6 +128,7 @@ export class PcbScene3dBuilder {
121
128
  componentBody,
122
129
  bodyMatches[index],
123
130
  components,
131
+ pads,
124
132
  board,
125
133
  thicknessMil,
126
134
  modelRegistry
@@ -208,6 +216,7 @@ export class PcbScene3dBuilder {
208
216
  * @param {{ modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }} componentBody
209
217
  * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent
210
218
  * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components
219
+ * @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
211
220
  * @param {{ centerX: number, centerY: number }} board
212
221
  * @param {number} thicknessMil
213
222
  * @param {{ resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null} modelRegistry
@@ -217,6 +226,7 @@ export class PcbScene3dBuilder {
217
226
  componentBody,
218
227
  matchedComponent,
219
228
  components,
229
+ pads,
220
230
  board,
221
231
  thicknessMil,
222
232
  modelRegistry
@@ -275,10 +285,134 @@ export class PcbScene3dBuilder {
275
285
  rotationDeg: modelRotation,
276
286
  dzMil: Number(componentBody.dzMil || 0)
277
287
  },
288
+ projection: PcbScene3dBuilder.#resolveProjectionDiagnostics(
289
+ componentBody,
290
+ matchedComponent,
291
+ pads,
292
+ resolvedModel
293
+ ),
278
294
  externalModel: resolvedModel
279
295
  }
280
296
  }
281
297
 
298
+ /**
299
+ * Explains which footprint projection source informed one external model.
300
+ * @param {object} componentBody Normalized component body row.
301
+ * @param {{ x: number, y: number, height?: number | null } | null} matchedComponent Matched component.
302
+ * @param {object[]} pads Normalized pad rows.
303
+ * @param {object | null} resolvedModel Resolved model metadata.
304
+ * @returns {{ source: string, reason: string, boundsMil: { width: number, depth: number, height: number } }}
305
+ */
306
+ static #resolveProjectionDiagnostics(
307
+ componentBody,
308
+ matchedComponent,
309
+ pads,
310
+ resolvedModel
311
+ ) {
312
+ const authoredBounds = PcbScene3dBuilder.#firstBounds([
313
+ componentBody?.projectionOverrideMil,
314
+ componentBody?.projectionOverride?.boundsMil,
315
+ componentBody?.projectionBoundsMil
316
+ ])
317
+ if (authoredBounds) {
318
+ return {
319
+ source: 'authored-override',
320
+ reason: 'Component body carried an explicit projection override.',
321
+ boundsMil: authoredBounds
322
+ }
323
+ }
324
+
325
+ const modelBounds = PcbScene3dBuilder.#firstBounds([
326
+ componentBody?.modelBoundsMil,
327
+ resolvedModel?.boundsMil
328
+ ])
329
+ if (modelBounds) {
330
+ return {
331
+ source: 'model-bounds',
332
+ reason: 'Resolved 3D model bounds were available.',
333
+ boundsMil: modelBounds
334
+ }
335
+ }
336
+
337
+ if (matchedComponent) {
338
+ const padSpan = PcbScene3dBuilder.#resolvePadSpan(
339
+ matchedComponent,
340
+ pads
341
+ )
342
+ if (padSpan.width > 0 || padSpan.depth > 0) {
343
+ return {
344
+ source: 'pad-fallback',
345
+ reason: 'Projection fell back to nearby component pad span.',
346
+ boundsMil: {
347
+ width: padSpan.width,
348
+ depth: padSpan.depth,
349
+ height: Number(matchedComponent.height || 0)
350
+ }
351
+ }
352
+ }
353
+
354
+ const body = PcbScene3dPackages.resolve(matchedComponent, padSpan)
355
+ return {
356
+ source: 'component-fallback',
357
+ reason: 'Projection fell back to the procedural component body.',
358
+ boundsMil: {
359
+ width: body.sizeMil.width,
360
+ depth: body.sizeMil.depth,
361
+ height: body.sizeMil.height
362
+ }
363
+ }
364
+ }
365
+
366
+ return {
367
+ source: 'model-anchor-fallback',
368
+ reason: 'Projection used the model anchor because no owner geometry was available.',
369
+ boundsMil: { width: 0, depth: 0, height: 0 }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Returns the first complete bounds object from candidate metadata.
375
+ * @param {unknown[]} candidates Candidate bounds records.
376
+ * @returns {{ width: number, depth: number, height: number } | null}
377
+ */
378
+ static #firstBounds(candidates) {
379
+ for (const candidate of candidates || []) {
380
+ const bounds = PcbScene3dBuilder.#normalizeBounds(candidate)
381
+ if (bounds) {
382
+ return bounds
383
+ }
384
+ }
385
+
386
+ return null
387
+ }
388
+
389
+ /**
390
+ * Normalizes width/depth/height bounds metadata.
391
+ * @param {unknown} candidate Candidate bounds record.
392
+ * @returns {{ width: number, depth: number, height: number } | null}
393
+ */
394
+ static #normalizeBounds(candidate) {
395
+ if (!candidate || typeof candidate !== 'object') {
396
+ return null
397
+ }
398
+
399
+ const width = Number(candidate.width ?? candidate.x ?? candidate.sizeX)
400
+ const depth = Number(candidate.depth ?? candidate.y ?? candidate.sizeY)
401
+ const height = Number(
402
+ candidate.height ?? candidate.z ?? candidate.sizeZ
403
+ )
404
+
405
+ if (
406
+ !Number.isFinite(width) ||
407
+ !Number.isFinite(depth) ||
408
+ !Number.isFinite(height)
409
+ ) {
410
+ return null
411
+ }
412
+
413
+ return { width, depth, height }
414
+ }
415
+
282
416
  /**
283
417
  * Resolves explicit body placements to component anchors using a unique
284
418
  * nearest-neighbor pass plus an ordered-affinity fallback for repeated
@@ -208,6 +208,49 @@ export class PcbScene3dModelRegistry {
208
208
  return this.#resolveExplicitMatch(componentBody?.name)
209
209
  }
210
210
 
211
+ /**
212
+ * Resolves a project-level full board assembly model for one PCB document.
213
+ * @param {{ fileName?: string }} documentModel
214
+ * @returns {{ origin: 'board-assembly', file?: File | Blob | null, name: string, relativePath: string, format: string } | null}
215
+ */
216
+ resolveBoardAssemblyModel(documentModel) {
217
+ const normalizedBoardBaseName = PcbScene3dModelRegistry.#normalizeToken(
218
+ PcbScene3dModelRegistry.#basenameWithoutExtension(
219
+ documentModel?.fileName
220
+ )
221
+ )
222
+ if (!normalizedBoardBaseName) {
223
+ return null
224
+ }
225
+
226
+ const rankedMatches = this.#modelFiles
227
+ .filter(
228
+ (file) =>
229
+ file.normalizedBaseName === normalizedBoardBaseName &&
230
+ PcbScene3dModelRegistry.#isBoardAssemblyPath(
231
+ file.relativePath
232
+ )
233
+ )
234
+ .sort(
235
+ (left, right) =>
236
+ PcbScene3dModelRegistry.#formatRank(left.format) -
237
+ PcbScene3dModelRegistry.#formatRank(right.format)
238
+ )
239
+
240
+ if (!rankedMatches.length) {
241
+ return null
242
+ }
243
+
244
+ const matchedFile = rankedMatches[0]
245
+ return {
246
+ origin: 'board-assembly',
247
+ file: matchedFile.file,
248
+ name: matchedFile.name,
249
+ relativePath: matchedFile.relativePath,
250
+ format: matchedFile.format
251
+ }
252
+ }
253
+
211
254
  /**
212
255
  * Normalizes one embedded payload for registry lookup.
213
256
  * @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string }} model
@@ -295,6 +338,37 @@ export class PcbScene3dModelRegistry {
295
338
  return format === 'wrl' ? 0 : 1
296
339
  }
297
340
 
341
+ /**
342
+ * Checks whether one model path is in a conventional board model folder.
343
+ * @param {string | undefined} relativePath
344
+ * @returns {boolean}
345
+ */
346
+ static #isBoardAssemblyPath(relativePath) {
347
+ return String(relativePath || '')
348
+ .replaceAll('\\', '/')
349
+ .split('/')
350
+ .some(
351
+ (part) =>
352
+ PcbScene3dModelRegistry.#normalizeToken(part) === '3dbodies'
353
+ )
354
+ }
355
+
356
+ /**
357
+ * Returns one path basename without its extension.
358
+ * @param {string | undefined} filePath
359
+ * @returns {string}
360
+ */
361
+ static #basenameWithoutExtension(filePath) {
362
+ const baseName =
363
+ String(filePath || '')
364
+ .replaceAll('\\', '/')
365
+ .split('/')
366
+ .filter(Boolean)
367
+ .at(-1) || ''
368
+
369
+ return baseName.replace(/\.[^.]+$/u, '')
370
+ }
371
+
298
372
  /**
299
373
  * Normalizes one lookup token.
300
374
  * @param {string | undefined} value