@tscircuit/schematic-viewer 2.0.49 → 2.0.51

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,145 @@
1
+ import React from "react"
2
+ import { AnalogSimulationViewer } from "../lib/components/AnalogSimulationViewer"
3
+ import * as Core from "@tscircuit/core"
4
+ import createNgspiceSpiceEngine from "@tscircuit/ngspice-spice-engine"
5
+
6
+ // TSX circuit definition
7
+ const SwitchCircuitElement = (
8
+ <board schMaxTraceDistance={10} routingDisabled>
9
+ <voltagesource name="V1" voltage="5V" />
10
+ <resistor name="R_base" resistance="10k" schY={2} />
11
+ <switch name="SW1" simSwitchFrequency="1kHz" schX={1.5} schY={2} />
12
+ <transistor
13
+ name="Q1"
14
+ type="npn"
15
+ footprint="sot23"
16
+ schX={2}
17
+ schY={0.3}
18
+ schRotation={180}
19
+ />
20
+ <resistor name="R_collector" resistance="10k" schY={-2} />
21
+
22
+ <trace from=".V1 > .pin1" to=".R_base > .pin1" />
23
+ <trace from=".R_base > .pin2" to=".SW1 > .pin1" />
24
+ <trace from=".SW1 > .pin2" to=".Q1 > .base" />
25
+
26
+ <trace from=".V1 > .pin1" to=".R_collector > .pin1" />
27
+ <trace from=".R_collector > .pin2" to=".Q1 > .collector" />
28
+
29
+ <trace from=".Q1 > .emitter" to=".V1 > .pin2" />
30
+
31
+ <voltageprobe name="VP_COLLECTOR" connectsTo=".R_collector > .pin2" />
32
+
33
+ <analogsimulation duration="4ms" timePerStep="1us" spiceEngine="ngspice" />
34
+ </board>
35
+ )
36
+
37
+ // Convert TSX to CircuitJSON and add simulation data
38
+ const createSimulatedCircuitJson = async () => {
39
+ try {
40
+ // Step 1: Create circuit with platform configuration
41
+ const circuit = new Core.Circuit()
42
+
43
+ const ngspiceEngine = await createNgspiceSpiceEngine()
44
+ circuit.setPlatform({
45
+ spiceEngineMap: {
46
+ ngspice: ngspiceEngine,
47
+ },
48
+ })
49
+
50
+ // Step 2: Add the circuit element
51
+ circuit.add(SwitchCircuitElement)
52
+ await circuit.renderUntilSettled()
53
+
54
+ // Step 3: Get CircuitJSON (includes simulation data if produced by the platform)
55
+ return circuit.getCircuitJson()
56
+ } catch (error) {
57
+ console.error("Simulation failed:", error)
58
+ // Return basic CircuitJSON if simulation fails
59
+ const fallbackCircuit = new Core.Circuit()
60
+ fallbackCircuit.add(SwitchCircuitElement)
61
+ await fallbackCircuit.renderUntilSettled()
62
+ return fallbackCircuit.getCircuitJson()
63
+ }
64
+ }
65
+
66
+ export default () => {
67
+ const [simulatedCircuitJson, setSimulatedCircuitJson] = React.useState<
68
+ any[] | null
69
+ >(null)
70
+ const [isLoading, setIsLoading] = React.useState(true)
71
+ const [error, setError] = React.useState<string | null>(null)
72
+
73
+ React.useEffect(() => {
74
+ const loadAndSimulateCircuit = async () => {
75
+ try {
76
+ setIsLoading(true)
77
+ setError(null)
78
+
79
+ const result = await createSimulatedCircuitJson()
80
+ setSimulatedCircuitJson(result)
81
+ } catch (err) {
82
+ setError(err instanceof Error ? err.message : "Failed to load circuit")
83
+ console.error("Error loading circuit:", err)
84
+ } finally {
85
+ setIsLoading(false)
86
+ }
87
+ }
88
+
89
+ loadAndSimulateCircuit()
90
+ }, [])
91
+
92
+ if (isLoading) {
93
+ return (
94
+ <div style={{ padding: "20px", textAlign: "center" }}>
95
+ <h2>Analog Simulation Viewer Example</h2>
96
+ <p>Converting TSX to CircuitJSON and running SPICE simulation...</p>
97
+ <div
98
+ style={{
99
+ display: "inline-block",
100
+ padding: "20px",
101
+ backgroundColor: "#f5f5f5",
102
+ borderRadius: "8px",
103
+ marginTop: "20px",
104
+ }}
105
+ >
106
+ Loading and simulating circuit...
107
+ </div>
108
+ </div>
109
+ )
110
+ }
111
+
112
+ if (error) {
113
+ return (
114
+ <div style={{ padding: "20px", textAlign: "center" }}>
115
+ <h2>Analog Simulation Viewer Example</h2>
116
+ <div
117
+ style={{
118
+ display: "inline-block",
119
+ padding: "20px",
120
+ backgroundColor: "#fef2f2",
121
+ borderRadius: "8px",
122
+ border: "1px solid #fecaca",
123
+ color: "#dc2626",
124
+ marginTop: "20px",
125
+ }}
126
+ >
127
+ <h4>Error:</h4>
128
+ <p>{error}</p>
129
+ </div>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ return (
135
+ simulatedCircuitJson && (
136
+ <AnalogSimulationViewer
137
+ circuitJson={simulatedCircuitJson}
138
+ containerStyle={{
139
+ width: "100vw",
140
+ height: "100vh",
141
+ }}
142
+ />
143
+ )
144
+ )
145
+ }
@@ -0,0 +1,13 @@
1
+ import { AnalogSimulationViewer } from "lib/components/AnalogSimulationViewer"
2
+ import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"
3
+
4
+ const circuitJson = renderToCircuitJson(
5
+ <board width="10mm" height="10mm" routingDisabled></board>,
6
+ )
7
+
8
+ export default () => (
9
+ <AnalogSimulationViewer
10
+ circuitJson={circuitJson}
11
+ containerStyle={{ height: "100%" }}
12
+ />
13
+ )
@@ -0,0 +1,300 @@
1
+ import {
2
+ convertCircuitJsonToSchematicSimulationSvg,
3
+ type ColorOverrides,
4
+ } from "circuit-to-svg"
5
+ import { useEffect, useState, useMemo, useRef } from "react"
6
+ import { useResizeHandling } from "../hooks/use-resize-handling"
7
+ import { useMouseMatrixTransform } from "use-mouse-matrix-transform"
8
+ import { toString as transformToString } from "transformation-matrix"
9
+ import type { CircuitJson } from "circuit-json"
10
+
11
+ interface Props {
12
+ circuitJson: CircuitJson
13
+ containerStyle?: React.CSSProperties
14
+ colorOverrides?: ColorOverrides
15
+ width?: number
16
+ height?: number
17
+ className?: string
18
+ }
19
+
20
+ export const AnalogSimulationViewer = ({
21
+ circuitJson: inputCircuitJson,
22
+ containerStyle,
23
+ colorOverrides,
24
+ width,
25
+ height,
26
+ className,
27
+ }: Props) => {
28
+ const [circuitJson, setCircuitJson] = useState<CircuitJson | null>(null)
29
+ const [isLoading, setIsLoading] = useState(true)
30
+ const [error, setError] = useState<string | null>(null)
31
+ const [svgObjectUrl, setSvgObjectUrl] = useState<string | null>(null)
32
+ const containerRef = useRef<HTMLDivElement>(null)
33
+ const imgRef = useRef<HTMLImageElement>(null)
34
+
35
+ const { containerWidth, containerHeight } = useResizeHandling(
36
+ containerRef as React.RefObject<HTMLElement>,
37
+ )
38
+
39
+ const [isDragging, setIsDragging] = useState(false)
40
+
41
+ const {
42
+ ref: transformRef,
43
+ cancelDrag: _cancelDrag,
44
+ transform: _svgToScreenProjection,
45
+ } = useMouseMatrixTransform({
46
+ onSetTransform(transform) {
47
+ if (imgRef.current) {
48
+ imgRef.current.style.transform = transformToString(transform)
49
+ }
50
+ },
51
+ })
52
+
53
+ const effectiveWidth = width || containerWidth || 1000
54
+ const effectiveHeight = height || containerHeight || 600
55
+
56
+ // Set CircuitJSON from props
57
+ useEffect(() => {
58
+ setIsLoading(true)
59
+ setError(null)
60
+ setCircuitJson(inputCircuitJson)
61
+ setIsLoading(false)
62
+ }, [inputCircuitJson])
63
+
64
+ // Find simulation experiment ID from circuit JSON
65
+ const simulationExperimentId = useMemo(() => {
66
+ if (!circuitJson) return null
67
+ const simulationElement = circuitJson.find(
68
+ (el) => el.type === "simulation_experiment",
69
+ )
70
+ return simulationElement?.simulation_experiment_id || null
71
+ }, [circuitJson])
72
+
73
+ // Find simulation graph IDs from circuit JSON
74
+ const simulationGraphIds = useMemo(() => {
75
+ if (!circuitJson) return []
76
+ return circuitJson
77
+ .filter((el) => el.type === "simulation_transient_voltage_graph")
78
+ .map((el) => el.simulation_transient_voltage_graph_id)
79
+ }, [circuitJson])
80
+
81
+ // Generate SVG from CircuitJSON
82
+ const simulationSvg = useMemo(() => {
83
+ if (
84
+ !circuitJson ||
85
+ !effectiveWidth ||
86
+ !effectiveHeight ||
87
+ !simulationExperimentId
88
+ )
89
+ return ""
90
+
91
+ try {
92
+ return convertCircuitJsonToSchematicSimulationSvg({
93
+ circuitJson,
94
+ simulation_experiment_id: simulationExperimentId,
95
+ simulation_transient_voltage_graph_ids: simulationGraphIds,
96
+ width: effectiveWidth,
97
+ height: effectiveHeight,
98
+ schematicOptions: { colorOverrides },
99
+ })
100
+ } catch (fallbackErr) {
101
+ console.error("Failed to generate fallback schematic SVG:", fallbackErr)
102
+ return ""
103
+ }
104
+ }, [
105
+ circuitJson,
106
+ effectiveWidth,
107
+ effectiveHeight,
108
+ colorOverrides,
109
+ simulationExperimentId,
110
+ simulationGraphIds,
111
+ ])
112
+
113
+ // Create/revoke object URL whenever the SVG changes
114
+ useEffect(() => {
115
+ if (!simulationSvg) {
116
+ setSvgObjectUrl(null)
117
+ return
118
+ }
119
+
120
+ try {
121
+ const blob = new Blob([simulationSvg], { type: "image/svg+xml" })
122
+ const url = URL.createObjectURL(blob)
123
+ setSvgObjectUrl(url)
124
+ return () => {
125
+ URL.revokeObjectURL(url)
126
+ }
127
+ } catch (error) {
128
+ console.error("Failed to create SVG object URL:", error)
129
+ setSvgObjectUrl(null)
130
+ }
131
+ }, [simulationSvg])
132
+
133
+ const containerBackgroundColor = useMemo(() => {
134
+ if (!simulationSvg) return "transparent"
135
+ const match = simulationSvg.match(
136
+ /<svg[^>]*style="[^"]*background-color:\s*([^;\"]+)/i,
137
+ )
138
+ return match?.[1] ?? "transparent"
139
+ }, [simulationSvg])
140
+
141
+ const handleMouseDown = (_e: React.MouseEvent) => {
142
+ setIsDragging(true)
143
+ }
144
+
145
+ const handleTouchStart = (_e: React.TouchEvent) => {
146
+ setIsDragging(true)
147
+ }
148
+
149
+ useEffect(() => {
150
+ const handleMouseUp = () => {
151
+ setIsDragging(false)
152
+ }
153
+
154
+ const handleTouchEnd = () => {
155
+ setIsDragging(false)
156
+ }
157
+
158
+ window.addEventListener("mouseup", handleMouseUp)
159
+ window.addEventListener("touchend", handleTouchEnd)
160
+
161
+ return () => {
162
+ window.removeEventListener("mouseup", handleMouseUp)
163
+ window.removeEventListener("touchend", handleTouchEnd)
164
+ }
165
+ }, [])
166
+
167
+ if (isLoading) {
168
+ return (
169
+ <div
170
+ style={{
171
+ display: "flex",
172
+ alignItems: "center",
173
+ justifyContent: "center",
174
+ backgroundColor: "#f5f5f5",
175
+ minHeight: "300px",
176
+ fontFamily: "sans-serif",
177
+ fontSize: "16px",
178
+ color: "#666",
179
+ ...containerStyle,
180
+ }}
181
+ className={className}
182
+ >
183
+ Loading circuit...
184
+ </div>
185
+ )
186
+ }
187
+
188
+ if (error) {
189
+ return (
190
+ <div
191
+ style={{
192
+ display: "flex",
193
+ alignItems: "center",
194
+ justifyContent: "center",
195
+ backgroundColor: "#fef2f2",
196
+ minHeight: "300px",
197
+ fontFamily: "sans-serif",
198
+ fontSize: "16px",
199
+ color: "#dc2626",
200
+ ...containerStyle,
201
+ }}
202
+ className={className}
203
+ >
204
+ <div style={{ textAlign: "center", padding: "20px" }}>
205
+ <div style={{ fontWeight: "bold", marginBottom: "8px" }}>
206
+ Circuit Conversion Error
207
+ </div>
208
+ <div style={{ fontSize: "14px" }}>{error}</div>
209
+ </div>
210
+ </div>
211
+ )
212
+ }
213
+
214
+ if (!simulationSvg) {
215
+ return (
216
+ <div
217
+ style={{
218
+ display: "flex",
219
+ flexDirection: "column",
220
+ alignItems: "center",
221
+ justifyContent: "center",
222
+ backgroundColor: "#f8fafc",
223
+ minHeight: "300px",
224
+ fontFamily: "sans-serif",
225
+ gap: "12px",
226
+ ...containerStyle,
227
+ }}
228
+ className={className}
229
+ >
230
+ <div style={{ fontSize: "16px", color: "#475569", fontWeight: 500 }}>
231
+ No Simulation Found
232
+ </div>
233
+ <div style={{ fontSize: "14px", color: "#64748b" }}>
234
+ Use{" "}
235
+ <code
236
+ style={{
237
+ backgroundColor: "#e2e8f0",
238
+ padding: "2px 6px",
239
+ borderRadius: "4px",
240
+ fontFamily: "monospace",
241
+ fontSize: "13px",
242
+ }}
243
+ >
244
+ {"<analogsimulation />"}
245
+ </code>{" "}
246
+ to create simulations
247
+ </div>
248
+ </div>
249
+ )
250
+ }
251
+
252
+ return (
253
+ <div
254
+ ref={(node) => {
255
+ containerRef.current = node
256
+ transformRef.current = node
257
+ }}
258
+ style={{
259
+ position: "relative",
260
+ backgroundColor: containerBackgroundColor,
261
+ overflow: "hidden",
262
+ minHeight: "300px",
263
+ cursor: isDragging ? "grabbing" : "grab",
264
+ ...containerStyle,
265
+ }}
266
+ className={className}
267
+ onMouseDown={handleMouseDown}
268
+ onTouchStart={handleTouchStart}
269
+ >
270
+ {svgObjectUrl ? (
271
+ <img
272
+ ref={imgRef}
273
+ src={svgObjectUrl}
274
+ alt="Circuit Simulation"
275
+ style={{
276
+ transformOrigin: "0 0",
277
+ width: "100%",
278
+ height: "100%",
279
+ display: "block",
280
+ objectFit: "contain",
281
+ }}
282
+ />
283
+ ) : (
284
+ <div
285
+ style={{
286
+ display: "flex",
287
+ alignItems: "center",
288
+ justifyContent: "center",
289
+ height: "100%",
290
+ minHeight: "300px",
291
+ color: "#666",
292
+ fontFamily: "sans-serif",
293
+ }}
294
+ >
295
+ Failed to render SVG
296
+ </div>
297
+ )}
298
+ </div>
299
+ )
300
+ }
@@ -172,7 +172,7 @@ export const SpicePlot = ({
172
172
  title: (tooltipItems) => {
173
173
  if (tooltipItems.length > 0) {
174
174
  const item = tooltipItems[0]
175
- return formatTimeWithUnits(item.parsed.x)
175
+ return formatTimeWithUnits(item.parsed.x as number)
176
176
  }
177
177
  return ""
178
178
  },
@@ -36,7 +36,7 @@ export const ViewMenu = ({
36
36
  const componentTypes = new Set()
37
37
  for (const comp of schematicComponents) {
38
38
  const sourceComp = su(circuitJson).source_component.get(
39
- comp.source_component_id,
39
+ comp.source_component_id!,
40
40
  )
41
41
  if (sourceComp?.ftype) {
42
42
  componentTypes.add(sourceComp.ftype)
@@ -57,7 +57,7 @@ export const useChangeSchematicTracesForMovedComponents = ({
57
57
  const src_traces = su(circuitJson)
58
58
  .source_trace.list()
59
59
  .filter((st) =>
60
- st.connected_source_port_ids?.some((spi) =>
60
+ st.connected_source_port_ids?.some((spi: string) =>
61
61
  src_port_ids.has(spi),
62
62
  ),
63
63
  )
@@ -66,7 +66,7 @@ export const useChangeSchematicTracesForMovedComponents = ({
66
66
  )
67
67
  const schematic_traces = su(circuitJson)
68
68
  .schematic_trace.list()
69
- .filter((st) => src_trace_ids.has(st.source_trace_id))
69
+ .filter((st) => src_trace_ids.has(st.source_trace_id!))
70
70
 
71
71
  // Make the connected traces dashed
72
72
  schematic_traces.forEach((trace) => {
@@ -122,7 +122,7 @@ export const useSchematicGroupsOverlay = (
122
122
 
123
123
  for (const comp of schematicComponents) {
124
124
  const sourceComp = su(circuitJson).source_component.get(
125
- comp.source_component_id,
125
+ comp.source_component_id!,
126
126
  )
127
127
  if (sourceComp?.source_group_id) {
128
128
  if (!groupMap.has(sourceComp.source_group_id)) {
package/lib/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { SchematicViewer } from "./components/SchematicViewer"
2
2
  export { MouseTracker } from "./components/MouseTracker"
3
3
  export { useMouseEventsOverBoundingBox } from "./hooks/useMouseEventsOverBoundingBox"
4
+ export { AnalogSimulationViewer } from "./components/AnalogSimulationViewer"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.49",
3
+ "version": "2.0.51",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -27,7 +27,7 @@
27
27
  "react-dom": "^19.1.0",
28
28
  "react-reconciler": "^0.31.0",
29
29
  "semver": "^7.7.2",
30
- "tscircuit": "^0.0.611",
30
+ "tscircuit": "^0.0.1037",
31
31
  "tsup": "^8.3.5",
32
32
  "vite": "^6.0.3"
33
33
  },
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "chart.js": "^4.5.0",
40
- "circuit-json-to-spice": "^0.0.10",
40
+ "circuit-json-to-spice": "^0.0.30",
41
41
  "debug": "^4.4.0",
42
42
  "performance-now": "^2.1.0",
43
43
  "react-chartjs-2": "^5.3.0",