import { isSymbolType } from '../core/model/AssetTypes'; import { drawSlots } from './drawOrder'; import type { DrawItem } from './items'; /** An object whose hit area is under the pointer; `layers` names a tilemap's layers with a tile there. */ export interface PointerHit { id: string; layers: readonly string[] | null; } /** * What lies under the pointer, front to back as it is drawn (#203). A tilemap counts only where * one of its layers has a tile, and at the height of the highest such layer: an empty cell is * empty space, and a sprite drawn above those layers is in front of the map. Symbols are drawn * above everything, so they come first. */ export function stackAt(items: readonly DrawItem[], hits: readonly PointerHit[]): string[] { const rank = new Map(); drawSlots(items).forEach((slot, i) => rank.set(`${slot.id}#${slot.layerId}`, i)); const symbolsFrom = rank.size; const index = new Map(items.map((item, i) => [item.id, i])); const ranked: { id: string; rank: number }[] = []; for (const hit of hits) { const at = index.get(hit.id); if (at === undefined) continue; const ranks = isSymbolType(items[at]!.type) ? [symbolsFrom + at] : (hit.layers ?? ['']).map((layerId) => rank.get(`${hit.id}#${layerId}`) ?? -1); const best = Math.max(-1, ...ranks); if (best >= 0) ranked.push({ id: hit.id, rank: best }); } return ranked.sort((a, b) => b.rank - a.rank).map((r) => r.id); } /** * What a plain press on `stack` does (#203). With a selected object under the pointer – hidden * behind others or not – the selection stays and a drag moves it; a click without moving then * passes on to the next object below, round to the top again. Otherwise the topmost is selected * and dragged at once. Null when nothing is under the pointer. */ export function pressOn( stack: readonly string[], selected: readonly string[], primary: string | null, ): { select: string | null; onClick: string | null } | null { if (stack.length === 0) return null; const held = primary !== null && stack.includes(primary) ? primary : stack.find((id) => selected.includes(id)); if (held === undefined) return { select: stack[0]!, onClick: null }; const next = stack[(stack.indexOf(held) + 1) % stack.length]!; return { select: null, onClick: next === held ? null : next }; }