From 0a9bd9140777fb27d525eab7d1c904ab4c19b253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EC=A1=B0?= Date: Wed, 28 Oct 2026 18:19:06 +0900 Subject: [PATCH] Add src/dxf.js --- src/dxf.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/dxf.js diff --git a/src/dxf.js b/src/dxf.js new file mode 100644 index 0000000..e76a9ce --- /dev/null +++ b/src/dxf.js @@ -0,0 +1,11 @@ +// DXF parsing and plan geometry helpers. +function parseDxf(text){const entities=[],dimensionTexts=[];const raw=text.replace(/^\ufeff/,'').split(/\r?\n/),pairs=[];for(let i=0;i+1p[0]===8)?.[1]||'0',entity={type,layer};layers.add(layer);if(type==='LINE'){entity.pts=[{x:num(rec,10),y:num(rec,20)},{x:num(rec,11),y:num(rec,21)}]}else if(type==='LWPOLYLINE'){const pts=[];let point=null;for(const [code,value] of rec){if(code===10){if(point)pts.push(point);point={x:Number(value),y:0}}else if(code===20&&point)point.y=Number(value)}if(point)pts.push(point);entity.pts=pts;entity.closed=!!(Number(rec.find(p=>p[0]===70)?.[1]||0)&1)}else if(type==='POLYLINE'){let pts=[];while(ip[0]===70)?.[1]||0)&1);if(pairs[i]?.[0]===0&&pairs[i][1]==='SEQEND')i++}else if(type==='TEXT'||type==='MTEXT'){entity.text=rec.filter(p=>p[0]===1||p[0]===3).map(p=>p[1]).join('').replace(/\\P/g,' ').replace(/[{}]/g,'').trim();entity.pos={x:num(rec,10),y:num(rec,20)};entity.rotation=num(rec,50);if(entity.text&&layer==='DIM')dimensionTexts.push(entity)}if(entity.pts)entities.push(entity)}if(!entities.length)throw Error('도면의 ENTITIES 섹션에서 지원되는 선 도형을 찾지 못했습니다. ASCII DXF인지 확인해 주세요.');return {entities,dimensionTexts,layers:[...layers].sort((a,b)=>a.localeCompare(b,undefined,{numeric:true))}}} +function num(rec,code){return Number(rec.find(p=>p[0]===code)?.[1]||0)} + +function eq(a,b){return Math.hypot(a.x-b.x,a.y-b.y)<1e-5}function closedLoops(entities,layer){const found=[];let lines=[];for(const e of entities.filter(x=>x.layer===layer)){if(e.pts?.length<2)continue;if(e.type==='LINE'||!e.closed){for(let i=0;i2)lines.push([e.pts.at(-1),e.pts[0]]);continue}if(e.pts.length>=3)found.push(e.pts.slice())}while(lines.length){let chain=lines.pop().map(p=>({...p})),changed=true;while(changed){changed=false;for(let i=0;i=4&&eq(chain[0],chain.at(-1)))found.push(chain.slice(0,-1))}return found} +function area(p){let a=0;for(let i=0;ip.y)!==(b.y>p.y)&&p.x<(b.x-a.x)*(p.y-a.y)/(b.y-a.y)+a.x)ok=!ok}return ok} +function dimValue(s){let t=String(s).replace(/\s/g,'').replace(/[,]/g,',').replace(/[mmMM]/g,'');if(/^\d{1,3}[,.]\d{3}$/.test(t))t=t.replace(/[,.]/g,'');else t=t.replace(/,/g,'');const n=Number(t);if(!Number.isFinite(n)||n<=0)return null;return n<10?n*1000:n} +// Match dimension lines and nearby labels to each plan outline. +function loopDims(poly,dimLines,dimensionTexts){const xs=poly.map(p=>p.x),ys=poly.map(p=>p.y),x0=Math.min(...xs),x1=Math.max(...xs),y0=Math.min(...ys),y1=Math.max(...ys),w=x1-x0,h=y1-y0;if(!(w>0&&h>0))return null;function best(axis){let best=null;for(const e of dimLines){const [a,b]=e.pts,dx=Math.abs(b.x-a.x),dy=Math.abs(b.y-a.y),horizontal=axis==='x';if(horizontal?dy>Math.max(.01,dx*.003):dx>Math.max(.01,dy*.003))continue;const lo=horizontal?Math.min(a.x,b.x):Math.min(a.y,b.y),hi=horizontal?Math.max(a.x,b.x):Math.max(a.y,b.y),span=horizontal?w:h,boxLo=horizontal?x0:y0,boxHi=horizontal?x1:y1,len=hi-lo,overlap=Math.max(0,Math.min(hi,boxHi)-Math.max(lo,boxLo))/span;if(overlap<.72)continue;const gap=horizontal?Math.min(Math.abs(a.y-y0),Math.abs(a.y-y1)):Math.min(Math.abs(a.x-x0),Math.abs(a.x-x1)),offset=gap/(horizontal?h:w);if(offset>.42)continue;const mid=(lo+hi)/2,axisPos=horizontal?a.y:a.x;for(const t of dimensionTexts){const val=dimValue(t.text),rot=((t.rotation%180)+180)%180;if(val==null||(horizontal?Math.min(rot,180-rot)>12:Math.abs(rot-90)>12))continue;const along=Math.abs((horizontal?t.pos.x:t.pos.y)-mid)/Math.max(len,1),perp=Math.abs((horizontal?t.pos.y:t.pos.x)-axisPos)/Math.max(len,1),textScore=along+perp;if(textScore>.42)continue;const lineScore=2*(1-overlap)+.5*Math.abs(Math.log(len/span))+offset+textScore;if(!best||lineScore