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