circuitjson-toolkit 1.0.3 → 1.0.16
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 +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { PcbInteractionPrimitiveModel } from './PcbInteractionPrimitiveModel.mjs'
|
|
2
|
+
import { PcbCandidateSelectionModel } from './PcbCandidateSelectionModel.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves PCB primitives contained by a measured board-space rectangle.
|
|
6
|
+
*/
|
|
7
|
+
export class PcbBoundsSelectionModel {
|
|
8
|
+
/**
|
|
9
|
+
* Resolves contained candidates and unique component/net selections.
|
|
10
|
+
* @param {object | object[]} documentModel Parsed PCB document model.
|
|
11
|
+
* @param {{ minX?: unknown, minY?: unknown, maxX?: unknown, maxY?: unknown }} bounds Board-space bounds.
|
|
12
|
+
* @param {{ side?: string, hiddenLayers?: string[], hiddenObjects?: string[] }} [options] Selection options.
|
|
13
|
+
* @returns {{ bounds: object | null, point: object | null, candidates: object[], selectedCandidate: object | null, componentKeys: string[], netNames: string[] }}
|
|
14
|
+
*/
|
|
15
|
+
static resolve(documentModel, bounds, options = {}) {
|
|
16
|
+
const normalizedBounds = PcbBoundsSelectionModel.normalizeBounds(bounds)
|
|
17
|
+
if (!normalizedBounds) return PcbBoundsSelectionModel.#empty()
|
|
18
|
+
|
|
19
|
+
const candidates = PcbBoundsSelectionModel.#containedPrimitives(
|
|
20
|
+
documentModel,
|
|
21
|
+
normalizedBounds,
|
|
22
|
+
options
|
|
23
|
+
).map((primitive) =>
|
|
24
|
+
PcbCandidateSelectionModel.fromPrimitive(primitive)
|
|
25
|
+
)
|
|
26
|
+
const uniqueCandidates =
|
|
27
|
+
PcbBoundsSelectionModel.#uniqueCandidates(candidates)
|
|
28
|
+
const selectedCandidate =
|
|
29
|
+
PcbCandidateSelectionModel.selectedCandidate(uniqueCandidates)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
bounds: normalizedBounds,
|
|
33
|
+
point: PcbBoundsSelectionModel.#center(normalizedBounds),
|
|
34
|
+
candidates: uniqueCandidates,
|
|
35
|
+
selectedCandidate,
|
|
36
|
+
componentKeys: PcbBoundsSelectionModel.#uniqueStrings(
|
|
37
|
+
uniqueCandidates.map((candidate) => candidate.componentKey)
|
|
38
|
+
),
|
|
39
|
+
netNames: PcbBoundsSelectionModel.#uniqueStrings(
|
|
40
|
+
uniqueCandidates.map((candidate) =>
|
|
41
|
+
PcbCandidateSelectionModel.netName(candidate)
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes board-space bounds.
|
|
49
|
+
* @param {{ minX?: unknown, minY?: unknown, maxX?: unknown, maxY?: unknown } | null | undefined} bounds Bounds candidate.
|
|
50
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number } | null}
|
|
51
|
+
*/
|
|
52
|
+
static normalizeBounds(bounds) {
|
|
53
|
+
const minX = PcbBoundsSelectionModel.#finite(bounds?.minX)
|
|
54
|
+
const minY = PcbBoundsSelectionModel.#finite(bounds?.minY)
|
|
55
|
+
const maxX = PcbBoundsSelectionModel.#finite(bounds?.maxX)
|
|
56
|
+
const maxY = PcbBoundsSelectionModel.#finite(bounds?.maxY)
|
|
57
|
+
if ([minX, minY, maxX, maxY].some((value) => value === null)) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const left = Math.min(minX, maxX)
|
|
62
|
+
const right = Math.max(minX, maxX)
|
|
63
|
+
const top = Math.min(minY, maxY)
|
|
64
|
+
const bottom = Math.max(minY, maxY)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
minX: left,
|
|
68
|
+
minY: top,
|
|
69
|
+
maxX: right,
|
|
70
|
+
maxY: bottom,
|
|
71
|
+
width: right - left,
|
|
72
|
+
height: bottom - top
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds an empty selection result.
|
|
78
|
+
* @returns {{ bounds: null, point: null, candidates: object[], selectedCandidate: null, componentKeys: string[], netNames: string[] }}
|
|
79
|
+
*/
|
|
80
|
+
static #empty() {
|
|
81
|
+
return {
|
|
82
|
+
bounds: null,
|
|
83
|
+
point: null,
|
|
84
|
+
candidates: [],
|
|
85
|
+
selectedCandidate: null,
|
|
86
|
+
componentKeys: [],
|
|
87
|
+
netNames: []
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns primitives that are visible and touch the measured bounds.
|
|
93
|
+
* @param {object | object[]} documentModel Parsed PCB document model.
|
|
94
|
+
* @param {object} bounds Normalized bounds.
|
|
95
|
+
* @param {object} options Selection options.
|
|
96
|
+
* @returns {object[]}
|
|
97
|
+
*/
|
|
98
|
+
static #containedPrimitives(documentModel, bounds, options) {
|
|
99
|
+
return PcbInteractionPrimitiveModel.build(
|
|
100
|
+
documentModel
|
|
101
|
+
).primitives.filter(
|
|
102
|
+
(primitive) =>
|
|
103
|
+
PcbBoundsSelectionModel.#isVisible(primitive, options) &&
|
|
104
|
+
PcbBoundsSelectionModel.#touchesBounds(primitive, bounds)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns true when a primitive should participate in area selection.
|
|
110
|
+
* @param {object} primitive Primitive row.
|
|
111
|
+
* @param {{ side?: string, hiddenLayers?: string[], hiddenObjects?: string[] }} options Selection options.
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
static #isVisible(primitive, options) {
|
|
115
|
+
const side = String(options?.side || '')
|
|
116
|
+
if (side && primitive.side && primitive.side !== side) return false
|
|
117
|
+
|
|
118
|
+
const hiddenLayers = new Set(
|
|
119
|
+
(Array.isArray(options?.hiddenLayers) ? options.hiddenLayers : [])
|
|
120
|
+
.map(String)
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
)
|
|
123
|
+
if (primitive.layer && hiddenLayers.has(String(primitive.layer))) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hiddenObjects = new Set(
|
|
128
|
+
(Array.isArray(options?.hiddenObjects) ? options.hiddenObjects : [])
|
|
129
|
+
.map(String)
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
)
|
|
132
|
+
return !hiddenObjects.has(PcbBoundsSelectionModel.#objectKey(primitive))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Returns true when a primitive overlaps or sits inside bounds.
|
|
137
|
+
* @param {object} primitive Primitive row.
|
|
138
|
+
* @param {object} bounds Normalized bounds.
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
static #touchesBounds(primitive, bounds) {
|
|
142
|
+
const primitiveBounds =
|
|
143
|
+
PcbBoundsSelectionModel.normalizeBounds(primitive?.bounds) ||
|
|
144
|
+
PcbBoundsSelectionModel.#pointBounds(primitive)
|
|
145
|
+
if (!primitiveBounds) return false
|
|
146
|
+
|
|
147
|
+
return !(
|
|
148
|
+
primitiveBounds.maxX < bounds.minX ||
|
|
149
|
+
primitiveBounds.minX > bounds.maxX ||
|
|
150
|
+
primitiveBounds.maxY < bounds.minY ||
|
|
151
|
+
primitiveBounds.minY > bounds.maxY
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Builds zero-area bounds around primitive center coordinates.
|
|
157
|
+
* @param {object} primitive Primitive row.
|
|
158
|
+
* @returns {object | null}
|
|
159
|
+
*/
|
|
160
|
+
static #pointBounds(primitive) {
|
|
161
|
+
const x = PcbBoundsSelectionModel.#finite(primitive?.x)
|
|
162
|
+
const y = PcbBoundsSelectionModel.#finite(primitive?.y)
|
|
163
|
+
if (x === null || y === null) return null
|
|
164
|
+
return PcbBoundsSelectionModel.normalizeBounds({
|
|
165
|
+
minX: x,
|
|
166
|
+
minY: y,
|
|
167
|
+
maxX: x,
|
|
168
|
+
maxY: y
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Deduplicates candidates by visible identity.
|
|
174
|
+
* @param {object[]} candidates Candidate rows.
|
|
175
|
+
* @returns {object[]}
|
|
176
|
+
*/
|
|
177
|
+
static #uniqueCandidates(candidates) {
|
|
178
|
+
const seen = new Set()
|
|
179
|
+
const unique = []
|
|
180
|
+
for (const candidate of candidates) {
|
|
181
|
+
const key = [
|
|
182
|
+
candidate.kind,
|
|
183
|
+
candidate.componentKey,
|
|
184
|
+
PcbCandidateSelectionModel.netName(candidate),
|
|
185
|
+
candidate.layer
|
|
186
|
+
].join('|')
|
|
187
|
+
if (seen.has(key)) continue
|
|
188
|
+
seen.add(key)
|
|
189
|
+
unique.push(candidate)
|
|
190
|
+
}
|
|
191
|
+
return unique
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Deduplicates non-empty strings in source order.
|
|
196
|
+
* @param {string[]} values Candidate values.
|
|
197
|
+
* @returns {string[]}
|
|
198
|
+
*/
|
|
199
|
+
static #uniqueStrings(values) {
|
|
200
|
+
return [
|
|
201
|
+
...new Set(values.map(String).map((value) => value.trim()))
|
|
202
|
+
].filter(Boolean)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolves the sidebar object key for a primitive.
|
|
207
|
+
* @param {object} primitive Primitive row.
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
static #objectKey(primitive) {
|
|
211
|
+
return (
|
|
212
|
+
{
|
|
213
|
+
board: 'page',
|
|
214
|
+
cutout: 'page',
|
|
215
|
+
pad: 'pads',
|
|
216
|
+
track: 'tracks',
|
|
217
|
+
via: 'vias',
|
|
218
|
+
zone: 'zones',
|
|
219
|
+
'copper-text': 'footprint-text',
|
|
220
|
+
copper_text: 'footprint-text',
|
|
221
|
+
silkscreen: 'footprint-text',
|
|
222
|
+
silkscreen_text: 'footprint-text',
|
|
223
|
+
fabrication: 'footprint-text'
|
|
224
|
+
}[primitive?.kind] || String(primitive?.kind || '')
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolves the center point for normalized bounds.
|
|
230
|
+
* @param {object} bounds Normalized bounds.
|
|
231
|
+
* @returns {{ x: number, y: number }}
|
|
232
|
+
*/
|
|
233
|
+
static #center(bounds) {
|
|
234
|
+
return {
|
|
235
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
236
|
+
y: (bounds.minY + bounds.maxY) / 2
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Converts a value to a finite number or null.
|
|
242
|
+
* @param {unknown} value Numeric candidate.
|
|
243
|
+
* @returns {number | null}
|
|
244
|
+
*/
|
|
245
|
+
static #finite(value) {
|
|
246
|
+
if (value === undefined || value === null || value === '') return null
|
|
247
|
+
const number = Number(value)
|
|
248
|
+
return Number.isFinite(number) ? number : null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes PCB hit candidates for shared click, hover, and bounds workflows.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbCandidateSelectionModel {
|
|
5
|
+
/**
|
|
6
|
+
* Returns the first component-backed candidate.
|
|
7
|
+
* @param {object[]} candidates Hit-test candidates.
|
|
8
|
+
* @returns {object | null}
|
|
9
|
+
*/
|
|
10
|
+
static componentCandidate(candidates) {
|
|
11
|
+
return (
|
|
12
|
+
(Array.isArray(candidates) ? candidates : []).find((candidate) =>
|
|
13
|
+
String(candidate?.componentKey || '').trim()
|
|
14
|
+
) || null
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns the first net-backed candidate.
|
|
20
|
+
* @param {object[]} candidates Hit-test candidates.
|
|
21
|
+
* @returns {object | null}
|
|
22
|
+
*/
|
|
23
|
+
static netCandidate(candidates) {
|
|
24
|
+
return (
|
|
25
|
+
(Array.isArray(candidates) ? candidates : []).find((candidate) =>
|
|
26
|
+
PcbCandidateSelectionModel.netName(candidate)
|
|
27
|
+
) || null
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the candidate chosen for user-facing selection.
|
|
33
|
+
* @param {object[]} candidates Hit-test candidates.
|
|
34
|
+
* @returns {object | null}
|
|
35
|
+
*/
|
|
36
|
+
static selectedCandidate(candidates) {
|
|
37
|
+
const rows = Array.isArray(candidates) ? candidates : []
|
|
38
|
+
return (
|
|
39
|
+
PcbCandidateSelectionModel.componentCandidate(rows) ||
|
|
40
|
+
PcbCandidateSelectionModel.netCandidate(rows) ||
|
|
41
|
+
rows[0] ||
|
|
42
|
+
null
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns one candidate's net name.
|
|
48
|
+
* @param {object | null} candidate Hit-test candidate.
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
static netName(candidate) {
|
|
52
|
+
return String(
|
|
53
|
+
candidate?.netName ?? candidate?.net ?? candidate?.net_name ?? ''
|
|
54
|
+
).trim()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds a normalized candidate from a primitive row.
|
|
59
|
+
* @param {object} primitive Primitive row.
|
|
60
|
+
* @returns {object}
|
|
61
|
+
*/
|
|
62
|
+
static fromPrimitive(primitive) {
|
|
63
|
+
return {
|
|
64
|
+
role: String(primitive?.kind || ''),
|
|
65
|
+
kind: String(primitive?.kind || ''),
|
|
66
|
+
componentKey: String(primitive?.componentKey || ''),
|
|
67
|
+
componentId: String(
|
|
68
|
+
primitive?.componentId || primitive?.componentKey || ''
|
|
69
|
+
),
|
|
70
|
+
netName: String(primitive?.netName || ''),
|
|
71
|
+
net: String(primitive?.netName || ''),
|
|
72
|
+
layer: String(primitive?.layer || ''),
|
|
73
|
+
layerKey: String(primitive?.layer || ''),
|
|
74
|
+
source: primitive?.source || primitive || null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|