editor object selection menu in panel
This commit is contained in:
parent
a5733a4f4e
commit
e08f80fc19
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,37 @@
|
||||||
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<details ref={ref}>
|
||||||
|
<summary
|
||||||
|
className={node.selected?.() ? 'selected' : ''}
|
||||||
|
onClick={() => node.onClick?.()}
|
||||||
|
>
|
||||||
|
{node.title}
|
||||||
|
</summary>
|
||||||
|
{node.children?.map((child) => <MenuNodeView key={child.id} node={child} />)}
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MenuView = observer(function () {
|
||||||
return (
|
return (
|
||||||
<nav className="menu">
|
<nav className="menu">
|
||||||
<details open className="menu-node">
|
{state.menu.nodes.map((node) => <MenuNodeView key={node.id} node={node} />)}
|
||||||
<summary className="menu-item">Editor</summary>
|
|
||||||
<details open className="menu-node">
|
|
||||||
<summary
|
|
||||||
className={`menu-item${selectedId == null ? ' active' : ''}`}
|
|
||||||
onClick={() => state.worldEditor.resetSelectedObject()}
|
|
||||||
>
|
|
||||||
Objects
|
|
||||||
</summary>
|
|
||||||
{objectIds.map(id => (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
className={`menu-item menu-leaf${selectedId === id ? ' active' : ''}`}
|
|
||||||
onClick={() => state.worldEditor.setSelectedObject(id, 'translate')}
|
|
||||||
>
|
|
||||||
{id}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</details>
|
|
||||||
</details>
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 >
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue