diff --git a/changelog/entries/2026-06-05-circuit-simulation.json b/changelog/entries/2026-06-05-circuit-simulation.json new file mode 100644 index 00000000..2668fe79 --- /dev/null +++ b/changelog/entries/2026-06-05-circuit-simulation.json @@ -0,0 +1,9 @@ +{ + "id": "2026-06-05-circuit-simulation", + "version": "0.9.4", + "date": "2026-06-05", + "category": "feat", + "title": "Live circuit simulation — circuits come alive", + "summary": "Hit Simulate and the schematic comes alive: wires colour by voltage, LEDs glow, and a DC motor's rotor spins — a transient MNA solver stepping every frame.", + "features": ["electronics", "simulation", "circuit"] +} diff --git a/crates/vcad-ecad-sim/src/circuit/devices.rs b/crates/vcad-ecad-sim/src/circuit/devices.rs new file mode 100644 index 00000000..83c53885 --- /dev/null +++ b/crates/vcad-ecad-sim/src/circuit/devices.rs @@ -0,0 +1,295 @@ +//! Lumped two-terminal devices and their MNA "stamps". +//! +//! Sign convention: each device has a `p` (positive) and `n` (negative) terminal, +//! and a device current defined as flowing **from `p` to `n` through the device**. +//! Node `0` is ground and never gets a matrix row/column. + +/// Thermal voltage at ~300 K (kT/q), in volts. +pub const VT: f64 = 0.025_852; + +/// Shockley diode model: `i = Is·(exp(v / (n·Vt)) − 1)`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct DiodeModel { + /// Saturation current Is (A). + pub is: f64, + /// Emission/ideality coefficient n. + pub n: f64, +} + +impl DiodeModel { + /// A generic small-signal silicon diode (Vf ≈ 0.65 V). + pub fn silicon() -> Self { + DiodeModel { is: 1e-14, n: 1.0 } + } + + /// A red LED (Vf ≈ 1.8 V at ~10 mA). Higher ideality + tiny Is push the knee up. + pub fn led() -> Self { + DiodeModel { is: 1e-18, n: 1.8 } + } + + /// Thermal voltage scaled by the ideality coefficient (n·Vt). + fn vte(&self) -> f64 { + self.n * VT + } + + /// Diode current at junction voltage `v` (clamped for numeric safety). + pub fn current(&self, v: f64) -> f64 { + let x = (v / self.vte()).min(60.0); + self.is * (x.exp() - 1.0) + } +} + +/// A brushed DC motor / gyrator: couples an electrical winding to a mechanical +/// rotor. The winding (R + L) carries the armature current; the back-EMF is +/// `Ke·ω` and the torque is `Kt·i`. SI units throughout. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MotorParams { + /// Winding resistance (Ω). + pub r: f64, + /// Winding inductance (H). + pub l: f64, + /// Back-EMF constant Ke (V·s/rad). + pub ke: f64, + /// Torque constant Kt (N·m/A). + pub kt: f64, + /// Rotor moment of inertia J (kg·m²). + pub j: f64, + /// Viscous friction b (N·m·s/rad). + pub b: f64, + /// External load torque opposing rotation (N·m). + pub load: f64, +} + +impl MotorParams { + /// A small hobby DC motor (~5 V, no-load ≈ 4700 RPM, stall ≈ 2.5 A at 5 V). + pub fn small_dc() -> Self { + MotorParams { + r: 2.0, + l: 0.5e-3, + ke: 0.01, + kt: 0.01, + j: 1e-5, + b: 1e-6, + load: 0.0, + } + } +} + +/// A circuit device connecting two nodes. In every variant `p`/`n` are the +/// positive/negative terminal node ids. +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(missing_docs)] // p/n are terminals; the remaining scalar is named per its unit. +pub enum Device { + /// Ideal resistor; `r` in Ω. + Resistor { p: usize, n: usize, r: f64 }, + /// Ideal capacitor; `c` in F. Backward-Euler companion model. + Capacitor { p: usize, n: usize, c: f64 }, + /// Ideal inductor; `l` in H. Backward-Euler companion model. + Inductor { p: usize, n: usize, l: f64 }, + /// Ideal independent voltage source; enforces `V(p) − V(n) = v`. + VSource { p: usize, n: usize, v: f64 }, + /// Ideal independent current source; pushes `i` A into `p`, out of `n`. + ISource { p: usize, n: usize, i: f64 }, + /// Nonlinear diode / LED (Shockley), solved by Newton-Raphson. + Diode { + p: usize, + n: usize, + model: DiodeModel, + }, + /// Brushed DC motor (electromechanical gyrator). Needs a branch current and + /// carries rotor state (updated post-solve). + Motor { + p: usize, + n: usize, + params: MotorParams, + }, +} + +/// Conductance stamp for a 2-terminal element between `p` and `n`. +fn stamp_conductance(a: &mut [f64], m: usize, p: usize, n: usize, g: f64) { + let mut add = |i: usize, j: usize, v: f64| { + if i != 0 && j != 0 { + a[(i - 1) * m + (j - 1)] += v; + } + }; + add(p, p, g); + add(p, n, -g); + add(n, p, -g); + add(n, n, g); +} + +/// Inject a current `i` into node `p` and out of node `n` (a current source). +fn inject(rhs: &mut [f64], p: usize, n: usize, i: f64) { + if p != 0 { + rhs[p - 1] += i; + } + if n != 0 { + rhs[n - 1] -= i; + } +} + +/// SPICE-style pn-junction limiting: damp a Newton step in junction voltage so +/// the exponential can't explode. `vnew` is this iteration's raw junction +/// voltage, `vold` the previous iteration's (limited) value. +fn pnjlim(vnew: f64, vold: f64, vte: f64, vcrit: f64) -> f64 { + if vnew > vcrit && (vnew - vold).abs() > 2.0 * vte { + if vold > 0.0 { + let arg = 1.0 + (vnew - vold) / vte; + if arg > 0.0 { + vold + vte * arg.ln() + } else { + vcrit + } + } else if vnew > 0.0 { + vte * (vnew / vte).ln() + } else { + vnew + } + } else { + vnew + } +} + +impl Device { + /// Whether this device needs its own MNA branch-current unknown. + pub fn needs_branch(&self) -> bool { + matches!(self, Device::VSource { .. } | Device::Motor { .. }) + } + + /// Stamp this device's contribution into the MNA matrix `a` and RHS `rhs`. + /// + /// - `m` is the system dimension, `nn` the node count (incl. ground). + /// - `branch` is a running branch-index counter; branch devices consume one. + /// - `cap_v` / `ind_i` are this device's companion history; `nl_prev` is its + /// previous-iteration nonlinear junction voltage (for Newton limiting). + /// - `guess` is the current Newton node-voltage estimate. + /// + /// Returns `Some(v)` with the new limited junction voltage for nonlinear + /// devices (so the caller can carry it into the next iteration), else `None`. + #[allow(clippy::too_many_arguments)] + pub fn stamp( + &self, + a: &mut [f64], + rhs: &mut [f64], + m: usize, + nn: usize, + branch: &mut usize, + dt: f64, + cap_v: f64, + ind_i: f64, + nl_prev: f64, + omega: f64, + guess: &[f64], + ) -> Option { + match *self { + Device::Resistor { p, n, r } => { + stamp_conductance(a, m, p, n, 1.0 / r); + None + } + Device::Capacitor { p, n, c } => { + let gc = c / dt; + stamp_conductance(a, m, p, n, gc); + // companion current source i_eq = gc·v_prev, injected into p + inject(rhs, p, n, gc * cap_v); + None + } + Device::Inductor { p, n, l } => { + let geq = dt / l; + stamp_conductance(a, m, p, n, geq); + // companion: i_L = geq·v + i_prev; the i_prev term leaves p + inject(rhs, p, n, -ind_i); + None + } + Device::VSource { p, n, v } => { + let br = (nn - 1) + *branch; + *branch += 1; + if p != 0 { + a[(p - 1) * m + br] += 1.0; + a[br * m + (p - 1)] += 1.0; + } + if n != 0 { + a[(n - 1) * m + br] -= 1.0; + a[br * m + (n - 1)] -= 1.0; + } + rhs[br] += v; + None + } + Device::ISource { p, n, i } => { + inject(rhs, p, n, i); + None + } + Device::Motor { p, n, params } => { + // Electrically a branch: v(p) − v(n) = i·Z + E, with winding + // impedance Z = R + L/dt and EMF E = Ke·ω_prev − (L/dt)·i_prev + // (back-EMF from the previous rotor speed + inductor history). + let z = params.r + params.l / dt; + let e = params.ke * omega - (params.l / dt) * ind_i; + let br = (nn - 1) + *branch; + *branch += 1; + if p != 0 { + a[(p - 1) * m + br] += 1.0; + a[br * m + (p - 1)] += 1.0; + } + if n != 0 { + a[(n - 1) * m + br] -= 1.0; + a[br * m + (n - 1)] -= 1.0; + } + a[br * m + br] -= z; + rhs[br] += e; + None + } + Device::Diode { p, n, model } => { + let vte = model.vte(); + let vcrit = vte * (vte / (std::f64::consts::SQRT_2 * model.is)).ln(); + let vd_raw = guess[p] - guess[n]; + let vd = pnjlim(vd_raw, nl_prev, vte, vcrit); + let ev = (vd / vte).min(60.0).exp(); + let id = model.is * (ev - 1.0); + let geq = (model.is / vte) * ev; // di/dv + let ieq = id - geq * vd; // companion (Norton) current + stamp_conductance(a, m, p, n, geq); + inject(rhs, p, n, -ieq); + Some(vd) + } + } + } + + /// Device current (A, `p`→`n`) computed directly from node voltages. Only + /// meaningful for memoryless, non-branch devices (R, I, diode); reactive and + /// branch devices report their current through other channels. + pub fn current(&self, node_v: &[f64]) -> f64 { + match *self { + Device::Resistor { p, n, r } => (node_v[p] - node_v[n]) / r, + Device::ISource { i, .. } => i, + Device::Diode { p, n, model } => model.current(node_v[p] - node_v[n]), + _ => 0.0, + } + } + + /// This device's primary scalar (resistance, source value, …). Diodes have + /// no single driven scalar, so they report 0. + pub fn primary(&self) -> f64 { + match *self { + Device::Resistor { r, .. } => r, + Device::Capacitor { c, .. } => c, + Device::Inductor { l, .. } => l, + Device::VSource { v, .. } => v, + Device::ISource { i, .. } => i, + Device::Diode { .. } => 0.0, + Device::Motor { params, .. } => params.load, + } + } + + /// Set this device's primary scalar, for live driving (switch, PWM, scrub). + pub fn set_primary(&mut self, value: f64) { + match self { + Device::Resistor { r, .. } => *r = value, + Device::Capacitor { c, .. } => *c = value, + Device::Inductor { l, .. } => *l = value, + Device::VSource { v, .. } => *v = value, + Device::ISource { i, .. } => *i = value, + Device::Diode { .. } => {} + Device::Motor { params, .. } => params.load = value, + } + } +} diff --git a/crates/vcad-ecad-sim/src/circuit/linalg.rs b/crates/vcad-ecad-sim/src/circuit/linalg.rs new file mode 100644 index 00000000..457b0da6 --- /dev/null +++ b/crates/vcad-ecad-sim/src/circuit/linalg.rs @@ -0,0 +1,118 @@ +//! Tiny dense linear solver (Gaussian elimination with partial pivoting). +//! +//! Circuit MNA systems are small (tens of unknowns for typical boards), so a +//! dense `O(n³)` solve is plenty and keeps the crate dependency-free and +//! WASM-friendly. If we ever need large sparse systems, this is the seam to +//! swap in a sparse factorization. + +/// Solve `a · x = b` in place via Gaussian elimination with partial pivoting. +/// +/// `a` is a row-major `n×n` matrix, `b` is length `n`. Both are consumed +/// (overwritten). Returns the solution `x`, or `None` if the system is singular +/// (a pivot falls below a small epsilon). +pub fn solve_dense(a: &mut [f64], b: &mut [f64], n: usize) -> Option> { + debug_assert_eq!(a.len(), n * n); + debug_assert_eq!(b.len(), n); + if n == 0 { + return Some(Vec::new()); + } + + for col in 0..n { + // Partial pivot: largest-magnitude entry in this column, at/below the diagonal. + let mut pivot_row = col; + let mut pivot_mag = a[col * n + col].abs(); + for r in (col + 1)..n { + let mag = a[r * n + col].abs(); + if mag > pivot_mag { + pivot_mag = mag; + pivot_row = r; + } + } + if pivot_mag < 1e-14 { + return None; // singular / structurally disconnected + } + if pivot_row != col { + for c in 0..n { + a.swap(pivot_row * n + c, col * n + c); + } + b.swap(pivot_row, col); + } + + // Eliminate entries below the pivot. + let pivot = a[col * n + col]; + for r in (col + 1)..n { + let factor = a[r * n + col] / pivot; + if factor != 0.0 { + for c in col..n { + a[r * n + c] -= factor * a[col * n + c]; + } + b[r] -= factor * b[col]; + } + } + } + + // Back-substitution. + let mut x = vec![0.0; n]; + for r in (0..n).rev() { + let mut sum = b[r]; + for c in (r + 1)..n { + sum -= a[r * n + c] * x[c]; + } + x[r] = sum / a[r * n + r]; + } + Some(x) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn solves_identity() { + let mut a = vec![1.0, 0.0, 0.0, 1.0]; + let mut b = vec![3.0, 4.0]; + let x = solve_dense(&mut a, &mut b, 2).unwrap(); + assert!((x[0] - 3.0).abs() < 1e-12); + assert!((x[1] - 4.0).abs() < 1e-12); + } + + #[test] + fn solves_2x2() { + // [2 1; 1 3] x = [5; 10] -> x = [1; 3] + let mut a = vec![2.0, 1.0, 1.0, 3.0]; + let mut b = vec![5.0, 10.0]; + let x = solve_dense(&mut a, &mut b, 2).unwrap(); + assert!((x[0] - 1.0).abs() < 1e-9, "x0 = {}", x[0]); + assert!((x[1] - 3.0).abs() < 1e-9, "x1 = {}", x[1]); + } + + #[test] + fn needs_pivoting() { + // Zero pivot in the first position forces a row swap. + // [0 1; 1 0] x = [2; 3] -> x = [3; 2] + let mut a = vec![0.0, 1.0, 1.0, 0.0]; + let mut b = vec![2.0, 3.0]; + let x = solve_dense(&mut a, &mut b, 2).unwrap(); + assert!((x[0] - 3.0).abs() < 1e-9); + assert!((x[1] - 2.0).abs() < 1e-9); + } + + #[test] + fn singular_returns_none() { + // Rank-deficient. + let mut a = vec![1.0, 2.0, 2.0, 4.0]; + let mut b = vec![1.0, 2.0]; + assert!(solve_dense(&mut a, &mut b, 2).is_none()); + } + + #[test] + fn solves_3x3() { + // [1 1 1; 0 2 5; 2 5 -1] x = [6; -4; 27] -> x = [5; 3; -2] + let mut a = vec![1.0, 1.0, 1.0, 0.0, 2.0, 5.0, 2.0, 5.0, -1.0]; + let mut b = vec![6.0, -4.0, 27.0]; + let x = solve_dense(&mut a, &mut b, 3).unwrap(); + assert!((x[0] - 5.0).abs() < 1e-9, "x0={}", x[0]); + assert!((x[1] - 3.0).abs() < 1e-9, "x1={}", x[1]); + assert!((x[2] + 2.0).abs() < 1e-9, "x2={}", x[2]); + } +} diff --git a/crates/vcad-ecad-sim/src/circuit/mod.rs b/crates/vcad-ecad-sim/src/circuit/mod.rs new file mode 100644 index 00000000..77c0c0fa --- /dev/null +++ b/crates/vcad-ecad-sim/src/circuit/mod.rs @@ -0,0 +1,308 @@ +//! Lumped-element circuit simulation — a generalized network solver. +//! +//! This is the SPICE-style, time-stepping counterpart to the field solvers +//! elsewhere in the stack. Each device "stamps" its contribution into a +//! Modified Nodal Analysis (MNA) system; reactive elements (C, L) use +//! backward-Euler **companion models** (a conductance in parallel with a history +//! current source) so a transient step is just "re-stamp with updated history, +//! solve". The result is node voltages + branch currents at each tick. +//! +//! The public surface mirrors the physics engine's gym/env: build a [`Circuit`], +//! wrap it in a [`CircuitEnv`], then `reset()` / `step()` / `observe()`. +//! +//! ``` +//! use vcad_ecad_sim::circuit::{Circuit, CircuitEnv, Device}; +//! // 5 V battery charging a 1 µF cap through a 1 kΩ resistor. +//! let mut c = Circuit::new(); +//! let vin = c.node(); +//! let mid = c.node(); +//! c.add(Device::VSource { p: vin, n: 0, v: 5.0 }); +//! c.add(Device::Resistor { p: vin, n: mid, r: 1_000.0 }); +//! let cap = c.add(Device::Capacitor { p: mid, n: 0, c: 1e-6 }); +//! let mut env = CircuitEnv::new(c, 1e-5); +//! env.reset(); +//! for _ in 0..1000 { env.step(); } // ~10 ms ≈ 10 τ +//! assert!(env.observe().node_voltages[mid] > 4.9); // cap fully charged +//! let _ = cap; +//! ``` + +mod linalg; +pub use linalg::solve_dense; + +mod devices; +pub use devices::{Device, DiodeModel, MotorParams}; + +/// A lumped-element circuit: a set of [`Device`]s connecting numbered nodes. +/// +/// Node `0` is always ground (the voltage reference). Allocate other nodes with +/// [`Circuit::node`]. +#[derive(Debug, Clone, Default)] +pub struct Circuit { + /// Number of nodes including ground (node 0). Starts at 1. + pub num_nodes: usize, + /// Devices in insertion order; the index is the device id. + pub devices: Vec, +} + +impl Circuit { + /// A fresh circuit containing only ground (node 0). + pub fn new() -> Self { + Circuit { + num_nodes: 1, + devices: Vec::new(), + } + } + + /// Allocate a new (non-ground) node and return its id. + pub fn node(&mut self) -> usize { + let id = self.num_nodes; + self.num_nodes += 1; + id + } + + /// Add a device, returning its id (its index in [`Circuit::devices`]). + pub fn add(&mut self, device: Device) -> usize { + self.devices.push(device); + self.devices.len() - 1 + } + + /// Number of voltage-source-like devices (each needs an MNA branch current). + fn num_branches(&self) -> usize { + self.devices.iter().filter(|d| d.needs_branch()).count() + } +} + +/// A snapshot of the circuit state after a step. +#[derive(Debug, Clone, Default)] +pub struct Observation { + /// Simulated time (s). + pub time: f64, + /// Voltage at each node, indexed by node id. `node_voltages[0]` is 0 (ground). + pub node_voltages: Vec, + /// Current through each device (A), indexed by device id. Positive flows + /// from the device's `p` terminal to its `n` terminal. + pub device_currents: Vec, + /// Rotor angle (rad) per device id; 0 for non-motors. Drives 3D spin. + pub rotor_angles: Vec, + /// Rotor angular velocity (rad/s) per device id; 0 for non-motors. + pub rotor_speeds: Vec, +} + +/// A steppable circuit simulation. Mirrors the physics engine's env: `reset`, +/// `step`, `observe`. +#[derive(Debug, Clone)] +pub struct CircuitEnv { + circuit: Circuit, + dt: f64, + time: f64, + /// Per-device companion history: previous capacitor voltage (device id → V). + cap_v: Vec, + /// Per-device companion history: previous inductor current (device id → A). + ind_i: Vec, + /// Per-device nonlinear junction voltage (device id → V), warm-started across + /// steps and limited across Newton iterations. + nl_state: Vec, + /// Per-device rotor angular velocity (device id → rad/s) for motors. + mech_omega: Vec, + /// Per-device rotor angle (device id → rad) for motors. + mech_theta: Vec, + /// Latest node voltages (length `num_nodes`, index 0 = ground = 0). + node_v: Vec, + /// Latest device currents (length `devices.len()`). + dev_i: Vec, + /// Newton iteration cap for nonlinear devices. + max_newton: usize, +} + +impl CircuitEnv { + /// Build an env around a circuit with a fixed timestep `dt` (seconds). + pub fn new(circuit: Circuit, dt: f64) -> Self { + let nd = circuit.devices.len(); + let nn = circuit.num_nodes; + CircuitEnv { + circuit, + dt, + time: 0.0, + cap_v: vec![0.0; nd], + ind_i: vec![0.0; nd], + nl_state: vec![0.0; nd], + mech_omega: vec![0.0; nd], + mech_theta: vec![0.0; nd], + node_v: vec![0.0; nn], + dev_i: vec![0.0; nd], + max_newton: 50, + } + } + + /// Reset to the power-on state: t = 0, capacitors discharged, inductors with + /// no current, all nodes at 0 V. + pub fn reset(&mut self) { + self.time = 0.0; + for v in &mut self.cap_v { + *v = 0.0; + } + for i in &mut self.ind_i { + *i = 0.0; + } + for v in &mut self.nl_state { + *v = 0.0; + } + for v in &mut self.mech_omega { + *v = 0.0; + } + for v in &mut self.mech_theta { + *v = 0.0; + } + for v in &mut self.node_v { + *v = 0.0; + } + for i in &mut self.dev_i { + *i = 0.0; + } + } + + /// The configured timestep (s). + pub fn dt(&self) -> f64 { + self.dt + } + + /// Change the timestep (s). + pub fn set_dt(&mut self, dt: f64) { + self.dt = dt; + } + + /// Mutate a device's primary scalar (resistance, source value, …). Lets a + /// caller drive the circuit live — a switch, a PWM source, a scrubbed value. + pub fn set_value(&mut self, device_id: usize, value: f64) { + if let Some(d) = self.circuit.devices.get_mut(device_id) { + d.set_primary(value); + } + } + + /// Read a device's primary scalar. + pub fn value(&self, device_id: usize) -> Option { + self.circuit.devices.get(device_id).map(|d| d.primary()) + } + + /// Advance the simulation by one timestep and return the new observation. + pub fn step(&mut self) -> Observation { + let nn = self.circuit.num_nodes; + let nb = self.circuit.num_branches(); + let m = (nn - 1) + nb; + + // Newton-Raphson outer loop: nonlinear devices linearise around the + // current node-voltage guess each iteration. Linear circuits converge in + // a single pass. + let mut guess = self.node_v.clone(); + for _ in 0..self.max_newton.max(1) { + let mut a = vec![0.0f64; m * m]; + let mut rhs = vec![0.0f64; m]; + let mut branch = 0usize; + + // Device is Copy, so reading it out releases the borrow on `circuit` + // and lets us update `nl_state[id]` in the same pass. + for id in 0..self.circuit.devices.len() { + let dev = self.circuit.devices[id]; + if let Some(vd) = dev.stamp( + &mut a, + &mut rhs, + m, + nn, + &mut branch, + self.dt, + self.cap_v[id], + self.ind_i[id], + self.nl_state[id], + self.mech_omega[id], + &guess, + ) { + self.nl_state[id] = vd; + } + } + + let solution = match solve_dense(&mut a, &mut rhs, m) { + Some(x) => x, + None => break, // singular — keep previous guess + }; + + let mut next = vec![0.0; nn]; + next[1..nn].copy_from_slice(&solution[..(nn - 1)]); + + // Convergence: largest node-voltage change between Newton iterations. + let mut delta = 0.0f64; + for node in 1..nn { + delta = delta.max((next[node] - guess[node]).abs()); + } + guess = next; + + // Stash branch currents for voltage-source-like devices. + let mut b = 0usize; + for (id, dev) in self.circuit.devices.iter().enumerate() { + if dev.needs_branch() { + self.dev_i[id] = solution[(nn - 1) + b]; + b += 1; + } + } + + if delta < 1e-9 { + break; + } + } + + self.node_v = guess; + + // Update companion history + record device currents. + for (id, dev) in self.circuit.devices.iter().enumerate() { + match *dev { + Device::Capacitor { p, n, c } => { + let v_new = self.node_v[p] - self.node_v[n]; + let gc = c / self.dt; + // companion current = gc·(v_new − v_prev) = C·dv/dt + self.dev_i[id] = gc * (v_new - self.cap_v[id]); + self.cap_v[id] = v_new; + } + Device::Inductor { p, n, l } => { + let v = self.node_v[p] - self.node_v[n]; + let geq = self.dt / l; + let i_new = geq * v + self.ind_i[id]; + self.dev_i[id] = i_new; + self.ind_i[id] = i_new; + } + Device::Motor { params, .. } => { + // Armature current solved as the branch current (already in + // dev_i via the branch stash). Advance the rotor (semi-implicit + // Euler) and the inductor-history current. + let i_m = self.dev_i[id]; + let omega_prev = self.mech_omega[id]; + let torque = params.kt * i_m - params.b * omega_prev - params.load; + let omega_new = omega_prev + (self.dt / params.j) * torque; + self.mech_theta[id] += self.dt * omega_new; + self.mech_omega[id] = omega_new; + self.ind_i[id] = i_m; + } + _ => { + if !dev.needs_branch() { + self.dev_i[id] = dev.current(&self.node_v); + } + } + } + } + + self.time += self.dt; + self.observe() + } + + /// The current state without advancing time. + pub fn observe(&self) -> Observation { + Observation { + time: self.time, + node_voltages: self.node_v.clone(), + device_currents: self.dev_i.clone(), + rotor_angles: self.mech_theta.clone(), + rotor_speeds: self.mech_omega.clone(), + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/vcad-ecad-sim/src/circuit/tests.rs b/crates/vcad-ecad-sim/src/circuit/tests.rs new file mode 100644 index 00000000..1b90da52 --- /dev/null +++ b/crates/vcad-ecad-sim/src/circuit/tests.rs @@ -0,0 +1,369 @@ +//! Golden tests for the transient solver (linear + nonlinear), validated against +//! the analytic behaviour of textbook circuits. + +use super::{Circuit, CircuitEnv, Device, DiodeModel, MotorParams}; + +#[test] +fn resistive_voltage_divider() { + // 5 V across two equal 1 kΩ resistors → midpoint sits at 2.5 V. + let mut c = Circuit::new(); + let vin = c.node(); + let mid = c.node(); + c.add(Device::VSource { + p: vin, + n: 0, + v: 5.0, + }); + c.add(Device::Resistor { + p: vin, + n: mid, + r: 1_000.0, + }); + c.add(Device::Resistor { + p: mid, + n: 0, + r: 1_000.0, + }); + + let mut env = CircuitEnv::new(c, 1e-6); + env.reset(); + let obs = env.step(); // purely resistive → exact in one step + assert!((obs.node_voltages[vin] - 5.0).abs() < 1e-9); + assert!( + (obs.node_voltages[mid] - 2.5).abs() < 1e-6, + "vmid = {}", + obs.node_voltages[mid] + ); +} + +#[test] +fn current_source_through_resistor() { + // 1 mA forced through 1 kΩ → 1 V. + let mut c = Circuit::new(); + let a = c.node(); + c.add(Device::ISource { + p: a, + n: 0, + i: 1e-3, + }); + c.add(Device::Resistor { + p: a, + n: 0, + r: 1_000.0, + }); + + let mut env = CircuitEnv::new(c, 1e-6); + env.reset(); + let obs = env.step(); + assert!( + (obs.node_voltages[a] - 1.0).abs() < 1e-6, + "va = {}", + obs.node_voltages[a] + ); +} + +#[test] +fn rc_charging_matches_analytic() { + // 5 V into R=1 kΩ, C=1 µF → τ = 1 ms. Check 63.2% at τ and ~99% at 5τ. + let v = 5.0; + let r = 1_000.0; + let cap = 1e-6; + let tau = r * cap; + + let mut c = Circuit::new(); + let vin = c.node(); + let mid = c.node(); + c.add(Device::VSource { p: vin, n: 0, v }); + c.add(Device::Resistor { p: vin, n: mid, r }); + c.add(Device::Capacitor { + p: mid, + n: 0, + c: cap, + }); + + let dt = tau / 1000.0; + let mut env = CircuitEnv::new(c, dt); + env.reset(); + + // Step to t = τ. + for _ in 0..1000 { + env.step(); + } + let v_tau = env.observe().node_voltages[mid]; + let expected_tau = v * (1.0 - (-1.0f64).exp()); // 0.632·V + assert!( + (v_tau - expected_tau).abs() < 0.02 * v, + "v(τ) = {v_tau}, expected ≈ {expected_tau}" + ); + + // Step to t = 5τ → essentially fully charged. + for _ in 0..4000 { + env.step(); + } + let v_5tau = env.observe().node_voltages[mid]; + assert!(v_5tau > 0.98 * v, "v(5τ) = {v_5tau}"); +} + +#[test] +fn rl_current_ramp_matches_analytic() { + // 5 V, R=10 Ω, L=1 mH → τ = 0.1 ms, steady current 0.5 A. + let v = 5.0; + let r = 10.0; + let l = 1e-3; + let tau = l / r; + let i_steady = v / r; + + let mut c = Circuit::new(); + let a = c.node(); + let b = c.node(); + c.add(Device::VSource { p: a, n: 0, v }); + c.add(Device::Resistor { p: a, n: b, r }); + let ind = c.add(Device::Inductor { p: b, n: 0, l }); + + let dt = tau / 1000.0; + let mut env = CircuitEnv::new(c, dt); + env.reset(); + + for _ in 0..1000 { + env.step(); + } + let i_tau = env.observe().device_currents[ind]; + let expected_tau = i_steady * (1.0 - (-1.0f64).exp()); + assert!( + (i_tau - expected_tau).abs() < 0.02 * i_steady, + "i(τ) = {i_tau}, expected ≈ {expected_tau}" + ); + + for _ in 0..4000 { + env.step(); + } + let i_5tau = env.observe().device_currents[ind]; + assert!(i_5tau > 0.98 * i_steady, "i(5τ) = {i_5tau}"); +} + +#[test] +fn reset_returns_to_power_on_state() { + let mut c = Circuit::new(); + let vin = c.node(); + let mid = c.node(); + c.add(Device::VSource { + p: vin, + n: 0, + v: 5.0, + }); + c.add(Device::Resistor { + p: vin, + n: mid, + r: 1_000.0, + }); + c.add(Device::Capacitor { + p: mid, + n: 0, + c: 1e-6, + }); + + let mut env = CircuitEnv::new(c, 1e-6); + env.reset(); + for _ in 0..500 { + env.step(); + } + assert!(env.observe().node_voltages[mid] > 0.1); + env.reset(); + let obs = env.observe(); + assert_eq!(obs.time, 0.0); + assert!(obs.node_voltages[mid].abs() < 1e-12); +} + +#[test] +fn silicon_diode_forward_drop() { + // 5 V through 1 kΩ into a forward diode → ~0.65 V drop, ~4.3 mA. + let mut c = Circuit::new(); + let vin = c.node(); + let a = c.node(); + c.add(Device::VSource { + p: vin, + n: 0, + v: 5.0, + }); + c.add(Device::Resistor { + p: vin, + n: a, + r: 1_000.0, + }); + let d = c.add(Device::Diode { + p: a, + n: 0, + model: DiodeModel::silicon(), + }); + + let mut env = CircuitEnv::new(c, 1e-6); + env.reset(); + for _ in 0..30 { + env.step(); + } + let obs = env.observe(); + let vf = obs.node_voltages[a]; + let i = obs.device_currents[d]; + assert!((0.55..0.75).contains(&vf), "Vf = {vf}"); + assert!((3e-3..5e-3).contains(&i), "i = {i}"); +} + +#[test] +fn led_forward_lights_at_expected_current() { + // 5 V through 330 Ω into a red LED → Vf ≈ 1.8 V, ~9–10 mA (LED "on"). + let mut c = Circuit::new(); + let vin = c.node(); + let a = c.node(); + c.add(Device::VSource { + p: vin, + n: 0, + v: 5.0, + }); + c.add(Device::Resistor { + p: vin, + n: a, + r: 330.0, + }); + let led = c.add(Device::Diode { + p: a, + n: 0, + model: DiodeModel::led(), + }); + + let mut env = CircuitEnv::new(c, 1e-6); + env.reset(); + for _ in 0..40 { + env.step(); + } + let obs = env.observe(); + let vf = obs.node_voltages[a]; + let i = obs.device_currents[led]; + assert!((1.5..2.1).contains(&vf), "LED Vf = {vf}"); + assert!((7e-3..12e-3).contains(&i), "LED current = {i}"); + // Kirchhoff: LED current ≈ resistor current. + assert!((i - (5.0 - vf) / 330.0).abs() < 1e-4); +} + +#[test] +fn diode_blocks_reverse() { + // Reverse-biased diode passes ~no current; node sits near the rail. + let mut c = Circuit::new(); + let vin = c.node(); + let a = c.node(); + c.add(Device::VSource { + p: vin, + n: 0, + v: 5.0, + }); + c.add(Device::Resistor { + p: vin, + n: a, + r: 1_000.0, + }); + // anode at ground, cathode at `a` → reverse biased by the +5 V rail. + let d = c.add(Device::Diode { + p: 0, + n: a, + model: DiodeModel::silicon(), + }); + + let mut env = CircuitEnv::new(c, 1e-6); + env.reset(); + for _ in 0..30 { + env.step(); + } + let obs = env.observe(); + assert!( + obs.node_voltages[a] > 4.9, + "v(a) = {}", + obs.node_voltages[a] + ); + assert!( + obs.device_currents[d].abs() < 1e-6, + "i = {}", + obs.device_currents[d] + ); +} + +#[test] +fn motor_spins_up_to_no_load_speed() { + // 5 V across a small DC motor → rotor accelerates to the analytic no-load + // speed ω = V / (Ke + R·b/Kt). + let mp = MotorParams::small_dc(); + let v = 5.0; + let expected = v / (mp.ke + mp.r * mp.b / mp.kt); + + let mut c = Circuit::new(); + let a = c.node(); + c.add(Device::VSource { p: a, n: 0, v }); + let motor = c.add(Device::Motor { + p: a, + n: 0, + params: mp, + }); + + let mut env = CircuitEnv::new(c, 1e-5); + env.reset(); + // ~1 s of sim time (mechanical τ ≈ 0.2 s) → well past spin-up. + for _ in 0..100_000 { + env.step(); + } + let obs = env.observe(); + let omega = obs.rotor_speeds[motor]; + let theta = obs.rotor_angles[motor]; + assert!( + (omega - expected).abs() < 0.03 * expected, + "ω = {omega}, expected ≈ {expected}" + ); + assert!(theta > 0.0, "rotor should have turned: θ = {theta}"); + // Armature current at no load balances friction: I = b·ω / Kt (small). + let i = obs.device_currents[motor].abs(); + assert!(i > 0.0 && i < 0.2, "no-load current = {i}"); +} + +#[test] +fn motor_slows_under_load() { + let mut mp = MotorParams::small_dc(); + mp.load = 5e-3; // 5 mN·m opposing torque + let v = 5.0; + + let mut c = Circuit::new(); + let a = c.node(); + c.add(Device::VSource { p: a, n: 0, v }); + let motor = c.add(Device::Motor { + p: a, + n: 0, + params: mp, + }); + + let mut env = CircuitEnv::new(c, 1e-5); + env.reset(); + for _ in 0..100_000 { + env.step(); + } + let loaded = env.observe().rotor_speeds[motor]; + + // Same motor, no load → must spin faster. + let mut mp0 = MotorParams::small_dc(); + mp0.load = 0.0; + let mut c2 = Circuit::new(); + let a2 = c2.node(); + c2.add(Device::VSource { p: a2, n: 0, v }); + let m2 = c2.add(Device::Motor { + p: a2, + n: 0, + params: mp0, + }); + let mut env2 = CircuitEnv::new(c2, 1e-5); + env2.reset(); + for _ in 0..100_000 { + env2.step(); + } + let unloaded = env2.observe().rotor_speeds[m2]; + + assert!( + loaded < unloaded, + "loaded {loaded} should be < unloaded {unloaded}" + ); + assert!(loaded > 0.0, "motor should still turn under load: {loaded}"); +} diff --git a/crates/vcad-ecad-sim/src/lib.rs b/crates/vcad-ecad-sim/src/lib.rs index c88eb4cc..3c16758d 100644 --- a/crates/vcad-ecad-sim/src/lib.rs +++ b/crates/vcad-ecad-sim/src/lib.rs @@ -9,10 +9,12 @@ //! //! # Modules //! +//! - [`circuit`] — Lumped-element transient circuit simulation (MNA network solver) //! - [`impedance`] — Characteristic impedance for microstrip and stripline geometries //! - [`signal_integrity`] — Propagation delay, crosstalk estimation, length matching //! - [`thermal`] — Component junction temperature and via thermal resistance +pub mod circuit; pub mod impedance; pub mod signal_integrity; pub mod thermal; diff --git a/crates/vcad-ecad-symbols/src/builtin.rs b/crates/vcad-ecad-symbols/src/builtin.rs index ed73190f..5f74cd47 100644 --- a/crates/vcad-ecad-symbols/src/builtin.rs +++ b/crates/vcad-ecad-symbols/src/builtin.rs @@ -429,6 +429,28 @@ pub fn builtin_symbols() -> Vec { ], }), }, + // DC Motor (electromechanical — spins under simulation) + SymbolDef { + id: "motor".into(), + name: "Motor".into(), + prefix: "M".into(), + default_value: "DC".into(), + pins: vec![ + pin("1", "+", PinType::Passive, -8.0, 15.0), + pin("2", "-", PinType::Passive, 48.0, 15.0), + ], + graphics: vec![ + sym_circle(20.0, 15.0, 14.0), + sym_line(-8.0, 15.0, 6.0, 15.0), + sym_line(34.0, 15.0, 48.0, 15.0), + // an "M" inside the circle + sym_line(14.0, 22.0, 14.0, 8.0), + sym_line(14.0, 8.0, 20.0, 15.0), + sym_line(20.0, 15.0, 26.0, 8.0), + sym_line(26.0, 8.0, 26.0, 22.0), + ], + footprint_template: None, + }, // NPN Transistor SymbolDef { id: "npn".into(), @@ -698,7 +720,14 @@ mod tests { #[test] fn builtin_symbols_count() { let symbols = builtin_symbols(); - assert_eq!(symbols.len(), 16); + assert_eq!(symbols.len(), 17); // + Motor + } + + #[test] + fn motor_symbol_present() { + let m = get_symbol("motor").expect("motor symbol"); + assert_eq!(m.prefix, "M"); + assert_eq!(m.pins.len(), 2); } #[test] diff --git a/crates/vcad-kernel-wasm/src/circuit_sim.rs b/crates/vcad-kernel-wasm/src/circuit_sim.rs new file mode 100644 index 00000000..7549d5c8 --- /dev/null +++ b/crates/vcad-kernel-wasm/src/circuit_sim.rs @@ -0,0 +1,165 @@ +//! WASM binding for the lumped-element circuit simulator. +//! +//! Exposes a stateful [`CircuitSim`] JS class so the app can build a circuit +//! once and step it on every animation frame (rather than re-parsing each tick): +//! +//! ```js +//! const sim = new wasm.CircuitSim(JSON.stringify({ dt: 1e-5, devices: [...] })); +//! const obs = sim.step(20); // advance 20 timesteps, get { time, nodeVoltages, deviceCurrents } +//! sim.setValue(0, 0); // open a switch (set a source to 0 V) +//! sim.reset(); +//! ``` + +use serde::{Deserialize, Serialize}; +use vcad_ecad_sim::circuit::{Circuit, CircuitEnv, Device, DiodeModel, MotorParams}; +use wasm_bindgen::prelude::*; + +/// One device in a [`CircuitSpec`]. `p`/`n` are node ids (0 = ground). +#[derive(Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +enum DeviceSpec { + Resistor { p: usize, n: usize, value: f64 }, + Capacitor { p: usize, n: usize, value: f64 }, + Inductor { p: usize, n: usize, value: f64 }, + Vsource { p: usize, n: usize, value: f64 }, + Isource { p: usize, n: usize, value: f64 }, + Diode { p: usize, n: usize }, + Led { p: usize, n: usize }, + Motor { p: usize, n: usize }, +} + +impl DeviceSpec { + fn max_node(&self) -> usize { + match *self { + DeviceSpec::Resistor { p, n, .. } + | DeviceSpec::Capacitor { p, n, .. } + | DeviceSpec::Inductor { p, n, .. } + | DeviceSpec::Vsource { p, n, .. } + | DeviceSpec::Isource { p, n, .. } + | DeviceSpec::Diode { p, n } + | DeviceSpec::Led { p, n } + | DeviceSpec::Motor { p, n } => p.max(n), + } + } + + fn to_device(&self) -> Device { + match *self { + DeviceSpec::Resistor { p, n, value } => Device::Resistor { p, n, r: value }, + DeviceSpec::Capacitor { p, n, value } => Device::Capacitor { p, n, c: value }, + DeviceSpec::Inductor { p, n, value } => Device::Inductor { p, n, l: value }, + DeviceSpec::Vsource { p, n, value } => Device::VSource { p, n, v: value }, + DeviceSpec::Isource { p, n, value } => Device::ISource { p, n, i: value }, + DeviceSpec::Diode { p, n } => Device::Diode { + p, + n, + model: DiodeModel::silicon(), + }, + DeviceSpec::Led { p, n } => Device::Diode { + p, + n, + model: DiodeModel::led(), + }, + DeviceSpec::Motor { p, n } => Device::Motor { + p, + n, + params: MotorParams::small_dc(), + }, + } + } +} + +/// JSON description of a circuit to simulate. +#[derive(Debug, Deserialize)] +struct CircuitSpec { + dt: f64, + devices: Vec, +} + +/// Serializable observation handed back to JS (camelCase fields). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct WasmObservation { + time: f64, + node_voltages: Vec, + device_currents: Vec, + rotor_angles: Vec, + rotor_speeds: Vec, +} + +impl From for WasmObservation { + fn from(o: vcad_ecad_sim::circuit::Observation) -> Self { + WasmObservation { + time: o.time, + node_voltages: o.node_voltages, + device_currents: o.device_currents, + rotor_angles: o.rotor_angles, + rotor_speeds: o.rotor_speeds, + } + } +} + +/// A live circuit simulation. Build from a [`CircuitSpec`] JSON, then `step`. +#[wasm_bindgen] +pub struct CircuitSim { + env: CircuitEnv, +} + +#[wasm_bindgen] +impl CircuitSim { + /// Build a simulation from a JSON `{ dt, devices: [...] }` spec. + #[wasm_bindgen(constructor)] + pub fn new(spec_json: &str) -> Result { + let spec: CircuitSpec = + serde_json::from_str(spec_json).map_err(|e| JsError::new(&e.to_string()))?; + + let max_node = spec.devices.iter().map(|d| d.max_node()).max().unwrap_or(0); + let mut circuit = Circuit::new(); + while circuit.num_nodes <= max_node { + circuit.node(); + } + for d in &spec.devices { + circuit.add(d.to_device()); + } + + let dt = if spec.dt > 0.0 { spec.dt } else { 1e-5 }; + let mut env = CircuitEnv::new(circuit, dt); + env.reset(); + Ok(CircuitSim { env }) + } + + /// Advance the simulation by `n` timesteps; returns the final observation. + #[wasm_bindgen(js_name = step)] + pub fn step(&mut self, n: usize) -> Result { + let mut obs = self.env.observe(); + for _ in 0..n.max(1) { + obs = self.env.step(); + } + let out = WasmObservation::from(obs); + serde_wasm_bindgen::to_value(&out).map_err(|e| JsError::new(&e.to_string())) + } + + /// Current state without advancing time. + #[wasm_bindgen(js_name = observe)] + pub fn observe(&self) -> Result { + let out = WasmObservation::from(self.env.observe()); + serde_wasm_bindgen::to_value(&out).map_err(|e| JsError::new(&e.to_string())) + } + + /// Reset to the power-on state (caps discharged, inductors zero, t = 0). + #[wasm_bindgen(js_name = reset)] + pub fn reset(&mut self) { + self.env.reset(); + } + + /// Mutate a device's primary scalar (drive a switch / PWM / scrubbed value). + #[wasm_bindgen(js_name = setValue)] + pub fn set_value(&mut self, device_id: usize, value: f64) { + self.env.set_value(device_id, value); + } + + /// The configured timestep (s). + #[wasm_bindgen(js_name = dt)] + pub fn dt(&self) -> f64 { + self.env.dt() + } +} diff --git a/crates/vcad-kernel-wasm/src/lib.rs b/crates/vcad-kernel-wasm/src/lib.rs index d21050da..1598c606 100644 --- a/crates/vcad-kernel-wasm/src/lib.rs +++ b/crates/vcad-kernel-wasm/src/lib.rs @@ -7,6 +7,8 @@ //! When compiled with the `ts-rs` feature, this crate exports TypeScript type definitions //! for all serializable types. Run `cargo test --features ts-rs` to generate types. +#[cfg(feature = "ecad")] +pub mod circuit_sim; pub mod document_engine; pub mod keybindings; pub mod sheet_metal; diff --git a/packages/app/src/components/Viewport.tsx b/packages/app/src/components/Viewport.tsx index 9ef30139..325c6fc8 100644 --- a/packages/app/src/components/Viewport.tsx +++ b/packages/app/src/components/Viewport.tsx @@ -19,6 +19,7 @@ import { useDrawingStore } from "@/stores/drawing-store"; import { useElectronicsStore } from "@/stores/electronics-store"; import { useDfmStore } from "@/stores/dfm-store"; import { useElectronicsSync } from "@/hooks/useElectronicsSync"; +import { useCircuitSim } from "@/hooks/useCircuitSim"; import { viewportPointerDown, viewportPointerMove, @@ -302,6 +303,8 @@ export function Viewport() { // Run electronics sync when in electronics mode useElectronicsSync(); + // Drive the live circuit simulation ("come alive") when enabled + useCircuitSim(); // Track drag distance on the viewport so click handlers can ignore the // click that follows a camera rotation / pan gesture. Without this, on diff --git a/packages/app/src/components/electronics/CircuitTabTools.tsx b/packages/app/src/components/electronics/CircuitTabTools.tsx index d24cfb93..0c3f14a9 100644 --- a/packages/app/src/components/electronics/CircuitTabTools.tsx +++ b/packages/app/src/components/electronics/CircuitTabTools.tsx @@ -51,7 +51,9 @@ export function CircuitTabTools() { const toggleComponentBodies = useElectronicsStore((s) => s.toggleComponentBodies); const pcbLayers = useElectronicsStore((s) => s.pcbLayers); const unplacedComponents = useElectronicsStore((s) => s.unplacedComponents); + const simulating = useElectronicsStore((s) => s.simulating); + const setSimulating = useElectronicsStore((s) => s.setSimulating); const setSchTool = useElectronicsStore((s) => s.setSchTool); const setPcbTool = useElectronicsStore((s) => s.setPcbTool); const setSchLabelName = useElectronicsStore((s) => s.setSchLabelName); @@ -101,6 +103,14 @@ export function CircuitTabTools() { if (layout === "schematic") { return ( <> + setSimulating(!simulating)} + iconColor={simulating ? "#ff5a36" : ELECTRONICS_TAB_COLORS.schematic} + > + + s.selection); const hoveredNet = useElectronicsStore((s) => s.hoveredNet); const netlist = useElectronicsStore((s) => s.netlist); + // Live circuit simulation state ("come alive") + const simulating = useElectronicsStore((s) => s.simulating); + const simNodeVoltages = useElectronicsStore((s) => s.simNodeVoltages); + const simDeviceCurrents = useElectronicsStore((s) => s.simDeviceCurrents); + const simRotorAngles = useElectronicsStore((s) => s.simRotorAngles); + const simNetToNode = useElectronicsStore((s) => s.simNetToNode); + const simRefToDevice = useElectronicsStore((s) => s.simRefToDevice); + const simOn = simulating && simNodeVoltages != null; + /** Voltage of a net under simulation (null when not simulating / unknown). */ + const netVoltage = (net: string | null): number | null => { + if (!simOn || !net || !simNetToNode || !simNodeVoltages) return null; + const node = simNetToNode[net]; + return node == null ? null : (simNodeVoltages[node] ?? null); + }; + /** Current through a component under simulation (A), or null. */ + const compCurrent = (ref: string): number | null => { + if (!simOn || !simRefToDevice || !simDeviceCurrents) return null; + const id = simRefToDevice[ref]; + return id == null ? null : (simDeviceCurrents[id] ?? null); + }; + /** Rotor angle (rad) of a motor component under simulation, else 0. */ + const motorAngle = (ref: string): number => { + if (!simOn || !simRefToDevice || !simRotorAngles) return 0; + const id = simRefToDevice[ref]; + return id == null ? 0 : (simRotorAngles[id] ?? 0); + }; const zoom = useElectronicsStore((s) => s.schZoom); const pan = useElectronicsStore((s) => s.schPan); const schTool = useElectronicsStore((s) => s.schTool); @@ -728,6 +760,8 @@ export function SchematicCanvas() { const overrides = wireOverrides.get(i); const s = overrides?.start ?? wire.start; const en = overrides?.end ?? wire.end; + const v = netVoltage(net); + const simStroke = v != null ? voltageColor(v) : null; return ( onWireClick(e, i, net)} onPointerEnter={() => net && setHoveredNet(net)} @@ -821,6 +861,44 @@ export function SchematicCanvas() { onPointerLeave={() => setHoveredNet(null)} opacity={moveDrag?.compIdx === i ? 0.6 : 1} > + {/* LED glow — brightness ∝ current (the circuit "comes alive") */} + {simOn && + comp.properties?.symbolId === "led" && + (() => { + const b = Math.max(0, Math.min(1, Math.abs(compCurrent(comp.ref) ?? 0) / 0.01)); + if (b <= 0.03) return null; + return ( + + ); + })()} + {/* Motor rotor — a spoke that spins at the rotor angle. */} + {simOn && + comp.properties?.symbolId === "motor" && + (() => { + const deg = ((motorAngle(comp.ref) * 180) / Math.PI) % 360; + const spinning = Math.abs(motorAngle(comp.ref)) > 0.01; + return ( + + + + + ); + })()} {renderSymbolGraphics( sym.graphics, highlighted ? colors.accent : colors.bodyStroke, diff --git a/packages/app/src/hooks/useCircuitSim.ts b/packages/app/src/hooks/useCircuitSim.ts new file mode 100644 index 00000000..61f8f005 --- /dev/null +++ b/packages/app/src/hooks/useCircuitSim.ts @@ -0,0 +1,92 @@ +/** + * Live circuit simulation driver — the "come alive" loop. + * + * When simulation is on, builds a circuit from the active schematic + netlist, + * then steps it on every animation frame and publishes the observation (node + * voltages + device currents) to the electronics store, where the schematic + * renderer reads it to colour wires by voltage and glow LEDs by current. + * + * The sim is rebuilt only when the circuit *structurally* changes (a component, + * value, or connection edit) — not on every netlist re-gen — so editing a value + * live re-solves without thrashing. + */ + +import { useEffect, useMemo, useRef } from "react"; +import { useDocumentStore } from "@vcad/core"; +import { createCircuitSim, type CircuitSimHandle } from "@vcad/engine"; +import { useElectronicsStore } from "@/stores/electronics-store"; +import { buildCircuitSpec } from "@/lib/circuit-build"; + +const STEPS_PER_FRAME = 80; + +export function useCircuitSim() { + const active = useElectronicsStore((s) => s.active); + const simulating = useElectronicsStore((s) => s.simulating); + const netlist = useElectronicsStore((s) => s.netlist); + const components = useDocumentStore((s) => s.document.schematic?.components); + + // A structural signature: rebuild the sim only when this changes, so a live + // netlist re-gen with identical topology doesn't reset the running solve. + const signature = useMemo(() => { + if (!simulating || !components || !netlist) return ""; + return JSON.stringify({ + c: components.map((c) => [c.ref, c.value, c.properties?.symbolId]), + n: netlist.nets.map((n) => [n.name, n.connections.length]), + }); + }, [simulating, components, netlist]); + + const simRef = useRef(null); + const rafRef = useRef(null); + + useEffect(() => { + if (!active || !simulating || !signature) return; + const comps = useDocumentStore.getState().document.schematic?.components; + const nl = useElectronicsStore.getState().netlist; + if (!comps || !nl) return; + + let cancelled = false; + const built = buildCircuitSpec(comps, nl); + const store = useElectronicsStore.getState(); + if (built.spec.devices.length === 0) { + store.clearSim(); + return; + } + store.setSimMaps( + Object.fromEntries(built.netToNode), + Object.fromEntries(built.refToDevice), + ); + + createCircuitSim(JSON.stringify(built.spec)).then((sim) => { + if (cancelled || !sim) return; + simRef.current = sim; + const loop = () => { + if (cancelled || !simRef.current) return; + try { + const obs = simRef.current.step(STEPS_PER_FRAME); + useElectronicsStore + .getState() + .setSimObservation(obs.nodeVoltages, obs.deviceCurrents, obs.rotorAngles); + } catch (e) { + console.warn("[circuit-sim] step failed", e); + return; + } + rafRef.current = requestAnimationFrame(loop); + }; + rafRef.current = requestAnimationFrame(loop); + }); + + return () => { + cancelled = true; + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + rafRef.current = null; + if (simRef.current) { + try { + simRef.current.free(); + } catch { + /* already freed */ + } + simRef.current = null; + } + }; + }, [active, simulating, signature]); +} diff --git a/packages/app/src/lib/circuit-build.ts b/packages/app/src/lib/circuit-build.ts new file mode 100644 index 00000000..ad644fce --- /dev/null +++ b/packages/app/src/lib/circuit-build.ts @@ -0,0 +1,183 @@ +/** + * Translate a schematic + its netlist into a circuit-sim spec. + * + * Nets become numbered nodes (ground → 0), components become devices, and + * power symbols define the rails: a `GND` net is the reference, a `VCC`-like + * net gets an independent voltage source. The returned maps let the renderer + * look the live state back up: a net's node voltage colours its wires, a + * component's device current drives its glow. + */ + +import type { SchematicComponent } from "@vcad/ir"; +import type { NetlistResult } from "@vcad/engine"; + +/** One device in a {@link CircuitSpec}; `p`/`n` are node ids (0 = ground). */ +export interface CircuitDeviceSpec { + kind: "resistor" | "capacitor" | "inductor" | "vsource" | "isource" | "diode" | "led" | "motor"; + p: number; + n: number; + value?: number; +} + +/** JSON spec passed to the WASM `CircuitSim`. */ +export interface CircuitSpec { + dt: number; + devices: CircuitDeviceSpec[]; +} + +/** A built circuit plus the maps needed to project results back onto the schematic. */ +export interface BuiltCircuit { + spec: CircuitSpec; + /** Net name → node id. */ + netToNode: Map; + /** Component ref → device index (into `spec.devices`). */ + refToDevice: Map; +} + +const SI_PREFIX: Record = { + p: 1e-12, + n: 1e-9, + u: 1e-6, + µ: 1e-6, + m: 1e-3, + k: 1e3, + K: 1e3, + M: 1e6, + G: 1e9, +}; + +/** + * Parse an engineering value like `"330"`, `"10k"`, `"100nF"`, `"4.7uF"`, + * `"10mH"`, `"2.2M"` into a base-unit number. Unit letters after the prefix + * (F, H, Ω, …) are ignored. Returns `fallback` if it can't parse. + */ +export function parseSiValue(s: string | undefined, fallback: number): number { + if (!s) return fallback; + const m = s.trim().match(/^([0-9]*\.?[0-9]+)\s*([pnuµmkKMG]?)/); + if (!m) return fallback; + const num = parseFloat(m[1]!); + if (!isFinite(num)) return fallback; + const prefix = m[2]!; + const mult = prefix ? (SI_PREFIX[prefix] ?? 1) : 1; + return num * mult; +} + +const isGround = (name: string) => /^(gnd|0|vss)$/i.test(name); +const isSupply = (name: string) => + /^(vcc|vdd|v\+)$/i.test(name) || /^\+?\d+(v\d*|v)$/i.test(name); + +/** + * Build a circuit-sim spec from a schematic + netlist. + * + * @param vcc supply voltage applied to each VCC-like net (default 5 V). + * @param dt simulation timestep (default 10 µs). + */ +export function buildCircuitSpec( + components: SchematicComponent[], + netlist: NetlistResult, + opts?: { dt?: number; vcc?: number }, +): BuiltCircuit { + const dt = opts?.dt ?? 1e-5; + const vcc = opts?.vcc ?? 5.0; + + // (ref, pinNumber) → net name + const pinNet = new Map(); + for (const net of netlist.nets) { + for (const conn of net.connections) { + pinNet.set(`${conn.component_ref}.${conn.pin_number}`, net.name); + } + } + + // A net's electrical role is decided first by the power symbols connected to + // it (robust to whatever the netlister names the net), then by net name. + const symById = new Map( + components.map((c) => [c.ref, (c.properties?.symbolId ?? "").toLowerCase()]), + ); + const netRole = (net: NetlistResult["nets"][number]): "gnd" | "vcc" | null => { + for (const conn of net.connections) { + const sym = symById.get(conn.component_ref); + if (sym === "gnd") return "gnd"; + if (sym === "vcc" || sym === "vdd") return "vcc"; + } + if (isGround(net.name)) return "gnd"; + if (isSupply(net.name)) return "vcc"; + return null; + }; + const roleOf = new Map(netlist.nets.map((n) => [n.name, netRole(n)])); + + // Assign node ids: every ground net → 0, the rest → 1, 2, … + const netToNode = new Map(); + for (const net of netlist.nets) { + if (roleOf.get(net.name) === "gnd") netToNode.set(net.name, 0); + } + let nextNode = 1; + for (const net of netlist.nets) { + if (!netToNode.has(net.name)) netToNode.set(net.name, nextNode++); + } + + const nodeOf = (ref: string, pin: string): number | null => { + const net = pinNet.get(`${ref}.${pin}`); + if (net === undefined) return null; + return netToNode.get(net) ?? null; + }; + + const devices: CircuitDeviceSpec[] = []; + const refToDevice = new Map(); + + // One voltage source per supply net (referenced to ground). + for (const net of netlist.nets) { + if (roleOf.get(net.name) === "vcc") { + const node = netToNode.get(net.name)!; + devices.push({ kind: "vsource", p: node, n: 0, value: vcc }); + } + } + + // Resolve a two-terminal device's p/n nodes by the component's own pin + // numbers (in order), with common conventions as fallbacks ("1"/"2", + // anode/cathode, +/-) so the mapping is robust to symbol pin naming. + const resolve = (comp: SchematicComponent, idx: number, fallbacks: string[]): number | null => { + const own = comp.pins[idx]?.number; + const cands = [own, ...fallbacks].filter((x): x is string => !!x); + for (const cnd of cands) { + const nd = nodeOf(comp.ref, cnd); + if (nd !== null) return nd; + } + return null; + }; + + // Components → two-terminal devices (first pin = p, second = n). + for (const comp of components) { + const sym = (comp.properties?.symbolId ?? "").toLowerCase(); + const p = resolve(comp, 0, ["1", "A", "+"]); + const n = resolve(comp, 1, ["2", "K", "-"]); + const add = (kind: CircuitDeviceSpec["kind"], value?: number) => { + if (p === null || n === null) return; // unconnected pin → skip + refToDevice.set(comp.ref, devices.length); + devices.push({ kind, p, n, value }); + }; + switch (sym) { + case "resistor": + add("resistor", parseSiValue(comp.value, 1_000)); + break; + case "capacitor": + add("capacitor", parseSiValue(comp.value, 1e-7)); + break; + case "inductor": + add("inductor", parseSiValue(comp.value, 1e-3)); + break; + case "led": + add("led"); + break; + case "diode": + add("diode"); + break; + case "motor": + add("motor"); + break; + default: + break; // vcc/gnd symbols define nets, not devices + } + } + + return { spec: { dt, devices }, netToNode, refToDevice }; +} diff --git a/packages/app/src/stores/electronics-store.ts b/packages/app/src/stores/electronics-store.ts index 16c0f616..09e18907 100644 --- a/packages/app/src/stores/electronics-store.ts +++ b/packages/app/src/stores/electronics-store.ts @@ -73,6 +73,14 @@ export interface ElectronicsState { orphanFootprints: string[]; unplacedComponents: string[]; + // Live circuit simulation ("come alive") + simulating: boolean; + simNodeVoltages: number[] | null; + simDeviceCurrents: number[] | null; + simRotorAngles: number[] | null; + simNetToNode: Record | null; + simRefToDevice: Record | null; + // PCB view state pcbZoom: number; pcbPan: Vec2; @@ -160,6 +168,17 @@ export interface ElectronicsState { setNetlist: (n: NetlistResult) => void; setOrphanFootprints: (refs: string[]) => void; setUnplacedComponents: (refs: string[]) => void; + setSimulating: (b: boolean) => void; + setSimMaps: ( + netToNode: Record, + refToDevice: Record, + ) => void; + setSimObservation: ( + nodeVoltages: number[], + deviceCurrents: number[], + rotorAngles: number[], + ) => void; + clearSim: () => void; startRouteFromRatsnest: (fpRef: string, padNum: string, net: string) => void; startRoute: (fpRef: string, padNum: string, net: string) => void; updateRoutePreview: (points: Vec2[]) => void; @@ -204,6 +223,13 @@ export const useElectronicsStore = create((set, get) => ({ orphanFootprints: [], unplacedComponents: [], + simulating: false, + simNodeVoltages: null, + simDeviceCurrents: null, + simRotorAngles: null, + simNetToNode: null, + simRefToDevice: null, + pcbZoom: 1, pcbPan: { x: 0, y: 0 }, pcbTool: "select", @@ -369,6 +395,30 @@ export const useElectronicsStore = create((set, get) => ({ setComponentBodies: (componentBodies) => set({ componentBodies }), toggleComponentBodies: () => set((s) => ({ showComponentBodies: !s.showComponentBodies })), setNetlist: (netlist) => set({ netlist }), + setSimulating: (simulating) => + set( + simulating + ? { simulating } + : { + simulating: false, + simNodeVoltages: null, + simDeviceCurrents: null, + simRotorAngles: null, + simNetToNode: null, + simRefToDevice: null, + }, + ), + setSimMaps: (simNetToNode, simRefToDevice) => set({ simNetToNode, simRefToDevice }), + setSimObservation: (simNodeVoltages, simDeviceCurrents, simRotorAngles) => + set({ simNodeVoltages, simDeviceCurrents, simRotorAngles }), + clearSim: () => + set({ + simNodeVoltages: null, + simDeviceCurrents: null, + simRotorAngles: null, + simNetToNode: null, + simRefToDevice: null, + }), setOrphanFootprints: (orphanFootprints) => set({ orphanFootprints }), setUnplacedComponents: (unplacedComponents) => set({ unplacedComponents }), diff --git a/packages/app/src/test/circuit-build.test.ts b/packages/app/src/test/circuit-build.test.ts new file mode 100644 index 00000000..c452191a --- /dev/null +++ b/packages/app/src/test/circuit-build.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from "vitest"; +import type { SchematicComponent } from "@vcad/ir"; +import type { NetlistResult } from "@vcad/engine"; +import { parseSiValue, buildCircuitSpec } from "@/lib/circuit-build"; + +describe("parseSiValue", () => { + it("parses plain, prefixed, and unit-suffixed values", () => { + expect(parseSiValue("330", 0)).toBe(330); + expect(parseSiValue("10k", 0)).toBe(10_000); + expect(parseSiValue("4.7k", 0)).toBe(4_700); + expect(parseSiValue("100nF", 0)).toBeCloseTo(1e-7); + expect(parseSiValue("1uF", 0)).toBeCloseTo(1e-6); + expect(parseSiValue("10mH", 0)).toBeCloseTo(1e-2); + expect(parseSiValue("2.2M", 0)).toBeCloseTo(2.2e6); + }); + it("distinguishes milli from mega by case", () => { + expect(parseSiValue("1m", 0)).toBeCloseTo(1e-3); + expect(parseSiValue("1M", 0)).toBeCloseTo(1e6); + }); + it("falls back when unparseable", () => { + expect(parseSiValue("", 42)).toBe(42); + expect(parseSiValue("abc", 42)).toBe(42); + expect(parseSiValue(undefined, 42)).toBe(42); + }); +}); + +const comp = (ref: string, symbolId: string, value = ""): SchematicComponent => ({ + ref, + value, + footprintId: "", + position: { x: 0, y: 0 }, + pins: [], + properties: { symbolId }, +}); + +describe("buildCircuitSpec", () => { + it("builds an LED + resistor circuit with a VCC source and ground", () => { + // VCC → R1 → (N1) → LED → GND + const netlist: NetlistResult = { + nets: [ + { name: "VCC", connections: [{ component_ref: "PWR1", pin_number: "1" }, { component_ref: "R1", pin_number: "1" }] }, + { name: "N1", connections: [{ component_ref: "R1", pin_number: "2" }, { component_ref: "D1", pin_number: "1" }] }, + { name: "GND", connections: [{ component_ref: "PWR2", pin_number: "1" }, { component_ref: "D1", pin_number: "2" }] }, + ], + }; + const components = [comp("R1", "resistor", "330"), comp("D1", "led"), comp("PWR1", "vcc"), comp("PWR2", "gnd")]; + + const { spec, netToNode, refToDevice } = buildCircuitSpec(components, netlist); + + expect(netToNode.get("GND")).toBe(0); + expect(netToNode.get("VCC")).toBeGreaterThan(0); + + // vsource(VCC,0,5) + resistor(330) + led + expect(spec.devices).toHaveLength(3); + const vsrc = spec.devices.find((d) => d.kind === "vsource")!; + expect(vsrc).toMatchObject({ n: 0, value: 5 }); + expect(vsrc.p).toBe(netToNode.get("VCC")); + + const r = spec.devices[refToDevice.get("R1")!]!; + expect(r).toMatchObject({ kind: "resistor", value: 330 }); + expect(r.p).toBe(netToNode.get("VCC")); + expect(r.n).toBe(netToNode.get("N1")); + + const led = spec.devices[refToDevice.get("D1")!]!; + expect(led).toMatchObject({ kind: "led", n: 0 }); + expect(led.p).toBe(netToNode.get("N1")); + + // power symbols are nets, not devices + expect(refToDevice.has("PWR1")).toBe(false); + }); + + it("skips components with an unconnected pin", () => { + const netlist: NetlistResult = { + nets: [{ name: "GND", connections: [{ component_ref: "R1", pin_number: "2" }] }], + }; + const { spec } = buildCircuitSpec([comp("R1", "resistor", "1k")], netlist); + expect(spec.devices).toHaveLength(0); // R1 pin 1 floating + }); +}); diff --git a/packages/engine/src/ecad.ts b/packages/engine/src/ecad.ts index 64da26b3..b808aeac 100644 --- a/packages/engine/src/ecad.ts +++ b/packages/engine/src/ecad.ts @@ -422,3 +422,50 @@ export async function netForWire( return null; } } + +// --------------------------------------------------------------------------- +// Circuit simulation (lumped-element transient solver) +// --------------------------------------------------------------------------- + +/** A snapshot from the circuit simulator. */ +export interface CircuitObservation { + time: number; + nodeVoltages: number[]; + deviceCurrents: number[]; + /** Rotor angle (rad) per device id; 0 for non-motors. */ + rotorAngles: number[]; + /** Rotor angular velocity (rad/s) per device id; 0 for non-motors. */ + rotorSpeeds: number[]; +} + +/** A live circuit simulation handle (a WASM `CircuitSim` instance). */ +export interface CircuitSimHandle { + /** Advance `n` timesteps and return the final observation. */ + step(n: number): CircuitObservation; + /** Current state without advancing. */ + observe(): CircuitObservation; + /** Reset to the power-on state. */ + reset(): void; + /** Mutate a device's primary scalar (drive a switch / scrubbed value). */ + setValue(deviceId: number, value: number): void; + /** Configured timestep (s). */ + dt(): number; + /** Release the WASM-side instance. */ + free(): void; +} + +/** + * Build a live circuit simulation from a spec JSON + * (`{ dt, devices: [{ kind, p, n, value }] }`). Returns null if the ECAD WASM + * isn't available or the spec is invalid. + */ +export async function createCircuitSim(specJson: string): Promise { + const wasm = await loadEcadWasm(); + if (!wasm || typeof wasm.CircuitSim !== "function") return null; + try { + return new wasm.CircuitSim(specJson) as CircuitSimHandle; + } catch (e) { + console.warn("[circuit-sim] build failed:", e); + return null; + } +} diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 3212d326..76cfdc37 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -146,6 +146,7 @@ export { builtinSymbols, computeRatsnest, componentMeshes, + createCircuitSim, } from "./ecad.js"; export type { DrcViolationResult, @@ -157,6 +158,8 @@ export type { FilledZoneResult, RatsnestLine, ComponentMesh, + CircuitObservation, + CircuitSimHandle, } from "./ecad.js"; // Parts library diff --git a/packages/kernel-wasm/vcad_kernel_wasm.d.ts b/packages/kernel-wasm/vcad_kernel_wasm.d.ts index 8a59ec49..6556aa9d 100644 --- a/packages/kernel-wasm/vcad_kernel_wasm.d.ts +++ b/packages/kernel-wasm/vcad_kernel_wasm.d.ts @@ -1,6 +1,38 @@ /* tslint:disable */ /* eslint-disable */ +/** + * A live circuit simulation. Build from a [`CircuitSpec`] JSON, then `step`. + */ +export class CircuitSim { + free(): void; + [Symbol.dispose](): void; + /** + * The configured timestep (s). + */ + dt(): number; + /** + * Build a simulation from a JSON `{ dt, devices: [...] }` spec. + */ + constructor(spec_json: string); + /** + * Current state without advancing time. + */ + observe(): any; + /** + * Reset to the power-on state (caps discharged, inductors zero, t = 0). + */ + reset(): void; + /** + * Mutate a device's primary scalar (drive a switch / PWM / scrubbed value). + */ + setValue(device_id: number, value: number): void; + /** + * Advance the simulation by `n` timesteps; returns the final observation. + */ + step(n: number): any; +} + /** * Physics simulation environment for robotics and RL. * @@ -2202,6 +2234,16 @@ export interface InitOutput { readonly solid_fillet: (a: number, b: number) => number; readonly solid_shell: (a: number, b: number) => number; readonly getCompiledModule: () => any; + readonly __wbg_wasmkeybindings_free: (a: number, b: number) => void; + readonly wasmkeybindings_chordFor: (a: number, b: number, c: number) => [number, number]; + readonly wasmkeybindings_commandsJson: (a: number) => [number, number]; + readonly wasmkeybindings_conflictsJson: (a: number, b: number, c: number) => [number, number]; + readonly wasmkeybindings_loadOverrides: (a: number, b: number, c: number) => number; + readonly wasmkeybindings_new: () => number; + readonly wasmkeybindings_resetAll: (a: number) => void; + readonly wasmkeybindings_resolve: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number]; + readonly wasmkeybindings_saveOverrides: (a: number) => [number, number]; + readonly wasmkeybindings_setBinding: (a: number, b: number, c: number, d: number, e: number) => void; readonly checkSheetMetal: (a: number, b: number, c: number, d: number) => [number, number]; readonly costSheetMetal: (a: number, b: number, c: number, d: number, e: number) => [number, number]; readonly evaluateSheetMetalChain: (a: number, b: number) => [number, number]; @@ -2210,7 +2252,94 @@ export interface InitOutput { readonly nestSheetMetalParts: (a: number, b: number, c: number, d: number) => [number, number]; readonly nestedSheetMetalDxf: (a: number, b: number) => [number, number]; readonly sheetMetalSequence: (a: number, b: number) => [number, number]; + readonly __wbg_get_slicersettings_first_layer_height: (a: number) => number; + readonly __wbg_get_slicersettings_infill_density: (a: number) => number; + readonly __wbg_get_slicersettings_infill_pattern: (a: number) => number; + readonly __wbg_get_slicersettings_layer_height: (a: number) => number; + readonly __wbg_get_slicersettings_line_width: (a: number) => number; + readonly __wbg_get_slicersettings_nozzle_diameter: (a: number) => number; + readonly __wbg_get_slicersettings_support_angle: (a: number) => number; + readonly __wbg_get_slicersettings_support_enabled: (a: number) => number; + readonly __wbg_get_slicersettings_wall_count: (a: number) => number; + readonly __wbg_get_wasmcamsettings_retract_z: (a: number) => number; + readonly __wbg_set_slicersettings_first_layer_height: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_infill_density: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_infill_pattern: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_layer_height: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_line_width: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_nozzle_diameter: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_support_angle: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_support_enabled: (a: number, b: number) => void; + readonly __wbg_set_slicersettings_wall_count: (a: number, b: number) => void; + readonly __wbg_set_wasmcamsettings_retract_z: (a: number, b: number) => void; + readonly __wbg_sliceresult_free: (a: number, b: number) => void; + readonly __wbg_slicersettings_free: (a: number, b: number) => void; + readonly __wbg_wasmcamsettings_free: (a: number, b: number) => void; + readonly analyzeForPrinting: (a: number) => [number, number, number]; + readonly camDropCutter: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; + readonly camExportGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; + readonly camExportLinuxCnc: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; + readonly camGenerateCircularPocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; + readonly camGenerateContour: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number]; + readonly camGenerateFace: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; + readonly camGeneratePocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; + readonly camGenerateRoughing3d: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; + readonly camGetDefaultTools: () => [number, number, number, number]; + readonly camToolpathStats: (a: number, b: number) => [number, number, number]; + readonly checkPrintability: (a: number, b: number, c: number) => [number, number, number]; + readonly estimatePrintCost: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; + readonly generate3mf: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; + readonly generate3mfWithGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number, number]; + readonly generateGcode: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; + readonly getSlicerPrinterProfiles: () => [number, number, number]; + readonly isCamAvailable: () => number; + readonly recommendPrintSettings: (a: number, b: number, c: number, d: number) => [number, number, number]; + readonly sliceMesh: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; + readonly sliceMeshWithProgress: (a: number, b: number, c: number, d: number, e: number, f: any) => [number, number, number]; + readonly sliceSolid: (a: number, b: number, c: number) => [number, number, number]; + readonly sliceresult_filamentGrams: (a: number) => number; + readonly sliceresult_filamentMm: (a: number) => number; + readonly sliceresult_getLayerPreview: (a: number, b: number) => [number, number, number]; + readonly sliceresult_layerCount: (a: number) => number; + readonly sliceresult_printTimeSeconds: (a: number) => number; + readonly sliceresult_statsJson: (a: number) => [number, number, number, number]; + readonly slicersettings_fromJson: (a: number, b: number) => [number, number, number]; + readonly slicersettings_new: () => number; + readonly wasmcamsettings_fromJson: (a: number, b: number) => [number, number, number]; + readonly wasmcamsettings_new: () => number; + readonly isSlicerAvailable: () => number; + readonly __wbg_set_wasmcamsettings_feed_rate: (a: number, b: number) => void; + readonly __wbg_set_wasmcamsettings_plunge_rate: (a: number, b: number) => void; + readonly __wbg_set_wasmcamsettings_safe_z: (a: number, b: number) => void; + readonly __wbg_set_wasmcamsettings_spindle_rpm: (a: number, b: number) => void; + readonly __wbg_set_wasmcamsettings_stepdown: (a: number, b: number) => void; + readonly __wbg_set_wasmcamsettings_stepover: (a: number, b: number) => void; + readonly __wbg_get_wasmcamsettings_feed_rate: (a: number) => number; + readonly __wbg_get_wasmcamsettings_plunge_rate: (a: number) => number; + readonly __wbg_get_wasmcamsettings_safe_z: (a: number) => number; + readonly __wbg_get_wasmcamsettings_spindle_rpm: (a: number) => number; + readonly __wbg_get_wasmcamsettings_stepdown: (a: number) => number; + readonly __wbg_get_wasmcamsettings_stepover: (a: number) => number; readonly __wbg_wasmdocumentengine_free: (a: number, b: number) => void; + readonly digitizeSketch: (a: number, b: number, c: number, d: number) => [number, number, number, number]; + readonly digitizeText: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; + readonly ecadBuiltinSymbols: () => [number, number, number]; + readonly ecadCheckDrc: (a: number, b: number) => [number, number, number]; + readonly ecadCheckErc: (a: number, b: number) => [number, number, number]; + readonly ecadComponentMeshes: (a: number, b: number) => [number, number, number]; + readonly ecadComputeRatsnest: (a: number, b: number, c: number, d: number) => [number, number, number]; + readonly ecadFillZones: (a: number, b: number) => [number, number, number]; + readonly ecadGenerateNetlist: (a: number, b: number) => [number, number, number]; + readonly ecadGetSymbol: (a: number, b: number) => [number, number, number]; + readonly ecadLayerZ: (a: number, b: number, c: number, d: number) => number; + readonly ecadNetForWire: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; + readonly ecadRouteNet: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; + readonly ecadRouteNetShove: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; + readonly ecadSnapToGridOrPin: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; + readonly isEcadAvailable: () => number; + readonly parseKicadPcb: (a: number, b: number) => [number, number, number]; + readonly readEmbroideryDst: (a: number, b: number) => [number, number, number, number]; + readonly readEmbroideryPes: (a: number, b: number) => [number, number, number, number]; readonly wasmdocumentengine_add_feature: (a: number, b: number, c: number) => any; readonly wasmdocumentengine_can_redo: (a: number) => number; readonly wasmdocumentengine_can_undo: (a: number) => number; @@ -2241,6 +2370,9 @@ export interface InitOutput { readonly wasmdocumentengine_set_visible: (a: number, b: number, c: number, d: number) => any; readonly wasmdocumentengine_undo: (a: number) => any; readonly wasmdocumentengine_update_feature: (a: number, b: number, c: number, d: number, e: number) => any; + readonly writeEmbroideryDst: (a: number, b: number) => [number, number, number, number]; + readonly writeEmbroideryPes: (a: number, b: number) => [number, number, number, number]; + readonly isEmbroideryAvailable: () => number; readonly __wbg_wasmsketchsession_free: (a: number, b: number) => void; readonly sketchCircleSegments: (a: number, b: number, c: number, d: number) => [number, number, number, number]; readonly sketchHitTest: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; @@ -2272,106 +2404,13 @@ export interface InitOutput { readonly wasmsketchsession_solve: (a: number) => number; readonly wasmsketchsession_toggleSelection: (a: number, b: number) => void; readonly wasmsketchsession_undo: (a: number) => number; - readonly __wbg_get_wasmcamsettings_feed_rate: (a: number) => number; - readonly __wbg_get_wasmcamsettings_plunge_rate: (a: number) => number; - readonly __wbg_get_wasmcamsettings_retract_z: (a: number) => number; - readonly __wbg_get_wasmcamsettings_safe_z: (a: number) => number; - readonly __wbg_get_wasmcamsettings_spindle_rpm: (a: number) => number; - readonly __wbg_get_wasmcamsettings_stepdown: (a: number) => number; - readonly __wbg_get_wasmcamsettings_stepover: (a: number) => number; - readonly __wbg_set_wasmcamsettings_feed_rate: (a: number, b: number) => void; - readonly __wbg_set_wasmcamsettings_plunge_rate: (a: number, b: number) => void; - readonly __wbg_set_wasmcamsettings_retract_z: (a: number, b: number) => void; - readonly __wbg_set_wasmcamsettings_safe_z: (a: number, b: number) => void; - readonly __wbg_set_wasmcamsettings_spindle_rpm: (a: number, b: number) => void; - readonly __wbg_set_wasmcamsettings_stepdown: (a: number, b: number) => void; - readonly __wbg_set_wasmcamsettings_stepover: (a: number, b: number) => void; - readonly __wbg_wasmcamsettings_free: (a: number, b: number) => void; - readonly __wbg_wasmkeybindings_free: (a: number, b: number) => void; - readonly camDropCutter: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; - readonly camExportGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; - readonly camExportLinuxCnc: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; - readonly camGenerateCircularPocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; - readonly camGenerateContour: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number]; - readonly camGenerateFace: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; - readonly camGeneratePocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; - readonly camGenerateRoughing3d: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; - readonly camGetDefaultTools: () => [number, number, number, number]; - readonly camToolpathStats: (a: number, b: number) => [number, number, number]; - readonly digitizeSketch: (a: number, b: number, c: number, d: number) => [number, number, number, number]; - readonly digitizeText: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; - readonly isCamAvailable: () => number; - readonly readEmbroideryDst: (a: number, b: number) => [number, number, number, number]; - readonly readEmbroideryPes: (a: number, b: number) => [number, number, number, number]; - readonly wasmcamsettings_fromJson: (a: number, b: number) => [number, number, number]; - readonly wasmcamsettings_new: () => number; - readonly wasmkeybindings_chordFor: (a: number, b: number, c: number) => [number, number]; - readonly wasmkeybindings_commandsJson: (a: number) => [number, number]; - readonly wasmkeybindings_conflictsJson: (a: number, b: number, c: number) => [number, number]; - readonly wasmkeybindings_loadOverrides: (a: number, b: number, c: number) => number; - readonly wasmkeybindings_new: () => number; - readonly wasmkeybindings_resetAll: (a: number) => void; - readonly wasmkeybindings_resolve: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number]; - readonly wasmkeybindings_saveOverrides: (a: number) => [number, number]; - readonly wasmkeybindings_setBinding: (a: number, b: number, c: number, d: number, e: number) => void; - readonly writeEmbroideryDst: (a: number, b: number) => [number, number, number, number]; - readonly writeEmbroideryPes: (a: number, b: number) => [number, number, number, number]; - readonly isEmbroideryAvailable: () => number; - readonly __wbg_get_slicersettings_first_layer_height: (a: number) => number; - readonly __wbg_get_slicersettings_infill_density: (a: number) => number; - readonly __wbg_get_slicersettings_infill_pattern: (a: number) => number; - readonly __wbg_get_slicersettings_layer_height: (a: number) => number; - readonly __wbg_get_slicersettings_line_width: (a: number) => number; - readonly __wbg_get_slicersettings_nozzle_diameter: (a: number) => number; - readonly __wbg_get_slicersettings_support_angle: (a: number) => number; - readonly __wbg_get_slicersettings_support_enabled: (a: number) => number; - readonly __wbg_get_slicersettings_wall_count: (a: number) => number; - readonly __wbg_set_slicersettings_first_layer_height: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_infill_density: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_infill_pattern: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_layer_height: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_line_width: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_nozzle_diameter: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_support_angle: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_support_enabled: (a: number, b: number) => void; - readonly __wbg_set_slicersettings_wall_count: (a: number, b: number) => void; - readonly __wbg_sliceresult_free: (a: number, b: number) => void; - readonly __wbg_slicersettings_free: (a: number, b: number) => void; - readonly analyzeForPrinting: (a: number) => [number, number, number]; - readonly checkPrintability: (a: number, b: number, c: number) => [number, number, number]; - readonly ecadBuiltinSymbols: () => [number, number, number]; - readonly ecadCheckDrc: (a: number, b: number) => [number, number, number]; - readonly ecadCheckErc: (a: number, b: number) => [number, number, number]; - readonly ecadComponentMeshes: (a: number, b: number) => [number, number, number]; - readonly ecadComputeRatsnest: (a: number, b: number, c: number, d: number) => [number, number, number]; - readonly ecadFillZones: (a: number, b: number) => [number, number, number]; - readonly ecadGenerateNetlist: (a: number, b: number) => [number, number, number]; - readonly ecadGetSymbol: (a: number, b: number) => [number, number, number]; - readonly ecadLayerZ: (a: number, b: number, c: number, d: number) => number; - readonly ecadNetForWire: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; - readonly ecadRouteNet: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; - readonly ecadRouteNetShove: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; - readonly ecadSnapToGridOrPin: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; - readonly estimatePrintCost: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; - readonly generate3mf: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; - readonly generate3mfWithGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number, number]; - readonly generateGcode: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; - readonly getSlicerPrinterProfiles: () => [number, number, number]; - readonly isEcadAvailable: () => number; - readonly parseKicadPcb: (a: number, b: number) => [number, number, number]; - readonly recommendPrintSettings: (a: number, b: number, c: number, d: number) => [number, number, number]; - readonly sliceMesh: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; - readonly sliceMeshWithProgress: (a: number, b: number, c: number, d: number, e: number, f: any) => [number, number, number]; - readonly sliceSolid: (a: number, b: number, c: number) => [number, number, number]; - readonly sliceresult_filamentGrams: (a: number) => number; - readonly sliceresult_filamentMm: (a: number) => number; - readonly sliceresult_getLayerPreview: (a: number, b: number) => [number, number, number]; - readonly sliceresult_layerCount: (a: number) => number; - readonly sliceresult_printTimeSeconds: (a: number) => number; - readonly sliceresult_statsJson: (a: number) => [number, number, number, number]; - readonly slicersettings_fromJson: (a: number, b: number) => [number, number, number]; - readonly slicersettings_new: () => number; - readonly isSlicerAvailable: () => number; + readonly __wbg_circuitsim_free: (a: number, b: number) => void; + readonly circuitsim_dt: (a: number) => number; + readonly circuitsim_new: (a: number, b: number) => [number, number, number]; + readonly circuitsim_observe: (a: number) => [number, number, number]; + readonly circuitsim_reset: (a: number) => void; + readonly circuitsim_setValue: (a: number, b: number, c: number) => void; + readonly circuitsim_step: (a: number, b: number) => [number, number, number]; readonly wasm_bindgen__closure__destroy__h30743bca3150d93c: (a: number, b: number) => void; readonly wasm_bindgen__closure__destroy__hfdadf281ff0f1c56: (a: number, b: number) => void; readonly wasm_bindgen__convert__closures_____invoke__h3c7e771ac0cfa72e: (a: number, b: number, c: any, d: any) => void; diff --git a/packages/kernel-wasm/vcad_kernel_wasm.js b/packages/kernel-wasm/vcad_kernel_wasm.js index 5c40287b..026f935c 100644 --- a/packages/kernel-wasm/vcad_kernel_wasm.js +++ b/packages/kernel-wasm/vcad_kernel_wasm.js @@ -1,5 +1,82 @@ /* @ts-self-types="./vcad_kernel_wasm.d.ts" */ +/** + * A live circuit simulation. Build from a [`CircuitSpec`] JSON, then `step`. + */ +export class CircuitSim { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + CircuitSimFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_circuitsim_free(ptr, 0); + } + /** + * The configured timestep (s). + * @returns {number} + */ + dt() { + const ret = wasm.circuitsim_dt(this.__wbg_ptr); + return ret; + } + /** + * Build a simulation from a JSON `{ dt, devices: [...] }` spec. + * @param {string} spec_json + */ + constructor(spec_json) { + const ptr0 = passStringToWasm0(spec_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.circuitsim_new(ptr0, len0); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + this.__wbg_ptr = ret[0] >>> 0; + CircuitSimFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Current state without advancing time. + * @returns {any} + */ + observe() { + const ret = wasm.circuitsim_observe(this.__wbg_ptr); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } + /** + * Reset to the power-on state (caps discharged, inductors zero, t = 0). + */ + reset() { + wasm.circuitsim_reset(this.__wbg_ptr); + } + /** + * Mutate a device's primary scalar (drive a switch / PWM / scrubbed value). + * @param {number} device_id + * @param {number} value + */ + setValue(device_id, value) { + wasm.circuitsim_setValue(this.__wbg_ptr, device_id, value); + } + /** + * Advance the simulation by `n` timesteps; returns the final observation. + * @param {number} n + * @returns {any} + */ + step(n) { + const ret = wasm.circuitsim_step(this.__wbg_ptr, n); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } +} +if (Symbol.dispose) CircuitSim.prototype[Symbol.dispose] = CircuitSim.prototype.free; + /** * Physics simulation environment for robotics and RL. * @@ -1545,7 +1622,7 @@ export class WasmCamSettings { * @returns {number} */ get feed_rate() { - const ret = wasm.__wbg_get_wasmcamsettings_feed_rate(this.__wbg_ptr); + const ret = wasm.__wbg_get_slicersettings_nozzle_diameter(this.__wbg_ptr); return ret; } /** @@ -1553,7 +1630,7 @@ export class WasmCamSettings { * @returns {number} */ get plunge_rate() { - const ret = wasm.__wbg_get_wasmcamsettings_plunge_rate(this.__wbg_ptr); + const ret = wasm.__wbg_get_slicersettings_line_width(this.__wbg_ptr); return ret; } /** @@ -1569,7 +1646,7 @@ export class WasmCamSettings { * @returns {number} */ get safe_z() { - const ret = wasm.__wbg_get_wasmcamsettings_safe_z(this.__wbg_ptr); + const ret = wasm.__wbg_get_slicersettings_support_angle(this.__wbg_ptr); return ret; } /** @@ -1577,7 +1654,7 @@ export class WasmCamSettings { * @returns {number} */ get spindle_rpm() { - const ret = wasm.__wbg_get_wasmcamsettings_spindle_rpm(this.__wbg_ptr); + const ret = wasm.__wbg_get_slicersettings_infill_density(this.__wbg_ptr); return ret; } /** @@ -1585,7 +1662,7 @@ export class WasmCamSettings { * @returns {number} */ get stepdown() { - const ret = wasm.__wbg_get_wasmcamsettings_stepdown(this.__wbg_ptr); + const ret = wasm.__wbg_get_slicersettings_first_layer_height(this.__wbg_ptr); return ret; } /** @@ -1593,7 +1670,7 @@ export class WasmCamSettings { * @returns {number} */ get stepover() { - const ret = wasm.__wbg_get_wasmcamsettings_stepover(this.__wbg_ptr); + const ret = wasm.__wbg_get_slicersettings_layer_height(this.__wbg_ptr); return ret; } /** @@ -1601,14 +1678,14 @@ export class WasmCamSettings { * @param {number} arg0 */ set feed_rate(arg0) { - wasm.__wbg_set_wasmcamsettings_feed_rate(this.__wbg_ptr, arg0); + wasm.__wbg_set_slicersettings_nozzle_diameter(this.__wbg_ptr, arg0); } /** * Plunge rate (mm/min). * @param {number} arg0 */ set plunge_rate(arg0) { - wasm.__wbg_set_wasmcamsettings_plunge_rate(this.__wbg_ptr, arg0); + wasm.__wbg_set_slicersettings_line_width(this.__wbg_ptr, arg0); } /** * Retract Z height (mm). @@ -1622,28 +1699,28 @@ export class WasmCamSettings { * @param {number} arg0 */ set safe_z(arg0) { - wasm.__wbg_set_wasmcamsettings_safe_z(this.__wbg_ptr, arg0); + wasm.__wbg_set_slicersettings_support_angle(this.__wbg_ptr, arg0); } /** * Spindle RPM. * @param {number} arg0 */ set spindle_rpm(arg0) { - wasm.__wbg_set_wasmcamsettings_spindle_rpm(this.__wbg_ptr, arg0); + wasm.__wbg_set_slicersettings_infill_density(this.__wbg_ptr, arg0); } /** * Stepdown distance (mm). * @param {number} arg0 */ set stepdown(arg0) { - wasm.__wbg_set_wasmcamsettings_stepdown(this.__wbg_ptr, arg0); + wasm.__wbg_set_slicersettings_first_layer_height(this.__wbg_ptr, arg0); } /** * Stepover distance (mm). * @param {number} arg0 */ set stepover(arg0) { - wasm.__wbg_set_wasmcamsettings_stepover(this.__wbg_ptr, arg0); + wasm.__wbg_set_slicersettings_layer_height(this.__wbg_ptr, arg0); } /** * Create from JSON. @@ -4126,7 +4203,7 @@ export function isEcadAvailable() { * @returns {boolean} */ export function isEmbroideryAvailable() { - const ret = wasm.isCamAvailable(); + const ret = wasm.isEcadAvailable(); return ret !== 0; } @@ -4153,7 +4230,7 @@ export function isPhysicsAvailable() { * @returns {boolean} */ export function isSlicerAvailable() { - const ret = wasm.isEcadAvailable(); + const ret = wasm.isCamAvailable(); return ret !== 0; } @@ -6952,12 +7029,12 @@ function __wbg_get_imports() { arg0.writeTexture(arg1, arg2, arg3, arg4); }, __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 2199, function: Function { arguments: [NamedExternref("GPUUncapturedErrorEvent")], shim_idx: 2200, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { dtor_idx: 2208, function: Function { arguments: [NamedExternref("GPUUncapturedErrorEvent")], shim_idx: 2209, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h30743bca3150d93c, wasm_bindgen__convert__closures_____invoke__hcf7d3eaee8800b37); return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 2983, function: Function { arguments: [Externref], shim_idx: 2984, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { dtor_idx: 2992, function: Function { arguments: [Externref], shim_idx: 2993, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__hfdadf281ff0f1c56, wasm_bindgen__convert__closures_____invoke__h9bdf540eb7e61590); return ret; }, @@ -7072,6 +7149,9 @@ const __wbindgen_enum_GpuIndexFormat = ["uint16", "uint32"]; const __wbindgen_enum_GpuTextureFormat = ["r8unorm", "r8snorm", "r8uint", "r8sint", "r16uint", "r16sint", "r16float", "rg8unorm", "rg8snorm", "rg8uint", "rg8sint", "r32uint", "r32sint", "r32float", "rg16uint", "rg16sint", "rg16float", "rgba8unorm", "rgba8unorm-srgb", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb", "rgb9e5ufloat", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rg32uint", "rg32sint", "rg32float", "rgba16uint", "rgba16sint", "rgba16float", "rgba32uint", "rgba32sint", "rgba32float", "stencil8", "depth16unorm", "depth24plus", "depth24plus-stencil8", "depth32float", "depth32float-stencil8", "bc1-rgba-unorm", "bc1-rgba-unorm-srgb", "bc2-rgba-unorm", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm", "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb", "etc2-rgb8unorm", "etc2-rgb8unorm-srgb", "etc2-rgb8a1unorm", "etc2-rgb8a1unorm-srgb", "etc2-rgba8unorm", "etc2-rgba8unorm-srgb", "eac-r11unorm", "eac-r11snorm", "eac-rg11unorm", "eac-rg11snorm", "astc-4x4-unorm", "astc-4x4-unorm-srgb", "astc-5x4-unorm", "astc-5x4-unorm-srgb", "astc-5x5-unorm", "astc-5x5-unorm-srgb", "astc-6x5-unorm", "astc-6x5-unorm-srgb", "astc-6x6-unorm", "astc-6x6-unorm-srgb", "astc-8x5-unorm", "astc-8x5-unorm-srgb", "astc-8x6-unorm", "astc-8x6-unorm-srgb", "astc-8x8-unorm", "astc-8x8-unorm-srgb", "astc-10x5-unorm", "astc-10x5-unorm-srgb", "astc-10x6-unorm", "astc-10x6-unorm-srgb", "astc-10x8-unorm", "astc-10x8-unorm-srgb", "astc-10x10-unorm", "astc-10x10-unorm-srgb", "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", "astc-12x12-unorm-srgb"]; +const CircuitSimFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_circuitsim_free(ptr >>> 0, 1)); const PhysicsSimFinalization = (typeof FinalizationRegistry === 'undefined') ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry(ptr => wasm.__wbg_physicssim_free(ptr >>> 0, 1)); diff --git a/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm b/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm index 52fe3622..3a1ac7fa 100644 Binary files a/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm and b/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm differ diff --git a/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm.d.ts b/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm.d.ts index 93ca6d37..4558e924 100644 --- a/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm.d.ts +++ b/packages/kernel-wasm/vcad_kernel_wasm_bg.wasm.d.ts @@ -128,6 +128,16 @@ export const solid_chamfer: (a: number, b: number) => number; export const solid_fillet: (a: number, b: number) => number; export const solid_shell: (a: number, b: number) => number; export const getCompiledModule: () => any; +export const __wbg_wasmkeybindings_free: (a: number, b: number) => void; +export const wasmkeybindings_chordFor: (a: number, b: number, c: number) => [number, number]; +export const wasmkeybindings_commandsJson: (a: number) => [number, number]; +export const wasmkeybindings_conflictsJson: (a: number, b: number, c: number) => [number, number]; +export const wasmkeybindings_loadOverrides: (a: number, b: number, c: number) => number; +export const wasmkeybindings_new: () => number; +export const wasmkeybindings_resetAll: (a: number) => void; +export const wasmkeybindings_resolve: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number]; +export const wasmkeybindings_saveOverrides: (a: number) => [number, number]; +export const wasmkeybindings_setBinding: (a: number, b: number, c: number, d: number, e: number) => void; export const checkSheetMetal: (a: number, b: number, c: number, d: number) => [number, number]; export const costSheetMetal: (a: number, b: number, c: number, d: number, e: number) => [number, number]; export const evaluateSheetMetalChain: (a: number, b: number) => [number, number]; @@ -136,7 +146,94 @@ export const getSheetMetalMaterials: () => [number, number]; export const nestSheetMetalParts: (a: number, b: number, c: number, d: number) => [number, number]; export const nestedSheetMetalDxf: (a: number, b: number) => [number, number]; export const sheetMetalSequence: (a: number, b: number) => [number, number]; +export const __wbg_get_slicersettings_first_layer_height: (a: number) => number; +export const __wbg_get_slicersettings_infill_density: (a: number) => number; +export const __wbg_get_slicersettings_infill_pattern: (a: number) => number; +export const __wbg_get_slicersettings_layer_height: (a: number) => number; +export const __wbg_get_slicersettings_line_width: (a: number) => number; +export const __wbg_get_slicersettings_nozzle_diameter: (a: number) => number; +export const __wbg_get_slicersettings_support_angle: (a: number) => number; +export const __wbg_get_slicersettings_support_enabled: (a: number) => number; +export const __wbg_get_slicersettings_wall_count: (a: number) => number; +export const __wbg_get_wasmcamsettings_retract_z: (a: number) => number; +export const __wbg_set_slicersettings_first_layer_height: (a: number, b: number) => void; +export const __wbg_set_slicersettings_infill_density: (a: number, b: number) => void; +export const __wbg_set_slicersettings_infill_pattern: (a: number, b: number) => void; +export const __wbg_set_slicersettings_layer_height: (a: number, b: number) => void; +export const __wbg_set_slicersettings_line_width: (a: number, b: number) => void; +export const __wbg_set_slicersettings_nozzle_diameter: (a: number, b: number) => void; +export const __wbg_set_slicersettings_support_angle: (a: number, b: number) => void; +export const __wbg_set_slicersettings_support_enabled: (a: number, b: number) => void; +export const __wbg_set_slicersettings_wall_count: (a: number, b: number) => void; +export const __wbg_set_wasmcamsettings_retract_z: (a: number, b: number) => void; +export const __wbg_sliceresult_free: (a: number, b: number) => void; +export const __wbg_slicersettings_free: (a: number, b: number) => void; +export const __wbg_wasmcamsettings_free: (a: number, b: number) => void; +export const analyzeForPrinting: (a: number) => [number, number, number]; +export const camDropCutter: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; +export const camExportGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; +export const camExportLinuxCnc: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; +export const camGenerateCircularPocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; +export const camGenerateContour: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number]; +export const camGenerateFace: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; +export const camGeneratePocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; +export const camGenerateRoughing3d: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; +export const camGetDefaultTools: () => [number, number, number, number]; +export const camToolpathStats: (a: number, b: number) => [number, number, number]; +export const checkPrintability: (a: number, b: number, c: number) => [number, number, number]; +export const estimatePrintCost: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; +export const generate3mf: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; +export const generate3mfWithGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number, number]; +export const generateGcode: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; +export const getSlicerPrinterProfiles: () => [number, number, number]; +export const isCamAvailable: () => number; +export const recommendPrintSettings: (a: number, b: number, c: number, d: number) => [number, number, number]; +export const sliceMesh: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; +export const sliceMeshWithProgress: (a: number, b: number, c: number, d: number, e: number, f: any) => [number, number, number]; +export const sliceSolid: (a: number, b: number, c: number) => [number, number, number]; +export const sliceresult_filamentGrams: (a: number) => number; +export const sliceresult_filamentMm: (a: number) => number; +export const sliceresult_getLayerPreview: (a: number, b: number) => [number, number, number]; +export const sliceresult_layerCount: (a: number) => number; +export const sliceresult_printTimeSeconds: (a: number) => number; +export const sliceresult_statsJson: (a: number) => [number, number, number, number]; +export const slicersettings_fromJson: (a: number, b: number) => [number, number, number]; +export const slicersettings_new: () => number; +export const wasmcamsettings_fromJson: (a: number, b: number) => [number, number, number]; +export const wasmcamsettings_new: () => number; +export const isSlicerAvailable: () => number; +export const __wbg_set_wasmcamsettings_feed_rate: (a: number, b: number) => void; +export const __wbg_set_wasmcamsettings_plunge_rate: (a: number, b: number) => void; +export const __wbg_set_wasmcamsettings_safe_z: (a: number, b: number) => void; +export const __wbg_set_wasmcamsettings_spindle_rpm: (a: number, b: number) => void; +export const __wbg_set_wasmcamsettings_stepdown: (a: number, b: number) => void; +export const __wbg_set_wasmcamsettings_stepover: (a: number, b: number) => void; +export const __wbg_get_wasmcamsettings_feed_rate: (a: number) => number; +export const __wbg_get_wasmcamsettings_plunge_rate: (a: number) => number; +export const __wbg_get_wasmcamsettings_safe_z: (a: number) => number; +export const __wbg_get_wasmcamsettings_spindle_rpm: (a: number) => number; +export const __wbg_get_wasmcamsettings_stepdown: (a: number) => number; +export const __wbg_get_wasmcamsettings_stepover: (a: number) => number; export const __wbg_wasmdocumentengine_free: (a: number, b: number) => void; +export const digitizeSketch: (a: number, b: number, c: number, d: number) => [number, number, number, number]; +export const digitizeText: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; +export const ecadBuiltinSymbols: () => [number, number, number]; +export const ecadCheckDrc: (a: number, b: number) => [number, number, number]; +export const ecadCheckErc: (a: number, b: number) => [number, number, number]; +export const ecadComponentMeshes: (a: number, b: number) => [number, number, number]; +export const ecadComputeRatsnest: (a: number, b: number, c: number, d: number) => [number, number, number]; +export const ecadFillZones: (a: number, b: number) => [number, number, number]; +export const ecadGenerateNetlist: (a: number, b: number) => [number, number, number]; +export const ecadGetSymbol: (a: number, b: number) => [number, number, number]; +export const ecadLayerZ: (a: number, b: number, c: number, d: number) => number; +export const ecadNetForWire: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; +export const ecadRouteNet: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; +export const ecadRouteNetShove: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; +export const ecadSnapToGridOrPin: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; +export const isEcadAvailable: () => number; +export const parseKicadPcb: (a: number, b: number) => [number, number, number]; +export const readEmbroideryDst: (a: number, b: number) => [number, number, number, number]; +export const readEmbroideryPes: (a: number, b: number) => [number, number, number, number]; export const wasmdocumentengine_add_feature: (a: number, b: number, c: number) => any; export const wasmdocumentengine_can_redo: (a: number) => number; export const wasmdocumentengine_can_undo: (a: number) => number; @@ -167,6 +264,9 @@ export const wasmdocumentengine_set_translation: (a: number, b: number, c: numbe export const wasmdocumentengine_set_visible: (a: number, b: number, c: number, d: number) => any; export const wasmdocumentengine_undo: (a: number) => any; export const wasmdocumentengine_update_feature: (a: number, b: number, c: number, d: number, e: number) => any; +export const writeEmbroideryDst: (a: number, b: number) => [number, number, number, number]; +export const writeEmbroideryPes: (a: number, b: number) => [number, number, number, number]; +export const isEmbroideryAvailable: () => number; export const __wbg_wasmsketchsession_free: (a: number, b: number) => void; export const sketchCircleSegments: (a: number, b: number, c: number, d: number) => [number, number, number, number]; export const sketchHitTest: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; @@ -198,106 +298,13 @@ export const wasmsketchsession_snapshot: (a: number) => [number, number, number, export const wasmsketchsession_solve: (a: number) => number; export const wasmsketchsession_toggleSelection: (a: number, b: number) => void; export const wasmsketchsession_undo: (a: number) => number; -export const __wbg_get_wasmcamsettings_feed_rate: (a: number) => number; -export const __wbg_get_wasmcamsettings_plunge_rate: (a: number) => number; -export const __wbg_get_wasmcamsettings_retract_z: (a: number) => number; -export const __wbg_get_wasmcamsettings_safe_z: (a: number) => number; -export const __wbg_get_wasmcamsettings_spindle_rpm: (a: number) => number; -export const __wbg_get_wasmcamsettings_stepdown: (a: number) => number; -export const __wbg_get_wasmcamsettings_stepover: (a: number) => number; -export const __wbg_set_wasmcamsettings_feed_rate: (a: number, b: number) => void; -export const __wbg_set_wasmcamsettings_plunge_rate: (a: number, b: number) => void; -export const __wbg_set_wasmcamsettings_retract_z: (a: number, b: number) => void; -export const __wbg_set_wasmcamsettings_safe_z: (a: number, b: number) => void; -export const __wbg_set_wasmcamsettings_spindle_rpm: (a: number, b: number) => void; -export const __wbg_set_wasmcamsettings_stepdown: (a: number, b: number) => void; -export const __wbg_set_wasmcamsettings_stepover: (a: number, b: number) => void; -export const __wbg_wasmcamsettings_free: (a: number, b: number) => void; -export const __wbg_wasmkeybindings_free: (a: number, b: number) => void; -export const camDropCutter: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; -export const camExportGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; -export const camExportLinuxCnc: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; -export const camGenerateCircularPocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number]; -export const camGenerateContour: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number]; -export const camGenerateFace: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; -export const camGeneratePocket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; -export const camGenerateRoughing3d: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number]; -export const camGetDefaultTools: () => [number, number, number, number]; -export const camToolpathStats: (a: number, b: number) => [number, number, number]; -export const digitizeSketch: (a: number, b: number, c: number, d: number) => [number, number, number, number]; -export const digitizeText: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; -export const isCamAvailable: () => number; -export const readEmbroideryDst: (a: number, b: number) => [number, number, number, number]; -export const readEmbroideryPes: (a: number, b: number) => [number, number, number, number]; -export const wasmcamsettings_fromJson: (a: number, b: number) => [number, number, number]; -export const wasmcamsettings_new: () => number; -export const wasmkeybindings_chordFor: (a: number, b: number, c: number) => [number, number]; -export const wasmkeybindings_commandsJson: (a: number) => [number, number]; -export const wasmkeybindings_conflictsJson: (a: number, b: number, c: number) => [number, number]; -export const wasmkeybindings_loadOverrides: (a: number, b: number, c: number) => number; -export const wasmkeybindings_new: () => number; -export const wasmkeybindings_resetAll: (a: number) => void; -export const wasmkeybindings_resolve: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number]; -export const wasmkeybindings_saveOverrides: (a: number) => [number, number]; -export const wasmkeybindings_setBinding: (a: number, b: number, c: number, d: number, e: number) => void; -export const writeEmbroideryDst: (a: number, b: number) => [number, number, number, number]; -export const writeEmbroideryPes: (a: number, b: number) => [number, number, number, number]; -export const isEmbroideryAvailable: () => number; -export const __wbg_get_slicersettings_first_layer_height: (a: number) => number; -export const __wbg_get_slicersettings_infill_density: (a: number) => number; -export const __wbg_get_slicersettings_infill_pattern: (a: number) => number; -export const __wbg_get_slicersettings_layer_height: (a: number) => number; -export const __wbg_get_slicersettings_line_width: (a: number) => number; -export const __wbg_get_slicersettings_nozzle_diameter: (a: number) => number; -export const __wbg_get_slicersettings_support_angle: (a: number) => number; -export const __wbg_get_slicersettings_support_enabled: (a: number) => number; -export const __wbg_get_slicersettings_wall_count: (a: number) => number; -export const __wbg_set_slicersettings_first_layer_height: (a: number, b: number) => void; -export const __wbg_set_slicersettings_infill_density: (a: number, b: number) => void; -export const __wbg_set_slicersettings_infill_pattern: (a: number, b: number) => void; -export const __wbg_set_slicersettings_layer_height: (a: number, b: number) => void; -export const __wbg_set_slicersettings_line_width: (a: number, b: number) => void; -export const __wbg_set_slicersettings_nozzle_diameter: (a: number, b: number) => void; -export const __wbg_set_slicersettings_support_angle: (a: number, b: number) => void; -export const __wbg_set_slicersettings_support_enabled: (a: number, b: number) => void; -export const __wbg_set_slicersettings_wall_count: (a: number, b: number) => void; -export const __wbg_sliceresult_free: (a: number, b: number) => void; -export const __wbg_slicersettings_free: (a: number, b: number) => void; -export const analyzeForPrinting: (a: number) => [number, number, number]; -export const checkPrintability: (a: number, b: number, c: number) => [number, number, number]; -export const ecadBuiltinSymbols: () => [number, number, number]; -export const ecadCheckDrc: (a: number, b: number) => [number, number, number]; -export const ecadCheckErc: (a: number, b: number) => [number, number, number]; -export const ecadComponentMeshes: (a: number, b: number) => [number, number, number]; -export const ecadComputeRatsnest: (a: number, b: number, c: number, d: number) => [number, number, number]; -export const ecadFillZones: (a: number, b: number) => [number, number, number]; -export const ecadGenerateNetlist: (a: number, b: number) => [number, number, number]; -export const ecadGetSymbol: (a: number, b: number) => [number, number, number]; -export const ecadLayerZ: (a: number, b: number, c: number, d: number) => number; -export const ecadNetForWire: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; -export const ecadRouteNet: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; -export const ecadRouteNetShove: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number]; -export const ecadSnapToGridOrPin: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; -export const estimatePrintCost: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number]; -export const generate3mf: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; -export const generate3mfWithGcode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number, number]; -export const generateGcode: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; -export const getSlicerPrinterProfiles: () => [number, number, number]; -export const isEcadAvailable: () => number; -export const parseKicadPcb: (a: number, b: number) => [number, number, number]; -export const recommendPrintSettings: (a: number, b: number, c: number, d: number) => [number, number, number]; -export const sliceMesh: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; -export const sliceMeshWithProgress: (a: number, b: number, c: number, d: number, e: number, f: any) => [number, number, number]; -export const sliceSolid: (a: number, b: number, c: number) => [number, number, number]; -export const sliceresult_filamentGrams: (a: number) => number; -export const sliceresult_filamentMm: (a: number) => number; -export const sliceresult_getLayerPreview: (a: number, b: number) => [number, number, number]; -export const sliceresult_layerCount: (a: number) => number; -export const sliceresult_printTimeSeconds: (a: number) => number; -export const sliceresult_statsJson: (a: number) => [number, number, number, number]; -export const slicersettings_fromJson: (a: number, b: number) => [number, number, number]; -export const slicersettings_new: () => number; -export const isSlicerAvailable: () => number; +export const __wbg_circuitsim_free: (a: number, b: number) => void; +export const circuitsim_dt: (a: number) => number; +export const circuitsim_new: (a: number, b: number) => [number, number, number]; +export const circuitsim_observe: (a: number) => [number, number, number]; +export const circuitsim_reset: (a: number) => void; +export const circuitsim_setValue: (a: number, b: number, c: number) => void; +export const circuitsim_step: (a: number, b: number) => [number, number, number]; export const wasm_bindgen__closure__destroy__h30743bca3150d93c: (a: number, b: number) => void; export const wasm_bindgen__closure__destroy__hfdadf281ff0f1c56: (a: number, b: number) => void; export const wasm_bindgen__convert__closures_____invoke__h3c7e771ac0cfa72e: (a: number, b: number, c: any, d: any) => void;