@tscircuit/schematic-viewer 2.0.24 → 2.0.26
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 +582 -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 +151 -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
package/dist/index.js
CHANGED
|
@@ -201,7 +201,7 @@ var enableDebug = () => {
|
|
|
201
201
|
var debug_default = debug;
|
|
202
202
|
|
|
203
203
|
// lib/components/SchematicViewer.tsx
|
|
204
|
-
import { useEffect as
|
|
204
|
+
import { useEffect as useEffect6, useMemo, useRef as useRef4, useState as useState4 } from "react";
|
|
205
205
|
import {
|
|
206
206
|
fromString,
|
|
207
207
|
identity,
|
|
@@ -386,6 +386,7 @@ var useComponentDragging = ({
|
|
|
386
386
|
var zIndexMap = {
|
|
387
387
|
schematicEditIcon: 50,
|
|
388
388
|
schematicGridIcon: 49,
|
|
389
|
+
spiceSimulationIcon: 51,
|
|
389
390
|
clickToInteractOverlay: 100
|
|
390
391
|
};
|
|
391
392
|
|
|
@@ -474,8 +475,523 @@ var GridIcon = ({
|
|
|
474
475
|
);
|
|
475
476
|
};
|
|
476
477
|
|
|
478
|
+
// lib/components/SpiceIcon.tsx
|
|
479
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
480
|
+
var SpiceIcon = () => /* @__PURE__ */ jsx3(
|
|
481
|
+
"svg",
|
|
482
|
+
{
|
|
483
|
+
width: "16",
|
|
484
|
+
height: "16",
|
|
485
|
+
viewBox: "0 0 24 24",
|
|
486
|
+
fill: "none",
|
|
487
|
+
stroke: "currentColor",
|
|
488
|
+
strokeWidth: "2",
|
|
489
|
+
strokeLinecap: "round",
|
|
490
|
+
strokeLinejoin: "round",
|
|
491
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M3 12h2.5l2.5-9 4 18 4-9h5.5" })
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// lib/components/SpiceSimulationIcon.tsx
|
|
496
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
497
|
+
var SpiceSimulationIcon = ({
|
|
498
|
+
onClick
|
|
499
|
+
}) => {
|
|
500
|
+
return /* @__PURE__ */ jsx4(
|
|
501
|
+
"div",
|
|
502
|
+
{
|
|
503
|
+
onClick,
|
|
504
|
+
style: {
|
|
505
|
+
position: "absolute",
|
|
506
|
+
top: "16px",
|
|
507
|
+
right: "56px",
|
|
508
|
+
backgroundColor: "#fff",
|
|
509
|
+
color: "#000",
|
|
510
|
+
padding: "8px",
|
|
511
|
+
borderRadius: "4px",
|
|
512
|
+
cursor: "pointer",
|
|
513
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
|
514
|
+
display: "flex",
|
|
515
|
+
alignItems: "center",
|
|
516
|
+
gap: "4px",
|
|
517
|
+
zIndex: zIndexMap.spiceSimulationIcon
|
|
518
|
+
},
|
|
519
|
+
children: /* @__PURE__ */ jsx4(SpiceIcon, {})
|
|
520
|
+
}
|
|
521
|
+
);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// lib/components/SpicePlot.tsx
|
|
525
|
+
import {
|
|
526
|
+
Chart as ChartJS,
|
|
527
|
+
CategoryScale,
|
|
528
|
+
LinearScale,
|
|
529
|
+
PointElement,
|
|
530
|
+
LineElement,
|
|
531
|
+
Title,
|
|
532
|
+
Tooltip,
|
|
533
|
+
Legend
|
|
534
|
+
} from "chart.js";
|
|
535
|
+
import { Line } from "react-chartjs-2";
|
|
536
|
+
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
537
|
+
ChartJS.register(
|
|
538
|
+
CategoryScale,
|
|
539
|
+
LinearScale,
|
|
540
|
+
PointElement,
|
|
541
|
+
LineElement,
|
|
542
|
+
Title,
|
|
543
|
+
Tooltip,
|
|
544
|
+
Legend
|
|
545
|
+
);
|
|
546
|
+
var colors = ["#8884d8", "#82ca9d", "#ffc658", "#ff7300", "#387908"];
|
|
547
|
+
var formatTimeWithUnits = (seconds) => {
|
|
548
|
+
if (seconds === 0) return "0s";
|
|
549
|
+
const absSeconds = Math.abs(seconds);
|
|
550
|
+
let unit = "s";
|
|
551
|
+
let scale = 1;
|
|
552
|
+
if (absSeconds < 1e-12) {
|
|
553
|
+
unit = "fs";
|
|
554
|
+
scale = 1e15;
|
|
555
|
+
} else if (absSeconds < 1e-9) {
|
|
556
|
+
unit = "ps";
|
|
557
|
+
scale = 1e12;
|
|
558
|
+
} else if (absSeconds < 1e-6) {
|
|
559
|
+
unit = "ns";
|
|
560
|
+
scale = 1e9;
|
|
561
|
+
} else if (absSeconds < 1e-3) {
|
|
562
|
+
unit = "us";
|
|
563
|
+
scale = 1e6;
|
|
564
|
+
} else if (absSeconds < 1) {
|
|
565
|
+
unit = "ms";
|
|
566
|
+
scale = 1e3;
|
|
567
|
+
}
|
|
568
|
+
return `${parseFloat((seconds * scale).toPrecision(3))}${unit}`;
|
|
569
|
+
};
|
|
570
|
+
var SpicePlot = ({
|
|
571
|
+
plotData,
|
|
572
|
+
nodes,
|
|
573
|
+
isLoading,
|
|
574
|
+
error
|
|
575
|
+
}) => {
|
|
576
|
+
if (isLoading) {
|
|
577
|
+
return /* @__PURE__ */ jsx5(
|
|
578
|
+
"div",
|
|
579
|
+
{
|
|
580
|
+
style: {
|
|
581
|
+
height: "300px",
|
|
582
|
+
width: "100%",
|
|
583
|
+
display: "flex",
|
|
584
|
+
alignItems: "center",
|
|
585
|
+
justifyContent: "center"
|
|
586
|
+
},
|
|
587
|
+
children: "Running simulation..."
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (error) {
|
|
592
|
+
return /* @__PURE__ */ jsxs2(
|
|
593
|
+
"div",
|
|
594
|
+
{
|
|
595
|
+
style: {
|
|
596
|
+
height: "300px",
|
|
597
|
+
width: "100%",
|
|
598
|
+
display: "flex",
|
|
599
|
+
alignItems: "center",
|
|
600
|
+
justifyContent: "center",
|
|
601
|
+
color: "red"
|
|
602
|
+
},
|
|
603
|
+
children: [
|
|
604
|
+
"Error: ",
|
|
605
|
+
error
|
|
606
|
+
]
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (plotData.length === 0) {
|
|
611
|
+
return /* @__PURE__ */ jsx5(
|
|
612
|
+
"div",
|
|
613
|
+
{
|
|
614
|
+
style: {
|
|
615
|
+
height: "300px",
|
|
616
|
+
width: "100%",
|
|
617
|
+
display: "flex",
|
|
618
|
+
alignItems: "center",
|
|
619
|
+
justifyContent: "center"
|
|
620
|
+
},
|
|
621
|
+
children: "No data to plot. Check simulation output or SPICE netlist."
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const chartData = {
|
|
626
|
+
datasets: nodes.map((node, i) => ({
|
|
627
|
+
label: node,
|
|
628
|
+
data: plotData.map((p) => ({
|
|
629
|
+
x: Number(p.name),
|
|
630
|
+
y: p[node]
|
|
631
|
+
})),
|
|
632
|
+
borderColor: colors[i % colors.length],
|
|
633
|
+
backgroundColor: colors[i % colors.length],
|
|
634
|
+
fill: false,
|
|
635
|
+
tension: 0.1
|
|
636
|
+
}))
|
|
637
|
+
};
|
|
638
|
+
const options = {
|
|
639
|
+
responsive: true,
|
|
640
|
+
maintainAspectRatio: false,
|
|
641
|
+
plugins: {
|
|
642
|
+
legend: {
|
|
643
|
+
position: "top",
|
|
644
|
+
labels: {
|
|
645
|
+
font: {
|
|
646
|
+
family: "sans-serif"
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
title: {
|
|
651
|
+
display: false
|
|
652
|
+
},
|
|
653
|
+
tooltip: {
|
|
654
|
+
callbacks: {
|
|
655
|
+
title: (tooltipItems) => {
|
|
656
|
+
if (tooltipItems.length > 0) {
|
|
657
|
+
const item = tooltipItems[0];
|
|
658
|
+
return formatTimeWithUnits(item.parsed.x);
|
|
659
|
+
}
|
|
660
|
+
return "";
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
scales: {
|
|
666
|
+
x: {
|
|
667
|
+
type: "linear",
|
|
668
|
+
title: {
|
|
669
|
+
display: true,
|
|
670
|
+
text: "Time",
|
|
671
|
+
font: {
|
|
672
|
+
family: "sans-serif"
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
ticks: {
|
|
676
|
+
callback: (value) => formatTimeWithUnits(value),
|
|
677
|
+
font: {
|
|
678
|
+
family: "sans-serif"
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
y: {
|
|
683
|
+
title: {
|
|
684
|
+
display: true,
|
|
685
|
+
text: "Voltage",
|
|
686
|
+
font: {
|
|
687
|
+
family: "sans-serif"
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
ticks: {
|
|
691
|
+
font: {
|
|
692
|
+
family: "sans-serif"
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
return /* @__PURE__ */ jsx5("div", { style: { position: "relative", height: "300px", width: "100%" }, children: /* @__PURE__ */ jsx5(Line, { options, data: chartData }) });
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// lib/components/SpiceSimulationOverlay.tsx
|
|
702
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
703
|
+
var SpiceSimulationOverlay = ({
|
|
704
|
+
spiceString,
|
|
705
|
+
onClose,
|
|
706
|
+
plotData,
|
|
707
|
+
nodes,
|
|
708
|
+
isLoading,
|
|
709
|
+
error
|
|
710
|
+
}) => {
|
|
711
|
+
return /* @__PURE__ */ jsx6(
|
|
712
|
+
"div",
|
|
713
|
+
{
|
|
714
|
+
style: {
|
|
715
|
+
position: "fixed",
|
|
716
|
+
top: 0,
|
|
717
|
+
left: 0,
|
|
718
|
+
right: 0,
|
|
719
|
+
bottom: 0,
|
|
720
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
721
|
+
display: "flex",
|
|
722
|
+
alignItems: "center",
|
|
723
|
+
justifyContent: "center",
|
|
724
|
+
zIndex: 1002,
|
|
725
|
+
fontFamily: "sans-serif"
|
|
726
|
+
},
|
|
727
|
+
children: /* @__PURE__ */ jsxs3(
|
|
728
|
+
"div",
|
|
729
|
+
{
|
|
730
|
+
style: {
|
|
731
|
+
backgroundColor: "white",
|
|
732
|
+
padding: "24px",
|
|
733
|
+
borderRadius: "12px",
|
|
734
|
+
width: "90%",
|
|
735
|
+
maxWidth: "900px",
|
|
736
|
+
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.15)"
|
|
737
|
+
},
|
|
738
|
+
children: [
|
|
739
|
+
/* @__PURE__ */ jsxs3(
|
|
740
|
+
"div",
|
|
741
|
+
{
|
|
742
|
+
style: {
|
|
743
|
+
display: "flex",
|
|
744
|
+
justifyContent: "space-between",
|
|
745
|
+
alignItems: "center",
|
|
746
|
+
marginBottom: "24px",
|
|
747
|
+
borderBottom: "1px solid #eee",
|
|
748
|
+
paddingBottom: "16px"
|
|
749
|
+
},
|
|
750
|
+
children: [
|
|
751
|
+
/* @__PURE__ */ jsx6(
|
|
752
|
+
"h2",
|
|
753
|
+
{
|
|
754
|
+
style: {
|
|
755
|
+
margin: 0,
|
|
756
|
+
fontSize: "22px",
|
|
757
|
+
fontWeight: 600,
|
|
758
|
+
color: "#333"
|
|
759
|
+
},
|
|
760
|
+
children: "SPICE Simulation"
|
|
761
|
+
}
|
|
762
|
+
),
|
|
763
|
+
/* @__PURE__ */ jsx6(
|
|
764
|
+
"button",
|
|
765
|
+
{
|
|
766
|
+
onClick: onClose,
|
|
767
|
+
style: {
|
|
768
|
+
background: "none",
|
|
769
|
+
border: "none",
|
|
770
|
+
fontSize: "28px",
|
|
771
|
+
cursor: "pointer",
|
|
772
|
+
color: "#888",
|
|
773
|
+
padding: 0,
|
|
774
|
+
lineHeight: 1
|
|
775
|
+
},
|
|
776
|
+
children: "\xD7"
|
|
777
|
+
}
|
|
778
|
+
)
|
|
779
|
+
]
|
|
780
|
+
}
|
|
781
|
+
),
|
|
782
|
+
/* @__PURE__ */ jsx6("div", { children: /* @__PURE__ */ jsx6(
|
|
783
|
+
SpicePlot,
|
|
784
|
+
{
|
|
785
|
+
plotData,
|
|
786
|
+
nodes,
|
|
787
|
+
isLoading,
|
|
788
|
+
error
|
|
789
|
+
}
|
|
790
|
+
) }),
|
|
791
|
+
/* @__PURE__ */ jsxs3("div", { style: { marginTop: "24px" }, children: [
|
|
792
|
+
/* @__PURE__ */ jsx6(
|
|
793
|
+
"h3",
|
|
794
|
+
{
|
|
795
|
+
style: {
|
|
796
|
+
marginTop: 0,
|
|
797
|
+
marginBottom: "12px",
|
|
798
|
+
fontSize: "18px",
|
|
799
|
+
fontWeight: 600,
|
|
800
|
+
color: "#333"
|
|
801
|
+
},
|
|
802
|
+
children: "SPICE Netlist"
|
|
803
|
+
}
|
|
804
|
+
),
|
|
805
|
+
/* @__PURE__ */ jsx6(
|
|
806
|
+
"pre",
|
|
807
|
+
{
|
|
808
|
+
style: {
|
|
809
|
+
backgroundColor: "#fafafa",
|
|
810
|
+
padding: "16px",
|
|
811
|
+
borderRadius: "6px",
|
|
812
|
+
maxHeight: "150px",
|
|
813
|
+
overflowY: "auto",
|
|
814
|
+
border: "1px solid #eee",
|
|
815
|
+
color: "#333",
|
|
816
|
+
fontSize: "13px",
|
|
817
|
+
fontFamily: "monospace"
|
|
818
|
+
},
|
|
819
|
+
children: spiceString
|
|
820
|
+
}
|
|
821
|
+
)
|
|
822
|
+
] })
|
|
823
|
+
]
|
|
824
|
+
}
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
);
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// lib/hooks/useSpiceSimulation.ts
|
|
831
|
+
import { useState as useState3, useEffect as useEffect5 } from "react";
|
|
832
|
+
|
|
833
|
+
// lib/workers/spice-simulation.worker.blob.js
|
|
834
|
+
var b64 = "dmFyIGU9bnVsbCxzPWFzeW5jKCk9Pihhd2FpdCBpbXBvcnQoImh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9ucG0vZWVjaXJjdWl0LWVuZ2luZUAxLjUuMi8rZXNtIikpLlNpbXVsYXRpb24sYz1hc3luYygpPT57aWYoZSYmZS5pc0luaXRpYWxpemVkKCkpcmV0dXJuO2xldCBpPWF3YWl0IHMoKTtlPW5ldyBpLGF3YWl0IGUuc3RhcnQoKX07c2VsZi5vbm1lc3NhZ2U9YXN5bmMgaT0+e3RyeXtpZihhd2FpdCBjKCksIWUpdGhyb3cgbmV3IEVycm9yKCJTaW11bGF0aW9uIG5vdCBpbml0aWFsaXplZCIpO2xldCB0PWkuZGF0YS5zcGljZVN0cmluZyxhPXQubWF0Y2goL3dyZGF0YVxzKyhcUyspXHMrKC4qKS9pKTtpZihhKXtsZXQgbz1gLnByb2JlICR7YVsyXS50cmltKCkuc3BsaXQoL1xzKy8pLmpvaW4oIiAiKX1gO3Q9dC5yZXBsYWNlKC93cmRhdGEuKi9pLG8pfWVsc2UgaWYoIXQubWF0Y2goL1wucHJvYmUvaSkpdGhyb3cgdC5tYXRjaCgvcGxvdFxzKyguKikvaSk/bmV3IEVycm9yKCJUaGUgJ3Bsb3QnIGNvbW1hbmQgaXMgbm90IHN1cHBvcnRlZCBmb3IgZGF0YSBleHRyYWN0aW9uLiBQbGVhc2UgdXNlICd3cmRhdGEgPGZpbGVuYW1lPiA8dmFyMT4gLi4uJyBvciAnLnByb2JlIDx2YXIxPiAuLi4nIGluc3RlYWQuIik6bmV3IEVycm9yKCJObyAnLnByb2JlJyBvciAnd3JkYXRhJyBjb21tYW5kIGZvdW5kIGluIFNQSUNFIGZpbGUuIFVzZSAnd3JkYXRhIDxmaWxlbmFtZT4gPHZhcjE+IC4uLicgdG8gc3BlY2lmeSBvdXRwdXQuIik7ZS5zZXROZXRMaXN0KHQpO2xldCBuPWF3YWl0IGUucnVuU2ltKCk7c2VsZi5wb3N0TWVzc2FnZSh7dHlwZToicmVzdWx0IixyZXN1bHQ6bn0pfWNhdGNoKHQpe3NlbGYucG9zdE1lc3NhZ2Uoe3R5cGU6ImVycm9yIixlcnJvcjp0Lm1lc3NhZ2V9KX19Owo=";
|
|
835
|
+
var blobUrl = null;
|
|
836
|
+
var getSpiceSimulationWorkerBlobUrl = () => {
|
|
837
|
+
if (typeof window === "undefined") return null;
|
|
838
|
+
if (blobUrl) return blobUrl;
|
|
839
|
+
try {
|
|
840
|
+
const blob = new Blob([atob(b64)], { type: "application/javascript" });
|
|
841
|
+
blobUrl = URL.createObjectURL(blob);
|
|
842
|
+
return blobUrl;
|
|
843
|
+
} catch (e) {
|
|
844
|
+
console.error("Failed to create blob URL for worker", e);
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// lib/hooks/useSpiceSimulation.ts
|
|
850
|
+
var parseEecEngineOutput = (result) => {
|
|
851
|
+
const columnData = {};
|
|
852
|
+
if (result.dataType === "real") {
|
|
853
|
+
result.data.forEach((col) => {
|
|
854
|
+
columnData[col.name] = col.values;
|
|
855
|
+
});
|
|
856
|
+
} else if (result.dataType === "complex") {
|
|
857
|
+
result.data.forEach((col) => {
|
|
858
|
+
columnData[col.name] = col.values.map((v) => v.real);
|
|
859
|
+
});
|
|
860
|
+
} else {
|
|
861
|
+
throw new Error("Unsupported data type in simulation result");
|
|
862
|
+
}
|
|
863
|
+
const timeKey = Object.keys(columnData).find(
|
|
864
|
+
(k) => k.toLowerCase() === "time" || k.toLowerCase() === "frequency"
|
|
865
|
+
);
|
|
866
|
+
if (!timeKey) {
|
|
867
|
+
throw new Error("No time or frequency data in simulation result");
|
|
868
|
+
}
|
|
869
|
+
const timeValues = columnData[timeKey];
|
|
870
|
+
const probedVariables = Object.keys(columnData).filter((k) => k !== timeKey);
|
|
871
|
+
const plotableNodes = probedVariables.map(
|
|
872
|
+
(n) => n.replace(/v\(([^)]+)\)/i, "$1")
|
|
873
|
+
);
|
|
874
|
+
const plotData = timeValues.map((t, i) => {
|
|
875
|
+
const point = { name: t.toExponential(2) };
|
|
876
|
+
probedVariables.forEach((variable, j) => {
|
|
877
|
+
point[plotableNodes[j]] = columnData[variable][i];
|
|
878
|
+
});
|
|
879
|
+
return point;
|
|
880
|
+
});
|
|
881
|
+
return { plotData, nodes: plotableNodes };
|
|
882
|
+
};
|
|
883
|
+
var useSpiceSimulation = (spiceString) => {
|
|
884
|
+
const [plotData, setPlotData] = useState3([]);
|
|
885
|
+
const [nodes, setNodes] = useState3([]);
|
|
886
|
+
const [isLoading, setIsLoading] = useState3(true);
|
|
887
|
+
const [error, setError] = useState3(null);
|
|
888
|
+
useEffect5(() => {
|
|
889
|
+
if (!spiceString) {
|
|
890
|
+
setIsLoading(false);
|
|
891
|
+
setPlotData([]);
|
|
892
|
+
setNodes([]);
|
|
893
|
+
setError(null);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
setIsLoading(true);
|
|
897
|
+
setError(null);
|
|
898
|
+
setPlotData([]);
|
|
899
|
+
setNodes([]);
|
|
900
|
+
const workerUrl = getSpiceSimulationWorkerBlobUrl();
|
|
901
|
+
if (!workerUrl) {
|
|
902
|
+
setError("Could not create SPICE simulation worker.");
|
|
903
|
+
setIsLoading(false);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const worker = new Worker(workerUrl, { type: "module" });
|
|
907
|
+
worker.onmessage = (event) => {
|
|
908
|
+
if (event.data.type === "result") {
|
|
909
|
+
try {
|
|
910
|
+
const { plotData: parsedData, nodes: parsedNodes } = parseEecEngineOutput(event.data.result);
|
|
911
|
+
setPlotData(parsedData);
|
|
912
|
+
setNodes(parsedNodes);
|
|
913
|
+
} catch (e) {
|
|
914
|
+
setError(e.message || "Failed to parse simulation result");
|
|
915
|
+
console.error(e);
|
|
916
|
+
}
|
|
917
|
+
} else if (event.data.type === "error") {
|
|
918
|
+
setError(event.data.error);
|
|
919
|
+
}
|
|
920
|
+
setIsLoading(false);
|
|
921
|
+
};
|
|
922
|
+
worker.onerror = (err) => {
|
|
923
|
+
setError(err.message);
|
|
924
|
+
setIsLoading(false);
|
|
925
|
+
};
|
|
926
|
+
worker.postMessage({ spiceString });
|
|
927
|
+
return () => {
|
|
928
|
+
worker.terminate();
|
|
929
|
+
};
|
|
930
|
+
}, [spiceString]);
|
|
931
|
+
return { plotData, nodes, isLoading, error };
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// lib/utils/spice-utils.ts
|
|
935
|
+
import { circuitJsonToSpice } from "circuit-json-to-spice";
|
|
936
|
+
var getSpiceFromCircuitJson = (circuitJson) => {
|
|
937
|
+
const spiceNetlist = circuitJsonToSpice(circuitJson);
|
|
938
|
+
const baseSpiceString = spiceNetlist.toSpiceString();
|
|
939
|
+
const lines = baseSpiceString.split("\n").filter((l) => l.trim() !== "");
|
|
940
|
+
const componentLines = lines.filter(
|
|
941
|
+
(l) => !l.startsWith("*") && !l.startsWith(".") && l.trim() !== ""
|
|
942
|
+
);
|
|
943
|
+
const allNodes = /* @__PURE__ */ new Set();
|
|
944
|
+
const capacitorNodes = /* @__PURE__ */ new Set();
|
|
945
|
+
for (const line of componentLines) {
|
|
946
|
+
const parts = line.trim().split(/\s+/);
|
|
947
|
+
if (parts.length < 3) continue;
|
|
948
|
+
const componentType = parts[0][0].toUpperCase();
|
|
949
|
+
let nodesOnLine = [];
|
|
950
|
+
if (["R", "C", "L", "V", "I", "D"].includes(componentType)) {
|
|
951
|
+
nodesOnLine = parts.slice(1, 3);
|
|
952
|
+
} else if (componentType === "Q" && parts.length >= 4) {
|
|
953
|
+
nodesOnLine = parts.slice(1, 4);
|
|
954
|
+
} else if (componentType === "M" && parts.length >= 5) {
|
|
955
|
+
nodesOnLine = parts.slice(1, 5);
|
|
956
|
+
} else if (componentType === "X") {
|
|
957
|
+
nodesOnLine = parts.slice(1, -1);
|
|
958
|
+
} else {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
nodesOnLine.forEach((node) => allNodes.add(node));
|
|
962
|
+
if (componentType === "C") {
|
|
963
|
+
nodesOnLine.forEach((node) => capacitorNodes.add(node));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
allNodes.delete("0");
|
|
967
|
+
capacitorNodes.delete("0");
|
|
968
|
+
const icLines = Array.from(capacitorNodes).map((node) => `.ic V(${node})=0`);
|
|
969
|
+
const probeNodes = Array.from(allNodes).map((node) => `V(${node})`);
|
|
970
|
+
const probeLine = probeNodes.length > 0 ? `.probe ${probeNodes.join(" ")}` : "";
|
|
971
|
+
const tranLine = ".tran 0.1ms 50ms UIC";
|
|
972
|
+
const endStatement = ".end";
|
|
973
|
+
const originalLines = baseSpiceString.split("\n");
|
|
974
|
+
let endIndex = -1;
|
|
975
|
+
for (let i = originalLines.length - 1; i >= 0; i--) {
|
|
976
|
+
if (originalLines[i].trim().toLowerCase().startsWith(endStatement)) {
|
|
977
|
+
endIndex = i;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const injectionLines = [...icLines, probeLine, tranLine].filter(Boolean);
|
|
982
|
+
let finalLines;
|
|
983
|
+
if (endIndex !== -1) {
|
|
984
|
+
const beforeEnd = originalLines.slice(0, endIndex);
|
|
985
|
+
const endLineAndAfter = originalLines.slice(endIndex);
|
|
986
|
+
finalLines = [...beforeEnd, ...injectionLines, ...endLineAndAfter];
|
|
987
|
+
} else {
|
|
988
|
+
finalLines = [...originalLines, ...injectionLines, endStatement];
|
|
989
|
+
}
|
|
990
|
+
return finalLines.join("\n");
|
|
991
|
+
};
|
|
992
|
+
|
|
477
993
|
// lib/components/SchematicViewer.tsx
|
|
478
|
-
import { jsx as
|
|
994
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
479
995
|
var SchematicViewer = ({
|
|
480
996
|
circuitJson,
|
|
481
997
|
containerStyle,
|
|
@@ -486,14 +1002,38 @@ var SchematicViewer = ({
|
|
|
486
1002
|
editingEnabled = false,
|
|
487
1003
|
debug: debug3 = false,
|
|
488
1004
|
clickToInteractEnabled = false,
|
|
489
|
-
colorOverrides
|
|
1005
|
+
colorOverrides,
|
|
1006
|
+
spiceSimulationEnabled = false
|
|
490
1007
|
}) => {
|
|
491
1008
|
if (debug3) {
|
|
492
1009
|
enableDebug();
|
|
493
1010
|
}
|
|
494
|
-
const [
|
|
495
|
-
const
|
|
496
|
-
|
|
1011
|
+
const [showSpiceOverlay, setShowSpiceOverlay] = useState4(false);
|
|
1012
|
+
const getCircuitHash = (circuitJson2) => {
|
|
1013
|
+
return `${circuitJson2?.length || 0}_${circuitJson2?.editCount || 0}`;
|
|
1014
|
+
};
|
|
1015
|
+
const circuitJsonKey = useMemo(
|
|
1016
|
+
() => getCircuitHash(circuitJson),
|
|
1017
|
+
[circuitJson]
|
|
1018
|
+
);
|
|
1019
|
+
const spiceString = useMemo(() => {
|
|
1020
|
+
if (!spiceSimulationEnabled) return null;
|
|
1021
|
+
try {
|
|
1022
|
+
return getSpiceFromCircuitJson(circuitJson);
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
console.error("Failed to generate SPICE string", e);
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
}, [circuitJsonKey, spiceSimulationEnabled]);
|
|
1028
|
+
const {
|
|
1029
|
+
plotData,
|
|
1030
|
+
nodes,
|
|
1031
|
+
isLoading: isSpiceSimLoading,
|
|
1032
|
+
error: spiceSimError
|
|
1033
|
+
} = useSpiceSimulation(spiceString);
|
|
1034
|
+
const [editModeEnabled, setEditModeEnabled] = useState4(defaultEditMode);
|
|
1035
|
+
const [snapToGrid, setSnapToGrid] = useState4(true);
|
|
1036
|
+
const [isInteractionEnabled, setIsInteractionEnabled] = useState4(
|
|
497
1037
|
!clickToInteractEnabled
|
|
498
1038
|
);
|
|
499
1039
|
const svgDivRef = useRef4(null);
|
|
@@ -517,12 +1057,9 @@ var SchematicViewer = ({
|
|
|
517
1057
|
}
|
|
518
1058
|
touchStartRef.current = null;
|
|
519
1059
|
};
|
|
520
|
-
const [internalEditEvents, setInternalEditEvents] =
|
|
1060
|
+
const [internalEditEvents, setInternalEditEvents] = useState4([]);
|
|
521
1061
|
const circuitJsonRef = useRef4(circuitJson);
|
|
522
|
-
|
|
523
|
-
return `${circuitJson2?.length || 0}_${circuitJson2?.editCount || 0}`;
|
|
524
|
-
};
|
|
525
|
-
useEffect5(() => {
|
|
1062
|
+
useEffect6(() => {
|
|
526
1063
|
const circuitHash = getCircuitHash(circuitJson);
|
|
527
1064
|
const circuitHashRef = getCircuitHash(circuitJsonRef.current);
|
|
528
1065
|
if (circuitHash !== circuitHashRef) {
|
|
@@ -540,7 +1077,7 @@ var SchematicViewer = ({
|
|
|
540
1077
|
svgDivRef.current.style.transform = transformToString(transform);
|
|
541
1078
|
},
|
|
542
1079
|
// @ts-ignore disabled is a valid prop but not typed
|
|
543
|
-
enabled: isInteractionEnabled
|
|
1080
|
+
enabled: isInteractionEnabled && !showSpiceOverlay
|
|
544
1081
|
});
|
|
545
1082
|
const { containerWidth, containerHeight } = useResizeHandling(containerRef);
|
|
546
1083
|
const svgString = useMemo(() => {
|
|
@@ -590,7 +1127,7 @@ var SchematicViewer = ({
|
|
|
590
1127
|
svgToScreenProjection,
|
|
591
1128
|
circuitJson,
|
|
592
1129
|
editEvents: editEventsWithUnappliedEditEvents,
|
|
593
|
-
enabled: editModeEnabled && isInteractionEnabled,
|
|
1130
|
+
enabled: editModeEnabled && isInteractionEnabled && !showSpiceOverlay,
|
|
594
1131
|
snapToGrid
|
|
595
1132
|
}
|
|
596
1133
|
);
|
|
@@ -608,7 +1145,7 @@ var SchematicViewer = ({
|
|
|
608
1145
|
editEvents: editEventsWithUnappliedEditEvents
|
|
609
1146
|
});
|
|
610
1147
|
const svgDiv = useMemo(
|
|
611
|
-
() => /* @__PURE__ */
|
|
1148
|
+
() => /* @__PURE__ */ jsx7(
|
|
612
1149
|
"div",
|
|
613
1150
|
{
|
|
614
1151
|
ref: svgDivRef,
|
|
@@ -621,7 +1158,7 @@ var SchematicViewer = ({
|
|
|
621
1158
|
),
|
|
622
1159
|
[svgString, isInteractionEnabled, clickToInteractEnabled]
|
|
623
1160
|
);
|
|
624
|
-
return /* @__PURE__ */
|
|
1161
|
+
return /* @__PURE__ */ jsxs4(
|
|
625
1162
|
"div",
|
|
626
1163
|
{
|
|
627
1164
|
ref: containerRef,
|
|
@@ -629,10 +1166,15 @@ var SchematicViewer = ({
|
|
|
629
1166
|
position: "relative",
|
|
630
1167
|
backgroundColor: containerBackgroundColor,
|
|
631
1168
|
overflow: "hidden",
|
|
632
|
-
cursor: isDragging ? "grabbing" : clickToInteractEnabled && !isInteractionEnabled ? "pointer" : "grab",
|
|
1169
|
+
cursor: showSpiceOverlay ? "auto" : isDragging ? "grabbing" : clickToInteractEnabled && !isInteractionEnabled ? "pointer" : "grab",
|
|
633
1170
|
minHeight: "300px",
|
|
634
1171
|
...containerStyle
|
|
635
1172
|
},
|
|
1173
|
+
onWheelCapture: (e) => {
|
|
1174
|
+
if (showSpiceOverlay) {
|
|
1175
|
+
e.stopPropagation();
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
636
1178
|
onMouseDown: (e) => {
|
|
637
1179
|
if (clickToInteractEnabled && !isInteractionEnabled) {
|
|
638
1180
|
e.preventDefault();
|
|
@@ -648,10 +1190,16 @@ var SchematicViewer = ({
|
|
|
648
1190
|
return;
|
|
649
1191
|
}
|
|
650
1192
|
},
|
|
651
|
-
onTouchStart:
|
|
652
|
-
|
|
1193
|
+
onTouchStart: (e) => {
|
|
1194
|
+
if (showSpiceOverlay) return;
|
|
1195
|
+
handleTouchStart(e);
|
|
1196
|
+
},
|
|
1197
|
+
onTouchEnd: (e) => {
|
|
1198
|
+
if (showSpiceOverlay) return;
|
|
1199
|
+
handleTouchEnd(e);
|
|
1200
|
+
},
|
|
653
1201
|
children: [
|
|
654
|
-
!isInteractionEnabled && clickToInteractEnabled && /* @__PURE__ */
|
|
1202
|
+
!isInteractionEnabled && clickToInteractEnabled && /* @__PURE__ */ jsx7(
|
|
655
1203
|
"div",
|
|
656
1204
|
{
|
|
657
1205
|
onClick: (e) => {
|
|
@@ -670,7 +1218,7 @@ var SchematicViewer = ({
|
|
|
670
1218
|
pointerEvents: "all",
|
|
671
1219
|
touchAction: "pan-x pan-y pinch-zoom"
|
|
672
1220
|
},
|
|
673
|
-
children: /* @__PURE__ */
|
|
1221
|
+
children: /* @__PURE__ */ jsx7(
|
|
674
1222
|
"div",
|
|
675
1223
|
{
|
|
676
1224
|
style: {
|
|
@@ -687,20 +1235,32 @@ var SchematicViewer = ({
|
|
|
687
1235
|
)
|
|
688
1236
|
}
|
|
689
1237
|
),
|
|
690
|
-
editingEnabled && /* @__PURE__ */
|
|
1238
|
+
editingEnabled && /* @__PURE__ */ jsx7(
|
|
691
1239
|
EditIcon,
|
|
692
1240
|
{
|
|
693
1241
|
active: editModeEnabled,
|
|
694
1242
|
onClick: () => setEditModeEnabled(!editModeEnabled)
|
|
695
1243
|
}
|
|
696
1244
|
),
|
|
697
|
-
editingEnabled && editModeEnabled && /* @__PURE__ */
|
|
1245
|
+
editingEnabled && editModeEnabled && /* @__PURE__ */ jsx7(
|
|
698
1246
|
GridIcon,
|
|
699
1247
|
{
|
|
700
1248
|
active: snapToGrid,
|
|
701
1249
|
onClick: () => setSnapToGrid(!snapToGrid)
|
|
702
1250
|
}
|
|
703
1251
|
),
|
|
1252
|
+
spiceSimulationEnabled && /* @__PURE__ */ jsx7(SpiceSimulationIcon, { onClick: () => setShowSpiceOverlay(true) }),
|
|
1253
|
+
showSpiceOverlay && /* @__PURE__ */ jsx7(
|
|
1254
|
+
SpiceSimulationOverlay,
|
|
1255
|
+
{
|
|
1256
|
+
spiceString,
|
|
1257
|
+
onClose: () => setShowSpiceOverlay(false),
|
|
1258
|
+
plotData,
|
|
1259
|
+
nodes,
|
|
1260
|
+
isLoading: isSpiceSimLoading,
|
|
1261
|
+
error: spiceSimError
|
|
1262
|
+
}
|
|
1263
|
+
),
|
|
704
1264
|
svgDiv
|
|
705
1265
|
]
|
|
706
1266
|
}
|