/* global React */
/* ===========================================================
VOLTLAB · ATERRAMENTO v2.1 (bugfix)
Resistência de Aterramento · Método das Resistências Mútuas
=========================================================== */
const { useState, useRef, useEffect, useCallback } = React;
// ─── Cálculo ─────────────────────────────────────────────────────────────────
function resistenciaPropria(rho, L, d) {
return (rho / (2 * Math.PI * L)) * Math.log(4 * L / d);
}
function resistenciaMutua(rho, Lm, b) {
if (b <= Lm) return 0;
return 0.183 * (rho / Lm) * Math.log10((b + Lm) / (b - Lm));
}
function calcularSistema(hastes, rho) {
const keys = Object.keys(hastes);
const n = keys.length;
if (n === 0) return null;
const matriz = Array.from({ length: n }, () => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
const hi = hastes[keys[i]], hj = hastes[keys[j]];
if (i === j) {
matriz[i][j] = resistenciaPropria(rho, hi.L, hi.D);
} else {
const e = Math.sqrt((hi.X - hj.X) ** 2 + (hi.Y - hj.Y) ** 2);
const b = Math.sqrt(hj.L ** 2 + e ** 2);
matriz[i][j] = resistenciaMutua(rho, hj.L, b);
}
}
}
const resPorHaste = matriz.map(row => row.reduce((a, v) => a + v, 0));
const somaRecip = resPorHaste.reduce((a, v) => (v > 0 ? a + 1 / v : a), 0);
const resTotal = somaRecip > 0 ? 1 / somaRecip : 0;
return { matriz, resPorHaste, resTotal, keys };
}
// ─── CSV parser ───────────────────────────────────────────────────────────────
function parseCSV(text) {
const lines = text.replace(/\r/g, '').split('\n').filter(Boolean);
const header = lines[0].split(',').map(h => h.trim().replace(/^\uFEFF/, ''));
const rows = lines.slice(1).map(line => {
const vals = line.split(',');
return Object.fromEntries(header.map((h, i) => [h, (vals[i] || '').trim()]));
});
const round = v => parseFloat(v.toFixed(4));
const hastes = {}, linhas = {}, mapaXY = {};
let cH = 1, cL = 1;
for (const row of rows) {
if (row['Name'] === 'Circle') {
const nome = 'haste_' + cH++;
const X = round(parseFloat(row['Center X']));
const Y = round(parseFloat(row['Center Y']));
hastes[nome] = { X, Y };
mapaXY[X + ',' + Y] = nome;
}
if (row['Name'] === 'Line') {
const nome = 'cabo_' + cL++;
const sx = round(parseFloat(row['Start X'])), sy = round(parseFloat(row['Start Y']));
const ex = round(parseFloat(row['End X'])), ey = round(parseFloat(row['End Y']));
linhas[nome] = { P_INICIAL: [sx, sy], P_FINAL: [ex, ey], COMPRIMENTO: parseFloat(row['Length']) };
}
}
for (const cabo of Object.values(linhas)) {
cabo.HASTE_DE = mapaXY[cabo.P_INICIAL[0] + ',' + cabo.P_INICIAL[1]] || null;
cabo.HASTE_PARA = mapaXY[cabo.P_FINAL[0] + ',' + cabo.P_FINAL[1]] || null;
}
return { hastes, linhas };
}
// ─── Classify (FORA do componente) ───────────────────────────────────────────
function classify(r) {
if (r <= 1) return { label: 'Excelente', pill: 'ok', col: 'var(--ok)' };
if (r <= 5) return { label: 'Bom', pill: 'ok', col: 'var(--ok)' };
if (r <= 10) return { label: 'Aceitável', pill: 'warn', col: 'var(--warn)' };
return { label: 'Verificar', pill: 'warn', col: 'var(--danger)' };
}
// ─── Canvas (FORA do componente) ─────────────────────────────────────────────
function GradeCanvas(props) {
var hastes = props.hastes, linhas = props.linhas, resPorHaste = props.resPorHaste, keys = props.keys;
var canvasRef = useRef(null);
useEffect(function() {
var canvas = canvasRef.current;
if (!canvas) return;
var ctx = canvas.getContext('2d');
var W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
var hasteList = Object.values(hastes);
if (!hasteList.length) return;
var xs = hasteList.map(function(h) { return h.X; });
var ys = hasteList.map(function(h) { return h.Y; });
var minX = Math.min.apply(null, xs), maxX = Math.max.apply(null, xs);
var minY = Math.min.apply(null, ys), maxY = Math.max.apply(null, ys);
var pad = 52, rangeX = maxX - minX || 1, rangeY = maxY - minY || 1;
var scale = Math.min((W - pad * 2) / rangeX, (H - pad * 2) / rangeY);
function toC(x, y) { return [pad + (x - minX) * scale, H - pad - (y - minY) * scale]; }
// Grid
ctx.strokeStyle = 'rgba(61,139,255,0.07)'; ctx.lineWidth = 1;
for (var i = 0; i <= 10; i++) {
var cx = pad + (i / 10) * (W - pad * 2);
ctx.beginPath(); ctx.moveTo(cx, 0); ctx.lineTo(cx, H); ctx.stroke();
var cy = pad + (i / 10) * (H - pad * 2);
ctx.beginPath(); ctx.moveTo(0, cy); ctx.lineTo(W, cy); ctx.stroke();
}
// Cabos
var linhaVals = Object.values(linhas);
for (var li = 0; li < linhaVals.length; li++) {
var cabo = linhaVals[li];
var p1 = toC(cabo.P_INICIAL[0], cabo.P_INICIAL[1]);
var p2 = toC(cabo.P_FINAL[0], cabo.P_FINAL[1]);
ctx.beginPath(); ctx.moveTo(p1[0], p1[1]); ctx.lineTo(p2[0], p2[1]);
ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 2;
ctx.setLineDash([6, 3]); ctx.stroke(); ctx.setLineDash([]);
}
// Hastes
var hasteKeys = Object.keys(hastes);
var maxRes = resPorHaste ? Math.max.apply(null, resPorHaste) : 1;
if (maxRes === 0) maxRes = 1;
hasteKeys.forEach(function(nome, idx) {
var h = hastes[nome];
var pt = toC(h.X, h.Y);
var ccx = pt[0], ccy = pt[1];
var res = resPorHaste ? resPorHaste[idx] : null;
if (res !== null && isFinite(res)) {
var g = ctx.createRadialGradient(ccx, ccy, 4, ccx, ccy, 22);
g.addColorStop(0, 'rgba(61,139,255,' + (0.3 * res / maxRes) + ')');
g.addColorStop(1, 'rgba(61,139,255,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(ccx, ccy, 22, 0, Math.PI * 2); ctx.fill();
}
ctx.fillStyle = '#3d8bff'; ctx.strokeStyle = '#0a0e1a'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(ccx, ccy, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#93c5fd'; ctx.font = '600 10px "JetBrains Mono",monospace';
ctx.textAlign = 'center';
ctx.fillText(nome.replace('haste_', '#'), ccx, ccy - 12);
if (res !== null && isFinite(res)) {
ctx.fillStyle = '#fbbf24'; ctx.font = '9px "JetBrains Mono",monospace';
ctx.fillText(res.toFixed(1) + 'Ω', ccx, ccy + 20);
}
});
});
return React.createElement('canvas', {
ref: canvasRef,
width: 580,
height: 360,
style: { width: '100%', borderRadius: 8, background: 'var(--bg-0)', border: '1px solid var(--border)', display: 'block' }
});
}
// ─── Helpers UI (FORA do componente) ─────────────────────────────────────────
function AtField(props) {
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
React.createElement('label', { style: { fontFamily: 'var(--ff-mono)', fontSize: 10, letterSpacing: '0.12em', color: 'var(--fg-mute)', textTransform: 'uppercase' } }, props.label),
props.children
);
}
function AtInput(props) {
var inputStyle = { width: '100%', padding: '8px 12px', background: 'var(--bg-0)', border: '1px solid var(--border)', borderRadius: 6, color: 'var(--fg)', fontFamily: 'var(--ff-mono)', fontSize: 13, outline: 'none' };
return React.createElement('input', Object.assign({}, props, {
style: inputStyle,
onFocus: function(e) { e.target.style.borderColor = 'var(--accent)'; },
onBlur: function(e) { e.target.style.borderColor = 'var(--border)'; },
}));
}
// ModeBtn FORA do componente — recebe modoManual e setModoManual como props
function ModeBtn(props) {
var active = (props.mode === 'manual') === props.modoManual;
return React.createElement('button', {
onClick: function() { props.setModoManual(props.mode === 'manual'); },
style: {
padding: '6px 14px', borderRadius: 6, fontFamily: 'var(--ff-mono)', fontSize: 11,
letterSpacing: '0.06em', cursor: 'pointer', transition: 'all .15s',
border: active ? '1px solid rgba(61,139,255,0.3)' : '1px solid transparent',
background: active ? 'var(--accent-soft)' : 'transparent',
color: active ? 'var(--accent)' : 'var(--fg-mute)',
}
}, props.children);
}
// ─── Componente principal ─────────────────────────────────────────────────────
function ModuloAterramento() {
var rhoState = useState(100);
var rho = rhoState[0], setRho = rhoState[1];
var LState = useState(2.4);
var L = LState[0], setL = LState[1];
var DState = useState(0.016);
var D = DState[0], setD = DState[1];
var csvState = useState(null);
var csvData = csvState[0], setCsvData = csvState[1];
var resultState = useState(null);
var resultado = resultState[0], setResultado = resultState[1];
var erroState = useState('');
var erro = erroState[0], setErro = erroState[1];
var arqState = useState('');
var nomeArq = arqState[0], setNomeArq = arqState[1];
var modoState = useState(false);
var modoManual = modoState[0], setModoManual = modoState[1];
var hastesState = useState([
{ X: 0, Y: 0 }, { X: 2, Y: 0 }, { X: 0, Y: 2 }, { X: 2, Y: 2 }
]);
var hastesManual = hastesState[0], setHastesManual = hastesState[1];
var fileRef = useRef(null);
var onCSV = useCallback(function(e) {
var file = e.target.files[0]; if (!file) return;
setNomeArq(file.name);
var reader = new FileReader();
reader.onload = function(ev) {
try {
var p = parseCSV(ev.target.result);
if (!Object.keys(p.hastes).length) { setErro('Nenhuma haste (Circle) no CSV.'); setCsvData(null); }
else { setErro(''); setCsvData(p); }
} catch (ex) { setErro('Erro CSV: ' + ex.message); setCsvData(null); }
};
reader.readAsText(file, 'utf-8');
}, []);
var calcular = useCallback(function() {
setErro('');
try {
var hastes = {}, linhas = {};
if (modoManual) {
hastesManual.forEach(function(h, i) {
hastes['haste_' + (i + 1)] = { X: +h.X || 0, Y: +h.Y || 0, L: +L || 2.4, D: +D || 0.016 };
});
} else {
if (!csvData) { setErro('Importe um CSV primeiro.'); return; }
Object.entries(csvData.hastes).forEach(function(entry) {
hastes[entry[0]] = Object.assign({}, entry[1], { L: +L || 2.4, D: +D || 0.016 });
});
linhas = csvData.linhas;
}
if (Object.keys(hastes).length < 2) { setErro('Mínimo 2 hastes para o cálculo.'); return; }
var res = calcularSistema(hastes, +rho || 100);
if (!res) { setErro('Erro interno no cálculo.'); return; }
setResultado(Object.assign({}, res, { hastes: hastes, linhas: linhas }));
} catch (err) {
setErro('Erro no cálculo: ' + err.message);
}
}, [modoManual, csvData, hastesManual, rho, L, D]);
var addHaste = function() { setHastesManual(function(h) { return h.concat([{ X: 0, Y: 0 }]); }); };
var removeHaste = function(i) { setHastesManual(function(h) { return h.filter(function(_, idx) { return idx !== i; }); }); };
var updateHaste = function(i, f, v) {
setHastesManual(function(h) { return h.map(function(it, idx) { return idx === i ? Object.assign({}, it, { [f]: v }) : it; }); });
};
var cls = resultado ? classify(resultado.resTotal) : null;
// ── Seção de resultado (segura) ──────────────────────────────────────────
var resultSection = null;
if (resultado && cls) {
var n = resultado.keys ? resultado.keys.length : 0;
resultSection = (
R ≤ 10 Ω — Sistemas de distribuição BT
R ≤ 100 Ω — Edificações residenciais
AutoCAD: Express Tools → Data Extraction · selecionar Circle e Line, campos Center X/Y e Start/End X/Y.
Configure os parâmetros e clique em Calcular Sistema.
| {h} | ); })}|||||
|---|---|---|---|---|---|
| {k} | {(h.X || 0).toFixed(4)} | {(h.Y || 0).toFixed(4)} | {isFinite(rP) ? rP.toFixed(4) : '—'} | {isFinite(tot - rP) ? (tot - rP).toFixed(4) : '—'} | {isFinite(tot) ? tot.toFixed(4) : '—'} |
| {resultado.keys.map(function(k) { return ( | {k.replace('haste_', '#')} | ); })}
|---|---|
| {resultado.keys[i] ? resultado.keys[i].replace('haste_', '#') : i} | {row.map(function(val, j) { return ({isFinite(val) ? val.toFixed(3) : '—'} | ); })}