object transform in editor
This commit is contained in:
@@ -5,6 +5,7 @@ import { SceneEditorCameraLayer } from "./SceneEditor/SceneEditorGameraLayer";
|
|||||||
import { SceneEditorObjectSelectionLayer } from "./SceneEditor/SceneEditorObjectSelectionLayer";
|
import { SceneEditorObjectSelectionLayer } from "./SceneEditor/SceneEditorObjectSelectionLayer";
|
||||||
import { LightsLayer } from "./LighsLayer";
|
import { LightsLayer } from "./LighsLayer";
|
||||||
import { StatsLayer } from './StatsLayer';
|
import { StatsLayer } from './StatsLayer';
|
||||||
|
import { SceneEditorObjectGizmosLayer } from "./SceneEditor/SceneEditorObjectGizmosLayer";
|
||||||
|
|
||||||
export const LayeredThreeView = observer(function () {
|
export const LayeredThreeView = observer(function () {
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ export const LayeredThreeView = observer(function () {
|
|||||||
<SceneEditorCameraLayer />
|
<SceneEditorCameraLayer />
|
||||||
<ObjectGeometriesLayer>
|
<ObjectGeometriesLayer>
|
||||||
<SceneEditorObjectSelectionLayer />
|
<SceneEditorObjectSelectionLayer />
|
||||||
|
<SceneEditorObjectGizmosLayer />
|
||||||
</ObjectGeometriesLayer>
|
</ObjectGeometriesLayer>
|
||||||
<StatsLayer />
|
<StatsLayer />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|||||||
@@ -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 <TransformControls
|
||||||
|
mode={selection.mode}
|
||||||
|
position={obj.position}
|
||||||
|
rotation={obj.rotation}
|
||||||
|
scale={obj.scale}
|
||||||
|
onObjectChange={onObjectChange}
|
||||||
|
/>;
|
||||||
|
},
|
||||||
|
));
|
||||||
@@ -1,37 +1,52 @@
|
|||||||
import { useEffect, useCallback } from "react";
|
import { useEffect, useCallback, useRef } from "react";
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { state } from "../../../state";
|
import { state } from "../../../state";
|
||||||
|
import { nextObjectEditMode } from "../../../state/worldEditorState";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { SelectionBoundingBox } from "./SelectionBoundingBox";
|
import { SelectionBoundingBox } from "./SelectionBoundingBox";
|
||||||
import { measuredRenderTime } from "../../../utils/layerTimings";
|
import { measuredRenderTime } from "../../../utils/layerTimings";
|
||||||
import { useObjectGeometryList } from "../ObjectGeometriesLayer";
|
import { useObjectGeometryList } from "../ObjectGeometriesLayer";
|
||||||
|
|
||||||
|
const DRAG_THRESHOLD = 4;
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
const pointer = new THREE.Vector2();
|
const pointer = new THREE.Vector2();
|
||||||
|
|
||||||
export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
|
export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
|
||||||
"ObjectSelection",
|
"ObjectSelection",
|
||||||
function () {
|
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 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) => {
|
const handlePointerDown = useCallback((e: PointerEvent) => {
|
||||||
if (e.button !== 0)
|
if (e.button !== 0)
|
||||||
return;
|
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.x = (e.offsetX / size.width) * 2 - 1;
|
||||||
pointer.y = -(e.offsetY / size.height) * 2 + 1;
|
pointer.y = -(e.offsetY / size.height) * 2 + 1;
|
||||||
|
|
||||||
raycaster.setFromCamera(pointer, camera);
|
raycaster.setFromCamera(pointer, camera);
|
||||||
|
|
||||||
const instancedMeshes = scene.children.filter(
|
const instancedMeshes = state.view.objectTypeGeometries.values.filter((ot) => ot.meshes?.length).flatMap((ot) => ot.meshes!);
|
||||||
(child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh,
|
|
||||||
);
|
|
||||||
|
|
||||||
const intersections = raycaster.intersectObjects(instancedMeshes, false);
|
const intersections = raycaster.intersectObjects(instancedMeshes, false);
|
||||||
|
|
||||||
@@ -41,9 +56,8 @@ export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hit = intersections[0];
|
const hit = intersections[0];
|
||||||
const mesh = hit.object as THREE.InstancedMesh;
|
|
||||||
const instanceId = hit.instanceId;
|
|
||||||
|
|
||||||
|
const instanceId = hit.instanceId;
|
||||||
if (instanceId === undefined) {
|
if (instanceId === undefined) {
|
||||||
state.worldEditor.resetSelection();
|
state.worldEditor.resetSelection();
|
||||||
return;
|
return;
|
||||||
@@ -53,21 +67,28 @@ export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
|
|||||||
if (!geometryList)
|
if (!geometryList)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const objectId = geometryList.getObjectIdByMesh(mesh, instanceId);
|
const mesh = hit.object as THREE.InstancedMesh;
|
||||||
if (objectId) {
|
const instanceGeometry = geometryList.getObjectGeometryByMesh(mesh, instanceId);
|
||||||
state.worldEditor.setSelectedObject({ id: objectId, editMode: "translate" });
|
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 {
|
} else {
|
||||||
state.worldEditor.resetSelection();
|
state.worldEditor.resetSelection();
|
||||||
}
|
}
|
||||||
}, [camera, scene, size, geometryListRef]);
|
}, [camera, size, geometryListRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const domElement = gl.domElement;
|
const domElement = gl.domElement;
|
||||||
domElement.addEventListener("pointerdown", handlePointerDown);
|
domElement.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
domElement.addEventListener("pointerup", handlePointerUp);
|
||||||
return () => {
|
return () => {
|
||||||
domElement.removeEventListener("pointerdown", handlePointerDown);
|
domElement.removeEventListener("pointerdown", handlePointerDown);
|
||||||
|
domElement.removeEventListener("pointerup", handlePointerUp);
|
||||||
};
|
};
|
||||||
}, [gl, handlePointerDown]);
|
}, [gl, handlePointerDown, handlePointerUp]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{selection &&
|
{selection &&
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import type { ObjectGeometryViewItem } from "../../../state/viewLayers";
|
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 obj = item.object;
|
||||||
const bb = item.objectTypeGeometry.boundingBox;
|
const bb = item.objectTypeGeometry.boundingBox;
|
||||||
|
|
||||||
@@ -41,4 +42,4 @@ export const SelectionBoundingBox = function({ item }: { item: ObjectGeometryVie
|
|||||||
<lineBasicMaterial color="#ffff00" />
|
<lineBasicMaterial color="#ffff00" />
|
||||||
</lineSegments>
|
</lineSegments>
|
||||||
</mesh>;
|
</mesh>;
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { MobxReconcilerMap, type ReconcilerMap } from '../utils/reconciler';
|
import { MobxReconcilerMap, type ReconcilerMap } from '../utils/reconciler';
|
||||||
import type { ObjectTypeGeometryViewItem } from '../state/viewLayers';
|
import type { ObjectGeometryViewItem, ObjectTypeGeometryViewItem } from '../state/viewLayers';
|
||||||
|
|
||||||
export class ObjectGeometryList {
|
export class ObjectGeometryList {
|
||||||
private items: ReconcilerMap<ObjectTypeGeometryViewItem>;
|
private items: ReconcilerMap<ObjectTypeGeometryViewItem>;
|
||||||
|
|
||||||
|
private readonly instancesByMesh = new Map<THREE.InstancedMesh, ObjectGeometryViewItem[]>();
|
||||||
|
|
||||||
private readonly matrixObj = new THREE.Object3D();
|
private readonly matrixObj = new THREE.Object3D();
|
||||||
private readonly _p = new THREE.Vector3();
|
private readonly _p = new THREE.Vector3();
|
||||||
private readonly _e = new THREE.Euler();
|
private readonly _e = new THREE.Euler();
|
||||||
private readonly _q = new THREE.Quaternion();
|
private readonly _q = new THREE.Quaternion();
|
||||||
|
|
||||||
private readonly _meshToObjectIds = new Map<THREE.InstancedMesh, string[]>();
|
|
||||||
|
|
||||||
constructor(scene: THREE.Scene, typeGeometries: Iterable<ObjectTypeGeometryViewItem>) {
|
constructor(scene: THREE.Scene, typeGeometries: Iterable<ObjectTypeGeometryViewItem>) {
|
||||||
this.items = new MobxReconcilerMap<ObjectTypeGeometryViewItem>({
|
this.items = new MobxReconcilerMap<ObjectTypeGeometryViewItem>({
|
||||||
reaction: () => typeGeometries,
|
reaction: () => typeGeometries,
|
||||||
@@ -31,17 +31,16 @@ export class ObjectGeometryList {
|
|||||||
this.items?.dispose();
|
this.items?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectIdByMesh(mesh: THREE.InstancedMesh, instanceId: number): string | undefined {
|
getObjectGeometryByMesh(mesh: THREE.InstancedMesh, instanceId: number): ObjectGeometryViewItem | undefined {
|
||||||
return this._meshToObjectIds.get(mesh)?.[instanceId];
|
return this.instancesByMesh.get(mesh)?.[instanceId];
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(): void {
|
tick(): void {
|
||||||
this._meshToObjectIds.clear();
|
this.instancesByMesh.clear();
|
||||||
|
|
||||||
this.items.values.forEach((typeGeometry) => {
|
this.items.values.forEach((typeGeometry) => {
|
||||||
const instances = typeGeometry.instances;
|
const instances = typeGeometry.instances;
|
||||||
const meshes = typeGeometry.meshes ?? [];
|
const meshes = typeGeometry.meshes ?? [];
|
||||||
const objectIds: string[] = [];
|
|
||||||
|
|
||||||
instances.forEach((item, instanceIdx) => {
|
instances.forEach((item, instanceIdx) => {
|
||||||
const obj = item.object;
|
const obj = item.object;
|
||||||
@@ -54,14 +53,12 @@ export class ObjectGeometryList {
|
|||||||
this.matrixObj.scale.set(...obj.scale);
|
this.matrixObj.scale.set(...obj.scale);
|
||||||
this.matrixObj.updateMatrix();
|
this.matrixObj.updateMatrix();
|
||||||
|
|
||||||
objectIds[instanceIdx] = obj.id;
|
|
||||||
|
|
||||||
for (const mesh of meshes)
|
for (const mesh of meshes)
|
||||||
mesh.setMatrixAt(instanceIdx, this.matrixObj.matrix);
|
mesh.setMatrixAt(instanceIdx, this.matrixObj.matrix);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mesh of meshes) {
|
for (const mesh of meshes) {
|
||||||
this._meshToObjectIds.set(mesh, objectIds);
|
this.instancesByMesh.set(mesh, typeGeometry.instances);
|
||||||
mesh.count = instances.length;
|
mesh.count = instances.length;
|
||||||
mesh.instanceMatrix.needsUpdate = true;
|
mesh.instanceMatrix.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { ViewItem } from "./base";
|
||||||
import type { ObjectTypeGeometryViewItem } from './objectTypeGeometry';
|
import type { ObjectTypeGeometryViewItem } from './objectTypeGeometry';
|
||||||
|
|
||||||
@@ -13,4 +14,8 @@ export class ObjectGeometryViewItem extends ViewItem {
|
|||||||
this.object = object;
|
this.object = object;
|
||||||
this.objectTypeGeometry = objectTypeGeometry;
|
this.objectTypeGeometry = objectTypeGeometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setObjectTransform(position: V3, rotation: V3, scale: V3) {
|
||||||
|
state.worldEditor.setObjectTransform(this.object.id, position, rotation, scale);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user