Files
HHY c2c6359a65 fix: v2.11 3D 全开/全关状态错误根治——分割树重放语义破译与扇五金解析重建
- polyId 语义破译: {idx,pos}=(分裂子树节点id,子区域序), 节点id=sp 0基序号, child0=法线指向侧
- replayShape 重写为树重放(锚点兜底), 704/31594/31729 全引用精确命中官方平面图, 798 扇矩形 100% 有效
- 坐标系修正: 模型 y 向下, 3D 翻转 y, 布局与原图一致
- 扇五金解析: Slide/DoubleSash=推拉, hm.openDirection 定铰链侧(含上悬), hm.openToward 定内外开, od=全开幅
- 移除错误的滑扇附着兜底; 2D 编辑器写回 polyId 同步树寻址
- 验证: 全关/46%/100% 三态截图 + ui-shots 6 页 + validate3d 全量 0 无效
2026-09-23 04:09:40 -07:00

71 lines
3.6 KiB
JavaScript

// 3D 几何完整性验证: 先 npx esbuild src/three/profileGeo.ts src/types/drawing.ts --format=cjs --platform=node --outfile=.val/编译产物, 再运行本脚本
// 3D 功能完整性验证: 对全部 254 套样板执行 3D 构建管线, 逐网格检测 NaN/有限值
const fs = require('fs');
const { profileGeo, geometryIsValid } = require('./.val/pg.cjs');
const { parseDrawing, inferSashGlyphs, replayShape } = require('./.val/drawing.cjs');
const THREE = require('./.val/pg.cjs').THREE || require('../node_modules/three/build/three.cjs');
const files = fs.readdirSync('D:/Code/huamenchuang/data/windoor-drawings')
.filter(f => f.endsWith('.dxf') === false && f.endsWith('.json'));
let fail = 0, total = 0, totalMesh = 0, sashCount = 0, badList = [];
for (const f of files) {
const raw = JSON.parse(fs.readFileSync('D:/Code/huamenchuang/data/windoor-drawings/' + f, 'utf8')).drawing_data;
let dd;
try { dd = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { fail++; badList.push([f, 'json']); continue; }
total++;
// 模拟 WindoorThreed.build 的网格生成
const pts = [];
for (const s of dd.sm) {
for (const r of replayShape(s).regions)
for (const seg of r) {
if (Number.isFinite(seg.ps?.x) && Number.isFinite(seg.ps?.y)) pts.push(seg.ps);
if (Number.isFinite(seg.pe?.x) && Number.isFinite(seg.pe?.y)) pts.push(seg.pe);
}
}
if (!pts.length) continue;
const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
const gw = Math.max(...xs) - Math.min(...xs), gh = Math.max(...ys) - Math.min(...ys);
const maxDim = Math.max(gw, gh);
if (!Number.isFinite(maxDim) || maxDim <= 0) { fail++; badList.push([f, 'maxDim']); continue; }
const S = 1 / maxDim;
const DEPTH = Math.max(0.05, (dd.s?.cs ?? 60) * S * 1.4);
const geos = [];
for (const shape of dd.sm) {
const { regions, sashes } = inferSashGlyphs(shape);
const bbs = regions.map(r => {
const p = r.flatMap(seg => [seg.ps, seg.pe]).filter(p => Number.isFinite(p.x) && Number.isFinite(p.y));
const X = p.map(q => q.x), Y = p.map(q => q.y);
return { x0: Math.min(...X), y0: Math.min(...Y), x1: Math.max(...X), y1: Math.max(...Y) };
});
sashCount += sashes.length;
// 区域玻璃盒 + 薄梃(用 profileGeo 模拟) + 扇面板挤出
for (const bb of bbs) {
const w = (bb.x1 - bb.x0) * S, h = (bb.y1 - bb.y0) * S;
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) { fail++; badList.push([f, 'region']); continue; }
geos.push(new THREE.BoxGeometry(w, h, 0.008));
geos.push(profileGeo(w, Math.max(0.01, DEPTH * 0.4), DEPTH * 0.55, 2));
}
for (const s of sashes) {
const w = (s.x1 - s.x0) * S, h = (s.y1 - s.y0) * S;
if (!Number.isFinite(w) || !Number.isFinite(h)) { fail++; badList.push([f, 'sash']); continue; }
if (w < 0.02 || h < 0.02) continue;
geos.push(profileGeo(w, h * 0.12 + 0.014, DEPTH * 0.7, 2));
}
// 整体外框
if (bbs.length) {
const X = bbs.flatMap(b => [b.x0, b.x1]), Y = bbs.flatMap(b => [b.y0, b.y1]);
geos.push(profileGeo((Math.max(...X) - Math.min(...X)) * S + 0.05, DEPTH, DEPTH, 3));
geos.push(profileGeo((Math.max(...Y) - Math.min(...Y)) * S + 0.05, DEPTH, DEPTH, 3));
}
}
for (const g of geos) {
totalMesh++;
if (!geometryIsValid(g)) { fail++; badList.push([f, 'geometry NaN']); }
}
}
console.log(`样板: ${total} | 网格几何: ${totalMesh} | 扇总数: ${sashCount}`);
console.log(fail === 0 ? '✓ 全部通过: 0 个 NaN/无效几何' : `✗ 失败 ${fail}:`);
if (badList.length) console.log(badList.slice(0, 10));
process.exit(fail === 0 ? 0 : 1);