Save verified native IFC import and user validation evidence

Real IFC2X3/IFC4 import tested. Full BIM authoring/export remains out of scope; 349-second building import performance remains to optimize.
This commit is contained in:
2026-09-16 04:27:37 +08:00
parent 9a7c4f0335
commit 72152ef356
120 changed files with 3020296 additions and 114 deletions

View File

@@ -0,0 +1,47 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
const dependencies=path.join(root,'freecad-source/build-web');
const source=process.env.FREECAD_IFC_SOURCE;
if(!source||!fs.existsSync(path.join(source,'cmake/CMakeLists.txt')))throw Error('Set FREECAD_IFC_SOURCE to a real IfcOpenShell source tree');
const build=path.resolve(process.env.FREECAD_IFC_BUILD??path.join(dependencies,'ifcopenshell-native/build'));
const output=path.join(root,'docs/current/user-tests-20260915',`ifc-build-${new Date().toISOString().replace(/[:.]/g,'-')}`);
fs.mkdirSync(output,{recursive:true});
const stages=[];
async function run(stage,command,args,timeout=1800000){
const started=performance.now(),log=fs.openSync(path.join(output,`${stage}.log`),'w');
const child=spawn(command,args,{cwd:root,stdio:['ignore',log,log],detached:true});
let timedOut=false;
const timer=setTimeout(()=>{timedOut=true;process.kill(-child.pid,'SIGTERM');},timeout);
const code=await new Promise(resolve=>{child.once('close',resolve);child.once('error',()=>resolve(-1));});
clearTimeout(timer);fs.closeSync(log);
stages.push({stage,command,args,exitCode:code,timedOut,seconds:(performance.now()-started)/1000});
fs.writeFileSync(path.join(output,'results.json'),JSON.stringify({source,build,stages},null,2)+'\n');
console.log(JSON.stringify(stages.at(-1)));
if(code!==0)throw Error(`${stage} failed; see ${output}`);
}
await run('configure','cmake',['-S',path.join(source,'cmake'),'-B',build,'-G','Ninja',
'-DCMAKE_TOOLCHAIN_FILE=/usr/share/emscripten/cmake/Modules/Platform/Emscripten.cmake',
'-DCMAKE_BUILD_TYPE=Release','-DCMAKE_CXX_FLAGS=-pthread -fexceptions','-DCMAKE_C_FLAGS=-pthread',
'-DCMAKE_CXX_FLAGS_RELEASE=-O1 -DNDEBUG','-DWASM_BUILD=ON','-DBUILD_SHARED_LIBS=OFF',
'-DBUILD_IFCPYTHON=OFF','-DBUILD_CONVERT=OFF','-DBUILD_GEOMSERVER=OFF','-DBUILD_IFCGEOM=ON',
'-DWITH_OPENCASCADE=ON','-DWITH_CGAL=OFF','-DCOLLADA_SUPPORT=OFF','-DHDF5_SUPPORT=OFF','-DIFCXML_SUPPORT=OFF',
'-DSCHEMA_VERSIONS=2x3;4','-DUSE_CCACHE=OFF','-DNO_WARN=ON',
`-DOCC_INCLUDE_DIR=${dependencies}/occt-pthread-install/include/opencascade`,
`-DOCC_LIBRARY_DIR=${dependencies}/occt-pthread-install/lib`,
`-DBOOST_ROOT=${dependencies}/boost-pthread-install`,'-DBoost_NO_SYSTEM_PATHS=ON',
'-DEigen3_DIR=/usr/share/eigen3/cmake','-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER',
'-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=NEVER','-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=NEVER']);
await run('compile','cmake',['--build',build,'--target','IfcParse','IfcGeom','geometry_kernel_opencascade','-j2']);
const artifacts=['ifcparse/libIfcParse.a','ifcgeom/libIfcGeom.a','ifcgeom/kernels/libgeometry_kernel_opencascade.a'].map(name=>{
const file=path.join(build,name);return {file,size:fs.statSync(file).size,sha256:createHash('sha256').update(fs.readFileSync(file)).digest('hex')};
});
fs.writeFileSync(path.join(output,'artifacts.json'),JSON.stringify(artifacts,null,2)+'\n');
await run('host-configure','cmake',['-S','freecad-source','-B',path.join(dependencies,'configure'),
`-DFREECAD_WEB_IFCOPENSHELL_SOURCE=${path.resolve(source)}`,`-DFREECAD_WEB_IFCOPENSHELL_BUILD=${build}`]);
await run('host-link','python3',['tools/build_web_module_artifacts.py','--target','FreeCADWebPartBoxProbe','--jobs','2','--timeout','600']);
console.log(output);

