TMF_005_Atlas_WebXR / load_obj.html
Ajinkya
Deployment: Clean logic-only sync
130bee5
<!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>