import * as THREE from 'three'; import { CONFIG, STATE } from '../core/State.js'; import { clearRelief, isReliefActive } from './ReliefRenderer.js'; export function clearThreads(scene) { if (!STATE.threads) return; if (CONFIG.debug) console.log("[VISUALIZER] Clearing all threads"); STATE.threads.forEach(t => { if (t.lineMesh) { scene.remove(t.lineMesh); if (t.lineMesh.geometry) t.lineMesh.geometry.dispose(); if (t.lineMesh.material) t.lineMesh.material.dispose(); } if (t.pointsMesh) { scene.remove(t.pointsMesh); if (t.pointsMesh.material) t.pointsMesh.material.dispose(); } if (t.labelSprite) { scene.remove(t.labelSprite); if (t.labelSprite.material) { if (t.labelSprite.material.map) t.labelSprite.material.map.dispose(); t.labelSprite.material.dispose(); } } }); STATE.threads = []; STATE.focusedThreads.clear(); STATE.hiddenThreads.clear(); // Clear Instanced Mesh if exists if (STATE.instancedMesh) { scene.remove(STATE.instancedMesh); if (STATE.instancedMesh.geometry) STATE.instancedMesh.geometry.dispose(); if (STATE.instancedMesh.material) STATE.instancedMesh.material.dispose(); STATE.instancedMesh = null; } // Clear Macro Meshes (New GPU Mode) if (STATE.macroMeshes) { if (STATE.macroMeshes.lines) { scene.remove(STATE.macroMeshes.lines); if (STATE.macroMeshes.lines.geometry) STATE.macroMeshes.lines.geometry.dispose(); if (STATE.macroMeshes.lines.material) STATE.macroMeshes.lines.material.dispose(); } if (STATE.macroMeshes.points) { scene.remove(STATE.macroMeshes.points); if (STATE.macroMeshes.points.geometry) STATE.macroMeshes.points.geometry.dispose(); if (STATE.macroMeshes.points.material) STATE.macroMeshes.points.material.dispose(); } STATE.macroMeshes = null; } } export function updateThreadVisibility() { if (!STATE.threads) return; const isRelief = (STATE.blanketSubMode === 'RELIEF'); // Automatically clean up active relief if we switch away from RELIEF submode if (!isRelief && isReliefActive() && STATE.scene) { clearRelief(STATE.scene); } // Update GPU Macro Meshes visibility if (STATE.macroMeshes) { if (STATE.macroMeshes.lines) STATE.macroMeshes.lines.visible = !isRelief; if (STATE.macroMeshes.points) STATE.macroMeshes.points.visible = !isRelief; } const hasFocus = STATE.focusedThreads.size > 0; STATE.threads.forEach((t, index) => { const isFocused = STATE.focusedThreads.has(index); const isHidden = STATE.hiddenThreads.has(index); // Visibility (Hidden) Logic if (isRelief || isHidden) { if (t.lineMesh) t.lineMesh.visible = false; if (t.pointsMesh) t.pointsMesh.visible = false; if (t.labelSprite) t.labelSprite.visible = false; return; // Skip opacity logic if hidden or in relief submode } else { if (t.lineMesh) t.lineMesh.visible = true; if (t.pointsMesh) t.pointsMesh.visible = true; if (t.labelSprite) t.labelSprite.visible = true; } // Polarity Filter Logic (Points Only) // We filter points by moving them out of view if they don't match the polarity const polarity = STATE.polarityMode || 'ALL'; if (t.pointsMesh && t.pointsMesh.geometry) { const positions = t.pointsMesh.geometry.attributes.position; const count = positions.count; let needsUpdate = false; for (let i = 0; i < count; i++) { const val = t.embedding[i]; let visible = true; if (polarity === 'POS' && val <= 0) visible = false; if (polarity === 'NEG' && val >= 0) visible = false; const currentY = positions.getY(i); // FIX: Flatten to 0 instead of moving to infinity const targetY = visible ? (val * CONFIG.amplitudeScale) : 0; // Optimization: Only update if changed if (Math.abs(currentY - targetY) > 0.001) { positions.setY(i, targetY); needsUpdate = true; } } if (needsUpdate) { positions.needsUpdate = true; } } // Focus (Opacity) Logic let targetOpacity = 1.0; let targetLineWidth = 1; if (hasFocus) { if (isFocused) { targetOpacity = 1.0; targetLineWidth = 2; } else { targetOpacity = 0.05; targetLineWidth = 1; } } else { targetOpacity = 1.0; targetLineWidth = 1; } // Apply to Line if (t.lineMesh && t.lineMesh.material) { t.lineMesh.material.opacity = targetOpacity; t.lineMesh.material.linewidth = targetLineWidth; t.lineMesh.material.needsUpdate = true; } // Apply to Points (Shader) if (t.pointsMesh && t.pointsMesh.material && t.pointsMesh.material.uniforms) { t.pointsMesh.material.uniforms.opacity.value = targetOpacity; } // Apply to Sprite if (t.labelSprite) { t.labelSprite.material.opacity = targetOpacity; } }); } export function updateThreadColor(thread, colorHex) { if (!thread) return; const newColor = new THREE.Color(colorHex); thread.color = newColor; // 1. Update Line if (thread.lineMesh && thread.lineMesh.material) { thread.lineMesh.material.color.set(newColor); thread.lineMesh.material.needsUpdate = true; } // 2. Update Points if (thread.pointsMesh && thread.pointsMesh.material) { if (thread.pointsMesh.material.uniforms && thread.pointsMesh.material.uniforms.color) { thread.pointsMesh.material.uniforms.color.value.set(newColor); } else if (thread.pointsMesh.material.color) { thread.pointsMesh.material.color.set(newColor); } thread.pointsMesh.material.needsUpdate = true; } // 3. Update Label (Fast Tinting) if (thread.labelSprite && thread.labelSprite.material) { thread.labelSprite.material.color.set(newColor); thread.labelSprite.material.needsUpdate = true; } }