import assert from 'node:assert/strict'; import {readFile} from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; import vm from 'node:vm'; import {fileURLToPath} from 'node:url'; const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); const sandbox={setTimeout,URL:{createObjectURL(){return 'blob:test'},revokeObjectURL(){}},document:{createElement(){return {click(){}}}}}; sandbox.window=sandbox; vm.createContext(sandbox); for(const file of ['src/dxf.js','src/model.js'])vm.runInContext(await readFile(path.join(root,file),'utf8'),sandbox,{filename:file}); function pair(code,value){return `${code}\n${value}`} function entity(type,fields){return [pair(0,type),...fields.map(([code,value])=>pair(code,value))].join('\n')} function polyline(layer,points){return entity('LWPOLYLINE',[[8,layer],[90,points.length],[70,1],...points.flatMap(p=>[[10,p[0]],[20,p[1]]])])} function line(layer,a,b){return entity('LINE',[[8,layer],[10,a[0]],[20,a[1]],[11,b[0]],[21,b[1]]])} function text(value,x,y,rotation){return entity('TEXT',[[8,'DIM'],[10,x],[20,y],[50,rotation],[1,value]])} function makeDxf({withInner=true,withDims=true}={}){ const items=[polyline('LN_out',[[1000,2000],[5000,2000],[5000,5000],[1000,5000]])]; if(withInner)items.push(polyline('LN_out',[[1500,2500],[4500,2500],[4500,4500],[1500,4500]])); if(withDims)items.push(line('DIM',[1000,1600],[5000,1600]),line('DIM',[5400,2000],[5400,5000]),text('4000',3000,1600,0),text('3000',5400,3500,90),text('1200',6000,3500,90),text('150',6500,3500,90)); return [pair(0,'SECTION'),pair(2,'ENTITIES'),...items,pair(0,'ENDSEC'),pair(0,'EOF')].join('\n')+'\n'; } test('DXF parsing detects plan loops and dimension labels',()=>{ const parsed=sandbox.window.CatchpitDxf.parseDxf(makeDxf()); assert.ok(parsed.layers.includes('LN_out')); assert.equal(parsed.dimensionTexts.length,4); assert.equal(sandbox.window.CatchpitDxf.closedLoops(parsed.entities,'LN_out').length,2); }); test('model uses drawing dimensions and places the lower-left point at the origin',()=>{ const parsed=sandbox.window.CatchpitDxf.parseDxf(makeDxf()); const {modelData,heightSource}=sandbox.window.CatchpitModel.createCatchpitModel({ entities:parsed.entities,dimensionTexts:parsed.dimensionTexts,outlineLayer:'LN_out',heightMm:900,baseThicknessMm:100,autoHeight:true }); assert.equal(modelData.dimX,4000); assert.equal(modelData.dimY,3000); assert.equal(modelData.minX,1000); assert.equal(modelData.minY,2000); assert.equal(modelData.parts[0].footprint[0].x,0); assert.equal(modelData.parts[0].footprint[0].y,0); assert.equal(modelData.height,1.2); assert.equal(modelData.baseThickness,.15); assert.equal(heightSource,'section DIM 1200 mm'); assert.deepEqual(Array.from(modelData.parts,p=>p.type),['base','wall']); assert.equal(modelData.parts[1].edgePairs.length,4); }); test('model rejects missing dimensions and missing inner boundary',()=>{ const noDimensions=sandbox.window.CatchpitDxf.parseDxf(makeDxf({withDims:false})); assert.throws(()=>sandbox.window.CatchpitModel.createCatchpitModel({entities:noDimensions.entities,dimensionTexts:[],outlineLayer:'LN_out',heightMm:1200,baseThicknessMm:150,autoHeight:false}),/DIM 치수/); const noInner=sandbox.window.CatchpitDxf.parseDxf(makeDxf({withInner:false})); assert.throws(()=>sandbox.window.CatchpitModel.createCatchpitModel({entities:noInner.entities,dimensionTexts:noInner.dimensionTexts,outlineLayer:'LN_out',heightMm:1200,baseThicknessMm:150,autoHeight:false}),/안쪽 경계선/); }); test('malformed DXF without supported geometry reports an error',()=>{ assert.throws(()=>sandbox.window.CatchpitDxf.parseDxf('0\nSECTION\n2\nENTITIES\n0\nENDSEC\n0\nEOF\n'),/지원되는 선 도형/); }); test('OBJ export includes closed slab and wall meshes',async()=>{ class TestBlob{constructor(parts){this.parts=parts}} let objText=''; sandbox.Blob=TestBlob; sandbox.URL.createObjectURL=blob=>{objText=blob.parts.join('');return 'blob:test'}; vm.runInContext(await readFile(path.join(root,'src/obj-export.js'),'utf8'),sandbox,{filename:'src/obj-export.js'}); const parsed=sandbox.window.CatchpitDxf.parseDxf(makeDxf()); const {modelData}=sandbox.window.CatchpitModel.createCatchpitModel({entities:parsed.entities,dimensionTexts:parsed.dimensionTexts,outlineLayer:'LN_out',heightMm:1200,baseThicknessMm:150,autoHeight:true}); sandbox.window.CatchpitObjExporter.download(modelData,'verification.dxf'); const meshes=new Map();let current=''; for(const row of objText.split(/\r?\n/)){ if(row.startsWith('o ')){current=row.slice(2);meshes.set(current,[])} else if(row.startsWith('f '))meshes.get(current).push(row.slice(2).split(/\s+/).map(Number)); } assert.deepEqual(Array.from(meshes.keys()),['바닥판','벽체']); for(const [name,faces] of meshes){ const edges=new Map(); for(const face of faces)for(let i=0;i0,`${name} faces should be exported`); assert.ok([...edges.values()].every(count=>count===2),`${name} mesh has an open or duplicated edge`); } });