feat(editor): focus camera on selected object
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled

This commit is contained in:
tom-boullay
2026-05-27 11:58:33 +02:00
parent 4ee13b0336
commit bfe184dea4
4 changed files with 67 additions and 1 deletions
+8
View File
@@ -11,6 +11,7 @@ import {
Redo2, Redo2,
RotateCw, RotateCw,
Save, Save,
ScanSearch,
Undo2, Undo2,
Unlock, Unlock,
X, X,
@@ -37,6 +38,7 @@ interface EditorControlsProps {
redoCount: number; redoCount: number;
onUndo: () => void; onUndo: () => void;
onRedo: () => void; onRedo: () => void;
onResetCamera: () => void;
onExportJson: () => void; onExportJson: () => void;
onSaveToServer?: (() => void | Promise<void>) | undefined; onSaveToServer?: (() => void | Promise<void>) | undefined;
onPlayerMode?: (() => void) | undefined; onPlayerMode?: (() => void) | undefined;
@@ -101,6 +103,7 @@ export function EditorControls({
redoCount, redoCount,
onUndo, onUndo,
onRedo, onRedo,
onResetCamera,
onExportJson, onExportJson,
onSaveToServer, onSaveToServer,
onPlayerMode, onPlayerMode,
@@ -268,6 +271,11 @@ export function EditorControls({
</button> </button>
)} )}
<button className="editor-action-button" onClick={onResetCamera}>
<ScanSearch size={16} aria-hidden="true" />
Reset camera
</button>
<label className="editor-checkbox-row"> <label className="editor-checkbox-row">
<input <input
type="checkbox" type="checkbox"
@@ -3,11 +3,15 @@ import { OrbitControls } from "@react-three/drei";
import { useThree } from "@react-three/fiber"; import { useThree } from "@react-three/fiber";
import gsap from "gsap"; import gsap from "gsap";
import * as THREE from "three"; import * as THREE from "three";
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
import { EditorMap } from "@/components/editor/scene/EditorMap"; import { EditorMap } from "@/components/editor/scene/EditorMap";
import { FlyController } from "@/controls/editor/FlyController"; import { FlyController } from "@/controls/editor/FlyController";
import type { CinematicDefinition } from "@/types/cinematics/cinematics"; import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor"; import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor";
const EDITOR_CAMERA_HOME_POSITION = new THREE.Vector3(0, 50, 100);
const EDITOR_CAMERA_HOME_TARGET = new THREE.Vector3(0, 0, 0);
export interface EditorCinematicPreviewRequest { export interface EditorCinematicPreviewRequest {
id: string; id: string;
cinematic: CinematicDefinition; cinematic: CinematicDefinition;
@@ -28,6 +32,7 @@ interface EditorSceneProps {
onNodeTransform: (nodeIndex: number, transform: MapNode) => void; onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
onUndo: () => void; onUndo: () => void;
onRedo: () => void; onRedo: () => void;
resetCameraRequest: number;
isPlayerMode?: boolean; isPlayerMode?: boolean;
cinematicPreviewRequest?: EditorCinematicPreviewRequest | null; cinematicPreviewRequest?: EditorCinematicPreviewRequest | null;
onCinematicPreviewComplete?: (() => void) | undefined; onCinematicPreviewComplete?: (() => void) | undefined;
@@ -48,11 +53,55 @@ export function EditorScene({
onNodeTransform, onNodeTransform,
onUndo, onUndo,
onRedo, onRedo,
resetCameraRequest,
isPlayerMode = false, isPlayerMode = false,
cinematicPreviewRequest = null, cinematicPreviewRequest = null,
onCinematicPreviewComplete, onCinematicPreviewComplete,
}: EditorSceneProps): React.JSX.Element { }: EditorSceneProps): React.JSX.Element {
const isCinematicPreviewing = cinematicPreviewRequest !== null; const isCinematicPreviewing = cinematicPreviewRequest !== null;
const camera = useThree((state) => state.camera);
const orbitControlsRef = useRef<OrbitControlsImpl | null>(null);
const previousSelectedNodeIndexRef = useRef<number | null>(null);
useEffect(() => {
if (selectedNodeIndex === previousSelectedNodeIndexRef.current) return;
previousSelectedNodeIndexRef.current = selectedNodeIndex;
if (selectedNodeIndex === null || isPlayerMode || isCinematicPreviewing) {
return;
}
const selectedNode = sceneData.mapNodes[selectedNodeIndex];
if (!selectedNode) return;
const controls = orbitControlsRef.current;
const target = new THREE.Vector3(...selectedNode.position);
const currentTarget = controls?.target ?? EDITOR_CAMERA_HOME_TARGET;
const cameraOffset = camera.position.clone().sub(currentTarget);
camera.position.copy(target).add(cameraOffset);
camera.lookAt(target);
controls?.target.copy(target);
controls?.update();
}, [
camera,
isCinematicPreviewing,
isPlayerMode,
sceneData,
selectedNodeIndex,
]);
useEffect(() => {
if (resetCameraRequest === 0 || isPlayerMode || isCinematicPreviewing) {
return;
}
const controls = orbitControlsRef.current;
camera.position.copy(EDITOR_CAMERA_HOME_POSITION);
camera.lookAt(EDITOR_CAMERA_HOME_TARGET);
controls?.target.copy(EDITOR_CAMERA_HOME_TARGET);
controls?.update();
}, [camera, isCinematicPreviewing, isPlayerMode, resetCameraRequest]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -109,6 +158,7 @@ export function EditorScene({
<FlyController disabled={isCinematicPreviewing} /> <FlyController disabled={isCinematicPreviewing} />
) : ( ) : (
<OrbitControls <OrbitControls
ref={orbitControlsRef}
enabled={!isCinematicPreviewing} enabled={!isCinematicPreviewing}
enableDamping enableDamping
dampingFactor={0.05} dampingFactor={0.05}
+2 -1
View File
@@ -1371,7 +1371,8 @@ canvas {
transform 160ms ease; transform 160ms ease;
} }
.editor-action-button + .editor-action-button { .editor-action-button + .editor-action-button,
.editor-player-button + .editor-action-button {
margin-top: 8px; margin-top: 8px;
} }
+7
View File
@@ -142,6 +142,7 @@ export function EditorPage(): React.JSX.Element {
const [isPlayerMode, setIsPlayerMode] = useState(false); const [isPlayerMode, setIsPlayerMode] = useState(false);
const [isSelectionLocked, setIsSelectionLocked] = useState(false); const [isSelectionLocked, setIsSelectionLocked] = useState(false);
const [lockTerrainSelection, setLockTerrainSelection] = useState(true); const [lockTerrainSelection, setLockTerrainSelection] = useState(true);
const [resetCameraRequest, setResetCameraRequest] = useState(0);
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>( const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
{ {
...INITIAL_SCENE_LOADING_STATE, ...INITIAL_SCENE_LOADING_STATE,
@@ -257,6 +258,10 @@ export function EditorPage(): React.JSX.Element {
setIsPlayerMode((prev) => !prev); setIsPlayerMode((prev) => !prev);
}, []); }, []);
const handleResetCamera = useCallback(() => {
setResetCameraRequest((request) => request + 1);
}, []);
const handlePreviewCinematic = useCallback( const handlePreviewCinematic = useCallback(
(cinematic: CinematicDefinition) => { (cinematic: CinematicDefinition) => {
setCinematicPreviewRequest({ setCinematicPreviewRequest({
@@ -375,6 +380,7 @@ export function EditorPage(): React.JSX.Element {
onNodeTransform={handleNodeTransform} onNodeTransform={handleNodeTransform}
onUndo={handleUndo} onUndo={handleUndo}
onRedo={handleRedo} onRedo={handleRedo}
resetCameraRequest={resetCameraRequest}
isPlayerMode={isPlayerMode} isPlayerMode={isPlayerMode}
cinematicPreviewRequest={cinematicPreviewRequest} cinematicPreviewRequest={cinematicPreviewRequest}
onCinematicPreviewComplete={handleCinematicPreviewComplete} onCinematicPreviewComplete={handleCinematicPreviewComplete}
@@ -405,6 +411,7 @@ export function EditorPage(): React.JSX.Element {
redoCount={redoCount} redoCount={redoCount}
onUndo={handleUndo} onUndo={handleUndo}
onRedo={handleRedo} onRedo={handleRedo}
onResetCamera={handleResetCamera}
onExportJson={handleExportJson} onExportJson={handleExportJson}
onSaveToServer={import.meta.env.DEV ? handleSaveToServer : undefined} onSaveToServer={import.meta.env.DEV ? handleSaveToServer : undefined}
onPlayerMode={handlePlayerMode} onPlayerMode={handlePlayerMode}