circuitjson-toolkit 1.0.17 → 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.
- package/NOTICE.md +31 -0
- package/README.md +267 -107
- package/docs/api.md +501 -96
- package/docs/capabilities.md +70 -0
- package/docs/migration/behaviors.md +45 -0
- package/docs/migration/parser.md +60 -0
- package/docs/migration/renderers.md +515 -0
- package/docs/migration/root.md +740 -0
- package/docs/migration.md +120 -0
- package/docs/model-format.md +175 -57
- package/docs/provenance.md +206 -0
- package/docs/release-notes-v1.1.0.md +154 -0
- package/docs/testing.md +117 -7
- package/package.json +31 -5
- package/spec/api-baseline-v1.0.17.json +1 -0
- package/spec/baseline-provenance-v1.0.17.json +7 -0
- package/spec/circuitjson-schema-snapshot.json +321 -0
- package/spec/circuitjson-schema-source.json +28 -0
- package/spec/feature-preservation.json +1 -0
- package/spec/library-scope.md +27 -20
- package/src/capabilities.mjs +1 -0
- package/src/core/ArchiveEntryPath.mjs +93 -0
- package/src/core/ArchiveLimits.mjs +31 -0
- package/src/core/ArchiveLimitsValidator.mjs +107 -0
- package/src/core/AsyncInputOwnership.mjs +56 -0
- package/src/core/AttachedValueLimits.mjs +67 -0
- package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
- package/src/core/CircuitJsonDocument.mjs +19 -61
- package/src/core/CircuitJsonElementTypes.mjs +10 -0
- package/src/core/CircuitJsonElementValidator.mjs +98 -847
- package/src/core/CircuitJsonIndexer.mjs +274 -194
- package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
- package/src/core/CircuitJsonParser.mjs +75 -13
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
- package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
- package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
- package/src/core/CircuitJsonUnitParsers.mjs +101 -0
- package/src/core/CircuitJsonUnits.mjs +13 -87
- package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
- package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
- package/src/core/CircuitJsonValidationUnits.mjs +6 -0
- package/src/core/ManufacturingService.mjs +323 -0
- package/src/core/Parser.mjs +343 -0
- package/src/core/ParserOptions.mjs +333 -0
- package/src/core/PcbBoundsSelectionModel.mjs +55 -19
- package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
- package/src/core/PcbInteractionIndex.mjs +368 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
- package/src/core/ProjectAsyncInputOwner.mjs +70 -0
- package/src/core/ProjectLoader.mjs +975 -0
- package/src/core/SimulationService.mjs +790 -0
- package/src/core/ToolkitCapabilities.mjs +130 -0
- package/src/core/ZipArchiveInspector.mjs +649 -0
- package/src/core/context/BinaryDataSnapshot.mjs +217 -0
- package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
- package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
- package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
- package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
- package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
- package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
- package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
- package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
- package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
- package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
- package/src/core/context/PcbSpatialIndex.mjs +701 -0
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
- package/src/core/context/StructuredDataSnapshot.mjs +683 -0
- package/src/core/contracts/DocumentResult.mjs +198 -0
- package/src/core/contracts/ProjectResult.mjs +96 -0
- package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
- package/src/core/contracts/ToolkitAsset.mjs +493 -0
- package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
- package/src/core/contracts/ToolkitError.mjs +176 -0
- package/src/core/contracts/ToolkitProgress.mjs +89 -0
- package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
- package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
- package/src/core/query/CircuitTraversal.mjs +343 -0
- package/src/core/query/ComponentGrouping.mjs +275 -0
- package/src/core/query/QueryNetlistBuilder.mjs +306 -0
- package/src/core/query/QueryService.mjs +435 -0
- package/src/core/query/RegexPattern.mjs +75 -0
- package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
- package/src/core/rendering/CanonicalBomRows.mjs +92 -0
- package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
- package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
- package/src/core/rendering/PcbRenderPlan.mjs +429 -0
- package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
- package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
- package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
- package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
- package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
- package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
- package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
- package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
- package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
- package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
- package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
- package/src/core/scene3d/Scene3dOptions.mjs +363 -0
- package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
- package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
- package/src/core/worker/ParserWorkerClient.mjs +997 -0
- package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
- package/src/core/worker/WorkerRequestData.mjs +623 -0
- package/src/core/worker/WorkerResponseData.mjs +905 -0
- package/src/extensions.mjs +37 -0
- package/src/index.mjs +14 -9
- package/src/interaction.mjs +2 -0
- package/src/manufacturing.mjs +1 -0
- package/src/parser.mjs +12 -2
- package/src/project.mjs +5 -0
- package/src/query.mjs +1 -0
- package/src/renderers.mjs +3 -29
- package/src/scene3d.mjs +3 -0
- package/src/simulation.mjs +1 -0
- package/src/styles/renderers.css +24 -0
- package/src/testing/ToolkitContractFixtures.mjs +124 -0
- package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
- package/src/testing/runToolkitContract.mjs +705 -0
- package/src/testing.mjs +3 -0
- package/src/ui/BomTableRenderer.mjs +304 -0
- package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
- package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
- package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
- package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
- package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
- package/src/ui/PcbSvgRenderer.mjs +41 -0
- package/src/ui/SafeSvgPaint.mjs +26 -0
- package/src/ui/SafeXmlText.mjs +60 -0
- package/src/ui/SchematicGeometryBounds.mjs +540 -0
- package/src/ui/SchematicSvgRenderer.mjs +110 -0
- package/src/ui/SchematicTableGeometry.mjs +319 -0
- package/src/ui/SchematicTextAnchor.mjs +55 -0
- package/src/ui/SchematicTextBounds.mjs +98 -0
- 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
|
+
}
|