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); + } }