diff --git a/webclient/src/components/layers/LayeredThreeView.tsx b/webclient/src/components/layers/LayeredThreeView.tsx
index a294153..c87b755 100644
--- a/webclient/src/components/layers/LayeredThreeView.tsx
+++ b/webclient/src/components/layers/LayeredThreeView.tsx
@@ -5,6 +5,7 @@ import { SceneEditorCameraLayer } from "./SceneEditor/SceneEditorGameraLayer";
import { SceneEditorObjectSelectionLayer } from "./SceneEditor/SceneEditorObjectSelectionLayer";
import { LightsLayer } from "./LighsLayer";
import { StatsLayer } from './StatsLayer';
+import { SceneEditorObjectGizmosLayer } from "./SceneEditor/SceneEditorObjectGizmosLayer";
export const LayeredThreeView = observer(function () {
@@ -15,6 +16,7 @@ export const LayeredThreeView = observer(function () {
+
diff --git a/webclient/src/components/layers/SceneEditor/SceneEditorObjectGizmosLayer.tsx b/webclient/src/components/layers/SceneEditor/SceneEditorObjectGizmosLayer.tsx
new file mode 100644
index 0000000..9efb8d1
--- /dev/null
+++ b/webclient/src/components/layers/SceneEditor/SceneEditorObjectGizmosLayer.tsx
@@ -0,0 +1,42 @@
+import { useCallback } from "react";
+import * as THREE from "three";
+import { TransformControls } from "@react-three/drei";
+import { state } from "../../../state";
+import { observer } from "mobx-react-lite";
+import { measuredRenderTime } from "../../../utils/layerTimings";
+import type { V3, R3 } from "../../../types";
+
+export const SceneEditorObjectGizmosLayer = observer(measuredRenderTime(
+ "ObjectGizmos",
+ function () {
+ const selection = state.view.selectedObject;
+
+ const onObjectChange = useCallback((e?: THREE.Event) => {
+ if (!selection || !e)
+ return;
+
+ const controlled = (e.target as { object?: THREE.Object3D }).object;
+ if (!controlled)
+ return;
+
+ selection.geometry.setObjectTransform(
+ controlled.position.toArray() as V3,
+ controlled.rotation.toArray().slice(0, 3) as R3,
+ controlled.scale.toArray() as V3,
+ );
+ }, [selection]);
+
+ if (!selection)
+ return null;
+
+ const obj = selection.geometry.object;
+
+ return ;
+ },
+));
diff --git a/webclient/src/components/layers/SceneEditor/SceneEditorObjectSelectionLayer.tsx b/webclient/src/components/layers/SceneEditor/SceneEditorObjectSelectionLayer.tsx
index 37e91d3..f523fd6 100644
--- a/webclient/src/components/layers/SceneEditor/SceneEditorObjectSelectionLayer.tsx
+++ b/webclient/src/components/layers/SceneEditor/SceneEditorObjectSelectionLayer.tsx
@@ -1,37 +1,52 @@
-import { useEffect, useCallback } from "react";
+import { useEffect, useCallback, useRef } from "react";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";
import { state } from "../../../state";
+import { nextObjectEditMode } from "../../../state/worldEditorState";
import { observer } from "mobx-react-lite";
import { SelectionBoundingBox } from "./SelectionBoundingBox";
import { measuredRenderTime } from "../../../utils/layerTimings";
import { useObjectGeometryList } from "../ObjectGeometriesLayer";
+const DRAG_THRESHOLD = 4;
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
"ObjectSelection",
function () {
- const selection = state.view.selectedObject;
- const camera = useThree(s => s.camera);
- const scene = useThree(s => s.scene);
- const gl = useThree(s => s.gl);
- const size = useThree(s => s.size);
const geometryListRef = useObjectGeometryList();
+ const selection = state.view.selectedObject;
+ const { camera, gl, size } = useThree();
+ const pointerDownPos = useRef<{ x: number; y: number } | null>(null);
+
const handlePointerDown = useCallback((e: PointerEvent) => {
if (e.button !== 0)
return;
+ pointerDownPos.current = { x: e.offsetX, y: e.offsetY };
+ }, []);
+
+ const handlePointerUp = useCallback((e: PointerEvent) => {
+ if (e.button !== 0)
+ return;
+
+ const down = pointerDownPos.current;
+ pointerDownPos.current = null;
+ if (!down)
+ return;
+
+ const dx = e.offsetX - down.x;
+ const dy = e.offsetY - down.y;
+ if (dx * dx + dy * dy > DRAG_THRESHOLD * DRAG_THRESHOLD)
+ return;
pointer.x = (e.offsetX / size.width) * 2 - 1;
pointer.y = -(e.offsetY / size.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
- const instancedMeshes = scene.children.filter(
- (child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh,
- );
+ const instancedMeshes = state.view.objectTypeGeometries.values.filter((ot) => ot.meshes?.length).flatMap((ot) => ot.meshes!);
const intersections = raycaster.intersectObjects(instancedMeshes, false);
@@ -41,9 +56,8 @@ export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
}
const hit = intersections[0];
- const mesh = hit.object as THREE.InstancedMesh;
- const instanceId = hit.instanceId;
+ const instanceId = hit.instanceId;
if (instanceId === undefined) {
state.worldEditor.resetSelection();
return;
@@ -53,21 +67,28 @@ export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
if (!geometryList)
return;
- const objectId = geometryList.getObjectIdByMesh(mesh, instanceId);
- if (objectId) {
- state.worldEditor.setSelectedObject({ id: objectId, editMode: "translate" });
+ const mesh = hit.object as THREE.InstancedMesh;
+ const instanceGeometry = geometryList.getObjectGeometryByMesh(mesh, instanceId);
+ if (instanceGeometry) {
+ const sel = state.worldEditor.selection;
+ const editMode = sel?.type === 'object' && sel.id === instanceGeometry.object.id
+ ? nextObjectEditMode(sel.editMode)
+ : 'translate';
+ state.worldEditor.setSelectedObject({ id: instanceGeometry.object.id, editMode });
} else {
state.worldEditor.resetSelection();
}
- }, [camera, scene, size, geometryListRef]);
+ }, [camera, size, geometryListRef]);
useEffect(() => {
const domElement = gl.domElement;
domElement.addEventListener("pointerdown", handlePointerDown);
+ domElement.addEventListener("pointerup", handlePointerUp);
return () => {
domElement.removeEventListener("pointerdown", handlePointerDown);
+ domElement.removeEventListener("pointerup", handlePointerUp);
};
- }, [gl, handlePointerDown]);
+ }, [gl, handlePointerDown, handlePointerUp]);
return <>
{selection &&
diff --git a/webclient/src/components/layers/SceneEditor/SelectionBoundingBox.tsx b/webclient/src/components/layers/SceneEditor/SelectionBoundingBox.tsx
index bca730d..004e0c6 100644
--- a/webclient/src/components/layers/SceneEditor/SelectionBoundingBox.tsx
+++ b/webclient/src/components/layers/SceneEditor/SelectionBoundingBox.tsx
@@ -1,8 +1,9 @@
import { useMemo } from "react";
import * as THREE from "three";
+import { observer } from "mobx-react-lite";
import type { ObjectGeometryViewItem } from "../../../state/viewLayers";
-export const SelectionBoundingBox = function({ item }: { item: ObjectGeometryViewItem }) {
+export const SelectionBoundingBox = observer(function({ item }: { item: ObjectGeometryViewItem }) {
const obj = item.object;
const bb = item.objectTypeGeometry.boundingBox;
@@ -41,4 +42,4 @@ export const SelectionBoundingBox = function({ item }: { item: ObjectGeometryVie
;
-};
+});
diff --git a/webclient/src/rendering/ObjectGeometryList.ts b/webclient/src/rendering/ObjectGeometryList.ts
index 12d5490..f8d0608 100644
--- a/webclient/src/rendering/ObjectGeometryList.ts
+++ b/webclient/src/rendering/ObjectGeometryList.ts
@@ -1,17 +1,17 @@
import * as THREE from 'three';
import { MobxReconcilerMap, type ReconcilerMap } from '../utils/reconciler';
-import type { ObjectTypeGeometryViewItem } from '../state/viewLayers';
+import type { ObjectGeometryViewItem, ObjectTypeGeometryViewItem } from '../state/viewLayers';
export class ObjectGeometryList {
private items: ReconcilerMap;
+ private readonly instancesByMesh = new Map();
+
private readonly matrixObj = new THREE.Object3D();
private readonly _p = new THREE.Vector3();
private readonly _e = new THREE.Euler();
private readonly _q = new THREE.Quaternion();
- private readonly _meshToObjectIds = new Map();
-
constructor(scene: THREE.Scene, typeGeometries: Iterable) {
this.items = new MobxReconcilerMap({
reaction: () => typeGeometries,
@@ -31,17 +31,16 @@ export class ObjectGeometryList {
this.items?.dispose();
}
- getObjectIdByMesh(mesh: THREE.InstancedMesh, instanceId: number): string | undefined {
- return this._meshToObjectIds.get(mesh)?.[instanceId];
+ getObjectGeometryByMesh(mesh: THREE.InstancedMesh, instanceId: number): ObjectGeometryViewItem | undefined {
+ return this.instancesByMesh.get(mesh)?.[instanceId];
}
tick(): void {
- this._meshToObjectIds.clear();
+ this.instancesByMesh.clear();
this.items.values.forEach((typeGeometry) => {
const instances = typeGeometry.instances;
const meshes = typeGeometry.meshes ?? [];
- const objectIds: string[] = [];
instances.forEach((item, instanceIdx) => {
const obj = item.object;
@@ -54,14 +53,12 @@ export class ObjectGeometryList {
this.matrixObj.scale.set(...obj.scale);
this.matrixObj.updateMatrix();
- objectIds[instanceIdx] = obj.id;
-
for (const mesh of meshes)
mesh.setMatrixAt(instanceIdx, this.matrixObj.matrix);
});
for (const mesh of meshes) {
- this._meshToObjectIds.set(mesh, objectIds);
+ this.instancesByMesh.set(mesh, typeGeometry.instances);
mesh.count = instances.length;
mesh.instanceMatrix.needsUpdate = true;
}
diff --git a/webclient/src/state/viewLayers/objectGeometry.ts b/webclient/src/state/viewLayers/objectGeometry.ts
index 8f5a8e9..fed48fe 100644
--- a/webclient/src/state/viewLayers/objectGeometry.ts
+++ b/webclient/src/state/viewLayers/objectGeometry.ts
@@ -1,4 +1,5 @@
-import type { ObjectInstance, ObjectInstanceRigidBody } from "../../types";
+import type { ObjectInstance, ObjectInstanceRigidBody, V3 } from "../../types";
+import { state } from "../rootState";
import { ViewItem } from "./base";
import type { ObjectTypeGeometryViewItem } from './objectTypeGeometry';
@@ -13,4 +14,8 @@ export class ObjectGeometryViewItem extends ViewItem {
this.object = object;
this.objectTypeGeometry = objectTypeGeometry;
}
+
+ setObjectTransform(position: V3, rotation: V3, scale: V3) {
+ state.worldEditor.setObjectTransform(this.object.id, position, rotation, scale);
+ }
}