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.
Files changed (63) hide show
  1. package/docs/api.md +6 -2
  2. package/docs/model-format.md +29 -4
  3. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +4 -0
  4. package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
  5. package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
  6. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +132 -1
  7. package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
  8. package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
  9. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
  10. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +692 -2
  11. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  12. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  13. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  14. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  15. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  16. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  17. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  18. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  19. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  20. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  21. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  22. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  23. package/package.json +1 -1
  24. package/src/core/altium/AltiumParser.mjs +7 -2
  25. package/src/core/altium/CiArtifactBundleBuilder.mjs +16 -5
  26. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  27. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  28. package/src/core/altium/DraftsmanDigestParser.mjs +246 -7
  29. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  30. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  31. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  32. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  33. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  34. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  35. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  36. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  37. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  38. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  39. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  40. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  41. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  42. package/src/core/altium/PcbModelParser.mjs +182 -18
  43. package/src/core/altium/PcbPickPlacePositionResolver.mjs +3 -0
  44. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  45. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  46. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  47. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  48. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  49. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  50. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  51. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  52. package/src/core/altium/PrjPcbModelParser.mjs +45 -0
  53. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  54. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  55. package/src/core/altium/ProjectDesignBundleBuilder.mjs +61 -2
  56. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  57. package/src/core/altium/SvgModelCrossLinkValidator.mjs +35 -2
  58. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +164 -0
  59. package/src/parser.mjs +15 -0
  60. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  61. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  62. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  63. 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.