원본 도면 뷰를 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 시점을 제공한다. - 화면: `src/ModelViewer.jsx`와 `src/App.jsx`. 끌어서 회전, 휠 확대/축소, Shift/오른쪽 끌기로 이동, 전체·평면·정면·측면·A-A·B-B 시점을 제공한다.
- 객체는 단일 중심선 모델에 구조선 114개와 삽입 블록 윤곽 40개, 총 154개 객체를 구성했다(단면 구조선 114개와 삽입 블록 윤곽 40개). 원본선 단위 선택·숨김과 MAJR/MINR/블록 윤곽 그룹 숨김을 지원한다. - 객체는 단일 중심선 모델에 구조선 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`. 분석 파일과 작업 로그는 화면 상단 링크에서 연다. - 실행: `npm run dev`. 분석 파일과 작업 로그는 화면 상단 링크에서 연다.
## 검증 결과 ## 검증 결과
+7 -6
View File
@@ -6,19 +6,20 @@
<meta name="theme-color" content="#eff2ed"> <meta name="theme-color" content="#eff2ed">
<meta name="description" content="도면의 중심선과 구조 단면을 연결한 광터지하차도 BLOCK8 3D 검토 모델입니다."> <meta name="description" content="도면의 중심선과 구조 단면을 연결한 광터지하차도 BLOCK8 3D 검토 모델입니다.">
<title>광터지하차도 BLOCK8 3D 모델</title> <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> </style>
</head> </head>
<body> <body>
<div id="root"></div> <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` 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(` `),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=` `);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+` 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 `+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===``||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 `).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> </body>
</html> </html>
Binary file not shown.
+12
View File
@@ -34,10 +34,12 @@ DXF에는 단위가 지정되어 있지 않습니다 (`$INSUNITS=0`). 단면 좌
- `public/drawings/광터지하차도_BLOCK8.dxf` — 등록된 원본 DXF - `public/drawings/광터지하차도_BLOCK8.dxf` — 등록된 원본 DXF
- `docs/도면분석.md` 및 `public/analysis/도면분석.md` — 도면 판독 및 값 근거 - `docs/도면분석.md` 및 `public/analysis/도면분석.md` — 도면 판독 및 값 근거
- `public/drawing-images/` — DXF에서 추출한 평면·상부평면·종단·A-A/B-B 벡터 이미지
- `public/작업기록.txt` — 초기화 이후 작업 이력 - `public/작업기록.txt` — 초기화 이후 작업 이력
- `src/model/block8.json` — 웹 뷰어가 읽는 모델 데이터 - `src/model/block8.json` — 웹 뷰어가 읽는 모델 데이터
- `scripts/extract_block8_model.py` — DXF에서 모델 데이터를 다시 추출 - `scripts/extract_block8_model.py` — DXF에서 모델 데이터를 다시 추출
- `scripts/render_block8_preview.py` — SVG 미리보기 생성 - `scripts/render_block8_preview.py` — SVG 미리보기 생성
- `scripts/render_block8_drawing_views.py` — 원본 DXF 도면 영역 이미지 생성
- `docs/BLOCK8_3D모델_미리보기.svg` — 모델 이미지 - `docs/BLOCK8_3D모델_미리보기.svg` — 모델 이미지
- `src/App.jsx`, `src/ModelViewer.jsx` — 웹 화면과 조작 기능 - `src/App.jsx`, `src/ModelViewer.jsx` — 웹 화면과 조작 기능
@@ -59,6 +61,8 @@ npm run build
npm run lint npm run lint
python3 scripts/extract_block8_model.py python3 scripts/extract_block8_model.py
python3 scripts/render_block8_preview.py python3 scripts/render_block8_preview.py
python3 scripts/render_block8_drawing_views.py
python3 scripts/build_standalone_html.py
``` ```
## 6. 검증 기록 ## 6. 검증 기록
@@ -76,3 +80,11 @@ python3 scripts/render_block8_preview.py
- `e473a7c` 평면도와 상부평면도 모델을 하나로 통합 - `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 시점을 제공한다. - 화면: `src/ModelViewer.jsx`와 `src/App.jsx`. 끌어서 회전, 휠 확대/축소, Shift/오른쪽 끌기로 이동, 전체·평면·정면·측면·A-A·B-B 시점을 제공한다.
- 객체는 단일 중심선 모델에 구조선 114개와 삽입 블록 윤곽 40개, 총 154개 객체를 구성했다(단면 구조선 114개와 삽입 블록 윤곽 40개). 원본선 단위 선택·숨김과 MAJR/MINR/블록 윤곽 그룹 숨김을 지원한다. - 객체는 단일 중심선 모델에 구조선 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`. 분석 파일과 작업 로그는 화면 상단 링크에서 연다. - 실행: `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개 객체) - 단일 중심선 기준 3D 모델 1개로 재생성 (154개 객체)
- 35.000m는 도면 뷰 배치 좌표 차이로 기록하고 모델 간격에서 제외 - 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로 묶는다.""" """Vite 빌드 결과를 독립 실행 가능한 BLOCK8 HTML로 묶는다."""
from html.parser import HTMLParser from html.parser import HTMLParser
import base64
import json
from pathlib import Path from pathlib import Path
import re import re
@@ -33,6 +35,10 @@ def main():
js = (DIST / "assets" / parser.script).read_text(encoding="utf-8") js = (DIST / "assets" / parser.script).read_text(encoding="utf-8")
css = (DIST / "assets" / parser.stylesheet).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:// 환경에서도 열리도록 상대 경로로 바꾼다. # public 폴더 안의 자료 링크가 file:// 환경에서도 열리도록 상대 경로로 바꾼다.
for path in ("analysis/", "drawings/", "BLOCK8_", "작업기록.txt"): for path in ("analysis/", "drawings/", "BLOCK8_", "작업기록.txt"):
js = js.replace(f"/{path}", f"./{path}") js = js.replace(f"/{path}", f"./{path}")
@@ -51,6 +57,7 @@ def main():
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script>window.BLOCK8_DRAWING_IMAGES={json.dumps(image_map, ensure_ascii=False)};</script>
<script>{js}</script> <script>{js}</script>
</body> </body>
</html> </html>
+3 -2
View File
@@ -25,19 +25,20 @@ GUIDE = """# 광터지하차도 BLOCK8 전달 자료 안내
- `scripts/`: DXF 모델 추출, SVG 미리보기 생성, 단독 HTML 생성, 전달 ZIP 생성 코드 - `scripts/`: DXF 모델 추출, SVG 미리보기 생성, 단독 HTML 생성, 전달 ZIP 생성 코드
- `public/`: 원본 DXF, 분석·작업·커밋 기록, 단독 HTML 결과물 - `public/`: 원본 DXF, 분석·작업·커밋 기록, 단독 HTML 결과물
- `docs/`: 상세 분석과 모델 미리보기 - `docs/`: 상세 분석과 모델 미리보기
- `public/drawing-images/`: 도면 SVG 오버레이와 픽셀-CAD 연결 정보
## 포함하지 않은 항목 ## 포함하지 않은 항목
`node_modules/`, `dist/`, `.git/`은 포함하지 않았습니다. 의존성은 `npm install`로 설치하고, 빌드 결과는 `npm run build`로 다시 만들 수 있습니다. Git 커밋 목록은 `public/BLOCK8_커밋기록.md`에서 확인하세요. `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(): def main():
files = [ROOT / "AGENTS.md", ROOT / "README.md", ROOT / "index.html", ROOT / "package.json", ROOT / "package-lock.json", ROOT / "vite.config.js"] 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"): 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)) files = sorted(set(files))
OUTPUT.parent.mkdir(parents=True, exist_ok=True) 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: '블록 내부 콘크리트 윤곽', insert: '블록 내부 콘크리트 윤곽',
} }
const viewNames = ['전체', '평면', '정면', '측면', 'A-A', 'B-B'] 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) { function pointRange(parts, key, coordinate) {
const values = parts.flatMap(part => [...part.a, ...part.b].map(point => point[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() { export default function App() {
const [view, setView] = useState('전체') const [view, setView] = useState('전체')
const [renderMode, setRenderMode] = useState(false) 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 [hidden, setHidden] = useState(new Set())
const [selected, setSelected] = useState(null) const [selected, setSelected] = useState(null)
const [tab, setTab] = useState('부재') const [tab, setTab] = useState('부재')
@@ -35,6 +46,7 @@ export default function App() {
minor: allParts.filter(part => part.group === 'minor').length, minor: allParts.filter(part => part.group === 'minor').length,
insert: allParts.filter(part => part.group === 'insert').length, insert: allParts.filter(part => part.group === 'insert').length,
} }
const activeDrawing = drawingMode === 'auto' ? drawingForView[view] : drawingMode
const togglePart = id => setHidden(current => { const togglePart = id => setHidden(current => {
const next = new Set(current) const next = new Set(current)
@@ -90,16 +102,31 @@ export default function App() {
<div className="viewer-card"> <div className="viewer-card">
<div className="viewer-head"> <div className="viewer-head">
<div><span className="eyebrow">도면 구조선 기반 · 열린 표면 모델</span><h2>BLOCK8 3D 구조 보기</h2></div> <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>
<div className="viewbar"> <div className="viewbar">
<div className="views">{viewNames.map(name => <button key={name} className={view === name ? 'active' : ''} onClick={() => setView(name)}>{name}</button>)}</div> <div className="views">{viewNames.map(name => <button key={name} className={view === name ? 'active' : ''} onClick={() => setView(name)}>{name}</button>)}</div>
<span>{visibleCount} / {allParts.length} 객체 표시</span> <span>{visibleCount} / {allParts.length} 객체 표시</span>
</div> </div>
<div className={`canvas-wrap ${renderMode ? 'render-mode' : ''}`}> <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> <span className="canvas-tag">{renderMode ? '명암을 더한 도면 검토 시각화 · 실사 렌더링 아님' : '평면도·상부평면도 공통 구조물 모델'}</span>
</div> </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="viewer-foot"><span>끌기: 회전 · 휠: 확대/축소 · Shift+끌기 또는 오른쪽 끌기: 이동</span><button onClick={() => { setView('전체'); setResetToken(value => value + 1); setSelected(null) }}>전체 보기 ↗</button></div>
<div className="selection" aria-live="polite"> <div className="selection" aria-live="polite">
{selectedPart && selectedUnit ? <> {selectedPart && selectedUnit ? <>
+67 -5
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react' 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 DEFAULT_CAMERA = { yaw: -0.66, pitch: 0.43, zoom: 1, panX: 0, panY: 0 }
const COLORS = { major: '#78917c', minor: '#d0aa69', insert: '#9a9074' } 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] 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) { function hexRgb(hex) {
return [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16)) return [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16))
} }
@@ -37,16 +66,28 @@ function inPolygon(point, polygon) {
return inside 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 canvasRef = useRef(null)
const cameraRef = useRef({ ...DEFAULT_CAMERA }) 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(() => {}) const drawRef = useRef(() => {})
useEffect(() => { useEffect(() => {
sceneRef.current = { units, hidden, selected, view, renderMode, onSelect } sceneRef.current = { units, hidden, selected, view, renderMode, onSelect, drawingVisible, drawing, drawingOpacity }
drawRef.current() 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(() => { useEffect(() => {
const canvas = canvasRef.current 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) canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr)
} }
ctx.setTransform(dpr, 0, 0, dpr, 0, 0) 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) const background = ctx.createLinearGradient(0, 0, 0, height)
background.addColorStop(0, renderMode ? '#e8efea' : '#f8faf8') background.addColorStop(0, renderMode ? '#e8efea' : '#f8faf8')
background.addColorStop(1, renderMode ? '#cbd8cf' : '#e9efea') background.addColorStop(1, renderMode ? '#cbd8cf' : '#e9efea')
@@ -125,6 +166,27 @@ export default function ModelViewer({ units, hidden, selected, onSelect, view, r
hitFaces.push(face) 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.font = '11px sans-serif'
ctx.textAlign = 'left' ctx.textAlign = 'left'
ctx.fillStyle = '#68806d' 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; } .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-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-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; } h2 { margin:5px 0 0; font-size:16px; letter-spacing:-.04em; }
.segmented { display:flex; padding:3px; border-radius:7px; background:#eaf0e7; } .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; } .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; } .views button.active { border-color:#ccdac9; background:#eef3ea; color:#426a50; }
.viewbar>span { color:#8b9687; font-size:9px; white-space:nowrap; } .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; } .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 { display:block; width:100%; height:485px; touch-action:none; cursor:grab; }
.model-canvas:active { cursor:grabbing; } .model-canvas:active { cursor:grabbing; }
.model-canvas:focus-visible { outline:2px solid #7d9a83; outline-offset:-2px; } .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; } .notice { gap:8px; }
.workspace { grid-template-columns:1fr; } .workspace { grid-template-columns:1fr; }
.model-canvas { height:380px; } .model-canvas { height:380px; }
.drawing-controls { flex-wrap:wrap; gap:8px 12px; }
.drawing-controls>span { flex-basis:100%; }
.side-card { order:2; } .side-card { order:2; }
.part-list { max-height:330px; } .part-list { max-height:330px; }
.bottom-grid { grid-template-columns:1fr; gap:9px; } .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 { width:100%; flex-wrap:wrap; }
.views button { padding:6px 8px; } .views button { padding:6px 8px; }
.model-canvas { height:300px; } .model-canvas { height:300px; }
.viewer-actions { flex-wrap:wrap; }
.viewer-foot { align-items:flex-start; } .viewer-foot { align-items:flex-start; }
.metrics strong { font-size:20px; } .metrics strong { font-size:20px; }
.metrics strong.el { font-size:16px; } .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
}
}
}