248 lines
7.0 KiB
TypeScript
248 lines
7.0 KiB
TypeScript
import {
|
|
createContext,
|
|
ReactNode,
|
|
useContext,
|
|
useMemo,
|
|
useState,
|
|
useCallback,
|
|
} from "react";
|
|
import { SCALE_FACTOR, UP_SCALE } from "./Constants";
|
|
|
|
const TransformContext = createContext<{
|
|
position: { x: number; y: number };
|
|
scale: number;
|
|
rotation: number;
|
|
screenCenter?: { x: number; y: number };
|
|
|
|
setPosition: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
|
|
setScale: React.Dispatch<React.SetStateAction<number>>;
|
|
setRotation: React.Dispatch<React.SetStateAction<number>>;
|
|
screenToLocal: (x: number, y: number) => { x: number; y: number };
|
|
localToScreen: (x: number, y: number) => { x: number; y: number };
|
|
rotateToAngle: (to: number, fromPosition?: { x: number; y: number }) => void;
|
|
setTransform: (
|
|
latitude: number,
|
|
longitude: number,
|
|
rotationDegrees?: number,
|
|
scale?: number
|
|
) => void;
|
|
setScaleOnly: (newScale: number) => void;
|
|
setScaleWithoutMovingCenter: (newScale: number) => void;
|
|
setScreenCenter: React.Dispatch<
|
|
React.SetStateAction<{ x: number; y: number } | undefined>
|
|
>;
|
|
setScaleAtCenter: (newScale: number) => void;
|
|
}>({
|
|
position: { x: 0, y: 0 },
|
|
scale: 1,
|
|
rotation: 0,
|
|
screenCenter: undefined,
|
|
setPosition: () => {},
|
|
setScale: () => {},
|
|
setRotation: () => {},
|
|
screenToLocal: () => ({ x: 0, y: 0 }),
|
|
localToScreen: () => ({ x: 0, y: 0 }),
|
|
rotateToAngle: () => {},
|
|
setTransform: () => {},
|
|
setScaleOnly: () => {},
|
|
setScaleWithoutMovingCenter: () => {},
|
|
setScreenCenter: () => {},
|
|
setScaleAtCenter: () => {},
|
|
});
|
|
|
|
// Provider component
|
|
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
const [scale, setScale] = useState(1);
|
|
const [rotation, setRotation] = useState(0);
|
|
const [screenCenter, setScreenCenter] = useState<{ x: number; y: number }>();
|
|
|
|
const screenToLocal = useCallback(
|
|
(screenX: number, screenY: number) => {
|
|
// Translate point relative to current pan position
|
|
const translatedX = (screenX - position.x) / scale;
|
|
const translatedY = (screenY - position.y) / scale;
|
|
|
|
// Rotate point around center
|
|
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
|
|
const sinRotation = Math.sin(-rotation);
|
|
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
|
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
|
|
|
return {
|
|
x: rotatedX / UP_SCALE,
|
|
y: rotatedY / UP_SCALE,
|
|
};
|
|
},
|
|
[position.x, position.y, scale, rotation]
|
|
);
|
|
|
|
// Inverse of screenToLocal
|
|
const localToScreen = useCallback(
|
|
(localX: number, localY: number) => {
|
|
const upscaledX = localX * UP_SCALE;
|
|
const upscaledY = localY * UP_SCALE;
|
|
|
|
const cosRotation = Math.cos(rotation);
|
|
const sinRotation = Math.sin(rotation);
|
|
const rotatedX = upscaledX * cosRotation - upscaledY * sinRotation;
|
|
const rotatedY = upscaledX * sinRotation + upscaledY * cosRotation;
|
|
|
|
const translatedX = rotatedX * scale + position.x;
|
|
const translatedY = rotatedY * scale + position.y;
|
|
|
|
return {
|
|
x: translatedX,
|
|
y: translatedY,
|
|
};
|
|
},
|
|
[position.x, position.y, scale, rotation]
|
|
);
|
|
|
|
const rotateToAngle = useCallback(
|
|
(to: number, fromPosition?: { x: number; y: number }) => {
|
|
const rotationDiff = to - rotation;
|
|
|
|
const center = screenCenter ?? { x: 0, y: 0 };
|
|
const cosDelta = Math.cos(rotationDiff);
|
|
const sinDelta = Math.sin(rotationDiff);
|
|
|
|
const currentFromPosition = fromPosition ?? position;
|
|
|
|
const newPosition = {
|
|
x:
|
|
center.x * (1 - cosDelta) +
|
|
currentFromPosition.x * cosDelta +
|
|
(center.y - currentFromPosition.y) * sinDelta,
|
|
y:
|
|
center.y * (1 - cosDelta) +
|
|
currentFromPosition.y * cosDelta +
|
|
(currentFromPosition.x - center.x) * sinDelta,
|
|
};
|
|
|
|
// Update both rotation and position in a single batch to avoid stale closure
|
|
setRotation(to);
|
|
setPosition(newPosition);
|
|
},
|
|
[rotation, position, screenCenter]
|
|
);
|
|
|
|
const setTransform = useCallback(
|
|
(
|
|
latitude: number,
|
|
longitude: number,
|
|
rotationDegrees?: number,
|
|
useScale?: number
|
|
) => {
|
|
const selectedRotation =
|
|
rotationDegrees !== undefined
|
|
? (rotationDegrees * Math.PI) / 180
|
|
: rotation;
|
|
const selectedScale =
|
|
useScale !== undefined ? useScale / SCALE_FACTOR : scale;
|
|
const center = screenCenter ?? { x: 0, y: 0 };
|
|
|
|
const newPosition = {
|
|
x: -latitude * UP_SCALE * selectedScale,
|
|
y: -longitude * UP_SCALE * selectedScale,
|
|
};
|
|
|
|
const cosRot = Math.cos(selectedRotation);
|
|
const sinRot = Math.sin(selectedRotation);
|
|
|
|
// Translate point relative to center, rotate, then translate back
|
|
const dx = newPosition.x;
|
|
const dy = newPosition.y;
|
|
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
|
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
|
|
|
// Batch state updates to avoid intermediate renders
|
|
setPosition(newPosition);
|
|
setRotation(selectedRotation);
|
|
setScale(selectedScale);
|
|
},
|
|
[rotation, scale, screenCenter]
|
|
);
|
|
|
|
const setScaleAtCenter = useCallback(
|
|
(newScale: number) => {
|
|
if (scale === newScale) return;
|
|
|
|
const center = screenCenter ?? { x: 0, y: 0 };
|
|
|
|
const actualZoomFactor = newScale / scale;
|
|
|
|
const newPosition = {
|
|
x: position.x + (center.x - position.x) * (1 - actualZoomFactor),
|
|
y: position.y + (center.y - position.y) * (1 - actualZoomFactor),
|
|
};
|
|
|
|
setPosition(newPosition);
|
|
setScale(newScale);
|
|
},
|
|
[position, scale, screenCenter]
|
|
);
|
|
|
|
const setScaleOnly = useCallback((newScale: number) => {
|
|
// Изменяем только масштаб, не трогая позицию и поворот
|
|
setScale(newScale);
|
|
}, []);
|
|
|
|
const setScaleWithoutMovingCenter = useCallback(
|
|
(newScale: number) => {
|
|
setScale(newScale);
|
|
},
|
|
[setScale]
|
|
);
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
position,
|
|
scale,
|
|
rotation,
|
|
screenCenter,
|
|
setPosition,
|
|
setScale,
|
|
setRotation,
|
|
rotateToAngle,
|
|
screenToLocal,
|
|
localToScreen,
|
|
setTransform,
|
|
setScaleOnly,
|
|
setScaleWithoutMovingCenter,
|
|
setScreenCenter,
|
|
setScaleAtCenter,
|
|
}),
|
|
[
|
|
position,
|
|
scale,
|
|
rotation,
|
|
screenCenter,
|
|
setScale,
|
|
rotateToAngle,
|
|
screenToLocal,
|
|
localToScreen,
|
|
setTransform,
|
|
setScaleOnly,
|
|
setScaleWithoutMovingCenter,
|
|
setScreenCenter,
|
|
setScaleAtCenter,
|
|
]
|
|
);
|
|
|
|
return (
|
|
<TransformContext.Provider value={value}>
|
|
{children}
|
|
</TransformContext.Provider>
|
|
);
|
|
};
|
|
|
|
// Custom hook for easy access to transform values
|
|
export const useTransform = () => {
|
|
const context = useContext(TransformContext);
|
|
if (!context) {
|
|
throw new Error("useTransform must be used within a TransformProvider");
|
|
}
|
|
return context;
|
|
};
|