Spaces:
Running
Running
File size: 10,241 Bytes
130bee5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | <!DOCTYPE html>
<html>
<head>
<title>MRI Atlas Explorer | Local File Mode</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; }
#file-input-container { padding: 15px; border-bottom: 1px solid #333; background: #1a1a1a; }
#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: 50px; margin-left: auto; height: 10px; cursor: pointer; }
.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: 170px; }
#container { flex-grow: 1; position: relative; }
#status { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #00ffcc; font-weight: bold; text-align: center; }
.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; }
input[type="file"] { font-size: 10px; color: #00ffcc; width: 100%; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 10px; }
</style>
</head>
<body>
<div id="sidebar">
<div id="header">BRAIN ATLAS EXPLORER</div>
<div id="file-input-container">
<label style="display:block; font-size:10px; margin-bottom:5px; color:#888;">LOAD SMOOTHED OBJ:</label>
<input type="file" id="fileItem" accept=".obj">
</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="status">Please select your .obj file to begin</div>
</div>
<script>
// --- 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 container = document.getElementById('container');
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);
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);
let meshes = {};
let currentObject = null;
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');
}
// --- FILE SELECTION LOGIC ---
document.getElementById('fileItem').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
document.getElementById('status').innerText = "Reading File...";
reader.onload = function(event) {
loadMesh(event.target.result);
};
reader.readAsText(file);
});
function loadMesh(contents) {
if (currentObject) scene.remove(currentObject);
document.getElementById('tree').innerHTML = "";
meshes = {};
const loader = new OBJLoader();
const object = loader.parse(contents);
currentObject = object;
const groups = { "Subcortical": [], "Cortex (L)": [], "Cortex (R)": [], "Vents/Other": [] };
object.traverse((child) => {
if (child.isMesh) {
const id = parseInt(child.name.replace('Segment_', ''));
let base_id = id > 2000 ? id - 1000 : id;
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);
child.visible = isDefault;
meshes[child.name] = child;
const item = { id, name, color, meshName: child.name, visible: isDefault };
if (id >= 2000) groups["Cortex (R)"].push(item);
else if (id >= 1000) groups["Cortex (L)"].push(item);
else if ([4, 5, 14, 15, 24, 31, 43, 44, 63].includes(id)) groups["Vents/Other"].push(item);
else groups["Subcortical"].push(item);
}
});
// Build Sidebar
const tree = document.getElementById('tree');
for (let gName in groups) {
if (groups[gName].length === 0) continue;
const gDiv = document.createElement('div');
gDiv.className = 'group';
gDiv.innerHTML = `<div class="group-header" onclick="this.nextSibling.classList.toggle('hidden')">
${gName} (${groups[gName].length}) <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" title="${item.name}">${item.name}</span>
<input type="range" min="0" max="100" value="100" id="alpha-${item.meshName}">
`;
iDiv.querySelector('input[type="checkbox"]').onchange = (e) => meshes[item.meshName].visible = e.target.checked;
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;
document.getElementById(`chk-${item.meshName}`).checked = true;
}
};
content.appendChild(iDiv);
});
tree.appendChild(gDiv);
}
scene.add(object);
document.getElementById('status').style.display = 'none';
const box = new THREE.Box3().setFromObject(object);
const center = box.getCenter(new THREE.Vector3());
camera.position.set(center.x, center.y + 150, 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> |