import * as THREE from 'three'; import { CONFIG, STATE } from '../core/State.js'; import { updateInstancedCloud } from './Instancer.js'; export function updateAllThreadPositions() { if (STATE.engineMode === 'MACRO') { // Update Instanced Mesh if (STATE.threads) { updateInstancedCloud(STATE.threads); } return; } if (!STATE.threads) return; if (CONFIG.debug) console.log(`[VISUALIZER] Updating Positions (Mode: ${STATE.currentZMode})`); // Calculate Max ID for Normalization // Ensure at least 1 to avoid division by zero const maxID = Math.max(...STATE.threads.map(t => t.tokenID || 0), 1); // Sort first to determine order (Rank-Based Distribution) // This ensures threads with similar Token IDs are visually adjacent const sortedThreads = [...STATE.threads].sort((a, b) => (a.tokenID || 0) - (b.tokenID || 0)); sortedThreads.forEach((t, rank) => { updateThreadTransform(t, rank, maxID); }); } export function updateThreadTransform(thread, rank, maxID = 1) { const mode = STATE.currentZMode || 'log'; const layoutMode = STATE.layoutMode || 'COMPARE'; // 1. Update Geometry (Internal Z-Spacing and Width) updateGeometryZ(thread); // 2. Calculate Mesh Position & Rotation let xPos = 0; let yPos = 0; let zPos = 0; let zRot = 0; if (layoutMode === 'BLANKET') { if (mode === 'linear') { // Linear Mode: Simple spacing side-by-side along X xPos = (rank !== undefined ? rank : 0) * CONFIG.threadSpacing; } else if (mode === 'log') { // Token ID Mode (Normalized Horizontal) const ratio = (thread.tokenID || 0) / maxID; const width = 200.0; xPos = ratio * width * CONFIG.zScale.log * CONFIG.threadSpacing; } else if (mode === 'radial') { zRot = (rank !== undefined ? rank : 0) * STATE.radialStep; xPos = (rank !== undefined ? rank : 0) * CONFIG.threadSpacing; // Offset radially? Just offset X for now } } else { // COMPARE Mode: Threads overlap by default, or spread by compareSpacing xPos = (rank !== undefined ? rank : 0) * (CONFIG.compareSpacing || 0.0); if (mode === 'radial') { zRot = (rank !== undefined ? rank : 0) * STATE.radialStep; } } // 3. Apply to Meshes if (thread.lineMesh) { thread.lineMesh.position.set(xPos, yPos, zPos); if (mode === 'radial') { thread.lineMesh.rotation.set(0, -Math.PI / 2, zRot); } else { thread.lineMesh.rotation.set(0, 0, zRot); } } if (thread.pointsMesh) { thread.pointsMesh.position.set(xPos, yPos, zPos); if (mode === 'radial') { thread.pointsMesh.rotation.set(0, -Math.PI / 2, zRot); } else { thread.pointsMesh.rotation.set(0, 0, zRot); } } // 4. Update Label Sprite Position if (thread.labelSprite) { // The label should be at the "START" of the thread (Index 0) // Local position of the start (relative to mesh origin) const tipLocal = new THREE.Vector3(0, 0, 0); // Apply Rotation if (mode === 'radial') { // Match the mesh rotation tipLocal.applyEuler(new THREE.Euler(0, -Math.PI / 2, zRot)); } else { // Linear/Log usually have 0 rotation tipLocal.applyEuler(new THREE.Euler(0, 0, zRot)); } // Apply Translation (Mesh Position) // Note: Mesh position is (xPos, yPos, zPos) // So global tip = tipLocal + MeshPosition const tipGlobal = tipLocal.clone().add(new THREE.Vector3(xPos, yPos, zPos)); // Adjust Y for label height (Amplitude of FIRST point) const firstY = (thread.embedding && thread.embedding.length > 0) ? (thread.embedding[0] * CONFIG.amplitudeScale) : 0.0; // Global Position (Float 1.5 units above the start) thread.labelSprite.position.set(tipGlobal.x, firstY + 1.5, tipGlobal.z); } } export function updateGeometryZ(thread) { if (!thread.lineMesh || !thread.lineMesh.geometry) return; const geometry = thread.lineMesh.geometry; const positions = geometry.attributes.position; const count = positions.count; // CONSTANT GEOMETRY: The thread shape is always the same. // X: 0 // Y: Amplitude // Z: Linear Length (0 to Length) scaled by threadWidth const spacing = CONFIG.pointSpacing * (CONFIG.threadWidth || 1.0); for (let i = 0; i < count; i++) { // X is always 0 positions.setX(i, 0); // Y is Amplitude (Preserved) // We assume Y is already correct from creation/updates. // Z is always linear internal length scaled positions.setZ(i, i * spacing); } positions.needsUpdate = true; geometry.computeBoundingSphere(); }