window.addEventListener('error',function(event){const box=document.getElementById('renderError');box.hidden=false;box.textContent='화면 실행 오류: '+event.message;}); (()=>{ 'use strict'; const initialDocument=JSON.parse(document.getElementById('modelData').textContent),scene=document.getElementById('scene'); let sourceDocument=initialDocument,data=initialDocument,selectedDiameter=initialDocument.diameter; const initialDrawing=document.querySelector('.drawing-viewport').innerHTML,initialSource=document.getElementById('sourceLink').getAttribute('href'); let sourceUrl=null,uploadBusy=false; const specification=document.getElementById('specification'); document.getElementById('localFileNotice').hidden=location.protocol!=='file:'; const tableKeys=['D','D1','D2','L','L1','L2','L3','B','b1','b2','b3','b4','H','h1','h2','h3','h4','h5','h6','h7']; let model='r2c',mode='closed',yaw=.87,pitch=.40,zoom=1,wire=false,drag=null,pending=false; const fmt=n=>Math.round(n).toLocaleString('ko-KR'); const presets={iso:[.87,.40],inlet:[Math.PI/2,0],outlet:[-Math.PI/2,0],side:[0,0],top:[0,Math.PI/2]}; function active(selector,key,value){document.querySelectorAll(selector).forEach(b=>{const on=b.dataset[key]===value;b.classList.toggle('active',on);b.setAttribute('aria-pressed',String(on));});} function rotate(p){const x=p[0],y=p[1],z=p[2]-data.height/2,c=Math.cos(yaw),s=Math.sin(yaw),cp=Math.cos(pitch),sp=Math.sin(pitch),h=x*c-y*s,d=x*s+y*c;return [h,d*cp-z*sp,z*cp+d*sp];} function polygons(){ const mesh=data.meshes[model],lift=mode==='exploded'&&model==='r2c'?data.height*.64:0,result=[]; mesh.faces.forEach((face,i)=>{const group=mesh.groups[i];if(mode==='cutaway'&&group.part==='upper')return;const pts=face.map(index=>{const p=mesh.vertices[index];return [p[0],p[1],p[2]+(group.part==='upper'?lift:0)];});result.push({pts,part:group.part,surface:group.surface});}); if(model==='r2c'&&mode!=='closed'){ for(let i=0;i[p[0],p[1],p[2]+lift]),part:'upper',surface:'end'}); }} } return result; } function shading(poly){const a=poly.pts[0],b=poly.pts[1],c=poly.pts[2],u=b.map((n,i)=>n-a[i]),v=c.map((n,i)=>n-a[i]);let normal=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]],len=Math.hypot(...normal)||1;normal=normal.map(n=>n/len);const light=Math.max(.48,.78+normal[0]*-.22+normal[1]*-.25+normal[2]*.28);let base=poly.part==='upper'?[194,204,203]:[150,171,176];if(poly.surface==='inner')base=poly.part==='upper'?[152,171,175]:[123,150,159];return 'rgb('+base.map(n=>Math.min(255,Math.round(n*light))).join(',')+')';} function render(){ pending=false;const polys=polygons(),all=polys.flatMap(poly=>poly.pts.map(rotate)),xs=all.map(p=>p[0]),ys=all.map(p=>p[2]),minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),scale=Math.min(780/(maxX-minX||1),490/(maxY-minY||1))*zoom; const project=p=>{const r=rotate(p);return [500+(r[0]-(minX+maxX)/2)*scale,345-(r[2]-(minY+maxY)/2)*scale];}; const grid=''; const sorted=polys.map(poly=>({poly,depth:poly.pts.reduce((sum,p)=>sum+rotate(p)[1],0)/poly.pts.length})).sort((a,b)=>b.depth-a.depth); let markup=grid; for(const {poly} of sorted){const pts=poly.pts.map(project).map(p=>p.map(n=>n.toFixed(2)).join(',')).join(' '),color=shading(poly);markup+='';} // Subtle front and rear perimeter lines retain readable openings without a dense wire grid. scene.innerHTML=markup;document.getElementById('status').textContent=(mode==='exploded'?'덮개 분리':mode==='cutaway'?'내부 보기':'전체 형상')+' · '+Math.round(zoom*100)+'%'; } function schedule(){if(!pending){pending=true;requestAnimationFrame(render);}} function info(){const d=data.dimensions,values=model==='r2c'?[['관로 내경 D','Ø'+fmt(data.diameter)],['부재 길이 L',fmt(data.length)],['외형 폭 B',fmt(data.width)],['외형 높이 H',fmt(data.height)],['관 중심 높이 h6',fmt(data.axisHeight)],['입구 폭 b3',fmt(d.b3)],['소켓 길이 L2',fmt(d.L2)]]:[['관로 내경','Ø'+fmt(data.diameter)],['유효 길이',fmt(data.pipeDimensions.nominalLength)],['전체 길이',fmt(Math.max(...data.pipeProfile.map(p=>p[0])))],['본체 외경','Ø'+fmt(data.pipeDimensions.outerDiameter)],['본체 두께',fmt(data.pipeDimensions.wallThickness)]];document.getElementById('dimensions').innerHTML=values.map((v,i)=>'
'+v[0]+'
'+v[1]+' mm
').join('');document.getElementById('modelName').textContent=(model==='r2c'?'R2C관':'원심력 콘크리트관')+' · Ø'+fmt(data.diameter);document.getElementById('shapeNote').textContent=model==='r2c'?'사각 입구 → Ø'+fmt(data.diameter)+' 원형 유로 → 단차 소켓':'원본 DXF 종단 폴리라인을 회전한 실제 단면 형상';} function reset(){[yaw,pitch]=presets.iso;zoom=1;active('[data-view]','view','iso');schedule();} function updateTable(){document.getElementById('sizeTable').innerHTML=sourceDocument.table.map(row=>''+tableKeys.map(key=>key==='D'?'':''+fmt(row[key])+'').join('')+'').join('');} function applySelection(){ const variant=sourceDocument.variants.find(v=>v.diameter===selectedDiameter); if(!variant)throw Error('선택한 규격의 모델을 찾지 못했습니다.'); data=model==='pipe'?sourceDocument:{...sourceDocument,...variant}; specification.value=String(selectedDiameter);specification.disabled=model==='pipe'; document.querySelectorAll('[data-mode]').forEach(b=>b.disabled=model==='pipe'&&b.dataset.mode==='exploded'); document.getElementById('specBasis').textContent=model==='pipe'?'원본 종단':data.basis==='drawn'?'원본 단면':'치수표 재구성'; document.getElementById('specNotice').textContent=model==='pipe'?'원심력관은 원본 Ø'+fmt(sourceDocument.diameter)+' 규격만 제공합니다.':data.reconstructionNote; document.getElementById('specWarning').textContent=model==='pipe'?'':(data.warnings||[]).join(' '); document.getElementById('limits').textContent=sourceDocument.limitations.join(' '); info();updateTable();schedule(); } function selectSpecification(value){selectedDiameter=Number(value);model='r2c';active('[data-model]','model',model);applySelection();} specification.addEventListener('change',e=>selectSpecification(e.target.value)); document.getElementById('sizeTable').addEventListener('click',e=>{const button=e.target.closest('[data-spec]');if(button)selectSpecification(button.dataset.spec);}); document.querySelectorAll('[data-model]').forEach(b=>b.addEventListener('click',()=>{model=b.dataset.model;mode='closed';active('[data-model]','model',model);active('[data-mode]','mode',mode);applySelection();reset();})); document.querySelectorAll('[data-mode]').forEach(b=>b.addEventListener('click',()=>{mode=b.dataset.mode;active('[data-mode]','mode',mode);schedule();})); document.querySelectorAll('[data-view]').forEach(b=>b.addEventListener('click',()=>{[yaw,pitch]=presets[b.dataset.view];active('[data-view]','view',b.dataset.view);schedule();})); document.getElementById('wire').addEventListener('click',e=>{wire=!wire;e.currentTarget.classList.toggle('active',wire);e.currentTarget.setAttribute('aria-pressed',String(wire));schedule();}); document.getElementById('zoomIn').addEventListener('click',()=>{zoom=Math.min(2.5,zoom*1.2);schedule();});document.getElementById('zoomOut').addEventListener('click',()=>{zoom=Math.max(.5,zoom/1.2);schedule();});document.getElementById('reset').addEventListener('click',reset); scene.addEventListener('pointerdown',e=>{drag={x:e.clientX,y:e.clientY,yaw,pitch};scene.setPointerCapture(e.pointerId);});scene.addEventListener('pointermove',e=>{if(!drag)return;yaw=drag.yaw+(e.clientX-drag.x)*.008;pitch=Math.max(-1.5,Math.min(1.5,drag.pitch+(e.clientY-drag.y)*.006));active('[data-view]','view','');schedule();});scene.addEventListener('pointerup',()=>drag=null);scene.addEventListener('pointercancel',()=>drag=null);scene.addEventListener('wheel',e=>{e.preventDefault();zoom=Math.max(.5,Math.min(2.5,zoom*Math.exp(-e.deltaY*.001)));schedule();},{passive:false});scene.addEventListener('keydown',e=>{if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key))return;e.preventDefault();yaw+=e.key==='ArrowLeft'?-.1:e.key==='ArrowRight'?.1:0;pitch=Math.max(-1.5,Math.min(1.5,pitch+(e.key==='ArrowUp'?.1:e.key==='ArrowDown'?-.1:0)));schedule();}); function save(name,text,type){const url=URL.createObjectURL(new Blob([text],{type})),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);} document.getElementById('download').addEventListener('click',()=>{const mesh=data.meshes[model],lines=['# DXF reconstructed model; units: mm'];for(const p of mesh.vertices)lines.push('v '+p.join(' '));for(const f of mesh.faces)lines.push('f '+f.map(i=>i+1).join(' '));save(model+'-D'+data.diameter+'.obj',lines.join('\n')+'\n','text/plain');});document.getElementById('evidence').addEventListener('click',()=>{const report={...data};delete report.meshes;delete report.variants;save('dxf-evidence-D'+data.diameter+'.json',JSON.stringify(report,null,2),'application/json');}); let drawing=document.querySelector('.drawing-viewport svg'); let regions={},vb=[],drawingDrag=null; function drawingRegions(){ // Measure rendered vectors, including block transforms and text. const bounds=drawing.querySelector('g').getBBox(); const pad=100; const all=[bounds.x-pad,bounds.y-pad,bounds.width+2*pad,bounds.height+2*pad]; const labels=Array.from(drawing.querySelectorAll('text')); const find=text=>labels.find(label=>label.textContent.includes(text)); const around=(label,width,height)=>{ if(!label)return all.slice(); const box=label.getBBox(); return [box.x-200,box.y-100,width+400,height+500]; }; const aa=find('단면 A-A'),bb=find('단면 B-B'); let sections=all.slice(); if(aa&&bb){ const a=around(aa,sourceDocument.width,sourceDocument.height); const b=around(bb,sourceDocument.width,sourceDocument.height); const x=Math.min(a[0],b[0]),y=Math.min(a[1],b[1]); sections=[x,y,Math.max(a[0]+a[2],b[0]+b[2])-x,Math.max(a[1]+a[3],b[1]+b[3])-y]; } regions={all,r2c:around(find('측면도'),sourceDocument.length,sourceDocument.height), sections,pipe:around(find('일반 B형'),sourceDocument.pipeDimensions.overallLength,sourceDocument.pipeDimensions.outerDiameter)}; vb=regions[document.getElementById('drawingView').value].slice(); applyView(); } function applyView(){drawing.setAttribute('viewBox',vb.join(' '));}document.getElementById('drawingView').addEventListener('change',e=>{vb=regions[e.target.value].slice();applyView();});document.getElementById('drawingReset').addEventListener('click',()=>{vb=regions[document.getElementById('drawingView').value].slice();applyView();});function bindDrawing(){drawing.addEventListener('pointerdown',e=>{drawingDrag={x:e.clientX,y:e.clientY,v:vb.slice()};drawing.setPointerCapture(e.pointerId);});drawing.addEventListener('pointermove',e=>{if(!drawingDrag)return;const r=drawing.getBoundingClientRect(),s=Math.max(drawingDrag.v[2]/r.width,drawingDrag.v[3]/r.height);vb=[drawingDrag.v[0]-(e.clientX-drawingDrag.x)*s,drawingDrag.v[1]-(e.clientY-drawingDrag.y)*s,drawingDrag.v[2],drawingDrag.v[3]];applyView();});drawing.addEventListener('pointerup',()=>drawingDrag=null);drawing.addEventListener('pointercancel',()=>drawingDrag=null);drawing.addEventListener('wheel',e=>{e.preventDefault();const f=Math.exp(Math.max(-.35,Math.min(.35,e.deltaY*.001))),width=Math.max(200,Math.min(40000,vb[2]*f)),ratio=width/vb[2];vb=[vb[0]+vb[2]*(1-ratio)/2,vb[1]+vb[3]*(1-ratio)/2,width,vb[3]*ratio];applyView();},{passive:false});} function documentInfo(){ specification.innerHTML=sourceDocument.variants.map(v=>'').join(''); document.getElementById('sourceNote').textContent=sourceDocument.source+' · SHA-256 '+sourceDocument.sha256.slice(0,16)+'… · 첨부 변환은 Python 서버에서 실행'; applySelection(); } function uploadMessage(text,error=false){const el=document.getElementById('uploadMessage');el.textContent=text;el.hidden=!text;el.classList.toggle('error',error);} function replaceDocument(documentData,svg,file){ sourceDocument=documentData;selectedDiameter=documentData.diameter;model='r2c';mode='closed'; if(sourceUrl){URL.revokeObjectURL(sourceUrl);sourceUrl=null;} if(file){sourceUrl=URL.createObjectURL(file);document.getElementById('sourceLink').href=sourceUrl;document.getElementById('sourceLink').download=file.name;}else{document.getElementById('sourceLink').href=initialSource;document.getElementById('sourceLink').download=initialDocument.source;} document.querySelector('.drawing-viewport').innerHTML=svg;drawing=document.querySelector('.drawing-viewport svg');drawingDrag=null;bindDrawing(); active('[data-model]','model',model);active('[data-mode]','mode',mode);documentInfo();if(drawing.closest('details').open)drawingRegions();reset(); } async function upload(file){ if(!file||uploadBusy)return; if(!/\.dxf$/i.test(file.name)){uploadMessage('.dxf 파일을 선택해 주세요.',true);return;} if(file.size>10*1024*1024){uploadMessage('파일은 10 MB 이하로 첨부해 주세요.',true);return;} if(location.protocol==='file:'){uploadMessage('파일 첨부 변환은 웹 서버에서 사용할 수 있습니다. python3 r2c.py serve 실행 후 http://localhost:8000 에 접속해 주세요.',true);return;} uploadBusy=true;document.getElementById('uploadButton').disabled=true;document.getElementById('sampleReset').disabled=true;uploadMessage(file.name+' · 도면과 규격을 분석하고 있습니다…'); const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000); try{ const response=await fetch('/api/convert',{method:'POST',headers:{'Content-Type':'application/octet-stream','X-Filename':encodeURIComponent(file.name)},body:file,signal:controller.signal}); if(!(response.headers.get('Content-Type')||'').includes('application/json'))throw Error('첨부 변환 API가 없습니다. python3 r2c.py serve로 실행한 사이트에서 첨부해 주세요.'); const payload=await response.json();if(!response.ok)throw Error(payload.error||'도면 변환에 실패했습니다.'); if(!payload.model?.variants?.length||!payload.drawingSvg?.startsWith('fileInput.click());fileInput.addEventListener('change',e=>upload(e.target.files[0])); dropZone.addEventListener('dragover',e=>{e.preventDefault();dropZone.classList.add('dragover');});dropZone.addEventListener('dragleave',()=>dropZone.classList.remove('dragover'));dropZone.addEventListener('drop',e=>{e.preventDefault();dropZone.classList.remove('dragover');upload(e.dataTransfer.files[0]);}); document.getElementById('sampleReset').addEventListener('click',()=>{replaceDocument(initialDocument,initialDrawing,null);uploadMessage('기본 도면으로 돌아왔습니다.');}); bindDrawing();documentInfo();render(); // Hidden SVGs have no measurable bounds until their details panel opens. document.querySelector('.drawing-card').addEventListener('toggle',event=>{ if(event.currentTarget.open)drawingRegions(); }); })();