{"id":6107,"date":"2026-03-13T14:35:30","date_gmt":"2026-03-13T17:35:30","guid":{"rendered":"https:\/\/fisica2.fica.unsl.edu.ar\/?page_id=6107"},"modified":"2026-03-26T11:18:33","modified_gmt":"2026-03-26T14:18:33","slug":"graficar_sup_equipotencial_lin_campo","status":"publish","type":"page","link":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/","title":{"rendered":"graficar_sup_equipotencial_lin_campo"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"6107\" class=\"elementor elementor-6107\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-8d78edf elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"8d78edf\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-99f902d\" data-id=\"99f902d\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-d45815a elementor-widget elementor-widget-html\" data-id=\"d45815a\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"es\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>Laboratorio Virtual - Cuba Electrolitica<\/title>\r\n    <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\r\n    <link href=\"https:\/\/fonts.googleapis.com\/css2?family=JetBrains+Mono:wght@400;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\r\n    <style>\r\n        :root {\r\n            --bg-primary: #0a0e17;\r\n            --bg-secondary: #111827;\r\n            --bg-tertiary: #1a2332;\r\n            --fg-primary: #e2e8f0;\r\n            --fg-secondary: #94a3b8;\r\n            --fg-muted: #64748b;\r\n            --accent-cyan: #22d3ee;\r\n            --positive: #ef4444;\r\n            --negative: #3b82f6;\r\n            --border: #2d3748;\r\n        }\r\n\r\n        * { box-sizing: border-box; }\r\n        body {\r\n            font-family: 'Space Grotesk', sans-serif;\r\n            background: var(--bg-primary);\r\n            color: var(--fg-primary);\r\n            margin: 0;\r\n            min-height: 100vh;\r\n            overflow-x: hidden;\r\n        }\r\n        .mono { font-family: 'JetBrains Mono', monospace; }\r\n        .panel {\r\n            background: var(--bg-secondary);\r\n            border: 1px solid var(--border);\r\n            border-radius: 12px;\r\n        }\r\n        .instrument-display {\r\n            background: linear-gradient(145deg, #0d1117 0%, #161b22 100%);\r\n            border: 2px solid #30363d;\r\n            border-radius: 8px;\r\n            box-shadow: inset 0 2px 10px rgba(0,0,0,0.5);\r\n        }\r\n        .lcd-display {\r\n            background: #0f1a0f;\r\n            color: #22c55e;\r\n            font-family: 'JetBrains Mono', monospace;\r\n            text-shadow: 0 0 10px rgba(34, 197, 94, 0.6);\r\n            border-radius: 4px;\r\n            padding: 12px 16px;\r\n        }\r\n        .btn-control {\r\n            background: linear-gradient(145deg, #2d3748, #1a202c);\r\n            border: 1px solid #4a5568;\r\n            color: var(--fg-primary);\r\n            padding: 8px 16px;\r\n            border-radius: 6px;\r\n            cursor: pointer;\r\n            transition: all 0.15s ease;\r\n            font-weight: 500;\r\n        }\r\n        .btn-control:hover { background: linear-gradient(145deg, #3d4758, #2a303c); }\r\n        .btn-control.active { background: linear-gradient(145deg, #2563eb, #1d4ed8); border-color: #3b82f6; }\r\n        \r\n        input[type=\"range\"] {\r\n            -webkit-appearance: none;\r\n            width: 100%;\r\n            height: 8px;\r\n            background: linear-gradient(90deg, var(--negative), #6b7280, var(--positive));\r\n            border-radius: 4px;\r\n        }\r\n        input[type=\"range\"]::-webkit-slider-thumb {\r\n            -webkit-appearance: none;\r\n            width: 20px; height: 20px;\r\n            background: var(--fg-primary);\r\n            border-radius: 50%;\r\n            cursor: pointer;\r\n        }\r\n        \r\n        .checkbox-custom {\r\n            appearance: none; width: 18px; height: 18px;\r\n            border: 2px solid var(--border); border-radius: 4px;\r\n            background: var(--bg-tertiary); cursor: pointer; position: relative;\r\n        }\r\n        .checkbox-custom:checked { background: var(--accent-cyan); border-color: var(--accent-cyan); }\r\n        .checkbox-custom:checked::after { content: '\u2713'; position: absolute; color: var(--bg-primary); font-size: 12px; top: 50%; left: 50%; transform: translate(-50%, -50%); }\r\n        \r\n        .led-indicator { width: 12px; height: 12px; border-radius: 50%; background: #374151; box-shadow: inset 0 1px 3px rgba(0,0,0,0.5); }\r\n        .led-indicator.on { background: #22c55e; box-shadow: 0 0 10px rgba(34, 197, 94, 0.6); }\r\n        \r\n        #mainCanvas { cursor: crosshair; }\r\n        .data-table { max-height: 200px; overflow-y: auto; }\r\n        .section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--fg-muted); margin-bottom: 12px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }\r\n        \r\n        .visualization-label { color: #ffffff; }\r\n        \r\n        .data-table tbody td {\r\n            background-color: #f8fafc;\r\n            color: #111827;\r\n            border: 1px solid #e2e8f0;\r\n        }\r\n        .data-table thead th {\r\n            background-color: var(--bg-tertiary);\r\n            color: var(--fg-muted);\r\n            border: 1px solid var(--border);\r\n            position: sticky;\r\n            top: 0;\r\n            z-index: 10;\r\n        }\r\n        \r\n        .used-point-row { opacity: 0.6; background-color: #e5e7eb !important; }\r\n        \r\n        \/* Estilos para la lista de curvas generadas *\/\r\n        .generated-list { max-height: 150px; overflow-y: auto; }\r\n        .generated-item {\r\n            display: flex;\r\n            justify-content: space-between;\r\n            align-items: center;\r\n            padding: 4px 8px;\r\n            border-bottom: 1px solid var(--border);\r\n            font-size: 12px;\r\n        }\r\n        .btn-delete-small {\r\n            background: #ef4444;\r\n            color: white;\r\n            border: none;\r\n            border-radius: 4px;\r\n            padding: 2px 6px;\r\n            font-size: 10px;\r\n            cursor: pointer;\r\n        }\r\n        .btn-delete-small:hover { background: #dc2626; }\r\n        \r\n        \r\n        \r\n        \r\n    <\/style>\r\n<\/head>\r\n<body class=\"p-4\">\r\n    <div class=\"max-w-[1800px] mx-auto\">\r\n       \r\n       \r\n       <header class=\"mb-4 flex items-center justify-between flex-wrap gap-2\">\r\n    <div>\r\n        <div class=\"flex items-center gap-8\"> \r\n            <h1 class=\"text-2xl font-bold tracking-tight\">Laboratorio Virtual - Cuba Electrolitica<\/h1>\r\n            \r\n            <div class=\"flex items-center gap-2\">\r\n                <a href=\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/FISAR_SIM\/\" target=\"_blank\">\r\n                <img decoding=\"async\" src=\"FISAR_SIM_1\" alt=\"Logo Fluxar\" class=\"max-h-8 w-auto object-contain\"><\/a>\r\n                <span style=\"font-size: 0.75rem; font-weight: 600; line-height: 1; color: #94a3b8; white-space: nowrap;\">\r\n                    FICA - UNSL\r\n                <\/span>\r\n            <\/div>\r\n        <\/div>\r\n        \r\n        <p class=\"text-sm text-[var(--fg-secondary)] mt-1\">Simulacion interactiva de superficies equipotenciales y lineas de campo<\/p>\r\n    <\/div>\r\n\r\n    <div class=\"flex items-center gap-10\">\r\n        <div class=\"flex items-center gap-2 text-sm\">\r\n            <div class=\"led-indicator off\" id=\"statusLed\"><\/div>\r\n            <span class=\"text-[var(--fg-secondary)]\" id=\"statusText\">Sistema inactivo<\/span>\r\n        <\/div>\r\n        <button class=\"btn-control text-sm\" onclick=\"resetSimulation()\">Reiniciar Todo<\/button>\r\n    <\/div>\r\n<\/header>\r\n       \r\n       \r\n       \r\n       \r\n\r\n        <div class=\"grid grid-cols-[320px_1fr_300px] gap-4\">\r\n            <!-- Panel Izquierdo -->\r\n            <div class=\"panel p-4 space-y-4 overflow-y-auto max-h-[calc(100vh-120px)]\">\r\n                <section>\r\n                    <h2 class=\"section-title\">Fuente de Alimentacion<\/h2>\r\n                    <div class=\"instrument-display p-4\">\r\n                        <div class=\"flex items-center justify-between mb-4\">\r\n                            <span class=\"text-sm font-medium\">Tension<\/span>\r\n                            <div class=\"flex items-center gap-2\">\r\n                                <div class=\"led-indicator\" id=\"powerLed\"><\/div>\r\n                                <span class=\"text-xs text-[var(--fg-muted)]\" id=\"powerStatus\">OFF<\/span>\r\n                            <\/div>\r\n                        <\/div>\r\n                        <div class=\"lcd-display text-center text-3xl font-bold mb-4\" id=\"voltageDisplay\">0.00 V<\/div>\r\n                        <div class=\"mb-4\">\r\n                            <input type=\"range\" id=\"voltageSlider\" min=\"0\" max=\"20\" step=\"0.1\" value=\"0\">\r\n                        <\/div>\r\n                        <div class=\"flex gap-2\">\r\n                            <button class=\"btn-control flex-1\" id=\"powerBtn\" onclick=\"togglePower()\">Encender<\/button>\r\n                            <button class=\"btn-control\" onclick=\"invertPolarity()\">Invertir<\/button>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/section>\r\n\r\n                <section>\r\n                    <h2 class=\"section-title\">Configuracion<\/h2>\r\n                    <select id=\"configSelect\" class=\"w-full mb-3 bg-gray-100 border border-gray-300 p-2 rounded text-gray-900 font-medium\" onchange=\"loadConfiguration()\">\r\n                        <option value=\"dipole\">Dipolo (dos circulos)<\/option>\r\n                        <option value=\"plates\">Placas paralelas<\/option>\r\n                        <option value=\"pointline\">Carga y linea<\/option>\r\n                    <\/select>\r\n                <\/section>\r\n\r\n                <section>\r\n                    <h2 class=\"section-title\">Visualizacion<\/h2>\r\n                    <div class=\"space-y-2\">\r\n                        <label class=\"flex items-center gap-3 cursor-pointer\">\r\n                            <input type=\"checkbox\" class=\"checkbox-custom\" id=\"showGrid\" checked onchange=\"render()\">\r\n                            <span class=\"text-sm visualization-label\">Cuadricula<\/span>\r\n                        <\/label>\r\n                        <label class=\"flex items-center gap-3 cursor-pointer\">\r\n                            <input type=\"checkbox\" class=\"checkbox-custom\" id=\"showUserCurves\" checked onchange=\"render()\">\r\n                            <span class=\"text-sm visualization-label\">Curvas de Medicion<\/span>\r\n                        <\/label>\r\n                        <label class=\"flex items-center gap-3 cursor-pointer\">\r\n                            <input type=\"checkbox\" class=\"checkbox-custom\" id=\"showFieldLines\" checked onchange=\"render()\">\r\n                            <span class=\"text-sm visualization-label\">Lineas de Campo (Trazadas)<\/span>\r\n                        <\/label>\r\n                        <label class=\"flex items-center gap-3 cursor-pointer\">\r\n                            <input type=\"checkbox\" class=\"checkbox-custom\" id=\"showPoints\" checked onchange=\"render()\">\r\n                            <span class=\"text-sm visualization-label\">Puntos Medidos<\/span>\r\n                        <\/label>\r\n                    <\/div>\r\n                <\/section>\r\n\r\n                <section>\r\n                    <h2 class=\"section-title\">Herramientas<\/h2>\r\n                     <div class=\"text-xs text-[var(--fg-muted)] p-2 bg-[var(--bg-tertiary)] rounded border border-[var(--border)] mb-3\">\r\n                        <strong>Instrucciones:<\/strong><br>\r\n                        1. Mida puntos haciendo clic (min 5).<br>\r\n                        2. Fije la traza correspondiente.<br>\r\n                        3. Exporte datos o imagen.\r\n                    <\/div>\r\n                    \r\n                    <button class=\"btn-control w-full bg-[var(--accent-cyan)] text-black font-bold mb-2\" onclick=\"generateCurvesFromPoints()\">\r\n                        Fijar Equipotencial\r\n                    <\/button>\r\n\r\n                    <button class=\"btn-control w-full bg-orange-500 text-black font-bold\" onclick=\"generateFieldLinesFromPoints()\">\r\n                        Fijar Linea de Campo\r\n                    <\/button>\r\n                <\/section>\r\n            <\/div>\r\n\r\n            <!-- Panel Central -->\r\n            <div class=\"panel overflow-hidden relative\" style=\"height: calc(100vh - 140px);\">\r\n                <div class=\"absolute top-0 left-0 right-0 bg-[var(--bg-tertiary)] px-4 py-2 flex items-center justify-between border-b border-[var(--border)] z-10\">\r\n                    <span class=\"text-sm font-medium\">Cuba Electrolitica<\/span>\r\n                    <div class=\"flex items-center gap-4 text-xs mono\">\r\n                        <span>Sonda: (<span id=\"probeX\">---<\/span>, <span id=\"probeY\">---<\/span>) cm<\/span>\r\n                        <span>Zoom: <span id=\"zoomLevel\">100<\/span>%<\/span>\r\n                    <\/div>\r\n                <\/div>\r\n                \r\n                <canvas id=\"mainCanvas\" class=\"w-full h-full\"><\/canvas>\r\n                \r\n                <div class=\"absolute bottom-4 right-4 flex gap-2 z-10\">\r\n                    <button class=\"btn-control px-3 py-2\" onclick=\"zoomIn()\">+<\/button>\r\n                    <button class=\"btn-control px-3 py-2\" onclick=\"zoomOut()\">-<\/button>\r\n                    <button class=\"btn-control px-3 py-2\" onclick=\"resetView()\">\u27f2<\/button>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- Panel Derecho -->\r\n            <div class=\"space-y-4\">\r\n                <div class=\"panel p-4\">\r\n                    <h2 class=\"section-title\">Voltimetro<\/h2>\r\n                    <div class=\"instrument-display p-4\">\r\n                        <div class=\"text-center mb-2\"><span class=\"text-xs text-[var(--fg-muted)]\">V(x, y)<\/span><\/div>\r\n                        <div class=\"lcd-display text-center text-4xl font-bold\" id=\"voltmeterDisplay\">0.00 V<\/div>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <div class=\"panel p-4\">\r\n                    <h2 class=\"section-title\">Registro de Datos<\/h2>\r\n                    <div class=\"data-table rounded overflow-hidden\">\r\n                        <table class=\"w-full text-xs mono\">\r\n                            <thead>\r\n                                <tr>\r\n                                    <th class=\"text-left py-1 px-2\">#<\/th>\r\n                                    <th class=\"text-right py-1 px-2\">X<\/th>\r\n                                    <th class=\"text-right py-1 px-2\">Y<\/th>\r\n                                    <th class=\"text-right py-1 px-2\">V<\/th>\r\n                                <\/tr>\r\n                            <\/thead>\r\n                            <tbody id=\"pointsTable\"><\/tbody>\r\n                        <\/table>\r\n                    <\/div>\r\n                    <div class=\"flex flex-col gap-2 mt-3\">\r\n                        <button class=\"btn-control flex-1 text-xs\" onclick=\"exportCSV()\">Exportar Todo a CSV<\/button>\r\n                        <button class=\"btn-control flex-1 text-xs bg-green-700 text-white\" onclick=\"downloadImage()\">Guardar Imagen<\/button>\r\n                    <\/div>\r\n                <\/div>\r\n                \r\n                <div class=\"panel p-4\">\r\n                    <h2 class=\"section-title\">Informacion<\/h2>\r\n                    <div class=\"text-sm space-y-1\">\r\n                        <div class=\"flex justify-between\">\r\n                            <span class=\"text-[var(--fg-secondary)]\">Puntos activos:<\/span>\r\n                            <span class=\"mono\" id=\"totalPoints\">0<\/span>\r\n                        <\/div>\r\n                        <div class=\"flex justify-between\">\r\n                            <span class=\"text-[var(--fg-secondary)]\">Equipotenciales fijadas:<\/span>\r\n                            <span class=\"mono\" id=\"totalCurves\">0<\/span>\r\n                        <\/div>\r\n                         <div class=\"flex justify-between\">\r\n                            <span class=\"text-[var(--fg-secondary)]\">Lineas de campo fijadas:<\/span>\r\n                            <span class=\"mono\" id=\"totalFieldLines\">0<\/span>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <!-- NUEVA SECCI\u00d3N: CURVAS GENERADAS -->\r\n                <div class=\"panel p-4\" id=\"generatedSection\">\r\n                    <h2 class=\"section-title\">Trazas Generadas<\/h2>\r\n                    <div class=\"generated-list space-y-1\" id=\"generatedList\">\r\n                        <!-- Se llena din\u00e1micamente -->\r\n                        <div class=\"text-xs text-center text-[var(--fg-muted)]\">Ninguna traza generada.<\/div>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <script>\r\n        \/\/ ==================== CORE SETUP ====================\r\n        const GRID_SIZE = 200; \r\n        const EPSILON = 1e-10;\r\n        const MIN_POINTS_CURVE = 5; \/\/ M\u00ednimo de puntos requeridos\r\n        \r\n        let canvas, ctx;\r\n        let state = {\r\n            powerOn: false, voltage: 0, polarity: 1, electrodes: [],\r\n            measuredPoints: [],\r\n            allPoints: [],\r\n            userCurves: [],\r\n            userFieldLines: [],\r\n            draggingElectrode: null,\r\n            probe: { x: 0, y: 0 },\r\n            view: { zoom: 1, offsetX: 0, offsetY: 0 },\r\n            gridCache: null, gridBounds: null, needsRecalculation: true\r\n        };\r\n\r\n        const configurations = {\r\n            dipole: () => [\r\n                { type: 'circle', x: -3, y: 0, radius: 0.5, charge: 1, color: '#ef4444', label: '+' },\r\n                { type: 'circle', x: 3, y: 0, radius: 0.5, charge: -1, color: '#3b82f6', label: '-' }\r\n            ],\r\n            plates: () => [\r\n                { type: 'plate', x: -4, y: 0, width: 0.3, height: 6, charge: 1, color: '#ef4444', label: '+' },\r\n                { type: 'plate', x: 4, y: 0, width: 0.3, height: 6, charge: -1, color: '#3b82f6', label: '-' }\r\n            ],\r\n            pointline: () => [\r\n                { type: 'circle', x: -3, y: 0, radius: 0.4, charge: 1, color: '#ef4444', label: '+' },\r\n                { type: 'plate', x: 3, y: 0, width: 0.3, height: 6, charge: -1, color: '#3b82f6', label: '-' }\r\n            ]\r\n        };\r\n\r\n        \/\/ ==================== INITIALIZATION ====================\r\n        window.addEventListener('DOMContentLoaded', () => {\r\n            canvas = document.getElementById('mainCanvas');\r\n            ctx = canvas.getContext('2d');\r\n            resizeCanvas();\r\n            window.addEventListener('resize', resizeCanvas);\r\n            \r\n            canvas.addEventListener('mousemove', handleMouseMove);\r\n            canvas.addEventListener('mousedown', handleMouseDown);\r\n            canvas.addEventListener('contextmenu', handleRightClick);\r\n            canvas.addEventListener('wheel', handleWheel, { passive: false });\r\n            canvas.addEventListener('mouseup', () => { state.draggingElectrode = null; canvas.style.cursor = 'crosshair'; });\r\n            \r\n            state.voltage = parseFloat(document.getElementById('voltageSlider').value);\r\n            loadConfiguration();\r\n            requestAnimationFrame(loop);\r\n        });\r\n\r\n        function resizeCanvas() {\r\n            const rect = canvas.parentElement.getBoundingClientRect();\r\n            canvas.width = rect.width;\r\n            canvas.height = rect.height;\r\n            state.needsRecalculation = true;\r\n        }\r\n\r\n        \/\/ ==================== PHYSICS ENGINE ====================\r\n        function calculatePotential(x, y) {\r\n            if (!state.powerOn || state.electrodes.length === 0) return 0;\r\n            \r\n            const V_max = state.voltage; \r\n            \r\n            let posElectrodes = state.electrodes.filter(e => e.charge > 0);\r\n            let negElectrodes = state.electrodes.filter(e => e.charge < 0);\r\n            \r\n            function isInside(electrode) {\r\n                if (electrode.type === 'circle') {\r\n                    let dist = Math.sqrt((x - electrode.x)**2 + (y - electrode.y)**2);\r\n                    return dist <= electrode.radius;\r\n                } else if (electrode.type === 'plate') {\r\n                    let w = electrode.width \/ 2;\r\n                    let h = electrode.height \/ 2;\r\n                    return x >= electrode.x - w && x <= electrode.x + w &&\r\n                           y >= electrode.y - h && y <= electrode.y + h;\r\n                }\r\n                return false;\r\n            }\r\n            \r\n            for (let e of posElectrodes) {\r\n                if (isInside(e)) return state.polarity > 0 ? V_max : 0;\r\n            }\r\n            for (let e of negElectrodes) {\r\n                if (isInside(e)) return state.polarity > 0 ? 0 : V_max;\r\n            }\r\n            \r\n            let plates = state.electrodes.filter(e => e.type === 'plate');\r\n            if (plates.length === 2) {\r\n                let p1 = plates[0];\r\n                let p2 = plates[1];\r\n                let leftP = p1.x < p2.x ? p1 : p2;\r\n                let rightP = p1.x < p2.x ? p2 : p1;\r\n                let xLeft = leftP.x + leftP.width\/2;\r\n                let xRight = rightP.x - rightP.width\/2;\r\n                let dist = Math.abs(xRight - xLeft);\r\n                \r\n                if (x > xLeft && x < xRight && dist > EPSILON) {\r\n                    let V_val = 0;\r\n                    if (leftP.charge > 0) {\r\n                        V_val = V_max * (1 - (x - xLeft) \/ dist);\r\n                    } else {\r\n                        V_val = V_max * (x - xLeft) \/ dist;\r\n                    }\r\n                    return state.polarity > 0 ? V_val : V_max - V_val;\r\n                }\r\n            }\r\n            \r\n            let minDistPos = Infinity;\r\n            for (let e of posElectrodes) {\r\n                let dist = Math.sqrt((x - e.x)**2 + (y - e.y)**2);\r\n                if (e.type === 'plate') {\r\n                    let dx = Math.max(0, Math.abs(x - e.x) - e.width\/2);\r\n                    let dy = Math.max(0, Math.abs(y - e.y) - e.height\/2);\r\n                    dist = Math.sqrt(dx*dx + dy*dy);\r\n                }\r\n                if (dist < minDistPos) minDistPos = dist;\r\n            }\r\n            \r\n            let minDistNeg = Infinity;\r\n            for (let e of negElectrodes) {\r\n                let dist = Math.sqrt((x - e.x)**2 + (y - e.y)**2);\r\n                if (e.type === 'plate') {\r\n                    let dx = Math.max(0, Math.abs(x - e.x) - e.width\/2);\r\n                    let dy = Math.max(0, Math.abs(y - e.y) - e.height\/2);\r\n                    dist = Math.sqrt(dx*dx + dy*dy);\r\n                }\r\n                if (dist < minDistNeg) minDistNeg = dist;\r\n            }\r\n            \r\n            minDistPos = Math.max(0.01, minDistPos);\r\n            minDistNeg = Math.max(0.01, minDistNeg);\r\n            \r\n            let wPos = 1.0 \/ minDistPos;\r\n            let wNeg = 1.0 \/ minDistNeg;\r\n            \r\n            let V_norm = wPos \/ (wPos + wNeg);\r\n            \r\n            let V_final = V_norm * V_max;\r\n            \r\n            if (state.polarity < 0) {\r\n                V_final = V_max - V_final;\r\n            }\r\n            \r\n            return V_final;\r\n        }\r\n\r\n        function updateGrid() {\r\n            if (!state.needsRecalculation) return;\r\n            const bounds = getViewBounds();\r\n            state.gridBounds = bounds;\r\n            state.gridCache = new Float32Array(GRID_SIZE * GRID_SIZE);\r\n            for (let i = 0; i < GRID_SIZE; i++) {\r\n                for (let j = 0; j < GRID_SIZE; j++) {\r\n                    const x = bounds.minX + (bounds.maxX - bounds.minX) * (i \/ GRID_SIZE);\r\n                    const y = bounds.minY + (bounds.maxY - bounds.minY) * (j \/ GRID_SIZE);\r\n                    state.gridCache[i + j * GRID_SIZE] = calculatePotential(x, y);\r\n                }\r\n            }\r\n            state.needsRecalculation = false;\r\n        }\r\n\r\n        function getViewBounds() {\r\n            const scale = 10 \/ state.view.zoom;\r\n            const aspect = canvas.height \/ canvas.width;\r\n            return {\r\n                minX: -scale + state.view.offsetX,\r\n                maxX: scale + state.view.offsetX,\r\n                minY: -scale * aspect + state.view.offsetY,\r\n                maxY: scale * aspect + state.view.offsetY\r\n            };\r\n        }\r\n\r\n        function worldToScreen(wx, wy) {\r\n            const b = state.gridBounds || getViewBounds();\r\n            const sx = (wx - b.minX) \/ (b.maxX - b.minX) * canvas.width;\r\n            const sy = canvas.height - (wy - b.minY) \/ (b.maxY - b.minY) * canvas.height;\r\n            return { x: sx, y: sy };\r\n        }\r\n\r\n        function screenToWorld(sx, sy) {\r\n            const b = getViewBounds();\r\n            const wx = b.minX + (sx \/ canvas.width) * (b.maxX - b.minX);\r\n            const wy = b.minY + ((canvas.height - sy) \/ canvas.height) * (b.maxY - b.minY);\r\n            return { x: wx, y: wy };\r\n        }\r\n\r\n        \/\/ ==================== RENDERING ====================\r\n        function loop() {\r\n            if (state.powerOn && state.needsRecalculation) updateGrid();\r\n            render();\r\n            requestAnimationFrame(loop);\r\n        }\r\n\r\n        function render() {\r\n            ctx.fillStyle = '#0a0e17';\r\n            ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n            if (document.getElementById('showGrid').checked) drawGrid();\r\n            \r\n            \/\/ Dibujar curvas y puntos\r\n            if (document.getElementById('showUserCurves').checked) drawUserCurves();\r\n            if (document.getElementById('showFieldLines').checked) drawUserFieldLines();\r\n            drawElectrodes();\r\n            if (document.getElementById('showPoints').checked) drawMeasuredPoints();\r\n            drawProbe();\r\n        }\r\n\r\n        function drawGrid() {\r\n            const b = getViewBounds();\r\n            ctx.strokeStyle = 'rgba(45, 55, 72, 0.4)';\r\n            ctx.lineWidth = 1;\r\n            const step = 1;\r\n            ctx.beginPath();\r\n            for (let x = Math.floor(b.minX); x <= Math.ceil(b.maxX); x += step) {\r\n                let p1 = worldToScreen(x, b.minY); let p2 = worldToScreen(x, b.maxY);\r\n                ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y);\r\n            }\r\n            for (let y = Math.floor(b.minY); y <= Math.ceil(b.maxY); y += step) {\r\n                let p1 = worldToScreen(b.minX, y); let p2 = worldToScreen(b.maxX, y);\r\n                ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y);\r\n            }\r\n            ctx.stroke();\r\n        }\r\n\r\n        function drawElectrodes() {\r\n            ctx.lineWidth = 2;\r\n            for (let el of state.electrodes) {\r\n                let p = worldToScreen(el.x, el.y);\r\n                ctx.fillStyle = el.color;\r\n                ctx.strokeStyle = 'white';\r\n                if (el.type === 'circle') {\r\n                    let r = el.radius \/ (getViewBounds().maxX - getViewBounds().minX) * canvas.width;\r\n                    ctx.beginPath(); ctx.arc(p.x, p.y, Math.max(5, r), 0, Math.PI*2); ctx.fill(); ctx.stroke();\r\n                    ctx.fillStyle = '#000000'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';\r\n                    ctx.fillText(el.label, p.x, p.y);\r\n                } else if (el.type === 'plate') {\r\n                    let w = el.width \/ (getViewBounds().maxX - getViewBounds().minX) * canvas.width;\r\n                    let h = el.height \/ (getViewBounds().maxY - getViewBounds().minY) * canvas.height;\r\n                    ctx.fillRect(p.x - w\/2, p.y - h\/2, w, h);\r\n                    ctx.strokeRect(p.x - w\/2, p.y - h\/2, w, h);\r\n                    ctx.fillStyle = '#000000'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign='center';\r\n                    ctx.fillText(el.label, p.x, p.y);\r\n                }\r\n            }\r\n        }\r\n\r\n        function drawMeasuredPoints() {\r\n            for (let pt of state.allPoints) {\r\n                let p = worldToScreen(pt.x, pt.y);\r\n                if (pt.used) {\r\n                    ctx.fillStyle = 'rgba(150, 150, 150, 0.5)';\r\n                    ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill();\r\n                } else {\r\n                    ctx.fillStyle = 'white';\r\n                    ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI*2); ctx.fill();\r\n                    ctx.fillStyle = '#22c55e';\r\n                    ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill();\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ ==================== TRAZADO DE CURVAS (Interpolaci\u00f3n Mejorada) ====================\r\n        \r\n        \/\/ Algoritmo de suavizado tipo B-Spline aproximada\r\n        function drawSmoothCurve(points, color, dashed = false) {\r\n            if (points.length < 2) return;\r\n\r\n            ctx.strokeStyle = color;\r\n            ctx.fillStyle = color;\r\n            ctx.lineWidth = 3;\r\n            if (dashed) ctx.setLineDash([8, 4]);\r\n            else ctx.setLineDash([]);\r\n\r\n            ctx.beginPath();\r\n\r\n            \/\/ Convertir a pantalla\r\n            let screenPoints = points.map(p => worldToScreen(p.x, p.y));\r\n            \r\n            ctx.moveTo(screenPoints[0].x, screenPoints[0].y);\r\n\r\n            if (screenPoints.length === 2) {\r\n                ctx.lineTo(screenPoints[1].x, screenPoints[1].y);\r\n            } else {\r\n                \/\/ Algoritmo: Promediado de esquinas (Corner Cutting \/ Chaikin's algorithm simplificado)\r\n                \/\/ Esto crea una curva que NO pasa por los puntos, sino que los \"suaviza\"\r\n                \/\/ similar a una B-Spline.\r\n                \r\n                \/\/ Iteramos 2 veces para mayor suavidad\r\n                let tempPts = [...screenPoints];\r\n                for (let iter = 0; iter < 2; iter++) {\r\n                    let newPts = [];\r\n                    newPts.push(tempPts[0]); \/\/ Mantener inicio\r\n                    for (let i = 0; i < tempPts.length - 1; i++) {\r\n                        let p0 = tempPts[i];\r\n                        let p1 = tempPts[i+1];\r\n                        \/\/ Crear puntos intermedios 1\/4 y 3\/4\r\n                        let q = { x: 0.75 * p0.x + 0.25 * p1.x, y: 0.75 * p0.y + 0.25 * p1.y };\r\n                        let r = { x: 0.25 * p0.x + 0.75 * p1.x, y: 0.25 * p0.y + 0.75 * p1.y };\r\n                        newPts.push(q);\r\n                        newPts.push(r);\r\n                    }\r\n                    newPts.push(tempPts[tempPts.length - 1]); \/\/ Mantener fin\r\n                    tempPts = newPts;\r\n                }\r\n                \r\n                \/\/ Dibujar la curva suavizada como polyline (los puntos est\u00e1n muy juntos, se ve curvo)\r\n                ctx.moveTo(tempPts[0].x, tempPts[0].y);\r\n                for(let i=1; i<tempPts.length; i++) {\r\n                    ctx.lineTo(tempPts[i].x, tempPts[i].y);\r\n                }\r\n            }\r\n            \r\n            ctx.stroke();\r\n            ctx.setLineDash([]);\r\n            ctx.lineWidth = 1;\r\n        }\r\n\r\n        function drawUserCurves() {\r\n            for (let curve of state.userCurves) {\r\n                if (curve.points.length < 2) continue;\r\n                drawSmoothCurve(curve.points, curve.color, true);\r\n                \r\n                \/\/ Etiqueta\r\n                let screenPoints = curve.points.map(p => worldToScreen(p.x, p.y));\r\n                let labelPoint = screenPoints[Math.floor(screenPoints.length \/ 2)];\r\n                ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; \r\n                ctx.fillRect(labelPoint.x - 30, labelPoint.y - 20, 60, 20);\r\n                ctx.fillStyle = curve.color; \r\n                ctx.font = 'bold 12px JetBrains Mono'; \r\n                ctx.textAlign = 'center';\r\n                ctx.fillText(curve.averagePotential.toFixed(1) + \" V\", labelPoint.x, labelPoint.y - 6);\r\n            }\r\n        }\r\n\r\n        function drawUserFieldLines() {\r\n            for (let line of state.userFieldLines) {\r\n                if (line.points.length < 2) continue;\r\n                \r\n                ctx.strokeStyle = '#fb923c'; \r\n                ctx.fillStyle = '#fb923c';\r\n                ctx.lineWidth = 2;\r\n                ctx.setLineDash([]);\r\n                ctx.beginPath();\r\n                \r\n                let screenPoints = line.points.map(p => worldToScreen(p.x, p.y));\r\n                \r\n                \/\/ Suavizado tipo B-Spline para l\u00edneas de campo\r\n                let tempPts = [...screenPoints];\r\n                if (tempPts.length > 2) {\r\n                    \/\/ Una sola iteraci\u00f3n para l\u00edneas de campo (m\u00e1s fiel a la direcci\u00f3n)\r\n                    let newPts = [];\r\n                    newPts.push(tempPts[0]);\r\n                    for (let i = 0; i < tempPts.length - 1; i++) {\r\n                        let p0 = tempPts[i];\r\n                        let p1 = tempPts[i+1];\r\n                        let q = { x: 0.75 * p0.x + 0.25 * p1.x, y: 0.75 * p0.y + 0.25 * p1.y };\r\n                        let r = { x: 0.25 * p0.x + 0.75 * p1.x, y: 0.25 * p0.y + 0.75 * p1.y };\r\n                        newPts.push(q);\r\n                        newPts.push(r);\r\n                    }\r\n                    newPts.push(tempPts[tempPts.length - 1]);\r\n                    tempPts = newPts;\r\n                }\r\n\r\n                ctx.moveTo(tempPts[0].x, tempPts[0].y);\r\n                for(let i=1; i<tempPts.length; i++) {\r\n                    ctx.lineTo(tempPts[i].x, tempPts[i].y);\r\n                }\r\n                ctx.stroke();\r\n\r\n                \/\/ Flechas\r\n                \/\/ Calculamos \u00e1ngulos en los puntos originales para precisi\u00f3n\r\n                for(let i = 0; i < screenPoints.length - 1; i += 3) {\r\n                    let p1 = screenPoints[i];\r\n                    let p2 = screenPoints[i+1];\r\n                    let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);\r\n                    \/\/ Dibujar flecha en el punto intermedio suavizado si existe\r\n                    let targetP = tempPts[Math.floor(tempPts.length * (i \/ screenPoints.length))];\r\n                    if(targetP) drawArrowhead(targetP.x, targetP.y, angle, 8, '#fb923c');\r\n                }\r\n            }\r\n            ctx.lineWidth = 1;\r\n        }\r\n\r\n        function drawArrowhead(x, y, angle, size, color) {\r\n            ctx.save();\r\n            ctx.translate(x, y);\r\n            ctx.rotate(angle);\r\n            ctx.fillStyle = color;\r\n            ctx.beginPath();\r\n            ctx.moveTo(0, 0);\r\n            ctx.lineTo(-size, -size\/2);\r\n            ctx.lineTo(-size, size\/2);\r\n            ctx.closePath();\r\n            ctx.fill();\r\n            ctx.restore();\r\n        }\r\n\r\n        function drawProbe() {\r\n            let p = worldToScreen(state.probe.x, state.probe.y);\r\n            let grd = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 30);\r\n            grd.addColorStop(0, 'rgba(34, 211, 238, 0.4)'); grd.addColorStop(1, 'transparent');\r\n            ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(p.x, p.y, 30, 0, Math.PI*2); ctx.fill();\r\n            ctx.strokeStyle = 'rgba(34, 211, 238, 0.9)'; ctx.lineWidth = 1;\r\n            ctx.beginPath();\r\n            ctx.moveTo(p.x-20, p.y); ctx.lineTo(p.x-8, p.y); ctx.moveTo(p.x+8, p.y); ctx.lineTo(p.x+20, p.y);\r\n            ctx.moveTo(p.x, p.y-20); ctx.lineTo(p.x, p.y-8); ctx.moveTo(p.x, p.y+8); ctx.lineTo(p.x, p.y+20);\r\n            ctx.stroke();\r\n            ctx.fillStyle = '#22d3ee'; ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill();\r\n        }\r\n\r\n        \/\/ ==================== LOGICA DE GENERACION ====================\r\n        \r\n        function sortPointsNearestNeighbor(points) {\r\n            if (points.length <= 1) return points;\r\n            let sorted = [];\r\n            let remaining = [...points];\r\n            let current = remaining.shift();\r\n            sorted.push(current);\r\n            while (remaining.length > 0) {\r\n                let nearestIndex = 0;\r\n                let minDist = Infinity;\r\n                for (let i = 0; i < remaining.length; i++) {\r\n                    let d = (current.x - remaining[i].x)**2 + (current.y - remaining[i].y)**2;\r\n                    if (d < minDist) { minDist = d; nearestIndex = i; }\r\n                }\r\n                current = remaining.splice(nearestIndex, 1)[0];\r\n                sorted.push(current);\r\n            }\r\n            return sorted;\r\n        }\r\n\r\n        function generateCurvesFromPoints() {\r\n            let activePoints = state.allPoints.filter(p => !p.used);\r\n            if (activePoints.length < MIN_POINTS_CURVE) {\r\n                alert(`Necesita seleccionar al menos ${MIN_POINTS_CURVE} puntos para trazar una curva. Puntos actuales: ${activePoints.length}.`);\r\n                return;\r\n            }\r\n            \r\n            let clusters = {};\r\n            let tolerance = 0.5; \r\n            activePoints.forEach(p => {\r\n                let keys = Object.keys(clusters).map(Number);\r\n                let found = false;\r\n                for (let k of keys) { if (Math.abs(p.V - k) < tolerance) { clusters[k].push(p); found = true; break; } }\r\n                if (!found) clusters[p.V] = [p];\r\n            });\r\n\r\n            let colors = ['#fbbf24', '#a78bfa', '#f472b6', '#34d399', '#fb923c', '#38bdf8'];\r\n            let newCurvesCount = 0;\r\n            \r\n            for (let V in clusters) {\r\n                let points = clusters[V];\r\n                if (points.length >= MIN_POINTS_CURVE) { \r\n                    let sorted = sortPointsNearestNeighbor(points);\r\n                    let sumV = points.reduce((acc, p) => acc + p.V, 0);\r\n                    let avgV = sumV \/ points.length;\r\n                    \r\n                    state.userCurves.push({ \r\n                        potential: parseFloat(V), \r\n                        averagePotential: avgV, \r\n                        points: sorted, \r\n                        color: colors[state.userCurves.length % colors.length] \r\n                    });\r\n                    \r\n                    points.forEach(p => p.used = true);\r\n                    newCurvesCount++;\r\n                }\r\n            }\r\n            \r\n            if (newCurvesCount > 0) {\r\n                state.measuredPoints = state.allPoints.filter(p => !p.used);\r\n                updateTable(); updateStats(); updateGeneratedList();\r\n            } else {\r\n                alert(`No se encontraron grupos con suficientes puntos (m\u00ednimo ${MIN_POINTS_CURVE}).`);\r\n            }\r\n        }\r\n\r\n        function generateFieldLinesFromPoints() {\r\n            let activePoints = state.allPoints.filter(p => !p.used);\r\n            if (activePoints.length < MIN_POINTS_CURVE) {\r\n                alert(`Necesita seleccionar al menos ${MIN_POINTS_CURVE} puntos para trazar una l\u00ednea. Puntos actuales: ${activePoints.length}.`);\r\n                return;\r\n            }\r\n\r\n            let sorted = sortPointsNearestNeighbor(activePoints);\r\n\r\n            state.userFieldLines.push({\r\n                points: sorted\r\n            });\r\n\r\n            sorted.forEach(p => p.used = true);\r\n            state.measuredPoints = state.allPoints.filter(p => !p.used);\r\n            updateTable(); updateStats(); updateGeneratedList();\r\n        }\r\n\r\n        \/\/ ==================== ELIMINACI\u00d3N DE TRAZAS ====================\r\n        \r\n        function deleteCurve(index) {\r\n            let curve = state.userCurves[index];\r\n            if (!curve) return;\r\n\r\n            \/\/ Filtrar puntos de esta curva del registro total\r\n            let pointsToRemove = new Set(curve.points.map(p => p)); \/\/ Set de referencias\r\n            state.allPoints = state.allPoints.filter(p => !pointsToRemove.has(p));\r\n            \r\n            \/\/ Eliminar la curva\r\n            state.userCurves.splice(index, 1);\r\n            \r\n            \/\/ Actualizar puntos activos (por si acaso)\r\n            state.measuredPoints = state.allPoints.filter(p => !p.used);\r\n            \r\n            updateTable(); updateStats(); updateGeneratedList();\r\n        }\r\n\r\n        function deleteFieldLine(index) {\r\n            let line = state.userFieldLines[index];\r\n            if (!line) return;\r\n\r\n            \/\/ Filtrar puntos\r\n            let pointsToRemove = new Set(line.points.map(p => p));\r\n            state.allPoints = state.allPoints.filter(p => !pointsToRemove.has(p));\r\n            \r\n            \/\/ Eliminar la l\u00ednea\r\n            state.userFieldLines.splice(index, 1);\r\n            \r\n            \/\/ Actualizar\r\n            state.measuredPoints = state.allPoints.filter(p => !p.used);\r\n            \r\n            updateTable(); updateStats(); updateGeneratedList();\r\n        }\r\n\r\n        function updateGeneratedList() {\r\n            let list = document.getElementById('generatedList');\r\n            list.innerHTML = '';\r\n\r\n            if (state.userCurves.length === 0 && state.userFieldLines.length === 0) {\r\n                list.innerHTML = '<div class=\"text-xs text-center text-[var(--fg-muted)]\">Ninguna traza generada.<\/div>';\r\n                return;\r\n            }\r\n\r\n            \/\/ Listar Equipotenciales\r\n            state.userCurves.forEach((c, i) => {\r\n                let div = document.createElement('div');\r\n                div.className = 'generated-item';\r\n                div.innerHTML = `\r\n                    <span style=\"color:${c.color}\">Equipotencial ${i+1} (${c.averagePotential.toFixed(1)}V)<\/span>\r\n                    <button class=\"btn-delete-small\" onclick=\"deleteCurve(${i})\">Borrar<\/button>\r\n                `;\r\n                list.appendChild(div);\r\n            });\r\n\r\n            \/\/ Listar L\u00edneas de Campo\r\n            state.userFieldLines.forEach((l, i) => {\r\n                let div = document.createElement('div');\r\n                div.className = 'generated-item';\r\n                div.innerHTML = `\r\n                    <span style=\"color:#fb923c\">L. Campo ${i+1}<\/span>\r\n                    <button class=\"btn-delete-small\" onclick=\"deleteFieldLine(${i})\">Borrar<\/button>\r\n                `;\r\n                list.appendChild(div);\r\n            });\r\n        }\r\n\r\n        \/\/ ==================== INTERACTION ====================\r\n        \r\n        function handleMouseMove(e) {\r\n            const rect = canvas.getBoundingClientRect();\r\n            const world = screenToWorld(e.clientX - rect.left, e.clientY - rect.top);\r\n            state.probe.x = world.x; state.probe.y = world.y;\r\n            document.getElementById('probeX').textContent = world.x.toFixed(2);\r\n            document.getElementById('probeY').textContent = world.y.toFixed(2);\r\n            let V = state.powerOn ? calculatePotential(world.x, world.y) : 0;\r\n            document.getElementById('voltmeterDisplay').textContent = V.toFixed(2) + ' V';\r\n            if (state.draggingElectrode !== null) {\r\n                state.electrodes[state.draggingElectrode].x = world.x;\r\n                state.electrodes[state.draggingElectrode].y = world.y;\r\n                state.needsRecalculation = true;\r\n            }\r\n        }\r\n\r\n        function handleMouseDown(e) {\r\n            if (e.button === 0) {\r\n                const rect = canvas.getBoundingClientRect();\r\n                const world = screenToWorld(e.clientX - rect.left, e.clientY - rect.top);\r\n                let found = false;\r\n                for (let i = 0; i < state.electrodes.length; i++) {\r\n                    let el = state.electrodes[i];\r\n                    let dist = Math.sqrt((world.x - el.x)**2 + (world.y - el.y)**2);\r\n                    let hitRadius = el.type === 'plate' ? 1.0 : (el.radius || 0.5) + 0.3;\r\n                    if (dist < hitRadius) { state.draggingElectrode = i; canvas.style.cursor = 'grabbing'; found = true; break; }\r\n                }\r\n                if (!found) addPoint(world.x, world.y);\r\n            }\r\n        }\r\n\r\n        function handleRightClick(e) {\r\n            e.preventDefault();\r\n            const rect = canvas.getBoundingClientRect();\r\n            const world = screenToWorld(e.clientX - rect.left, e.clientY - rect.top);\r\n            deleteNearestPoint(world.x, world.y);\r\n        }\r\n\r\n        function handleWheel(e) {\r\n            e.preventDefault();\r\n            const delta = e.deltaY > 0 ? 0.9 : 1.1;\r\n            state.view.zoom = Math.max(0.5, Math.min(5, state.view.zoom * delta));\r\n            document.getElementById('zoomLevel').textContent = Math.round(state.view.zoom * 100);\r\n            state.needsRecalculation = true;\r\n        }\r\n\r\n        \/\/ ==================== UI ACTIONS ====================\r\n        \r\n        function addPoint(x, y) {\r\n            let V = calculatePotential(x, y);\r\n            let newPoint = { x, y, V, used: false };\r\n            state.allPoints.push(newPoint);\r\n            state.measuredPoints.push(newPoint);\r\n            updateTable(); updateStats();\r\n        }\r\n\r\n        function deleteNearestPoint(x, y) {\r\n            if (state.measuredPoints.length === 0) return;\r\n            let minD = Infinity, idxActive = -1;\r\n            state.measuredPoints.forEach((p, i) => {\r\n                let d = (p.x - x)**2 + (p.y - y)**2;\r\n                if (d < minD) { minD = d; idxActive = i; }\r\n            });\r\n            if (idxActive !== -1 && minD < 2.0) { \r\n                let pointToRemove = state.measuredPoints[idxActive];\r\n                state.measuredPoints.splice(idxActive, 1);\r\n                let idxInAll = state.allPoints.indexOf(pointToRemove);\r\n                if (idxInAll > -1) state.allPoints.splice(idxInAll, 1);\r\n                updateTable(); updateStats();\r\n            }\r\n        }\r\n\r\n        function updateTable() {\r\n            let tbody = document.getElementById('pointsTable');\r\n            tbody.innerHTML = '';\r\n            state.allPoints.forEach((p, i) => {\r\n                let tr = document.createElement('tr');\r\n                tr.className = p.used ? \"used-point-row\" : \"\";\r\n                tr.innerHTML = `<td class=\"py-1 px-2\">${i+1}<\/td>\r\n                                <td class=\"text-right py-1 px-2\">${p.x.toFixed(2)}<\/td>\r\n                                <td class=\"text-right py-1 px-2\">${p.y.toFixed(2)}<\/td>\r\n                                <td class=\"text-right py-1 px-2\">${p.V.toFixed(2)}<\/td>`;\r\n                tbody.appendChild(tr);\r\n            });\r\n            let container = tbody.parentElement;\r\n            container.scrollTop = container.scrollHeight;\r\n        }\r\n\r\n        function updateStats() {\r\n            document.getElementById('totalPoints').textContent = state.measuredPoints.length;\r\n            document.getElementById('totalCurves').textContent = state.userCurves.length;\r\n            document.getElementById('totalFieldLines').textContent = state.userFieldLines.length;\r\n        }\r\n\r\n        function togglePower() {\r\n            state.powerOn = !state.powerOn;\r\n            let btn = document.getElementById('powerBtn');\r\n            if (state.powerOn) {\r\n                btn.textContent = 'Apagar'; btn.classList.add('active');\r\n                document.getElementById('powerLed').classList.add('on');\r\n                document.getElementById('statusLed').classList.add('on');\r\n                document.getElementById('statusLed').classList.remove('off');\r\n                document.getElementById('statusText').textContent = 'Sistema activo';\r\n                state.needsRecalculation = true;\r\n            } else {\r\n                btn.textContent = 'Encender'; btn.classList.remove('active');\r\n                document.getElementById('powerLed').classList.remove('on');\r\n                document.getElementById('statusLed').classList.remove('on');\r\n                document.getElementById('statusLed').classList.add('off');\r\n                document.getElementById('statusText').textContent = 'Sistema inactivo';\r\n            }\r\n        }\r\n\r\n        function invertPolarity() {\r\n            state.polarity *= -1;\r\n            state.electrodes.forEach(el => {\r\n                el.charge *= -1;\r\n                el.color = el.charge > 0 ? '#ef4444' : '#3b82f6';\r\n                el.label = el.charge > 0 ? '+' : '-';\r\n            });\r\n            state.needsRecalculation = true;\r\n        }\r\n\r\n        function loadConfiguration() {\r\n            const val = document.getElementById('configSelect').value;\r\n            state.electrodes = configurations[val] ? configurations[val]() : [];\r\n            state.needsRecalculation = true;\r\n        }\r\n\r\n        \/\/ Correcci\u00f3n Exportaci\u00f3n CSV\r\n        function exportCSV() {\r\n            let csv = 'ID,Tipo,X (cm),Y (cm),Potencial (V),Estado\\n';\r\n            \r\n            state.allPoints.forEach((p, i) => {\r\n                let status = p.used ? \"Fijado\" : \"Activo\";\r\n                csv += `${i+1},Punto,${p.x.toFixed(4)},${p.y.toFixed(4)},${p.V.toFixed(4)},${status}\\n`;\r\n            });\r\n\r\n            state.userCurves.forEach((c, ci) => {\r\n                csv += `# Equipotencial ${ci+1},Promedio: ${c.averagePotential.toFixed(2)} V\\n`;\r\n            });\r\n\r\n            state.userFieldLines.forEach((l, li) => {\r\n                csv += `# Linea de Campo ${li+1}\\n`;\r\n            });\r\n\r\n            \/\/ Crear Blob y enlace de descarga\r\n            let blob = new Blob([\"\\ufeff\" + csv], { type: 'text\/csv;charset=utf-8;' }); \/\/ BOM for Excel\r\n            let link = document.createElement(\"a\");\r\n            let url = URL.createObjectURL(blob);\r\n            link.setAttribute(\"href\", url);\r\n            link.setAttribute(\"download\", \"datos_laboratorio.csv\");\r\n            link.style.visibility = 'hidden';\r\n            document.body.appendChild(link);\r\n            link.click();\r\n            document.body.removeChild(link);\r\n        }\r\n\r\n        \/\/ Correcci\u00f3n Descarga Imagen\r\n        function downloadImage() {\r\n            \/\/ Asegurar renderizado actual\r\n            render();\r\n            \r\n            \/\/ Crear enlace temporal\r\n            let link = document.createElement('a');\r\n            link.download = 'cuba_electrolitica.png';\r\n            link.href = canvas.toDataURL('image\/png');\r\n            document.body.appendChild(link); \/\/ Firefox requirement\r\n            link.click();\r\n            document.body.removeChild(link);\r\n        }\r\n\r\n        function zoomIn() { state.view.zoom = Math.min(5, state.view.zoom*1.2); document.getElementById('zoomLevel').textContent = Math.round(state.view.zoom*100); state.needsRecalculation = true; }\r\n        function zoomOut() { state.view.zoom = Math.max(0.5, state.view.zoom\/1.2); document.getElementById('zoomLevel').textContent = Math.round(state.view.zoom*100); state.needsRecalculation = true; }\r\n        function resetView() { state.view = {zoom:1, offsetX:0, offsetY:0}; document.getElementById('zoomLevel').textContent='100'; state.needsRecalculation = true; }\r\n        \r\n        function resetSimulation() {\r\n            state.allPoints = [];\r\n            state.measuredPoints = [];\r\n            state.userCurves = [];\r\n            state.userFieldLines = [];\r\n            state.voltage = 0; state.powerOn = false;\r\n            document.getElementById('voltageSlider').value = 0;\r\n            document.getElementById('voltageDisplay').textContent = '0.00 V';\r\n            document.getElementById('powerBtn').textContent = 'Encender';\r\n            document.getElementById('powerBtn').classList.remove('active');\r\n            document.getElementById('powerLed').classList.remove('on');\r\n            document.getElementById('statusLed').classList.remove('on');\r\n            document.getElementById('statusLed').classList.add('off');\r\n            document.getElementById('statusText').textContent = 'Sistema inactivo';\r\n            loadConfiguration();\r\n            updateTable(); updateStats(); updateGeneratedList();\r\n        }\r\n\r\n        document.getElementById('voltageSlider').addEventListener('input', function() {\r\n            state.voltage = parseFloat(this.value);\r\n            document.getElementById('voltageDisplay').textContent = state.voltage.toFixed(2) + ' V';\r\n            state.needsRecalculation = true;\r\n        });\r\n    <\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Laboratorio Virtual &#8211; Cuba Electrolitica Laboratorio Virtual &#8211; Cuba Electrolitica FICA &#8211; UNSL Simulacion interactiva de superficies equipotenciales y lineas de campo Sistema inactivo Reiniciar&hellip;<\/p>\n","protected":false},"author":7,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"class_list":["post-6107","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>graficar_sup_equipotencial_lin_campo - F\u00edsica 2<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"graficar_sup_equipotencial_lin_campo - F\u00edsica 2\" \/>\n<meta property=\"og:description\" content=\"Laboratorio Virtual &#8211; Cuba Electrolitica Laboratorio Virtual &#8211; Cuba Electrolitica FICA &#8211; UNSL Simulacion interactiva de superficies equipotenciales y lineas de campo Sistema inactivo Reiniciar&hellip;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/\" \/>\n<meta property=\"og:site_name\" content=\"F\u00edsica 2\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-26T14:18:33+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minuto\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/\",\"name\":\"graficar_sup_equipotencial_lin_campo - F\u00edsica 2\",\"isPartOf\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#website\"},\"datePublished\":\"2026-03-13T17:35:30+00:00\",\"dateModified\":\"2026-03-26T14:18:33+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/#breadcrumb\"},\"inLanguage\":\"es-AR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Inicio\",\"item\":\"https:\/\/fisica2.fica.unsl.edu.ar\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"graficar_sup_equipotencial_lin_campo\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#website\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/\",\"name\":\"F\u00edsica 2\",\"description\":\"FICA - UNSL\",\"publisher\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/fisica2.fica.unsl.edu.ar\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es-AR\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#organization\",\"name\":\"SAC- Secretar\u00eda General FICA\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es-AR\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png\",\"contentUrl\":\"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png\",\"width\":4483,\"height\":1231,\"caption\":\"SAC- Secretar\u00eda General FICA\"},\"image\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"graficar_sup_equipotencial_lin_campo - F\u00edsica 2","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/","og_locale":"es_ES","og_type":"article","og_title":"graficar_sup_equipotencial_lin_campo - F\u00edsica 2","og_description":"Laboratorio Virtual &#8211; Cuba Electrolitica Laboratorio Virtual &#8211; Cuba Electrolitica FICA &#8211; UNSL Simulacion interactiva de superficies equipotenciales y lineas de campo Sistema inactivo Reiniciar&hellip;","og_url":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/","og_site_name":"F\u00edsica 2","article_modified_time":"2026-03-26T14:18:33+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"1 minuto"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/","url":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/","name":"graficar_sup_equipotencial_lin_campo - F\u00edsica 2","isPartOf":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#website"},"datePublished":"2026-03-13T17:35:30+00:00","dateModified":"2026-03-26T14:18:33+00:00","breadcrumb":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/#breadcrumb"},"inLanguage":"es-AR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/graficar_sup_equipotencial_lin_campo\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https:\/\/fisica2.fica.unsl.edu.ar\/"},{"@type":"ListItem","position":2,"name":"graficar_sup_equipotencial_lin_campo"}]},{"@type":"WebSite","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#website","url":"https:\/\/fisica2.fica.unsl.edu.ar\/","name":"F\u00edsica 2","description":"FICA - UNSL","publisher":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/fisica2.fica.unsl.edu.ar\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es-AR"},{"@type":"Organization","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#organization","name":"SAC- Secretar\u00eda General FICA","url":"https:\/\/fisica2.fica.unsl.edu.ar\/","logo":{"@type":"ImageObject","inLanguage":"es-AR","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/","url":"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png","contentUrl":"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png","width":4483,"height":1231,"caption":"SAC- Secretar\u00eda General FICA"},"image":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages\/6107","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/comments?post=6107"}],"version-history":[{"count":73,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages\/6107\/revisions"}],"predecessor-version":[{"id":6755,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages\/6107\/revisions\/6755"}],"wp:attachment":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/media?parent=6107"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}