// 全量样板"渲染器消费路径"校验(与 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')