/** * A frame's collision outlines (#141) as a collider wears them (#193): centred on the collider's * origin, one image pixel to one unit – the size a sprite of that frame is placed with. */ import type { TextureFrame } from './Asset'; import { convexPieces } from './polygon'; import type { Project } from './Project'; import type { Vec2 } from './Properties'; import { findFrame } from './texture'; export interface FrameOutline { /** The frame's size in pixels, which is the collider's footprint. */ w: number; h: number; /** Every outline the frame has, in the collider's own units. */ rings: Vec2[][]; /** What collides: the convex pieces of every ring that is a polygon. */ pieces: Vec2[][]; } /** One per drawn outline list, so a frame is cut into pieces once and not on every test. */ const cache = new WeakMap(); function isCurrent( known: { from: TextureFrame['collision']; outline: FrameOutline }, frame: TextureFrame, ): boolean { return known.from === frame.collision && known.outline.w === frame.w && known.outline.h === frame.h; } /** * What the frame with that id blocks with, or null when the id names no frame. A frame without a * drawn outline has no rings: the collider then collides with nothing, like a broken polygon. */ export function frameOutline(project: Project | null, frameId: string): FrameOutline | null { const found = project ? findFrame(project, frameId) : null; if (!found) return null; const { frame } = found; const known = cache.get(frame); if (known && isCurrent(known, frame)) return known.outline; const centre = (p: Vec2): Vec2 => ({ x: p.x - frame.w / 2, y: p.y - frame.h / 2 }); const rings = (frame.collision ?? []).map((ring) => ring.map(centre)); const outline = { w: frame.w, h: frame.h, rings, pieces: rings.flatMap((ring) => convexPieces(ring)) }; cache.set(frame, { from: frame.collision, outline }); return outline; }