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
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a deterministic schematic render-operation sidecar for SVG CI diffs.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicRenderOpsSidecarBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.render-ops.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a render-operation sidecar.
|
|
13
|
+
* @param {object} schematic Normalized schematic model.
|
|
14
|
+
* @param {{ contentHeight: number, profile?: string, semanticMetadata?: object }} options Build options.
|
|
15
|
+
* @returns {object}
|
|
16
|
+
*/
|
|
17
|
+
static build(schematic, options = {}) {
|
|
18
|
+
const contentHeight = Number(options.contentHeight || 0)
|
|
19
|
+
const elementByRecordId =
|
|
20
|
+
SchematicRenderOpsSidecarBuilder.#elementByRecordId(
|
|
21
|
+
options.semanticMetadata
|
|
22
|
+
)
|
|
23
|
+
const records = [
|
|
24
|
+
...SchematicRenderOpsSidecarBuilder.#lineRecords(
|
|
25
|
+
schematic?.lines || [],
|
|
26
|
+
contentHeight,
|
|
27
|
+
elementByRecordId
|
|
28
|
+
),
|
|
29
|
+
...SchematicRenderOpsSidecarBuilder.#rectangleRecords(
|
|
30
|
+
schematic?.rectangles || [],
|
|
31
|
+
contentHeight,
|
|
32
|
+
elementByRecordId
|
|
33
|
+
),
|
|
34
|
+
...SchematicRenderOpsSidecarBuilder.#roundedRectangleRecords(
|
|
35
|
+
schematic?.roundedRectangles || [],
|
|
36
|
+
contentHeight,
|
|
37
|
+
elementByRecordId
|
|
38
|
+
),
|
|
39
|
+
...SchematicRenderOpsSidecarBuilder.#ellipseRecords(
|
|
40
|
+
schematic?.ellipses || [],
|
|
41
|
+
contentHeight,
|
|
42
|
+
elementByRecordId
|
|
43
|
+
),
|
|
44
|
+
...SchematicRenderOpsSidecarBuilder.#arcRecords(
|
|
45
|
+
schematic?.arcs || [],
|
|
46
|
+
contentHeight,
|
|
47
|
+
elementByRecordId
|
|
48
|
+
),
|
|
49
|
+
...SchematicRenderOpsSidecarBuilder.#bezierRecords(
|
|
50
|
+
schematic?.beziers || [],
|
|
51
|
+
contentHeight,
|
|
52
|
+
elementByRecordId
|
|
53
|
+
),
|
|
54
|
+
...SchematicRenderOpsSidecarBuilder.#pieRecords(
|
|
55
|
+
schematic?.pies || [],
|
|
56
|
+
contentHeight,
|
|
57
|
+
elementByRecordId
|
|
58
|
+
),
|
|
59
|
+
...SchematicRenderOpsSidecarBuilder.#imageRecords(
|
|
60
|
+
schematic?.images || [],
|
|
61
|
+
contentHeight,
|
|
62
|
+
elementByRecordId
|
|
63
|
+
),
|
|
64
|
+
...SchematicRenderOpsSidecarBuilder.#textRecords(
|
|
65
|
+
schematic?.texts || [],
|
|
66
|
+
contentHeight,
|
|
67
|
+
elementByRecordId
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
schema: SchematicRenderOpsSidecarBuilder.SCHEMA_ID,
|
|
73
|
+
profile: String(options.profile || 'default'),
|
|
74
|
+
coordinateSpace: {
|
|
75
|
+
x: 'svg',
|
|
76
|
+
y: 'svg',
|
|
77
|
+
units: 'schematic-display-units'
|
|
78
|
+
},
|
|
79
|
+
summary: {
|
|
80
|
+
recordCount: records.length,
|
|
81
|
+
operationCount: records.reduce(
|
|
82
|
+
(count, record) => count + record.operations.length,
|
|
83
|
+
0
|
|
84
|
+
),
|
|
85
|
+
failedRecordCount: records.filter((record) => record.failed)
|
|
86
|
+
.length
|
|
87
|
+
},
|
|
88
|
+
records
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds line operation records.
|
|
94
|
+
* @param {object[]} lines Line rows.
|
|
95
|
+
* @param {number} contentHeight Render content height.
|
|
96
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
97
|
+
* @returns {object[]}
|
|
98
|
+
*/
|
|
99
|
+
static #lineRecords(lines, contentHeight, elementByRecordId) {
|
|
100
|
+
return (lines || []).map((line, index) => {
|
|
101
|
+
const recordId = SchematicRenderOpsSidecarBuilder.#recordId(
|
|
102
|
+
line,
|
|
103
|
+
'line',
|
|
104
|
+
index
|
|
105
|
+
)
|
|
106
|
+
return {
|
|
107
|
+
elementKey:
|
|
108
|
+
elementByRecordId.get(recordId)?.elementKey ||
|
|
109
|
+
'schematic-line-' + index,
|
|
110
|
+
recordId,
|
|
111
|
+
primitive: 'line',
|
|
112
|
+
operations: [
|
|
113
|
+
{
|
|
114
|
+
type: 'line',
|
|
115
|
+
x1: SchematicRenderOpsSidecarBuilder.#number(line.x1),
|
|
116
|
+
y1: SchematicRenderOpsSidecarBuilder.#y(
|
|
117
|
+
contentHeight,
|
|
118
|
+
line.y1
|
|
119
|
+
),
|
|
120
|
+
x2: SchematicRenderOpsSidecarBuilder.#number(line.x2),
|
|
121
|
+
y2: SchematicRenderOpsSidecarBuilder.#y(
|
|
122
|
+
contentHeight,
|
|
123
|
+
line.y2
|
|
124
|
+
),
|
|
125
|
+
stroke: line.color,
|
|
126
|
+
width: line.width
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds rectangle operation records.
|
|
135
|
+
* @param {object[]} rectangles Rectangle rows.
|
|
136
|
+
* @param {number} contentHeight Render content height.
|
|
137
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
138
|
+
* @returns {object[]}
|
|
139
|
+
*/
|
|
140
|
+
static #rectangleRecords(rectangles, contentHeight, elementByRecordId) {
|
|
141
|
+
return (rectangles || []).map((rectangle, index) => {
|
|
142
|
+
const recordId = SchematicRenderOpsSidecarBuilder.#recordId(
|
|
143
|
+
rectangle,
|
|
144
|
+
'rectangle',
|
|
145
|
+
index
|
|
146
|
+
)
|
|
147
|
+
return {
|
|
148
|
+
elementKey:
|
|
149
|
+
elementByRecordId.get(recordId)?.elementKey ||
|
|
150
|
+
'schematic-rectangle-' + index,
|
|
151
|
+
recordId,
|
|
152
|
+
primitive: 'rectangle',
|
|
153
|
+
operations: [
|
|
154
|
+
SchematicRenderOpsSidecarBuilder.#stripEmpty({
|
|
155
|
+
type: 'rectangle',
|
|
156
|
+
x: SchematicRenderOpsSidecarBuilder.#number(
|
|
157
|
+
rectangle.x
|
|
158
|
+
),
|
|
159
|
+
y: SchematicRenderOpsSidecarBuilder.#number(
|
|
160
|
+
contentHeight -
|
|
161
|
+
Number(rectangle.y || 0) -
|
|
162
|
+
Number(rectangle.height || 0)
|
|
163
|
+
),
|
|
164
|
+
width: rectangle.width,
|
|
165
|
+
height: rectangle.height,
|
|
166
|
+
stroke: rectangle.color,
|
|
167
|
+
fill: rectangle.fill,
|
|
168
|
+
widthStroke: rectangle.lineWidth
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Builds rounded-rectangle operation records.
|
|
177
|
+
* @param {object[]} rectangles Rounded rectangle rows.
|
|
178
|
+
* @param {number} contentHeight Render content height.
|
|
179
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
180
|
+
* @returns {object[]}
|
|
181
|
+
*/
|
|
182
|
+
static #roundedRectangleRecords(
|
|
183
|
+
rectangles,
|
|
184
|
+
contentHeight,
|
|
185
|
+
elementByRecordId
|
|
186
|
+
) {
|
|
187
|
+
return (rectangles || []).map((rectangle, index) =>
|
|
188
|
+
SchematicRenderOpsSidecarBuilder.#singleOperationRecord(
|
|
189
|
+
rectangle,
|
|
190
|
+
'rounded-rectangle',
|
|
191
|
+
index,
|
|
192
|
+
elementByRecordId,
|
|
193
|
+
{
|
|
194
|
+
type: 'rounded-rectangle',
|
|
195
|
+
x: SchematicRenderOpsSidecarBuilder.#number(rectangle.x),
|
|
196
|
+
y: SchematicRenderOpsSidecarBuilder.#boxY(
|
|
197
|
+
contentHeight,
|
|
198
|
+
rectangle.y,
|
|
199
|
+
rectangle.height
|
|
200
|
+
),
|
|
201
|
+
width: rectangle.width,
|
|
202
|
+
height: rectangle.height,
|
|
203
|
+
radius: rectangle.radius,
|
|
204
|
+
stroke: rectangle.color,
|
|
205
|
+
fill: rectangle.fill,
|
|
206
|
+
widthStroke: rectangle.lineWidth
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Builds ellipse operation records.
|
|
214
|
+
* @param {object[]} ellipses Ellipse rows.
|
|
215
|
+
* @param {number} contentHeight Render content height.
|
|
216
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
217
|
+
* @returns {object[]}
|
|
218
|
+
*/
|
|
219
|
+
static #ellipseRecords(ellipses, contentHeight, elementByRecordId) {
|
|
220
|
+
return (ellipses || []).map((ellipse, index) =>
|
|
221
|
+
SchematicRenderOpsSidecarBuilder.#singleOperationRecord(
|
|
222
|
+
ellipse,
|
|
223
|
+
'ellipse',
|
|
224
|
+
index,
|
|
225
|
+
elementByRecordId,
|
|
226
|
+
{
|
|
227
|
+
type: 'ellipse',
|
|
228
|
+
cx: SchematicRenderOpsSidecarBuilder.#number(ellipse.x),
|
|
229
|
+
cy: SchematicRenderOpsSidecarBuilder.#y(
|
|
230
|
+
contentHeight,
|
|
231
|
+
ellipse.y
|
|
232
|
+
),
|
|
233
|
+
rx: ellipse.radiusX,
|
|
234
|
+
ry: ellipse.radiusY,
|
|
235
|
+
stroke: ellipse.color,
|
|
236
|
+
fill: ellipse.fill,
|
|
237
|
+
widthStroke: ellipse.lineWidth
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Builds arc operation records.
|
|
245
|
+
* @param {object[]} arcs Arc rows.
|
|
246
|
+
* @param {number} contentHeight Render content height.
|
|
247
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
248
|
+
* @returns {object[]}
|
|
249
|
+
*/
|
|
250
|
+
static #arcRecords(arcs, contentHeight, elementByRecordId) {
|
|
251
|
+
return (arcs || []).map((arc, index) =>
|
|
252
|
+
SchematicRenderOpsSidecarBuilder.#singleOperationRecord(
|
|
253
|
+
arc,
|
|
254
|
+
'arc',
|
|
255
|
+
index,
|
|
256
|
+
elementByRecordId,
|
|
257
|
+
{
|
|
258
|
+
type: 'arc',
|
|
259
|
+
cx: SchematicRenderOpsSidecarBuilder.#number(arc.x),
|
|
260
|
+
cy: SchematicRenderOpsSidecarBuilder.#y(
|
|
261
|
+
contentHeight,
|
|
262
|
+
arc.y
|
|
263
|
+
),
|
|
264
|
+
radius: arc.radius,
|
|
265
|
+
startAngle: arc.startAngle,
|
|
266
|
+
endAngle: arc.endAngle,
|
|
267
|
+
stroke: arc.color,
|
|
268
|
+
width: arc.width
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Builds Bezier operation records.
|
|
276
|
+
* @param {object[]} beziers Bezier rows.
|
|
277
|
+
* @param {number} contentHeight Render content height.
|
|
278
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
279
|
+
* @returns {object[]}
|
|
280
|
+
*/
|
|
281
|
+
static #bezierRecords(beziers, contentHeight, elementByRecordId) {
|
|
282
|
+
return (beziers || []).map((bezier, index) =>
|
|
283
|
+
SchematicRenderOpsSidecarBuilder.#singleOperationRecord(
|
|
284
|
+
bezier,
|
|
285
|
+
'bezier',
|
|
286
|
+
index,
|
|
287
|
+
elementByRecordId,
|
|
288
|
+
{
|
|
289
|
+
type: 'bezier',
|
|
290
|
+
segments: (bezier.segments || []).map((segment) =>
|
|
291
|
+
SchematicRenderOpsSidecarBuilder.#bezierSegment(
|
|
292
|
+
segment,
|
|
293
|
+
contentHeight
|
|
294
|
+
)
|
|
295
|
+
),
|
|
296
|
+
stroke: bezier.color,
|
|
297
|
+
width: bezier.width
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Builds pie operation records.
|
|
305
|
+
* @param {object[]} pies Pie rows.
|
|
306
|
+
* @param {number} contentHeight Render content height.
|
|
307
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
308
|
+
* @returns {object[]}
|
|
309
|
+
*/
|
|
310
|
+
static #pieRecords(pies, contentHeight, elementByRecordId) {
|
|
311
|
+
return (pies || []).map((pie, index) =>
|
|
312
|
+
SchematicRenderOpsSidecarBuilder.#singleOperationRecord(
|
|
313
|
+
pie,
|
|
314
|
+
'pie',
|
|
315
|
+
index,
|
|
316
|
+
elementByRecordId,
|
|
317
|
+
{
|
|
318
|
+
type: 'pie',
|
|
319
|
+
cx: SchematicRenderOpsSidecarBuilder.#number(pie.x),
|
|
320
|
+
cy: SchematicRenderOpsSidecarBuilder.#y(
|
|
321
|
+
contentHeight,
|
|
322
|
+
pie.y
|
|
323
|
+
),
|
|
324
|
+
radiusX: pie.radius,
|
|
325
|
+
radiusY: pie.radiusY,
|
|
326
|
+
startAngle: pie.startAngle,
|
|
327
|
+
endAngle: pie.endAngle,
|
|
328
|
+
stroke: pie.color,
|
|
329
|
+
fill: pie.fill,
|
|
330
|
+
widthStroke: pie.lineWidth
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Builds image operation records.
|
|
338
|
+
* @param {object[]} images Image rows.
|
|
339
|
+
* @param {number} contentHeight Render content height.
|
|
340
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
341
|
+
* @returns {object[]}
|
|
342
|
+
*/
|
|
343
|
+
static #imageRecords(images, contentHeight, elementByRecordId) {
|
|
344
|
+
return (images || []).map((image, index) =>
|
|
345
|
+
SchematicRenderOpsSidecarBuilder.#singleOperationRecord(
|
|
346
|
+
image,
|
|
347
|
+
'image',
|
|
348
|
+
index,
|
|
349
|
+
elementByRecordId,
|
|
350
|
+
{
|
|
351
|
+
type: 'image',
|
|
352
|
+
x: SchematicRenderOpsSidecarBuilder.#number(image.x),
|
|
353
|
+
y: SchematicRenderOpsSidecarBuilder.#boxY(
|
|
354
|
+
contentHeight,
|
|
355
|
+
image.y,
|
|
356
|
+
image.height
|
|
357
|
+
),
|
|
358
|
+
width: image.width,
|
|
359
|
+
height: image.height,
|
|
360
|
+
nativeFormat: image.nativeFormat || image.format
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Builds text operation records.
|
|
368
|
+
* @param {object[]} texts Text rows.
|
|
369
|
+
* @param {number} contentHeight Render content height.
|
|
370
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
371
|
+
* @returns {object[]}
|
|
372
|
+
*/
|
|
373
|
+
static #textRecords(texts, contentHeight, elementByRecordId) {
|
|
374
|
+
return (texts || []).map((text, index) => {
|
|
375
|
+
const recordId = SchematicRenderOpsSidecarBuilder.#recordId(
|
|
376
|
+
text,
|
|
377
|
+
'text',
|
|
378
|
+
index
|
|
379
|
+
)
|
|
380
|
+
return {
|
|
381
|
+
elementKey:
|
|
382
|
+
elementByRecordId.get(recordId)?.elementKey ||
|
|
383
|
+
'schematic-text-' + index,
|
|
384
|
+
recordId,
|
|
385
|
+
primitive: text.recordType === '28' ? 'text-frame' : 'text',
|
|
386
|
+
operations: [
|
|
387
|
+
SchematicRenderOpsSidecarBuilder.#stripEmpty({
|
|
388
|
+
type: 'string',
|
|
389
|
+
x: SchematicRenderOpsSidecarBuilder.#number(text.x),
|
|
390
|
+
y: SchematicRenderOpsSidecarBuilder.#y(
|
|
391
|
+
contentHeight,
|
|
392
|
+
text.y
|
|
393
|
+
),
|
|
394
|
+
text: text.text,
|
|
395
|
+
fill: text.color,
|
|
396
|
+
fontFamily: text.fontFamily,
|
|
397
|
+
fontSize: text.fontSize
|
|
398
|
+
})
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Builds a semantic element lookup by record id.
|
|
406
|
+
* @param {object | undefined} semanticMetadata Semantic sidecar.
|
|
407
|
+
* @returns {Map<string, object>}
|
|
408
|
+
*/
|
|
409
|
+
static #elementByRecordId(semanticMetadata) {
|
|
410
|
+
return new Map(
|
|
411
|
+
(semanticMetadata?.elements || [])
|
|
412
|
+
.filter((element) => element.recordId)
|
|
413
|
+
.map((element) => [String(element.recordId), element])
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Builds a single-operation record for primitive sidecar rows.
|
|
419
|
+
* @param {object} primitive Source primitive.
|
|
420
|
+
* @param {string} primitiveKind Primitive kind.
|
|
421
|
+
* @param {number} index Primitive index.
|
|
422
|
+
* @param {Map<string, object>} elementByRecordId Semantic element lookup.
|
|
423
|
+
* @param {object} operation Render operation.
|
|
424
|
+
* @returns {object}
|
|
425
|
+
*/
|
|
426
|
+
static #singleOperationRecord(
|
|
427
|
+
primitive,
|
|
428
|
+
primitiveKind,
|
|
429
|
+
index,
|
|
430
|
+
elementByRecordId,
|
|
431
|
+
operation
|
|
432
|
+
) {
|
|
433
|
+
const recordId = SchematicRenderOpsSidecarBuilder.#recordId(
|
|
434
|
+
primitive,
|
|
435
|
+
primitiveKind,
|
|
436
|
+
index
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
elementKey:
|
|
441
|
+
elementByRecordId.get(recordId)?.elementKey ||
|
|
442
|
+
'schematic-' + primitiveKind + '-' + index,
|
|
443
|
+
recordId,
|
|
444
|
+
primitive: primitiveKind,
|
|
445
|
+
operations: [
|
|
446
|
+
SchematicRenderOpsSidecarBuilder.#stripEmpty(operation)
|
|
447
|
+
]
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Projects one Bezier segment into SVG coordinates.
|
|
453
|
+
* @param {object} segment Source segment.
|
|
454
|
+
* @param {number} contentHeight Render content height.
|
|
455
|
+
* @returns {object}
|
|
456
|
+
*/
|
|
457
|
+
static #bezierSegment(segment, contentHeight) {
|
|
458
|
+
return {
|
|
459
|
+
start: SchematicRenderOpsSidecarBuilder.#point(
|
|
460
|
+
segment.start,
|
|
461
|
+
contentHeight
|
|
462
|
+
),
|
|
463
|
+
control1: SchematicRenderOpsSidecarBuilder.#point(
|
|
464
|
+
segment.control1,
|
|
465
|
+
contentHeight
|
|
466
|
+
),
|
|
467
|
+
control2: SchematicRenderOpsSidecarBuilder.#point(
|
|
468
|
+
segment.control2,
|
|
469
|
+
contentHeight
|
|
470
|
+
),
|
|
471
|
+
end: SchematicRenderOpsSidecarBuilder.#point(
|
|
472
|
+
segment.end,
|
|
473
|
+
contentHeight
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Projects one point into SVG coordinates.
|
|
480
|
+
* @param {object} point Source point.
|
|
481
|
+
* @param {number} contentHeight Render content height.
|
|
482
|
+
* @returns {{ x: number, y: number }}
|
|
483
|
+
*/
|
|
484
|
+
static #point(point, contentHeight) {
|
|
485
|
+
return {
|
|
486
|
+
x: SchematicRenderOpsSidecarBuilder.#number(point?.x),
|
|
487
|
+
y: SchematicRenderOpsSidecarBuilder.#y(contentHeight, point?.y)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Returns a source record id or a deterministic fallback.
|
|
493
|
+
* @param {object} record Source record.
|
|
494
|
+
* @param {string} primitive Primitive kind.
|
|
495
|
+
* @param {number} index Primitive index.
|
|
496
|
+
* @returns {string}
|
|
497
|
+
*/
|
|
498
|
+
static #recordId(record, primitive, index) {
|
|
499
|
+
const candidate =
|
|
500
|
+
record?.recordId ?? record?.sourceRecordId ?? record?.sourceIndex
|
|
501
|
+
return candidate === undefined || candidate === null || candidate === ''
|
|
502
|
+
? 'schematic-' + primitive + '-' + index
|
|
503
|
+
: String(candidate)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Projects one schematic Y coordinate into SVG coordinates.
|
|
508
|
+
* @param {number} contentHeight Render content height.
|
|
509
|
+
* @param {unknown} y Source Y.
|
|
510
|
+
* @returns {number}
|
|
511
|
+
*/
|
|
512
|
+
static #y(contentHeight, y) {
|
|
513
|
+
return SchematicRenderOpsSidecarBuilder.#number(
|
|
514
|
+
contentHeight - Number(y || 0)
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Projects a source rectangle/image top-left corner into SVG coordinates.
|
|
520
|
+
* @param {number} contentHeight Render content height.
|
|
521
|
+
* @param {unknown} y Source Y.
|
|
522
|
+
* @param {unknown} height Source height.
|
|
523
|
+
* @returns {number}
|
|
524
|
+
*/
|
|
525
|
+
static #boxY(contentHeight, y, height) {
|
|
526
|
+
return SchematicRenderOpsSidecarBuilder.#number(
|
|
527
|
+
contentHeight - Number(y || 0) - Number(height || 0)
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Formats a stable numeric value.
|
|
533
|
+
* @param {unknown} value Source value.
|
|
534
|
+
* @returns {number}
|
|
535
|
+
*/
|
|
536
|
+
static #number(value) {
|
|
537
|
+
const parsed = Number(value || 0)
|
|
538
|
+
return Number.isInteger(parsed) ? parsed : Number(parsed.toFixed(6))
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Removes undefined and empty string fields.
|
|
543
|
+
* @param {Record<string, unknown>} value Source value.
|
|
544
|
+
* @returns {object}
|
|
545
|
+
*/
|
|
546
|
+
static #stripEmpty(value) {
|
|
547
|
+
return Object.fromEntries(
|
|
548
|
+
Object.entries(value || {}).filter(
|
|
549
|
+
([, entryValue]) =>
|
|
550
|
+
entryValue !== undefined && entryValue !== ''
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
@@ -19,6 +19,7 @@ import { SchematicRegionRenderer } from './SchematicRegionRenderer.mjs'
|
|
|
19
19
|
import { SchematicSheetSymbolRenderer } from './SchematicSheetSymbolRenderer.mjs'
|
|
20
20
|
import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
|
|
21
21
|
import { TextGeometrySidecarBuilder } from './TextGeometrySidecarBuilder.mjs'
|
|
22
|
+
import { SchematicRenderOpsSidecarBuilder } from './SchematicRenderOpsSidecarBuilder.mjs'
|
|
22
23
|
import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
|
|
23
24
|
|
|
24
25
|
const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
|
|
@@ -102,6 +103,15 @@ export class SchematicSvgRenderer {
|
|
|
102
103
|
semanticContext
|
|
103
104
|
)
|
|
104
105
|
: ''
|
|
106
|
+
const renderOperationsMarkup =
|
|
107
|
+
renderOptions.includeRenderOperationsSidecar
|
|
108
|
+
? SchematicSvgRenderer.#buildRenderOperationsMetadataMarkup(
|
|
109
|
+
renderedSchematic,
|
|
110
|
+
contentHeight,
|
|
111
|
+
semanticMetadata,
|
|
112
|
+
renderOptions
|
|
113
|
+
)
|
|
114
|
+
: ''
|
|
105
115
|
const drawableComponents = components.filter(
|
|
106
116
|
(component) =>
|
|
107
117
|
SchematicSvgRenderer.#isDrawableSchematicComponent(component) &&
|
|
@@ -544,6 +554,7 @@ export class SchematicSvgRenderer {
|
|
|
544
554
|
escapeHtml(JSON.stringify(semanticMetadata)) +
|
|
545
555
|
'</metadata>' +
|
|
546
556
|
textGeometryMarkup +
|
|
557
|
+
renderOperationsMarkup +
|
|
547
558
|
markerDefsMarkup +
|
|
548
559
|
'<g class="schematic-content"' +
|
|
549
560
|
' clip-path="url(#' +
|
|
@@ -625,7 +636,7 @@ export class SchematicSvgRenderer {
|
|
|
625
636
|
/**
|
|
626
637
|
* Normalizes schematic SVG export options.
|
|
627
638
|
* @param {Record<string, unknown>} options Raw render options.
|
|
628
|
-
* @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, includeTextGeometrySidecar: boolean }}
|
|
639
|
+
* @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, includeTextGeometrySidecar: boolean, includeRenderOperationsSidecar: boolean, renderOperationProfile: string }}
|
|
629
640
|
*/
|
|
630
641
|
static #normalizeRenderOptions(options) {
|
|
631
642
|
const includeViewBox =
|
|
@@ -639,7 +650,13 @@ export class SchematicSvgRenderer {
|
|
|
639
650
|
),
|
|
640
651
|
includeTextGeometrySidecar:
|
|
641
652
|
options?.includeTextGeometrySidecar === true ||
|
|
642
|
-
options?.textGeometry === 'sidecar'
|
|
653
|
+
options?.textGeometry === 'sidecar',
|
|
654
|
+
includeRenderOperationsSidecar:
|
|
655
|
+
options?.includeRenderOperationsSidecar === true ||
|
|
656
|
+
options?.renderOperations === 'sidecar',
|
|
657
|
+
renderOperationProfile: String(
|
|
658
|
+
options?.renderOperationProfile || 'default'
|
|
659
|
+
)
|
|
643
660
|
}
|
|
644
661
|
}
|
|
645
662
|
|
|
@@ -676,6 +693,35 @@ export class SchematicSvgRenderer {
|
|
|
676
693
|
)
|
|
677
694
|
}
|
|
678
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Builds optional render-operation metadata markup.
|
|
698
|
+
* @param {object} schematic Rendered schematic model.
|
|
699
|
+
* @param {number} contentHeight Render content height.
|
|
700
|
+
* @param {object} semanticMetadata Semantic metadata.
|
|
701
|
+
* @param {object} renderOptions Normalized render options.
|
|
702
|
+
* @returns {string}
|
|
703
|
+
*/
|
|
704
|
+
static #buildRenderOperationsMetadataMarkup(
|
|
705
|
+
schematic,
|
|
706
|
+
contentHeight,
|
|
707
|
+
semanticMetadata,
|
|
708
|
+
renderOptions
|
|
709
|
+
) {
|
|
710
|
+
const metadata = SchematicRenderOpsSidecarBuilder.build(schematic, {
|
|
711
|
+
contentHeight,
|
|
712
|
+
semanticMetadata,
|
|
713
|
+
profile: renderOptions.renderOperationProfile
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
return (
|
|
717
|
+
'<metadata id="schematic-render-operations" data-schema="' +
|
|
718
|
+
SchematicRenderOpsSidecarBuilder.SCHEMA_ID +
|
|
719
|
+
'">' +
|
|
720
|
+
escapeHtml(JSON.stringify(metadata)) +
|
|
721
|
+
'</metadata>'
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
|
|
679
725
|
/**
|
|
680
726
|
* Builds reusable SVG marker definitions for authored line endpoints.
|
|
681
727
|
* @param {{ startMarker?: object, endMarker?: object }[]} lines Drawable lines.
|