import {hslFromString, randomWord, normalizeString} from './utils.js'; const THREE=window.THREE;const OrbitControls=THREE.OrbitControls;const TextGeometry=THREE.TextGeometry;const FontLoader=THREE.FontLoader; let scene,camera,renderer,controls,raycaster,mouse,tooltip,font,rootGroup,starsGroup,comet,cometTrail=[],cometTimer=0,cometWordTimer=0,cometWord=''; let minimapCtx,minimapScale=0.025,minimapDots=[];const MINIMAP_DOT_SIZE=2;const fontLoader=new FontLoader(); const usersCenters={}; export function getUsersCenters(){return usersCenters;} export function userCenter(uid){if(usersCenters[uid])return usersCenters[uid];let hash=0;for(let i=0;i{minimapScale*=1.4;drawMinimap([],{})});document.getElementById('zoomOutButton').addEventListener('click',()=>{minimapScale/=1.4;drawMinimap([],{})});window.addEventListener('resize',onResize);window.addEventListener('mousemove',onPointerMove);fontLoader.load('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/fonts/helvetiker_regular.typeface.json',f=>{font=f;});animate();} export function clearScene(){rootGroup.clear();} export function focusOnUser(uid){const c=userCenter(uid);controls.target.copy(c);camera.position.copy(c.clone().add(new THREE.Vector3(0,8,26)));controls.update();} export function teleportToUser(uid){const c=userCenter(uid);controls.target.copy(c);camera.position.copy(c.clone().add(new THREE.Vector3(0,8,26)));controls.update();} function animate(){requestAnimationFrame(animate);controls.update();updateRaycast();updateComet();renderer.render(scene,camera);} function onResize(){camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);} function onPointerMove(e){mouse.x=e.clientX/window.innerWidth*2-1;mouse.y=-(e.clientY/window.innerHeight)*2+1;tooltip.style.left=`${e.clientX+16}px`;tooltip.style.top=`${e.clientY}px`} function updateRaycast(){raycaster.setFromCamera(mouse,camera);const meshes=[];rootGroup.traverse(o=>{if(o.isMesh&&!o.userData.ignoreHit)meshes.push(o)});const is=raycaster.intersectObjects(meshes,false);if(is.length>0){const o=is[0].object;const d=o.userData;let html=`${d.label||d.hashtag||'Nodo'}`;if(d.level!=null)html+=`
Nivel: ${d.level}`;tooltip.innerHTML=html;tooltip.style.display='block';}else{tooltip.style.display='none';}} function addBackgroundStars(){starsGroup=new THREE.Group();const geo=new THREE.BufferGeometry();const count=2000;const positions=new Float32Array(count*3);for(let i=0;i.045){cometTimer=0;leaveTrailWord();}if(cometWordTimer>3){cometWordTimer=0;cometWord=randomWord();}for(let i=cometTrail.length-1;i>=0;i--){const m=cometTrail[i];m.material.opacity-=.01;m.position.y+=.03;if(m.material.opacity<=0){m.geometry.dispose();m.material.dispose();scene.remove(m);cometTrail.splice(i,1);}}} function leaveTrailWord(){if(!font)return;const s=cometWord||randomWord();const g=new TextGeometry(s.toUpperCase(),{font,size:.9,height:.02,curveSegments:4,bevelEnabled:false});g.computeBoundingBox();const m=new THREE.MeshBasicMaterial({color:0x4ade80,transparent:true,opacity:.8});const mesh=new THREE.Mesh(g,m);mesh.position.copy(comet.position);mesh.position.y+=.6;mesh.rotation.y=Math.random()*Math.PI;cometTrail.push(mesh);scene.add(mesh);} export function usernameSphere(uid,uname){const center=userCenter(uid);const mat=new THREE.MeshPhysicalMaterial({color:0x60a5fa,emissive:0x1e293b,roughness:.25,metalness:.35,clearcoat:.6,clearcoatRoughness:.2});const geo=new THREE.SphereGeometry(2.2,32,32);const s=new THREE.Mesh(geo,mat);s.position.copy(center);s.userData={ignoreHit:false,label:uname};rootGroup.add(s);if(font){const tg=new TextGeometry(uname.toUpperCase(),{font,size:.9,height:.05,curveSegments:6,bevelEnabled:false});tg.computeBoundingBox();const tm=new THREE.MeshBasicMaterial({color:0x93c5fd,transparent:true,opacity:.9});const text=new THREE.Mesh(tg,tm);text.position.copy(center);text.position.y+=3.1;text.position.x-=(tg.boundingBox.max.x-tg.boundingBox.min.x)/2;text.userData={ignoreHit:false,label:uname};rootGroup.add(text);}} export function addNeuronMesh(uid,label,level,pos,baseColor){const mat=new THREE.MeshStandardMaterial({color:new THREE.Color(baseColor),roughness:.4,metalness:.2,emissive:new THREE.Color(baseColor).multiplyScalar(.15)});const r=level===1?.45:level===2?.28:.18;const geo=new THREE.IcosahedronGeometry(r,1);const m=new THREE.Mesh(geo,mat);m.position.copy(pos);m.userData={label,level};const rimGeo=new THREE.RingGeometry(r*1.2,r*1.35,24);const rimMat=new THREE.MeshBasicMaterial({color:0x94ffa8,transparent:true,opacity:.18,side:THREE.DoubleSide});const rim=new THREE.Mesh(rimGeo,rimMat);rim.position.copy(pos);rim.rotation.x=Math.PI/2;rim.userData={ignoreHit:true};rootGroup.add(rim);rootGroup.add(m);if(font){const tgeo=new TextGeometry(label.toUpperCase(),{font,size:r*.9,height:.02,curveSegments:4});tgeo.computeBoundingBox();const tmat=new THREE.MeshBasicMaterial({color:new THREE.Color(baseColor),transparent:true,opacity:.85});const tm=new THREE.Mesh(tgeo,tmat);tm.position.copy(pos);tm.position.y+=r+.08;tm.position.x-=(tgeo.boundingBox.max.x-tgeo.boundingBox.min.x)/2;tm.userData={label,level};rootGroup.add(tm);}} // Option B: hierarchical tree around a topic root like the original implementation export function visualizeTree(topic, lista_palabras, origin){ const acc=[]; const rootCol=hslFromString(topic).color; // Root sphere and label const rootMat=new THREE.MeshStandardMaterial({color:new THREE.Color(rootCol),roughness:.5,metalness:.1}); const rootGeo=new THREE.SphereGeometry(0.4,16,16); const root=new THREE.Mesh(rootGeo,rootMat); root.position.copy(origin); root.userData={label:topic,level:0}; rootGroup.add(root); if(font){ const tg=new TextGeometry(topic.toUpperCase(),{font,size:.3,height:.02,curveSegments:4}); tg.computeBoundingBox(); const tm=new THREE.MeshBasicMaterial({color:new THREE.Color(rootCol),transparent:true,opacity:.8}); const text=new THREE.Mesh(tg,tm); text.position.copy(origin); text.position.y+=0.5; text.position.x-=(tg.boundingBox.max.x-tg.boundingBox.min.x)/2; text.userData={label:topic,level:0,isText:true}; rootGroup.add(text);} acc.push({label:topic,level:0,position:origin.clone()}); function addBranchNode(currentTag, level, parentPos, parentColor){ const {color,h}=hslFromString(currentTag); const nodeColor=(level===1)?color:parentColor; const theta=(h/360)*Math.PI*2; let phiHash=0; for(let i=0;i=0;i--){const d=Math.hypot(x-minimapDots[i].x,y-minimapDots[i].y);if(d