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.
- package/AGENTS.md +15 -0
- 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,343 @@
|
|
|
1
|
+
import { ComponentGrouping } from './ComponentGrouping.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Traverses canonical trace connectivity with stable bounded breadth-first work.
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitTraversal {
|
|
7
|
+
/**
|
|
8
|
+
* Prepares reusable trace and membership indexes without expanding edges.
|
|
9
|
+
* @param {Record<string, any>} netlist Canonical query netlist.
|
|
10
|
+
* @returns {{ tracesById: Map<string, object>, traceIdsByMembership: Map<string, string[]>, traceIdsByComponent: Map<string, string[]> }} Traversal graph.
|
|
11
|
+
*/
|
|
12
|
+
static prepare(netlist) {
|
|
13
|
+
const tracesById = new Map()
|
|
14
|
+
const traceIdsByMembership = new Map()
|
|
15
|
+
const traceIdsByComponent = new Map()
|
|
16
|
+
for (const trace of netlist.traces || []) {
|
|
17
|
+
tracesById.set(trace.id, trace)
|
|
18
|
+
for (const key of CircuitTraversal.#membershipKeys(trace)) {
|
|
19
|
+
if (!traceIdsByMembership.has(key)) {
|
|
20
|
+
traceIdsByMembership.set(key, [])
|
|
21
|
+
}
|
|
22
|
+
traceIdsByMembership.get(key).push(trace.id)
|
|
23
|
+
}
|
|
24
|
+
for (const componentId of trace.sourceComponentIds || []) {
|
|
25
|
+
if (!traceIdsByComponent.has(componentId)) {
|
|
26
|
+
traceIdsByComponent.set(componentId, [])
|
|
27
|
+
}
|
|
28
|
+
traceIdsByComponent.get(componentId).push(trace.id)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const ids of [
|
|
32
|
+
...traceIdsByMembership.values(),
|
|
33
|
+
...traceIdsByComponent.values()
|
|
34
|
+
]) {
|
|
35
|
+
ids.sort(ComponentGrouping.compareIds)
|
|
36
|
+
}
|
|
37
|
+
return { tracesById, traceIdsByMembership, traceIdsByComponent }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Traverses from one or more canonical source ids.
|
|
42
|
+
* @param {{ tracesById: Map<string, object>, traceIdsByMembership: Map<string, string[]>, traceIdsByComponent: Map<string, string[]> }} graph Prepared graph.
|
|
43
|
+
* @param {Record<string, string>} request Starting source ids.
|
|
44
|
+
* @param {{ maxDepth: number, maxResults: number }} options Bounds.
|
|
45
|
+
* @returns {object[]} Ordered connectivity records.
|
|
46
|
+
*/
|
|
47
|
+
static trace(graph, request, options) {
|
|
48
|
+
const initial = CircuitTraversal.#initialTraceIds(
|
|
49
|
+
graph,
|
|
50
|
+
request,
|
|
51
|
+
options.maxResults
|
|
52
|
+
)
|
|
53
|
+
const queue = initial.map((id) => ({
|
|
54
|
+
id,
|
|
55
|
+
depth: 0,
|
|
56
|
+
path: [{ traceId: id, via: null }]
|
|
57
|
+
}))
|
|
58
|
+
const queued = new Set(initial)
|
|
59
|
+
const visited = new Set()
|
|
60
|
+
const expandedMemberships = new Set()
|
|
61
|
+
const results = []
|
|
62
|
+
let cursor = 0
|
|
63
|
+
|
|
64
|
+
while (cursor < queue.length && results.length < options.maxResults) {
|
|
65
|
+
const current = queue[cursor]
|
|
66
|
+
cursor += 1
|
|
67
|
+
if (visited.has(current.id)) continue
|
|
68
|
+
visited.add(current.id)
|
|
69
|
+
const trace = graph.tracesById.get(current.id)
|
|
70
|
+
if (!trace) continue
|
|
71
|
+
results.push({
|
|
72
|
+
...structuredClone(trace),
|
|
73
|
+
depth: current.depth,
|
|
74
|
+
path: [...current.path]
|
|
75
|
+
})
|
|
76
|
+
if (results.length >= options.maxResults) break
|
|
77
|
+
if (current.depth >= options.maxDepth) continue
|
|
78
|
+
|
|
79
|
+
const sources = []
|
|
80
|
+
for (const key of CircuitTraversal.#membershipKeys(trace).sort(
|
|
81
|
+
ComponentGrouping.compareIds
|
|
82
|
+
)) {
|
|
83
|
+
if (expandedMemberships.has(key)) continue
|
|
84
|
+
expandedMemberships.add(key)
|
|
85
|
+
const ids = graph.traceIdsByMembership.get(key)
|
|
86
|
+
if (!ids) continue
|
|
87
|
+
sources.push({
|
|
88
|
+
key,
|
|
89
|
+
ids,
|
|
90
|
+
via: CircuitTraversal.#connector(key)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
const capacity = options.maxResults - queue.length
|
|
94
|
+
for (const neighbor of CircuitTraversal.#takeSortedUnique(
|
|
95
|
+
sources,
|
|
96
|
+
capacity,
|
|
97
|
+
new Set([...visited, ...queued])
|
|
98
|
+
)) {
|
|
99
|
+
queued.add(neighbor.id)
|
|
100
|
+
queue.push({
|
|
101
|
+
id: neighbor.id,
|
|
102
|
+
depth: current.depth + 1,
|
|
103
|
+
path: [
|
|
104
|
+
...current.path,
|
|
105
|
+
{ traceId: neighbor.id, via: neighbor.via }
|
|
106
|
+
]
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return results
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolves starting traces from direct selector indexes.
|
|
115
|
+
* @param {{ tracesById: Map<string, object>, traceIdsByMembership: Map<string, string[]>, traceIdsByComponent: Map<string, string[]> }} graph Prepared graph.
|
|
116
|
+
* @param {Record<string, string>} request Start request.
|
|
117
|
+
* @param {number} maxResults Maximum starting records.
|
|
118
|
+
* @returns {string[]} Stable starting trace ids.
|
|
119
|
+
*/
|
|
120
|
+
static #initialTraceIds(graph, request, maxResults) {
|
|
121
|
+
const sources = []
|
|
122
|
+
if (
|
|
123
|
+
request.sourceTraceId &&
|
|
124
|
+
graph.tracesById.has(request.sourceTraceId)
|
|
125
|
+
) {
|
|
126
|
+
sources.push({
|
|
127
|
+
key: 'trace',
|
|
128
|
+
ids: [request.sourceTraceId],
|
|
129
|
+
via: null
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
if (request.sourceComponentId) {
|
|
133
|
+
const ids = graph.traceIdsByComponent.get(request.sourceComponentId)
|
|
134
|
+
if (ids) {
|
|
135
|
+
sources.push({ key: 'component', ids, via: null })
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (request.sourcePortId) {
|
|
139
|
+
const ids = graph.traceIdsByMembership.get(
|
|
140
|
+
`port:${request.sourcePortId}`
|
|
141
|
+
)
|
|
142
|
+
if (ids) {
|
|
143
|
+
sources.push({ key: 'port', ids, via: null })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (request.sourceNetId) {
|
|
147
|
+
const ids = graph.traceIdsByMembership.get(
|
|
148
|
+
`net:${request.sourceNetId}`
|
|
149
|
+
)
|
|
150
|
+
if (ids) {
|
|
151
|
+
sources.push({ key: 'net', ids, via: null })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return CircuitTraversal.#takeSortedUnique(sources, maxResults).map(
|
|
155
|
+
(row) => row.id
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Takes a bounded sorted union without materializing complete source lists.
|
|
161
|
+
* @param {{ key: string, ids: Iterable<string>, via: object | null }[]} sources Sorted id sources.
|
|
162
|
+
* @param {number} limit Maximum returned rows.
|
|
163
|
+
* @param {Set<string>} [excluded] Ids that must not be returned.
|
|
164
|
+
* @returns {{ id: string, via: object | null }[]} Stable unique rows.
|
|
165
|
+
*/
|
|
166
|
+
static #takeSortedUnique(sources, limit, excluded = new Set()) {
|
|
167
|
+
if (limit <= 0) return []
|
|
168
|
+
const heap = sources
|
|
169
|
+
.map((source, index) =>
|
|
170
|
+
CircuitTraversal.#iteratorState(source, index)
|
|
171
|
+
)
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
CircuitTraversal.#heapify(heap)
|
|
174
|
+
const seen = new Set(excluded)
|
|
175
|
+
const result = []
|
|
176
|
+
|
|
177
|
+
while (heap.length && result.length < limit) {
|
|
178
|
+
const current = CircuitTraversal.#popHeap(heap)
|
|
179
|
+
const accepted = !seen.has(current.id)
|
|
180
|
+
if (accepted) {
|
|
181
|
+
seen.add(current.id)
|
|
182
|
+
result.push({ id: current.id, via: current.via })
|
|
183
|
+
if (result.length >= limit) break
|
|
184
|
+
}
|
|
185
|
+
const next = current.iterator.next()
|
|
186
|
+
if (!next.done) {
|
|
187
|
+
current.id = String(next.value)
|
|
188
|
+
CircuitTraversal.#pushHeap(heap, current)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return result
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Opens one sorted source iterator and reads only its first id.
|
|
196
|
+
* @param {{ key: string, ids: Iterable<string>, via: object | null }} source Id source.
|
|
197
|
+
* @param {number} index Stable source index.
|
|
198
|
+
* @returns {object | null} Active iterator state.
|
|
199
|
+
*/
|
|
200
|
+
static #iteratorState(source, index) {
|
|
201
|
+
const iterator = source.ids[Symbol.iterator]()
|
|
202
|
+
const first = iterator.next()
|
|
203
|
+
return first.done
|
|
204
|
+
? null
|
|
205
|
+
: {
|
|
206
|
+
id: String(first.value),
|
|
207
|
+
index,
|
|
208
|
+
iterator,
|
|
209
|
+
key: source.key,
|
|
210
|
+
via: source.via
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Orders active iterator heads by id and deterministic connector source.
|
|
216
|
+
* @param {object} left Left iterator state.
|
|
217
|
+
* @param {object} right Right iterator state.
|
|
218
|
+
* @returns {number} Stable sort order.
|
|
219
|
+
*/
|
|
220
|
+
static #compareIteratorStates(left, right) {
|
|
221
|
+
return (
|
|
222
|
+
ComponentGrouping.compareIds(left.id, right.id) ||
|
|
223
|
+
ComponentGrouping.compareIds(left.key, right.key) ||
|
|
224
|
+
left.index - right.index
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Builds a minimum heap of active iterator heads in linear time.
|
|
230
|
+
* @param {object[]} heap Iterator state heap.
|
|
231
|
+
* @returns {void}
|
|
232
|
+
*/
|
|
233
|
+
static #heapify(heap) {
|
|
234
|
+
for (
|
|
235
|
+
let index = Math.floor(heap.length / 2) - 1;
|
|
236
|
+
index >= 0;
|
|
237
|
+
index -= 1
|
|
238
|
+
) {
|
|
239
|
+
CircuitTraversal.#siftDown(heap, index)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Removes the smallest active iterator head.
|
|
245
|
+
* @param {object[]} heap Iterator state heap.
|
|
246
|
+
* @returns {object} Smallest iterator state.
|
|
247
|
+
*/
|
|
248
|
+
static #popHeap(heap) {
|
|
249
|
+
const first = heap[0]
|
|
250
|
+
const last = heap.pop()
|
|
251
|
+
if (heap.length) {
|
|
252
|
+
heap[0] = last
|
|
253
|
+
CircuitTraversal.#siftDown(heap, 0)
|
|
254
|
+
}
|
|
255
|
+
return first
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Adds one active iterator head while preserving heap order.
|
|
260
|
+
* @param {object[]} heap Iterator state heap.
|
|
261
|
+
* @param {object} state Iterator state.
|
|
262
|
+
* @returns {void}
|
|
263
|
+
*/
|
|
264
|
+
static #pushHeap(heap, state) {
|
|
265
|
+
let index = heap.length
|
|
266
|
+
heap.push(state)
|
|
267
|
+
while (index > 0) {
|
|
268
|
+
const parentIndex = Math.floor((index - 1) / 2)
|
|
269
|
+
const parent = heap[parentIndex]
|
|
270
|
+
if (CircuitTraversal.#compareIteratorStates(parent, state) <= 0) {
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
heap[index] = parent
|
|
274
|
+
index = parentIndex
|
|
275
|
+
}
|
|
276
|
+
heap[index] = state
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Restores minimum-heap order below one replaced iterator head.
|
|
281
|
+
* @param {object[]} heap Iterator state heap.
|
|
282
|
+
* @param {number} startIndex Replaced head index.
|
|
283
|
+
* @returns {void}
|
|
284
|
+
*/
|
|
285
|
+
static #siftDown(heap, startIndex) {
|
|
286
|
+
const state = heap[startIndex]
|
|
287
|
+
let index = startIndex
|
|
288
|
+
while (true) {
|
|
289
|
+
const leftIndex = index * 2 + 1
|
|
290
|
+
if (leftIndex >= heap.length) break
|
|
291
|
+
const rightIndex = leftIndex + 1
|
|
292
|
+
let childIndex = leftIndex
|
|
293
|
+
if (
|
|
294
|
+
rightIndex < heap.length &&
|
|
295
|
+
CircuitTraversal.#compareIteratorStates(
|
|
296
|
+
heap[rightIndex],
|
|
297
|
+
heap[leftIndex]
|
|
298
|
+
) < 0
|
|
299
|
+
) {
|
|
300
|
+
childIndex = rightIndex
|
|
301
|
+
}
|
|
302
|
+
if (
|
|
303
|
+
CircuitTraversal.#compareIteratorStates(
|
|
304
|
+
heap[childIndex],
|
|
305
|
+
state
|
|
306
|
+
) >= 0
|
|
307
|
+
) {
|
|
308
|
+
break
|
|
309
|
+
}
|
|
310
|
+
heap[index] = heap[childIndex]
|
|
311
|
+
index = childIndex
|
|
312
|
+
}
|
|
313
|
+
heap[index] = state
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Builds shared-membership keys for one trace.
|
|
318
|
+
* @param {Record<string, any>} trace Trace record.
|
|
319
|
+
* @returns {string[]} Membership keys.
|
|
320
|
+
*/
|
|
321
|
+
static #membershipKeys(trace) {
|
|
322
|
+
return [
|
|
323
|
+
...(trace.sourcePortIds || []).map((id) => `port:${id}`),
|
|
324
|
+
...(trace.sourceNetIds || []).map((id) => `net:${id}`),
|
|
325
|
+
...(trace.internalConnectionIds || []).map(
|
|
326
|
+
(id) => `internalConnection:${id}`
|
|
327
|
+
)
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Converts one membership key into a clone-safe path connector.
|
|
333
|
+
* @param {string} key Membership key.
|
|
334
|
+
* @returns {{ kind: 'port' | 'net' | 'internalConnection', id: string }} Connector record.
|
|
335
|
+
*/
|
|
336
|
+
static #connector(key) {
|
|
337
|
+
const separator = key.indexOf(':')
|
|
338
|
+
return {
|
|
339
|
+
kind: key.slice(0, separator),
|
|
340
|
+
id: key.slice(separator + 1)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
const COMPONENT_VALUE_FIELDS_BY_TYPE = Object.freeze({
|
|
2
|
+
simple_battery: ['capacity'],
|
|
3
|
+
simple_capacitor: ['display_capacitance', 'capacitance'],
|
|
4
|
+
simple_crystal: ['frequency'],
|
|
5
|
+
simple_current_source: ['current'],
|
|
6
|
+
simple_fuse: ['current_rating_amps'],
|
|
7
|
+
simple_inductor: ['display_inductance', 'inductance'],
|
|
8
|
+
simple_potentiometer: ['display_max_resistance', 'max_resistance'],
|
|
9
|
+
simple_power_source: ['voltage'],
|
|
10
|
+
simple_resistor: ['display_resistance', 'resistance'],
|
|
11
|
+
simple_resonator: ['frequency'],
|
|
12
|
+
simple_voltage_source: ['voltage']
|
|
13
|
+
})
|
|
14
|
+
const COMPONENT_VALUE_FALLBACK_FIELDS = [
|
|
15
|
+
'display_resistance',
|
|
16
|
+
'display_capacitance',
|
|
17
|
+
'display_inductance',
|
|
18
|
+
'display_max_resistance',
|
|
19
|
+
'resistance',
|
|
20
|
+
'capacitance',
|
|
21
|
+
'inductance',
|
|
22
|
+
'frequency',
|
|
23
|
+
'max_resistance',
|
|
24
|
+
'voltage',
|
|
25
|
+
'current',
|
|
26
|
+
'capacity'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds deterministic component and net records from prepared CircuitJSON indexes.
|
|
31
|
+
*/
|
|
32
|
+
export class ComponentGrouping {
|
|
33
|
+
/**
|
|
34
|
+
* Builds source-component rows with their canonical source-port ids.
|
|
35
|
+
* @param {Record<string, any>} elementsIndex Prepared elements index.
|
|
36
|
+
* @param {Record<string, any>} relationsIndex Prepared relations index.
|
|
37
|
+
* @returns {object[]} Stable component records.
|
|
38
|
+
*/
|
|
39
|
+
static components(elementsIndex, relationsIndex) {
|
|
40
|
+
const components =
|
|
41
|
+
elementsIndex.elementsByType.get('source_component') || []
|
|
42
|
+
const byComponent =
|
|
43
|
+
relationsIndex.relationsByField.get('source_component_id') ||
|
|
44
|
+
new Map()
|
|
45
|
+
return components
|
|
46
|
+
.map((component) => {
|
|
47
|
+
const id = String(component.source_component_id || '').trim()
|
|
48
|
+
const related = byComponent.get(id) || []
|
|
49
|
+
const pins = related
|
|
50
|
+
.filter((element) => element?.type === 'source_port')
|
|
51
|
+
.map((port) => ComponentGrouping.#pin(port))
|
|
52
|
+
.sort((left, right) =>
|
|
53
|
+
ComponentGrouping.compareIds(left.id, right.id)
|
|
54
|
+
)
|
|
55
|
+
return ComponentGrouping.#withoutEmpty({
|
|
56
|
+
id,
|
|
57
|
+
name: String(
|
|
58
|
+
component.display_name || component.name || id
|
|
59
|
+
),
|
|
60
|
+
designator: String(component.name || id),
|
|
61
|
+
type: String(component.ftype || ''),
|
|
62
|
+
value: ComponentGrouping.#componentValue(component),
|
|
63
|
+
footprint: ComponentGrouping.#footprint(component, related),
|
|
64
|
+
mpn: ComponentGrouping.#mpn(component),
|
|
65
|
+
description: ComponentGrouping.#text(component.description),
|
|
66
|
+
doNotPopulate:
|
|
67
|
+
component.do_not_populate === true ||
|
|
68
|
+
component.exclude_from_bom === true,
|
|
69
|
+
pinIds: pins.map((pin) => pin.id),
|
|
70
|
+
pins
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
.filter((component) => component.id)
|
|
74
|
+
.sort((left, right) =>
|
|
75
|
+
ComponentGrouping.compareIds(left.id, right.id)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Builds source-net rows with trace and port membership.
|
|
81
|
+
* @param {Record<string, any>} elementsIndex Prepared elements index.
|
|
82
|
+
* @param {object[]} traces Canonical trace records.
|
|
83
|
+
* @returns {object[]} Stable net records.
|
|
84
|
+
*/
|
|
85
|
+
static nets(elementsIndex, traces) {
|
|
86
|
+
const nets = elementsIndex.elementsByType.get('source_net') || []
|
|
87
|
+
const tracesByNetId = new Map()
|
|
88
|
+
for (const trace of traces) {
|
|
89
|
+
for (const netId of trace.sourceNetIds) {
|
|
90
|
+
if (!tracesByNetId.has(netId)) {
|
|
91
|
+
tracesByNetId.set(netId, [])
|
|
92
|
+
}
|
|
93
|
+
tracesByNetId.get(netId).push(trace)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return nets
|
|
97
|
+
.map((net) => {
|
|
98
|
+
const id = String(net.source_net_id || '').trim()
|
|
99
|
+
const connected = tracesByNetId.get(id) || []
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
name: String(net.name || id),
|
|
103
|
+
traceIds: connected.map((trace) => trace.id),
|
|
104
|
+
portIds: [
|
|
105
|
+
...new Set(
|
|
106
|
+
connected.flatMap((trace) => trace.sourcePortIds)
|
|
107
|
+
)
|
|
108
|
+
].sort(ComponentGrouping.compareIds)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.filter((net) => net.id)
|
|
112
|
+
.sort((left, right) =>
|
|
113
|
+
ComponentGrouping.compareIds(left.id, right.id)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compares stable ids without locale-dependent collation.
|
|
119
|
+
* @param {unknown} left Left id.
|
|
120
|
+
* @param {unknown} right Right id.
|
|
121
|
+
* @returns {number} Sort order.
|
|
122
|
+
*/
|
|
123
|
+
static compareIds(left, right) {
|
|
124
|
+
const leftText = String(left || '')
|
|
125
|
+
const rightText = String(right || '')
|
|
126
|
+
return leftText < rightText ? -1 : leftText > rightText ? 1 : 0
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Builds one canonical source-port record.
|
|
131
|
+
* @param {Record<string, any>} port Source port.
|
|
132
|
+
* @returns {object} Port record.
|
|
133
|
+
*/
|
|
134
|
+
static #pin(port) {
|
|
135
|
+
return ComponentGrouping.#withoutEmpty({
|
|
136
|
+
id: String(port.source_port_id || '').trim(),
|
|
137
|
+
name: ComponentGrouping.#text(port.name),
|
|
138
|
+
pinNumber:
|
|
139
|
+
port.pin_number === undefined ? undefined : port.pin_number,
|
|
140
|
+
netIds: ComponentGrouping.#ids([
|
|
141
|
+
port.source_net_id,
|
|
142
|
+
port.connected_source_net_id,
|
|
143
|
+
port.source_net_ids,
|
|
144
|
+
port.connected_source_net_ids
|
|
145
|
+
])
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves the first linked PCB footprint before legacy source fields.
|
|
151
|
+
* @param {Record<string, any>} component Source component.
|
|
152
|
+
* @param {object[]} related Elements related by source-component id.
|
|
153
|
+
* @returns {string | undefined} Footprint name.
|
|
154
|
+
*/
|
|
155
|
+
static #footprint(component, related) {
|
|
156
|
+
const pcbComponents = related
|
|
157
|
+
.filter((element) => element?.type === 'pcb_component')
|
|
158
|
+
.sort((left, right) =>
|
|
159
|
+
ComponentGrouping.compareIds(
|
|
160
|
+
left.pcb_component_id,
|
|
161
|
+
right.pcb_component_id
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
for (const pcbComponent of pcbComponents) {
|
|
165
|
+
const footprint = ComponentGrouping.#text(
|
|
166
|
+
pcbComponent.metadata?.kicad_footprint?.footprintName
|
|
167
|
+
)
|
|
168
|
+
if (footprint) return footprint
|
|
169
|
+
}
|
|
170
|
+
return ComponentGrouping.#text(
|
|
171
|
+
component.footprint || component.footprint_name
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolves standard display and typed source-component values.
|
|
177
|
+
* @param {Record<string, any>} component Source component.
|
|
178
|
+
* @returns {string | undefined} Canonical display value.
|
|
179
|
+
*/
|
|
180
|
+
static #componentValue(component) {
|
|
181
|
+
const typedFields =
|
|
182
|
+
COMPONENT_VALUE_FIELDS_BY_TYPE[String(component.ftype || '')] || []
|
|
183
|
+
const fields = [
|
|
184
|
+
'display_value',
|
|
185
|
+
...typedFields,
|
|
186
|
+
'value',
|
|
187
|
+
...COMPONENT_VALUE_FALLBACK_FIELDS
|
|
188
|
+
]
|
|
189
|
+
for (const field of new Set(fields)) {
|
|
190
|
+
const value = ComponentGrouping.#scalarText(component[field])
|
|
191
|
+
if (value !== undefined) return value
|
|
192
|
+
}
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolves the first source-native manufacturer/supplier part number.
|
|
198
|
+
* @param {Record<string, any>} component Source component.
|
|
199
|
+
* @returns {string | undefined} Part number.
|
|
200
|
+
*/
|
|
201
|
+
static #mpn(component) {
|
|
202
|
+
const direct = ComponentGrouping.#text(
|
|
203
|
+
component.manufacturer_part_number ||
|
|
204
|
+
component.mpn ||
|
|
205
|
+
component.part_number
|
|
206
|
+
)
|
|
207
|
+
if (direct) return direct
|
|
208
|
+
const suppliers = component.supplier_part_numbers
|
|
209
|
+
if (!suppliers || typeof suppliers !== 'object') return undefined
|
|
210
|
+
for (const value of Object.values(suppliers)) {
|
|
211
|
+
const candidates = Array.isArray(value) ? value : [value]
|
|
212
|
+
for (const candidate of candidates) {
|
|
213
|
+
const text = ComponentGrouping.#text(candidate)
|
|
214
|
+
if (text) return text
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return undefined
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Flattens scalar/array relation ids into a stable unique list.
|
|
222
|
+
* @param {unknown[]} values Relation values.
|
|
223
|
+
* @returns {string[]} Stable ids.
|
|
224
|
+
*/
|
|
225
|
+
static #ids(values) {
|
|
226
|
+
return [
|
|
227
|
+
...new Set(
|
|
228
|
+
values
|
|
229
|
+
.flatMap((value) =>
|
|
230
|
+
Array.isArray(value) ? value : [value]
|
|
231
|
+
)
|
|
232
|
+
.map((value) => String(value || '').trim())
|
|
233
|
+
.filter(Boolean)
|
|
234
|
+
)
|
|
235
|
+
].sort(ComponentGrouping.compareIds)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Normalizes optional text without executing caller coercion hooks.
|
|
240
|
+
* @param {unknown} value Text candidate.
|
|
241
|
+
* @returns {string | undefined} Normalized text.
|
|
242
|
+
*/
|
|
243
|
+
static #text(value) {
|
|
244
|
+
return typeof value === 'string' && value.trim()
|
|
245
|
+
? value.trim()
|
|
246
|
+
: undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Normalizes a string or finite numeric scalar for query matching.
|
|
251
|
+
* @param {unknown} value Scalar candidate.
|
|
252
|
+
* @returns {string | undefined} Normalized scalar text.
|
|
253
|
+
*/
|
|
254
|
+
static #scalarText(value) {
|
|
255
|
+
if (typeof value === 'string') return ComponentGrouping.#text(value)
|
|
256
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
257
|
+
? String(value)
|
|
258
|
+
: undefined
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Omits undefined, empty-string, and false optional fields.
|
|
263
|
+
* @param {Record<string, any>} record Record candidate.
|
|
264
|
+
* @returns {Record<string, any>} Compact record.
|
|
265
|
+
*/
|
|
266
|
+
static #withoutEmpty(record) {
|
|
267
|
+
return Object.fromEntries(
|
|
268
|
+
Object.entries(record).filter(([, value]) => {
|
|
269
|
+
if (value === undefined || value === '') return false
|
|
270
|
+
if (value === false) return false
|
|
271
|
+
return true
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|