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
🔍 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:
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user