feat: v1.5 3D 门窗开合动画
- 扇推断逻辑抽取为共享模块 inferSashGlyphs(2D符号与3D动画共用) - WindoorThreed 重写: 扇面板独立网格组 平开扇绕铰链旋转(od定铰链侧, 门75°/平开32°/双向门40°) 推拉扇沿轨道平移(双扇前后轨交替) 区域归属扇的不再重复渲染静态玻璃 - 开合控制条: 开合度滑杆+自动演示+全开/全关, 平滑插值动画 - 组合画布 3D 预览自动获得开合能力
This commit is contained in:
@@ -213,3 +213,90 @@ export function splitPolySegments(region: Segment[], axis: 'v' | 'h', pos: numbe
|
||||
if (polyA.length < 3 || polyB.length < 3) return null
|
||||
return { a: toSegs(polyA), b: toSegs(polyB) }
|
||||
}
|
||||
|
||||
/** 扇推断结果(模型坐标,供 2D 符号绘制与 3D 开合动画共用) */
|
||||
export interface SashGlyph {
|
||||
x0: number; y0: number; x1: number; y1: number
|
||||
od: number
|
||||
openToward?: string
|
||||
isDoor?: boolean
|
||||
wtf?: boolean
|
||||
kind: 'sliding' | 'casement'
|
||||
}
|
||||
|
||||
/**
|
||||
* 扇样式推断: 有效bd → polyId区域(双扇对半) → 帧bbox按扇数竖向均分。
|
||||
* 返回重放区域与推断出的扇矩形列表。
|
||||
*/
|
||||
export function inferSashGlyphs(shape: FrameShape, replayed?: Segment[][]): {
|
||||
regions: Segment[][]
|
||||
sashes: SashGlyph[]
|
||||
} {
|
||||
const replay = replayed ?? replayShape(shape).regions
|
||||
const regions = replay
|
||||
const regionBBs = regions.map(r => bboxOf(regionPoints(r)))
|
||||
const glyphs: SashGlyph[] = []
|
||||
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 kindOf = (sub: SashShape) => (sub.type === 'DoubleSash' ? 'sliding' : 'casement') as 'sliding' | 'casement'
|
||||
const pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape) => {
|
||||
const kind = kindOf(sub)
|
||||
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),
|
||||
kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
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),
|
||||
kind: kindOf(u.sub),
|
||||
})
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
return { regions, sashes: glyphs }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user