circuitjson-toolkit 1.0.17 → 1.1.1

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/NOTICE.md +31 -0
  2. package/README.md +274 -107
  3. package/docs/api.md +507 -96
  4. package/docs/capabilities.md +70 -0
  5. package/docs/migration/behaviors.md +45 -0
  6. package/docs/migration/parser.md +60 -0
  7. package/docs/migration/renderers.md +515 -0
  8. package/docs/migration/root.md +740 -0
  9. package/docs/migration.md +126 -0
  10. package/docs/model-format.md +175 -57
  11. package/docs/provenance.md +206 -0
  12. package/docs/release-notes-v1.1.0.md +154 -0
  13. package/docs/release-notes-v1.1.1.md +24 -0
  14. package/docs/testing.md +122 -7
  15. package/package.json +32 -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 +999 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +650 -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
@@ -0,0 +1,304 @@
1
+ import { CircuitJsonDocumentContext } from '../core/context/CircuitJsonDocumentContext.mjs'
2
+ import { CanonicalBomRows } from '../core/rendering/CanonicalBomRows.mjs'
3
+ import { CanonicalBomOrder } from '../core/rendering/CanonicalBomOrder.mjs'
4
+ import { CanonicalRenderOptions } from '../core/rendering/CanonicalRenderOptions.mjs'
5
+ import { SafeXmlText } from './SafeXmlText.mjs'
6
+
7
+ const PREPARED_BOM_ROWS = new WeakMap()
8
+ const PREPARED_BOM_TABLES = new WeakMap()
9
+
10
+ /**
11
+ * Renders canonical grouped BOM data as deterministic HTML.
12
+ */
13
+ export class BomTableRenderer {
14
+ /**
15
+ * Renders a DocumentInput or existing grouped BOM rows.
16
+ * @param {unknown} document DocumentInput or grouped BOM rows.
17
+ * @param {Record<string, any>} [options] Canonical render options.
18
+ * @returns {string} Deterministic BOM HTML.
19
+ */
20
+ static render(document, options = {}) {
21
+ const normalized = CanonicalRenderOptions.normalize(options)
22
+ CanonicalRenderOptions.requireCanonicalFidelity(normalized.fidelity)
23
+ const legacyRows = BomTableRenderer.#legacyRows(document)
24
+ if (legacyRows) {
25
+ return BomTableRenderer.#renderRows(legacyRows)
26
+ }
27
+ const context = CircuitJsonDocumentContext.prepare(document)
28
+ const rows = context.getOrCreateDerived('render', 'bom-rows-v1', () => {
29
+ const built = CanonicalBomRows.build(context.model)
30
+ BomTableRenderer.#freeze(built)
31
+ PREPARED_BOM_ROWS.set(built, context)
32
+ return built
33
+ })
34
+ if (!Array.isArray(rows) || PREPARED_BOM_ROWS.get(rows) !== context) {
35
+ throw CanonicalRenderOptions.error(
36
+ 'BOM rendering encountered a derived-row cache collision.'
37
+ )
38
+ }
39
+ const table = context.getOrCreateDerived(
40
+ 'render',
41
+ 'bom-table-v1',
42
+ () => {
43
+ const entry = Object.freeze({
44
+ html: BomTableRenderer.#renderRows(rows)
45
+ })
46
+ PREPARED_BOM_TABLES.set(entry, context)
47
+ return entry
48
+ }
49
+ )
50
+ if (
51
+ !table ||
52
+ typeof table !== 'object' ||
53
+ PREPARED_BOM_TABLES.get(table) !== context
54
+ ) {
55
+ throw CanonicalRenderOptions.error(
56
+ 'BOM rendering encountered a derived-table cache collision.'
57
+ )
58
+ }
59
+ return table.html
60
+ }
61
+
62
+ /**
63
+ * Copies a legacy grouped-row input through safe own data descriptors.
64
+ * @param {unknown} value Input candidate.
65
+ * @returns {object[] | null} Safe grouped rows or null for DocumentInput.
66
+ */
67
+ static #legacyRows(value) {
68
+ const entries = BomTableRenderer.#arrayEntries(value)
69
+ if (!entries?.length) return null
70
+ const descriptors = entries.map((row) =>
71
+ BomTableRenderer.#rowDescriptors(row)
72
+ )
73
+ if (descriptors.some((row) => row?.type)) return null
74
+ if (descriptors.some((row) => !row)) return null
75
+ return descriptors
76
+ .map((row) => BomTableRenderer.#copyRow(row))
77
+ .sort(CanonicalBomOrder.compareRows)
78
+ }
79
+
80
+ /**
81
+ * Copies top-level array entries through own data descriptors.
82
+ * @param {unknown} value Array candidate.
83
+ * @returns {unknown[] | null} Safe entries or null.
84
+ */
85
+ static #arrayEntries(value) {
86
+ let isArray
87
+ let descriptors
88
+ try {
89
+ isArray = Array.isArray(value)
90
+ if (!isArray || Object.getPrototypeOf(value) !== Array.prototype) {
91
+ return null
92
+ }
93
+ descriptors = Object.getOwnPropertyDescriptors(value)
94
+ } catch {
95
+ return null
96
+ }
97
+ const length = descriptors.length?.value
98
+ if (!Number.isSafeInteger(length) || length < 0 || length > 1000000) {
99
+ return null
100
+ }
101
+ const result = []
102
+ for (let index = 0; index < length; index += 1) {
103
+ const descriptor = descriptors[String(index)]
104
+ if (!descriptor || descriptor.get || descriptor.set) {
105
+ throw CanonicalRenderOptions.error(
106
+ 'BOM input arrays must contain only plain data entries.'
107
+ )
108
+ }
109
+ result.push(descriptor.value)
110
+ }
111
+ return result
112
+ }
113
+
114
+ /**
115
+ * Inspects one possible legacy row without reading its fields.
116
+ * @param {unknown} row Row candidate.
117
+ * @returns {Record<string, PropertyDescriptor> | null} Descriptors or null.
118
+ */
119
+ static #rowDescriptors(row) {
120
+ if (!row || typeof row !== 'object' || Array.isArray(row)) return null
121
+ try {
122
+ const prototype = Object.getPrototypeOf(row)
123
+ if (prototype !== Object.prototype && prototype !== null)
124
+ return null
125
+ return Object.getOwnPropertyDescriptors(row)
126
+ } catch {
127
+ return null
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Copies and validates one legacy grouped BOM row.
133
+ * @param {Record<string, PropertyDescriptor>} descriptors Row descriptors.
134
+ * @returns {object} Safe grouped row.
135
+ */
136
+ static #copyRow(descriptors) {
137
+ for (const descriptor of Object.values(descriptors)) {
138
+ if (descriptor.get || descriptor.set) {
139
+ throw CanonicalRenderOptions.error(
140
+ 'BOM rows must contain only plain data fields.'
141
+ )
142
+ }
143
+ }
144
+ const designators = BomTableRenderer.#stringArray(
145
+ descriptors.designators?.value
146
+ )
147
+ if (!designators?.length) {
148
+ throw CanonicalRenderOptions.error(
149
+ 'BOM rows require a non-empty designators array.'
150
+ )
151
+ }
152
+ const quantity = descriptors.quantity?.value
153
+ if (
154
+ quantity !== undefined &&
155
+ (!Number.isSafeInteger(quantity) || quantity < 0)
156
+ ) {
157
+ throw CanonicalRenderOptions.error(
158
+ 'BOM row quantity must be a nonnegative integer.'
159
+ )
160
+ }
161
+ return {
162
+ designators: [...new Set(designators)].sort(
163
+ CanonicalBomOrder.compareDesignators
164
+ ),
165
+ quantity: quantity ?? designators.length,
166
+ value: BomTableRenderer.#scalar(descriptors.value?.value),
167
+ pattern: BomTableRenderer.#scalar(descriptors.pattern?.value),
168
+ source: BomTableRenderer.#scalar(descriptors.source?.value)
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Copies a dense plain string array without indexed property reads.
174
+ * @param {unknown} value Array candidate.
175
+ * @returns {string[] | null} Safe string array.
176
+ */
177
+ static #stringArray(value) {
178
+ let descriptors
179
+ try {
180
+ if (!Array.isArray(value)) return null
181
+ if (Object.getPrototypeOf(value) !== Array.prototype) return null
182
+ descriptors = Object.getOwnPropertyDescriptors(value)
183
+ } catch {
184
+ return null
185
+ }
186
+ const length = descriptors.length?.value
187
+ if (!Number.isSafeInteger(length) || length < 1 || length > 4096) {
188
+ return null
189
+ }
190
+ const result = []
191
+ for (let index = 0; index < length; index += 1) {
192
+ const descriptor = descriptors[String(index)]
193
+ if (
194
+ !descriptor ||
195
+ descriptor.get ||
196
+ descriptor.set ||
197
+ typeof descriptor.value !== 'string' ||
198
+ !descriptor.value.trim()
199
+ ) {
200
+ return null
201
+ }
202
+ result.push(descriptor.value.trim())
203
+ }
204
+ return result
205
+ }
206
+
207
+ /**
208
+ * Normalizes an optional primitive BOM display value.
209
+ * @param {unknown} value Value candidate.
210
+ * @returns {string | number | boolean} Safe scalar.
211
+ */
212
+ static #scalar(value) {
213
+ if (value === undefined || value === null) return ''
214
+ if (!['string', 'number', 'boolean'].includes(typeof value)) {
215
+ throw CanonicalRenderOptions.error(
216
+ 'BOM row display fields must be scalar values.'
217
+ )
218
+ }
219
+ if (typeof value === 'number' && !Number.isFinite(value)) {
220
+ throw CanonicalRenderOptions.error(
221
+ 'BOM row display fields must be finite.'
222
+ )
223
+ }
224
+ return value
225
+ }
226
+
227
+ /**
228
+ * Iteratively freezes renderer-owned BOM rows without recursion limits.
229
+ * @param {unknown} value Renderer-owned value.
230
+ * @returns {unknown} Frozen value.
231
+ */
232
+ static #freeze(value) {
233
+ const pending = [value]
234
+ const visited = new WeakSet()
235
+ while (pending.length) {
236
+ const current = pending.pop()
237
+ if (
238
+ !current ||
239
+ typeof current !== 'object' ||
240
+ visited.has(current)
241
+ ) {
242
+ continue
243
+ }
244
+ visited.add(current)
245
+ for (const descriptor of Object.values(
246
+ Object.getOwnPropertyDescriptors(current)
247
+ )) {
248
+ if ('value' in descriptor) pending.push(descriptor.value)
249
+ }
250
+ Object.freeze(current)
251
+ }
252
+ return value
253
+ }
254
+
255
+ /**
256
+ * Renders normalized grouped rows.
257
+ * @param {object[]} rows Grouped BOM rows.
258
+ * @returns {string} Deterministic BOM HTML.
259
+ */
260
+ static #renderRows(rows) {
261
+ if (!rows.length) {
262
+ return '<section class="bom-empty">No BOM rows recovered.</section>'
263
+ }
264
+ return (
265
+ '<table class="bom-table"><thead><tr>' +
266
+ '<th>Designators</th><th>Qty</th><th>Value</th><th>Pattern</th><th>Source</th>' +
267
+ '</tr></thead><tbody>' +
268
+ rows.map((row) => BomTableRenderer.#renderRow(row)).join('') +
269
+ '</tbody></table>'
270
+ )
271
+ }
272
+
273
+ /**
274
+ * Renders one escaped BOM row.
275
+ * @param {object} row Grouped BOM row.
276
+ * @returns {string} Table row HTML.
277
+ */
278
+ static #renderRow(row) {
279
+ return (
280
+ '<tr><td>' +
281
+ BomTableRenderer.#escape((row.designators || []).join(', ')) +
282
+ '</td><td>' +
283
+ BomTableRenderer.#escape(
284
+ row.quantity ?? row.designators?.length ?? 0
285
+ ) +
286
+ '</td><td>' +
287
+ BomTableRenderer.#escape(row.value ?? '') +
288
+ '</td><td>' +
289
+ BomTableRenderer.#escape(row.pattern ?? '') +
290
+ '</td><td>' +
291
+ BomTableRenderer.#escape(row.source ?? '') +
292
+ '</td></tr>'
293
+ )
294
+ }
295
+
296
+ /**
297
+ * Escapes HTML text.
298
+ * @param {unknown} value Raw value.
299
+ * @returns {string} Escaped text.
300
+ */
301
+ static #escape(value) {
302
+ return SafeXmlText.escape(value)
303
+ }
304
+ }
@@ -0,0 +1,80 @@
1
+ import { SafeXmlText } from './SafeXmlText.mjs'
2
+
3
+ /**
4
+ * Serializes every prepared PCB board substrate in stable primitive order.
5
+ */
6
+ export class CircuitJsonPcbBoardSvgRenderer {
7
+ /**
8
+ * Renders all board primitives in one prepared primitive model.
9
+ * @param {{ primitives?: object[] }} model Primitive model.
10
+ * @returns {string} Board substrate markup.
11
+ */
12
+ static render(model) {
13
+ const markup = []
14
+ for (const board of model.primitives || []) {
15
+ if (board.kind !== 'board' || !board.bounds) continue
16
+ markup.push(CircuitJsonPcbBoardSvgRenderer.#renderBoard(board))
17
+ }
18
+ return markup.join('')
19
+ }
20
+
21
+ /**
22
+ * Renders one polygonal or rectangular board primitive.
23
+ * @param {object} board Board primitive.
24
+ * @returns {string} Board markup.
25
+ */
26
+ static #renderBoard(board) {
27
+ if (Array.isArray(board.points) && board.points.length >= 3) {
28
+ return (
29
+ '<polygon class="pcb-board" data-layer="board" points="' +
30
+ SafeXmlText.escape(
31
+ CircuitJsonPcbBoardSvgRenderer.#points(board.points)
32
+ ) +
33
+ '"></polygon>'
34
+ )
35
+ }
36
+ return (
37
+ '<rect class="pcb-board" x="' +
38
+ CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.minX) +
39
+ '" y="' +
40
+ CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.minY) +
41
+ '" width="' +
42
+ CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.width) +
43
+ '" height="' +
44
+ CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.height) +
45
+ '" rx="' +
46
+ CircuitJsonPcbBoardSvgRenderer.#number(
47
+ Math.min(board.bounds.width, board.bounds.height) * 0.018
48
+ ) +
49
+ '" data-layer="board"></rect>'
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Formats polygon points for an SVG attribute.
55
+ * @param {{ x: number, y: number }[]} points Polygon points.
56
+ * @returns {string} SVG point list.
57
+ */
58
+ static #points(points) {
59
+ return points
60
+ .map(
61
+ (point) =>
62
+ CircuitJsonPcbBoardSvgRenderer.#number(point.x) +
63
+ ',' +
64
+ CircuitJsonPcbBoardSvgRenderer.#number(point.y)
65
+ )
66
+ .join(' ')
67
+ }
68
+
69
+ /**
70
+ * Formats one finite SVG number.
71
+ * @param {unknown} value Number candidate.
72
+ * @returns {string} SVG number.
73
+ */
74
+ static #number(value) {
75
+ const number = Number(value)
76
+ return Number.isFinite(number)
77
+ ? Number(number.toFixed(6)).toString()
78
+ : '0'
79
+ }
80
+ }
@@ -1,3 +1,5 @@
1
+ import { SafeXmlText } from './SafeXmlText.mjs'
2
+
1
3
  /**
2
4
  * Renders shared SVG attributes for CircuitJSON PCB primitives.
3
5
  */
