@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 CHANGED
Binary file
package/dist/index.js CHANGED
@@ -457,7 +457,7 @@ var enableDebug = () => {
457
457
  var debug_default = debug;
458
458
 
459
459
  // lib/components/SchematicViewer.tsx
460
- import { useEffect as useEffect7, useMemo as useMemo2, useRef as useRef4, useState as useState4 } from "react";
460
+ import { useEffect as useEffect8, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
461
461
  import {
462
462
  fromString,
463
463
  identity,
@@ -962,6 +962,7 @@ var SpiceSimulationIcon = ({
962
962
  };
963
963
 
964
964
  // lib/components/SpicePlot.tsx
965
+ import { useMemo as useMemo2 } from "react";
965
966
  import {
966
967
  Chart as ChartJS,
967
968
  CategoryScale,
@@ -1013,6 +1014,14 @@ var SpicePlot = ({
1013
1014
  isLoading,
1014
1015
  error
1015
1016
  }) => {
1017
+ const yAxisLabel = useMemo2(() => {
1018
+ const hasVoltage = nodes.some((n) => n.toLowerCase().startsWith("v("));
1019
+ const hasCurrent = nodes.some((n) => n.toLowerCase().startsWith("i("));
1020
+ if (hasVoltage && hasCurrent) return "Value";
1021
+ if (hasVoltage) return "Voltage (V)";
1022
+ if (hasCurrent) return "Current (A)";
1023
+ return "Value";
1024
+ }, [nodes]);
1016
1025
  if (isLoading) {
1017
1026
  return /* @__PURE__ */ jsx7(
1018
1027
  "div",
@@ -1122,7 +1131,7 @@ var SpicePlot = ({
1122
1131
  y: {
1123
1132
  title: {
1124
1133
  display: true,
1125
- text: "Voltage",
1134
+ text: yAxisLabel,
1126
1135
  font: {
1127
1136
  family: "sans-serif"
1128
1137
  }
@@ -1139,6 +1148,7 @@ var SpicePlot = ({
1139
1148
  };
1140
1149
 
1141
1150
  // lib/components/SpiceSimulationOverlay.tsx
1151
+ import { useEffect as useEffect6, useState as useState3 } from "react";
1142
1152
  import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1143
1153
  var SpiceSimulationOverlay = ({
1144
1154
  spiceString,
@@ -1146,8 +1156,34 @@ var SpiceSimulationOverlay = ({
1146
1156
  plotData,
1147
1157
  nodes,
1148
1158
  isLoading,
1149
- error
1159
+ error,
1160
+ simOptions,
1161
+ onSimOptionsChange
1150
1162
  }) => {
1163
+ const [startTimeDraft, setStartTimeDraft] = useState3(
1164
+ String(simOptions.startTime)
1165
+ );
1166
+ const [durationDraft, setDurationDraft] = useState3(
1167
+ String(simOptions.duration)
1168
+ );
1169
+ useEffect6(() => {
1170
+ setStartTimeDraft(String(simOptions.startTime));
1171
+ setDurationDraft(String(simOptions.duration));
1172
+ }, [simOptions.startTime, simOptions.duration]);
1173
+ const handleRerun = () => {
1174
+ onSimOptionsChange({
1175
+ ...simOptions,
1176
+ startTime: Number(startTimeDraft),
1177
+ duration: Number(durationDraft)
1178
+ });
1179
+ };
1180
+ const filteredNodes = nodes.filter((node) => {
1181
+ const isVoltage = node.toLowerCase().startsWith("v(");
1182
+ const isCurrent = node.toLowerCase().startsWith("i(");
1183
+ if (simOptions.showVoltage && isVoltage) return true;
1184
+ if (simOptions.showCurrent && isCurrent) return true;
1185
+ return false;
1186
+ });
1151
1187
  return /* @__PURE__ */ jsx8(
1152
1188
  "div",
1153
1189
  {
@@ -1223,11 +1259,119 @@ var SpiceSimulationOverlay = ({
1223
1259
  SpicePlot,
1224
1260
  {
1225
1261
  plotData,
1226
- nodes,
1262
+ nodes: filteredNodes,
1227
1263
  isLoading,
1228
1264
  error
1229
1265
  }
1230
1266
  ) }),
1267
+ /* @__PURE__ */ jsxs5(
1268
+ "div",
1269
+ {
1270
+ style: {
1271
+ marginTop: "16px",
1272
+ padding: "12px",
1273
+ backgroundColor: "#f7f7f7",
1274
+ borderRadius: "6px",
1275
+ display: "flex",
1276
+ flexWrap: "wrap",
1277
+ gap: "24px",
1278
+ alignItems: "center",
1279
+ fontSize: "14px"
1280
+ },
1281
+ children: [
1282
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: "16px" }, children: [
1283
+ /* @__PURE__ */ jsxs5(
1284
+ "label",
1285
+ {
1286
+ style: { display: "flex", alignItems: "center", gap: "6px" },
1287
+ children: [
1288
+ /* @__PURE__ */ jsx8(
1289
+ "input",
1290
+ {
1291
+ type: "checkbox",
1292
+ checked: simOptions.showVoltage,
1293
+ onChange: (e) => onSimOptionsChange({
1294
+ ...simOptions,
1295
+ showVoltage: e.target.checked
1296
+ })
1297
+ }
1298
+ ),
1299
+ "Voltage"
1300
+ ]
1301
+ }
1302
+ ),
1303
+ /* @__PURE__ */ jsxs5(
1304
+ "label",
1305
+ {
1306
+ style: { display: "flex", alignItems: "center", gap: "6px" },
1307
+ children: [
1308
+ /* @__PURE__ */ jsx8(
1309
+ "input",
1310
+ {
1311
+ type: "checkbox",
1312
+ checked: simOptions.showCurrent,
1313
+ onChange: (e) => onSimOptionsChange({
1314
+ ...simOptions,
1315
+ showCurrent: e.target.checked
1316
+ })
1317
+ }
1318
+ ),
1319
+ "Current"
1320
+ ]
1321
+ }
1322
+ )
1323
+ ] }),
1324
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: "16px", alignItems: "center" }, children: [
1325
+ /* @__PURE__ */ jsx8("label", { htmlFor: "startTime", children: "Start Time (ms):" }),
1326
+ /* @__PURE__ */ jsx8(
1327
+ "input",
1328
+ {
1329
+ id: "startTime",
1330
+ type: "number",
1331
+ value: startTimeDraft,
1332
+ onChange: (e) => setStartTimeDraft(e.target.value),
1333
+ style: {
1334
+ width: "80px",
1335
+ padding: "4px 8px",
1336
+ borderRadius: "4px",
1337
+ border: "1px solid #ccc"
1338
+ }
1339
+ }
1340
+ ),
1341
+ /* @__PURE__ */ jsx8("label", { htmlFor: "duration", children: "Duration (ms):" }),
1342
+ /* @__PURE__ */ jsx8(
1343
+ "input",
1344
+ {
1345
+ id: "duration",
1346
+ type: "number",
1347
+ value: durationDraft,
1348
+ onChange: (e) => setDurationDraft(e.target.value),
1349
+ style: {
1350
+ width: "80px",
1351
+ padding: "4px 8px",
1352
+ borderRadius: "4px",
1353
+ border: "1px solid #ccc"
1354
+ }
1355
+ }
1356
+ ),
1357
+ /* @__PURE__ */ jsx8(
1358
+ "button",
1359
+ {
1360
+ onClick: handleRerun,
1361
+ style: {
1362
+ padding: "4px 12px",
1363
+ borderRadius: "4px",
1364
+ border: "1px solid #ccc",
1365
+ backgroundColor: "#f0f0f0",
1366
+ cursor: "pointer"
1367
+ },
1368
+ children: "Rerun"
1369
+ }
1370
+ )
1371
+ ] })
1372
+ ]
1373
+ }
1374
+ ),
1231
1375
  /* @__PURE__ */ jsxs5("div", { style: { marginTop: "24px" }, children: [
1232
1376
  /* @__PURE__ */ jsx8(
1233
1377
  "h3",
@@ -1268,7 +1412,7 @@ var SpiceSimulationOverlay = ({
1268
1412
  };
1269
1413
 
1270
1414
  // lib/hooks/useSpiceSimulation.ts
1271
- import { useState as useState3, useEffect as useEffect6 } from "react";
1415
+ import { useState as useState4, useEffect as useEffect7 } from "react";
1272
1416
 
1273
1417
  // lib/workers/spice-simulation.worker.blob.js
1274
1418
  var b64 = "dmFyIGU9bnVsbCxzPWFzeW5jKCk9Pihhd2FpdCBpbXBvcnQoImh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9ucG0vZWVjaXJjdWl0LWVuZ2luZUAxLjUuMi8rZXNtIikpLlNpbXVsYXRpb24sYz1hc3luYygpPT57aWYoZSYmZS5pc0luaXRpYWxpemVkKCkpcmV0dXJuO2xldCBpPWF3YWl0IHMoKTtlPW5ldyBpLGF3YWl0IGUuc3RhcnQoKX07c2VsZi5vbm1lc3NhZ2U9YXN5bmMgaT0+e3RyeXtpZihhd2FpdCBjKCksIWUpdGhyb3cgbmV3IEVycm9yKCJTaW11bGF0aW9uIG5vdCBpbml0aWFsaXplZCIpO2xldCB0PWkuZGF0YS5zcGljZVN0cmluZyxhPXQubWF0Y2goL3dyZGF0YVxzKyhcUyspXHMrKC4qKS9pKTtpZihhKXtsZXQgbz1gLnByb2JlICR7YVsyXS50cmltKCkuc3BsaXQoL1xzKy8pLmpvaW4oIiAiKX1gO3Q9dC5yZXBsYWNlKC93cmRhdGEuKi9pLG8pfWVsc2UgaWYoIXQubWF0Y2goL1wucHJvYmUvaSkpdGhyb3cgdC5tYXRjaCgvcGxvdFxzKyguKikvaSk/bmV3IEVycm9yKCJUaGUgJ3Bsb3QnIGNvbW1hbmQgaXMgbm90IHN1cHBvcnRlZCBmb3IgZGF0YSBleHRyYWN0aW9uLiBQbGVhc2UgdXNlICd3cmRhdGEgPGZpbGVuYW1lPiA8dmFyMT4gLi4uJyBvciAnLnByb2JlIDx2YXIxPiAuLi4nIGluc3RlYWQuIik6bmV3IEVycm9yKCJObyAnLnByb2JlJyBvciAnd3JkYXRhJyBjb21tYW5kIGZvdW5kIGluIFNQSUNFIGZpbGUuIFVzZSAnd3JkYXRhIDxmaWxlbmFtZT4gPHZhcjE+IC4uLicgdG8gc3BlY2lmeSBvdXRwdXQuIik7ZS5zZXROZXRMaXN0KHQpO2xldCBuPWF3YWl0IGUucnVuU2ltKCk7c2VsZi5wb3N0TWVzc2FnZSh7dHlwZToicmVzdWx0IixyZXN1bHQ6bn0pfWNhdGNoKHQpe3NlbGYucG9zdE1lc3NhZ2Uoe3R5cGU6ImVycm9yIixlcnJvcjp0Lm1lc3NhZ2V9KX19Owo=";
@@ -1308,24 +1452,22 @@ var parseEecEngineOutput = (result) => {
1308
1452
  }
1309
1453
  const timeValues = columnData[timeKey];
1310
1454
  const probedVariables = Object.keys(columnData).filter((k) => k !== timeKey);
1311
- const plotableNodes = probedVariables.map(
1312
- (n) => n.replace(/v\(([^)]+)\)/i, "$1")
1313
- );
1455
+ const plotableNodes = probedVariables;
1314
1456
  const plotData = timeValues.map((t, i) => {
1315
1457
  const point = { name: t.toExponential(2) };
1316
- probedVariables.forEach((variable, j) => {
1317
- point[plotableNodes[j]] = columnData[variable][i];
1458
+ probedVariables.forEach((variable) => {
1459
+ point[variable] = columnData[variable][i];
1318
1460
  });
1319
1461
  return point;
1320
1462
  });
1321
1463
  return { plotData, nodes: plotableNodes };
1322
1464
  };
1323
1465
  var useSpiceSimulation = (spiceString) => {
1324
- const [plotData, setPlotData] = useState3([]);
1325
- const [nodes, setNodes] = useState3([]);
1326
- const [isLoading, setIsLoading] = useState3(true);
1327
- const [error, setError] = useState3(null);
1328
- useEffect6(() => {
1466
+ const [plotData, setPlotData] = useState4([]);
1467
+ const [nodes, setNodes] = useState4([]);
1468
+ const [isLoading, setIsLoading] = useState4(true);
1469
+ const [error, setError] = useState4(null);
1470
+ useEffect7(() => {
1329
1471
  if (!spiceString) {
1330
1472
  setIsLoading(false);
1331
1473
  setPlotData([]);
@@ -1373,7 +1515,19 @@ var useSpiceSimulation = (spiceString) => {
1373
1515
 
1374
1516
  // lib/utils/spice-utils.ts
1375
1517
  import { circuitJsonToSpice } from "circuit-json-to-spice";
1376
- var getSpiceFromCircuitJson = (circuitJson) => {
1518
+ var formatSimTime = (seconds) => {
1519
+ if (seconds === 0) return "0";
1520
+ const absSeconds = Math.abs(seconds);
1521
+ const precision = (v) => v.toPrecision(4);
1522
+ if (absSeconds >= 1) return precision(seconds);
1523
+ if (absSeconds >= 1e-3) return `${precision(seconds * 1e3)}m`;
1524
+ if (absSeconds >= 1e-6) return `${precision(seconds * 1e6)}u`;
1525
+ if (absSeconds >= 1e-9) return `${precision(seconds * 1e9)}n`;
1526
+ if (absSeconds >= 1e-12) return `${precision(seconds * 1e12)}p`;
1527
+ if (absSeconds >= 1e-15) return `${precision(seconds * 1e15)}f`;
1528
+ return seconds.toExponential(3);
1529
+ };
1530
+ var getSpiceFromCircuitJson = (circuitJson, options) => {
1377
1531
  const spiceNetlist = circuitJsonToSpice(circuitJson);
1378
1532
  const baseSpiceString = spiceNetlist.toSpiceString();
1379
1533
  const lines = baseSpiceString.split("\n").filter((l) => l.trim() !== "");
@@ -1382,13 +1536,18 @@ var getSpiceFromCircuitJson = (circuitJson) => {
1382
1536
  );
1383
1537
  const allNodes = /* @__PURE__ */ new Set();
1384
1538
  const capacitorNodes = /* @__PURE__ */ new Set();
1539
+ const componentNamesToProbeCurrent = /* @__PURE__ */ new Set();
1385
1540
  for (const line of componentLines) {
1386
1541
  const parts = line.trim().split(/\s+/);
1387
1542
  if (parts.length < 3) continue;
1388
- const componentType = parts[0][0].toUpperCase();
1543
+ const componentName = parts[0];
1544
+ const componentType = componentName[0].toUpperCase();
1389
1545
  let nodesOnLine = [];
1390
1546
  if (["R", "C", "L", "V", "I", "D"].includes(componentType)) {
1391
1547
  nodesOnLine = parts.slice(1, 3);
1548
+ if (componentType === "V") {
1549
+ componentNamesToProbeCurrent.add(componentName);
1550
+ }
1392
1551
  } else if (componentType === "Q" && parts.length >= 4) {
1393
1552
  nodesOnLine = parts.slice(1, 4);
1394
1553
  } else if (componentType === "M" && parts.length >= 5) {
@@ -1406,9 +1565,23 @@ var getSpiceFromCircuitJson = (circuitJson) => {
1406
1565
  allNodes.delete("0");
1407
1566
  capacitorNodes.delete("0");
1408
1567
  const icLines = Array.from(capacitorNodes).map((node) => `.ic V(${node})=0`);
1409
- const probeNodes = Array.from(allNodes).map((node) => `V(${node})`);
1410
- const probeLine = probeNodes.length > 0 ? `.probe ${probeNodes.join(" ")}` : "";
1411
- const tranLine = ".tran 0.1ms 50ms UIC";
1568
+ const probes = [];
1569
+ const probeVoltages = Array.from(allNodes).map((node) => `V(${node})`);
1570
+ probes.push(...probeVoltages);
1571
+ const probeCurrents = Array.from(componentNamesToProbeCurrent).map(
1572
+ (name) => `I(${name})`
1573
+ );
1574
+ probes.push(...probeCurrents);
1575
+ const probeLine = probes.length > 0 ? `.probe ${probes.join(" ")}` : "";
1576
+ const tstart_ms = options?.startTime ?? 0;
1577
+ const duration_ms = options?.duration ?? 20;
1578
+ const tstart = tstart_ms * 1e-3;
1579
+ const duration = duration_ms * 1e-3;
1580
+ const tstop = tstart + duration;
1581
+ const tstep = duration / 50;
1582
+ const tranLine = `.tran ${formatSimTime(tstep)} ${formatSimTime(
1583
+ tstop
1584
+ )} ${formatSimTime(tstart)} UIC`;
1412
1585
  const endStatement = ".end";
1413
1586
  const originalLines = baseSpiceString.split("\n");
1414
1587
  let endIndex = -1;
@@ -1448,36 +1621,49 @@ var SchematicViewer = ({
1448
1621
  if (debug3) {
1449
1622
  enableDebug();
1450
1623
  }
1451
- const [showSpiceOverlay, setShowSpiceOverlay] = useState4(false);
1624
+ const [showSpiceOverlay, setShowSpiceOverlay] = useState5(false);
1625
+ const [spiceSimOptions, setSpiceSimOptions] = useState5({
1626
+ showVoltage: true,
1627
+ showCurrent: false,
1628
+ startTime: 0,
1629
+ // in ms
1630
+ duration: 20
1631
+ // in ms
1632
+ });
1452
1633
  const getCircuitHash = (circuitJson2) => {
1453
1634
  return `${circuitJson2?.length || 0}_${circuitJson2?.editCount || 0}`;
1454
1635
  };
1455
- const circuitJsonKey = useMemo2(
1636
+ const circuitJsonKey = useMemo3(
1456
1637
  () => getCircuitHash(circuitJson),
1457
1638
  [circuitJson]
1458
1639
  );
1459
- const spiceString = useMemo2(() => {
1640
+ const spiceString = useMemo3(() => {
1460
1641
  if (!spiceSimulationEnabled) return null;
1461
1642
  try {
1462
- return getSpiceFromCircuitJson(circuitJson);
1643
+ return getSpiceFromCircuitJson(circuitJson, spiceSimOptions);
1463
1644
  } catch (e) {
1464
1645
  console.error("Failed to generate SPICE string", e);
1465
1646
  return null;
1466
1647
  }
1467
- }, [circuitJsonKey, spiceSimulationEnabled]);
1648
+ }, [
1649
+ circuitJsonKey,
1650
+ spiceSimulationEnabled,
1651
+ spiceSimOptions.startTime,
1652
+ spiceSimOptions.duration
1653
+ ]);
1468
1654
  const {
1469
1655
  plotData,
1470
1656
  nodes,
1471
1657
  isLoading: isSpiceSimLoading,
1472
1658
  error: spiceSimError
1473
1659
  } = useSpiceSimulation(spiceString);
1474
- const [editModeEnabled, setEditModeEnabled] = useState4(defaultEditMode);
1475
- const [snapToGrid, setSnapToGrid] = useState4(true);
1476
- const [isInteractionEnabled, setIsInteractionEnabled] = useState4(
1660
+ const [editModeEnabled, setEditModeEnabled] = useState5(defaultEditMode);
1661
+ const [snapToGrid, setSnapToGrid] = useState5(true);
1662
+ const [isInteractionEnabled, setIsInteractionEnabled] = useState5(
1477
1663
  !clickToInteractEnabled
1478
1664
  );
1479
- const [showViewMenu, setShowViewMenu] = useState4(false);
1480
- const [showSchematicGroups, setShowSchematicGroups] = useState4(false);
1665
+ const [showViewMenu, setShowViewMenu] = useState5(false);
1666
+ const [showSchematicGroups, setShowSchematicGroups] = useState5(false);
1481
1667
  const svgDivRef = useRef4(null);
1482
1668
  const touchStartRef = useRef4(null);
1483
1669
  const handleTouchStart = (e) => {
@@ -1499,9 +1685,9 @@ var SchematicViewer = ({
1499
1685
  }
1500
1686
  touchStartRef.current = null;
1501
1687
  };
1502
- const [internalEditEvents, setInternalEditEvents] = useState4([]);
1688
+ const [internalEditEvents, setInternalEditEvents] = useState5([]);
1503
1689
  const circuitJsonRef = useRef4(circuitJson);
1504
- useEffect7(() => {
1690
+ useEffect8(() => {
1505
1691
  const circuitHash = getCircuitHash(circuitJson);
1506
1692
  const circuitHashRef = getCircuitHash(circuitJsonRef.current);
1507
1693
  if (circuitHash !== circuitHashRef) {
@@ -1522,7 +1708,7 @@ var SchematicViewer = ({
1522
1708
  enabled: isInteractionEnabled && !showSpiceOverlay
1523
1709
  });
1524
1710
  const { containerWidth, containerHeight } = useResizeHandling(containerRef);
1525
- const svgString = useMemo2(() => {
1711
+ const svgString = useMemo3(() => {
1526
1712
  if (!containerWidth || !containerHeight) return "";
1527
1713
  return convertCircuitJsonToSchematicSvg(circuitJson, {
1528
1714
  width: containerWidth,
@@ -1534,13 +1720,13 @@ var SchematicViewer = ({
1534
1720
  colorOverrides
1535
1721
  });
1536
1722
  }, [circuitJsonKey, containerWidth, containerHeight]);
1537
- const containerBackgroundColor = useMemo2(() => {
1723
+ const containerBackgroundColor = useMemo3(() => {
1538
1724
  const match = svgString.match(
1539
1725
  /<svg[^>]*style="[^"]*background-color:\s*([^;\"]+)/i
1540
1726
  );
1541
1727
  return match?.[1] ?? "transparent";
1542
1728
  }, [svgString]);
1543
- const realToSvgProjection = useMemo2(() => {
1729
+ const realToSvgProjection = useMemo3(() => {
1544
1730
  if (!svgString) return identity();
1545
1731
  const transformString = svgString.match(
1546
1732
  /data-real-to-screen-transform="([^"]+)"/
@@ -1558,7 +1744,7 @@ var SchematicViewer = ({
1558
1744
  onEditEvent(event);
1559
1745
  }
1560
1746
  };
1561
- const editEventsWithUnappliedEditEvents = useMemo2(() => {
1747
+ const editEventsWithUnappliedEditEvents = useMemo3(() => {
1562
1748
  return [...unappliedEditEvents, ...internalEditEvents];
1563
1749
  }, [unappliedEditEvents, internalEditEvents]);
1564
1750
  const { handleMouseDown, isDragging, activeEditEvent } = useComponentDragging(
@@ -1592,7 +1778,7 @@ var SchematicViewer = ({
1592
1778
  circuitJsonKey,
1593
1779
  showGroups: showSchematicGroups
1594
1780
  });
1595
- const svgDiv = useMemo2(
1781
+ const svgDiv = useMemo3(
1596
1782
  () => /* @__PURE__ */ jsx9(
1597
1783
  "div",
1598
1784
  {
@@ -1724,7 +1910,9 @@ var SchematicViewer = ({
1724
1910
  plotData,
1725
1911
  nodes,
1726
1912
  isLoading: isSpiceSimLoading,
1727
- error: spiceSimError
1913
+ error: spiceSimError,
1914
+ simOptions: spiceSimOptions,
1915
+ onSimOptionsChange: setSpiceSimOptions
1728
1916
  }
1729
1917
  ),
1730
1918
  svgDiv