test: 全量254套模板视觉校验(结构级verify-sash-panels+渲染级patrol-render-all常驻资产, 570扇/726子格0异常, 渲染巡检254/254 OK)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// 全量样板渲染巡检: 逐套在效果图渲染页切换样板, 收集 JS 异常 + 画布活性(非空白像素占比)
|
||||
// 用法: node scripts/patrol-render-all.mjs <token> [--shot=12]
|
||||
import puppeteer from 'puppeteer-core'
|
||||
import fs from 'fs'
|
||||
|
||||
const token = process.argv[2]
|
||||
const shotN = Number((process.argv.find(a => a.startsWith('--shot=')) ?? '--shot=12').split('=')[1]) || 12
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
||||
headless: 'new', args: ['--window-size=1440,900', '--enable-unsafe-swiftshader'],
|
||||
defaultViewport: { width: 1440, height: 900 },
|
||||
})
|
||||
const page = await browser.newPage()
|
||||
const errs = []
|
||||
page.on('pageerror', e => errs.push(String(e).slice(0, 200)))
|
||||
page.on('console', m => { if (m.type() === 'error' && !m.text().includes('favicon')) errs.push('[console] ' + m.text().slice(0, 150)) })
|
||||
|
||||
await page.goto(`http://localhost:8000/rendering?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
|
||||
// 取全部样板名
|
||||
const names = await page.evaluate(async () => {
|
||||
const token = localStorage.getItem('hmc_token')
|
||||
const res = await fetch('/api/windoor/templates?page=1&page_size=254', { headers: { Authorization: `Bearer ${token}` } })
|
||||
const d = (await res.json()).data
|
||||
const rows = Array.isArray(d) ? d : d.data
|
||||
return rows.map(r => r.name)
|
||||
})
|
||||
console.log('样板总数:', names.length)
|
||||
|
||||
const results = []
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const name = names[i]
|
||||
errs.length = 0
|
||||
try {
|
||||
// 打开下拉 → 输入过滤 → 选中
|
||||
await page.evaluate(() => document.querySelector('.el-select')?.querySelector('.el-select__wrapper, .el-input__inner')?.dispatchEvent(new MouseEvent('click', { bubbles: true })))
|
||||
await new Promise(r => setTimeout(r, 350))
|
||||
await page.keyboard.type(name, { delay: 5 })
|
||||
await new Promise(r => setTimeout(r, 550))
|
||||
const picked = await page.evaluate(nm => {
|
||||
const items = [...document.querySelectorAll('.el-select-dropdown__item')]
|
||||
const el = items.find(x => x.textContent.trim() === nm)
|
||||
if (!el) return false
|
||||
el.click()
|
||||
return true
|
||||
}, name)
|
||||
if (!picked) { results.push([name, 'SKIP', '下拉未命中']); await page.keyboard.press('Escape'); continue }
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
// 画布活性: 非透明像素占比
|
||||
const alive = await page.evaluate(() => {
|
||||
const cv = document.querySelector('.render-canvas')
|
||||
if (!cv || !cv.width) return -1
|
||||
const ctx = cv.getContext('2d')
|
||||
const d = ctx.getImageData(0, 0, cv.width, cv.height).data
|
||||
let n = 0
|
||||
for (let p = 3; p < d.length; p += 40) if (d[p] > 0) n++
|
||||
return n / (d.length / 40)
|
||||
})
|
||||
results.push([name, errs.length ? 'ERR' : (alive > 0.02 ? 'OK' : 'BLANK'),
|
||||
errs.length ? errs[0] : `alive=${alive.toFixed(3)}`])
|
||||
} catch (e) {
|
||||
results.push([name, 'ERR', String(e).slice(0, 120)])
|
||||
}
|
||||
if ((i + 1) % 40 === 0) console.log(`进度 ${i + 1}/${names.length}`)
|
||||
}
|
||||
const bad = results.filter(r => r[1] !== 'OK' && r[1] !== 'SKIP')
|
||||
console.log(`\n结果: OK=${results.filter(r => r[1] === 'OK').length} BLANK=${results.filter(r => r[1] === 'BLANK').length} ERR=${results.filter(r => r[1] === 'ERR').length} SKIP=${results.filter(r => r[1] === 'SKIP').length}`)
|
||||
for (const r of bad) console.log(' ' + r.join(' | '))
|
||||
fs.writeFileSync('scripts/.render-patrol.json', JSON.stringify(results))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,114 @@
|
||||
// 全量样板"渲染器消费路径"校验(与 WindoorCanvas/RenderCanvas/WindoorThreed 同逻辑):
|
||||
// ① 链路异常(A级: 会导致渲染中断) ② 扇形状未匹配/锚点错位(B级) ③ 扇内子格异常(C级)
|
||||
// ④ 源站格条解析(D级) ⑤ sashes 序与视觉序交叉信息(E级)
|
||||
// 用法: npx esbuild src/types/drawing.ts --bundle --format=esm --outfile=scripts/.drawing-bundle.mjs && node scripts/verify-sash-panels.mjs
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { parseDrawing, replayShape, inferSashGlyphs, inferSashPanels, bboxOf, regionPoints } from './.drawing-bundle.mjs'
|
||||
|
||||
const DIR = 'D:/Code/huamenchuang/data/windoor-drawings'
|
||||
const issues = [] // {file, level, msg}
|
||||
let stats = { files: 0, shapes: 0, glyphs: 0, panels: 0, panelBoards: 0, decos: 0, matched: 0, fallback: 0 }
|
||||
|
||||
const collectSash = (s, out) => {
|
||||
if (!['Frame', 'Slide'].includes(String(s.type))) out.push(s)
|
||||
for (const x of s.sm ?? []) collectSash(x, out)
|
||||
for (const x of s.sashes ?? []) collectSash(x, out)
|
||||
}
|
||||
|
||||
for (const f of fs.readdirSync(DIR).filter(x => x.endsWith('.json')).sort()) {
|
||||
stats.files++
|
||||
const file = path.basename(f, '.json')
|
||||
let row, raw
|
||||
try {
|
||||
row = JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8'))
|
||||
raw = typeof row.drawing_data === 'string' ? JSON.parse(row.drawing_data) : row.drawing_data
|
||||
if (raw && typeof raw === 'object' && typeof raw.drawing_data === 'string') raw = JSON.parse(raw.drawing_data)
|
||||
else if (raw && typeof raw === 'object' && raw.drawing_data && typeof raw.drawing_data === 'object') raw = raw.drawing_data
|
||||
} catch (e) {
|
||||
issues.push({ file, level: 'A', msg: 'JSON 解析失败: ' + e.message })
|
||||
continue
|
||||
}
|
||||
let dd
|
||||
try {
|
||||
dd = parseDrawing(raw)
|
||||
} catch (e) {
|
||||
issues.push({ file, level: 'A', msg: 'parseDrawing 失败: ' + e.message })
|
||||
continue
|
||||
}
|
||||
for (const shape of dd.sm ?? []) {
|
||||
stats.shapes++
|
||||
let rp, glyphs
|
||||
try {
|
||||
rp = replayShape(shape)
|
||||
glyphs = inferSashGlyphs(shape, rp).sashes
|
||||
} catch (e) {
|
||||
issues.push({ file, level: 'A', msg: `重放/扇推断异常: ${e.message}` })
|
||||
continue
|
||||
}
|
||||
const sashShapes = []
|
||||
try { collectSash(shape, sashShapes) } catch (e) { issues.push({ file, level: 'A', msg: '形状收集异常: ' + e.message }); continue }
|
||||
|
||||
glyphs.forEach((g, gi) => {
|
||||
stats.glyphs++
|
||||
const gRect = { x0: g.x0, y0: g.y0, x1: g.x1, y1: g.y1 }
|
||||
if (![gRect.x0, gRect.y0, gRect.x1, gRect.y1].every(Number.isFinite)) {
|
||||
issues.push({ file, level: 'B', msg: `扇${gi} 矩形含非有限值` })
|
||||
return
|
||||
}
|
||||
// 锚点归属匹配(与渲染器同)
|
||||
let matched = null, byAnchor = false
|
||||
for (const sh of sashShapes) {
|
||||
const sps = sh.mm?.sp ?? []
|
||||
if (sps.some(sp => {
|
||||
const pt = sp.line?.pt
|
||||
return pt && pt.x >= gRect.x0 && pt.x <= gRect.x1 && pt.y >= gRect.y0 && pt.y <= gRect.y1
|
||||
})) { matched = sh; byAnchor = true; break }
|
||||
}
|
||||
if (!matched && sashShapes.length) { matched = sashShapes[gi % sashShapes.length]; stats.fallback++ }
|
||||
if (!matched) {
|
||||
if ((shape.mm?.sp ?? []).length || (shape.sm ?? []).length) issues.push({ file, level: 'B', msg: `扇${gi} 无可匹配扇形状` })
|
||||
return
|
||||
}
|
||||
if (byAnchor) stats.matched++
|
||||
let panels
|
||||
try {
|
||||
panels = inferSashPanels(matched, gRect)
|
||||
} catch (e) {
|
||||
issues.push({ file, level: 'C', msg: `扇${gi}(${matched.type}) inferSashPanels 异常: ${e.message}` })
|
||||
return
|
||||
}
|
||||
if (!panels.length) { issues.push({ file, level: 'C', msg: `扇${gi} 零子格` }); return }
|
||||
for (const [pi, p] of panels.entries()) {
|
||||
stats.panels++
|
||||
if (![p.x0, p.y0, p.x1, p.y1].every(Number.isFinite) || p.x1 - p.x0 <= 1 || p.y1 - p.y0 <= 1) {
|
||||
issues.push({ file, level: 'C', msg: `扇${gi} 子格${pi} 几何无效 [${p.x0?.toFixed(0)},${p.y0?.toFixed(0)}~${p.x1?.toFixed(0)},${p.y1?.toFixed(0)}]` })
|
||||
continue
|
||||
}
|
||||
if (!p.glass) stats.panelBoards++
|
||||
if (p.deco) {
|
||||
stats.decos++
|
||||
if (!['decorationChinese', 'decorationEuropean', 'decorationModern'].includes(p.deco.tp) && !p.deco.tp.startsWith('decoration')) {
|
||||
issues.push({ file, level: 'D', msg: `扇${gi} 子格${pi} 未知格条风格: ${p.deco.tp}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
// E级: 多扇同形状组时, 扇形状序与 glyph 序交叉提示(锚点命中率低即疑似)
|
||||
if (!byAnchor && sashShapes.length > 1 && (matched.mm?.sp ?? []).length) {
|
||||
issues.push({ file, level: 'E', msg: `扇${gi} 锚点未命中, 回退下标匹配(可能序交叉)` })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`统计: 文件=${stats.files} 形状=${stats.shapes} 扇=${stats.glyphs} 子格=${stats.panels}(裙板=${stats.panelBoards}, 格条=${stats.decos}) 锚点命中=${stats.matched} 回退=${stats.fallback}`)
|
||||
const byLevel = {}
|
||||
for (const it of issues) (byLevel[it.level] = byLevel[it.level] ?? []).push(it)
|
||||
for (const lv of ['A', 'B', 'C', 'D', 'E']) {
|
||||
const list = byLevel[lv] ?? []
|
||||
console.log(`[${lv}级] ${list.length} 项`)
|
||||
for (const it of list.slice(0, 40)) console.log(` ${it.file}: ${it.msg}`)
|
||||
if (list.length > 40) console.log(` ... 其余 ${list.length - 40} 项`)
|
||||
}
|
||||
fs.writeFileSync('scripts/.sash-panel-issues.json', JSON.stringify(issues, null, 1))
|
||||
console.log('明细已写 scripts/.sash-panel-issues.json')
|
||||
Reference in New Issue
Block a user