mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-13 07:25:07 +00:00
improve query param handling, add support for linking too coords
This commit is contained in:
parent
2a36c18f92
commit
55c1067682
39 changed files with 939 additions and 678 deletions
|
|
@ -36,8 +36,13 @@ export function useCameras() {
|
|||
|
||||
export function CamerasProvider({ children }: { children: ReactNode }) {
|
||||
const { camera } = useThree();
|
||||
const [cameraIndex, setCameraIndex] = useState(0);
|
||||
const [cameraIndex, setCameraIndex] = useState(-1);
|
||||
const [cameraMap, setCameraMap] = useState<Record<string, CameraEntry>>({});
|
||||
const [initialViewState, setInitialViewState] = useState(() => ({
|
||||
initialized: false,
|
||||
position: null,
|
||||
quarternion: null,
|
||||
}));
|
||||
|
||||
const registerCamera = useCallback((camera: CameraEntry) => {
|
||||
setCameraMap((prevCameraMap) => ({
|
||||
|
|
@ -55,38 +60,70 @@ export function CamerasProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
const cameraCount = Object.keys(cameraMap).length;
|
||||
|
||||
const nextCamera = useCallback(() => {
|
||||
setCameraIndex((prev) => {
|
||||
if (cameraCount === 0) {
|
||||
return 0;
|
||||
}
|
||||
return (prev + 1) % cameraCount;
|
||||
});
|
||||
}, [cameraCount]);
|
||||
|
||||
const setCamera = useCallback(
|
||||
(index: number) => {
|
||||
console.log(`[CamerasProvider] setCamera(${index})`);
|
||||
if (index >= 0 && index < cameraCount) {
|
||||
console.log(`[CamerasProvider] setCameraIndex(${index})`);
|
||||
setCameraIndex(index);
|
||||
const cameraId = Object.keys(cameraMap)[index];
|
||||
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);
|
||||
console.log(`[CamerasProvider] Done updating camera.`);
|
||||
}
|
||||
},
|
||||
[cameraCount],
|
||||
[camera, cameraCount, cameraMap],
|
||||
);
|
||||
|
||||
const nextCamera = useCallback(() => {
|
||||
console.log(`[CamerasProvider] nextCamera()`, cameraCount, cameraIndex);
|
||||
setCamera(cameraCount ? (cameraIndex + 1) % cameraCount : -1);
|
||||
}, [cameraCount, cameraIndex, setCamera]);
|
||||
|
||||
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);
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith("#c")) {
|
||||
const [positionString, quarternionString] = hash.slice(2).split("~");
|
||||
const position = positionString.split(",").map((s) => parseFloat(s));
|
||||
const quarternion = quarternionString
|
||||
.split(",")
|
||||
.map((s) => parseFloat(s));
|
||||
setInitialViewState({
|
||||
initialized: true,
|
||||
position: new Vector3(...position),
|
||||
quarternion: new Quaternion(...quarternion),
|
||||
});
|
||||
} else {
|
||||
setInitialViewState({
|
||||
initialized: true,
|
||||
position: null,
|
||||
quarternion: null,
|
||||
});
|
||||
}
|
||||
}, [cameraIndex, cameraMap, camera]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialViewState.initialized && initialViewState.position) {
|
||||
camera.position.copy(initialViewState.position);
|
||||
if (initialViewState.quarternion) {
|
||||
camera.quaternion.copy(initialViewState.quarternion);
|
||||
}
|
||||
}
|
||||
}, [initialViewState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialViewState.initialized || initialViewState.position) return;
|
||||
if (cameraCount > 0 && cameraIndex === -1) {
|
||||
console.log(`[CamerasProvider] setCamera(0) in useEffect`);
|
||||
setCamera(0);
|
||||
}
|
||||
}, [cameraCount, setCamera, cameraIndex]);
|
||||
|
||||
const context: CamerasContextValue = useMemo(
|
||||
() => ({
|
||||
|
|
|
|||
60
src/components/CopyCoordinatesButton.tsx
Normal file
60
src/components/CopyCoordinatesButton.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { RefObject, useCallback, useRef, useState } from "react";
|
||||
import { FaMapPin } from "react-icons/fa";
|
||||
import { FaClipboardCheck } from "react-icons/fa6";
|
||||
import { Camera, Quaternion, Vector3 } from "three";
|
||||
|
||||
function encodeViewHash({
|
||||
position,
|
||||
quaternion,
|
||||
}: {
|
||||
position: Vector3;
|
||||
quaternion: Quaternion;
|
||||
}) {
|
||||
const trunc = (num: number) => parseFloat(num.toFixed(3));
|
||||
const encodedPosition = `${trunc(position.x)},${trunc(position.y)},${trunc(position.z)}`;
|
||||
const encodedQuaternion = `${trunc(quaternion.x)},${trunc(quaternion.y)},${trunc(quaternion.z)},${trunc(quaternion.w)}`;
|
||||
return `#c${encodedPosition}~${encodedQuaternion}`;
|
||||
}
|
||||
|
||||
export function CopyCoordinatesButton({
|
||||
cameraRef,
|
||||
}: {
|
||||
cameraRef: RefObject<Camera | null>;
|
||||
}) {
|
||||
const [showCopied, setShowCopied] = useState(false);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const handleCopyLink = useCallback(async () => {
|
||||
clearTimeout(timerRef.current);
|
||||
const camera = cameraRef.current;
|
||||
if (!camera) return;
|
||||
const hash = encodeViewHash(camera);
|
||||
// Update the URL hash
|
||||
const fullPath = `${window.location.pathname}${window.location.search}${hash}`;
|
||||
const fullUrl = `${window.location.origin}${fullPath}`;
|
||||
window.history.replaceState(null, "", fullPath);
|
||||
try {
|
||||
await navigator.clipboard.writeText(fullUrl);
|
||||
setShowCopied(true);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setShowCopied(false);
|
||||
}, 1300);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, [cameraRef]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="IconButton CopyCoordinatesButton"
|
||||
aria-label="Copy coordinates URL"
|
||||
title="Copy coordinates URL"
|
||||
onClick={handleCopyLink}
|
||||
data-copied={showCopied ? "true" : "false"}
|
||||
>
|
||||
<FaMapPin className="MapPin" />
|
||||
<FaClipboardCheck className="ClipboardCheck" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
import { useControls, useDebug, useSettings } from "./SettingsProvider";
|
||||
import { MissionSelect } from "./MissionSelect";
|
||||
import { RefObject } from "react";
|
||||
import { Camera } from "three";
|
||||
import { CopyCoordinatesButton } from "./CopyCoordinatesButton";
|
||||
|
||||
export function InspectorControls({
|
||||
missionName,
|
||||
missionType,
|
||||
onChangeMission,
|
||||
cameraRef,
|
||||
}: {
|
||||
missionName: string;
|
||||
missionType: string;
|
||||
|
|
@ -15,6 +19,7 @@ export function InspectorControls({
|
|||
missionName: string;
|
||||
missionType: string;
|
||||
}) => void;
|
||||
cameraRef: RefObject<Camera | null>;
|
||||
}) {
|
||||
const {
|
||||
fogEnabled,
|
||||
|
|
@ -41,6 +46,7 @@ export function InspectorControls({
|
|||
missionType={missionType}
|
||||
onChange={onChangeMission}
|
||||
/>
|
||||
<CopyCoordinatesButton cameraRef={cameraRef} />
|
||||
<div className="CheckboxField">
|
||||
<input
|
||||
id="fogInput"
|
||||
|
|
|
|||
|
|
@ -139,7 +139,6 @@ function useExecutedMission(
|
|||
interface MissionProps {
|
||||
name: string;
|
||||
missionType: string;
|
||||
setMissionType: (type: string) => void;
|
||||
onLoadingChange?: (isLoading: boolean, progress?: number) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export function MissionSelect({
|
|||
if (!searchValue)
|
||||
return { type: "grouped" as const, groups: defaultGroups };
|
||||
const matches = matchSorter(allMissions, searchValue, {
|
||||
keys: ["displayName", "missionName"],
|
||||
keys: ["displayName", "missionName", "missionTypes", "groupName"],
|
||||
});
|
||||
return { type: "flat" as const, missions: matches };
|
||||
}, [searchValue]);
|
||||
|
|
|
|||
|
|
@ -45,8 +45,15 @@ function CameraMovement() {
|
|||
const controls = new PointerLockControls(camera, gl.domElement);
|
||||
controlsRef.current = controls;
|
||||
|
||||
return () => {
|
||||
controls.dispose();
|
||||
};
|
||||
}, [camera, gl.domElement]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (controls.isLocked) {
|
||||
const controls = controlsRef.current;
|
||||
if (!controls || controls.isLocked) {
|
||||
nextCamera();
|
||||
} else if (e.target === gl.domElement) {
|
||||
// Only lock if clicking directly on the canvas (not on UI elements)
|
||||
|
|
@ -58,9 +65,8 @@ function CameraMovement() {
|
|||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
controls.dispose();
|
||||
};
|
||||
}, [camera, gl, nextCamera]);
|
||||
}, [nextCamera]);
|
||||
|
||||
// Handle number keys 1-9 for camera selection
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue