diff --git a/package.json b/package.json index ff66fff..48974ba 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "install": "^0.13.0", "mobx": "^6.15.4", "mobx-react-lite": "^4.1.1", + "mobx-utils": "^6.1.1", "npm": "^11.16.0", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60ab854..e398580 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: mobx-react-lite: specifier: ^4.1.1 version: 4.1.1(mobx@6.15.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + mobx-utils: + specifier: ^6.1.1 + version: 6.1.1(mobx@6.15.4) npm: specifier: ^11.16.0 version: 11.16.0 @@ -1017,6 +1020,11 @@ packages: react-native: optional: true + mobx-utils@6.1.1: + resolution: {integrity: sha512-ZR4tOKucWAHOdMjqElRl2BEvrzK7duuDdKmsbEbt2kzgVpuLuoYLiDCjc3QwWQl8CmOlxPgaZQpZ7emwNqPkIg==} + peerDependencies: + mobx: ^6.0.0 + mobx@6.15.4: resolution: {integrity: sha512-do+2UsEKRVT70W/QqP2F2sju2x4p2xZo+5/azXqKjXgTk2jfmzsLjzwW0YI8CBEjy4ZUdU8EunXocXXwJdCrtw==} @@ -2289,6 +2297,10 @@ snapshots: optionalDependencies: react-dom: 19.2.6(react@19.2.6) + mobx-utils@6.1.1(mobx@6.15.4): + dependencies: + mobx: 6.15.4 + mobx@6.15.4: {} ms@2.1.3: {} diff --git a/src/components/ObjectView.tsx b/src/components/ObjectView.tsx index 0a55217..0fdd318 100644 --- a/src/components/ObjectView.tsx +++ b/src/components/ObjectView.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react-lite"; import type { ObjectInstance } from "../types"; -import { useEffect, useRef, type RefObject } from "react"; +import { useRef, type RefObject } from "react"; import type { Group } from "three"; import { TransformControls, useHelper } from "@react-three/drei"; import { BoxHelper } from "three"; @@ -15,7 +15,6 @@ type ObjectViewProps = { export const ObjectView = observer(function ({ object }: ObjectViewProps) { const groupRef = useRef(null); - const controlsRef = useRef(null); const isSelected = state.worldEditor.isEnabled && state.worldEditor.selectedObjectId === object.id; @@ -29,48 +28,33 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) { ? state.worldEditor.selectedObjectMode : undefined; - useEffect(() => { - if (!isSelected) - return; - const controls = controlsRef.current; - if (!controls) - return; - const onDragChanged = (e: { value: boolean }) => { - state.worldEditor.setIsDragging(e.value); - if (!e.value) { - const group = groupRef.current; - if (group) - state.worldEditor.setObjectTransform( - object.id, - group.position.toArray(), - group.rotation.toArray().slice(0, 3) as R3, - group.scale.toArray(), - ); - } - }; - controls.addEventListener('dragging-changed', onDragChanged); - return () => { - controls.removeEventListener('dragging-changed', onDragChanged); - state.worldEditor.setIsDragging(false); - }; - }, [isSelected]); - - const handleClick = (e: ThreeEvent) => { + function handleClick(e: ThreeEvent) { if (!state.worldEditor.isEnabled) return; if (e.delta > 5) return; e.stopPropagation(); - state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode)); }; + function handleTransformEnd() { + const group = groupRef.current; + if (group) + state.worldEditor.setObjectTransform( + object.id, + group.position.toArray(), + group.rotation.toArray().slice(0, 3) as R3, + group.scale.toArray(), + ); + }; + return (<> { (isSelected && (selectionMode !== undefined) && groupRef.current) && } mode={selectionMode} + onMouseUp={handleTransformEnd} /> } >(null); + const cameraAtStart = useRef(''); + + function cameraSnapshot(camera: OrthographicCamera | PerspectiveCamera): string { + return JSON.stringify({ + position: camera.position, + rotation: camera.rotation, + }); + } + + const handleStart = () => { + const controls = controlsRef.current; + if (!controls) + return; + cameraAtStart.current = cameraSnapshot(controls.object); + }; + const handleEnd = () => { const controls = controlsRef.current; if (!controls) return; + const snapshot = cameraSnapshot(controls.object); + if (snapshot === cameraAtStart.current) + return; const [x, y, z] = controls.object.rotation.toArray(); state.worldEditor.setCamera({ position: controls.object.position.toArray(), @@ -36,7 +56,7 @@ export const SceneEditorView = observer(function () { return (<> + diff --git a/src/state/worldEditorState.ts b/src/state/worldEditorState.ts index 9eefcee..e4b480c 100644 --- a/src/state/worldEditorState.ts +++ b/src/state/worldEditorState.ts @@ -27,7 +27,6 @@ export class WorldEditorState { public selectedObjectId: string | undefined; public selectedObjectMode: SelectionEditMode | undefined; - public isDragging: boolean = false; constructor(world: WorldState) { this.world = world; @@ -40,7 +39,6 @@ export class WorldEditorState { public setCamera(value: Pos3): void { this.world.data.editorCamera = value; - this.world.save(); } public setSelectedObject(id: string, mode: SelectionEditMode): void { @@ -53,12 +51,6 @@ export class WorldEditorState { this.selectedObjectMode = undefined; } - public setIsDragging(value: boolean): void { - this.isDragging = value; - if (!value) - this.world.save(); - } - public get scene(): Scene { return this.world.data.initialScene; } diff --git a/src/state/worldState.ts b/src/state/worldState.ts index b5fbe46..09b8e04 100644 --- a/src/state/worldState.ts +++ b/src/state/worldState.ts @@ -1,4 +1,5 @@ -import { makeAutoObservable, toJS } from "mobx"; +import { makeAutoObservable, reaction, toJS } from "mobx"; +import { deepObserve } from "mobx-utils"; import { WorldFactory } from "../model/worldFactory"; import type { ObjectType, World } from "../types"; import type { VoxelType } from "../types/voxel"; @@ -10,16 +11,35 @@ import { clone } from "../utils"; export class WorldState { public data: World = WorldFactory.create(); + private saveTimer: ReturnType | undefined; + constructor() { makeAutoObservable(this); + this.setupAutoSave(); + } + + private setupAutoSave() { + let disposeDeep: (() => void) | undefined; + reaction( + () => this.data, + (data) => { + disposeDeep?.(); + disposeDeep = deepObserve(data, () => this.save()); + }, + { fireImmediately: true }, + ); } public reset() { - state.worldEditor.resetSelectedObject(); + console.log('Resetting world...'); + this.data = WorldFactory.create(); + state.worldEditor.resetSelectedObject(); } public loadMock() { + console.log('Mocking world...'); + const objectId1 = 'object1'; this.data = { @@ -59,16 +79,24 @@ export class WorldState { }, }; state.worldEditor.resetSelectedObject(); - this.save(); } public load() { + console.log('Loading world...'); this.data = WorldFactory.load() ?? WorldFactory.create(); state.worldEditor.resetSelectedObject(); } public save(): void { - WorldFactory.save(toJS(this.data)); + console.log('Saving world...'); + console.log((new Error('').stack!)); + const { objectTypes, voxelTypes, ...debug } = toJS(this.data); + console.log(JSON.stringify(debug, undefined, 4)); + // debounce + clearTimeout(this.saveTimer); + this.saveTimer = setTimeout(() => { + WorldFactory.save(toJS(this.data)); + }, 500); } public getObjectTypeById(id: string): ObjectType | undefined { @@ -92,4 +120,4 @@ export class WorldState { state.worldEditor.resetSelectedObject(); } } -} \ No newline at end of file +}