import type { PropertyBag, Vec2 } from '../core/model/Properties'; import { cameraFrame, rectAround, type WorldRect } from '../core/properties/camera'; /** * The world point (`x`, `y`) sits at the centre of the screen rectangle `rect` (viewport-local * pixels) at `scale` pixels per world unit. */ export interface View { x: number; y: number; scale: number; rect: { x: number; y: number; w: number; h: number }; } /** Free-panning view: the whole viewport is the rectangle. */ export function freeView(x: number, y: number, zoom: number, w: number, h: number): View { return { x, y, scale: zoom, rect: { x: 0, y: 0, w, h } }; } /** The camera's frame fitted into `w`×`h` and centred; the remaining margin is letterbox. */ export function cameraView(camera: { x: number; y: number; props: PropertyBag }, w: number, h: number): View { const frame = cameraFrame(camera.props); const scale = Math.min(w / frame.w, h / frame.h); const fw = frame.w * scale; const fh = frame.h * scale; return { x: camera.x, y: camera.y, scale, rect: { x: (w - fw) / 2, y: (h - fh) / 2, w: fw, h: fh } }; } export function screenToWorld(view: View, sx: number, sy: number): Vec2 { const cx = view.rect.x + view.rect.w / 2; const cy = view.rect.y + view.rect.h / 2; return { x: (sx - cx) / view.scale + view.x, y: (sy - cy) / view.scale + view.y }; } export function worldToScreen(view: View, wx: number, wy: number): Vec2 { const cx = view.rect.x + view.rect.w / 2; const cy = view.rect.y + view.rect.h / 2; return { x: (wx - view.x) * view.scale + cx, y: (wy - view.y) * view.scale + cy }; } export function worldBounds(view: View): WorldRect { return rectAround(view.x, view.y, view.rect.w / view.scale, view.rect.h / view.scale); } /** Whether a viewport-local point lies on the drawn rectangle rather than in the letterbox. */ export function isInside(view: View, sx: number, sy: number): boolean { const { x, y, w, h } = view.rect; return sx >= x && sx <= x + w && sy >= y && sy <= y + h; }