editor now allows object type selection
This commit is contained in:
parent
e08f80fc19
commit
37e8f5ccd3
34
src/App.tsx
34
src/App.tsx
|
|
@ -2,20 +2,22 @@ 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 { Panel } from './components/Panel';
|
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
import { LeftPanel } from './components/LeftPanel';
|
||||||
|
|
||||||
function StateToUrlSync() {
|
function StateToUrlSync() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useEffect(() => reaction(
|
useEffect(() => reaction(
|
||||||
() => ({
|
() => ({
|
||||||
isGame: state.isGamePlaying,
|
isGame: state.isGamePlaying,
|
||||||
selectedId: state.worldEditor.selectedObjectId,
|
selectedObjectId: state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id,
|
||||||
|
selectedObjectTypeId: state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id,
|
||||||
}),
|
}),
|
||||||
({ isGame, selectedId }) => {
|
({ isGame, selectedObjectId, selectedObjectTypeId}) => {
|
||||||
if (isGame) navigate('/game');
|
if (isGame) navigate('/game');
|
||||||
else if (selectedId) navigate(`/editor/object/${selectedId}`);
|
else if (selectedObjectId) navigate(`/editor/object/${selectedObjectId}`);
|
||||||
|
else if (selectedObjectTypeId) navigate(`/editor/objectType/${selectedObjectTypeId}`);
|
||||||
else navigate('/editor');
|
else navigate('/editor');
|
||||||
},
|
},
|
||||||
), []);
|
), []);
|
||||||
|
|
@ -25,7 +27,7 @@ function StateToUrlSync() {
|
||||||
function EditorRoute() {
|
function EditorRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.isGamePlaying) state.stopGame();
|
if (state.isGamePlaying) state.stopGame();
|
||||||
state.worldEditor.resetSelectedObject();
|
state.worldEditor.resetSelection();
|
||||||
}, []);
|
}, []);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -35,10 +37,23 @@ function EditorObjectRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (state.isGamePlaying) state.stopGame();
|
if (state.isGamePlaying) state.stopGame();
|
||||||
const mode = state.worldEditor.selectedObjectId === id
|
const editMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id === id
|
||||||
? state.worldEditor.selectedObjectMode ?? 'translate'
|
? state.worldEditor.selection.editMode ?? 'translate'
|
||||||
: 'translate';
|
: 'translate';
|
||||||
state.worldEditor.setSelectedObject(id, mode);
|
state.worldEditor.setSelectedObject({ id, editMode });
|
||||||
|
}, [id]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorObjectTypeRoute() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return;
|
||||||
|
if (state.isGamePlaying) state.stopGame();
|
||||||
|
const editMode = state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id === id
|
||||||
|
? state.worldEditor.selection.editMode ?? 'scripts'
|
||||||
|
: 'scripts';
|
||||||
|
state.worldEditor.setSelectedObjectType({ id, editMode });
|
||||||
}, [id]);
|
}, [id]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +74,7 @@ function EditorLayout() {
|
||||||
<>
|
<>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ThreeView />
|
<ThreeView />
|
||||||
<Panel side="left" />
|
<LeftPanel />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +88,7 @@ export const App = function () {
|
||||||
<Route element={<EditorLayout />}>
|
<Route element={<EditorLayout />}>
|
||||||
<Route path="/editor" element={<EditorRoute />} />
|
<Route path="/editor" element={<EditorRoute />} />
|
||||||
<Route path="/editor/object/:id" element={<EditorObjectRoute />} />
|
<Route path="/editor/object/:id" element={<EditorObjectRoute />} />
|
||||||
|
<Route path="/editor/objectType/:id" element={<EditorObjectTypeRoute />} />
|
||||||
<Route path="/game" element={<GameRoute />} />
|
<Route path="/game" element={<GameRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import './Panel.scss';
|
||||||
|
import { RenderInfoView } from "./RenderInfoView";
|
||||||
|
import { MenuView } from "./MenuView";
|
||||||
|
import { state } from "../state";
|
||||||
|
import { Panel } from "./Panel";
|
||||||
|
|
||||||
|
export const LeftPanel = observer(function () {
|
||||||
|
const isGame = state.game && !state.game.isPaused;
|
||||||
|
|
||||||
|
if (isGame)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
function handleLoadWorld(): void {
|
||||||
|
state.world.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoadMockWorld(): void {
|
||||||
|
state.world.loadMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Panel side='left'>
|
||||||
|
<MenuView />
|
||||||
|
<div className="gap" />
|
||||||
|
<button onClick={handleLoadWorld}>Load</button>
|
||||||
|
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
||||||
|
<div className="debug"><RenderInfoView /></div>
|
||||||
|
</Panel>
|
||||||
|
});
|
||||||
|
|
@ -6,7 +6,7 @@ import { TransformControls, useHelper } from "@react-three/drei";
|
||||||
import { BoxHelper } from "three";
|
import { BoxHelper } from "three";
|
||||||
import type { ThreeEvent } from "@react-three/fiber";
|
import type { ThreeEvent } from "@react-three/fiber";
|
||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
import { nextSelectionEditMode } from "../state/worldEditorState";
|
import { nextObjectEditMode } from "../state/worldEditorState";
|
||||||
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
||||||
|
|
||||||
type ObjectEditorViewProps = {
|
type ObjectEditorViewProps = {
|
||||||
|
|
@ -23,8 +23,12 @@ type SelectionOverlayProps = {
|
||||||
// re-render on selection change, not all N objects in the scene.
|
// re-render on selection change, not all N objects in the scene.
|
||||||
const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: SelectionOverlayProps) {
|
const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: SelectionOverlayProps) {
|
||||||
const isSelected = state.worldEditor.isEnabled &&
|
const isSelected = state.worldEditor.isEnabled &&
|
||||||
state.worldEditor.selectedObjectId === objectId;
|
state.worldEditor.selection?.type === 'object' &&
|
||||||
const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined;
|
state.worldEditor.selection.id === objectId;
|
||||||
|
const selectionMode = isSelected &&
|
||||||
|
state.worldEditor.selection?.type === 'object'
|
||||||
|
? state.worldEditor.selection?.editMode
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Stable virtual ref that reads through to the group inside the handle
|
// Stable virtual ref that reads through to the group inside the handle
|
||||||
const groupRef = useMemo<RefObject<Group | null>>(
|
const groupRef = useMemo<RefObject<Group | null>>(
|
||||||
|
|
@ -60,10 +64,10 @@ export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewP
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Reading selection state inside an event handler: not tracked by observer.
|
// Reading selection state inside an event handler: not tracked by observer.
|
||||||
const currentMode = state.worldEditor.isEnabled &&
|
const currentMode = state.worldEditor.isEnabled &&
|
||||||
state.worldEditor.selectedObjectId === object.id
|
state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
|
||||||
? state.worldEditor.selectedObjectMode
|
? state.worldEditor.selection?.editMode
|
||||||
: undefined;
|
: undefined;
|
||||||
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(currentMode));
|
state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransformEnd() {
|
function handleTransformEnd() {
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,16 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import './Panel.scss';
|
import './Panel.scss';
|
||||||
import { RenderInfoView } from "./RenderInfoView";
|
import type { ReactNode } from "react";
|
||||||
import { MenuView } from "./MenuView";
|
|
||||||
import { state } from "../state";
|
|
||||||
|
|
||||||
export type PanelProps = {
|
export type PanelProps = {
|
||||||
side?: 'left' | 'right';
|
side?: 'left' | 'right';
|
||||||
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Panel = observer(function ({ side = 'left' }: PanelProps) {
|
export const Panel = observer(function ({ children, side = 'left' }: PanelProps) {
|
||||||
|
|
||||||
const isGame = state.game && !state.game.isPaused;
|
|
||||||
|
|
||||||
if (isGame)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
function handleCloneTest1Object(): void {
|
|
||||||
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLoadWorld(): void {
|
|
||||||
state.world.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLoadMockWorld(): void {
|
|
||||||
state.world.loadMock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={`panel ${side}`}>
|
return <div className={`panel ${side}`}>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<MenuView />
|
{children}
|
||||||
<div className="gap" />
|
|
||||||
<button onClick={handleLoadWorld}>Load</button>
|
|
||||||
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
|
||||||
<div className="debug"><RenderInfoView /></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div >
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export const ThreeView = observer(function () {
|
||||||
<div style={{ position: 'fixed', inset: 0, overflow: 'hidden' }}>
|
<div style={{ position: 'fixed', inset: 0, overflow: 'hidden' }}>
|
||||||
<Canvas
|
<Canvas
|
||||||
// camera={state.world.character.camera}
|
// camera={state.world.character.camera}
|
||||||
onPointerMissed={() => state.worldEditor.resetSelectedObject()}
|
onPointerMissed={() => state.worldEditor.resetSelection()}
|
||||||
>
|
>
|
||||||
<Stats parent={chartRef as RefObject<HTMLElement>} />
|
<Stats parent={chartRef as RefObject<HTMLElement>} />
|
||||||
<RenderInfoUpdater />
|
<RenderInfoUpdater />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { makeAutoObservable } from "mobx";
|
import { makeAutoObservable } from "mobx";
|
||||||
import type { ObjectType, RuntimeObjectInstance } from "../types";
|
|
||||||
import { state } from "./rootState";
|
import { state } from "./rootState";
|
||||||
|
|
||||||
export type MenuNode = {
|
export type MenuNode = {
|
||||||
|
|
@ -20,13 +19,15 @@ export class MenuState {
|
||||||
.map((ot) => ({
|
.map((ot) => ({
|
||||||
id: `ot-${ot.id}`,
|
id: `ot-${ot.id}`,
|
||||||
title: ot.name,
|
title: ot.name,
|
||||||
|
onClick: () => { state.worldEditor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
|
||||||
|
selected: () => state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection?.id === ot.id,
|
||||||
children: Object.values(state.worldEditor.scene.objects)
|
children: Object.values(state.worldEditor.scene.objects)
|
||||||
.filter((o) => o.typeId === ot.id)
|
.filter((o) => o.typeId === ot.id)
|
||||||
.map((o) => ({
|
.map((o) => ({
|
||||||
id: `o-${o.id}`,
|
id: `o-${o.id}`,
|
||||||
title: o.id,
|
title: o.id,
|
||||||
onClick: () => { state.worldEditor.setSelectedObject(o.id, 'translate') },
|
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
|
||||||
selected: () => state.worldEditor.selectedObjectId === o.id,
|
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
|
||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -36,8 +37,8 @@ export class MenuState {
|
||||||
{
|
{
|
||||||
id: 'editor-objects-menu',
|
id: 'editor-objects-menu',
|
||||||
title: 'Objects',
|
title: 'Objects',
|
||||||
onClick: () => { state.worldEditor.resetSelectedObject() },
|
onClick: () => { state.worldEditor.resetSelection() },
|
||||||
selected: () => state.worldEditor.selectedObjectId === undefined,
|
selected: () => !state.worldEditor.selection,
|
||||||
children: this.editorObjectTypesMenu,
|
children: this.editorObjectTypesMenu,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,54 @@ import { randomId } from "../utils";
|
||||||
import { state } from "./rootState";
|
import { state } from "./rootState";
|
||||||
import { populateRuntimeObject } from "../utils/runtime";
|
import { populateRuntimeObject } from "../utils/runtime";
|
||||||
|
|
||||||
export const SelectionEditModeEnum = [
|
export const ObjectEditModeEnum = [
|
||||||
'translate',
|
'translate',
|
||||||
'rotate',
|
'rotate',
|
||||||
'scale',
|
'scale',
|
||||||
] as const;
|
] as const;
|
||||||
type SelectionEditModeTuple = typeof SelectionEditModeEnum;
|
type ObjectEditModeTuple = typeof ObjectEditModeEnum;
|
||||||
export type SelectionEditMode = SelectionEditModeTuple[number];
|
export type ObjectEditMode = ObjectEditModeTuple[number];
|
||||||
|
|
||||||
export function nextSelectionEditMode(mode: SelectionEditMode | undefined): SelectionEditMode {
|
export function nextObjectEditMode(mode: ObjectEditMode | undefined): ObjectEditMode {
|
||||||
if (mode === undefined)
|
if (mode === undefined)
|
||||||
return 'translate';
|
return ObjectEditModeEnum[0];
|
||||||
|
|
||||||
const idx = SelectionEditModeEnum.indexOf(mode);
|
const idx = ObjectEditModeEnum.indexOf(mode);
|
||||||
return SelectionEditModeEnum[(idx + 1) % SelectionEditModeEnum.length];
|
return ObjectEditModeEnum[(idx + 1) % ObjectEditModeEnum.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ObjectTypeEditModeEnum = [
|
||||||
|
'scripts',
|
||||||
|
] as const;
|
||||||
|
type ObjectTypeEditModeTuple = typeof ObjectTypeEditModeEnum;
|
||||||
|
export type ObjectTypeEditMode = ObjectTypeEditModeTuple[number];
|
||||||
|
|
||||||
|
export function nextObjectTypeEditMode(mode: ObjectTypeEditMode | undefined): ObjectTypeEditMode {
|
||||||
|
if (mode === undefined)
|
||||||
|
return ObjectTypeEditModeEnum[0];
|
||||||
|
|
||||||
|
const idx = ObjectTypeEditModeEnum.indexOf(mode);
|
||||||
|
return ObjectTypeEditModeEnum[(idx + 1) % ObjectTypeEditModeEnum.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectedObject = {
|
||||||
|
type: 'object',
|
||||||
|
id: string;
|
||||||
|
editMode: ObjectEditMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectedObjectType = {
|
||||||
|
type: 'objectType',
|
||||||
|
id: string;
|
||||||
|
editMode: ObjectTypeEditMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Selection = SelectedObject | SelectedObjectType;
|
||||||
|
|
||||||
export class WorldEditorState {
|
export class WorldEditorState {
|
||||||
private readonly world: WorldState;
|
private readonly world: WorldState;
|
||||||
|
|
||||||
public selectedObjectId: string | undefined;
|
public selection: Selection | undefined;
|
||||||
public selectedObjectMode: SelectionEditMode | undefined;
|
|
||||||
|
|
||||||
constructor(world: WorldState) {
|
constructor(world: WorldState) {
|
||||||
this.world = world;
|
this.world = world;
|
||||||
|
|
@ -41,14 +68,33 @@ export class WorldEditorState {
|
||||||
this.world.data.editorCamera = value;
|
this.world.data.editorCamera = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setSelectedObject(id: string, mode: SelectionEditMode): void {
|
public setSelection(value: Selection | undefined): void {
|
||||||
this.selectedObjectId = id;
|
this.selection = value;
|
||||||
this.selectedObjectMode = mode;
|
console.log(JSON.stringify(this.selection));
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetSelectedObject(): void {
|
public setSelectedObject(value: Omit<SelectedObject, 'type'> | undefined): void {
|
||||||
this.selectedObjectId = undefined;
|
this.setSelection(value
|
||||||
this.selectedObjectMode = undefined;
|
? {
|
||||||
|
type: 'object',
|
||||||
|
...value,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSelectedObjectType(value: Omit<SelectedObjectType, 'type'> | undefined): void {
|
||||||
|
this.setSelection(value
|
||||||
|
? {
|
||||||
|
type: 'objectType',
|
||||||
|
...value,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetSelection() {
|
||||||
|
this.setSelection(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get scene(): RuntimeScene {
|
public get scene(): RuntimeScene {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export class WorldState {
|
||||||
this.withoutAutoSave(() => {
|
this.withoutAutoSave(() => {
|
||||||
this.data = WorldFactory.create();
|
this.data = WorldFactory.create();
|
||||||
});
|
});
|
||||||
state.worldEditor.resetSelectedObject();
|
state.worldEditor.resetSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadMock() {
|
public loadMock() {
|
||||||
|
|
@ -97,7 +97,7 @@ export class WorldState {
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(objects);
|
console.log(objects);
|
||||||
state.worldEditor.resetSelectedObject();
|
state.worldEditor.resetSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public load() {
|
public load() {
|
||||||
|
|
@ -105,7 +105,7 @@ export class WorldState {
|
||||||
this.withoutAutoSave(() => {
|
this.withoutAutoSave(() => {
|
||||||
this.data = WorldFactory.load() ?? WorldFactory.create();
|
this.data = WorldFactory.load() ?? WorldFactory.create();
|
||||||
});
|
});
|
||||||
state.worldEditor.resetSelectedObject();
|
state.worldEditor.resetSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveData(data: World): void {
|
private saveData(data: World): void {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue