fix: v2.19 组合水平镜像的 3D 开向同步+放大层菜单可见性

- mirrorUnit 补铰链侧换向: hm.openDirection/sashes.openDirection left↔right 镜像(含变体)
- 数学确认 mirPt+norm.x 翻转组合下重放树拓扑保持, 扇 polyId 无需换位
- rotateUnit 补 hm.openDirection 刚体旋转换向(left→up→right→down)
- ctx-menu z-index 40→2100, 放大预览时右键菜单不再被盖
- 验证: 镜像前后开合 50% 对比截图(铰链侧正确换向), 全量校验 0 失败
This commit is contained in:
HHY
2026-09-24 08:53:06 -07:00
parent bbdf03f56e
commit 095620110f
11 changed files with 33 additions and 2 deletions
+31 -2
View File
@@ -93,6 +93,21 @@ function bbOfLocal(shapes: any[]): BBox | null {
}
// ---------- 旋转/镜像编辑 ----------
/** 铰链侧随刚体旋转换向: 顺时针 left→up→right→down→left */
function rotDir(d: unknown, deg: number) {
const s = String(d ?? '')
if (!s) return undefined
const order = ['left', 'up', 'right', 'down']
const mapOne = (one: string) => {
const i = order.indexOf(one)
if (i < 0) return one
const step = ((deg / 90) % 4 + 4) % 4 // 顺时针步数
return order[(i + step) % 4]
}
// 形如 left_with_down: 两段分别映射
return s.split('_with_').map(mapOne).join('_with_')
}
/** 旋转整个单元(绕其几何中心),同步旋转分割线法向与开向角度 */
function rotateUnit(uid: number, deg: 90 | -90 | 180) {
const target = props.shapes.filter(s => s.__uid === uid)
@@ -117,8 +132,11 @@ function rotateUnit(uid: number, deg: 90 | -90 | 180) {
for (const sub of (s.sm ?? [])) {
if (sub.bd) { rotPt(sub.bd.from); rotPt(sub.bd.to) }
if (typeof sub.od === 'number') sub.od = (sub.od + deg + 360) % 360
const hm = (sub as any).hm
if (hm?.openDirection) hm.openDirection = rotDir(hm.openDirection, deg)
if (Array.isArray(sub.sashes)) for (const e of sub.sashes) {
if (typeof e?.od === 'number') e.od = (e.od + deg + 360) % 360
if ((e as any)?.hm?.openDirection) (e as any).hm.openDirection = rotDir((e as any).hm.openDirection, deg)
}
}
}
@@ -126,13 +144,20 @@ function rotateUnit(uid: number, deg: 90 | -90 | 180) {
emit('change')
}
/** 水平镜像整个单元(开向同步镜像) */
/** 水平镜像整个单元(几何+分割线法向+开向角度+铰链侧同步镜像) */
function mirrorUnit(uid: number) {
const target = props.shapes.filter(s => s.__uid === uid)
const bb = bbOfLocal(target)
if (!bb) return
const cx2 = (bb.x0 + bb.x1) / 2
const mirPt = (p: any) => { p.x = 2 * cx2 - p.x }
// 铰链侧换向: left↔right(含 *_with_* 变体), up 不变
const flipDir = (d: unknown) => {
const s = String(d ?? '')
if (s.startsWith('left')) return 'right' + s.slice(4)
if (s.startsWith('right')) return 'left' + s.slice(5)
return s || undefined
}
for (const s of target) {
for (const r of (s.polygon?.polygon ?? [])) for (const seg of r) { mirPt(seg.ps); mirPt(seg.pe) }
for (const sp of (s.mm?.sp ?? [])) {
@@ -142,8 +167,12 @@ function mirrorUnit(uid: number) {
for (const sub of (s.sm ?? [])) {
if (sub.bd) { mirPt(sub.bd.from); mirPt(sub.bd.to) }
if (typeof sub.od === 'number') sub.od = (180 - sub.od + 360) % 360
const hm = (sub as any).hm
if (hm?.openDirection) hm.openDirection = flipDir(hm.openDirection)
if (Array.isArray(sub.sashes)) for (const e of sub.sashes) {
if (typeof e?.od === 'number') e.od = (180 - e.od + 360) % 360
if ((e as any)?.hm?.openDirection) (e as any).hm.openDirection = flipDir((e as any).hm.openDirection)
if (e?.openDirection) e.openDirection = flipDir(e.openDirection)
}
}
}
@@ -552,7 +581,7 @@ canvas { width: 100%; height: 100%; display: block; cursor: grab; }
canvas:active { cursor: grabbing; }
.hud { position: absolute; left: 10px; top: 10px; background: rgba(255,255,255,0.9);
border-radius: 6px; padding: 3px 10px; font-size: 12px; color: #606266; }
.ctx-menu { position: absolute; z-index: 40; background: #fff; border-radius: 8px;
.ctx-menu { position: absolute; z-index: 2100; background: #fff; border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.16); padding: 4px 0; min-width: 150px; }
.ctx-title { font-size: 11px; color: #909399; padding: 4px 12px; border-bottom: 1px solid #f0f0f0; }
.ctx-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; font-size: 13px;