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,335 @@
|
|
|
1
|
+
import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds standards-shaped CircuitJSON for selected-part ZIP exports.
|
|
5
|
+
*/
|
|
6
|
+
export class SelectedPartCircuitJsonExportAdapter {
|
|
7
|
+
/**
|
|
8
|
+
* Builds a CircuitJSON element array for one selected part.
|
|
9
|
+
* @param {{ designator?: string, symbol?: object, footprint?: object }} selectedPart Selected part data.
|
|
10
|
+
* @param {object} documentModel Active document model.
|
|
11
|
+
* @param {string} partName Export artifact name.
|
|
12
|
+
* @returns {object[]}
|
|
13
|
+
*/
|
|
14
|
+
static build(selectedPart, documentModel, partName) {
|
|
15
|
+
const designator = selectedPart.designator || 'selected-part'
|
|
16
|
+
const idToken =
|
|
17
|
+
SelectedPartCircuitJsonExportAdapter.#safeIdentifier(designator)
|
|
18
|
+
const sourceComponentId = 'source_component_' + idToken
|
|
19
|
+
const pcbComponentId = 'pcb_component_' + idToken
|
|
20
|
+
const circuitJson = [
|
|
21
|
+
{
|
|
22
|
+
type: 'source_project_metadata',
|
|
23
|
+
name: documentModel?.fileName || 'Selected part export',
|
|
24
|
+
software_used_string:
|
|
25
|
+
documentModel?.sourceFormat || documentModel?.fileType || ''
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: 'source_component',
|
|
29
|
+
source_component_id: sourceComponentId,
|
|
30
|
+
name: partName,
|
|
31
|
+
ftype: 'simple_chip',
|
|
32
|
+
manufacturer_part_number: selectedPart.symbol?.value || '',
|
|
33
|
+
supplier_part_numbers: {}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'schematic_component',
|
|
37
|
+
schematic_component_id: 'schematic_component_' + idToken,
|
|
38
|
+
source_component_id: sourceComponentId,
|
|
39
|
+
center: { x: 0, y: 0 },
|
|
40
|
+
size: SelectedPartCircuitJsonExportAdapter.#schematicSize(
|
|
41
|
+
selectedPart
|
|
42
|
+
),
|
|
43
|
+
rotation: 0
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'pcb_component',
|
|
47
|
+
pcb_component_id: pcbComponentId,
|
|
48
|
+
source_component_id: sourceComponentId,
|
|
49
|
+
center: SelectedPartCircuitJsonExportAdapter.#footprintCenter(
|
|
50
|
+
selectedPart
|
|
51
|
+
),
|
|
52
|
+
layer: 'top',
|
|
53
|
+
rotation: 0,
|
|
54
|
+
width: SelectedPartCircuitJsonExportAdapter.#footprintSize(
|
|
55
|
+
selectedPart
|
|
56
|
+
).width,
|
|
57
|
+
height: SelectedPartCircuitJsonExportAdapter.#footprintSize(
|
|
58
|
+
selectedPart
|
|
59
|
+
).height
|
|
60
|
+
},
|
|
61
|
+
...SelectedPartCircuitJsonExportAdapter.#sourcePorts(
|
|
62
|
+
selectedPart,
|
|
63
|
+
sourceComponentId
|
|
64
|
+
),
|
|
65
|
+
...SelectedPartCircuitJsonExportAdapter.#pcbPads(
|
|
66
|
+
selectedPart,
|
|
67
|
+
pcbComponentId
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
CircuitJsonDocument.assertModel(circuitJson)
|
|
72
|
+
return circuitJson
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds source port entries.
|
|
77
|
+
* @param {{ symbol?: { pins?: object[] } }} selectedPart Selected part data.
|
|
78
|
+
* @param {string} sourceComponentId Source component id.
|
|
79
|
+
* @returns {object[]}
|
|
80
|
+
*/
|
|
81
|
+
static #sourcePorts(selectedPart, sourceComponentId) {
|
|
82
|
+
return SelectedPartCircuitJsonExportAdapter.#array(
|
|
83
|
+
selectedPart.symbol?.pins
|
|
84
|
+
).map((pin, index) => {
|
|
85
|
+
const pinName = String(pin.name || index + 1)
|
|
86
|
+
const pinNumber = String(pin.number || index + 1)
|
|
87
|
+
const entry = {
|
|
88
|
+
type: 'source_port',
|
|
89
|
+
source_port_id:
|
|
90
|
+
sourceComponentId +
|
|
91
|
+
'_port_' +
|
|
92
|
+
SelectedPartCircuitJsonExportAdapter.#safeIdentifier(
|
|
93
|
+
pinNumber
|
|
94
|
+
),
|
|
95
|
+
source_component_id: sourceComponentId,
|
|
96
|
+
name: pinName,
|
|
97
|
+
port_hints: [pinNumber]
|
|
98
|
+
}
|
|
99
|
+
const numericPinNumber =
|
|
100
|
+
SelectedPartCircuitJsonExportAdapter.#numericPinNumber(
|
|
101
|
+
pinNumber
|
|
102
|
+
)
|
|
103
|
+
if (numericPinNumber !== null) entry.pin_number = numericPinNumber
|
|
104
|
+
return entry
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds PCB SMT pad entries.
|
|
110
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
111
|
+
* @param {string} pcbComponentId PCB component id.
|
|
112
|
+
* @returns {object[]}
|
|
113
|
+
*/
|
|
114
|
+
static #pcbPads(selectedPart, pcbComponentId) {
|
|
115
|
+
return SelectedPartCircuitJsonExportAdapter.#array(
|
|
116
|
+
selectedPart.footprint?.pads
|
|
117
|
+
).map((pad, index) =>
|
|
118
|
+
SelectedPartCircuitJsonExportAdapter.#pcbPad(
|
|
119
|
+
pad,
|
|
120
|
+
index,
|
|
121
|
+
pcbComponentId
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Builds one PCB SMT pad entry.
|
|
128
|
+
* @param {object} pad Pad data.
|
|
129
|
+
* @param {number} index Pad index.
|
|
130
|
+
* @param {string} pcbComponentId PCB component id.
|
|
131
|
+
* @returns {object}
|
|
132
|
+
*/
|
|
133
|
+
static #pcbPad(pad, index, pcbComponentId) {
|
|
134
|
+
const padNumber = String(pad.number || index + 1)
|
|
135
|
+
const width = SelectedPartCircuitJsonExportAdapter.#number(pad.width, 1)
|
|
136
|
+
const height = SelectedPartCircuitJsonExportAdapter.#number(
|
|
137
|
+
pad.height,
|
|
138
|
+
1
|
|
139
|
+
)
|
|
140
|
+
const rotation = SelectedPartCircuitJsonExportAdapter.#number(
|
|
141
|
+
pad.ccw_rotation ?? pad.rotation,
|
|
142
|
+
0
|
|
143
|
+
)
|
|
144
|
+
const shape =
|
|
145
|
+
rotation !== 0
|
|
146
|
+
? 'rotated_rect'
|
|
147
|
+
: SelectedPartCircuitJsonExportAdapter.#padShape(
|
|
148
|
+
pad,
|
|
149
|
+
width,
|
|
150
|
+
height
|
|
151
|
+
)
|
|
152
|
+
const entry = {
|
|
153
|
+
type: 'pcb_smtpad',
|
|
154
|
+
shape,
|
|
155
|
+
pcb_smtpad_id:
|
|
156
|
+
pcbComponentId +
|
|
157
|
+
'_pad_' +
|
|
158
|
+
SelectedPartCircuitJsonExportAdapter.#safeIdentifier(padNumber),
|
|
159
|
+
pcb_component_id: pcbComponentId,
|
|
160
|
+
port_hints: [padNumber],
|
|
161
|
+
x: SelectedPartCircuitJsonExportAdapter.#number(pad.x, 0),
|
|
162
|
+
y: SelectedPartCircuitJsonExportAdapter.#number(pad.y, 0),
|
|
163
|
+
layer: SelectedPartCircuitJsonExportAdapter.#padLayer(pad)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (shape === 'circle') {
|
|
167
|
+
entry.radius = Math.max(width, height) / 2
|
|
168
|
+
} else {
|
|
169
|
+
entry.width = width
|
|
170
|
+
entry.height = height
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (shape === 'rotated_rect') entry.ccw_rotation = rotation
|
|
174
|
+
return entry
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the schematic component size.
|
|
179
|
+
* @param {{ symbol?: { pins?: object[] } }} selectedPart Selected part data.
|
|
180
|
+
* @returns {{ width: number, height: number }}
|
|
181
|
+
*/
|
|
182
|
+
static #schematicSize(selectedPart) {
|
|
183
|
+
const pinCount = SelectedPartCircuitJsonExportAdapter.#array(
|
|
184
|
+
selectedPart.symbol?.pins
|
|
185
|
+
).length
|
|
186
|
+
const edge = Math.max(2.54, Math.ceil(Math.sqrt(pinCount || 1)) * 2.54)
|
|
187
|
+
return { width: edge, height: edge }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolves footprint size from owned pads.
|
|
192
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
193
|
+
* @returns {{ width: number, height: number }}
|
|
194
|
+
*/
|
|
195
|
+
static #footprintSize(selectedPart) {
|
|
196
|
+
const bounds =
|
|
197
|
+
SelectedPartCircuitJsonExportAdapter.#footprintBounds(selectedPart)
|
|
198
|
+
return {
|
|
199
|
+
width: Math.max(bounds.maxX - bounds.minX, 1),
|
|
200
|
+
height: Math.max(bounds.maxY - bounds.minY, 1)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolves footprint center from owned pads.
|
|
206
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
207
|
+
* @returns {{ x: number, y: number }}
|
|
208
|
+
*/
|
|
209
|
+
static #footprintCenter(selectedPart) {
|
|
210
|
+
const bounds =
|
|
211
|
+
SelectedPartCircuitJsonExportAdapter.#footprintBounds(selectedPart)
|
|
212
|
+
return {
|
|
213
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
214
|
+
y: (bounds.minY + bounds.maxY) / 2
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolves footprint pad bounds.
|
|
220
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
221
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
222
|
+
*/
|
|
223
|
+
static #footprintBounds(selectedPart) {
|
|
224
|
+
const pads = SelectedPartCircuitJsonExportAdapter.#array(
|
|
225
|
+
selectedPart.footprint?.pads
|
|
226
|
+
)
|
|
227
|
+
if (!pads.length)
|
|
228
|
+
return { minX: -0.5, minY: -0.5, maxX: 0.5, maxY: 0.5 }
|
|
229
|
+
|
|
230
|
+
return pads.reduce(
|
|
231
|
+
(bounds, pad) => {
|
|
232
|
+
const x = SelectedPartCircuitJsonExportAdapter.#number(pad.x, 0)
|
|
233
|
+
const y = SelectedPartCircuitJsonExportAdapter.#number(pad.y, 0)
|
|
234
|
+
const halfWidth =
|
|
235
|
+
SelectedPartCircuitJsonExportAdapter.#number(pad.width, 1) /
|
|
236
|
+
2
|
|
237
|
+
const halfHeight =
|
|
238
|
+
SelectedPartCircuitJsonExportAdapter.#number(
|
|
239
|
+
pad.height,
|
|
240
|
+
1
|
|
241
|
+
) / 2
|
|
242
|
+
return {
|
|
243
|
+
minX: Math.min(bounds.minX, x - halfWidth),
|
|
244
|
+
minY: Math.min(bounds.minY, y - halfHeight),
|
|
245
|
+
maxX: Math.max(bounds.maxX, x + halfWidth),
|
|
246
|
+
maxY: Math.max(bounds.maxY, y + halfHeight)
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
minX: Infinity,
|
|
251
|
+
minY: Infinity,
|
|
252
|
+
maxX: -Infinity,
|
|
253
|
+
maxY: -Infinity
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolves an SMT pad shape.
|
|
260
|
+
* @param {object} pad Pad data.
|
|
261
|
+
* @param {number} width Pad width.
|
|
262
|
+
* @param {number} height Pad height.
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
static #padShape(pad, width, height) {
|
|
266
|
+
const rawShape = String(
|
|
267
|
+
pad.shape || pad.shapeTopName || pad.shapeName || ''
|
|
268
|
+
).toLowerCase()
|
|
269
|
+
if (
|
|
270
|
+
rawShape.includes('circle') ||
|
|
271
|
+
rawShape.includes('round') ||
|
|
272
|
+
rawShape.includes('oval')
|
|
273
|
+
) {
|
|
274
|
+
return width === height ? 'circle' : 'rect'
|
|
275
|
+
}
|
|
276
|
+
return 'rect'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Resolves an SMT pad layer.
|
|
281
|
+
* @param {object} pad Pad data.
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
static #padLayer(pad) {
|
|
285
|
+
const layer = String(pad.layer || pad.layerName || '').toLowerCase()
|
|
286
|
+
if (
|
|
287
|
+
layer.includes('bottom') ||
|
|
288
|
+
layer === 'bottom' ||
|
|
289
|
+
pad.layerId === 32
|
|
290
|
+
) {
|
|
291
|
+
return 'bottom'
|
|
292
|
+
}
|
|
293
|
+
return 'top'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Returns a number pin when a pin token is numeric.
|
|
298
|
+
* @param {unknown} value Candidate pin number.
|
|
299
|
+
* @returns {number | null}
|
|
300
|
+
*/
|
|
301
|
+
static #numericPinNumber(value) {
|
|
302
|
+
const text = String(value || '').trim()
|
|
303
|
+
const parsed = Number(text)
|
|
304
|
+
return text && Number.isFinite(parsed) ? parsed : null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Creates a safe CircuitJSON id token.
|
|
309
|
+
* @param {unknown} value Raw value.
|
|
310
|
+
* @returns {string}
|
|
311
|
+
*/
|
|
312
|
+
static #safeIdentifier(value) {
|
|
313
|
+
return String(value || 'selected_part').replace(/[^a-z0-9_]/giu, '_')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Normalizes a possible array.
|
|
318
|
+
* @param {unknown} value Candidate array.
|
|
319
|
+
* @returns {object[]}
|
|
320
|
+
*/
|
|
321
|
+
static #array(value) {
|
|
322
|
+
return Array.isArray(value) ? value : []
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Reads a finite number with fallback.
|
|
327
|
+
* @param {unknown} value Candidate number.
|
|
328
|
+
* @param {number} fallback Fallback number.
|
|
329
|
+
* @returns {number}
|
|
330
|
+
*/
|
|
331
|
+
static #number(value, fallback) {
|
|
332
|
+
const parsed = Number(value)
|
|
333
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const PSPICE_NUMBER_TOKEN = String.raw`([+-]?(?:(?:\d+(?:\.\d*)?)|(?:\.\d+))(?:[eE][+-]?\d+)?)`
|
|
2
|
+
const PSPICE_COMPARISON_OPERATOR = '(?:<=|>=|==|!=|(?<![!<>=])=(?!=)|<|>)'
|
|
3
|
+
const PSPICE_COMPARISON_OPERAND = String.raw`(?:V\s*\([^)]*\)|\{[^}\r\n]+\}|${PSPICE_NUMBER_TOKEN}(?:[a-zA-Z]+)?|[A-Za-z_][\w.$]*)`
|
|
4
|
+
const PSPICE_COMPARISON_EXPRESSION = String.raw`${PSPICE_COMPARISON_OPERAND}\s*${PSPICE_COMPARISON_OPERATOR}\s*${PSPICE_COMPARISON_OPERAND}`
|
|
5
|
+
const PSPICE_COMPARISON_BEFORE_CARET_PATTERN = new RegExp(
|
|
6
|
+
String.raw`${PSPICE_COMPARISON_EXPRESSION}\s*$`,
|
|
7
|
+
'i'
|
|
8
|
+
)
|
|
9
|
+
const PSPICE_COMPARISON_AFTER_CARET_PATTERN = new RegExp(
|
|
10
|
+
String.raw`^\s*\+?\s*${PSPICE_COMPARISON_EXPRESSION}`,
|
|
11
|
+
'i'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rewrites narrow, well-understood SPICE compatibility syntax.
|
|
16
|
+
*/
|
|
17
|
+
export class SpiceCompatibilityPreprocessor {
|
|
18
|
+
/**
|
|
19
|
+
* Returns a netlist with supported compatibility syntax rewritten.
|
|
20
|
+
* @param {string} spiceString Raw SPICE netlist text.
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
static rewrite(spiceString) {
|
|
24
|
+
return SpiceCompatibilityPreprocessor.#rewriteValueBooleanCarets(
|
|
25
|
+
SpiceCompatibilityPreprocessor.#rewriteResistorTemperaturePairs(
|
|
26
|
+
spiceString
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rewrites resistor TC pairs to separate TC1 and TC2 assignments.
|
|
33
|
+
* @param {string} spiceString Raw SPICE netlist text.
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
static #rewriteResistorTemperaturePairs(spiceString) {
|
|
37
|
+
return String(spiceString || '')
|
|
38
|
+
.split(/\r?\n/)
|
|
39
|
+
.map((line) => {
|
|
40
|
+
if (!/^\s*r/i.test(line)) return line
|
|
41
|
+
|
|
42
|
+
return line.replace(
|
|
43
|
+
new RegExp(
|
|
44
|
+
String.raw`\bTC\s*=\s*${PSPICE_NUMBER_TOKEN}\s*,\s*${PSPICE_NUMBER_TOKEN}\b`,
|
|
45
|
+
'gi'
|
|
46
|
+
),
|
|
47
|
+
'TC1=$1 TC2=$2'
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
.join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Rewrites boolean caret operators inside VALUE expression blocks.
|
|
55
|
+
* @param {string} spiceString Raw SPICE netlist text.
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
static #rewriteValueBooleanCarets(spiceString) {
|
|
59
|
+
let result = ''
|
|
60
|
+
let cursor = 0
|
|
61
|
+
const valueStartPattern = /\bVALUE\s*\{/gi
|
|
62
|
+
|
|
63
|
+
for (;;) {
|
|
64
|
+
valueStartPattern.lastIndex = cursor
|
|
65
|
+
const match = valueStartPattern.exec(spiceString)
|
|
66
|
+
if (!match) break
|
|
67
|
+
|
|
68
|
+
const blockStart = match.index
|
|
69
|
+
const firstBraceIndex = spiceString.indexOf('{', blockStart)
|
|
70
|
+
const blockEnd =
|
|
71
|
+
SpiceCompatibilityPreprocessor.#findBalancedBlockEnd(
|
|
72
|
+
spiceString,
|
|
73
|
+
firstBraceIndex
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (blockEnd === -1) break
|
|
77
|
+
|
|
78
|
+
result += spiceString.slice(cursor, blockStart)
|
|
79
|
+
const block = spiceString.slice(blockStart, blockEnd)
|
|
80
|
+
result += block.replace(/\s+\^\s+/g, (operator, offset, full) => {
|
|
81
|
+
if (
|
|
82
|
+
SpiceCompatibilityPreprocessor.#isValueBooleanCaret(
|
|
83
|
+
full,
|
|
84
|
+
offset,
|
|
85
|
+
operator.length
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
return operator.replace('^', '!=')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return operator
|
|
92
|
+
})
|
|
93
|
+
cursor = blockEnd
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result + spiceString.slice(cursor)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Finds the exclusive end offset of a balanced brace block.
|
|
101
|
+
* @param {string} text Source text.
|
|
102
|
+
* @param {number} firstBraceIndex Offset of the opening brace.
|
|
103
|
+
* @returns {number}
|
|
104
|
+
*/
|
|
105
|
+
static #findBalancedBlockEnd(text, firstBraceIndex) {
|
|
106
|
+
if (firstBraceIndex < 0) return -1
|
|
107
|
+
|
|
108
|
+
let depth = 0
|
|
109
|
+
for (let index = firstBraceIndex; index < text.length; index += 1) {
|
|
110
|
+
const character = text[index]
|
|
111
|
+
if (character === '{') {
|
|
112
|
+
depth += 1
|
|
113
|
+
} else if (character === '}') {
|
|
114
|
+
depth -= 1
|
|
115
|
+
if (depth === 0) return index + 1
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return -1
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true when a caret separates two comparison expressions.
|
|
124
|
+
* @param {string} block VALUE block text.
|
|
125
|
+
* @param {number} caretOffset Caret operator offset.
|
|
126
|
+
* @param {number} operatorLength Operator token length.
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
static #isValueBooleanCaret(block, caretOffset, operatorLength) {
|
|
130
|
+
return (
|
|
131
|
+
PSPICE_COMPARISON_BEFORE_CARET_PATTERN.test(
|
|
132
|
+
block.slice(0, caretOffset)
|
|
133
|
+
) &&
|
|
134
|
+
PSPICE_COMPARISON_AFTER_CARET_PATTERN.test(
|
|
135
|
+
block.slice(caretOffset + operatorLength)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const NUMBER_SUFFIX_MULTIPLIERS = {
|
|
2
|
+
t: 1e12,
|
|
3
|
+
g: 1e9,
|
|
4
|
+
meg: 1e6,
|
|
5
|
+
k: 1e3,
|
|
6
|
+
m: 1e-3,
|
|
7
|
+
ms: 1e-3,
|
|
8
|
+
u: 1e-6,
|
|
9
|
+
us: 1e-6,
|
|
10
|
+
n: 1e-9,
|
|
11
|
+
ns: 1e-9,
|
|
12
|
+
p: 1e-12,
|
|
13
|
+
ps: 1e-12,
|
|
14
|
+
f: 1e-15,
|
|
15
|
+
fs: 1e-15,
|
|
16
|
+
s: 1
|
|
17
|
+
}
|
|
18
|
+
const VOLTAGE_PROBE_COMMENT_PATTERN =
|
|
19
|
+
/^\s*\*\s*(?:ecadforge_voltage_probe|circuitjson_voltage_probe|simulation_voltage_probe)\s+(.+)\s*$/
|
|
20
|
+
const CURRENT_PROBE_COMMENT_PATTERN =
|
|
21
|
+
/^\s*\*\s*(?:ecadforge_current_probe|circuitjson_current_probe|simulation_current_probe)\s+(.+)\s*$/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses SPICE directives and metadata comments used by simulation helpers.
|
|
25
|
+
*/
|
|
26
|
+
export class SpiceDirectiveParser {
|
|
27
|
+
/**
|
|
28
|
+
* Parses transient directive timing parameters.
|
|
29
|
+
* @param {string} spiceString SPICE netlist text.
|
|
30
|
+
* @returns {{ tstep?: number, tstop?: number, tstart?: number, tmax?: number, uic?: boolean } | null}
|
|
31
|
+
*/
|
|
32
|
+
static parseTransient(spiceString) {
|
|
33
|
+
for (const rawLine of String(spiceString || '').split(/\r?\n/)) {
|
|
34
|
+
const line = rawLine.trim()
|
|
35
|
+
if (!line || line.startsWith('*')) continue
|
|
36
|
+
if (!line.toLowerCase().startsWith('.tran')) continue
|
|
37
|
+
|
|
38
|
+
const [withoutComment = ''] = line.split(';')
|
|
39
|
+
const tokens = withoutComment.split(/\s+/).filter(Boolean)
|
|
40
|
+
const values = []
|
|
41
|
+
let uic = false
|
|
42
|
+
|
|
43
|
+
for (const token of tokens.slice(1)) {
|
|
44
|
+
if (token.toLowerCase() === 'uic') {
|
|
45
|
+
uic = true
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const value = SpiceDirectiveParser.parseNumber(token)
|
|
50
|
+
if (value !== undefined) values.push(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...(values[0] !== undefined ? { tstep: values[0] } : {}),
|
|
55
|
+
...(values[1] !== undefined ? { tstop: values[1] } : {}),
|
|
56
|
+
...(values[2] !== undefined ? { tstart: values[2] } : {}),
|
|
57
|
+
...(values[3] !== undefined ? { tmax: values[3] } : {}),
|
|
58
|
+
...(uic ? { uic: true } : {})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parses requested transient plot tokens from the first PRINT directive.
|
|
67
|
+
* @param {string} spiceString SPICE netlist text.
|
|
68
|
+
* @returns {Map<string, string> | null}
|
|
69
|
+
*/
|
|
70
|
+
static parseRequestedPlots(spiceString) {
|
|
71
|
+
const match = String(spiceString || '').match(/\.print\s+tran\s+(.*)/i)
|
|
72
|
+
if (!match?.[1]) return null
|
|
73
|
+
|
|
74
|
+
const tokens = match[1].match(/[VI]\s*\([^)]+\)/gi)
|
|
75
|
+
if (!tokens) return null
|
|
76
|
+
|
|
77
|
+
const plots = new Map()
|
|
78
|
+
for (const token of tokens) {
|
|
79
|
+
const normalizedToken = SpiceDirectiveParser.normalizeVector(token)
|
|
80
|
+
if (!plots.has(normalizedToken)) {
|
|
81
|
+
plots.set(normalizedToken, token)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return plots
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extracts voltage probe metadata comments from a netlist.
|
|
90
|
+
* @param {string} spiceString SPICE netlist text.
|
|
91
|
+
* @returns {Map<string, object>}
|
|
92
|
+
*/
|
|
93
|
+
static extractVoltageProbeMetadata(spiceString) {
|
|
94
|
+
return SpiceDirectiveParser.#extractProbeMetadata(
|
|
95
|
+
spiceString,
|
|
96
|
+
VOLTAGE_PROBE_COMMENT_PATTERN,
|
|
97
|
+
(parsed) => {
|
|
98
|
+
if (
|
|
99
|
+
typeof parsed.simulation_voltage_probe_id !== 'string' ||
|
|
100
|
+
typeof parsed.spice_vector !== 'string' ||
|
|
101
|
+
typeof parsed.source_node_name !== 'string'
|
|
102
|
+
) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
simulation_voltage_probe_id:
|
|
108
|
+
parsed.simulation_voltage_probe_id,
|
|
109
|
+
name:
|
|
110
|
+
typeof parsed.name === 'string'
|
|
111
|
+
? parsed.name
|
|
112
|
+
: undefined,
|
|
113
|
+
spice_vector: parsed.spice_vector,
|
|
114
|
+
source_node_name: parsed.source_node_name,
|
|
115
|
+
reference_node_name:
|
|
116
|
+
typeof parsed.reference_node_name === 'string'
|
|
117
|
+
? parsed.reference_node_name
|
|
118
|
+
: undefined
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extracts current probe metadata comments from a netlist.
|
|
126
|
+
* @param {string} spiceString SPICE netlist text.
|
|
127
|
+
* @returns {Map<string, object>}
|
|
128
|
+
*/
|
|
129
|
+
static extractCurrentProbeMetadata(spiceString) {
|
|
130
|
+
return SpiceDirectiveParser.#extractProbeMetadata(
|
|
131
|
+
spiceString,
|
|
132
|
+
CURRENT_PROBE_COMMENT_PATTERN,
|
|
133
|
+
(parsed) => {
|
|
134
|
+
if (
|
|
135
|
+
typeof parsed.simulation_current_probe_id !== 'string' ||
|
|
136
|
+
typeof parsed.spice_vector !== 'string'
|
|
137
|
+
) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
simulation_current_probe_id:
|
|
143
|
+
parsed.simulation_current_probe_id,
|
|
144
|
+
name:
|
|
145
|
+
typeof parsed.name === 'string'
|
|
146
|
+
? parsed.name
|
|
147
|
+
: undefined,
|
|
148
|
+
spice_vector: parsed.spice_vector,
|
|
149
|
+
source_component_id:
|
|
150
|
+
typeof parsed.source_component_id === 'string'
|
|
151
|
+
? parsed.source_component_id
|
|
152
|
+
: undefined,
|
|
153
|
+
source_trace_id:
|
|
154
|
+
typeof parsed.source_trace_id === 'string'
|
|
155
|
+
? parsed.source_trace_id
|
|
156
|
+
: undefined
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Normalizes a simulator vector token for map lookups.
|
|
164
|
+
* @param {string} value Raw vector token.
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
static normalizeVector(value) {
|
|
168
|
+
return String(value || '')
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/\s/g, '')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parses a SPICE numeric token with common suffix multipliers.
|
|
175
|
+
* @param {string} token Numeric token.
|
|
176
|
+
* @returns {number | undefined}
|
|
177
|
+
*/
|
|
178
|
+
static parseNumber(token) {
|
|
179
|
+
const normalized = String(token || '')
|
|
180
|
+
.replace(/[,]/g, '')
|
|
181
|
+
.toLowerCase()
|
|
182
|
+
const match = normalized.match(
|
|
183
|
+
/^([+-]?\d*\.?\d+(?:e[+-]?\d+)?)([a-z]+)?$/i
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if (!match) return undefined
|
|
187
|
+
|
|
188
|
+
const base = Number.parseFloat(match[1] || '')
|
|
189
|
+
if (!Number.isFinite(base)) return undefined
|
|
190
|
+
|
|
191
|
+
const suffix = match[2] || ''
|
|
192
|
+
if (!suffix) return base
|
|
193
|
+
|
|
194
|
+
const multiplier =
|
|
195
|
+
NUMBER_SUFFIX_MULTIPLIERS[suffix] ??
|
|
196
|
+
NUMBER_SUFFIX_MULTIPLIERS[suffix.replace(/s$/, '')] ??
|
|
197
|
+
1
|
|
198
|
+
|
|
199
|
+
return base * multiplier
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extracts JSON probe metadata comments with a validator callback.
|
|
204
|
+
* @param {string} spiceString SPICE netlist text.
|
|
205
|
+
* @param {RegExp} pattern Comment matcher.
|
|
206
|
+
* @param {(parsed: object) => object | null} shapeMetadata Metadata shaper.
|
|
207
|
+
* @returns {Map<string, object>}
|
|
208
|
+
*/
|
|
209
|
+
static #extractProbeMetadata(spiceString, pattern, shapeMetadata) {
|
|
210
|
+
const metadata = new Map()
|
|
211
|
+
|
|
212
|
+
for (const line of String(spiceString || '').split(/\r?\n/)) {
|
|
213
|
+
const match = line.match(pattern)
|
|
214
|
+
if (!match?.[1]) continue
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const shapedMetadata = shapeMetadata(JSON.parse(match[1]))
|
|
218
|
+
if (!shapedMetadata) continue
|
|
219
|
+
|
|
220
|
+
metadata.set(
|
|
221
|
+
SpiceDirectiveParser.normalizeVector(
|
|
222
|
+
shapedMetadata.spice_vector
|
|
223
|
+
),
|
|
224
|
+
shapedMetadata
|
|
225
|
+
)
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return metadata
|
|
230
|
+
}
|
|
231
|
+
}
|