From e08f80fc19091f9b00551a44bf4f3fd905df6ad1 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Thu, 4 Jun 2026 16:08:58 +0300 Subject: [PATCH] editor object selection menu in panel --- src/components/JoystickView.tsx | 2 +- src/components/MenuView.scss | 67 ++++++++++++++++----------------- src/components/MenuView.tsx | 49 +++++++++++++----------- src/components/Panel.scss | 10 ++++- src/components/Panel.tsx | 3 +- src/state/menuState.ts | 56 +++++++++++++++++++++++++++ src/state/rootState.ts | 3 ++ 7 files changed, 127 insertions(+), 63 deletions(-) create mode 100644 src/state/menuState.ts diff --git a/src/components/JoystickView.tsx b/src/components/JoystickView.tsx index c0043e1..60f88c3 100644 --- a/src/components/JoystickView.tsx +++ b/src/components/JoystickView.tsx @@ -106,7 +106,7 @@ export function JoystickView() {
summary { - list-style: none; - &::-webkit-details-marker { display: none; } + & details { + &>summary { + list-style: none; + + &::-webkit-details-marker { + display: none; + } + + display: block; + padding: 8px 8px; + cursor: pointer; + border-radius: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: rgba(255, 255, 255, 1); + + &:hover { + background: rgba(255, 255, 255, 0.08); + color: #fff; + } + + &.selected { + background: rgba(255, 255, 255, 0.15); + color: #fff; + } + } + + & details { + padding-left: 12px; + } } -} - -.menu-item { - display: block; - padding: 8px 8px; - cursor: pointer; - border-radius: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: rgba(255, 255, 255, 0.7); - - &:hover { - background: rgba(255, 255, 255, 0.08); - color: #fff; - } - - &.active { - background: rgba(255, 255, 255, 0.15); - color: #fff; - } -} - -.menu-node .menu-node > summary { - padding-left: 12px; -} - -.menu-node .menu-node .menu-leaf { - padding-left: 24px; -} +} \ No newline at end of file diff --git a/src/components/MenuView.tsx b/src/components/MenuView.tsx index b5a20dc..b1acf3c 100644 --- a/src/components/MenuView.tsx +++ b/src/components/MenuView.tsx @@ -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(null); + + useEffect(() => { + if (forceOpen && ref.current) + ref.current.open = true; + }, [forceOpen]); + return ( +
+ node.onClick?.()} + > + {node.title} + + {node.children?.map((child) => )} +
+ ); +}); + +export const MenuView = observer(function () { return ( ); }); diff --git a/src/components/Panel.scss b/src/components/Panel.scss index d7593b2..96dfee6 100644 --- a/src/components/Panel.scss +++ b/src/components/Panel.scss @@ -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; diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 00823be..8dd123b 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -30,10 +30,9 @@ export const Panel = observer(function ({ side = 'left' }: PanelProps) { return
+
- -
diff --git a/src/state/menuState.ts b/src/state/menuState.ts new file mode 100644 index 0000000..fc89ece --- /dev/null +++ b/src/state/menuState.ts @@ -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))); + } + +} diff --git a/src/state/rootState.ts b/src/state/rootState.ts index 568415c..398ae37 100644 --- a/src/state/rootState.ts +++ b/src/state/rootState.ts @@ -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, }, ); }