// 分割树重放语义实证分析: node scripts/analyze-split-tree.mjs // V0=现实现(切片顺序跟随多边形遍历) vs V1=规范化排序(竖切左先/横切上先, 屏幕坐标y向下) // 验证信号: 扇 polyId.idx 与玻璃 fls pid.idx 是否落在最终区域索引范围内 import fs from 'fs' import path from 'path' const DIR = 'D:/Code/huamenchuang/data/windoor-drawings' // ---- 与 drawing.ts 相同的底层工具 ---- 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 spAxis = sp => { const n = sp.line?.norm ?? { x: 1, y: 0 } return Math.abs(n.x) >= Math.abs(n.y) ? 'v' : 'h' } const spPos = sp => (spAxis(sp) === 'v' ? sp.line.pt.x : sp.line.pt.y) function splitPolySegments(region, axis, pos, sortPieces) { const pts = region.map(s => s.ps) const n = pts.length const get = p => (axis === 'v' ? p.x : p.y) const crossings = [] for (let i = 0; i < n; i++) { const a = pts[i], b = pts[(i + 1) % n] const da = get(a) - pos, db = get(b) - pos if ((da < 0 && db > 0) || (da > 0 && db < 0)) { const t = da / (da - db) crossings.push({ i, p: axis === 'v' ? { x: pos, y: a.y + (b.y - a.y) * t } : { x: a.x + (b.x - a.x) * t, y: pos } }) } } if (crossings.length !== 2) return null const [c1, c2] = crossings if (c1.i === c2.i) return null const collect = (from, to) => { const ps = [from.p] for (let k = (from.i + 1) % n; ; k = (k + 1) % n) { ps.push(pts[k]) if (k === to.i) break } ps.push(to.p) return ps.filter((p, i) => i === 0 || Math.abs(p.x - ps[i - 1].x) > 0.01 || Math.abs(p.y - ps[i - 1].y) > 0.01) } const toSegs = ps => ps.map((p, i) => ({ ps: p, pe: ps[(i + 1) % ps.length], name: 'segment' })) const pieces = [toSegs(collect(c1, c2)), toSegs(collect(c2, c1))].filter(ps => ps.length >= 3) if (pieces.length < 2) return null if (sortPieces) { pieces.sort((A, B) => { const ca = bboxOf(A.map(s => s.ps)), cb = bboxOf(B.map(s => s.ps)) return axis === 'v' ? ca.x0 - cb.x0 : ca.y0 - cb.y0 }) } return { a: pieces[0], b: pieces[1] } } function replay(shape, sortPieces) { const base = (shape.polygon?.polygon ?? []) .map(r => r.filter(s => s?.ps && s?.pe && [s.ps.x, s.ps.y, s.pe.x, s.pe.y].every(Number.isFinite)) .map(s => ({ ps: { ...s.ps }, pe: { ...s.pe } }))) .filter(r => r.length >= 3) const regions = base.map(r => r) for (const sp of (shape.mm?.sp ?? [])) { if (!sp?.line?.pt || !sp.line.norm) continue const axis = spAxis(sp), pos = spPos(sp) let idx = sp.polyId?.idx ?? -1 if (!Number.isInteger(idx) || idx < 0 || idx >= regions.length) idx = 0 const target = regions[idx] if (!target || target.length < 3) continue const bb = bboxOf(target.map(s => s.ps)) const lo = axis === 'v' ? bb.x0 : bb.y0, hi = axis === 'v' ? bb.x1 : bb.y1 if (pos <= lo + 0.5 || pos >= hi - 0.5) continue const split = splitPolySegments(target, axis, pos, sortPieces) if (!split) continue regions.splice(idx, 1, split.a, split.b) } return regions } // ---- 全量扫描 ---- const files = fs.readdirSync(DIR).filter(f => f.endsWith('.json')) let sashStat = { v0ok: 0, v1ok: 0, total: 0, v0miss: [], v1miss: [] } let glassStat = { v0ok: 0, v1ok: 0, total: 0, v0miss: [], v1miss: [] } const hmDist = {} let sample704 = null for (const f of files) { const row = JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8')) const dd = typeof row.drawing_data === 'string' ? JSON.parse(row.drawing_data) : row.drawing_data for (const shape of (dd.sm ?? [])) { const r0 = replay(shape, false) const r1 = replay(shape, true) for (const sub of (shape.sm ?? [])) { const e = sub.enabled === false ? sub.sashes ?? [] : [sub] if (!e.length) continue sashStat.total++ const idx = sub.polyId?.idx if (idx == null || idx < 0) continue // 未挂接, 走兜底 if (idx < r0.length) sashStat.v0ok++; else sashStat.v0miss.push(`${f} idx=${idx} n=${r0.length}`) if (idx < r1.length) sashStat.v1ok++; else sashStat.v1miss.push(`${f} idx=${idx} n=${r1.length}`) const hm = sub.hm ?? {} const key = `type=${sub.type} slide=${hm.slide ?? '-'} dir=${hm.openDirection ?? '-'} toward=${hm.openToward ?? '-'} hinge=${hm.hingeType ?? '-'} od=${sub.od ?? '-'}` hmDist[key] = (hmDist[key] ?? 0) + 1 } for (const fl of (shape.mm?.fls?.fls ?? [])) { glassStat.total++ const idx = fl.pid?.idx if (idx == null || idx < 0) continue if (idx < r0.length) glassStat.v0ok++; else glassStat.v0miss.push(`${f}`) if (idx < r1.length) glassStat.v1ok++; else glassStat.v1miss.push(`${f}`) } if (f === '704.json' && !sample704) { sample704 = r1.map(r => bboxOf(r.map(s => s.ps))) } } } console.log('样板数:', files.length) console.log('扇引用: 总数', sashStat.total, '| V0 有效', sashStat.v0ok, '| V1 有效', sashStat.v1ok) console.log('V0 越界样例:', sashStat.v0miss.slice(0, 8)) console.log('V1 越界样例:', sashStat.v1miss.slice(0, 8)) console.log('玻璃引用: 总数', glassStat.total, '| V0 有效', glassStat.v0ok, '| V1 有效', glassStat.v1ok) console.log('玻璃 V0 越界数:', glassStat.v0miss.length, '| V1 越界数:', glassStat.v1miss.length) console.log('\n704 V1 区域:') sample704?.forEach((b, i) => console.log(' region', i, JSON.stringify(b))) console.log('\nhm 五金字段分布 (按频次):') Object.entries(hmDist).sort((a, b) => b[1] - a[1]).forEach(([k, v]) => console.log(' ', v, k))