@@ -119,10 +121,6 @@ export class CircuitJsonPcbPrimitiveAttributeRenderer {
119
121
  * @returns {string}
120
122
  */
121
123
  static #escapeHtml(value) {
122
- return String(value ?? '')
123
- .replaceAll('&', '&amp;')
124
- .replaceAll('<', '&lt;')
125
- .replaceAll('>', '&gt;')
126
- .replaceAll('"', '&quot;')
124
+ return SafeXmlText.escape(value)
127
125
  }
128
126
  }
@@ -1,6 +1,10 @@
1
1
  import { PcbInteractionPrimitiveModel } from '../core/PcbInteractionPrimitiveModel.mjs'
2
+ import { PcbRenderPlan } from '../core/rendering/PcbRenderPlan.mjs'
3
+ import { CanonicalSvgDocument } from '../core/rendering/CanonicalSvgDocument.mjs'
4
+ import { CircuitJsonPcbBoardSvgRenderer } from './CircuitJsonPcbBoardSvgRenderer.mjs'
2
5
  import { CircuitJsonPcbPrimitiveAttributeRenderer } from './CircuitJsonPcbPrimitiveAttributeRenderer.mjs'
3
6
  import { CircuitJsonPcbViaSvgRenderer } from './CircuitJsonPcbViaSvgRenderer.mjs'
