@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.
@@ -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
- }, [circuitJsonKey, spiceSimulationEnabled])
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: "Voltage",
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={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.map((n) =>
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, j) => {
77
- point[plotableNodes[j]] = columnData[variable][i]
74
+ probedVariables.forEach((variable) => {
75
+ point[variable] = columnData[variable][i]
78
76
  })
79
77
  return point
80
78
  })
@@ -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 const getSpiceFromCircuitJson = (circuitJson: CircuitJson): string => {
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 componentType = parts[0][0].toUpperCase()
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 probeNodes = Array.from(allNodes).map((node) => `V(${node})`)
53
- const probeLine =
54
- probeNodes.length > 0 ? `.probe ${probeNodes.join(" ")}` : ""
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 tranLine = ".tran 0.1ms 50ms UIC"
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.30",
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": "18",
24
+ "react": "^19.1.0",
26
25
  "react-cosmos": "^6.2.1",
27
26
  "react-cosmos-plugin-vite": "^6.2.0",
28
- "react-dom": "18",
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.9",
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",