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:
1 parent
ff2c057aa7
commit
455980b052
1 file changed
+19
-5
+19
-5
@@ -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">◀ 5 m</button><span id="staLabel" style="font-weight:600">4+745</span><button id="next" type="button">5 m ▶</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();
|
||||
|
||||
Reference in new issue
Block a user