Build DXF-based R2C 3D viewer and local web server

This commit is contained in:
eglee committed 2026-10-20 21:23:57 +09:00
1 parent 277c7005ab
commit 11fd3badf6
8 files changed
+74912 -103

No files matched your search

+69 -100
View File
@@ -1,104 +1,73 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#101b2a">
<title>R2C 일반도 · 3D 뷰어</title>
<style>
:root{color-scheme:dark;--bg:#101a28;--panel:#172435;--panel2:#1b2b3e;--line:#2d4055;--muted:#95a7b9;--text:#edf3f8;--cyan:#65d3d0;--amber:#f1bd70;--red:#ff8278}
*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);font:14px/1.5 Inter,"Noto Sans KR",system-ui,-apple-system,"Segoe UI",sans-serif}button,a,input{font:inherit}button{color:inherit}
.app{min-height:100vh;display:grid;grid-template-rows:68px minmax(0,1fr);padding:0 26px 24px;max-width:1680px;margin:auto}.topbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);gap:16px}.brand{display:flex;align-items:center;gap:12px}.mark{height:34px;width:34px;border:1px solid #47707a;border-radius:10px;display:grid;place-items:center;color:var(--cyan);font-weight:800;letter-spacing:-1px;background:linear-gradient(140deg,#173d4b,#172537)}.brand-title{font-weight:700;letter-spacing:.01em}.brand-sub{color:var(--muted);font-size:11px;margin-top:1px}.top-actions{display:flex;align-items:center;gap:9px}.pill{border:1px solid var(--line);border-radius:99px;padding:6px 11px;color:#c8d5df;font-size:11px;white-space:nowrap}.pill .dot{display:inline-block;width:6px;height:6px;background:#65d3a2;border-radius:50%;margin:0 7px 1px 1px}.pdf-link{display:flex;align-items:center;gap:7px;color:var(--text);text-decoration:none;padding:8px 12px;border-radius:8px;border:1px solid var(--line);background:var(--panel);font-size:12px}.pdf-link:hover{border-color:#5b758d;background:var(--panel2)}
main{display:grid;grid-template-columns:minmax(0,1fr) 314px;gap:18px;padding-top:18px;min-height:0}.stage-wrap{min-width:0;display:grid;grid-template-rows:auto minmax(400px,1fr) auto;gap:12px}.stage-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:20px}.eyebrow{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--cyan);font-weight:700}.stage-heading h1{font-size:20px;line-height:1.25;margin:3px 0 0;letter-spacing:-.025em}.stage-note{font-size:11px;color:var(--muted);text-align:right}.canvas-shell{position:relative;border:1px solid #2b3e51;border-radius:12px;overflow:hidden;min-height:400px;background:#e7edf0;box-shadow:0 20px 60px #0002}.canvas-shell:after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#fff2,transparent 18%,transparent 78%,#23394d12)}canvas{display:block;width:100%;height:100%;touch-action:none;cursor:grab}canvas:active{cursor:grabbing}.canvas-hud{position:absolute;z-index:1;left:16px;top:14px;display:flex;gap:7px;pointer-events:none}.hud-tag{background:#f7fafbe8;border:1px solid #cfdae0;color:#415365;padding:5px 8px;border-radius:6px;font-size:10px;font-weight:650}.canvas-hint{position:absolute;z-index:1;right:16px;bottom:14px;color:#506376;font-size:10px;background:#f7fafbe8;border:1px solid #cfdae0;border-radius:6px;padding:5px 8px;pointer-events:none}.viewbar{display:flex;justify-content:space-between;align-items:center;gap:12px}.views{display:flex;gap:6px;flex-wrap:wrap}.view-btn,.tool-btn{border:1px solid var(--line);background:var(--panel);border-radius:7px;padding:7px 11px;font-size:11px;color:#bfccd7;cursor:pointer}.view-btn:hover,.tool-btn:hover{background:var(--panel2)}.view-btn.active{background:#17464b;border-color:#337b7b;color:#baf0e9}.tools{display:flex;gap:6px;align-items:center}.tool-btn{display:flex;align-items:center;gap:6px}.tool-btn.active{border-color:#397d79;color:#9ce4dc;background:#163c43}.tool-btn svg,.pdf-link svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
aside{display:flex;flex-direction:column;gap:12px;min-width:0}.card{border:1px solid var(--line);background:var(--panel);border-radius:10px;padding:15px}.card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.card-title{font-weight:680;font-size:12px}.card-kicker{color:var(--muted);font-size:10px}.status{color:var(--cyan);font-size:10px;background:#15383c;border:1px solid #255456;padding:3px 7px;border-radius:99px}.dims{display:grid;gap:7px}.dim-row{display:flex;justify-content:space-between;align-items:center;color:#c3d0da;font-size:11px}.dim-row strong{font-size:12px;color:#f2f6f8;font-variant-numeric:tabular-nums}.dim-row span{color:var(--muted)}.dim-row .guess{color:var(--amber)}.divider{height:1px;background:var(--line);margin:5px 0}.range-wrap{margin-top:13px}.range-top{display:flex;justify-content:space-between;color:#c3d0da;font-size:10px;margin-bottom:6px}.range-top output{color:var(--cyan);font-variant-numeric:tabular-nums}input[type=range]{appearance:none;width:100%;height:3px;background:#3a4b5f;border-radius:5px;outline:none}input[type=range]::-webkit-slider-thumb{appearance:none;width:13px;height:13px;border-radius:50%;background:var(--cyan);border:2px solid #163541;cursor:pointer}input[type=range]::-moz-range-thumb{width:11px;height:11px;border-radius:50%;background:var(--cyan);border:2px solid #163541;cursor:pointer}.materials{display:grid;gap:9px}.mat-row{display:flex;align-items:center;gap:9px;font-size:11px;color:#c6d3dd}.swatch{width:9px;height:9px;border-radius:3px;background:#bbc4c8;border:1px solid #d5dcde}.swatch.rebar{background:#e47a69;border-color:#f59c89}.swatch.dimline{background:#d69b4d;border-color:#f5cb7b}.mat-row small{margin-left:auto;color:var(--muted);font-size:10px}.info{background:#202d3a;border:1px solid #3e4b53}.info .card-title{display:flex;align-items:center;gap:7px}.info .icon{width:17px;height:17px;display:grid;place-items:center;border-radius:50%;font-size:11px;color:#2f2412;background:#eab969;font-weight:800}.info p{margin:0;color:#bdc9d2;font-size:10px;line-height:1.65}.info strong{color:#f1d29c;font-weight:650}.file-name{font-size:10px;color:#aab9c6;overflow-wrap:anywhere;margin-top:9px;padding-top:9px;border-top:1px solid #354454}.footer-note{color:#71869a;font-size:10px;margin:0 2px}.toast{position:absolute;left:50%;top:20px;transform:translate(-50%,-8px);opacity:0;transition:.2s;background:#142735;color:#bceee8;border:1px solid #38716e;border-radius:7px;padding:7px 11px;font-size:11px;pointer-events:none;z-index:3}.toast.show{opacity:1;transform:translate(-50%,0)}
@media(max-width:950px){.app{padding:0 16px 18px}main{grid-template-columns:minmax(0,1fr) 270px;gap:12px}.stage-wrap{grid-template-rows:auto minmax(360px,1fr) auto}.canvas-shell{min-height:360px}}
@media(max-width:720px){.app{grid-template-rows:60px 1fr;padding:0 12px 14px}.brand-sub,.top-actions .pill{display:none}main{grid-template-columns:1fr;padding-top:13px}.stage-wrap{grid-template-rows:auto minmax(330px,55vh) auto}.canvas-shell{min-height:330px}.stage-heading h1{font-size:17px}.stage-note{font-size:10px;max-width:130px}.viewbar{align-items:flex-start;flex-direction:column}.tools{width:100%}aside{display:grid;grid-template-columns:1fr 1fr}.card.info{grid-column:1/-1}.card{padding:12px}.topbar{height:60px}}
@media(max-width:430px){aside{grid-template-columns:1fr}.card.info{grid-column:auto}.canvas-hint{right:9px;bottom:9px}.canvas-hud{left:9px;top:9px}.app{padding-inline:9px}}
</style>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#111c29">
<title>R2C · DXF 도면 3D 뷰어</title>
<style>
:root{--bg:#101b28;--panel:#172635;--border:#2b4052;--fg:#edf3f6;--muted:#9caebb;--mint:#8de1cf;--gold:#edc28a}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}button,a,select{font:inherit}button,select{cursor:pointer}button:focus-visible,a:focus-visible,select:focus-visible{outline:2px solid var(--mint);outline-offset:3px}button{color:inherit}a{color:var(--mint)}.wrap{max-width:1600px;margin:auto;padding:0 28px 30px}header{display:flex;justify-content:space-between;align-items:center;gap:16px;height:82px;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:12px}.symbol{width:39px;height:39px;border:1px solid #428379;color:var(--mint);border-radius:11px;display:grid;place-items:center;font-weight:800}.brand strong{display:block;font-size:15px}.brand small{color:var(--muted);font-size:11px}.actions{display:flex;gap:8px;align-items:center}.badge{font-size:10px;color:var(--mint);padding:5px 10px;border:1px solid #345f5b;border-radius:20px}.link,.btn{border:1px solid var(--border);background:var(--panel);padding:8px 12px;border-radius:7px;text-decoration:none;font-size:12px;color:var(--fg);white-space:nowrap}.btn:hover,.link:hover{background:#24394a}.btn.active{background:#204a49;border-color:#4a8b7f;color:#bcf4e6}h1{font-size:25px;font-weight:650;letter-spacing:-.04em;margin:4px 0 6px}.intro{display:flex;justify-content:space-between;align-items:end;margin:25px 0 19px;gap:20px}.eyebrow{color:var(--mint);font-size:10px;font-weight:700;letter-spacing:.15em}.subtitle{color:var(--muted);font-size:12px;margin:0}.intro-note{text-align:right;color:var(--muted);font-size:11px}.workspace{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:18px}.main{min-width:0}.toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}.seg{display:flex;gap:5px;flex-wrap:wrap}.seg .btn{padding:6px 10px;font-size:11px}.stage{position:relative;isolation:isolate;height:clamp(420px,61vh,760px);border:1px solid #465662;border-radius:12px;overflow:hidden;background:radial-gradient(ellipse at 45% 38%,#f5f7f6,#dfe7e9)}#scene{display:block;position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:grab}#scene:active{cursor:grabbing}.stage-label{position:absolute;left:17px;top:15px;pointer-events:none;font-size:10px;color:#526673;letter-spacing:.12em;z-index:2}.stage-label strong{display:block;font-size:12px;color:#283e49;letter-spacing:.03em;margin-top:3px}.stage-bottom{position:absolute;left:17px;bottom:14px;color:#526673;font-size:10px;pointer-events:none}.stage-status{position:absolute;right:14px;top:14px;border:1px solid #b7c9cc;background:#f9fcfaca;padding:5px 9px;border-radius:7px;color:#35635f;font-size:10px;pointer-events:none}.zoom{position:absolute;right:13px;bottom:13px;display:flex;gap:5px}.zoom button{background:#f5f8f8e8;border:1px solid #b5c6cd;border-radius:6px;color:#334b59;width:31px;height:30px;font-weight:650}.viewbar{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:10px}.help{font-size:10px;color:var(--muted)}.error{position:absolute;inset:55px 15px auto;z-index:5;padding:12px;color:#7b2f22;background:#ffefdf;border-radius:7px;font-size:12px}.error[hidden]{display:none}.cards{display:flex;flex-direction:column;gap:12px}.card{padding:16px;border:1px solid var(--border);border-radius:10px;background:var(--panel)}.card h2{font-size:12px;margin:0 0 13px;display:flex;justify-content:space-between;align-items:center}.small-tag{font-size:9px;font-weight:400;letter-spacing:.05em;color:var(--mint)}.dims{margin:0;display:grid;grid-template-columns:1fr auto;gap:8px;font-size:12px}.dims dt{color:var(--muted)}.dims dd{margin:0;font-variant-numeric:tabular-nums}.dims dd.main-value{color:var(--mint);font-weight:650}.rule{border:0;border-top:1px solid var(--border);margin:13px 0}.micro{font-size:11px;color:var(--muted);margin:0;line-height:1.7}.legend{display:flex;gap:8px;align-items:center;font-size:11px;margin-bottom:7px}.swatch{width:10px;height:10px;border-radius:3px;background:#bcc8c9}.swatch.bottom{background:#81979d}.swatch.inner{background:#6b8189}.card .btn{width:100%;margin-top:12px;text-align:center}.limits{color:var(--muted);font-size:10px;line-height:1.7;margin-top:10px}.drawing-card{border:1px solid var(--border);border-radius:10px;margin-top:22px;background:var(--panel);overflow:hidden}.drawing-card>summary{cursor:pointer;list-style:none;display:flex;justify-content:space-between;gap:12px;padding:16px;font-size:13px;font-weight:600}.drawing-card>summary::after{content:'+';color:var(--mint)}.drawing-card[open]>summary::after{content:'−'}.drawing-controls{display:flex;gap:7px;flex-wrap:wrap;align-items:center;padding:0 16px 12px}.drawing-controls select{background:var(--bg);border:1px solid var(--border);color:var(--fg);border-radius:6px;padding:6px 9px;font-size:11px}.drawing-viewport{height:440px;background:#f7fafbLine truncated
@media(max-width:1050px){.workspace{grid-template-columns:minmax(0,1fr) 270px}.wrap{padding:0 17px 25px}.intro-note{display:none}.viewbar{align-items:start;flex-direction:column}.stage{height:540px}}@media(max-width:740px){header{height:72px}.badge{display:none}.wrap{padding:0 12px 20px}.workspace{grid-template-columns:1fr}h1{font-size:21px}.intro{margin-top:20px}.stage{height:440px}.cards{display:grid;grid-template-columns:1fr 1fr}.card:last-child{grid-column:1/-1}.drawing-viewport{height:350px}.toolbar{align-items:start}.brand small{display:none}}@media(max-width:440px){.cards{grid-template-columns:1fr}.stage{height:390px}.toolbar{flex-direction:column}.actions .link{font-size:10px;padding:7px}.help{display:none}}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand"><div class="mark">R2</div><div><div class="brand-title">도면 3D 뷰어</div><div class="brand-sub">R2C · 일반도 기반 형상 검토</div></div></div>
<div class="top-actions"><span class="pill"><i class="dot"></i>로컬 인터랙티브 모델</span><a class="pdf-link" href="./drawing/R2C_일반도_20260601.pdf" target="_blank" rel="noopener"><svg viewBox="0 0 24 24"><path d="M14 3h7v7M10 14 21 3M19 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h6"/></svg>원본 도면 열기</a></div>
</header>
<main>
<section class="stage-wrap">
<div class="stage-heading"><div><div class="eyebrow">STRUCTURAL MODEL / R2C</div><h1>R2C 일반도 3D 형상</h1></div><div class="stage-note">마우스로 회전 · 휠로 확대/축소</div></div>
<div class="canvas-shell"><canvas id="scene" aria-label="R2C 도면 기반 3D 모델"></canvas><div class="canvas-hud"><span class="hud-tag">RC BOX · SCHEMATIC</span><span class="hud-tag">단위 mm</span></div><div class="canvas-hint">드래그하여 모델을 회전하세요</div><div class="toast" id="toast"></div></div>
<div class="viewbar"><div class="views" role="group" aria-label="보기 방향"><button class="view-btn active" data-view="iso">등각</button><button class="view-btn" data-view="front">정면</button><button class="view-btn" data-view="side">측면</button><button class="view-btn" data-view="top">평면</button></div><div class="tools"><button class="tool-btn active" id="rebarBtn" aria-pressed="true"><svg viewBox="0 0 24 24"><path d="M5 19 19 5M4 12l8-8M12 20l8-8M4 17l3 3M17 4l3 3"/></svg>철근 표시</button><button class="tool-btn" id="resetBtn"><svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 2.6-6.4L3 8M3 3v5h5"/></svg>초기화</button></div></div>
</section>
<aside>
<section class="card"><div class="card-head"><span class="card-title">주요 치수</span><span class="status">도면 확인</span></div><div class="dims"><div class="dim-row"><span>외형 폭 <span>W</span></span><strong>2,500 mm</strong></div><div class="dim-row"><span>외형 높이 <span>H</span></span><strong>1,620 mm</strong></div><div class="dim-row"><span>벽체 두께 <span>T</span></span><strong>120 mm</strong></div><div class="divider"></div><div class="dim-row"><span>모델 깊이 <span class="guess">L · 시각화값</span></span><strong id="depthReadout">1,600 mm</strong></div></div><div class="range-wrap"><div class="range-top"><label for="depth">깊이 조절</label><output id="depthOutput">1,600 mm</output></div><input id="depth" type="range" min="800" max="3200" step="100" value="1600"></div></section>
<section class="card"><div class="card-head"><span class="card-title">표현 요소</span><span class="card-kicker">LEGEND</span></div><div class="materials"><div class="mat-row"><i class="swatch"></i>철근 콘크리트<small>회색</small></div><div class="mat-row"><i class="swatch rebar"></i>철근 배근 개략<small>표시 전환 가능</small></div><div class="mat-row"><i class="swatch dimline"></i>치수 기준 형상<small>mm</small></div></div></section>
<section class="card info"><div class="card-head"><span class="card-title"><i class="icon">i</i>모델 참고</span></div><p>도면의 <strong>2,500 × 1,620 mm</strong> 외형 치수와 <strong>120 mm</strong> 치수 표기를 반영한 개략 모델입니다. 도면에서 깊이를 특정하기 어려워 조절값으로 제공하며, 철근 표시는 배근 형상을 이해하기 위한 시각화입니다.</p><div class="file-name">R2C_일반도_20260601.pdf</div></section>
<p class="footer-note">형상 검토용 시각화 · 제작·시공 전 원본 도면과 구조 검토 자료를 확인하세요.</p>
</aside>
</main>
</div>
<script>
(()=>{
const canvas=document.getElementById('scene'),ctx=canvas.getContext('2d');
const W=2500,H=1620,T=120;
let L=1600, yaw=-.72,pitch=.48,zoom=1,showRebar=true,drag=null;
const fmt=n=>new Intl.NumberFormat('ko-KR').format(n);
function resize(){const r=canvas.getBoundingClientRect(),d=Math.min(devicePixelRatio||1,2);canvas.width=Math.round(r.width*d);canvas.height=Math.round(r.height*d);ctx.setTransform(d,0,0,d,0,0);draw()}
const V=(x,y,z)=>[x,y,z];
function quad(a,b,c,d,color,edge='#67737b',normal){return {pts:[a,b,c,d],color,edge,normal}}
function faces(){
const x=W/2,y=L/2,z=H,ix=x-T,iy=y,iz0=T,iz1=H-T, f=[];
const cTop='#c4cdd0',cFront='#d2d9da',cSide='#aeb9be',cInner='#99a7ad',cBottom='#aab5b9';
// outside shell surfaces
f.push(quad(V(-x,-y,z),V(x,-y,z),V(x,y,z),V(-x,y,z),cTop,'#76838a',[0,0,1]));
f.push(quad(V(-x,-y,0),V(-x,y,0),V(x,y,0),V(x,-y,0),cBottom,'#68767d',[0,0,-1]));
f.push(quad(V(-x,-y,0),V(-x,-y,z),V(-x,y,z),V(-x,y,0),cSide,'#66747c',[-1,0,0]));
f.push(quad(V(x,-y,0),V(x,y,0),V(x,y,z),V(x,-y,z),'#b8c2c6','#6d7980',[1,0,0]));
// tunnel interior
f.push(quad(V(-ix,-y,iz1),V(ix,-y,iz1),V(ix,y,iz1),V(-ix,y,iz1),'#b8c2c5','#6c7a81',[0,0,-1]));
f.push(quad(V(-ix,-y,iz0),V(-ix,y,iz0),V(ix,y,iz0),V(ix,-y,iz0),'#a9b5b9','#68777d',[0,0,1]));
f.push(quad(V(-ix,-y,iz0),V(-ix,-y,iz1),V(-ix,y,iz1),V(-ix,y,iz0),cInner,'#64727a',[1,0,0]));
f.push(quad(V(ix,-y,iz0),V(ix,y,iz0),V(ix,y,iz1),V(ix,-y,iz1),'#9eacb1','#64727a',[-1,0,0]));
// visible entrance ring at front
f.push(quad(V(-x,-y,z),V(x,-y,z),V(ix,-y,iz1),V(-ix,-y,iz1),cFront,'#68767d',[0,-1,0]));
f.push(quad(V(-x,-y,0),V(-ix,-y,iz0),V(ix,-y,iz0),V(x,-y,0),'#bdc7ca','#68767d',[0,-1,0]));
f.push(quad(V(-x,-y,0),V(-x,-y,z),V(-ix,-y,iz1),V(-ix,-y,iz0),'#c8d1d3','#68767d',[0,-1,0]));
f.push(quad(V(x,-y,0),V(ix,-y,iz0),V(ix,-y,iz1),V(x,-y,z),'#c2cccf','#68767d',[0,-1,0]));
// rear ring, visible as a shaded face through tunnel
f.push(quad(V(-x,y,z),V(-ix,y,iz1),V(ix,y,iz1),V(x,y,z),'#b6c0c3','#77838a',[0,1,0]));
f.push(quad(V(-x,y,0),V(x,y,0),V(ix,y,iz0),V(-ix,y,iz0),'#9eaaaf','#77838a',[0,1,0]));
f.push(quad(V(-x,y,0),V(-ix,y,iz0),V(-ix,y,iz1),V(-x,y,z),'#a9b5b9','#77838a',[0,1,0]));
f.push(quad(V(x,y,0),V(x,y,z),V(ix,y,iz1),V(ix,y,iz0),'#a5b1b6','#77838a',[0,1,0]));
return f;
}
function rot(p){let [x,y,z]=p;const cy=Math.cos(yaw),sy=Math.sin(yaw),cp=Math.cos(pitch),sp=Math.sin(pitch);const x1=x*cy-y*sy,y1=x*sy+y*cy;return [x1,y1*cp-z*sp,y1*sp+z*cp]}
function projection(){const r=canvas.getBoundingClientRect(),scale=Math.min(r.width/(W+L*.68),r.height/(H+L*.5))*1.14*zoom;return {cx:r.width*.5,cy:r.height*.53,scale}}
function project(p,P){const [x,y,z]=rot(p);return [P.cx+x*P.scale,P.cy-(z-y*.45)*P.scale]}
function shade(hex,n){const l=Math.max(.72,Math.min(1.12,.88+(-n[0]*.13-n[1]*.17+n[2]*.22)));const h=hex.replace('#',''),v=[0,2,4].map(i=>Math.min(255,Math.round(parseInt(h.slice(i,i+2),16)*l)));return `rgb(${v.join(',')})`}
function drawGrid(P){const r=canvas.getBoundingClientRect();ctx.fillStyle='#e7edf0';ctx.fillRect(0,0,r.width,r.height);ctx.save();ctx.strokeStyle='#d5dfe3';ctx.lineWidth=1;const extent=4200,step=400;for(let x=-extent;x<=extent;x+=step){const a=project([x,-extent,-4],P),b=project([x,extent,-4],P);ctx.beginPath();ctx.moveTo(...a);ctx.lineTo(...b);ctx.stroke()}for(let y=-extent;y<=extent;y+=step){const a=project([-extent,y,-4],P),b=project([extent,y,-4],P);ctx.beginPath();ctx.moveTo(...a);ctx.lineTo(...b);ctx.stroke()}ctx.restore()}
function line3(a,b,P,color,width=1){const A=project(a,P),B=project(b,P);ctx.beginPath();ctx.moveTo(...A);ctx.lineTo(...B);ctx.strokeStyle=color;ctx.lineWidth=width;ctx.stroke()}
function drawRebar(P){const rebar='#d97567',thin=2;
// perimeter bars following the two opening rings
for(const y of [-L/2+T*.46,L/2-T*.46]){
const x=W/2-T*.34,z0=T*.48,z1=H-T*.48;
const corners=[V(-x,y,z0),V(x,y,z0),V(x,y,z1),V(-x,y,z1)];
for(let i=0;i<4;i++)line3(corners[i],corners[(i+1)%4],P,rebar,thin);
}
// longitudinal bars at slab edges and sidewalls
for(const xx of [-W/2+T*.46,W/2-T*.46])for(const zz of [T*.47,H-T*.47])line3(V(xx,-L/2+T*.46,zz),V(xx,L/2-T*.46,zz),P,rebar,thin);
for(let i=1;i<=5;i++){const x=-W/2+(W*i/6);line3(V(x,-L/2+T*.46,T*.48),V(x,-L/2+T*.46,H-T*.48),P,'#cb7165',1.25)}
// simple ties along the visible near side of the two walls
for(let i=1;i<8;i++){const yy=-L/2+T+(L-2*T)*i/8;line3(V(-W/2+T*.42,yy,T*.42),V(-W/2+T*.42,yy,H-T*.42),'#d8897d',1);line3(V(W/2-T*.42,yy,T*.42),V(W/2-T*.42,yy,H-T*.42),'#d8897d',1)}
}
function dimension(a,b,label,offset,P){const A=project(a,P),B=project(b,P),ox=offset[0],oy=offset[1],C=[A[0]+ox,A[1]+oy],D=[B[0]+ox,B[1]+oy];ctx.save();ctx.strokeStyle='#b17935';ctx.fillStyle='#795421';ctx.lineWidth=1;ctx.setLineDash([4,3]);ctx.beginPath();ctx.moveTo(...A);ctx.lineTo(...C);ctx.moveTo(...B);ctx.lineTo(...D);ctx.setLineDash([]);ctx.moveTo(...C);ctx.lineTo(...D);ctx.stroke();const ang=Math.atan2(D[1]-C[1],D[0]-C[0]);for(const [p,sgn] of [[C,1],[D,-1]]){ctx.beginPath();ctx.moveTo(...p);ctx.lineTo(p[0]+Math.cos(ang+sgn*.45)*8,p[1]+Math.sin(ang+sgn*.45)*8);ctx.moveTo(...p);ctx.lineTo(p[0]+Math.cos(ang-sgn*.45)*8,p[1]+Math.sin(ang-sgn*.45)*8);ctx.stroke()}ctx.font='600 11px system-ui';ctx.textAlign='center';ctx.textBaseline='middle';const mx=(C[0]+D[0])/2,my=(C[1]+D[1])/2;ctx.fillStyle='#e7edf0';ctx.fillRect(mx-35,my-9,70,18);ctx.fillStyle='#795421';ctx.fillText(label,mx,my);ctx.restore()}
function draw(){const r=canvas.getBoundingClientRect();if(!r.width||!r.height)return;const P=projection();drawGrid(P);const fs=faces().map(face=>({face,depth:face.pts.reduce((s,p)=>s+rot(p)[1],0)/4})).sort((a,b)=>a.depth-b.depth);ctx.lineJoin='round';for(const {face}=fs){const pts=face.pts.map(p=>project(p,P));ctx.beginPath();ctx.moveTo(...pts[0]);for(let i=1;i<pts.length;i++)ctx.lineTo(...pts[i]);ctx.closePath();ctx.fillStyle=shade(face.color,face.normal);ctx.fill();ctx.strokeStyle=face.edge;ctx.lineWidth=1.1;ctx.stroke()}if(showRebar)drawRebar(P);dimension(V(-W/2,-L/2,0),V(W/2,-L/2,0),'2,500 mm',[0,36],P);dimension(V(-W/2,-L/2,0),V(-W/2,-L/2,H),'1,620 mm',[-48,-2],P)}
const views={iso:[-.72,.48],front:[0,.02],side:[-Math.PI/2,.02],top:[0,Math.PI/2-.02]};
document.querySelectorAll('.view-btn').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('.view-btn').forEach(x=>x.classList.toggle('active',x===b));[yaw,pitch]=views[b.dataset.view];draw()}));
document.getElementById('depth').addEventListener('input',e=>{L=Number(e.target.value);const val=fmt(L)+' mm';document.getElementById('depthOutput').textContent=val;document.getElementById('depthReadout').textContent=val;draw()});
document.getElementById('rebarBtn').addEventListener('click',e=>{showRebar=!showRebar;e.currentTarget.classList.toggle('active',showRebar);e.currentTarget.setAttribute('aria-pressed',String(showRebar));draw()});
document.getElementById('resetBtn').addEventListener('click',()=>{[yaw,pitch]=views.iso;zoom=1;L=1600;document.getElementById('depth').value=L;document.getElementById('depthOutput').textContent='1,600 mm';document.getElementById('depthReadout').textContent='1,600 mm';draw();const t=document.getElementById('toast');t.textContent='기본 시점으로 돌아왔습니다';t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1300)});
canvas.addEventListener('pointerdown',e=>{drag={x:e.clientX,y:e.clientY,yaw,pitch};canvas.setPointerCapture(e.pointerId)});canvas.addEventListener('pointermove',e=>{if(!drag)return;yaw=drag.yaw+(e.clientX-drag.x)*.008;pitch=Math.max(-.08,Math.min(1.48,drag.pitch+(e.clientY-drag.y)*.006));document.querySelectorAll('.view-btn').forEach(x=>x.classList.remove('active'));draw()});canvas.addEventListener('pointerup',()=>drag=null);canvas.addEventListener('pointercancel',()=>drag=null);canvas.addEventListener('wheel',e=>{e.preventDefault();zoom=Math.max(.55,Math.min(1.8,zoom*Math.exp(-e.deltaY*.001)));draw()},{passive:false});
new ResizeObserver(resize).observe(canvas);resize();
})();
</script>
</body>
</html>
<body><div class="wrap">
<header><div class="brand"><div class="symbol">R2</div><div><strong>R2C 도면 스튜디오</strong><small>DXF → 단면 재구성 → 웹 3D</small></div></div><div class="actions"><span class="badge">DXF 엔티티 추출</span><a class="link" id="sourceLink" href="./drawing/R2C_일반도_20260601.dxf" download>원본 DXF ↓</a><button class="btn" id="download">3D 모델 ↓</button></div></header>
<div class="intro"><div><div class="eyebrow">DRAWING / RECONSTRUCTION</div><h1>사각 입구에서 원형 관로까지</h1><p class="subtitle">R2C관의 전환부와 소켓을 원본 단면으로 확인하세요.</p></div><div class="intro-note">모든 치수는 mm 기준<br>원본 A-A / B-B 단면 및 변화치수표 반영</div></div>
<div class="workspace"><section class="main"><div class="toolbar"><div class="seg" aria-label="표시할 부재"><button class="btn active" data-model="r2c">R2C관</button><button class="btn" data-model="pipe">원심력 콘크리트관</button></div><div class="seg" aria-label="분해 표시"><button class="btn active" data-mode="closed">전체</button><button class="btn" data-mode="exploded">덮개 분리</button><button class="btn" data-mode="cutaway">내부 보기</button></div></div>
<div class="stage"><div class="stage-label">SECTION-BASED 3D<strong id="modelName">R2C · Ø1,000</strong></div><span class="stage-status" id="status">기본 시점</span><svg id="scene" viewBox="0 0 1000 700" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="DXF에서 재구성한 R2C관 3D 모델" tabindex="0"><polygon points="487.27,400.31 485.50,399.73 536.42,363.38 536.89,364.17" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,399.73 485.50,384.86 532.68,356.63 536.42,363.38" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,384.86 485.50,384.57 532.61,356.48 532.68,356.63" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,384.57 485.50,383.95 532.45,356.18 532.61,356.48" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="502.91,405.44 487.27,400.31 536.89,364.17 541.50,371.62" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,383.95 485.50,369.17 528.68,348.59 532.45,356.18" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,369.17 485.50,355.06 525.12,340.51 528.68,348.59" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="516.62,409.95 502.91,405.44 541.50,371.62 546.42,378.79" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,355.06 485.50,341.98 521.94,332.30 525.12,340.51" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,341.98 485.50,329.72 519.15,323.98 521.94,332.30" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="528.84,413.97 516.62,409.95 546.42,378.79 551.63,385.66" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,329.72 485.50,318.11 516.78,315.59 519.15,323.98" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,318.11 485.50,307.01 514.82,307.17 516.78,315.59" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="539.89,417.60 528.84,413.97 551.63,385.66 557.12,392.20" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,307.01 485.50,296.29 513.29,298.76 514.82,307.17" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,296.29 485.50,285.84 512.19,290.37 513.29,298.76" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="550.02,420.92 539.89,417.60 557.12,392.20 562.84,398.37" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,285.84 485.50,275.57 511.53,282.07 512.19,290.37" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="559.41,424.01 550.02,420.92 562.84,398.37 568.79,404.16" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="485.50,275.57 485.50,265.39 511.31,273.87 511.53,282.07" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="528.68,348.59 525.12,340.51 525.27,340.78 528.69,348.53" fill="rgb(108,131,139)" stroke="rgb(108,131,139)" stroke-width=".45"/><polygon points="532.45,356.18 528.68,348.59 528.69,348.53 532.31,355.83" fill="rgb(108,132,140)" stroke="rgb(108,132,140)" stroke-width=".45"/><polygon points="532.61,356.48 532.45,356.18 532.31,355.83 532.46,356.11" fill="rgb(108,132,140)" stroke="rgb(108,132,140)" stroke-width=".45"/><polygon points="532.68,356.63 532.61,356.48 532.46,356.11 532.53,356.25" fill="rgb(108,132,140)" stroke="rgb(108,132,140)" stroke-width=".45"/><polygon points="525.12,340.51 521.94,332.30 522.21,332.89 525.27,340.78" fill="rgb(107,131,138)" stroke="rgb(107,131,138)" stroke-width=".45"/><polygon points="536.42,363.38 532.68,356.63 532.53,356.25 536.12,362.74" fill="rgb(109,132,140)" stroke="rgb(109,132,140)" stroke-width=".45"/><polygon points="536.89,364.17 536.42,363.38 536.12,362.74 536.57,363.50" fill="rgb(109,133,141)" stroke="rgb(109,133,141)" stroke-width=".45"/><polygon points="521.94,332.30 519.15,323.98 519.54,324.90 522.21,332.89" fill="rgb(106,130,137)" stroke="rgb(106,130,137)" stroke-width=".45"/><polygon points="541.50,371.62 536.89,364.17 536.57,363.50 541.00,370.65" fill="rgb(109,133,141)" stroke="rgb(109,133,141)" stroke-width=".45"/><polygon points="519.15,323.98 516.78,315.59 517.26,316.85 519.54,324.90" fill="rgb(105,128,136)" stroke="rgb(105,128,136)" stroke-width=".45"/><polygon points="546.42,378.79 541.50,371.62 541.00,370.65 545.73,377.54" fill="rgb(109,133,141)" stroke="rgb(109,133,141)" stroke-width=".45"/><polygon points="568.20,426.90 559.41,424.01 568.79,404.16 574.93,409.53" fill="rgb(84,96,99)" stroke="rgb(84,96,99)" stroke-width=".45"/><polygon points="516.78,315.59 514.82,307.17 515.38,308.76 517.26,316.85" fill="rgb(104,127,135)" stroke="rgb(104,127,135)" stroke-width=".45"/><polygon points="485.50,265.39 485.50,255.21 511.53Line truncated
<div class="viewbar"><div class="seg" aria-label="시점"><button class="btn active" data-view="iso">등각</button><button class="btn" data-view="inlet">입구 A-A</button><button class="btn" data-view="outlet">소켓 B-B</button><button class="btn" data-view="side">측면</button><button class="btn" data-view="top">평면</button><button class="btn" id="wire" aria-pressed="false">윤곽선</button></div><span class="help">드래그 회전 · 휠 확대 · 방향키 회전</span></div></section>
<aside class="cards"><section class="card"><h2>도면에서 읽은 치수 <span class="small-tag">DXF VERIFIED</span></h2><dl class="dims" id="dimensions"></dl><hr class="rule"><p class="micro" id="shapeNote">사각 입구 → Ø1,000 원형 유로 → 단차 소켓</p></section><section class="card"><h2>부재와 내부 형상</h2><div class="legend"><i class="swatch"></i>상부 덮개</div><div class="legend"><i class="swatch bottom"></i>하부 본체</div><div class="legend"><i class="swatch inner"></i>전환부·관로·소켓</div><p class="micro">덮개 분리 또는 내부 보기를 선택하면 사각–원형 전환 면이 드러납니다.</p></section><section class="card"><h2>재구성 기준 <span class="small-tag">A-A / B-B</span></h2><p class="micro">외곽선·원호·치수표에서 단면과 소켓 위치를 추출합니다. 원심력관은 실제 종단 폴리라인을 회전시켜 만듭니다.</p><div class="limits" id="limits"></div><button class="btn" id="evidence">추출 근거 JSON ↓</button></section></aside></div>
<details class="drawing-card"><summary>원본 DXF 벡터 도면과 비교</summary><div class="drawing-controls"><select id="drawingView" aria-label="도면 영역"><option value="all">전체 일반도</option><option value="r2c">R2C 평면·측면·단면</option><option value="sections">A-A / B-B 단면</option><option value="pipe">원심력관 종단·단면</option></select><button class="btn" id="drawingReset">영역 맞춤</button><span class="micro">드래그 이동 · 휠 확대</span></div><div class="drawing-viewport"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -5940 16820 5940" aria-label="DXF 원본 벡터 도면"><rect x="0" y="-5940" width="16820" height="5940" fill="#f7fafb"/><g font-family="system-ui,sans-serif"><polyline points="10666.486,-4695.680 10556.486,-4695.680" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="10666.486,-3695.680 10556.486,-3695.680" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="10566.486,-4685.680 10566.486,-3705.680" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><g transform="translate(10566.4858629485,-4695.680351079151) rotate(-90.0) scale(10.0,10.0)"><polyline points="-0.250,-0.000 0.250,-0.000 -0.250,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/><polyline points="-0.500,-0.000 -1.000,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/></g><g transform="translate(10566.4858629485,-3695.680351078918) rotate(-270.0) scale(10.0,10.0)"><polyline points="-0.250,-0.000 0.250,-0.000 -0.250,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/><polyline points="-0.500,-0.000 -1.000,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/></g><text x="10536.47083288838" y="-4195.680351079034" font-size="30.0" fill="#cc6c49" transform="rotate(-0.0 10536.47083288838 -4195.680351079034)">D</text><polyline points="10766.486,-4785.386 10656.486,-4785.386" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="10766.486,-3605.975 10656.486,-3605.975" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="10666.486,-4775.386 10666.486,-3615.975" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><g transform="translate(10666.4858629485,-4785.38574328777) rotate(-90.0) scale(10.0,10.0)"><polyline points="-0.250,-0.000 0.250,-0.000 -0.250,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/><polyline points="-0.500,-0.000 -1.000,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/></g><g transform="translate(10666.4858629485,-3605.97495887056) rotate(-270.0) scale(10.0,10.0)"><polyline points="-0.250,-0.000 0.250,-0.000 -0.250,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/><polyline points="-0.500,-0.000 -1.000,-0.000" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/></g><text x="10636.47083288838" y="-4195.680351079165" font-size="30.0" fill="#cc6c49" transform="rotate(-0.0 10636.47083288838 -4195.680351079165)">D1</text><polyline points="970.038,-4513.028 970.038,-3345.028" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="874.038,-4515.028 874.038,-3343.028" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="850.038,-4539.028 850.038,-3319.028" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><text x="788.3174449396392" y="-5049.357368415149" font-size="100.0" fill="#263c4b" transform="rotate(-0.0 788.3174449396392 -5049.357368415149)">▣ 원심력 철근콘크리트관 Ø1000 일반 B형</text><polyline points="3470.038,-3429.028 970.038,-3429.028 970.038,-3345.028 874.038,-3343.028 850.038,-3319.028 850.038,-3263.028 1015.038,-3263.028 1180.038,-3347.028 3430.038,-3347.028 3430.038,-3354.028 3470.038,-3354.028 3470.038,-3429.028" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/><polyline points="3470.038,-4429.028 970.038,-4429.028 970.038,-4513.028 874.038,-4515.028 850.038,-4539.028 850.038,-4595.028 1015.038,-4595.028 1180.038,-4511.028 3430.038,-4511.028 3430.038,-4504.028 3470.038,-4504.028 3470.038,-4429.028" fill="none" stroke="#263c4b" stroke-width="4" stroke-linejoin="round"/><polyline points="970.038,-4513.028 781.162,-4513.028" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="756.863,-3929.028 3570.038,-3929.028" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="850.038,-4695.028 850.038,-4595.028" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="1015.038,-4695.028 1015.038,-4595.028" fill="none" stroke="#cc6c49" stroke-width="4" stroke-linejoin="round"/><polyline points="1180.038,-4695.028 1180.038,-4511.028" fill="nLine truncated
<details class="drawing-card"><summary>변화치수표 · 원본에서 추출한 7개 규격</summary><div class="table-scroll"><table><thead><tr><th>D</th><th>L</th><th>B</th><th>H</th><th>D1</th><th>D2</th><th>L1</th><th>L2</th><th>L3</th></tr></thead><tbody id="sizeTable"></tbody></table></div><div class="drawing-note">3D 모델은 실제 단면이 그려진 Ø1,000 규격입니다. 다른 규격은 치수표 비교용입니다.</div></details>
<p class="foot" id="sourceNote">Python 표준 라이브러리로 생성한 독립 실행형 웹 뷰어 · 외부 CDN 및 추가 패키지 불필요</p>
</div>
<script id="modelData" type="application/json">{"source":"R2C_일반도_20260601.dxf","sha256":"24ecb64f2a9875bed65c0e152b1361463ec11767d687bd337744ed75ef144b32","title":"R2C관 · 사각–원형 전환관","diameter":1000.0,"dimensions":{"D":1000.0,"D1":1180.0,"D2":1183.0,"L":1620.0,"L1":86.0,"B":1540.0,"b1":150.0,"b2":120.0,"b3":1310.0,"b4":115.0,"H":1486.0,"h1":166.0,"h2":200.0,"h3":120.0,"h4":205.0,"h5":115.0,"h6":666.0,"h7":820.0,"L2":120.0,"L3":24.0},"width":1540.0,"height":1486.0,"length":1620.0,"axisHeight":666.0,"profiles":[{"x":0,"kind":"rectangle","halfWidth":655.0000000004202,"top":705.0,"bottom":500.0},{"x":100.0,"kind":"rectangle","halfWidth":650.0000000003911,"top":700.0,"bottom":500.0},{"x":1400.0,"kind":"circle","radius":500.0},{"x":1500.0,"kind":"circle","radius":500.0},{"x":1500.0,"kind":"circle","radius":579.705392},{"x":1509.998477,"kind":"circle","radius":589.879916},{"x":1596.003655,"kind":"circle","radius":591.381142},{"x":1620.0,"kind":"circle","radius":615.8}],"pipeProfile":[[2620.0,500.0],[120.0,500.0],[120.0,584.0],[24.0,586.0],[0.0,610.0],[0.0,666.0],[165.0,666.0],[330.0,582.0],[2580.0,582.0],[2580.0,575.0],[2620.0,575.0]],"pipeDimensions":{"nominalLength":2500.0,"overallLength":2620.0,"outerDiameter":1164.0,"wallThickness":82.0},"table":[{"D":700.0,"D1":880.0,"D2":883.0,"L":1605.0,"L1":75.0,"B":1100.0,"b1":100.0,"b2":100.0,"b3":910.0,"b4":95.0,"H":1068.0,"h1":118.0,"h2":150.0,"h3":100.0,"h4":155.0,"h5":95.0,"h6":468.0,"h7":600.0,"L2":105.0,"L3":30.0},{"D":800.0,"D1":980.0,"D2":983.0,"L":1610.0,"L1":80.0,"B":1300.0,"b1":150.0,"b2":100.0,"b3":1110.0,"b4":95.0,"H":1234.0,"h1":134.0,"h2":200.0,"h3":100.0,"h4":205.0,"h5":95.0,"h6":534.0,"h7":700.0,"L2":110.0,"L3":30.0},{"D":900.0,"D1":1080.0,"D2":1083.0,"L":1615.0,"L1":85.0,"B":1440.0,"b1":150.0,"b2":120.0,"b3":1210.0,"b4":115.0,"H":1372.0,"h1":152.0,"h2":200.0,"h3":120.0,"h4":205.0,"h5":115.0,"h6":602.0,"h7":770.0,"L2":115.0,"L3":30.0},{"D":1000.0,"D1":1180.0,"D2":1183.0,"L":1620.0,"L1":86.0,"B":1540.0,"b1":150.0,"b2":120.0,"b3":1310.0,"b4":115.0,"H":1486.0,"h1":166.0,"h2":200.0,"h3":120.0,"h4":205.0,"h5":115.0,"h6":666.0,"h7":820.0,"L2":120.0,"L3":24.0},{"D":1100.0,"D1":1280.0,"D2":1283.0,"L":1625.0,"L1":100.0,"B":1780.0,"b1":200.0,"b2":140.0,"b3":1510.0,"b4":135.0,"H":1669.0,"h1":179.0,"h2":250.0,"h3":140.0,"h4":255.0,"h5":135.0,"h6":729.0,"h7":940.0,"L2":125.0,"L3":25.0},{"D":1200.0,"D1":1380.0,"D2":1383.0,"L":1630.0,"L1":104.0,"B":1880.0,"b1":200.0,"b2":140.0,"b3":1610.0,"b4":135.0,"H":1783.0,"h1":193.0,"h2":250.0,"h3":140.0,"h4":255.0,"h5":135.0,"h6":793.0,"h7":990.0,"L2":130.0,"L3":26.0},{"D":1350.0,"D1":1530.0,"D2":1533.0,"L":1635.0,"L1":108.0,"B":2030.0,"b1":200.0,"b2":140.0,"b3":1760.0,"b4":135.0,"H":1949.0,"h1":209.0,"h2":250.0,"h3":140.0,"h4":255.0,"h5":135.0,"h6":884.0,"h7":1065.0,"L2":135.0,"L3":27.0}],"counts":{"DIMENSION":76,"HATCH":11,"LINE":521,"TEXT":184,"LWPOLYLINE":12,"CIRCLE":3,"SPLINE":12,"ARC":17,"ELLIPSE":27,"MTEXT":2,"INSERT":3,"SOLID":2},"evidence":{"sectionA":"44F","sectionB":["356","357","35B","35C","35D","36F","370","37F","380","381"],"pipeProfile":"195","stations":[100.0,1400.0,1500.0,1509.998477,1596.003655]},"limitations":["2D 일반도 A-A/B-B와 치수표를 이용한 단면 기반 재구성입니다.","사각–원형 전환 면은 단면 사이를 직선 보간했습니다. 철근과 개스킷은 모델에 포함하지 않았습니다.","상·하부 접합부의 작은 홈은 원본 2D 도면에서 확인할 수 있으며 3D에서는 생략했습니다."],"meshes":{"r2c":{"vertices":[[-810.0,770.0,666.0],[-810.0,770.0,716.468466],[-810.0,770.0,767.372423],[-810.0,770.0,819.162523],[-810.0,770.0,872.320878],[-810.0,770.0,927.379779],[-810.0,770.0,984.944443],[-810.0,770.0,1045.721978],[-810.0,770.0,1110.559707],[-810.0,770.0,1180.497551],[-810.0,770.0,1256.841781],[-810.0,770.0,1341.271877],[-810.0,770.0,1436.0],[-810.0,770.0,1486.0],[-810.0,761.843972,1486.0],[-810.0,761.428571,1486.0],[-810.0,719.1207,1486.0],[-810.0,629.20813,1486.0],[-810.0,547.906483,1486.0],[-810.0,473.427221,1486.0],[-810.0,404.379249,1486.0],[-810.0,339.655121,1486.0],[-810.0,278.352492,1486.0],[-810.0,219.718338,1486.0],[-810.0,163.108141,1486.0],[-810.0,107.955048,1486.0],[-810.0,53.74564,1486.0],[-810.0,-0.0,1486.0],[-810.0,-53.74564,1486.0],[-810.0,-107.955048,1486.0],[-810.0,-163.108141,1486.0],[-810.0,-219.718338,1486.0],[-810.0,-278.352492,1486.0],[-810.0,-339.655121,1486.0],[-810.0,-404.379249,1486.0],[-810.0,-473.427221,1486.0],[-810.0,-547.906483,1486.0],[-810.0,-629.20813,1486.0],[-810.0,-719.1207,1486.0],[-810.0,-761.428571,1486.0],[-810.0,-761.843972,1486.0],[-810.0,-770.0,1486.0],[-810.0,-770.0,1436.0],[-810.0,-770.0,1341.271877],[-810.0,-770.0,1256.841781],[-810.0,-770.0,1180.497551],[-810.0,-770.0,1110.559707],[-810.0,-770.0,1045.721978],[-810.0,-770.0,984.944443],[-810.0,-770.0,927.379779],[-810.0,-770.0,872.320878],[-810.0,-770.0,819.162523],[-810.0,-770.0,767.372423],[-810.0,-770.0,716.468466],[-810.0,Line truncated
<script>
window.addEventListener('error',function(event){const box=document.getElementById('renderError');box.hidden=false;box.textContent='화면 실행 오류: '+event.message;});
(()=>{
'use strict';
const data=JSON.parse(document.getElementById('modelData').textContent),scene=document.getElementById('scene');
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<data.profiles.length-1;i++){const a=data.profiles[i],b=data.profiles[i+1];if(b.x-a.x<.001)continue;for(const sign of [-1,1]){
const ra=a.kind==='circle'?a.radius:a.halfWidth,rb=b.kind==='circle'?b.radius:b.halfWidth;
const pts=[[a.x-data.length/2,sign*data.width/2,data.axisHeight],[b.x-data.length/2,sign*data.width/2,data.axisHeight],[b.x-data.length/2,sign*rb,data.axisHeight],[a.x-data.length/2,sign*ra,data.axisHeight]];
result.push({pts,part:'lower',surface:'end'});if(lift)result.push({pts:pts.map(p=>[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='<defs><pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse"><path d="M 40 0 L 0 0 0 40" fill="none" stroke="#98b1bb" stroke-opacity=".18" stroke-width="1"/></pattern></defs><rect width="1000" height="700" fill="url(#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+='<polygon points="'+pts+'" fill="'+color+'" stroke="'+(wire?'#3b5965':color)+'" stroke-width="'+(wire?'.7':'.45')+'" stroke-linejoin="round"/>';}
// 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)=>'<dt>'+v[0]+'</dt><dd class="'+(i===0?'main-value':'')+'">'+v[1]+' <small>mm</small></dd>').join('');document.getElementById('modelName').textContent=(model==='r2c'?'R2C관':'원심력 콘크리트관')+' · Ø'+fmt(data.diameter);document.getElementById('shapeNote').textContent=model==='r2c'?'사각 입구 → Ø1,000 원형 유로 → 단차 소켓':'종단 폴리라인 195를 관 중심축으로 회전한 실제 단면 형상';}
function reset(){[yaw,pitch]=presets.iso;zoom=1;active('[data-view]','view','iso');schedule();}
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);document.querySelectorAll('[data-mode]').forEach(x=>x.disabled=model==='pipe'&&x.dataset.mode==='exploded');info();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+'.obj',lines.join('\n')+'\n','text/plain');});document.getElementById('evidence').addEventListener('click',()=>{const report={...data};delete report.meshes;save('dxf-evidence.json',JSON.stringify(report,null,2),'application/json');});
document.getElementById('sizeTable').innerHTML=data.table.map(row=>'<tr class="'+(row.D===data.diameter?'current':'')+'">'+['D','L','B','H','D1','D2','L1','L2','L3'].map(key=>'<td>'+fmt(row[key])+'</td>').join('')+'</tr>').join('');document.getElementById('limits').textContent=data.limitations.join(' ');document.getElementById('sourceNote').textContent=data.source+' · SHA-256 '+data.sha256.slice(0,16)+'… · 외부 라이브러리 없이 생성';
const drawing=document.querySelector('.drawing-viewport svg'),regions={all:[0,-5940,16820,5940],r2c:[8750,-5500,7900,5000],sections:[11350,-2800,4200,1950],pipe:[550,-4900,4800,1900]};let vb=regions.all.slice(),drawingDrag=null;
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();});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});
info();render();
})();
</script>
</body></html>