Spaces:
Sleeping
Sleeping
| import * as THREE from 'three'; | |
| import { CONFIG, STATE } from '../core/State.js'; | |
| import { vertexShader, fragmentShader } from '../engine/Shaders.js'; | |
| import { updateThreadTransform } from './LayoutEngine.js'; | |
| import { updateThreadVisibility } from './ThreadManager.js'; | |
| import { createLabelSprite } from './LabelHelper.js'; | |
| import { PALETTE_3D, getActivationColorPerceptual as getActivationColor } from '../engine/palette.js'; | |
| // ─── Helpers CPU ──────────────────────────────────────────────────────────── | |
| function lerp(a, b, t) { return a + (b - a) * t; } | |
| // ───────────────────────────────────────────────────────────────────────── | |
| export function createThread(label, embedding, scene, colorHex, tokenID = 0) { | |
| if (CONFIG.debug) console.log(`[VISUALIZER] Creating thread for: "${label}" (ID: ${tokenID}) with color: ${colorHex.toString(16)}`); | |
| const count = embedding.length; | |
| STATE.vertexCount = count; | |
| // 1. Data Prep | |
| const points = []; | |
| const intensities = []; | |
| const indices = []; | |
| // Color dominante del thread: valor con mayor |activación| → define el color de la línea | |
| let maxAbsVal = 0, dominantVal = 0; | |
| for (let i = 0; i < count; i++) { | |
| const absV = Math.abs(embedding[i]); | |
| if (absV > maxAbsVal) { maxAbsVal = absV; dominantVal = embedding[i]; } | |
| } | |
| const lineActivation = getActivationColor(dominantVal); | |
| // Normalizador: garantiza que el mayor valor llegue a ±1.0 en el shader | |
| const safeMax = maxAbsVal > 0 ? maxAbsVal : 1.0; | |
| for (let i = 0; i < count; i++) { | |
| const val = embedding[i]; | |
| const x = 0; // Geometría extendida a lo largo del eje Z | |
| const y = val * CONFIG.amplitudeScale; | |
| const z = i * CONFIG.pointSpacing; | |
| points.push(x, y, z); | |
| intensities.push(val / safeMax); // Normalizado → rango [-1, 1] | |
| indices.push(i); | |
| } | |
| const geometry = new THREE.BufferGeometry(); | |
| geometry.setAttribute('position', new THREE.Float32BufferAttribute(points, 3)); | |
| geometry.setAttribute('intensity', new THREE.Float32BufferAttribute(intensities, 1)); | |
| geometry.setAttribute('vertexIndex', new THREE.Float32BufferAttribute(indices, 1)); | |
| // 2. Línea: color uniforme derivado del valor dominante del thread | |
| const lineMaterial = new THREE.LineBasicMaterial({ | |
| color: new THREE.Color(lineActivation.r, lineActivation.g, lineActivation.b), | |
| linewidth: CONFIG.threadThickness || 1.0, | |
| opacity: 0.6, | |
| transparent: true | |
| }); | |
| const lineMesh = new THREE.Line(geometry, lineMaterial); | |
| // PREVENT FRUSTRUM CULLING ISSUES (Common cause of disappearing meshes) | |
| lineMesh.frustumCulled = false; | |
| // 3. Points Mesh (Heatmap Shader) | |
| const uniforms = { | |
| highlightIndex: { value: -1.0 }, | |
| pointSpacing: { value: CONFIG.pointSpacing }, | |
| dotSize: { value: CONFIG.pointSpacing * CONFIG.amplitudeScale * 0.8 }, | |
| opacity: { value: 1.0 }, | |
| color: { value: new THREE.Color(colorHex) } | |
| }; | |
| const pointsMaterial = new THREE.ShaderMaterial({ | |
| uniforms: uniforms, | |
| vertexShader: vertexShader, | |
| fragmentShader: fragmentShader, | |
| transparent: true, | |
| depthWrite: false, | |
| blending: THREE.NormalBlending | |
| }); | |
| const pointsMesh = new THREE.Points(geometry, pointsMaterial); | |
| pointsMesh.frustumCulled = false; | |
| // 4. Critical: Add to Scene | |
| scene.add(lineMesh); | |
| scene.add(pointsMesh); | |
| // 5. Label Sprite | |
| const labelText = `${label} [${tokenID}]`; | |
| const sprite = createLabelSprite(labelText, 0, embedding[0] * CONFIG.amplitudeScale + 0.5, 0, new THREE.Color(colorHex)); | |
| scene.add(sprite); | |
| const thread = { | |
| id: Date.now() + Math.random(), | |
| label: label, | |
| lineMesh, | |
| pointsMesh, | |
| uniforms, | |
| embedding, | |
| color: new THREE.Color(colorHex), | |
| tokenID: tokenID, | |
| labelSprite: sprite | |
| }; | |
| // Apply Transform (Position/Rotation) based on current mode | |
| updateThreadTransform(thread); | |
| // Apply initial visibility based on current state | |
| const hasFocus = STATE.focusedThreads.size > 0; | |
| // New threads are not focused by default if focus mode is on | |
| const initialOpacity = hasFocus ? 0.05 : 1.0; | |
| lineMesh.material.opacity = initialOpacity; | |
| uniforms.opacity.value = initialOpacity; | |
| updateThreadVisibility(); | |
| return thread; | |
| } | |
| export function createGhostThread(vectorArray, label, scene, tokenID = 0) { | |
| if (CONFIG.debug) console.log(`[VISUALIZER] Creating Ghost Thread: "${label}" (ID: ${tokenID})`); | |
| const count = vectorArray.length; | |
| const points = []; | |
| for (let i = 0; i < count; i++) { | |
| const x = 0; | |
| const y = vectorArray[i] * CONFIG.amplitudeScale; | |
| const z = i * CONFIG.pointSpacing; | |
| points.push(new THREE.Vector3(x, y, z)); | |
| } | |
| const geometry = new THREE.BufferGeometry().setFromPoints(points); | |
| // CRITICAL: LineDashedMaterial requires computeLineDistances | |
| // Result Thread Styling: Neon Fuchsia, thicker, on top | |
| const material = new THREE.LineDashedMaterial({ | |
| color: PALETTE_3D.THREAD_RESULT, // Neon Fuchsia | |
| dashSize: 3.0, | |
| gapSize: 1.0, | |
| linewidth: 3, // Attempt thicker line | |
| scale: 1, | |
| opacity: 1.0, | |
| transparent: true, | |
| depthTest: false // Always render on top | |
| }); | |
| const line = new THREE.Line(geometry, material); | |
| line.frustumCulled = false; | |
| line.renderOrder = 999; // High render order | |
| // Add to Scene | |
| scene.add(line); | |
| line.computeLineDistances(); // Essential for dashes to appear | |
| // Label Sprite for Ghost | |
| // Note: zPos is 0 initially, updated by updateThreadTransform | |
| const labelText = `${label} [${tokenID}]`; | |
| const sprite = createLabelSprite(labelText, 0, vectorArray[0] * CONFIG.amplitudeScale + 1.0, 0, new THREE.Color(PALETTE_3D.THREAD_RESULT)); | |
| scene.add(sprite); | |
| const thread = { | |
| id: 'ghost-' + Date.now(), | |
| label: label, | |
| lineMesh: line, | |
| embedding: vectorArray, | |
| isGhost: true, | |
| color: new THREE.Color(PALETTE_3D.THREAD_RESULT), | |
| tokenID: tokenID, | |
| labelSprite: sprite | |
| }; | |
| // Apply Transform | |
| updateThreadTransform(thread); | |
| // Apply initial visibility | |
| const hasFocus = STATE.focusedThreads.size > 0; | |
| // Ghost thread is usually the result, so maybe we want to see it? | |
| // But if focus mode is strictly "only focused items", and it's not focused, it should be dimmed. | |
| // However, usually the user wants to see the result. | |
| // For now, adhere to strict logic: if not in focusedThreads, dim it. | |
| // But wait, the user might want to focus the result. | |
| const initialOpacity = hasFocus ? 0.05 : 1.0; | |
| line.material.opacity = initialOpacity; | |
| updateThreadVisibility(); | |
| return thread; | |
| } | |