mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 20:25:01 +00:00
left click when locked to cycle camera
This commit is contained in:
parent
106d684d64
commit
42f4f9ae9d
|
|
@ -21,8 +21,9 @@ Click inside the map preview area to capture the mouse.
|
||||||
| <kbd>Space</kbd> | Up |
|
| <kbd>Space</kbd> | Up |
|
||||||
| <kbd>Shift</kbd> | Down |
|
| <kbd>Shift</kbd> | Down |
|
||||||
| <kbd>Esc</kbd> | Release mouse |
|
| <kbd>Esc</kbd> | Release mouse |
|
||||||
| △ Scroll/mouse wheel up | Increase speed |
|
| <small>Left click</small> | Next observer camera |
|
||||||
| ▽ Scroll/mouse wheel down | Decrease speed |
|
| △ <small>Scroll/mouse wheel up</small> | Increase speed |
|
||||||
|
| ▽ <small>Scroll/mouse wheel down</small> | Decrease speed |
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
|
||||||
15
app/page.tsx
15
app/page.tsx
|
|
@ -11,6 +11,7 @@ import { SettingsProvider } from "@/src/components/SettingsProvider";
|
||||||
import { ObserverCamera } from "@/src/components/ObserverCamera";
|
import { ObserverCamera } from "@/src/components/ObserverCamera";
|
||||||
import { AudioProvider } from "@/src/components/AudioContext";
|
import { AudioProvider } from "@/src/components/AudioContext";
|
||||||
import { DebugElements } from "@/src/components/DebugElements";
|
import { DebugElements } from "@/src/components/DebugElements";
|
||||||
|
import { CamerasProvider } from "@/src/components/CamerasProvider";
|
||||||
|
|
||||||
// three.js has its own loaders for textures and models, but we need to load other
|
// three.js has its own loaders for textures and models, but we need to load other
|
||||||
// stuff too, e.g. missions, terrains, and more. This client is used for those.
|
// stuff too, e.g. missions, terrains, and more. This client is used for those.
|
||||||
|
|
@ -37,12 +38,14 @@ function MapInspector() {
|
||||||
<main>
|
<main>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<Canvas shadows frameloop="always">
|
<Canvas shadows frameloop="always">
|
||||||
<AudioProvider>
|
<CamerasProvider>
|
||||||
<ObserverControls />
|
<AudioProvider>
|
||||||
<Mission key={missionName} name={missionName} />
|
<Mission key={missionName} name={missionName} />
|
||||||
<ObserverCamera />
|
<ObserverCamera />
|
||||||
<DebugElements />
|
<DebugElements />
|
||||||
</AudioProvider>
|
<ObserverControls />
|
||||||
|
</AudioProvider>
|
||||||
|
</CamerasProvider>
|
||||||
<EffectComposer>
|
<EffectComposer>
|
||||||
<N8AO intensity={3} aoRadius={3} quality="performance" />
|
<N8AO intensity={3} aoRadius={3} quality="performance" />
|
||||||
</EffectComposer>
|
</EffectComposer>
|
||||||
|
|
|
||||||
42
src/components/Camera.tsx
Normal file
42
src/components/Camera.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { useEffect, useId, useMemo, useRef } from "react";
|
||||||
|
import { PerspectiveCamera } from "@react-three/drei";
|
||||||
|
import { useCameras } from "./CamerasProvider";
|
||||||
|
import { useSettings } from "./SettingsProvider";
|
||||||
|
import {
|
||||||
|
ConsoleObject,
|
||||||
|
getPosition,
|
||||||
|
getProperty,
|
||||||
|
getRotation,
|
||||||
|
} from "../mission";
|
||||||
|
import { Quaternion, Vector3 } from "three";
|
||||||
|
|
||||||
|
export function Camera({ object }: { object: ConsoleObject }) {
|
||||||
|
const { fov } = useSettings();
|
||||||
|
const { registerCamera, unregisterCamera } = useCameras();
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
const dataBlock = getProperty(object, "dataBlock").value;
|
||||||
|
const [x, y, z] = useMemo(() => getPosition(object), [object]);
|
||||||
|
const q = useMemo(() => getRotation(object), [object]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dataBlock === "Observer") {
|
||||||
|
const camera = { id, position: new Vector3(x, y, z), rotation: q };
|
||||||
|
registerCamera(camera);
|
||||||
|
return () => {
|
||||||
|
unregisterCamera(camera);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [id, dataBlock, registerCamera, unregisterCamera, x, y, z, q]);
|
||||||
|
|
||||||
|
// Maps can define preset observer camera locations. You should be able to jump
|
||||||
|
// to an observer camera position and then fly around from that starting point
|
||||||
|
// But, we wouldn't want the user to take control of the actual camera's
|
||||||
|
// position, because then if you want to cycle back through them again, the
|
||||||
|
// "fixed" camera location has moved. There are two approaches for fixing this:
|
||||||
|
// make Camera render an actual PerspectiveCamera, switch it when cycling,
|
||||||
|
// but clone a new "flying" camera when the user moves. The other is to not have
|
||||||
|
// multiple cameras at all, but rather update the one camera with fixed position
|
||||||
|
// information when cycling. This uses the latter approach.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
94
src/components/CamerasProvider.tsx
Normal file
94
src/components/CamerasProvider.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { Quaternion, Vector3 } from "three";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
interface CameraEntry {
|
||||||
|
id: string;
|
||||||
|
position: Vector3;
|
||||||
|
rotation: Quaternion;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CamerasContextValue {
|
||||||
|
registerCamera: (camera: any) => void;
|
||||||
|
unregisterCamera: (camera: any) => void;
|
||||||
|
nextCamera: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CamerasContext = createContext<CamerasContextValue | null>(null);
|
||||||
|
|
||||||
|
export function useCameras() {
|
||||||
|
const context = useContext(CamerasContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCameras must be used within CamerasProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CamerasProvider({ children }: { children: ReactNode }) {
|
||||||
|
const { camera } = useThree();
|
||||||
|
const [cameraIndex, setCameraIndex] = useState(0);
|
||||||
|
const [cameraMap, setCameraMap] = useState<Record<string, CameraEntry>>({});
|
||||||
|
|
||||||
|
const registerCamera = useCallback((camera: CameraEntry) => {
|
||||||
|
setCameraMap((prevCameraMap) => ({
|
||||||
|
...prevCameraMap,
|
||||||
|
[camera.id]: camera,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unregisterCamera = useCallback((camera: CameraEntry) => {
|
||||||
|
setCameraMap((prevCameraMap) => {
|
||||||
|
const { [camera.id]: removedCamera, ...remainingCameras } = prevCameraMap;
|
||||||
|
return remainingCameras;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const nextCamera = useCallback(() => {
|
||||||
|
setCameraIndex((prev) => {
|
||||||
|
const cameraCount = Object.keys(cameraMap).length;
|
||||||
|
if (cameraCount === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (prev + 1) % cameraCount;
|
||||||
|
});
|
||||||
|
}, [cameraMap]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cameraCount = Object.keys(cameraMap).length;
|
||||||
|
if (cameraIndex < cameraCount) {
|
||||||
|
const cameraId = Object.keys(cameraMap)[cameraIndex];
|
||||||
|
const cameraInfo = cameraMap[cameraId];
|
||||||
|
camera.position.copy(cameraInfo.position);
|
||||||
|
// Apply coordinate system correction for Torque3D to Three.js
|
||||||
|
const correction = new Quaternion().setFromAxisAngle(
|
||||||
|
new Vector3(0, 1, 0),
|
||||||
|
-Math.PI / 2
|
||||||
|
);
|
||||||
|
camera.quaternion.copy(cameraInfo.rotation).multiply(correction);
|
||||||
|
}
|
||||||
|
}, [cameraIndex, cameraMap, camera]);
|
||||||
|
|
||||||
|
const context: CamerasContextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
registerCamera,
|
||||||
|
unregisterCamera,
|
||||||
|
nextCamera,
|
||||||
|
}),
|
||||||
|
[registerCamera, unregisterCamera, nextCamera]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CamerasContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</CamerasContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,9 @@ const excludeMissions = new Set([
|
||||||
"SkiFree_Randomizer",
|
"SkiFree_Randomizer",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SOURCE_GROUP_NAMES = {
|
const sourceGroupNames = {
|
||||||
"Classic_maps_v1.vl2": "Classic",
|
"Classic_maps_v1.vl2": "Classic",
|
||||||
|
"DynamixFinalPack.vl2": "Official",
|
||||||
"missions.vl2": "Official",
|
"missions.vl2": "Official",
|
||||||
"S5maps.vl2": "S5",
|
"S5maps.vl2": "S5",
|
||||||
"S8maps.vl2": "S8",
|
"S8maps.vl2": "S8",
|
||||||
|
|
@ -27,7 +28,7 @@ const groupedMissions = getMissionList().reduce(
|
||||||
(groupMap, missionName) => {
|
(groupMap, missionName) => {
|
||||||
const missionInfo = getMissionInfo(missionName);
|
const missionInfo = getMissionInfo(missionName);
|
||||||
const source = getSource(missionInfo.resourcePath);
|
const source = getSource(missionInfo.resourcePath);
|
||||||
const groupName = SOURCE_GROUP_NAMES[source] ?? null;
|
const groupName = sourceGroupNames[source] ?? null;
|
||||||
const groupMissions = groupMap.get(groupName) ?? [];
|
const groupMissions = groupMap.get(groupName) ?? [];
|
||||||
if (!excludeMissions.has(missionName)) {
|
if (!excludeMissions.has(missionName)) {
|
||||||
groupMissions.push({
|
groupMissions.push({
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { KeyboardControls, useKeyboardControls } from "@react-three/drei";
|
import { KeyboardControls, useKeyboardControls } from "@react-three/drei";
|
||||||
import { PointerLockControls } from "three-stdlib";
|
import { PointerLockControls } from "three-stdlib";
|
||||||
import { useControls } from "./SettingsProvider";
|
import { useControls } from "./SettingsProvider";
|
||||||
|
import { useCameras } from "./CamerasProvider";
|
||||||
|
|
||||||
enum Controls {
|
enum Controls {
|
||||||
forward = "forward",
|
forward = "forward",
|
||||||
|
|
@ -22,6 +23,7 @@ function CameraMovement() {
|
||||||
const { speedMultiplier, setSpeedMultiplier } = useControls();
|
const { speedMultiplier, setSpeedMultiplier } = useControls();
|
||||||
const [subscribe, getKeys] = useKeyboardControls<Controls>();
|
const [subscribe, getKeys] = useKeyboardControls<Controls>();
|
||||||
const { camera, gl } = useThree();
|
const { camera, gl } = useThree();
|
||||||
|
const { nextCamera } = useCameras();
|
||||||
const controlsRef = useRef<PointerLockControls | null>(null);
|
const controlsRef = useRef<PointerLockControls | null>(null);
|
||||||
|
|
||||||
// Scratch vectors to avoid allocations each frame
|
// Scratch vectors to avoid allocations each frame
|
||||||
|
|
@ -35,19 +37,21 @@ function CameraMovement() {
|
||||||
controlsRef.current = controls;
|
controlsRef.current = controls;
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
// Only lock if clicking directly on the canvas (not on UI elements)
|
if (controls.isLocked) {
|
||||||
if (e.target === gl.domElement) {
|
nextCamera();
|
||||||
|
} else if (e.target === gl.domElement) {
|
||||||
|
// Only lock if clicking directly on the canvas (not on UI elements)
|
||||||
controls.lock();
|
controls.lock();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
gl.domElement.addEventListener("click", handleClick);
|
document.addEventListener("click", handleClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
gl.domElement.removeEventListener("click", handleClick);
|
document.removeEventListener("click", handleClick);
|
||||||
controls.dispose();
|
controls.dispose();
|
||||||
};
|
};
|
||||||
}, [camera, gl]);
|
}, [camera, gl, nextCamera]);
|
||||||
|
|
||||||
// Handle mousewheel for speed adjustment
|
// Handle mousewheel for speed adjustment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ import { Item } from "./Item";
|
||||||
import { Turret } from "./Turret";
|
import { Turret } from "./Turret";
|
||||||
import { AudioEmitter } from "./AudioEmitter";
|
import { AudioEmitter } from "./AudioEmitter";
|
||||||
import { WayPoint } from "./WayPoint";
|
import { WayPoint } from "./WayPoint";
|
||||||
|
import { Camera } from "./Camera";
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
AudioEmitter,
|
AudioEmitter,
|
||||||
|
Camera,
|
||||||
InteriorInstance,
|
InteriorInstance,
|
||||||
Item,
|
Item,
|
||||||
SimGroup,
|
SimGroup,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue