openpencil/packages/core/src/geometry.ts
Danila Poyarkov cc7f055dfc Format
2026-04-06 14:40:19 +03:00

171 lines
4.9 KiB
TypeScript

import type { Effect, Stroke } from './scene-graph'
import type { Rect, Vector } from './types'
export function degToRad(degrees: number): number {
return (degrees * Math.PI) / 180
}
export function radToDeg(radians: number): number {
return (radians * 180) / Math.PI
}
export function rotatePoint(px: number, py: number, cx: number, cy: number, rad: number): Vector {
const cos = Math.cos(rad)
const sin = Math.sin(rad)
return {
x: cx + (px - cx) * cos - (py - cy) * sin,
y: cy + (px - cx) * sin + (py - cy) * cos
}
}
export function rotatedCorners(
cx: number,
cy: number,
hw: number,
hh: number,
rotationDeg: number
): [Vector, Vector, Vector, Vector] {
const rad = degToRad(rotationDeg)
const cos = Math.cos(rad)
const sin = Math.sin(rad)
return [
{ x: cx + -hw * cos - -hh * sin, y: cy + -hw * sin + -hh * cos },
{ x: cx + hw * cos - -hh * sin, y: cy + hw * sin + -hh * cos },
{ x: cx + hw * cos - hh * sin, y: cy + hw * sin + hh * cos },
{ x: cx + -hw * cos - hh * sin, y: cy + -hw * sin + hh * cos }
]
}
export function rotatedBBox(
x: number,
y: number,
w: number,
h: number,
rotationDeg: number
): { left: number; right: number; top: number; bottom: number; centerX: number; centerY: number } {
if (rotationDeg === 0) {
return {
left: x,
right: x + w,
top: y,
bottom: y + h,
centerX: x + w / 2,
centerY: y + h / 2
}
}
const corners = rotatedCorners(x + w / 2, y + h / 2, w / 2, h / 2, rotationDeg)
let left = Infinity,
right = -Infinity,
top = Infinity,
bottom = -Infinity
for (const c of corners) {
left = Math.min(left, c.x)
right = Math.max(right, c.x)
top = Math.min(top, c.y)
bottom = Math.max(bottom, c.y)
}
return { left, right, top, bottom, centerX: (left + right) / 2, centerY: (top + bottom) / 2 }
}
export function computeBounds(items: Iterable<Rect>): Rect {
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity
for (const item of items) {
minX = Math.min(minX, item.x)
minY = Math.min(minY, item.y)
maxX = Math.max(maxX, item.x + item.width)
maxY = Math.max(maxY, item.y + item.height)
}
if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 }
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
}
function strokeOverflow(strokes?: Stroke[]): number {
let overflow = 0
for (const stroke of strokes ?? []) {
if (!stroke.visible) continue
let extra = 0
if (stroke.align === 'OUTSIDE') extra = stroke.weight
else if (stroke.align === 'CENTER') extra = stroke.weight / 2
overflow = Math.max(overflow, extra)
}
return overflow
}
function effectOverflow(effects?: Effect[]) {
let left = 0
let right = 0
let top = 0
let bottom = 0
for (const effect of effects ?? []) {
if (!effect.visible) continue
if (
effect.type !== 'DROP_SHADOW' &&
effect.type !== 'LAYER_BLUR' &&
effect.type !== 'FOREGROUND_BLUR'
) {
continue
}
const blurSpread = effect.radius + effect.spread
left = Math.max(left, blurSpread + Math.max(0, -effect.offset.x))
right = Math.max(right, blurSpread + Math.max(0, effect.offset.x))
top = Math.max(top, blurSpread + Math.max(0, -effect.offset.y))
bottom = Math.max(bottom, blurSpread + Math.max(0, effect.offset.y))
}
return { left, right, top, bottom }
}
export function computeAbsoluteBounds(
nodes: Iterable<{ id: string; width: number; height: number }>,
getAbsolutePosition: (id: string) => Vector
): Rect {
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity
for (const n of nodes) {
const abs = getAbsolutePosition(n.id)
minX = Math.min(minX, abs.x)
minY = Math.min(minY, abs.y)
maxX = Math.max(maxX, abs.x + n.width)
maxY = Math.max(maxY, abs.y + n.height)
}
if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 }
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
}
export function computeVisualBounds(
nodes: Iterable<{
id: string
width: number
height: number
rotation?: number
strokes?: Stroke[]
effects?: Effect[]
}>,
getAbsolutePosition: (id: string) => Vector
): Rect {
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity
for (const n of nodes) {
const abs = getAbsolutePosition(n.id)
const bbox = rotatedBBox(abs.x, abs.y, n.width, n.height, n.rotation ?? 0)
const stroke = strokeOverflow(n.strokes)
const effects = effectOverflow(n.effects)
minX = Math.min(minX, bbox.left - stroke - effects.left)
minY = Math.min(minY, bbox.top - stroke - effects.top)
maxX = Math.max(maxX, bbox.right + stroke + effects.right)
maxY = Math.max(maxY, bbox.bottom + stroke + effects.bottom)
}
if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 }
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
}