Add DXF verification: update src/main.js

This commit is contained in:
bjkim committed 2026-10-29 12:13:28 +09:00
1 parent 67f3845014
commit c23ab468ad
1 file changed
+6 -33
+6 -33
View File
@@ -1,4 +1,4 @@
// UI state, model construction, canvas rendering, and viewer controls.
// UI state, model rendering, and viewer controls.
const $=id=>document.getElementById(id);let entities=[],dimensionTexts=[],fileName='',modelData=null,yaw=-.72,pitch=.62,zoom=1,panX=0,panY=0,drag=null;
const canvas=document.createElement('canvas');$('viewport').appendChild(canvas);const ctx=canvas.getContext('2d');
const status=(msg,error=false)=>{$('status').textContent=msg;$('status').classList.toggle('error',error)};
@@ -9,40 +9,13 @@ $('file').addEventListener('change',e=>loadFile(e.target.files[0]));$('drop').ad
// Build the slab and hollow wall from dimensioned outer and inner plan loops.
function build(){
try{
const found=CatchpitDxf.closedLoops(entities,$('outline').value);
if(!found.length)throw Error('외곽선을 찾지 못했습니다. LN_out 레이어를 확인하세요.');
// Reuse the same DIM line set when scoring every closed outline.
const dimLines=entities.filter(e=>e.layer==='DIM'&&e.type==='LINE'&&e.pts?.length===2);
const candidates=found.map(poly=>({poly,dims:CatchpitDxf.loopDims(poly,dimLines,dimensionTexts),area:Math.abs(CatchpitDxf.area(poly))})).filter(c=>c.dims).sort((a,b)=>a.dims.score-b.dims.score||b.area-a.area);
if(!candidates.length)throw Error('평면 외곽에 대응하는 가로·세로 DIM 치수를 찾지 못했습니다.');
const chosen=candidates[0],outer=chosen.poly,minX=Math.min(...outer.map(p=>p.x)),minY=Math.min(...outer.map(p=>p.y));
const insideLoops=found.filter(h=>h!==outer&&CatchpitDxf.inside(h[0],outer)).sort((a,b)=>Math.abs(CatchpitDxf.area(b))-Math.abs(CatchpitDxf.area(a)));
if(!insideLoops.length)throw Error('벽체와 내부 공간을 분리하려면 평면도에 안쪽 경계선이 필요합니다.');
const inner=insideLoops[0],scaleX=chosen.dims.scaleX*.001,scaleY=chosen.dims.scaleY*.001;
let height=Number($('height').value)*.001,baseThickness=Number($('thickness').value)*.001,heightSource='manual';
if($('autoHeight').checked){const used=chosen.dims.dyText;const vertical=dimensionTexts.filter(t=>Math.abs((((t.rotation%180)+180)%180)-90)<12&&t!==used).map(t=>({t,value:CatchpitDxf.dimValue(t.text)})).filter(v=>v.value&&v.value>=100).sort((a,b)=>b.value-a.value);if(vertical.length){height=vertical[0].value*.001;heightSource=`section DIM ${vertical[0].value} mm`;const slab=vertical.filter(v=>v.value<height*1000).sort((a,b)=>a.value-b.value)[0];if(slab)baseThickness=slab.value*.001;$('height').value=vertical[0].value;if(slab)$('thickness').value=slab.value}}
if(!(height>0&&baseThickness>=0))throw Error('벽체 높이와 바닥 두께를 확인해 주세요.');
const outerW=(Math.max(...outer.map(p=>p.x))-minX)*scaleX,outerH=(Math.max(...outer.map(p=>p.y))-minY)*scaleY,innerW=(Math.max(...inner.map(p=>p.x))-Math.min(...inner.map(p=>p.x)))*scaleX,innerH=(Math.max(...inner.map(p=>p.y))-Math.min(...inner.map(p=>p.y)))*scaleY;
const wallThickness=((outerW-innerW)+(outerH-innerH))/4;
const toModel=p=>({x:(p.x-minX)*scaleX,y:(p.y-minY)*scaleY});
const parts=[{name:'바닥판',type:'base',footprint:outer.map(toModel),z0:-baseThickness,z1:0,color:'#3c9881',visible:true}];
const edgePairs=[];
const unused=new Set(inner.map((_,i)=>i));
for(let i=0;i<outer.length;i++){
const a=outer[i],b=outer[(i+1)%outer.length],dx=b.x-a.x,dy=b.y-a.y,len=Math.hypot(dx,dy)||1;
let best=-1,bestScore=Infinity;
for(const k of unused){const q=inner[k],r=inner[(k+1)%inner.length],ex=r.x-q.x,ey=r.y-q.y,el=Math.hypot(ex,ey)||1,parallel=1-Math.abs((dx*ex+dy*ey)/(len*el)),dist=Math.hypot((a.x+b.x-q.x-r.x)/2,(a.y+b.y-q.y-r.y)/2);const score=parallel+dist/(len*2);if(score<bestScore){bestScore=score;best=k}}
if(best<0)throw Error('안쪽 경계에서 벽체별 대응 구간을 찾지 못했습니다.');
unused.delete(best);const q=inner[best],r=inner[(best+1)%inner.length],qa=Math.hypot(q.x-a.x,q.y-a.y)<=Math.hypot(r.x-a.x,r.y-a.y)?q:r,qb=qa===q?r:q;
const innerStart=qa===q?best:(best+1)%inner.length,innerEnd=(innerStart+1)%inner.length;
edgePairs.push({outerStart:i,outerEnd:(i+1)%outer.length,innerStart,innerEnd});
}
if(edgePairs.length<3)throw Error('평면 내부 경계에서 벽체를 만들 수 없습니다.');
parts.push({name:'벽체',type:'wall',footprint:outer.map(toModel),innerFootprint:inner.map(toModel),edgePairs,z0:0,z1:height,color:'#438dcc',visible:true});
modelData={outer,inner,minX,minY,scaleX,scaleY,height,baseThickness,wallThickness,parts,dimX:chosen.dims.dx,dimY:chosen.dims.dy};
const result=CatchpitModel.createCatchpitModel({entities,dimensionTexts,outlineLayer:$('outline').value,heightMm:$('height').value,baseThicknessMm:$('thickness').value,autoHeight:$('autoHeight').checked});
modelData=result.modelData;const {heightSource}=result;
if($('autoHeight').checked){$('height').value=modelData.height*1000;$('thickness').value=modelData.baseThickness*1000}
const {parts,wallThickness,minX,minY,height,dimX,dimY}=modelData;
$('partRows').innerHTML='';for(const part of parts){const label=document.createElement('label');label.className='partrow';const toggle=document.createElement('input');toggle.type='checkbox';toggle.checked=true;toggle.addEventListener('change',()=>{part.visible=toggle.checked;draw()});const swatch=document.createElement('i');swatch.className='swatch';swatch.style.background=part.color;const text=document.createElement('span');text.textContent=part.name;label.append(toggle,swatch,text);$('partRows').append(label)}$('parts').classList.remove('hidden');
$('empty').classList.add('hidden');$('badge').classList.remove('hidden');$('legend').classList.remove('hidden');$('reset').disabled=false;$('export').disabled=false;
$('meta').textContent=`${fileName} · 평면 ${chosen.dims.dx} × ${chosen.dims.dy} mm · 벽체 높이 ${(height*1000).toFixed(0)} mm`;
$('meta').textContent=`${fileName} · 평면 ${dimX} × ${dimY} mm · 벽체 높이 ${(height*1000).toFixed(0)} mm`;
status(`모델 생성 완료 · 바닥판 1 · 벽체 1 · 벽 두께 약 ${(wallThickness*1000).toFixed(0)} mm · ${heightSource}`);
$('footstatus').textContent=`좌하단 기준점 (${minX.toFixed(3)}, ${minY.toFixed(3)}) → (0, 0, 0)`;fit();
}catch(e){status(e.message,true)}