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
ref={moveZoneRef}
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
ref={lookZoneRef}

View File

@ -1,16 +1,15 @@
.menu {
user-select: none;
font-size: 13px;
}
.menu-node {
> summary {
& details {
&>summary {
list-style: none;
&::-webkit-details-marker { display: none; }
}
&::-webkit-details-marker {
display: none;
}
.menu-item {
display: block;
padding: 8px 8px;
cursor: pointer;
@ -18,23 +17,21 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: rgba(255, 255, 255, 0.7);
color: rgba(255, 255, 255, 1);
&:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
&.active {
&.selected {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
}
.menu-node .menu-node > summary {
& details {
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 { useEffect, useRef } from 'react';
import { state } from '../state';
import './MenuView.scss';
import type { MenuNode } from '../state/menuState';
export const MenuView = observer(function () {
const objectIds = Object.keys(state.worldEditor.scene.objects);
const selectedId = state.worldEditor.selectedObjectId;
export const MenuNodeView = observer(function ({ node }: { node: MenuNode }) {
const forceOpen = state.menu.nodeContainsSelected(node);
const ref = useRef<HTMLDetailsElement>(null);
useEffect(() => {
if (forceOpen && ref.current)
ref.current.open = true;
}, [forceOpen]);
return (
<nav className="menu">
<details open className="menu-node">
<summary className="menu-item">Editor</summary>
<details open className="menu-node">
<details ref={ref}>
<summary
className={`menu-item${selectedId == null ? ' active' : ''}`}
onClick={() => state.worldEditor.resetSelectedObject()}
className={node.selected?.() ? 'selected' : ''}
onClick={() => node.onClick?.()}
>
Objects
{node.title}
</summary>
{objectIds.map(id => (
<div
key={id}
className={`menu-item menu-leaf${selectedId === id ? ' active' : ''}`}
onClick={() => state.worldEditor.setSelectedObject(id, 'translate')}
>
{id}
</div>
))}
</details>
{node.children?.map((child) => <MenuNodeView key={child.id} node={child} />)}
</details>
);
});
export const MenuView = observer(function () {
return (
<nav className="menu">
{state.menu.nodes.map((node) => <MenuNodeView key={node.id} node={node} />)}
</nav>
);
});

View File

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

View File

@ -30,10 +30,9 @@ export const Panel = observer(function ({ side = 'left' }: PanelProps) {
return <div className={`panel ${side}`}>
<div className="container">
<MenuView />
<div className="gap" />
<button onClick={handleLoadWorld}>Load</button>
<button onClick={handleLoadMockWorld}>Load mock world</button>
<button onClick={handleCloneTest1Object}>Clone test1</button>
<div className="gap" />
<div className="debug"><RenderInfoView /></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 { WorldEditorState } from "./worldEditorState";
import { GameState } from "./gameState";
import { MenuState } from "./menuState";
export type RenderInfo = {
calls: number,
@ -16,6 +17,7 @@ export class RootState {
public readonly worldEditor: WorldEditorState;
public game: GameState | undefined;
public renderInfo: RenderInfo | undefined;
public readonly menu = new MenuState();
constructor() {
this.worldEditor = new WorldEditorState(this.world);
@ -24,6 +26,7 @@ export class RootState {
this,
{
world: false,
menu: false,
},
);
}