/* global React, ReactDOM */
const { useState, useEffect, useRef, useMemo } = React;
/* ===========================================================
HOOK: Persistência automática via localStorage
=========================================================== */
const useLocalStorage = (key, initial) => {
const [state, setState] = useState(() => {
try {
const raw = localStorage.getItem(key);
return raw !== null ? JSON.parse(raw) : initial;
} catch { return initial; }
});
useEffect(() => {
try { localStorage.setItem(key, JSON.stringify(state)); }
catch { /* Quota exceeded - silencioso */ }
}, [key, state]);
return [state, setState];
};
window.useLocalStorage = useLocalStorage;
/* ===========================================================
TWEAKS DEFAULTS
=========================================================== */
const TWEAK_DEFAULTS = {
"accent": "#3D8BFF",
"density": "comfortable",
"showCircuitry": true,
"monoReadouts": true
};
const ACCENT_MAP = {
"#3D8BFF": { primary: "oklch(72% 0.18 245)", glow: "oklch(72% 0.22 245 / 0.35)", soft: "oklch(72% 0.18 245 / 0.12)" },
"#3DD6E6": { primary: "oklch(82% 0.14 200)", glow: "oklch(82% 0.18 200 / 0.40)", soft: "oklch(82% 0.14 200 / 0.12)" },
"#5BE38C": { primary: "oklch(84% 0.18 145)", glow: "oklch(84% 0.22 145 / 0.35)", soft: "oklch(84% 0.18 145 / 0.12)" },
"#F2B043": { primary: "oklch(80% 0.16 75)", glow: "oklch(80% 0.20 75 / 0.35)", soft: "oklch(80% 0.16 75 / 0.12)" },
};
/* ===========================================================
ICONS — minimal stroke icons
=========================================================== */
const Dropdown = ({ value, onChange, options }) => {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const onDocClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener("mousedown", onDocClick);
return () => document.removeEventListener("mousedown", onDocClick);
}, [open]);
const current = options.find(o => o.value === value) || options[0];
return (
{open && (
{options.map(o => (
{ onChange(o.value); setOpen(false); }}
>{o.label}
))}
)}
);
};
const Icon = ({ name, size = 18, stroke = 1.5 }) => {
const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" };
const paths = {
dashboard: <>>,
calc: <>>,
ruler: <>>,
bolt: <>>,
wave: <>>,
pipe: <>>,
cable: <>>,
flash: <>>,
shield: <>>,
transformer: <>>,
sigma: <>>,
chart: <>>,
bulb: <>>,
diagram: <>>,
refresh: <>>,
settings: <>>,
arrow: <>>,
arrowL: <>>,
check: <>>,
plus: <>>,
user: <>>,
search: <>>,
bell: <>>,
download: <>>,
spark: <>>,
grid: <>>,
lock: <>>,
mail: <>>,
eye: <>>,
};
return ;
};
/* ===========================================================
BACKGROUND — animated circuitry SVG layer
=========================================================== */
const Circuitry = ({ show }) => {
if (!show) return null;
return (
);
};
/* ===========================================================
BRAND MARK — logo "V" (volt) bicolor, segue o --accent do tema
=========================================================== */
const BrandMark = ({ size = 28 }) => (
);
window.VL_BrandMark = BrandMark;
/* ===========================================================
LANDING / LOGIN / CADASTRO
=========================================================== */
const Landing = ({ onEnter, onGuest, showCircuitry }) => {
const [pwVisible, setPwVisible] = useState(false);
const [mode, setMode] = useState("login"); // "login" | "register" | "verify" | "forgot"
const [email, setEmail] = useState("");
const [senha, setSenha] = useState("");
const [nome, setNome] = useState("");
const [codigo, setCodigo] = useState("");
const [manterSessao, setManterSessao] = useState(true);
const [erro, setErro] = useState("");
const [busy, setBusy] = useState(false);
const fpRef = useRef("");
useEffect(() => {
if (window.VL_fingerprint) {
window.VL_fingerprint().then(fp => { fpRef.current = fp; });
}
}, []);
const concluirLogin = (res) => {
window.VL_AUTH.setToken(res.token, manterSessao);
window.VL_SESSION = { token: res.token, user: res.user, plan: res.plan, fingerprint: fpRef.current };
onEnter();
};
const handleLogin = async (e) => {
e.preventDefault();
setErro("");
if (!window.VL_AUTH) { onEnter(); return; }
if (!email || !senha) { setErro("Preencha e-mail e senha."); return; }
setBusy(true);
const res = await window.VL_AUTH.login({
email: email.trim().toLowerCase(),
senha,
fingerprint: fpRef.current,
remember: manterSessao,
});
setBusy(false);
if (!res.ok) { setErro(res.error); return; }
concluirLogin(res);
};
const handleRegister = async (e) => {
e.preventDefault();
setErro("");
if (!window.VL_AUTH) { onEnter(); return; }
if (!nome.trim()) { setErro("Informe seu nome completo."); return; }
if (!email.includes("@")) { setErro("E-mail inválido."); return; }
if (senha.length < 6) { setErro("A senha deve ter ao menos 6 caracteres."); return; }
setBusy(true);
const emailNorm = email.trim().toLowerCase();
const res = await window.VL_AUTH.requestRegistrationCode({ nome: nome.trim(), email: emailNorm, senha });
setBusy(false);
if (!res.ok) { setErro(res.error); return; }
alert(`VoltLab — Verificação de conta\n\nEnviamos um código de confirmação para ${emailNorm}.\n\nSeu código é: ${res.codeForTesting}\n\nDigite-o na próxima tela para concluir o cadastro.`);
setMode("verify");
};
const handleVerify = async (e) => {
e.preventDefault();
setErro("");
if (!codigo) { setErro("Informe o código de 6 dígitos."); return; }
setBusy(true);
const emailNorm = email.trim().toLowerCase();
const reg = await window.VL_AUTH.verifyAndRegister({ email: emailNorm, code: codigo });
if (!reg.ok) { setBusy(false); setErro(reg.error); return; }
const res = await window.VL_AUTH.login({ email: emailNorm, senha, fingerprint: fpRef.current, remember: manterSessao });
setBusy(false);
if (!res.ok) { setErro(res.error); return; }
concluirLogin(res);
};
const handleForgot = (e) => {
e.preventDefault();
alert("Se o e-mail estiver cadastrado, um link de recuperação foi enviado.");
setMode("login");
};
return (
v 4.2 — ENGINEERING SUITE
Plataforma inteligente
de engenharia elétrica
Dimensionamento normativo, análise de circuitos e ferramentas técnicas
em um único ambiente — projetado para engenheiros que exigem precisão.
{/* MODO DE ACESSO — sem conta */}
{mode === "login" && (
<>
ACESSO LIVRE · SEM CADASTRO
Entrar na plataforma
Acesse o ambiente de projetos diretamente — sem conta e sem senha. Seus dados ficam salvos apenas neste navegador.
Todos os módulos técnicos liberados. Nenhum dado é enviado para servidores.
>
)}
{/* MODO DE CADASTRO */}
{mode === "register" && (
<>
NOVA CONTA
Criar Conta
Crie sua conta para obter acesso aos módulos técnicos.
>
)}
{/* MODO VERIFICAR CÓDIGO */}
{mode === "verify" && (
<>
VERIFICAÇÃO DE SEGURANÇA
Confirme seu e-mail
Enviamos um código de 6 dígitos para {email}. Insira-o abaixo para liberar seu acesso.
>
)}
{/* MODO RECUPERAÇÃO DE SENHA */}
{mode === "forgot" && (
<>
RECUPERAÇÃO DE ACESSO
Redefinir senha
Informe o e-mail cadastrado e enviaremos um link seguro para você criar uma nova senha.
>
)}
);
};
/* ===========================================================
SIDEBAR
=========================================================== */
const NAV = [
{ id: "dashboard", label: "Dashboard", icon: "dashboard", section: "PRINCIPAL" },
{ id: "calculators", label: "Calculadoras", icon: "calc", section: "PRINCIPAL" },
{ id: "current", label: "Corrente Elétrica", icon: "bolt", section: "MÓDULOS TÉCNICOS", badge: "NOVO" },
{ id: "drop", label: "Queda de Tensão", icon: "wave", section: "MÓDULOS TÉCNICOS", badge: "NOVO" },
{ id: "cables", label: "Dimensionamento", icon: "ruler", section: "MÓDULOS TÉCNICOS", badge: "NOVO" },
{ id: "secao", label: "Seção do Condutor", icon: "cable", section: "MÓDULOS TÉCNICOS", badge: "NOVO" },
{ id: "eletroduto", label: "Taxa de Ocupação", icon: "pipe", section: "MÓDULOS TÉCNICOS", badge: "ATIVO" },
{ id: "fp", label: "Banco Capacitor", icon: "sigma", section: "MÓDULOS TÉCNICOS", badge: "NOVO" },
{ id: "short", label: "Curto-Circuito", icon: "flash", section: "MÓDULOS TÉCNICOS" },
{ id: "spda", label: "SPDA", icon: "shield", section: "MÓDULOS TÉCNICOS" },
{ id: "trafo", label: "Transformadores", icon: "transformer", section: "MÓDULOS TÉCNICOS" },
{ id: "motores", label: "Quadro de Motores", icon: "bolt", section: "MÓDULOS TÉCNICOS", badge: "NOVO" },
{ id: "demand", label: "Demandas", icon: "chart", section: "MÓDULOS TÉCNICOS" },
{ id: "light", label: "Iluminação", icon: "bulb", section: "MÓDULOS TÉCNICOS" },
{ id: "aterramento", label: "Aterramento", icon: "spark", section: "MÓDULOS TÉCNICOS" },
{ id: "quadros", label: "Quadros & Partidas", icon: "diagram", section: "FERRAMENTAS", badge: "NOVO" },
{ id: "montagem", label: "Montagem de Quadros", icon: "grid", section: "FERRAMENTAS", badge: "NOVO" },
{ id: "infra", label: "Infraestrutura", icon: "pipe", section: "FERRAMENTAS", badge: "NOVO" },
{ id: "cad", label: "Desenho Técnico", icon: "ruler", section: "FERRAMENTAS", badge: "NOVO" },
{ id: "diagrams", label: "Diagramas Unifilares", icon: "diagram", section: "FERRAMENTAS" },
{ id: "simulador", label: "Simulador de Circuitos", icon: "flash", section: "FERRAMENTAS", badge: "NOVO" },
{ id: "convert", label: "Conversores", icon: "refresh", section: "FERRAMENTAS" },
{ id: "settings", label: "Configurações", icon: "settings", section: "SISTEMA" },
{ id: "minha-conta", label: "Minha Conta", icon: "user", section: "CONTA" },
];
const Sidebar = ({ current, onNav, onLogout }) => {
const isAdmin = window.VL_SESSION?.user?.role === 'ADMIN';
const sections = useMemo(() => {
const map = {};
NAV
.filter(n => !n.adminOnly || isAdmin)
.forEach(n => { (map[n.section] = map[n.section] || []).push(n); });
return map;
}, [isAdmin]);
return (
);
};
/* ===========================================================
APP HEADER
=========================================================== */
const AppHeader = ({ title, eyebrow, crumbs, onAction, actionLabel = "Novo cálculo", current, onExportReport, onHeaderNav, onMenu }) => (
{crumbs.map((c, i) => (
{c}
{i < crumbs.length - 1 && /}
))}
{eyebrow}
{title}
{["current","drop","cables","fp"].includes(current) && onExportReport && (
)}
);
/* ===========================================================
DASHBOARD
=========================================================== */
const KPI = ({ label, value, unit, delta, trend, spark }) => (
{label}
{delta}
{value}
{unit}
);
const Dashboard = ({ onOpenCalc }) => {
const userName = window.VL_SESSION?.user?.nome?.split(' ')[0] || "Engenheiro";
const userAcessos = window.VL_SESSION?.user?.acessos || 1;
const getGreeting = () => {
const hour = new Date().getHours();
if (hour >= 5 && hour < 12) return "Bom dia";
if (hour >= 12 && hour < 18) return "Boa tarde";
return "Boa noite";
};
const greeting = getGreeting();
return (
AMBIENTE DE TRABALHO · {userName.toUpperCase()}
{greeting}, {userName}. Seu ambiente está pronto.
Você já acessou a plataforma {userAcessos} {userAcessos === 1 ? 'vez' : 'vezes'}.
RECENTES
Cálculos do projeto atual
IDMóduloProjetoResultadoStatus
Nenhum cálculo realizado recentemente.
Acesse as calculadoras pelo menu lateral para começar.
ATALHOS
Módulos frequentes
{[
["eletroduto","Eletroduto","pipe","NBR 5410"],
["drop","Queda de Tensão","wave","≤ 4%"],
["short","Curto-Circuito","flash","Icc·t²"],
["cables","Cabos","cable","Ampacidade"],
["spda","SPDA","shield","NBR 5419"],
["montagem","Montagem de Quadros","grid","Layout DIN"],
["light","Iluminação","bulb","8995-1"],
["aterramento","Aterramento","spark","R ≤ 10Ω"],
].map(([id, label, icon, sub]) => (
))}
CURVA DE DEMANDA · 24H
Geral — 13.8/0.38 kV
Demanda
Reativa
Limite
A curva será gerada assim que houver dados de demanda.
);
};
/* ===========================================================
CALCULATORS GRID
=========================================================== */
const CALCS = [
{ id: "eletroduto", icon: "pipe", name: "Eletroduto", norm: "NBR 5410 — Anexo H", tag: "Dimensionamento", desc: "Cálculo de ocupação e bitola mínima de eletroduto para múltiplos condutores." },
{ id: "drop", icon: "wave", name: "Queda de Tensão", norm: "NBR 5410 6.2.7", tag: "Análise", desc: "Cálculo de ΔV em circuitos monofásicos, bifásicos e trifásicos." },
{ id: "current", icon: "bolt", name: "Corrente Nominal", norm: "IEC 60364", tag: "Análise", desc: "Determinação da corrente de projeto a partir da carga e tensão." },
{ id: "cables", icon: "cable", name: "Dimensionamento de Cabos", norm: "NBR 5410 — Tabela 36", tag: "Dimensionamento", desc: "Bitola por ampacidade, queda de tensão e curto-circuito." },
{ id: "secao", icon: "cable", name: "Seção do Condutor", norm: "NBR 5410 — item 6.2", tag: "Dimensionamento", desc: "Seção por todos os critérios: ampacidade, queda, seção mínima, sobrecarga e curto." },
{ id: "short", icon: "flash", name: "Corrente de Curto-Circuito", norm: "IEC 60909", tag: "Proteção", desc: "Cálculo de Icc simétrica e assimétrica em barramentos." },
{ id: "spda", icon: "shield", name: "SPDA", norm: "NBR 5419", tag: "Proteção", desc: "Classificação do SPDA, malha e número de captores." },
{ id: "trafo", icon: "transformer", name: "Transformadores", norm: "NBR 5440", tag: "Equipamento", desc: "Dimensionamento de potência e correntes primária/secundária." },
{ id: "motores", icon: "bolt", name: "Quadro de Motores", norm: "WEG · NBR 5410", tag: "Equipamento", desc: "Lista de motores: corrente nominal, fator de partida e dimensionamento de cabo." },
{ id: "fp", icon: "sigma", name: "Banco de Capacitores", norm: "ANEEL 414/2010", tag: "Qualidade", desc: "Dimensionamento do banco capacitivo para correção de fator de potência." },
{ id: "demand", icon: "chart", name: "Demandas", norm: "Concessionárias", tag: "Projeto", desc: "Demanda provável por tipologia de unidade consumidora." },
{ id: "light", icon: "bulb", name: "Iluminação", norm: "NBR ISO 8995", tag: "Conforto", desc: "Método dos lumens, fator de utilização e densidade luminosa." },
{ id: "quadros", icon: "diagram", name: "Quadros & Partidas", norm: "WEG · IEC 60947", tag: "Ferramentas", desc: "Diagrama interativo de quadros de partida (direta e estrela-triângulo) com componentes WEG." },
{ id: "cad", icon: "ruler", name: "Desenho Técnico", norm: "CAD 2D", tag: "Ferramentas", desc: "Editor CAD com pan/zoom, snap-grid, ferramentas de desenho, biblioteca de símbolos e camadas." },
{ id: "diagrams", icon: "diagram", name: "Diagramas Unifilares", norm: "—", tag: "Ferramentas", desc: "Editor para diagramas unifilares e multifilares simplificados." },
{ id: "simulador", icon: "flash", name: "Simulador de Circuitos", norm: "MNA · Transitório", tag: "Ferramentas", desc: "Simulação interativa de circuitos (estilo Falstad) com análise nodal e transitória." },
{ id: "convert", icon: "refresh", name: "Conversores", norm: "SI · USCS", tag: "Ferramentas", desc: "Conversão entre unidades elétricas, mecânicas e térmicas." },
{ id: "infra", icon: "pipe", name: "Infraestrutura", norm: "Quantitativo / BOM", tag: "Ferramentas", desc: "Quantitativo de eletrocalhas, leitos, perfilados e eletrodutos por distância." },
];
const CALC_TAGS = {
"Dimensionamento": { hue: 245, icon: "pipe" },
"Análise": { hue: 200, icon: "wave" },
"Proteção": { hue: 45, icon: "shield" },
"Equipamento": { hue: 290, icon: "transformer" },
"Qualidade": { hue: 150, icon: "sigma" },
"Projeto": { hue: 330, icon: "chart" },
"Conforto": { hue: 95, icon: "bulb" },
"Ferramentas": { hue: 265, icon: "diagram" },
};
const tagTone = (tag) => `oklch(74% 0.135 ${(CALC_TAGS[tag]?.hue ?? 245)})`;
const tagSoft = (tag) => `oklch(74% 0.135 ${(CALC_TAGS[tag]?.hue ?? 245)} / 0.13)`;
const Calculators = ({ onOpen }) => {
const [filter, setFilter] = useState("Todos");
const [query, setQuery] = useState("");
const tags = ["Todos", ...Array.from(new Set(CALCS.map(c => c.tag)))];
const list = CALCS.filter(c => {
const okTag = filter === "Todos" || c.tag === filter;
const q = query.trim().toLowerCase();
const okQuery = !q || [c.name, c.norm, c.desc, c.tag].join(" ").toLowerCase().includes(q);
return okTag && okQuery;
});
const tagCount = (t) => t === "Todos" ? CALCS.length : CALCS.filter(c => c.tag === t).length;
return (
{tags.map(t => (
))}
{list.length === 0 ? (
Nenhum módulo encontrado
Tente outro termo ou limpe o filtro de categoria.
) : (
{list.map((c, i) => (
))}
)}
);
};
/* ===========================================================
TAXA DE OCUPAÇÃO — eletrodutos, eletrocalhas e leitos
=========================================================== */
const INFRA_TYPES = [
{ id: "ELETRODUTO", label: "Eletroduto", shape: "circle" },
{ id: "ELETROCALHA", label: "Eletrocalha", shape: "rect" },
{ id: "LEITO", label: "Leito", shape: "rect" },
];
const CABLE_BITOLAS = ["1.5","2.5","4","6","10","16","25","35","50","70","95","120","150","185","240"];
const Eletroduto = () => {
const D = window.VL_DATA;
const [rows, setRows] = useLocalStorage("vl_eletroduto_rows", [
{ bit: "2.5", qty: 3, iso: "PVC" },
{ bit: "4", qty: 2, iso: "PVC" },
{ bit: "10", qty: 1, iso: "PVC" },
]);
const [infra, setInfra] = useLocalStorage("vl_eletroduto_infra", "ELETRODUTO");
const [material, setMaterial] = useLocalStorage("vl_eletroduto_material", "PVC");
const [dim, setDim] = useLocalStorage("vl_eletroduto_dim", "");
const infraType = INFRA_TYPES.find(t => t.id === infra);
const dimOptions = useMemo(() => {
if (!D) return [];
if (infra === "ELETRODUTO") {
const obj = D.conduits[material] || {};
return Object.entries(obj).map(([k,area]) => ({ key: k, area, label: k }));
}
if (infra === "ELETROCALHA") return Object.entries(D.eletrocalhas).map(([k,v]) => ({ key: k, ...v, label: k + " mm" }));
return Object.entries(D.leitos).map(([k,v]) => ({ key: k, ...v, label: k + " mm" }));
}, [infra, material, D]);
const totalCond = rows.reduce((s,r) => s + r.qty, 0);
const cableList = useMemo(() => {
if (!D) return [];
return rows.flatMap(r => {
const d = D.cableDiameters[r.bit]?.[r.iso] || 0;
const a = Math.PI * (d/2)**2;
return Array.from({length: r.qty}, () => ({ bit: r.bit, iso: r.iso, d, a }));
});
}, [rows, D]);
const totalArea = cableList.reduce((s,c) => s + c.a, 0);
const limitPct = useMemo(() => {
if (infra === "ELETRODUTO") return totalCond === 1 ? 53 : totalCond <= 3 ? 31 : 40;
return 40;
}, [infra, totalCond]);
const autoDim = useMemo(() => {
if (!dimOptions.length) return null;
const required = totalArea / (limitPct/100);
return dimOptions.find(d => d.area >= required) || dimOptions[dimOptions.length - 1];
}, [dimOptions, totalArea, limitPct]);
const selectedDim = useMemo(() => {
if (!dim) return autoDim;
return dimOptions.find(d => d.key === dim) || autoDim;
}, [dim, dimOptions, autoDim]);
const occupancyPct = selectedDim ? (totalArea / selectedDim.area) * 100 : 0;
const status = occupancyPct <= limitPct ? "ok" : "warn";
const updateRow = (i, patch) => setRows(rs => rs.map((r,idx) => idx===i ? {...r, ...patch} : r));
const removeRow = (i) => setRows(rs => rs.filter((_,idx) => idx!==i));
const addRow = () => setRows(rs => [...rs, { bit: "2.5", qty: 1, iso: "PVC" }]);
if (!D) return Carregando dados normativos...
;
return (
ENTRADA · INFRAESTRUTURA E CABOS
Configuração da bandeja, leito ou eletroduto
AUTO-CÁLCULO
{INFRA_TYPES.map(t => (
))}
{infra === "ELETRODUTO" ? (
{Object.entries(D.conduitMaterials).filter(([k]) => D.conduits[k]).map(([k,label]) => (
))}
) : (
40%
Eletrocalhas e leitos · NBR 5410
)}
#
Bitola
Isolação
Quantidade
Área unit.
{rows.map((r,i) => {
const d = D.cableDiameters[r.bit]?.[r.iso] || 0;
const unit = Math.PI * (d/2)**2;
return (
{String(i+1).padStart(2,"0")}
updateRow(i, { bit: v })}
options={CABLE_BITOLAS.map(b => ({ value: b, label: `${b} mm²` }))}
/>
updateRow(i, { iso: v })}
options={[
{ value: "PVC", label: "PVC" },
{ value: "EPR", label: "EPR" },
{ value: "XLPE", label: "XLPE" },
]}
/>
{String(r.qty).padStart(2,"0")}
Ø{d.toFixed(1)} · {(unit*r.qty).toFixed(1)} mm²
);
})}
Total condutores
{String(totalCond).padStart(2,"0")}
Área total ocupada
{totalArea.toFixed(1)} mm²
Limite NBR aplicado
{limitPct} %
Área mínima necessária
{(totalArea/(limitPct/100)).toFixed(1)} mm²
REFERÊNCIA TÉCNICA · DIMENSÕES DISPONÍVEIS
{infra === "ELETRODUTO" ? `Eletroduto ${D.conduitMaterials[material]}` : infra === "ELETROCALHA" ? "Eletrocalhas perfuradas" : "Leitos para cabos"}
Fonte: NBR 5410 / catálogos técnicos
{infra === "ELETRODUTO" ? "NOMINAL" : "L×A (mm)"}
ÁREA ÚTIL (mm²)
ÁREA NECESSÁRIA
STATUS
UTILIZAÇÃO
{dimOptions.map(c => {
const required = totalArea/(limitPct/100);
const ok = c.area >= required;
const isSel = selectedDim && c.key === selectedDim.key;
const util = (totalArea / c.area) * 100;
return (
setDim(c.key)} style={{cursor:"pointer"}}>
{c.label}
{c.area}
{required.toFixed(0)} mm²
{ok ? "Atende" : "Insuficiente"}
{util.toFixed(0)}%
);
})}
);
};
const ConduitDiagram = ({ totalArea, pipeArea, cables }) => {
const R = 90;
const pipeArea_px = Math.PI * R * R;
const scale = pipeArea_px / pipeArea;
const list = cables.map(c => ({ rad: Math.sqrt((c.a * scale) / Math.PI), bit: c.bit }));
list.sort((a,b) => b.rad - a.rad);
const placed = [];
list.forEach(c => {
if (placed.length === 0) {
placed.push({ x: 0, y: 0, rad: c.rad });
return;
}
let placedOk = false;
const maxRingR = R - c.rad - 1;
for (let ringR = c.rad + 2; ringR <= maxRingR && !placedOk; ringR += 1) {
const steps = Math.max(8, Math.floor((2*Math.PI*ringR) / (c.rad*1.4)));
for (let i = 0; i < steps; i++) {
const ang = (i / steps) * Math.PI * 2;
const x = Math.cos(ang) * ringR;
const y = Math.sin(ang) * ringR;
const ok = placed.every(p => {
const dx = p.x - x, dy = p.y - y;
return Math.sqrt(dx*dx + dy*dy) >= (p.rad + c.rad) * 1.01;
});
if (ok) {
placed.push({ x, y, rad: c.rad });
placedOk = true;
break;
}
}
}
});
return (
);
};
const TrayDiagram = ({ dims, cables }) => {
if (!dims) return null;
const VBW = 240, VBH = 130;
const padX = 14, padY = 12;
const innerW = VBW - padX*2 - 12;
const aspect = dims.largura / dims.altura;
let trayW = innerW, trayH = trayW / aspect;
if (trayH > VBH - padY*2 - 12) {
trayH = VBH - padY*2 - 12;
trayW = trayH * aspect;
}
const trayX = (VBW - trayW)/2;
const trayY = (VBH - trayH)/2;
const trayAreaPx = trayW * trayH;
const trayAreaMm = dims.area;
const scale = trayAreaPx / trayAreaMm;
const list = cables.map(c => ({ rad: Math.sqrt((c.a * scale) / Math.PI) }));
list.sort((a,b) => b.rad - a.rad);
const placed = [];
let x = trayX + 4, y = trayY + trayH - 4 - (list[0]?.rad || 0);
let rowH = 0;
list.forEach(c => {
if (x + c.rad*2 > trayX + trayW - 4) {
x = trayX + 4;
y -= rowH*2 + 2;
rowH = 0;
}
placed.push({ x: x + c.rad, y: y, rad: c.rad });
x += c.rad*2 + 2;
if (c.rad > rowH) rowH = c.rad;
});
return (
);
};
/* ===========================================================
PLACEHOLDER VIEW
=========================================================== */
const Placeholder = ({ name }) => (
Módulo "{name}"
Esta calculadora segue a mesma arquitetura visual e técnica da calculadora de eletroduto. Disponível em breve no protótipo.
MÓDULO EM CONFIGURAÇÃO · NBR ASSOCIADA
);
/* ===========================================================
APP ROOT
=========================================================== */
const APP_TITLES = {
dashboard: { eyebrow: "VISÃO GERAL", title: "Workspace de engenharia", crumbs: ["VOLTLAB","Dashboard"] },
calculators: { eyebrow: "BIBLIOTECA", title: "Catálogo de calculadoras", crumbs: ["VOLTLAB","Calculadoras"] },
eletroduto: { eyebrow: "INFRAESTRUTURA · NBR 5410", title: "Taxa de ocupação", crumbs: ["VOLTLAB","Calculadoras","Taxa de Ocupação"] },
current: { eyebrow: "CIRCUITOS · NBR 5410", title: "Corrente elétrica", crumbs: ["VOLTLAB","Calculadoras","Corrente"] },
drop: { eyebrow: "CIRCUITOS · NBR 5410", title: "Queda de tensão", crumbs: ["VOLTLAB","Calculadoras","Queda de Tensão"] },
cables: { eyebrow: "DIMENSIONAMENTO · NBR 5410", title: "Dimensionamento de cabos", crumbs: ["VOLTLAB","Calculadoras","Cabos"] },
fp: { eyebrow: "QUALIDADE DE ENERGIA · ANEEL 414", title: "Banco de capacitores", crumbs: ["VOLTLAB","Calculadoras","Banco de Capacitores"] },
quadros: { eyebrow: "FERRAMENTAS · DIMENSIONAMENTO WEG", title: "Quadros & Diagramas de Partidas", crumbs: ["VOLTLAB","Ferramentas","Quadros & Partidas"] },
cad: { eyebrow: "FERRAMENTAS · DESENHO 2D", title: "Editor CAD", crumbs: ["VOLTLAB","Ferramentas","Desenho Técnico"] },
light: { eyebrow: "ILUMINAÇÃO · NBR ISO/CIE 8995-1", title: "Dimensionamento de Iluminação", crumbs: ["VOLTLAB","Calculadoras","Iluminação"] },
aterramento: { eyebrow: "ATERRAMENTO · MÉTODO DAS RESISTÊNCIAS MÚTUAS", title: "Resistência de Aterramento", crumbs: ["VOLTLAB","Calculadoras","Aterramento"] },
montagem: { eyebrow: "FERRAMENTAS · MONTAGEM DE QUADROS ELÉTRICOS", title: "Montagem de Quadros — Layout DIN", crumbs: ["VOLTLAB","Ferramentas","Montagem de Quadros"] },
secao: { eyebrow: "DIMENSIONAMENTO · NBR 5410 (item 6.2)", title: "Seção do Condutor", crumbs: ["VOLTLAB","Calculadoras","Seção do Condutor"] },
simulador: { eyebrow: "FERRAMENTAS · SIMULAÇÃO DE CIRCUITOS", title: "Simulador de Circuitos", crumbs: ["VOLTLAB","Ferramentas","Simulador"] },
demand: { eyebrow: "MÓDULOS TÉCNICOS", title: "Cálculo de Demandas", crumbs: ["VOLTLAB","Calculadoras","Demandas"] },
motores: { eyebrow: "EQUIPAMENTOS · MOTORES", title: "Quadro de Motores", crumbs: ["VOLTLAB","Calculadoras","Quadro de Motores"] },
convert: { eyebrow: "FERRAMENTAS", title: "Conversores Elétricos", crumbs: ["VOLTLAB","Ferramentas","Conversores"] },
infra: { eyebrow: "FERRAMENTAS · INFRAESTRUTURA", title: "Cálculo de Infraestrutura", crumbs: ["VOLTLAB","Ferramentas","Infraestrutura"] },
settings: { eyebrow: "SISTEMA · PREFERÊNCIAS", title: "Configurações", crumbs: ["VOLTLAB","Sistema","Configurações"] },
};
const App = () => {
const t = window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}];
const [tweaks, setTweak] = t;
useEffect(() => { window.Dropdown_VL = Dropdown; }, []);
const [screen, setScreen] = useState(() => window.location.hash === "#app" ? "app" : "landing");
const [current, setCurrent] = useState("dashboard");
const [mobileNav, setMobileNav] = useState(false);
const [guest, setGuest] = useState(false);
useEffect(() => {
const a = ACCENT_MAP[tweaks.accent] || ACCENT_MAP["#3D8BFF"];
document.documentElement.style.setProperty("--accent", a.primary);
document.documentElement.style.setProperty("--accent-glow", a.glow);
document.documentElement.style.setProperty("--accent-soft", a.soft);
document.documentElement.style.setProperty("--accent-2", "oklch(82% 0.14 200)");
document.documentElement.dataset.density = tweaks.density;
document.documentElement.dataset.mono = tweaks.monoReadouts ? "on" : "off";
}, [tweaks]);
const goCalc = (id) => {
setCurrent(id);
};
const enterApp = () => {
window.location.hash = "#app";
setScreen("app");
};
const enterGuest = () => {
window.VL_SESSION = { token: "guest", user: { nome: "Visitante", email: "", role: "GUEST" }, plan: null, guest: true };
setGuest(true);
enterApp();
};
const goLanding = () => {
try { history.replaceState(null, "", window.location.pathname + window.location.search); }
catch { window.location.hash = ""; }
setScreen("landing");
};
const doLogout = () => {
if (window.VL_AUTH) window.VL_AUTH.logout();
window.VL_SESSION = null;
setGuest(false);
goLanding();
};
const renderView = () => {
const guard = (id, component) => {
if (!window.VL_moduloLiberado || window.VL_moduloLiberado(id)) return component;
if (window.VL_ModuloBloqueado) {
return window.VL_goUpgrade && window.VL_goUpgrade(id)}
/>;
}
return component;
};
if (current === "dashboard") return ;
if (current === "calculators") return ;
if (current === "settings" && window.VL_Settings) return ;
if (current === "minha-conta" && window.VL_UserPanel && window.VL_SESSION)
return setCurrent("dashboard")} />;
if (current === "eletroduto") return guard("eletroduto", );
if (current === "current" && window.VL_Corrente) return guard("current", );
if (current === "drop" && window.VL_QuedaTensao) return guard("drop", );
if (current === "cables" && window.VL_Dimensionamento) return guard("cables", );
if (current === "fp" && window.VL_Capacitor) return guard("fp", );
if (current === "quadros" && window.VL_Quadros) return guard("quadros", );
if (current === "cad" && window.VL_CAD) return guard("cad", );
if (current === "diagrams" && window.VL_Unifilar) return guard("diagrams", );
if (current === "demand" && window.VL_Demandas) return guard("demand", );
if (current === "convert" && window.VL_Conversores) return guard("convert", );
if (current === "infra" && window.VL_Infraestrutura) return guard("infra", );
if (current === "trafo" && window.VL_Transformador) return guard("trafo", );
if (current === "motores" && window.VL_QuadroMotores) return guard("motores", );
if (current === "short" && window.VL_CurtoCircuito) return guard("short", );
if (current === "spda" && window.VL_SPDA) return guard("spda", );
if (current === "light" && window.VL_Iluminacao) return guard("light", );
if (current === "aterramento" && window.ModuloAterramento) return guard("aterramento", );
if (current === "montagem" && window.VL_QuadroMontagem) return guard("montagem", );
if (current === "secao" && window.VL_SecaoCondutor) return guard("secao", );
if (current === "simulador" && window.SimuladorCircuitos) return guard("simulador", );
const found = NAV.find(n => n.id === current);
return ;
};
if (screen === "landing") {
return (
<>
{window.TweaksPanel && (
)}
>
);
}
const meta = APP_TITLES[current] || { eyebrow: "MÓDULO", title: NAV.find(n=>n.id===current)?.label || "", crumbs: ["VOLTLAB","Módulos", NAV.find(n=>n.id===current)?.label || ""] };
const appShell = (
{ setCurrent(id); setMobileNav(false); }} onLogout={doLogout}/>
setMobileNav(v => !v)}
onAction={() => setCurrent("eletroduto")}
actionLabel="Novo cálculo"
/>
{renderView()}
{window.VL_ProjectBar && }
);
return (window.VL_AuthGuard && !guest)
? {appShell}
: appShell;
};
const TweaksContent = ({ tweaks, setTweak }) => {
const T = window;
return (
<>
setTweak("accent", v)}
options={Object.keys(ACCENT_MAP)}
/>
setTweak("density", v)}
options={[{value:"comfortable",label:"Conforto"},{value:"compact",label:"Compacta"}]}
/>
setTweak("showCircuitry", v)}/>
setTweak("monoReadouts", v)}/>
>
);
};
ReactDOM.createRoot(document.getElementById("root")).render();