diff --git a/CHANGELOG b/CHANGELOG index 16dedbc..d44773b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # CHANGELOG +- [2026-09-23 03:26] FIX: v2.10 3D 预览"窗户嵌入墙体内"双重根因修复。①室外墙体定位: 原墙体(3×型深厚)与窗体同在 z=0 居中, 整窗埋入墙厚内部(洞口内只见深处玻璃); 改为墙厚 2.4×型深(≈240 实墙比例)且外墙面退至窗框外表面之后(窗框外露 30% 型深, 对齐外平开窗贴墙安装), 窗洞净空由固定 0.05 改为自适应外框面宽 fwMax+0.015 安装缝(防宽面型材边缘被墙面遮挡); ②平开扇铰链偏移符号修复: 扇子元素为面板中心局部坐标, 绕铰链边旋转应平移 +(cxr-hingeX)(原负号致面板整体多偏移一个扇宽——关闭态扇面板跑出框外右侧、开启态扇悬空脱离窗框, 实测样板704 偏移恰为扇宽 1137mm 与截图吻合); ③草地垫对齐新墙面、网格降至墙脚消除穿墙线; 真实浏览器三态截图验证(45°全关/正面全关/46%开合: 框体凸出外墙、扇全在框内、开扇贴框外旋✓) (Files: frontend/src/components/WindoorThreed.vue, frontend/scripts/verify-wall.mjs, shots/verify-wall-*.png) + - [2026-09-23 13:35] FEAT: v2.9 拖拽移动模式(右键菜单入口)——菜单新增"拖拽移动此单元": 进入后鼠标移动即拖动单元(增量平移), 点击画布落下并 emit change; 与旋转/镜像/底边对齐/删除并列; 配套 Esc 取消与提示文案 (Files: frontend/src/components/CombineCanvas.vue, frontend/scripts/verify-move.mjs) - [2026-09-23 00:20] FEAT: v2.8 3D 预览放大模式——组合页 3D 预览卡片加放大按钮(近全屏浮层 inset 2.5vh/2.5vw + z-index 2001), 默认高度 300→380, 切换后派发 resize 让 3D 自适应; drawing.ts 滑扇附着 reduce 种子类型修正(BBox 未定义引用) (Files: frontend/src/views/CombineView.vue, frontend/src/types/drawing.ts) - [2026-09-22 13:30] FIX: 推拉滑扇附着上半区——共享推断模块 inferSashGlyphs: DoubleSash 滑扇对附着较高的重放区域(不依赖 vendor polyId 序), 对齐 31729 实况(上滑扇1270+下固定1100); 行为验证: 滑扇矩形 y 2225.8-3370.8 ✓; 同步 verify 脚本 console 过滤放宽 (Files: frontend/src/types/drawing.ts, frontend/scripts/vei-check.mjs, frontend/scripts/verify-v3.mjs) diff --git a/frontend/scripts/verify-wall.mjs b/frontend/scripts/verify-wall.mjs new file mode 100644 index 0000000..ab1a664 --- /dev/null +++ b/frontend/scripts/verify-wall.mjs @@ -0,0 +1,52 @@ +// 室外场景墙体/窗体位置修复验证截图: node verify-wall.mjs +// ① 默认(室外+45°+全关) ② 正面视角全关(检查扇矩形是否出框) ③ 46% 开合(按压-移动-释放驱动 el-slider) +import puppeteer from 'puppeteer-core' +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() +page.on('pageerror', e => console.log(`[JS异常] ${String(e).slice(0, 200)}`)) +const token = process.argv[2] +const sleep = ms => new Promise(r => setTimeout(r, ms)) +await page.goto(`http://localhost:8000/threed?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' }) +await sleep(2600) +const cv = await (await page.$('.threed-wrap canvas')).boundingBox() +await page.screenshot({ path: 'D:/Code/huamenchuang/shots/verify-wall-closed.png', clip: cv }) +console.log('✓ 默认(室外+45°+全关)截图完成') +// 切正面视角(真实鼠标点击 radio) +const frontBtn = await page.evaluateHandle(() => { + const spans = [...document.querySelectorAll('.el-radio-button__inner')] + return spans.find(s => s.textContent.trim() === '正面') +}) +const fb = await frontBtn.asElement().boundingBox() +await page.mouse.click(fb.x + fb.width / 2, fb.y + fb.height / 2) +await sleep(800) +await page.screenshot({ path: 'D:/Code/huamenchuang/shots/verify-wall-front.png', clip: cv }) +console.log('✓ 正面视角截图完成') +// 切回 45° +const degBtn = await page.evaluateHandle(() => { + const spans = [...document.querySelectorAll('.el-radio-button__inner')] + return spans.find(s => s.textContent.trim() === '45°') +}) +const db = await degBtn.asElement().boundingBox() +await page.mouse.click(db.x + db.width / 2, db.y + db.height / 2) +await sleep(600) +// 46% 开合: 真实按压-移动-释放驱动 el-slider +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 runway = await page.$('.ctrl .el-slider__runway') +const rr = await runway.boundingBox() +for (let i = 1; i <= 12; i++) { + await page.mouse.move(rr.x + rr.width * 0.46, 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/verify-wall-open46.png', clip: cv }) +console.log('✓ 46% 开合截图完成') +await browser.close() diff --git a/frontend/src/components/WindoorThreed.vue b/frontend/src/components/WindoorThreed.vue index d8a9524..68cb03c 100644 --- a/frontend/src/components/WindoorThreed.vue +++ b/frontend/src/components/WindoorThreed.vue @@ -321,7 +321,8 @@ function build(drawing: DrawingData) { const hingeLeft = Math.cos(odRad) < 0 const hingeX = hingeLeft ? cxr - w / 2 : cxr + w / 2 group.position.set(hingeX, cyr, 0) - group.children.forEach(child => { child.position.x -= (cxr - hingeX) }) + // 子元素为面板中心局部坐标: 平移 +(cxr-hingeX) 使 t=0 世界位置不变, 仅将旋转支点移到铰链边 + group.children.forEach(child => { child.position.x += (cxr - hingeX) }) const maxAng = (s.isDoor ? 75 : s.wtf ? 40 : 32) * Math.PI / 180 const dirSign = s.openToward === 'inward' ? 1 : -1 sashAnims.push({ group, kind: 'casement', baseX: hingeX, slideDir: 0, @@ -344,12 +345,17 @@ function build(drawing: DrawingData) { } // ---- 环境场景(室内/室外/影棚)---- + let gridY = -gh * S / 2 - DEPTH * 2.2 if (scene3d.value === 'outdoor') { - // 室外: 建筑外墙围绕窗洞 + 草地 - const hw = gw * S / 2 + 0.05 // 窗洞半宽 - const hh = gh * S / 2 + 0.05 // 窗洞半高 + // 室外: 建筑外墙围绕窗洞 + 草地(墙体外墙面退至窗框外表面之后, 窗框外露30%型深, 避免整窗陷入墙体内) + const fwMax = Math.max(0.018, ...drawing.sm.map(sh => fwOf(sh, S))) + const gap = fwMax + 0.015 // 窗洞净空: 外框面宽 + 安装缝(固定值会遮挡宽面型材) + const hw = gw * S / 2 + gap // 窗洞半宽 + const hh = gh * S / 2 + gap // 窗洞半高 const side = 0.55 // 上下墙高度/左右墙宽度 - const wt = DEPTH * 3 + const wt = DEPTH * 2.4 // 墙厚(≈240 实墙比例) + const wallFront = DEPTH * 0.2 // 外墙面 z + const wallZ = wallFront - wt / 2 const wallBoxes: number[][] = [ [-hw - side, hh, side * 2 + hw * 2, side], // 上 [-hw - side, -hh - side, side * 2 + hw * 2, side], // 下 @@ -358,15 +364,16 @@ function build(drawing: DrawingData) { ] for (const [px, py, pw, ph] of wallBoxes) { const m = new THREE.Mesh(new THREE.BoxGeometry(pw, ph, wt), wallMat) - m.position.set(px + pw / 2, py + ph / 2, 0) + m.position.set(px + pw / 2, py + ph / 2, wallZ) m.receiveShadow = true m.userData.isWall = true sceneRoot.add(m) } const ground = new THREE.Mesh(new THREE.BoxGeometry(3, 0.02, DEPTH * 8), groundMat) - ground.position.set(0, -hh - side - 0.02, DEPTH * 2) + ground.position.set(0, -hh - side - 0.02, wallFront + DEPTH * 4) ground.receiveShadow = true sceneRoot.add(ground) + gridY = -hh - side - 0.008 // 网格降至墙脚, 避免穿墙 } else if (scene3d.value === 'indoor') { // 室内: 窗后整墙 + 室内地板(木色) const wall = new THREE.Mesh(new THREE.BoxGeometry(gw * S + 1.4, gh * S + 1.2, 0.02), wallMat) @@ -392,7 +399,7 @@ function build(drawing: DrawingData) { hasOpenable.value = sashAnims.length > 0 const grid = new THREE.GridHelper(2.4, 12, 0xc8cdd2, 0xdfe3e8) - grid.position.y = -gh * S / 2 - DEPTH * 2.2 + grid.position.y = gridY scene.add(grid) // 阴影相机范围 diff --git a/shots/verify-wall-closed.png b/shots/verify-wall-closed.png new file mode 100644 index 0000000..ddcda57 Binary files /dev/null and b/shots/verify-wall-closed.png differ diff --git a/shots/verify-wall-front.png b/shots/verify-wall-front.png new file mode 100644 index 0000000..9d419cc Binary files /dev/null and b/shots/verify-wall-front.png differ diff --git a/shots/verify-wall-open46.png b/shots/verify-wall-open46.png new file mode 100644 index 0000000..8b7cda6 Binary files /dev/null and b/shots/verify-wall-open46.png differ