Spaces:
Sleeping
Sleeping
| 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(); | |
| } | |