import { ref, type Ref } from "vue"; import type { FlatKey } from "../../domain/phy"; import { dropAction, hitKey, keyCenter, type DropAction } from "./logic"; type Point = { x: number; y: number }; type Options = { svg: Ref; zoom: Ref; pan: Ref; selected: Ref; keys: () => FlatKey[]; registered: () => ReadonlySet; previewing: () => boolean; onDrop: (source: string, action: DropAction) => void; onPreviewPick: (id: string) => void; }; /** Pointer travel (px) before a press on a key becomes a drag. */ const DRAG_START = 4; /** Canvas pointer handling: click/Shift+click selection, dragging a key to reorder, range selection, pan. T-31. */ export function useIdxPointer(o: Options) { const drag = ref<{ id: string; point: Point; action?: DropAction } | null>(null); const band = ref<{ start: Point; end: Point } | null>(null); const spaceHeld = ref(false); let press: { id?: string; client: Point; add: boolean; moved: boolean; pan?: Point } | null = null; function world(event: PointerEvent): Point { const svg = o.svg.value!; const local = new DOMPoint(event.clientX, event.clientY).matrixTransform(svg.getScreenCTM()!.inverse()); return { x: (local.x - o.pan.value.x) / o.zoom.value, y: (local.y - o.pan.value.y) / o.zoom.value }; } function begin(event: PointerEvent, id?: string) { const add = event.shiftKey || event.ctrlKey || event.metaKey; const panning = event.button === 1 || spaceHeld.value; if (event.button !== 0 && !panning) return; event.preventDefault(); if (!panning && id && o.previewing()) { o.onPreviewPick(id); return; } o.svg.value?.setPointerCapture?.(event.pointerId); press = { id: panning ? undefined : id, client: { x: event.clientX, y: event.clientY }, add, moved: false, }; if (panning) press.pan = { ...o.pan.value }; else if (!id && !o.previewing()) band.value = { start: world(event), end: world(event) }; } function move(event: PointerEvent) { if (!press) return; const dx = event.clientX - press.client.x, dy = event.clientY - press.client.y; if (!press.moved && Math.hypot(dx, dy) < DRAG_START) return; press.moved = true; if (press.pan) { // viewBox matches the SVG's pixel size, so client deltas are viewBox deltas o.pan.value = { x: press.pan.x + dx, y: press.pan.y + dy }; return; } const point = world(event); if (press.id) { const action = dropAction(press.id, hitKey(o.keys(), point), o.registered()); drag.value = { id: press.id, point, action }; } else if (band.value) band.value = { ...band.value, end: point }; } function end() { if (!press) return; const { id, add, moved } = press; if (id && !moved) { o.selected.value = add ? o.selected.value.includes(id) ? o.selected.value.filter((x) => x !== id) : [...o.selected.value, id] : [id]; } else if (id && drag.value?.action) o.onDrop(id, drag.value.action); else if (!id && !press.pan) { const b = band.value; if (!moved || !b) { if (!add) o.selected.value = []; } else { const [x0, x1] = [Math.min(b.start.x, b.end.x), Math.max(b.start.x, b.end.x)]; const [y0, y1] = [Math.min(b.start.y, b.end.y), Math.max(b.start.y, b.end.y)]; const inside = o .keys() .filter((k) => { const c = keyCenter(k); return c.x >= x0 && c.x <= x1 && c.y >= y0 && c.y <= y1; }) .map((k) => k.key.id); o.selected.value = add ? [...new Set([...o.selected.value, ...inside])] : inside; } } cancel(); } function cancel() { press = null; drag.value = null; band.value = null; } return { drag, band, spaceHeld, begin, move, end, cancel }; }