fix: v2.10 3D 室外场景窗户陷入墙体+平开扇出框双重修复

- 墙体与窗体 z 轴关系: 墙厚 3→2.4×型深, 外墙面退至窗框外表面之后(窗框外露 30% 型深), 消除整窗埋入墙厚
- 窗洞净空自适应外框面宽(fwMax+0.015), 替代固定 0.05
- 平开扇铰链偏移符号修复(+(cxr-hingeX)), 消除关闭态扇出框/开启态扇悬空
- 草地垫对齐新墙面, 网格降至墙脚
- 新增 verify-wall.mjs 真实浏览器三态截图验证
This commit is contained in:
HHY
2026-09-23 03:27:14 -07:00
parent 6b11b6803b
commit fbef67e450
6 changed files with 69 additions and 8 deletions
+52
View File
@@ -0,0 +1,52 @@
// 室外场景墙体/窗体位置修复验证截图: node verify-wall.mjs <token>
// ① 默认(室外+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()
+15 -8
View File
@@ -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)
// 阴影相机范围