/** * The eyedropper of an object's place (#276): instead of the dropdown, a click in the view of the * holder names the target. A click only makes a candidate – one on the candidate passes on to the * object behind it, as a click does with the selection – and Enter or Take writes it, one undo step * like the dropdown. The pick lives in `EditorState`; the view, the strip over it and the editor's * tabs follow it from there. */ import type { ObjectChoice } from '../core/model/objectRefs'; import { findAsset } from '../core/model/Project'; import { objectName } from '../core/model/SceneObject'; import { pickOn } from '../viewport/pick'; import { shownObject } from './inspector/objectQueries'; import type { EditorState } from './state'; export interface ObjectPick { /** Whatever started it – its button tells a second press, which cancels, by this. */ owner: object; /** The scene or container the pick happens in, shown by its own view. */ holderId: string; /** What the strip says is being filled: "Watcher · target". */ name: string; /** What the place names now, as it stores it. */ current: string; /** What may be picked, asked anew each time: the dropdown's choices. */ choices: () => ObjectChoice[]; /** The object picked so far, as the view knows it. */ candidate: string | null; /** Writes the place – what picking in the dropdown does. */ take: (ref: string) => void; /** Called once when it is over, written or not. */ ended: () => void; } /** How a pick ended: written, cancelled by the user, or dropped because the editor moved on. */ export type PickEnd = 'take' | 'cancel' | 'drop'; /** What a field hands over to pick for it; the rest is the host's. */ export interface PickRequest { /** The field's own name, "target" or "crowd · [2]". */ field: string; current: string; take: (ref: string) => void; ended: () => void; } /** How a field starts and stops the eyedropper (see `FieldContext.objectPicker`). */ export interface ObjectPicker { /** Starts picking for `request`, or – when `owner` is picking already – cancels. */ toggle(owner: object, request: PickRequest): void; picking(owner: object): boolean; } /** The eyedropper of the places on one placed object; `choices` is what its dropdowns offer. */ export function objectPicker( state: EditorState, holderId: string, objectId: string, choices: () => ObjectChoice[], ): ObjectPicker { return { picking: (owner) => state.objectPick?.owner === owner, toggle: (owner, request) => { if (state.objectPick?.owner === owner) return state.endObjectPick('cancel'); if (!pickView(state, holderId)) { state.setNotice('Open the scene this object is in to pick from it'); return; } const shown = shownObject(state.project, holderId, objectId); if (!shown) return; state.startObjectPick({ owner, holderId, name: `${objectName(shown.object, shown.asset)} · ${request.field}`, current: request.current, choices, candidate: choices().find((c) => c.ref === request.current)?.objectId ?? null, take: request.take, ended: request.ended, }); }, }; } /** Which view shows `holderId` for picking: the scene tab for the scene open, a container's tab for it. */ export function pickView(state: EditorState, holderId: string): 'scene' | 'container' | null { if (state.activeScene?.id === holderId) return 'scene'; return findAsset(state.project, holderId)?.type === 'container' ? 'container' : null; } /** A click of the eyedropper on what lies under the pointer, front to back. */ export function pickAt(state: EditorState, stack: readonly string[]): void { const pick = state.objectPick; if (!pick) return; const offered = new Set(pick.choices().map((c) => c.objectId)); const next = pickOn(stack, (id) => offered.has(id), pick.candidate); if (next.refused) state.setNotice(`Not something ${pick.name} can name – the dropdown lists what can`); state.movePick(next.candidate); } /** The candidate as the dropdown names it; empty while there is none. */ export function candidateLabel(pick: ObjectPick): string { return pick.choices().find((c) => c.objectId === pick.candidate)?.label ?? ''; }