diff --git a/tests/verification.test.mjs b/tests/verification.test.mjs new file mode 100644 index 0000000..965eaeb --- /dev/null +++ b/tests/verification.test.mjs @@ -0,0 +1,86 @@ +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`); + } +});