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>Shift</kbd> | Down |
|
||||
| <kbd>Esc</kbd> | Release mouse |
|
||||
| △ Scroll/mouse wheel up | Increase speed |
|
||||
| ▽ Scroll/mouse wheel down | Decrease speed |
|
||||
| <small>Left click</small> | Next observer camera |
|
||||
| △ <small>Scroll/mouse wheel up</small> | Increase speed |
|
||||
| ▽ <small>Scroll/mouse wheel down</small> | Decrease speed |
|
||||
|
||||
|
||||
## 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 { AudioProvider } from "@/src/components/AudioContext";
|
||||
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
|
||||
// stuff too, e.g. missions, terrains, and more. This client is used for those.
|
||||
|
|
@ -37,12 +38,14 @@ function MapInspector() {
|
|||
<main>
|
||||
<SettingsProvider>
|
||||
<Canvas shadows frameloop="always">
|
||||
<AudioProvider>
|
||||
<ObserverControls />
|
||||
<Mission key={missionName} name={missionName} />
|
||||
<ObserverCamera />
|
||||
<DebugElements />
|
||||
</AudioProvider>
|
||||
<CamerasProvider>
|
||||
<AudioProvider>
|
||||
<Mission key={missionName} name={missionName} />
|
||||
<ObserverCamera />
|
||||
<DebugElements />
|
||||
<ObserverControls />
|
||||
</AudioProvider>
|
||||
</CamerasProvider>
|
||||
<EffectComposer>
|
||||
<N8AO intensity={3} aoRadius={3} quality="performance" />
|
||||
</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",
|
||||
]);
|
||||
|
||||
const SOURCE_GROUP_NAMES = {
|
||||
const sourceGroupNames = {
|
||||
"Classic_maps_v1.vl2": "Classic",
|
||||
"DynamixFinalPack.vl2": "Official",
|
||||
"missions.vl2": "Official",
|
||||
"S5maps.vl2": "S5",
|
||||
"S8maps.vl2": "S8",
|
||||
|
|
@ -27,7 +28,7 @@ const groupedMissions = getMissionList().reduce(
|
|||
(groupMap, missionName) => {
|
||||
const missionInfo = getMissionInfo(missionName);
|
||||
const source = getSource(missionInfo.resourcePath);
|
||||
const groupName = SOURCE_GROUP_NAMES[source] ?? null;
|
||||
const groupName = sourceGroupNames[source] ?? null;
|
||||
const groupMissions = groupMap.get(groupName) ?? [];
|
||||
if (!excludeMissions.has(missionName)) {
|
||||
groupMissions.push({
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|||
import { KeyboardControls, useKeyboardControls } from "@react-three/drei";
|
||||
import { PointerLockControls } from "three-stdlib";
|
||||
import { useControls } from "./SettingsProvider";
|
||||
import { useCameras } from "./CamerasProvider";
|
||||
|
||||
enum Controls {
|
||||
forward = "forward",
|
||||
|
|
@ -22,6 +23,7 @@ function CameraMovement() {
|
|||
const { speedMultiplier, setSpeedMultiplier } = useControls();
|
||||
const [subscribe, getKeys] = useKeyboardControls<Controls>();
|
||||
const { camera, gl } = useThree();
|
||||
const { nextCamera } = useCameras();
|
||||
const controlsRef = useRef<PointerLockControls | null>(null);
|
||||
|
||||
// Scratch vectors to avoid allocations each frame
|
||||
|
|
@ -35,19 +37,21 @@ function CameraMovement() {
|
|||
controlsRef.current = controls;
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
// Only lock if clicking directly on the canvas (not on UI elements)
|
||||
if (e.target === gl.domElement) {
|
||||
if (controls.isLocked) {
|
||||
nextCamera();
|
||||
} else if (e.target === gl.domElement) {
|
||||
// Only lock if clicking directly on the canvas (not on UI elements)
|
||||
controls.lock();
|
||||
}
|
||||
};
|
||||
|
||||
gl.domElement.addEventListener("click", handleClick);
|
||||
document.addEventListener("click", handleClick);
|
||||
|
||||
return () => {
|
||||
gl.domElement.removeEventListener("click", handleClick);
|
||||
document.removeEventListener("click", handleClick);
|
||||
controls.dispose();
|
||||
};
|
||||
}, [camera, gl]);
|
||||
}, [camera, gl, nextCamera]);
|
||||
|
||||
// Handle mousewheel for speed adjustment
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import { Item } from "./Item";
|
|||
import { Turret } from "./Turret";
|
||||
import { AudioEmitter } from "./AudioEmitter";
|
||||
import { WayPoint } from "./WayPoint";
|
||||
import { Camera } from "./Camera";
|
||||
|
||||
const componentMap = {
|
||||
AudioEmitter,
|
||||
Camera,
|
||||
InteriorInstance,
|
||||
Item,
|
||||
SimGroup,
|
||||
|
|
|
|||
Loading…
Reference in a new issue