View File

@@ -210,6 +210,8 @@ const remainingCases = createRemainingCases({ root, output, fixture, step, open,
try {
await setup();
if (process.env.FREECAD_MANUAL_IDS?.split(',').includes('FIX-IFC-REOPEN'))
await test('FIX-IFC-REOPEN','IFC 原生文档保存重开回归',remainingCases['FIX-IFC-REOPEN']);
await test('UI-01','首次启动与刷新',async()=>{
await step('打开隔离测试站点',()=>open(),'React 已挂载,跨源隔离生效');
await step('检查树、属性、视口和 OPFS',async()=>{const s=await evaluate("({tree:!!document.querySelector('[role=tree]'),properties:!!document.querySelector('[aria-label=\"Object Properties\"]'),canvas:!!document.querySelector('canvas'),opfs:typeof navigator.storage.getDirectory==='function'})");check(Object.values(s).every(Boolean),'Missing main workspace region');return s;},'主工作区完整,不以路由登记代替运行结果');

View File

@@ -13,6 +13,8 @@ const git = (args, cwd = native, input) => execFileSync('git', args, {
cwd, env, input, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
}).trim();
const parent = git(['rev-parse', 'HEAD']);
const previousSnapshot = process.env.FREECAD_SNAPSHOT_PARENT;
const commitParent = previousSnapshot ? git(['rev-parse', '--verify', `${previousSnapshot}^{commit}`]) : parent;
const branch = `workspace-snapshot-${new Date().toISOString().replace(/[:.]/g, '-')}`;
const excluded = new Set(['node_modules', '.git', '__pycache__', 'dist', 'offline-package',
'continuation-runs', 'periodic-optimization', 'scheduler-bin']);
@@ -31,6 +33,7 @@ function addFiles(relative, predicate = () => true) {
}
addFiles('web/src'); addFiles('web/tools'); addFiles('web/public/api');
addFiles('web/public/samples');
for (const name of ['ifcopenshell-notice.txt', 'ifcopenshell-COPYING.txt', 'ifcopenshell-COPYING.LESSER.txt']) addFiles(`web/public/${name}`);
for (const name of fs.readdirSync(path.join(root, 'web'))) {
if (/\.(html|json|ts)$/.test(name)) addFiles(`web/${name}`);
}
@@ -79,10 +82,10 @@ const unique = [...new Set(entries)].sort();
for (let index = 0; index < unique.length; index += 150)
git([`--git-dir=${gitDir}`, `--work-tree=${root}`, 'add', '-f', '--', ...unique.slice(index, index + 150)], root);
const tree = git(['write-tree']);
const commit = git(['commit-tree', tree, '-p', parent], native,
'Save FreeCAD Web workspace sources and user validation evidence\n\nIFC native binding remains incomplete; preserve accurate test results.\n');
const commit = git(['commit-tree', tree, '-p', commitParent], native,
'Save verified native IFC import and user validation evidence\n\nReal IFC2X3/IFC4 import tested. Full BIM authoring/export remains out of scope; 349-second building import performance remains to optimize.\n');
git(['update-ref', `refs/heads/${branch}`, commit, '0000000000000000000000000000000000000000']);
const receipt = { status: 'committed_locally', branch, commit, parent, sourceTree, externalFiles: unique.length,
const receipt = { status: 'committed_locally', branch, commit, parent: commitParent, sourceBase: parent, sourceTree, externalFiles: unique.length,
nativeChangedFiles: nativeChanged.length, remote: 'https://gitea.meswork.com/wangdequan/FreeCADWasmDev.git',
omissions: ['WASM/build artifacts', 'dependency sources', 'node_modules', 'agent/session logs', 'credentials', 'RAM-backed IfcOpenShell source'],
primaryIndexPreserved: true, mainPreserved: true };

View File

@@ -50,13 +50,14 @@ const resolutionChecks = {
'UT-002': ['FIX-GROUP'],
'UT-003': ['DOC-01'],
'UT-004': [...modules.map(m=>`UI-03-${m.module}`),'FIX-WINDOW'],
'UT-005': ['FILE-02'],
'UT-005': ['FILE-02','FIX-IFC-REOPEN'],
};
for (const defect of defects) {
const checks = resolutionChecks[defect.id];
defect.status = checks.every(id=>latest.get(id)?.status==='passed'
&& (defect.id!=='UT-004'||id==='FIX-WINDOW'||latest.get(id)?.steps.some(s=>s.name==='窗口宿主初始化及启动错误'&&s.status==='passed'))) ? 'resolved_by_retest' : 'open';
defect.retestCases = checks;
if (defect.id === 'UT-005') defect.resolutionScope = 'IfcOpenShellNative real IFC2X3/IFC4 import and FCStd persistence only; full ifcopenshell Python authoring/export API remains outside this closure.';
defect.retestEvidence = checks.flatMap(evidenceFor);
// Keep the original failure separate from newer passing screenshots.
defect.originalEvidence = [];
@@ -75,10 +76,10 @@ const short = s=>String(s??'').replaceAll('|','/').replaceAll('\n',' ').slice(0,
const lines=['# 用户手册测试总报告','',`生成时间:${new Date().toISOString()}`,'',
`手册 ${manual.length} 项:通过 ${counts.passed};失败 ${counts.failed};阻塞/覆盖不足 ${counts.blocked};未运行 ${counts.not_run}。`,
'',`实际工作台入口 ${modules.length} 个,通过 ${modules.filter(m=>m.result?.status==='passed').length} 个。不得将路由登记 1452/1452 或独立演示页烟测作为上线验收。`,
'', '## 上线结论','', `**当前不具备完整上线放行依据。** 登记缺陷 ${defects.length} 个,${defects.filter(d=>d.status==='resolved_by_retest').length} 个已通过针对性复测。手册尚未通过:${coverage.filter(c=>c.status!=='passed').map(c=>c.id+' '+c.status).join(';')||'无'}。本机性能样本通过不等于生产容量认证,入口可用不代表全部业务命令已经验收。`,
'', '## 上线结论','', `**当前不具备完整上线放行依据。** 登记缺陷 ${defects.length} 个,${defects.filter(d=>d.status==='resolved_by_retest').length} 个已通过针对性复测。手册尚未通过:${coverage.filter(c=>c.status!=='passed').map(c=>c.id+' '+c.status).join(';')||'无'}。本机性能样本通过不等于生产容量认证,入口可用不代表全部业务命令已经验收。IFC 导入通过仅指 IfcOpenShellNative 真实导入绑定,不代表完整 BIM 编辑/导出 API。`,
'', '## 手册逐项覆盖','', '| 编号 | 项目 | 状态 | 依据 |','|---|---|---|---|',
...coverage.map(c=>`| ${c.id} | ${c.title} | ${c.status} | ${short(c.detail)} ${c.evidence[0]?`[过程](${c.evidence[0]})`:''} |`),
'', '## 缺陷与修复复测','',...defects.flatMap(d=>[`### ${d.id} ${d.title} (${d.severity})`,'',`当前状态:${d.status}`,'',`原始问题:${d.reason}`,'',...d.steps.map((s,i)=>`${i+1}. ${s}`),'',...d.originalEvidence.map((p,i)=>`[原始证据 ${i+1}](${p})`),'',...d.retestEvidence.map((p,i)=>`[修复复测 ${i+1}](${p})`),'']),
'', '## 缺陷与修复复测','',...defects.flatMap(d=>[`### ${d.id} ${d.title} (${d.severity})`,'',`当前状态:${d.status}`,'',`原始问题:${d.reason}`,'',d.resolutionScope??'',...d.steps.map((s,i)=>`${i+1}. ${s}`),'',...d.originalEvidence.map((p,i)=>`[原始证据 ${i+1}](${p})`),'',...d.retestEvidence.map((p,i)=>`[修复复测 ${i+1}](${p})`),'']),
'## 批次与耗时','',...runs.map(run=>{const r=JSON.parse(fs.readFileSync(path.join(directory,run,'results.json')));return `- [${run}](${run}/report.md):${r.results.length} 条案例记录,累计案例耗时 ${r.results.reduce((n,x)=>n+(x.seconds??0),0).toFixed(1)} 秒。`; }),
'', '## 解释与限制','',
'- 同一手册项目以最新批次为准,旧失败和复测证据均保留。旧 summary.tsv 不纳入本报告。',

View File

@@ -0,0 +1,42 @@
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import { loadWasmProbe } from './wasm-probe-runtime.mjs';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
const output=path.join(root,'docs/current/user-tests-20260915',`ifc-contract-${new Date().toISOString().replace(/[:.]/g,'-')}`);
fs.mkdirSync(output,{recursive:true});
const log=s=>fs.appendFileSync(path.join(output,'native.log'),s+'\n');
const results=[];
try {
const {kernel}=await loadWasmProbe(path.join(root,'freecad-source/build-web/configure/bin'),'FreeCADWebPartBoxProbe',{print:log,printErr:log});
const call=(command,parameters,document='Contract')=>{
const reply=JSON.parse(kernel.ccall('fc_web_dispatch_json','string',['string'],[JSON.stringify({cmd:'part.viewCommand',args:{schema:'freecad-web/document-view/v1',owner:'PartBox',document,command,parameters:{...parameters,documentId:document}}})]));
results.push({command,reply});fs.writeFileSync(path.join(output,'results.json'),JSON.stringify(results,null,2)+'\n');assert.equal(reply.ok,true,JSON.stringify(reply));return reply.result;
};
for(const name of ['ColumnPSetsOfSets.ifc']) {
const bytes=fs.readFileSync(path.join(root,'freecad-source/build-web/ifcopenshell-native/fixtures',name));
const value=call('Std_Open',{operation:'open',files:[{filename:name,fileData:bytes.toString('base64')}]});
assert.equal(value.errors.length,0,JSON.stringify(value.errors));assert.ok(value.opened[0].documents[0].objectCount>0);
const doc=value.activeDocument;
const read=call('Std_CommandLine',{operation:'submit',source:`import json; print(json.dumps({'count':len(App.getDocument('${doc}').Objects),'properties':[(o.Name,o.IfcPropertySets) for o in App.getDocument('${doc}').Objects if 'IfcPropertySets' in o.PropertiesList],'volumes':[o.Shape.Volume for o in App.getDocument('${doc}').Objects if o.isDerivedFrom('Part::Feature')]}))`},doc);
assert.equal(read.stderr,'');const state=JSON.parse(read.stdout.trim());assert.ok(state.volumes.some(v=>v>0));
if(name==='ColumnPSetsOfSets.ifc') {
assert.ok(state.properties.length>0,'Property set not mapped');
const columnSets=JSON.parse(state.properties.find(([id])=>id==='Ifc_93')[1]).map(s=>s.Name);
assert.ok(columnSets.includes('PSet_1')&&columnSets.includes('PSet_2'),'IFC4 property-definition set was not expanded');
assert.ok(Math.abs(state.volumes[0]-30862141.083286542)<1,'Column geometry changed or units were scaled incorrectly');
}
fs.writeFileSync(path.join(output,name+'.json'),JSON.stringify(state,null,2)+'\n');
}
const before=call('Std_CommandLine',{operation:'submit',source:'import json; print(json.dumps(sorted(App.listDocuments())))'});
const library=call('Std_Open',{operation:'open',files:[{filename:'wall.ifc',fileData:fs.readFileSync(path.join(root,'freecad-source/build-web/ifcopenshell-native/fixtures/wall.ifc')).toString('base64')}]});
assert.equal(library.errors.length,1,'Type library without product geometry must not report a successful import');
const bad=call('Std_Open',{operation:'open',files:[{filename:'invalid.ifc',fileData:Buffer.from('this is not an IFC file').toString('base64')}]});
assert.equal(bad.errors.length,1);
const after=call('Std_CommandLine',{operation:'submit',source:'import json; print(json.dumps(sorted(App.listDocuments())))'});
assert.equal(after.stdout,before.stdout,'Failed IFC open modified existing documents');
fs.writeFileSync(path.join(output,'summary.json'),JSON.stringify({status:'passed',fixtures:2,invalidFileRejected:true,documentsPreserved:true},null,2)+'\n');
console.log(JSON.stringify({status:'passed',output}));
}catch(error){fs.writeFileSync(path.join(output,'failure.json'),JSON.stringify({error:String(error)},null,2)+'\n');console.error(String(error));process.exitCode=1;}
process.exit(process.exitCode??0);

View File

@@ -0,0 +1,35 @@
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import { loadWasmProbe } from './wasm-probe-runtime.mjs';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
const output=path.join(root,'docs/current/user-tests-20260915',`ifc-native-${new Date().toISOString().replace(/[:.]/g,'-')}`);
fs.mkdirSync(output,{recursive:true});
const fixture=path.join(root,'FreeCADBaseLib/sources/src/Qt-5.15.13/qt3d/src/3rdparty/assimp/src/test/models/IFC/AC14-FZK-Haus.ifc');
const log=message=>fs.appendFileSync(path.join(output,'native.log'),String(message)+'\n');
const results=[];
console.log(output);
try {
const {kernel}=await loadWasmProbe(path.join(root,'freecad-source/build-web/configure/bin'),'FreeCADWebPartBoxProbe',{print:log,printErr:log});
const dispatch=(command,parameters,document='IfcValidation')=>{
const started=performance.now();
const reply=JSON.parse(kernel.ccall('fc_web_dispatch_json','string',['string'],[JSON.stringify({cmd:'part.viewCommand',args:{schema:'freecad-web/document-view/v1',owner:'PartBox',document,command,parameters:{...parameters,documentId:document}}})]));
results.push({command,seconds:(performance.now()-started)/1000,reply});
fs.writeFileSync(path.join(output,'results.json'),JSON.stringify(results,null,2)+'\n');
console.log(JSON.stringify({command,ok:reply.ok,error:reply.error,seconds:results.at(-1).seconds}));
assert.equal(reply.ok,true,JSON.stringify(reply));return reply.result;
};
const opened=dispatch('Std_Open',{operation:'open',files:[{filename:'AC14-FZK-Haus.ifc',fileData:fs.readFileSync(fixture).toString('base64')}]});
assert.equal(opened.errors.length,0,JSON.stringify(opened.errors));
assert.ok(opened.opened[0].documents[0].objectCount>20);
const doc=opened.activeDocument;
const inspect=dispatch('Std_CommandLine',{operation:'submit',source:`import json; print(json.dumps({'objects':len(App.getDocument('${doc}').Objects),'shapes':sum(1 for o in App.getDocument('${doc}').Objects if hasattr(o,'Shape') and not o.Shape.isNull()),'groups':sum(1 for o in App.getDocument('${doc}').Objects if hasattr(o,'Group')),'properties':sum(1 for o in App.getDocument('${doc}').Objects if 'IfcPropertySets' in o.PropertiesList),'volume':sum(o.Shape.Volume for o in App.getDocument('${doc}').Objects if hasattr(o,'Shape'))}))`},doc);
assert.equal(inspect.stderr,'');
const state=JSON.parse(inspect.stdout.trim());assert.ok(state.shapes>20);assert.ok(state.groups>=3);assert.ok(state.volume>0);
const saved=dispatch('Std_SaveAs',{filename:'/tmp/IfcValidation.FCStd'},doc);
assert.ok(saved.files[0].contentsBase64.length>1000);
fs.writeFileSync(path.join(output,'IfcValidation.FCStd'),Buffer.from(saved.files[0].contentsBase64,'base64'));
fs.writeFileSync(path.join(output,'summary.json'),JSON.stringify({status:'passed',state,scope:'Native IFC import and FCStd save; not browser acceptance'},null,2)+'\n');
} catch(error) {fs.writeFileSync(path.join(output,'failure.json'),JSON.stringify({status:'failed',error:String(error)},null,2)+'\n');console.error(String(error));process.exitCode=1;}
process.exit(process.exitCode??0);

View File

@@ -44,7 +44,7 @@ export function createRemainingCases(h) {
return until('Native open response',"window.__freecadWorkbenchState?.response",r=>{
if(r?.kind==='error')throw Error(JSON.stringify(r.error));
return r?.kind==='result';
},60000);
},180000);
}
const downloadSnapshot=()=>Object.fromEntries(fs.readdirSync(path.join(output,'downloads')).map(n=>[n,fs.statSync(path.join(output,'downloads',n)).mtimeMs]));
async function downloadedFile(extension,previous={}) {
@@ -68,7 +68,7 @@ export function createRemainingCases(h) {
await h.input('命令 JSON 参数',JSON.stringify(args));
await until('Selected command ready',"[...document.querySelectorAll('button')].some(b=>b.textContent==='执行选中命令'&&!b.disabled)");
await button('执行选中命令');
const response=await until('Native command result',"window.__freecadWorkbenchState?.response",r=>{if(r?.kind==='error')throw Error(JSON.stringify(r.error));return r?.kind==='result';},60000);
const response=await until('Native command result',"window.__freecadWorkbenchState?.response",r=>{if(r?.kind==='error')throw Error(JSON.stringify(r.error));return r?.kind==='result';},180000);
const row=response.result?.records?.[0];
if(row&&'status' in row)check(['success','exposed','ok'].includes(row.status),JSON.stringify(row));
const result=row?.result??response.result;
@@ -96,6 +96,22 @@ export function createRemainingCases(h) {
const r=await openResult(),v=r.result.records[0].result;check(!v.sceneError&&v.errors.length===0,'Restore failed');return v.activeDocument;
}
const cases={
'FIX-IFC-REOPEN':async()=>{
let documentId;
await step('打开实际 IFC 导入后保存的 FCStd',async()=>{
const file=path.join(root,'docs/current/user-tests-20260915/ifc-native-2026-09-15T15-50-07-990Z/IfcValidation.FCStd');
await open('/workbench.html?module=Web&command=Std_Open');await chooseLocalFile(file);
const response=await openResult(),value=response.result.records[0].result;
check(value.errors.length===0&&!value.sceneError,'FCStd restore or scene failed');
documentId=value.activeDocument;
await until('Restored native mesh viewport',"Number(document.querySelector('[data-native-mesh-count]')?.dataset.nativeMeshCount)",n=>n>20,180000);
return {documentId,documents:value.opened[0].documents,meshCount:value.scene?.meshes.length};
},'147 个对象恢复,空间层级及原生几何仍存在,视口非空');
await step('核对恢复后实体及原始 IFC 附件',async()=>{
const value=await inspect(documentId,`{'groups':sum(1 for o in App.getDocument('${documentId}').Objects if o.isDerivedFrom('App::DocumentObjectGroup')),'solids':sum(1 for o in App.getDocument('${documentId}').Objects if o.isDerivedFrom('Part::Feature') and not o.Shape.isNull()),'source':App.getDocument('${documentId}').getObject('IfcSource').OriginalIFC,'ids':sum(1 for o in App.getDocument('${documentId}').Objects if 'IfcGlobalId' in o.PropertiesList)}`);
check(value.groups>=3&&value.solids>20&&value.source&&value.ids>20,'IFC provenance or native objects not restored');return value;
},'实体、空间分组、IFC GlobalId 和内嵌源文件保留');
},
'GEO-01':async()=>{
await step('创建 2 x 3 x 4 Box',async()=>{await open();return box();},'1 个对象,原生体积约 24');
await step('读取原生包围盒属性',async()=>{const bounds=await evaluate("JSON.parse(document.querySelector('[data-native-bounds]').dataset.nativeBounds)");check(bounds.length===6&&bounds.every((n,i)=>Math.abs(n-[0,0,0,2,3,4][i])<1e-5),'OCCT bounds differ');return {bounds,volume:await volume(24),tolerance:1e-5};},'原生返回 [0,0,0]-[2,3,4],容差 1e-5 mm');
@@ -132,7 +148,7 @@ export function createRemainingCases(h) {
['ifc','FreeCADBaseLib/sources/src/Qt-5.15.13/qt3d/src/3rdparty/assimp/src/test/models/IFC/AC14-FZK-Haus.ifc'],
])await step(`${format.toUpperCase()} 实际注册能力及文件处理`,async()=>{
await open('/workbench.html?module=Web&command=Std_Open');await chooseLocalFile(path.join(root,p));
const response=await until('File handled',"window.__freecadWorkbenchState?.response",r=>r?.kind==='result'||r?.kind==='error',60000);
const response=await until('File handled',"window.__freecadWorkbenchState?.response",r=>r?.kind==='result'||r?.kind==='error',format==='ifc'?600000:60000);
if(response.kind==='error') {
const error=response.error;
check(error.code==='file_open_failed'&&error.diagnostic?.openResult?.errors?.length,'Unstructured format failure');
@@ -141,6 +157,12 @@ export function createRemainingCases(h) {
}
const value=response.result.records[0].result;
check(value.opened.length>0&&!value.sceneError,'Imported document or display missing');
if(format==='ifc') {
check(value.opened[0].documents[0].objectCount>20,'IFC import returned no real building document');
check(!value.opened[0].documents[0].partialRestore&&!value.opened[0].documents[0].restoreError,'IFC restore incomplete');
value.testScope='Real IfcOpenShellNative import-only binding. Not the full ifcopenshell Python authoring/export API.';
await until('IFC meshes rendered',"({count:Number(document.querySelector('[data-native-mesh-count]')?.dataset.nativeMeshCount),canvas:!!document.querySelector('canvas')})",s=>s.count>20&&s.canvas,180000);
}
report.push({format,file:p,sha256:hash(path.join(root,p)),supported:true,imported:true,result:value});json('format-validation.json',report);return value;
},'支持则完整导入;依赖或登记缺口记录失败,不伪装支持');
},