@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.
- package/README.md +1 -2
- 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
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,531 @@ 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
|
+
let worker;
|
|
901
|
+
if (import.meta.env.DEV) {
|
|
902
|
+
worker = new Worker(
|
|
903
|
+
new URL("../workers/spice-simulation.worker.ts", import.meta.url),
|
|
904
|
+
{ type: "module" }
|
|
905
|
+
);
|
|
906
|
+
} else {
|
|
907
|
+
const workerUrl = getSpiceSimulationWorkerBlobUrl();
|
|
908
|
+
if (!workerUrl) {
|
|
909
|
+
setError("Could not create SPICE simulation worker.");
|
|
910
|
+
setIsLoading(false);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
worker = new Worker(workerUrl, { type: "module" });
|
|
914
|
+
}
|
|
915
|
+
worker.onmessage = (event) => {
|
|
916
|
+
if (event.data.type === "result") {
|
|
917
|
+
try {
|
|
918
|
+
const { plotData: parsedData, nodes: parsedNodes } = parseEecEngineOutput(event.data.result);
|
|
919
|
+
setPlotData(parsedData);
|
|
920
|
+
setNodes(parsedNodes);
|
|
921
|
+
} catch (e) {
|
|
922
|
+
setError(e.message || "Failed to parse simulation result");
|
|
923
|
+
console.error(e);
|
|
924
|
+
}
|
|
925
|
+
} else if (event.data.type === "error") {
|
|
926
|
+
setError(event.data.error);
|
|
927
|
+
}
|
|
928
|
+
setIsLoading(false);
|
|
929
|
+
};
|
|
930
|
+
worker.onerror = (err) => {
|
|
931
|
+
setError(err.message);
|
|
932
|
+
setIsLoading(false);
|
|
933
|
+
};
|
|
934
|
+
worker.postMessage({ spiceString });
|
|
935
|
+
return () => {
|
|
936
|
+
worker.terminate();
|
|
937
|
+
};
|
|
938
|
+
}, [spiceString]);
|
|
939
|
+
return { plotData, nodes, isLoading, error };
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// lib/utils/spice-utils.ts
|
|
943
|
+
import { circuitJsonToSpice } from "circuit-json-to-spice";
|
|
944
|
+
var getSpiceFromCircuitJson = (circuitJson) => {
|
|
945
|
+
const spiceNetlist = circuitJsonToSpice(circuitJson);
|
|
946
|
+
const baseSpiceString = spiceNetlist.toSpiceString();
|
|
947
|
+
const lines = baseSpiceString.split("\n").filter((l) => l.trim() !== "");
|
|
948
|
+
const componentLines = lines.filter(
|
|
949
|
+
(l) => !l.startsWith("*") && !l.startsWith(".") && l.trim() !== ""
|
|
950
|
+
);
|
|
951
|
+
const allNodes = /* @__PURE__ */ new Set();
|
|
952
|
+
const capacitorNodes = /* @__PURE__ */ new Set();
|
|
953
|
+
for (const line of componentLines) {
|
|
954
|
+
const parts = line.trim().split(/\s+/);
|
|
955
|
+
if (parts.length < 3) continue;
|
|
956
|
+
const componentType = parts[0][0].toUpperCase();
|
|
957
|
+
let nodesOnLine = [];
|
|
958
|
+
if (["R", "C", "L", "V", "I", "D"].includes(componentType)) {
|
|
959
|
+
nodesOnLine = parts.slice(1, 3);
|
|
960
|
+
} else if (componentType === "Q" && parts.length >= 4) {
|
|
961
|
+
nodesOnLine = parts.slice(1, 4);
|
|
962
|
+
} else if (componentType === "M" && parts.length >= 5) {
|
|
963
|
+
nodesOnLine = parts.slice(1, 5);
|
|
964
|
+
} else if (componentType === "X") {
|
|
965
|
+
nodesOnLine = parts.slice(1, -1);
|
|
966
|
+
} else {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
nodesOnLine.forEach((node) => allNodes.add(node));
|
|
970
|
+
if (componentType === "C") {
|
|
971
|
+
nodesOnLine.forEach((node) => capacitorNodes.add(node));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
allNodes.delete("0");
|
|
975
|
+
capacitorNodes.delete("0");
|
|
976
|
+
const icLines = Array.from(capacitorNodes).map((node) => `.ic V(${node})=0`);
|
|
977
|
+
const probeNodes = Array.from(allNodes).map((node) => `V(${node})`);
|
|
978
|
+
const probeLine = probeNodes.length > 0 ? `.probe ${probeNodes.join(" ")}` : "";
|
|
979
|
+
const tranLine = ".tran 0.1ms 50ms UIC";
|
|
980
|
+
const endStatement = ".end";
|
|
981
|
+
const originalLines = baseSpiceString.split("\n");
|
|
982
|
+
let endIndex = -1;
|
|
983
|
+
for (let i = originalLines.length - 1; i >= 0; i--) {
|
|
984
|
+
if (originalLines[i].trim().toLowerCase().startsWith(endStatement)) {
|
|
985
|
+
endIndex = i;
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const injectionLines = [...icLines, probeLine, tranLine].filter(Boolean);
|
|
990
|
+
let finalLines;
|
|
991
|
+
if (endIndex !== -1) {
|
|
992
|
+
const beforeEnd = originalLines.slice(0, endIndex);
|
|
993
|
+
const endLineAndAfter = originalLines.slice(endIndex);
|
|
994
|
+
finalLines = [...beforeEnd, ...injectionLines, ...endLineAndAfter];
|
|
995
|
+
} else {
|
|
996
|
+
finalLines = [...originalLines, ...injectionLines, endStatement];
|
|
997
|
+
}
|
|
998
|
+
return finalLines.join("\n");
|
|
999
|
+
};
|
|
1000
|
+
|
|
477
1001
|
// lib/components/SchematicViewer.tsx
|
|
478
|
-
import { jsx as
|
|
1002
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
479
1003
|
var SchematicViewer = ({
|
|
480
1004
|
circuitJson,
|
|
481
1005
|
containerStyle,
|
|
@@ -486,14 +1010,38 @@ var SchematicViewer = ({
|
|
|
486
1010
|
editingEnabled = false,
|
|
487
1011
|
debug: debug3 = false,
|
|
488
1012
|
clickToInteractEnabled = false,
|
|
489
|
-
colorOverrides
|
|
1013
|
+
colorOverrides,
|
|
1014
|
+
spiceSimulationEnabled = false
|
|
490
1015
|
}) => {
|
|
491
1016
|
if (debug3) {
|
|
492
1017
|
enableDebug();
|
|
493
1018
|
}
|
|
494
|
-
const [
|
|
495
|
-
const
|
|
496
|
-
|
|
1019
|
+
const [showSpiceOverlay, setShowSpiceOverlay] = useState4(false);
|
|
1020
|
+
const getCircuitHash = (circuitJson2) => {
|
|
1021
|
+
return `${circuitJson2?.length || 0}_${circuitJson2?.editCount || 0}`;
|
|
1022
|
+
};
|
|
1023
|
+
const circuitJsonKey = useMemo(
|
|
1024
|
+
() => getCircuitHash(circuitJson),
|
|
1025
|
+
[circuitJson]
|
|
1026
|
+
);
|
|
1027
|
+
const spiceString = useMemo(() => {
|
|
1028
|
+
if (!spiceSimulationEnabled) return null;
|
|
1029
|
+
try {
|
|
1030
|
+
return getSpiceFromCircuitJson(circuitJson);
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
console.error("Failed to generate SPICE string", e);
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
}, [circuitJsonKey, spiceSimulationEnabled]);
|
|
1036
|
+
const {
|
|
1037
|
+
plotData,
|
|
1038
|
+
nodes,
|
|
1039
|
+
isLoading: isSpiceSimLoading,
|
|
1040
|
+
error: spiceSimError
|
|
1041
|
+
} = useSpiceSimulation(spiceString);
|
|
1042
|
+
const [editModeEnabled, setEditModeEnabled] = useState4(defaultEditMode);
|
|
1043
|
+
const [snapToGrid, setSnapToGrid] = useState4(true);
|
|
1044
|
+
const [isInteractionEnabled, setIsInteractionEnabled] = useState4(
|
|
497
1045
|
!clickToInteractEnabled
|
|
498
1046
|
);
|
|
499
1047
|
const svgDivRef = useRef4(null);
|
|
@@ -517,12 +1065,9 @@ var SchematicViewer = ({
|
|
|
517
1065
|
}
|
|
518
1066
|
touchStartRef.current = null;
|
|
519
1067
|
};
|
|
520
|
-
const [internalEditEvents, setInternalEditEvents] =
|
|
1068
|
+
const [internalEditEvents, setInternalEditEvents] = useState4([]);
|
|
521
1069
|
const circuitJsonRef = useRef4(circuitJson);
|
|
522
|
-
|
|
523
|
-
return `${circuitJson2?.length || 0}_${circuitJson2?.editCount || 0}`;
|
|
524
|
-
};
|
|
525
|
-
useEffect5(() => {
|
|
1070
|
+
useEffect6(() => {
|
|
526
1071
|
const circuitHash = getCircuitHash(circuitJson);
|
|
527
1072
|
const circuitHashRef = getCircuitHash(circuitJsonRef.current);
|
|
528
1073
|
if (circuitHash !== circuitHashRef) {
|
|
@@ -540,7 +1085,7 @@ var SchematicViewer = ({
|
|
|
540
1085
|
svgDivRef.current.style.transform = transformToString(transform);
|
|
541
1086
|
},
|
|
542
1087
|
// @ts-ignore disabled is a valid prop but not typed
|
|
543
|
-
enabled: isInteractionEnabled
|
|
1088
|
+
enabled: isInteractionEnabled && !showSpiceOverlay
|
|
544
1089
|
});
|
|
545
1090
|
const { containerWidth, containerHeight } = useResizeHandling(containerRef);
|
|
546
1091
|
const svgString = useMemo(() => {
|
|
@@ -590,7 +1135,7 @@ var SchematicViewer = ({
|
|
|
590
1135
|
svgToScreenProjection,
|
|
591
1136
|
circuitJson,
|
|
592
1137
|
editEvents: editEventsWithUnappliedEditEvents,
|
|
593
|
-
enabled: editModeEnabled && isInteractionEnabled,
|
|
1138
|
+
enabled: editModeEnabled && isInteractionEnabled && !showSpiceOverlay,
|
|
594
1139
|
snapToGrid
|
|
595
1140
|
}
|
|
596
1141
|
);
|
|
@@ -608,7 +1153,7 @@ var SchematicViewer = ({
|
|
|
608
1153
|
editEvents: editEventsWithUnappliedEditEvents
|
|
609
1154
|
});
|
|
610
1155
|
const svgDiv = useMemo(
|
|
611
|
-
() => /* @__PURE__ */
|
|
1156
|
+
() => /* @__PURE__ */ jsx7(
|
|
612
1157
|
"div",
|
|
613
1158
|
{
|
|
614
1159
|
ref: svgDivRef,
|
|
@@ -621,7 +1166,7 @@ var SchematicViewer = ({
|
|
|
621
1166
|
),
|
|
622
1167
|
[svgString, isInteractionEnabled, clickToInteractEnabled]
|
|
623
1168
|
);
|
|
624
|
-
return /* @__PURE__ */
|
|
1169
|
+
return /* @__PURE__ */ jsxs4(
|
|
625
1170
|
"div",
|
|
626
1171
|
{
|
|
627
1172
|
ref: containerRef,
|
|
@@ -629,10 +1174,15 @@ var SchematicViewer = ({
|
|
|
629
1174
|
position: "relative",
|
|
630
1175
|
backgroundColor: containerBackgroundColor,
|
|
631
1176
|
overflow: "hidden",
|
|
632
|
-
cursor: isDragging ? "grabbing" : clickToInteractEnabled && !isInteractionEnabled ? "pointer" : "grab",
|
|
1177
|
+
cursor: showSpiceOverlay ? "auto" : isDragging ? "grabbing" : clickToInteractEnabled && !isInteractionEnabled ? "pointer" : "grab",
|
|
633
1178
|
minHeight: "300px",
|
|
634
1179
|
...containerStyle
|
|
635
1180
|
},
|
|
1181
|
+
onWheelCapture: (e) => {
|
|
1182
|
+
if (showSpiceOverlay) {
|
|
1183
|
+
e.stopPropagation();
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
636
1186
|
onMouseDown: (e) => {
|
|
637
1187
|
if (clickToInteractEnabled && !isInteractionEnabled) {
|
|
638
1188
|
e.preventDefault();
|
|
@@ -648,10 +1198,16 @@ var SchematicViewer = ({
|
|
|
648
1198
|
return;
|
|
649
1199
|
}
|
|
650
1200
|
},
|
|
651
|
-
onTouchStart:
|
|
652
|
-
|
|
1201
|
+
onTouchStart: (e) => {
|
|
1202
|
+
if (showSpiceOverlay) return;
|
|
1203
|
+
handleTouchStart(e);
|
|
1204
|
+
},
|
|
1205
|
+
onTouchEnd: (e) => {
|
|
1206
|
+
if (showSpiceOverlay) return;
|
|
1207
|
+
handleTouchEnd(e);
|
|
1208
|
+
},
|
|
653
1209
|
children: [
|
|
654
|
-
!isInteractionEnabled && clickToInteractEnabled && /* @__PURE__ */
|
|
1210
|
+
!isInteractionEnabled && clickToInteractEnabled && /* @__PURE__ */ jsx7(
|
|
655
1211
|
"div",
|
|
656
1212
|
{
|
|
657
1213
|
onClick: (e) => {
|
|
@@ -670,7 +1226,7 @@ var SchematicViewer = ({
|
|
|
670
1226
|
pointerEvents: "all",
|
|
671
1227
|
touchAction: "pan-x pan-y pinch-zoom"
|
|
672
1228
|
},
|
|
673
|
-
children: /* @__PURE__ */
|
|
1229
|
+
children: /* @__PURE__ */ jsx7(
|
|
674
1230
|
"div",
|
|
675
1231
|
{
|
|
676
1232
|
style: {
|
|
@@ -687,20 +1243,32 @@ var SchematicViewer = ({
|
|
|
687
1243
|
)
|
|
688
1244
|
}
|
|
689
1245
|
),
|
|
690
|
-
editingEnabled && /* @__PURE__ */
|
|
1246
|
+
editingEnabled && /* @__PURE__ */ jsx7(
|
|
691
1247
|
EditIcon,
|
|
692
1248
|
{
|
|
693
1249
|
active: editModeEnabled,
|
|
694
1250
|
onClick: () => setEditModeEnabled(!editModeEnabled)
|
|
695
1251
|
}
|
|
696
1252
|
),
|
|
697
|
-
editingEnabled && editModeEnabled && /* @__PURE__ */
|
|
1253
|
+
editingEnabled && editModeEnabled && /* @__PURE__ */ jsx7(
|
|
698
1254
|
GridIcon,
|
|
699
1255
|
{
|
|
700
1256
|
active: snapToGrid,
|
|
701
1257
|
onClick: () => setSnapToGrid(!snapToGrid)
|
|
702
1258
|
}
|
|
703
1259
|
),
|
|
1260
|
+
spiceSimulationEnabled && /* @__PURE__ */ jsx7(SpiceSimulationIcon, { onClick: () => setShowSpiceOverlay(true) }),
|
|
1261
|
+
showSpiceOverlay && /* @__PURE__ */ jsx7(
|
|
1262
|
+
SpiceSimulationOverlay,
|
|
1263
|
+
{
|
|
1264
|
+
spiceString,
|
|
1265
|
+
onClose: () => setShowSpiceOverlay(false),
|
|
1266
|
+
plotData,
|
|
1267
|
+
nodes,
|
|
1268
|
+
isLoading: isSpiceSimLoading,
|
|
1269
|
+
error: spiceSimError
|
|
1270
|
+
}
|
|
1271
|
+
),
|
|
704
1272
|
svgDiv
|
|
705
1273
|
]
|
|
706
1274
|
}
|