import type { FlatKey } from "../../domain/phy"; import { insertNextTo, swapKeys, type OrderContext } from "../../domain/idxOrder"; type Point = { x: number; y: number }; export type DropAction = { kind: "insert"; target: string; after: boolean } | { kind: "swap"; target: string }; /** World point in the key's own frame (origin at its top-left, x along its width). */ export function toKeyLocal(k: FlatKey, p: Point): Point { const a = (-k.rotation * Math.PI) / 180, dx = p.x - k.x, dy = p.y - k.y; return { x: dx * Math.cos(a) - dy * Math.sin(a), y: dx * Math.sin(a) + dy * Math.cos(a) }; } /** World center of a key. */ export function keyCenter(k: FlatKey): Point { const a = (k.rotation * Math.PI) / 180, hw = k.key.size.w / 2, hh = k.key.size.h / 2; return { x: k.x + hw * Math.cos(a) - hh * Math.sin(a), y: k.y + hw * Math.sin(a) + hh * Math.cos(a) }; } /** Topmost key under a world point, with the point's position across the key width (0..1). */ export function hitKey(keys: readonly FlatKey[], p: Point): { id: string; fraction: number } | undefined { for (let i = keys.length - 1; i >= 0; i--) { const k = keys[i]!, local = toKeyLocal(k, p); if (local.x >= 0 && local.y >= 0 && local.x <= k.key.size.w && local.y <= k.key.size.h) return { id: k.key.id, fraction: local.x / k.key.size.w }; } return undefined; } /** * Drop result of `source` at a hit: the outer quarters insert before or after the key, the middle * swaps. Unregistered keys cannot swap, so their drop splits the key in halves. Unregistered targets * and the source itself take no drop. T-31. */ export function dropAction( source: string, hit: { id: string; fraction: number } | undefined, registered: ReadonlySet, ): DropAction | undefined { if (!hit || hit.id === source || !registered.has(hit.id)) return undefined; if (!registered.has(source)) return { kind: "insert", target: hit.id, after: hit.fraction >= 0.5 }; if (hit.fraction < 0.25) return { kind: "insert", target: hit.id, after: false }; if (hit.fraction > 0.75) return { kind: "insert", target: hit.id, after: true }; return { kind: "swap", target: hit.id }; } /** New order after dropping `source`. */ export function applyDrop( order: readonly string[], ctx: OrderContext, source: string, action: DropAction, ): string[] { return action.kind === "swap" ? swapKeys(order, source, action.target) : insertNextTo(order, ctx, source, action.target, action.after); } /** Output number of each shown key: its position after dropping keys outside the configuration. */ export function outputNumbers(order: readonly string[], visible: ReadonlySet): Map { return new Map(order.filter((id) => visible.has(id)).map((id, i) => [id, i])); }