/* ============================================================================ VOLTLAB · Simulador de Circuitos (estilo Falstad) ---------------------------------------------------------------------------- Motor: Análise Nodal Modificada (MNA) + análise transitória (Euler implícito) Não-lineares (diodo/LED): iteração de Newton-Raphson com amortecimento Renderização: com animação de corrente e coloração por tensão Expõe: window.SimuladorCircuitos (componente React) ============================================================================ */ (function () { const { useState, useRef, useEffect, useCallback } = React; /* ------------------------------ Constantes ------------------------------ */ const GRID = 16; const GMIN = 1e-9; // condutância mínima nó→terra (estabilidade) const C = { bg: '#0C1018', panel: '#121826', panel2: '#0F1420', border: 'rgba(61,139,255,0.16)', grid: 'rgba(61,139,255,0.07)', text: '#E8EDF5', dim: '#8FA3BF', blue: '#3D8BFF', cyan: '#57D8E6', amber: '#FFB84D', red: '#FF6B6B', green: '#5CE0A1', body: '#A9BBD4', selGlow: 'rgba(61,139,255,0.85)', }; const SCOPE_COLORS = ['#57D8E6', '#FFB84D', '#FF6B6B', '#9B8CFF', '#5CE0A1', '#FF8FD2']; /* ----------------------- Notação de engenharia -------------------------- */ const ENG_TIERS = [ [1e9, 'G'], [1e6, 'M'], [1e3, 'k'], [1, ''], [1e-3, 'm'], [1e-6, 'µ'], [1e-9, 'n'], [1e-12, 'p'], ]; function fmtEng(v, unit) { if (!isFinite(v)) return '—'; const a = Math.abs(v); if (a < 1e-15) return '0 ' + unit; for (const [f, p] of ENG_TIERS) { if (a >= f * 0.9999) { const x = v / f; const s = Math.abs(x) >= 100 ? x.toFixed(0) : Math.abs(x) >= 10 ? x.toFixed(1) : x.toFixed(2); return s.replace(/\.?0+$/, '') + ' ' + p + unit; } } return v.toExponential(1) + ' ' + unit; } function parseEng(str) { if (typeof str === 'number') return str; const m = String(str).trim().replace(',', '.').match(/^(-?[\d.]+)\s*([GMkKmuµnp]?)/); if (!m) return NaN; const mult = { G: 1e9, M: 1e6, k: 1e3, K: 1e3, m: 1e-3, u: 1e-6, 'µ': 1e-6, n: 1e-9, p: 1e-12, '': 1 }[m[2]]; return parseFloat(m[1]) * mult; } /* --------------------------- Tipos de elemento --------------------------- */ // vsCount: quantas variáveis de corrente MNA o elemento adiciona const TYPES = { wire: { nome: 'Fio', prefix: 'W', vs: 1 }, resistor: { nome: 'Resistor', prefix: 'R', vs: 0, def: { R: 1000 } }, capacitor: { nome: 'Capacitor', prefix: 'C', vs: 0, def: { Cap: 100e-6 } }, inductor: { nome: 'Indutor', prefix: 'L', vs: 0, def: { L: 100e-3 } }, dc: { nome: 'Fonte CC', prefix: 'V', vs: 1, def: { V: 12 } }, ac: { nome: 'Fonte CA', prefix: 'V', vs: 1, def: { A: 10, F: 60, Off: 0 } }, ground: { nome: 'Terra', prefix: 'GND', vs: 0, single: true }, switch: { nome: 'Chave', prefix: 'S', vs: 1, def: { fechada: false } }, diode: { nome: 'Diodo', prefix: 'D', vs: 0, def: { Is: 1e-9, nVt: 1.5 * 0.025852 } }, led: { nome: 'LED', prefix: 'D', vs: 0, def: { Is: 1e-18, nVt: 2.0 * 0.025852 } }, }; const PALETTE = ['wire', 'resistor', 'capacitor', 'inductor', 'dc', 'ac', 'ground', 'switch', 'diode', 'led']; let UID = 1; const counters = {}; function newElement(type, x1, y1, x2, y2, props) { const t = TYPES[type]; counters[type] = (counters[type] || 0) + 1; return { id: UID++, type, name: t.prefix + counters[type], p1: { x: x1, y: y1 }, p2: t.single ? { x: x1, y: y1 } : { x: x2, y: y2 }, props: Object.assign({}, t.def || {}, props || {}), scope: false, // estado de simulação nodes: [], volts: [0, 0], current: 0, vsIdx: -1, capV: 0, indI: 0, dV: 0, curcount: 0, }; } function resetState(el) { el.capV = 0; el.indI = 0; el.dV = 0; el.current = 0; el.volts = [0, 0]; el.curcount = 0; } /* ------------------------------ Motor MNA ------------------------------- */ function assignNodes(els) { const map = new Map(); let n = 0; const key = (p) => p.x + ',' + p.y; const gndKeys = new Set(); els.forEach((e) => { if (e.type === 'ground') gndKeys.add(key(e.p1)); }); els.forEach((e) => { const posts = e.type === 'ground' ? [e.p1] : [e.p1, e.p2]; e.nodes = posts.map((p) => { const k = key(p); if (gndKeys.has(k)) return -1; if (!map.has(k)) map.set(k, n++); return map.get(k); }); }); return n; } /* ---- Buffer MNA pré-alocado (elimina alocação de matriz a cada passo) ---- */ const _mna = { cap: 0, A: null, z: null, row: null, x: null }; function _mnaEnsure(n) { if (n > _mna.cap) { const c = n + 8; _mna.A = new Float64Array(c * c); _mna.z = new Float64Array(c); _mna.row = new Float64Array(c); _mna.x = new Float64Array(c); _mna.cap = c; } } // Eliminação gaussiana sobre buffer flat _mna.A (row-major, n×n) function solveLinear(n) { const A = _mna.A, z = _mna.z, row = _mna.row, x = _mna.x; x.fill(0, 0, n); for (let c = 0; c < n; c++) { let piv = c, pivAbs = Math.abs(A[c * n + c]); for (let r = c + 1; r < n; r++) { const ab = Math.abs(A[r * n + c]); if (ab > pivAbs) { pivAbs = ab; piv = r; } } if (pivAbs < 1e-13) return null; if (piv !== c) { const cn = c * n, pn = piv * n; row.set(A.subarray(cn, cn + n)); A.copyWithin(cn, pn, pn + n); A.set(row.subarray(0, n), pn); const tz = z[piv]; z[piv] = z[c]; z[c] = tz; } const cn = c * n, inv = 1 / A[cn + c]; for (let r = c + 1; r < n; r++) { const f = A[r * n + c] * inv; if (f === 0) continue; const rn = r * n; for (let k = c; k < n; k++) A[rn + k] -= f * A[cn + k]; z[r] -= f * z[c]; } } for (let r = n - 1; r >= 0; r--) { let s = z[r]; const rn = r * n; for (let k = r + 1; k < n; k++) s -= A[rn + k] * x[k]; x[r] = s / A[rn + r]; } return x; } const limExp = (x) => (x > 50 ? Math.exp(50) * (x - 49) : Math.exp(x)); const nv = (x, n) => (n < 0 ? 0 : x[n]); function stampG(n, a, b, g) { const A = _mna.A; if (a >= 0) { A[a*n+a] += g; if (b >= 0) A[a*n+b] -= g; } if (b >= 0) { A[b*n+b] += g; if (a >= 0) A[b*n+a] -= g; } } function stampI(a, b, i) { // fonte de corrente entrando no nó a, saindo de b const z = _mna.z; if (a >= 0) z[a] += i; if (b >= 0) z[b] -= i; } function stampVS(n, nN, a, b, k, V) { // + em a, − em b const A = _mna.A, z = _mna.z, r = nN + k; if (a >= 0) { A[a*n+r] += 1; A[r*n+a] += 1; } if (b >= 0) { A[b*n+r] -= 1; A[r*n+b] -= 1; } z[r] = V; } function stepCircuit(els, nNodes, dt, t) { let m = 0; let hasNL = false; for (const e of els) { e.vsIdx = -1; const T = TYPES[e.type]; if (T.vs && !(e.type === 'switch' && !e.props.fechada)) e.vsIdx = m++; if (e.type === 'diode' || e.type === 'led') hasNL = true; } const size = nNodes + m; if (size === 0) return { ok: true, x: null }; _mnaEnsure(size); const A = _mna.A, z = _mna.z; let x = null; const maxIter = hasNL ? 50 : 1; for (let it = 0; it < maxIter; it++) { // Zera apenas as células usadas — sem alocação A.fill(0, 0, size * size); z.fill(0, 0, size); for (let i = 0; i < nNodes; i++) A[i * size + i] += GMIN; for (const e of els) { const a = e.nodes[0], b = e.nodes[1]; switch (e.type) { case 'resistor': stampG(size, a, b, 1 / Math.max(e.props.R, 1e-6)); break; case 'capacitor': { const g = e.props.Cap / dt; stampG(size, a, b, g); stampI(a, b, g * e.capV); break; } case 'inductor': { const g = dt / Math.max(e.props.L, 1e-12); stampG(size, a, b, g); stampI(a, b, -e.indI); break; } case 'dc': stampVS(size, nNodes, a, b, e.vsIdx, e.props.V); break; case 'ac': { const v = e.props.Off + e.props.A * Math.sin(2 * Math.PI * e.props.F * t); stampVS(size, nNodes, a, b, e.vsIdx, v); break; } case 'wire': stampVS(size, nNodes, a, b, e.vsIdx, 0); break; case 'switch': if (e.props.fechada) stampVS(size, nNodes, a, b, e.vsIdx, 0); break; case 'diode': case 'led': { const { Is, nVt } = e.props; const ex = limExp(e.dV / nVt); const geq = Math.max((Is / nVt) * ex, 1e-12); const Id = Is * (ex - 1); const Ieq = Id - geq * e.dV; stampG(size, a, b, geq); stampI(a, b, -Ieq); break; } default: break; } } x = solveLinear(size); if (!x) return { ok: false, x: null }; if (!hasNL) break; let conv = true; for (const e of els) { if (e.type !== 'diode' && e.type !== 'led') continue; const vd = nv(x, e.nodes[0]) - nv(x, e.nodes[1]); const dv = vd - e.dV; if (Math.abs(dv) > 1e-4) conv = false; e.dV += Math.max(-0.4, Math.min(0.4, dv)); } if (conv) break; } // commit: tensões, correntes e estados for (const e of els) { const v0 = nv(x, e.nodes[0]); const v1 = e.type === 'ground' ? 0 : nv(x, e.nodes[1]); e.volts = [v0, v1]; switch (e.type) { case 'resistor': e.current = (v0 - v1) / Math.max(e.props.R, 1e-6); break; case 'capacitor': { const g = e.props.Cap / dt; e.current = g * ((v0 - v1) - e.capV); e.capV = v0 - v1; break; } case 'inductor': { e.indI += (dt / Math.max(e.props.L, 1e-12)) * (v0 - v1); e.current = e.indI; break; } case 'dc': case 'ac': case 'wire': e.current = x[nNodes + e.vsIdx]; break; case 'switch': e.current = e.props.fechada ? x[nNodes + e.vsIdx] : 0; break; case 'diode': case 'led': { const { Is, nVt } = e.props; e.current = Is * (limExp((v0 - v1) / nVt) - 1); break; } default: e.current = 0; } } return { ok: true, x }; } /* ------------------------------ Desenho --------------------------------- */ const VC_NEG = [255, 107, 107], VC_ZERO = [82, 96, 122], VC_POS = [87, 216, 230]; function voltColor(v, maxV) { const t = Math.max(-1, Math.min(1, v / maxV)); const a = t < 0 ? VC_NEG : VC_POS; const k = Math.abs(t); const r = Math.round(VC_ZERO[0] + (a[0] - VC_ZERO[0]) * k); const g = Math.round(VC_ZERO[1] + (a[1] - VC_ZERO[1]) * k); const b = Math.round(VC_ZERO[2] + (a[2] - VC_ZERO[2]) * k); return `rgb(${r},${g},${b})`; } function geom(el) { const dx = el.p2.x - el.p1.x, dy = el.p2.y - el.p1.y; const len = Math.hypot(dx, dy) || 1; const ux = dx / len, uy = dy / len; const cx = (el.p1.x + el.p2.x) / 2, cy = (el.p1.y + el.p2.y) / 2; return { dx, dy, len, ux, uy, cx, cy, px: -uy, py: ux }; } function drawDots(ctx, el, g) { const I = el.current; if (Math.abs(I) < 1e-7) return; ctx.fillStyle = C.amber; const spacing = 16; let off = ((el.curcount % spacing) + spacing) % spacing; for (let d = off; d < g.len; d += spacing) { ctx.beginPath(); ctx.arc(el.p1.x + g.ux * d, el.p1.y + g.uy * d, 2.1, 0, Math.PI * 2); ctx.fill(); } } function lead(ctx, x1, y1, x2, y2, color, w) { ctx.strokeStyle = color; ctx.lineWidth = w || 2; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } function drawLabel(ctx, el, g, txt, extra) { ctx.font = '500 10px "JetBrains Mono", monospace'; ctx.fillStyle = C.dim; ctx.textAlign = 'center'; const ox = g.px * 13, oy = g.py * 13; ctx.fillText(txt, g.cx + ox, g.cy + oy + 3.5); if (extra) { ctx.fillStyle = 'rgba(143,163,191,0.65)'; ctx.fillText(extra, g.cx - ox, g.cy - oy + 3.5); } } function drawElement(ctx, el, maxV, selected, running) { const g = geom(el); const c0 = voltColor(el.volts[0], maxV); const c1 = voltColor(el.volts[1], maxV); if (selected) { ctx.save(); ctx.shadowColor = C.selGlow; ctx.shadowBlur = 9; } const BODY = Math.min(32, g.len * 0.55); const b1x = g.cx - g.ux * BODY / 2, b1y = g.cy - g.uy * BODY / 2; const b2x = g.cx + g.ux * BODY / 2, b2y = g.cy + g.uy * BODY / 2; switch (el.type) { case 'wire': lead(ctx, el.p1.x, el.p1.y, g.cx, g.cy, c0); lead(ctx, g.cx, g.cy, el.p2.x, el.p2.y, c1); break; case 'ground': { lead(ctx, el.p1.x, el.p1.y, el.p1.x, el.p1.y + 8, c0); ctx.strokeStyle = C.body; ctx.lineWidth = 2; const y = el.p1.y + 8; [[10, 0], [6.5, 4], [3, 8]].forEach(([w, dy]) => { ctx.beginPath(); ctx.moveTo(el.p1.x - w, y + dy); ctx.lineTo(el.p1.x + w, y + dy); ctx.stroke(); }); break; } case 'resistor': { lead(ctx, el.p1.x, el.p1.y, b1x, b1y, c0); lead(ctx, b2x, b2y, el.p2.x, el.p2.y, c1); ctx.strokeStyle = C.body; ctx.lineWidth = 2; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(b1x, b1y); const segs = 6, amp = 5; for (let i = 1; i < segs; i++) { const f = i / segs, s = i % 2 ? 1 : -1; ctx.lineTo(b1x + (b2x - b1x) * f + g.px * amp * s, b1y + (b2y - b1y) * f + g.py * amp * s); } ctx.lineTo(b2x, b2y); ctx.stroke(); drawLabel(ctx, el, g, fmtEng(el.props.R, 'Ω')); break; } case 'capacitor': { const gap = 7; const m1x = g.cx - g.ux * gap / 2, m1y = g.cy - g.uy * gap / 2; const m2x = g.cx + g.ux * gap / 2, m2y = g.cy + g.uy * gap / 2; lead(ctx, el.p1.x, el.p1.y, m1x, m1y, c0); lead(ctx, m2x, m2y, el.p2.x, el.p2.y, c1); ctx.lineWidth = 2.5; ctx.strokeStyle = c0; ctx.beginPath(); ctx.moveTo(m1x + g.px * 10, m1y + g.py * 10); ctx.lineTo(m1x - g.px * 10, m1y - g.py * 10); ctx.stroke(); ctx.strokeStyle = c1; ctx.beginPath(); ctx.moveTo(m2x + g.px * 10, m2y + g.py * 10); ctx.lineTo(m2x - g.px * 10, m2y - g.py * 10); ctx.stroke(); drawLabel(ctx, el, g, fmtEng(el.props.Cap, 'F')); break; } case 'inductor': { lead(ctx, el.p1.x, el.p1.y, b1x, b1y, c0); lead(ctx, b2x, b2y, el.p2.x, el.p2.y, c1); ctx.strokeStyle = C.body; ctx.lineWidth = 2; const loops = 4, step = BODY / loops, r = step / 2; const ang = Math.atan2(g.uy, g.ux); for (let i = 0; i < loops; i++) { const ccx = b1x + g.ux * (i * step + r), ccy = b1y + g.uy * (i * step + r); ctx.beginPath(); ctx.arc(ccx, ccy, r, ang + Math.PI, ang, false); ctx.stroke(); } drawLabel(ctx, el, g, fmtEng(el.props.L, 'H')); break; } case 'dc': { const gap = 6; const m1x = g.cx - g.ux * gap / 2, m1y = g.cy - g.uy * gap / 2; const m2x = g.cx + g.ux * gap / 2, m2y = g.cy + g.uy * gap / 2; lead(ctx, el.p1.x, el.p1.y, m1x, m1y, c0); lead(ctx, m2x, m2y, el.p2.x, el.p2.y, c1); ctx.lineWidth = 2.5; ctx.strokeStyle = c0; // placa longa = + (p1) ctx.beginPath(); ctx.moveTo(m1x + g.px * 12, m1y + g.py * 12); ctx.lineTo(m1x - g.px * 12, m1y - g.py * 12); ctx.stroke(); ctx.strokeStyle = c1; // placa curta = − ctx.beginPath(); ctx.moveTo(m2x + g.px * 5, m2y + g.py * 5); ctx.lineTo(m2x - g.px * 5, m2y - g.py * 5); ctx.stroke(); ctx.font = '600 9px "JetBrains Mono", monospace'; ctx.fillStyle = C.dim; ctx.fillText('+', m1x - g.ux * 7 + g.px * 17, m1y - g.uy * 7 + g.py * 17 + 3); drawLabel(ctx, el, g, fmtEng(el.props.V, 'V')); break; } case 'ac': { const r = 13; const e1x = g.cx - g.ux * r, e1y = g.cy - g.uy * r; const e2x = g.cx + g.ux * r, e2y = g.cy + g.uy * r; lead(ctx, el.p1.x, el.p1.y, e1x, e1y, c0); lead(ctx, e2x, e2y, el.p2.x, el.p2.y, c1); ctx.strokeStyle = C.body; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(g.cx, g.cy, r, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = C.cyan; ctx.lineWidth = 1.6; ctx.beginPath(); for (let i = -8; i <= 8; i++) { const f = i / 8; const px = g.cx + g.ux * f * 8 + g.px * Math.sin(f * Math.PI) * 5; const py = g.cy + g.uy * f * 8 + g.py * Math.sin(f * Math.PI) * 5; i === -8 ? ctx.moveTo(px, py) : ctx.lineTo(px, py); } ctx.stroke(); drawLabel(ctx, el, g, fmtEng(el.props.A, 'V') + ' · ' + fmtEng(el.props.F, 'Hz')); break; } case 'switch': { const gap = 22; const m1x = g.cx - g.ux * gap / 2, m1y = g.cy - g.uy * gap / 2; const m2x = g.cx + g.ux * gap / 2, m2y = g.cy + g.uy * gap / 2; lead(ctx, el.p1.x, el.p1.y, m1x, m1y, c0); lead(ctx, m2x, m2y, el.p2.x, el.p2.y, c1); ctx.fillStyle = C.body; ctx.beginPath(); ctx.arc(m1x, m1y, 2.6, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(m2x, m2y, 2.6, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = el.props.fechada ? C.green : C.body; ctx.lineWidth = 2.2; ctx.beginPath(); ctx.moveTo(m1x, m1y); if (el.props.fechada) ctx.lineTo(m2x, m2y); else ctx.lineTo(m1x + g.ux * gap * 0.85 + g.px * 11, m1y + g.uy * gap * 0.85 + g.py * 11); ctx.stroke(); drawLabel(ctx, el, g, el.props.fechada ? 'fechada' : 'aberta'); break; } case 'diode': case 'led': { const h = 8, w = 12; const m1x = g.cx - g.ux * w / 2, m1y = g.cy - g.uy * w / 2; // ânodo (p1) const m2x = g.cx + g.ux * w / 2, m2y = g.cy + g.uy * w / 2; // cátodo lead(ctx, el.p1.x, el.p1.y, m1x, m1y, c0); lead(ctx, m2x, m2y, el.p2.x, el.p2.y, c1); const on = el.current > 1e-4; if (el.type === 'led' && on) { ctx.save(); ctx.fillStyle = 'rgba(255,184,77,0.18)'; ctx.beginPath(); ctx.arc(g.cx, g.cy, 14 + Math.min(8, el.current * 60), 0, Math.PI * 2); ctx.fill(); ctx.restore(); } ctx.fillStyle = el.type === 'led' ? (on ? C.amber : '#7d6233') : C.body; ctx.beginPath(); ctx.moveTo(m1x + g.px * h, m1y + g.py * h); ctx.lineTo(m1x - g.px * h, m1y - g.py * h); ctx.lineTo(m2x, m2y); ctx.closePath(); ctx.fill(); ctx.strokeStyle = C.body; ctx.lineWidth = 2.4; ctx.beginPath(); ctx.moveTo(m2x + g.px * h, m2y + g.py * h); ctx.lineTo(m2x - g.px * h, m2y - g.py * h); ctx.stroke(); if (el.type === 'led') { ctx.strokeStyle = on ? C.amber : C.dim; ctx.lineWidth = 1.4; for (const o of [0, 5]) { const sx = g.cx + g.px * (h + 4) + g.ux * o, sy = g.cy + g.py * (h + 4) + g.uy * o; ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx + g.px * 6 + g.ux * 3, sy + g.py * 6 + g.uy * 3); ctx.stroke(); } } break; } default: break; } if (selected) ctx.restore(); if (running && el.type !== 'ground') drawDots(ctx, el, g); } function distToSeg(px, py, x1, y1, x2, y2) { const dx = x2 - x1, dy = y2 - y1; const l2 = dx * dx + dy * dy; let t = l2 ? ((px - x1) * dx + (py - y1) * dy) / l2 : 0; t = Math.max(0, Math.min(1, t)); return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy)); } /* ------------------------------ Exemplos -------------------------------- */ function exemplos(nome) { const G = GRID; const E = (t, x1, y1, x2, y2, p) => newElement(t, x1 * G, y1 * G, x2 * G, y2 * G, p); switch (nome) { case 'rc': { const els = [ E('dc', 5, 7, 5, 15, { V: 12 }), E('wire', 5, 7, 9, 7), E('switch', 9, 7, 14, 7, { fechada: false }), E('resistor', 14, 7, 20, 7, { R: 1000 }), E('wire', 20, 7, 24, 7), E('capacitor', 24, 7, 24, 15, { Cap: 470e-6 }), E('wire', 24, 15, 5, 15), E('ground', 5, 15, 5, 15), ]; els[5].scope = true; els[3].scope = true; return els; } case 'rlc': { const els = [ E('dc', 5, 7, 5, 15, { V: 10 }), E('switch', 5, 7, 11, 7, { fechada: false }), E('inductor', 11, 7, 17, 7, { L: 250e-3 }), E('resistor', 17, 7, 23, 7, { R: 10 }), E('capacitor', 23, 7, 23, 15, { Cap: 22e-6 }), E('wire', 23, 15, 5, 15), E('ground', 5, 15, 5, 15), ]; els[4].scope = true; els[2].scope = true; return els; } case 'retificador': { const els = [ E('ac', 4, 7, 4, 15, { A: 12, F: 60, Off: 0 }), E('wire', 4, 7, 8, 7), E('diode', 8, 7, 13, 7), E('wire', 13, 7, 17, 7), E('resistor', 17, 7, 17, 15, { R: 470 }), E('wire', 17, 7, 22, 7), E('capacitor', 22, 7, 22, 15, { Cap: 220e-6 }), E('wire', 22, 15, 4, 15), E('wire', 17, 15, 17, 15), E('ground', 4, 15, 4, 15), ]; // conecta R ao barramento inferior els[8] = E('wire', 17, 15, 22, 15); els[0].scope = true; els[4].scope = true; return els; } case 'led': { const els = [ E('dc', 6, 7, 6, 14, { V: 5 }), E('wire', 6, 7, 11, 7), E('resistor', 11, 7, 17, 7, { R: 220 }), E('led', 17, 7, 22, 7), E('wire', 22, 7, 22, 14), E('wire', 22, 14, 6, 14), E('ground', 6, 14, 6, 14), ]; els[3].scope = true; return els; } default: return []; } } /* ----------------------------- Componente ------------------------------- */ function SimuladorCircuitos() { const canvasRef = useRef(null); const wrapRef = useRef(null); const scopeRef = useRef(null); const sim = useRef({ els: exemplos('rc'), t: 0, dt: 50e-6, running: true, speed: 180, // passos por quadro singular: false, maxV: 5, scopeData: new Map(), // id -> {v:[], i:[]} mode: 'select', selectedId: null, drag: null, hoverNode: null, }).current; const [, force] = useState(0); const rerender = useCallback(() => force((x) => x + 1), []); const [tick, setTick] = useState(0); /* ------- laço de simulação + desenho ------- */ useEffect(() => { let raf; const loop = () => { const cv = canvasRef.current; if (!cv) { raf = requestAnimationFrame(loop); return; } const ctx = cv.getContext('2d'); const dpr = window.devicePixelRatio || 1; const W = cv.clientWidth, H = cv.clientHeight; if (cv.width !== W * dpr || cv.height !== H * dpr) { cv.width = W * dpr; cv.height = H * dpr; } ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // simula if (sim.running && sim.els.length) { const n = assignNodes(sim.els); let ok = true; for (let s = 0; s < sim.speed; s++) { const r = stepCircuit(sim.els, n, sim.dt, sim.t); if (!r.ok) { ok = false; break; } sim.t += sim.dt; } sim.singular = !ok; // animação dos pontos for (const e of sim.els) e.curcount += Math.max(-3, Math.min(3, e.current * 1.4)) * 4; // escala de cor let mv = 5; for (const e of sim.els) mv = Math.max(mv, Math.abs(e.volts[0]), Math.abs(e.volts[1])); sim.maxV = sim.maxV * 0.98 + mv * 0.02; // osciloscópio for (const e of sim.els) { if (!e.scope) { sim.scopeData.delete(e.id); continue; } let d = sim.scopeData.get(e.id); if (!d) { d = { v: [], i: [], name: e.name, type: e.type }; sim.scopeData.set(e.id, d); } d.name = e.name; d.v.push(e.volts[0] - e.volts[1]); d.i.push(e.current); if (d.v.length > 460) { d.v.shift(); d.i.shift(); } } } /* ------- canvas principal ------- */ ctx.fillStyle = C.bg; ctx.fillRect(0, 0, W, H); ctx.fillStyle = C.grid; for (let x = 0; x < W; x += GRID) for (let y = 0; y < H; y += GRID) ctx.fillRect(x - 0.5, y - 0.5, 1.2, 1.2); for (const e of sim.els) drawElement(ctx, e, sim.maxV, e.id === sim.selectedId, sim.running); // pré-visualização de colocação if (sim.drag && sim.drag.kind === 'place') { ctx.strokeStyle = 'rgba(61,139,255,0.6)'; ctx.lineWidth = 2; ctx.setLineDash([5, 4]); ctx.beginPath(); ctx.moveTo(sim.drag.x1, sim.drag.y1); ctx.lineTo(sim.drag.x2, sim.drag.y2); ctx.stroke(); ctx.setLineDash([]); } /* ------- osciloscópio ------- */ const sc = scopeRef.current; if (sc) { const sctx = sc.getContext('2d'); const SW = sc.clientWidth, SH = sc.clientHeight; if (sc.width !== SW * dpr || sc.height !== SH * dpr) { sc.width = SW * dpr; sc.height = SH * dpr; } sctx.setTransform(dpr, 0, 0, dpr, 0, 0); sctx.fillStyle = C.panel2; sctx.fillRect(0, 0, SW, SH); sctx.strokeStyle = 'rgba(61,139,255,0.12)'; sctx.beginPath(); sctx.moveTo(0, SH / 2); sctx.lineTo(SW, SH / 2); sctx.stroke(); let ci = 0; for (const [, d] of sim.scopeData) { const col = SCOPE_COLORS[ci % SCOPE_COLORS.length]; let mv = 1e-9; for (let _i = 0; _i < d.v.length; _i++) { const _av = Math.abs(d.v[_i]); if (_av > mv) mv = _av; } sctx.strokeStyle = col; sctx.lineWidth = 1.6; sctx.beginPath(); d.v.forEach((v, i) => { const x = (i / 459) * SW; const y = SH / 2 - (v / mv) * (SH / 2 - 6); i === 0 ? sctx.moveTo(x, y) : sctx.lineTo(x, y); }); sctx.stroke(); sctx.font = '500 10px "JetBrains Mono", monospace'; sctx.fillStyle = col; const last = d.v[d.v.length - 1] || 0, lastI = d.i[d.i.length - 1] || 0; sctx.fillText(`${d.name} ${fmtEng(last, 'V')} ${fmtEng(lastI, 'A')} (esc. ${fmtEng(mv, 'V')})`, 8, 14 + ci * 13); ci++; } if (ci === 0) { sctx.fillStyle = C.dim; sctx.font = '400 11px "IBM Plex Sans", sans-serif'; sctx.fillText('Selecione um elemento e ative "Osciloscópio" para plotar tensão aqui.', 8, SH / 2 + 4); } } raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); const iv = setInterval(() => setTick((t) => t + 1), 250); // atualiza painel return () => { cancelAnimationFrame(raf); clearInterval(iv); }; }, []); // eslint-disable-line /* ------- interação ------- */ const snap = (v) => Math.round(v / GRID) * GRID; const getPos = (ev) => { const r = canvasRef.current.getBoundingClientRect(); return { x: ev.clientX - r.left, y: ev.clientY - r.top }; }; const hitTest = (x, y) => { let best = null, bd = 7; for (let i = sim.els.length - 1; i >= 0; i--) { const e = sim.els[i]; const p2 = e.type === 'ground' ? { x: e.p1.x, y: e.p1.y + 16 } : e.p2; const d = distToSeg(x, y, e.p1.x, e.p1.y, p2.x, p2.y); if (d < bd) { bd = d; best = e; } } return best; }; const onDown = (ev) => { ev.preventDefault(); canvasRef.current.setPointerCapture(ev.pointerId); const { x, y } = getPos(ev); if (sim.mode === 'select') { const e = hitTest(x, y); sim.selectedId = e ? e.id : null; sim.drag = e ? { kind: 'move', id: e.id, lx: x, ly: y, moved: false } : null; rerender(); } else if (sim.mode === 'ground') { sim.els.push(newElement('ground', snap(x), snap(y))); rerender(); } else { sim.drag = { kind: 'place', type: sim.mode, x1: snap(x), y1: snap(y), x2: snap(x), y2: snap(y) }; } }; const onMove = (ev) => { if (!sim.drag) return; const { x, y } = getPos(ev); if (sim.drag.kind === 'place') { sim.drag.x2 = snap(x); sim.drag.y2 = snap(y); } else if (sim.drag.kind === 'move') { const e = sim.els.find((q) => q.id === sim.drag.id); if (!e) return; const dx = snap(x) - snap(sim.drag.lx), dy = snap(y) - snap(sim.drag.ly); if (dx || dy) { e.p1.x += dx; e.p1.y += dy; e.p2.x += dx; e.p2.y += dy; sim.drag.lx = x; sim.drag.ly = y; sim.drag.moved = true; } } }; const onUp = () => { const d = sim.drag; sim.drag = null; if (!d) return; if (d.kind === 'place') { const len = Math.hypot(d.x2 - d.x1, d.y2 - d.y1); if (len >= GRID) { const el = newElement(d.type, d.x1, d.y1, d.x2, d.y2); sim.els.push(el); sim.selectedId = el.id; rerender(); } } else if (d.kind === 'move' && !d.moved) { const e = sim.els.find((q) => q.id === d.id); if (e && e.type === 'switch') { e.props.fechada = !e.props.fechada; rerender(); } } }; useEffect(() => { const onKey = (ev) => { if (/INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName || '')) return; if ((ev.key === 'Delete' || ev.key === 'Backspace') && sim.selectedId != null) { sim.els = sim.els.filter((e) => e.id !== sim.selectedId); sim.scopeData.delete(sim.selectedId); sim.selectedId = null; rerender(); } if (ev.key === ' ') { ev.preventDefault(); sim.running = !sim.running; rerender(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); // eslint-disable-line /* ------- ações ------- */ const sel = sim.els.find((e) => e.id === sim.selectedId) || null; const resetSim = () => { sim.t = 0; sim.els.forEach(resetState); sim.scopeData.forEach((d) => { d.v.length = 0; d.i.length = 0; }); }; const loadExample = (nome) => { if (!nome) return; sim.els = exemplos(nome); sim.scopeData.clear(); sim.selectedId = null; resetSim(); sim.running = true; rerender(); }; const setProp = (key, str) => { const v = parseEng(str); if (sel && isFinite(v)) { sel.props[key] = v; rerender(); } }; const propFields = sel ? ({ resistor: [['R', 'Resistência (Ω)']], capacitor: [['Cap', 'Capacitância (F)']], inductor: [['L', 'Indutância (H)']], dc: [['V', 'Tensão (V)']], ac: [['A', 'Amplitude (V)'], ['F', 'Frequência (Hz)'], ['Off', 'Offset (V)']], }[sel.type] || []) : []; const hasGround = sim.els.some((e) => e.type === 'ground'); /* ------- UI ------- */ return (
{/* paleta */} {/* canvas */}
t = {fmtEng(sim.t, 's')} {sim.els.length} elementos {!hasGround && sim.els.length > 0 && ⚠ adicione um Terra como referência} {sim.singular && ⚠ circuito indeterminado (verifique conexões)} arraste para desenhar · clique para selecionar · clique na chave para acionar · Del exclui · Espaço pausa
{/* propriedades */}
Osciloscópio
); } /* -------------------------------- CSS ----------------------------------- */ const CSS = ` .vlsim{display:flex;flex-direction:column;height:calc(100vh - 200px);min-height:560px;background:${C.bg};color:${C.text}; font-family:'IBM Plex Sans',sans-serif;border:1px solid ${C.border};border-radius:14px;overflow:hidden} .vlsim *{box-sizing:border-box} .vlsim h1,.vlsim h2,.vlsim h3{font-family:'Space Grotesk',sans-serif;margin:0} .vlsim-bar{display:flex;align-items:center;justify-content:flex-start;gap:10px;flex-wrap:wrap; padding:12px 18px;background:${C.panel};border-bottom:1px solid ${C.border}} .vlsim-title{display:flex;align-items:center;gap:12px} .vlsim-logo{width:34px;height:34px;display:grid;place-items:center;border-radius:9px; background:linear-gradient(135deg,rgba(61,139,255,.25),rgba(87,216,230,.15));border:1px solid ${C.border};font-size:16px} .vlsim-title h1{font-size:15px;font-weight:600;letter-spacing:.2px} .vlsim-title p{margin:1px 0 0;font-size:11px;color:${C.dim}} .vlsim-controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .vlsim-btn{background:${C.panel2};color:${C.text};border:1px solid ${C.border};border-radius:8px; padding:7px 13px;font:500 12px 'IBM Plex Sans',sans-serif;cursor:pointer;transition:.15s} .vlsim-btn:hover{border-color:${C.blue};color:#fff} .vlsim-btn.primary{background:linear-gradient(135deg,${C.blue},#2f6fd6);border-color:transparent;color:#fff} .vlsim-btn.on{border-color:${C.cyan};color:${C.cyan}} .vlsim-btn.danger:hover{border-color:${C.red};color:${C.red}} .vlsim-btn.block{width:100%} .vlsim-select{background:${C.panel2};color:${C.text};border:1px solid ${C.border};border-radius:8px; padding:7px 10px;font:500 12px 'IBM Plex Sans',sans-serif;cursor:pointer} .vlsim-slider{display:flex;align-items:center;gap:8px;font-size:11px;color:${C.dim}} .vlsim-slider input{accent-color:${C.blue};width:110px} .vlsim-main{display:flex;flex:1;min-height:0} .vlsim-palette{width:128px;flex-shrink:0;background:${C.panel};border-right:1px solid ${C.border}; padding:10px 8px;display:flex;flex-direction:column;gap:4px;overflow-y:auto} .vlsim-pal-sep{height:1px;background:${C.border};margin:5px 2px} .vlsim-tool{display:flex;align-items:center;gap:8px;background:transparent;border:1px solid transparent; color:${C.dim};border-radius:8px;padding:7px 9px;font:500 11.5px 'IBM Plex Sans',sans-serif;cursor:pointer;text-align:left;transition:.12s} .vlsim-tool:hover{color:${C.text};background:${C.panel2}} .vlsim-tool.active{color:${C.cyan};background:rgba(87,216,230,.08);border-color:rgba(87,216,230,.35)} .vlsim-tool-ic{width:26px;text-align:center;font-family:'JetBrains Mono',monospace;font-size:12px;flex-shrink:0} .vlsim-canvas-wrap{flex:1;position:relative;min-width:0} .vlsim-canvas-wrap canvas{width:100%;height:100%;display:block;cursor:crosshair;touch-action:none} .vlsim-status{position:absolute;left:0;right:0;bottom:0;display:flex;gap:16px;flex-wrap:wrap;align-items:center; padding:5px 12px;font:500 10.5px 'JetBrains Mono',monospace;color:${C.dim}; background:linear-gradient(transparent,rgba(12,16,24,.92));pointer-events:none} .vlsim-status .warn{color:${C.amber}} .vlsim-status .hint{margin-left:auto;opacity:.55;font-family:'IBM Plex Sans',sans-serif} .vlsim-props{width:248px;flex-shrink:0;background:${C.panel};border-left:1px solid ${C.border}; padding:14px;overflow-y:auto} .vlsim-props h2{font-size:12px;text-transform:uppercase;letter-spacing:1.2px;color:${C.dim};margin-bottom:12px} .vlsim-empty{font-size:12px;color:${C.dim};line-height:1.55} .vlsim-prop-name{display:flex;align-items:baseline;gap:8px;margin-bottom:12px} .vlsim-prop-name strong{font-family:'JetBrains Mono',monospace;font-size:15px;color:${C.cyan}} .vlsim-prop-name span{font-size:11px;color:${C.dim}} .vlsim-field{display:block;margin-bottom:10px} .vlsim-field span{display:block;font-size:10.5px;color:${C.dim};margin-bottom:4px} .vlsim-field input{width:100%;background:${C.panel2};border:1px solid ${C.border};border-radius:7px; color:${C.text};padding:7px 9px;font:500 12.5px 'JetBrains Mono',monospace;outline:none} .vlsim-field input:focus{border-color:${C.blue}} .vlsim-meas{margin:12px 0;border:1px solid ${C.border};border-radius:9px;overflow:hidden} .vlsim-meas div{display:flex;justify-content:space-between;padding:6px 10px;font-size:11px;color:${C.dim}} .vlsim-meas div+div{border-top:1px solid ${C.border}} .vlsim-meas code{font:600 11.5px 'JetBrains Mono',monospace;color:${C.text}} .vlsim-check{display:flex;align-items:center;gap:8px;font-size:12px;margin:10px 0;cursor:pointer} .vlsim-check input{accent-color:${C.cyan}} .vlsim-prop-actions{display:flex;gap:8px;margin-top:10px} .vlsim-prop-actions .vlsim-btn{flex:1;padding:7px 6px;font-size:11px} .vlsim-legend{margin-top:18px;padding-top:12px;border-top:1px solid ${C.border}} .vlsim-legend h3{font-size:10.5px;text-transform:uppercase;letter-spacing:1px;color:${C.dim};margin-bottom:8px} .vlsim-legend div{display:flex;align-items:center;gap:8px;font-size:11px;color:${C.dim};margin-bottom:5px} .vlsim-legend i{width:14px;height:3.5px;border-radius:2px;display:inline-block} .vlsim-scope{flex-shrink:0;border-top:1px solid ${C.border};background:${C.panel}} .vlsim-scope-head{padding:6px 14px 0;font:600 10.5px 'Space Grotesk',sans-serif; text-transform:uppercase;letter-spacing:1.2px;color:${C.dim}} .vlsim-scope canvas{width:100%;height:118px;display:block} @media (max-width:900px){ .vlsim-main{flex-direction:column} .vlsim-palette{width:100%;flex-direction:row;flex-wrap:wrap;border-right:none;border-bottom:1px solid ${C.border}} .vlsim-pal-sep{display:none} .vlsim-canvas-wrap{min-height:380px} .vlsim-props{width:100%;border-left:none;border-top:1px solid ${C.border}} }`; /* --------------------------- Registro global ---------------------------- */ if (!document.getElementById('vl-simulador-css')) { const st = document.createElement('style'); st.id = 'vl-simulador-css'; st.textContent = CSS; document.head.appendChild(st); } window.SimuladorCircuitos = SimuladorCircuitos; if (window.VOLTLAB_MODULES && Array.isArray(window.VOLTLAB_MODULES)) { window.VOLTLAB_MODULES.push({ id: 'simulador', nome: 'Simulador de Circuitos', descricao: 'Simulação interativa de circuitos (estilo Falstad)', component: SimuladorCircuitos, }); } })();