@tscircuit/schematic-viewer 2.0.30 → 2.0.31
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/bun.lockb +0 -0
- package/dist/index.js +226 -38
- package/dist/index.js.map +1 -1
- package/examples/example12-spice-boost-converter.fixture.tsx +78 -0
- package/lib/components/SchematicViewer.tsx +15 -2
- package/lib/components/SpicePlot.tsx +11 -1
- package/lib/components/SpiceSimulationOverlay.tsx +127 -1
- package/lib/hooks/useSpiceSimulation.ts +3 -5
- package/lib/utils/spice-utils.ts +53 -6
- package/package.json +6 -5
- /package/examples/{example9-spice-simulation.fixture.tsx → example9-spice-rc-charging-voltage-divider.fixture.tsx} +0 -0
|
@@ -58,6 +58,12 @@ export const SchematicViewer = ({
|
|
|
58
58
|
enableDebug()
|
|
59
59
|
}
|
|
60
60
|
const [showSpiceOverlay, setShowSpiceOverlay] = useState(false)
|
|
61
|
+
const [spiceSimOptions, setSpiceSimOptions] = useState({
|
|
62
|
+
showVoltage: true,
|
|
63
|
+
showCurrent: false,
|
|
64
|
+
startTime: 0, // in ms
|
|
65
|
+
duration: 20, // in ms
|
|
66
|
+
})
|
|
61
67
|
|
|
62
68
|
const getCircuitHash = (circuitJson: CircuitJson) => {
|
|
63
69
|
return `${circuitJson?.length || 0}_${(circuitJson as any)?.editCount || 0}`
|
|
@@ -71,12 +77,17 @@ export const SchematicViewer = ({
|
|
|
71
77
|
const spiceString = useMemo(() => {
|
|
72
78
|
if (!spiceSimulationEnabled) return null
|
|
73
79
|
try {
|
|
74
|
-
return getSpiceFromCircuitJson(circuitJson)
|
|
80
|
+
return getSpiceFromCircuitJson(circuitJson, spiceSimOptions)
|
|
75
81
|
} catch (e) {
|
|
76
82
|
console.error("Failed to generate SPICE string", e)
|
|
77
83
|
return null
|
|
78
84
|
}
|
|
79
|
-
}, [
|
|
85
|
+
}, [
|
|
86
|
+
circuitJsonKey,
|
|
87
|
+
spiceSimulationEnabled,
|
|
88
|
+
spiceSimOptions.startTime,
|
|
89
|
+
spiceSimOptions.duration,
|
|
90
|
+
])
|
|
80
91
|
|
|
81
92
|
const {
|
|
82
93
|
plotData,
|
|
@@ -369,6 +380,8 @@ export const SchematicViewer = ({
|
|
|
369
380
|
nodes={nodes}
|
|
370
381
|
isLoading={isSpiceSimLoading}
|
|
371
382
|
error={spiceSimError}
|
|
383
|
+
simOptions={spiceSimOptions}
|
|
384
|
+
onSimOptionsChange={setSpiceSimOptions}
|
|
372
385
|
/>
|
|
373
386
|
)}
|
|
374
387
|
{svgDiv}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
1
2
|
import {
|
|
2
3
|
Chart as ChartJS,
|
|
3
4
|
type ChartOptions,
|
|
@@ -61,6 +62,15 @@ export const SpicePlot = ({
|
|
|
61
62
|
isLoading: boolean
|
|
62
63
|
error: string | null
|
|
63
64
|
}) => {
|
|
65
|
+
const yAxisLabel = useMemo(() => {
|
|
66
|
+
const hasVoltage = nodes.some((n) => n.toLowerCase().startsWith("v("))
|
|
67
|
+
const hasCurrent = nodes.some((n) => n.toLowerCase().startsWith("i("))
|
|
68
|
+
if (hasVoltage && hasCurrent) return "Value"
|
|
69
|
+
if (hasVoltage) return "Voltage (V)"
|
|
70
|
+
if (hasCurrent) return "Current (A)"
|
|
71
|
+
return "Value"
|
|
72
|
+
}, [nodes])
|
|
73
|
+
|
|
64
74
|
if (isLoading) {
|
|
65
75
|
return (
|
|
66
76
|
<div
|
|
@@ -171,7 +181,7 @@ export const SpicePlot = ({
|
|
|
171
181
|
y: {
|
|
172
182
|
title: {
|
|
173
183
|
display: true,
|
|
174
|
-
text:
|
|
184
|
+
text: yAxisLabel,
|
|
175
185
|
font: {
|
|
176
186
|
family: "sans-serif",
|
|
177
187
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SpicePlot } from "./SpicePlot"
|
|
2
2
|
import type { PlotPoint } from "../hooks/useSpiceSimulation"
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
3
4
|
|
|
4
5
|
interface SpiceSimulationOverlayProps {
|
|
5
6
|
spiceString: string | null
|
|
@@ -8,6 +9,15 @@ interface SpiceSimulationOverlayProps {
|
|
|
8
9
|
nodes: string[]
|
|
9
10
|
isLoading: boolean
|
|
10
11
|
error: string | null
|
|
12
|
+
simOptions: {
|
|
13
|
+
showVoltage: boolean
|
|
14
|
+
showCurrent: boolean
|
|
15
|
+
startTime: number
|
|
16
|
+
duration: number
|
|
17
|
+
}
|
|
18
|
+
onSimOptionsChange: (
|
|
19
|
+
options: SpiceSimulationOverlayProps["simOptions"],
|
|
20
|
+
) => void
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
export const SpiceSimulationOverlay = ({
|
|
@@ -17,7 +27,37 @@ export const SpiceSimulationOverlay = ({
|
|
|
17
27
|
nodes,
|
|
18
28
|
isLoading,
|
|
19
29
|
error,
|
|
30
|
+
simOptions,
|
|
31
|
+
onSimOptionsChange,
|
|
20
32
|
}: SpiceSimulationOverlayProps) => {
|
|
33
|
+
const [startTimeDraft, setStartTimeDraft] = useState(
|
|
34
|
+
String(simOptions.startTime),
|
|
35
|
+
)
|
|
36
|
+
const [durationDraft, setDurationDraft] = useState(
|
|
37
|
+
String(simOptions.duration),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
setStartTimeDraft(String(simOptions.startTime))
|
|
42
|
+
setDurationDraft(String(simOptions.duration))
|
|
43
|
+
}, [simOptions.startTime, simOptions.duration])
|
|
44
|
+
|
|
45
|
+
const handleRerun = () => {
|
|
46
|
+
onSimOptionsChange({
|
|
47
|
+
...simOptions,
|
|
48
|
+
startTime: Number(startTimeDraft),
|
|
49
|
+
duration: Number(durationDraft),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const filteredNodes = nodes.filter((node) => {
|
|
54
|
+
const isVoltage = node.toLowerCase().startsWith("v(")
|
|
55
|
+
const isCurrent = node.toLowerCase().startsWith("i(")
|
|
56
|
+
if (simOptions.showVoltage && isVoltage) return true
|
|
57
|
+
if (simOptions.showCurrent && isCurrent) return true
|
|
58
|
+
return false
|
|
59
|
+
})
|
|
60
|
+
|
|
21
61
|
return (
|
|
22
62
|
<div
|
|
23
63
|
style={{
|
|
@@ -82,11 +122,97 @@ export const SpiceSimulationOverlay = ({
|
|
|
82
122
|
<div>
|
|
83
123
|
<SpicePlot
|
|
84
124
|
plotData={plotData}
|
|
85
|
-
nodes={
|
|
125
|
+
nodes={filteredNodes}
|
|
86
126
|
isLoading={isLoading}
|
|
87
127
|
error={error}
|
|
88
128
|
/>
|
|
89
129
|
</div>
|
|
130
|
+
<div
|
|
131
|
+
style={{
|
|
132
|
+
marginTop: "16px",
|
|
133
|
+
padding: "12px",
|
|
134
|
+
backgroundColor: "#f7f7f7",
|
|
135
|
+
borderRadius: "6px",
|
|
136
|
+
display: "flex",
|
|
137
|
+
flexWrap: "wrap",
|
|
138
|
+
gap: "24px",
|
|
139
|
+
alignItems: "center",
|
|
140
|
+
fontSize: "14px",
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<div style={{ display: "flex", gap: "16px" }}>
|
|
144
|
+
<label
|
|
145
|
+
style={{ display: "flex", alignItems: "center", gap: "6px" }}
|
|
146
|
+
>
|
|
147
|
+
<input
|
|
148
|
+
type="checkbox"
|
|
149
|
+
checked={simOptions.showVoltage}
|
|
150
|
+
onChange={(e) =>
|
|
151
|
+
onSimOptionsChange({
|
|
152
|
+
...simOptions,
|
|
153
|
+
showVoltage: e.target.checked,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
/>
|
|
157
|
+
Voltage
|
|
158
|
+
</label>
|
|
159
|
+
<label
|
|
160
|
+
style={{ display: "flex", alignItems: "center", gap: "6px" }}
|
|
161
|
+
>
|
|
162
|
+
<input
|
|
163
|
+
type="checkbox"
|
|
164
|
+
checked={simOptions.showCurrent}
|
|
165
|
+
onChange={(e) =>
|
|
166
|
+
onSimOptionsChange({
|
|
167
|
+
...simOptions,
|
|
168
|
+
showCurrent: e.target.checked,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
/>
|
|
172
|
+
Current
|
|
173
|
+
</label>
|
|
174
|
+
</div>
|
|
175
|
+
<div style={{ display: "flex", gap: "16px", alignItems: "center" }}>
|
|
176
|
+
<label htmlFor="startTime">Start Time (ms):</label>
|
|
177
|
+
<input
|
|
178
|
+
id="startTime"
|
|
179
|
+
type="number"
|
|
180
|
+
value={startTimeDraft}
|
|
181
|
+
onChange={(e) => setStartTimeDraft(e.target.value)}
|
|
182
|
+
style={{
|
|
183
|
+
width: "80px",
|
|
184
|
+
padding: "4px 8px",
|
|
185
|
+
borderRadius: "4px",
|
|
186
|
+
border: "1px solid #ccc",
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
<label htmlFor="duration">Duration (ms):</label>
|
|
190
|
+
<input
|
|
191
|
+
id="duration"
|
|
192
|
+
type="number"
|
|
193
|
+
value={durationDraft}
|
|
194
|
+
onChange={(e) => setDurationDraft(e.target.value)}
|
|
195
|
+
style={{
|
|
196
|
+
width: "80px",
|
|
197
|
+
padding: "4px 8px",
|
|
198
|
+
borderRadius: "4px",
|
|
199
|
+
border: "1px solid #ccc",
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
<button
|
|
203
|
+
onClick={handleRerun}
|
|
204
|
+
style={{
|
|
205
|
+
padding: "4px 12px",
|
|
206
|
+
borderRadius: "4px",
|
|
207
|
+
border: "1px solid #ccc",
|
|
208
|
+
backgroundColor: "#f0f0f0",
|
|
209
|
+
cursor: "pointer",
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
Rerun
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
90
216
|
<div style={{ marginTop: "24px" }}>
|
|
91
217
|
<h3
|
|
92
218
|
style={{
|
|
@@ -67,14 +67,12 @@ const parseEecEngineOutput = (
|
|
|
67
67
|
}
|
|
68
68
|
const timeValues = columnData[timeKey]
|
|
69
69
|
const probedVariables = Object.keys(columnData).filter((k) => k !== timeKey)
|
|
70
|
-
const plotableNodes = probedVariables
|
|
71
|
-
n.replace(/v\(([^)]+)\)/i, "$1"),
|
|
72
|
-
)
|
|
70
|
+
const plotableNodes = probedVariables
|
|
73
71
|
|
|
74
72
|
const plotData: PlotPoint[] = timeValues.map((t: number, i: number) => {
|
|
75
73
|
const point: PlotPoint = { name: t.toExponential(2) }
|
|
76
|
-
probedVariables.forEach((variable
|
|
77
|
-
point[
|
|
74
|
+
probedVariables.forEach((variable) => {
|
|
75
|
+
point[variable] = columnData[variable][i]
|
|
78
76
|
})
|
|
79
77
|
return point
|
|
80
78
|
})
|
package/lib/utils/spice-utils.ts
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import { circuitJsonToSpice } from "circuit-json-to-spice"
|
|
2
2
|
import type { CircuitJson } from "circuit-json"
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export interface SpiceSimOptions {
|
|
5
|
+
showVoltage: boolean
|
|
6
|
+
showCurrent: boolean
|
|
7
|
+
startTime: number // in ms
|
|
8
|
+
duration: number // in ms
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const formatSimTime = (seconds: number): string => {
|
|
12
|
+
if (seconds === 0) return "0"
|
|
13
|
+
const absSeconds = Math.abs(seconds)
|
|
14
|
+
|
|
15
|
+
const precision = (v: number) => v.toPrecision(4)
|
|
16
|
+
|
|
17
|
+
if (absSeconds >= 1) return precision(seconds)
|
|
18
|
+
if (absSeconds >= 1e-3) return `${precision(seconds * 1e3)}m`
|
|
19
|
+
if (absSeconds >= 1e-6) return `${precision(seconds * 1e6)}u`
|
|
20
|
+
if (absSeconds >= 1e-9) return `${precision(seconds * 1e9)}n`
|
|
21
|
+
if (absSeconds >= 1e-12) return `${precision(seconds * 1e12)}p`
|
|
22
|
+
if (absSeconds >= 1e-15) return `${precision(seconds * 1e15)}f`
|
|
23
|
+
|
|
24
|
+
return seconds.toExponential(3)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getSpiceFromCircuitJson = (
|
|
28
|
+
circuitJson: CircuitJson,
|
|
29
|
+
options?: Partial<SpiceSimOptions>,
|
|
30
|
+
): string => {
|
|
5
31
|
const spiceNetlist = circuitJsonToSpice(circuitJson as any)
|
|
6
32
|
const baseSpiceString = spiceNetlist.toSpiceString()
|
|
7
33
|
|
|
@@ -12,16 +38,22 @@ export const getSpiceFromCircuitJson = (circuitJson: CircuitJson): string => {
|
|
|
12
38
|
|
|
13
39
|
const allNodes = new Set<string>()
|
|
14
40
|
const capacitorNodes = new Set<string>()
|
|
41
|
+
const componentNamesToProbeCurrent = new Set<string>()
|
|
15
42
|
|
|
16
43
|
for (const line of componentLines) {
|
|
17
44
|
const parts = line.trim().split(/\s+/)
|
|
18
45
|
if (parts.length < 3) continue
|
|
19
46
|
|
|
20
|
-
const
|
|
47
|
+
const componentName = parts[0]
|
|
48
|
+
const componentType = componentName[0].toUpperCase()
|
|
21
49
|
let nodesOnLine: string[] = []
|
|
22
50
|
|
|
23
51
|
if (["R", "C", "L", "V", "I", "D"].includes(componentType)) {
|
|
24
52
|
nodesOnLine = parts.slice(1, 3)
|
|
53
|
+
// Only probe current on voltage sources
|
|
54
|
+
if (componentType === "V") {
|
|
55
|
+
componentNamesToProbeCurrent.add(componentName)
|
|
56
|
+
}
|
|
25
57
|
} else if (componentType === "Q" && parts.length >= 4) {
|
|
26
58
|
// BJT
|
|
27
59
|
nodesOnLine = parts.slice(1, 4)
|
|
@@ -49,11 +81,26 @@ export const getSpiceFromCircuitJson = (circuitJson: CircuitJson): string => {
|
|
|
49
81
|
|
|
50
82
|
const icLines = Array.from(capacitorNodes).map((node) => `.ic V(${node})=0`)
|
|
51
83
|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
84
|
+
const probes: string[] = []
|
|
85
|
+
const probeVoltages = Array.from(allNodes).map((node) => `V(${node})`)
|
|
86
|
+
probes.push(...probeVoltages)
|
|
87
|
+
const probeCurrents = Array.from(componentNamesToProbeCurrent).map(
|
|
88
|
+
(name) => `I(${name})`,
|
|
89
|
+
)
|
|
90
|
+
probes.push(...probeCurrents)
|
|
91
|
+
|
|
92
|
+
const probeLine = probes.length > 0 ? `.probe ${probes.join(" ")}` : ""
|
|
93
|
+
|
|
94
|
+
const tstart_ms = options?.startTime ?? 0
|
|
95
|
+
const duration_ms = options?.duration ?? 20
|
|
96
|
+
const tstart = tstart_ms * 1e-3 // s
|
|
97
|
+
const duration = duration_ms * 1e-3 // s
|
|
55
98
|
|
|
56
|
-
const
|
|
99
|
+
const tstop = tstart + duration
|
|
100
|
+
const tstep = duration / 50
|
|
101
|
+
const tranLine = `.tran ${formatSimTime(tstep)} ${formatSimTime(
|
|
102
|
+
tstop,
|
|
103
|
+
)} ${formatSimTime(tstart)} UIC`
|
|
57
104
|
|
|
58
105
|
const endStatement = ".end"
|
|
59
106
|
const originalLines = baseSpiceString.split("\n")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/schematic-viewer",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.31",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -15,18 +15,19 @@
|
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@biomejs/biome": "^1.9.4",
|
|
18
|
-
"tscircuit": "^0.0.580",
|
|
19
18
|
"@types/bun": "latest",
|
|
20
19
|
"@types/debug": "^4.1.12",
|
|
21
20
|
"@types/react": "^19.0.1",
|
|
22
21
|
"@types/react-dom": "^19.0.2",
|
|
23
22
|
"@types/recharts": "^2.0.1",
|
|
24
23
|
"@vitejs/plugin-react": "^4.3.4",
|
|
25
|
-
"react": "
|
|
24
|
+
"react": "^19.1.0",
|
|
26
25
|
"react-cosmos": "^6.2.1",
|
|
27
26
|
"react-cosmos-plugin-vite": "^6.2.0",
|
|
28
|
-
"react-dom": "
|
|
27
|
+
"react-dom": "^19.1.0",
|
|
29
28
|
"react-reconciler": "^0.31.0",
|
|
29
|
+
"semver": "^7.7.2",
|
|
30
|
+
"tscircuit": "^0.0.611",
|
|
30
31
|
"tsup": "^8.3.5",
|
|
31
32
|
"vite": "^6.0.3"
|
|
32
33
|
},
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"chart.js": "^4.5.0",
|
|
39
|
-
"circuit-json-to-spice": "^0.0.
|
|
40
|
+
"circuit-json-to-spice": "^0.0.10",
|
|
40
41
|
"debug": "^4.4.0",
|
|
41
42
|
"performance-now": "^2.1.0",
|
|
42
43
|
"react-chartjs-2": "^5.3.0",
|
|
File without changes
|