feat: viewer station navigation (slider, 5 m steps) and flat shading for solid model (Task 8)

- 구간 보기: station slider 4+745~5+480 and 5 m prev/next buttons; camera frames a 30 m window
- flat shading (one normal per triangle) so the solid surfaces are not smeared
- excavation band drawn opaque

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5Cun3RHQKna9qxE6ybZ4A
This commit is contained in:
조선두andClaude Haiku 5.5 committed 2026-10-21 22:17:55 +00:00
1 parent ff2c057aa7
commit 455980b052
1 file changed
+19 -5
+19 -5
View File
@@ -28,6 +28,11 @@ html,body{margin:0;height:100%;background:var(--bg);color:var(--fg);font-family:
<h1>대산2터널 본선 3D 모델</h1>
<div style="color:var(--fg);opacity:.75">측점 4+745 ~ 5+480 · 단위 m</div>
<div style="display:flex;gap:6px;margin:6px 0 2px"><button id="viewAll" type="button">전체 보기</button><button id="viewStart" type="button">입구 (4+745) 보기</button></div>
<h2>구간 보기 (측점)</h2>
<input type="range" id="sta" min="4745" max="5480" step="5" value="4745" style="width:100%;margin:4px 0">
<div style="display:flex;justify-content:space-between;align-items:center;gap:6px">
<button id="prev" type="button">&#9664; 5 m</button><span id="staLabel" style="font-weight:600">4+745</span><button id="next" type="button">5 m &#9654;</button>
</div>
<h2>지보패턴 (라이닝·굴착 여유대)</h2>
<div id="pats"></div>
<h2>굴착 여유대</h2>
@@ -107,11 +112,11 @@ Promise.all([
const c = META_BY_ID[o.name];
o.userData = { id: o.name, group: c ? c.group : '', pattern: c && c.pattern ? c.pattern : null,
from: c ? c.from_sta : null, to: c ? c.to_sta : null, label: c ? c.label : o.name };
o.geometry.computeVertexNormals(); // GLB normals from the exporter are not reliable for shading
o.geometry = o.geometry.toNonIndexed(); // flat shading: one normal per triangle, no smoothing
o.geometry.computeVertexNormals();
const rgb = c && c.color ? c.color : [200, 200, 200];
o.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255),
roughness: 0.75, metalness: 0.0, side: THREE.DoubleSide });
if (o.userData.group === '굴착면') { o.material.transparent = true; o.material.opacity = 0.55; o.material.depthWrite = false; }
roughness: 0.8, metalness: 0.0, side: THREE.DoubleSide, flatShading: true });
meshes.push(o);
});
const box = new THREE.Box3().setFromObject(root);
@@ -126,14 +131,23 @@ Promise.all([
};
const overview = () => viewFrom(c, d * 0.75);
const focusStation = (sta) => {
sta = Math.max(4745, Math.min(5480, sta));
const bb = new THREE.Box3(); let any = false;
for (const m of meshes) {
if (m.userData.group === '라이닝' && m.userData.from <= sta && sta < m.userData.to) { bb.union(new THREE.Box3().setFromObject(m)); any = true; }
const u = m.userData;
if (!m.visible || u.from == null) continue;
if (u.to < sta - 15 || u.from > sta + 15) continue;
bb.union(new THREE.Box3().setFromObject(m)); any = true;
}
if (!any) return;
const cc = bb.getCenter(new THREE.Vector3()), ss = bb.getSize(new THREE.Vector3());
viewFrom(cc, Math.max(ss.x, ss.y, ss.z) * 2.2 + 12);
viewFrom(cc, Math.max(ss.x, ss.y, ss.z) * 0.9 + 25);
staEl.value = String(sta); staLabel.textContent = fmtSta(sta);
};
const staEl = document.getElementById('sta'), staLabel = document.getElementById('staLabel');
staEl.addEventListener('input', () => focusStation(Number(staEl.value)));
document.getElementById('prev').addEventListener('click', () => focusStation(Number(staEl.value) - 5));
document.getElementById('next').addEventListener('click', () => focusStation(Number(staEl.value) + 5));
document.getElementById('viewAll').addEventListener('click', overview);
document.getElementById('viewStart').addEventListener('click', () => focusStation(4745));
overview();