Right-click in the scene view opens the object menu (#204)
The object menu lived only on the hierarchy rows. Now a right-click in the scene view opens the same menu for the object under the cursor. - The menu moves out of HierarchyPanel into src/editor/objectMenu.ts. Hierarchy and scene view call the same function, so their entries cannot drift apart. Only the hierarchy adds "Expand all" and "Collapse all". - Which object the menu is for follows the press rule from #203. A selected object under the cursor, even a hidden one, keeps the selection and the menu acts on all of it. Otherwise the topmost object is selected first. - The rule is the pure function menuTargetOf in src/viewport/pick.ts, next to pressOn. - Empty space, including an empty tilemap cell, opens no menu. - The container tab gets the same, because ContainerCanvas extends the scene view. Tests: - ./check.sh: 142 files, 2094 tests, exit 0 (was 2090) - e2e 39,40 fresh: ALL PASSED, 80 checks - e2e 1-16,39,40 fresh: ALL PASSED, 565 checks - New block 40r (3 checks): menu on a hidden selected crate, topmost object without a selection, no menu on an empty cell. - Counter-check: a right-click that always takes the topmost object fails 2 unit tests and the hidden-crate e2e check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad2a2d7921
commit
8c5846f956
7 files changed
+220
-77
No files matched your search
@@ -0,0 +1,91 @@
|
||||
import { UnpackContainerCommand } from '../core/commands/containerCommands';
|
||||
import { SetParentCommand } from '../core/commands/sceneObjectCommands';
|
||||
import { findObject } from '../core/model/hierarchy';
|
||||
import { isLiveInstance, isTemplateContent } from '../core/model/instance';
|
||||
import { findAsset, type ObjectHolder } from '../core/model/Project';
|
||||
import { Menu, type MenuItem } from '../ui';
|
||||
import {
|
||||
addCollider,
|
||||
addRigidbody,
|
||||
canAddCollider,
|
||||
canAddRigidbody,
|
||||
createContainer,
|
||||
removeObjects,
|
||||
} from './actions';
|
||||
import { hasAssetEditor } from './assetTabs';
|
||||
import type { EditorState } from './state';
|
||||
|
||||
/**
|
||||
* The menu of a placed object, the same from the hierarchy and the scene view (#204). `source` is
|
||||
* the stored holder, `holder` the one with container content resolved in; `extras` go above
|
||||
* removing, where only the hierarchy has anything to add.
|
||||
*/
|
||||
export function openObjectMenu(
|
||||
state: EditorState,
|
||||
source: ObjectHolder,
|
||||
holder: ObjectHolder,
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
extras: readonly MenuItem[] = [],
|
||||
): Menu {
|
||||
const inScene = holder.type === 'scene';
|
||||
const object = findObject(holder, id);
|
||||
const asset = object && findAsset(state.project, object.assetId);
|
||||
// Content of a placed container belongs to its template: values can be changed here, the
|
||||
// structure cannot (see `model/instance.ts`).
|
||||
const fromTemplate = isTemplateContent(source, id);
|
||||
const instance = Boolean(object && isLiveInstance(state.project, source, object));
|
||||
// Inside a multi-selection the menu acts on all selected objects.
|
||||
const selected = state.selectedObjectIds.includes(id) ? state.selectedObjectIds : [id];
|
||||
const many = selected.length > 1;
|
||||
const menu = new Menu([
|
||||
{
|
||||
label: 'Select asset',
|
||||
onSelect: () => object && state.select({ kind: 'asset', assetId: object.assetId }),
|
||||
},
|
||||
{
|
||||
// The same as double-clicking the asset in the asset panel: selects it and opens its tab.
|
||||
// A kind without an editor of its own would only be selected, so it is offered greyed out.
|
||||
label: 'Open asset',
|
||||
disabled: !asset || !hasAssetEditor(asset.type),
|
||||
onSelect: () => object && state.openAsset(object.assetId),
|
||||
},
|
||||
{
|
||||
label: 'Add collider',
|
||||
disabled: !object || many || !canAddCollider(state.project, source, object),
|
||||
onSelect: () => addCollider(state, holder.id, id),
|
||||
},
|
||||
{
|
||||
label: 'Add rigidbody',
|
||||
disabled: !object || many || !canAddRigidbody(state.project, source, object),
|
||||
onSelect: () => addRigidbody(state, holder.id, id),
|
||||
},
|
||||
{
|
||||
label: many ? `Create container from ${selected.length} objects` : 'Create container',
|
||||
disabled: selected.some((oid) => isTemplateContent(source, oid)),
|
||||
onSelect: () => createContainer(state, holder.id, selected),
|
||||
},
|
||||
{
|
||||
label: 'Unpack container',
|
||||
disabled: !instance,
|
||||
onSelect: () => state.execute(new UnpackContainerCommand(holder.id, id)),
|
||||
},
|
||||
{
|
||||
label: 'Unparent (move to top level)',
|
||||
disabled: !object?.parentId || many || fromTemplate,
|
||||
onSelect: () => state.execute(new SetParentCommand(holder.id, id, '')),
|
||||
},
|
||||
...extras,
|
||||
{
|
||||
label: many
|
||||
? `Remove ${selected.length} objects from ${inScene ? 'scene' : 'container'}`
|
||||
: `Remove from ${inScene ? 'scene' : 'container'}`,
|
||||
danger: true,
|
||||
disabled: selected.some((oid) => isTemplateContent(source, oid)),
|
||||
onSelect: () => removeObjects(state, holder.id, selected),
|
||||
},
|
||||
]);
|
||||
menu.open(x, y);
|
||||
return menu;
|
||||
}
|
||||
Reference in new issue
Block a user