/* global React */
/* ===========================================================
VOLTLAB · Calculation Modules — Corrente, Queda, Dimensionamento
Logic adapted from technical reference (NBR 5410).
=========================================================== */
const { useState, useMemo, useEffect } = React;
const D = window.VL_DATA;
// =====================================================
// HOOKS E LÓGICA DE EXPORTAÇÃO (JSON/CSV)
// =====================================================
const usePersist = (key, initial) => {
if (window.useLocalStorage) return window.useLocalStorage(key, initial);
return React.useState(initial); // fallback
};
const buildReport = (moduleName, inputs, result) => ({
modulo: moduleName, norma: "NBR 5410 / ANEEL 414",
geradoEm: new Date().toLocaleString("pt-BR"), entradas: inputs, resultado: result,
});
const downloadBlob = (content, filename, mime) => {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
};
window.VL_exportReport = (format) => {
if (!window._VL_lastExport) return;
const report = window._VL_lastExport();
if (!report) return;
if (format === "csv") {
const rows = [
["Campo", "Valor"], ["Módulo", report.modulo], ["Gerado em", report.geradoEm],
["---ENTRADAS---", ""], ...Object.entries(report.entradas).map(([k, v]) => [k, v]),
["---RESULTADO---", ""], ...Object.entries(report.resultado).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : v])
];
const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(";")).join("\n");
downloadBlob(csv, `voltlab-${report.modulo}.csv`, "text/csv;charset=utf-8");
} else {
downloadBlob(JSON.stringify(report, null, 2), `voltlab-${report.modulo}.json`, "application/json");
}
};
/* Small shared atoms ---------------------------------------- */
const VL_Icon = ({ name, size = 18 }) => {
const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
const paths = {
bolt: ,
wave: ,
cable: <>>,
pipe: <>>,
refresh: <>>,
download: <>>,
plus: <>>,
check: ,
warn: <>>,
info: <>>,
};
return ;
};
const ResultBig = ({ label, value, unit, status }) => (
);
const MetricCell = ({ label, value, unit, status, big }) => (
{label}
{value}
{unit && {unit}}
);
const StatusBanner = ({ kind, children }) => (
{children}
);
const MemoryStep = ({ n, title, children, isFormula }) => (
{String(n).padStart(2, "0")}
{title}
{children}
);
const FieldBlock = ({ label, hint, children }) => (
{children}
);
const NumberInput = ({ value, onChange, suffix, step, min, max, placeholder }) => (
onChange(e.target.value)}
step={step ?? "0.01"}
min={min}
max={max}
placeholder={placeholder ?? "0"}
/>
{suffix && {suffix}}
);
const SelectInput = ({ value, onChange, children, options }) => {
if (options && window.Dropdown_VL) {
return ;
}
return (
);
};
const Seg = ({ value, onChange, options }) => (
{options.map(o => (
))}
);
/* ===========================================================
1. CORRENTE ELÉTRICA
=========================================================== */
const Corrente = () => {
const [potencia, setPotencia] = useState("50");
const [unidade, setUnidade] = useState("kW");
const [tensao, setTensao] = useState("380");
const [sistema, setSistema] = useState("trifasico");
const [decimais, setDecimais] = useState("3");
const calc = useMemo(() => {
const P = parseFloat(potencia);
const V = parseFloat(tensao);
const d = parseInt(decimais);
if (!P || !V || P <= 0 || V <= 0) return null;
let Pw;
if (unidade === "W") Pw = P;
else if (unidade === "kW") Pw = P * 1000;
else Pw = P * 735.5; // CV
let I, formula, sub;
if (sistema === "trifasico") {
I = Pw / (V * Math.sqrt(3));
formula = "I = P / (V · √3)";
sub = `I = ${Pw.toLocaleString("pt-BR")} / (${V} · 1,732)`;
} else {
I = Pw / V;
formula = "I = P / V";
sub = `I = ${Pw.toLocaleString("pt-BR")} / ${V}`;
}
return { I, Pw, formula, sub, d };
}, [potencia, unidade, tensao, sistema, decimais]);
return (
ENTRADA · POTÊNCIA E TENSÃO
Dados do circuito
AUTO-CÁLCULO
{window.VL_ProjetoSync && { const t=s.get("tensao"); if(t!=null) setTensao(String(t)); const sis=s.get("sistema"); if(sis) setSistema(sis); }}
push={()=>({ tensao: parseFloat(tensao), sistema, corrente: calc? +calc.I.toFixed(2): undefined, potencia: calc? +(calc.Pw/1000).toFixed(3): undefined })}
/>}
Sistema{sistema === "trifasico" ? "3F" : sistema === "bifasico" ? "2F" : "1F"} · {tensao || 0} V
Potência ativa{(calc?.Pw ?? 0).toLocaleString("pt-BR", {maximumFractionDigits: 1})} W
Conversão{unidade} → W
);
};
/* ===========================================================
2. QUEDA DE TENSÃO
=========================================================== */
const QuedaTensao = () => {
const [dist, setDist] = usePersist("vl_queda_dist", "265");
const [tensao, setTensao] = usePersist("vl_queda_tensao", "380");
const [corrente, setCorrente] = usePersist("vl_queda_corrente", "16.05");
const [secao, setSecao] = usePersist("vl_queda_secao", "10");
const [sistema, setSistema] = usePersist("vl_queda_sistema", "trifasico");
const [material, setMaterial] = usePersist("vl_queda_material", "0.0175");
const calc = useMemo(() => {
const L = parseFloat(dist), V = parseFloat(tensao), I = parseFloat(corrente), S = parseFloat(secao);
const rho = parseFloat(material);
if (!L || !V || !I || !S || L<=0 || V<=0 || I<=0 || S<=0) return null;
let dV, formula, sub;
if (sistema === "monofasico") {
dV = (2 * rho * L * I) / S;
formula = "ΔV = 2 · ρ · L · I / S";
sub = `ΔV = 2 · ${rho} · ${L} · ${I} / ${S}`;
} else {
dV = (Math.sqrt(3) * rho * L * I) / S;
formula = "ΔV = √3 · ρ · L · I / S";
sub = `ΔV = 1,732 · ${rho} · ${L} · ${I} / ${S}`;
}
const pct = (dV / V) * 100;
const limite = 4;
const status = pct > limite ? "danger" : pct > 3 ? "warn" : "ok";
return { dV, pct, limite, status, formula, sub, rho, L, V, I, S };
}, [dist, tensao, corrente, secao, sistema, material]);
useEffect(() => {
window._VL_lastExport = () => calc ? buildReport("queda-tensao", {
distancia_m: dist, tensao_V: tensao, corrente_A: corrente, secao_mm2: secao, sistema, material: material === "0.0175" ? "Cobre" : "Alumínio",
}, {
queda_V: calc.dV.toFixed(3), queda_pct: calc.pct.toFixed(2), status: calc.status, limite_pct: calc.limite,
}) : null;
return () => { window._VL_lastExport = null; };
}, [calc]);
return (
ENTRADA · PARÂMETROS DO CIRCUITO
Configuração do trecho
AUTO-CÁLCULO
{window.VL_ProjetoSync && { const c=s.get("corrente"); if(c!=null) setCorrente(String(c)); const t=s.get("tensao"); if(t!=null) setTensao(String(t)); const sec=s.get("secao"); if(sec!=null) setSecao(String(sec)); const sis=s.get("sistema"); if(sis&&sis!=="bifasico") setSistema(sis); }}
push={()=>({ tensao: parseFloat(tensao), corrente: parseFloat(corrente), secao: parseFloat(secao), sistema })}
/>}
({ value: String(s), label: `${s} mm²` }))
}/>
L · I{(parseFloat(dist)||0) * (parseFloat(corrente)||0)}
Sistema{sistema === "trifasico" ? "Trifásico" : "Monofásico"}
Limite NBR≤ 4%
);
};
/* ===========================================================
3. DIMENSIONAMENTO DE CABOS
=========================================================== */
const Dimensionamento = () => {
const [tipo, setTipo] = useState("3CA");
const [tempAmb, setTempAmb] = useState("40");
const [dist, setDist] = useState("300");
const [tensao, setTensao] = useState("380");
const [potencia, setPotencia] = useState("150");
const [quedaMax, setQuedaMax] = useState("5");
const [fp, setFp] = useState("0.8");
const [rend, setRend] = useState("90");
const [isolacao, setIsolacao] = useState("PVC");
const [agrup, setAgrup] = useState("1");
// adjust defaults when type changes
useEffect(() => {
if (tipo === "CC") { setPotencia("20"); setTensao("24"); setDist("100"); }
else if (tipo === "3CA") { setPotencia("150"); setTensao("380"); setDist("300"); }
else { setPotencia("7000"); setTensao("220"); setDist("300"); }
}, [tipo]);
const result = useMemo(() => {
const L = parseFloat(dist), V = parseFloat(tensao), P = parseFloat(potencia);
const qMax = parseFloat(quedaMax) / 100;
if (!L || !V || !P || !qMax) return null;
const rho20 = 0.0175;
const alpha = 0.00393;
const rho = rho20 * (1 + alpha * (parseFloat(tempAmb) - 20));
let corrente, table = [];
let secaoQueda = null, secaoAmpacidade = null;
const sections = D.commercialSections;
const fpN = parseFloat(fp), rendN = parseFloat(rend) / 100;
if (tipo === "CC") {
const Rl = (V * V) / P;
corrente = P / V;
sections.forEach(s => {
const Rcond = rho * L / s;
const Rcirc = Rl + 2 * Rcond;
const Vf = (Rl / Rcirc) * V;
const qV = V - Vf;
const qPct = (qV / V) * 100;
table.push({ s, Rcond, qV, qPct });
if (!secaoQueda && (qPct / 100) <= qMax) secaoQueda = s;
});
} else if (tipo === "3CA") {
const Pkw = P * 0.736; // CV → kW
corrente = (Pkw * 1000) / (Math.sqrt(3) * V * rendN * fpN);
const sinFi = Math.sin(Math.acos(fpN));
const X = 0.08 * L / 1000;
sections.forEach(s => {
const Rcond = rho * L / s;
const qV = Math.sqrt(3) * corrente * (Rcond * fpN + X * sinFi);
const qPct = (qV / V) * 100;
table.push({ s, Rcond, qV, qPct });
if (!secaoQueda && (qPct / 100) <= qMax) secaoQueda = s;
});
const fctT = D.getTempFactor(parseFloat(tempAmb), isolacao);
const fctA = D.getAgrupFactor(parseInt(agrup));
const iBase = corrente / (fctT * fctA);
secaoAmpacidade = D.getSectionByAmpacity(iBase, isolacao, true);
} else { // 1CA
corrente = P / V;
sections.forEach(s => {
const Rcond = rho * L / s;
const qV = 2 * Rcond * corrente;
const qPct = (qV / V) * 100;
table.push({ s, Rcond, qV, qPct });
if (!secaoQueda && (qPct / 100) <= qMax) secaoQueda = s;
});
const fctT = D.getTempFactor(parseFloat(tempAmb), isolacao);
const fctA = D.getAgrupFactor(parseInt(agrup));
const iBase = corrente / (fctT * fctA);
secaoAmpacidade = D.getSectionByAmpacity(iBase, isolacao, false);
}
const secaoFinal = Math.max(secaoQueda || 300, secaoAmpacidade || 0);
const final = table.find(t => t.s === secaoFinal);
return { corrente, table, secaoQueda, secaoAmpacidade, secaoFinal, final, rho };
}, [tipo, tempAmb, dist, tensao, potencia, quedaMax, fp, rend, isolacao, agrup]);
const isCA = tipo !== "CC";
const potLabel = tipo === "3CA" ? "Potência (CV)" : "Potência (W)";
return (
ENTRADA · DIMENSIONAMENTO NBR 5410
Parâmetros do sistema e da instalação
AUTO-CÁLCULO
{window.VL_ProjetoSync && { const t=s.get("tensao"); if(t!=null) setTensao(String(t)); }}
push={()=>({ tensao: parseFloat(tensao), corrente: result? +result.corrente.toFixed(2): undefined, secao: result? result.secaoFinal: undefined })}
/>}
{isCA && (
<>
>
)}
{result && (
TABELA COMPLETA DE SEÇÕES
Iteração por bitola comercial
ρ corrigida = {result.rho.toFixed(6)} Ω·mm²/m
SEÇÃO (mm²)
R CONDUTOR (Ω)
QUEDA (V)
QUEDA (%)
STATUS
{result.table.map(r => {
const isSel = r.s === result.secaoFinal;
const ok = r.qPct <= parseFloat(quedaMax);
return (
{r.s}
{r.Rcond.toFixed(4)}
{r.qV.toFixed(3)} V
{r.qPct.toFixed(2)} %
{ok ? "Atende" : "Não atende"}
);
})}
)}
);
};
/* ===========================================================
4. BANCO DE CAPACITORES — correção de fator de potência
=========================================================== */
// Bancos comerciais (kVAr) — degraus padrão
const CAP_STEPS_BT = [2.5, 5, 7.5, 10, 12.5, 15, 20, 25, 30, 37.5, 50, 60, 75, 100];
const CAP_STEPS_MT = [50, 75, 100, 150, 200, 300, 400, 500, 750, 1000];
const Capacitor = () => {
const [pAtiva, setPAtiva] = useState("100");
const [fpAtual, setFpAtual] = useState("0.75");
const [fpDesejado, setFpDesejado] = useState("0.92");
const [tensao, setTensao] = useState("380");
const [sistema, setSistema] = useState("trifasico");
const [freq, setFreq] = useState("60");
const [classe, setClasse] = useState("BT"); // BT ou MT
const calc = useMemo(() => {
const P = parseFloat(pAtiva);
const fp1 = parseFloat(fpAtual);
const fp2 = parseFloat(fpDesejado);
const V = parseFloat(tensao);
const f = parseFloat(freq);
if (!P || !fp1 || !fp2 || !V || P <= 0 || fp1 <= 0 || fp1 > 1 || fp2 <= 0 || fp2 > 1) return null;
if (fp2 <= fp1) return { error: "FP desejado deve ser maior que o atual" };
const phi1 = Math.acos(fp1);
const phi2 = Math.acos(fp2);
const tan1 = Math.tan(phi1);
const tan2 = Math.tan(phi2);
// Qc em kVAr
const Qc = P * (tan1 - tan2);
// Antes
const S1 = P / fp1;
const Q1 = P * tan1;
const I1 = sistema === "trifasico"
? (S1 * 1000) / (Math.sqrt(3) * V)
: (S1 * 1000) / V;
// Depois
const S2 = P / fp2;
const Q2 = P * tan2;
const I2 = sistema === "trifasico"
? (S2 * 1000) / (Math.sqrt(3) * V)
: (S2 * 1000) / V;
// Redução de corrente / perdas
const redCorrente = ((I1 - I2) / I1) * 100;
const redPerdas = (1 - Math.pow(I2 / I1, 2)) * 100;
// Capacitância equivalente (μF) — banco delta trifásico
const omega = 2 * Math.PI * f;
let cap_uF;
if (sistema === "trifasico") {
// Delta: C = Qc / (3 · ω · V²) — kVAr e V em SI
cap_uF = (Qc * 1000) / (3 * omega * V * V) * 1e6;
} else {
cap_uF = (Qc * 1000) / (omega * V * V) * 1e6;
}
// Bancos comerciais — proposta de degraus
const steps = classe === "MT" ? CAP_STEPS_MT : CAP_STEPS_BT;
// pick smallest single step >= Qc, OR closest combination of equal steps
const singleStep = steps.find(s => s >= Qc) ?? steps[steps.length - 1];
// best combination: try 1-6 steps of each commercial value
let best = { step: singleStep, n: 1, total: singleStep, sobra: singleStep - Qc };
steps.forEach(s => {
for (let n = 1; n <= 8; n++) {
const total = s * n;
if (total >= Qc) {
const sobra = total - Qc;
if (sobra < best.sobra || (sobra === best.sobra && n < best.n)) {
best = { step: s, n, total, sobra };
}
break;
}
}
});
// Multa ANEEL — referência 414/2010 (FP < 0,92)
const limite = 0.92;
const multa = fp1 < limite;
return {
P, fp1, fp2, V, f,
Qc, S1, S2, Q1, Q2, I1, I2,
redCorrente, redPerdas, cap_uF,
singleStep, best, multa, limite,
tan1, tan2, phi1, phi2,
};
}, [pAtiva, fpAtual, fpDesejado, tensao, sistema, freq, classe]);
const fpClass = (fp) => fp >= 0.92 ? "ok" : fp >= 0.85 ? "warn" : "danger";
return (
ENTRADA · CORREÇÃO DE FATOR DE POTÊNCIA
Dados da carga e meta de FP
ANEEL 414
{/* Quick comparison */}
ANTES
{parseFloat(fpAtual || 0).toFixed(2)}
→
DEPOIS
{parseFloat(fpDesejado || 0).toFixed(2)}
{calc && !calc.error && (
ANÁLISE DETALHADA · ANTES × DEPOIS
Impacto da correção no sistema
Frequência: {calc.f} Hz
GRANDEZA
SÍMBOLO
ANTES
DEPOIS
VARIAÇÃO
MEMÓRIA DE CÁLCULO
φ₁ = arccos({calc.fp1.toFixed(2)}) = {(calc.phi1*180/Math.PI).toFixed(2)}°
φ₂ = arccos({calc.fp2.toFixed(2)}) = {(calc.phi2*180/Math.PI).toFixed(2)}°
Qc = P · (tan φ₁ − tan φ₂)
Qc = {calc.P} · ({calc.tan1.toFixed(4)} − {calc.tan2.toFixed(4)})
Qc = {calc.Qc.toFixed(2)} kVAr
{sistema === "trifasico" ? "C = Qc / (3 · ω · V²)" : "C = Qc / (ω · V²)"}
{" → "}
{calc.cap_uF.toFixed(2)} μF
)}
);
};
const FPDial = ({ fp, status }) => {
const R = 38, C = 2 * Math.PI * R;
const pct = Math.max(0, Math.min(1, fp));
const dash = pct * C;
const color = status === "ok" ? "var(--ok)" : status === "warn" ? "var(--warn)" : "var(--danger)";
return (
);
};
const FpRow = ({ label, sym, unit, before, after, delta, good }) => (
{label}
{sym}
{before} {unit}
{after} {unit}
{delta}
);
/* ===========================================================
5. TRANSFORMADORES
=========================================================== */
const Transformador = () => {
const [potencia, setPotencia] = usePersist("vl_tr_pot", "500");
const [vPrim, setVPrim] = usePersist("vl_tr_vp", "13.8");
const [vSec, setVSec] = usePersist("vl_tr_vs", "380");
const calc = useMemo(() => {
const S = parseFloat(potencia), Vp = parseFloat(vPrim), Vs = parseFloat(vSec);
if (!S || !Vp || !Vs) return null;
// I = S / (√3 * V)
const ip = (S * 1000) / (Math.sqrt(3) * Vp * 1000);
const is = (S * 1000) / (Math.sqrt(3) * Vs);
// Dimensionamento rápido de proteção (1.25x In para Trafo)
const djSec = is * 1.25;
return { S, Vp, Vs, ip, is, djSec };
}, [potencia, vPrim, vSec]);
useEffect(() => {
window._VL_lastExport = () => calc ? buildReport("transformador", {
potencia_kVA: potencia, tensao_primaria_kV: vPrim, tensao_secundaria_V: vSec
}, {
corrente_primaria_A: calc.ip.toFixed(2), corrente_secundaria_A: calc.is.toFixed(2)
}) : null;
return () => { window._VL_lastExport = null; };
}, [calc]);
return (
DADOS DE ENTRADA · NBR 5440
Especificações do Transformador
);
};
/* ===========================================================
6. CURTO-CIRCUITO (MÉTODO INFINITO / IMPEDÂNCIA)
=========================================================== */
const CurtoCircuito = () => {
const [potencia, setPotencia] = usePersist("vl_cc_pot", "500");
const [z, setZ] = usePersist("vl_cc_z", "4.5");
const [tensao, setTensao] = usePersist("vl_cc_v", "380");
const calc = useMemo(() => {
const S = parseFloat(potencia), imp = parseFloat(z), V = parseFloat(tensao);
if (!S || !imp || !V) return null;
// Corrente Nominal
const In = (S * 1000) / (Math.sqrt(3) * V);
// Corrente de Curto-Circuito Simétrica (Icc = In / Z%)
const Icc = In / (imp / 100);
const Icc_kA = Icc / 1000;
// Corrente Assimétrica de pico (Aprox 2.5x Icc simétrica em BT)
const Icc_pico = Icc_kA * 2.5;
return { In, Icc, Icc_kA, Icc_pico, imp };
}, [potencia, z, tensao]);
useEffect(() => {
window._VL_lastExport = () => calc ? buildReport("curto-circuito", {
potencia_trafo_kVA: potencia, impedancia_pct: z, tensao_V: tensao
}, {
icc_simetrica_kA: calc.Icc_kA.toFixed(2), icc_pico_kA: calc.Icc_pico.toFixed(2)
}) : null;
return () => { window._VL_lastExport = null; };
}, [calc]);
return (
DADOS DE ENTRADA · IEC 60909
Parâmetros da Fonte (Transformador)
);
};
/* ===========================================================
7. SPDA (SISTEMA DE PROTEÇÃO CONTRA DESCARGAS ATMOSFÉRICAS)
Baseado na NBR 5419-3:2015 (Tabelas 2 e 4)
=========================================================== */
const SPDA = () => {
const [classe, setClasse] = usePersist("vl_spda_classe", "III");
const [altura, setAltura] = usePersist("vl_spda_h", "15");
const [comprimento, setComprimento] = usePersist("vl_spda_c", "30");
const [largura, setLargura] = usePersist("vl_spda_l", "20");
const calc = useMemo(() => {
const h = parseFloat(altura), c = parseFloat(comprimento), l = parseFloat(largura);
if (!h || !c || !l) return null;
// Parâmetros NBR 5419-3:2015 (Tabelas 2 e 4)
// r: Raio da Esfera Rolante (m) | malha: Tamanho da Malha (m) | descida: Espaçamento médio (m)
const param = {
"I": { r: 20, malha: 5, descida: 10 },
"II": { r: 30, malha: 10, descida: 10 },
"III": { r: 45, malha: 15, descida: 15 },
"IV": { r: 60, malha: 20, descida: 20 }
}[classe];
const perimetro = 2 * (c + l);
// NBR 5419-3: O número mínimo de condutores de descida é 2, posicionados no perímetro
let nDescidas = Math.ceil(perimetro / param.descida);
if (nDescidas < 2) nDescidas = 2;
// Aplicação do Método do Ângulo de Proteção (Franklin)
// Só é válido se a altura da edificação for menor ou igual ao raio da esfera da classe escolhida.
const franklinAplicavel = h <= param.r;
return { h, c, l, perimetro, param, nDescidas, franklinAplicavel };
}, [classe, altura, comprimento, largura]);
useEffect(() => {
window._VL_lastExport = () => calc ? buildReport("spda", {
classe_protecao: classe, altura_m: altura, comprimento_m: comprimento, largura_m: largura
}, {
raio_esfera_rolante_m: calc.param.r,
malha_captores_m: `${calc.param.malha} x ${calc.param.malha}`,
descidas_minimas: calc.nDescidas,
espacamento_descidas_m: calc.param.descida
}) : null;
return () => { window._VL_lastExport = null; };
}, [calc, classe, altura, comprimento, largura]);
return (
DADOS DE ENTRADA · NBR 5419-3:2015
Geometria e Nível de Proteção
ATUALIZADO
);
};
/* Export to window for app.jsx to use */
Object.assign(window, {
VL_Corrente: Corrente,
VL_QuedaTensao: QuedaTensao,
VL_Dimensionamento: Dimensionamento,
VL_Capacitor: Capacitor,
VL_Transformador: Transformador,
VL_CurtoCircuito: CurtoCircuito,
// ADICIONE ESTA LINHA:
VL_SPDA: SPDA,
VL_Icon, VL_ResultBig: ResultBig, VL_MetricCell: MetricCell,
// ... resto do código
});