altium-toolkit 1.0.9 → 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 +80 -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 +166 -0
- 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/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- 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/pcb_svg_semantics_a1.schema.json +27 -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_bundle_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -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/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +12 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
- 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/ParserCompatibilityFuzzer.mjs +192 -0
- 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 +211 -22
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- 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/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +69 -2
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +21 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/PcbSvgRenderer.mjs +65 -0
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds Draftsman board-view/cache metadata sidecars from text containers.
|
|
7
|
+
*/
|
|
8
|
+
export class DraftsmanBoardViewMetadataBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.draftsman.board-view-cache.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes board-view/cache metadata used by drawing review surfaces.
|
|
13
|
+
* @param {string} text Decoded container text.
|
|
14
|
+
* @param {object[]} pages Parsed pages.
|
|
15
|
+
* @returns {object | undefined}
|
|
16
|
+
*/
|
|
17
|
+
static build(text, pages) {
|
|
18
|
+
const layerColors = DraftsmanBoardViewMetadataBuilder.#globalTagFields(
|
|
19
|
+
text,
|
|
20
|
+
['LayerColor', 'LayerColorV2']
|
|
21
|
+
).map((fields) =>
|
|
22
|
+
DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
23
|
+
id: fields.Id || fields.ID,
|
|
24
|
+
layerId: DraftsmanBoardViewMetadataBuilder.#integer(
|
|
25
|
+
fields.LayerId
|
|
26
|
+
),
|
|
27
|
+
layerName:
|
|
28
|
+
fields.LayerName || fields.DisplayName || fields.Name,
|
|
29
|
+
role: fields.Role || fields.LayerRole,
|
|
30
|
+
color: fields.Color,
|
|
31
|
+
fields
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
const pcbParameters = Object.fromEntries(
|
|
35
|
+
DraftsmanBoardViewMetadataBuilder.#globalTagFields(text, [
|
|
36
|
+
'PCBParameter',
|
|
37
|
+
'DrawingDocumentParameterData'
|
|
38
|
+
])
|
|
39
|
+
.map((fields) => [
|
|
40
|
+
fields.Name || fields.ParameterName || '',
|
|
41
|
+
fields.Value || ''
|
|
42
|
+
])
|
|
43
|
+
.filter(([name]) => name)
|
|
44
|
+
)
|
|
45
|
+
const boardAssemblyViews =
|
|
46
|
+
DraftsmanBoardViewMetadataBuilder.#globalTagFields(text, [
|
|
47
|
+
'BoardAssemblyView',
|
|
48
|
+
'BoardAssemblyInformation'
|
|
49
|
+
]).map((fields) =>
|
|
50
|
+
DraftsmanBoardViewMetadataBuilder.#boardAssemblyView(fields)
|
|
51
|
+
)
|
|
52
|
+
const boardFabricationViews =
|
|
53
|
+
DraftsmanBoardViewMetadataBuilder.#globalTagFields(text, [
|
|
54
|
+
'BoardFabricationView',
|
|
55
|
+
'BoardFabricationInformation'
|
|
56
|
+
]).map((fields) =>
|
|
57
|
+
DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
58
|
+
id: fields.Id || fields.ID,
|
|
59
|
+
pageId: fields.PageId,
|
|
60
|
+
sourceDocumentName:
|
|
61
|
+
fields.SourceDocumentName || fields.SourceDocument,
|
|
62
|
+
drillTableId: fields.DrillTableId,
|
|
63
|
+
fields
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
const boardProjections =
|
|
67
|
+
DraftsmanBoardViewMetadataBuilder.#globalTagFields(text, [
|
|
68
|
+
'BoardProjection',
|
|
69
|
+
'BoardProjectionInformation'
|
|
70
|
+
]).map((fields) =>
|
|
71
|
+
DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
72
|
+
id: fields.Id || fields.ID,
|
|
73
|
+
source: fields.Source,
|
|
74
|
+
width: DraftsmanBoardViewMetadataBuilder.#number(
|
|
75
|
+
fields.Width
|
|
76
|
+
),
|
|
77
|
+
height: DraftsmanBoardViewMetadataBuilder.#number(
|
|
78
|
+
fields.Height
|
|
79
|
+
),
|
|
80
|
+
scale: DraftsmanBoardViewMetadataBuilder.#number(
|
|
81
|
+
fields.Scale
|
|
82
|
+
),
|
|
83
|
+
fields
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
const generatedGeometry =
|
|
87
|
+
DraftsmanBoardViewMetadataBuilder.#generatedGeometry(text, pages)
|
|
88
|
+
const cacheLayers = DraftsmanBoardViewMetadataBuilder.#cacheLayers(text)
|
|
89
|
+
const displayLayers =
|
|
90
|
+
DraftsmanBoardViewMetadataBuilder.#displayLayers(text)
|
|
91
|
+
const cachePrimitives =
|
|
92
|
+
DraftsmanBoardViewMetadataBuilder.#cachePrimitives(text, pages)
|
|
93
|
+
const highlightGroups =
|
|
94
|
+
DraftsmanBoardViewMetadataBuilder.#highlightGroups(text)
|
|
95
|
+
const layerTiles = DraftsmanBoardViewMetadataBuilder.#layerTiles(
|
|
96
|
+
text,
|
|
97
|
+
pages
|
|
98
|
+
)
|
|
99
|
+
const diagnostics = DraftsmanBoardViewMetadataBuilder.#diagnostics({
|
|
100
|
+
cacheLayers,
|
|
101
|
+
displayLayers,
|
|
102
|
+
cachePrimitives,
|
|
103
|
+
highlightGroups,
|
|
104
|
+
layerTiles
|
|
105
|
+
})
|
|
106
|
+
const summary = {
|
|
107
|
+
layerColorCount: layerColors.length,
|
|
108
|
+
pcbParameterCount: Object.keys(pcbParameters).length,
|
|
109
|
+
boardAssemblyViewCount: boardAssemblyViews.length,
|
|
110
|
+
boardFabricationViewCount: boardFabricationViews.length,
|
|
111
|
+
boardProjectionCount: boardProjections.length,
|
|
112
|
+
generatedGeometryCount: generatedGeometry.length,
|
|
113
|
+
cacheLayerCount: cacheLayers.length,
|
|
114
|
+
displayLayerCount: displayLayers.length,
|
|
115
|
+
cachePrimitiveCount: cachePrimitives.length,
|
|
116
|
+
highlightGroupCount: highlightGroups.length,
|
|
117
|
+
layerTileCount: layerTiles.length,
|
|
118
|
+
selectedRoutePrimitiveCount: cachePrimitives.filter(
|
|
119
|
+
(primitive) => primitive.highlightState === 'selected'
|
|
120
|
+
).length,
|
|
121
|
+
drillPrimitiveCount: cachePrimitives.filter((primitive) =>
|
|
122
|
+
DraftsmanBoardViewMetadataBuilder.#isDrillPrimitive(primitive)
|
|
123
|
+
).length,
|
|
124
|
+
diagnosticCount: diagnostics.length
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!Object.values(summary).some((value) => value > 0)) {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
schema: DraftsmanBoardViewMetadataBuilder.SCHEMA_ID,
|
|
133
|
+
summary,
|
|
134
|
+
layerColors,
|
|
135
|
+
pcbParameters,
|
|
136
|
+
boardAssemblyViews,
|
|
137
|
+
boardFabricationViews,
|
|
138
|
+
boardProjections,
|
|
139
|
+
generatedGeometry,
|
|
140
|
+
cacheLayers,
|
|
141
|
+
displayLayers,
|
|
142
|
+
cachePrimitives,
|
|
143
|
+
highlightGroups,
|
|
144
|
+
layerTiles,
|
|
145
|
+
diagnostics
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Normalizes one board assembly view row.
|
|
151
|
+
* @param {Record<string, string>} fields Native fields.
|
|
152
|
+
* @returns {object}
|
|
153
|
+
*/
|
|
154
|
+
static #boardAssemblyView(fields) {
|
|
155
|
+
return DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
156
|
+
id: fields.Id || fields.ID,
|
|
157
|
+
pageId: fields.PageId,
|
|
158
|
+
sourceDocumentName:
|
|
159
|
+
fields.SourceDocumentName || fields.SourceDocument,
|
|
160
|
+
variantName: fields.VariantName || fields.AssemblyVariant,
|
|
161
|
+
layerSet: DraftsmanBoardViewMetadataBuilder.#list(fields.LayerSet),
|
|
162
|
+
fields
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extracts generated board-view geometry descriptors from pages.
|
|
168
|
+
* @param {string} text Decoded text.
|
|
169
|
+
* @param {object[]} pages Parsed pages.
|
|
170
|
+
* @returns {object[]}
|
|
171
|
+
*/
|
|
172
|
+
static #generatedGeometry(text, pages) {
|
|
173
|
+
return DraftsmanBoardViewMetadataBuilder.#pageBlocks(text).flatMap(
|
|
174
|
+
(pageBlock) =>
|
|
175
|
+
DraftsmanBoardViewMetadataBuilder.#tagFields(pageBlock.body, [
|
|
176
|
+
'BoardView',
|
|
177
|
+
'BoardAssemblyView',
|
|
178
|
+
'BoardFabricationView'
|
|
179
|
+
]).map((fields) =>
|
|
180
|
+
DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
181
|
+
pageIndex:
|
|
182
|
+
pages.find((page) => page.id === pageBlock.id)
|
|
183
|
+
?.index ?? pageBlock.index,
|
|
184
|
+
pageId: pageBlock.id,
|
|
185
|
+
id: fields.Id || fields.ID,
|
|
186
|
+
name: fields.Name || fields.Title,
|
|
187
|
+
geometrySource: fields.GeometrySource,
|
|
188
|
+
primitiveCount:
|
|
189
|
+
DraftsmanBoardViewMetadataBuilder.#integer(
|
|
190
|
+
fields.PrimitiveCount
|
|
191
|
+
),
|
|
192
|
+
fields
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Extracts cached PCB layer descriptors.
|
|
200
|
+
* @param {string} text Decoded text.
|
|
201
|
+
* @returns {object[]}
|
|
202
|
+
*/
|
|
203
|
+
static #cacheLayers(text) {
|
|
204
|
+
return DraftsmanBoardViewMetadataBuilder.#globalTagFields(text, [
|
|
205
|
+
'BoardCacheLayer',
|
|
206
|
+
'PcbCacheLayer',
|
|
207
|
+
'PcbCachedLayer'
|
|
208
|
+
]).map((fields) => {
|
|
209
|
+
const layerId = DraftsmanBoardViewMetadataBuilder.#integer(
|
|
210
|
+
fields.LayerId || fields.LayerID
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
214
|
+
id: fields.Id || fields.ID,
|
|
215
|
+
layerId,
|
|
216
|
+
layerKey: DraftsmanBoardViewMetadataBuilder.#layerKey(layerId),
|
|
217
|
+
layerName:
|
|
218
|
+
fields.LayerName || fields.DisplayName || fields.Name,
|
|
219
|
+
role: fields.Role || fields.LayerRole,
|
|
220
|
+
color: fields.Color,
|
|
221
|
+
primitiveCount: DraftsmanBoardViewMetadataBuilder.#integer(
|
|
222
|
+
fields.PrimitiveCount
|
|
223
|
+
),
|
|
224
|
+
fields
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extracts drawing display-layer descriptors.
|
|
231
|
+
* @param {string} text Decoded text.
|
|
232
|
+
* @returns {object[]}
|
|
233
|
+
*/
|
|
234
|
+
static #displayLayers(text) {
|
|
235
|
+
return DraftsmanBoardViewMetadataBuilder.#globalTagFields(text, [
|
|
236
|
+
'BoardDisplayLayer',
|
|
237
|
+
'PcbDisplayLayer',
|
|
238
|
+
'PcbViewLayer'
|
|
239
|
+
]).map((fields) =>
|
|
240
|
+
DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
241
|
+
id: fields.Id || fields.ID,
|
|
242
|
+
cacheLayerId: fields.CacheLayerId || fields.CacheLayerID,
|
|
243
|
+
role: fields.Role || fields.LayerRole,
|
|
244
|
+
color: fields.Color,
|
|
245
|
+
visible: DraftsmanBoardViewMetadataBuilder.#boolean(
|
|
246
|
+
fields.Visible
|
|
247
|
+
),
|
|
248
|
+
fields
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Extracts cached PCB primitive descriptors from page-local board views.
|
|
255
|
+
* @param {string} text Decoded text.
|
|
256
|
+
* @param {object[]} pages Parsed pages.
|
|
257
|
+
* @returns {object[]}
|
|
258
|
+
*/
|
|
259
|
+
static #cachePrimitives(text, pages) {
|
|
260
|
+
return DraftsmanBoardViewMetadataBuilder.#pageBlocks(text).flatMap(
|
|
261
|
+
(pageBlock) =>
|
|
262
|
+
DraftsmanBoardViewMetadataBuilder.#tagFields(pageBlock.body, [
|
|
263
|
+
'BoardCachePrimitive',
|
|
264
|
+
'PcbCachePrimitive',
|
|
265
|
+
'CachedPrimitive'
|
|
266
|
+
]).map((fields) => {
|
|
267
|
+
const layerId = DraftsmanBoardViewMetadataBuilder.#integer(
|
|
268
|
+
fields.LayerId || fields.LayerID
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
272
|
+
pageIndex:
|
|
273
|
+
pages.find((page) => page.id === pageBlock.id)
|
|
274
|
+
?.index ?? pageBlock.index,
|
|
275
|
+
pageId: pageBlock.id,
|
|
276
|
+
id: fields.Id || fields.ID,
|
|
277
|
+
cacheLayerId:
|
|
278
|
+
fields.CacheLayerId || fields.CacheLayerID,
|
|
279
|
+
primitiveKind: fields.PrimitiveKind || fields.Kind,
|
|
280
|
+
layerId,
|
|
281
|
+
layerKey:
|
|
282
|
+
DraftsmanBoardViewMetadataBuilder.#layerKey(
|
|
283
|
+
layerId
|
|
284
|
+
),
|
|
285
|
+
net: fields.Net || fields.NetName,
|
|
286
|
+
netClass: fields.NetClass,
|
|
287
|
+
component: fields.Component || fields.Designator,
|
|
288
|
+
padNumber: fields.PadNumber || fields.PinNumber,
|
|
289
|
+
routeGroup: fields.RouteGroup || fields.RouteId,
|
|
290
|
+
highlightState:
|
|
291
|
+
fields.HighlightState || fields.SelectionState,
|
|
292
|
+
holeKind: fields.HoleKind,
|
|
293
|
+
holePlating: fields.HolePlating,
|
|
294
|
+
holeRender: fields.HoleRender,
|
|
295
|
+
fields
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Extracts route/net-class highlight groups used by cached board views.
|
|
303
|
+
* @param {string} text Decoded text.
|
|
304
|
+
* @returns {object[]}
|
|
305
|
+
*/
|
|
306
|
+
static #highlightGroups(text) {
|
|
307
|
+
return DraftsmanBoardViewMetadataBuilder.#globalTagFields(text, [
|
|
308
|
+
'BoardHighlightGroup',
|
|
309
|
+
'PcbHighlightGroup',
|
|
310
|
+
'BoardRouteHighlight'
|
|
311
|
+
]).map((fields) =>
|
|
312
|
+
DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
313
|
+
id: fields.Id || fields.ID,
|
|
314
|
+
name: fields.Name || fields.Title,
|
|
315
|
+
selectorKind: fields.SelectorKind || fields.Kind,
|
|
316
|
+
netClasses: DraftsmanBoardViewMetadataBuilder.#list(
|
|
317
|
+
fields.NetClasses || fields.NetClass
|
|
318
|
+
),
|
|
319
|
+
differentialPairClasses:
|
|
320
|
+
DraftsmanBoardViewMetadataBuilder.#list(
|
|
321
|
+
fields.DifferentialPairClasses ||
|
|
322
|
+
fields.DifferentialPairClass
|
|
323
|
+
),
|
|
324
|
+
differentialPairs: DraftsmanBoardViewMetadataBuilder.#list(
|
|
325
|
+
fields.DifferentialPairs || fields.DifferentialPair
|
|
326
|
+
),
|
|
327
|
+
nets: DraftsmanBoardViewMetadataBuilder.#list(
|
|
328
|
+
fields.Nets || fields.NetNames || fields.Net
|
|
329
|
+
),
|
|
330
|
+
highlightColor: fields.HighlightColor || fields.Color,
|
|
331
|
+
contextColor: fields.ContextColor,
|
|
332
|
+
minimumRoutedLength:
|
|
333
|
+
fields.MinimumRoutedLength || fields.RoutedLengthMinimum,
|
|
334
|
+
connectedRouteOnly: DraftsmanBoardViewMetadataBuilder.#boolean(
|
|
335
|
+
fields.ConnectedRouteOnly
|
|
336
|
+
),
|
|
337
|
+
targetFillRatio: DraftsmanBoardViewMetadataBuilder.#number(
|
|
338
|
+
fields.TargetFillRatio
|
|
339
|
+
),
|
|
340
|
+
tileSpacing: DraftsmanBoardViewMetadataBuilder.#number(
|
|
341
|
+
fields.TileSpacing
|
|
342
|
+
),
|
|
343
|
+
layerSet: DraftsmanBoardViewMetadataBuilder.#list(
|
|
344
|
+
fields.LayerSet || fields.LayerIds
|
|
345
|
+
),
|
|
346
|
+
fields
|
|
347
|
+
})
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Extracts page-local board layer tile descriptors.
|
|
353
|
+
* @param {string} text Decoded text.
|
|
354
|
+
* @param {object[]} pages Parsed pages.
|
|
355
|
+
* @returns {object[]}
|
|
356
|
+
*/
|
|
357
|
+
static #layerTiles(text, pages) {
|
|
358
|
+
return DraftsmanBoardViewMetadataBuilder.#pageBlocks(text).flatMap(
|
|
359
|
+
(pageBlock) =>
|
|
360
|
+
DraftsmanBoardViewMetadataBuilder.#tagFields(pageBlock.body, [
|
|
361
|
+
'BoardLayerTile',
|
|
362
|
+
'PcbLayerTile',
|
|
363
|
+
'BoardRouteTile'
|
|
364
|
+
]).map((fields) => {
|
|
365
|
+
const layerId = DraftsmanBoardViewMetadataBuilder.#integer(
|
|
366
|
+
fields.LayerId || fields.LayerID
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
370
|
+
pageIndex:
|
|
371
|
+
pages.find((page) => page.id === pageBlock.id)
|
|
372
|
+
?.index ?? pageBlock.index,
|
|
373
|
+
pageId: pageBlock.id,
|
|
374
|
+
id: fields.Id || fields.ID,
|
|
375
|
+
highlightGroupId:
|
|
376
|
+
fields.HighlightGroupId || fields.HighlightGroupID,
|
|
377
|
+
layerId,
|
|
378
|
+
layerKey:
|
|
379
|
+
DraftsmanBoardViewMetadataBuilder.#layerKey(
|
|
380
|
+
layerId
|
|
381
|
+
),
|
|
382
|
+
layerName:
|
|
383
|
+
fields.LayerName ||
|
|
384
|
+
fields.DisplayName ||
|
|
385
|
+
fields.Name,
|
|
386
|
+
row: DraftsmanBoardViewMetadataBuilder.#integer(
|
|
387
|
+
fields.Row
|
|
388
|
+
),
|
|
389
|
+
column: DraftsmanBoardViewMetadataBuilder.#integer(
|
|
390
|
+
fields.Column
|
|
391
|
+
),
|
|
392
|
+
x: DraftsmanBoardViewMetadataBuilder.#number(fields.X),
|
|
393
|
+
y: DraftsmanBoardViewMetadataBuilder.#number(fields.Y),
|
|
394
|
+
width: DraftsmanBoardViewMetadataBuilder.#number(
|
|
395
|
+
fields.Width
|
|
396
|
+
),
|
|
397
|
+
height: DraftsmanBoardViewMetadataBuilder.#number(
|
|
398
|
+
fields.Height
|
|
399
|
+
),
|
|
400
|
+
scale: DraftsmanBoardViewMetadataBuilder.#number(
|
|
401
|
+
fields.Scale
|
|
402
|
+
),
|
|
403
|
+
fields
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Builds preservation diagnostics for unresolved cache references.
|
|
411
|
+
* @param {{ cacheLayers: object[], displayLayers: object[], cachePrimitives: object[], highlightGroups: object[], layerTiles: object[] }} input Parsed cache sections.
|
|
412
|
+
* @returns {object[]}
|
|
413
|
+
*/
|
|
414
|
+
static #diagnostics(input) {
|
|
415
|
+
const diagnostics = []
|
|
416
|
+
const cacheLayerIds = new Set(
|
|
417
|
+
input.cacheLayers.map((layer) => layer.id).filter(Boolean)
|
|
418
|
+
)
|
|
419
|
+
const highlightGroupIds = new Set(
|
|
420
|
+
input.highlightGroups.map((group) => group.id).filter(Boolean)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
for (const displayLayer of input.displayLayers) {
|
|
424
|
+
if (
|
|
425
|
+
!displayLayer.cacheLayerId ||
|
|
426
|
+
cacheLayerIds.has(displayLayer.cacheLayerId)
|
|
427
|
+
) {
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
diagnostics.push({
|
|
432
|
+
code: 'draftsman.board-view-cache.unresolved-display-layer-cache',
|
|
433
|
+
severity: 'warning',
|
|
434
|
+
message:
|
|
435
|
+
'Display-layer metadata references an unknown cached PCB layer.',
|
|
436
|
+
displayLayerId: displayLayer.id,
|
|
437
|
+
cacheLayerId: displayLayer.cacheLayerId
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const primitive of input.cachePrimitives) {
|
|
442
|
+
if (
|
|
443
|
+
!primitive.cacheLayerId ||
|
|
444
|
+
cacheLayerIds.has(primitive.cacheLayerId)
|
|
445
|
+
) {
|
|
446
|
+
continue
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
diagnostics.push({
|
|
450
|
+
code: 'draftsman.board-view-cache.unresolved-primitive-cache',
|
|
451
|
+
severity: 'warning',
|
|
452
|
+
message:
|
|
453
|
+
'Cached primitive metadata references an unknown cached PCB layer.',
|
|
454
|
+
primitiveId: primitive.id,
|
|
455
|
+
cacheLayerId: primitive.cacheLayerId
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
for (const tile of input.layerTiles) {
|
|
460
|
+
if (
|
|
461
|
+
!tile.highlightGroupId ||
|
|
462
|
+
highlightGroupIds.has(tile.highlightGroupId)
|
|
463
|
+
) {
|
|
464
|
+
continue
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
diagnostics.push({
|
|
468
|
+
code: 'draftsman.board-view-cache.unresolved-layer-tile-highlight-group',
|
|
469
|
+
severity: 'warning',
|
|
470
|
+
message:
|
|
471
|
+
'Layer tile metadata references an unknown highlight group.',
|
|
472
|
+
layerTileId: tile.id,
|
|
473
|
+
highlightGroupId: tile.highlightGroupId
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return diagnostics
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Extracts page ids and raw bodies for sidecar scans.
|
|
482
|
+
* @param {string} text Decoded text.
|
|
483
|
+
* @returns {{ index: number, id: string, body: string }[]}
|
|
484
|
+
*/
|
|
485
|
+
static #pageBlocks(text) {
|
|
486
|
+
const blocks = []
|
|
487
|
+
const pagePattern =
|
|
488
|
+
/<Page\b([^>]*)>([\s\S]*?)<\/Page>|<Page\b([^>]*)\/>/giu
|
|
489
|
+
let match = pagePattern.exec(text || '')
|
|
490
|
+
while (match) {
|
|
491
|
+
const fields = DraftsmanBoardViewMetadataBuilder.#attributes(
|
|
492
|
+
match[1] || match[3] || ''
|
|
493
|
+
)
|
|
494
|
+
blocks.push({
|
|
495
|
+
index: blocks.length,
|
|
496
|
+
id: fields.Id || fields.ID || '',
|
|
497
|
+
body: match[2] || ''
|
|
498
|
+
})
|
|
499
|
+
match = pagePattern.exec(text || '')
|
|
500
|
+
}
|
|
501
|
+
return blocks
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Extracts tag fields from a full container while ignoring page-contained
|
|
506
|
+
* copies of the same tags.
|
|
507
|
+
* @param {string} text Container text.
|
|
508
|
+
* @param {string[]} tagNames Tag names.
|
|
509
|
+
* @returns {Record<string, string>[]}
|
|
510
|
+
*/
|
|
511
|
+
static #globalTagFields(text, tagNames) {
|
|
512
|
+
const stripped = String(text || '').replace(
|
|
513
|
+
/<Page\b[^>]*>[\s\S]*?<\/Page>|<Page\b[^>]*\/>/giu,
|
|
514
|
+
''
|
|
515
|
+
)
|
|
516
|
+
return DraftsmanBoardViewMetadataBuilder.#tagFields(stripped, tagNames)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Extracts attributes from matching XML-like tags.
|
|
521
|
+
* @param {string} body Text body.
|
|
522
|
+
* @param {string[]} tagNames Tag names.
|
|
523
|
+
* @returns {Record<string, string>[]}
|
|
524
|
+
*/
|
|
525
|
+
static #tagFields(body, tagNames) {
|
|
526
|
+
const tags = tagNames
|
|
527
|
+
.map((tagName) => tagName.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'))
|
|
528
|
+
.join('|')
|
|
529
|
+
const pattern = new RegExp(
|
|
530
|
+
'<\\s*(' +
|
|
531
|
+
tags +
|
|
532
|
+
')\\b([^>]*)>([\\s\\S]*?)<\\/\\s*\\1\\s*>|<\\s*(' +
|
|
533
|
+
tags +
|
|
534
|
+
')\\b([^>]*)\\/>',
|
|
535
|
+
'giu'
|
|
536
|
+
)
|
|
537
|
+
const rows = []
|
|
538
|
+
let match = pattern.exec(body || '')
|
|
539
|
+
while (match) {
|
|
540
|
+
const attributes = DraftsmanBoardViewMetadataBuilder.#attributes(
|
|
541
|
+
match[2] || match[5] || ''
|
|
542
|
+
)
|
|
543
|
+
const text = String(match[3] || '').trim()
|
|
544
|
+
rows.push(
|
|
545
|
+
DraftsmanBoardViewMetadataBuilder.#stripEmpty({
|
|
546
|
+
...attributes,
|
|
547
|
+
value: attributes.Value || text || undefined
|
|
548
|
+
})
|
|
549
|
+
)
|
|
550
|
+
match = pattern.exec(body || '')
|
|
551
|
+
}
|
|
552
|
+
return rows
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Parses XML-like attributes.
|
|
557
|
+
* @param {string} text Raw attribute text.
|
|
558
|
+
* @returns {Record<string, string>}
|
|
559
|
+
*/
|
|
560
|
+
static #attributes(text) {
|
|
561
|
+
const fields = {}
|
|
562
|
+
const pattern = /([A-Za-z0-9_.:-]+)\s*=\s*("([^"]*)"|'([^']*)')/gu
|
|
563
|
+
let match = pattern.exec(text || '')
|
|
564
|
+
while (match) {
|
|
565
|
+
fields[match[1]] = match[3] ?? match[4] ?? ''
|
|
566
|
+
match = pattern.exec(text || '')
|
|
567
|
+
}
|
|
568
|
+
return fields
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Parses a numeric value.
|
|
573
|
+
* @param {string | undefined} value Raw value.
|
|
574
|
+
* @returns {number | undefined}
|
|
575
|
+
*/
|
|
576
|
+
static #number(value) {
|
|
577
|
+
const parsed = Number.parseFloat(String(value || '').trim())
|
|
578
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Parses an integer value.
|
|
583
|
+
* @param {string | undefined} value Raw value.
|
|
584
|
+
* @returns {number | undefined}
|
|
585
|
+
*/
|
|
586
|
+
static #integer(value) {
|
|
587
|
+
const parsed = Number.parseInt(String(value || '').trim(), 10)
|
|
588
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Parses an optional boolean value.
|
|
593
|
+
* @param {string | undefined} value Raw value.
|
|
594
|
+
* @returns {boolean | undefined}
|
|
595
|
+
*/
|
|
596
|
+
static #boolean(value) {
|
|
597
|
+
const normalized = String(value ?? '')
|
|
598
|
+
.trim()
|
|
599
|
+
.toLowerCase()
|
|
600
|
+
if (!normalized) return undefined
|
|
601
|
+
return ['true', 't', '1', 'yes'].includes(normalized)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Splits a comma/semicolon list.
|
|
606
|
+
* @param {string | undefined} value Raw list value.
|
|
607
|
+
* @returns {string[] | undefined}
|
|
608
|
+
*/
|
|
609
|
+
static #list(value) {
|
|
610
|
+
const items = String(value || '')
|
|
611
|
+
.split(/[;,]/u)
|
|
612
|
+
.map((item) => item.trim())
|
|
613
|
+
.filter(Boolean)
|
|
614
|
+
return items.length ? items : undefined
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Builds a stable layer key.
|
|
619
|
+
* @param {number | undefined} layerId Numeric layer id.
|
|
620
|
+
* @returns {string | undefined}
|
|
621
|
+
*/
|
|
622
|
+
static #layerKey(layerId) {
|
|
623
|
+
return Number.isFinite(layerId) ? 'L' + layerId : undefined
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Returns true when a cached primitive should be counted as drill-related.
|
|
628
|
+
* @param {object} primitive Cached primitive.
|
|
629
|
+
* @returns {boolean}
|
|
630
|
+
*/
|
|
631
|
+
static #isDrillPrimitive(primitive) {
|
|
632
|
+
return (
|
|
633
|
+
Boolean(primitive.holeKind) ||
|
|
634
|
+
/(?:drill|hole|via)/iu.test(String(primitive.primitiveKind || ''))
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Removes empty values from an object.
|
|
640
|
+
* @param {Record<string, unknown>} object Raw object.
|
|
641
|
+
* @returns {object}
|
|
642
|
+
*/
|
|
643
|
+
static #stripEmpty(object) {
|
|
644
|
+
return Object.fromEntries(
|
|
645
|
+
Object.entries(object).filter(
|
|
646
|
+
([, value]) =>
|
|
647
|
+
value !== undefined &&
|
|
648
|
+
value !== '' &&
|
|
649
|
+
(!Array.isArray(value) || value.length > 0)
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
}
|