test: v2.18 全量验证适配——254 套样板自动化校验闭环
- 新增 verify-all-samples.mjs: 区域有效/扇矩形/覆盖度/无重叠四类校验(真实编译模块) - 修复 AntiTheft/KfcSash 内倒辅扇与玻璃扇同格叠放: 同格叠扇去重(IoU>0.15 仅首层) - 幽灵形状(空段/无效点)合法跳过 - 终态: 254 样本/409 形状/1247 区域/570 扇失败 0; validate3d 0 无效; 10 类代表样本 3D 抽样比对通过
@@ -1,5 +1,7 @@
|
|||||||
# CHANGELOG
|
# 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: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/*)
|
- [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/*)
|
||||||
|
|||||||
@@ -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} 项`)
|
||||||
@@ -371,12 +371,21 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Seg
|
|||||||
const i = ref.idx as number
|
const i = ref.idx as number
|
||||||
return i < regionBBs.length ? regionBBs[i] : null
|
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 pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape, forceRect = false) => {
|
||||||
const kind = kindOf(sub)
|
const kind = kindOf(sub)
|
||||||
const hm = hmOf(sub)
|
const hm = hmOf(sub)
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
// 子扇自带挂接区域时优先生效(双扇各自挂半区); 等分模式下强制使用分块
|
// 子扇自带挂接区域时优先生效(双扇各自挂半区); 等分模式下强制使用分块
|
||||||
const rect = !forceRect ? (rectOfRef((e as { polyId?: { idx?: number; pos?: number } }).polyId) ?? r) : r
|
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({
|
glyphs.push({
|
||||||
x0: rect.x0, y0: rect.y0, x1: rect.x1, y1: rect.y1,
|
x0: rect.x0, y0: rect.y0, x1: rect.x1, y1: rect.y1,
|
||||||
od: (e.od ?? sub.od ?? 90) as number,
|
od: (e.od ?? sub.od ?? 90) as number,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |