circuitjson-toolkit 1.0.16 → 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 (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. package/src/workers/parser.worker.mjs +59 -0
@@ -1,4 +1,5 @@
1
1
  import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
2
+ import { SchematicTableGeometry } from './SchematicTableGeometry.mjs'
2
3
 
3
4
  /**
4
5
  * Renders schematic table frames and cells from element-array metadata.
@@ -11,7 +12,7 @@ export class CircuitJsonSchematicTableSvgRenderer {
11
12
  * @returns {string}
12
13
  */
13
14
  static render(index, formatters) {
14
- const tables = CircuitJsonSchematicTableSvgRenderer.#tableModels(index)
15
+ const tables = SchematicTableGeometry.models(index)
15
16
  const tableMarkup = [...tables.values()]
16
17
  .map((table) =>
17
18
  CircuitJsonSchematicTableSvgRenderer.#tableElement(
@@ -38,62 +39,6 @@ export class CircuitJsonSchematicTableSvgRenderer {
38
39
  : ''
39
40
  }
40
41
 
41
- /**
42
- * Builds table models keyed by table id.
43
- * @param {{ elementsByType: Map<string, object[]> }} index Element index.
44
- * @returns {Map<string, object>}
45
- */
46
- static #tableModels(index) {
47
- const tables = new Map()
48
- for (const table of CircuitJsonSchematicTableSvgRenderer.#all(
49
- index,
50
- 'schematic_table'
51
- )) {
52
- const model =
53
- CircuitJsonSchematicTableSvgRenderer.#tableModel(table)
54
- if (model) tables.set(model.id, model)
55
- }
56
- return tables
57
- }
58
-
59
- /**
60
- * Builds one schematic table model.
61
- * @param {object} element Table element.
62
- * @returns {object | null}
63
- */
64
- static #tableModel(element) {
65
- const id = String(element.schematic_table_id || '').trim()
66
- const columns = CircuitJsonSchematicTableSvgRenderer.#lengths(
67
- element.column_widths || element.columnWidths || element.columns
68
- )
69
- const rows = CircuitJsonSchematicTableSvgRenderer.#lengths(
70
- element.row_heights || element.rowHeights || element.rows
71
- )
72
- const gridWidth = CircuitJsonSchematicTableSvgRenderer.#sum(columns)
73
- const gridHeight = CircuitJsonSchematicTableSvgRenderer.#sum(rows)
74
- const rect = CircuitJsonSchematicTableSvgRenderer.#rect(
75
- element,
76
- gridWidth,
77
- gridHeight
78
- )
79
- if (!id || !rect) return null
80
-
81
- return {
82
- id,
83
- element,
84
- ...rect,
85
- columns,
86
- rows,
87
- columnOffsets:
88
- CircuitJsonSchematicTableSvgRenderer.#offsets(columns),
89
- rowOffsets: CircuitJsonSchematicTableSvgRenderer.#offsets(rows),
90
- padding: CircuitJsonUnits.length(element.cell_padding, 0),
91
- borderWidth:
92
- CircuitJsonUnits.optionalLength(element.border_width) ??
93
- CircuitJsonUnits.optionalLength(element.borderWidth)
94
- }
95
- }
96
-
97
42
  /**
98
43
  * Renders one table frame.
99
44
  * @param {object} table Table model.
@@ -122,16 +67,9 @@ export class CircuitJsonSchematicTableSvgRenderer {
122
67
  * @returns {string}
123
68
  */
124
69
  static #tableCellElement(element, table, formatters) {
125
- const rect = CircuitJsonSchematicTableSvgRenderer.#cellRect(
126
- element,
127
- table
128
- )
70
+ const rect = SchematicTableGeometry.cellRect(element, table)
129
71
  if (!rect) return ''
130
- const text = CircuitJsonSchematicTableSvgRenderer.#textPosition(
131
- element,
132
- table,
133
- rect
134
- )
72
+ const text = SchematicTableGeometry.textPosition(element, table, rect)
135
73
  const fontSize = CircuitJsonUnits.optionalLength(element.font_size)
136
74
 
137
75
  return (
@@ -164,106 +102,6 @@ export class CircuitJsonSchematicTableSvgRenderer {
164
102
  )
165
103
  }
166
104
 
