142 lines
8.9 KiB
JavaScript
142 lines
8.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
|
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
export const relationIds=['BIM:desktop:BIM_Ungroup','BIM:desktop:BIM_Unclone'];
|
|
export async function runRelationCases(kernel, ids) {
|
|
if (!ids.length) return [];
|
|
const out=fs.mkdtempSync(path.join(os.tmpdir(),'freecad-bim-relations-'));
|
|
const results=[];
|
|
const python=source=>{
|
|
const value=JSON.parse(kernel.ccall('fc_web_bim_test_python','string',['string'],[
|
|
'import FreeCAD as App, Part, json\n'+source]));
|
|
if(value.ok===false) throw Error(value.error);
|
|
return value;
|
|
};
|
|
try {
|
|
execFileSync(path.join(root,'web/node_modules/.bin/tsc'),['--target','ES2022','--module','ESNext','--moduleResolution','Bundler',
|
|
'--strict','--skipLibCheck','--outDir',out,path.join(root,'web/src/bim-python-family-batch-facade.ts'),
|
|
path.join(root,'web/src/bim-desktop-adapter.ts')],{cwd:root,stdio:'pipe'});
|
|
const facade=await import(pathToFileURL(path.join(out,'bim-python-family-batch-facade.js')));
|
|
const adapter=await import(pathToFileURL(path.join(out,'bim-desktop-adapter.js')));
|
|
const {FacadeSerialQueue}=await import(pathToFileURL(path.join(out,'facade-envelope.js')));
|
|
const queue=new FacadeSerialQueue();
|
|
for(const capabilityId of ids) {
|
|
const command=capabilityId.split(':').at(-1), document='RelationCase', other='RelationOther';
|
|
const record={id:capabilityId,module:'BIM',command,sourcePath:`src/Mod/BIM/bimcommands/${command==='BIM_Ungroup'?'BimUngroup':'BimUnclone'}.py`,sourceLine:1};
|
|
const row={capabilityId,status:'failed',cases:[]};results.push(row);
|
|
let sequence=0;
|
|
let selectedObject='Selected', sourceObject='SourceWall';
|
|
const invoke=async overrides=>{
|
|
const args=adapter.prepareBimDesktopArgs(record,{documentId:document,action:'apply',selection:[{object:selectedObject}],...overrides});
|
|
const request=facade.createBimPythonFamilyBatchRequest(`relation-${command}-${++sequence}`,[{apiId:capabilityId,name:command,
|
|
family:'desktop-command',sourcePath:record.sourcePath,sourceLine:1,args}],'tier-2-pure');
|
|
return adapter.validateBimDesktopResponse(record,await facade.dispatchBimPythonFamilyBatch(queue,request,kernel),capabilityId);
|
|
};
|
|
const snapshot=()=>python(`
|
|
d=App.getDocument('${document}')
|
|
result=json.dumps({'active':App.ActiveDocument.Name,'objects':[
|
|
{'name':o.Name,'label':o.Label,'group':[c.Name for c in o.Group] if 'Group' in o.PropertiesList else None,
|
|
'clone':o.CloneOf.Name if 'CloneOf' in o.PropertiesList and o.CloneOf else None,
|
|
'volume':o.Shape.Volume if o.isDerivedFrom('Part::Feature') else None,
|
|
'position':list(o.Placement.Base) if hasattr(o,'Placement') else None}
|
|
for o in d.Objects], 'otherVolume':App.getDocument('${other}').getObject('Independent').Shape.Volume})`);
|
|
const add=(input,actual,name)=>row.cases.push({input,actual,assertions:[{name,passed:true}]});
|
|
try {
|
|
python(`
|
|
for name in ['${document}','${other}']:
|
|
if name in App.listDocuments(): App.closeDocument(name)
|
|
d=App.newDocument('${document}'); d.UndoMode=1
|
|
first=d.addObject('Part::Box','Selected'); first.Length=2; first.Width=3; first.Height=4
|
|
second=d.addObject('Part::Box','Unselected'); second.Length=3; second.Width=4; second.Height=5
|
|
group=d.addObject('App::DocumentObjectGroup','Group'); group.addObject(first); group.addObject(second)
|
|
secondGroup=d.addObject('App::DocumentObjectGroup','OtherGroup')
|
|
d.recompute()
|
|
other=App.newDocument('${other}'); other.addObject('Part::Box','Independent'); other.recompute()
|
|
result=json.dumps({'ready':True})`);
|
|
if(command==='BIM_Unclone') {
|
|
const created=python(`
|
|
import Arch
|
|
d=App.getDocument('${document}'); App.setActiveDocument(d.Name)
|
|
d.removeObject('Selected'); d.removeObject('Unselected')
|
|
source=Arch.makeWall(length=12,width=3,height=4,name='SourceWall')
|
|
clone=Arch.makeWall(length=12,width=3,height=4,name='Selected')
|
|
clone.CloneOf=source; clone.Placement.Base=App.Vector(10,20,30)
|
|
d.getObject('Group').addObject(clone); d.recompute()
|
|
App.setActiveDocument('${other}')
|
|
result=json.dumps({'clone':clone.Name,'source':source.Name})`);
|
|
selectedObject=created.clone;sourceObject=created.source;
|
|
}
|
|
const original=snapshot();
|
|
if(command==='BIM_Ungroup') {
|
|
const preview=await invoke({action:'read'});
|
|
assert.equal(preview.kind,'result',JSON.stringify(preview));
|
|
assert.deepEqual(snapshot(),original);
|
|
assert.equal(preview.result.records[0].result.memberships.length,1);
|
|
add({command,action:'read'},preview,'read reports native membership without mutating either document');
|
|
}
|
|
const reply=await invoke({});assert.equal(reply.kind,'result',JSON.stringify(reply));
|
|
const after=snapshot();
|
|
assert.equal(after.active,original.active);assert.equal(after.otherVolume,original.otherVolume);
|
|
if(command==='BIM_Ungroup') {
|
|
assert.deepEqual(after.objects.find(o=>o.name==='Group').group,['Unselected']);
|
|
assert.deepEqual(after.objects.find(o=>o.name==='OtherGroup').group,[]);
|
|
assert.deepEqual(after.objects.filter(o=>o.volume!==null),original.objects.filter(o=>o.volume!==null));
|
|
} else {
|
|
const clone=after.objects.find(o=>o.name===selectedObject);
|
|
assert.equal(clone.clone,null);assert.ok(Math.abs(clone.volume-144)<1e-6);assert.deepEqual(clone.position,[10,20,30]);
|
|
assert.deepEqual(after.objects.find(o=>o.name==='Group').group,[selectedObject]);
|
|
}
|
|
add({command,action:'apply',selection:[selectedObject]},{reply,original,after},'native relationship changes preserve geometry, unrelated objects and the active document');
|
|
python(`App.getDocument('${document}').undo(); result=json.dumps({'undo':True})`);
|
|
assert.deepEqual(snapshot(),original);
|
|
python(`App.getDocument('${document}').redo(); result=json.dumps({'redo':True})`);
|
|
assert.deepEqual(snapshot(),after);
|
|
add({command,action:'undo-redo'},snapshot(),'native undo and redo restore exact relationships and geometry');
|
|
for(const invalid of [{selection:[{object:'Missing'}]},{selection:[{object:selectedObject,documentId:other}]},{activeTask:true}]) {
|
|
const before=snapshot(); const rejected=await invoke(invalid);
|
|
assert.equal(rejected.kind,'error',JSON.stringify(rejected));assert.deepEqual(snapshot(),before);
|
|
add({command,...invalid},rejected,'invalid input is rejected without changing either document');
|
|
}
|
|
const saved=python(`
|
|
import os, shutil
|
|
d=App.getDocument('${document}'); d.saveAs('/tmp/RelationSaved.FCStd')
|
|
shutil.copyfile('/tmp/RelationSaved.FCStd','/tmp/RelationRestored.FCStd')
|
|
r=App.openDocument('/tmp/RelationRestored.FCStd'); r.recompute()
|
|
result=json.dumps({'bytes':os.path.getsize('/tmp/RelationSaved.FCStd'), 'objects':[o.Name for o in r.Objects],
|
|
'members':[o.Name for o in r.getObject('Group').Group], 'independent':r.Name!=d.Name})
|
|
App.closeDocument(r.Name); os.remove('/tmp/RelationSaved.FCStd'); os.remove('/tmp/RelationRestored.FCStd'); App.setActiveDocument('${other}')`);
|
|
assert.ok(saved.bytes>100);assert.deepEqual(saved.objects,after.objects.map(o=>o.name));
|
|
assert.equal(saved.independent,true);
|
|
assert.deepEqual(saved.members,after.objects.find(o=>o.name==='Group').group);
|
|
add({command,action:'save-reopen'},saved,'FCStd persists the resulting native relationships');
|
|
if(command==='BIM_Unclone') {
|
|
const independent=python(`
|
|
d=App.getDocument('${document}')
|
|
d.getObject(${JSON.stringify(sourceObject)}).Width=6; d.recompute()
|
|
result=json.dumps({'sourceVolume':d.getObject(${JSON.stringify(sourceObject)}).Shape.Volume,'cloneVolume':d.getObject(${JSON.stringify(selectedObject)}).Shape.Volume,
|
|
'clonePosition':list(d.getObject(${JSON.stringify(selectedObject)}).Placement.Base)})`);
|
|
assert.ok(Math.abs(independent.sourceVolume-288)<1e-6);assert.ok(Math.abs(independent.cloneVolume-144)<1e-6);
|
|
assert.deepEqual(independent.clonePosition,[10,20,30]);
|
|
add({command,action:'edit-source-after-unclone',width:6},independent,'uncloned geometry stays independent after source changes');
|
|
}
|
|
row.status='passed';
|
|
} catch(error) {
|
|
row.error=String(error.stack??error);
|
|
row.cases.push({input:{command,phase:'relation-workflow'},assertions:[{name:'relationship workflow completes',passed:false,actual:row.error}]});
|
|
}
|
|
finally {
|
|
try {python(`
|
|
for name in ['${document}','${other}']:
|
|
if name in App.listDocuments(): App.closeDocument(name)
|
|
result=json.dumps({'closed':True})`);} catch(error){row.status='failed';row.error=String(error);}
|
|
}
|
|
}
|
|
} finally {fs.rmSync(out,{recursive:true,force:true});}
|
|
return results;
|
|
}
|