UNPKG

aframe-babia-components

Version:

A data visualization set of components for A-Frame.

1,627 lines (1,356 loc) 93.5 kB
// babia-dome.js - Componente Base Universal VR // Compatible con cualquier caso de uso: planetario, sensores, redes, etc. AFRAME.registerComponent('babia-dome', { schema: { // Datos dataUrl: { type: 'string', default: 'data.json' }, // Geometría sphereRadius: { type: 'number', default: 10 }, nodeSize: { type: 'number', default: 0.15 }, // Visual showLabels: { type: 'boolean', default: false }, animateNodes: { type: 'boolean', default: true }, colorByValue: { type: 'boolean', default: false }, nodeOpacity: { type: 'number', default: 1.0 }, // Mapeo de métricas colorByCVE: { type: 'boolean', default: false }, colorByLicense: { type: 'boolean', default: false }, licenseFilter: { type: 'string', default: '' }, sizeByPackageSize: { type: 'boolean', default: false }, linkColorByType: { type: 'boolean', default: false }, // Físicas enablePhysics: { type: 'boolean', default: true }, physicsStrength: { type: 'number', default: 0.5 }, collisionRadius: { type: 'number', default: 0.25 }, // Links - NUEVOS PARÁMETROS PARA GROSOR showLinks: { type: 'boolean', default: true }, linkSegments: { type: 'number', default: 30 }, linkOpacity: { type: 'number', default: 0.5 }, linkWidth: { type: 'number', default: 0.08 }, // Grosor del enlace (radio del cilindro) linkStyle: { type: 'string', default: 'line' }, // 'line' o 'cylinder' // Tooltip - Retraso en VR tooltipDelayVR: { type: 'number', default: 1000 }, // Tiempo en ms antes de mostrar tooltip en VR // Escalas minNodeSize: { type: 'number', default: 0.1 }, maxNodeSize: { type: 'number', default: 0.4 }, }, init: function() { console.log('🚀 Inicializando Visualizador Universal v2.1 - Enlaces Gruesos'); this.nodes = []; this.nodeElements = []; this.linkElements = []; this.simulation = null; this.simulationNodes = []; this.simulationLinks = []; this.constellations = []; // Estado del sistema this.state = { hoveredNode: null, tooltipEl: null, tooltipBg: null, errorTooltipEl: null, errorTooltipBg: null, isVRMode: false, physicsRunning: this.data.enablePhysics, tooltipTimer: null }; this.setupTooltip(); this.setupErrorTooltip(); this.setupRaycasterEvents(); this.detectVRMode(); this.loadData(); }, detectVRMode: function() { const scene = this.el.sceneEl; const self = this; scene.addEventListener('enter-vr', function() { console.log('🥽 VR Mode Activated'); self.state.isVRMode = true; self.repositionTooltipForVR(); }); scene.addEventListener('exit-vr', function() { console.log('🖥️ Desktop Mode'); self.state.isVRMode = false; self.repositionTooltipForDesktop(); }); }, setupTooltip: function() { const self = this; const scene = document.querySelector('a-scene'); const createTooltip = function() { const cameraEl = document.querySelector('a-entity[camera], a-camera'); if (!cameraEl) { setTimeout(createTooltip, 100); return; } // Fondo del tooltip const bg = document.createElement('a-plane'); bg.setAttribute('id', 'tooltip-background'); bg.setAttribute('width', '2'); bg.setAttribute('height', '0.5'); bg.setAttribute('color', '#000000'); bg.setAttribute('opacity', '0.9'); bg.setAttribute('position', '0 -0.3 -1.5'); bg.setAttribute('visible', 'false'); // Texto del tooltip const tooltipEl = document.createElement('a-text'); tooltipEl.setAttribute('id', 'node-tooltip'); tooltipEl.setAttribute('position', '0 -0.3 -1.49'); tooltipEl.setAttribute('width', '1.8'); tooltipEl.setAttribute('align', 'center'); tooltipEl.setAttribute('color', '#4ECDC4'); tooltipEl.setAttribute('value', ''); tooltipEl.setAttribute('wrap-count', '40'); tooltipEl.setAttribute('visible', 'false'); cameraEl.appendChild(bg); cameraEl.appendChild(tooltipEl); self.state.tooltipEl = tooltipEl; self.state.tooltipBg = bg; console.log('✅ Tooltip creado'); }; if (scene.hasLoaded) { createTooltip(); } else { scene.addEventListener('loaded', createTooltip); } }, repositionTooltipForVR: function() { if (this.state.tooltipEl && this.state.tooltipBg) { this.state.tooltipEl.setAttribute('position', '0 -0.4 -1'); this.state.tooltipBg.setAttribute('position', '0 -0.4 -1.01'); this.state.tooltipEl.setAttribute('width', '1.5'); this.state.tooltipBg.setAttribute('width', '1.7'); } }, repositionTooltipForDesktop: function() { if (this.state.tooltipEl && this.state.tooltipBg) { this.state.tooltipEl.setAttribute('position', '0 -0.3 -1.5'); this.state.tooltipBg.setAttribute('position', '0 -0.3 -1.51'); this.state.tooltipEl.setAttribute('width', '1.8'); this.state.tooltipBg.setAttribute('width', '2'); } }, setupErrorTooltip: function() { const self = this; const scene = document.querySelector('a-scene'); const createErrorTooltip = function() { const cameraEl = document.querySelector('a-entity[camera], a-camera'); if (!cameraEl) { setTimeout(createErrorTooltip, 100); return; } // Fondo del tooltip de error const bg = document.createElement('a-plane'); bg.setAttribute('id', 'error-tooltip-background'); bg.setAttribute('width', '1.4'); bg.setAttribute('height', '0.6'); bg.setAttribute('color', '#C85A54'); bg.setAttribute('opacity', '0.9'); bg.setAttribute('position', '-1.2 -0.8 -1.5'); bg.setAttribute('visible', 'false'); bg.setAttribute('material', 'shader: flat'); // Borde rojo del tooltip de error const border = document.createElement('a-plane'); border.setAttribute('id', 'error-tooltip-border'); border.setAttribute('width', '1.45'); border.setAttribute('height', '0.65'); border.setAttribute('color', '#E08B7E'); border.setAttribute('opacity', '0.6'); border.setAttribute('position', '-1.2 -0.8 -1.51'); border.setAttribute('visible', 'false'); border.setAttribute('material', 'shader: flat'); // Texto del tooltip de error const errorEl = document.createElement('a-text'); errorEl.setAttribute('id', 'error-tooltip'); errorEl.setAttribute('position', '-1.2 -0.75 -1.49'); errorEl.setAttribute('width', '1.2'); errorEl.setAttribute('align', 'center'); errorEl.setAttribute('color', '#FFFFFF'); errorEl.setAttribute('value', ''); errorEl.setAttribute('wrap-count', '15'); errorEl.setAttribute('visible', 'false'); errorEl.setAttribute('font', 'https://cdn.aframe.io/fonts/Roboto-msdf.json'); cameraEl.appendChild(border); cameraEl.appendChild(bg); cameraEl.appendChild(errorEl); self.state.errorTooltipEl = errorEl; self.state.errorTooltipBg = bg; console.log('✅ Error tooltip creado'); }; if (scene.hasLoaded) { createErrorTooltip(); } else { scene.addEventListener('loaded', createErrorTooltip); } }, showErrorScene: function(error) { const errorMessage = error.message || 'Error desconocido al cargar datos'; if (this.state.errorTooltipEl && this.state.errorTooltipBg) { const fullMessage = `❌ Error\n${errorMessage}`; this.state.errorTooltipEl.setAttribute('value', fullMessage); this.state.errorTooltipEl.setAttribute('visible', 'true'); this.state.errorTooltipBg.setAttribute('visible', 'true'); // Animación de parpadeo para el tooltip de error this.state.errorTooltipBg.setAttribute('animation', { property: 'opacity', from: 0.8, to: 0.9, dur: 1000, dir: 'alternate', loop: true, easing: 'easeInOutQuad' }); } console.error('⚠️ Escena vacía - No hay datos válidos para mostrar'); }, setupRaycasterEvents: function() { const self = this; this.el.addEventListener('raycaster-intersected', function(evt) { self.onRaycasterIntersected(evt); }); this.el.addEventListener('raycaster-intersected-cleared', function(evt) { self.onRaycasterCleared(evt); }); this.el.addEventListener('mouseenter', function(evt) { self.onMouseEnter(evt); }); this.el.addEventListener('mouseleave', function(evt) { self.onMouseLeave(evt); }); }, onRaycasterIntersected: function(evt) { const intersection = evt.detail && evt.detail.intersection; if (!intersection) return; let obj = intersection.object; let depth = 0; while (obj && !obj.userData.nodeData && depth < 10) { obj = obj.parent; depth++; } if (obj && obj.userData.nodeData) { const nodeData = obj.userData.nodeData; this.state.hoveredNode = nodeData; if (obj.el) { const sphere = obj.el.querySelector('a-sphere'); if (sphere) { sphere.setAttribute('scale', '1.8 1.8 1.8'); } } // En VR, usar retraso; en desktop, mostrar inmediatamente if (this.state.isVRMode) { // Cancelar timer anterior si existe if (this.state.tooltipTimer) { clearTimeout(this.state.tooltipTimer); } // Iniciar nuevo timer this.state.tooltipTimer = setTimeout(() => { this.showTooltip(nodeData); }, this.data.tooltipDelayVR); } else { // Modo desktop: mostrar inmediatamente this.showTooltip(nodeData); } } }, onRaycasterCleared: function(evt) { if (this.state.hoveredNode) { // Cancelar timer si existe if (this.state.tooltipTimer) { clearTimeout(this.state.tooltipTimer); this.state.tooltipTimer = null; } this.hideTooltip(); const nodeEl = this.nodeElements.find(el => el.nodeData && el.nodeData.id === this.state.hoveredNode.id ); if (nodeEl) { const sphere = nodeEl.querySelector('a-sphere'); if (sphere) { sphere.setAttribute('scale', '1 1 1'); } } this.state.hoveredNode = null; } }, onMouseEnter: function(evt) { this.onRaycasterIntersected(evt); }, onMouseLeave: function(evt) { this.onRaycasterCleared(evt); }, showTooltip: function(nodeData) { if (this.state.tooltipEl && this.state.tooltipBg) { let text = `${nodeData.label}`; if (nodeData.type) { text += `\nTipo: ${nodeData.type}`; } if (nodeData.license) { text += `\n\u00A9 License: ${nodeData.license}`; } this.state.tooltipEl.setAttribute('value', text); this.state.tooltipEl.setAttribute('visible', 'true'); this.state.tooltipBg.setAttribute('visible', 'true'); } }, hideTooltip: function() { if (this.state.tooltipEl && this.state.tooltipBg) { this.state.tooltipEl.setAttribute('value', ''); this.state.tooltipEl.setAttribute('visible', 'false'); this.state.tooltipBg.setAttribute('visible', 'false'); } }, loadData: async function() { console.log('📂 Cargando datos desde:', this.data.dataUrl); try { const response = await fetch(this.data.dataUrl); const data = await response.json(); if (!data.nodes || !Array.isArray(data.nodes)) { throw new Error('Formato inválido: falta array "nodes"'); } const requiredFields = ['id', 'lat', 'lon', 'label', 'color']; // Filtrar nodos incompletos const validNodes = []; const invalidNodes = []; data.nodes.forEach((node, index) => { const missingFields = requiredFields.filter(field => !(field in node)); if (missingFields.length > 0) { invalidNodes.push({ id: node.id || `nodo_${index}`, missingFields: missingFields }); } else { validNodes.push(node); } }); // Avisos en consola sobre nodos descartados if (invalidNodes.length > 0) { console.warn(`⚠️ ${invalidNodes.length} nodo(s) descartado(s) por campos faltantes:`); invalidNodes.forEach(invalid => { console.warn(` - Nodo "${invalid.id}": faltan campos [${invalid.missingFields.join(', ')}]`); }); } if (validNodes.length === 0) { throw new Error('No hay nodos válidos en el archivo JSON'); } console.log(`✅ ${validNodes.length} nodo(s) válido(s) cargado(s)`); // Filtrar enlaces que referencien nodos inexistentes const validNodeIds = new Set(validNodes.map(n => n.id)); const linksData = data.links || []; const validLinks = []; const invalidLinks = []; linksData.forEach((link, index) => { const sourceExists = validNodeIds.has(link.source); const targetExists = validNodeIds.has(link.target); if (!sourceExists || !targetExists) { invalidLinks.push({ index: index, source: link.source, target: link.target, reason: !sourceExists && !targetExists ? 'source y target inexistentes' : !sourceExists ? 'source inexistente' : 'target inexistente' }); } else { validLinks.push(link); } }); // Avisos en consola sobre enlaces descartados if (invalidLinks.length > 0) { console.warn(`⚠️ ${invalidLinks.length} enlace(s) descartado(s) por referencia(s) inválida(s):`); invalidLinks.forEach(invalid => { console.warn(` - Enlace ${invalid.index}: ${invalid.source} → ${invalid.target} (${invalid.reason})`); }); } if (validLinks.length > 0) { console.log(`✅ ${validLinks.length} enlace(s) válido(s) cargado(s)`); } else { console.log(`ℹ️ Sin enlaces en los datos`); } this.createVisualization(validNodes, validLinks, data.constellations || []); } catch (error) { console.error('❌ Error cargando datos:', error); this.showErrorScene(error); } }, createVisualization: function(nodesData, linksData, constellationsData) { console.log('🎨 Creando visualización...'); const container = this.el; this.simulationNodes = nodesData.map((nodeData, index) => { const targetPos = this.latLonToVector3( nodeData.lat, nodeData.lon, this.data.sphereRadius ); const length = Math.sqrt(targetPos.x ** 2 + targetPos.y ** 2 + targetPos.z ** 2); const factor = this.data.sphereRadius / length; return { ...nodeData, targetX: targetPos.x, targetY: targetPos.y, targetZ: targetPos.z, x: targetPos.x * factor, y: targetPos.y * factor, z: targetPos.z * factor, vx: 0, vy: 0, vz: 0, index: index }; }); this.simulationLinks = linksData.map(link => ({ ...link, source: link.source, target: link.target })); this.constellations = constellationsData; if (this.data.enablePhysics && typeof d3 !== 'undefined' && d3.forceSimulation) { console.log('⚙️ Inicializando simulación física'); this.setupPhysicsSimulation(); } else { console.log('📍 Modo estático (sin físicas)'); } console.log('🔵 Creando', this.simulationNodes.length, 'nodos...'); this.simulationNodes.forEach((nodeData, index) => { const nodeEl = this.createNodeElement(nodeData, container); this.nodeElements.push(nodeEl); container.appendChild(nodeEl); }); if (this.data.showLinks && this.simulationLinks.length > 0) { console.log('🔗 Creando', this.simulationLinks.length, 'enlaces...'); this.createLinkElements(container); } this.createReferenceSphere(); this.createCoordinateGrid(); if (this.simulation) { this.startPhysicsUpdate(); } console.log('✅ Visualización completa:', this.simulationNodes.length, 'nodos', this.simulationLinks.length, 'enlaces'); }, setupPhysicsSimulation: function() { this.simulation = d3.forceSimulation(this.simulationNodes, 3) .numDimensions(3) .force("x", d3.forceX(d => d.targetX).strength(this.data.physicsStrength)) .force("y", d3.forceY(d => d.targetY).strength(this.data.physicsStrength)) .force("z", d3.forceZ(d => d.targetZ).strength(this.data.physicsStrength)) .force("collide", d3.forceCollide(this.data.collisionRadius).strength(1).iterations(2)) .force("radial", d3.forceRadial(this.data.sphereRadius).strength(0.15)) .force("charge", d3.forceManyBody().strength(-1).distanceMax(1.5)) .velocityDecay(0.9) .alphaDecay(0.005) .alphaMin(0.001) .alpha(0.2); if (this.simulationLinks.length > 0) { this.simulation.force("link", d3.forceLink(this.simulationLinks) .id(d => d.id) .distance(1.2) .strength(0.4) .iterations(2) ); } this.simulation.force("spherical", () => { this.simulationNodes.forEach(node => { const dx = node.x; const dy = node.y; const dz = node.z; const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); if (Math.abs(distance - this.data.sphereRadius) > 0.5) { const factor = this.data.sphereRadius / distance; node.x *= factor; node.y *= factor; node.z *= factor; } }); }); console.log('⚡ Simulación física inicializada'); }, startPhysicsUpdate: function() { const self = this; let lastTime = Date.now(); let tickCount = 0; this.simulation.on('tick', () => { const currentTime = Date.now(); const deltaTime = currentTime - lastTime; if (deltaTime < 16) return; lastTime = currentTime; tickCount++; self.simulationNodes.forEach((nodeData, i) => { const nodeEl = self.nodeElements[i]; if (nodeEl && nodeEl.parentNode) { if (tickCount % 5 === 0) { const distance = Math.sqrt( nodeData.x ** 2 + nodeData.y ** 2 + nodeData.z ** 2 ); const factor = self.data.sphereRadius / distance; nodeData.x *= factor; nodeData.y *= factor; nodeData.z *= factor; } nodeEl.setAttribute('position', { x: nodeData.x, y: nodeData.y, z: nodeData.z }); const line = nodeEl.lineElement; if (line && line.parentNode) { line.setAttribute('line', { start: '0 0 0', end: `${nodeData.x} ${nodeData.y} ${nodeData.z}`, color: line.getAttribute('line').color, opacity: 0.0 }); line.setAttribute('visible', 'false'); } } }); if (self.data.showLinks) { self.updateLinkArcs(); } }); this.simulation.on('end', () => { console.log('✅ Simulación estabilizada'); }); }, createNodeElement: function(nodeData, container) { const node = document.createElement('a-entity'); const sphere = document.createElement('a-sphere'); // Calcular tamaño basado en npm_size_bytes si está activado let nodeRadius = this.data.nodeSize; if (this.data.sizeByPackageSize && nodeData.npm_size_bytes) { const minSize = this.data.minNodeSize; const maxSize = this.data.maxNodeSize; // Normalizar tamaño de paquete (en bytes) a escala logarítmica const minBytes = 2000; const maxBytes = 400000; const normalized = Math.log(Math.max(minBytes, nodeData.npm_size_bytes)) / Math.log(maxBytes); nodeRadius = minSize + (maxSize - minSize) * Math.min(1, normalized); } sphere.setAttribute('radius', nodeRadius); sphere.setAttribute('class', 'interactive-node'); // Calcular color primero (antes de configurar el material) let color; if (this.data.colorByLicense) { color = this.licenseToColor(nodeData.license); } else if (this.data.colorByCVE) { if (nodeData.cvss_score !== null && nodeData.cvss_score !== undefined) { color = this.cvssScoreToColor(nodeData.cvss_score); } else { // Sin CVE/sin vulnerabilidades - usar azul oscuro uniforme color = '#1a3a5c'; } } else if (this.data.colorByValue && nodeData.value !== undefined) { color = this.valueToColor(nodeData.value); } else { color = this.toColor(nodeData.color); } // Filtro por licencia: atenuar los que no encajan const matchesLicense = this.licenseMatchesFilter(nodeData.license); if (!matchesLicense) { color = '#222222'; } const materialConfig = { shader: 'standard', color: color, emissive: color, emissiveIntensity: matchesLicense ? 0.6 : 0.05, roughness: 0.4, opacity: matchesLicense ? this.data.nodeOpacity : 0.15, transparent: matchesLicense ? (this.data.nodeOpacity < 1) : true }; // Aplicar textura si el nodo tiene campo 'texture' if (nodeData.texture) { const textureId = `#texture-${nodeData.texture}`; const textureElement = document.querySelector(textureId); if (textureElement) { materialConfig.src = textureId; // IMPORTANTE: Usar blanco para no alterar la textura materialConfig.color = '#ffffff'; materialConfig.emissive = '#ffffff'; // Opcional: reducir emissive para que se vea mejor la textura materialConfig.emissiveIntensity = 0.2; console.log(`✅ Textura aplicada: ${textureId}`); } else { // Si no hay textura, usar el color calculado materialConfig.color = color; materialConfig.emissive = color; console.warn(`⚠️ Textura no encontrada: ${textureId} para nodo ${nodeData.id}`); } } sphere.setAttribute('material', materialConfig); sphere.setAttribute('opacity', this.data.nodeOpacity); if (this.data.animateNodes) { const delay = nodeData.index * 20; sphere.setAttribute('animation', { property: 'scale', from: '1 1 1', to: '1.3 1.3 1.3', dur: 3000, delay: delay, dir: 'alternate', loop: true, easing: 'easeInOutQuad' }); } node.appendChild(sphere); node.sphereElement = sphere; sphere.addEventListener('loaded', () => { if (sphere.object3D) { sphere.object3D.userData.nodeData = nodeData; sphere.object3D.el = node; } }); const line = document.createElement('a-entity'); line.setAttribute('line', { start: '0 0 0', end: `${nodeData.x} ${nodeData.y} ${nodeData.z}`, color: color, opacity: 0 }); line.setAttribute('visible', 'false'); container.appendChild(line); node.lineElement = line; if (this.data.showLabels) { const label = document.createElement('a-text'); label.setAttribute('value', `${nodeData.label}`); label.setAttribute('align', 'center'); label.setAttribute('width', 3); label.setAttribute('color', '#ffffff'); label.setAttribute('position', `0 ${this.data.nodeSize + 0.2} 0`); label.setAttribute('look-at', '[camera]'); node.appendChild(label); node.labelElement = label; } node.setAttribute('position', { x: nodeData.x, y: nodeData.y, z: nodeData.z }); node.setAttribute('class', 'node-container'); node.nodeData = nodeData; return node; }, createLinkElements: function(container) { this.simulationLinks.forEach((link, index) => { const linkObject = new THREE.Group(); linkObject.linkData = link; container.object3D.add(linkObject); this.linkElements.push(linkObject); }); console.log(`🔗 ${this.linkElements.length} enlaces creados`); }, // MÉTODO ACTUALIZADO: Crear enlaces cilíndricos gruesos updateLinkArcs: function() { const LINK_STYLES = { constellation: { color: 0xffffff, opacity: 0.9, width: this.data.linkWidth * 1.2 }, proximity: { color: 0x27EEF5, opacity: 0.8, width: this.data.linkWidth }, quimical: { color: 0xFF0D0D, opacity: 0.7, width: this.data.linkWidth } }; // Estilos para dependency type (direct/transitive) const DEPENDENCY_STYLES = { direct: { color: 0x3050F8, opacity: 0.9, width: this.data.linkWidth * 1.2 }, // Azul transitive: { color: 0x808080, opacity: 0.5, width: this.data.linkWidth } // Gris }; const DEFAULT_LINK_STYLE = { color: 0xffffff, opacity: 0.6, width: this.data.linkWidth }; this.simulationLinks.forEach((link, i) => { const linkObject = this.linkElements[i]; if (!linkObject) return; const sourceNode = link.source; const targetNode = link.target; if (!sourceNode || !targetNode) return; // Limpiar geometría anterior while (linkObject.children.length > 0) { const child = linkObject.children[0]; if (child.geometry) child.geometry.dispose(); if (child.material) child.material.dispose(); linkObject.remove(child); } // Elegir estilo basado en dependencyType si está activado let style; if (this.data.linkColorByType && DEPENDENCY_STYLES[link.type]) { style = DEPENDENCY_STYLES[link.type]; } else { style = LINK_STYLES[link.type] || DEFAULT_LINK_STYLE; } // Elegir entre cilindro o línea if (this.data.linkStyle === 'cylinder') { this.createCylindricalBond(linkObject, sourceNode, targetNode, style); } else { this.createLineBond(linkObject, sourceNode, targetNode, style); } }); }, // NUEVO MÉTODO: Crear enlace cilíndrico (estilo molecular) createCylindricalBond: function(linkObject, sourceNode, targetNode, style) { const start = new THREE.Vector3(sourceNode.x, sourceNode.y, sourceNode.z); const end = new THREE.Vector3(targetNode.x, targetNode.y, targetNode.z); const direction = new THREE.Vector3().subVectors(end, start); const length = direction.length(); const midpoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5); // Crear geometría de cilindro const geometry = new THREE.CylinderGeometry( style.width, // radio superior style.width, // radio inferior length, // altura 8, // segmentos radiales 1 // segmentos de altura ); // Material con color y opacidad const material = new THREE.MeshStandardMaterial({ color: new THREE.Color(style.color), opacity: style.opacity * this.data.linkOpacity, transparent: true, metalness: 0.3, roughness: 0.6 }); const cylinder = new THREE.Mesh(geometry, material); // Posicionar en el punto medio cylinder.position.copy(midpoint); // Rotar para que apunte del origen al destino const axis = new THREE.Vector3(0, 1, 0); const quaternion = new THREE.Quaternion().setFromUnitVectors( axis, direction.clone().normalize() ); cylinder.setRotationFromQuaternion(quaternion); linkObject.add(cylinder); }, // NUEVO MÉTODO: Crear enlace de línea (estilo original mejorado) createLineBond: function(linkObject, sourceNode, targetNode, style) { const arcPoints = this.calculateGeodesicArc( sourceNode, targetNode, this.data.sphereRadius, this.data.linkSegments ); const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(arcPoints.length * 3); arcPoints.forEach((p, idx) => { positions[idx * 3] = p.x; positions[idx * 3 + 1] = p.y; positions[idx * 3 + 2] = p.z; }); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.LineBasicMaterial({ color: new THREE.Color(style.color), opacity: style.opacity * this.data.linkOpacity, transparent: true, linewidth: style.width * 10 // LineBasicMaterial tiene limitaciones de grosor }); const line = new THREE.Line(geometry, material); linkObject.add(line); }, calculateGeodesicArc: function(pos1, pos2, radius, segments) { const points = []; const v1 = new THREE.Vector3(pos1.x, pos1.y, pos1.z); const v2 = new THREE.Vector3(pos2.x, pos2.y, pos2.z); const v1Norm = v1.clone().normalize(); const v2Norm = v2.clone().normalize(); const omega = Math.acos(Math.max(-1, Math.min(1, v1Norm.dot(v2Norm)))); const sinOmega = Math.sin(omega); if (Math.abs(sinOmega) < 0.001) { for (let i = 0; i <= segments; i++) { const t = i / segments; points.push({ x: v1.x * (1 - t) + v2.x * t, y: v1.y * (1 - t) + v2.y * t, z: v1.z * (1 - t) + v2.z * t }); } return points; } for (let i = 0; i <= segments; i++) { const t = i / segments; const a = Math.sin((1 - t) * omega) / sinOmega; const b = Math.sin(t * omega) / sinOmega; const x = a * v1.x + b * v2.x; const y = a * v1.y + b * v2.y; const z = a * v1.z + b * v2.z; const length = Math.sqrt(x * x + y * y + z * z); points.push({ x: (x / length) * radius, y: (y / length) * radius, z: (z / length) * radius }); } return points; }, latLonToVector3: function(lat, lon, radius) { const phi = (90 - lat) * (Math.PI / 180); const theta = (lon + 180) * (Math.PI / 180); const x = -(radius * Math.sin(phi) * Math.cos(theta)); const y = radius * Math.cos(phi); const z = radius * Math.sin(phi) * Math.sin(theta); return { x, y, z }; }, licenseToColor: function(license) { if (!license) return '#666666'; const l = String(license).toUpperCase(); if (l.includes('AGPL')) return '#ff00ff'; // copyleft fuerte if (l.includes('LGPL')) return '#ff8800'; if (l.includes('GPL')) return '#ff2222'; // GPL — restrictiva if (l.includes('MPL')) return '#ffcc00'; if (l.includes('APACHE')) return '#00aaff'; if (l.includes('BSD')) return '#00ddaa'; if (l.includes('MIT') || l.includes('ISC')) return '#33cc66'; if (l.includes('CC0') || l.includes('UNLICENSE')) return '#222222'; return '#888888'; }, licenseMatchesFilter: function(license) { const f = (this.data.licenseFilter || '').trim(); if (!f) return true; if (!license) return false; const lic = String(license).toUpperCase(); return f.split(',').map(s => s.trim().toUpperCase()).filter(Boolean) .some(tok => lic.includes(tok)); }, cvssScoreToColor: function(score) { const normalized = score / 10; if (normalized < 0.5) { const g = 255; const r = Math.floor(normalized * 2 * 255); return `#${r.toString(16).padStart(2, '0')}ff00`; } else if (normalized < 0.7) { const r = 255; const g = Math.floor((1 - (normalized - 0.5) / 0.2) * 255); return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}00`; } else { return '#ff0000'; } }, valueToColor: function(value) { const normalized = value / 100; if (normalized < 0.33) { return `#${Math.floor(normalized * 3 * 255).toString(16).padStart(2, '0')}ff00`; } else if (normalized < 0.66) { return `#ffff${Math.floor((1 - (normalized - 0.33) * 3) * 255).toString(16).padStart(2, '0')}`; } else { return `#ff${Math.floor((1 - (normalized - 0.66) * 3) * 255).toString(16).padStart(2, '0')}00`; } }, toColor: function(color) { if (!color) return '#FFFFFF'; if (color.startsWith('#')) { return color; } const colors = { 'red': '#FF0D0D', 'blue': '#3050F8', 'gray': '#afaeae', 'grey': '#afaeae', 'black': '#000000', 'green': '#46aa03', 'yellow': '#f4d35e', 'purple': '#9b59b6', 'pink': '#ff6f91', 'cyan': '#4ECDC4', 'teal': '#008080', 'brown': '#8b4513', 'white': '#ffffff', 'gold': '#ffd700', 'silver': '#c0c0c0', 'bronze': '#cd7f32', 'lime': '#b5eb22', 'orange': '#f08a5d' }; return colors[color.toLowerCase()] || '#FFFFFF'; }, createReferenceSphere: function() { const sphere = document.createElement('a-sphere'); sphere.setAttribute('radius', this.data.sphereRadius); sphere.setAttribute('color', '#1a1a2e'); sphere.setAttribute('opacity', 0.1); sphere.setAttribute('side', 'double'); sphere.setAttribute('wireframe', true); sphere.setAttribute('wireframe-linewidth', 1); this.el.appendChild(sphere); }, createCoordinateGrid: function() { const radius = this.data.sphereRadius; // Líneas de latitud for (let lat = -60; lat <= 60; lat += 30) { const points = []; for (let lon = -180; lon <= 180; lon += 10) { const pos = this.latLonToVector3(lat, lon, radius); points.push(`${pos.x} ${pos.y} ${pos.z}`); } for (let i = 0; i < points.length - 1; i++) { const segment = document.createElement('a-entity'); segment.setAttribute('line', { start: points[i], end: points[i + 1], color: '#ffffff', opacity: 0.15 }); this.el.appendChild(segment); } } // Líneas de longitud for (let lon = -180; lon < 180; lon += 30) { const points = []; for (let lat = -90; lat <= 90; lat += 10) { const pos = this.latLonToVector3(lat, lon, radius); points.push(`${pos.x} ${pos.y} ${pos.z}`); } for (let i = 0; i < points.length - 1; i++) { const segment = document.createElement('a-entity'); segment.setAttribute('line', { start: points[i], end: points[i + 1], color: '#ffffff', opacity: 0.15 }); this.el.appendChild(segment); } } }, // Métodos públicos para control externo pausePhysics: function() { if (this.simulation) { this.simulation.stop(); this.state.physicsRunning = false; console.log('⏸️ Físicas pausadas'); } }, resumePhysics: function() { if (this.simulation) { this.simulation.restart(); this.state.physicsRunning = true; console.log('▶️ Físicas reanudadas'); } }, updatePhysicsStrength: function(strength) { if (this.simulation) { this.simulation.force("x").strength(strength); this.simulation.force("y").strength(strength); this.simulation.force("z").strength(strength); this.simulation.alpha(0.3).restart(); console.log('💪 Fuerza física actualizada:', strength); } }, toggleLinks: function(show) { this.linkElements.forEach(linkObj => { linkObj.visible = show; }); console.log('Enlaces:', show ? 'Visibles' : 'Ocultos'); }, setColorMode: function(mode) { // mode: 'cve' | 'license' | 'default' this.data.colorByCVE = (mode === 'cve'); this.data.colorByLicense = (mode === 'license'); this.recolorNodes(); console.log('🎨 Color mode:', mode); }, setLicenseFilter: function(str) { this.data.licenseFilter = str || ''; this.recolorNodes(); console.log('🔎 License filter:', this.data.licenseFilter); }, recolorNodes: function() { this.nodeElements.forEach(nodeEl => { const sphere = nodeEl.sphereElement || nodeEl.querySelector('a-sphere'); if (!sphere) return; const data = (sphere.object3D && sphere.object3D.userData && sphere.object3D.userData.nodeData) || nodeEl.nodeData; if (!data) return; let color; if (this.data.colorByLicense) { color = this.licenseToColor(data.license); } else if (this.data.colorByCVE) { color = (data.cvss_score != null) ? this.cvssScoreToColor(data.cvss_score) : '#1a3a5c'; } else if (this.data.colorByValue && data.value !== undefined) { color = this.valueToColor(data.value); } else { color = this.toColor(data.color); } const matches = this.licenseMatchesFilter(data.license); if (!matches) color = '#222222'; sphere.setAttribute('material', { shader: 'standard', color: color, emissive: color, emissiveIntensity: matches ? 0.6 : 0.05, roughness: 0.4, opacity: matches ? this.data.nodeOpacity : 0.15, transparent: true }); }); }, getLicenseStats: function() { const stats = {}; (this.simulationNodes || []).forEach(n => { const k = n.license || 'UNKNOWN'; stats[k] = (stats[k] || 0) + 1; }); return stats; }, toggleLabels: function(show) { this.nodeElements.forEach(nodeEl => { if (nodeEl.labelElement) { nodeEl.labelElement.setAttribute('visible', show); } }); console.log('Etiquetas:', show ? 'Visibles' : 'Ocultas'); } }); //d3-force-3d (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-binarytree'), require('d3-quadtree'), require('d3-octree'), require('d3-dispatch'), require('d3-timer')) : typeof define === 'function' && define.amd ? define(['exports', 'd3-binarytree', 'd3-quadtree', 'd3-octree', 'd3-dispatch', 'd3-timer'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3, global.d3, global.d3)); })(this, (function (exports, d3Binarytree, d3Quadtree, d3Octree, d3Dispatch, d3Timer) { 'use strict'; function center(x, y, z) { var nodes, strength = 1; if (x == null) x = 0; if (y == null) y = 0; if (z == null) z = 0; function force() { var i, n = nodes.length, node, sx = 0, sy = 0, sz = 0; for (i = 0; i < n; ++i) { node = nodes[i], sx += node.x || 0, sy += node.y || 0, sz += node.z || 0; } for (sx = (sx / n - x) * strength, sy = (sy / n - y) * strength, sz = (sz / n - z) * strength, i = 0; i < n; ++i) { node = nodes[i]; if (sx) { node.x -= sx; } if (sy) { node.y -= sy; } if (sz) { node.z -= sz; } } } force.initialize = function(_) { nodes = _; }; force.x = function(_) { return arguments.length ? (x = +_, force) : x; }; force.y = function(_) { return arguments.length ? (y = +_, force) : y; }; force.z = function(_) { return arguments.length ? (z = +_, force) : z; }; force.strength = function(_) { return arguments.length ? (strength = +_, force) : strength; }; return force; } function constant(x) { return function() { return x; }; } function jiggle(random) { return (random() - 0.5) * 1e-6; } function x$2(d) { return d.x + d.vx; } function y$2(d) { return d.y + d.vy; } function z$2(d) { return d.z + d.vz; } function collide(radius) { var nodes, nDim, radii, random, strength = 1, iterations = 1; if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius); function force() { var i, n = nodes.length, tree, node, xi, yi, zi, ri, ri2; for (var k = 0; k < iterations; ++k) { tree = (nDim === 1 ? d3Binarytree.binarytree(nodes, x$2) :(nDim === 2 ? d3Quadtree.quadtree(nodes, x$2, y$2) :(nDim === 3 ? d3Octree.octree(nodes, x$2, y$2, z$2) :null ))).visitAfter(prepare); for (i = 0; i < n; ++i) { node = nodes[i]; ri = radii[node.index], ri2 = ri * ri; xi = node.x + node.vx; if (nDim > 1) { yi = node.y + node.vy; } if (nDim > 2) { zi = node.z + node.vz; } tree.visit(apply); } } function apply(treeNode, arg1, arg2, arg3, arg4, arg5, arg6) { var args = [arg1, arg2, arg3, arg4, arg5, arg6]; var x0 = args[0], y0 = args[1], z0 = args[2], x1 = args[nDim], y1 = args[nDim+1], z1 = args[nDim+2]; var data = treeNode.data, rj = treeNode.r, r = ri + rj; if (data) { if (data.index > node.index) { var x = xi - data.x - data.vx, y = (nDim > 1 ? yi - data.y - data.vy : 0), z = (nDim > 2 ? zi - data.z - data.vz : 0), l = x * x + y * y + z * z; if (l < r * r) { if (x === 0) x = jiggle(random), l += x * x; if (nDim > 1 && y === 0) y = jiggle(random), l += y * y; if (nDim > 2 && z === 0) z = jiggle(random), l += z * z; l = (r - (l = Math.sqrt(l))) / l * strength; node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj)); if (nDim > 1) { node.vy += (y *= l) * r; } if (nDim > 2) { node.vz += (z *= l) * r; } data.vx -= x * (r = 1 - r); if (nDim > 1) { data.vy -= y * r; } if (nDim > 2) { data.vz -= z * r; } } } return; } return x0 > xi + r || x1 < xi - r || (nDim > 1 && (y0 > yi + r || y1 < yi - r)) || (nDim > 2 && (z0 > zi + r || z1 < zi - r)); } } function prepare(treeNode) { if (treeNode.data) return treeNode.r = radii[treeNode.data.index]; for (var i = treeNode.r = 0; i < Math.pow(2, nDim); ++i) { if (treeNode[i] && treeNode[i].r > treeNode.r) { treeNode.r = treeNode[i].r; } } } function initialize() { if (!nodes) return; var i, n = nodes.length, node; radii = new Array(n); for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes); } force.initialize = function(_nodes, ...args) { nodes = _nodes; random = args.find(arg => typeof arg === 'function') || Math.random; nDim = args.find(arg => [1, 2, 3].includes(arg)) || 2; initialize(); }; force.iterations = function(_) { return arguments.length ? (iterations = +_, force) : iterations; }; force.strength = function(_) { return arguments.length ? (strength = +_, force) : strength; }; force.radius = function(_) { return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius; }; return force; } function index(d) { return d.index; } function find(nodeById, nodeId) { var node = nodeById.get(nodeId); if (!node) throw new Error("node not found: " + nodeId); return node; } function link(links) { var id = index, strength = defaultStrength, strengths, distance = constant(30), distances, nodes, nDim, count, bias, random, iterations = 1; if (links == null) links = []; function defaultStrength(link) { return 1 / Math.min(count[link.source.index], count[link.target.index]); } function force(alpha) { for (var k = 0, n = links.length; k < iterations; ++k) { for (var i = 0, link, source, target, x = 0, y = 0, z = 0, l, b; i < n; ++i) { link = links[i], source = link.source, target = link.target; x = target.x + target.vx - source.x - source.vx || jiggle(random); if (nDim > 1) { y = target.y + target.vy - source.y - source.vy || jiggle(random); } if (nDim > 2) { z = target.z + target.vz - source.z - source.vz || jiggle(random); } l = Math.sqrt(x * x + y * y + z * z); l = (l - distances[i]) / l * alpha * strengths[i]; x *= l, y *= l, z *= l; target.vx -= x * (b = bias[i]); if (nDim > 1) { target.vy -= y * b; } if (nDim > 2) { target.vz -= z * b; } source.vx += x * (b = 1 - b); if (nDim > 1) { source.vy += y * b; } if (nDim > 2) { source.vz += z * b; } } } } function initialize() { if (!nodes) return; var i, n = nodes.length, m = links.length, nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d])), link; for (i = 0, count = new Array(n); i < m; ++i) { link = links[i], link.index = i; if (typeof link.source !== "object") link.source = find(nodeById, link.source); if (typeof link.target !== "object") link.target = find(nodeById, link.target); count[link.source.index] = (count[link.source.index] || 0) + 1; count[link.target.index] = (count[link.target.index] || 0) + 1; } for (i = 0, bias = new Array(m); i < m; ++i) { link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]); } strengths = new Array(m), initializeStrength(); distances = new Array(m), initializeDistance(); } function initializeStrength() { if (!nodes) return; for (var i = 0, n = links.length; i < n; ++i) { strengths[i] = +strength(links[i], i, links); } } function initializeDistance() { if (!nodes) return; for (var i = 0, n = links.length; i < n; ++i) { distances[i] = +distance(links[i], i, links); } } force.initialize = function(_nodes, ...args) { nodes = _nodes; random = args.find(arg => typeof arg === 'function') || Math.random; nDim = args.find(arg => [1, 2, 3].includes(arg)) || 2; initialize(); }; force.links = function(_) { return arguments.length ? (links = _, initialize(), force) : links; }; force.id = function(_) { return arguments.length ? (id = _, force) : id; }; force.iterations = function(_) { return arguments.length ? (iterations = +_, force) : iterations; }; force.strength = function(_) { return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength; }; force.distance = function(_) { return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance; }; return force; } const a = 1664525; const c = 1013904223; const m = 4294967296; // 2^32 function lcg() { let s = 1; return () => (s = (a * s + c) % m) / m; } var MAX_DIMENSIONS = 3; function x$1(d) { return d.x; } function y$1(d) { return d.y; } function z$1(d) { return d.z; } var initialRadius = 10, initialAngleRoll = Math.PI * (3 - Math.sqrt(5)), // Golden ratio angle initialAngleYaw = Math.PI * 20 / (9 + Math.sqrt(221)); // Markov irrational number function simulation(nodes, numDimensions) { numDimensions = numDimensions || 2; var nDim = Math.min(MAX_DIMENSIONS, Math.max(1, Math.round(numDimensions))), simulation, alpha = 1, alphaMin = 0.001, alphaDecay = 1 - Math.pow(alphaMin, 1 / 300), alphaTarget = 0, velocityDecay = 0.6, forces = new Map(), stepper = d3Timer.timer(step), event = d3Dispatch.dispatch("tick", "end"), random = lcg(); if (nodes == null) nodes = []; function step() { tick(); event.call("tick", simulation); if (alpha < alphaMin) { stepper.stop(); event.call("end", simulation); } } function tick(iterations) { var i, n = nodes.length, node; if (iterations === undefined) iterations = 1; for (var k = 0; k < iterations; ++k) { alpha += (alphaTarget - alpha) * alphaDecay; forces.forEach(function (force) { force(alpha); }); for (i = 0; i < n; ++i) { node = nodes[i]; if (node.fx == null) node.x += node.vx *= velocityDecay; else node.x = node.fx, node.vx = 0; if (nDim > 1) { if (node.fy == null) node.y += node.vy *= velocityDecay; else node.y = node.fy, node.vy = 0; } if (nDim > 2) { if (node.fz == null) node.z += node.vz *= velocityDecay; else node.z = node.fz, node.vz = 0; } } } return simulation; } function initializeNodes() { for (var i = 0, n = nodes.length, node; i < n; ++i) { node = nodes[i], node.index = i; if (node.fx != null) node.x = node.fx; if (node.fy != null) node.y = node.fy; if (node.fz != null) node.z = node.fz; if (isNaN(node.x) || (nDim > 1 && isNaN(node.y)) || (nDim > 2 && isNaN(node.z))) { var radius = initialRadius * (nDim > 2 ? Math.cbrt(0.5 + i) : (nDim > 1 ? Math.sqrt(0.5 + i) : i)), rollAngle = i * initialAngleRoll, yawAngle = i * initialAngleYaw; if (nDim === 1) { node.x = radius; } else if (nDim === 2) { node.x = radius * Math.cos(rollAngle); node.y = radius * Math.sin(rollAngle); } else { // 3 dimensions: use spherical distribution along 2 irrational number angles node.x = radius * Math.sin(rollAngle) * Math.cos(yawAngle); node.y = radius * Math.cos(rollAngle); node.z = radius * Math.sin(rollAngle) * Math.sin(yawAngle); } } if (isNaN(node.vx) || (nDim > 1 && isNaN(node.vy)) || (nDim > 2 && isNaN(node.vz))) { node.vx = 0; if (nDim > 1) { node.vy = 0; } if (nDim > 2) { node.vz = 0; } } } } function initializeForce(force) { if (force.initialize) force.initialize(nodes, random, nDim); return force; } initializeNodes(); return si