Files
catchpit2Dto3D/tests/verification.test.mjs
T

87 lines
5.1 KiB
JavaScript

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;i<face.length;i++){
const a=face[i],b=face[(i+1)%face.length],key=a<b?`${a}:${b}`:`${b}:${a}`;
edges.set(key,(edges.get(key)||0)+1);
}
assert.ok(faces.length>0,`${name} faces should be exported`);
assert.ok([...edges.values()].every(count=>count===2),`${name} mesh has an open or duplicated edge`);
}
});