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
@@ -0,0 +1,164 @@
1
+ import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
2
+ import { SafeXmlText } from './SafeXmlText.mjs'
3
+
4
+ /**
5
+ * Renders every standard CircuitJSON schematic debug-object variant.
6
+ */
7
+ export class CircuitJsonSchematicDebugRenderer {
8
+ /**
9
+ * Renders one debug object or an empty string for invalid geometry.
10
+ * @param {object} element Debug element.
11
+ * @returns {string} Debug SVG group.
12
+ */
13
+ static render(element) {
14
+ if (
15
+ element.shape === undefined ||
16
+ element.legacy_shape_omitted === true
17
+ ) {
18
+ return CircuitJsonSchematicDebugRenderer.#legacyRect(element)
19
+ }
20
+ const shape = String(element.shape || 'rect')
21
+ const geometry = CircuitJsonSchematicDebugRenderer.#geometry(
22
+ element,
23
+ shape
24
+ )
25
+ if (!geometry) return ''
26
+ const label = String(element.label ?? element.message ?? '').trim()
27
+ return (
28
+ '<g class="schematic-debug-object" data-schematic-debug-object-id="' +
29
+ CircuitJsonSchematicDebugRenderer.#escape(
30
+ element.schematic_debug_object_id || ''
31
+ ) +
32
+ '"><title>' +
33
+ CircuitJsonSchematicDebugRenderer.#escape(label) +
34
+ '</title>' +
35
+ geometry.markup +
36
+ '<text x="' +
37
+ CircuitJsonSchematicDebugRenderer.#number(geometry.label.x) +
38
+ '" y="' +
39
+ CircuitJsonSchematicDebugRenderer.#number(geometry.label.y) +
40
+ '" text-anchor="middle" dominant-baseline="central">' +
41
+ CircuitJsonSchematicDebugRenderer.#escape(label) +
42
+ '</text></g>'
43
+ )
44
+ }
45
+
46
+ /**
47
+ * Preserves the original unqualified rectangle markup byte-for-byte.
48
+ * @param {object} element Legacy debug element.
49
+ * @returns {string} Legacy SVG group.
50
+ */
51
+ static #legacyRect(element) {
52
+ const center = CircuitJsonUnits.optionalPoint(element.center)
53
+ const size = CircuitJsonUnits.optionalSize(element.size || element)
54
+ if (!center || !size) return ''
55
+ const message = String(element.message || '').trim()
56
+ return (
57
+ '<g class="schematic-debug-object" data-schematic-debug-object-id="' +
58
+ CircuitJsonSchematicDebugRenderer.#escape(
59
+ element.schematic_debug_object_id || ''
60
+ ) +
61
+ '"><title>' +
62
+ CircuitJsonSchematicDebugRenderer.#escape(message) +
63
+ '</title><rect x="' +
64
+ CircuitJsonSchematicDebugRenderer.#number(
65
+ center.x - size.width / 2
66
+ ) +
67
+ '" y="' +
68
+ CircuitJsonSchematicDebugRenderer.#number(
69
+ center.y - size.height / 2
70
+ ) +
71
+ '" width="' +
72
+ CircuitJsonSchematicDebugRenderer.#number(size.width) +
73
+ '" height="' +
74
+ CircuitJsonSchematicDebugRenderer.#number(size.height) +
75
+ '"></rect><text x="' +
76
+ CircuitJsonSchematicDebugRenderer.#number(center.x) +
77
+ '" y="' +
78
+ CircuitJsonSchematicDebugRenderer.#number(center.y) +
79
+ '" text-anchor="middle" dominant-baseline="central">' +
80
+ CircuitJsonSchematicDebugRenderer.#escape(message) +
81
+ '</text></g>'
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Builds variant geometry and its label anchor.
87
+ * @param {object} element Debug element.
88
+ * @param {string} shape Standard shape discriminant.
89
+ * @returns {{ markup: string, label: { x: number, y: number } } | null} Geometry.
90
+ */
91
+ static #geometry(element, shape) {
92
+ if (shape === 'line') {
93
+ const start = CircuitJsonUnits.optionalPoint(element.start)
94
+ const end = CircuitJsonUnits.optionalPoint(element.end)
95
+ if (!start || !end) return null
96
+ return {
97
+ markup:
98
+ '<line class="schematic-debug-object__line" x1="' +
99
+ CircuitJsonSchematicDebugRenderer.#number(start.x) +
100
+ '" y1="' +
101
+ CircuitJsonSchematicDebugRenderer.#number(start.y) +
102
+ '" x2="' +
103
+ CircuitJsonSchematicDebugRenderer.#number(end.x) +
104
+ '" y2="' +
105
+ CircuitJsonSchematicDebugRenderer.#number(end.y) +
106
+ '"></line>',
107
+ label: { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }
108
+ }
109
+ }
110
+ const center = CircuitJsonUnits.optionalPoint(element.center)
111
+ if (!center) return null
112
+ if (shape === 'point') {
113
+ return {
114
+ markup:
115
+ '<circle class="schematic-debug-object__point" cx="' +
116
+ CircuitJsonSchematicDebugRenderer.#number(center.x) +
117
+ '" cy="' +
118
+ CircuitJsonSchematicDebugRenderer.#number(center.y) +
119
+ '" r="0.25"></circle>',
120
+ label: center
121
+ }
122
+ }
123
+ const size = CircuitJsonUnits.optionalSize(element.size || element)
124
+ if (!size) return null
125
+ return {
126
+ markup:
127
+ '<rect class="schematic-debug-object__rect" x="' +
128
+ CircuitJsonSchematicDebugRenderer.#number(
129
+ center.x - size.width / 2
130
+ ) +
131
+ '" y="' +
132
+ CircuitJsonSchematicDebugRenderer.#number(
133
+ center.y - size.height / 2
134
+ ) +
135
+ '" width="' +
136
+ CircuitJsonSchematicDebugRenderer.#number(size.width) +
137
+ '" height="' +
138
+ CircuitJsonSchematicDebugRenderer.#number(size.height) +
139
+ '"></rect>',
140
+ label: center
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Formats one finite SVG number.
146
+ * @param {unknown} value Number candidate.
147
+ * @returns {string} SVG number.
148
+ */
149
+ static #number(value) {
150
+ const number = Number(value)
151
+ return Number.isFinite(number)
152
+ ? Number(number.toFixed(6)).toString()
153
+ : '0'
154
+ }
155
+
156
+ /**
157
+ * Escapes XML text and attribute data.
158
+ * @param {unknown} value Text candidate.
159
+ * @returns {string} Escaped text.
160
+ */
161
+ static #escape(value) {
162
+ return SafeXmlText.escape(value)
163
+ }
164
+ }
@@ -0,0 +1,210 @@
1
+ import { BinaryDataSnapshot } from '../core/context/BinaryDataSnapshot.mjs'
2
+ import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
3
+ import { SafeXmlText } from './SafeXmlText.mjs'
4
+
5
+ const IMAGE_MEDIA_TYPES = new Set([
6
+ 'image/bmp',
7
+ 'image/gif',
8
+ 'image/jpeg',
9
+ 'image/png',
10
+ 'image/webp'
11
+ ])
12
+ const BASE64_ALPHABET =
13
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
14
+
15
+ /** Renders canonical asset-backed schematic images into deterministic SVG. */
16
+ export class CircuitJsonSchematicImageSvgRenderer {
17
+ /**
18
+ * Renders every resolvable canonical image in authored order.
19
+ * @param {object[]} images Canonical schematic image rows.
20
+ * @param {object[]} assets Canonical document assets.
21
+ * @returns {string} SVG image group or an empty string.
22
+ */
23
+ static render(images, assets) {
24
+ const byId = CircuitJsonSchematicImageSvgRenderer.#assetIndex(
25
+ assets || []
26
+ )
27
+ const markup = (images || [])
28
+ .map((image, index) => ({ image, index }))
29
+ .sort(
30
+ (left, right) =>
31
+ CircuitJsonSchematicImageSvgRenderer.#order(left.image) -
32
+ CircuitJsonSchematicImageSvgRenderer.#order(
33
+ right.image
34
+ ) || left.index - right.index
35
+ )
36
+ .map(({ image }) =>
37
+ CircuitJsonSchematicImageSvgRenderer.#image(
38
+ image,
39
+ byId.get(String(image.asset_id || ''))
40
+ )
41
+ )
42
+ .filter(Boolean)
43
+ return markup.length
44
+ ? '<g class="schematic-images">' + markup.join('') + '</g>'
45
+ : ''
46
+ }
47
+
48
+ /**
49
+ * Builds an exact asset-id index and marks duplicate ids ambiguous.
50
+ * @param {object[]} assets Canonical assets.
51
+ * @returns {Map<string, object | null>} Assets by exact id.
52
+ */
53
+ static #assetIndex(assets) {
54
+ const byId = new Map()
55
+ for (const asset of assets) {
56
+ const id = String(asset?.id || '')
57
+ if (!id) continue
58
+ byId.set(id, byId.has(id) ? null : asset)
59
+ }
60
+ return byId
61
+ }
62
+
63
+ /**
64
+ * Renders one image when its exact payload is available.
65
+ * @param {object} image Canonical image row.
66
+ * @param {object | null | undefined} asset Resolved asset.
67
+ * @returns {string} SVG image markup or an empty string.
68
+ */
69
+ static #image(image, asset) {
70
+ if (!asset || asset.kind !== 'schematic-image') return ''
71
+ const mediaType = String(asset.mediaType || '').toLowerCase()
72
+ if (!IMAGE_MEDIA_TYPES.has(mediaType)) return ''
73
+ const bytes = CircuitJsonSchematicImageSvgRenderer.#assetBytes(asset)
74
+ if (!bytes?.byteLength) return ''
75
+ const center = CircuitJsonUnits.optionalPoint(image.center)
76
+ const size = CircuitJsonUnits.optionalSize(image.size)
77
+ if (!center || !size || size.width <= 0 || size.height <= 0) return ''
78
+ const rotation = CircuitJsonUnits.angle(image.rotation, 0)
79
+ const opacity = CircuitJsonSchematicImageSvgRenderer.#opacity(
80
+ image.opacity
81
+ )
82
+ const attributes = [
83
+ 'class="schematic-image"',
84
+ 'data-schematic-image-id="' +
85
+ SafeXmlText.escape(image.schematic_image_id || '') +
86
+ '"',
87
+ 'data-asset-id="' + SafeXmlText.escape(image.asset_id || '') + '"',
88
+ 'x="' +
89
+ CircuitJsonSchematicImageSvgRenderer.#number(
90
+ center.x - size.width / 2
91
+ ) +
92
+ '"',
93
+ 'y="' +
94
+ CircuitJsonSchematicImageSvgRenderer.#number(
95
+ center.y - size.height / 2
96
+ ) +
97
+ '"',
98
+ 'width="' +
99
+ CircuitJsonSchematicImageSvgRenderer.#number(size.width) +
100
+ '"',
101
+ 'height="' +
102
+ CircuitJsonSchematicImageSvgRenderer.#number(size.height) +
103
+ '"',
104
+ 'href="data:' +
105
+ mediaType +
106
+ ';base64,' +
107
+ CircuitJsonSchematicImageSvgRenderer.#base64(bytes) +
108
+ '"',
109
+ 'preserveAspectRatio="' +
110
+ (image.preserve_aspect_ratio === false
111
+ ? 'none'
112
+ : 'xMidYMid meet') +
113
+ '"'
114
+ ]
115
+ if (rotation) {
116
+ attributes.push(
117
+ 'transform="rotate(' +
118
+ CircuitJsonSchematicImageSvgRenderer.#number(rotation) +
119
+ ' ' +
120
+ CircuitJsonSchematicImageSvgRenderer.#number(center.x) +
121
+ ' ' +
122
+ CircuitJsonSchematicImageSvgRenderer.#number(center.y) +
123
+ ')"'
124
+ )
125
+ }
126
+ if (opacity !== null) {
127
+ attributes.push(
128
+ 'opacity="' +
129
+ CircuitJsonSchematicImageSvgRenderer.#number(opacity) +
130
+ '"'
131
+ )
132
+ }
133
+ return '<image ' + attributes.join(' ') + '></image>'
134
+ }
135
+
136
+ /**
137
+ * Reads one exact binary asset range without accepting string payloads.
138
+ * @param {object} asset Canonical asset.
139
+ * @returns {Uint8Array | null} Isolated bytes.
140
+ */
141
+ static #assetBytes(asset) {
142
+ let data
143
+ try {
144
+ data = asset.data
145
+ } catch {
146
+ return null
147
+ }
148
+ const range = BinaryDataSnapshot.describe(data)
149
+ return range ? BinaryDataSnapshot.copyBytes(data, range) : null
150
+ }
151
+
152
+ /**
153
+ * Encodes exact bytes without Node-only globals.
154
+ * @param {Uint8Array} bytes Binary bytes.
155
+ * @returns {string} RFC 4648 base64 text.
156
+ */
157
+ static #base64(bytes) {
158
+ let result = ''
159
+ for (let index = 0; index < bytes.length; index += 3) {
160
+ const first = bytes[index]
161
+ const hasSecond = index + 1 < bytes.length
162
+ const hasThird = index + 2 < bytes.length
163
+ const second = hasSecond ? bytes[index + 1] : 0
164
+ const third = hasThird ? bytes[index + 2] : 0
165
+ const bits = (first << 16) | (second << 8) | third
166
+ result += BASE64_ALPHABET[(bits >>> 18) & 63]
167
+ result += BASE64_ALPHABET[(bits >>> 12) & 63]
168
+ result += hasSecond ? BASE64_ALPHABET[(bits >>> 6) & 63] : '='
169
+ result += hasThird ? BASE64_ALPHABET[bits & 63] : '='
170
+ }
171
+ return result
172
+ }
173
+
174
+ /**
175
+ * Resolves a valid optional opacity.
176
+ * @param {unknown} value Candidate.
177
+ * @returns {number | null} Opacity or null.
178
+ */
179
+ static #opacity(value) {
180
+ return typeof value === 'number' &&
181
+ Number.isFinite(value) &&
182
+ value >= 0 &&
183
+ value <= 1
184
+ ? value
185
+ : null
186
+ }
187
+
188
+ /**
189
+ * Resolves an authored render order.
190
+ * @param {object} image Image row.
191
+ * @returns {number} Sort value.
192
+ */
193
+ static #order(image) {
194
+ return Number.isSafeInteger(image?.render_order)
195
+ ? image.render_order
196
+ : Number.MAX_SAFE_INTEGER
197
+ }
198
+
199
+ /**
200
+ * Formats one deterministic SVG number.
201
+ * @param {number} value Number value.
202
+ * @returns {string} SVG number.
203
+ */
204
+ static #number(value) {
205
+ return Number(Number(value).toFixed(6)).toString()
206
+ }
207
+ }
208
+
209
+ Object.freeze(CircuitJsonSchematicImageSvgRenderer.prototype)
210
+ Object.freeze(CircuitJsonSchematicImageSvgRenderer)
@@ -0,0 +1,86 @@
1
+ import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
2
+ import { CircuitJsonSchematicSvgPrimitiveAttributes } from './CircuitJsonSchematicSvgPrimitiveAttributes.mjs'
3
+
4
+ /**
5
+ * Renders standard trace edges while preserving legacy line aliases.
6
+ */
7
+ export class CircuitJsonSchematicLineRenderer {
8
+ /**
9
+ * Renders all usable standard edges or one legacy fallback segment.
10
+ * @param {object} element Line or trace element.
11
+ * @returns {string[]} SVG line elements.
12
+ */
13
+ static render(element) {
14
+ if (
15
+ element.type === 'schematic_trace' &&
16
+ Array.isArray(element.edges)
17
+ ) {
18
+ const edges = element.edges
19
+ .map((edge) =>
20
+ CircuitJsonSchematicLineRenderer.#segment(
21
+ element,
22
+ edge?.from,
23
+ edge?.to
24
+ )
25
+ )
26
+ .filter(Boolean)
27
+ if (edges.length) return edges
28
+ }
29
+ const fallback = CircuitJsonSchematicLineRenderer.#segment(element)
30
+ return fallback ? [fallback] : []
31
+ }
32
+
33
+ /**
34
+ * Renders one line segment from explicit endpoints or legacy aliases.
35
+ * @param {object} element Source element.
36
+ * @param {unknown} [startValue] Standard edge start.
37
+ * @param {unknown} [endValue] Standard edge end.
38
+ * @returns {string} SVG line or empty string.
39
+ */
40
+ static #segment(element, startValue, endValue) {
41
+ const start = CircuitJsonUnits.optionalPoint(
42
+ startValue !== undefined
43
+ ? startValue
44
+ : {
45
+ x: element.x1 ?? element.start?.x,
46
+ y: element.y1 ?? element.start?.y
47
+ }
48
+ )
49
+ const end = CircuitJsonUnits.optionalPoint(
50
+ endValue !== undefined
51
+ ? endValue
52
+ : {
53
+ x: element.x2 ?? element.end?.x,
54
+ y: element.y2 ?? element.end?.y
55
+ }
56
+ )
57
+ if (!start || !end) return ''
58
+ return (
59
+ '<line class="schematic-wire" x1="' +
60
+ CircuitJsonSchematicLineRenderer.#number(start.x) +
61
+ '" y1="' +
62
+ CircuitJsonSchematicLineRenderer.#number(start.y) +
63
+ '" x2="' +
64
+ CircuitJsonSchematicLineRenderer.#number(end.x) +
65
+ '" y2="' +
66
+ CircuitJsonSchematicLineRenderer.#number(end.y) +
67
+ '"' +
68
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(element, {
69
+ fill: false
70
+ }) +
71
+ '></line>'
72
+ )
73
+ }
74
+
75
+ /**
76
+ * Formats one finite SVG number.
77
+ * @param {unknown} value Number candidate.
78
+ * @returns {string} SVG number.
79
+ */
80
+ static #number(value) {
81
+ const number = Number(value)
82
+ return Number.isFinite(number)
83
+ ? Number(number.toFixed(6)).toString()
84
+ : '0'
85
+ }
86
+ }
@@ -0,0 +1,98 @@
1
+ import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
2
+ import { CircuitJsonSchematicSvgPrimitiveAttributes } from './CircuitJsonSchematicSvgPrimitiveAttributes.mjs'
3
+ import { SafeXmlText } from './SafeXmlText.mjs'
4
+
5
+ /** Renders hierarchical child-sheet symbols without treating them as pages. */
6
+ export class CircuitJsonSchematicSheetSymbolSvgRenderer {
7
+ /**
8
+ * Renders child-sheet symbols in their authored order.
9
+ * @param {object[]} symbols Canonical sheet-symbol rows.
10
+ * @returns {string} SVG group or an empty string.
11
+ */
12
+ static render(symbols) {
13
+ const markup = (symbols || [])
14
+ .map((symbol, index) => ({ symbol, index }))
15
+ .sort(
16
+ (left, right) =>
17
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#order(
18
+ left.symbol
19
+ ) -
20
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#order(
21
+ right.symbol
22
+ ) || left.index - right.index
23
+ )
24
+ .map(({ symbol }) =>
25
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#symbol(symbol)
26
+ )
27
+ .filter(Boolean)
28
+ return markup.length
29
+ ? '<g class="schematic-sheet-symbols">' + markup.join('') + '</g>'
30
+ : ''
31
+ }
32
+
33
+ /**
34
+ * Renders one child-sheet box and label.
35
+ * @param {object} symbol Sheet-symbol row.
36
+ * @returns {string} SVG markup or an empty string.
37
+ */
38
+ static #symbol(symbol) {
39
+ const center = CircuitJsonUnits.optionalPoint(symbol.center)
40
+ const size = CircuitJsonUnits.optionalSize(symbol)
41
+ if (!center || !size || size.width <= 0 || size.height <= 0) return ''
42
+ const sourceFileName = String(symbol.source_file_name || '')
43
+ return (
44
+ '<g class="schematic-sheet-symbol" data-schematic-sheet-symbol-id="' +
45
+ SafeXmlText.escape(symbol.schematic_sheet_symbol_id || '') +
46
+ '"' +
47
+ (sourceFileName
48
+ ? ' data-source-file-name="' +
49
+ SafeXmlText.escape(sourceFileName) +
50
+ '"'
51
+ : '') +
52
+ '><rect class="schematic-sheet-symbol__body" x="' +
53
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#number(
54
+ center.x - size.width / 2
55
+ ) +
56
+ '" y="' +
57
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#number(
58
+ center.y - size.height / 2
59
+ ) +
60
+ '" width="' +
61
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#number(size.width) +
62
+ '" height="' +
63
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#number(size.height) +
64
+ '"' +
65
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(symbol) +
66
+ '></rect><text class="schematic-sheet-symbol__label" x="' +
67
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#number(center.x) +
68
+ '" y="' +
69
+ CircuitJsonSchematicSheetSymbolSvgRenderer.#number(center.y) +
70
+ '" text-anchor="middle" dominant-baseline="central">' +
71
+ SafeXmlText.escapeText(symbol.name || '') +
72
+ '</text></g>'
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Resolves one authored render order.
78
+ * @param {object} symbol Sheet-symbol row.
79
+ * @returns {number} Sort value.
80
+ */
81
+ static #order(symbol) {
82
+ return Number.isSafeInteger(symbol?.render_order)
83
+ ? symbol.render_order
84
+ : Number.MAX_SAFE_INTEGER
85
+ }
86
+
87
+ /**
88
+ * Formats one deterministic SVG number.
89
+ * @param {number} value Number value.
90
+ * @returns {string} SVG number.
91
+ */
92
+ static #number(value) {
93
+ return Number(Number(value).toFixed(6)).toString()
94
+ }
95
+ }
96
+
97
+ Object.freeze(CircuitJsonSchematicSheetSymbolSvgRenderer.prototype)
98
+ Object.freeze(CircuitJsonSchematicSheetSymbolSvgRenderer)