diff --git a/src/App.tsx b/src/App.tsx index 2630044..6c0b314 100644 --- a/src/App.tsx +++ b/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() { <> - + ); } @@ -73,6 +88,7 @@ export const App = function () { }> } /> } /> + } /> } /> diff --git a/src/components/LeftPanel.tsx b/src/components/LeftPanel.tsx new file mode 100644 index 0000000..a1b3925 --- /dev/null +++ b/src/components/LeftPanel.tsx @@ -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 + +
+ + +
+ +}); diff --git a/src/components/ObjectEditorView.tsx b/src/components/ObjectEditorView.tsx index 2c2d887..9ab31ee 100644 --- a/src/components/ObjectEditorView.tsx +++ b/src/components/ObjectEditorView.tsx @@ -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>( @@ -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() { diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 8dd123b..ed732f8 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -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
- -
- - -
+ {children}
}); diff --git a/src/components/ThreeView.tsx b/src/components/ThreeView.tsx index 25630aa..76aa4c2 100644 --- a/src/components/ThreeView.tsx +++ b/src/components/ThreeView.tsx @@ -44,7 +44,7 @@ export const ThreeView = observer(function () {
state.worldEditor.resetSelectedObject()} + onPointerMissed={() => state.worldEditor.resetSelection()} > } /> diff --git a/src/state/menuState.ts b/src/state/menuState.ts index fc89ece..afcda9a 100644 --- a/src/state/menuState.ts +++ b/src/state/menuState.ts @@ -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, } ] diff --git a/src/state/worldEditorState.ts b/src/state/worldEditorState.ts index ef8e0aa..e4e4be6 100644 --- a/src/state/worldEditorState.ts +++ b/src/state/worldEditorState.ts @@ -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 | undefined): void { + this.setSelection(value + ? { + type: 'object', + ...value, + } + : undefined, + ); + } + + public setSelectedObjectType(value: Omit | undefined): void { + this.setSelection(value + ? { + type: 'objectType', + ...value, + } + : undefined, + ); + } + + public resetSelection() { + this.setSelection(undefined); } public get scene(): RuntimeScene { diff --git a/src/state/worldState.ts b/src/state/worldState.ts index f4cb792..c814976 100644 --- a/src/state/worldState.ts +++ b/src/state/worldState.ts @@ -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 {