원본 도면 뷰를 3D 모델 기준면에 투영

This commit is contained in:
김지영 committed 2026-10-20 15:14:22 +09:00
1 parent f8ecd4f5c0
commit 27b3230dba
18 files changed
+489 -15

No files matched your search

+3
View File
@@ -65,6 +65,9 @@
- 화면: `src/ModelViewer.jsx`와 `src/App.jsx`. 끌어서 회전, 휠 확대/축소, Shift/오른쪽 끌기로 이동, 전체·평면·정면·측면·A-A·B-B 시점을 제공한다.
- 객체는 단일 중심선 모델에 구조선 114개와 삽입 블록 윤곽 40개, 총 154개 객체를 구성했다(단면 구조선 114개와 삽입 블록 윤곽 40개). 원본선 단위 선택·숨김과 MAJR/MINR/블록 윤곽 그룹 숨김을 지원한다.
- 화면의 면은 검토용 열린 표면 모델이다. 구조선 사이를 연결했으며 모든 두께/접합을 확정한 닫힌 체적이나 실사 렌더링은 아니다.
- `scripts/render_block8_drawing_views.py`는 DXF 도면 영역의 선·문자를 잘라 평면도, 상부평면도, 종단면도, A-A, B-B SVG 이미지 5개와 픽셀-CAD 좌표 연결값을 만든다. 뷰어에서 현재 보기와 도면 종류를 연결하고 투명도를 조절할 수 있다.
- A-A/B-B 이미지는 각 끝 측점의 단면 평면에 놓고, 종단면 이미지는 STA 주석과 EL 주석으로 길이·높이를 변환해 구조물 측면에 놓는다. 평면도는 DXF 좌표를 모델 좌표에 대응시키며, 상부평면도 표기는 도면 간 35m 배치 차이를 이동해 단일 모델 위에 포개고 화면 표시용 EL 108.8m에 둔다.
- 이들은 도면 요소를 3D 화면의 기준 평면에 투영한 검토용 이미지다. 평면의 표시 높이와 종단면의 측면 이격은 보이도록 둔 시각화 위치이며 실제 부재나 설계 고도를 뜻하지 않는다. DXF 글꼴 및 일부 블록 표현은 SVG에서 단순화될 수 있다.
- 실행: `npm run dev`. 분석 파일과 작업 로그는 화면 상단 링크에서 연다.
## 검증 결과
+7 -6
View File
@@ -6,19 +6,20 @@
<meta name="theme-color" content="#eff2ed">
<meta name="description" content="도면의 중심선과 구조 단면을 연결한 광터지하차도 BLOCK8 3D 검토 모델입니다.">
<title>광터지하차도 BLOCK8 3D 모델</title>
<style>:root{color:#263a32;font-synthesis:none;text-rendering:optimizelegibility;background:#eff2ed;font-family:Malgun Gothic,Apple SD Gothic Neo,sans-serif}*{box-sizing:border-box}body{min-width:320px;min-height:100vh;margin:0}button,input{font:inherit}button{color:inherit;cursor:pointer;background:0 0;border:0}a{color:inherit;text-decoration:none}button:focus-visible,a:focus-visible,input:focus-visible{outline-offset:3px;outline:2px solid #bc763b}.page-shell{width:min(100% - 64px,1600px);margin:auto;padding-bottom:24px}.topbar{border-bottom:1px solid #d9e1d7;align-items:center;gap:18px;min-height:72px;display:flex}.brand{white-space:nowrap;align-items:center;gap:10px;font-size:14px;font-weight:700;display:flex}.brand-mark{color:#fff;background:#446b58;border-radius:10px;place-items:center;width:34px;height:34px;font-size:11px;display:grid}.project-chip{color:#7a897e;margin-right:auto;font-size:11px}.file-links{gap:7px;display:flex}.file-links a{color:#536d5b;white-space:nowrap;background:#fafcf8;border:1px solid #d3ddd2;border-radius:7px;padding:9px 11px;font-size:10px}.file-links a:hover,.source-links a:hover{color:#315c42;border-color:#92a994}.intro{justify-content:space-between;align-items:center;gap:18px;padding:29px 0 20px;display:flex}.kicker,.eyebrow{color:#7a8d7e;letter-spacing:.08em;font-size:10px;font-weight:700;display:block}h1{letter-spacing:-.055em;margin:8px 0;font-size:clamp(26px,3.3vw,34px)}h1 em{color:#738b79;margin-left:7px;font-size:.8em;font-style:normal;font-weight:400}.intro p{color:#7c8d80;margin:0;font-size:11px;line-height:1.7}.status-pill{color:#9b793c;white-space:nowrap;background:#f8f4e9;border:1px solid #e0d6bd;border-radius:30px;align-items:center;gap:8px;padding:9px 13px;font-size:10px;display:flex}.status-pill i{background:#b08a45;border-radius:50%;width:7px;height:7px}.metrics{background:#f9fbf7;border:1px solid #dce4d9;border-radius:11px;grid-template-columns:repeat(4,1fr);margin-bottom:13px;padding:17px 0;display:grid}.metrics article{border-right:1px solid #e3e8df;min-width:0;padding:0 18px}.metrics article:last-child{border:0}.metrics small{color:#7d8b7d;font-size:9px;display:block}.metrics strong{white-space:nowrap;margin:8px 0 5px;font-family:Georgia,serif;font-size:clamp(19px,2.3vw,26px);font-weight:400;display:block}.metrics strong.el{font-size:clamp(17px,2vw,22px)}.metrics strong i{color:#899584;font-family:sans-serif;font-size:11px;font-style:normal}.metrics p{color:#8b9687;margin:0;font-size:9px;line-height:1.55}.notice{background:#f5f0e4;border:1px solid #e6dcc7;border-radius:8px;align-items:flex-start;gap:11px;margin-bottom:15px;padding:10px 13px;display:flex}.notice b{color:#967136;white-space:nowrap;padding-top:2px;font-size:10px}.notice p{color:#8b7e62;margin:0;font-size:10px;line-height:1.65}.workspace{grid-template-columns:minmax(0,1fr) 322px;align-items:start;gap:16px;display:grid}.viewer-card,.side-card,.info-card{background:#fafcf8;border:1px solid #dbe4d7;border-radius:11px;min-width:0;overflow:hidden}.viewer-head{justify-content:space-between;align-items:center;gap:12px;padding:17px 19px 13px;display:flex}h2{letter-spacing:-.04em;margin:5px 0 0;font-size:16px}.segmented{background:#eaf0e7;border-radius:7px;padding:3px;display:flex}.segmented button{color:#7f8d7b;white-space:nowrap;border-radius:5px;padding:6px 9px;font-size:9px}.segmented button.active{color:#496c52;background:#fff;box-shadow:0 1px 4px #233c2912}.viewbar{justify-content:space-between;align-items:center;gap:10px;padding:0 17px 10px;display:flex}.views{gap:3px;display:flex}.views button{border:1px solid #0000;border-radius:5px;padding:6px 9px;font-size:9px}.views button.active{color:#426a50;background:#eef3ea;border-color:#ccdac9}.viewbar>span{color:#8b9687;white-space:nowrap;font-size:9px}.canvas-wrap{background:#f4f7f3;border-top:1px solid #dfe8da;border-bottom:1px solid #dfe8da;position:relative;overflow:hidden}.model-canvas{touch-action:none;cursor:grab;width:100%;height:485px;display:block}.model-canvas:active{cursor:grabbing}.model-canvas:focus-visible{outline-offset:-2px;outline:2px solid #7d9a83}.canvas-tag{color:#6c846f;pointer-events:none;background:#fcfffbd9;border:1px solid #d5e0d2;border-radius:5px;max-width:calc(100% - 28px);padding:6px 8px;font-size:8px;position:absolute;top:12px;left:14px}.render-mode .model-canvas{filter:saturate(.92)contrast(1.04)}.viewer-foot{color:#8b9885;justify-content:space-between;align-items:center;gap:10px;padding:11px 17px;font-size:8px;display:flex}.viewer-foot button{color:#56735b;white-space:nowrap;font-size:9px}.selection{border-top:1px solid #e2e9dd;min-height:95px;margin:0 18px 17px;padding:14px 0 0}.selection:has(h3){display:block}.selection:not(:has(h3)){align-items:center;gap:11px;display:flex}.selection-dot{background:#91a78f;border-radius:50%;flex:none;width:9px;height:9px}.selection strong{font-size:10px}.selection p{color:#819084;margin:5px 0 0;font-size:9px;line-height:1.6}.selection-title{justify-content:space-between;align-Line truncated
<style>:root{color:#263a32;font-synthesis:none;text-rendering:optimizelegibility;background:#eff2ed;font-family:Malgun Gothic,Apple SD Gothic Neo,sans-serif}*{box-sizing:border-box}body{min-width:320px;min-height:100vh;margin:0}button,input{font:inherit}button{color:inherit;cursor:pointer;background:0 0;border:0}a{color:inherit;text-decoration:none}button:focus-visible,a:focus-visible,input:focus-visible{outline-offset:3px;outline:2px solid #bc763b}.page-shell{width:min(100% - 64px,1600px);margin:auto;padding-bottom:24px}.topbar{border-bottom:1px solid #d9e1d7;align-items:center;gap:18px;min-height:72px;display:flex}.brand{white-space:nowrap;align-items:center;gap:10px;font-size:14px;font-weight:700;display:flex}.brand-mark{color:#fff;background:#446b58;border-radius:10px;place-items:center;width:34px;height:34px;font-size:11px;display:grid}.project-chip{color:#7a897e;margin-right:auto;font-size:11px}.file-links{gap:7px;display:flex}.file-links a{color:#536d5b;white-space:nowrap;background:#fafcf8;border:1px solid #d3ddd2;border-radius:7px;padding:9px 11px;font-size:10px}.file-links a:hover,.source-links a:hover{color:#315c42;border-color:#92a994}.intro{justify-content:space-between;align-items:center;gap:18px;padding:29px 0 20px;display:flex}.kicker,.eyebrow{color:#7a8d7e;letter-spacing:.08em;font-size:10px;font-weight:700;display:block}h1{letter-spacing:-.055em;margin:8px 0;font-size:clamp(26px,3.3vw,34px)}h1 em{color:#738b79;margin-left:7px;font-size:.8em;font-style:normal;font-weight:400}.intro p{color:#7c8d80;margin:0;font-size:11px;line-height:1.7}.status-pill{color:#9b793c;white-space:nowrap;background:#f8f4e9;border:1px solid #e0d6bd;border-radius:30px;align-items:center;gap:8px;padding:9px 13px;font-size:10px;display:flex}.status-pill i{background:#b08a45;border-radius:50%;width:7px;height:7px}.metrics{background:#f9fbf7;border:1px solid #dce4d9;border-radius:11px;grid-template-columns:repeat(4,1fr);margin-bottom:13px;padding:17px 0;display:grid}.metrics article{border-right:1px solid #e3e8df;min-width:0;padding:0 18px}.metrics article:last-child{border:0}.metrics small{color:#7d8b7d;font-size:9px;display:block}.metrics strong{white-space:nowrap;margin:8px 0 5px;font-family:Georgia,serif;font-size:clamp(19px,2.3vw,26px);font-weight:400;display:block}.metrics strong.el{font-size:clamp(17px,2vw,22px)}.metrics strong i{color:#899584;font-family:sans-serif;font-size:11px;font-style:normal}.metrics p{color:#8b9687;margin:0;font-size:9px;line-height:1.55}.notice{background:#f5f0e4;border:1px solid #e6dcc7;border-radius:8px;align-items:flex-start;gap:11px;margin-bottom:15px;padding:10px 13px;display:flex}.notice b{color:#967136;white-space:nowrap;padding-top:2px;font-size:10px}.notice p{color:#8b7e62;margin:0;font-size:10px;line-height:1.65}.workspace{grid-template-columns:minmax(0,1fr) 322px;align-items:start;gap:16px;display:grid}.viewer-card,.side-card,.info-card{background:#fafcf8;border:1px solid #dbe4d7;border-radius:11px;min-width:0;overflow:hidden}.viewer-head{justify-content:space-between;align-items:center;gap:12px;padding:17px 19px 13px;display:flex}.viewer-actions{align-items:center;gap:8px;display:flex}.drawing-toggle{color:#5c765f;white-space:nowrap;border:1px solid #d4dfd2;border-radius:6px;padding:7px 10px;font-size:9px}.drawing-toggle.active{color:#385f42;background:#e9f0e7;border-color:#8aa48c}h2{letter-spacing:-.04em;margin:5px 0 0;font-size:16px}.segmented{background:#eaf0e7;border-radius:7px;padding:3px;display:flex}.segmented button{color:#7f8d7b;white-space:nowrap;border-radius:5px;padding:6px 9px;font-size:9px}.segmented button.active{color:#496c52;background:#fff;box-shadow:0 1px 4px #233c2912}.viewbar{justify-content:space-between;align-items:center;gap:10px;padding:0 17px 10px;display:flex}.views{gap:3px;display:flex}.views button{border:1px solid #0000;border-radius:5px;padding:6px 9px;font-size:9px}.views button.active{color:#426a50;background:#eef3ea;border-color:#ccdac9}.viewbar>span{color:#8b9687;white-space:nowrap;font-size:9px}.canvas-wrap{background:#f4f7f3;border-top:1px solid #dfe8da;border-bottom:1px solid #dfe8da;position:relative;overflow:hidden}.drawing-controls{background:#f7faf5;border-bottom:1px solid #e2e9dd;align-items:center;gap:14px;padding:9px 15px;display:flex}.drawing-controls label{color:#647568;white-space:nowrap;align-items:center;gap:7px;font-size:9px;display:flex}.drawing-controls select{color:#526657;background:#fff;border:1px solid #d6e0d4;border-radius:5px;max-width:180px;padding:5px 7px;font-size:9px}.drawing-controls input{accent-color:#59785e;width:95px}.drawing-controls>span{color:#879287;font-size:8px;line-height:1.5}.model-canvas{touch-action:none;cursor:grab;width:100%;height:485px;display:block}.model-canvas:active{cursor:grabbing}.model-canvas:focus-visible{outline-offset:-2px;outline:2px solid #7d9a83}.canvas-tag{color:#6c846f;pointer-events:none;background:#fcfffbd9;border:1px solid #d5e0d2;border-radius:5px;max-width:calc(100% - 28pLine truncated
</style>
</head>
<body>
<div id="root"></div>
<script>var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t,t.exports),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.for(`react.view_transition`),m=Symbol.iterator;function h(e){return typeof e!=`object`||!e?null:(e=m&&e[m]||e[`@@iterator`],typeof e==`function`?e:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,v={};function y(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}y.prototype.isReactComponent={},y.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},y.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function b(){}b.prototype=y.prototype;function x(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}var ee=x.prototype=new b;ee.constructor=x,_(ee,y.prototype),ee.isPureReactComponent=!0;var te=Array.isArray;function ne(){}var S={H:null,A:null,T:null,S:null},re=Object.prototype.hasOwnProperty;function C(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ie(e,t){return C(e.type,t,e.props)}function ae(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function oe(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var se=/\/+/g;function ce(e,t){return typeof e==`object`&&e&&e.key!=null?oe(``+e.key):t.toString(36)}function le(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(ne,ne):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function ue(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,ue(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ce(e,0):a,te(o)?(i=``,c!=null&&(i=c.replace(se,`$&/`)+`/`),ue(o,r,i,``,function(e){return e})):o!=null&&(ae(o)&&(o=ie(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(se,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(te(e))for(var u=0;u<e.length;u++)a=e[u],s=l+ce(a,u),c+=ue(a,r,i,s,o);else if(u=h(e),typeof u==`function`)for(e=u.call(e),u=0;!(a=e.next()).done;)a=a.value,s=l+ce(a,u++),c+=ue(a,r,i,s,o);else if(s===`object`){if(typeof e.then==`function`)return ue(le(e),r,i,a,o);throw r=String(e),Error(`Objects are not valid as a React child (found: `+(r===`[object Object]`?`object with keys {`+Object.keys(e).join(`, `)+`}`:r)+`). If you meant to render a collection of children, use an array instead.`)}return c}function de(e,t,n){if(e==null)return e;var r=[],i=0;return ue(e,r,``,``,function(e){return t.call(n,e,i++)}),r}function fe(e){if(e._status===-1){var t=e._result,n=t();n.then(function(t){(e._status===0||e._status===-1)&&(e._status=1,e._result=t,n.status===void 0&&(n.status=`fulfilled`,n.value=t))},function(t){(e._status===0||e._status===-1)&&(e._status=2,e._result=t,n.status===void 0&&(n.status=`rejected`,n.reason=t))}),e._statuLine truncated
<script>window.BLOCK8_DRAWING_IMAGES={"plan": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDAwIiBoZWlnaHQ9IjExNjAiIHZpZXdCb3g9IjAgMCAxNDAwIDExNjAiIHJvbGU9ImltZyIgYXJpYS1sYWJlbD0i7Y+J66m064+EIOybkOuzuCBEWEYg7JiB7JetIj4KPHRpdGxlPu2PieuptOuPhCDCtyDsm5Drs7ggRFhGIOuyoe2EsCDsuqHsspg8L3RpdGxlPgo8Zz48cGF0aCBkPSJNMTE1OC41NSwzMDIuOTYgTDExNjMuMzMsMTExNC45NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDM4YjllIiBzdHJva2Utd2lkdGg9IjEuMjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMzY0LjEzLDMwMi45NiBMMzU5LjM1LDExMTQuOTUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQzOGI5ZSIgc3Ryb2tlLXdpZHRoPSIxLjI1IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTQ2MS4wOCw3NjkuNDcgTDQ2MC42OSw4NTcuOTEiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNDYwLjY5LDg1Ny45MSBMNDYwLjY1LDg2NS45MSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDI1MjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik00NjEuNjEsNjQ5LjQ3IEw0NjIuMDAsNTYxLjAzIiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTQ2Mi4wMCw1NjEuMDMgTDQ2Mi4wMyw1NTMuMDMiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTE5OS4xNiwzMzguNzIgTDEyNDEuMTYsMzM4LjQ4IiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTExOTguOTUsMzAyLjcyIEwxMjQwLjk1LDMwMi40OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDI1MjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xMjM5LjE2LDMzOC40OSBMMTIzOC45NSwzMDIuNDkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTIwMS4yMiw2ODguNzIgTDEyNDMuMjIsNjg4LjQ3IiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTExOTkuMTYsMzM4LjcyIEwxMjQxLjE2LDMzOC40OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDI1MjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xMjQxLjIyLDY4OC40OCBMMTIzOS4xNiwzMzguNDkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTIwMS40Niw3MjguNzIgTDEyNDMuNDUsNzI4LjQ3IiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTEyMDEuMjIsNjg4LjcyIEwxMjQzLjIyLDY4OC40NyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDI1MjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xMjQxLjQ1LDcyOC40OCBMMTI0MS4yMiw2ODguNDgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTIwMy41MSwxMDc4LjcxIEwxMjQ1LjUxLDEwNzguNDYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTIwMS40Niw3MjguNzIgTDEyNDMuNDUsNzI4LjQ3IiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTEyNDMuNTEsMTA3OC40OCBMMTI0MS40NSw3MjguNDgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTIwMy43MywxMTE0LjcxIEwxMjQ1LjczLDExMTQuNDYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTIwMy41MSwxMDc4LjcxIEwxMjQ1LjUxLDEwNzguNDYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTI0My43MywxMTE0LjQ4IEwxMjQzLjUxLDEwNzguNDgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMzIzLjUyLDMzOC43MiBMMjgxLjUyLDMzOC40OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDI1MjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zMjMuNzMsMzAyLjcyIEwyODEuNzMsMzAyLjQ4IiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI4My41MiwzMzguNDkgTDI4My43MywzMDIuNDkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMzIxLjQ2LDY4OC43MiBMMjc5LjQ2LDY4OC40NyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDI1MjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zMjMuNTIsMzM4LjcyIEwyODEuNTIsMzM4LjQ4IiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI4MS40Niw2ODguNDggTDI4My41MiwzMzguNDkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMzIxLjIzLDcyOC43MiBMMjc5LjIzLDcyOC40NyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDI1MjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zMjEuNDYsNjg4LjcyIEwyNzkuNDYsNjg4LjQ3IiBmaWxsPSJub25lIiBzdHJva2U9IiNkMjUyNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI4MS4yMyw3MjguNDggTDI4MS40Niw2ODguNDgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QyNTI0NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMzE5LjE3LDEwNzguNzEgTDI3Ny4xNywxMDc4LjQ2IiBmaWxsPSJub25lIiBzdHJvLine truncated
<script>var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t,t.exports),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.for(`react.view_transition`),m=Symbol.iterator;function h(e){return typeof e!=`object`||!e?null:(e=m&&e[m]||e[`@@iterator`],typeof e==`function`?e:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,v={};function y(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}y.prototype.isReactComponent={},y.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},y.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function b(){}b.prototype=y.prototype;function ee(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}var x=ee.prototype=new b;x.constructor=ee,_(x,y.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},te=Object.prototype.hasOwnProperty;function T(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ne(e,t){return T(e.type,t,e.props)}function re(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function ie(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ae=/\/+/g;function oe(e,t){return typeof e==`object`&&e&&e.key!=null?ie(``+e.key):t.toString(36)}function se(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function ce(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,ce(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+oe(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(ae,`$&/`)+`/`),ce(o,r,i,``,function(e){return e})):o!=null&&(re(o)&&(o=ne(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ae,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u<e.length;u++)a=e[u],s=l+oe(a,u),c+=ce(a,r,i,s,o);else if(u=h(e),typeof u==`function`)for(e=u.call(e),u=0;!(a=e.next()).done;)a=a.value,s=l+oe(a,u++),c+=ce(a,r,i,s,o);else if(s===`object`){if(typeof e.then==`function`)return ce(se(e),r,i,a,o);throw r=String(e),Error(`Objects are not valid as a React child (found: `+(r===`[object Object]`?`object with keys {`+Object.keys(e).join(`, `)+`}`:r)+`). If you meant to render a collection of children, use an array instead.`)}return c}function le(e,t,n){if(e==null)return e;var r=[],i=0;return ce(e,r,``,``,function(e){return t.call(n,e,i++)}),r}function ue(e){if(e._status===-1){var t=e._result,n=t();n.then(function(t){(e._status===0||e._status===-1)&&(e._status=1,e._result=t,n.status===void 0&&(n.status=`fulfilled`,n.value=t))},function(t){(e._status===0||e._status===-1)&&(e._status=2,e._result=t,n.status===void 0&&(n.status=`rejected`,n.reason=t))}),e._status===-1&Line truncated
at`)?` (<anonymous>)`:-1<e.stack.indexOf(`@`)?`@unknown:0:0`:``}return`
`+Be+e+Ve}var Ue=!1;function We(e,t){if(!e||Ue)return``;Ue=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var n=function(){throw Error()};if(Object.defineProperty(n.prototype,"props",{set:function(){throw Error()}}),typeof Reflect==`object`&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}n=!1;try{var i=Object.getOwnPropertyDescriptor(e.prototype,`props`);Object.defineProperty(e.prototype,"props",{configurable:!0,set:function(){throw Error()}}),n=!0,new e}finally{n&&(i===void 0?delete e.prototype.props:Object.defineProperty(e.prototype,"props",i))}}}else{try{throw Error()}catch(e){r=e}(n=e())&&typeof n.catch==`function`&&n.catch(function(){})}}catch(e){if(e&&r&&typeof e.stack==`string`)return[e.stack,r.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName=`DetermineComponentFrameRoot`;var i=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,`name`);i&&i.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,"name",{value:`DetermineComponentFrameRoot`});var a=r.DetermineComponentFrameRoot(),o=a[0],s=a[1];if(o&&s){var c=o.split(`
`+Re+e+ze}var Ve=!1;function He(e,t){if(!e||Ve)return``;Ve=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var n=function(){throw Error()};if(Object.defineProperty(n.prototype,"props",{set:function(){throw Error()}}),typeof Reflect==`object`&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}n=!1;try{var i=Object.getOwnPropertyDescriptor(e.prototype,`props`);Object.defineProperty(e.prototype,"props",{configurable:!0,set:function(){throw Error()}}),n=!0,new e}finally{n&&(i===void 0?delete e.prototype.props:Object.defineProperty(e.prototype,"props",i))}}}else{try{throw Error()}catch(e){r=e}(n=e())&&typeof n.catch==`function`&&n.catch(function(){})}}catch(e){if(e&&r&&typeof e.stack==`string`)return[e.stack,r.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName=`DetermineComponentFrameRoot`;var i=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,`name`);i&&i.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,"name",{value:`DetermineComponentFrameRoot`});var a=r.DetermineComponentFrameRoot(),o=a[0],s=a[1];if(o&&s){var c=o.split(`
`),l=s.split(`
`);for(i=r=0;r<c.length&&!c[r].includes(`DetermineComponentFrameRoot`);)r++;for(;i<l.length&&!l[i].includes(`DetermineComponentFrameRoot`);)i++;if(r===c.length||i===l.length)for(r=c.length-1,i=l.length-1;1<=r&&0<=i&&c[r]!==l[i];)i--;for(;1<=r&&0<=i;r--,i--)if(c[r]!==l[i]){if(r!==1||i!==1)do if(r--,i--,0>i||c[r]!==l[i]){var u=`
`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Ue=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?He(n):``}function Ge(e,t){switch(e.tag){case 26:case 27:case 5:return He(e.type);case 16:return He(`Lazy`);case 13:return e.child!==t&&t!==null?He(`Suspense Fallback`):He(`Suspense`);case 19:return He(`SuspenseList`);case 0:case 15:return We(e.type,!1);case 11:return We(e.type.render,!1);case 1:return We(e.type,!0);case 31:return He(`Activity`);case 30:return He(`ViewTransition`);default:return``}}function Ke(e){try{var t=``,n=null;do t+=Ge(e,n),n=e,e=e.return;while(e);return t}catch(e){return`
`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Ve=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?Be(n):``}function Ue(e,t){switch(e.tag){case 26:case 27:case 5:return Be(e.type);case 16:return Be(`Lazy`);case 13:return e.child!==t&&t!==null?Be(`Suspense Fallback`):Be(`Suspense`);case 19:return Be(`SuspenseList`);case 0:case 15:return He(e.type,!1);case 11:return He(e.type.render,!1);case 1:return He(e.type,!0);case 31:return Be(`Activity`);case 30:return Be(`ViewTransition`);default:return``}}function We(e){try{var t=``,n=null;do t+=Ue(e,n),n=e,e=e.return;while(e);return t}catch(e){return`
Error generating stack: `+e.message+`
`+e.stack}}var qe=Object.prototype.hasOwnProperty,Je=t.unstable_scheduleCallback,Ye=t.unstable_cancelCallback,Xe=t.unstable_shouldYield,Ze=t.unstable_requestPaint,Qe=t.unstable_now,$e=t.unstable_getCurrentPriorityLevel,et=t.unstable_ImmediatePriority,tt=t.unstable_UserBlockingPriority,nt=t.unstable_NormalPriority,rt=t.unstable_LowPriority,it=t.unstable_IdlePriority,at=t.log,ot=t.unstable_setDisableYieldValue,st=null,ct=null;function lt(e){if(typeof at==`function`&&ot(e),ct&&typeof ct.setStrictMode==`function`)try{ct.setStrictMode(st,e)}catch{}}var ut=Math.clz32?Math.clz32:pt,dt=Math.log,ft=Math.LN2;function pt(e){return e>>>=0,e===0?32:31-(dt(e)/ft|0)|0}var mt=256,ht=262144,gt=4194304;function _t(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&-e;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function vt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=_t(n))):i=_t(o):i=_t(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=_t(n))):i=_t(o)):i=_t(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function yt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function bt(e,t){t&8&&(t|=t&32);var n=e.entangledLanes;if(n!==0)for(e=e.entanglements,n&=t;0<n;){var r=31-ut(n),i=1<<r;t|=e[r],n&=~i}return t}function xt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function St(){var e=gt;return gt<<=1,!(gt&62914560)&&(gt=4194304),e}function Ct(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function wt(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Tt(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0<n;){var u=31-ut(n),d=1<<u;s[u]=0,c[u]=-1;var f=l[u];if(f!==null)for(l[u]=null,u=0;u<f.length;u++){var p=f[u];p!==null&&(p.lane&=-536870913)}n&=~d}r!==0&&Et(e,r,0),a!==0&&i===0&&e.tag!==0&&(e.suspendedLanes|=a&~(o&~t))}function Et(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-ut(t);e.entangledLanes|=t,e.entanglements[r]=e.entanglements[r]|1073741824|n&261930}function Dt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-ut(n),i=1<<r;i&t|e[r]&t&&(e[r]|=t),n&=~i}}function Ot(e,t){var n=t&-t;return n=n&42?1:kt(n),(n&(e.suspendedLanes|t))===0?n:0}function kt(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function At(e){return e&=-e,2<e?8<e?e&134217727?32:268435456:8:2}function jt(){var e=T.p;return e===0?(e=window.event,e===void 0?32:Ch(e.type)):e}function Mt(e,t){var n=T.p;try{return T.p=e,t()}finally{T.p=n}}var Nt=Math.random().toString(36).slice(2),Pt=`__reactFiber$`+Nt,Ft=`__reactProps$`+Nt,It=`__reactContainer$`+Nt,Lt=`__reactEvents$`+Nt,Rt=`__reactListeners$`+Nt,zt=`__reactHandles$`+Nt,Bt=`__reactResources$`+Nt,Vt=`__reactMarker$`+Nt,Ht=`__reactLoad$`+Nt;function Ut(e){delete e[Pt],delete e[Ft],delete e[Rt],delete e[zt]}function Wt(e){var t;if(t=e[Pt])return t;for(var n=e.parentNode;n;){if(t=n[It]||n[Pt]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=fm(e);e!==null;){if(n=e[Pt])return n;e=fm(e)}return t}e=n,n=e.parentNode}return null}function Gt(e){if(e=e[Pt]||e[It]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function Kt(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(i(33))}function qt(e){var t=e[Bt];return t||=e[Bt]={hoistableStyles:new Map,hoistableScripts:new Map},t}function D(e){e[Vt]=!0}function Jt(e){e[Ht]=void 0}var Yt=new Set,Xt={};function Zt(e,t){Qt(e,t),Qt(e+`Capture`,t)}function Qt(e,t){for(Xt[e]=t,e=0;e<t.lenLine truncated
`).replace(Qf,``)}function ep(e,t){return t=$f(t),$f(e)===t}function $(e,t,n,r,a,o){switch(n){case`children`:if(typeof r==`string`)t===`body`||t===`textarea`&&r===``||xn(e,r);else if(typeof r==`number`||typeof r==`bigint`)t!==`body`&&xn(e,``+r);else return;break;case`className`:on(e,`class`,r);break;case`tabIndex`:on(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:on(e,n,r);break;case`style`:wn(e,r,o);return;case`data`:if(t!==`object`){on(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)||r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=On(r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}if(typeof o==`function`&&(n===`formAction`?(t!==`input`&&$(e,t,`name`,a.name,a,null),$(e,t,`formEncType`,a.formEncType,a,null),$(e,t,`formMethod`,a.formMethod,a,null),$(e,t,`formTarget`,a.formTarget,a,null)):($(e,t,`encType`,a.encType,a,null),$(e,t,`method`,a.method,a,null),$(e,t,`target`,a.target,a,null))),r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=On(r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=kn);return;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=On(r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`credentialless`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:Q(`beforetoggle`,e),Q(`toggle`,e),an(e,`popover`,r);break;case`xlinkActuate`:sn(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:sn(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:sn(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:sn(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:sn(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:sn(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:sn(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:sn(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:sn(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:an(e,`is`,r);break;case`innerText`:case`textContent`:return;default:if(!(2<n.length)||n[0]!==`o`&&n[0]!==`O`||n[1]!==`n`&&n[1]!==`N`)n=En.get(n)||n,an(e,n,r);else return}O=!0}function tp(e,t,n,r,a,o){switch(n){case`style`:wn(e,r,o);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`children`:if(typeof r==`string`)xn(e,r);else if(typeof r==`number`||typeof r==`bigint`)xn(e,``+r);else return;break;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`onClick`:r!=null&&(e.onclick=kn);return;case`suppressContentLine truncated
`+e.stack}}var Ge=Object.prototype.hasOwnProperty,Ke=t.unstable_scheduleCallback,qe=t.unstable_cancelCallback,Je=t.unstable_shouldYield,Ye=t.unstable_requestPaint,Xe=t.unstable_now,Ze=t.unstable_getCurrentPriorityLevel,Qe=t.unstable_ImmediatePriority,$e=t.unstable_UserBlockingPriority,et=t.unstable_NormalPriority,tt=t.unstable_LowPriority,nt=t.unstable_IdlePriority,rt=t.log,it=t.unstable_setDisableYieldValue,at=null,ot=null;function st(e){if(typeof rt==`function`&&it(e),ot&&typeof ot.setStrictMode==`function`)try{ot.setStrictMode(at,e)}catch{}}var ct=Math.clz32?Math.clz32:dt,lt=Math.log,ut=Math.LN2;function dt(e){return e>>>=0,e===0?32:31-(lt(e)/ut|0)|0}var ft=256,pt=262144,mt=4194304;function ht(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&-e;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function gt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=ht(n))):i=ht(o):i=ht(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=ht(n))):i=ht(o)):i=ht(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function _t(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function vt(e,t){t&8&&(t|=t&32);var n=e.entangledLanes;if(n!==0)for(e=e.entanglements,n&=t;0<n;){var r=31-ct(n),i=1<<r;t|=e[r],n&=~i}return t}function yt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function bt(){var e=mt;return mt<<=1,!(mt&62914560)&&(mt=4194304),e}function xt(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function St(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ct(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0<n;){var u=31-ct(n),d=1<<u;s[u]=0,c[u]=-1;var f=l[u];if(f!==null)for(l[u]=null,u=0;u<f.length;u++){var p=f[u];p!==null&&(p.lane&=-536870913)}n&=~d}r!==0&&wt(e,r,0),a!==0&&i===0&&e.tag!==0&&(e.suspendedLanes|=a&~(o&~t))}function wt(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-ct(t);e.entangledLanes|=t,e.entanglements[r]=e.entanglements[r]|1073741824|n&261930}function Tt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-ct(n),i=1<<r;i&t|e[r]&t&&(e[r]|=t),n&=~i}}function Et(e,t){var n=t&-t;return n=n&42?1:Dt(n),(n&(e.suspendedLanes|t))===0?n:0}function Dt(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function Ot(e){return e&=-e,2<e?8<e?e&134217727?32:268435456:8:2}function kt(){var e=D.p;return e===0?(e=window.event,e===void 0?32:Ch(e.type)):e}function At(e,t){var n=D.p;try{return D.p=e,t()}finally{D.p=n}}var jt=Math.random().toString(36).slice(2),Mt=`__reactFiber$`+jt,Nt=`__reactProps$`+jt,Pt=`__reactContainer$`+jt,Ft=`__reactEvents$`+jt,It=`__reactListeners$`+jt,Lt=`__reactHandles$`+jt,Rt=`__reactResources$`+jt,zt=`__reactMarker$`+jt,Bt=`__reactLoad$`+jt;function Vt(e){delete e[Mt],delete e[Nt],delete e[It],delete e[Lt]}function Ht(e){var t;if(t=e[Mt])return t;for(var n=e.parentNode;n;){if(t=n[Pt]||n[Mt]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=fm(e);e!==null;){if(n=e[Mt])return n;e=fm(e)}return t}e=n,n=e.parentNode}return null}function Ut(e){if(e=e[Mt]||e[Pt]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function Wt(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(i(33))}function Gt(e){var t=e[Rt];return t||=e[Rt]={hoistableStyles:new Map,hoistableScripts:new Map},t}function Kt(e){e[zt]=!0}function qt(e){e[Bt]=void 0}var Jt=new Set,Yt={};function Xt(e,t){Zt(e,t),Zt(e+`Capture`,t)}function Zt(e,t){for(Yt[e]=t,e=0;e<t.leLine truncated
`).replace(Qf,``)}function ep(e,t){return t=$f(t),$f(e)===t}function $(e,t,n,r,a,o){switch(n){case`children`:if(typeof r==`string`)t===`body`||t===`textarea`&&r===``||bn(e,r);else if(typeof r==`number`||typeof r==`bigint`)t!==`body`&&bn(e,``+r);else return;break;case`className`:an(e,`class`,r);break;case`tabIndex`:an(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:an(e,n,r);break;case`style`:Cn(e,r,o);return;case`data`:if(t!==`object`){an(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)||r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=Dn(r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}if(typeof o==`function`&&(n===`formAction`?(t!==`input`&&$(e,t,`name`,a.name,a,null),$(e,t,`formEncType`,a.formEncType,a,null),$(e,t,`formMethod`,a.formMethod,a,null),$(e,t,`formTarget`,a.formTarget,a,null)):($(e,t,`encType`,a.encType,a,null),$(e,t,`method`,a.method,a,null),$(e,t,`target`,a.target,a,null))),r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=Dn(r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=On);return;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=Dn(r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`credentialless`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:Q(`beforetoggle`,e),Q(`toggle`,e),rn(e,`popover`,r);break;case`xlinkActuate`:on(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:on(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:on(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:on(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:on(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:on(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:on(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:on(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:on(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:rn(e,`is`,r);break;case`innerText`:case`textContent`:return;default:if(!(2<n.length)||n[0]!==`o`&&n[0]!==`O`||n[1]!==`n`&&n[1]!==`N`)n=Tn.get(n)||n,rn(e,n,r);else return}k=!0}function tp(e,t,n,r,a,o){switch(n){case`style`:Cn(e,r,o);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`children`:if(typeof r==`string`)bn(e,r);else if(typeof r==`number`||typeof r==`bigint`)bn(e,``+r);else return;break;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`onClick`:r!=null&&(e.onclick=On);return;case`suppressContentLine truncated
</body>
</html>
Binary file not shown.
+12
View File
@@ -34,10 +34,12 @@ DXF에는 단위가 지정되어 있지 않습니다 (`$INSUNITS=0`). 단면 좌
- `public/drawings/광터지하차도_BLOCK8.dxf` — 등록된 원본 DXF
- `docs/도면분석.md` 및 `public/analysis/도면분석.md` — 도면 판독 및 값 근거
- `public/drawing-images/` — DXF에서 추출한 평면·상부평면·종단·A-A/B-B 벡터 이미지
- `public/작업기록.txt` — 초기화 이후 작업 이력
- `src/model/block8.json` — 웹 뷰어가 읽는 모델 데이터
- `scripts/extract_block8_model.py` — DXF에서 모델 데이터를 다시 추출
- `scripts/render_block8_preview.py` — SVG 미리보기 생성
- `scripts/render_block8_drawing_views.py` — 원본 DXF 도면 영역 이미지 생성
- `docs/BLOCK8_3D모델_미리보기.svg` — 모델 이미지
- `src/App.jsx`, `src/ModelViewer.jsx` — 웹 화면과 조작 기능
@@ -59,6 +61,8 @@ npm run build
npm run lint
python3 scripts/extract_block8_model.py
python3 scripts/render_block8_preview.py
python3 scripts/render_block8_drawing_views.py
python3 scripts/build_standalone_html.py
```
## 6. 검증 기록
@@ -76,3 +80,11 @@ python3 scripts/render_block8_preview.py
- `e473a7c` 평면도와 상부평면도 모델을 하나로 통합
원격 저장소에는 푸시하지 않은 로컬 커밋 기록입니다.
## 8. 도면 이미지 함께 보기
- `public/drawing-images/`에는 DXF에서 잘라낸 평면도, 상부평면도, 종단면도, A-A, B-B SVG 이미지가 있습니다.
- 웹 화면의 `도면 함께 보기`를 켜면 현재 뷰와 연결된 도면을 모델 화면 위에 참고 오버레이로 표시합니다. 도면 종류를 바꾸거나 투명도를 조절할 수 있습니다.
- 오버레이는 DXF 픽셀-CAD 좌표 변환으로 3D 기준면에 투영한 검토 이미지입니다. A-A/B-B는 끝 측점 단면에, 종단은 STA/EL 주석에 맞춰 모델 옆에 놓습니다. 평면도와 상부평면도는 같은 구조물이 되도록 도면 배치 차이를 보정하며, 평면 기준면의 EL과 종단의 측면 이격은 보기 위한 임시 위치입니다. 도면 글꼴 및 블록 표현은 SVG에서 단순화될 수 있으므로 최종 치수 검증용으로 사용하지 않습니다.
- DXF에서 벡터를 추출하는 명령: `python3 scripts/render_block8_drawing_views.py`
- 단독 HTML 갱신 순서: `npm run build` 후 `python3 scripts/build_standalone_html.py`
+3
View File
@@ -65,6 +65,9 @@
- 화면: `src/ModelViewer.jsx`와 `src/App.jsx`. 끌어서 회전, 휠 확대/축소, Shift/오른쪽 끌기로 이동, 전체·평면·정면·측면·A-A·B-B 시점을 제공한다.
- 객체는 단일 중심선 모델에 구조선 114개와 삽입 블록 윤곽 40개, 총 154개 객체를 구성했다(단면 구조선 114개와 삽입 블록 윤곽 40개). 원본선 단위 선택·숨김과 MAJR/MINR/블록 윤곽 그룹 숨김을 지원한다.
- 화면의 면은 검토용 열린 표면 모델이다. 구조선 사이를 연결했으며 모든 두께/접합을 확정한 닫힌 체적이나 실사 렌더링은 아니다.
- `scripts/render_block8_drawing_views.py`는 DXF 도면 영역의 선·문자를 잘라 평면도, 상부평면도, 종단면도, A-A, B-B SVG 이미지 5개와 픽셀-CAD 좌표 연결값을 만든다. 뷰어에서 현재 보기와 도면 종류를 연결하고 투명도를 조절할 수 있다.
- A-A/B-B 이미지는 각 끝 측점의 단면 평면에 놓고, 종단면 이미지는 STA 주석과 EL 주석으로 길이·높이를 변환해 구조물 측면에 놓는다. 평면도는 DXF 좌표를 모델 좌표에 대응시키며, 상부평면도 표기는 도면 간 35m 배치 차이를 이동해 단일 모델 위에 포개고 화면 표시용 EL 108.8m에 둔다.
- 이들은 도면 요소를 3D 화면의 기준 평면에 투영한 검토용 이미지다. 평면의 표시 높이와 종단면의 측면 이격은 보이도록 둔 시각화 위치이며 실제 부재나 설계 고도를 뜻하지 않는다. DXF 글꼴 및 일부 블록 표현은 SVG에서 단순화될 수 있다.
- 실행: `npm run dev`. 분석 파일과 작업 로그는 화면 상단 링크에서 연다.
## 검증 결과
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="741" viewBox="0 0 1400 741" role="img" aria-label="종단면도 원본 DXF 영역">
<title>종단면도 · 원본 DXF 벡터 캡처</title>
<g><path d="M351.76,377.60 L351.76,638.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1174.46,351.19 L1174.46,658.55" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M763.11,695.97 L895.54,695.97" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M763.11,654.79 L763.11,695.97" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M227.82,638.59 L227.82,377.61" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M268.99,377.61 L225.76,377.61" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M268.99,638.59 L225.76,638.59" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M268.99,601.53 L268.99,377.61" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M310.17,377.61 L266.94,377.61" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M268.99,638.59 L268.99,601.53" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M310.17,601.53 L266.94,601.53" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M310.17,638.59 L266.94,638.59" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M351.79,206.07 L351.79,164.89" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M351.79,164.89 L211.09,164.89" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.50,206.07 L1174.50,164.89" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.50,164.89 L1315.20,164.89" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.91,206.07 L1199.62,206.07" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.50,206.07 L1174.91,206.07" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.91,247.25 L1174.91,204.01" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M351.79,206.07 L1174.50,206.07" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.50,247.25 L1174.50,204.01" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M351.38,206.07 L351.79,206.07" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M351.79,247.25 L351.79,204.01" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M351.38,206.07 L326.67,206.07" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M351.38,247.25 L351.38,204.01" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,457.75 L1278.23,457.75" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.88,478.34 L1195.46,457.75" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,621.50 L1203.70,633.85" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,621.50 L1187.23,633.85" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1187.23,633.85 L1203.70,633.85" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,621.50 L1195.46,662.68" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,662.68 L1303.06,662.68" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.88,621.50 L1216.05,621.50" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,351.18 L1203.70,338.82" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,351.18 L1187.23,338.82" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1187.23,338.82 L1203.70,338.82" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,351.18 L1195.46,310.00" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1195.46,310.00 L1303.06,310.00" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.88,351.18 L1216.05,351.18" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M330.76,601.53 L322.52,613.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M330.76,601.53 L338.99,613.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M338.99,613.88 L322.52,613.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M330.76,601.53 L330.76,666.18" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M330.76,666.18 L223.16,666.18" fill="none" stroke="#d25245" stroke-width="0.8" stroLine truncated
</svg>

After

Width:  |  Height:  |  Size: 48 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1160" viewBox="0 0 1400 1160" role="img" aria-label="평면도 원본 DXF 영역">
<title>평면도 · 원본 DXF 벡터 캡처</title>
<g><path d="M1158.55,302.96 L1163.33,1114.95" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M364.13,302.96 L359.35,1114.95" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M461.08,769.47 L460.69,857.91" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M460.69,857.91 L460.65,865.91" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M461.61,649.47 L462.00,561.03" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M462.00,561.03 L462.03,553.03" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1199.16,338.72 L1241.16,338.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1198.95,302.72 L1240.95,302.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1239.16,338.49 L1238.95,302.49" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1201.22,688.72 L1243.22,688.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1199.16,338.72 L1241.16,338.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1241.22,688.48 L1239.16,338.49" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1201.46,728.72 L1243.45,728.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1201.22,688.72 L1243.22,688.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1241.45,728.48 L1241.22,688.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1203.51,1078.71 L1245.51,1078.46" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1201.46,728.72 L1243.45,728.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1243.51,1078.48 L1241.45,728.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1203.73,1114.71 L1245.73,1114.46" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1203.51,1078.71 L1245.51,1078.46" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1243.73,1114.48 L1243.51,1078.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M323.52,338.72 L281.52,338.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M323.73,302.72 L281.73,302.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M283.52,338.49 L283.73,302.49" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M321.46,688.72 L279.46,688.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M323.52,338.72 L281.52,338.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M281.46,688.48 L283.52,338.49" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M321.23,728.72 L279.23,728.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M321.46,688.72 L279.46,688.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M281.23,728.48 L281.46,688.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M319.17,1078.71 L277.17,1078.46" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M321.23,728.72 L279.23,728.47" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M279.17,1078.48 L281.23,728.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M318.96,1114.71 L276.96,1114.46" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M319.17,1078.71 L277.17,1078.46" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M278.96,1114.48 L279.17,1078.48" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1158.24,182.96 L1158.01,142.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1158.01,142.96 L1294.69,142.16" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M364.44,182.96 L364.67,142.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M364.67,142.96 L227.99,142.16" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M364.20,222.96 L364.45,180.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1158.48,222.96 L1158.23,180.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M364.37,262.96 L364.61,220.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M363.97,262.96 L364.21,220.96" fill="none" stroke="#d25245" stroke-wLine truncated
</svg>

After

Width:  |  Height:  |  Size: 65 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="831" viewBox="0 0 1400 831" role="img" aria-label="단면 A-A · STA. 0K+319.000 원본 DXF 영역">
<title>단면 A-A · STA. 0K+319.000 · 원본 DXF 벡터 캡처</title>
<g><path d="M699.44,600.98 L743.19,600.98" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M721.31,622.85 L721.31,579.10" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M277.25,654.22 L316.62,654.22" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M316.62,654.22 L699.44,654.22" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M699.44,654.22 L743.19,654.22" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M743.19,654.22 L1126.00,654.22" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1126.00,654.22 L1165.37,654.22" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M360.37,609.51 L362.56,609.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M362.56,609.51 L362.56,623.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M362.56,623.60 L380.06,623.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M380.06,623.60 L380.06,609.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M380.06,609.51 L382.25,609.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,609.51 L382.25,607.32" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,607.32 L382.25,610.82" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,610.82 L393.19,610.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,610.60 L677.56,604.91" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M677.56,604.91 L699.44,604.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M699.44,604.48 L699.44,614.85" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1060.37,607.32 L1060.37,610.82" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1060.37,607.32 L1060.37,609.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1060.37,609.51 L1062.56,609.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1062.56,609.51 L1062.56,623.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1062.56,623.60 L1080.06,623.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1080.06,623.60 L1080.06,609.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1080.06,609.51 L1082.25,609.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M743.19,614.85 L743.19,604.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M743.19,604.48 L765.06,604.91" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M765.06,604.91 L1049.44,610.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1049.44,610.60 L1060.37,610.82" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,610.38 L393.19,610.16" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,609.95 L393.19,609.73" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,609.51 L393.19,609.29" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,609.07 L393.19,608.85" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,608.63 L393.19,608.41" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,608.20 L393.19,607.98" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,607.76 L393.19,607.54" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,607.32 L393.19,607.10" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,607.32 L382.25,610.82" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,610.60 L393.19,607.10" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,610.16 L677.56,604.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,609.73 L677.56,604.04" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,609.29 L677.56,603.60" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,608.85 L677.56,603.16" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,608.41 L677.56,602.73" fill="none" stroke="#438b9e" stroke-Line truncated
</svg>

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="831" viewBox="0 0 1400 831" role="img" aria-label="단면 B-B · STA. 0K+339.000 원본 DXF 영역">
<title>단면 B-B · STA. 0K+339.000 · 원본 DXF 벡터 캡처</title>
<g><path d="M699.44,614.95 L743.19,614.95" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M721.31,636.82 L721.31,593.07" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M277.25,664.15 L316.62,664.15" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M316.62,664.15 L699.44,664.15" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M699.44,664.15 L743.19,664.15" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M743.19,664.15 L1126.00,664.15" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1126.00,664.15 L1165.37,664.15" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M360.37,623.48 L362.56,623.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M362.56,623.48 L362.56,633.53" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M362.56,633.53 L380.06,633.53" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M380.06,633.53 L380.06,623.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M380.06,623.48 L382.25,623.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,623.48 L382.25,621.29" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,621.29 L382.25,624.79" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,624.79 L393.19,624.57" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,624.57 L677.56,618.89" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M677.56,618.89 L699.44,618.45" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M699.44,618.45 L699.44,624.78" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1060.37,621.29 L1060.37,624.79" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1060.37,621.29 L1060.37,623.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1060.37,623.48 L1062.56,623.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1062.56,623.48 L1062.56,633.53" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1062.56,633.53 L1080.06,633.53" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1080.06,633.53 L1080.06,623.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1080.06,623.48 L1082.25,623.48" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M743.19,624.78 L743.19,618.45" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M743.19,618.45 L765.06,618.89" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M765.06,618.89 L1049.44,624.57" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M1049.44,624.57 L1060.37,624.79" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,624.35 L393.19,624.14" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,623.92 L393.19,623.70" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,623.48 L393.19,623.26" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,623.04 L393.19,622.82" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,622.60 L393.19,622.39" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,622.17 L393.19,621.95" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,621.73 L393.19,621.51" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,621.29 L393.19,621.07" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M382.25,621.29 L382.25,624.79" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,624.57 L393.19,621.07" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,624.14 L677.56,618.45" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,623.70 L677.56,618.01" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,623.26 L677.56,617.57" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,622.82 L677.56,617.14" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M393.19,622.39 L677.56,616.70" fill="none" stroke="#438b9e" stroke-Line truncated
</svg>

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1097" viewBox="0 0 1400 1097" role="img" aria-label="상부평면도 원본 DXF 영역">
<title>상부평면도 · 원본 DXF 벡터 캡처</title>
<g><path d="M1095.93,286.29 L1100.44,1054.38" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M344.45,286.29 L339.93,1054.38" fill="none" stroke="#438b9e" stroke-width="1.25" stroke-linejoin="round"/><path d="M436.15,727.58 L435.78,811.24" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M435.78,811.24 L435.75,818.80" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M436.65,614.06 L437.02,530.41" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M437.02,530.41 L437.06,522.84" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1138.57,1039.02 L1178.30,1038.79" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1138.46,1020.11 L1178.19,1019.87" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1176.41,1038.80 L1176.30,1019.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1138.46,1020.11 L1178.19,1019.87" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1136.51,689.03 L1176.24,688.80" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1176.30,1019.88 L1174.35,688.81" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1136.51,689.03 L1176.24,688.80" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1136.29,651.19 L1176.02,650.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.35,688.81 L1174.13,650.97" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1136.29,651.19 L1176.02,650.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1134.34,320.12 L1174.07,319.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1174.13,650.97 L1172.18,319.89" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1134.34,320.12 L1174.07,319.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1134.23,301.20 L1173.96,300.97" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1172.18,319.89 L1172.07,300.98" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1176.41,1038.80 L1216.14,1038.57" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1172.07,300.98 L1211.80,300.74" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1214.24,1038.58 L1209.90,300.75" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M306.03,320.12 L266.30,319.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M306.14,301.20 L266.41,300.97" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M268.20,319.89 L268.31,300.98" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M304.08,651.19 L264.36,650.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M306.03,320.12 L266.30,319.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M266.25,650.97 L268.20,319.89" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M303.86,689.03 L264.13,688.80" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M304.08,651.19 L264.36,650.96" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M266.03,688.81 L266.25,650.97" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M301.91,1020.11 L262.19,1019.87" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M303.86,689.03 L264.13,688.80" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M264.08,1019.88 L266.03,688.81" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M301.80,1039.02 L262.07,1038.79" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M301.91,1020.11 L262.19,1019.87" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M263.97,1038.80 L264.08,1019.88" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M263.97,1038.80 L224.24,1038.57" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M268.31,300.98 L228.58,300.74" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M226.13,1038.58 L230.47,300.75" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1095.64,172.77 L1095.41,134.94" fill="none" stroke="#d25245" stroke-width="0.8" stroke-linejoin="round"/><path d="M1095.41,134.94 L1224.70,134.18" fill="none" stroke="#d25245Line truncated
</svg>

After

Width:  |  Height:  |  Size: 66 KiB

+7
View File
@@ -37,3 +37,10 @@
- 단일 중심선 기준 3D 모델 1개로 재생성 (154개 객체)
- 35.000m는 도면 뷰 배치 좌표 차이로 기록하고 모델 간격에서 제외
- 분석 문서, 웹 뷰어, 모델 미리보기 갱신
7. 도면 뷰 이미지와 3D 모델 동시 보기
- 원본 DXF에서 평면도·상부평면도·종단면도·단면 A-A/B-B 영역을 SVG 참고 이미지로 추출
- 뷰어 현재 방향과 도면 이미지를 연결하고 도면 선택/표시/투명도 조절 추가
- CAD 픽셀좌표를 모델 좌표로 변환해 A-A/B-B 끝 단면, 종단 측면, 평면 기준면으로 투영
- 평면도·상부평면도는 35m 도면 배치 차이를 보정해 단일 구조물에 중첩
- 단독 HTML과 전달용 소스 묶음에 추출 이미지를 포함
+7
View File
@@ -2,6 +2,8 @@
"""Vite 빌드 결과를 독립 실행 가능한 BLOCK8 HTML로 묶는다."""
from html.parser import HTMLParser
import base64
import json
from pathlib import Path
import re
@@ -33,6 +35,10 @@ def main():
js = (DIST / "assets" / parser.script).read_text(encoding="utf-8")
css = (DIST / "assets" / parser.stylesheet).read_text(encoding="utf-8")
image_map = {}
for name in ("plan", "upper-plan", "longitudinal", "section-a", "section-b"):
image = ROOT / "public/drawing-images" / f"BLOCK8_{name}.svg"
image_map[name] = "data:image/svg+xml;base64," + base64.b64encode(image.read_bytes()).decode("ascii")
# public 폴더 안의 자료 링크가 file:// 환경에서도 열리도록 상대 경로로 바꾼다.
for path in ("analysis/", "drawings/", "BLOCK8_", "작업기록.txt"):
js = js.replace(f"/{path}", f"./{path}")
@@ -51,6 +57,7 @@ def main():
</head>
<body>
<div id="root"></div>
<script>window.BLOCK8_DRAWING_IMAGES={json.dumps(image_map, ensure_ascii=False)};</script>
<script>{js}</script>
</body>
</html>
+3 -2
View File
@@ -25,19 +25,20 @@ GUIDE = """# 광터지하차도 BLOCK8 전달 자료 안내
- `scripts/`: DXF 모델 추출, SVG 미리보기 생성, 단독 HTML 생성, 전달 ZIP 생성 코드
- `public/`: 원본 DXF, 분석·작업·커밋 기록, 단독 HTML 결과물
- `docs/`: 상세 분석과 모델 미리보기
- `public/drawing-images/`: 도면 SVG 오버레이와 픽셀-CAD 연결 정보
## 포함하지 않은 항목
`node_modules/`, `dist/`, `.git/`은 포함하지 않았습니다. 의존성은 `npm install`로 설치하고, 빌드 결과는 `npm run build`로 다시 만들 수 있습니다. Git 커밋 목록은 `public/BLOCK8_커밋기록.md`에서 확인하세요.
현재 기준 커밋: `89065ab` (단독 HTML file 실행 호환성 개선)
현재 압축본에는 DXF에서 생성한 평면·상부평면·종단·A-A/B-B 오버레이 이미지가 포함되어 있습니다. 3D 기준면 배치는 참고 시각화이며, 도면 요소의 최종 치수 검증은 원본 CAD에서 수행하세요.
"""
def main():
files = [ROOT / "AGENTS.md", ROOT / "README.md", ROOT / "index.html", ROOT / "package.json", ROOT / "package-lock.json", ROOT / "vite.config.js"]
for folder in ("src", "scripts", "docs", "public"):
files.extend(path for path in (ROOT / folder).rglob("*") if path.is_file() and path.name not in EXCLUDED)
files.extend(path for path in (ROOT / folder).rglob("*") if path.is_file() and path.name not in EXCLUDED and "__pycache__" not in path.parts and path.suffix != ".pyc")
files = sorted(set(files))
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""BLOCK8 DXF에서 평면/종단/단면 영역을 벡터 SVG 참조 이미지로 추출한다."""
from collections import defaultdict
from html import escape
import json
import math
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[1]
sys.dont_write_bytecode = True
sys.path.insert(0, str(ROOT / "scripts"))
import extract_block8_model as dxf
OUTPUT = ROOT / "public/drawing-images"
WIDTH = 1400
VIEWS = {
"plan": {"title": "평면도", "bounds": (1_195_000, 1_230_000, -90_000, -61_000)},
"upper-plan": {"title": "상부평면도", "bounds": (1_230_000, 1_267_000, -90_000, -61_000)},
"longitudinal": {"title": "종단면도", "bounds": (1_213_000, 1_247_000, -108_000, -90_000)},
"section-a": {"title": "단면 A-A · STA. 0K+319.000", "bounds": (1_280_000, 1_312_000, -83_000, -64_000)},
"section-b": {"title": "단면 B-B · STA. 0K+339.000", "bounds": (1_280_000, 1_312_000, -102_000, -83_000)},
}
ACI = {
1: "#d25245", 2: "#b58b32", 3: "#43845c", 4: "#438b9e",
5: "#5273a6", 6: "#995d96", 7: "#26332b", 8: "#777f78", 9: "#a1aaa2",
}
FALLBACK = {
"CS-CONC-MAJR": "#26332b", "CS-CONC-MINR": "#3d817e",
"CS-CNTL": "#c05045", "CS-DIML": "#ad584c", "CS-DIMT": "#43845c",
"CS-NOTE": "#43845c", "CS-TEXT": "#43845c", "CZ-TEX1": "#995d96",
"CZ-TEX2": "#43845c", "CS-SYMB": "#43845c", "CS-CUTL": "#5273a6",
}
def compose(a, b):
return (
a[0]*b[0] + a[2]*b[1], a[1]*b[0] + a[3]*b[1],
a[0]*b[2] + a[2]*b[3], a[1]*b[2] + a[3]*b[3],
a[0]*b[4] + a[2]*b[5] + a[4], a[1]*b[4] + a[3]*b[5] + a[5],
)
def transform(matrix, point):
x, y = point
return matrix[0]*x + matrix[2]*y + matrix[4], matrix[1]*x + matrix[3]*y + matrix[5]
def make_insert_matrix(entity, block):
p = dxf.props(entity)
ix, iy = float(dxf.first(p, 10, "0")), float(dxf.first(p, 20, "0"))
sx, sy = float(dxf.first(p, 41, "1")), float(dxf.first(p, 42, "1"))
angle = math.radians(float(dxf.first(p, 50, "0")))
bx, by = block["base"][:2]
co, si = math.cos(angle), math.sin(angle)
a, c = co*sx, -si*sy
b, e = si*sx, co*sy
return a, b, c, e, ix-a*bx-c*by, iy-b*bx-e*by
def entity_layer(entity, inherited=None):
name = dxf.layer(entity)
return inherited if name == "0" and inherited else name
def parse_primitives(entity, blocks, matrix, inherited=None, depth=0, visited=()):
if depth > 5:
return []
kind = entity["type"]
layer = entity_layer(entity, inherited)
p = dxf.props(entity)
if kind == "INSERT":
name = dxf.first(p, 2, "")
block = blocks.get(name)
if not block or name in visited:
return []
next_matrix = compose(matrix, make_insert_matrix(entity, block))
result = []
for child in block["entities"]:
result.extend(parse_primitives(child, blocks, next_matrix, layer, depth+1, visited+(name,)))
return result
if kind in ("LINE", "LWPOLYLINE", "ARC", "CIRCLE", "SOLID", "TRACE"):
points = []
if kind == "LINE":
points = [(float(dxf.first(p, 10)), float(dxf.first(p, 20))), (float(dxf.first(p, 11)), float(dxf.first(p, 21)))]
elif kind == "LWPOLYLINE":
raw_x, raw_y = [float(v) for v in p.get(10, [])], [float(v) for v in p.get(20, [])]
points = list(zip(raw_x, raw_y))
if int(dxf.first(p, 70, "0")) & 1 and points:
points.append(points[0])
elif kind == "ARC":
points = dxf.arc_points(entity, 48)
elif kind == "CIRCLE":
cx, cy, r = float(dxf.first(p, 10)), float(dxf.first(p, 20)), float(dxf.first(p, 40))
points = [(cx+r*math.cos(2*math.pi*i/64), cy+r*math.sin(2*math.pi*i/64)) for i in range(65)]
else:
points = [(float(dxf.first(p, x)), float(dxf.first(p, y))) for x, y in ((10,20),(11,21),(12,22),(13,23)) if x in p and y in p]
if points:
points.append(points[0])
return [("path", [transform(matrix, point) for point in points], layer)] if len(points) > 1 else []
if kind in ("TEXT", "MTEXT", "ATTRIB", "ATTDEF"):
x, y = float(dxf.first(p, 10, "0")), float(dxf.first(p, 20, "0"))
pos = transform(matrix, (x, y))
text = "".join(p.get(1, []) + p.get(3, []))
text = text.replace("\\P", " ").replace("%%d", "°").replace("%%p", "±").replace("%%c", "Ø")
text = re.sub(r"\\[A-Za-z][^;]*;", "", text)
size = float(dxf.first(p, 40, "250"))
# Text heights are in drawing units; image uses the same uniform drawing scale.
angle = float(dxf.first(p, 50, "0"))
align = int(dxf.first(p, 72, "0"))
anchor = {0: "start", 1: "middle", 2: "end"}.get(align, "start")
return [("text", [pos, text, size, angle, anchor], layer)] if text.strip() else []
return []
def get_colors(pairs):
begin, end = dxf.parse_sections(pairs, "TABLES")
colors = {}
for record in dxf.records(pairs, begin+2, end):
if record["type"] != "LAYER":
continue
p = dxf.props(record)
name = dxf.first(p, 2, "")
try:
colors[name] = ACI.get(abs(int(dxf.first(p, 62, "7"))), FALLBACK.get(name, "#374239"))
except ValueError:
colors[name] = FALLBACK.get(name, "#374239")
return colors
def render_view(name, spec, entities, blocks, colors):
x0, x1, y0, y1 = spec["bounds"]
HEIGHT = round(WIDTH * (y1-y0) / (x1-x0))
scale = WIDTH / (x1-x0)
def screen(point):
x, y = point
return (x-x0)*scale, HEIGHT-(y-y0)*scale
primitives = []
for entity in entities:
primitives.extend(parse_primitives(entity, blocks, (1,0,0,1,0,0)))
elements = []
for kind, data, layer in primitives:
color = colors.get(layer, FALLBACK.get(layer, "#667267"))
if kind == "path":
px = [point[0] for point in data]
py = [point[1] for point in data]
if max(px) < x0 or min(px) > x1 or max(py) < y0 or min(py) > y1:
continue
# Canvas crop clips geometry outside the requested drawing region.
points = [screen(point) for point in data]
path = "M" + " L".join(f"{x:.2f},{y:.2f}" for x,y in points)
width = 1.25 if layer == "CS-CONC-MAJR" else 0.8
elements.append(f'<path d="{path}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
else:
(x,y), text, height, angle, anchor = data
if not (x0-1000 <= x <= x1+1000 and y0-1000 <= y <= y1+1000):
continue
sx, sy = screen((x,y))
size = max(4, min(26, height*scale))
escaped = escape(text, quote=False)
# Negative angle compensates for the SVG screen's downward-positive y axis.
elements.append(f'<text x="{sx:.2f}" y="{sy:.2f}" font-size="{size:.2f}" text-anchor="{anchor}" fill="{color}" transform="rotate({-angle:.2f} {sx:.2f} {sy:.2f})">{escaped}</text>')
svg = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}" role="img" aria-label="{escape(spec["title"])} 원본 DXF 영역">',
f'<title>{escape(spec["title"])} · 원본 DXF 벡터 캡처</title>',
f'<g>{"".join(elements)}</g>',
'</svg>',
]
path = OUTPUT / f"BLOCK8_{name}.svg"
path.write_text("\n".join(svg), encoding="utf-8")
print(f"{path.relative_to(ROOT)}: {len(elements)}개 도면 요소")
return {
"title": spec["title"],
"pixelSize": [WIDTH, HEIGHT],
"cadBounds": {
"xMin": x0,
"xMax": x1,
"yMin": y0,
"yMax": y1,
},
"placement": None,
}
def main():
pairs = dxf.read_pairs(dxf.SOURCE)
entities = dxf.parse_modelspace(pairs)
blocks = dxf.parse_blocks(pairs)
colors = get_colors(pairs)
OUTPUT.mkdir(parents=True, exist_ok=True)
metadata = {name: render_view(name, spec, entities, blocks, colors) for name, spec in VIEWS.items()}
metadata["plan"]["placement"] = {"kind": "plan", "xOffset": 0, "displayEL": 108.8}
metadata["upper-plan"]["placement"] = {"kind": "plan", "xOffset": -35_000, "displayEL": 108.8}
metadata["longitudinal"]["placement"] = {
"kind": "longitudinal",
"stationCad": [1_218_301.059683612, 1_241_698.059683612],
"elevationOffset": 205.2898,
"lateralOffset": 15,
}
metadata["section-a"]["placement"] = {
"kind": "section", "stationT": -0.0005005005,
"cadXCenter": 1_296_487.109248054,
"cadYBase": -78_059.37855593952, "baseEL": 101.777,
}
metadata["section-b"]["placement"] = {
"kind": "section", "stationT": 1.0005005005,
"cadXCenter": 1_296_487.109248054,
"cadYBase": -97_286.37855593952, "baseEL": 101.292,
}
model_metadata = ROOT / "src/model/drawingViews.json"
model_metadata.write_text(json.dumps(metadata, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"{model_metadata.relative_to(ROOT)}: 도면/모델 배치 기준 기록")
if __name__ == "__main__":
main()
+29 -2
View File
@@ -8,6 +8,14 @@ const categoryLabels = {
insert: '블록 내부 콘크리트 윤곽',
}
const viewNames = ['전체', '평면', '정면', '측면', 'A-A', 'B-B']
const drawingNames = {
plan: '평면도',
'upper-plan': '상부평면도',
longitudinal: '종단면도',
'section-a': '단면 A-A',
'section-b': '단면 B-B',
}
const drawingForView = { 전체: 'plan', 평면: 'plan', 정면: 'section-a', 측면: 'longitudinal', 'A-A': 'section-a', 'B-B': 'section-b' }
function pointRange(parts, key, coordinate) {
const values = parts.flatMap(part => [...part.a, ...part.b].map(point => point[coordinate]))
@@ -17,6 +25,9 @@ function pointRange(parts, key, coordinate) {
export default function App() {
const [view, setView] = useState('전체')
const [renderMode, setRenderMode] = useState(false)
const [drawingVisible, setDrawingVisible] = useState(false)
const [drawingMode, setDrawingMode] = useState('auto')
const [drawingOpacity, setDrawingOpacity] = useState(0.68)
const [hidden, setHidden] = useState(new Set())
const [selected, setSelected] = useState(null)
const [tab, setTab] = useState('부재')
@@ -35,6 +46,7 @@ export default function App() {
minor: allParts.filter(part => part.group === 'minor').length,
insert: allParts.filter(part => part.group === 'insert').length,
}
const activeDrawing = drawingMode === 'auto' ? drawingForView[view] : drawingMode
const togglePart = id => setHidden(current => {
const next = new Set(current)
@@ -90,16 +102,31 @@ export default function App() {
<div className="viewer-card">
<div className="viewer-head">
<div><span className="eyebrow">도면 구조선 기반 · 열린 표면 모델</span><h2>BLOCK8 3D 구조 보기</h2></div>
<div className="segmented"><button className={!renderMode ? 'active' : ''} onClick={() => setRenderMode(false)}>기본 모델</button><button className={renderMode ? 'active' : ''} onClick={() => setRenderMode(true)}>명암 보기</button></div>
<div className="viewer-actions">
<div className="segmented"><button className={!renderMode ? 'active' : ''} onClick={() => setRenderMode(false)}>기본 모델</button><button className={renderMode ? 'active' : ''} onClick={() => setRenderMode(true)}>명암 보기</button></div>
<button className={`drawing-toggle ${drawingVisible ? 'active' : ''}`} onClick={() => setDrawingVisible(value => !value)}>{drawingVisible ? '도면 숨김' : '도면 함께 보기'}</button>
</div>
</div>
<div className="viewbar">
<div className="views">{viewNames.map(name => <button key={name} className={view === name ? 'active' : ''} onClick={() => setView(name)}>{name}</button>)}</div>
<span>{visibleCount} / {allParts.length} 객체 표시</span>
</div>
<div className={`canvas-wrap ${renderMode ? 'render-mode' : ''}`}>
<ModelViewer units={model.units} hidden={hidden} selected={selected} onSelect={setSelected} view={view} renderMode={renderMode} resetToken={resetToken} />
<ModelViewer units={model.units} hidden={hidden} selected={selected} onSelect={setSelected} view={view} renderMode={renderMode} resetToken={resetToken} drawingVisible={drawingVisible} drawing={activeDrawing} drawingOpacity={drawingOpacity} />
<span className="canvas-tag">{renderMode ? '명암을 더한 도면 검토 시각화 · 실사 렌더링 아님' : '평면도·상부평면도 공통 구조물 모델'}</span>
</div>
{drawingVisible && <div className="drawing-controls">
<label>도면 선택
<select value={drawingMode} onChange={event => setDrawingMode(event.target.value)}>
<option value="auto">현재 보기와 연결</option>
{Object.entries(drawingNames).map(([id, name]) => <option key={id} value={id}>{name}</option>)}
</select>
</label>
<label className="opacity-control">도면 투명도
<input type="range" min="0.25" max="0.95" step="0.05" value={drawingOpacity} onChange={event => setDrawingOpacity(Number(event.target.value))} />
</label>
<span>A-A/B-B는 해당 끝 측점 단면에 놓이며, 평면·종단 이미지는 CAD 좌표와 측점·EL 주석 기준으로 모델과 함께 투영됩니다. 평면 이미지는 화면 표시용 높이에 놓인 참고면입니다.</span>
</div>}
<div className="viewer-foot"><span>끌기: 회전 · 휠: 확대/축소 · Shift+끌기 또는 오른쪽 끌기: 이동</span><button onClick={() => { setView('전체'); setResetToken(value => value + 1); setSelected(null) }}>전체 보기 ↗</button></div>
<div className="selection" aria-live="polite">
{selectedPart && selectedUnit ? <>
+67 -5
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react'
import drawingViews from './model/drawingViews.json' with { type: 'json' }
const DEFAULT_CAMERA = { yaw: -0.66, pitch: 0.43, zoom: 1, panX: 0, panY: 0 }
const COLORS = { major: '#78917c', minor: '#d0aa69', insert: '#9a9074' }
@@ -18,6 +19,34 @@ function worldPoint(unit, pointA, pointB, pointIndex, t) {
return [cx + lateral * normalX, elevation, cz + lateral * normalZ]
}
function centerlinePoint(unit, t, lateral, elevation) {
const angle = (unit.startAngle + (unit.endAngle - unit.startAngle) * t) * Math.PI / 180
const cx = unit.center[0] + unit.radius * Math.cos(angle)
const cz = unit.center[1] + unit.radius * Math.sin(angle)
return [cx - lateral * Math.cos(angle), elevation, cz - lateral * Math.sin(angle)]
}
function drawingPoint(unit, drawingId, pixelX, pixelY) {
const drawing = drawingViews[drawingId]
const [pixelWidth, pixelHeight] = drawing.pixelSize
const bounds = drawing.cadBounds
const cadX = bounds.xMin + (pixelX / pixelWidth) * (bounds.xMax - bounds.xMin)
const cadY = bounds.yMax - (pixelY / pixelHeight) * (bounds.yMax - bounds.yMin)
const placement = drawing.placement
if (placement.kind === 'plan') {
return [(cadX + placement.xOffset - unit.planOrigin[0]) / 1000, placement.displayEL, (cadY - unit.planOrigin[1]) / 1000]
}
if (placement.kind === 'section') {
const lateral = (cadX - placement.cadXCenter) / 1000
const elevation = placement.baseEL + (cadY - placement.cadYBase) / 1000
return centerlinePoint(unit, placement.stationT, lateral, elevation)
}
const [stationStart, stationEnd] = placement.stationCad
const t = (cadX - stationStart) / (stationEnd - stationStart)
const elevation = cadY / 1000 + placement.elevationOffset
return centerlinePoint(unit, t, placement.lateralOffset, elevation)
}
function hexRgb(hex) {
return [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16))
}
@@ -37,16 +66,28 @@ function inPolygon(point, polygon) {
return inside
}
export default function ModelViewer({ units, hidden, selected, onSelect, view, renderMode, resetToken }) {
export default function ModelViewer({ units, hidden, selected, onSelect, view, renderMode, resetToken, drawingVisible, drawing, drawingOpacity }) {
const canvasRef = useRef(null)
const cameraRef = useRef({ ...DEFAULT_CAMERA })
const sceneRef = useRef({ units, hidden, selected, view, renderMode, onSelect })
const drawingImagesRef = useRef({})
const sceneRef = useRef({ units, hidden, selected, view, renderMode, onSelect, drawingVisible, drawing, drawingOpacity })
const drawRef = useRef(() => {})
useEffect(() => {
sceneRef.current = { units, hidden, selected, view, renderMode, onSelect }
sceneRef.current = { units, hidden, selected, view, renderMode, onSelect, drawingVisible, drawing, drawingOpacity }
drawRef.current()
}, [units, hidden, selected, view, renderMode, onSelect])
}, [units, hidden, selected, view, renderMode, onSelect, drawingVisible, drawing, drawingOpacity])
useEffect(() => {
const images = {}
for (const id of Object.keys(drawingViews)) {
const image = new Image()
image.onload = () => drawRef.current()
image.src = window.BLOCK8_DRAWING_IMAGES?.[id] || `/drawing-images/BLOCK8_${id}.svg`
images[id] = image
}
drawingImagesRef.current = images
}, [])
useEffect(() => {
const canvas = canvasRef.current
@@ -74,7 +115,7 @@ export default function ModelViewer({ units, hidden, selected, onSelect, view, r
canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr)
}
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
const { units, hidden, selected, renderMode } = sceneRef.current
const { units, hidden, selected, renderMode, drawingVisible, drawing, drawingOpacity } = sceneRef.current
const background = ctx.createLinearGradient(0, 0, 0, height)
background.addColorStop(0, renderMode ? '#e8efea' : '#f8faf8')
background.addColorStop(1, renderMode ? '#cbd8cf' : '#e9efea')
@@ -125,6 +166,27 @@ export default function ModelViewer({ units, hidden, selected, onSelect, view, r
hitFaces.push(face)
}
if (drawingVisible && drawing && units.length) {
const image = drawingImagesRef.current[drawing]
if (image?.complete && image.naturalWidth) {
const unit = units[0]
const [imageWidth, imageHeight] = drawingViews[drawing].pixelSize
const p00 = project(drawingPoint(unit, drawing, 0, 0), width, height)
const p10 = project(drawingPoint(unit, drawing, imageWidth, 0), width, height)
const p01 = project(drawingPoint(unit, drawing, 0, imageHeight), width, height)
ctx.save()
ctx.globalAlpha = drawingOpacity
ctx.globalCompositeOperation = 'source-over'
ctx.transform(
(p10[0]-p00[0])/imageWidth, (p10[1]-p00[1])/imageWidth,
(p01[0]-p00[0])/imageHeight, (p01[1]-p00[1])/imageHeight,
p00[0], p00[1],
)
ctx.drawImage(image, 0, 0, imageWidth, imageHeight)
ctx.restore()
}
}
ctx.font = '11px sans-serif'
ctx.textAlign = 'left'
ctx.fillStyle = '#68806d'
+11
View File
@@ -34,6 +34,9 @@ h1 em { margin-left:7px; color:#738b79; font-size:.8em; font-style:normal; font-
.workspace { display:grid; grid-template-columns:minmax(0,1fr) 322px; gap:16px; align-items:start; }
.viewer-card,.side-card,.info-card { min-width:0; overflow:hidden; border:1px solid #dbe4d7; border-radius:11px; background:#fafcf8; }
.viewer-head { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:17px 19px 13px; }
.viewer-actions { display:flex; align-items:center; gap:8px; }
.drawing-toggle { padding:7px 10px; border:1px solid #d4dfd2; border-radius:6px; color:#5c765f; font-size:9px; white-space:nowrap; }
.drawing-toggle.active { border-color:#8aa48c; background:#e9f0e7; color:#385f42; }
h2 { margin:5px 0 0; font-size:16px; letter-spacing:-.04em; }
.segmented { display:flex; padding:3px; border-radius:7px; background:#eaf0e7; }
.segmented button { padding:6px 9px; border-radius:5px; color:#7f8d7b; font-size:9px; white-space:nowrap; }
@@ -44,6 +47,11 @@ h2 { margin:5px 0 0; font-size:16px; letter-spacing:-.04em; }
.views button.active { border-color:#ccdac9; background:#eef3ea; color:#426a50; }
.viewbar>span { color:#8b9687; font-size:9px; white-space:nowrap; }
.canvas-wrap { position:relative; overflow:hidden; border-top:1px solid #dfe8da; border-bottom:1px solid #dfe8da; background:#f4f7f3; }
.drawing-controls { display:flex; align-items:center; gap:14px; padding:9px 15px; border-bottom:1px solid #e2e9dd; background:#f7faf5; }
.drawing-controls label { display:flex; align-items:center; gap:7px; color:#647568; font-size:9px; white-space:nowrap; }
.drawing-controls select { max-width:180px; padding:5px 7px; border:1px solid #d6e0d4; border-radius:5px; background:#fff; color:#526657; font-size:9px; }
.drawing-controls input { width:95px; accent-color:#59785e; }
.drawing-controls>span { color:#879287; font-size:8px; line-height:1.5; }
.model-canvas { display:block; width:100%; height:485px; touch-action:none; cursor:grab; }
.model-canvas:active { cursor:grabbing; }
.model-canvas:focus-visible { outline:2px solid #7d9a83; outline-offset:-2px; }
@@ -128,6 +136,8 @@ footer { padding-top:15px; color:#89958a; font-size:8px; }
.notice { gap:8px; }
.workspace { grid-template-columns:1fr; }
.model-canvas { height:380px; }
.drawing-controls { flex-wrap:wrap; gap:8px 12px; }
.drawing-controls>span { flex-basis:100%; }
.side-card { order:2; }
.part-list { max-height:330px; }
.bottom-grid { grid-template-columns:1fr; gap:9px; }
@@ -138,6 +148,7 @@ footer { padding-top:15px; color:#89958a; font-size:8px; }
.views { width:100%; flex-wrap:wrap; }
.views button { padding:6px 8px; }
.model-canvas { height:300px; }
.viewer-actions { flex-wrap:wrap; }
.viewer-foot { align-items:flex-start; }
.metrics strong { font-size:20px; }
.metrics strong.el { font-size:16px; }
+100
View File
@@ -0,0 +1,100 @@
{
"plan": {
"title": "평면도",
"pixelSize": [
1400,
1160
],
"cadBounds": {
"xMin": 1195000,
"xMax": 1230000,
"yMin": -90000,
"yMax": -61000
},
"placement": {
"kind": "plan",
"xOffset": 0,
"displayEL": 108.8
}
},
"upper-plan": {
"title": "상부평면도",
"pixelSize": [
1400,
1097
],
"cadBounds": {
"xMin": 1230000,
"xMax": 1267000,
"yMin": -90000,
"yMax": -61000
},
"placement": {
"kind": "plan",
"xOffset": -35000,
"displayEL": 108.8
}
},
"longitudinal": {
"title": "종단면도",
"pixelSize": [
1400,
741
],
"cadBounds": {
"xMin": 1213000,
"xMax": 1247000,
"yMin": -108000,
"yMax": -90000
},
"placement": {
"kind": "longitudinal",
"stationCad": [
1218301.059683612,
1241698.059683612
],
"elevationOffset": 205.2898,
"lateralOffset": 15
}
},
"section-a": {
"title": "단면 A-A · STA. 0K+319.000",
"pixelSize": [
1400,
831
],
"cadBounds": {
"xMin": 1280000,
"xMax": 1312000,
"yMin": -83000,
"yMax": -64000
},
"placement": {
"kind": "section",
"stationT": -0.0005005005,
"cadXCenter": 1296487.109248054,
"cadYBase": -78059.37855593952,
"baseEL": 101.777
}
},
"section-b": {
"title": "단면 B-B · STA. 0K+339.000",
"pixelSize": [
1400,
831
],
"cadBounds": {
"xMin": 1280000,
"xMax": 1312000,
"yMin": -102000,
"yMax": -83000
},
"placement": {
"kind": "section",
"stationT": 1.0005005005,
"cadXCenter": 1296487.109248054,
"cadYBase": -97286.37855593952,
"baseEL": 101.292
}
}
}