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

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