7
+ import { SafeXmlText } from './SafeXmlText.mjs'
4
8
  /**
5
9
  * Renders standards-shaped PCB element arrays into app-compatible SVG.
6
10
  */
@@ -14,6 +18,54 @@ export class CircuitJsonPcbSvgRenderer {
14
18
  static render(documentModel, options = {}) {
15
19
  const side = options.side === 'bottom' ? 'bottom' : 'top'
16
20
  const model = PcbInteractionPrimitiveModel.build(documentModel)
21
+ return CircuitJsonPcbSvgRenderer.#renderModel(model, side)
22
+ }
23
+
24
+ /**
25
+ * Renders multiple board sides from one prepared primitive model.
26
+ * @param {object | object[]} documentModel Parsed document model.
27
+ * @param {unknown[]} [sides] Requested board sides.
28
+ * @returns {string[]} SVG markup in requested order.
29
+ */
30
+ static renderSides(documentModel, sides = ['top', 'bottom']) {
31
+ const model = PcbInteractionPrimitiveModel.build(documentModel)
32
+ const requested = Array.isArray(sides) ? sides : [sides]
33
+ return requested.map((side) =>
34
+ CircuitJsonPcbSvgRenderer.#renderModel(
35
+ model,
36
+ side === 'bottom' ? 'bottom' : 'top'
37
+ )
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Renders one prepared PCB plan without rebuilding primitives.
43
+ * @param {Record<string, any>} plan Prepared PCB render plan.
44
+ * @param {{ layerIds?: string[] | null }} [options] Prepared-plan options.
45
+ * @returns {string} SVG markup.
46
+ */
47
+ static renderPlan(plan, options = {}) {
48
+ PcbRenderPlan.requirePrepared(plan)
49
+ const layerIds =
50
+ options.layerIds === undefined
51
+ ? plan.selectedLayerIds
52
+ : options.layerIds
53
+ return CanonicalSvgDocument.decorate(
54
+ CircuitJsonPcbSvgRenderer.#renderModel(
55
+ PcbRenderPlan.modelForLayers(plan, layerIds),
56
+ plan.side
57
+ ),
58
+ plan.svg
59
+ )
60
+ }
61
+
62
+ /**
63
+ * Wraps one prepared primitive model in the stable SVG document.
64
+ * @param {Record<string, any>} model Prepared primitive model.
65
+ * @param {'top' | 'bottom'} side Active side.
66
+ * @returns {string} SVG markup.
67
+ */
68
+ static #renderModel(model, side) {
17
69
  const viewBox = CircuitJsonPcbSvgRenderer.#viewBox(model.bounds)
18
70
  return (
19
71
  '<svg class="pcb-svg pcb-svg--app-palette pcb-svg--circuitjson pcb-svg--' +
@@ -21,48 +73,13 @@ export class CircuitJsonPcbSvgRenderer {
21
73
  '" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="' +
22
74
  CircuitJsonPcbSvgRenderer.#formatViewBox(viewBox) +
23
75
  '">' +
24
- CircuitJsonPcbSvgRenderer.#renderBoard(model) +
76
+ CircuitJsonPcbBoardSvgRenderer.render(model) +
25
77
  CircuitJsonPcbSvgRenderer.#renderCopper(model, side) +
26
78
  CircuitJsonPcbSvgRenderer.#renderComponentLabels(model) +
27
79
  CircuitJsonPcbSvgRenderer.#renderOverlays(model, side) +
28
80
  '</svg>'
29
81
  )
30
82
  }
31
- /**
32
- * Renders the board substrate.
33
- * @param {{ primitives: object[] }} model Primitive model.
34
- * @returns {string}
35
- */
36
- static #renderBoard(model) {
37
- const board = model.primitives.find(
38
- (primitive) => primitive.kind === 'board'
39
- )
40
- if (!board?.bounds) return ''
41
- if (Array.isArray(board.points) && board.points.length >= 3) {
42
- return (
43
- '<polygon class="pcb-board" data-layer="board" points="' +
44
- CircuitJsonPcbSvgRenderer.#escapeHtml(
45
- CircuitJsonPcbSvgRenderer.#pointsAttribute(board.points)
46
- ) +
47
- '"></polygon>'
48
- )
49
- }
50
- return (
51
- '<rect class="pcb-board" x="' +
52
- CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.minX) +
53
- '" y="' +
54
- CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.minY) +
55
- '" width="' +
56
- CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.width) +
57
- '" height="' +
58
- CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.height) +
59
- '" rx="' +
60
- CircuitJsonPcbSvgRenderer.#formatNumber(
61
- Math.min(board.bounds.width, board.bounds.height) * 0.018
62
- ) +
63
- '" data-layer="board"></rect>'
64
- )
65
- }
66
83
  /**
67
84
  * Renders copper and drilled primitives.
68
85
  * @param {{ primitives: object[] }} model Primitive model.
@@ -755,9 +772,16 @@ export class CircuitJsonPcbSvgRenderer {
755
772
  if (primitiveSide) return primitiveSide === side
756
773
 
757
774
  const layer = String(primitive.layer || '').toLowerCase()
758
- if (side === 'bottom') return layer === 'bottom'
775
+ if (side === 'bottom') {
776
+ return layer === 'bottom' || layer.startsWith('bottom_')
777
+ }
759
778
 
760
- return layer === 'top' || !layer || layer === 'board'
779
+ return (
780
+ layer === 'top' ||
781
+ layer.startsWith('top_') ||
782
+ !layer ||
783
+ layer === 'board'
784
+ )
761
785
  }
762
786
 
763
787
  /**
@@ -955,10 +979,6 @@ export class CircuitJsonPcbSvgRenderer {
955
979
  * @returns {string}
956
980
  */
957
981
  static #escapeHtml(value) {
958
- return String(value ?? '')
959
- .replaceAll('&', '&amp;')
960
- .replaceAll('<', '&lt;')
961
- .replaceAll('>', '&gt;')
962
- .replaceAll('"', '&quot;')
982
+ return SafeXmlText.escape(value)
963
983
  }
964
984
  }
@@ -1,3 +1,5 @@
1
+ import { SafeXmlText } from './SafeXmlText.mjs'
2
+
1
3
  /**
2
4
  * Renders via-like drilled primitives as SVG shapes.
3
5
  */
@@ -159,10 +161,6 @@ export class CircuitJsonPcbViaSvgRenderer {
159
161
  * @returns {string}
160
162
  */
161
163
  static #escapeHtml(value) {
162
- return String(value ?? '')
163
- .replaceAll('&', '&amp;')
164
- .replaceAll('"', '&quot;')
165
- .replaceAll('<', '&lt;')
166
- .replaceAll('>', '&gt;')
164
+ return SafeXmlText.escape(value)
167
165
  }
168
166
  }