/** * The frame round the selected shapes of a vector drawing (#291), without Pixi: where it is drawn, * which of its handles a press lands on and what a pull on that handle means. The handles are the * scene view's (`sizeGizmo.ts`) – eight squares that size and a knob on a stem that turns. * * Unlike a scene object's, the frame stands a few pixels off what it frames. A shape's own points * lie on its bounds, a square's exactly on the corners, and a handle drawn there would hide the * point under it; off the bounds neither is ever in the other's reach. */ import { applyVector, type Mat } from '../../core/model/matrix'; import type { Vec2 } from '../../core/model/Properties'; import type { Extent } from '../../core/model/vectorPaint'; import { extentCentre, extentCorners, type FrameSide, frameScale, rotationAbout, } from '../../core/model/vectorTransform'; import { knobAt, type SizeGrip, sizeGripAt } from '../sizeGizmo'; /** Screen pixels between what is framed and the frame. More than a point's reach plus a handle's. */ export const FRAME_PAD = 15; /** Screen pixels around a handle that grab it. */ export const FRAME_GRIP = 6; /** How far the knob stands out from the top edge, in screen pixels. */ export const FRAME_STEM = 26; /** Degrees a turn snaps to while Shift is held. */ export const TURN_SNAP = 15; export interface FrameBox { /** Top left, top right, bottom right, bottom left – in the pixels `toLocal` hands out. */ corners: Vec2[]; /** The middle of each edge: top, right, bottom, left. */ edges: Vec2[]; base: Vec2; knob: Vec2; } /** * The frame as it is drawn: the corners of `extent` taken through `m` – the map of a drag under way, * so the frame turns with what it holds – then onto the screen and `FRAME_PAD` pixels outwards. */ export function frameBox(extent: Extent, m: Mat, toLocal: (x: number, y: number) => Vec2): FrameBox { const origin = toLocal(0, 0); const direction = (v: Vec2): Vec2 => { const moved = applyVector(m, v); const tip = toLocal(moved.x, moved.y); const d = { x: tip.x - origin.x, y: tip.y - origin.y }; const length = Math.hypot(d.x, d.y); return length < 1e-9 ? v : { x: d.x / length, y: d.y / length }; }; const right = direction({ x: 1, y: 0 }); const down = direction({ x: 0, y: 1 }); const out: [number, number][] = [ [-1, -1], [1, -1], [1, 1], [-1, 1], ]; const corners = extentCorners(extent, m).map((corner, i) => { const at = toLocal(corner.x, corner.y); const [sx, sy] = out[i]!; return { x: at.x + FRAME_PAD * (sx * right.x + sy * down.x), y: at.y + FRAME_PAD * (sx * right.y + sy * down.y), }; }); const edges = corners.map((a, i) => { const b = corners[(i + 1) % corners.length]!; return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; }); return { corners, edges, ...knobAt(corners, FRAME_STEM) }; } /** The handle of the frame under `p`, the knob first; null beside all of them. */ export function frameGripAt(box: FrameBox, p: Vec2): SizeGrip | null { return sizeGripAt(box.corners, box.knob, p, FRAME_GRIP, { sizable: true, corners: true }); } /** The arrow a handle shows: along the direction it pulls in; the knob shows a hand. */ export function frameCursor(grip: SizeGrip): string { if (grip.kind === 'knob') return 'grab'; const { sx, sy } = grip.side; if (sx === 0) return 'ns-resize'; if (sy === 0) return 'ew-resize'; return sx === sy ? 'nwse-resize' : 'nesw-resize'; } /** Where the handle on `side` sits on the framed extent itself, in canvas pixels. */ export function sidePoint(extent: Extent, side: FrameSide): Vec2 { const centre = extentCentre(extent); return { x: side.sx === 0 ? centre.x : side.sx > 0 ? extent.x1 : extent.x0, y: side.sy === 0 ? centre.y : side.sy > 0 ? extent.y1 : extent.y0, }; } /** * The map a size handle has reached: the pointer went from `press` to `now` (canvas pixels), and the * handle went the same way from where it sat – so a press beside the handle's middle does not jump. * `snap` puts the place the handle is pulled to on the grid. */ export function frameSizing( extent: Extent, side: FrameSide, press: Vec2, now: Vec2, options: { proportional: boolean; centred: boolean; snap?: (p: Vec2) => Vec2 }, ): Mat { const from = sidePoint(extent, side); const to = { x: from.x + now.x - press.x, y: from.y + now.y - press.y }; return frameScale(extent, side, options.snap ? options.snap(to) : to, options); } /** The map of a turn by `sweep` degrees round the middle: to a tenth, or in steps of `TURN_SNAP`. */ export function frameTurning(extent: Extent, sweep: number, snap: boolean): Mat { const degrees = snap ? Math.round(sweep / TURN_SNAP) * TURN_SNAP : Math.round(sweep * 10) / 10; return rotationAbout(extentCentre(extent), degrees); }