From 96e046a97ea16463aee1d834f57fbf0b5f2621ad Mon Sep 17 00:00:00 2001 From: 28raining Date: Sat, 11 Apr 2026 14:32:38 -0700 Subject: [PATCH 1/4] User can change component names, allowing 2 components to have the same name --- src/App.jsx | 68 ++++++++++++--- src/ChoseTF.jsx | 35 +++++--- src/ComponentAdjuster.jsx | 61 ++++++++----- src/VisioJSSchematic.jsx | 176 +++++++++++++++++++++++--------------- src/circuitIds.js | 60 +++++++++++++ src/common.js | 2 +- src/initialSchematic.js | 6 ++ src/new_solveMNA.js | 40 +++++---- src/visiojs_to_matrix.js | 104 ++++++---------------- tests/circuit.test.js | 130 +++++++++++++++++++--------- toDo.md | 8 +- 11 files changed, 438 insertions(+), 252 deletions(-) create mode 100644 src/circuitIds.js diff --git a/src/App.jsx b/src/App.jsx index ca7f1fb..cdca301 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useRef, useCallback } from "react"; import "./App.css"; import "visiojs/dist/visiojs.css"; // Import VisioJS styles // import { startupSchematic } from "./startupSchematic.js"; @@ -8,7 +8,7 @@ import { VisioJSSchematic } from "./VisioJSSchematic.jsx"; import { ComponentAdjuster } from "./ComponentAdjuster.jsx"; import { FreqAdjusters } from "./FreqAdjusters.jsx"; // import Grid from "@mui/material/Grid"; -import { units, formatMathML } from "./common.js"; +import { units, formatMathML, addShapes } from "./common.js"; import { calcBilinear, new_calculate_tf } from "./new_solveMNA.js"; import { NavBar } from "./NavBar.jsx"; @@ -26,17 +26,17 @@ import Button from "@mui/material/Button"; import Grid from "@mui/material/Grid"; const initialComponents = { - L0: { + "11111111-1111-1111-1111-111111111101": { type: "inductor", value: 1, unit: "uH", }, - R0: { + "11111111-1111-1111-1111-111111111102": { type: "resistor", value: 10, unit: "KΩ", }, - C0: { + "11111111-1111-1111-1111-111111111103": { type: "capacitor", value: 10, unit: "fF", @@ -51,6 +51,7 @@ const initialSettings = { resolution: 100, }; import { initialSchematic } from "./initialSchematic.js"; +import { ensureCircuitIds } from "./circuitIds.js"; var urlContainsState = false; // Flag to check if URL contains state function stateFromURL() { @@ -61,7 +62,7 @@ function stateFromURL() { var modifiedComponents = initialComponents; var modifiedSettings = initialSettings; - var modifiedSchematic = initialSchematic; // Default to initial schematic if no URL param is + var modifiedSchematic = ensureCircuitIds(structuredClone(initialSchematic)); // Default to initial schematic if no URL param is if (componentsParam) { urlContainsState = true; // Set the flag if components are present in the URL @@ -87,7 +88,7 @@ function stateFromURL() { // Decode the Base64 string back into a Uint8Array const compressedBinary = Uint8Array.from(atob(decodeURIComponent(schematicParam)), (char) => char.charCodeAt(0)); const decompressed = pako.inflate(compressedBinary, { to: "string" }); // Decompress the data using pako - modifiedSchematic = JSON.parse(decompressed); // Parse the decompressed JSON string into an object + modifiedSchematic = ensureCircuitIds(JSON.parse(decompressed)); // Parse the decompressed JSON string into an object } return [modifiedComponents, modifiedSettings, modifiedSchematic]; } @@ -101,12 +102,16 @@ function App() { const [nodes, setNodes] = useState([]); const [fullyConnectedComponents, setFullyConnectedComponents] = useState({}); - const [results, setResults] = useState({ text: "", mathML: "", complexResponse: "", solver: null, probeName: "", drivers: [] }); + /** All placed shapes with connectors (incl. not yet wired to vin); used for display names on new parts. */ + const [schematicComponents, setSchematicComponents] = useState({}); + const [results, setResults] = useState({ text: "", mathML: "", complexResponse: "", solver: null, probeName: "", probeDisplayLabel: "", drivers: [] }); const [numericResults, setNumericResults] = useState({ numericML: "", numericText: "" }); const [bilinearResults, setBilinearResults] = useState({ bilinearML: "", bilinearText: "" }); const [componentValues, setComponentValues] = useState(modifiedComponents); const [settings, setSettings] = useState(modifiedSettings); const [schemHistory, setSchemHistory] = useState({ pointer: 0, state: [modifiedSchematic] }); + /** Bumped when labels are edited in the adjuster so visiojs always redraws the canvas. */ + const [schematicSyncNonce, setSchematicSyncNonce] = useState(0); const [urlSnackbar, setUrlSnackbar] = useState(false); const [errorSnackbar, setErrorSnackbar] = useState(false); const [unsolveSnackbar, setUnsolveSnackbar] = useState(false); @@ -192,7 +197,11 @@ function App() { } const componentValuesSolved = {}; - for (const key in componentValues) componentValuesSolved[key] = componentValues[key].value * units[componentValues[key].type][componentValues[key].unit]; + for (const id in componentValues) { + const sym = fullyConnectedComponents[id]?.sympySymbol; + if (!sym) continue; + componentValuesSolved[sym] = componentValues[id].value * units[componentValues[id].type][componentValues[id].unit]; + } const [freq_new, setFreqNew] = useState(null); const [mag_new, setMagNew] = useState(null); @@ -218,7 +227,11 @@ function App() { } const fRange = { fmin: settings.fmin * units.frequency[settings.fminUnit], fmax: settings.fmax * units.frequency[settings.fmaxUnit] }; const componentValuesSolved2 = {}; - for (const key in componentValues) componentValuesSolved2[key] = componentValues[key].value * units[componentValues[key].type][componentValues[key].unit]; + for (const id in componentValues) { + const sym = fullyConnectedComponents[id]?.sympySymbol; + if (!sym) continue; + componentValuesSolved2[sym] = componentValues[id].value * units[componentValues[id].type][componentValues[id].unit]; + } const { freq_new, mag_new, phase_new, numericML, numericText } = await new_calculate_tf( results.solver, fRange, @@ -230,7 +243,7 @@ function App() { setMagNew(mag_new); setPhaseNew(phase_new); if (numericML && numericText && results.probeName && results.drivers) { - const formattedNumericML = formatMathML(numericML, results.probeName, results.drivers); + const formattedNumericML = formatMathML(numericML, results.probeDisplayLabel || results.probeName, results.drivers); setNumericResults({ numericML: formattedNumericML, numericText: numericText }); } }; @@ -243,7 +256,27 @@ function App() { clearTimeout(debounceTimerRef.current); } }; - }, [results, settings, componentValues]); + }, [results, settings, componentValues, fullyConnectedComponents]); + + const handleDisplayNameChange = useCallback((circuitId, text) => { + setSchemHistory((h) => { + const i = h.pointer; + const newState = JSON.parse(JSON.stringify(h.state[i])); + const sh = newState.shapes.find((s) => s && s.circuitId === circuitId); + if (sh) { + if (!sh.label) { + const shapeType = sh.image?.split(".")[0]; + const template = shapeType ? addShapes[shapeType] : null; + if (template?.label) sh.label = JSON.parse(JSON.stringify(template.label)); + } + if (sh.label) sh.label.text = text; + } + const state = [...h.state]; + state[i] = newState; + return { ...h, state }; + }); + setSchematicSyncNonce((n) => n + 1); + }, []); function stateToURL() { const url = new URL(window.location.href); @@ -308,10 +341,18 @@ function App() { setHistory={setSchemHistory} setComponentValues={setComponentValues} setFullyConnectedComponents={setFullyConnectedComponents} + setSchematicComponents={setSchematicComponents} + schematicSyncNonce={schematicSyncNonce} />
- +
@@ -319,6 +360,7 @@ function App() { setResults={setResults} nodes={nodes} fullyConnectedComponents={fullyConnectedComponents} + schematicComponents={schematicComponents} componentValuesSolved={componentValuesSolved} setUnsolveSnackbar={setUnsolveSnackbar} /> diff --git a/src/ChoseTF.jsx b/src/ChoseTF.jsx index cf89da2..474a935 100644 --- a/src/ChoseTF.jsx +++ b/src/ChoseTF.jsx @@ -7,7 +7,20 @@ import CircularProgress from "@mui/material/CircularProgress"; import { initPyodideAndSympy } from "./pyodideLoader"; import { emptyResults, formatMathML } from "./common.js"; // Import the emptyResults object -export function ChoseTF({ setResults, nodes, fullyConnectedComponents, componentValuesSolved, setUnsolveSnackbar }) { +/** UUIDs contain "-"; differential probes must not use "-" as the pair delimiter. */ +export const PROBE_PAIR_DELIM = "::"; + +function probeDisplayLabel(p, fcc, schematicComponents) { + if (p.includes(PROBE_PAIR_DELIM)) { + const [a, b] = p.split(PROBE_PAIR_DELIM); + const la = fcc[a]?.displayName ?? schematicComponents?.[a]?.displayName ?? a; + const lb = fcc[b]?.displayName ?? schematicComponents?.[b]?.displayName ?? b; + return `${la}-${lb}`; + } + return fcc[p]?.displayName ?? schematicComponents?.[p]?.displayName ?? p; +} + +export function ChoseTF({ setResults, nodes, fullyConnectedComponents, schematicComponents, componentValuesSolved, setUnsolveSnackbar }) { const [loading, setLoading] = useState(false); const [calculating, setCalculating] = useState(false); const [loadedPyo, setLoadedPyo] = useState(null); @@ -43,17 +56,10 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component //if there's 2 vprobes, add P1-P0 and P0-P1 to the probes object const vprobes = probes.filter((p) => fullyConnectedComponents[p].type == "vprobe"); if (vprobes.length == 2) { - probes.push(`${vprobes[0]}-${vprobes[1]}`); - probes.push(`${vprobes[1]}-${vprobes[0]}`); + probes.push(`${vprobes[0]}${PROBE_PAIR_DELIM}${vprobes[1]}`); + probes.push(`${vprobes[1]}${PROBE_PAIR_DELIM}${vprobes[0]}`); } - // add a value field based on if user chose algebraic or numeric - const valueForAlgebra = {}; - for (const c in fullyConnectedComponents) { - if (c in componentValuesSolved) valueForAlgebra[c] = componentValuesSolved[c]; - // else valueForAlgebra[c] = c; - } - // console.log("componentValuesSolved", componentValuesSolved, fullyConnectedComponents, algebraic); return ( {drivers.length == 0 || probes.length == 0 ? ( @@ -67,7 +73,7 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component Calculate... {probes.map((p) => { - const int_probes = p.includes("-") ? p.split("-") : [p]; + const int_probes = p.includes(PROBE_PAIR_DELIM) ? p.split(PROBE_PAIR_DELIM) : [p]; return ( @@ -83,20 +89,21 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component setResults({ ...emptyResults }); // Reset results to empty //this console log is for collecting data for testing // console.log(nodes.length, int_probes, fullyConnectedComponents, valueForAlgebra, loadedPyo); - const [textResult, mathml] = await build_and_solve_mna(nodes.length, int_probes, fullyConnectedComponents, valueForAlgebra, loadedPyo); + const [textResult, mathml] = await build_and_solve_mna(nodes.length, int_probes, fullyConnectedComponents, componentValuesSolved, loadedPyo); if (textResult === "" && mathml === "") { setUnsolveSnackbar((x) => { if (!x) return true; else return x; }); } - const editedMathMl = formatMathML(mathml, p, drivers); + const editedMathMl = formatMathML(mathml, probeDisplayLabel(p, fullyConnectedComponents, schematicComponents), drivers); setResults({ text: textResult, mathML: editedMathMl, complexResponse: "", solver: loadedPyo, probeName: p, + probeDisplayLabel: probeDisplayLabel(p, fullyConnectedComponents, schematicComponents), drivers: drivers, }); setCalculating(false); @@ -110,7 +117,7 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component ) : ( - {p} + {probeDisplayLabel(p, fullyConnectedComponents, schematicComponents)} {drivers[0] == "vin" ? ( V diff --git a/src/ComponentAdjuster.jsx b/src/ComponentAdjuster.jsx index dbc6e90..d5df1d1 100644 --- a/src/ComponentAdjuster.jsx +++ b/src/ComponentAdjuster.jsx @@ -1,29 +1,49 @@ import Grid from "@mui/material/Grid"; import TextField from "@mui/material/TextField"; -import Typography from "@mui/material/Typography"; import Stack from "@mui/material/Stack"; import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; import FormControl from "@mui/material/FormControl"; import { units } from "./common"; -export function ComponentAdjuster({ componentValues, setComponentValues }) { - function handleValueChange(name, value) { - setComponentValues((prevValues) => ({ - ...prevValues, - [name]: { ...prevValues[name], value: value }, - })); +export function ComponentAdjuster({ componentValues, setComponentValues, fullyConnectedComponents, schematicComponents, onDisplayNameChange }) { + function handleValueChange(id, value) { + setComponentValues((prevValues) => { + const sym = fullyConnectedComponents[id]?.sympySymbol; + const t = prevValues[id]?.type; + const next = { ...prevValues, [id]: { ...prevValues[id], value } }; + if (sym && t) { + for (const k of Object.keys(next)) { + if (k !== id && fullyConnectedComponents[k]?.sympySymbol === sym && fullyConnectedComponents[k]?.type === t) { + next[k] = { ...next[k], value }; + } + } + } + return next; + }); } - function handleUnitChange(name, value) { - setComponentValues((prevValues) => ({ - ...prevValues, - [name]: { ...prevValues[name], unit: value }, - })); + function handleUnitChange(id, value) { + setComponentValues((prevValues) => { + const sym = fullyConnectedComponents[id]?.sympySymbol; + const t = prevValues[id]?.type; + const next = { ...prevValues, [id]: { ...prevValues[id], unit: value } }; + if (sym && t) { + for (const k of Object.keys(next)) { + if (k !== id && fullyConnectedComponents[k]?.sympySymbol === sym && fullyConnectedComponents[k]?.type === t) { + next[k] = { ...next[k], unit: value }; + } + } + } + return next; + }); + } + + function handleDisplayNameInput(id, text) { + onDisplayNameChange(id, text); } return ( @@ -31,18 +51,15 @@ export function ComponentAdjuster({ componentValues, setComponentValues }) { {Object.keys(componentValues).map((key) => ( - - - {key} - + handleValueChange(key, e.target.value)} - // fullWidth + onChange={(e) => handleDisplayNameInput(key, e.target.value)} /> + handleValueChange(key, e.target.value)} />
diff --git a/src/VisioJSSchematic.jsx b/src/VisioJSSchematic.jsx index 825759d..a95a66b 100644 --- a/src/VisioJSSchematic.jsx +++ b/src/VisioJSSchematic.jsx @@ -43,25 +43,6 @@ Object.keys(shapesWithLabels).forEach((key) => { initialLabels[key] = `${shapesWithLabels[key]}0`; }); -/** - * visiojs `su()` only creates the label when the shape group is new; for existing shapes - * it updates transform but never refreshes label text. Patch the SVG from schematic state. - */ -function syncVisioLabelsFromSchematicState(state) { - const root = document.querySelector("#visiojs_top"); - if (!root || !state?.shapes) return; - state.shapes.forEach((shape, index) => { - if (shape == null || !shape.label) return; - const g = root.querySelector(`#shape_${index}`); - const textEl = g?.querySelector("text.visiojs_label"); - if (textEl) { - textEl.textContent = shape.label.text ?? ""; - if (shape.label.x != null) textEl.setAttribute("x", String(shape.label.x)); - if (shape.label.y != null) textEl.setAttribute("y", String(shape.label.y)); - } - }); -} - function calculateNextIndex(components, type, prefix) { if (!components || Object.keys(components).length === 0) return initialLabels[type]; const sameType = Object.values(components).filter((c) => c.type === type); @@ -88,7 +69,6 @@ export function VisioJSSchematic({ setSchematicComponents, history, setHistory, - schematicSyncNonce = 0, }) { const [nextComponent, setNextComponent] = useState(initialLabels); const [vjs, setVjs] = useState(null); @@ -196,7 +176,6 @@ export function VisioJSSchematic({ ); const lastSynced = useRef({ p: -1, sig: "" }); - const lastSchematicSyncNonce = useRef(-1); /** visiojs.redraw uses internals that only exist after init(); effects run in order so init must run in the same effect before redraw. */ const vjsInitedRef = useRef(null); useEffect(() => { @@ -210,16 +189,12 @@ export function VisioJSSchematic({ } const st = history.state[history.pointer]; const sig = JSON.stringify(st); - const forcedFromAdjuster = schematicSyncNonce !== lastSchematicSyncNonce.current; - if (forcedFromAdjuster) lastSchematicSyncNonce.current = schematicSyncNonce; - if (!forcedFromAdjuster && lastSynced.current.p === history.pointer && lastSynced.current.sig === sig) return; + if (lastSynced.current.p === history.pointer && lastSynced.current.sig === sig) return; lastSynced.current = { p: history.pointer, sig }; const stForVjs = JSON.parse(JSON.stringify(st)); vjs.redraw(stForVjs); - syncVisioLabelsFromSchematicState(stForVjs); - requestAnimationFrame(() => syncVisioLabelsFromSchematicState(stForVjs)); regenerateNodeMaps(stForVjs); - }, [vjs, history.pointer, history.state, regenerateNodeMaps, schematicSyncNonce]); + }, [vjs, history.pointer, history.state, regenerateNodeMaps]); useEffect(() => { if (vjs) return;