sidebar
This commit is contained in:
parent
8a23a49863
commit
90162d47cf
|
|
@ -2,7 +2,7 @@ import { BrowserRouter, Link, Outlet, Route, Routes, useNavigate, useParams } fr
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { reaction } from 'mobx';
|
import { reaction } from 'mobx';
|
||||||
import { ThreeView } from './components/ThreeView';
|
import { ThreeView } from './components/ThreeView';
|
||||||
import { Toolbar } from './components/Toolbar';
|
import { Panel } from './components/Panel';
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ function EditorLayout() {
|
||||||
<>
|
<>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ThreeView />
|
<ThreeView />
|
||||||
<Toolbar />
|
<Panel side="left" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
.panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 30%;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.container {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
&>*:not(.debug):not(.gap) {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.gap {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.debug {
|
||||||
|
border: 1px solid #ffffff20;
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 75%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
&>.chart {
|
||||||
|
display: inline;
|
||||||
|
|
||||||
|
& div {
|
||||||
|
display: inline;
|
||||||
|
position: relative !important;
|
||||||
|
z-index: unset !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.metrics {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25em 0.5em;
|
||||||
|
|
||||||
|
&>* {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import './Panel.scss';
|
||||||
|
import { RenderInfoView } from "./RenderInfoView";
|
||||||
|
import { state } from "../state";
|
||||||
|
|
||||||
|
export type PanelProps = {
|
||||||
|
side?: 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Panel = observer(function ({ side = 'left' }: PanelProps) {
|
||||||
|
|
||||||
|
function handleCloneTest1Object(): void {
|
||||||
|
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoadWorld(): void {
|
||||||
|
state.world.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoadMockWorld(): void {
|
||||||
|
state.world.loadMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={`panel ${side}`}>
|
||||||
|
<div className="container">
|
||||||
|
<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 >
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { state } from "../state";
|
||||||
|
import { chartRef } from "./chartRef";
|
||||||
|
|
||||||
|
export const RenderInfoView = observer(function () {
|
||||||
|
const info = state.renderInfo;
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
el.appendChild(chartRef.current);
|
||||||
|
return () => { el.removeChild(chartRef.current); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="chart" ref={containerRef} />
|
||||||
|
{info != null && (
|
||||||
|
<div className="metrics">
|
||||||
|
<span>draw: {info.calls}</span>
|
||||||
|
<span>tri: {info.triangles}</span>
|
||||||
|
<span>shaders: {info.shaders}</span>
|
||||||
|
<span>geometries: {info.geometries}</span>
|
||||||
|
<span>textures: {info.textures}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
});
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { KeyboardControls, Stats } from '@react-three/drei';
|
import { KeyboardControls, Stats } from '@react-three/drei';
|
||||||
import { useRef } from 'react';
|
import { action } from 'mobx';
|
||||||
|
import { chartRef } from './chartRef';
|
||||||
|
|
||||||
function RenderInfoUpdater({ domRef }: { domRef: React.RefObject<HTMLDivElement | null> }) {
|
function RenderInfoUpdater() {
|
||||||
const { gl } = useThree();
|
const { gl } = useThree();
|
||||||
useFrame(() => {
|
useFrame(action(() => {
|
||||||
if (!domRef.current)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const { calls, triangles } = gl.info.render;
|
const { calls, triangles } = gl.info.render;
|
||||||
const shaders = gl.info.programs?.length ?? 0;
|
const shaders = gl.info.programs?.length ?? 0;
|
||||||
const { geometries, textures } = gl.info.memory;
|
const { geometries, textures } = gl.info.memory;
|
||||||
|
state.setRenderInfo({
|
||||||
domRef.current.textContent = `draw: ${calls} | tri: ${triangles} | shaders: ${shaders} | geometries: ${geometries} | textures: ${textures}`;
|
calls,
|
||||||
|
triangles,
|
||||||
|
shaders,
|
||||||
|
geometries,
|
||||||
|
textures,
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,6 +24,7 @@ import { observer } from 'mobx-react-lite';
|
||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
import { GameView } from './GameView';
|
import { GameView } from './GameView';
|
||||||
import { SceneEditorView } from './SceneEditorView';
|
import { SceneEditorView } from './SceneEditorView';
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
|
||||||
const IconStop = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" fill="currentColor" /></svg>;
|
const IconStop = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" fill="currentColor" /></svg>;
|
||||||
const IconPause = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="4" height="10" fill="currentColor" /><rect x="8" y="2" width="4" height="10" fill="currentColor" /></svg >;
|
const IconPause = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="4" height="10" fill="currentColor" /><rect x="8" y="2" width="4" height="10" fill="currentColor" /></svg >;
|
||||||
|
|
@ -28,7 +32,6 @@ const IconPlay = () => <svg viewBox="0 0 14 14"><polygon points="3,1 13,7 3,13"
|
||||||
|
|
||||||
export const ThreeView = observer(function () {
|
export const ThreeView = observer(function () {
|
||||||
const isGame = state.isGamePlaying;
|
const isGame = state.isGamePlaying;
|
||||||
const infoRef = useRef<HTMLDivElement>(null);
|
|
||||||
return (
|
return (
|
||||||
<KeyboardControls map={[
|
<KeyboardControls map={[
|
||||||
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
|
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
|
||||||
|
|
@ -42,16 +45,10 @@ export const ThreeView = observer(function () {
|
||||||
// camera={state.world.character.camera}
|
// camera={state.world.character.camera}
|
||||||
onPointerMissed={() => state.worldEditor.resetSelectedObject()}
|
onPointerMissed={() => state.worldEditor.resetSelectedObject()}
|
||||||
>
|
>
|
||||||
<Stats />
|
<Stats parent={chartRef as RefObject<HTMLElement>} />
|
||||||
<RenderInfoUpdater domRef={infoRef} />
|
<RenderInfoUpdater />
|
||||||
{isGame ? <GameView /> : <SceneEditorView />}
|
{isGame ? <GameView /> : <SceneEditorView />}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
<div ref={infoRef} style={{
|
|
||||||
position: 'absolute', bottom: 8, left: 8,
|
|
||||||
color: 'white', fontSize: 11, fontFamily: 'monospace',
|
|
||||||
background: 'rgba(0,0,0,0.5)', padding: '2px 6px', borderRadius: 3,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}} />
|
|
||||||
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
|
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
|
||||||
{
|
{
|
||||||
state.game
|
state.game
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
import { state } from "../state";
|
|
||||||
|
|
||||||
export const Toolbar = observer(function () {
|
|
||||||
|
|
||||||
function handleCloneTest1Object(): void {
|
|
||||||
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLoadWorld(): void {
|
|
||||||
state.world.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLoadMockWorld(): void {
|
|
||||||
state.world.loadMock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="toolbar">
|
|
||||||
<button onClick={handleLoadWorld}>Load</button>
|
|
||||||
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
|
||||||
<button onClick={handleCloneTest1Object}>Clone test1</button>
|
|
||||||
</div>
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export const chartRef = { current: document.createElement('span') };
|
||||||
|
|
@ -48,10 +48,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
width: 1126px;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
|
||||||
border-inline: 1px solid var(--border);
|
border-inline: 1px solid var(--border);
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,19 @@ import { WorldState } from "./worldState";
|
||||||
import { WorldEditorState } from "./worldEditorState";
|
import { WorldEditorState } from "./worldEditorState";
|
||||||
import { GameState } from "./gameState";
|
import { GameState } from "./gameState";
|
||||||
|
|
||||||
|
export type RenderInfo = {
|
||||||
|
calls: number,
|
||||||
|
triangles: number,
|
||||||
|
shaders: number,
|
||||||
|
geometries: number,
|
||||||
|
textures: number,
|
||||||
|
}
|
||||||
|
|
||||||
export class RootState {
|
export class RootState {
|
||||||
public readonly world = new WorldState();
|
public readonly world = new WorldState();
|
||||||
public readonly worldEditor: WorldEditorState;
|
public readonly worldEditor: WorldEditorState;
|
||||||
public game: GameState | undefined;
|
public game: GameState | undefined;
|
||||||
|
public renderInfo: RenderInfo | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.worldEditor = new WorldEditorState(this.world);
|
this.worldEditor = new WorldEditorState(this.world);
|
||||||
|
|
@ -37,6 +46,10 @@ export class RootState {
|
||||||
this.game.pause();
|
this.game.pause();
|
||||||
this.game = undefined;
|
this.game = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setRenderInfo(value: RenderInfo | undefined) {
|
||||||
|
this.renderInfo = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const state = new RootState();
|
export const state = new RootState();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue