fix: v2.1 3D 渲染问题根治(profileGeo 自交破面+扇矩形 NaN)
- profileGeo 外轮廓路径自相交(闭合斜边穿过槽口)导致三角剖分破面 重写为正确顶点顺序的 U 槽简单多边形 - 型材截面挤出抽为共享模块 three/profileGeo.ts(+geometryIsValid 校验器) - inferSashGlyphs 兜底 bboxOf 误传段对象→修正为有限值点集 (修复扇矩形 NaN 序列化为 null 的问题) - 新增 scripts/validate3d.cjs: 254 样板×3700 网格×798 扇 逐网格 NaN 检测全部通过; 窄条扇合法跳过
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
|
||||
# 验证编译产物
|
||||
.val/
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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);
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
bboxOf, inferSashGlyphs, parseDrawing, regionPoints, replayShape,
|
||||
type DrawingData, type Pt,
|
||||
} from '../types/drawing'
|
||||
import { profileGeo } from '../three/profileGeo'
|
||||
|
||||
interface BBox { x0: number; y0: number; x1: number; y1: number }
|
||||
|
||||
@@ -110,42 +111,6 @@ function overlapArea(a: BBox, b: BBox): number {
|
||||
}
|
||||
|
||||
/** 型材截面: 多腔体 + 玻璃槽(比例依据真实 DXF 截面学习: 深度46-90/面宽65-130/腔体3-6/壁厚≈深度1/30) */
|
||||
function profileGeo(len: number, fw: number, depth: number, chambers: number): THREE.BufferGeometry {
|
||||
const t = Math.max(0.004, depth * 0.14) // 壁厚
|
||||
const g = Math.max(0.006, depth * 0.2) // 玻璃槽深
|
||||
const s = new THREE.Shape()
|
||||
// 外轮廓: u=0 侧开玻璃槽
|
||||
s.moveTo(0, t)
|
||||
s.lineTo(g, t)
|
||||
s.lineTo(g, fw - t)
|
||||
s.lineTo(0, fw - t)
|
||||
s.lineTo(0, 0)
|
||||
s.lineTo(depth, 0)
|
||||
s.lineTo(depth, fw)
|
||||
s.lineTo(g, fw)
|
||||
s.closePath()
|
||||
// 腔体孔洞(隔热/加强腔)
|
||||
const cH = fw - t * 2
|
||||
const usable = depth - g - t * (chambers + 1)
|
||||
const cw = usable / chambers
|
||||
if (cw > t) {
|
||||
for (let i = 0; i < chambers; i++) {
|
||||
const hx = g + t + i * (cw + t)
|
||||
const hole = new THREE.Path()
|
||||
hole.moveTo(hx, t)
|
||||
hole.lineTo(hx + cw, t)
|
||||
hole.lineTo(hx + cw, t + cH)
|
||||
hole.lineTo(hx, t + cH)
|
||||
hole.closePath()
|
||||
s.holes.push(hole)
|
||||
}
|
||||
}
|
||||
const geo = new THREE.ExtrudeGeometry(s, { depth: len, bevelEnabled: false })
|
||||
geo.translate(0, 0, -len / 2)
|
||||
return geo
|
||||
}
|
||||
|
||||
/** 沿 X 轴的型材杆(槽口朝 -Z 玻璃面) */
|
||||
function barX(parent: THREE.Group, mat: THREE.Material, len: number, fw: number,
|
||||
depth: number, cx: number, cy: number, cz: number, chambers = 2) {
|
||||
const geo = profileGeo(len, fw, depth, chambers)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 型材截面挤出几何(自研实现,思路对齐源站 DXF 截面挤出方案)
|
||||
* U 型槽多腔体截面:沿长度方向(Z)挤出并居中,槽口朝玻璃面(u=0 侧)。
|
||||
* 截面比例依据真实 DXF 学习:深度 46-90 / 面宽 65-130 / 腔体 3-6 / 壁厚≈深度 1/7(视觉强化)。
|
||||
*/
|
||||
import * as THREE from 'three'
|
||||
|
||||
export function profileGeo(len: number, fw: number, depth: number, chambers = 2): THREE.BufferGeometry {
|
||||
const t = Math.max(0.004, depth * 0.14) // 壁厚
|
||||
const g = Math.max(0.006, depth * 0.2) // 玻璃槽深
|
||||
const s = new THREE.Shape()
|
||||
// 外轮廓(U 槽口在 u=0 侧, v∈[t, fw-t]) — 顶点顺序保证简单多边形不自交
|
||||
s.moveTo(0, 0)
|
||||
s.lineTo(depth, 0)
|
||||
s.lineTo(depth, fw)
|
||||
s.lineTo(0, fw)
|
||||
s.lineTo(0, fw - t)
|
||||
s.lineTo(g, fw - t)
|
||||
s.lineTo(g, t)
|
||||
s.lineTo(0, t)
|
||||
s.closePath()
|
||||
// 腔体孔洞(隔热/加强腔)
|
||||
const cH = fw - t * 2
|
||||
const usable = depth - g - t * (chambers + 1)
|
||||
const cw = usable / chambers
|
||||
if (Number.isFinite(cw) && cw > t) {
|
||||
for (let i = 0; i < chambers; i++) {
|
||||
const hx = g + t + i * (cw + t)
|
||||
const hole = new THREE.Path()
|
||||
hole.moveTo(hx, t)
|
||||
hole.lineTo(hx + cw, t)
|
||||
hole.lineTo(hx + cw, t + cH)
|
||||
hole.lineTo(hx, t + cH)
|
||||
hole.closePath()
|
||||
s.holes.push(hole)
|
||||
}
|
||||
}
|
||||
const geo = new THREE.ExtrudeGeometry(s, { depth: len, bevelEnabled: false })
|
||||
geo.translate(0, 0, -len / 2)
|
||||
return geo
|
||||
}
|
||||
|
||||
/** 校验几何: 位置属性全部有限 且 包围球半径有限 */
|
||||
export function geometryIsValid(geo: THREE.BufferGeometry): boolean {
|
||||
geo.computeBoundingSphere()
|
||||
if (!Number.isFinite(geo.boundingSphere?.radius ?? NaN)) return false
|
||||
const pos = geo.getAttribute('position')
|
||||
if (!pos) return false
|
||||
for (let i = 0; i < pos.count * 3; i++) {
|
||||
if (!Number.isFinite(pos.array[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -285,7 +285,14 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: Segment[][]): {
|
||||
}
|
||||
}
|
||||
if (unmatched.length && regions.length) {
|
||||
const fb = bboxOf(regions.flat())
|
||||
const flatPts: Pt[] = []
|
||||
for (const r of regions) {
|
||||
for (const seg of r) {
|
||||
if (Number.isFinite(seg.ps?.x) && Number.isFinite(seg.ps?.y)) flatPts.push(seg.ps)
|
||||
if (Number.isFinite(seg.pe?.x) && Number.isFinite(seg.pe?.y)) flatPts.push(seg.pe)
|
||||
}
|
||||
}
|
||||
const fb = bboxOf(flatPts)
|
||||
const K = unmatched.reduce((s, u) => s + u.entries.length, 0)
|
||||
const colW = (fb.x1 - fb.x0) / K
|
||||
let i = 0
|
||||
|
||||
Reference in New Issue
Block a user