fix(editor): update transforms while dragging
🔍 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:
@@ -38,7 +38,8 @@ interface EditorControlsProps {
|
|||||||
redoCount: number;
|
redoCount: number;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
onRedo: () => void;
|
onRedo: () => void;
|
||||||
onResetCamera: () => void;
|
cameraActionLabel: string;
|
||||||
|
onCameraAction: () => void;
|
||||||
onExportJson: () => void;
|
onExportJson: () => void;
|
||||||
onSaveToServer?: (() => void | Promise<void>) | undefined;
|
onSaveToServer?: (() => void | Promise<void>) | undefined;
|
||||||
onPlayerMode?: (() => void) | undefined;
|
onPlayerMode?: (() => void) | undefined;
|
||||||
@@ -103,7 +104,8 @@ export function EditorControls({
|
|||||||
redoCount,
|
redoCount,
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
onResetCamera,
|
cameraActionLabel,
|
||||||
|
onCameraAction,
|
||||||
onExportJson,
|
onExportJson,
|
||||||
onSaveToServer,
|
onSaveToServer,
|
||||||
onPlayerMode,
|
onPlayerMode,
|
||||||
@@ -271,9 +273,9 @@ export function EditorControls({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button className="editor-action-button" onClick={onResetCamera}>
|
<button className="editor-action-button" onClick={onCameraAction}>
|
||||||
<ScanSearch size={16} aria-hidden="true" />
|
<ScanSearch size={16} aria-hidden="true" />
|
||||||
Reset camera
|
{cameraActionLabel}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label className="editor-checkbox-row">
|
<label className="editor-checkbox-row">
|
||||||
|
|||||||
@@ -6,16 +6,11 @@ import * as THREE from "three";
|
|||||||
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import {
|
|
||||||
normalizeMapScale,
|
|
||||||
useTerrainSnappedPosition,
|
|
||||||
} from "@/hooks/three/useTerrainHeight";
|
|
||||||
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
||||||
import {
|
import {
|
||||||
isEditorVisibleMapNode,
|
isEditorVisibleMapNode,
|
||||||
getTerrainMapNode,
|
getTerrainMapNode,
|
||||||
} from "@/utils/map/mapRuntimeClassification";
|
} from "@/utils/map/mapRuntimeClassification";
|
||||||
import { getVegetationScaleMultiplier } from "@/world/vegetation/vegetationConfig";
|
|
||||||
|
|
||||||
interface EditorMapProps {
|
interface EditorMapProps {
|
||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
@@ -161,6 +156,11 @@ export function EditorMap({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTransformMouseUp = () => {
|
const handleTransformMouseUp = () => {
|
||||||
|
syncSelectedObjectTransform();
|
||||||
|
onTransformEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncSelectedObjectTransform = () => {
|
||||||
if (selectedNodeIndex !== null) {
|
if (selectedNodeIndex !== null) {
|
||||||
const obj = objectsMapRef.current.get(selectedNodeIndex);
|
const obj = objectsMapRef.current.get(selectedNodeIndex);
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
@@ -175,7 +175,6 @@ export function EditorMap({
|
|||||||
onNodeTransform(selectedNodeIndex, updatedNode);
|
onNodeTransform(selectedNodeIndex, updatedNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onTransformEnd();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [selectedObject, setSelectedObject] = useState<THREE.Object3D | null>(
|
const [selectedObject, setSelectedObject] = useState<THREE.Object3D | null>(
|
||||||
@@ -279,6 +278,7 @@ export function EditorMap({
|
|||||||
mode={transformMode}
|
mode={transformMode}
|
||||||
onMouseDown={handleTransformMouseDown}
|
onMouseDown={handleTransformMouseDown}
|
||||||
onMouseUp={handleTransformMouseUp}
|
onMouseUp={handleTransformMouseUp}
|
||||||
|
onObjectChange={syncSelectedObjectTransform}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -299,23 +299,14 @@ function EditorModelNode({
|
|||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const snappedPosition = useTerrainSnappedPosition(node.position);
|
|
||||||
const vegetationScaleMultiplier = getVegetationScaleMultiplier(node.name);
|
|
||||||
const normalizedScale = vegetationScaleMultiplier
|
|
||||||
? ([
|
|
||||||
vegetationScaleMultiplier,
|
|
||||||
vegetationScaleMultiplier,
|
|
||||||
vegetationScaleMultiplier,
|
|
||||||
] satisfies MapNode["scale"])
|
|
||||||
: normalizeMapScale(node.scale);
|
|
||||||
const originalMaterialsRef = useRef(
|
const originalMaterialsRef = useRef(
|
||||||
new Map<THREE.Mesh, THREE.Material | THREE.Material[]>(),
|
new Map<THREE.Mesh, THREE.Material | THREE.Material[]>(),
|
||||||
);
|
);
|
||||||
const { scene } = useLoggedGLTF(modelUrl, {
|
const { scene } = useLoggedGLTF(modelUrl, {
|
||||||
scope: "EditorMap.EditorModelNode",
|
scope: "EditorMap.EditorModelNode",
|
||||||
position: snappedPosition,
|
position: node.position,
|
||||||
rotation: node.rotation,
|
rotation: node.rotation,
|
||||||
scale: normalizedScale,
|
scale: node.scale,
|
||||||
});
|
});
|
||||||
const sceneInstance = useClonedObject(scene);
|
const sceneInstance = useClonedObject(scene);
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
@@ -324,16 +315,7 @@ function EditorModelNode({
|
|||||||
isSelectionLocked,
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(
|
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
||||||
groupRef,
|
|
||||||
index,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
position: snappedPosition,
|
|
||||||
scale: normalizedScale,
|
|
||||||
},
|
|
||||||
objectsMapRef,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!groupRef.current) return;
|
if (!groupRef.current) return;
|
||||||
@@ -390,9 +372,9 @@ function EditorModelNode({
|
|||||||
<primitive
|
<primitive
|
||||||
ref={groupRef}
|
ref={groupRef}
|
||||||
object={sceneInstance}
|
object={sceneInstance}
|
||||||
position={snappedPosition}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={normalizedScale}
|
scale={node.scale}
|
||||||
{...pointerHandlers}
|
{...pointerHandlers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -440,33 +422,22 @@ function EditorFallbackNode({
|
|||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps) {
|
}: EditorNodeCommonProps) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
const snappedPosition = useTerrainSnappedPosition(node.position);
|
|
||||||
const normalizedScale = normalizeMapScale(node.scale);
|
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
isSelectionLocked,
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(
|
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
||||||
meshRef,
|
|
||||||
index,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
position: snappedPosition,
|
|
||||||
scale: normalizedScale,
|
|
||||||
},
|
|
||||||
objectsMapRef,
|
|
||||||
);
|
|
||||||
|
|
||||||
const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
|
const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
ref={meshRef}
|
ref={meshRef}
|
||||||
position={snappedPosition}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={normalizedScale}
|
scale={node.scale}
|
||||||
{...pointerHandlers}
|
{...pointerHandlers}
|
||||||
>
|
>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { OrbitControls } from "@react-three/drei";
|
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";
|
||||||
@@ -33,6 +33,7 @@ interface EditorSceneProps {
|
|||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
onRedo: () => void;
|
onRedo: () => void;
|
||||||
resetCameraRequest: number;
|
resetCameraRequest: number;
|
||||||
|
focusSelectedCameraRequest: number;
|
||||||
isPlayerMode?: boolean;
|
isPlayerMode?: boolean;
|
||||||
cinematicPreviewRequest?: EditorCinematicPreviewRequest | null;
|
cinematicPreviewRequest?: EditorCinematicPreviewRequest | null;
|
||||||
onCinematicPreviewComplete?: (() => void) | undefined;
|
onCinematicPreviewComplete?: (() => void) | undefined;
|
||||||
@@ -54,6 +55,7 @@ export function EditorScene({
|
|||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
resetCameraRequest,
|
resetCameraRequest,
|
||||||
|
focusSelectedCameraRequest,
|
||||||
isPlayerMode = false,
|
isPlayerMode = false,
|
||||||
cinematicPreviewRequest = null,
|
cinematicPreviewRequest = null,
|
||||||
onCinematicPreviewComplete,
|
onCinematicPreviewComplete,
|
||||||
@@ -63,6 +65,21 @@ export function EditorScene({
|
|||||||
const orbitControlsRef = useRef<OrbitControlsImpl | null>(null);
|
const orbitControlsRef = useRef<OrbitControlsImpl | null>(null);
|
||||||
const previousSelectedNodeIndexRef = useRef<number | null>(null);
|
const previousSelectedNodeIndexRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const focusCameraOnNode = useCallback(
|
||||||
|
(node: MapNode): void => {
|
||||||
|
const controls = orbitControlsRef.current;
|
||||||
|
const target = new THREE.Vector3(...node.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],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedNodeIndex === previousSelectedNodeIndexRef.current) return;
|
if (selectedNodeIndex === previousSelectedNodeIndexRef.current) return;
|
||||||
previousSelectedNodeIndexRef.current = selectedNodeIndex;
|
previousSelectedNodeIndexRef.current = selectedNodeIndex;
|
||||||
@@ -74,19 +91,35 @@ export function EditorScene({
|
|||||||
const selectedNode = sceneData.mapNodes[selectedNodeIndex];
|
const selectedNode = sceneData.mapNodes[selectedNodeIndex];
|
||||||
if (!selectedNode) return;
|
if (!selectedNode) return;
|
||||||
|
|
||||||
const controls = orbitControlsRef.current;
|
focusCameraOnNode(selectedNode);
|
||||||
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,
|
camera,
|
||||||
isCinematicPreviewing,
|
isCinematicPreviewing,
|
||||||
isPlayerMode,
|
isPlayerMode,
|
||||||
|
focusCameraOnNode,
|
||||||
|
sceneData,
|
||||||
|
selectedNodeIndex,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
focusSelectedCameraRequest === 0 ||
|
||||||
|
selectedNodeIndex === null ||
|
||||||
|
isPlayerMode ||
|
||||||
|
isCinematicPreviewing
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedNode = sceneData.mapNodes[selectedNodeIndex];
|
||||||
|
if (!selectedNode) return;
|
||||||
|
|
||||||
|
focusCameraOnNode(selectedNode);
|
||||||
|
}, [
|
||||||
|
focusSelectedCameraRequest,
|
||||||
|
focusCameraOnNode,
|
||||||
|
isCinematicPreviewing,
|
||||||
|
isPlayerMode,
|
||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -143,6 +143,11 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
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 [resetCameraRequest, setResetCameraRequest] = useState(0);
|
||||||
|
const [focusSelectedCameraRequest, setFocusSelectedCameraRequest] =
|
||||||
|
useState(0);
|
||||||
|
const [cameraViewMode, setCameraViewMode] = useState<"home" | "object">(
|
||||||
|
"home",
|
||||||
|
);
|
||||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||||
{
|
{
|
||||||
...INITIAL_SCENE_LOADING_STATE,
|
...INITIAL_SCENE_LOADING_STATE,
|
||||||
@@ -186,6 +191,9 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
|
|
||||||
const handleSelectNode = useCallback((index: number | null) => {
|
const handleSelectNode = useCallback((index: number | null) => {
|
||||||
setSelectedNodeIndex(index);
|
setSelectedNodeIndex(index);
|
||||||
|
if (index !== null) {
|
||||||
|
setCameraViewMode("object");
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClearSelection = useCallback(() => {
|
const handleClearSelection = useCallback(() => {
|
||||||
@@ -258,9 +266,16 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
setIsPlayerMode((prev) => !prev);
|
setIsPlayerMode((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleResetCamera = useCallback(() => {
|
const handleCameraAction = useCallback(() => {
|
||||||
|
if (selectedNodeIndex !== null && cameraViewMode === "home") {
|
||||||
|
setFocusSelectedCameraRequest((request) => request + 1);
|
||||||
|
setCameraViewMode("object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setResetCameraRequest((request) => request + 1);
|
setResetCameraRequest((request) => request + 1);
|
||||||
}, []);
|
setCameraViewMode("home");
|
||||||
|
}, [cameraViewMode, selectedNodeIndex]);
|
||||||
|
|
||||||
const handlePreviewCinematic = useCallback(
|
const handlePreviewCinematic = useCallback(
|
||||||
(cinematic: CinematicDefinition) => {
|
(cinematic: CinematicDefinition) => {
|
||||||
@@ -381,6 +396,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
onRedo={handleRedo}
|
onRedo={handleRedo}
|
||||||
resetCameraRequest={resetCameraRequest}
|
resetCameraRequest={resetCameraRequest}
|
||||||
|
focusSelectedCameraRequest={focusSelectedCameraRequest}
|
||||||
isPlayerMode={isPlayerMode}
|
isPlayerMode={isPlayerMode}
|
||||||
cinematicPreviewRequest={cinematicPreviewRequest}
|
cinematicPreviewRequest={cinematicPreviewRequest}
|
||||||
onCinematicPreviewComplete={handleCinematicPreviewComplete}
|
onCinematicPreviewComplete={handleCinematicPreviewComplete}
|
||||||
@@ -411,7 +427,12 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
redoCount={redoCount}
|
redoCount={redoCount}
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
onRedo={handleRedo}
|
onRedo={handleRedo}
|
||||||
onResetCamera={handleResetCamera}
|
cameraActionLabel={
|
||||||
|
selectedNodeIndex !== null && cameraViewMode === "home"
|
||||||
|
? "Center on object"
|
||||||
|
: "Reset camera"
|
||||||
|
}
|
||||||
|
onCameraAction={handleCameraAction}
|
||||||
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