Add src/dxf.js

This commit is contained in:
bjkim committed 2026-10-28 18:19:06 +09:00
1 parent fc9e3997c9
commit 0a9bd91407
1 file changed
+11
+11
View File
@@ -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+1<raw.length;i+=2){const code=Number(raw[i].trim());if(Number.isFinite(code))pairs.push([code,raw[i+1].trim()])}let inEntities=false,section='',layers=new Set();for(let i=0;i<pairs.length;){const [code,val]=pairs[i];if(code===0&&val==='SECTION'){section=pairs[i+1]?.[1]==='ENTITIES'?'ENTITIES':pairs[i+1]?.[1]||'';inEntities=section==='ENTITIES';i+=2;continue}if(code===0&&val==='ENDSEC'){inEntities=false;section='';i++;continue}if(code!==0||!inEntities){i++;continue}const type=val;i++;const rec=[];while(i<pairs.length&&pairs[i][0]!==0)rec.push(pairs[i++]);let layer=rec.find(p=>p[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(i<pairs.length&&pairs[i][0]===0&&pairs[i][1]==='VERTEX'){i++;const vr=[];while(i<pairs.length&&pairs[i][0]!==0)vr.push(pairs[i++]);pts.push({x:num(vr,10),y:num(vr,20)})}entity.pts=pts;entity.closed=!!(Number(rec.find(p=>p[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;i<e.pts.length-1;i++)lines.push([e.pts[i],e.pts[i+1]]);if(e.closed&&e.pts.length>2)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<lines.length;i++){const p=lines[i];if(eq(chain.at(-1),p[0]))chain.push(p[1]);else if(eq(chain.at(-1),p[1]))chain.push(p[0]);else if(eq(chain[0],p[1]))chain.unshift(p[0]);else if(eq(chain[0],p[0]))chain.unshift(p[1]);else continue;lines.splice(i,1);changed=true;break}}if(chain.length>=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;i<p.length;i++){const q=p[(i+1)%p.length];a+=p[i].x*q.y-q.x*p[i].y}return a/2}function inside(p,poly){let ok=false;for(let i=0,j=poly.length-1;i<poly.length;j=i++){const a=poly[i],b=poly[j];if((a.y>p.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<best.score)best={value:val,text:t.text,entity:t,score:lineScore}}}return best}const dx=best('x'),dy=best('y');if(!dx||!dy)return null;const sx=dx.value/w,sy=dy.value/h;return{dx:dx.value,dy:dy.value,dxText:dx.entity,dyText:dy.entity,scaleX:sx,scaleY:sy,score:dx.score+dy.score+2*Math.abs(Math.log(sx/sy))}}
window.CatchpitDxf={parseDxf,num,eq,closedLoops,area,inside,dimValue,loopDims};