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; }