object selection
This commit is contained in:
@@ -2,6 +2,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { ObjectGeometriesLayer } from "./ObjectGeometriesLayer";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { SceneEditorCameraLayer } from "./SceneEditor/SceneEditorGameraLayer";
|
||||
import { SceneEditorObjectSelectionLayer } from "./SceneEditor/SceneEditorObjectSelectionLayer";
|
||||
import { LightsLayer } from "./LighsLayer";
|
||||
import { StatsLayer } from './StatsLayer';
|
||||
|
||||
@@ -12,7 +13,9 @@ export const LayeredThreeView = observer(function () {
|
||||
<LightsLayer />
|
||||
{/* <Perf position="bottom-right" /> */}
|
||||
<SceneEditorCameraLayer />
|
||||
<ObjectGeometriesLayer />
|
||||
<ObjectGeometriesLayer>
|
||||
<SceneEditorObjectSelectionLayer />
|
||||
</ObjectGeometriesLayer>
|
||||
<StatsLayer />
|
||||
</Canvas>
|
||||
</>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { createContext, useContext, useEffect, useRef } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { ObjectGeometryList } from "../../rendering/ObjectGeometryList";
|
||||
import { observer } from "mobx-react-lite";
|
||||
@@ -6,22 +6,32 @@ import { state } from "../../state";
|
||||
import { useLayerFrame } from "../../utils/hooks/useLayerFrame";
|
||||
import { measuredRenderTime } from "../../utils/layerTimings";
|
||||
|
||||
export const ObjectGeometriesLayer = observer(measuredRenderTime("ObjectGeometries", function () {
|
||||
const geometries = state.view.objectGeometries;
|
||||
export const ObjectGeometryListContext = createContext<React.RefObject<ObjectGeometryList | null>>(
|
||||
{ current: null },
|
||||
);
|
||||
|
||||
export function useObjectGeometryList() {
|
||||
return useContext(ObjectGeometryListContext);
|
||||
}
|
||||
|
||||
export const ObjectGeometriesLayer = observer(measuredRenderTime("ObjectGeometries", function ({ children }: { children?: React.ReactNode }) {
|
||||
const typeGeometries = state.view.objectTypeGeometries;
|
||||
|
||||
const threeScene = useThree(s => s.scene);
|
||||
const list = useRef<ObjectGeometryList | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const l = new ObjectGeometryList(threeScene, geometries);
|
||||
const l = new ObjectGeometryList(threeScene, typeGeometries);
|
||||
list.current = l;
|
||||
return () => {
|
||||
l.dispose();
|
||||
list.current = null;
|
||||
};
|
||||
}, [threeScene, geometries]);
|
||||
}, [threeScene, typeGeometries]);
|
||||
|
||||
useLayerFrame("ObjectGeometries", () => list.current?.tick());
|
||||
|
||||
return null;
|
||||
return <ObjectGeometryListContext.Provider value={list}>
|
||||
{children}
|
||||
</ObjectGeometryListContext.Provider>;
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMemo } from "react";
|
||||
import { state } from "../../../state";
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import type { ObjectGeometryViewItem } from "../../../state/viewLayers";
|
||||
|
||||
export function ObjectClickTarget({ item }: { item: ObjectGeometryViewItem }) {
|
||||
const obj = item.object;
|
||||
const bb = item.objectTypeGeometry.boundingBox;
|
||||
|
||||
const size = useMemo(() => {
|
||||
if (!bb)
|
||||
return [1, 1, 1] as const;
|
||||
|
||||
return [bb.max[0] - bb.min[0], bb.max[1] - bb.min[1], bb.max[2] - bb.min[2]] as const;
|
||||
}, [bb]);
|
||||
|
||||
const center = useMemo(() => {
|
||||
if (!bb)
|
||||
return [0.5, 0.5, 0.5] as const;
|
||||
|
||||
return [
|
||||
(bb.min[0] + bb.max[0]) / 2,
|
||||
(bb.min[1] + bb.max[1]) / 2,
|
||||
(bb.min[2] + bb.max[2]) / 2,
|
||||
] as const;
|
||||
}, [bb]);
|
||||
|
||||
const handleClick = (e: ThreeEvent<MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
state.worldEditor.setSelectedObject({ id: obj.id, editMode: "translate" });
|
||||
};
|
||||
|
||||
return <mesh
|
||||
position={obj.position}
|
||||
rotation={obj.rotation}
|
||||
scale={obj.scale}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<mesh position={center}>
|
||||
<boxGeometry args={size} />
|
||||
<meshBasicMaterial visible={false} />
|
||||
</mesh>
|
||||
</mesh>;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import { state } from "../../../state";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { SelectionBoundingBox } from "./SelectionBoundingBox";
|
||||
import { measuredRenderTime } from "../../../utils/layerTimings";
|
||||
import { useObjectGeometryList } from "../ObjectGeometriesLayer";
|
||||
|
||||
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 handlePointerDown = useCallback((e: PointerEvent) => {
|
||||
if (e.button !== 0)
|
||||
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 intersections = raycaster.intersectObjects(instancedMeshes, false);
|
||||
|
||||
if (intersections.length === 0) {
|
||||
state.worldEditor.resetSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
const hit = intersections[0];
|
||||
const mesh = hit.object as THREE.InstancedMesh;
|
||||
const instanceId = hit.instanceId;
|
||||
|
||||
if (instanceId === undefined) {
|
||||
state.worldEditor.resetSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
const geometryList = geometryListRef.current;
|
||||
if (!geometryList)
|
||||
return;
|
||||
|
||||
const objectId = geometryList.getObjectIdByMesh(mesh, instanceId);
|
||||
if (objectId) {
|
||||
state.worldEditor.setSelectedObject({ id: objectId, editMode: "translate" });
|
||||
} else {
|
||||
state.worldEditor.resetSelection();
|
||||
}
|
||||
}, [camera, scene, size, geometryListRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const domElement = gl.domElement;
|
||||
domElement.addEventListener("pointerdown", handlePointerDown);
|
||||
return () => {
|
||||
domElement.removeEventListener("pointerdown", handlePointerDown);
|
||||
};
|
||||
}, [gl, handlePointerDown]);
|
||||
|
||||
return <>
|
||||
{selection &&
|
||||
<SelectionBoundingBox item={selection.geometry} />
|
||||
}
|
||||
</>;
|
||||
},
|
||||
));
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import type { ObjectGeometryViewItem } from "../../../state/viewLayers";
|
||||
|
||||
export const SelectionBoundingBox = function({ item }: { item: ObjectGeometryViewItem }) {
|
||||
const obj = item.object;
|
||||
const bb = item.objectTypeGeometry.boundingBox;
|
||||
|
||||
const geometry = useMemo(() => {
|
||||
if (!bb)
|
||||
return undefined;
|
||||
|
||||
const box = new THREE.BoxGeometry(
|
||||
bb.max[0] - bb.min[0],
|
||||
bb.max[1] - bb.min[1],
|
||||
bb.max[2] - bb.min[2],
|
||||
);
|
||||
return new THREE.EdgesGeometry(box);
|
||||
}, [bb]);
|
||||
|
||||
const center = useMemo(() => {
|
||||
if (!bb)
|
||||
return [0.5, 0.5, 0.5] as const;
|
||||
|
||||
return [
|
||||
(bb.min[0] + bb.max[0]) / 2,
|
||||
(bb.min[1] + bb.max[1]) / 2,
|
||||
(bb.min[2] + bb.max[2]) / 2,
|
||||
] as const;
|
||||
}, [bb]);
|
||||
|
||||
if (!geometry)
|
||||
return null;
|
||||
|
||||
return <mesh
|
||||
position={obj.position}
|
||||
rotation={obj.rotation}
|
||||
scale={obj.scale}
|
||||
>
|
||||
<lineSegments position={center} geometry={geometry}>
|
||||
<lineBasicMaterial color="#ffff00" />
|
||||
</lineSegments>
|
||||
</mesh>;
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export const StatsLayer = function () {
|
||||
bottom: "8px",
|
||||
right: "8px",
|
||||
color: "#0f0",
|
||||
fontSize: "14px",
|
||||
fontSize: "10px",
|
||||
fontFamily: "monospace",
|
||||
zIndex: "9999",
|
||||
pointerEvents: "none",
|
||||
@@ -50,15 +50,10 @@ export const StatsLayer = function () {
|
||||
|
||||
for (const [name, entry] of Object.entries(layerTimings)) {
|
||||
const parts: string[] = [];
|
||||
if (entry.frame.frames > 0) {
|
||||
parts.push(`frame ${(entry.frame.acc / entry.frame.frames).toFixed(3)}`);
|
||||
}
|
||||
if (entry.render.frames > 0) {
|
||||
parts.push(`render ${(entry.render.acc / entry.render.frames).toFixed(3)}`);
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
lines.push(`${name} ${parts.join(" ")} ms`);
|
||||
}
|
||||
parts.push(`frame ${(entry.frame.frames > 0 ? (entry.frame.acc / entry.frame.frames) : 0).toFixed(3)}`);
|
||||
parts.push(`render ${(entry.render.frames > 0 ? (entry.render.acc / entry.render.frames) : 0).toFixed(3)}`);
|
||||
lines.push(`${name} ${parts.join(" ")} ms`);
|
||||
|
||||
entry.frame.acc = 0;
|
||||
entry.frame.frames = 0;
|
||||
entry.render.acc = 0;
|
||||
|
||||
Reference in New Issue
Block a user