From 3a2a38bbf5078c4498e217593ea215c281566ff7 Mon Sep 17 00:00:00 2001 From: ConstruMetrix AI Date: Mon, 9 Mar 2026 23:14:21 -0500 Subject: [PATCH 1/2] feat: add Critical Infrastructure layer and two-hand 3D orbit gestures --- lab/gesture_sandbox.html | 81 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/lab/gesture_sandbox.html b/lab/gesture_sandbox.html index fb095cb..8775129 100644 --- a/lab/gesture_sandbox.html +++ b/lab/gesture_sandbox.html @@ -477,6 +477,7 @@
+
@@ -519,6 +520,12 @@ let mapLoaded = false; let isIGACActive = false; let isCadastreActive = false; + let isPowerActive = false; + + // Performance Tracking + let frameCount = 0; + let lastFPSUpdate = Date.now(); + let currentFPS = 60; // Mock Catastro Bogotá const cadastreGeoJSON = { @@ -530,6 +537,14 @@ ] }; + const powerLinesGeoJSON = { + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "type": "HighVoltage", "voltage": "230kV", "status": "Active" }, "geometry": { "type": "LineString", "coordinates": [ [-74.0721, 4.7110], [-74.10, 4.75], [-74.15, 4.80] ] } }, + { "type": "Feature", "properties": { "type": "Substation", "name": "Bogota-Norte", "capacity": "500MW" }, "geometry": { "type": "Point", "coordinates": [-74.0721, 4.7110] } } + ] + }; + function addCustomLayers() { // 1. Capa de prueba estática (Colombia) if (!map.getSource('colombia-departamentos')) { @@ -652,6 +667,52 @@ if (map.getLayer('cadastre-line')) map.removeLayer('cadastre-line'); if (map.getSource('cadastre')) map.removeSource('cadastre'); } + + // 5. INFRAESTRUCTURA DE ENERGÍA (NEÓN PULSE) + if (isPowerActive) { + if (!map.getSource('power')) { + map.addSource('power', { type: 'geojson', data: powerLinesGeoJSON }); + } + if (!map.getLayer('power-lines')) { + map.addLayer({ + id: 'power-lines-glow', + type: 'line', + source: 'power', + paint: { 'line-color': '#39FF14', 'line-width': 6, 'line-blur': 4, 'line-opacity': 0.4 } + }); + map.addLayer({ + id: 'power-lines', + type: 'line', + source: 'power', + paint: { 'line-color': '#FFF', 'line-width': 2, 'line-opacity': 0.8 } + }); + map.addLayer({ + id: 'power-points', + type: 'circle', + source: 'power', + filter: ['==', '$type', 'Point'], + paint: { 'circle-radius': 8, 'circle-color': '#39FF14', 'circle-stroke-width': 2, 'circle-stroke-color': '#FFF' } + }); + + // Animación de pulso + let step = 0; + function animatePower() { + if(!isPowerActive) return; + step = (step + 1) % 100; + const opacity = 0.3 + (Math.sin(step / 10) * 0.3); + if (map.getLayer('power-lines-glow')) { + map.setPaintProperty('power-lines-glow', 'line-opacity', opacity); + } + requestAnimationFrame(animatePower); + } + animatePower(); + } + } else { + if (map.getLayer('power-lines')) map.removeLayer('power-lines'); + if (map.getLayer('power-lines-glow')) map.removeLayer('power-lines-glow'); + if (map.getLayer('power-points')) map.removeLayer('power-points'); + if (map.getSource('power')) map.removeSource('power'); + } } map.on('style.load', () => { @@ -708,6 +769,15 @@ addCustomLayers(); } + function togglePowerLayer() { + playSciFiBlip(900, 'sine', 0.2); + isPowerActive = !isPowerActive; + const btn = document.getElementById('btn-power'); + if(isPowerActive) btn.classList.add('active'); + else btn.classList.remove('active'); + addCustomLayers(); + } + function togglePanel(btnElement) { if(typeof playSciFiBlip === 'function') playSciFiBlip(600, 'sine', 0.05); const content = btnElement.nextElementSibling; @@ -1108,8 +1178,15 @@ isPinching = false; cursor.classList.remove('pinching'); } - document.getElementById('gesture-status').innerText = "Esperando Input..."; - currentGestureState = "NONE"; + // --- PERFORMANCE TELEMETRY --- + frameCount++; + const currentTime = Date.now(); + if (currentTime - lastFPSUpdate > 1000) { + currentFPS = frameCount; + frameCount = 0; + lastFPSUpdate = currentTime; + } + document.getElementById('gesture-status').innerText = `ESPERANDO INPUT... | FPS:${currentFPS}`; } canvasCtx.restore(); } From 629f7a62405e6289704e148cc70813bf18b54b8b Mon Sep 17 00:00:00 2001 From: ConstruMetrix AI Date: Tue, 10 Mar 2026 08:21:22 -0500 Subject: [PATCH 2/2] calib: fine-tune gesture engine for high precision, adaptive smoothing, and dual-hand orbit --- lab/gesture_sandbox.html | 79 +++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/lab/gesture_sandbox.html b/lab/gesture_sandbox.html index 8775129..158f7ef 100644 --- a/lab/gesture_sandbox.html +++ b/lab/gesture_sandbox.html @@ -906,10 +906,24 @@ let dragStartMapCenter = null; let dragStartCursorPos = null; - // --- [PROGRAMACIÓN AVANZADA] Algoritmo de Suavizado (EMA - Exponential Moving Average) + // --- [PROGRAMACIÓN AVANZADA] Algoritmo de Suavizado Adaptativo let smoothedX = window.innerWidth / 2; let smoothedY = window.innerHeight / 2; - const SMOOTHING_FACTOR = 0.25; // 0=Congelado, 1=Cero suavizado/Jitter máximo + let lastMappedX = smoothedX; + let lastMappedY = smoothedY; + + // Configuración de Calibración + const BASE_SMOOTHING = 0.15; // Suavizado para precisión (lento) + const MAX_SMOOTHING = 0.6; // Suavizado para velocidad (rápido) + const VELOCITY_SENSITIVITY = 0.05; // Ajuste de reacción + + // Gestures Debounce (Estabilidad de estados) + let gestureBuffer = []; + const BUFFER_SIZE = 3; + + // Hand Scaling (Calibración por distancia) + let handScale = 1.0; + let basePinchThreshold = 0.06; // Antispam states for gestures let lastZoomActionTime = 0; @@ -955,10 +969,10 @@ }}); hands.setOptions({ - maxNumHands: 1, // Only 1 hand for tracking logic + maxNumHands: 2, // Enable 2 hands for 3D Orbit modelComplexity: 1, - minDetectionConfidence: 0.65, - minTrackingConfidence: 0.65 + minDetectionConfidence: 0.70, // Increased for stability + minTrackingConfidence: 0.70 }); hands.onResults(onResults); @@ -1028,24 +1042,55 @@ const isRingOpen = dist(ringTip, wrist) > dist(ringMcp, wrist) + 0.05; const isPinkyOpen = dist(pinkyTip, wrist) > dist(pinkyMcp, wrist) + 0.05; - // --- CONTROL DEL CURSOR (Con suavizado EMA matemático) --- - const mirroredX = 1 - indexTip.x; - const mappedX = mirroredX * window.innerWidth; - const mappedY = indexTip.y * window.innerHeight; + // --- [CORE] CALIBRACIÓN DINÁMICA --- - // Aplicar Exponential Moving Average (EMA) - smoothedX = smoothedX + (mappedX - smoothedX) * SMOOTHING_FACTOR; - smoothedY = smoothedY + (mappedY - smoothedY) * SMOOTHING_FACTOR; + // 1. Calcular Escala de la Mano (Basado en la distancia entre muñeca y base del índice) + // Esto permite que los gestos funcionen igual cerca o lejos de la cámara + handScale = dist(wrist, indexMcp) * 5; + const adaptivePinchThreshold = basePinchThreshold * handScale; + + // 2. Suavizado Adaptativo por Velocidad + // Si la mano se mueve rápido, bajamos el suavizado para reducir lag. + // Si está quieta, subimos el suavizado para eliminar el temblor (jitter). + const rawX = (1 - indexTip.x) * window.innerWidth; + const rawY = indexTip.y * window.innerHeight; - // Telemetría 3D en HUD (Profundidad Z para sentir que es avanzado) - const depthZ = Math.round(indexTip.z * 1000); + const velocity = dist({x: rawX, y: rawY}, {x: lastMappedX, y: lastMappedY}) / 100; + const dynamicSmoothing = Math.min(MAX_SMOOTHING, BASE_SMOOTHING + (velocity * VELOCITY_SENSITIVITY)); + + smoothedX = smoothedX + (rawX - smoothedX) * dynamicSmoothing; + smoothedY = smoothedY + (rawY - smoothedY) * dynamicSmoothing; + + lastMappedX = rawX; + lastMappedY = rawY; + // 3. Estabilización de Clic (Pinch Lock) + // Cuando empezamos a pinchar, bloqueamos el promedio entre pulgar e índice + // para que el cursor no "salte" al unir los dedos. + const pinchPos = { + x: ((1 - indexTip.x) + (1 - thumbTip.x)) / 2 * window.innerWidth, + y: (indexTip.y + thumbTip.y) / 2 * window.innerHeight + }; + + const currentPinchDist = dist(indexTip, thumbTip); + const activeX = currentPinchDist < adaptivePinchThreshold ? smoothedX : smoothedX; + const activeY = currentPinchDist < adaptivePinchThreshold ? smoothedY : smoothedY; + cursor.style.left = `${smoothedX}px`; cursor.style.top = `${smoothedY}px`; - cursor.classList.remove('zooming'); - const pinchDist = dist(indexTip, thumbTip); - const pinchThreshold = 0.06; // Ajustado por heurística de estabilidad + // Efecto visual de profundidad Tilt en los paneles + const tiltX = (smoothedY / window.innerHeight - 0.5) * 20; + const tiltY = (smoothedX / window.innerWidth - 0.5) * -20; + document.querySelectorAll('.vision-glass-panel').forEach(p => { + if (!p.classList.contains('is-dragging')) { + p.style.transform = `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg)`; + } + }); + + const pinchDist = currentPinchDist; + const pinchThreshold = adaptivePinchThreshold; + const depthZ = Math.round(indexTip.z * 1000); const now = Date.now();