// 全量样板验证适配(使用真实编译模块, 单一事实源): 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} 项`)