fix: 门窗渲染语义区分(门=贯穿长把手+锁点+型材同色裙板, 窗=短把手), 对照官方参考图三渲染器同步

This commit is contained in:
HHY
2026-10-05 08:22:19 -07:00
parent 078ca3529d
commit 64bda5e97b
4 changed files with 35 additions and 11 deletions
+1
View File
@@ -1,5 +1,6 @@
# CHANGELOG # CHANGELOG
- [2026-10-05 07:52] FIX: v2.41 门窗渲染语义区分(对齐官方图纸)。对照官方参考图(windoor-images/31337/31343/31637)逐项核对门的视觉语义, 三渲染器同步: ①门把手贯穿长条(约55%扇高, 官方门统一长条贯穿中缝侧) vs 窗短把手(20%) ②门上下锁点小方块(RenderCanvas 此前缺失, 补齐; WindoorCanvas 已有) ③裙板色=型材同色系(RenderCanvas 取 shape.cm.g 暗化 62%, 解析失败退中性深灰; WindoorCanvas 图纸深灰+竖纹肌理+浅色编号; 3D 0x606369)——此前裙板米色与型材脱节; 门窗开向符号(V形虚线顶点指铰链侧)经官方图核对为同一语义无需区分; 视觉验收: 31343/31337 门(长把手+锁点+型材色裙板)与 704 窗(短把手)对比官方图一致; 回归: 24 页巡检 0 异常+冒烟 60/60 (Files: frontend/src/components/RenderCanvas.vue, frontend/src/components/WindoorCanvas.vue, frontend/src/components/WindoorThreed.vue)
- [2026-10-05 07:30] TEST: v2.40 全量 254 套模板视觉校验——v2.39 消费路径全量回归 0 缺陷。新增两个常驻校验资产: ①frontend/scripts/verify-sash-panels.mjs(结构级: 渲染器同路径全量跑 重放/扇推断/锚点匹配/inferSashPanels, A~E 五级异常分类+子格覆盖度断言, 实测 254 文件/409 形状/570 扇/726 子格(裙板 150/格条 58) 0 异常) ②frontend/scripts/patrol-render-all.mjs(渲染级: 无头浏览器逐套切换样板真实渲染, 收集 JS 异常+画布非透明像素活性, 实测 254/254 OK 0 空白 0 异常); 6 套代表性样板视觉抽验通过(704 平开窗/31637 推拉门格条裙板/31343 双扇门裙板/31353 三扇推拉16条扇内分割/31593 平开窗/31603 四扇推拉)——v2.39 为渲染器/工具层共性修复, 全量样板自动受益, 无需逐套修改 (Files: frontend/scripts/verify-sash-panels.mjs, frontend/scripts/patrol-render-all.mjs) - [2026-10-05 07:30] TEST: v2.40 全量 254 套模板视觉校验——v2.39 消费路径全量回归 0 缺陷。新增两个常驻校验资产: ①frontend/scripts/verify-sash-panels.mjs(结构级: 渲染器同路径全量跑 重放/扇推断/锚点匹配/inferSashPanels, A~E 五级异常分类+子格覆盖度断言, 实测 254 文件/409 形状/570 扇/726 子格(裙板 150/格条 58) 0 异常) ②frontend/scripts/patrol-render-all.mjs(渲染级: 无头浏览器逐套切换样板真实渲染, 收集 JS 异常+画布非透明像素活性, 实测 254/254 OK 0 空白 0 异常); 6 套代表性样板视觉抽验通过(704 平开窗/31637 推拉门格条裙板/31343 双扇门裙板/31353 三扇推拉16条扇内分割/31593 平开窗/31603 四扇推拉)——v2.39 为渲染器/工具层共性修复, 全量样板自动受益, 无需逐套修改 (Files: frontend/scripts/verify-sash-panels.mjs, frontend/scripts/patrol-render-all.mjs)
- [2026-10-05 07:14] FEAT: v2.39 渲染视觉与图纸对齐——源站原生扇内结构全量消费(扇内分割/裙板/格条/编号)。数据层诊断: 704(mm.sp=2 三区域+A1 平开扇)/31637(Frame 无分割+Slide 双推拉扇, 扇自带 mm.sp 裙板分割+mm.fls 玻璃/Panel 挂接+mm.dbs.bars decorationChinese 格条, hc=2 主格+gp=80 回纹)数据本身完整, 自研渲染器只消费顶层 mm.sp 未下钻扇内。新增 types/drawing.ts inferSashPanels(扇矩形构造临时 shape 复用 replayShape 全语义重放扇内分割, fls 按 polyId{idx,pos} 树寻址挂玻璃/裙板(type=Panel 判定), dbs.bars 格条解析 tp/gp/hc); 三渲染器消费: WindoorCanvas(2D 图纸: 裙板实色+格条内框/主格/四角回纹+玻璃编号 A1~A4)+RenderCanvas(效果图: 同语义)+WindoorThreed(3D: 裙板实体板+格条条体; 修复单 region 多扇时第二扇被 used 丢弃(改 glyph 自身矩形+共享区域保留静态底玻璃)); 扇形状按 sp 锚点归属匹配(修 sashes 数组序与视觉序交叉); WindoorCanvas 挂载加 ResizeObserver+零尺寸保护(修 el-dialog 内首帧 0 尺寸空白, 与 v2.38 3D 同型); 视觉验收: 704 渲染(开向三角/把手/编号)与 31637 四视图(双推拉+中式格条+裙板)均与官方参考图对齐; 回归: 254 套数据校验 0 失败+24 页巡检 0 异常+冒烟 60/60 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorCanvas.vue, frontend/src/components/RenderCanvas.vue, frontend/src/components/WindoorThreed.vue) - [2026-10-05 07:14] FEAT: v2.39 渲染视觉与图纸对齐——源站原生扇内结构全量消费(扇内分割/裙板/格条/编号)。数据层诊断: 704(mm.sp=2 三区域+A1 平开扇)/31637(Frame 无分割+Slide 双推拉扇, 扇自带 mm.sp 裙板分割+mm.fls 玻璃/Panel 挂接+mm.dbs.bars decorationChinese 格条, hc=2 主格+gp=80 回纹)数据本身完整, 自研渲染器只消费顶层 mm.sp 未下钻扇内。新增 types/drawing.ts inferSashPanels(扇矩形构造临时 shape 复用 replayShape 全语义重放扇内分割, fls 按 polyId{idx,pos} 树寻址挂玻璃/裙板(type=Panel 判定), dbs.bars 格条解析 tp/gp/hc); 三渲染器消费: WindoorCanvas(2D 图纸: 裙板实色+格条内框/主格/四角回纹+玻璃编号 A1~A4)+RenderCanvas(效果图: 同语义)+WindoorThreed(3D: 裙板实体板+格条条体; 修复单 region 多扇时第二扇被 used 丢弃(改 glyph 自身矩形+共享区域保留静态底玻璃)); 扇形状按 sp 锚点归属匹配(修 sashes 数组序与视觉序交叉); WindoorCanvas 挂载加 ResizeObserver+零尺寸保护(修 el-dialog 内首帧 0 尺寸空白, 与 v2.38 3D 同型); 视觉验收: 704 渲染(开向三角/把手/编号)与 31637 四视图(双推拉+中式格条+裙板)均与官方参考图对齐; 回归: 254 套数据校验 0 失败+24 页巡检 0 异常+冒烟 60/60 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorCanvas.vue, frontend/src/components/RenderCanvas.vue, frontend/src/components/WindoorThreed.vue)
- [2026-10-05 06:23] FIX: v2.38 效果图页切 Tab 后 3D 展示空白修复。根因: WindoorThreed 在 /rendering 页以 v-show=false(display:none) 挂载, 容器 clientWidth=0 时 Three.js 以 0 尺寸初始化(camera.aspect=0/0=NaN), 页内切 Tab 显示后仅 window resize 事件无法触发重设尺寸→画布永远空白(直接访问 /threed 路由正常, 故 24 页巡检未覆盖); 修复: onResize 加零尺寸保护(避免 NaN 投影矩阵)+ResizeObserver 监听容器(显示瞬间尺寸 0→正常自动恢复)+初始 setSize 兜底 ||1; 无头复验 /rendering 切 Tab 场景渲染正常; 巡检 24 页 0 异常 (Files: frontend/src/components/WindoorThreed.vue) - [2026-10-05 06:23] FIX: v2.38 效果图页切 Tab 后 3D 展示空白修复。根因: WindoorThreed 在 /rendering 页以 v-show=false(display:none) 挂载, 容器 clientWidth=0 时 Three.js 以 0 尺寸初始化(camera.aspect=0/0=NaN), 页内切 Tab 显示后仅 window resize 事件无法触发重设尺寸→画布永远空白(直接访问 /threed 路由正常, 故 24 页巡检未覆盖); 修复: onResize 加零尺寸保护(避免 NaN 投影矩阵)+ResizeObserver 监听容器(显示瞬间尺寸 0→正常自动恢复)+初始 setSize 兜底 ||1; 无头复验 /rendering 切 Tab 场景渲染正常; 巡检 24 页 0 异常 (Files: frontend/src/components/WindoorThreed.vue)
+22 -5
View File
@@ -84,6 +84,12 @@ function render() {
// 扇层:扇内子格(裙板/格条/编号) + 扇料框 + 开向符号 + 把手(语义同 WindoorCanvas,视觉对齐 2D 图纸) // 扇层:扇内子格(裙板/格条/编号) + 扇料框 + 开向符号 + 把手(语义同 WindoorCanvas,视觉对齐 2D 图纸)
const rp = replayShape(shape) const rp = replayShape(shape)
const glyphs = inferSashGlyphs(shape, rp).sashes const glyphs = inferSashGlyphs(shape, rp).sashes
// 裙板色 = 型材色(cm.g rgba 字符串 → 不透明深色), 解析失败退中性深灰
const cmRaw = typeof shape.cm?.g === 'string' ? shape.cm.g : ''
const cmMatch = cmRaw.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
const frameTint = cmMatch
? `rgb(${Math.round(+cmMatch[1] * 0.62)},${Math.round(+cmMatch[2] * 0.62)},${Math.round(+cmMatch[3] * 0.62)})`
: '#6b6e73'
const sashShapes: SashShape[] = [] const sashShapes: SashShape[] = []
const collect = (s: FrameShape | SashShape) => { const collect = (s: FrameShape | SashShape) => {
if (!['Frame', 'Slide'].includes(String(s.type))) sashShapes.push(s as SashShape) if (!['Frame', 'Slide'].includes(String(s.type))) sashShapes.push(s as SashShape)
@@ -105,10 +111,10 @@ function render() {
const px0 = tx(p.x0), px1 = tx(p.x1), py0 = ty(p.y0), py1 = ty(p.y1) const px0 = tx(p.x0), px1 = tx(p.x1), py0 = ty(p.y0), py1 = ty(p.y1)
if (px1 - px0 < 4 || py1 - py0 < 4) continue if (px1 - px0 < 4 || py1 - py0 < 4) continue
if (!p.glass) { if (!p.glass) {
// 裙板/实板:暖米色 + 内凹描边 // 裙板/实板:型材同色系(取 cm.g, 官方图纸裙板与框扇同色) + 内凹描边
ctx.fillStyle = '#ddd3c0' ctx.fillStyle = frameTint
ctx.fillRect(px0, py0, px1 - px0, py1 - py0) ctx.fillRect(px0, py0, px1 - px0, py1 - py0)
ctx.strokeStyle = 'rgba(90,84,72,0.55)' ctx.strokeStyle = 'rgba(0,0,0,0.35)'
ctx.lineWidth = 1.5 ctx.lineWidth = 1.5
ctx.strokeRect(px0 + 3, py0 + 3, px1 - px0 - 6, py1 - py0 - 6) ctx.strokeRect(px0 + 3, py0 + 3, px1 - px0 - 6, py1 - py0 - 6)
} else if (p.deco) { } else if (p.deco) {
@@ -183,9 +189,9 @@ function render() {
ctx.stroke() ctx.stroke()
ctx.setLineDash([]) ctx.setLineDash([])
} }
// 把手: 铰链对面侧白条 // 把手: 铰链对面侧白条(门=贯穿长条, 窗=短条) + 门上下锁点块
const dir = g.openDirection ?? 'left' const dir = g.openDirection ?? 'left'
const hl = Math.max(10, h * (g.isDoor ? 0.34 : 0.2)) const hl = Math.max(10, h * (g.isDoor ? 0.55 : 0.2))
const hw2 = Math.max(4, 5) const hw2 = Math.max(4, 5)
ctx.fillStyle = '#f4f4f6' ctx.fillStyle = '#f4f4f6'
ctx.strokeStyle = '#3c4147' ctx.strokeStyle = '#3c4147'
@@ -200,6 +206,17 @@ function render() {
} }
ctx.fill() ctx.fill()
ctx.stroke() ctx.stroke()
// 门锁点(把手上下两端小方块)
if (g.isDoor && g.kind === 'casement' && !dir.startsWith('up')) {
const dx = dir.startsWith('left') ? bx1 - hw2 / 2 - 3 : bx0 + hw2 / 2 + 3
const dot = Math.max(3, hw2 * 0.8)
for (const oy of [cy - hl / 3, cy + hl / 3]) {
ctx.beginPath()
ctx.rect(dx - dot / 2, oy - dot / 2, dot, dot)
ctx.fill()
ctx.stroke()
}
}
}) })
// 外框 // 外框
const all = dd.sm.flatMap(s => replayShape(s).regions.flat().map(seg => seg.ps)) const all = dd.sm.flatMap(s => replayShape(s).regions.flat().map(seg => seg.ps))
+10 -5
View File
@@ -596,10 +596,15 @@ function drawFrame(
const px0 = tx(p.x0), px1 = tx(p.x1), py0 = ty(p.y0), py1 = ty(p.y1) const px0 = tx(p.x0), px1 = tx(p.x1), py0 = ty(p.y0), py1 = ty(p.y1)
if (px1 - px0 < 4 || py1 - py0 < 4) continue if (px1 - px0 < 4 || py1 - py0 < 4) continue
if (!p.glass) { if (!p.glass) {
ctx.fillStyle = '#e5dfd3' // 裙板: 型材深色(官方图纸裙板与框扇同色系) + 竖纹肌理
ctx.fillStyle = '#5f6268'
ctx.fillRect(px0, py0, px1 - px0, py1 - py0) ctx.fillRect(px0, py0, px1 - px0, py1 - py0)
ctx.strokeStyle = STYLE.profileDark ctx.strokeStyle = 'rgba(255,255,255,0.14)'
ctx.lineWidth = 1 ctx.lineWidth = 1
for (let vx = px0 + 7; vx < px1 - 3; vx += 7) {
ctx.beginPath(); ctx.moveTo(vx, py0 + 3); ctx.lineTo(vx, py1 - 3); ctx.stroke()
}
ctx.strokeStyle = STYLE.profileDark
ctx.strokeRect(px0 + 2, py0 + 2, px1 - px0 - 4, py1 - py0 - 4) ctx.strokeRect(px0 + 2, py0 + 2, px1 - px0 - 4, py1 - py0 - 4)
} else if (p.deco) { } else if (p.deco) {
// hc 等分主格 + gp 内框 + 四角回纹块(近似源站 decorationChinese) // hc 等分主格 + gp 内框 + 四角回纹块(近似源站 decorationChinese)
@@ -627,7 +632,7 @@ function drawFrame(
} }
if (p.glassText) { if (p.glassText) {
const fs2 = Math.max(9, Math.min(14, (px1 - px0) * 0.1)) const fs2 = Math.max(9, Math.min(14, (px1 - px0) * 0.1))
ctx.fillStyle = p.glass ? 'rgba(255,255,255,0.92)' : STYLE.label ctx.fillStyle = 'rgba(255,255,255,0.92)'
ctx.font = `bold ${fs2}px sans-serif` ctx.font = `bold ${fs2}px sans-serif`
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
@@ -668,9 +673,9 @@ function drawFrame(
ctx.stroke() ctx.stroke()
ctx.setLineDash([]) ctx.setLineDash([])
} }
// 把手: 铰链对面侧(白条+描边+上下锁点) // 把手: 铰链对面侧(白条+描边; 门=贯穿长条, 窗=短条)+上下锁点
const dir = g.openDirection ?? 'left' const dir = g.openDirection ?? 'left'
const hl = Math.max(8, h * (g.isDoor ? 0.36 : 0.2)) const hl = Math.max(8, h * (g.isDoor ? 0.55 : 0.2))
const hw2 = Math.max(3.5, lw * 0.55) const hw2 = Math.max(3.5, lw * 0.55)
ctx.fillStyle = '#f4f4f6' ctx.fillStyle = '#f4f4f6'
ctx.strokeStyle = STYLE.profileDark ctx.strokeStyle = STYLE.profileDark
+2 -1
View File
@@ -361,8 +361,9 @@ function build(drawing: DrawingData) {
const lmy = (gRect.y1 / 2 + gRect.y0 / 2 - (p.y0 + p.y1) / 2) * S const lmy = (gRect.y1 / 2 + gRect.y0 / 2 - (p.y0 + p.y1) / 2) * S
if (![pw, ph, lmx, lmy].every(Number.isFinite) || pw <= 0.01 || ph <= 0.01) continue if (![pw, ph, lmx, lmy].every(Number.isFinite) || pw <= 0.01 || ph <= 0.01) continue
if (!p.glass) { if (!p.glass) {
// 裙板: 型材深色系(与框扇同色域, 对齐官方门板观感)
const board = new THREE.Mesh(new THREE.BoxGeometry(pw, ph, 0.012), const board = new THREE.Mesh(new THREE.BoxGeometry(pw, ph, 0.012),
new THREE.MeshStandardMaterial({ color: 0xd9cdb8, roughness: 0.7, metalness: 0.05 })) new THREE.MeshStandardMaterial({ color: 0x606369, roughness: 0.65, metalness: 0.1 }))
board.position.set(lmx, lmy, 0.004) board.position.set(lmx, lmy, 0.004)
group.add(board) group.add(board)
} else if (p.deco) { } else if (p.deco) {