feat: v1.5 3D 门窗开合动画

- 扇推断逻辑抽取为共享模块 inferSashGlyphs(2D符号与3D动画共用)
- WindoorThreed 重写: 扇面板独立网格组
  平开扇绕铰链旋转(od定铰链侧, 门75°/平开32°/双向门40°)
  推拉扇沿轨道平移(双扇前后轨交替)
  区域归属扇的不再重复渲染静态玻璃
- 开合控制条: 开合度滑杆+自动演示+全开/全关, 平滑插值动画
- 组合画布 3D 预览自动获得开合能力
This commit is contained in:
HHY
2026-09-21 01:59:12 -07:00
parent b404653862
commit 1585445436
4 changed files with 225 additions and 90 deletions
+3 -63
View File
@@ -37,7 +37,7 @@
import { onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
bboxOf, parseDrawing, regionPoints, replayShape, spAxis, spPos,
bboxOf, inferSashGlyphs, parseDrawing, regionPoints, replayShape, spAxis, spPos,
type DrawingData, type FrameShape, type Pt, type Segment, type SashShape, type SplitLine,
} from '../types/drawing'
@@ -245,68 +245,8 @@ function drawFrame(
ctx.strokeRect(x0 + inset, y0 + inset, (x1 - x0) - inset * 2, (y1 - y0) - inset * 2)
}
// 扇(推断: 有效bd → polyId区域(双扇对半) → 帧bbox按扇数均分)
const regionBBs = regions.map(pts => bboxOf(pts))
type Glyph = { x0: number; y0: number; x1: number; y1: number; od: number; openToward?: string; isDoor?: boolean; wtf?: boolean }
const glyphs: Glyph[] = []
const bdRect = (bd: NonNullable<SashShape['bd']>) => {
const r = {
x0: Math.min(bd.from.x, bd.to.x), y0: Math.min(bd.from.y, bd.to.y),
x1: Math.max(bd.from.x, bd.to.x), y1: Math.max(bd.from.y, bd.to.y),
}
return !bd.hidden && r.x1 - r.x0 > 10 && r.y1 - r.y0 > 10 ? r : null
}
const pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape) => {
for (const e of entries) {
glyphs.push({
x0: r.x0, y0: r.y0, x1: r.x1, y1: r.y1,
od: (e.od ?? sub.od ?? 90) as number,
openToward: (e.openToward ?? sub.openToward) as string | undefined,
isDoor: Boolean(e.isDoor ?? sub.isDoor),
wtf: Boolean(e.wtf ?? sub.wtf ?? sub.withTurningFrame),
})
}
}
const unmatched: { sub: SashShape; entries: any[] }[] = []
for (const sub of (shape.sm ?? []) as SashShape[]) {
const raw = (sub.sashes?.length ? sub.sashes : [sub]) as any[]
const entries = raw.filter(e => e && e.enabled !== false)
if (!entries.length) continue
const rect = sub.bd ? bdRect(sub.bd) : null
if (rect) { pushGlyphs(rect, entries, sub); continue }
const idx = sub.polyId?.idx
const rb = idx != null && regionBBs[idx] ? regionBBs[idx] : null
if (rb) {
if (entries.length === 2) {
const w = rb.x1 - rb.x0, h = rb.y1 - rb.y0
const halves = w >= h
? [{ ...rb, x1: (rb.x0 + rb.x1) / 2 }, { ...rb, x0: (rb.x0 + rb.x1) / 2 }]
: [{ ...rb, y1: (rb.y0 + rb.y1) / 2 }, { ...rb, y0: (rb.y0 + rb.y1) / 2 }]
entries.forEach((e, k) => pushGlyphs(halves[k], [e], sub))
} else {
pushGlyphs(rb, entries, sub)
}
} else {
unmatched.push({ sub, entries })
}
}
if (unmatched.length && regions.length) {
const fb = bboxOf(regions.flat())
const K = unmatched.reduce((s, u) => s + u.entries.length, 0)
const colW = (fb.x1 - fb.x0) / K
let i = 0
for (const u of unmatched) {
for (const e of u.entries) {
glyphs.push({
x0: fb.x0 + i * colW, x1: fb.x0 + (i + 1) * colW, y0: fb.y0, y1: fb.y1,
od: (e.od ?? u.sub.od ?? 90) as number,
openToward: (e.openToward ?? u.sub.openToward) as string | undefined,
isDoor: Boolean(e.isDoor ?? u.sub.isDoor),
})
i++
}
}
}
// 扇(推断: 有效bd → polyId区域(双扇对半) → 帧bbox按扇数均分) — 共享推断模块
const glyphs = inferSashGlyphs(shape, rp.regions).sashes
for (const g of glyphs) {
const bx0 = tx(g.x0), bx1 = tx(g.x1), by0 = ty(g.y1), by1 = ty(g.y0)
const w = bx1 - bx0, h = by1 - by0