Choosing the target of an object variable meant finding its name in a dropdown. With many objects, or several with the same sprite, the name alone does not say which one it is. The eyedropper lets you click the object where you see it instead. - Every object dropdown gets an eyedropper button next to it. This covers variables, struct fields and list entries, in the Inspector and in the Variables tab. - The button opens the Scene View or the container tab the object lives in. A strip above the view names the field and the current candidate and offers Take and Cancel. The cursor is a crosshair. - A click picks a candidate and writes nothing yet. The candidate gets its own cyan frame, the selection stays on the object with the variable. - Clicking follows the selection rule. `pickOn` in `src/viewport/pick.ts` uses `pressOn` and only counts objects the dropdown offers. A click on empty space keeps the candidate. - Enter or Take writes through the same handlers as the dropdown. That is one undo step, or none if nothing changes. - Esc, Cancel, a right click in the view or a second click on the button cancel without writing. So do a tab switch, another selection, undo, redo, play and loading a project. - After Take or Cancel the editor goes back to the tab the pick came from. - While the eyedropper runs, the view does not drag, marquee, paint, show handles or open its context menu. Panning and zooming stay. Found on the way: - Started from the Variables tab, the click missed the object. The Scene View is hidden while that tab is open, and the layout skips hidden children. The strip appeared then, the view moved down by 32 px but kept its old height. `Editor.display()` now lays the row out again after showing it. Tests (on melektaus-atx): - unit pick and object-pick: 27 passed, 13 new - gate: 196 files, 2844 tests, CHECK EXIT 0. Two more come from svg-icons, which checks every icon twice. - e2e 57: ALL PASSED, 12 checks, 5 new (57e to 57g) - Counter-checks work. Without the layout fix 57g fails. Picking objects the dropdown does not offer fails 2 tests. Writing on cancel fails 2 more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
/**
|
||
* 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 ?? '';
|
||
}
|