@tscircuit/schematic-viewer 2.0.23 → 2.0.25

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.
@@ -0,0 +1,193 @@
1
+ import {
2
+ Chart as ChartJS,
3
+ type ChartOptions,
4
+ CategoryScale,
5
+ LinearScale,
6
+ PointElement,
7
+ LineElement,
8
+ Title,
9
+ Tooltip,
10
+ Legend,
11
+ } from "chart.js"
12
+ import { Line } from "react-chartjs-2"
13
+ import type { PlotPoint } from "../hooks/useSpiceSimulation"
14
+
15
+ ChartJS.register(
16
+ CategoryScale,
17
+ LinearScale,
18
+ PointElement,
19
+ LineElement,
20
+ Title,
21
+ Tooltip,
22
+ Legend,
23
+ )
24
+
25
+ const colors = ["#8884d8", "#82ca9d", "#ffc658", "#ff7300", "#387908"]
26
+
27
+ const formatTimeWithUnits = (seconds: number) => {
28
+ if (seconds === 0) return "0s"
29
+ const absSeconds = Math.abs(seconds)
30
+
31
+ let unit = "s"
32
+ let scale = 1
33
+ if (absSeconds < 1e-12) {
34
+ unit = "fs"
35
+ scale = 1e15
36
+ } else if (absSeconds < 1e-9) {
37
+ unit = "ps"
38
+ scale = 1e12
39
+ } else if (absSeconds < 1e-6) {
40
+ unit = "ns"
41
+ scale = 1e9
42
+ } else if (absSeconds < 1e-3) {
43
+ unit = "us"
44
+ scale = 1e6
45
+ } else if (absSeconds < 1) {
46
+ unit = "ms"
47
+ scale = 1e3
48
+ }
49
+
50
+ return `${parseFloat((seconds * scale).toPrecision(3))}${unit}`
51
+ }
52
+
53
+ export const SpicePlot = ({
54
+ plotData,
55
+ nodes,
56
+ isLoading,
57
+ error,
58
+ }: {
59
+ plotData: PlotPoint[]
60
+ nodes: string[]
61
+ isLoading: boolean
62
+ error: string | null
63
+ }) => {
64
+ if (isLoading) {
65
+ return (
66
+ <div
67
+ style={{
68
+ height: "300px",
69
+ width: "100%",
70
+ display: "flex",
71
+ alignItems: "center",
72
+ justifyContent: "center",
73
+ }}
74
+ >
75
+ Running simulation...
76
+ </div>
77
+ )
78
+ }
79
+
80
+ if (error) {
81
+ return (
82
+ <div
83
+ style={{
84
+ height: "300px",
85
+ width: "100%",
86
+ display: "flex",
87
+ alignItems: "center",
88
+ justifyContent: "center",
89
+ color: "red",
90
+ }}
91
+ >
92
+ Error: {error}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ if (plotData.length === 0) {
98
+ return (
99
+ <div
100
+ style={{
101
+ height: "300px",
102
+ width: "100%",
103
+ display: "flex",
104
+ alignItems: "center",
105
+ justifyContent: "center",
106
+ }}
107
+ >
108
+ No data to plot. Check simulation output or SPICE netlist.
109
+ </div>
110
+ )
111
+ }
112
+
113
+ const chartData = {
114
+ datasets: nodes.map((node, i) => ({
115
+ label: node,
116
+ data: plotData.map((p) => ({
117
+ x: Number(p.name),
118
+ y: p[node] as number,
119
+ })),
120
+ borderColor: colors[i % colors.length],
121
+ backgroundColor: colors[i % colors.length],
122
+ fill: false,
123
+ tension: 0.1,
124
+ })),
125
+ }
126
+
127
+ const options: ChartOptions<"line"> = {
128
+ responsive: true,
129
+ maintainAspectRatio: false,
130
+ plugins: {
131
+ legend: {
132
+ position: "top" as const,
133
+ labels: {
134
+ font: {
135
+ family: "sans-serif",
136
+ },
137
+ },
138
+ },
139
+ title: {
140
+ display: false,
141
+ },
142
+ tooltip: {
143
+ callbacks: {
144
+ title: (tooltipItems) => {
145
+ if (tooltipItems.length > 0) {
146
+ const item = tooltipItems[0]
147
+ return formatTimeWithUnits(item.parsed.x)
148
+ }
149
+ return ""
150
+ },
151
+ },
152
+ },
153
+ },
154
+ scales: {
155
+ x: {
156
+ type: "linear",
157
+ title: {
158
+ display: true,
159
+ text: "Time",
160
+ font: {
161
+ family: "sans-serif",
162
+ },
163
+ },
164
+ ticks: {
165
+ callback: (value) => formatTimeWithUnits(value as number),
166
+ font: {
167
+ family: "sans-serif",
168
+ },
169
+ },
170
+ },
171
+ y: {
172
+ title: {
173
+ display: true,
174
+ text: "Voltage",
175
+ font: {
176
+ family: "sans-serif",
177
+ },
178
+ },
179
+ ticks: {
180
+ font: {
181
+ family: "sans-serif",
182
+ },
183
+ },
184
+ },
185
+ },
186
+ }
187
+
188
+ return (
189
+ <div style={{ position: "relative", height: "300px", width: "100%" }}>
190
+ <Line options={options} data={chartData} />
191
+ </div>
192
+ )
193
+ }
@@ -0,0 +1,31 @@
1
+ import { SpiceIcon } from "./SpiceIcon"
2
+ import { zIndexMap } from "../utils/z-index-map"
3
+
4
+ export const SpiceSimulationIcon = ({
5
+ onClick,
6
+ }: {
7
+ onClick: () => void
8
+ }) => {
9
+ return (
10
+ <div
11
+ onClick={onClick}
12
+ style={{
13
+ position: "absolute",
14
+ top: "16px",
15
+ right: "56px",
16
+ backgroundColor: "#fff",
17
+ color: "#000",
18
+ padding: "8px",
19
+ borderRadius: "4px",
20
+ cursor: "pointer",
21
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
22
+ display: "flex",
23
+ alignItems: "center",
24
+ gap: "4px",
25
+ zIndex: zIndexMap.spiceSimulationIcon,
26
+ }}
27
+ >
28
+ <SpiceIcon />
29
+ </div>
30
+ )
31
+ }
@@ -0,0 +1,121 @@
1
+ import { SpicePlot } from "./SpicePlot"
2
+ import type { PlotPoint } from "../hooks/useSpiceSimulation"
3
+
4
+ interface SpiceSimulationOverlayProps {
5
+ spiceString: string | null
6
+ onClose: () => void
7
+ plotData: PlotPoint[]
8
+ nodes: string[]
9
+ isLoading: boolean
10
+ error: string | null
11
+ }
12
+
13
+ export const SpiceSimulationOverlay = ({
14
+ spiceString,
15
+ onClose,
16
+ plotData,
17
+ nodes,
18
+ isLoading,
19
+ error,
20
+ }: SpiceSimulationOverlayProps) => {
21
+ return (
22
+ <div
23
+ style={{
24
+ position: "fixed",
25
+ top: 0,
26
+ left: 0,
27
+ right: 0,
28
+ bottom: 0,
29
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
30
+ display: "flex",
31
+ alignItems: "center",
32
+ justifyContent: "center",
33
+ zIndex: 1002,
34
+ fontFamily: "sans-serif",
35
+ }}
36
+ >
37
+ <div
38
+ style={{
39
+ backgroundColor: "white",
40
+ padding: "24px",
41
+ borderRadius: "12px",
42
+ width: "90%",
43
+ maxWidth: "900px",
44
+ boxShadow: "0 4px 20px rgba(0, 0, 0, 0.15)",
45
+ }}
46
+ >
47
+ <div
48
+ style={{
49
+ display: "flex",
50
+ justifyContent: "space-between",
51
+ alignItems: "center",
52
+ marginBottom: "24px",
53
+ borderBottom: "1px solid #eee",
54
+ paddingBottom: "16px",
55
+ }}
56
+ >
57
+ <h2
58
+ style={{
59
+ margin: 0,
60
+ fontSize: "22px",
61
+ fontWeight: 600,
62
+ color: "#333",
63
+ }}
64
+ >
65
+ SPICE Simulation
66
+ </h2>
67
+ <button
68
+ onClick={onClose}
69
+ style={{
70
+ background: "none",
71
+ border: "none",
72
+ fontSize: "28px",
73
+ cursor: "pointer",
74
+ color: "#888",
75
+ padding: 0,
76
+ lineHeight: 1,
77
+ }}
78
+ >
79
+ &times;
80
+ </button>
81
+ </div>
82
+ <div>
83
+ <SpicePlot
84
+ plotData={plotData}
85
+ nodes={nodes}
86
+ isLoading={isLoading}
87
+ error={error}
88
+ />
89
+ </div>
90
+ <div style={{ marginTop: "24px" }}>
91
+ <h3
92
+ style={{
93
+ marginTop: 0,
94
+ marginBottom: "12px",
95
+ fontSize: "18px",
96
+ fontWeight: 600,
97
+ color: "#333",
98
+ }}
99
+ >
100
+ SPICE Netlist
101
+ </h3>
102
+ <pre
103
+ style={{
104
+ backgroundColor: "#fafafa",
105
+ padding: "16px",
106
+ borderRadius: "6px",
107
+ maxHeight: "150px",
108
+ overflowY: "auto",
109
+ border: "1px solid #eee",
110
+ color: "#333",
111
+ fontSize: "13px",
112
+ fontFamily: "monospace",
113
+ }}
114
+ >
115
+ {spiceString}
116
+ </pre>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ )
121
+ }
@@ -0,0 +1,161 @@
1
+ import { useState, useEffect } from "react"
2
+ import type * as EecircuitEngine from "../types/eecircuit-engine"
3
+ // @ts-ignore
4
+ import { getSpiceSimulationWorkerBlobUrl } from "../workers/spice-simulation.worker.blob.js"
5
+
6
+ // Types from eecircuit-engine interface
7
+ type RealDataType = {
8
+ name: string
9
+ type: string
10
+ values: number[]
11
+ }
12
+ type ComplexNumber = {
13
+ real: number
14
+ img: number
15
+ }
16
+ type ComplexDataType = {
17
+ name: string
18
+ type: string
19
+ values: ComplexNumber[]
20
+ }
21
+ type EecEngineResult =
22
+ | {
23
+ header: string
24
+ numVariables: number
25
+ variableNames: string[]
26
+ numPoints: number
27
+ dataType: "real"
28
+ data: RealDataType[]
29
+ }
30
+ | {
31
+ header: string
32
+ numVariables: number
33
+ variableNames: string[]
34
+ numPoints: number
35
+ dataType: "complex"
36
+ data: ComplexDataType[]
37
+ }
38
+
39
+ export interface PlotPoint {
40
+ name: string // time or sweep variable
41
+ [key: string]: number | string
42
+ }
43
+
44
+ const parseEecEngineOutput = (
45
+ result: EecEngineResult,
46
+ ): { plotData: PlotPoint[]; nodes: string[] } => {
47
+ const columnData: Record<string, number[]> = {}
48
+
49
+ if (result.dataType === "real") {
50
+ result.data.forEach((col) => {
51
+ columnData[col.name] = col.values
52
+ })
53
+ } else if (result.dataType === "complex") {
54
+ result.data.forEach((col) => {
55
+ // For now, plot the real part of complex numbers
56
+ columnData[col.name] = col.values.map((v) => v.real)
57
+ })
58
+ } else {
59
+ throw new Error("Unsupported data type in simulation result")
60
+ }
61
+
62
+ const timeKey = Object.keys(columnData).find(
63
+ (k) => k.toLowerCase() === "time" || k.toLowerCase() === "frequency",
64
+ )
65
+ if (!timeKey) {
66
+ throw new Error("No time or frequency data in simulation result")
67
+ }
68
+ const timeValues = columnData[timeKey]
69
+ const probedVariables = Object.keys(columnData).filter((k) => k !== timeKey)
70
+ const plotableNodes = probedVariables.map((n) =>
71
+ n.replace(/v\(([^)]+)\)/i, "$1"),
72
+ )
73
+
74
+ const plotData: PlotPoint[] = timeValues.map((t: number, i: number) => {
75
+ const point: PlotPoint = { name: t.toExponential(2) }
76
+ probedVariables.forEach((variable, j) => {
77
+ point[plotableNodes[j]] = columnData[variable][i]
78
+ })
79
+ return point
80
+ })
81
+
82
+ return { plotData, nodes: plotableNodes }
83
+ }
84
+
85
+ type WorkerMessage =
86
+ | {
87
+ type: "result"
88
+ result: EecEngineResult
89
+ }
90
+ | { type: "error"; error: string }
91
+
92
+ export const useSpiceSimulation = (spiceString: string | null) => {
93
+ const [plotData, setPlotData] = useState<PlotPoint[]>([])
94
+ const [nodes, setNodes] = useState<string[]>([])
95
+ const [isLoading, setIsLoading] = useState(true)
96
+ const [error, setError] = useState<string | null>(null)
97
+
98
+ useEffect(() => {
99
+ if (!spiceString) {
100
+ setIsLoading(false)
101
+ setPlotData([])
102
+ setNodes([])
103
+ setError(null)
104
+ return
105
+ }
106
+ setIsLoading(true)
107
+ setError(null)
108
+ setPlotData([])
109
+ setNodes([])
110
+
111
+ let worker: Worker
112
+
113
+ // In development, Vite will handle the worker bundling.
114
+ // In production, we use a pre-built blob URL.
115
+ if (import.meta.env.DEV) {
116
+ worker = new Worker(
117
+ new URL("../workers/spice-simulation.worker.ts", import.meta.url),
118
+ { type: "module" },
119
+ )
120
+ } else {
121
+ const workerUrl = getSpiceSimulationWorkerBlobUrl()
122
+
123
+ if (!workerUrl) {
124
+ setError("Could not create SPICE simulation worker.")
125
+ setIsLoading(false)
126
+ return
127
+ }
128
+ worker = new Worker(workerUrl, { type: "module" })
129
+ }
130
+
131
+ worker.onmessage = (event: MessageEvent<WorkerMessage>) => {
132
+ if (event.data.type === "result") {
133
+ try {
134
+ const { plotData: parsedData, nodes: parsedNodes } =
135
+ parseEecEngineOutput(event.data.result)
136
+ setPlotData(parsedData)
137
+ setNodes(parsedNodes)
138
+ } catch (e: any) {
139
+ setError(e.message || "Failed to parse simulation result")
140
+ console.error(e)
141
+ }
142
+ } else if (event.data.type === "error") {
143
+ setError(event.data.error)
144
+ }
145
+ setIsLoading(false)
146
+ }
147
+
148
+ worker.onerror = (err) => {
149
+ setError(err.message)
150
+ setIsLoading(false)
151
+ }
152
+
153
+ worker.postMessage({ spiceString })
154
+
155
+ return () => {
156
+ worker.terminate()
157
+ }
158
+ }, [spiceString])
159
+
160
+ return { plotData, nodes, isLoading, error }
161
+ }
@@ -0,0 +1,147 @@
1
+ export declare type ComplexDataType = {
2
+ name: string
3
+
4
+ type: string
5
+
6
+ values: ComplexNumber[]
7
+ }
8
+
9
+ export declare type ComplexNumber = {
10
+ real: number
11
+
12
+ img: number
13
+ }
14
+
15
+ /**
16
+
17
+ * Read output from spice
18
+
19
+ */
20
+
21
+ export declare type RealDataType = {
22
+ name: string
23
+
24
+ type: string
25
+
26
+ values: RealNumber[]
27
+ }
28
+
29
+ export declare type RealNumber = number
30
+
31
+ export declare type ResultType =
32
+ | {
33
+ header: string
34
+
35
+ numVariables: number
36
+
37
+ variableNames: string[]
38
+
39
+ numPoints: number
40
+
41
+ dataType: "real"
42
+
43
+ data: RealDataType[]
44
+ }
45
+ | {
46
+ header: string
47
+
48
+ numVariables: number
49
+
50
+ variableNames: string[]
51
+
52
+ numPoints: number
53
+
54
+ dataType: "complex"
55
+
56
+ data: ComplexDataType[]
57
+ }
58
+
59
+ export declare class Simulation {
60
+ private pass
61
+
62
+ private commandList
63
+
64
+ private cmd
65
+
66
+ private dataRaw
67
+
68
+ private results
69
+
70
+ private output
71
+
72
+ private info
73
+
74
+ private initInfo
75
+
76
+ private error
77
+
78
+ private initialized
79
+
80
+ private netList
81
+
82
+ private initPromiseResolve
83
+
84
+ private runPromiseResolve
85
+
86
+ private getInput
87
+
88
+ /**
89
+
90
+ * Internal startup method that sets up the Module and simulation loop.
91
+
92
+ */
93
+
94
+ private startInternal
95
+
96
+ /**
97
+
98
+ * Public start method.
99
+
100
+ * Returns a promise that resolves when the simulation module is initialized.
101
+
102
+ */
103
+
104
+ start: () => Promise<void>
105
+
106
+ /**
107
+
108
+ * Triggers a simulation run and returns a promise that resolves with the results.
109
+
110
+ */
111
+
112
+ runSim: () => Promise<ResultType>
113
+
114
+ /**
115
+
116
+ * Waits for a new simulation trigger.
117
+
118
+ */
119
+
120
+ private waitForNextRun
121
+
122
+ /**
123
+
124
+ * Resolves the waiting promise to continue the simulation loop.
125
+
126
+ */
127
+
128
+ private continueRun
129
+
130
+ private outputEvent
131
+
132
+ setNetList: (input: string) => void
133
+
134
+ private setOutputEvent
135
+
136
+ getInfo: () => string
137
+
138
+ getInitInfo: () => string
139
+
140
+ getError: () => string[]
141
+
142
+ isInitialized: () => boolean
143
+
144
+ private log_debug
145
+ }
146
+
147
+ export {}