Spaces:
Running
Running
| <html> | |
| <head> | |
| <title>TMF_005 | High-Res Tree Explorer</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/OBJLoader.js"></script> | |
| <style> | |
| body { margin: 0; background: #050505; color: #ccc; font-family: 'Segoe UI', sans-serif; overflow: hidden; display: flex; } | |
| #sidebar { width: 350px; height: 100vh; background: #111; border-right: 1px solid #333; display: flex; flex-direction: column; z-index: 10; } | |
| #header { padding: 15px; border-bottom: 1px solid #333; font-size: 14px; font-weight: bold; color: #00ffcc; text-align: center; } | |
| #tree { flex-grow: 1; overflow-y: auto; padding: 10px; font-size: 11px; } | |
| .group { margin-bottom: 8px; } | |
| .group-header { cursor: pointer; background: #1a1a1a; padding: 6px 10px; border-radius: 4px; display: flex; justify-content: space-between; font-weight: bold; color: #fff; border: 1px solid #222; } | |
| .group-content { padding-left: 5px; margin-top: 5px; border-left: 1px solid #222; } | |
| .hidden { display: none; } | |
| .item { display: flex; align-items: center; padding: 4px; border-radius: 3px; border-bottom: 1px solid #1a1a1a; } | |
| .item:hover { background: #1a1a1a; } | |
| .item input[type="checkbox"] { margin-right: 8px; } | |
| .item input[type="range"] { width: 40px; margin-left: auto; height: 10px; } | |
| .color-dot { width: 10px; height: 10px; border-radius: 2px; margin-right: 8px; flex-shrink: 0; } | |
| .label-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; } | |
| #container { flex-grow: 1; position: relative; } | |
| #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #00ffcc; font-weight: bold; } | |
| .controls { padding: 10px; display: flex; gap: 5px; border-bottom: 1px solid #333; justify-content: center; } | |
| button { background: #333; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; font-size: 10px; } | |
| button:hover { background: #444; } | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-thumb { background: #333; border-radius: 10px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="sidebar"> | |
| <div id="header">TMF_005 | ANATOMICAL ATLAS</div> | |
| <div class="controls"> | |
| <button onclick="toggleGlobal(false)">Hide All</button> | |
| <button onclick="toggleGlobal(true)">Show All</button> | |
| </div> | |
| <div id="tree"></div> | |
| </div> | |
| <div id="container"><div id="loading">CLEANING MESH SURFACES...</div></div> | |
| <script> | |
| // --- FULL ANATOMICAL LOOKUP TABLE --- | |
| const lut_base = { | |
| 2: "White Matter", 3: "Gray Matter", 4: "Lateral Ventricle", 5: "Inf Lat Vent", 7: "Cerebellum WM", 8: "Cerebellum CX", | |
| 10: "Thalamus", 11: "Caudate", 12: "Putamen", 13: "Pallidum", 14: "3rd Ventricle", 15: "4th Ventricle", | |
| 16: "Brain Stem", 17: "Hippocampus", 18: "Amygdala", 24: "CSF", 26: "Accumbens", 28: "Ventral DC (Hypothalamus)", 31: "Choroid Plexus", | |
| 1002: "Caudal Ant Cingulate", 1003: "Caudal Mid Frontal", 1005: "Cuneus", 1006: "Entorhinal", 1007: "Fusiform", 1008: "Inferior Parietal", | |
| 1009: "Inferior Temporal", 1010: "Ismthmus Cingulate", 1011: "Lateral Occipital", 1012: "Lateral Orbitofrontal", 1013: "Lingual", | |
| 1014: "Medial Orbitofrontal", 1015: "Middle Temporal", 1016: "Parahippocampal", 1017: "Paracentral", 1018: "Pars Nasalis", | |
| 1019: "Pars Orbitalis", 1020: "Pars Triangularis", 1021: "Pericalcarine", 1022: "Postcentral", 1023: "Posterior Cingulate", | |
| 1024: "Precentral", 1025: "Precuneus", 1026: "Rostral Ant Cingulate", 1027: "Rostral Mid Frontal", 1028: "Superior Frontal", | |
| 1029: "Superior Parietal", 1030: "Superior Temporal", 1031: "Supramarginal", 1034: "Transverse Temporal", 1035: "Insula" | |
| }; | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(75, (window.innerWidth - 350) / window.innerHeight, 0.1, 2000); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth - 350, window.innerHeight); | |
| document.getElementById('container').appendChild(renderer.domElement); | |
| const controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.6)); | |
| const light = new THREE.PointLight(0xffffff, 1); | |
| camera.add(light); scene.add(camera); | |
| const meshes = {}; | |
| const groups = { | |
| "Subcortical (Deep)": [], | |
| "Cortex (Left)": [], | |
| "Cortex (Right)": [], | |
| "Ventricles & CSF": [] | |
| }; | |
| function getColor(name) { | |
| let hash = 0; | |
| for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } | |
| return "#" + (hash & 0x00FFFFFF).toString(16).padStart(6, '0'); | |
| } | |
| const loader = new THREE.OBJLoader(); | |
| loader.load('highres_smooth_brain.obj', (object) => { | |
| document.getElementById('loading').style.display = 'none'; | |
| object.traverse((child) => { | |
| if (child.isMesh) { | |
| const id = parseInt(child.name.replace('Segment_', '')); | |
| let base_id = id > 2000 ? id - 1000 : id; // Normalize right cortex to left ID for name lookup | |
| let name = (id >= 2000 ? "R " : id >= 1000 ? "L " : "") + (lut_base[base_id] || lut_base[id] || "Part " + id); | |
| const color = getColor(child.name); | |
| child.material = new THREE.MeshStandardMaterial({ | |
| color: color, | |
| side: THREE.DoubleSide, | |
| transparent: true, | |
| opacity: 1.0 | |
| }); | |
| const isDefault = [17, 53, 28, 60].includes(id); // Hypothalamus/Hippocampus | |
| child.visible = isDefault; | |
| meshes[child.name] = child; | |
| const item = { id, name, color, meshName: child.name, visible: isDefault }; | |
| if (id >= 2000) groups["Cortex (Right)"].push(item); | |
| else if (id >= 1000) groups["Cortex (Left)"].push(item); | |
| else if ([4, 5, 14, 15, 24, 31, 43, 44, 63].includes(id)) groups["Ventricles & CSF"].push(item); | |
| else groups["Subcortical (Deep)"].push(item); | |
| } | |
| }); | |
| const tree = document.getElementById('tree'); | |
| for (let gName in groups) { | |
| const gDiv = document.createElement('div'); | |
| gDiv.className = 'group'; | |
| // Categories are collapsed by default | |
| gDiv.innerHTML = `<div class="group-header" onclick="this.nextSibling.classList.toggle('hidden')"> | |
| ${gName} <span>▾</span></div><div class="group-content hidden"></div>`; | |
| const content = gDiv.querySelector('.group-content'); | |
| groups[gName].sort((a, b) => a.id - b.id).forEach(item => { | |
| const iDiv = document.createElement('div'); | |
| iDiv.className = 'item'; | |
| iDiv.innerHTML = ` | |
| <input type="checkbox" id="chk-${item.meshName}" ${item.visible ? 'checked' : ''}> | |
| <div class="color-dot" style="background:${item.color}"></div> | |
| <span class="label-text">${item.name}</span> | |
| <input type="range" min="0" max="100" value="100" title="Opacity" id="alpha-${item.meshName}"> | |
| `; | |
| // Visibility Click | |
| iDiv.querySelector('input[type="checkbox"]').onchange = (e) => { | |
| meshes[item.meshName].visible = e.target.checked; | |
| }; | |
| // Opacity Slider | |
| iDiv.querySelector('input[type="range"]').oninput = (e) => { | |
| meshes[item.meshName].material.opacity = e.target.value / 100; | |
| if (e.target.value < 100) meshes[item.meshName].visible = true; // Auto-show if sliding | |
| }; | |
| content.appendChild(iDiv); | |
| }); | |
| tree.appendChild(gDiv); | |
| } | |
| scene.add(object); | |
| const box = new THREE.Box3().setFromObject(object); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| camera.position.set(center.x, center.y + 200, center.z + 280); | |
| controls.target.copy(center); | |
| controls.update(); | |
| }); | |
| function toggleGlobal(show) { | |
| for (let id in meshes) { | |
| meshes[id].visible = show; | |
| const chk = document.getElementById(`chk-${id}`); | |
| if (chk) chk.checked = show; | |
| } | |
| } | |
| function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } | |
| animate(); | |
| window.addEventListener('resize', () => { | |
| renderer.setSize(window.innerWidth - 350, window.innerHeight); | |
| camera.aspect = (window.innerWidth - 350) / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |