From e37cb0869896210b699109a1a511e53aac8dbb72 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Tue, 2 Jun 2026 13:14:23 +0300 Subject: [PATCH] world structure: arrays to records object move with mouse --- src/components/ObjectView.tsx | 56 ++++++++++++++++++++++++----------- src/components/ThreeView.tsx | 5 +++- src/components/Toolbar.tsx | 10 +++++-- src/components/WorldView.tsx | 7 ++--- src/model/worldFactory.ts | 2 +- src/state/world.ts | 45 +++++++++++++++------------- src/state/worldEditor.ts | 55 +++++++++++++++++++++++++++++++--- src/types/scene.ts | 2 +- src/types/world.ts | 6 ++-- 9 files changed, 134 insertions(+), 54 deletions(-) diff --git a/src/components/ObjectView.tsx b/src/components/ObjectView.tsx index 4f00766..88e2e83 100644 --- a/src/components/ObjectView.tsx +++ b/src/components/ObjectView.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; import type { ObjectInstance } from "../types"; -import { useRef } from "react"; -import type { Mesh } from "three"; -import { Edges } from "@react-three/drei"; +import { useEffect, useRef, type RefObject } from "react"; +import type { Group, Mesh } from "three"; +import { Edges, TransformControls } from "@react-three/drei"; import { state } from "../state"; type ObjectViewProps = { @@ -10,39 +10,61 @@ type ObjectViewProps = { } export const ObjectView = observer(function ({ object }: ObjectViewProps) { - const meshRef = useRef(null); + const groupRef = useRef(null); + const controlsRef = useRef(null); const objectType = state.world.getObjectTypeById(object.typeId); if (!objectType) return null; - - const isSelected = state.worldEditor.isEnabled && + const isSelected = state.worldEditor.isEnabled && state.worldEditor.selectedObjectId === object.id; + useEffect(() => { + if (!isSelected) + return; + const controls = controlsRef.current; + if (!controls) + return; + const onDragChanged = (e: { value: boolean }) => state.worldEditor.setIsDragging(e.value); + controls.addEventListener('dragging-changed', onDragChanged); + return () => { + controls.removeEventListener('dragging-changed', onDragChanged); + state.worldEditor.setIsDragging(false); + }; + }, [isSelected]); + const handleClick = (e: { stopPropagation: () => void }) => { if (!state.worldEditor.isEnabled) return; e.stopPropagation(); + state.worldEditor.setSelectedObjectId(object.id); }; + const handleObjectChange = () => { + const group = groupRef.current; + if (!group) + return; + + const { x, y, z } = group.position; + state.worldEditor.setObjectPosition(object.id, [x, y, z]); + }; + return (<> - + {(isSelected && groupRef.current) && } onChange={handleObjectChange} mode="translate" />} + { objectType.voxels.map((v, idx) => { const vt = state.world.getVoxelTypeById(v.typeId); - return - - - {isSelected && } - + return ( + + + + {isSelected && } + + ); }) } ); }); - diff --git a/src/components/ThreeView.tsx b/src/components/ThreeView.tsx index a156dbc..66d4ffd 100644 --- a/src/components/ThreeView.tsx +++ b/src/components/ThreeView.tsx @@ -6,7 +6,10 @@ import { WorldView } from './WorldView'; export const ThreeView = observer(function () { return (
- + state.worldEditor.setSelectedObjectId(undefined)} + >
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index b970166..95afdac 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -5,11 +5,16 @@ import type { RunningGameState } from "../types"; export const Toolbar = observer(function () { - function handleCloneTest1ObjectClick(): void { + function handleCloneTest1Object(): void { state.worldEditor.addObjectCloneAtRandomPosition('test1'); } + function handleLoadMockWorld(): void { + state.world.loadMock(); + } + return
+ {state.worldEditor.isEnabled &&
EDITOR MODE
} {state.world.isPlaying ? <> @@ -21,6 +26,7 @@ export const Toolbar = observer(function () { : } - + +
}); diff --git a/src/components/WorldView.tsx b/src/components/WorldView.tsx index fbe4bbe..c6c2e98 100644 --- a/src/components/WorldView.tsx +++ b/src/components/WorldView.tsx @@ -11,8 +11,6 @@ export const WorldView = observer(function () { const world = state.world; const controlsRef = useRef>(null); - console.log(world.currentScene.objects); - useFrame((_, delta) => { world.tick(delta); }); @@ -31,13 +29,14 @@ export const WorldView = observer(function () { return (<> - {world.currentScene.objects.map((obj) => )} + {Object.values(world.currentScene.objects).map((obj) => )} ) }); diff --git a/src/model/worldFactory.ts b/src/model/worldFactory.ts index 549824d..9ac08ac 100644 --- a/src/model/worldFactory.ts +++ b/src/model/worldFactory.ts @@ -5,7 +5,7 @@ export class WorldFactory { public static create(): World { return { objectTypes: [], - intialScene: { + initialScene: { character: { position: [0, 0, 0], look: [0, 0, 0], diff --git a/src/state/world.ts b/src/state/world.ts index aa97282..5c5a080 100644 --- a/src/state/world.ts +++ b/src/state/world.ts @@ -22,47 +22,50 @@ export class WorldState { } public loadMock() { + + const objTypeId = 'test1'; + const voxelTypeId = 'red'; + const objectId = 'red'; + this.data = { - objectTypes: [ - { - id: 'test1', + objectTypes: { + [objTypeId]: { + id: objTypeId, name: 'Test Object', voxels: [ { - typeId: 'red', + typeId: voxelTypeId, position: [0, 0, 0], - color: 'red', - opacity: 1, }, ], }, - ], - voxelTypes: [ - { - id: 'red', + }, + voxelTypes: { + [voxelTypeId]: { + id: voxelTypeId, collidable: true, name: 'Red', opacity: 1, color: 'red', } - ], + }, editorCamera: { position: [0, 2, 10], look: [0, 0, 0], }, - intialScene: { + initialScene: { character: { position: [0, 0, 0], look: [0, 0, 0], }, - objects: [ - { - id: randomId(), - typeId: 'test1', + objects: { + [objectId]: { + id: objectId, + typeId: objTypeId, position: [0, 0, 0], rotation: [0, 0, 0], }, - ], + }, }, gameRules: { gravity: true, @@ -88,7 +91,7 @@ export class WorldState { public get currentScene(): Scene { return this.isPlaying ? (this.data.state as RunningGameState).scene - : this.data.intialScene; + : this.data.initialScene; } public get currentCamera(): Pos3 { @@ -115,7 +118,7 @@ export class WorldState { playing: true, paused: false, time: 0, - scene: clone(this.data.intialScene), + scene: clone(this.data.initialScene), } } @@ -137,10 +140,10 @@ export class WorldState { } public getObjectTypeById(id: string): ObjectType | undefined { - return this.data.objectTypes.find((obj) => obj.id === id); + return this.data.objectTypes[id]; } public getVoxelTypeById(id: string): VoxelType | undefined { - return this.data.voxelTypes.find((obj) => obj.id === id); + return this.data.voxelTypes[id]; } } \ No newline at end of file diff --git a/src/state/worldEditor.ts b/src/state/worldEditor.ts index fa807f0..06a946b 100644 --- a/src/state/worldEditor.ts +++ b/src/state/worldEditor.ts @@ -1,14 +1,15 @@ import { makeAutoObservable } from "mobx"; import type { WorldState } from "./world"; -import type { ObjectInstance } from "../types"; +import type { ObjectInstance, Scene, World } from "../types"; import { createObjectInstance } from "../utils/object"; import { randomId } from "../utils"; -import type { Pos3 } from "../types/3d"; +import type { Pos3, V3 } from "../types/3d"; export class WorldEditorState { private readonly world: WorldState; public selectedObjectId: string | undefined; + public isDragging: boolean = false; constructor(world: WorldState) { this.world = world; @@ -21,7 +22,6 @@ export class WorldEditorState { public setCamera(value: Pos3): void { this.world.data.editorCamera = value; - console.log(JSON.stringify(this.world.data.editorCamera)); this.world.save(); } @@ -29,6 +29,53 @@ export class WorldEditorState { this.selectedObjectId = value; } + public setIsDragging(value: boolean): void { + this.isDragging = value; + if (!value) + this.world.save(); + } + + public get scene(): Scene { + return this.world.data.initialScene; + } + + public mutateWorld(mutation: Partial): void { + this.world.data = { + ...this.world.data, + ...mutation, + } + } + + public mutateScene(mutation: Partial): void { + this.mutateWorld({ + initialScene: { + ...this.world.data.initialScene, + ...mutation, + } + }); + } + + public mutateObject(mutation: Partial & Pick): void { + this.mutateScene({ + objects: { + ...this.world.data.initialScene.objects, + [mutation.id]: { + ...this.world.data.initialScene.objects[mutation.id], + ...mutation, + }, + }, + }); + } + + public setObjectPosition(id: string, pos: V3): void { + const obj = this.scene.objects[id]; + if (!obj) + return; + + obj.position = pos; + this.mutateObject(obj); + } + public addObjectCloneAtRandomPosition(typeId: string): ObjectInstance { return this.addObjectClone( typeId, @@ -44,7 +91,7 @@ export class WorldEditorState { obj.position = [pos.x, pos.y, pos.z]; obj.rotation = [0, 0, 0]; - this.world.data.intialScene.objects.push(obj); + this.scene.objects[obj.id] = obj; return obj; } diff --git a/src/types/scene.ts b/src/types/scene.ts index ca5cb15..88b5c1f 100644 --- a/src/types/scene.ts +++ b/src/types/scene.ts @@ -3,5 +3,5 @@ import type { ObjectInstance } from "./object"; export type Scene = { character: CharacterState; - objects: ObjectInstance[]; + objects: Record; } \ No newline at end of file diff --git a/src/types/world.ts b/src/types/world.ts index 6c4e34d..df77431 100644 --- a/src/types/world.ts +++ b/src/types/world.ts @@ -6,10 +6,10 @@ import type { Pos3 } from "./3d"; import type { VoxelType } from "./voxel"; export type World = { - objectTypes: ObjectType[]; - voxelTypes: VoxelType[]; + objectTypes: Record; + voxelTypes: Record; editorCamera: Pos3; - intialScene: Scene; + initialScene: Scene; gameRules: GameRules; state: GameState; }