diff --git a/CHANGELOG b/CHANGELOG index 50abdc0..6da27f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # CHANGELOG +- [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 06:30] FEAT: v2.15 图纸封面图一致性收尾——官方 PNG 剩余元素补齐。①分段尺寸标注: 按重放区域分割点生成蓝色分段(每格宽/高)+总尺寸双排标注(端部刻度, 左侧文字旋转), 替换原仅总宽高文本; ②玻璃配置第二行: fl.spet.sp(如 5+12+5)画于编号下方(704 无配置不画, 与官方一致); ③中梃与外框交点白色连接件方块; 修复 spIdx 遍历变量; 验证: 704 画布与官方封面图逐元素一致(分段 663/1137/393/1207+总 1800/1600), ui-shots 6 页无报错 (Files: frontend/src/components/WindoorCanvas.vue, shots/*) diff --git a/frontend/scripts/verify-door.mjs b/frontend/scripts/verify-door.mjs new file mode 100644 index 0000000..ac528e0 --- /dev/null +++ b/frontend/scripts/verify-door.mjs @@ -0,0 +1,38 @@ +// 门类 3D 建模验证: 切换样板 31337(对开门) 截图关/开: node verify-door.mjs +import puppeteer from 'puppeteer-core' +const token = process.argv[2] +const sleep = ms => new Promise(r => setTimeout(r, ms)) +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', '--use-gl=angle', '--use-angle=swiftshader'], + defaultViewport: { width: 1440, height: 900 }, +}) +const page = await browser.newPage() +await page.goto(`http://localhost:8000/threed?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' }) +await sleep(1500) +// 打开样板下拉并选择 31337 +await page.click('.el-select__wrapper, .el-select') +await sleep(600) +const items = await page.$$('.el-select-dropdown__item') +for (const it of items) { + const txt = await it.evaluate(el => el.textContent.trim()) + if (txt.includes('31337')) { await it.click(); break } +} +await sleep(2200) +const cv = await (await page.$('.threed-wrap canvas')).boundingBox() +await page.screenshot({ path: 'D:/Code/huamenchuang/shots/door-31337-closed.png', clip: cv }) +// 拖滑杆到 55% +const handle = await page.$('.ctrl .el-slider__button') +const hr = await handle.boundingBox() +await page.mouse.move(hr.x + hr.width / 2, hr.y + hr.height / 2) +await page.mouse.down() +const rr = await (await page.$('.ctrl .el-slider__runway')).boundingBox() +for (let i = 1; i <= 10; i++) { + await page.mouse.move(rr.x + rr.width * 0.55, hr.y + hr.height / 2, { steps: 1 }) + await sleep(40) +} +await page.mouse.up() +await sleep(1400) +await page.screenshot({ path: 'D:/Code/huamenchuang/shots/door-31337-open.png', clip: cv }) +console.log('✓ 31337 对开门 3D 截图完成') +await browser.close() diff --git a/frontend/src/components/WindoorCanvas.vue b/frontend/src/components/WindoorCanvas.vue index 5c6f763..5f60758 100644 --- a/frontend/src/components/WindoorCanvas.vue +++ b/frontend/src/components/WindoorCanvas.vue @@ -522,6 +522,25 @@ function drawFrame( } ctx.textBaseline = 'alphabetic' + // 纱扇(Screen): 官方图纸的网格纹理(不生成开合扇) + for (const sub of (shape.sm ?? []) as SashShape[]) { + if (String(sub.type ?? '') !== 'Screen' || sub.enabled === false) continue + const segs = rp.resolveRef?.(sub.polyId?.idx ?? -1, sub.polyId?.pos) + if (!segs) continue + const bb = bboxOf(regionPoints(segs)) + const x0 = tx(bb.x0), y0 = ty(bb.y0), x1 = tx(bb.x1), y1 = ty(bb.y1) + ctx.save() + ctx.beginPath() + ctx.rect(x0, y0, x1 - x0, y1 - y0) + ctx.clip() + ctx.strokeStyle = 'rgba(60,66,72,0.32)' + ctx.lineWidth = 0.7 + const step = Math.max(5, (x1 - x0) / 22) + for (let gx = x0; gx < x1; gx += step) { ctx.beginPath(); ctx.moveTo(gx, y0); ctx.lineTo(gx, y1); ctx.stroke() } + for (let gy = y0; gy < y1; gy += step) { ctx.beginPath(); ctx.moveTo(x0, gy); ctx.lineTo(x1, gy); ctx.stroke() } + ctx.restore() + } + // 悬停格淡高亮(可编辑提示) if (props.editable && hoverRegion && hoverRegion.shapeIdx === shapeIdx && !(selRegion.value && selRegion.value.shapeIdx === shapeIdx && selRegion.value.ri === hoverRegion.ri)) { diff --git a/frontend/src/types/drawing.ts b/frontend/src/types/drawing.ts index eaaffce..8c6516f 100644 --- a/frontend/src/types/drawing.ts +++ b/frontend/src/types/drawing.ts @@ -336,7 +336,8 @@ function hmOf(sub: SashShape): { openDirection?: string; openToward?: string } { /** * 扇样式推断: 有效bd → polyId{idx,pos}挂接区域(原站以区域挂接扇) → 帧bbox按扇数均分兜底。 - * kind 由类型决定(Slide/DoubleSash=推拉, 其余=平开); 开合五金取 hm.openDirection/hm.openToward。 + * kind 由类型决定(仅 Slide=推拉, DoubleSash/DoubleKfcSash=双扇对开门按平开); 纱扇(Screen)与 + * 玻璃扇同格挂接故不单独渲染; 同格多扇沿宽度等分; 开合五金取 hm.openDirection/hm.openToward。 */ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Segment[][]): { regions: Segment[][] @@ -358,8 +359,8 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Seg const ok = [r.x0, r.y0, r.x1, r.y1].every(v => Number.isFinite(v)) return ok && !bd.hidden && r.x1 - r.x0 > 10 && r.y1 - r.y0 > 10 ? r : null } - const kindOf = (sub: SashShape) => (sub.type === 'DoubleSash' || sub.type === 'Slide' - ? 'sliding' as const : 'casement' as const) + // kind 仅 Slide=推拉; DoubleSash/DoubleKfcSash=双扇对开门(KfcSash=内开内倒), 均按平开处理 + const kindOf = (sub: SashShape) => (String(sub.type) === 'Slide' ? 'sliding' as const : 'casement' as const) // polyId{idx,pos} 树寻址; 无树信息时退化为扁平下标 const rectOfRef = (ref?: { idx?: number; pos?: number }) => { if (!ref || !Number.isInteger(ref.idx) || (ref.idx as number) < 0) return null @@ -370,18 +371,18 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Seg const i = ref.idx as number return i < regionBBs.length ? regionBBs[i] : null } - const pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape) => { + const pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape, forceRect = false) => { const kind = kindOf(sub) const hm = hmOf(sub) for (const e of entries) { - // 子扇自带挂接区域时优先生效(双扇各自挂半区) - const rect = rectOfRef((e as { polyId?: { idx?: number; pos?: number } }).polyId) ?? r + // 子扇自带挂接区域时优先生效(双扇各自挂半区); 等分模式下强制使用分块 + const rect = !forceRect ? (rectOfRef((e as { polyId?: { idx?: number; pos?: number } }).polyId) ?? r) : r glyphs.push({ x0: rect.x0, y0: rect.y0, x1: rect.x1, y1: rect.y1, od: (e.od ?? sub.od ?? 90) as number, openToward: (e.openToward ?? sub.openToward ?? hm.openToward) as string | undefined, openDirection: (e.openDirection ?? hm.openDirection) as string | undefined, - isDoor: Boolean(e.isDoor ?? sub.isDoor), + isDoor: Boolean(e.isDoor || sub.isDoor), wtf: Boolean(e.wtf ?? sub.wtf ?? sub.withTurningFrame), kind, }) @@ -389,21 +390,22 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Seg } const unmatched: { sub: SashShape; entries: any[] }[] = [] for (const sub of (shape.sm ?? []) as SashShape[]) { + const type = String(sub.type ?? '') + if (type === 'Screen' || type === 'DoubleScreen') continue // 纱扇与玻璃扇同格挂接, 不单独渲染 const raw = (sub.sashes?.length ? sub.sashes : [sub]) as any[] const entries = raw.filter(e => e && e.enabled !== false) if (!entries.length) continue + // 扇数: 多 entries 依 entries 数; Double*(双扇/对开门)取 2 + const n = Math.max(entries.length, type.startsWith('Double') ? 2 : 1) let rect = sub.bd ? bdRect(sub.bd) : null if (!rect) rect = rectOfRef(sub.polyId) if (rect) { - if (entries.length === 2 && !raw.some(e => e?.polyId?.idx != null)) { - // 双扇共区且无独立挂接: 沿长边对半 - const w = rect.x1 - rect.x0, h = rect.y1 - rect.y0 - const halves = w >= h - ? [{ ...rect, x1: (rect.x0 + rect.x1) / 2 }, { ...rect, x0: (rect.x0 + rect.x1) / 2 }] - : [{ ...rect, y1: (rect.y0 + rect.y1) / 2 }, { ...rect, y0: (rect.y0 + rect.y1) / 2 }] - entries.forEach((e, k) => pushGlyphs(halves[k], [e], sub)) - } else { - pushGlyphs(rect, entries, sub) + // 同格多扇(对开门/双扇/多滑扇): 沿宽度等分, 每块取对应扇属性 + for (let k = 0; k < n; k++) { + const part = n > 1 + ? { x0: rect.x0 + ((rect.x1 - rect.x0) * k) / n, y0: rect.y0, x1: rect.x0 + ((rect.x1 - rect.x0) * (k + 1)) / n, y1: rect.y1 } + : rect + pushGlyphs(part, [entries[Math.min(k, entries.length - 1)]], sub, n > 1) } continue } @@ -428,7 +430,7 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Seg od: (e.od ?? u.sub.od ?? 90) as number, openToward: (e.openToward ?? u.sub.openToward ?? hmOf(u.sub).openToward) as string | undefined, openDirection: (e.openDirection ?? hmOf(u.sub).openDirection) as string | undefined, - isDoor: Boolean(e.isDoor ?? u.sub.isDoor), + isDoor: Boolean(e.isDoor || u.sub.isDoor), kind: kindOf(u.sub), }) i++ diff --git a/shots/02-combine-two.png b/shots/02-combine-two.png index c7e982b..e6348fa 100644 Binary files a/shots/02-combine-two.png and b/shots/02-combine-two.png differ diff --git a/shots/04-combine-3d.png b/shots/04-combine-3d.png index 20c98fa..71b5d85 100644 Binary files a/shots/04-combine-3d.png and b/shots/04-combine-3d.png differ diff --git a/shots/06-dashboard.png b/shots/06-dashboard.png index e7f3372..2e15000 100644 Binary files a/shots/06-dashboard.png and b/shots/06-dashboard.png differ diff --git a/shots/door-31337-closed.png b/shots/door-31337-closed.png new file mode 100644 index 0000000..6a5b62a Binary files /dev/null and b/shots/door-31337-closed.png differ diff --git a/shots/door-31337-open.png b/shots/door-31337-open.png new file mode 100644 index 0000000..47086eb Binary files /dev/null and b/shots/door-31337-open.png differ diff --git a/shots/verify-31729-3d.png b/shots/verify-31729-3d.png new file mode 100644 index 0000000..81d42da Binary files /dev/null and b/shots/verify-31729-3d.png differ