167
- /**
168
- * Resolves one cell rectangle.
169
- * @param {object} element Cell element.
170
- * @param {object | undefined} table Parent table model.
171
- * @returns {object | null}
172
- */
173
- static #cellRect(element, table) {
174
- if (table?.columns?.length && table?.rows?.length) {
175
- return CircuitJsonSchematicTableSvgRenderer.#gridCellRect(
176
- element,
177
- table
178
- )
179
- }
180
- return CircuitJsonSchematicTableSvgRenderer.#rect(element)
181
- }
182
-
183
- /**
184
- * Resolves one grid cell rectangle.
185
- * @param {object} element Cell element.
186
- * @param {object} table Parent table model.
187
- * @returns {object | null}
188
- */
189
- static #gridCellRect(element, table) {
190
- const row = CircuitJsonSchematicTableSvgRenderer.#index(
191
- element.row ?? element.row_index ?? element.rowIndex
192
- )
193
- const column = CircuitJsonSchematicTableSvgRenderer.#index(
194
- element.column ??
195
- element.col ??
196
- element.column_index ??
197
- element.columnIndex
198
- )
199
- if (row === null || column === null) return null
200
- const rowSpan = CircuitJsonSchematicTableSvgRenderer.#span(
201
- element.row_span ?? element.rowSpan
202
- )
203
- const columnSpan = CircuitJsonSchematicTableSvgRenderer.#span(
204
- element.col_span ?? element.column_span ?? element.colSpan
205
- )
206
- const width = CircuitJsonSchematicTableSvgRenderer.#sum(
207
- table.columns.slice(column, column + columnSpan)
208
- )
209
- const height = CircuitJsonSchematicTableSvgRenderer.#sum(
210
- table.rows.slice(row, row + rowSpan)
211
- )
212
- if (width <= 0 || height <= 0) return null
213
-
214
- return {
215
- x: table.x + (table.columnOffsets[column] || 0),
216
- y: table.y + (table.rowOffsets[row] || 0),
217
- width,
218
- height
219
- }
220
- }
221
-
222
- /**
223
- * Resolves one text position inside a cell.
224
- * @param {object} element Cell element.
225
- * @param {object | undefined} table Parent table model.
226
- * @param {object} rect Cell rectangle.
227
- * @returns {{ x: number, y: number, anchor: string, baseline: string }}
228
- */
229
- static #textPosition(element, table, rect) {
230
- const padding = Number(table?.padding || 0)
231
- const horizontal = String(
232
- element.horizontal_align || element.horizontalAlign || ''
233
- ).toLowerCase()
234
- const vertical = String(
235
- element.vertical_align || element.verticalAlign || ''
236
- ).toLowerCase()
237
- const x =
238
- horizontal === 'left' || horizontal === 'start'
239
- ? rect.x + padding
240
- : horizontal === 'right' || horizontal === 'end'
241
- ? rect.x + rect.width - padding
242
- : rect.x + rect.width / 2
243
- const y =
244
- vertical === 'top'
245
- ? rect.y + padding
246
- : vertical === 'bottom'
247
- ? rect.y + rect.height - padding
248
- : rect.y + rect.height / 2
249
- return {
250
- x,
251
- y,
252
- anchor:
253
- horizontal === 'left' || horizontal === 'start'
254
- ? 'start'
255
- : horizontal === 'right' || horizontal === 'end'
256
- ? 'end'
257
- : 'middle',
258
- baseline:
259
- vertical === 'top'
260
- ? 'hanging'
261
- : vertical === 'bottom'
262
- ? 'text-after-edge'
263
- : 'central'
264
- }
265
- }
266
-
267
105
  /**
268
106
  * Builds SVG rect attributes.
269
107
  * @param {object} rect Rect model.
@@ -301,132 +139,6 @@ export class CircuitJsonSchematicTableSvgRenderer {
301
139
  : ' font-size="' + formatters.formatNumber(fontSize) + '"'
302
140
  }
303
141
 
304
- /**
305
- * Resolves a rectangle from center/size or anchor metadata.
306
- * @param {object} element Element row.
307
- * @param {number} [fallbackWidth] Fallback width.
308
- * @param {number} [fallbackHeight] Fallback height.
309
- * @returns {object | null}
310
- */
311
- static #rect(element, fallbackWidth = 0, fallbackHeight = 0) {
312
- const size = CircuitJsonSchematicTableSvgRenderer.#size(
313
- element,
314
- fallbackWidth,
315
- fallbackHeight
316
- )
317
- const anchor =
318
- CircuitJsonUnits.optionalPoint(element.anchor_position) || null
319
- if (size && anchor) {
320
- return CircuitJsonSchematicTableSvgRenderer.#anchoredRect(
321
- anchor,
322
- size,
323
- element.anchor
324
- )
325
- }
326
- const center = CircuitJsonUnits.optionalPoint(element.center || element)
327
- if (!center || !size) return null
328
- return {
329
- x: center.x - size.width / 2,
330
- y: center.y - size.height / 2,
331
- ...size
332
- }
333
- }
334
-
335
- /**
336
- * Resolves a width/height pair.
337
- * @param {object} element Element row.
338
- * @param {number} fallbackWidth Fallback width.
339
- * @param {number} fallbackHeight Fallback height.
340
- * @returns {{ width: number, height: number } | null}
341
- */
342
- static #size(element, fallbackWidth, fallbackHeight) {
343
- const size = CircuitJsonUnits.optionalSize(element.size || element)
344
- const width = size?.width ?? fallbackWidth
345
- const height = size?.height ?? fallbackHeight
346
- if (width <= 0 || height <= 0) return null
347
- return { width, height }
348
- }
349
-
350
- /**
351
- * Builds an anchored rectangle.
352
- * @param {{ x: number, y: number }} point Anchor point.
353
- * @param {{ width: number, height: number }} size Size.
354
- * @param {unknown} anchor Anchor name.
355
- * @returns {object}
356
- */
357
- static #anchoredRect(point, size, anchor) {
358
- const text = String(anchor || 'top_left').toLowerCase()
359
- const x = text.includes('right')
360
- ? point.x - size.width
361
- : text.includes('center')
362
- ? point.x - size.width / 2
363
- : point.x
364
- const y = text.includes('bottom')
365
- ? point.y - size.height
366
- : text.includes('middle') || text === 'center'
367
- ? point.y - size.height / 2
368
- : point.y
369
- return { x, y, ...size }
370
- }
371
-
372
- /**
373
- * Resolves an array of positive lengths.
374
- * @param {unknown} value Length array candidate.
375
- * @returns {number[]}
376
- */
377
- static #lengths(value) {
378
- return (Array.isArray(value) ? value : [])
379
- .map((entry) =>
380
- CircuitJsonUnits.optionalLength(
381
- entry?.width ?? entry?.height ?? entry
382
- )
383
- )
384
- .filter((entry) => entry !== null && entry > 0)
385
- }
386
-
387
- /**
388
- * Builds cumulative offsets for a length list.
389
- * @param {number[]} values Length values.
390
- * @returns {number[]}
391
- */
392
- static #offsets(values) {
393
- let total = 0
394
- return values.map((value) => {
395
- const offset = total
396
- total += value
397
- return offset
398
- })
399
- }
400
-
401
- /**
402
- * Sums numeric values.
403
- * @param {number[]} values Values.
404
- * @returns {number}
405
- */
406
- static #sum(values) {
407
- return values.reduce((total, value) => total + value, 0)
408
- }
409
-
410
- /**
411
- * Resolves a zero-based row or column index.
412
- * @param {unknown} value Index candidate.
413
- * @returns {number | null}
414
- */
415
- static #index(value) {
416
- const number = Number(value)
417
- return Number.isInteger(number) && number >= 0 ? number : null
418
- }
419
-
420
- /**
421
- * Resolves a positive row or column span.
422
- * @param {unknown} value Span candidate.
423
- * @returns {number}
424
- */
425
- static #span(value) {
426
- const number = Number(value)
427
- return Number.isInteger(number) && number > 0 ? number : 1
428
- }
429
-
430
142
  /**
431
143
  * Returns indexed element rows.
432
144
  * @param {{ elementsByType: Map<string, object[]> }} index Element index.
@@ -0,0 +1,41 @@
1
+ import { PcbRenderPlan } from '../core/rendering/PcbRenderPlan.mjs'
2
+ import { CircuitJsonPcbSvgRenderer } from './CircuitJsonPcbSvgRenderer.mjs'
3
+
4
+ /**
5
+ * Renders canonical CircuitJSON PCB documents from one reusable plan.
6
+ */
7
+ export class PcbSvgRenderer {
8
+ /**
9
+ * Renders one combined PCB SVG.
10
+ * @param {unknown} document DocumentResult, CircuitJSON model, or context.
11
+ * @param {Record<string, any>} [options] Canonical render options.
12
+ * @returns {string} SVG markup.
13
+ */
14
+ static render(document, options = {}) {
15
+ const plan = PcbRenderPlan.prepare(document, options)
16
+ return CircuitJsonPcbSvgRenderer.renderPlan(plan)
17
+ }
18
+
19
+ /**
20
+ * Renders every selected PCB layer from one primitive preparation.
21
+ * @param {unknown} document DocumentResult, CircuitJSON model, or context.
22
+ * @param {Record<string, any>} [options] Canonical render options.
23
+ * @returns {{ schema: 'ecad-toolkit.render-set.v1', items: object[], diagnostics: object[], statistics: Record<string, number> }} Render set.
24
+ */
25
+ static renderLayers(document, options = {}) {
26
+ const plan = PcbRenderPlan.prepare(document, options)
27
+ return {
28
+ schema: 'ecad-toolkit.render-set.v1',
29
+ items: plan.layers.map((layer) => ({
30
+ id: layer.id,
31
+ side: plan.side,
32
+ layerIds: [layer.id],
33
+ svg: CircuitJsonPcbSvgRenderer.renderPlan(plan, {
34
+ layerIds: [layer.id]
35
+ })
36
+ })),
37
+ diagnostics: structuredClone(plan.diagnostics),
38
+ statistics: { ...plan.statistics }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,26 @@
1
+ const NUMERIC_COLOR =
2
+ /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(\s*[-+.%0-9,\s/]+\)$/iu
3
+
4
+ /**
5
+ * Normalizes literal SVG paint without admitting resource URL contexts.
6
+ */
7
+ export class SafeSvgPaint {
8
+ /**
9
+ * Returns a bounded literal CSS/SVG color or an empty string.
10
+ * @param {unknown} value Paint candidate.
11
+ * @returns {string} Safe literal paint.
12
+ */
13
+ static color(value) {
14
+ const text = String(value ?? '').trim()
15
+ if (!text || text.length > 128) return ''
16
+ if (
17
+ /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/iu.test(
18
+ text
19
+ )
20
+ ) {
21
+ return text
22
+ }
23
+ if (/^[a-z]{3,24}$/iu.test(text)) return text
24
+ return NUMERIC_COLOR.test(text) ? text : ''
25
+ }
26
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Sanitizes and escapes untrusted scalar data for deterministic XML output.
3
+ */
4
+ export class SafeXmlText {
5
+ /**
6
+ * Escapes XML text and attribute data, including quotation marks.
7
+ * @param {unknown} value Scalar value.
8
+ * @returns {string} Well-formed escaped XML.
9
+ */
10
+ static escape(value) {
11
+ return SafeXmlText.#escape(value, true)
12
+ }
13
+
14
+ /**
15
+ * Escapes XML character data while leaving quotation marks literal.
16
+ * @param {unknown} value Scalar value.
17
+ * @returns {string} Well-formed escaped XML text.
18
+ */
19
+ static escapeText(value) {
20
+ return SafeXmlText.#escape(value, false)
21
+ }
22
+
23
+ /**
24
+ * Escapes allowed characters and replaces invalid XML scalars.
25
+ * @param {unknown} value Scalar value.
26
+ * @param {boolean} quote Whether quotation marks require escaping.
27
+ * @returns {string} Safe XML data.
28
+ */
29
+ static #escape(value, quote) {
30
+ let result = ''
31
+ for (const candidate of String(value ?? '')) {
32
+ const character = SafeXmlText.#isAllowed(candidate)
33
+ ? candidate
34
+ : '\ufffd'
35
+ if (character === '&') result += '&amp;'
36
+ else if (character === '<') result += '&lt;'
37
+ else if (character === '>') result += '&gt;'
38
+ else if (quote && character === '"') result += '&quot;'
39
+ else result += character
40
+ }
41
+ return result
42
+ }
43
+
44
+ /**
45
+ * Returns whether one Unicode scalar belongs to the XML 1.0 Char set.
46
+ * @param {string} character One iterated character.
47
+ * @returns {boolean} Whether XML permits the character.
48
+ */
49
+ static #isAllowed(character) {
50
+ const codePoint = character.codePointAt(0)
51
+ return (
52
+ codePoint === 0x09 ||
53
+ codePoint === 0x0a ||
54
+ codePoint === 0x0d ||
55
+ (codePoint >= 0x20 && codePoint <= 0xd7ff) ||
56
+ (codePoint >= 0xe000 && codePoint <= 0xfffd) ||
57
+ (codePoint >= 0x10000 && codePoint <= 0x10ffff)
58
+ )
59
+ }
60
+ }