import type { Padding, SceneElement, Viewport } from './domino' /** * 计算元素移动到安全视野内的 Viewport 变换 (纯算法,无 DOM 依赖) */ export function calculateScrollIntoViewTransform( element: SceneElement, viewport: Viewport, containerRect: { width: number; height: number }, padding: Required, options: { force?: boolean block?: 'start' | 'center' | 'end' | 'nearest' inline?: 'start' | 'center' | 'end' | 'nearest' targetScale?: number }, ): Viewport | null { const { x: currentX, y: currentY, scale: currentScale } = viewport const { force, block = 'nearest', inline = 'nearest', targetScale } = options const scale = targetScale ?? currentScale // 计算安全区域 const safeLeft = padding.left const safeTop = padding.top const safeRight = containerRect.width - padding.right const safeBottom = containerRect.height - padding.bottom const safeWidth = safeRight - safeLeft const safeHeight = safeBottom - safeTop // 元素即时边界 const elW = element.width * scale const elH = element.height * scale const elLeft = element.x * scale + currentX const elRight = elLeft + elW const elTop = element.y * scale + currentY const elBottom = elTop + elH const isXOutOfView = force || elLeft < safeLeft || elRight > safeRight const isYOutOfView = force || elTop < safeTop || elBottom > safeBottom let nextX = currentX let nextY = currentY if (isXOutOfView) { if (inline === 'center') { nextX = safeLeft + (safeWidth - elW) / 2 - element.x * scale } else if ( inline === 'start' || (inline === 'nearest' && elLeft < safeLeft) ) { nextX = safeLeft - element.x * scale } else if ( inline === 'end' || (inline === 'nearest' && elRight > safeRight) ) { nextX = safeRight - elW - element.x * scale } } if (isYOutOfView) { if (block === 'center') { nextY = safeTop + (safeHeight - elH) / 2 - element.y * scale } else if (block === 'start' || (block === 'nearest' && elTop < safeTop)) { nextY = safeTop - element.y * scale } else if ( block === 'end' || (block === 'nearest' && elBottom > safeBottom) ) { nextY = safeBottom - elH - element.y * scale } } const hasChange = nextX !== currentX || nextY !== currentY || (targetScale !== undefined && targetScale !== currentScale) return hasChange ? { x: nextX, y: nextY, scale } : null } /** * 计算适应屏幕的 Viewport 变换 (纯算法,无 DOM 依赖) */ export function calculateZoomToFitTransform( elements: SceneElement[], containerRect: { width: number; height: number }, padding: Required, ): Viewport | null { if (elements.length === 0) return null let minX = Infinity let minY = Infinity let maxX = -Infinity let maxY = -Infinity elements.forEach(el => { minX = Math.min(minX, el.x) minY = Math.min(minY, el.y) maxX = Math.max(maxX, el.x + el.width) maxY = Math.max(maxY, el.y + el.height) }) const contentWidth = maxX - minX const contentHeight = maxY - minY const safeWidth = containerRect.width - padding.left - padding.right const safeHeight = containerRect.height - padding.top - padding.bottom if (safeWidth <= 0 || safeHeight <= 0) return null const scaleX = safeWidth / contentWidth const scaleY = safeHeight / contentHeight const targetScale = Math.max( 0.02, Math.min(4, Math.min(scaleX, scaleY) * 0.95), ) const nextX = padding.left + (safeWidth - contentWidth * targetScale) / 2 - minX * targetScale const nextY = padding.top + (safeHeight - contentHeight * targetScale) / 2 - minY * targetScale return { x: nextX, y: nextY, scale: targetScale } } /** * 计算元素的绝对位置和旋转 (世界坐标) */ export function getElementWorldRect( element: SceneElement, elements: Record, ) { let x = element.x let y = element.y let rotation = element.rotation || 0 let curr = element const visited = new Set([element.id]) while (curr.parentId) { if (visited.has(curr.parentId)) { console.warn( 'Circular dependency detected in element tree', curr.parentId, ) break } const parent = elements[curr.parentId] if (!parent) break x += parent.x y += parent.y rotation += parent.rotation || 0 visited.add(curr.parentId) curr = parent } return { x, y, width: element.width, height: element.height, rotation, } } /** * 计算一组元素的包围盒 (考虑旋转和父级偏移) */ export function getElementsBounds( ids: string[], elements: Record, ) { if (ids.length === 0) return null let minX = Infinity let minY = Infinity let maxX = -Infinity let maxY = -Infinity ids.forEach(id => { const rawEl = elements[id] if (!rawEl) return const el = getElementWorldRect(rawEl, elements) const rad = (el.rotation * Math.PI) / 180 const cx = el.x + el.width / 2 const cy = el.y + el.height / 2 const corners = [ [-el.width / 2, -el.height / 2], [el.width / 2, -el.height / 2], [el.width / 2, el.height / 2], [-el.width / 2, el.height / 2], ] corners.forEach(([px, py]) => { const rx = px * Math.cos(rad) - py * Math.sin(rad) const ry = px * Math.sin(rad) + py * Math.cos(rad) const wx = rx + cx const wy = ry + cy minX = Math.min(minX, wx) minY = Math.min(minY, wy) maxX = Math.max(maxX, wx) maxY = Math.max(maxY, wy) }) }) if (minX === Infinity) return null return { left: minX, top: minY, width: maxX - minX, height: maxY - minY, right: maxX, bottom: maxY, centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2, } }