172 lines
21 KiB
JavaScript
172 lines
21 KiB
JavaScript
import {NetworkRenderer} from './renderer.js';
|
||
import {layerOf} from './geometry.js';
|
||
export const app={network:null,renderer:null,selected:null,facilities:[]};
|
||
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
|
||
const labels={junction:'맨홀',outfall:'방류구',storage:'저류시설',conduit:'관로',pump:'펌프',weir:'위어'};
|
||
const fmt=(n,d=2)=>n==null?'확인 불가':Number(n).toLocaleString('ko-KR',{minimumFractionDigits:d,maximumFractionDigits:d});
|
||
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const unit=(n,d=2)=>n==null?'확인 불가':`${fmt(n,d)} m`;
|
||
const row=(name,value)=>`<div><dt>${esc(name)}</dt><dd>${value}</dd></div>`;
|
||
const props=rows=>`<dl class="properties">${rows.join('')}</dl>`;
|
||
const section=(name,content)=>`<section class="detail-section"><h3>${esc(name)}</h3>${content}</section>`;
|
||
let toastTimer;
|
||
function toast(text){$('#toast').textContent=text;$('#toast').classList.add('visible');clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('#toast').classList.remove('visible'),3500);}
|
||
function coord(n){return n.xy?`${fmt(n.xy[0],3)}<br>${fmt(n.xy[1],3)}`:'좌표 없음 · 미배치';}
|
||
function isPlaced(f){return f.xy!=null||f.renderable;}
|
||
function getFacility(id){return app.facilities.find(f=>f.id===id);}
|
||
function linked(id){return `<button class="connection-link" data-select="${esc(id)}"><span>${esc(id)}</span><small>제원 보기 ↗</small></button>`;}
|
||
function sourceRow(f){return row('원본 위치',`[${f.type==='junction'?'JUNCTIONS':f.type==='outfall'?'OUTFALLS':f.type==='storage'?'STORAGE':f.type==='conduit'?'CONDUITS':f.type==='pump'?'PUMPS':'WEIRS'}]<br>${f.line}행`);}
|
||
function clearDetails(){
|
||
const n=app.network,s=n.summary;
|
||
$('#details').innerHTML=`<div class="overview-icon" aria-hidden="true">⌖</div><h2>연결을 선택해 보세요.</h2><p class="inspector-intro">시설을 클릭하거나 ID를 검색하면<br>단면, 표고, 연결 정보를 확인할 수 있습니다.</p><div class="overview-list">${[['입력 노드',n.nodes.length],['배치 노드',s.coordinateCount],['배치 관로',s.renderedConduits],['방류구',s.counts.outfall],['저류시설',s.counts.storage],['펌프 / 위어',`${s.counts.pump} / ${s.counts.weir}`]].map(([a,b])=>`<div><span>${esc(a)}</span><b>${esc(b)}</b></div>`).join('')}</div><p class="notice warning">좌표 없는 방류구·관로와 평면 길이 0인 관로는 임의로 배치하지 않았습니다.</p><button class="focus-button" id="inspect-missing">배치 누락·좌표 겹침 보기 ↗</button>${section('모델을 읽는 기준',`<p class="fine-print">평면 좌표계와 단위는 미확인입니다.<br>XY 1단위=1m로 가정한 관망입니다.</p><p class="fine-print">맨홀 내경 1.2m는 표현용 가정입니다. 상단은 바닥 표고+최대 깊이이며 측량 지반고가 아닙니다.</p><p class="fine-print">원형·사각관은 입력 내부 단면을 표면으로 표시합니다. 벽 두께와 실제 시공 선형은 복원하지 않았습니다.</p>`)}`;
|
||
$('#inspect-missing').onclick=()=>openAudit('placement');
|
||
}
|
||
function drawProfile(f){
|
||
const canvas=$('#profile');if(!canvas)return;const c=canvas.getContext('2d'),dpr=Math.min(devicePixelRatio||1,2),w=240,h=150;canvas.width=w*dpr;canvas.height=h*dpr;c.scale(dpr,dpr);c.clearRect(0,0,w,h);
|
||
const a=app.network.nodes.find(n=>n.id===f.fromNode),b=app.network.nodes.find(n=>n.id===f.toNode),z1=f.invertStart,z2=f.invertEnd,diam=f.section.height;
|
||
const low=Math.min(a.invert,b.invert,z1,z2)-.25,high=Math.max(a.top??a.invert+1,b.top??b.invert+1,z1+diam,z2+diam)+.4,py=z=>117-(z-low)/(high-low)*91;
|
||
const x1=36,x2=204;
|
||
c.font='9px system-ui';c.fillStyle='#648263';c.textAlign='center';c.fillText(f.fromNode,x1,13);c.fillText(f.toNode,x2,13);
|
||
for(const [n,x] of [[a,x1],[b,x2]]){const top=n.top??n.invert+1;c.fillStyle='#dce8d9';c.fillRect(x-10,py(top),20,py(n.invert)-py(top));c.strokeStyle='#83a17d';c.strokeRect(x-10,py(top),20,py(n.invert)-py(top));}
|
||
c.fillStyle='#61a7a24d';c.strokeStyle='#498c84';c.lineWidth=1.4;c.beginPath();c.moveTo(x1,py(z1));c.lineTo(x2,py(z2));c.lineTo(x2,py(z2+diam));c.lineTo(x1,py(z1+diam));c.closePath();c.fill();c.stroke();
|
||
c.setLineDash([3,3]);c.strokeStyle='#c9d6c3';c.beginPath();c.moveTo(16,126);c.lineTo(224,126);c.stroke();c.setLineDash([]);
|
||
c.fillStyle='#587455';c.textAlign='left';c.fillText(`관저 ${fmt(z1,3)}`,13,142);c.textAlign='right';c.fillText(`${fmt(z2,3)} m`,227,142);
|
||
}
|
||
export function selectFacility(f){
|
||
if(f&&window.innerWidth<=1000)setPanel('inspector',true);
|
||
if(!f)return;app.selected=f;app.renderer.set({selected:f.pickId});$('#view-name').textContent=f.id;
|
||
let html=`<span class="detail-type ${f.type}">${labels[f.type]}</span><h2 id="selected-id">${esc(f.id)}</h2>`;
|
||
const reviewReasons=app.network.issues.filter(i=>['length_mismatch','adverse_slope','missing_coordinate','unplaced_link','coincident_endpoints'].includes(i.code)&&i.ids.includes(f.id));
|
||
if(reviewReasons.length)html+=section('점검 필요 · 오류 확정 아님',`<ul class="review-reasons">${reviewReasons.map(i=>`<li>${esc(i.message)}</li>`).join('')}</ul>`);
|
||
const placed=isPlaced(f),hidden=!app.renderer.state.layers[layerOf(f)];
|
||
html+=`<button class="focus-button" id="focus-selected" ${placed?'':'disabled'}>${placed?'선택 시설로 이동 ⌖':'좌표·형상 확인 필요 · 이동 불가'}</button>`;
|
||
if(!placed)html+=`<p class="notice warning">${f.xy===null?'좌표가 없어 위치를 배치하지 않았습니다.':f.id==='UHR1-01411'?'양 끝의 평면 좌표가 같아 관로 형상을 생략했습니다.':'연결 노드 좌표 또는 단면이 없어 형상을 배치하지 않았습니다.'}</p>`;
|
||
if(hidden)html+='<p class="notice warning">이 시설의 표시 레이어가 꺼져 있습니다. 시설로 이동하면 해당 레이어를 켭니다.</p>';
|
||
if(['junction','storage','outfall'].includes(f.type)){
|
||
html+=section('위치와 표고',props([row('원본 X / Y',coord(f)),row('바닥 표고',unit(f.invert,3)),...(f.maxDepth!=null?[row('최대 깊이',unit(f.maxDepth,3)),row('모델상 상단',unit(f.top,3))]:[]),sourceRow(f)]));
|
||
if(f.type==='junction')html+='<p class="notice">내경 1.2m의 표현용 맨홀입니다. 벽 두께·뚜껑 규격은 미상이며 상단은 측량 지반고가 아닙니다.</p>';
|
||
if(f.type==='outfall')html+=section('방류 경계 조건',props([row('유형',esc(f.boundary)),row('입력 수위 / 시계열',esc(f.stage??'지정값 없음')),row('역류 방지문',esc(f.flapGate))]))+'<p class="notice">입력 경계 조건을 표시합니다. 계산된 수위나 유량이 아닙니다. 방류구 높이·크기 기호는 표현용입니다.</p>';
|
||
if(f.type==='storage')html+=section('저류 모델',props([row('형상 방식',esc(f.storageShape)),row('면적–깊이 곡선',esc(f.storageCurve))]))+'<p class="notice">높이는 입력 최대 깊이입니다. 평면 모양과 크기는 위치를 구분하는 기호이며 실제 저류조 치수가 아닙니다.</p>';
|
||
html+=section(`연결 시설 · ${f.connections.length}`,`<div class="connection-list">${f.connections.map(linked).join('')}</div>`);
|
||
}else{
|
||
html+=section('입력 연결 방향',`<div class="connection-list">${linked(f.fromNode)}${linked(f.toNode)}</div><p class="fine-print">${esc(f.fromNode)} → ${esc(f.toNode)}<br>INP 연결 순서이며 실제 유동 방향이 아닙니다.</p>`);
|
||
if(f.type==='conduit'){
|
||
const xs=f.section;
|
||
html+=section('단면 제원 · 입력 원값',props([row('단면',esc(xs?.shape??'누락')), ...(xs?.shape==='CIRCULAR'?[row('안지름',`Ø ${unit(xs.diameter,3)}`)]:xs?[row('폭 × 높이',`${fmt(xs.width,3)} × ${fmt(xs.height,3)} m`)]:[]),row('병렬 관 수',esc(xs?.barrels??'확인 불가')),row('조도계수 Manning n',fmt(f.roughness,3))]));
|
||
html+=section('길이와 경사',props([row('입력 길이',unit(f.length,3)),row('좌표상 평면 거리',f.planDistance!=null?`${fmt(f.planDistance,3)} 가정 m`:'좌표 누락'),row('거리 − 입력 길이',f.lengthDelta!=null?`${f.lengthDelta>=0?'+':''}${unit(f.lengthDelta,3)}`:'확인 불가'),row('상대 차이',f.lengthDifferenceRatio!=null?`${fmt(f.lengthDifferenceRatio*100,2)} %`:'확인 불가'),row('입력 길이 기준 경사',f.slopeInput!=null?`${fmt(f.slopeInput*100,3)} %`:'확인 불가'),row('좌표 거리 기준 경사',f.slopePlan!=null?`${fmt(f.slopePlan*100,3)} %`:'계산 불가')]));
|
||
if(f.lengthDifferenceRatio>.05)html+='<p class="notice warning">입력 길이와 좌표 거리의 차이가 5%를 넘습니다. 입력 길이에 맞춰 노드 위치를 옮기지 않았습니다.</p>';
|
||
html+='<p class="fine-print">좌표 거리와 차이는 XY 1단위=1m 가정입니다. 경사=(시작 관저고−끝 관저고)/각 길이. 양수는 입력 방향으로 낮아짐을 뜻하며 실제 흐름은 미해석입니다.</p>';
|
||
html+=section('관저고와 연결 높이',props([row('시작 끝 오프셋',unit(f.inletOffset,3)),row('도착 끝 오프셋',unit(f.outletOffset,3)),row('시작 관저고',unit(f.invertStart,3)),row('도착 관저고',unit(f.invertEnd,3)),row('시작 중심 표고',unit(f.centerStart,3)),row('도착 중심 표고',unit(f.centerEnd,3)),row('오프셋 해석',esc(app.network.meta.offsetMode))]));
|
||
if(xs&&f.invertStart!=null&&f.invertEnd!=null)html+='<div class="profile-wrap"><canvas id="profile" aria-label="선택 관로의 입력 표고 기준 연결 모식도"></canvas><p class="profile-caption">원본 표고 연결 모식도 · 가로 길이는 축척 아님<br>상단=바닥 표고+최대 깊이 (측량 지반고 아님)</p></div>';
|
||
html+=section('형상 구성',props([row('중간 꺾임',`${f.vertices.length}개`),row('평면 연결',f.vertices.length?'입력 중간점 연결':'양 끝 좌표 직선 연결'),sourceRow(f)]));
|
||
html+='<p class="fine-print">내부 단면 표면만 표시하며 관 벽 두께는 미상입니다. 이 형상은 실제 시공 선형을 복원한 것이 아닙니다.</p>';
|
||
}else if(f.type==='pump'){
|
||
html+=section('펌프 입력',props([row('펌프 곡선',esc(f.pumpCurve)),row('입력 초기 상태',esc(f.initialStatus)),row('기동 깊이',unit(f.startupDepth,3)),row('정지 깊이',unit(f.shutoffDepth,3)),sourceRow(f)]));
|
||
html+='<p class="notice">기동·정지 깊이는 입력 제어값입니다. 펌프 기호 및 연결선은 모식 표현이며 병렬 펌프는 선택을 위해 기호를 옆으로 구분했습니다. 실제 배관고·펌프 외형을 뜻하지 않습니다.</p>';
|
||
}else{
|
||
html+=section('위어 입력',props([row('위어 형식',esc(f.weirType)),row('마루 오프셋',unit(f.crestOffset,3)),row('마루 표고',unit(f.crestElevation,3)),row('유량계수',fmt(f.dischargeCoefficient,3)),row('입력 단면',esc(f.section?.shape)),row('입력 폭 / 높이',f.section?`${fmt(f.section.width,3)} / ${fmt(f.section.height,3)} m`:'단면 누락'),sourceRow(f)]));
|
||
html+='<p class="notice">위어는 일반 관로와 구분한 위치·연결 기호입니다. 폭과 마루 형상의 실제 시공 상세는 재현하지 않습니다.</p>';
|
||
}
|
||
}
|
||
$('#details').innerHTML=html;$('#focus-selected').onclick=()=>focusSelected();
|
||
$$('#details [data-select]').forEach(b=>b.onclick=()=>selectFacility(getFacility(b.dataset.select)));
|
||
drawProfile(f);
|
||
}
|
||
function focusSelected(){
|
||
const f=app.selected;if(!f)return;if(!isPlaced(f)){toast('좌표 또는 형상 확인이 필요해 이동할 수 없습니다.');return;}
|
||
const layer=layerOf(f);$(`[data-layer="${layer}"]`).checked=true;app.renderer.setLayer(layer,true);app.renderer.focus(f);toast(`${f.id} · 제원은 입력 원값입니다.`);
|
||
}
|
||
function updateSearch(){
|
||
const q=$('#search').value.trim().toLowerCase();const container=$('#search-results');
|
||
if(!q){container.replaceChildren();$('#search-status').textContent='';return;}
|
||
const result=app.facilities.filter(f=>f.id.toLowerCase().includes(q)).sort((a,b)=>(b.id.toLowerCase()===q)-(a.id.toLowerCase()===q)||a.id.localeCompare(b.id,undefined,{numeric:true}));
|
||
$('#search-status').textContent=result.length?`${result.length}개 결과${result.length>40?' · 앞 40개 표시':''}`:'일치하는 시설이 없습니다.';
|
||
container.innerHTML=result.slice(0,40).map(f=>`<button class="search-result ${isPlaced(f)?'':'missing'}" data-pick="${f.pickId}"><span>${esc(f.id)}</span><small>${esc(labels[f.type])}${isPlaced(f)?'':' · 미배치'}</small></button>`).join('');
|
||
container.querySelectorAll('button').forEach(b=>b.onclick=()=>selectFacility(app.facilities.find(f=>f.pickId===Number(b.dataset.pick))));
|
||
}
|
||
function updateScales(){
|
||
const height=Number($('#height-scale').value),size=Number($('#size-scale').value);app.renderer.set({height,size,section:$('#section-mode').checked});
|
||
$('#height-output').value=`×${height}`;$('#size-output').value=`×${size}`;$('#height-badge').textContent=`높이 ×${height}`;$('#size-badge').textContent=`단면·맨홀 폭 ×${size}`;$('#section-badge').hidden=!$('#section-mode').checked;
|
||
}
|
||
function setPanel(panel,open){
|
||
const narrow=window.innerWidth<=1000;
|
||
if(narrow&&open)document.body.classList.add(panel==='controls'?'hide-inspector':'hide-controls');
|
||
document.body.classList.toggle(`hide-${panel}`,!open);
|
||
for(const name of ['controls','inspector']){
|
||
const visible=!document.body.classList.contains(`hide-${name}`);
|
||
$(`#toggle-${name}`).setAttribute('aria-expanded',String(visible));
|
||
$(`#${name}-panel`).inert=!visible;
|
||
}
|
||
$('#panel-backdrop').hidden=!narrow||['controls','inspector'].every(n=>document.body.classList.contains(`hide-${n}`));
|
||
}
|
||
function initPanels(){
|
||
const narrow=matchMedia('(max-width: 1000px)');
|
||
const layout=()=>{setPanel('controls',!narrow.matches);setPanel('inspector',!narrow.matches);};
|
||
layout();narrow.addEventListener('change',layout);
|
||
for(const name of ['controls','inspector'])$(`#toggle-${name}`).onclick=()=>setPanel(name,document.body.classList.contains(`hide-${name}`));
|
||
$('#panel-backdrop').onclick=()=>{setPanel('controls',false);setPanel('inspector',false);};
|
||
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&narrow.matches&&!$('dialog[open]')){
|
||
const wasControls=!document.body.classList.contains('hide-controls');
|
||
setPanel('controls',false);setPanel('inspector',false);
|
||
$(`#toggle-${wasControls?'controls':'inspector'}`).focus();
|
||
}});
|
||
}
|
||
function syncAuditHighlight(){
|
||
const enabled=$('#audit-highlight').checked,reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
const steady=$('#audit-steady').checked||reduced;
|
||
$('#audit-steady').disabled=!enabled||reduced;
|
||
$('#audit-highlight-note').textContent=reduced?'동작 줄이기 설정에 따라 고정 강조합니다. 오류 확정이 아닌 점검 필요 표시입니다.':steady?'고정 빨간색으로 강조합니다. 오류 확정이 아닌 점검 필요 표시입니다.':'빨간색 1초 · 원래 색 1초 반복. 오류 확정이 아닌 점검 필요 표시입니다.';
|
||
app.renderer.setAuditHighlight(enabled,steady);
|
||
}
|
||
function reset(){
|
||
$('#audit-highlight').checked=false;$('#audit-steady').checked=false;syncAuditHighlight();
|
||
$('#height-scale').value=3;$('#size-scale').value=3;$('#section-mode').checked=false;$$('[data-layer]').forEach(b=>{b.checked=true;app.renderer.setLayer(b.dataset.layer,true);});updateScales();app.renderer.set({selected:0});app.selected=null;$('#search').value='';updateSearch();clearDetails();$('#view-name').textContent='전체 관망';app.renderer.setView('iso');app.renderer.fit();toast('표시·배율·시점을 초기화했습니다.');
|
||
}
|
||
function frame(renderer,error){
|
||
if(error==='contextlost'){fatal('그래픽 연결이 중단되었습니다. 페이지를 새로고침해 주세요.');return;}
|
||
const visible=app.facilities.filter(f=>renderer.visible(f));const nodes=visible.filter(f=>f.xy).length,conduits=visible.filter(f=>f.type==='conduit').length,other=visible.filter(f=>['pump','weir'].includes(f.type)).length;
|
||
$('#visible-count').textContent=`표시 노드 ${nodes} · 관로 ${conduits} · 펌프·위어 ${other}`;
|
||
const names={iso:'입체 보기',plan:'평면 보기',side:'측면 보기',custom:'사용자 시점'};$('#view-badge').textContent=names[renderer.view];$$('[data-view]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.view===renderer.view)));
|
||
$('#render-status').textContent=`로컬 모델 · 표시 시설 ${visible.length}`;
|
||
$('#model-status').textContent=`수리 제원 m · 원점 X ${fmt(app.network.meta.origin[0],1)} / Y ${fmt(app.network.meta.origin[1],1)}`;
|
||
}
|
||
function openAudit(filter='placement'){$('#audit-filter').value=filter;updateAudit();$('#audit-dialog').showModal();}
|
||
function updateAudit(){
|
||
const filter=$('#audit-filter').value,issues=app.network.issues.filter(i=>filter==='all'||(filter==='placement'?['missing_coordinate','unplaced_link','coincident_endpoints'].includes(i.code):i.code===filter));
|
||
$('#audit-list').innerHTML=issues.length?issues.map(i=>`<div class="audit-row"><span>${esc(i.message)}</span>${i.ids[0]&&getFacility(i.ids[0])?`<button data-select="${esc(i.ids[0])}">시설 보기</button>`:''}</div>`).join(''):'<p class="fine-print">이 항목에 점검 사항이 없습니다.</p>';
|
||
$$('#audit-list [data-select]').forEach(b=>b.onclick=()=>{selectFacility(getFacility(b.dataset.select));$('#audit-dialog').close();});
|
||
}
|
||
function fatal(message){$('#loading').hidden=true;$('#fatal-error').hidden=false;$('#fatal-error').textContent=message;$('#render-status').textContent='실행 확인 필요';}
|
||
async function main(){
|
||
initPanels();
|
||
try{
|
||
const response=await fetch('data/network.json',{cache:'no-store'});if(!response.ok)throw Error('관망 데이터를 읽을 수 없습니다. web/data/network.json 파일이 포함되어 있는지 확인해 주세요.');
|
||
app.network=await response.json();const n=app.network,s=n.summary;
|
||
app.facilities=[...n.nodes,...n.links].map((f,i)=>({...f,pickId:i+1}));
|
||
app.renderer=new NetworkRenderer($('#scene'),n,app.facilities,selectFacility,frame);
|
||
const unplaced=app.facilities.filter(f=>!isPlaced(f));
|
||
$('#audit-highlight-count').textContent=`표시 가능한 점검 관로 ${app.renderer.auditPickIds.size}개`;
|
||
$('#audit-unplaced').textContent=`위치 표시 불가 ${unplaced.length}개 · 사유 보기 ↗`;
|
||
$('#audit-unplaced').onclick=()=>openAudit('placement');
|
||
$('#audit-highlight').onchange=syncAuditHighlight;$('#audit-steady').onchange=syncAuditHighlight;
|
||
matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change',syncAuditHighlight);syncAuditHighlight();
|
||
$('#node-count').textContent=s.counts.junction;$('#pipe-count').textContent=s.counts.conduit;
|
||
for(const [layer,count] of Object.entries({junction:s.counts.junction,conduit:s.counts.conduit,outfall:s.counts.outfall,other:s.counts.storage+s.counts.pump+s.counts.weir}))$(`#layer-${layer}-count`).textContent=count;
|
||
$('#circle-count').textContent=s.shapes.CIRCULAR;$('#rect-count').textContent=s.shapes.RECT_CLOSED;$('#source-name').textContent=n.meta.sourceName;
|
||
$('#audit-count').textContent=`배치 점검 ${n.issues.filter(i=>['missing_coordinate','unplaced_link','coincident_endpoints'].includes(i.code)).length}`;
|
||
$('#audit-summary').innerHTML=`<div class="audit-summary-grid">${[['노드 / 관로',`${n.nodes.length} / ${s.counts.conduit}`],['원형 / 사각관',`${s.shapes.CIRCULAR} / ${s.shapes.RECT_CLOSED}`],['단면·연결 누락',`${(s.issues.missing_section??0)+(s.issues.unknown_endpoint??0)}건`],['길이 차이 > 5%',`${s.lengthOver5} / ${s.lengthCompared}`],['중간 꺾임 좌표',`${s.vertexCount}개`],['연결 성분',`${s.componentSizes.length}개`]].map(([a,b])=>`<div>${esc(a)}<strong>${esc(b)}</strong></div>`).join('')}</div><p class="notice">CMS · 오프셋 ${esc(n.meta.offsetMode)} · 수리 제원 m<br>평면 좌표계 미확인 / MAP Units=${esc(n.meta.mapUnits)} / XY 단위 가정</p>`;
|
||
clearDetails();$('#search').oninput=updateSearch;$('#search').onkeydown=e=>{if(e.key==='Enter')$('#search-results button')?.click();};
|
||
$$('[data-layer]').forEach(b=>b.onchange=()=>app.renderer.setLayer(b.dataset.layer,b.checked));
|
||
$('#show-all').onclick=()=>$$('[data-layer]').forEach(b=>{b.checked=true;app.renderer.setLayer(b.dataset.layer,true);});
|
||
for(const id of ['height-scale','size-scale','section-mode'])$(`#${id}`).addEventListener('input',updateScales);
|
||
$('#actual-scale').onclick=()=>{$('#height-scale').value=1;$('#size-scale').value=1;updateScales();toast('표고·단면 배율 ×1. XY 단위와 맨홀 규격은 여전히 가정입니다.');};
|
||
$('#fit').onclick=()=>{app.renderer.fit();toast('표시 중인 관망을 화면에 맞췄습니다.');};$('#reset').onclick=reset;
|
||
$$('[data-view]').forEach(b=>b.onclick=()=>app.renderer.setView(b.dataset.view));
|
||
$('#open-audit').onclick=()=>openAudit();$('#audit-filter').onchange=updateAudit;$('#open-help').onclick=()=>$('#help-dialog').showModal();
|
||
$$('.close-dialog').forEach(b=>b.onclick=()=>b.closest('dialog').close());
|
||
document.addEventListener('keydown',e=>{if(e.target.matches('input,select,textarea,button')||$('dialog[open]'))return;const k=e.key.toLowerCase();if(k==='f')app.renderer.fit();if(k==='r')reset();if(['1','2','3'].includes(k))app.renderer.setView(['iso','plan','side'][Number(k)-1]);});
|
||
$('#loading').hidden=true;app.renderer.request();
|
||
}catch(error){console.error(error);fatal(`${error.message} 이 뷰어는 file://가 아닌 로컬 HTTP 서버에서 실행해야 합니다.`);}
|
||
}
|
||
main();
|