Files
test1/components/image-editor/components/canvas/math.ts
2026-03-20 07:33:46 +00:00

228 lines
5.8 KiB
TypeScript

import type { Padding, SceneElement, Viewport } from './domino'
/**
* 计算元素移动到安全视野内的 Viewport 变换 (纯算法,无 DOM 依赖)
*/
export function calculateScrollIntoViewTransform(
element: SceneElement,
viewport: Viewport,
containerRect: { width: number; height: number },
padding: Required<Padding>,
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<Padding>,
): 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<string, SceneElement>,
) {
let x = element.x
let y = element.y
let rotation = element.rotation || 0
let curr = element
const visited = new Set<string>([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<string, SceneElement>,
) {
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,
}
}