Compare commits
No commits in common. "9145277681d8e08d5413843b45e72c2fc79d30f8" and "dabd120f10d6f4f5c1d9e7fd5e194969eb28a9df" have entirely different histories.
9145277681
...
dabd120f10
|
|
@ -16,7 +16,6 @@
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"mobx": "^6.15.4",
|
"mobx": "^6.15.4",
|
||||||
"mobx-react-lite": "^4.1.1",
|
"mobx-react-lite": "^4.1.1",
|
||||||
"mobx-utils": "^6.1.1",
|
|
||||||
"npm": "^11.16.0",
|
"npm": "^11.16.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ importers:
|
||||||
mobx-react-lite:
|
mobx-react-lite:
|
||||||
specifier: ^4.1.1
|
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)
|
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:
|
npm:
|
||||||
specifier: ^11.16.0
|
specifier: ^11.16.0
|
||||||
version: 11.16.0
|
version: 11.16.0
|
||||||
|
|
@ -1020,11 +1017,6 @@ packages:
|
||||||
react-native:
|
react-native:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
mobx-utils@6.1.1:
|
|
||||||
resolution: {integrity: sha512-ZR4tOKucWAHOdMjqElRl2BEvrzK7duuDdKmsbEbt2kzgVpuLuoYLiDCjc3QwWQl8CmOlxPgaZQpZ7emwNqPkIg==}
|
|
||||||
peerDependencies:
|
|
||||||
mobx: ^6.0.0
|
|
||||||
|
|
||||||
mobx@6.15.4:
|
mobx@6.15.4:
|
||||||
resolution: {integrity: sha512-do+2UsEKRVT70W/QqP2F2sju2x4p2xZo+5/azXqKjXgTk2jfmzsLjzwW0YI8CBEjy4ZUdU8EunXocXXwJdCrtw==}
|
resolution: {integrity: sha512-do+2UsEKRVT70W/QqP2F2sju2x4p2xZo+5/azXqKjXgTk2jfmzsLjzwW0YI8CBEjy4ZUdU8EunXocXXwJdCrtw==}
|
||||||
|
|
||||||
|
|
@ -2297,10 +2289,6 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
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: {}
|
mobx@6.15.4: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { ObjectInstance } from "../types";
|
import type { ObjectInstance } from "../types";
|
||||||
import { useRef, type RefObject } from "react";
|
import { useEffect, useRef, type RefObject } from "react";
|
||||||
import type { Group } from "three";
|
import type { Group } from "three";
|
||||||
import { TransformControls, useHelper } from "@react-three/drei";
|
import { TransformControls, useHelper } from "@react-three/drei";
|
||||||
import { BoxHelper } from "three";
|
import { BoxHelper } from "three";
|
||||||
|
|
@ -15,6 +15,7 @@ type ObjectViewProps = {
|
||||||
|
|
||||||
export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
||||||
const groupRef = useRef<Group>(null);
|
const groupRef = useRef<Group>(null);
|
||||||
|
const controlsRef = useRef<any>(null);
|
||||||
|
|
||||||
const isSelected = state.worldEditor.isEnabled &&
|
const isSelected = state.worldEditor.isEnabled &&
|
||||||
state.worldEditor.selectedObjectId === object.id;
|
state.worldEditor.selectedObjectId === object.id;
|
||||||
|
|
@ -28,24 +29,40 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
||||||
? state.worldEditor.selectedObjectMode
|
? state.worldEditor.selectedObjectMode
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
function handleClick(e: ThreeEvent<MouseEvent>) {
|
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<MouseEvent>) => {
|
||||||
if (!state.worldEditor.isEnabled)
|
if (!state.worldEditor.isEnabled)
|
||||||
return;
|
return;
|
||||||
if (e.delta > 5)
|
if (e.delta > 5)
|
||||||
return;
|
return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode));
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleTransformEnd() {
|
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode));
|
||||||
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 (<>
|
return (<>
|
||||||
|
|
@ -54,7 +71,6 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
||||||
<TransformControls
|
<TransformControls
|
||||||
object={groupRef as RefObject<Group>}
|
object={groupRef as RefObject<Group>}
|
||||||
mode={selectionMode}
|
mode={selectionMode}
|
||||||
onMouseUp={handleTransformEnd}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<group
|
<group
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { observer } from 'mobx-react-lite';
|
||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
import { SceneView } from './SceneView';
|
import { SceneView } from './SceneView';
|
||||||
import type { Pos3 } from '../types/3d';
|
import type { Pos3 } from '../types/3d';
|
||||||
import type { OrthographicCamera, PerspectiveCamera } from 'three';
|
|
||||||
|
|
||||||
const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
|
const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
|
||||||
const { camera: threeCamera } = useThree();
|
const { camera: threeCamera } = useThree();
|
||||||
|
|
@ -23,29 +22,10 @@ const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
|
||||||
export const SceneEditorView = observer(function () {
|
export const SceneEditorView = observer(function () {
|
||||||
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
||||||
|
|
||||||
const cameraAtStart = useRef<string>('');
|
|
||||||
|
|
||||||
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 handleEnd = () => {
|
||||||
const controls = controlsRef.current;
|
const controls = controlsRef.current;
|
||||||
if (!controls)
|
if (!controls)
|
||||||
return;
|
return;
|
||||||
const snapshot = cameraSnapshot(controls.object);
|
|
||||||
if (snapshot === cameraAtStart.current)
|
|
||||||
return;
|
|
||||||
const [x, y, z] = controls.object.rotation.toArray();
|
const [x, y, z] = controls.object.rotation.toArray();
|
||||||
state.worldEditor.setCamera({
|
state.worldEditor.setCamera({
|
||||||
position: controls.object.position.toArray(),
|
position: controls.object.position.toArray(),
|
||||||
|
|
@ -56,7 +36,7 @@ export const SceneEditorView = observer(function () {
|
||||||
return (<>
|
return (<>
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
ref={controlsRef}
|
ref={controlsRef}
|
||||||
onStart={handleStart}
|
enabled={!state.worldEditor.isDragging}
|
||||||
onEnd={handleEnd}
|
onEnd={handleEnd}
|
||||||
makeDefault
|
makeDefault
|
||||||
enableDamping={false}
|
enableDamping={false}
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,11 @@ export const Toolbar = observer(function () {
|
||||||
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLoadWorld(): void {
|
|
||||||
state.world.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLoadMockWorld(): void {
|
function handleLoadMockWorld(): void {
|
||||||
state.world.loadMock();
|
state.world.loadMock();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="toolbar">
|
return <div className="toolbar">
|
||||||
<button onClick={handleLoadWorld}>Load</button>
|
|
||||||
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
||||||
<button onClick={handleCloneTest1Object}>Clone test1</button>
|
<button onClick={handleCloneTest1Object}>Clone test1</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { makeAutoObservable } from "mobx";
|
import { makeAutoObservable } from "mobx";
|
||||||
import type { WorldState } from "./worldState";
|
import type { WorldState } from "./worldState";
|
||||||
import type { RunningGameState, Scene } from "../types";
|
import type { RunningGameState, Scene } from "../types";
|
||||||
|
import { clone } from "../utils";
|
||||||
import type { Pos3 } from "../types/3d";
|
import type { Pos3 } from "../types/3d";
|
||||||
import type { CameraProps } from "@react-three/fiber";
|
import type { CameraProps } from "@react-three/fiber";
|
||||||
|
|
||||||
|
|
@ -38,11 +39,15 @@ export class GameState {
|
||||||
}
|
}
|
||||||
|
|
||||||
public resume(): void {
|
public resume(): void {
|
||||||
this.state.paused = false;
|
const state = clone(this.world.data.state) as RunningGameState;
|
||||||
|
state.paused = false;
|
||||||
|
this.world.data.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public pause(): void {
|
||||||
this.state.paused = true;
|
const state = clone(this.world.data.state) as RunningGameState;
|
||||||
|
state.paused = true;
|
||||||
|
this.world.data.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable } from "mobx";
|
||||||
import type { WorldState } from "./worldState";
|
import type { WorldState } from "./worldState";
|
||||||
import type { ObjectInstance, Scene } from "../types";
|
import type { ObjectInstance, Scene, World } from "../types";
|
||||||
import { createObjectInstance } from "../utils/object";
|
import { createObjectInstance } from "../utils/object";
|
||||||
import { randomId } from "../utils";
|
import { randomId } from "../utils";
|
||||||
import type { Pos3, R3, V3 } from "../types/3d";
|
import type { Pos3, R3, V3 } from "../types/3d";
|
||||||
|
|
@ -27,6 +27,7 @@ export class WorldEditorState {
|
||||||
|
|
||||||
public selectedObjectId: string | undefined;
|
public selectedObjectId: string | undefined;
|
||||||
public selectedObjectMode: SelectionEditMode | undefined;
|
public selectedObjectMode: SelectionEditMode | undefined;
|
||||||
|
public isDragging: boolean = false;
|
||||||
|
|
||||||
constructor(world: WorldState) {
|
constructor(world: WorldState) {
|
||||||
this.world = world;
|
this.world = world;
|
||||||
|
|
@ -39,6 +40,7 @@ export class WorldEditorState {
|
||||||
|
|
||||||
public setCamera(value: Pos3): void {
|
public setCamera(value: Pos3): void {
|
||||||
this.world.data.editorCamera = value;
|
this.world.data.editorCamera = value;
|
||||||
|
this.world.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setSelectedObject(id: string, mode: SelectionEditMode): void {
|
public setSelectedObject(id: string, mode: SelectionEditMode): void {
|
||||||
|
|
@ -51,6 +53,12 @@ export class WorldEditorState {
|
||||||
this.selectedObjectMode = undefined;
|
this.selectedObjectMode = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setIsDragging(value: boolean): void {
|
||||||
|
this.isDragging = value;
|
||||||
|
if (!value)
|
||||||
|
this.world.save();
|
||||||
|
}
|
||||||
|
|
||||||
public get scene(): Scene {
|
public get scene(): Scene {
|
||||||
return this.world.data.initialScene;
|
return this.world.data.initialScene;
|
||||||
}
|
}
|
||||||
|
|
@ -59,15 +67,49 @@ export class WorldEditorState {
|
||||||
return this.world.data.editorCamera;
|
return this.world.data.editorCamera;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setObjectTransform(id: string, position: V3, rotation: R3, scale: V3): void {
|
public mutateWorld(mutation: Partial<World>): void {
|
||||||
|
this.world.data = {
|
||||||
|
...this.world.data,
|
||||||
|
...mutation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutateScene(mutation: Partial<Scene>): void {
|
||||||
|
this.mutateWorld({
|
||||||
|
initialScene: {
|
||||||
|
...this.world.data.initialScene,
|
||||||
|
...mutation,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutateObject(mutation: Partial<ObjectInstance> & Pick<ObjectInstance, 'id'>): void {
|
||||||
|
this.mutateScene({
|
||||||
|
objects: {
|
||||||
|
...this.world.data.initialScene.objects,
|
||||||
|
[mutation.id]: {
|
||||||
|
...this.world.data.initialScene.objects[mutation.id],
|
||||||
|
...mutation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setObjectTransform(
|
||||||
|
id: string,
|
||||||
|
position: V3,
|
||||||
|
rotation: R3,
|
||||||
|
scale: V3,
|
||||||
|
): void {
|
||||||
const obj = this.scene.objects[id];
|
const obj = this.scene.objects[id];
|
||||||
if (!obj)
|
if (!obj)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
runInAction(() => { // all in one go
|
this.mutateObject({
|
||||||
obj.position = position;
|
...obj,
|
||||||
obj.rotation = rotation;
|
position,
|
||||||
obj.scale = scale;
|
rotation,
|
||||||
|
scale,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { makeAutoObservable, reaction, toJS } from "mobx";
|
import { makeAutoObservable } from "mobx";
|
||||||
import { deepObserve } from "mobx-utils";
|
|
||||||
import { WorldFactory } from "../model/worldFactory";
|
import { WorldFactory } from "../model/worldFactory";
|
||||||
import type { ObjectType, World } from "../types";
|
import type { ObjectType, World } from "../types";
|
||||||
import type { VoxelType } from "../types/voxel";
|
import type { VoxelType } from "../types/voxel";
|
||||||
|
|
@ -11,35 +10,16 @@ import { clone } from "../utils";
|
||||||
export class WorldState {
|
export class WorldState {
|
||||||
public data: World = WorldFactory.create();
|
public data: World = WorldFactory.create();
|
||||||
|
|
||||||
private saveTimer: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
this.setupAutoSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupAutoSave() {
|
|
||||||
let disposeDeep: (() => void) | undefined;
|
|
||||||
reaction(
|
|
||||||
() => this.data,
|
|
||||||
(data) => {
|
|
||||||
disposeDeep?.();
|
|
||||||
disposeDeep = deepObserve(data, () => this.save());
|
|
||||||
},
|
|
||||||
{ fireImmediately: true },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset() {
|
public reset() {
|
||||||
console.log('Resetting world...');
|
|
||||||
|
|
||||||
this.data = WorldFactory.create();
|
|
||||||
state.worldEditor.resetSelectedObject();
|
state.worldEditor.resetSelectedObject();
|
||||||
|
this.data = WorldFactory.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadMock() {
|
public loadMock() {
|
||||||
console.log('Mocking world...');
|
|
||||||
|
|
||||||
const objectId1 = 'object1';
|
const objectId1 = 'object1';
|
||||||
|
|
||||||
this.data = {
|
this.data = {
|
||||||
|
|
@ -79,24 +59,16 @@ export class WorldState {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
state.worldEditor.resetSelectedObject();
|
state.worldEditor.resetSelectedObject();
|
||||||
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public load() {
|
public load() {
|
||||||
console.log('Loading world...');
|
|
||||||
this.data = WorldFactory.load() ?? WorldFactory.create();
|
this.data = WorldFactory.load() ?? WorldFactory.create();
|
||||||
state.worldEditor.resetSelectedObject();
|
state.worldEditor.resetSelectedObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
public save(): void {
|
public save(): void {
|
||||||
console.log('Saving world...');
|
WorldFactory.save(this.data);
|
||||||
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 {
|
public getObjectTypeById(id: string): ObjectType | undefined {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue