altium-toolkit 1.0.10 → 1.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.
- package/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +132 -1
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +692 -2
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +7 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +16 -5
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +246 -7
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +182 -18
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +3 -0
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +45 -0
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +61 -2
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +35 -2
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +164 -0
- package/src/parser.mjs +15 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
import { ProjectBomPnpReconciliationBuilder } from './ProjectBomPnpReconciliationBuilder.mjs'
|
|
6
7
|
import { ProjectVariantViewBuilder } from './ProjectVariantViewBuilder.mjs'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -114,6 +115,12 @@ export class ProjectDesignBundleBuilder {
|
|
|
114
115
|
})
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
bundle.reconciliation = ProjectBomPnpReconciliationBuilder.build({
|
|
119
|
+
bundle,
|
|
120
|
+
documentModels,
|
|
121
|
+
effectiveVariant: bundle.effectiveVariant
|
|
122
|
+
})
|
|
123
|
+
|
|
117
124
|
return bundle
|
|
118
125
|
}
|
|
119
126
|
|
|
@@ -309,15 +316,67 @@ export class ProjectDesignBundleBuilder {
|
|
|
309
316
|
* @returns {object[]}
|
|
310
317
|
*/
|
|
311
318
|
static #buildBom(documentModels) {
|
|
319
|
+
const noBomDesignators =
|
|
320
|
+
ProjectDesignBundleBuilder.#noBomDesignators(documentModels)
|
|
312
321
|
const pcbBom = documentModels
|
|
313
322
|
.filter((model) => model?.kind === 'pcb')
|
|
314
323
|
.flatMap((model) => model.bom || [])
|
|
315
324
|
|
|
316
325
|
if (pcbBom.length) {
|
|
317
|
-
return
|
|
326
|
+
return ProjectDesignBundleBuilder.#filterBomRows(
|
|
327
|
+
pcbBom,
|
|
328
|
+
noBomDesignators
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return ProjectDesignBundleBuilder.#filterBomRows(
|
|
333
|
+
documentModels.flatMap((model) => model.bom || []),
|
|
334
|
+
noBomDesignators
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Removes component-kind no-BOM designators from normalized BOM rows.
|
|
340
|
+
* @param {object[]} rows BOM rows.
|
|
341
|
+
* @param {Set<string>} noBomDesignators Designators excluded from BOMs.
|
|
342
|
+
* @returns {object[]}
|
|
343
|
+
*/
|
|
344
|
+
static #filterBomRows(rows, noBomDesignators) {
|
|
345
|
+
if (!noBomDesignators.size) return rows
|
|
346
|
+
|
|
347
|
+
return (rows || [])
|
|
348
|
+
.map((row) => {
|
|
349
|
+
const designators = (row.designators || []).filter(
|
|
350
|
+
(designator) => !noBomDesignators.has(designator)
|
|
351
|
+
)
|
|
352
|
+
return {
|
|
353
|
+
...row,
|
|
354
|
+
designators,
|
|
355
|
+
quantity: designators.length || row.quantity
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
.filter((row) => row.designators.length > 0)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Collects PCB components whose native kind excludes BOM output.
|
|
363
|
+
* @param {object[]} documentModels Parsed document models.
|
|
364
|
+
* @returns {Set<string>}
|
|
365
|
+
*/
|
|
366
|
+
static #noBomDesignators(documentModels) {
|
|
367
|
+
const designators = new Set()
|
|
368
|
+
|
|
369
|
+
for (const model of documentModels.filter(
|
|
370
|
+
(item) => item?.kind === 'pcb'
|
|
371
|
+
)) {
|
|
372
|
+
for (const component of model.pcb?.components || []) {
|
|
373
|
+
if (component.componentKind?.includeInBom !== false) continue
|
|
374
|
+
const designator = String(component.designator || '').trim()
|
|
375
|
+
if (designator) designators.add(designator)
|
|
376
|
+
}
|
|
318
377
|
}
|
|
319
378
|
|
|
320
|
-
return
|
|
379
|
+
return designators
|
|
321
380
|
}
|
|
322
381
|
|
|
323
382
|
/**
|
|
@@ -14,7 +14,8 @@ export class ProjectOutJobDigestBuilder {
|
|
|
14
14
|
* @returns {object}
|
|
15
15
|
*/
|
|
16
16
|
static build(project) {
|
|
17
|
-
const
|
|
17
|
+
const projectDocuments = project?.documents || []
|
|
18
|
+
const documents = projectDocuments
|
|
18
19
|
.filter((document) => document.kind === 'output-job')
|
|
19
20
|
.map((document) => ({
|
|
20
21
|
documentIndex: document.index,
|
|
@@ -22,23 +23,39 @@ export class ProjectOutJobDigestBuilder {
|
|
|
22
23
|
normalizedPath: document.normalizedPath,
|
|
23
24
|
fileName: document.fileName
|
|
24
25
|
}))
|
|
26
|
+
const context = {
|
|
27
|
+
defaultPcbDocumentPath:
|
|
28
|
+
projectDocuments.find((document) => document.kind === 'pcb')
|
|
29
|
+
?.path || ''
|
|
30
|
+
}
|
|
25
31
|
const outputGroups = (project?.outputGroups || []).map((group) =>
|
|
26
|
-
ProjectOutJobDigestBuilder.#outputGroup(group)
|
|
32
|
+
ProjectOutJobDigestBuilder.#outputGroup(group, context)
|
|
27
33
|
)
|
|
28
34
|
const outputCount = outputGroups.reduce(
|
|
29
35
|
(sum, group) => sum + group.outputCount,
|
|
30
36
|
0
|
|
31
37
|
)
|
|
38
|
+
const outputs = outputGroups.flatMap((group) => group.outputs)
|
|
39
|
+
const expectedArtifacts =
|
|
40
|
+
ProjectOutJobDigestBuilder.#expectedArtifacts(outputGroups)
|
|
32
41
|
|
|
33
42
|
return {
|
|
34
43
|
schema: ProjectOutJobDigestBuilder.SCHEMA_ID,
|
|
35
44
|
summary: {
|
|
36
45
|
outJobDocumentCount: documents.length,
|
|
37
46
|
outputGroupCount: outputGroups.length,
|
|
38
|
-
outputCount
|
|
47
|
+
outputCount,
|
|
48
|
+
typedOutputCount: outputs.filter(
|
|
49
|
+
(output) => output.normalizedType !== 'unsupported'
|
|
50
|
+
).length,
|
|
51
|
+
unsupportedOutputCount: outputs.filter(
|
|
52
|
+
(output) => output.normalizedType === 'unsupported'
|
|
53
|
+
).length,
|
|
54
|
+
expectedArtifactCount: expectedArtifacts.manifest.outputs.length
|
|
39
55
|
},
|
|
40
56
|
documents,
|
|
41
57
|
outputGroups,
|
|
58
|
+
expectedArtifacts,
|
|
42
59
|
outputsByDocumentPath:
|
|
43
60
|
ProjectOutJobDigestBuilder.#outputsByDocumentPath(outputGroups)
|
|
44
61
|
}
|
|
@@ -47,19 +64,12 @@ export class ProjectOutJobDigestBuilder {
|
|
|
47
64
|
/**
|
|
48
65
|
* Normalizes one output group.
|
|
49
66
|
* @param {object} group Project output group.
|
|
67
|
+
* @param {{ defaultPcbDocumentPath: string }} context Project context.
|
|
50
68
|
* @returns {object}
|
|
51
69
|
*/
|
|
52
|
-
static #outputGroup(group) {
|
|
70
|
+
static #outputGroup(group, context) {
|
|
53
71
|
const outputs = (group.outputs || []).map((output) => ({
|
|
54
|
-
|
|
55
|
-
type: output.type,
|
|
56
|
-
name: output.name,
|
|
57
|
-
documentPath: output.documentPath,
|
|
58
|
-
normalizedDocumentPath: ProjectOutJobDigestBuilder.#normalizePath(
|
|
59
|
-
output.documentPath
|
|
60
|
-
),
|
|
61
|
-
variantName: output.variantName,
|
|
62
|
-
isDefault: output.isDefault
|
|
72
|
+
...ProjectOutJobDigestBuilder.#typedOutput(output, group, context)
|
|
63
73
|
}))
|
|
64
74
|
|
|
65
75
|
return {
|
|
@@ -88,6 +98,7 @@ export class ProjectOutJobDigestBuilder {
|
|
|
88
98
|
outputGroupIndex: group.index,
|
|
89
99
|
outputIndex: output.index,
|
|
90
100
|
type: output.type,
|
|
101
|
+
normalizedType: output.normalizedType,
|
|
91
102
|
name: output.name,
|
|
92
103
|
variantName: output.variantName,
|
|
93
104
|
isDefault: output.isDefault
|
|
@@ -106,4 +117,404 @@ export class ProjectOutJobDigestBuilder {
|
|
|
106
117
|
static #normalizePath(path) {
|
|
107
118
|
return String(path || '').replace(/\\/g, '/')
|
|
108
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds one typed output row.
|
|
123
|
+
* @param {object} output Raw output row.
|
|
124
|
+
* @param {object} group Owning output group.
|
|
125
|
+
* @param {{ defaultPcbDocumentPath: string }} context Project context.
|
|
126
|
+
* @returns {object}
|
|
127
|
+
*/
|
|
128
|
+
static #typedOutput(output, group, context) {
|
|
129
|
+
const config = ProjectOutJobDigestBuilder.#mergedConfig(output)
|
|
130
|
+
const normalizedType = ProjectOutJobDigestBuilder.#normalizedType(
|
|
131
|
+
output.type
|
|
132
|
+
)
|
|
133
|
+
const category = ProjectOutJobDigestBuilder.#category(normalizedType)
|
|
134
|
+
const documentPath = ProjectOutJobDigestBuilder.#documentPath(
|
|
135
|
+
normalizedType,
|
|
136
|
+
output,
|
|
137
|
+
config,
|
|
138
|
+
context
|
|
139
|
+
)
|
|
140
|
+
const normalizedDocumentPath =
|
|
141
|
+
ProjectOutJobDigestBuilder.#normalizePath(documentPath)
|
|
142
|
+
const settings = ProjectOutJobDigestBuilder.#settings(
|
|
143
|
+
normalizedType,
|
|
144
|
+
output,
|
|
145
|
+
config,
|
|
146
|
+
documentPath
|
|
147
|
+
)
|
|
148
|
+
const base = {
|
|
149
|
+
index: output.index,
|
|
150
|
+
type: output.type,
|
|
151
|
+
normalizedType,
|
|
152
|
+
name: output.name,
|
|
153
|
+
documentPath: output.documentPath || '',
|
|
154
|
+
normalizedDocumentPath,
|
|
155
|
+
variantName: output.variantName || config.VariantName || '',
|
|
156
|
+
isDefault: output.isDefault,
|
|
157
|
+
category,
|
|
158
|
+
settings
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
expectedArtifact: ProjectOutJobDigestBuilder.#expectedArtifact(
|
|
164
|
+
base,
|
|
165
|
+
group
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolves the source document path for one output row.
|
|
172
|
+
* @param {string} normalizedType Stable output type.
|
|
173
|
+
* @param {object} output Raw output row.
|
|
174
|
+
* @param {Record<string, string>} config Merged config fields.
|
|
175
|
+
* @param {{ defaultPcbDocumentPath: string }} context Project context.
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
static #documentPath(normalizedType, output, config, context) {
|
|
179
|
+
const explicit = output.documentPath || config.DocumentPath || ''
|
|
180
|
+
if (explicit) return explicit
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
normalizedType === 'bom' &&
|
|
184
|
+
ProjectOutJobDigestBuilder.#boolean(config.IncludePcbData) === true
|
|
185
|
+
) {
|
|
186
|
+
return context.defaultPcbDocumentPath || ''
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return ''
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Merges parsed configuration rows into one field lookup.
|
|
194
|
+
* @param {object} output Output row.
|
|
195
|
+
* @returns {Record<string, string>}
|
|
196
|
+
*/
|
|
197
|
+
static #mergedConfig(output) {
|
|
198
|
+
const config = {}
|
|
199
|
+
|
|
200
|
+
for (const row of output.configRows || []) {
|
|
201
|
+
if (row.record && !config.Record) {
|
|
202
|
+
config.Record = row.record
|
|
203
|
+
}
|
|
204
|
+
Object.assign(config, row.fields || {})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return config
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolves a stable output type token.
|
|
212
|
+
* @param {string} type Native output type.
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*/
|
|
215
|
+
static #normalizedType(type) {
|
|
216
|
+
const normalized = String(type || '')
|
|
217
|
+
.trim()
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[\s_]+/gu, '-')
|
|
220
|
+
|
|
221
|
+
if (normalized.includes('gerber')) return 'gerber'
|
|
222
|
+
if (normalized.includes('ncdrill') || normalized.includes('nc-drill')) {
|
|
223
|
+
return 'nc-drill'
|
|
224
|
+
}
|
|
225
|
+
if (normalized.includes('odb')) return 'odb'
|
|
226
|
+
if (
|
|
227
|
+
normalized.includes('pickplace') ||
|
|
228
|
+
normalized.includes('pick-place')
|
|
229
|
+
) {
|
|
230
|
+
return 'pick-place'
|
|
231
|
+
}
|
|
232
|
+
if (normalized.includes('wirelist')) return 'wirelist'
|
|
233
|
+
if (normalized.includes('bom')) return 'bom'
|
|
234
|
+
if (normalized.includes('step')) return 'step'
|
|
235
|
+
if (normalized.includes('schematicprint')) return 'schematic-print'
|
|
236
|
+
if (
|
|
237
|
+
normalized.includes('pcbdrawing') ||
|
|
238
|
+
normalized.includes('draftsman')
|
|
239
|
+
) {
|
|
240
|
+
return 'pcb-drawing'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return 'unsupported'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resolves an output category.
|
|
248
|
+
* @param {string} normalizedType Stable output type.
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
static #category(normalizedType) {
|
|
252
|
+
return (
|
|
253
|
+
{
|
|
254
|
+
gerber: 'fabrication',
|
|
255
|
+
'nc-drill': 'fabrication',
|
|
256
|
+
odb: 'fabrication',
|
|
257
|
+
'pick-place': 'assembly',
|
|
258
|
+
wirelist: 'netlist',
|
|
259
|
+
bom: 'report',
|
|
260
|
+
step: 'export',
|
|
261
|
+
'schematic-print': 'documentation',
|
|
262
|
+
'pcb-drawing': 'documentation'
|
|
263
|
+
}[normalizedType] || 'unsupported'
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builds typed settings for one output row.
|
|
269
|
+
* @param {string} normalizedType Stable output type.
|
|
270
|
+
* @param {object} output Output row.
|
|
271
|
+
* @param {Record<string, string>} config Merged config fields.
|
|
272
|
+
* @param {string} documentPath Resolved output document path.
|
|
273
|
+
* @returns {object}
|
|
274
|
+
*/
|
|
275
|
+
static #settings(normalizedType, output, config, documentPath) {
|
|
276
|
+
const common = ProjectOutJobDigestBuilder.#stripEmpty({
|
|
277
|
+
record: config.Record || '',
|
|
278
|
+
documentPath
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
switch (normalizedType) {
|
|
282
|
+
case 'gerber':
|
|
283
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
284
|
+
...common,
|
|
285
|
+
units: config.GerberUnit || config.Units || '',
|
|
286
|
+
decimals: ProjectOutJobDigestBuilder.#number(
|
|
287
|
+
config.NumberOfDecimals
|
|
288
|
+
),
|
|
289
|
+
plotLayers: ProjectOutJobDigestBuilder.#plotLayers(
|
|
290
|
+
config['Plot.Set']
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
case 'nc-drill':
|
|
294
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
295
|
+
...common,
|
|
296
|
+
units: config.Units || '',
|
|
297
|
+
separatePlated:
|
|
298
|
+
ProjectOutJobDigestBuilder.#boolean(
|
|
299
|
+
config.SeparatePlated
|
|
300
|
+
) ??
|
|
301
|
+
ProjectOutJobDigestBuilder.#boolean(
|
|
302
|
+
config.GenerateSeparatePlatedNonPlatedFiles
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
case 'pick-place':
|
|
306
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
307
|
+
...common,
|
|
308
|
+
units: config.Units || '',
|
|
309
|
+
generateCsv: ProjectOutJobDigestBuilder.#boolean(
|
|
310
|
+
config.GenerateCSVFormat
|
|
311
|
+
),
|
|
312
|
+
includeStandardNoBom: ProjectOutJobDigestBuilder.#boolean(
|
|
313
|
+
config.IncludeStandardNoBOM
|
|
314
|
+
)
|
|
315
|
+
})
|
|
316
|
+
case 'wirelist':
|
|
317
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
318
|
+
...common,
|
|
319
|
+
units: config.Units || '',
|
|
320
|
+
generateText: ProjectOutJobDigestBuilder.#boolean(
|
|
321
|
+
config.GenerateTextFormat
|
|
322
|
+
),
|
|
323
|
+
includeVariations: ProjectOutJobDigestBuilder.#boolean(
|
|
324
|
+
config.IncludeVariations
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
case 'bom':
|
|
328
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
329
|
+
...common,
|
|
330
|
+
includePcbData: ProjectOutJobDigestBuilder.#boolean(
|
|
331
|
+
config.IncludePcbData
|
|
332
|
+
),
|
|
333
|
+
includeAlternatives: ProjectOutJobDigestBuilder.#boolean(
|
|
334
|
+
config.IncludeAlternatives
|
|
335
|
+
),
|
|
336
|
+
batchMode: ProjectOutJobDigestBuilder.#number(
|
|
337
|
+
config.BatchMode
|
|
338
|
+
),
|
|
339
|
+
viewType: ProjectOutJobDigestBuilder.#number(
|
|
340
|
+
config.ViewType
|
|
341
|
+
)
|
|
342
|
+
})
|
|
343
|
+
case 'step':
|
|
344
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
345
|
+
...common,
|
|
346
|
+
exportModelsOption: ProjectOutJobDigestBuilder.#number(
|
|
347
|
+
config.ExportModelsOption
|
|
348
|
+
),
|
|
349
|
+
exportHolesOption: ProjectOutJobDigestBuilder.#number(
|
|
350
|
+
config.ExportHolesOption
|
|
351
|
+
)
|
|
352
|
+
})
|
|
353
|
+
case 'schematic-print':
|
|
354
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
355
|
+
...common,
|
|
356
|
+
paperKind: config.PaperKind || '',
|
|
357
|
+
printScale: ProjectOutJobDigestBuilder.#number(
|
|
358
|
+
config.PrintScale
|
|
359
|
+
)
|
|
360
|
+
})
|
|
361
|
+
case 'pcb-drawing':
|
|
362
|
+
return ProjectOutJobDigestBuilder.#stripEmpty({
|
|
363
|
+
...common,
|
|
364
|
+
variantName: config.VariantName || ''
|
|
365
|
+
})
|
|
366
|
+
default:
|
|
367
|
+
return common
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Builds expected artifact metadata for all output rows.
|
|
373
|
+
* @param {object[]} outputGroups Output groups.
|
|
374
|
+
* @returns {object}
|
|
375
|
+
*/
|
|
376
|
+
static #expectedArtifacts(outputGroups) {
|
|
377
|
+
const outputs = outputGroups.flatMap((group) =>
|
|
378
|
+
group.outputs.map((output) => output.expectedArtifact)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
schema: 'altium-toolkit.project.expected-artifacts.a1',
|
|
383
|
+
summary: {
|
|
384
|
+
outputCount: outputs.length,
|
|
385
|
+
unsupportedOutputCount: outputs.filter(
|
|
386
|
+
(output) => output.unsupported
|
|
387
|
+
).length
|
|
388
|
+
},
|
|
389
|
+
manifest: {
|
|
390
|
+
outputs
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Builds one expected artifact row.
|
|
397
|
+
* @param {object} output Typed output row.
|
|
398
|
+
* @param {object} group Owning group.
|
|
399
|
+
* @returns {object}
|
|
400
|
+
*/
|
|
401
|
+
static #expectedArtifact(output, group) {
|
|
402
|
+
const artifact = ProjectOutJobDigestBuilder.#stripEmpty({
|
|
403
|
+
key:
|
|
404
|
+
ProjectOutJobDigestBuilder.#slug(group.name || 'outputs') +
|
|
405
|
+
'/' +
|
|
406
|
+
String(output.index).padStart(2, '0') +
|
|
407
|
+
'-' +
|
|
408
|
+
ProjectOutJobDigestBuilder.#slug(
|
|
409
|
+
output.name || output.normalizedType
|
|
410
|
+
),
|
|
411
|
+
outputGroupName: group.name || '',
|
|
412
|
+
outputName: output.name || '',
|
|
413
|
+
outputType: output.normalizedType,
|
|
414
|
+
category: output.category,
|
|
415
|
+
documentPath:
|
|
416
|
+
output.settings.documentPath || output.documentPath || '',
|
|
417
|
+
normalizedDocumentPath: output.normalizedDocumentPath,
|
|
418
|
+
variantName: output.variantName,
|
|
419
|
+
format: ProjectOutJobDigestBuilder.#format(output),
|
|
420
|
+
units: output.settings.units,
|
|
421
|
+
unsupported:
|
|
422
|
+
output.normalizedType === 'unsupported' ? true : undefined
|
|
423
|
+
})
|
|
424
|
+
artifact.variantName = output.variantName || ''
|
|
425
|
+
return artifact
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Resolves the expected artifact format token.
|
|
430
|
+
* @param {object} output Typed output row.
|
|
431
|
+
* @returns {string}
|
|
432
|
+
*/
|
|
433
|
+
static #format(output) {
|
|
434
|
+
if (output.normalizedType === 'pick-place') {
|
|
435
|
+
return output.settings.generateCsv === false
|
|
436
|
+
? 'pick-place-text'
|
|
437
|
+
: 'pick-place-csv'
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
{
|
|
442
|
+
gerber: 'gerber',
|
|
443
|
+
'nc-drill': 'nc-drill',
|
|
444
|
+
odb: 'odb',
|
|
445
|
+
wirelist: 'wirelist',
|
|
446
|
+
bom: 'bom',
|
|
447
|
+
step: 'step',
|
|
448
|
+
'schematic-print': 'pdf',
|
|
449
|
+
'pcb-drawing': 'pcbdwf'
|
|
450
|
+
}[output.normalizedType] || 'unknown'
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Parses plot-layer tokens from Altium plot-layer state strings.
|
|
456
|
+
* @param {string | undefined} value Plot-layer state.
|
|
457
|
+
* @returns {string[] | undefined}
|
|
458
|
+
*/
|
|
459
|
+
static #plotLayers(value) {
|
|
460
|
+
const layers = String(value || '')
|
|
461
|
+
.split(',')
|
|
462
|
+
.map((segment) => segment.trim().split('~')[0])
|
|
463
|
+
.filter(Boolean)
|
|
464
|
+
|
|
465
|
+
return layers.length ? layers : undefined
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Parses one numeric field.
|
|
470
|
+
* @param {unknown} value Raw value.
|
|
471
|
+
* @returns {number | undefined}
|
|
472
|
+
*/
|
|
473
|
+
static #number(value) {
|
|
474
|
+
const parsed = Number.parseFloat(String(value ?? '').trim())
|
|
475
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Parses one optional boolean field.
|
|
480
|
+
* @param {unknown} value Raw value.
|
|
481
|
+
* @returns {boolean | undefined}
|
|
482
|
+
*/
|
|
483
|
+
static #boolean(value) {
|
|
484
|
+
const raw = String(value ?? '')
|
|
485
|
+
.trim()
|
|
486
|
+
.toLowerCase()
|
|
487
|
+
if (!raw) return undefined
|
|
488
|
+
return ['1', 't', 'true', 'yes'].includes(raw)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Removes empty values while preserving booleans and zeroes.
|
|
493
|
+
* @param {object} value Source object.
|
|
494
|
+
* @returns {object}
|
|
495
|
+
*/
|
|
496
|
+
static #stripEmpty(value) {
|
|
497
|
+
return Object.fromEntries(
|
|
498
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
499
|
+
if (Array.isArray(entryValue)) return entryValue.length > 0
|
|
500
|
+
if (typeof entryValue === 'string') return entryValue.length > 0
|
|
501
|
+
return entryValue !== null && entryValue !== undefined
|
|
502
|
+
})
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Builds a stable slug for output manifest keys.
|
|
508
|
+
* @param {string} value Raw value.
|
|
509
|
+
* @returns {string}
|
|
510
|
+
*/
|
|
511
|
+
static #slug(value) {
|
|
512
|
+
return (
|
|
513
|
+
String(value || '')
|
|
514
|
+
.trim()
|
|
515
|
+
.toLowerCase()
|
|
516
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
517
|
+
.replace(/^-|-$/gu, '') || 'output'
|
|
518
|
+
)
|
|
519
|
+
}
|
|
109
520
|
}
|
|
@@ -15,6 +15,18 @@ export class SvgModelCrossLinkValidator {
|
|
|
15
15
|
* @returns {object}
|
|
16
16
|
*/
|
|
17
17
|
static validate(documentModel, svgMarkup) {
|
|
18
|
+
return SvgModelCrossLinkValidator.validateSet(documentModel, [
|
|
19
|
+
svgMarkup
|
|
20
|
+
])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates a set of semantic SVG fragments against one normalized model.
|
|
25
|
+
* @param {object} documentModel Normalized schematic or PCB model.
|
|
26
|
+
* @param {string[]} svgMarkups SVG markup strings.
|
|
27
|
+
* @returns {object}
|
|
28
|
+
*/
|
|
29
|
+
static validateSet(documentModel, svgMarkups) {
|
|
18
30
|
const documentKind =
|
|
19
31
|
SvgModelCrossLinkValidator.#documentKind(documentModel)
|
|
20
32
|
const expectedElements =
|
|
@@ -22,7 +34,9 @@ export class SvgModelCrossLinkValidator {
|
|
|
22
34
|
const expectedByKey = new Map(
|
|
23
35
|
expectedElements.map((element) => [element.elementKey, element])
|
|
24
36
|
)
|
|
25
|
-
const svgElements =
|
|
37
|
+
const svgElements = (svgMarkups || []).flatMap((svgMarkup) =>
|
|
38
|
+
SvgModelCrossLinkValidator.#svgElements(svgMarkup)
|
|
39
|
+
)
|
|
26
40
|
const renderedKeys = new Set(
|
|
27
41
|
svgElements.map((element) => element.elementKey).filter(Boolean)
|
|
28
42
|
)
|
|
@@ -41,12 +55,13 @@ export class SvgModelCrossLinkValidator {
|
|
|
41
55
|
documentModel,
|
|
42
56
|
svgElements
|
|
43
57
|
)
|
|
44
|
-
const metadata = SvgModelCrossLinkValidator.#
|
|
58
|
+
const metadata = SvgModelCrossLinkValidator.#metadataSet(svgMarkups)
|
|
45
59
|
|
|
46
60
|
return {
|
|
47
61
|
schema: SvgModelCrossLinkValidator.SCHEMA,
|
|
48
62
|
documentKind,
|
|
49
63
|
summary: {
|
|
64
|
+
svgCount: (svgMarkups || []).length,
|
|
50
65
|
expectedElementCount: expectedElements.length,
|
|
51
66
|
renderedElementCount: renderedKeys.size,
|
|
52
67
|
linkedElementCount:
|
|
@@ -375,6 +390,24 @@ export class SvgModelCrossLinkValidator {
|
|
|
375
390
|
}
|
|
376
391
|
}
|
|
377
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Extracts semantic metadata from a set of SVG fragments.
|
|
395
|
+
* @param {string[]} svgMarkups SVG markup strings.
|
|
396
|
+
* @returns {{ schema: string, elements: object[] }}
|
|
397
|
+
*/
|
|
398
|
+
static #metadataSet(svgMarkups) {
|
|
399
|
+
const metadataRows = (svgMarkups || []).map((svgMarkup) =>
|
|
400
|
+
SvgModelCrossLinkValidator.#metadata(svgMarkup)
|
|
401
|
+
)
|
|
402
|
+
const schema =
|
|
403
|
+
metadataRows.find((metadata) => metadata.schema)?.schema || ''
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
schema,
|
|
407
|
+
elements: metadataRows.flatMap((metadata) => metadata.elements)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
378
411
|
/**
|
|
379
412
|
* Converts a data attribute token to a camelCase object key.
|
|
380
413
|
* @param {string} value Attribute token.
|