editor object selection menu in panel

This commit is contained in:
azykov@mail.ru 2026-06-04 16:08:58 +03:00
parent a5733a4f4e
commit e08f80fc19
No known key found for this signature in database
7 changed files with 127 additions and 63 deletions

View File

@ -106,7 +106,7 @@ export function JoystickView() {
<div <div
ref={moveZoneRef} ref={moveZoneRef}
id="move-zone" id="move-zone"
style={{ position: 'absolute', left: 0, bottom: 0, width: '30vw', height: '30vw', touchAction: 'none', background: 'red' }} style={{ position: 'absolute', left: 0, bottom: 0, width: '30vw', height: '30vw', touchAction: 'none' }}
/> />
<div <div
ref={lookZoneRef} ref={lookZoneRef}

View File

@ -1,16 +1,15 @@
.menu { .menu {
user-select: none; user-select: none;
font-size: 13px; font-size: 13px;
}
.menu-node { & details {
> summary { &>summary {
list-style: none; list-style: none;
&::-webkit-details-marker { display: none; }
} &::-webkit-details-marker {
display: none;
} }
.menu-item {
display: block; display: block;
padding: 8px 8px; padding: 8px 8px;
cursor: pointer; cursor: pointer;
@ -18,23 +17,21 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 1);
&:hover { &:hover {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
color: #fff; color: #fff;
} }
&.active { &.selected {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
color: #fff; color: #fff;
} }
} }
.menu-node .menu-node > summary { & details {
padding-left: 12px; padding-left: 12px;
} }
}
.menu-node .menu-node .menu-leaf {
padding-left: 24px;
} }

View File

@ -1,33 +1,36 @@
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useEffect, useRef } from 'react';
import { state } from '../state'; import { state } from '../state';
import './MenuView.scss'; import './MenuView.scss';
import type { MenuNode } from '../state/menuState';
export const MenuView = observer(function () { export const MenuNodeView = observer(function ({ node }: { node: MenuNode }) {
const objectIds = Object.keys(state.worldEditor.scene.objects); const forceOpen = state.menu.nodeContainsSelected(node);
const selectedId = state.worldEditor.selectedObjectId;
const ref = useRef<HTMLDetailsElement>(null);
useEffect(() => {
if (forceOpen && ref.current)
ref.current.open = true;
}, [forceOpen]);
return ( return (
<nav className="menu"> <details ref={ref}>
<details open className="menu-node">
<summary className="menu-item">Editor</summary>
<details open className="menu-node">
<summary <summary
className={`menu-item${selectedId == null ? ' active' : ''}`} className={node.selected?.() ? 'selected' : ''}
onClick={() => state.worldEditor.resetSelectedObject()} onClick={() => node.onClick?.()}
> >
Objects {node.title}
</summary> </summary>
{objectIds.map(id => ( {node.children?.map((child) => <MenuNodeView key={child.id} node={child} />)}
<div
key={id}
className={`menu-item menu-leaf${selectedId === id ? ' active' : ''}`}
onClick={() => state.worldEditor.setSelectedObject(id, 'translate')}
>
{id}
</div>
))}
</details>
</details> </details>
);
});
export const MenuView = observer(function () {
return (
<nav className="menu">
{state.menu.nodes.map((node) => <MenuNodeView key={node.id} node={node} />)}
</nav> </nav>
); );
}); });

View File

@ -24,6 +24,7 @@
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
padding: 4px; padding: 4px;
border-radius: 4px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -38,11 +39,16 @@
} }
&>.debug { &>.debug {
border: 1px solid #ffffff20; // border: 1px solid #ffffff20;
padding: 2px; border-radius: 2px;
background: #00000040;
margin: -4px;
padding: 4px;
font-size: 75%; font-size: 75%;
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
align-items: start;
&>.chart { &>.chart {
display: inline; display: inline;

View File

@ -30,10 +30,9 @@ export const Panel = observer(function ({ side = 'left' }: PanelProps) {
return <div className={`panel ${side}`}> return <div className={`panel ${side}`}>
<div className="container"> <div className="container">
<MenuView /> <MenuView />
<div className="gap" />
<button onClick={handleLoadWorld}>Load</button> <button onClick={handleLoadWorld}>Load</button>
<button onClick={handleLoadMockWorld}>Load mock world</button> <button onClick={handleLoadMockWorld}>Load mock world</button>
<button onClick={handleCloneTest1Object}>Clone test1</button>
<div className="gap" />
<div className="debug"><RenderInfoView /></div> <div className="debug"><RenderInfoView /></div>
</div> </div>
</div > </div >

56
src/state/menuState.ts Normal file
View File

@ -0,0 +1,56 @@
import { makeAutoObservable } from "mobx";
import type { ObjectType, RuntimeObjectInstance } from "../types";
import { state } from "./rootState";
export type MenuNode = {
id: string;
title: string;
onClick?: () => void;
selected?: () => boolean;
children?: MenuNode[];
}
export class MenuState {
constructor() {
makeAutoObservable(this);
}
private get editorObjectTypesMenu(): MenuNode[] {
return Object.values(state.world.data.objectTypes)
.map((ot) => ({
id: `ot-${ot.id}`,
title: ot.name,
children: Object.values(state.worldEditor.scene.objects)
.filter((o) => o.typeId === ot.id)
.map((o) => ({
id: `o-${o.id}`,
title: o.id,
onClick: () => { state.worldEditor.setSelectedObject(o.id, 'translate') },
selected: () => state.worldEditor.selectedObjectId === o.id,
}))
}));
}
private get editorMenu(): MenuNode[] {
return [
{
id: 'editor-objects-menu',
title: 'Objects',
onClick: () => { state.worldEditor.resetSelectedObject() },
selected: () => state.worldEditor.selectedObjectId === undefined,
children: this.editorObjectTypesMenu,
}
]
}
public get nodes(): MenuNode[] {
return [
...this.editorMenu,
]
}
public nodeContainsSelected(node: MenuNode): boolean {
return !!(node.selected?.() || node.children?.some((child) => this.nodeContainsSelected(child)));
}
}

View File

@ -2,6 +2,7 @@ import { makeAutoObservable } from "mobx";
import { WorldState } from "./worldState"; import { WorldState } from "./worldState";
import { WorldEditorState } from "./worldEditorState"; import { WorldEditorState } from "./worldEditorState";
import { GameState } from "./gameState"; import { GameState } from "./gameState";
import { MenuState } from "./menuState";
export type RenderInfo = { export type RenderInfo = {
calls: number, calls: number,
@ -16,6 +17,7 @@ export class RootState {
public readonly worldEditor: WorldEditorState; public readonly worldEditor: WorldEditorState;
public game: GameState | undefined; public game: GameState | undefined;
public renderInfo: RenderInfo | undefined; public renderInfo: RenderInfo | undefined;
public readonly menu = new MenuState();
constructor() { constructor() {
this.worldEditor = new WorldEditorState(this.world); this.worldEditor = new WorldEditorState(this.world);
@ -24,6 +26,7 @@ export class RootState {
this, this,
{ {
world: false, world: false,
menu: false,
}, },
); );
} }