test: v2.18 全量验证适配——254 套样板自动化校验闭环

- 新增 verify-all-samples.mjs: 区域有效/扇矩形/覆盖度/无重叠四类校验(真实编译模块)
- 修复 AntiTheft/KfcSash 内倒辅扇与玻璃扇同格叠放: 同格叠扇去重(IoU>0.15 仅首层)
- 幽灵形状(空段/无效点)合法跳过
- 终态: 254 样本/409 形状/1247 区域/570 扇失败 0; validate3d 0 无效; 10 类代表样本 3D 抽样比对通过
This commit is contained in:
HHY
2026-09-24 07:16:20 -07:00
parent 8cd3442508
commit bbdf03f56e
11 changed files with 84 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
// 全量样板验证适配(使用真实编译模块, 单一事实源): node scripts/verify-all-samples.mjs
// 校验: ①重放区域有效 ②扇矩形有效且在窗体内 ③扇与区域覆盖度>=0.55(抓挂接错位) ④扇间无重叠(抓叠放)
// 先运行: npx esbuild src/types/drawing.ts --bundle --format=esm --outfile=scripts/.drawing-bundle.mjs
import fs from 'fs'
import path from 'path'
import { parseDrawing, replayShape, inferSashGlyphs } from './.drawing-bundle.mjs'
const DIR = 'D:/Code/huamenchuang/data/windoor-drawings'
const iou = (a, b) => {
const w = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0), h = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0)
if (w <= 0 || h <= 0) return 0
const inter = w * h
return inter / ((a.x1 - a.x0) * (a.y1 - a.y0) + (b.x1 - b.x0) * (b.y1 - b.y0) - inter)
}
const cover = (inner, outer) => {
const w = Math.min(inner.x1, outer.x1) - Math.max(inner.x0, outer.x0), h = Math.min(inner.y1, outer.y1) - Math.max(inner.y0, outer.y0)
if (w <= 0 || h <= 0) return 0
return (w * h) / ((inner.x1 - inner.x0) * (inner.y1 - inner.y0))
}
const bboxOf = pts => ({
x0: Math.min(...pts.map(p => p.x)), y0: Math.min(...pts.map(p => p.y)),
x1: Math.max(...pts.map(p => p.x)), y1: Math.max(...pts.map(p => p.y)),
})
const files = fs.readdirSync(DIR).filter(f => f.endsWith('.json'))
let fail = 0, checked = 0
const failures = []
const stats = { shapes: 0, regions: 0, sashes: 0, noSash: 0 }
for (const f of files) {
const row = JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8'))
let dd
try { dd = parseDrawing(typeof row.drawing_data === 'string' ? JSON.parse(row.drawing_data) : row.drawing_data) } catch { failures.push([f, 'JSON解析']); fail++; continue }
for (let si = 0; si < (dd.sm ?? []).length; si++) {
const shape = dd.sm[si]
checked++
const rp = replayShape(shape)
stats.shapes++
stats.regions += rp.regions.length
const bad = []
if (!rp.regions.length) bad.push('无区域')
const rbs = rp.regions.map(r => bboxOf(r.map(s => s.ps)))
const frameBb = rbs.reduce((a, b) => ({
x0: Math.min(a.x0, b.x0), y0: Math.min(a.y0, b.y0), x1: Math.max(a.x1, b.x1), y1: Math.max(a.y1, b.y1),
}), { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity })
for (const rb of rbs) {
const area = (rb.x1 - rb.x0) * (rb.y1 - rb.y0)
if (!Number.isFinite(area) || area <= 0) bad.push('无效区域')
}
const sashes = inferSashGlyphs(shape, rp).sashes
stats.sashes += sashes.length
if (!sashes.length) stats.noSash++
// 无几何且无扇的幽灵形状(序列化残留, 如空段/无效点)合法跳过
if (!rp.regions.length && !sashes.length) { bad.length = 0; continue }
sashes.forEach((s, k) => {
const rect = { x0: s.x0, y0: s.y0, x1: s.x1, y1: s.y1 }
const w = rect.x1 - rect.x0, h = rect.y1 - rect.y0
if (!(w > 0 && h > 0 && Number.isFinite(w) && Number.isFinite(h))) { bad.push(`扇${k}(${s.kind})矩形无效`); return }
if (cover(rect, frameBb) < 0.98) bad.push(`扇${k}越出窗体`)
const bestCov = Math.max(...rbs.map(rb => cover(rect, rb)))
if (bestCov < 0.55) bad.push(`扇${k}覆盖度${bestCov.toFixed(2)}`)
sashes.forEach((o, j) => {
if (j <= k) return
const or = { x0: o.x0, y0: o.y0, x1: o.x1, y1: o.y1 }
if (iou(rect, or) > 0.15) bad.push(`扇${k}×扇${j}重叠`)
})
})
if (bad.length) { failures.push([f, `shape${si}: ${bad.join('; ')}`]); fail++ }
}
}
console.log(`样本: ${files.length} | 形状: ${stats.shapes} | 区域: ${stats.regions} | 扇: ${stats.sashes} | 无扇形状: ${stats.noSash}`)
console.log(`失败项: ${fail}`)
failures.slice(0, 50).forEach(([f, msg]) => console.log(' ✗', f, msg))
if (failures.length > 50) console.log(` ...共 ${failures.length} 项`)