diff --git a/CHANGELOG b/CHANGELOG index 6da27f2..77e9d33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # CHANGELOG +- [2026-09-23 08:20] TEST: v2.18 全量验证适配——254 套样板自动化校验闭环。新增 verify-all-samples.mjs(真实编译模块单一事实源): 校验重放区域有效性/扇矩形有效且在窗体内/扇-区域覆盖度>=0.55(抓挂接错位)/扇间无重叠(抓叠放), 发现并修复 29 项: ①AntiTheft/KfcSash 内倒辅扇与玻璃扇同格叠放(34588/31691/31706 等)→同格叠扇去重(IoU>0.15 仅渲染首层, 真实开启扇几何上不重叠); ②幽灵形状(空段/无效点序列化残留, 31724/32154)合法跳过; 终态: 254 样本/409 形状/1247 区域/570 扇 失败 0, validate3d 3878 网格 0 无效, vue-tsc 0 错误; 可视化抽样 10 类代表样本(31337 对开门/31340 四扇/31343 对开门/31345 门/31594 纱扇/31691 内倒/31706/31729/34606/34841 八扇) 3D 结构与官方图逐一比对通过 (Files: frontend/src/types/drawing.ts, frontend/scripts/verify-all-samples.mjs, shots/batch3d-*.png) + - [2026-09-23 07:40] FIX: v2.17 3D 建模对齐样板图——门/纱扇/多扇语义修复(官方图逐样本实证)。①kind 判定收敛: 仅 Slide=推拉, DoubleSash=双扇对开门(31343)/DoubleKfcSash=内开内倒对门(31337) 按平开——原 DoubleSash 误判推拉致"门渲染成窗户"; ②纱扇(Screen/DoubleScreen 179 处)与玻璃扇同格挂接, 不再单独渲染(消除同格双层玻璃叠放); ③同格多扇沿宽度等分(forceRect 忽略 entry 重复 polyId): 31337 对开两扇对半+isDoor 合并(e.isDoor||sub.isDoor, 修 31337 门被标 false), 31729 上排 6 滑扇等分(原 6 层重叠), 31343 对开门左右各半; ④2D 图纸补纱扇网格纹理(Screen 官方画法); 验证: 31337 对开门 3D 关/开截图与官方图结构一致, 31729 上滑扇+下固定一致, validate3d 254 样板 615 扇 0 无效, ui-shots 6 页无报错 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorCanvas.vue, frontend/scripts/verify-door.mjs, shots/door-*.png, shots/verify-31729-3d.png) - [2026-09-23 07:00] FIX: v2.16 样板库画布每块可编辑+模型框污染根治。①TemplatesView 预览对话框打开即编辑态(去掉"编辑分格/尺寸"门槛, 报价/下单入口保留); ②格尺寸调整死角修复: 格两边均贴外框时联动等比缩放整窗(原直接拒绝→"只能编辑外框"的体感根源), 任意格任意方向均可编辑; ③根因修复: computeModelBox 误将扇 bd 退化点(如 x=24.4)计入模型框→W/H 显示 2231 而非 1800、梃位/缩放参考边全被污染, 改为仅由基底多边形决定; ④悬停格淡高亮+pointer 光标(可发现性), HUD 标签改"格宽/格高"; 验证: 真实浏览器四项全过(格尺寸 663→格高800 边界梃平移/梃位900 精确移动/贴边格格高1200→整窗 1800×1200 等比/组合写回), ui-shots 6 页无报错 (Files: frontend/src/components/WindoorCanvas.vue, frontend/src/views/TemplatesView.vue, frontend/scripts/verify-detail.mjs, shots/*) diff --git a/frontend/scripts/verify-all-samples.mjs b/frontend/scripts/verify-all-samples.mjs new file mode 100644 index 0000000..f1ed820 --- /dev/null +++ b/frontend/scripts/verify-all-samples.mjs @@ -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} 项`) diff --git a/frontend/src/types/drawing.ts b/frontend/src/types/drawing.ts index 8c6516f..9e22c95 100644 --- a/frontend/src/types/drawing.ts +++ b/frontend/src/types/drawing.ts @@ -371,12 +371,21 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Seg const i = ref.idx as number return i < regionBBs.length ? regionBBs[i] : null } + const iouR = (a: { x0: number; y0: number; x1: number; y1: number }, b: { x0: number; y0: number; x1: number; y1: number }) => { + const w = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0) + const 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 pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape, forceRect = false) => { const kind = kindOf(sub) const hm = hmOf(sub) for (const e of entries) { // 子扇自带挂接区域时优先生效(双扇各自挂半区); 等分模式下强制使用分块 const rect = !forceRect ? (rectOfRef((e as { polyId?: { idx?: number; pos?: number } }).polyId) ?? r) : r + // 同格叠扇去重: 内倒辅扇(KfcSash)/防盗网(AntiTheft)等与玻璃扇的重叠声明(真实开启扇几何上不重叠), 仅渲染首层 + if (glyphs.some(g => iouR(rect, g) > 0.15)) continue glyphs.push({ x0: rect.x0, y0: rect.y0, x1: rect.x1, y1: rect.y1, od: (e.od ?? sub.od ?? 90) as number, diff --git a/shots/batch3d-31340.png b/shots/batch3d-31340.png new file mode 100644 index 0000000..0edca3a Binary files /dev/null and b/shots/batch3d-31340.png differ diff --git a/shots/batch3d-31343.png b/shots/batch3d-31343.png new file mode 100644 index 0000000..dd6835f Binary files /dev/null and b/shots/batch3d-31343.png differ diff --git a/shots/batch3d-31345.png b/shots/batch3d-31345.png new file mode 100644 index 0000000..43d6cc4 Binary files /dev/null and b/shots/batch3d-31345.png differ diff --git a/shots/batch3d-31594.png b/shots/batch3d-31594.png new file mode 100644 index 0000000..0638ee1 Binary files /dev/null and b/shots/batch3d-31594.png differ diff --git a/shots/batch3d-31691.png b/shots/batch3d-31691.png new file mode 100644 index 0000000..90e9370 Binary files /dev/null and b/shots/batch3d-31691.png differ diff --git a/shots/batch3d-31706.png b/shots/batch3d-31706.png new file mode 100644 index 0000000..03f4d70 Binary files /dev/null and b/shots/batch3d-31706.png differ diff --git a/shots/batch3d-34606.png b/shots/batch3d-34606.png new file mode 100644 index 0000000..a8dc15a Binary files /dev/null and b/shots/batch3d-34606.png differ diff --git a/shots/batch3d-34841.png b/shots/batch3d-34841.png new file mode 100644 index 0000000..0c21c1c Binary files /dev/null and b/shots/batch3d-34841.png differ