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,540 @@
1
+ import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
2
+ import { CircuitJsonSchematicSvgArcPath } from './CircuitJsonSchematicSvgArcPath.mjs'
3
+ import { CircuitJsonSchematicSvgPortMetadata } from './CircuitJsonSchematicSvgPortMetadata.mjs'
4
+ import { SchematicTableGeometry } from './SchematicTableGeometry.mjs'
5
+ import { SchematicTextBounds } from './SchematicTextBounds.mjs'
6
+
7
+ /**
8
+ * Derives finite schematic canvas bounds from selected CircuitJSON geometry.
9
+ */
10
+ export class SchematicGeometryBounds {
11
+ /**
12
+ * Preserves the direct legacy renderer's explicit-or-fixed canvas contract.
13
+ * @param {{ elementsByType: Map<string, object[]> }} index Legacy element index.
14
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number }} Legacy bounds.
15
+ */
16
+ static legacy(index) {
17
+ const sheet = (index.elementsByType.get('schematic_sheet') || [])[0]
18
+ const width = CircuitJsonUnits.optionalLength(sheet?.width)
19
+ const height = CircuitJsonUnits.optionalLength(sheet?.height)
20
+ return width !== null && height !== null
21
+ ? { minX: 0, minY: 0, maxX: width, maxY: height, width, height }
22
+ : {
23
+ minX: -10,
24
+ minY: -10,
25
+ maxX: 10,
26
+ maxY: 10,
27
+ width: 20,
28
+ height: 20
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Resolves explicit sheet dimensions or selected content extents.
34
+ * @param {{ elements?: object[], elementsByType: Map<string, object[]> }} index Selected schematic index.
35
+ * @param {Map<string, object> | null} [preparedSourcePorts] Shared source-port lookup.
36
+ * @param {WeakMap<object, string> | null} [portHintCache] Render-scoped hint cache.
37
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number }} Bounds.
38
+ */
39
+ static resolve(index, preparedSourcePorts = null, portHintCache = null) {
40
+ const sheet = (index.elementsByType.get('schematic_sheet') || [])[0]
41
+ const width = CircuitJsonUnits.optionalLength(sheet?.width)
42
+ const height = CircuitJsonUnits.optionalLength(sheet?.height)
43
+ if (width !== null && height !== null) {
44
+ return {
45
+ minX: 0,
46
+ minY: 0,
47
+ maxX: width,
48
+ maxY: height,
49
+ width,
50
+ height
51
+ }
52
+ }
53
+
54
+ const bounds = {
55
+ minX: Infinity,
56
+ minY: Infinity,
57
+ maxX: -Infinity,
58
+ maxY: -Infinity
59
+ }
60
+ const sourcePorts =
61
+ preparedSourcePorts ||
62
+ CircuitJsonSchematicSvgPortMetadata.sourcePorts(
63
+ index.elementsByType.get('source_port') || []
64
+ )
65
+ const sourceNames = new Map(
66
+ (index.elementsByType.get('source_component') || []).map(
67
+ (element) => [
68
+ String(element.source_component_id || '').trim(),
69
+ String(element.name || element.source_component_id || '')
70
+ ]
71
+ )
72
+ )
73
+ const generatedLabels = SchematicGeometryBounds.#generatedLabels(
74
+ index,
75
+ sourceNames
76
+ )
77
+ const tables = SchematicTableGeometry.models(index)
78
+ for (const element of index.elements || []) {
79
+ if (!String(element.type || '').startsWith('schematic_')) continue
80
+ SchematicGeometryBounds.#includeElement(
81
+ bounds,
82
+ element,
83
+ sourcePorts,
84
+ generatedLabels,
85
+ tables,
86
+ portHintCache
87
+ )
88
+ }
89
+ if (!Number.isFinite(bounds.minX)) {
90
+ return {
91
+ minX: -10,
92
+ minY: -10,
93
+ maxX: 10,
94
+ maxY: 10,
95
+ width: 20,
96
+ height: 20
97
+ }
98
+ }
99
+ return SchematicGeometryBounds.#minimumExtent(bounds)
100
+ }
101
+
102
+ /**
103
+ * Adds every standard geometry field from one schematic element.
104
+ * @param {Record<string, number>} bounds Mutable bounds.
105
+ * @param {object} element CircuitJSON element.
106
+ * @param {Map<string, object>} sourcePorts Source-port lookup.
107
+ * @param {WeakMap<object, string>} generatedLabels Generated label lookup.
108
+ * @param {Map<string, object>} tables Table-model lookup.
109
+ * @param {WeakMap<object, string> | null} portHintCache Render-scoped hint cache.
110
+ * @returns {void}
111
+ */
112
+ static #includeElement(
113
+ bounds,
114
+ element,
115
+ sourcePorts,
116
+ generatedLabels,
117
+ tables,
118
+ portHintCache
119
+ ) {
120
+ const geometry = {
121
+ minX: Infinity,
122
+ minY: Infinity,
123
+ maxX: -Infinity,
124
+ maxY: -Infinity
125
+ }
126
+ const table = tables.get(
127
+ String(element.schematic_table_id || '').trim()
128
+ )
129
+ if (element.type === 'schematic_table') {
130
+ const rect = table || SchematicTableGeometry.tableRect(element)
131
+ if (rect) {
132
+ SchematicGeometryBounds.#include(geometry, {
133
+ x: rect.x,
134
+ y: rect.y
135
+ })
136
+ SchematicGeometryBounds.#include(geometry, {
137
+ x: rect.x + rect.width,
138
+ y: rect.y + rect.height
139
+ })
140
+ }
141
+ } else if (element.type === 'schematic_table_cell') {
142
+ const rect = SchematicTableGeometry.cellRect(element, table)
143
+ if (rect) {
144
+ SchematicGeometryBounds.#include(geometry, {
145
+ x: rect.x,
146
+ y: rect.y
147
+ })
148
+ SchematicGeometryBounds.#include(geometry, {
149
+ x: rect.x + rect.width,
150
+ y: rect.y + rect.height
151
+ })
152
+ }
153
+ }
154
+ let center = SchematicGeometryBounds.#point(
155
+ element.center ||
156
+ element.position ||
157
+ element.anchor_position ||
158
+ element
159
+ )
160
+ if (!center && SchematicGeometryBounds.#isDiagnostic(element)) {
161
+ center = { x: 0, y: 0 }
162
+ }
163
+ const size =
164
+ CircuitJsonUnits.optionalSize(element.size || element) ||
165
+ (element.type === 'schematic_component'
166
+ ? { width: 4, height: 3 }
167
+ : null)
168
+ if (center && size && element.type === 'schematic_image') {
169
+ SchematicGeometryBounds.#includeRotatedImage(
170
+ geometry,
171
+ center,
172
+ size,
173
+ element.rotation
174
+ )
175
+ } else if (center && size) {
176
+ SchematicGeometryBounds.#include(geometry, {
177
+ x: center.x - size.width / 2,
178
+ y: center.y - size.height / 2
179
+ })
180
+ SchematicGeometryBounds.#include(geometry, {
181
+ x: center.x + size.width / 2,
182
+ y: center.y + size.height / 2
183
+ })
184
+ } else if (center) {
185
+ SchematicGeometryBounds.#include(geometry, center)
186
+ }
187
+
188
+ const radius = CircuitJsonUnits.optionalLength(element.radius)
189
+ if (center && radius !== null) {
190
+ const visibleRadius = Math.max(0, radius)
191
+ SchematicGeometryBounds.#include(geometry, {
192
+ x: center.x - visibleRadius,
193
+ y: center.y - visibleRadius
194
+ })
195
+ SchematicGeometryBounds.#include(geometry, {
196
+ x: center.x + visibleRadius,
197
+ y: center.y + visibleRadius
198
+ })
199
+ }
200
+ const visibleRadius = SchematicGeometryBounds.#visibleRadius(element)
201
+ if (center && visibleRadius !== null) {
202
+ SchematicGeometryBounds.#include(geometry, {
203
+ x: center.x - visibleRadius,
204
+ y: center.y - visibleRadius
205
+ })
206
+ SchematicGeometryBounds.#include(geometry, {
207
+ x: center.x + visibleRadius,
208
+ y: center.y + visibleRadius
209
+ })
210
+ }
211
+
212
+ if (element.type === 'schematic_arc') {
213
+ const arcBounds =
214
+ CircuitJsonSchematicSvgArcPath.boundsFromThreePoints(
215
+ SchematicGeometryBounds.#point(element.start),
216
+ SchematicGeometryBounds.#point(element.mid),
217
+ SchematicGeometryBounds.#point(element.end)
218
+ )
219
+ if (arcBounds) {
220
+ SchematicGeometryBounds.#include(geometry, {
221
+ x: arcBounds.minX,
222
+ y: arcBounds.minY
223
+ })
224
+ SchematicGeometryBounds.#include(geometry, {
225
+ x: arcBounds.maxX,
226
+ y: arcBounds.maxY
227
+ })
228
+ }
229
+ }
230
+
231
+ for (const field of ['start', 'mid', 'end', 'from', 'to']) {
232
+ SchematicGeometryBounds.#include(
233
+ geometry,
234
+ SchematicGeometryBounds.#point(element[field])
235
+ )
236
+ }
237
+ for (const field of ['points', 'path', 'junctions']) {
238
+ for (const point of Array.isArray(element[field])
239
+ ? element[field]
240
+ : []) {
241
+ SchematicGeometryBounds.#include(
242
+ geometry,
243
+ SchematicGeometryBounds.#point(point)
244
+ )
245
+ }
246
+ }
247
+ for (const edge of Array.isArray(element.edges) ? element.edges : []) {
248
+ SchematicGeometryBounds.#include(
249
+ geometry,
250
+ SchematicGeometryBounds.#point(edge?.from)
251
+ )
252
+ SchematicGeometryBounds.#include(
253
+ geometry,
254
+ SchematicGeometryBounds.#point(edge?.to)
255
+ )
256
+ }
257
+ for (const point of [
258
+ { x: element.x1, y: element.y1 },
259
+ { x: element.x2, y: element.y2 }
260
+ ]) {
261
+ SchematicGeometryBounds.#include(
262
+ geometry,
263
+ SchematicGeometryBounds.#point(point)
264
+ )
265
+ }
266
+ if (Number.isFinite(geometry.minX)) {
267
+ const margin = SchematicGeometryBounds.#strokeMargin(element, table)
268
+ SchematicGeometryBounds.#include(bounds, {
269
+ x: geometry.minX - margin,
270
+ y: geometry.minY - margin
271
+ })
272
+ SchematicGeometryBounds.#include(bounds, {
273
+ x: geometry.maxX + margin,
274
+ y: geometry.maxY + margin
275
+ })
276
+ }
277
+ SchematicGeometryBounds.#includeVisibleText(
278
+ bounds,
279
+ element,
280
+ sourcePorts,
281
+ generatedLabels,
282
+ tables,
283
+ portHintCache
284
+ )
285
+ }
286
+
287
+ /**
288
+ * Includes the exact axis-aligned extents of a center-rotated image.
289
+ * @param {Record<string, number>} bounds Mutable bounds.
290
+ * @param {{ x: number, y: number }} center Image center.
291
+ * @param {{ width: number, height: number }} size Image size.
292
+ * @param {unknown} rotation Rotation in canonical degrees.
293
+ * @returns {void}
294
+ */
295
+ static #includeRotatedImage(bounds, center, size, rotation) {
296
+ const radians = (CircuitJsonUnits.angle(rotation, 0) * Math.PI) / 180
297
+ const cosine = Math.abs(Math.cos(radians))
298
+ const sine = Math.abs(Math.sin(radians))
299
+ const halfWidth = (cosine * size.width + sine * size.height) / 2
300
+ const halfHeight = (sine * size.width + cosine * size.height) / 2
301
+ SchematicGeometryBounds.#include(bounds, {
302
+ x: center.x - halfWidth,
303
+ y: center.y - halfHeight
304
+ })
305
+ SchematicGeometryBounds.#include(bounds, {
306
+ x: center.x + halfWidth,
307
+ y: center.y + halfHeight
308
+ })
309
+ }
310
+
311
+ /**
312
+ * Returns the visible radius for one fixed-size schematic marker.
313
+ * @param {object} element CircuitJSON element.
314
+ * @returns {number | null} Marker radius.
315
+ */
316
+ static #visibleRadius(element) {
317
+ if (element.type === 'schematic_port') return 0.35
318
+ if (element.type === 'schematic_voltage_probe') return 0.45
319
+ if (
320
+ element.type === 'schematic_debug_object' &&
321
+ element.shape === 'point'
322
+ ) {
323
+ return 0.25
324
+ }
325
+ return SchematicGeometryBounds.#isDiagnostic(element) ? 0.45 : null
326
+ }
327
+
328
+ /**
329
+ * Resolves half of the effective explicit SVG stroke or table border.
330
+ * @param {object} element CircuitJSON element.
331
+ * @param {object | undefined} table Related table model.
332
+ * @returns {number} Nonnegative stroke margin.
333
+ */
334
+ static #strokeMargin(element, table) {
335
+ const width =
336
+ element.type === 'schematic_table' ||
337
+ element.type === 'schematic_table_cell'
338
+ ? table?.borderWidth
339
+ : CircuitJsonUnits.optionalLength(
340
+ element.stroke_width ?? element.strokeWidth
341
+ )
342
+ return Math.max(0, Number(width) || 0) / 2
343
+ }
344
+
345
+ /**
346
+ * Returns whether an element is rendered as a diagnostic marker.
347
+ * @param {object} element CircuitJSON element.
348
+ * @returns {boolean} Whether the element is a schematic diagnostic.
349
+ */
350
+ static #isDiagnostic(element) {
351
+ const type = String(element.type || '')
352
+ return type.startsWith('schematic_') && /(?:error|warning)/iu.test(type)
353
+ }
354
+
355
+ /**
356
+ * Includes visible standard text, port, probe, and debug labels.
357
+ * @param {Record<string, number>} bounds Mutable bounds.
358
+ * @param {object} element CircuitJSON element.
359
+ * @param {Map<string, object>} sourcePorts Source-port lookup.
360
+ * @param {WeakMap<object, string>} generatedLabels Generated label lookup.
361
+ * @param {Map<string, object>} tables Table-model lookup.
362
+ * @param {WeakMap<object, string> | null} portHintCache Render-scoped hint cache.
363
+ * @returns {void}
364
+ */
365
+ static #includeVisibleText(
366
+ bounds,
367
+ element,
368
+ sourcePorts,
369
+ generatedLabels,
370
+ tables,
371
+ portHintCache
372
+ ) {
373
+ const type = String(element.type || '')
374
+ let point = null
375
+ let text = ''
376
+ let fontSize = CircuitJsonUnits.optionalLength(element.font_size) ?? 1
377
+ let anchor = element.anchor || element.anchor_side || ''
378
+ let rotation = CircuitJsonUnits.angle(
379
+ element.ccw_rotation ?? element.rotation,
380
+ 0
381
+ )
382
+ if (type === 'schematic_text' || type === 'schematic_net_label') {
383
+ point = SchematicGeometryBounds.#point(
384
+ element.anchor_position || element.position || element.center
385
+ )
386
+ text = String(element.text || element.name || '')
387
+ } else if (
388
+ type === 'schematic_component' ||
389
+ type === 'schematic_symbol'
390
+ ) {
391
+ point = SchematicGeometryBounds.#point(element.center || element)
392
+ text = generatedLabels.get(element) || ''
393
+ anchor = 'center'
394
+ rotation = 0
395
+ } else if (type === 'schematic_group') {
396
+ point = SchematicGeometryBounds.#point(element.center || element)
397
+ text = String(element.name || element.schematic_group_id || '')
398
+ anchor = 'center'
399
+ rotation = 0
400
+ } else if (type === 'schematic_table_cell') {
401
+ const table = tables.get(
402
+ String(element.schematic_table_id || '').trim()
403
+ )
404
+ const rect = SchematicTableGeometry.cellRect(element, table)
405
+ if (rect) {
406
+ const position = SchematicTableGeometry.textPosition(
407
+ element,
408
+ table,
409
+ rect
410
+ )
411
+ point = { x: position.x, y: position.y }
412
+ anchor = position.namedAnchor
413
+ }
414
+ text = String(element.text || '')
415
+ rotation = 0
416
+ } else if (type === 'schematic_port') {
417
+ const center = SchematicGeometryBounds.#point(element.center)
418
+ if (center) point = { x: center.x + 0.7, y: center.y }
419
+ const source = sourcePorts.get(String(element.source_port_id || ''))
420
+ text = CircuitJsonSchematicSvgPortMetadata.label(
421
+ element,
422
+ source,
423
+ portHintCache
424
+ )
425
+ anchor = 'center_left'
426
+ rotation = 0
427
+ } else if (type === 'schematic_voltage_probe') {
428
+ const center = SchematicGeometryBounds.#point(
429
+ element.position || element.center
430
+ )
431
+ if (center) point = { x: center.x + 0.65, y: center.y }
432
+ text = String(element.name || element.label || '')
433
+ anchor = 'center_left'
434
+ rotation = 0
435
+ } else if (type === 'schematic_debug_object') {
436
+ point = SchematicGeometryBounds.#point(element.center)
437
+ if (!point) {
438
+ const start = SchematicGeometryBounds.#point(element.start)
439
+ const end = SchematicGeometryBounds.#point(element.end)
440
+ if (start && end) {
441
+ point = {
442
+ x: (start.x + end.x) / 2,
443
+ y: (start.y + end.y) / 2
444
+ }
445
+ }
446
+ }
447
+ text = String(element.label ?? element.message ?? '')
448
+ anchor = 'center'
449
+ rotation = 0
450
+ }
451
+ if (!point || !text) return
452
+ const textBounds = SchematicTextBounds.resolve(point, text, {
453
+ fontSize,
454
+ anchor,
455
+ rotation
456
+ })
457
+ SchematicGeometryBounds.#include(bounds, {
458
+ x: textBounds.minX,
459
+ y: textBounds.minY
460
+ })
461
+ SchematicGeometryBounds.#include(bounds, {
462
+ x: textBounds.maxX,
463
+ y: textBounds.maxY
464
+ })
465
+ }
466
+
467
+ /**
468
+ * Reproduces generated component and symbol labels in renderer order.
469
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
470
+ * @param {Map<string, string>} sourceNames Source-component names.
471
+ * @returns {WeakMap<object, string>} Element-label lookup.
472
+ */
473
+ static #generatedLabels(index, sourceNames) {
474
+ const labels = new WeakMap()
475
+ for (const [type, prefix] of [
476
+ ['schematic_component', 'U'],
477
+ ['schematic_symbol', 'SYM']
478
+ ]) {
479
+ const elements = index.elementsByType.get(type) || []
480
+ for (
481
+ let elementIndex = 0;
482
+ elementIndex < elements.length;
483
+ elementIndex += 1
484
+ ) {
485
+ const element = elements[elementIndex]
486
+ if (element.show_label === false) continue
487
+ labels.set(
488
+ element,
489
+ sourceNames.get(
490
+ String(element.source_component_id || '')
491
+ ) || String(element.name || prefix + (elementIndex + 1))
492
+ )
493
+ }
494
+ }
495
+ return labels
496
+ }
497
+
498
+ /**
499
+ * Resolves one optional finite point.
500
+ * @param {unknown} value Point candidate.
501
+ * @returns {{ x: number, y: number } | null} Point.
502
+ */
503
+ static #point(value) {
504
+ return CircuitJsonUnits.optionalPoint(value)
505
+ }
506
+
507
+ /**
508
+ * Extends mutable bounds with one optional point.
509
+ * @param {Record<string, number>} bounds Mutable bounds.
510
+ * @param {{ x: number, y: number } | null} point Point.
511
+ * @returns {void}
512
+ */
513
+ static #include(bounds, point) {
514
+ if (!point) return
515
+ bounds.minX = Math.min(bounds.minX, point.x)
516
+ bounds.minY = Math.min(bounds.minY, point.y)
517
+ bounds.maxX = Math.max(bounds.maxX, point.x)
518
+ bounds.maxY = Math.max(bounds.maxY, point.y)
519
+ }
520
+
521
+ /**
522
+ * Gives point-only and label-bearing geometry a stable visible canvas.
523
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Recovered bounds.
524
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number }} Expanded bounds.
525
+ */
526
+ static #minimumExtent(bounds) {
527
+ const centerX = (bounds.minX + bounds.maxX) / 2
528
+ const centerY = (bounds.minY + bounds.maxY) / 2
529
+ const width = Math.max(bounds.maxX - bounds.minX, 4)
530
+ const height = Math.max(bounds.maxY - bounds.minY, 4)
531
+ return {
532
+ minX: centerX - width / 2,
533
+ minY: centerY - height / 2,
534
+ maxX: centerX + width / 2,
535
+ maxY: centerY + height / 2,
536
+ width,
537
+ height
538
+ }
539
+ }
540
+ }
@@ -0,0 +1,110 @@
1
+ import { CircuitJsonDocumentContext } from '../core/context/CircuitJsonDocumentContext.mjs'
2
+ import { ToolkitError } from '../core/contracts/ToolkitError.mjs'
3
+ import { CanonicalRenderOptions } from '../core/rendering/CanonicalRenderOptions.mjs'
4
+ import { CanonicalSvgDocument } from '../core/rendering/CanonicalSvgDocument.mjs'
5
+ import { SchematicSheetSelector } from '../core/rendering/SchematicSheetSelector.mjs'
6
+ import { CircuitJsonSchematicSvgRenderer } from './CircuitJsonSchematicSvgRenderer.mjs'
7
+
8
+ const PREPARED_SCHEMATIC_SVGS = new WeakMap()
9
+
10
+ /**
11
+ * Renders canonical CircuitJSON schematic documents through shared indexes.
12
+ */
13
+ export class SchematicSvgRenderer {
14
+ /**
15
+ * Renders one schematic sheet as deterministic SVG.
16
+ * @param {unknown} document DocumentResult, CircuitJSON model, or context.
17
+ * @param {Record<string, any>} [options] Canonical render options.
18
+ * @returns {string} SVG markup.
19
+ */
20
+ static render(document, options = {}) {
21
+ const normalized = CanonicalRenderOptions.normalize(options, {
22
+ sheetId: true,
23
+ svg: true
24
+ })
25
+ CanonicalRenderOptions.requireCanonicalFidelity(normalized.fidelity)
26
+ const context = CircuitJsonDocumentContext.prepare(document, {
27
+ indexes: ['elements']
28
+ })
29
+ SchematicSvgRenderer.#requireSheet(context.model, normalized.sheetId)
30
+ const sheetId = SchematicSvgRenderer.#resolveSheetId(
31
+ context.model,
32
+ normalized.sheetId
33
+ )
34
+ const key = `schematic-svg-v1:${sheetId || '*'}`
35
+ const cached = context.getOrCreateDerived('render', key, () => {
36
+ const index = context.getIndex('elements')
37
+ const selected = sheetId
38
+ ? SchematicSheetSelector.select(index, sheetId)
39
+ : index
40
+ const entry = Object.freeze({
41
+ svg: CircuitJsonSchematicSvgRenderer.render(selected, {
42
+ assets: context.assets
43
+ })
44
+ })
45
+ PREPARED_SCHEMATIC_SVGS.set(entry, { context, key })
46
+ return entry
47
+ })
48
+ if (
49
+ !cached ||
50
+ typeof cached !== 'object' ||
51
+ PREPARED_SCHEMATIC_SVGS.get(cached)?.context !== context ||
52
+ PREPARED_SCHEMATIC_SVGS.get(cached)?.key !== key
53
+ ) {
54
+ throw CanonicalRenderOptions.error(
55
+ 'Schematic rendering encountered a derived-cache collision.'
56
+ )
57
+ }
58
+ return CanonicalSvgDocument.decorate(cached.svg, normalized.svg)
59
+ }
60
+
61
+ /**
62
+ * Requires a requested CircuitJSON schematic sheet to exist.
63
+ * @param {object[]} model CircuitJSON model.
64
+ * @param {string | null} sheetId Requested sheet id.
65
+ * @returns {void}
66
+ */
67
+ static #requireSheet(model, sheetId) {
68
+ if (
69
+ !sheetId ||
70
+ model.some(
71
+ (element) =>
72
+ element.type === 'schematic_sheet' &&
73
+ element.schematic_sheet_id === sheetId
74
+ )
75
+ ) {
76
+ return
77
+ }
78
+ throw new ToolkitError(`Unknown schematic sheet: ${sheetId}.`, {
79
+ code: 'ERR_RENDER_SHEET',
80
+ category: 'validation',
81
+ format: 'circuitjson',
82
+ details: { sheetId }
83
+ })
84
+ }
85
+
86
+ /**
87
+ * Resolves an explicit sheet or the first stable CircuitJSON sheet.
88
+ * @param {object[]} model CircuitJSON model.
89
+ * @param {string | null} requestedSheetId Explicit sheet id.
90
+ * @returns {string | null} Selected sheet id.
91
+ */
92
+ static #resolveSheetId(model, requestedSheetId) {
93
+ if (requestedSheetId) return requestedSheetId
94
+ const sheets = model
95
+ .filter((element) => element.type === 'schematic_sheet')
96
+ .sort((left, right) => {
97
+ const leftIndex = Number.isFinite(left.sheet_index)
98
+ ? left.sheet_index
99
+ : Infinity
100
+ const rightIndex = Number.isFinite(right.sheet_index)
101
+ ? right.sheet_index
102
+ : Infinity
103
+ if (leftIndex !== rightIndex) return leftIndex - rightIndex
104
+ const leftId = String(left.schematic_sheet_id || '')
105
+ const rightId = String(right.schematic_sheet_id || '')
106
+ return leftId < rightId ? -1 : leftId > rightId ? 1 : 0
107
+ })
108
+ return sheets[0]?.schematic_sheet_id || null
109
+ }
110
+ }