@tscircuit/schematic-viewer 2.0.24 → 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.
- package/bun.lockb +0 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +590 -22
- package/dist/index.js.map +1 -1
- package/dist/workers/spice-simulation.worker.js +1 -0
- package/examples/example9-spice-simulation.fixture.tsx +77 -0
- package/lib/components/SchematicViewer.tsx +69 -13
- package/lib/components/SpiceIcon.tsx +14 -0
- package/lib/components/SpicePlot.tsx +193 -0
- package/lib/components/SpiceSimulationIcon.tsx +31 -0
- package/lib/components/SpiceSimulationOverlay.tsx +121 -0
- package/lib/hooks/useSpiceSimulation.ts +161 -0
- package/lib/types/eecircuit-engine.d.ts +147 -0
- package/lib/utils/spice-utils.ts +81 -0
- package/lib/utils/z-index-map.ts +1 -0
- package/lib/workers/spice-simulation.worker.ts +51 -0
- package/package.json +11 -10
- package/scripts/build-worker-blob-url.ts +55 -0
- package/tsup-webworker.config.ts +13 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { circuitJsonToSpice } from "circuit-json-to-spice"
|
|
2
|
+
import type { CircuitJson } from "circuit-json"
|
|
3
|
+
|
|
4
|
+
export const getSpiceFromCircuitJson = (circuitJson: CircuitJson): string => {
|
|
5
|
+
const spiceNetlist = circuitJsonToSpice(circuitJson as any)
|
|
6
|
+
const baseSpiceString = spiceNetlist.toSpiceString()
|
|
7
|
+
|
|
8
|
+
const lines = baseSpiceString.split("\n").filter((l) => l.trim() !== "")
|
|
9
|
+
const componentLines = lines.filter(
|
|
10
|
+
(l) => !l.startsWith("*") && !l.startsWith(".") && l.trim() !== "",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const allNodes = new Set<string>()
|
|
14
|
+
const capacitorNodes = new Set<string>()
|
|
15
|
+
|
|
16
|
+
for (const line of componentLines) {
|
|
17
|
+
const parts = line.trim().split(/\s+/)
|
|
18
|
+
if (parts.length < 3) continue
|
|
19
|
+
|
|
20
|
+
const componentType = parts[0][0].toUpperCase()
|
|
21
|
+
let nodesOnLine: string[] = []
|
|
22
|
+
|
|
23
|
+
if (["R", "C", "L", "V", "I", "D"].includes(componentType)) {
|
|
24
|
+
nodesOnLine = parts.slice(1, 3)
|
|
25
|
+
} else if (componentType === "Q" && parts.length >= 4) {
|
|
26
|
+
// BJT
|
|
27
|
+
nodesOnLine = parts.slice(1, 4)
|
|
28
|
+
} else if (componentType === "M" && parts.length >= 5) {
|
|
29
|
+
// MOSFET
|
|
30
|
+
nodesOnLine = parts.slice(1, 5)
|
|
31
|
+
} else if (componentType === "X") {
|
|
32
|
+
// Subcircuit
|
|
33
|
+
// Assume last part is model name, everything in between is a node
|
|
34
|
+
nodesOnLine = parts.slice(1, -1)
|
|
35
|
+
} else {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
nodesOnLine.forEach((node) => allNodes.add(node))
|
|
40
|
+
|
|
41
|
+
if (componentType === "C") {
|
|
42
|
+
nodesOnLine.forEach((node) => capacitorNodes.add(node))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Do not probe/set IC for ground
|
|
47
|
+
allNodes.delete("0")
|
|
48
|
+
capacitorNodes.delete("0")
|
|
49
|
+
|
|
50
|
+
const icLines = Array.from(capacitorNodes).map((node) => `.ic V(${node})=0`)
|
|
51
|
+
|
|
52
|
+
const probeNodes = Array.from(allNodes).map((node) => `V(${node})`)
|
|
53
|
+
const probeLine =
|
|
54
|
+
probeNodes.length > 0 ? `.probe ${probeNodes.join(" ")}` : ""
|
|
55
|
+
|
|
56
|
+
const tranLine = ".tran 0.1ms 50ms UIC"
|
|
57
|
+
|
|
58
|
+
const endStatement = ".end"
|
|
59
|
+
const originalLines = baseSpiceString.split("\n")
|
|
60
|
+
let endIndex = -1
|
|
61
|
+
for (let i = originalLines.length - 1; i >= 0; i--) {
|
|
62
|
+
if (originalLines[i].trim().toLowerCase().startsWith(endStatement)) {
|
|
63
|
+
endIndex = i
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const injectionLines = [...icLines, probeLine, tranLine].filter(Boolean)
|
|
69
|
+
|
|
70
|
+
let finalLines: string[]
|
|
71
|
+
|
|
72
|
+
if (endIndex !== -1) {
|
|
73
|
+
const beforeEnd = originalLines.slice(0, endIndex)
|
|
74
|
+
const endLineAndAfter = originalLines.slice(endIndex)
|
|
75
|
+
finalLines = [...beforeEnd, ...injectionLines, ...endLineAndAfter]
|
|
76
|
+
} else {
|
|
77
|
+
finalLines = [...originalLines, ...injectionLines, endStatement]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return finalLines.join("\n")
|
|
81
|
+
}
|
package/lib/utils/z-index-map.ts
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type * as EecircuitEngine from "../types/eecircuit-engine"
|
|
2
|
+
|
|
3
|
+
let sim: EecircuitEngine.Simulation | null = null
|
|
4
|
+
|
|
5
|
+
const fetchSimulation = async (): Promise<
|
|
6
|
+
typeof EecircuitEngine.Simulation
|
|
7
|
+
> => {
|
|
8
|
+
const module = await import(
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
"https://cdn.jsdelivr.net/npm/eecircuit-engine@1.5.2/+esm"
|
|
11
|
+
)
|
|
12
|
+
return module.Simulation as typeof EecircuitEngine.Simulation
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const initializeSimulation = async () => {
|
|
16
|
+
if (sim && sim.isInitialized()) return
|
|
17
|
+
const Simulation = await fetchSimulation()
|
|
18
|
+
sim = new Simulation()
|
|
19
|
+
await sim.start()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
self.onmessage = async (event: MessageEvent<{ spiceString: string }>) => {
|
|
23
|
+
try {
|
|
24
|
+
await initializeSimulation()
|
|
25
|
+
if (!sim) throw new Error("Simulation not initialized")
|
|
26
|
+
|
|
27
|
+
let engineSpiceString = event.data.spiceString
|
|
28
|
+
const wrdataMatch = engineSpiceString.match(/wrdata\s+(\S+)\s+(.*)/i)
|
|
29
|
+
if (wrdataMatch) {
|
|
30
|
+
const variables = wrdataMatch[2].trim().split(/\s+/)
|
|
31
|
+
const probeLine = `.probe ${variables.join(" ")}`
|
|
32
|
+
engineSpiceString = engineSpiceString.replace(/wrdata.*/i, probeLine)
|
|
33
|
+
} else if (!engineSpiceString.match(/\.probe/i)) {
|
|
34
|
+
const plotMatch = engineSpiceString.match(/plot\s+(.*)/i)
|
|
35
|
+
if (plotMatch) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"The 'plot' command is not supported for data extraction. Please use 'wrdata <filename> <var1> ...' or '.probe <var1> ...' instead.",
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
"No '.probe' or 'wrdata' command found in SPICE file. Use 'wrdata <filename> <var1> ...' to specify output.",
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
sim.setNetList(engineSpiceString)
|
|
46
|
+
const result = await sim.runSim()
|
|
47
|
+
self.postMessage({ type: "result", result })
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
self.postMessage({ type: "error", error: e.message })
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/schematic-viewer",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.25",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "cosmos",
|
|
8
|
-
"build": "tsup-
|
|
8
|
+
"build:webworker": "tsup --config tsup-webworker.config.ts",
|
|
9
|
+
"build:blob-url": "bun scripts/build-worker-blob-url.ts",
|
|
10
|
+
"build": "bun run build:webworker && bun run build:blob-url && tsup-node ./lib/index.ts --dts --format esm --sourcemap",
|
|
9
11
|
"build:site": "cosmos-export",
|
|
10
|
-
"vercel-build": "bun run build:site",
|
|
12
|
+
"vercel-build": "bun run build:webworker && bun run build:blob-url && bun run build:site",
|
|
11
13
|
"format": "biome format --write .",
|
|
12
14
|
"format:check": "biome format ."
|
|
13
15
|
},
|
|
14
16
|
"devDependencies": {
|
|
15
17
|
"@biomejs/biome": "^1.9.4",
|
|
16
|
-
"
|
|
17
|
-
"@tscircuit/props": "^0.0.172",
|
|
18
|
+
"tscircuit": "^0.0.580",
|
|
18
19
|
"@types/bun": "latest",
|
|
19
20
|
"@types/debug": "^4.1.12",
|
|
20
21
|
"@types/react": "^19.0.1",
|
|
21
22
|
"@types/react-dom": "^19.0.2",
|
|
23
|
+
"@types/recharts": "^2.0.1",
|
|
22
24
|
"@vitejs/plugin-react": "^4.3.4",
|
|
23
|
-
"circuit-json": "^0.0.154",
|
|
24
|
-
"circuit-to-svg": "^0.0.155",
|
|
25
25
|
"react": "18",
|
|
26
26
|
"react-cosmos": "^6.2.1",
|
|
27
27
|
"react-cosmos-plugin-vite": "^6.2.0",
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"typescript": "^5.0.0",
|
|
35
|
-
"
|
|
36
|
-
"@tscircuit/props": "*",
|
|
37
|
-
"circuit-to-svg": "*"
|
|
35
|
+
"tscircuit": "*"
|
|
38
36
|
},
|
|
39
37
|
"dependencies": {
|
|
38
|
+
"chart.js": "^4.5.0",
|
|
39
|
+
"circuit-json-to-spice": "^0.0.9",
|
|
40
40
|
"debug": "^4.4.0",
|
|
41
41
|
"performance-now": "^2.1.0",
|
|
42
|
+
"react-chartjs-2": "^5.3.0",
|
|
42
43
|
"use-mouse-matrix-transform": "^1.2.2"
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
const workerInputPath = path.resolve(
|
|
5
|
+
process.cwd(),
|
|
6
|
+
"dist/workers/spice-simulation.worker.js",
|
|
7
|
+
)
|
|
8
|
+
const workerOutputPath = path.resolve(
|
|
9
|
+
process.cwd(),
|
|
10
|
+
"lib/workers/spice-simulation.worker.blob.js",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const build = async () => {
|
|
14
|
+
try {
|
|
15
|
+
await fs.mkdir(path.dirname(workerOutputPath), { recursive: true })
|
|
16
|
+
const workerCode = await fs.readFile(workerInputPath, "utf-8")
|
|
17
|
+
|
|
18
|
+
const base64 = Buffer.from(workerCode).toString("base64")
|
|
19
|
+
|
|
20
|
+
const blobUrlModule = `// This file is generated by scripts/build-worker-blob-url.ts
|
|
21
|
+
// Do not edit this file directly.
|
|
22
|
+
|
|
23
|
+
const b64 = "${base64}";
|
|
24
|
+
|
|
25
|
+
let blobUrl = null;
|
|
26
|
+
|
|
27
|
+
export const getSpiceSimulationWorkerBlobUrl = () => {
|
|
28
|
+
if (typeof window === "undefined") return null;
|
|
29
|
+
if (blobUrl) return blobUrl;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const blob = new Blob([atob(b64)], { type: "application/javascript" });
|
|
33
|
+
blobUrl = URL.createObjectURL(blob);
|
|
34
|
+
return blobUrl;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error("Failed to create blob URL for worker", e);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
await fs.writeFile(workerOutputPath, blobUrlModule)
|
|
43
|
+
console.log(`Wrote worker blob URL module to ${workerOutputPath}`)
|
|
44
|
+
} catch (e: any) {
|
|
45
|
+
if (e.code === "ENOENT") {
|
|
46
|
+
console.error(`Error: Worker file not found at ${workerInputPath}`)
|
|
47
|
+
console.error("Please run 'bun run build:webworker' first.")
|
|
48
|
+
} else {
|
|
49
|
+
console.error("Failed to build worker blob URL", e)
|
|
50
|
+
}
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
build()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "tsup"
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["lib/workers/spice-simulation.worker.ts"],
|
|
5
|
+
outDir: "dist/workers",
|
|
6
|
+
format: ["esm"],
|
|
7
|
+
platform: "browser",
|
|
8
|
+
dts: false,
|
|
9
|
+
splitting: false,
|
|
10
|
+
sourcemap: false,
|
|
11
|
+
clean: true,
|
|
12
|
+
minify: true,
|
|
13
|
+
})
|