diff --git a/src/pages/MapPage/index.tsx b/src/pages/MapPage/index.tsx index 33fdf5d..57bee77 100644 --- a/src/pages/MapPage/index.tsx +++ b/src/pages/MapPage/index.tsx @@ -124,6 +124,7 @@ export const clearMapCaches = () => { interface ApiRoute { id: number; route_number: string; + route_name: string; path: [number, number][]; center_latitude: number; center_longitude: number; @@ -370,6 +371,7 @@ class MapStore { this.routes = routeResponses.map((res) => ({ id: res.data.id, route_number: res.data.route_number, + route_name: res.data.route_name || "", path: res.data.path, center_latitude: res.data.center_latitude, center_longitude: res.data.center_longitude, @@ -2302,6 +2304,8 @@ const MapSightbar: React.FC = observer( }); feature.setId(`route-${route.id}`); feature.set("featureType", "route"); + feature.set("routeName", route.route_name); + feature.set("routeNumber", route.route_number); return feature; }); @@ -2317,11 +2321,18 @@ const MapSightbar: React.FC = observer( const filteredFeatures = useMemo(() => { if (!searchQuery.trim()) return allFeatures; - return allFeatures.filter((f) => - ((f.get("name") as string) || "") - .toLowerCase() - .includes(searchQuery.toLowerCase()) - ); + const normalizedQuery = searchQuery.toLowerCase(); + return allFeatures.filter((f) => { + const candidates = [ + (f.get("name") as string) || "", + (f.get("description") as string) || "", + (f.get("routeName") as string) || "", + (f.get("routeNumber") as string) || "", + ]; + return candidates.some((value) => + value.toLowerCase().includes(normalizedQuery) + ); + }); }, [allFeatures, searchQuery]); const handleFeatureClick = useCallback( @@ -2649,6 +2660,40 @@ const MapSightbar: React.FC = observer( featureType === "station" && description && description.trim() !== ""; + const routeName = + featureType === "route" + ? ((feature.get("routeName") as string) || "") + : ""; + const routeNumber = + featureType === "route" + ? ((feature.get("routeNumber") as string) || fName) + : ""; + const routeNumberTrimmed = routeNumber.trim(); + const routeNameTrimmed = routeName.trim(); + const displayName = + featureType === "route" + ? routeNumberTrimmed || fName + : fName; + const showRouteName = + featureType === "route" && + routeNameTrimmed !== "" && + routeNameTrimmed !== displayName; + const titleParts: string[] = []; + if (featureType === "route") { + if (routeNumberTrimmed) { + titleParts.push(routeNumberTrimmed); + } + if (routeNameTrimmed) { + titleParts.push(routeNameTrimmed); + } + } + const titleText = + featureType === "route" + ? titleParts.join(" • ") || + routeNumberTrimmed || + routeNameTrimmed || + fName + : fName; return (
= observer( checked={isChecked} onChange={() => handleCheckboxChange(fId)} onClick={(e) => e.stopPropagation()} - aria-label={`Выбрать ${fName}`} + aria-label={`Выбрать ${titleText}`} />
= observer( - {fName} + {displayName}
{showDescription && ( @@ -2702,6 +2747,11 @@ const MapSightbar: React.FC = observer( {description} )} + {showRouteName && ( +
+ {routeNameTrimmed} +
+ )}
+ )} @@ -520,14 +545,15 @@ const LinkedItemsContentsInner = < /> - + )} @@ -587,14 +613,15 @@ const LinkedItemsContentsInner = < - + )} diff --git a/src/pages/Route/route-preview/LeftSidebar.tsx b/src/pages/Route/route-preview/LeftSidebar.tsx index c43bf9e..d48d637 100644 --- a/src/pages/Route/route-preview/LeftSidebar.tsx +++ b/src/pages/Route/route-preview/LeftSidebar.tsx @@ -1,12 +1,19 @@ -import { Stack, Typography, Button } from "@mui/material"; +import { Box, Stack, Typography, Button } from "@mui/material"; import { useNavigate, useNavigationType } from "react-router"; import { MediaViewer } from "@widgets"; import { useMapData } from "./MapDataContext"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; import { authInstance } from "@shared"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import LanguageSelector from "./web-gl/LanguageSelector"; -export const LeftSidebar = observer(() => { +type LeftSidebarProps = { + open: boolean; + onToggle: () => void; +}; + +export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => { const navigate = useNavigate(); const navigationType = useNavigationType(); // PUSH, POP, REPLACE const { routeData } = useMapData(); @@ -35,101 +42,146 @@ export const LeftSidebar = observer(() => { }; return ( - - - + -
} > - {carrierThumbnail && ( + Назад + + + +
+ {carrierThumbnail && ( + + )} + + При поддержке Правительства + +
+
+ + + + + + + + {carrierLogo && ( )} - - При поддержке Правительства - {" "} -
+
+ + + #ВсемПоПути +
- - - - + {!open && ( + + #ВсемПоПути + + )} - - {carrierLogo && ( - - )} - - - - #ВсемПоПути - - +
+ +
+ ); }); diff --git a/src/pages/Route/route-preview/MapDataContext.tsx b/src/pages/Route/route-preview/MapDataContext.tsx index dfe7de7..5d9b1d4 100644 --- a/src/pages/Route/route-preview/MapDataContext.tsx +++ b/src/pages/Route/route-preview/MapDataContext.tsx @@ -41,6 +41,8 @@ const MapDataContext = createContext<{ latitude: number, longitude: number ) => void; + setIconSize: (size: number) => void; + setFontSize: (size: number) => void; saveChanges: () => void; }>({ originalRouteData: undefined, @@ -61,6 +63,8 @@ const MapDataContext = createContext<{ setStationOffset: () => {}, setStationAlign: () => {}, setSightCoordinates: () => {}, + setIconSize: () => {}, + setFontSize: () => {}, saveChanges: () => {}, }); @@ -164,9 +168,57 @@ export const MapDataProvider = observer( }); } - function setMapCenter(x: number, y: number) { + function setIconSize(size: number) { + const clamped = Math.max(50, Math.min(300, size)); setRouteChanges((prev) => { - return { ...prev, center_latitude: x, center_longitude: y }; + if (prev.icon_size === clamped) { + return prev; + } + return { ...prev, icon_size: clamped }; + }); + } + + function setFontSize(size: number) { + const clamped = Math.max(50, Math.min(300, size)); + setRouteChanges((prev) => { + if (prev.font_size === clamped) { + return prev; + } + return { ...prev, font_size: clamped }; + }); + } + + function setMapCenter(latitude: number, longitude: number) { + const epsilon = 1e-6; + + setRouteChanges((prev) => { + const prevLat = prev.center_latitude; + const prevLon = prev.center_longitude; + + if ( + prevLat !== undefined && + prevLon !== undefined && + Math.abs(prevLat - latitude) < epsilon && + Math.abs(prevLon - longitude) < epsilon + ) { + return prev; + } + + return { + ...prev, + center_latitude: latitude, + center_longitude: longitude, + }; + }); + + setRouteData((routePrev) => { + if (!routePrev) return routePrev; + + return { + ...routePrev, + center_latitude: latitude, + center_longitude: longitude, + }; }); } @@ -179,12 +231,42 @@ export const MapDataProvider = observer( async function saveStationChanges() { for (const station of stationChanges) { await authInstance.patch(`/route/${routeId}/station`, station); + + setStationData((prev) => { + const updated = { ...prev }; + Object.keys(updated).forEach((lang) => { + updated[lang] = updated[lang].map((s) => + s.id === station.station_id + ? { + ...s, + offset_x: station.offset_x, + offset_y: station.offset_y, + } + : s + ); + }); + return updated; + }); } } async function saveSightChanges() { for (const sight of sightChanges) { await authInstance.patch(`/route/${routeId}/sight`, sight); + + setSightData((prev) => + prev + ? prev.map((s) => + s.id === sight.sight_id + ? { + ...s, + latitude: sight.latitude, + longitude: sight.longitude, + } + : s + ) + : prev + ); } } @@ -320,6 +402,14 @@ export const MapDataProvider = observer( latitude: number, longitude: number ) { + setSightData((prev) => + prev + ? prev.map((sight) => + sight.id === sightId ? { ...sight, latitude, longitude } : sight + ) + : prev + ); + setSightChanges((prev) => { const existingIndex = prev.findIndex( (sight) => sight.sight_id === sightId @@ -375,6 +465,8 @@ export const MapDataProvider = observer( setStationOffset, setStationAlign, setSightCoordinates, + setIconSize, + setFontSize, }), [ originalRouteData, @@ -387,6 +479,8 @@ export const MapDataProvider = observer( isStationLoading, isSightLoading, selectedSight, + setIconSize, + setFontSize, ] ); diff --git a/src/pages/Route/route-preview/RightSidebar.tsx b/src/pages/Route/route-preview/RightSidebar.tsx index dd09d29..428e6bf 100644 --- a/src/pages/Route/route-preview/RightSidebar.tsx +++ b/src/pages/Route/route-preview/RightSidebar.tsx @@ -2,7 +2,6 @@ import { Button, Stack, TextField, Typography, Slider } from "@mui/material"; import { useMapData } from "./MapDataContext"; import { useEffect, useState } from "react"; import { useTransform } from "./TransformContext"; -import { coordinatesToLocal, localToCoordinates } from "./utils"; import { SCALE_FACTOR } from "./Constants"; import { toast } from "react-toastify"; @@ -13,18 +12,10 @@ export function RightSidebar() { saveChanges, originalRouteData, setMapRotation, - setMapCenter, + setIconSize: updateIconSize, + setFontSize: updateFontSize, } = useMapData(); - const { - rotation, - position, - screenToLocal, - screenCenter, - rotateToAngle, - setTransform, - scale, - setScaleAtCenter, - } = useTransform(); + const { rotation, rotateToAngle, scale, setScaleAtCenter } = useTransform(); const [minScale, setMinScale] = useState(1); const [maxScale, setMaxScale] = useState(5); @@ -34,6 +25,8 @@ export function RightSidebar() { }); const [rotationDegrees, setRotationDegrees] = useState(0); const [isUserEditing, setIsUserEditing] = useState(false); + const [iconSize, setIconSize] = useState(100); + const [fontSize, setFontSize] = useState(100); useEffect(() => { if (originalRouteData) { @@ -50,6 +43,8 @@ export function RightSidebar() { x: originalRouteData.center_latitude ?? 0, y: originalRouteData.center_longitude ?? 0, }); + setIconSize(originalRouteData.icon_size ?? 100); + setFontSize(originalRouteData.font_size ?? 100); } }, [originalRouteData]); @@ -70,33 +65,55 @@ export function RightSidebar() { }, [rotationDegrees]); useEffect(() => { - if (!isUserEditing) { - const center = screenCenter ?? { x: 0, y: 0 }; - const localCenter = screenToLocal(center.x, center.y); - const coordinates = localToCoordinates(localCenter.x, localCenter.y); - setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude }); + if (isUserEditing) { + return; } - }, [ - position, - screenCenter, - screenToLocal, - localToCoordinates, - setLocalCenter, - isUserEditing, - ]); - useEffect(() => { - setMapCenter(localCenter.x, localCenter.y); - }, [localCenter]); + const latitude = routeData?.center_latitude ?? 0; + const longitude = routeData?.center_longitude ?? 0; + + setLocalCenter((prev) => { + if ( + Math.abs(prev.x - latitude) < 1e-6 && + Math.abs(prev.y - longitude) < 1e-6 + ) { + return prev; + } + return { x: latitude, y: longitude }; + }); + }, [isUserEditing, routeData?.center_latitude, routeData?.center_longitude]); function setRotationFromDegrees(degrees: number) { rotateToAngle((degrees * Math.PI) / 180); } - function pan({ x, y }: { x: number; y: number }) { - const coordinates = coordinatesToLocal(x, y); - setTransform(coordinates.x, coordinates.y); - } + const handleIconSizeChange = (value: number) => { + if (!Number.isFinite(value)) { + return; + } + const clamped = Math.max(50, Math.min(300, Math.round(value))); + setIconSize(clamped); + updateIconSize(clamped); + }; + + const handleFontSizeChange = (value: number) => { + if (!Number.isFinite(value)) { + return; + } + const clamped = Math.max(50, Math.min(300, Math.round(value))); + setFontSize(clamped); + updateFontSize(clamped); + }; + + useEffect(() => { + const next = routeData?.icon_size ?? originalRouteData?.icon_size ?? 100; + setIconSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev)); + }, [routeData?.icon_size, originalRouteData?.icon_size]); + + useEffect(() => { + const next = routeData?.font_size ?? originalRouteData?.font_size ?? 100; + setFontSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev)); + }, [routeData?.font_size, originalRouteData?.font_size]); if (!routeData) { return null; @@ -176,6 +193,10 @@ export function RightSidebar() { newMaxScale = 3; } + if (newMaxScale > 300) { + newMaxScale = 300; + } + setMaxScale(newMaxScale); if (newMaxScale - minScale < 2) { @@ -204,7 +225,7 @@ export function RightSidebar() { slotProps={{ input: { min: 3, - max: 10, + max: 300, }, }} /> @@ -268,6 +289,62 @@ export function RightSidebar() { }} /> + + Размер иконок: {iconSize}% + + + { + if (typeof value === "number") { + handleIconSizeChange(value); + } + }} + min={50} + max={300} + step={1} + sx={{ + color: "#fff", + "& .MuiSlider-thumb": { + backgroundColor: "#fff", + }, + "& .MuiSlider-track": { + backgroundColor: "#fff", + }, + "& .MuiSlider-rail": { + backgroundColor: "#666", + }, + }} + /> + + + Размер шрифта: {fontSize}% + + + { + if (typeof value === "number") { + handleFontSizeChange(value); + } + }} + min={50} + max={300} + step={1} + sx={{ + color: "#fff", + "& .MuiSlider-thumb": { + backgroundColor: "#fff", + }, + "& .MuiSlider-track": { + backgroundColor: "#fff", + }, + "& .MuiSlider-rail": { + backgroundColor: "#666", + }, + }} + /> + { setIsUserEditing(true); setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) })); - pan({ x: Number(e.target.value), y: localCenter.y }); }} - onBlur={() => setIsUserEditing(false)} + onBlur={() => { + setIsUserEditing(false); + }} style={{ backgroundColor: "#222", borderRadius: 4 }} sx={{ "& .MuiInputLabel-root": { @@ -334,9 +412,10 @@ export function RightSidebar() { onChange={(e) => { setIsUserEditing(true); setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) })); - pan({ x: localCenter.x, y: Number(e.target.value) }); }} - onBlur={() => setIsUserEditing(false)} + onBlur={() => { + setIsUserEditing(false); + }} style={{ backgroundColor: "#222", borderRadius: 4 }} sx={{ "& .MuiInputLabel-root": { diff --git a/src/pages/Route/route-preview/index.tsx b/src/pages/Route/route-preview/index.tsx index 395b1d0..adfc1e8 100644 --- a/src/pages/Route/route-preview/index.tsx +++ b/src/pages/Route/route-preview/index.tsx @@ -9,7 +9,7 @@ import { TilingSprite, Text, } from "pixi.js"; -import { Stack } from "@mui/material"; +import { Box, Stack } from "@mui/material"; import { MapDataProvider, useMapData } from "./MapDataContext"; import { TransformProvider, useTransform } from "./TransformContext"; import { InfiniteCanvas } from "./InfiniteCanvas"; @@ -26,6 +26,7 @@ import { Sight } from "./Sight"; import { SightData } from "./types"; import { Station } from "./Station"; import { UP_SCALE } from "./Constants"; +import { WebGLRouteMapPrototype } from "./webgl-prototype/WebGLRouteMapPrototype"; import CircularProgress from "@mui/material/CircularProgress"; extend({ @@ -51,14 +52,33 @@ const Loading = () => { return null; }; export const RoutePreview = () => { - const { routeData, stationData, sightData } = useMapData(); + const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true); return ( - {routeData && stationData && sightData ? : null} - + + setIsLeftSidebarOpen((prev) => !prev)} + /> + @@ -165,8 +185,7 @@ export const RouteMap = observer(() => { return (
- - + {/* {stationData[language].map((obj, index) => ( @@ -184,7 +203,8 @@ export const RouteMap = observer(() => { return ; })} - + */} +
); }); diff --git a/src/pages/Route/route-preview/types.ts b/src/pages/Route/route-preview/types.ts index d99942f..37d8751 100644 --- a/src/pages/Route/route-preview/types.ts +++ b/src/pages/Route/route-preview/types.ts @@ -3,6 +3,8 @@ export interface RouteData { carrier_id: number; center_latitude: number; center_longitude: number; + icon_size: number; + font_size: number; governor_appeal: number; id: number; path: [number, number][]; diff --git a/src/pages/Route/route-preview/web-gl/LanguageSelector.tsx b/src/pages/Route/route-preview/web-gl/LanguageSelector.tsx new file mode 100644 index 0000000..ef92193 --- /dev/null +++ b/src/pages/Route/route-preview/web-gl/LanguageSelector.tsx @@ -0,0 +1,217 @@ +import { useEffect, useMemo, useRef, useState, type ReactElement } from "react"; +import { observer } from "mobx-react-lite"; +import { languageStore } from "@shared"; + +const LANGUAGES = ["ru", "zh", "en"] as const; +type Language = (typeof LANGUAGES)[number]; + +type LanguageSelectorProps = { + onBack?: () => void; + isSidebarOpen?: boolean; +}; + +const renderLanguageIcon = (lang: Language): ReactElement => { + switch (lang) { + case "ru": + return ( + + + + + + ); + case "zh": + return ( + + + + + + + + ); + case "en": + default: + return ( + + + + + ); + } +}; + +const CollapsedIcon = () => ( + + + + +); + +const ArrowIcon = ({ rotation }: { rotation: number }) => ( + + + + +); + +const LanguageSelector = observer( + ({ onBack, isSidebarOpen = true }: LanguageSelectorProps) => { + const { language, setLanguage } = languageStore; + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + if (!isOpen) { + return; + } + + const handleOutside = (event: PointerEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener("pointerdown", handleOutside); + return () => { + document.removeEventListener("pointerdown", handleOutside); + }; + }, [isOpen]); + + const handleSelect = (code: Language) => { + setLanguage(code); + setIsOpen(false); + }; + + const toggle = () => setIsOpen((prev) => !prev); + + const containerWidth = useMemo(() => { + const BUTTON_SIZE = 56; + const GAP = 8; + const backWidth = onBack ? BUTTON_SIZE + GAP : 0; + const toggleWidth = BUTTON_SIZE; + const collapsedWidth = backWidth + toggleWidth + BUTTON_SIZE; + const expandedWidth = + backWidth + + toggleWidth + + LANGUAGES.length * BUTTON_SIZE + + (LANGUAGES.length - 1) * GAP; + return isOpen ? expandedWidth : collapsedWidth; + }, [isOpen, onBack]); + + return ( +
+
+
+ + {isOpen ? ( + LANGUAGES.map((lang) => ( + + )) + ) : ( +
+ +
+ )} +
+
+
+ ); + } +); + +export default LanguageSelector; diff --git a/src/pages/Route/route-preview/web-gl/web-gl-version.tsx b/src/pages/Route/route-preview/web-gl/WebGLMap.tsx similarity index 89% rename from src/pages/Route/route-preview/web-gl/web-gl-version.tsx rename to src/pages/Route/route-preview/web-gl/WebGLMap.tsx index 8c4190b..e9f65d1 100644 --- a/src/pages/Route/route-preview/web-gl/web-gl-version.tsx +++ b/src/pages/Route/route-preview/web-gl/WebGLMap.tsx @@ -1,3 +1,24 @@ +import React, { useEffect, useMemo, useRef, useCallback } from "react"; +import { observer } from "mobx-react-lite"; +import { useMapData } from "./MapDataContext"; +import { useTransform } from "./transformContext"; +import { coordinatesToLocal } from "./utils"; +import { + UP_SCALE, + PATH_COLOR, + BACKGROUND_COLOR, + UNPASSED_STATION_COLOR, + BUS_COLOR, +} from "./Constants"; +import { SCALE_FACTOR } from "../../assets/Constants"; +import { apiStore } from "../../api/ApiStore/store"; +import { useGeolocationStore } from "../../stores/hooks/useGeolocationStore"; +import { useAnimatedPolarPosition } from "../../hooks/useAnimatedPosition"; +import { useCameraAnimationStore } from "../../stores"; +import { TramIconWebGL } from "./TramIconWebGL"; +const sightIcon = new URL("../../assets/images/sight.svg", import.meta.url) + .href; + function initWebGLContext( canvas: HTMLCanvasElement ): WebGLRenderingContext | null { @@ -53,11 +74,13 @@ export const WebGLMap = observer(() => { const cameraAnimationStore = useCameraAnimationStore(); + // Ref для хранения ограничений масштаба const scaleLimitsRef = useRef({ min: null as number | null, max: null as number | null, }); + // Обновляем ограничения масштаба при изменении routeData useEffect(() => { if ( routeData?.scale_min !== undefined && @@ -70,6 +93,7 @@ export const WebGLMap = observer(() => { } }, [routeData?.scale_min, routeData?.scale_max]); + // Функция для ограничения масштаба значениями с бекенда const clampScale = useCallback((value: number) => { const { min, max } = scaleLimitsRef.current; @@ -87,6 +111,7 @@ export const WebGLMap = observer(() => { const setPositionRef = useRef(setPosition); const setScaleRef = useRef(setScale); + // Обновляем refs при изменении функций useEffect(() => { setPositionRef.current = setPosition; }, [setPosition]); @@ -95,6 +120,7 @@ export const WebGLMap = observer(() => { setScaleRef.current = setScale; }, [setScale]); + // Логирование данных маршрута для отладки useEffect(() => { if (routeData) { } @@ -119,6 +145,7 @@ export const WebGLMap = observer(() => { setPositionImmediate: setYellowDotPositionImmediate, } = useAnimatedPolarPosition(0, 0, 800); + // Build transformed route path (map coords) const routePath = useMemo(() => { if (!routeData?.path || routeData?.path.length === 0) return new Float32Array(); @@ -174,6 +201,7 @@ export const WebGLMap = observer(() => { rotationAngle, ]); + // Настройка CameraAnimationStore callback - только один раз при монтировании useEffect(() => { const callback = (newPos: { x: number; y: number }, newZoom: number) => { setPosition(newPos); @@ -182,13 +210,15 @@ export const WebGLMap = observer(() => { cameraAnimationStore.setUpdateCallback(callback); + // Синхронизируем начальное состояние только один раз cameraAnimationStore.syncState(position, scale); return () => { cameraAnimationStore.setUpdateCallback(null); }; - }, []); + }, []); // Пустой массив - выполняется только при монтировании + // Установка границ зума useEffect(() => { if ( routeData?.scale_min !== undefined && @@ -199,23 +229,28 @@ export const WebGLMap = observer(() => { } }, [routeData?.scale_min, routeData?.scale_max, cameraAnimationStore]); + // Автоматический режим - таймер для включения через 5 секунд бездействия useEffect(() => { const interval = setInterval(() => { const timeSinceActivity = Date.now() - userActivityTimestamp; if (timeSinceActivity >= 5000 && !isAutoMode) { + // 5 секунд бездействия - включаем авто режим setIsAutoMode(true); } - }, 1000); + }, 1000); // Проверяем каждую секунду return () => clearInterval(interval); }, [userActivityTimestamp, isAutoMode, setIsAutoMode]); + // Следование за желтой точкой с зумом при включенном авто режиме useEffect(() => { + // Пропускаем обновление если анимация уже идет if (cameraAnimationStore.isActivelyAnimating) { return; } if (isAutoMode && transformedTramCoords && screenCenter) { + // Преобразуем станции в формат для CameraAnimationStore const transformedStations = stationData ? stationData .map((station: any) => { @@ -256,8 +291,10 @@ export const WebGLMap = observer(() => { cameraAnimationStore.setMaxZoom(scaleLimitsRef.current!.max); cameraAnimationStore.setMinZoom(scaleLimitsRef.current!.min); + // Синхронизируем текущее состояние камеры перед запуском анимации cameraAnimationStore.syncState(positionRef.current, scaleRef.current); + // Запускаем анимацию к желтой точке cameraAnimationStore.followTram( transformedTramCoords, screenCenter, @@ -277,6 +314,7 @@ export const WebGLMap = observer(() => { rotationAngle, ]); + // Station label overlay positions (DOM overlay) const stationLabels = useMemo(() => { if (!stationData || !routeData) return [] as Array<{ x: number; y: number; name: string; sub?: string }>; @@ -339,6 +377,7 @@ export const WebGLMap = observer(() => { selectedLanguage as any, ]); + // Build transformed stations (map coords) const stationPoints = useMemo(() => { if (!stationData || !routeData) return new Float32Array(); const centerLat = routeData.center_latitude; @@ -368,6 +407,7 @@ export const WebGLMap = observer(() => { rotationAngle, ]); + // Build transformed sights (map coords) const sightPoints = useMemo(() => { if (!sightData || !routeData) return new Float32Array(); const centerLat = routeData.center_latitude; @@ -511,6 +551,8 @@ export const WebGLMap = observer(() => { const handleResize = () => { const changed = resizeCanvasToDisplaySize(canvas); if (!gl) return; + // Update screen center when canvas size changes + // Use physical pixels (canvas.width) instead of CSS pixels setScreenCenter({ x: canvas.width / 2, y: canvas.height / 2, @@ -546,6 +588,7 @@ export const WebGLMap = observer(() => { const rx = x * cos - y * sin; const ry = x * sin + y * cos; + // В авторежиме используем анимацию, иначе мгновенное обновление if (isAutoMode) { animateYellowDotTo(rx, ry); } else { @@ -644,18 +687,21 @@ export const WebGLMap = observer(() => { const vertexCount = routePath.length / 2; if (vertexCount > 1) { + // Generate thick line geometry using triangles with proper joins const generateThickLine = (points: Float32Array, width: number) => { const vertices: number[] = []; const halfWidth = width / 2; if (points.length < 4) return new Float32Array(); + // Process each segment for (let i = 0; i < points.length - 2; i += 2) { const x1 = points[i]; const y1 = points[i + 1]; const x2 = points[i + 2]; const y2 = points[i + 3]; + // Calculate perpendicular vector const dx = x2 - x1; const dy = y2 - y1; const length = Math.sqrt(dx * dx + dy * dy); @@ -664,14 +710,18 @@ export const WebGLMap = observer(() => { const perpX = (-dy / length) * halfWidth; const perpY = (dx / length) * halfWidth; + // Create quad (two triangles) for this line segment + // Triangle 1 vertices.push(x1 + perpX, y1 + perpY); vertices.push(x1 - perpX, y1 - perpY); vertices.push(x2 + perpX, y2 + perpY); + // Triangle 2 vertices.push(x1 - perpX, y1 - perpY); vertices.push(x2 - perpX, y2 - perpY); vertices.push(x2 + perpX, y2 + perpY); + // Add simple join triangles to fill gaps if (i < points.length - 4) { const x3 = points[i + 4]; const y3 = points[i + 5]; @@ -683,6 +733,7 @@ export const WebGLMap = observer(() => { const perpX2 = (-dy2 / length2) * halfWidth; const perpY2 = (dx2 / length2) * halfWidth; + // Simple join - just connect the endpoints vertices.push(x2 + perpX, y2 + perpY); vertices.push(x2 - perpX, y2 - perpY); vertices.push(x2 + perpX2, y2 + perpY2); @@ -704,18 +755,22 @@ export const WebGLMap = observer(() => { gl.uniform4f(uniforms.u_color, r1, g1, b1, 1); if (tramSegIndex >= 0) { + // Используем точную позицию желтой точки для определения конца красной линии const animatedPos = animatedYellowDotPosition; if ( animatedPos && animatedPos.x !== undefined && animatedPos.y !== undefined ) { + // Создаем массив точек от начала маршрута до позиции желтой точки const passedPoints: number[] = []; + // Добавляем все точки до текущего сегмента for (let i = 0; i <= tramSegIndex; i++) { passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]); } + // Добавляем точную позицию желтой точки как конечную точку passedPoints.push(animatedPos.x, animatedPos.y); if (passedPoints.length >= 4) { @@ -734,6 +789,7 @@ export const WebGLMap = observer(() => { const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255; gl.uniform4f(uniforms.u_color, r2, g2, b2, 1); + // Серая линия начинается точно от позиции желтой точки const animatedPos = animatedYellowDotPosition; if ( animatedPos && @@ -742,8 +798,10 @@ export const WebGLMap = observer(() => { ) { const unpassedPoints: number[] = []; + // Добавляем позицию желтой точки как начальную точку серой линии unpassedPoints.push(animatedPos.x, animatedPos.y); + // Добавляем все точки после текущего сегмента for (let i = tramSegIndex + 1; i < vertexCount; i++) { unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]); } @@ -759,6 +817,7 @@ export const WebGLMap = observer(() => { } } + // Draw stations if (stationPoints.length > 0) { gl.useProgram(pprog); const a_pos_pts = gl.getAttribLocation(pprog, "a_pos"); @@ -776,6 +835,7 @@ export const WebGLMap = observer(() => { gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); + // Draw station outlines (black background) gl.uniform1f(u_pointSize, 10 * scale * 1.5); const r_outline = ((BACKGROUND_COLOR >> 16) & 0xff) / 255; const g_outline = ((BACKGROUND_COLOR >> 8) & 0xff) / 255; @@ -783,12 +843,15 @@ export const WebGLMap = observer(() => { gl.uniform4f(u_color_pts, r_outline, g_outline, b_outline, 1); gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2); + // Draw station cores (colored based on passed/unpassed) gl.uniform1f(u_pointSize, 8.0 * scale * 1.5); + // Draw passed stations (red) if (tramSegIndex >= 0) { const passedStations = []; for (let i = 0; i < stationData.length; i++) { if (i <= tramSegIndex) { + // @ts-ignore passedStations.push(stationPoints[i * 2], stationPoints[i * 2 + 1]); } } @@ -806,11 +869,13 @@ export const WebGLMap = observer(() => { } } + // Draw unpassed stations (gray) if (tramSegIndex >= 0) { const unpassedStations = []; for (let i = 0; i < stationData.length; i++) { if (i > tramSegIndex) { unpassedStations.push( + // @ts-ignore stationPoints[i * 2], stationPoints[i * 2 + 1] ); @@ -829,6 +894,7 @@ export const WebGLMap = observer(() => { gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2); } } else { + // If no tram position, draw all stations as unpassed const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255; const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255; const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255; @@ -970,6 +1036,7 @@ export const WebGLMap = observer(() => { if (passedStations.length) gl.drawArrays(gl.POINTS, 0, passedStations.length / 2); + // Draw black dots with white outline for terminal stations (startStopId and endStopId) - 5x larger if ( stationData && stationData.length > 0 && @@ -982,6 +1049,7 @@ export const WebGLMap = observer(() => { const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); + // Find terminal stations using startStopId and endStopId from context const startStationData = stationData.find( (station) => station.id.toString() === apiStore.context?.startStopId ); @@ -991,6 +1059,7 @@ export const WebGLMap = observer(() => { const terminalStations: number[] = []; + // Transform start station coordinates if found if (startStationData) { const startLocal = coordinatesToLocal( startStationData.latitude - centerLat, @@ -1003,6 +1072,7 @@ export const WebGLMap = observer(() => { terminalStations.push(startRx, startRy); } + // Transform end station coordinates if found if (endStationData) { const endLocal = coordinatesToLocal( endStationData.latitude - centerLat, @@ -1016,10 +1086,12 @@ export const WebGLMap = observer(() => { } if (terminalStations.length > 0) { + // Determine if each terminal station is passed const terminalStationData: any[] = []; if (startStationData) terminalStationData.push(startStationData); if (endStationData) terminalStationData.push(endStationData); + // Get tram segment index for comparison let tramSegIndex = -1; const coords: any = apiStore?.context?.currentCoordinates; if (coords && centerLat !== undefined && centerLon !== undefined) { @@ -1034,6 +1106,7 @@ export const WebGLMap = observer(() => { const tx = wx * cosR - wy * sinR; const ty = wx * sinR + wy * cosR; + // Find closest segment to tram position let best = -1; let bestD = Infinity; for (let i = 0; i < routePath.length - 2; i += 2) { @@ -1058,6 +1131,7 @@ export const WebGLMap = observer(() => { tramSegIndex = best; } + // Check if each terminal station is passed const isStartPassed = startStationData ? (() => { const sx = terminalStations[0]; @@ -1133,41 +1207,46 @@ export const WebGLMap = observer(() => { gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); + // Draw colored outline based on passed status - 24 pixels (x2) gl.uniform1f(u_pointSize, 18.0 * scale); if (startStationData && endStationData) { + // Both stations - draw each with its own color if (isStartPassed) { - gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); + gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных } else { - gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); + gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных } - gl.drawArrays(gl.POINTS, 0, 1); + gl.drawArrays(gl.POINTS, 0, 1); // Draw start station if (isEndPassed) { - gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); + gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных } else { - gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); + gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных } - gl.drawArrays(gl.POINTS, 1, 1); + gl.drawArrays(gl.POINTS, 1, 1); // Draw end station } else { + // Single station - use appropriate color const isPassed = startStationData ? isStartPassed : isEndPassed; if (isPassed) { - gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); + gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных } else { - gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); + gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных } gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2); } + // Draw dark center - 12 pixels (x2) gl.uniform1f(u_pointSize, 11.0 * scale); const r_center = ((BACKGROUND_COLOR >> 16) & 0xff) / 255; const g_center = ((BACKGROUND_COLOR >> 8) & 0xff) / 255; const b_center = (BACKGROUND_COLOR & 0xff) / 255; - gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0); + gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0); // Dark color gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2); } } } + // Draw yellow dot for tram position if (animatedYellowDotPosition) { const rx = animatedYellowDotPosition.x; const ry = animatedYellowDotPosition.y; @@ -1269,6 +1348,7 @@ export const WebGLMap = observer(() => { }); const onPointerDown = (e: PointerEvent) => { + // Отслеживаем активность пользователя updateUserActivity(); if (isAutoMode) { setIsAutoMode(false); @@ -1301,6 +1381,7 @@ export const WebGLMap = observer(() => { const onPointerMove = (e: PointerEvent) => { if (!activePointers.has(e.pointerId)) return; + // Отслеживаем активность пользователя updateUserActivity(); const rect = canvas.getBoundingClientRect(); @@ -1326,6 +1407,7 @@ export const WebGLMap = observer(() => { }; } + // Process the pinch gesture if (pinchStart) { const currentDistance = getDistance(p1, p2); const zoomFactor = currentDistance / pinchStart.distance; @@ -1344,6 +1426,7 @@ export const WebGLMap = observer(() => { } else if (isDragging && activePointers.size === 1) { const p = Array.from(activePointers.values())[0]; + // Проверяем валидность значений if ( !startMouse || !startPos || @@ -1371,6 +1454,7 @@ export const WebGLMap = observer(() => { }; const onPointerUp = (e: PointerEvent) => { + // Отслеживаем активность пользователя updateUserActivity(); canvas.releasePointerCapture(e.pointerId); @@ -1390,6 +1474,7 @@ export const WebGLMap = observer(() => { }; const onPointerCancel = (e: PointerEvent) => { + // Handle pointer cancellation (e.g., when touch is interrupted) updateUserActivity(); canvas.releasePointerCapture(e.pointerId); activePointers.delete(e.pointerId); @@ -1403,6 +1488,7 @@ export const WebGLMap = observer(() => { const onWheel = (e: WheelEvent) => { e.preventDefault(); + // Отслеживаем активность пользователя updateUserActivity(); if (isAutoMode) { setIsAutoMode(false); @@ -1410,6 +1496,7 @@ export const WebGLMap = observer(() => { cameraAnimationStore.stopAnimation(); const rect = canvas.getBoundingClientRect(); + // Convert mouse coordinates from CSS pixels to physical canvas pixels const mouseX = (e.clientX - rect.left) * (canvas.width / canvas.clientWidth); const mouseY = @@ -1516,6 +1603,7 @@ export const WebGLMap = observer(() => { const sy = (ry * scale + position.y) / dpr; const size = 30; + // Обработчик клика для выбора достопримечательности const handleSightClick = () => { const { setSelectedSightId, diff --git a/src/pages/Route/route-preview/webgl-prototype/WebGLRouteMapPrototype.tsx b/src/pages/Route/route-preview/webgl-prototype/WebGLRouteMapPrototype.tsx new file mode 100644 index 0000000..b203455 --- /dev/null +++ b/src/pages/Route/route-preview/webgl-prototype/WebGLRouteMapPrototype.tsx @@ -0,0 +1,1786 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { PointerEvent as ReactPointerEvent } from "react"; +import { observer } from "mobx-react-lite"; + +import { useMapData } from "../MapDataContext"; +import { useTransform } from "../TransformContext"; +import { coordinatesToLocal, localToCoordinates } from "../utils"; +import { + BACKGROUND_COLOR, + PATH_COLOR, + SCALE_FACTOR, + UP_SCALE, +} from "../Constants"; +import { languageStore } from "@shared"; +import { SightData } from "../types"; + +const SIGHT_ICON_URL = "/sight_icon.svg"; + +type Vec2 = { x: number; y: number }; + +type Transform = { + scale: number; + translation: Vec2; +}; + +type ProgramBundle = { + program: WebGLProgram; + attribLocations: { + a_position: number; + }; + uniformLocations: { + u_translation: WebGLUniformLocation | null; + u_scale: WebGLUniformLocation | null; + u_resolution: WebGLUniformLocation | null; + u_color: WebGLUniformLocation | null; + u_pointSize?: WebGLUniformLocation | null; + }; +}; + +const clamp = (value: number, min: number, max: number) => + Math.min(max, Math.max(min, value)); + +const toColor = (value: number): [number, number, number] => [ + ((value >> 16) & 0xff) / 255, + ((value >> 8) & 0xff) / 255, + (value & 0xff) / 255, +]; + +const lineVertexSource = ` + attribute vec2 a_position; + uniform vec2 u_translation; + uniform float u_scale; + uniform vec2 u_resolution; + + void main() { + vec2 screen = a_position * u_scale + u_translation; + vec2 zeroToOne = screen / u_resolution; + vec2 clip = zeroToOne * 2.0 - 1.0; + gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); + } +`; + +const lineFragmentSource = ` + precision mediump float; + uniform vec4 u_color; + + void main() { + gl_FragColor = u_color; + } +`; + +const pointVertexSource = ` + attribute vec2 a_position; + uniform vec2 u_translation; + uniform float u_scale; + uniform vec2 u_resolution; + uniform float u_pointSize; + + void main() { + vec2 screen = a_position * u_scale + u_translation; + vec2 zeroToOne = screen / u_resolution; + vec2 clip = zeroToOne * 2.0 - 1.0; + gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); + gl_PointSize = u_pointSize; + } +`; + +const pointFragmentSource = ` + precision mediump float; + uniform vec4 u_color; + + void main() { + vec2 centered = gl_PointCoord * 2.0 - 1.0; + float dist = dot(centered, centered); + if (dist > 1.0) { + discard; + } + gl_FragColor = u_color; + } +`; + +function compileShader( + gl: WebGLRenderingContext, + type: number, + source: string +) { + const shader = gl.createShader(type); + if (!shader) { + throw new Error("Failed to create shader"); + } + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const info = gl.getShaderInfoLog(shader); + gl.deleteShader(shader); + throw new Error(`Shader compilation failed: ${info ?? "unknown error"}`); + } + return shader; +} + +function createProgram( + gl: WebGLRenderingContext, + vertexSource: string, + fragmentSource: string +): ProgramBundle { + const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource); + const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource); + + const program = gl.createProgram(); + if (!program) { + throw new Error("Failed to create program"); + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const info = gl.getProgramInfoLog(program); + gl.deleteProgram(program); + throw new Error(`Program link failed: ${info ?? "unknown error"}`); + } + + const attribLocations = { + a_position: gl.getAttribLocation(program, "a_position"), + }; + + const uniformLocations = { + u_translation: gl.getUniformLocation(program, "u_translation"), + u_scale: gl.getUniformLocation(program, "u_scale"), + u_resolution: gl.getUniformLocation(program, "u_resolution"), + u_color: gl.getUniformLocation(program, "u_color"), + u_pointSize: gl.getUniformLocation(program, "u_pointSize"), + }; + + return { program, attribLocations, uniformLocations }; +} + +const computeWorldVertices = (path?: [number, number][]): Float32Array => { + if (!path || path.length === 0) { + return new Float32Array(); + } + + const verts: number[] = []; + for (const [latitude, longitude] of path) { + const local = coordinatesToLocal(latitude, longitude); + verts.push(local.x * UP_SCALE, local.y * UP_SCALE); + } + return new Float32Array(verts); +}; + +const computeStationVertices = ( + stations?: Array<{ latitude: number; longitude: number }> +): Float32Array => { + if (!stations || stations.length === 0) { + return new Float32Array(); + } + + const verts: number[] = []; + for (const station of stations) { + const local = coordinatesToLocal(station.latitude, station.longitude); + verts.push(local.x * UP_SCALE, local.y * UP_SCALE); + } + return new Float32Array(verts); +}; + +const rotateVertices = ( + vertices: Float32Array, + angle: number +): Float32Array => { + if (!vertices || vertices.length === 0 || Math.abs(angle) < 1e-6) { + return vertices; + } + + const cos = Math.cos(angle); + const sin = Math.sin(angle); + const rotated = new Float32Array(vertices.length); + + for (let i = 0; i < vertices.length; i += 2) { + const x = vertices[i]; + const y = vertices[i + 1]; + rotated[i] = x * cos - y * sin; + rotated[i + 1] = x * sin + y * cos; + } + + return rotated; +}; + +const DRAG_THRESHOLD_PX = 4; + +type StationDragState = { + stationId: number; + pointerId: number; + rotatedBase: Vec2; + camera: Transform; + captureTarget: HTMLElement | null; + lastOffset: Vec2; + initialPointer: Vec2; + hasMoved: boolean; + pointerDelta: Vec2; +}; + +type SightDragState = { + sight: SightData; + pointerId: number; + camera: Transform; + offset: Vec2; + rotation: number; + initialClient: Vec2; + hasMoved: boolean; + captureTarget: HTMLElement | null; + lastCoordinates: { latitude: number; longitude: number }; + rotatedBase: Vec2; + pointerDelta: Vec2; +}; + +type SightLivePosition = { + latitude: number; + longitude: number; + offsetX: number; + offsetY: number; +}; + +const generateThickLineGeometry = ( + points: Float32Array, + width: number +): Float32Array => { + if (points.length < 4 || width <= 0) { + return new Float32Array(); + } + + const halfWidth = width / 2; + const vertices: number[] = []; + + for (let i = 0; i < points.length - 2; i += 2) { + const x1 = points[i]; + const y1 = points[i + 1]; + const x2 = points[i + 2]; + const y2 = points[i + 3]; + + const dx = x2 - x1; + const dy = y2 - y1; + const length = Math.hypot(dx, dy); + if (length === 0) { + continue; + } + + const ux = dx / length; + const uy = dy / length; + const px = -uy * halfWidth; + const py = ux * halfWidth; + + const v1x = x1 + px; + const v1y = y1 + py; + const v2x = x1 - px; + const v2y = y1 - py; + const v3x = x2 + px; + const v3y = y2 + py; + const v4x = x2 - px; + const v4y = y2 - py; + + vertices.push(v1x, v1y, v2x, v2y, v3x, v3y); + vertices.push(v2x, v2y, v4x, v4y, v3x, v3y); + + if (i < points.length - 4) { + const x3 = points[i + 4]; + const y3 = points[i + 5]; + const dx2 = x3 - x2; + const dy2 = y3 - y2; + const length2 = Math.hypot(dx2, dy2); + if (length2 > 0) { + const ux2 = dx2 / length2; + const uy2 = dy2 / length2; + const px2 = -uy2 * halfWidth; + const py2 = ux2 * halfWidth; + + const joint1x = x2 + px; + const joint1y = y2 + py; + const joint2x = x2 - px; + const joint2y = y2 - py; + const joint3x = x2 + px2; + const joint3y = y2 + py2; + const joint4x = x2 - px2; + const joint4y = y2 - py2; + + vertices.push(joint1x, joint1y, joint2x, joint2y, joint3x, joint3y); + vertices.push(joint2x, joint2y, joint4x, joint4y, joint3x, joint3y); + } + } + } + + return new Float32Array(vertices); +}; + +const computeViewTransform = ( + vertices: Float32Array, + width: number, + height: number +): Transform => { + if (vertices.length < 4) { + return { + scale: 1, + translation: { x: width * 0.5, y: height * 0.5 }, + }; + } + + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + for (let i = 0; i < vertices.length; i += 2) { + const x = vertices[i]; + const y = vertices[i + 1]; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + const worldWidth = Math.max(1, maxX - minX); + const worldHeight = Math.max(1, maxY - minY); + + const padding = 0.1; + const scale = Math.min( + (width * (1 - padding)) / worldWidth, + (height * (1 - padding)) / worldHeight + ); + + const centerX = (minX + maxX) * 0.5; + const centerY = (minY + maxY) * 0.5; + + const translation = { + x: width * 0.5 - centerX * scale, + y: height * 0.5 - centerY * scale, + }; + + return { scale, translation }; +}; + +const backgroundColor = toColor(BACKGROUND_COLOR); +const pathColor = toColor(PATH_COLOR); + +export const WebGLRouteMapPrototype = observer(() => { + const { + originalRouteData, + routeData, + stationData, + sightData, + setSelectedSight, + setStationOffset, + setSightCoordinates, + } = useMapData(); + const { language } = languageStore; + const { setScale: setSharedScale, scale: sharedScale } = useTransform(); + + const containerRef = useRef(null); + const canvasRef = useRef(null); + const glRef = useRef(null); + + const lineProgramRef = useRef(null); + const lineBufferRef = useRef(null); + const pointProgramRef = useRef(null); + const pointBufferRef = useRef(null); + + const transformRef = useRef(null); + const lastTransformRef = useRef(null); + const [transformState, setTransformState] = useState(null); + const clampTransformScale = useCallback((transform: Transform): Transform => { + const { min, max } = scaleLimitsRef.current; + const clampedScale = clamp(transform.scale, min, max); + if (clampedScale === transform.scale) { + lastTransformRef.current = transform; + return transform; + } + + const canvas = canvasRef.current; + if (!canvas || canvas.width === 0 || canvas.height === 0) { + const adjusted = { ...transform, scale: clampedScale }; + lastTransformRef.current = adjusted; + return adjusted; + } + + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const worldCenterX = (centerX - transform.translation.x) / transform.scale; + const worldCenterY = (centerY - transform.translation.y) / transform.scale; + + const adjusted = { + scale: clampedScale, + translation: { + x: centerX - worldCenterX * clampedScale, + y: centerY - worldCenterY * clampedScale, + }, + }; + lastTransformRef.current = adjusted; + return adjusted; + }, []); + + const scaleLimitsRef = useRef({ min: 0.1, max: 100 }); + const drawSceneRef = useRef<() => void>(() => {}); + + const activePointersRef = useRef>(new Map()); + const dragStateRef = useRef<{ lastPos: Vec2 } | null>(null); + const pinchStateRef = useRef<{ + initialDistance: number; + initialScale: number; + worldMidpoint: Vec2; + } | null>(null); + + const stationDragStateRef = useRef(null); + const sightDragStateRef = useRef(null); + const suppressAutoFitRef = useRef(false); + const skipNextAutoFitRef = useRef(false); + const [liveStationOffsets, setLiveStationOffsets] = useState< + Map + >(new Map()); + const [liveSightPositions, setLiveSightPositions] = useState< + Map + >(new Map()); + const lastCenterRef = useRef<{ + latitude: number | null; + longitude: number | null; + }>({ latitude: null, longitude: null }); + const lastAppliedCenterRef = useRef<{ + latitude: number | null; + longitude: number | null; + }>({ latitude: null, longitude: null }); + + const getRelativePointerPosition = useCallback( + (clientX: number, clientY: number) => { + const container = containerRef.current; + if (!container) return null; + const rect = container.getBoundingClientRect(); + return { + x: clientX - rect.left, + y: clientY - rect.top, + }; + }, + [] + ); + + const getWorldPosition = useCallback( + (clientX: number, clientY: number, camera: Transform) => { + const relative = getRelativePointerPosition(clientX, clientY); + if (!relative) return null; + const dpr = Math.max(1, window.devicePixelRatio || 1); + return { + x: (relative.x * dpr - camera.translation.x) / camera.scale, + y: (relative.y * dpr - camera.translation.y) / camera.scale, + }; + }, + [getRelativePointerPosition] + ); + + const [canvasSize, setCanvasSize] = useState<{ + width: number; + height: number; + }>({ width: 0, height: 0 }); + + const routeVertices = useMemo( + () => computeWorldVertices(originalRouteData?.path), + [originalRouteData?.path] + ); + + const stationVertices = useMemo( + () => + computeStationVertices( + stationData?.ru?.map((station) => ({ + latitude: station.latitude, + longitude: station.longitude, + })) + ), + [stationData?.ru] + ); + + const rotationAngle = useMemo(() => { + const deg = routeData?.rotate ?? originalRouteData?.rotate ?? 0; + return (deg * Math.PI) / 180; + }, [routeData?.rotate, originalRouteData?.rotate]); + + const computeCenterCoordinates = useCallback( + (transform: Transform) => { + const canvas = canvasRef.current; + if (!canvas) return; + + const width = canvas.width || canvas.clientWidth; + const height = canvas.height || canvas.clientHeight; + if (!width || !height) return; + + const worldX = (width / 2 - transform.translation.x) / transform.scale; + const worldY = (height / 2 - transform.translation.y) / transform.scale; + + const cos = Math.cos(rotationAngle); + const sin = Math.sin(rotationAngle); + const unrotatedX = worldX * cos + worldY * sin; + const unrotatedY = -worldX * sin + worldY * cos; + + const localX = unrotatedX / UP_SCALE; + const localY = unrotatedY / UP_SCALE; + + const { latitude, longitude } = localToCoordinates(localX, localY); + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { + return; + } + + lastCenterRef.current = { + latitude: Math.round(latitude * 1e6) / 1e6, + longitude: Math.round(longitude * 1e6) / 1e6, + }; + }, + [rotationAngle] + ); + + const updateTransform = useCallback( + (next: Transform) => { + const adjusted = clampTransformScale(next); + transformRef.current = adjusted; + setTransformState(adjusted); + setSharedScale(adjusted.scale); + computeCenterCoordinates(adjusted); + }, + [clampTransformScale, setSharedScale, computeCenterCoordinates] + ); + + const rotatedRouteVertices = useMemo( + () => rotateVertices(routeVertices, rotationAngle), + [routeVertices, rotationAngle] + ); + + const rotatedStationVertices = useMemo( + () => rotateVertices(stationVertices, rotationAngle), + [stationVertices, rotationAngle] + ); + + const resetTransform = useCallback(() => { + transformRef.current = null; + setTransformState(null); + }, []); + + const handleStationPointerMove = useCallback( + (event: PointerEvent) => { + const state = stationDragStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + if (!state.hasMoved) { + const pointerDx = event.clientX - state.initialPointer.x; + const pointerDy = event.clientY - state.initialPointer.y; + if (Math.hypot(pointerDx, pointerDy) >= DRAG_THRESHOLD_PX) { + state.hasMoved = true; + } + } + + if (!state.hasMoved) { + return; + } + + event.preventDefault(); + + const world = getWorldPosition( + event.clientX, + event.clientY, + state.camera + ); + if (!world) return; + + const adjustedWorldX = world.x - state.pointerDelta.x; + const adjustedWorldY = world.y - state.pointerDelta.y; + + const newOffsetX = adjustedWorldX - state.rotatedBase.x; + const newOffsetY = adjustedWorldY - state.rotatedBase.y; + + state.lastOffset = { x: newOffsetX, y: newOffsetY }; + setLiveStationOffsets((prev) => { + const next = new Map(prev); + next.set(state.stationId, { x: newOffsetX, y: newOffsetY }); + return next; + }); + }, + [getWorldPosition, setLiveStationOffsets] + ); + + const handleStationPointerUp = useCallback( + (event: PointerEvent) => { + const state = stationDragStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + if (state.captureTarget && state.captureTarget.releasePointerCapture) { + state.captureTarget.releasePointerCapture(event.pointerId); + state.captureTarget.style.cursor = "grab"; + } + + setLiveStationOffsets((prev) => { + if (!prev.has(state.stationId)) return prev; + const next = new Map(prev); + next.delete(state.stationId); + return next; + }); + + if (state.hasMoved) { + setStationOffset( + state.stationId, + state.lastOffset.x, + state.lastOffset.y + ); + } + + suppressAutoFitRef.current = false; + skipNextAutoFitRef.current = true; + + stationDragStateRef.current = null; + window.removeEventListener("pointermove", handleStationPointerMove); + window.removeEventListener("pointerup", handleStationPointerUp); + window.removeEventListener("pointercancel", handleStationPointerUp); + if (typeof document !== "undefined") { + document.body.style.cursor = ""; + } + }, + [handleStationPointerMove, setLiveStationOffsets, setStationOffset] + ); + + const handleStationPointerDown = useCallback( + ( + event: ReactPointerEvent, + stationId: number, + rotatedBase: Vec2, + currentOffset: Vec2 + ) => { + event.preventDefault(); + event.stopPropagation(); + + const camera = + transformState ?? transformRef.current ?? lastTransformRef.current; + if (!camera) return; + + suppressAutoFitRef.current = true; + + const pointerWorld = getWorldPosition( + event.clientX, + event.clientY, + camera + ); + const labelWorldX = rotatedBase.x + currentOffset.x; + const labelWorldY = rotatedBase.y + currentOffset.y; + const pointerDelta = pointerWorld + ? { + x: pointerWorld.x - labelWorldX, + y: pointerWorld.y - labelWorldY, + } + : { x: 0, y: 0 }; + + const captureTarget = event.currentTarget; + if (captureTarget.setPointerCapture) { + captureTarget.setPointerCapture(event.pointerId); + } + captureTarget.style.cursor = "grabbing"; + + stationDragStateRef.current = { + stationId, + pointerId: event.pointerId, + rotatedBase, + camera, + captureTarget, + lastOffset: currentOffset, + initialPointer: { x: event.clientX, y: event.clientY }, + hasMoved: false, + pointerDelta, + }; + + setLiveStationOffsets((prev) => { + const next = new Map(prev); + next.set(stationId, currentOffset); + return next; + }); + + if (typeof document !== "undefined") { + document.body.style.cursor = "grabbing"; + } + window.addEventListener("pointermove", handleStationPointerMove); + window.addEventListener("pointerup", handleStationPointerUp); + window.addEventListener("pointercancel", handleStationPointerUp); + }, + [ + handleStationPointerMove, + handleStationPointerUp, + transformState, + getWorldPosition, + setLiveStationOffsets, + ] + ); + + const handleSightPointerMove = useCallback( + (event: PointerEvent) => { + const state = sightDragStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + event.preventDefault(); + + const world = getWorldPosition( + event.clientX, + event.clientY, + state.camera + ); + if (!world) return; + + if (!state.hasMoved) { + const dx = event.clientX - state.initialClient.x; + const dy = event.clientY - state.initialClient.y; + if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) { + state.hasMoved = true; + } + } + + if (!state.hasMoved) { + return; + } + + const adjustedWorldX = world.x - state.pointerDelta.x; + const adjustedWorldY = world.y - state.pointerDelta.y; + + const newRotatedBaseX = adjustedWorldX - state.offset.x; + const newRotatedBaseY = adjustedWorldY - state.offset.y; + + const cos = Math.cos(state.rotation); + const sin = Math.sin(state.rotation); + + const baseX = newRotatedBaseX * cos + newRotatedBaseY * sin; + const baseY = -newRotatedBaseX * sin + newRotatedBaseY * cos; + + const localX = baseX / UP_SCALE; + const localY = baseY / UP_SCALE; + + const coords = localToCoordinates(localX, localY); + state.lastCoordinates = coords; + state.rotatedBase = { x: newRotatedBaseX, y: newRotatedBaseY }; + setLiveSightPositions((prev) => { + const next = new Map(prev); + next.set(state.sight.id, { + latitude: coords.latitude, + longitude: coords.longitude, + offsetX: state.offset.x, + offsetY: state.offset.y, + }); + return next; + }); + }, + [getWorldPosition, setLiveSightPositions] + ); + + const handleSightPointerUp = useCallback( + (event: PointerEvent) => { + const state = sightDragStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + if (state.captureTarget && state.captureTarget.releasePointerCapture) { + state.captureTarget.releasePointerCapture(event.pointerId); + state.captureTarget.style.cursor = "grab"; + } + + window.removeEventListener("pointermove", handleSightPointerMove); + window.removeEventListener("pointerup", handleSightPointerUp); + window.removeEventListener("pointercancel", handleSightPointerUp); + if (typeof document !== "undefined") { + document.body.style.cursor = ""; + } + + if (state.hasMoved) { + setSightCoordinates( + state.sight.id, + state.lastCoordinates.latitude, + state.lastCoordinates.longitude + ); + } else { + setSelectedSight(state.sight); + } + + suppressAutoFitRef.current = false; + skipNextAutoFitRef.current = true; + + sightDragStateRef.current = null; + }, + [handleSightPointerMove, setSelectedSight, setSightCoordinates] + ); + + const handleSightPointerDown = useCallback( + ( + event: ReactPointerEvent, + sight: SightData, + offset: Vec2, + rotatedBase: Vec2, + currentCoords: { latitude: number; longitude: number } + ) => { + event.preventDefault(); + event.stopPropagation(); + + const camera = + transformState ?? transformRef.current ?? lastTransformRef.current; + if (!camera) return; + + suppressAutoFitRef.current = true; + + const pointerWorld = getWorldPosition( + event.clientX, + event.clientY, + camera + ); + const labelWorldX = rotatedBase.x + offset.x; + const labelWorldY = rotatedBase.y + offset.y; + const pointerDelta = pointerWorld + ? { + x: pointerWorld.x - labelWorldX, + y: pointerWorld.y - labelWorldY, + } + : { x: 0, y: 0 }; + + const captureTarget = event.currentTarget; + if (captureTarget.setPointerCapture) { + captureTarget.setPointerCapture(event.pointerId); + } + captureTarget.style.cursor = "grabbing"; + + sightDragStateRef.current = { + sight, + pointerId: event.pointerId, + camera, + offset, + rotation: rotationAngle, + initialClient: { x: event.clientX, y: event.clientY }, + hasMoved: false, + captureTarget, + lastCoordinates: currentCoords, + rotatedBase, + pointerDelta, + }; + + if (typeof document !== "undefined") { + document.body.style.cursor = "grabbing"; + } + window.addEventListener("pointermove", handleSightPointerMove); + window.addEventListener("pointerup", handleSightPointerUp); + window.addEventListener("pointercancel", handleSightPointerUp); + }, + [ + handleSightPointerMove, + handleSightPointerUp, + rotationAngle, + transformState, + getWorldPosition, + ] + ); + + useEffect(() => { + return () => { + window.removeEventListener("pointermove", handleStationPointerMove); + window.removeEventListener("pointerup", handleStationPointerUp); + window.removeEventListener("pointercancel", handleStationPointerUp); + window.removeEventListener("pointermove", handleSightPointerMove); + window.removeEventListener("pointerup", handleSightPointerUp); + window.removeEventListener("pointercancel", handleSightPointerUp); + }; + }, [ + handleStationPointerMove, + handleStationPointerUp, + handleSightPointerMove, + handleSightPointerUp, + ]); + + const ensureContext = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas || glRef.current) { + return; + } + + const gl = + canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); + + if (!(gl instanceof WebGLRenderingContext)) { + console.error("WebGL is not supported in this browser"); + return; + } + + glRef.current = gl; + + try { + const lineProgram = createProgram( + gl, + lineVertexSource, + lineFragmentSource + ); + const pointProgram = createProgram( + gl, + pointVertexSource, + pointFragmentSource + ); + + lineProgramRef.current = lineProgram; + pointProgramRef.current = pointProgram; + + lineBufferRef.current = gl.createBuffer(); + pointBufferRef.current = gl.createBuffer(); + } catch (error) { + console.error("Failed to initialize WebGL", error); + } + }, []); + + useEffect(() => { + ensureContext(); + }, [ensureContext]); + + useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + if (typeof ResizeObserver === "undefined") { + const handleResize = () => { + setCanvasSize({ + width: container.clientWidth, + height: container.clientHeight, + }); + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + } + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect; + setCanvasSize({ width, height }); + } + }); + observer.observe(container); + return () => observer.disconnect(); + }, []); + + const drawScene = useCallback(() => { + const gl = glRef.current; + const canvas = canvasRef.current; + const lineProgram = lineProgramRef.current; + const pointProgram = pointProgramRef.current; + const lineBuffer = lineBufferRef.current; + const pointBuffer = pointBufferRef.current; + + if ( + !gl || + !canvas || + !lineProgram || + !pointProgram || + !lineBuffer || + !pointBuffer + ) { + return; + } + + const dpr = Math.max(1, window.devicePixelRatio || 1); + const displayWidth = Math.max(1, Math.floor(canvasSize.width * dpr)); + const displayHeight = Math.max(1, Math.floor(canvasSize.height * dpr)); + + if (canvas.width !== displayWidth || canvas.height !== displayHeight) { + canvas.width = displayWidth; + canvas.height = displayHeight; + } + + gl.viewport(0, 0, canvas.width, canvas.height); + gl.clearColor( + backgroundColor[0], + backgroundColor[1], + backgroundColor[2], + 1 + ); + gl.clear(gl.COLOR_BUFFER_BIT); + + const fallbackVertices = + rotatedRouteVertices.length > 0 + ? rotatedRouteVertices + : rotatedStationVertices; + + const routeMinRaw = routeData?.scale_min ?? originalRouteData?.scale_min; + const routeMaxRaw = routeData?.scale_max ?? originalRouteData?.scale_max; + const hasRouteScaleLimits = + typeof routeMinRaw === "number" && + typeof routeMaxRaw === "number" && + routeMaxRaw >= routeMinRaw && + routeMinRaw > 0; + + if (hasRouteScaleLimits) { + scaleLimitsRef.current = { + min: routeMinRaw / SCALE_FACTOR, + max: routeMaxRaw / SCALE_FACTOR, + }; + } + + let transform = transformRef.current; + if (!transform || !Number.isFinite(transform.scale)) { + transform = computeViewTransform( + fallbackVertices, + canvas.width, + canvas.height + ); + if (!hasRouteScaleLimits) { + const baseScale = Math.max(0.1, transform.scale || 1); + scaleLimitsRef.current = { + min: baseScale * 0.25, + max: baseScale * 16, + }; + } + transform = clampTransformScale(transform); + updateTransform(transform); + } else { + const clamped = clampTransformScale(transform); + if (clamped !== transform) { + updateTransform(clamped); + transform = clamped; + } + } + + const { scale, translation } = transform; + const pointOuterSizePx = clamp(scale * 13.3333, 6, 120); + const pointInnerSizePx = pointOuterSizePx * 0.8; + + if (rotatedRouteVertices.length >= 4) { + gl.useProgram(lineProgram.program); + gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer); + const lineWidth = pointInnerSizePx / scale; + const thickVertices = generateThickLineGeometry( + rotatedRouteVertices, + lineWidth + ); + if (thickVertices.length === 0) { + gl.bufferData(gl.ARRAY_BUFFER, rotatedRouteVertices, gl.STATIC_DRAW); + } else { + gl.bufferData(gl.ARRAY_BUFFER, thickVertices, gl.STATIC_DRAW); + } + + gl.enableVertexAttribArray(lineProgram.attribLocations.a_position); + gl.vertexAttribPointer( + lineProgram.attribLocations.a_position, + 2, + gl.FLOAT, + false, + 0, + 0 + ); + + if (lineProgram.uniformLocations.u_resolution) { + gl.uniform2f( + lineProgram.uniformLocations.u_resolution, + canvas.width, + canvas.height + ); + } + if (lineProgram.uniformLocations.u_translation) { + gl.uniform2f( + lineProgram.uniformLocations.u_translation, + translation.x, + translation.y + ); + } + if (lineProgram.uniformLocations.u_scale) { + gl.uniform1f(lineProgram.uniformLocations.u_scale, scale); + } + if (lineProgram.uniformLocations.u_color) { + gl.uniform4f( + lineProgram.uniformLocations.u_color, + pathColor[0], + pathColor[1], + pathColor[2], + 1 + ); + } + + const vertexCount = + thickVertices.length > 0 + ? thickVertices.length / 2 + : rotatedRouteVertices.length / 2; + const mode = thickVertices.length > 0 ? gl.TRIANGLES : gl.LINE_STRIP; + gl.drawArrays(mode, 0, vertexCount); + } + + if (rotatedStationVertices.length >= 2) { + gl.useProgram(pointProgram.program); + gl.bindBuffer(gl.ARRAY_BUFFER, pointBuffer); + gl.bufferData(gl.ARRAY_BUFFER, rotatedStationVertices, gl.STATIC_DRAW); + + gl.enableVertexAttribArray(pointProgram.attribLocations.a_position); + gl.vertexAttribPointer( + pointProgram.attribLocations.a_position, + 2, + gl.FLOAT, + false, + 0, + 0 + ); + + if (pointProgram.uniformLocations.u_resolution) { + gl.uniform2f( + pointProgram.uniformLocations.u_resolution, + canvas.width, + canvas.height + ); + } + if (pointProgram.uniformLocations.u_translation) { + gl.uniform2f( + pointProgram.uniformLocations.u_translation, + translation.x, + translation.y + ); + } + if (pointProgram.uniformLocations.u_scale) { + gl.uniform1f(pointProgram.uniformLocations.u_scale, scale); + } + + if (pointProgram.uniformLocations.u_pointSize) { + gl.uniform1f( + pointProgram.uniformLocations.u_pointSize, + pointOuterSizePx + ); + } + if (pointProgram.uniformLocations.u_color) { + gl.uniform4f( + pointProgram.uniformLocations.u_color, + backgroundColor[0], + backgroundColor[1], + backgroundColor[2], + 1 + ); + } + gl.drawArrays(gl.POINTS, 0, rotatedStationVertices.length / 2); + if (pointProgram.uniformLocations.u_pointSize) { + gl.uniform1f( + pointProgram.uniformLocations.u_pointSize, + pointInnerSizePx + ); + } + if (pointProgram.uniformLocations.u_color) { + gl.uniform4f( + pointProgram.uniformLocations.u_color, + pathColor[0], + pathColor[1], + pathColor[2], + 1 + ); + } + gl.drawArrays(gl.POINTS, 0, rotatedStationVertices.length / 2); + } + }, [ + canvasSize.height, + canvasSize.width, + rotatedRouteVertices, + rotatedStationVertices, + routeData?.scale_min, + routeData?.scale_max, + originalRouteData?.scale_min, + originalRouteData?.scale_max, + rotationAngle, + ]); + + useEffect(() => { + drawSceneRef.current = drawScene; + }, [drawScene]); + + useEffect(() => { + if (!transformState && transformRef.current) { + setTransformState({ ...transformRef.current }); + } + }, [transformState]); + + useEffect(() => { + if (!transformRef.current) { + return; + } + + const current = transformRef.current; + const { min, max } = scaleLimitsRef.current; + const targetScale = clamp(sharedScale, min, max); + + if (Math.abs(current.scale - targetScale) < 1e-3) { + if (targetScale !== sharedScale) { + setSharedScale(targetScale); + } + return; + } + + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const worldCenterX = (centerX - current.translation.x) / current.scale; + const worldCenterY = (centerY - current.translation.y) / current.scale; + + const updated: Transform = { + scale: targetScale, + translation: { + x: centerX - worldCenterX * targetScale, + y: centerY - worldCenterY * targetScale, + }, + }; + + transformRef.current = updated; + lastTransformRef.current = updated; + setTransformState(updated); + if (targetScale !== sharedScale) { + setSharedScale(targetScale); + } + drawSceneRef.current(); + }, [sharedScale, setSharedScale]); + + useEffect(() => { + if (suppressAutoFitRef.current) { + return; + } + if (skipNextAutoFitRef.current) { + skipNextAutoFitRef.current = false; + return; + } + resetTransform(); + }, [ + routeVertices, + stationVertices, + canvasSize.width, + canvasSize.height, + rotationAngle, + resetTransform, + ]); + + useEffect(() => { + drawScene(); + }, [drawScene]); + + const applyCenterFromCoordinates = useCallback( + (latitude: number, longitude: number) => { + const roundedLat = Math.round(latitude * 1e6) / 1e6; + const roundedLon = Math.round(longitude * 1e6) / 1e6; + lastCenterRef.current = { + latitude: roundedLat, + longitude: roundedLon, + }; + }, + [] + ); + + useEffect(() => { + const latitude = routeData?.center_latitude; + const longitude = routeData?.center_longitude; + if (latitude == null || longitude == null) { + return; + } + + const prevApplied = lastAppliedCenterRef.current; + const epsilon = 1e-7; + if ( + prevApplied.latitude != null && + prevApplied.longitude != null && + Math.abs(prevApplied.latitude - latitude) < epsilon && + Math.abs(prevApplied.longitude - longitude) < epsilon + ) { + return; + } + + lastAppliedCenterRef.current = { latitude, longitude }; + applyCenterFromCoordinates(latitude, longitude); + }, [ + routeData?.center_latitude, + routeData?.center_longitude, + applyCenterFromCoordinates, + ]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + canvas.style.touchAction = "none"; + canvas.style.cursor = "grab"; + + const getEventPosition = (event: PointerEvent | WheelEvent): Vec2 => { + const currentCanvas = canvasRef.current; + if (!currentCanvas) { + return { x: 0, y: 0 }; + } + const rect = currentCanvas.getBoundingClientRect(); + const scaleX = currentCanvas.width / Math.max(rect.width, 1); + const scaleY = currentCanvas.height / Math.max(rect.height, 1); + return { + x: (event.clientX - rect.left) * scaleX, + y: (event.clientY - rect.top) * scaleY, + }; + }; + + const applyTranslation = (dx: number, dy: number) => { + const transform = transformRef.current; + if (!transform) return; + const next = { + scale: transform.scale, + translation: { + x: transform.translation.x + dx, + y: transform.translation.y + dy, + }, + }; + updateTransform(next); + drawSceneRef.current(); + }; + + const handlePointerDown = (event: PointerEvent) => { + if (event.pointerType === "mouse" && event.button !== 0) { + return; + } + + event.preventDefault(); + const position = getEventPosition(event); + activePointersRef.current.set(event.pointerId, position); + canvas.setPointerCapture(event.pointerId); + + if (activePointersRef.current.size === 1) { + dragStateRef.current = { lastPos: position }; + pinchStateRef.current = null; + canvas.style.cursor = "grabbing"; + } else if (activePointersRef.current.size === 2) { + dragStateRef.current = null; + + const pointers = Array.from(activePointersRef.current.values()); + const [p1, p2] = pointers; + const distance = Math.hypot(p1.x - p2.x, p1.y - p2.y); + const midpoint: Vec2 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; + const transform = transformRef.current; + if (!transform) return; + + const worldMidpoint: Vec2 = { + x: (midpoint.x - transform.translation.x) / transform.scale, + y: (midpoint.y - transform.translation.y) / transform.scale, + }; + + pinchStateRef.current = { + initialDistance: Math.max(distance, 1), + initialScale: transform.scale, + worldMidpoint, + }; + } + }; + + const handlePointerMove = (event: PointerEvent) => { + if (!activePointersRef.current.has(event.pointerId)) { + return; + } + + const position = getEventPosition(event); + activePointersRef.current.set(event.pointerId, position); + + if (activePointersRef.current.size === 1 && dragStateRef.current) { + const { lastPos } = dragStateRef.current; + applyTranslation(position.x - lastPos.x, position.y - lastPos.y); + dragStateRef.current = { lastPos: position }; + return; + } + + if (activePointersRef.current.size === 2 && pinchStateRef.current) { + const pointers = Array.from(activePointersRef.current.values()); + const [p1, p2] = pointers; + const distance = Math.max(Math.hypot(p1.x - p2.x, p1.y - p2.y), 1); + const midpoint: Vec2 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; + const transform = transformRef.current; + if (!transform) return; + + const { initialDistance, initialScale, worldMidpoint } = + pinchStateRef.current; + const scaleRatio = distance / initialDistance; + const unclampedScale = initialScale * scaleRatio; + const clampedScale = clamp( + unclampedScale, + scaleLimitsRef.current.min, + scaleLimitsRef.current.max + ); + + updateTransform({ + scale: clampedScale, + translation: { + x: midpoint.x - worldMidpoint.x * clampedScale, + y: midpoint.y - worldMidpoint.y * clampedScale, + }, + }); + drawSceneRef.current(); + } + }; + + const handlePointerUp = (event: PointerEvent) => { + if (!activePointersRef.current.has(event.pointerId)) { + return; + } + + canvas.releasePointerCapture(event.pointerId); + activePointersRef.current.delete(event.pointerId); + + if (activePointersRef.current.size === 0) { + dragStateRef.current = null; + pinchStateRef.current = null; + canvas.style.cursor = "grab"; + } else if (activePointersRef.current.size === 1) { + const remaining = Array.from(activePointersRef.current.values())[0]; + dragStateRef.current = { lastPos: remaining }; + pinchStateRef.current = null; + canvas.style.cursor = "grabbing"; + } + }; + + const handleWheel = (event: WheelEvent) => { + event.preventDefault(); + const transform = transformRef.current; + if (!transform) return; + + const position = getEventPosition(event); + const delta = event.deltaY > 0 ? 0.9 : 1.1; + const unclampedScale = transform.scale * delta; + const clampedScale = clamp( + unclampedScale, + scaleLimitsRef.current.min, + scaleLimitsRef.current.max + ); + + if (clampedScale === transform.scale) { + return; + } + + const worldPoint = { + x: (position.x - transform.translation.x) / transform.scale, + y: (position.y - transform.translation.y) / transform.scale, + }; + + updateTransform({ + scale: clampedScale, + translation: { + x: position.x - worldPoint.x * clampedScale, + y: position.y - worldPoint.y * clampedScale, + }, + }); + drawSceneRef.current(); + }; + + canvas.addEventListener("pointerdown", handlePointerDown); + canvas.addEventListener("pointermove", handlePointerMove); + canvas.addEventListener("pointerup", handlePointerUp); + canvas.addEventListener("pointercancel", handlePointerUp); + canvas.addEventListener("pointerleave", handlePointerUp); + canvas.addEventListener("wheel", handleWheel, { passive: false }); + + return () => { + canvas.removeEventListener("pointerdown", handlePointerDown); + canvas.removeEventListener("pointermove", handlePointerMove); + canvas.removeEventListener("pointerup", handlePointerUp); + canvas.removeEventListener("pointercancel", handlePointerUp); + canvas.removeEventListener("pointerleave", handlePointerUp); + canvas.removeEventListener("wheel", handleWheel as EventListener); + }; + }, []); + + return ( +
+ + {stationData?.ru?.length ? ( +
+ {stationData.ru.map((station, index) => { + const camera = + transformState ?? + transformRef.current ?? + lastTransformRef.current; + if (!camera) { + return null; + } + const translatedStation = stationData?.[language]?.[index]; + + const local = coordinatesToLocal( + station.latitude, + station.longitude + ); + const baseX = local.x * UP_SCALE; + const baseY = local.y * UP_SCALE; + + const cos = Math.cos(rotationAngle); + const sin = Math.sin(rotationAngle); + const rotatedX = baseX * cos - baseY * sin; + const rotatedY = baseX * sin + baseY * cos; + + const liveStationOffset = liveStationOffsets.get(station.id); + const hasCustomOffset = + typeof station.offset_x === "number" && + typeof station.offset_y === "number" && + (station.offset_x !== 0 || station.offset_y !== 0); + + const baseOffsetX = hasCustomOffset ? station.offset_x : 25; + const baseOffsetY = hasCustomOffset ? station.offset_y : 0; + + const offsetX = + liveStationOffset?.x !== undefined + ? liveStationOffset.x + : baseOffsetX; + const offsetY = + liveStationOffset?.y !== undefined + ? liveStationOffset.y + : baseOffsetY; + + const labelX = + (rotatedX + offsetX) * camera.scale + camera.translation.x; + const labelY = + (rotatedY + offsetY) * camera.scale + camera.translation.y; + + const dpr = Math.max(1, window.devicePixelRatio || 1); + const cssX = labelX / dpr; + const cssY = labelY / dpr; + const rotationCss = `${rotationAngle}rad`; + const counterRotationCss = `${-rotationAngle}rad`; + + const showSecondary = + language !== "ru" && + translatedStation && + translatedStation.name && + translatedStation.name !== station.name; + + const fontSizePercent = + routeData?.font_size ?? originalRouteData?.font_size ?? 100; + const fontScale = fontSizePercent / 100; + const primaryFontSize = 16 * fontScale; + const secondaryFontSize = 13 * fontScale; + const secondaryMarginTop = 5 * fontScale; + + return ( +
+ handleStationPointerDown( + event, + station.id, + { + x: rotatedX, + y: rotatedY, + }, + { x: offsetX, y: offsetY } + ) + } + style={{ + position: "absolute", + left: cssX, + top: cssY, + transform: "translate(0, -50%)", + color: "#fff", + fontFamily: "Roboto, sans-serif", + textAlign: "left", + pointerEvents: "auto", + cursor: "grab", + userSelect: "none", + touchAction: "none", + }} + > +
+
+
+ {station.name} +
+ {showSecondary ? ( +
+ {translatedStation?.name} +
+ ) : null} +
+
+
+ ); + })} +
+ ) : null} + {sightData?.length ? ( +
+ {sightData.map((sight, index) => { + const camera = + transformState ?? + transformRef.current ?? + lastTransformRef.current; + if (!camera) { + return null; + } + const liveSightPosition = liveSightPositions.get(sight.id); + const latitude = liveSightPosition?.latitude ?? sight.latitude; + const longitude = liveSightPosition?.longitude ?? sight.longitude; + const local = coordinatesToLocal(latitude, longitude); + const baseX = local.x * UP_SCALE; + const baseY = local.y * UP_SCALE; + + const cos = Math.cos(rotationAngle); + const sin = Math.sin(rotationAngle); + const rotatedX = baseX * cos - baseY * sin; + const rotatedY = baseX * sin + baseY * cos; + + const rawOffsetX = (sight as any)?.offset_x ?? 0; + const rawOffsetY = (sight as any)?.offset_y ?? 0; + + const DEFAULT_LABEL_OFFSET_X = 25; + const DEFAULT_LABEL_OFFSET_Y = 0; + + const offsetX = + liveSightPosition?.offsetX ?? + (rawOffsetX === 0 && rawOffsetY === 0 + ? DEFAULT_LABEL_OFFSET_X + : rawOffsetX); + const offsetY = + liveSightPosition?.offsetY ?? + (rawOffsetX === 0 && rawOffsetY === 0 + ? DEFAULT_LABEL_OFFSET_Y + : rawOffsetY); + + const labelX = + (rotatedX + offsetX) * camera.scale + camera.translation.x; + const labelY = + (rotatedY + offsetY) * camera.scale + camera.translation.y; + + const dpr = Math.max(1, window.devicePixelRatio || 1); + const cssX = labelX / dpr; + const cssY = labelY / dpr; + const iconSizePercent = + routeData?.icon_size ?? originalRouteData?.icon_size ?? 100; + const iconSize = 30 * (iconSizePercent / 100); + const iconLeft = cssX - iconSize; + const iconTop = cssY - iconSize; + const labelHeight = 24; + const labelPadding = 6; + const labelFontSize = 14; + + return ( +
+ handleSightPointerDown( + event, + sight, + { + x: offsetX, + y: offsetY, + }, + { x: rotatedX, y: rotatedY }, + { latitude, longitude } + ) + } + style={{ + position: "absolute", + left: iconLeft, + top: iconTop, + height: iconSize, + display: "flex", + alignItems: "center", + gap: 8, + pointerEvents: "auto", + cursor: "grab", + userSelect: "none", + touchAction: "none", + }} + > + +
+ {index + 1} +
+
+ ); + })} +
+ ) : null} + {routeVertices.length === 0 && ( +
+ Нет данных для отображения маршрута +
+ )} +
+ ); +}); + +export default WebGLRouteMapPrototype; diff --git a/src/pages/Sight/LinkedStations.tsx b/src/pages/Sight/LinkedStations.tsx index f26d6b6..a417cb4 100644 --- a/src/pages/Sight/LinkedStations.tsx +++ b/src/pages/Sight/LinkedStations.tsx @@ -2,7 +2,6 @@ import { useState, useEffect } from "react"; import { Stack, Typography, - Button, Accordion, AccordionSummary, AccordionDetails, @@ -16,11 +15,21 @@ import { TableRow, Paper, TableBody, + Checkbox, + FormControlLabel, + Tabs, + Tab, + Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { authInstance, languageStore, selectedCityStore } from "@shared"; +import { + AnimatedCircleButton, + authInstance, + languageStore, + selectedCityStore, +} from "@shared"; type Field = { label: string; @@ -93,6 +102,16 @@ const LinkedStationsContentsInner = < const [selectedItemId, setSelectedItemId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [selectedItems, setSelectedItems] = useState>(new Set()); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedToDetach, setSelectedToDetach] = useState>( + new Set() + ); + const [isLinkingSingle, setIsLinkingSingle] = useState(false); + const [isLinkingBulk, setIsLinkingBulk] = useState(false); + const [isBulkDetaching, setIsBulkDetaching] = useState(false); + const [detachingIds, setDetachingIds] = useState>(new Set()); useEffect(() => {}, [error]); @@ -110,6 +129,11 @@ const LinkedStationsContentsInner = < }) .sort((a, b) => a.name.localeCompare(b.name)); + const filteredAvailableItems = availableItems.filter((item) => { + if (!searchQuery.trim()) return true; + return String(item.name).toLowerCase().includes(searchQuery.toLowerCase()); + }); + useEffect(() => { if (updatedLinkedItems) { setLinkedItems(updatedLinkedItems); @@ -120,6 +144,18 @@ const LinkedStationsContentsInner = < setItemsParent?.(linkedItems); }, [linkedItems, setItemsParent]); + useEffect(() => { + setSelectedToDetach((prev) => { + const updated = new Set(); + linkedItems.forEach((item) => { + if (prev.has(item.id)) { + updated.add(item.id); + } + }); + return updated; + }); + }, [linkedItems]); + const linkItem = () => { if (selectedItemId !== null) { setError(null); @@ -127,6 +163,7 @@ const LinkedStationsContentsInner = < station_id: selectedItemId, }; + setIsLinkingSingle(true); authInstance .post(`/${parentResource}/${parentId}/${childResource}`, requestData) .then(() => { @@ -140,12 +177,20 @@ const LinkedStationsContentsInner = < .catch((error) => { console.error("Error linking station:", error); setError("Failed to link station"); + }) + .finally(() => { + setIsLinkingSingle(false); }); } }; const deleteItem = (itemId: number) => { setError(null); + setDetachingIds((prev) => { + const next = new Set(prev); + next.add(itemId); + return next; + }); authInstance .delete(`/${parentResource}/${parentId}/${childResource}`, { data: { [`${childResource}_id`]: itemId }, @@ -157,9 +202,119 @@ const LinkedStationsContentsInner = < .catch((error) => { console.error("Error deleting station:", error); setError("Failed to delete station"); + }) + .finally(() => { + setDetachingIds((prev) => { + const next = new Set(prev); + next.delete(itemId); + return next; + }); }); }; + const handleCheckboxChange = (itemId: number) => { + const updated = new Set(selectedItems); + if (updated.has(itemId)) { + updated.delete(itemId); + } else { + updated.add(itemId); + } + setSelectedItems(updated); + }; + + const handleBulkLink = () => { + if (selectedItems.size === 0) return; + setError(null); + + setIsLinkingBulk(true); + Promise.all( + Array.from(selectedItems).map((id) => + authInstance.post(`/${parentResource}/${parentId}/${childResource}`, { + station_id: id, + }) + ) + ) + .then(() => { + const newItems = allItems.filter((item) => + selectedItems.has(item.id) + ); + setLinkedItems([...linkedItems, ...newItems]); + setSelectedItems(new Set()); + onUpdate?.(); + }) + .catch((error) => { + console.error("Error bulk linking stations:", error); + setError("Failed to link stations"); + }) + .finally(() => { + setIsLinkingBulk(false); + }); + }; + + const toggleDetachSelection = (itemId: number) => { + const updated = new Set(selectedToDetach); + if (updated.has(itemId)) { + updated.delete(itemId); + } else { + updated.add(itemId); + } + setSelectedToDetach(updated); + }; + + const handleToggleAllDetach = (checked: boolean) => { + if (!checked) { + setSelectedToDetach(new Set()); + return; + } + setSelectedToDetach(new Set(linkedItems.map((item) => item.id))); + }; + + const handleBulkDetach = () => { + const idsToDetach = Array.from(selectedToDetach); + if (idsToDetach.length === 0) return; + setError(null); + + setIsBulkDetaching(true); + setDetachingIds((prev) => { + const next = new Set(prev); + idsToDetach.forEach((id) => next.add(id)); + return next; + }); + + Promise.all( + idsToDetach.map((itemId) => + authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, { + data: { [`${childResource}_id`]: itemId }, + }) + ) + ) + .then(() => { + setLinkedItems( + linkedItems.filter((item) => !idsToDetach.includes(item.id)) + ); + setSelectedToDetach(new Set()); + onUpdate?.(); + }) + .catch((error) => { + console.error("Error bulk deleting stations:", error); + setError("Failed to delete stations"); + }) + .finally(() => { + setDetachingIds((prev) => { + const next = new Set(prev); + idsToDetach.forEach((id) => next.delete(id)); + return next; + }); + setIsBulkDetaching(false); + }); + }; + + const allSelectedForDetach = + linkedItems.length > 0 && + linkedItems.every((item) => selectedToDetach.has(item.id)); + const isIndeterminateDetach = + selectedToDetach.size > 0 && !allSelectedForDetach; + useEffect(() => { if (parentId) { setIsLoading(true); @@ -203,6 +358,16 @@ const LinkedStationsContentsInner = < + {type === "edit" && ( + + handleToggleAllDetach(e.target.checked)} + /> + + )} @@ -218,6 +383,15 @@ const LinkedStationsContentsInner = < {linkedItems.map((item, index) => ( + {type === "edit" && ( + + toggleDetachSelection(item.id)} + /> + + )} {index + 1} {fields.map((field, idx) => ( @@ -228,7 +402,7 @@ const LinkedStationsContentsInner = < ))} {type === "edit" && ( - + )} @@ -248,6 +424,20 @@ const LinkedStationsContentsInner = < )} + {type === "edit" && linkedItems.length > 0 && ( + + + Отвязать выбранные ({selectedToDetach.size}) + + + )} + {linkedItems.length === 0 && !isLoading && ( Остановки не найдены @@ -256,48 +446,127 @@ const LinkedStationsContentsInner = < {type === "edit" && !disableCreation && ( - Добавить остановку - item.id === selectedItemId) || null - } - onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)} - options={availableItems} - getOptionLabel={(item) => String(item.name)} - renderInput={(params) => ( - - )} - isOptionEqualToValue={(option, value) => option.id === value?.id} - filterOptions={(options, { inputValue }) => { - const searchWords = inputValue - .toLowerCase() - .split(" ") - .filter(Boolean); - return options.filter((option) => { - const optionWords = String(option.name) - .toLowerCase() - .split(" "); - return searchWords.every((searchWord) => - optionWords.some((word) => word.startsWith(searchWord)) - ); - }); - }} - renderOption={(props, option) => ( -
  • - {String(option.name)} -
  • - )} - /> - - + + + + + + {activeTab === 0 && ( + + item.id === selectedItemId) || + null + } + onChange={(_, newValue) => + setSelectedItemId(newValue?.id || null) + } + options={availableItems} + getOptionLabel={(item) => String(item.name)} + renderInput={(params) => ( + + )} + isOptionEqualToValue={(option, value) => + option.id === value?.id + } + filterOptions={(options, { inputValue }) => { + const searchWords = inputValue + .toLowerCase() + .split(" ") + .filter(Boolean); + return options.filter((option) => { + const optionWords = String(option.name) + .toLowerCase() + .split(" "); + return searchWords.every((searchWord) => + optionWords.some((word) => word.startsWith(searchWord)) + ); + }); + }} + renderOption={(props, option) => ( +
  • + {String(option.name)} +
  • + )} + /> + + + Добавить + +
    + )} + + {activeTab === 1 && ( + + setSearchQuery(e.target.value)} + placeholder="Введите название остановки..." + size="small" + /> + + + + {filteredAvailableItems.map((item) => ( + handleCheckboxChange(item.id)} + size="small" + /> + } + label={String(item.name)} + sx={{ + margin: 0, + "& .MuiFormControlLabel-label": { + fontSize: "0.9rem", + }, + }} + /> + ))} + {filteredAvailableItems.length === 0 && ( + + {searchQuery.trim() + ? "Остановки не найдены" + : "Нет доступных остановок"} + + )} + + + + + Добавить выбранные ({selectedItems.size}) + + + )} +
    )} diff --git a/src/pages/Station/LinkedSights.tsx b/src/pages/Station/LinkedSights.tsx index 584eeda..a5b610c 100644 --- a/src/pages/Station/LinkedSights.tsx +++ b/src/pages/Station/LinkedSights.tsx @@ -2,7 +2,6 @@ import { useState, useEffect } from "react"; import { Stack, Typography, - Button, Accordion, AccordionSummary, AccordionDetails, @@ -16,11 +15,21 @@ import { TableRow, Paper, TableBody, + Checkbox, + FormControlLabel, + Tabs, + Tab, + Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { authInstance, languageStore, selectedCityStore } from "@shared"; +import { + AnimatedCircleButton, + authInstance, + languageStore, + selectedCityStore, +} from "@shared"; type Field = { label: string; @@ -93,6 +102,16 @@ const LinkedSightsContentsInner = < const [selectedItemId, setSelectedItemId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [selectedItems, setSelectedItems] = useState>(new Set()); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedToDetach, setSelectedToDetach] = useState>( + new Set() + ); + const [isLinkingSingle, setIsLinkingSingle] = useState(false); + const [isLinkingBulk, setIsLinkingBulk] = useState(false); + const [isBulkDetaching, setIsBulkDetaching] = useState(false); + const [detachingIds, setDetachingIds] = useState>(new Set()); useEffect(() => {}, [error]); @@ -111,6 +130,11 @@ const LinkedSightsContentsInner = < }) .sort((a, b) => a.name.localeCompare(b.name)); + const filteredAvailableItems = availableItems.filter((item) => { + if (!searchQuery.trim()) return true; + return String(item.name).toLowerCase().includes(searchQuery.toLowerCase()); + }); + useEffect(() => { if (updatedLinkedItems) { setLinkedItems(updatedLinkedItems); @@ -121,6 +145,18 @@ const LinkedSightsContentsInner = < setItemsParent?.(linkedItems); }, [linkedItems, setItemsParent]); + useEffect(() => { + setSelectedToDetach((prev) => { + const updated = new Set(); + linkedItems.forEach((item) => { + if (prev.has(item.id)) { + updated.add(item.id); + } + }); + return updated; + }); + }, [linkedItems]); + const linkItem = () => { if (selectedItemId !== null) { setError(null); @@ -128,6 +164,7 @@ const LinkedSightsContentsInner = < sight_id: selectedItemId, }; + setIsLinkingSingle(true); authInstance .post(`/${parentResource}/${parentId}/${childResource}`, requestData) .then(() => { @@ -141,12 +178,20 @@ const LinkedSightsContentsInner = < .catch((error) => { console.error("Error linking sight:", error); setError("Failed to link sight"); + }) + .finally(() => { + setIsLinkingSingle(false); }); } }; const deleteItem = (itemId: number) => { setError(null); + setDetachingIds((prev) => { + const next = new Set(prev); + next.add(itemId); + return next; + }); authInstance .delete(`/${parentResource}/${parentId}/${childResource}`, { data: { [`${childResource}_id`]: itemId }, @@ -158,9 +203,119 @@ const LinkedSightsContentsInner = < .catch((error) => { console.error("Error deleting sight:", error); setError("Failed to delete sight"); + }) + .finally(() => { + setDetachingIds((prev) => { + const next = new Set(prev); + next.delete(itemId); + return next; + }); }); }; + const handleCheckboxChange = (itemId: number) => { + const updated = new Set(selectedItems); + if (updated.has(itemId)) { + updated.delete(itemId); + } else { + updated.add(itemId); + } + setSelectedItems(updated); + }; + + const handleBulkLink = () => { + if (selectedItems.size === 0) return; + setError(null); + + setIsLinkingBulk(true); + Promise.all( + Array.from(selectedItems).map((id) => + authInstance.post(`/${parentResource}/${parentId}/${childResource}`, { + sight_id: id, + }) + ) + ) + .then(() => { + const newItems = allItems.filter((item) => + selectedItems.has(item.id) + ); + setLinkedItems([...linkedItems, ...newItems]); + setSelectedItems(new Set()); + onUpdate?.(); + }) + .catch((error) => { + console.error("Error bulk linking sights:", error); + setError("Failed to link sights"); + }) + .finally(() => { + setIsLinkingBulk(false); + }); + }; + + const toggleDetachSelection = (itemId: number) => { + const updated = new Set(selectedToDetach); + if (updated.has(itemId)) { + updated.delete(itemId); + } else { + updated.add(itemId); + } + setSelectedToDetach(updated); + }; + + const handleToggleAllDetach = (checked: boolean) => { + if (!checked) { + setSelectedToDetach(new Set()); + return; + } + setSelectedToDetach(new Set(linkedItems.map((item) => item.id))); + }; + + const handleBulkDetach = () => { + const idsToDetach = Array.from(selectedToDetach); + if (idsToDetach.length === 0) return; + setError(null); + + setIsBulkDetaching(true); + setDetachingIds((prev) => { + const next = new Set(prev); + idsToDetach.forEach((id) => next.add(id)); + return next; + }); + + Promise.all( + idsToDetach.map((itemId) => + authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, { + data: { [`${childResource}_id`]: itemId }, + }) + ) + ) + .then(() => { + setLinkedItems( + linkedItems.filter((item) => !idsToDetach.includes(item.id)) + ); + setSelectedToDetach(new Set()); + onUpdate?.(); + }) + .catch((error) => { + console.error("Error bulk deleting sights:", error); + setError("Failed to delete sights"); + }) + .finally(() => { + setDetachingIds((prev) => { + const next = new Set(prev); + idsToDetach.forEach((id) => next.delete(id)); + return next; + }); + setIsBulkDetaching(false); + }); + }; + + const allSelectedForDetach = + linkedItems.length > 0 && + linkedItems.every((item) => selectedToDetach.has(item.id)); + const isIndeterminateDetach = + selectedToDetach.size > 0 && !allSelectedForDetach; + useEffect(() => { if (parentId) { setIsLoading(true); @@ -204,6 +359,16 @@ const LinkedSightsContentsInner = <
    + {type === "edit" && ( + + handleToggleAllDetach(e.target.checked)} + /> + + )} @@ -219,6 +384,15 @@ const LinkedSightsContentsInner = < {linkedItems.map((item, index) => ( + {type === "edit" && ( + + toggleDetachSelection(item.id)} + /> + + )} {index + 1} {fields.map((field, idx) => ( @@ -229,7 +403,7 @@ const LinkedSightsContentsInner = < ))} {type === "edit" && ( - + )} @@ -249,6 +425,20 @@ const LinkedSightsContentsInner = < )} + {type === "edit" && linkedItems.length > 0 && ( + + + Отвязать выбранные ({selectedToDetach.size}) + + + )} + {linkedItems.length === 0 && !isLoading && ( Достопримечательности не найдены @@ -258,53 +448,132 @@ const LinkedSightsContentsInner = < {type === "edit" && !disableCreation && ( - Добавить достопримечательность + Добавить достопримечательности - item.id === selectedItemId) || null - } - onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)} - options={availableItems} - getOptionLabel={(item) => String(item.name)} - renderInput={(params) => ( - - )} - isOptionEqualToValue={(option, value) => option.id === value?.id} - filterOptions={(options, { inputValue }) => { - const searchWords = inputValue - .toLowerCase() - .split(" ") - .filter(Boolean); - return options.filter((option) => { - const optionWords = String(option.name) - .toLowerCase() - .split(" "); - return searchWords.every((searchWord) => - optionWords.some((word) => word.startsWith(searchWord)) - ); - }); - }} - renderOption={(props, option) => ( -
  • - {String(option.name)} -
  • - )} - /> - - + + + + + + {activeTab === 0 && ( + + item.id === selectedItemId) || + null + } + onChange={(_, newValue) => + setSelectedItemId(newValue?.id || null) + } + options={availableItems} + getOptionLabel={(item) => String(item.name)} + renderInput={(params) => ( + + )} + isOptionEqualToValue={(option, value) => + option.id === value?.id + } + filterOptions={(options, { inputValue }) => { + const searchWords = inputValue + .toLowerCase() + .split(" ") + .filter(Boolean); + return options.filter((option) => { + const optionWords = String(option.name) + .toLowerCase() + .split(" "); + return searchWords.every((searchWord) => + optionWords.some((word) => word.startsWith(searchWord)) + ); + }); + }} + renderOption={(props, option) => ( +
  • + {String(option.name)} +
  • + )} + /> + + + Добавить + +
    + )} + + {activeTab === 1 && ( + + setSearchQuery(e.target.value)} + placeholder="Введите название..." + size="small" + /> + + + + {filteredAvailableItems.map((item) => ( + handleCheckboxChange(item.id)} + size="small" + /> + } + label={String(item.name)} + sx={{ + margin: 0, + "& .MuiFormControlLabel-label": { + fontSize: "0.9rem", + }, + }} + /> + ))} + {filteredAvailableItems.length === 0 && ( + + {searchQuery.trim() + ? "Достопримечательности не найдены" + : "Нет доступных достопримечательностей"} + + )} + + + + + Добавить выбранные ({selectedItems.size}) + + + )} +
    )} diff --git a/src/shared/store/LanguageStore/index.tsx b/src/shared/store/LanguageStore/index.tsx index dab5420..16c48f2 100644 --- a/src/shared/store/LanguageStore/index.tsx +++ b/src/shared/store/LanguageStore/index.tsx @@ -6,10 +6,24 @@ class LanguageStore { constructor() { makeAutoObservable(this); + + if (typeof window !== "undefined") { + const storedLanguage = window.localStorage.getItem("appLanguage"); + if ( + storedLanguage && + ["ru", "en", "zh"].includes(storedLanguage.toLowerCase()) + ) { + this.language = storedLanguage.toLowerCase() as Language; + } + } } setLanguage = (language: Language) => { this.language = language; + + if (typeof window !== "undefined") { + window.localStorage.setItem("appLanguage", language); + } }; } diff --git a/src/shared/ui/AnimatedCircleButton.tsx b/src/shared/ui/AnimatedCircleButton.tsx new file mode 100644 index 0000000..b6bee0f --- /dev/null +++ b/src/shared/ui/AnimatedCircleButton.tsx @@ -0,0 +1,171 @@ +import { forwardRef } from "react"; +import { Button, ButtonProps, CircularProgress } from "@mui/material"; +import { alpha, keyframes, styled } from "@mui/material/styles"; +import type { Theme } from "@mui/material/styles"; + +type AnimatedCircleButtonProps = ButtonProps & { + disableAnimation?: boolean; + loading?: boolean; +}; + +type StyledButtonProps = AnimatedCircleButtonProps & { theme: Theme }; + +const loadingPulse = keyframes` + 0% { + transform: translate(-50%, -50%) scale(0.6); + opacity: 0.35; + } + 50% { + transform: translate(-50%, -50%) scale(1.45); + opacity: 0.15; + } + 100% { + transform: translate(-50%, -50%) scale(0.6); + opacity: 0; + } +`; + +const StyledButton = styled(Button, { + shouldForwardProp: (prop) => + prop !== "disableAnimation" && prop !== "loading", +})((props: StyledButtonProps) => { + const { + theme, + disableAnimation = false, + color, + variant = "text", + disabled = false, + loading = false, + } = props; + + const shouldAnimate = !disableAnimation && (!disabled || loading); + const pointerBlocked = loading; + + const paletteMainMap: Record = { + primary: theme.palette.primary.main, + secondary: theme.palette.secondary.main, + error: theme.palette.error.main, + warning: theme.palette.warning.main, + info: theme.palette.info.main, + success: theme.palette.success.main, + inherit: theme.palette.primary.main, + }; + + const paletteMain = + (color && paletteMainMap[String(color)]) ?? theme.palette.primary.main; + + const pulseColor = + variant === "outlined" || variant === "text" + ? alpha(paletteMain, 0.18) + : alpha(paletteMain, 0.3); + + return { + position: "relative", + overflow: "hidden", + borderRadius: 5, + zIndex: 0, + transition: "transform 0.2s ease, box-shadow 0.2s ease", + pointerEvents: pointerBlocked ? "none" : undefined, + "&::after": shouldAnimate + ? { + content: '""', + position: "absolute", + width: "12px", + height: "12px", + backgroundColor: pulseColor, + borderRadius: "50%", + top: "50%", + left: "50%", + pointerEvents: "none", + zIndex: 0, + ...(loading + ? { + opacity: 0.35, + transform: "translate(-50%, -50%) scale(0.6)", + animation: `${loadingPulse} 1.2s ease-in-out infinite`, + } + : { + opacity: 0, + transform: "translate(-50%, -50%) scale(0)", + transition: "transform 0.45s ease, opacity 0.45s ease", + }), + } + : {}, + ...(loading + ? {} + : { + "&:hover": { + transform: "translateY(-1px)", + boxShadow: theme.shadows[4], + }, + "&:hover::after": shouldAnimate + ? { + transform: "translate(-50%, -50%) scale(15)", + opacity: 1, + } + : {}, + "&:active": { + transform: "translateY(0)", + boxShadow: theme.shadows[2], + }, + "&:active::after": shouldAnimate + ? { + transform: "translate(-50%, -50%) scale(18)", + opacity: 0.4, + } + : {}, + }), + "&.Mui-disabled": { + boxShadow: "none", + transform: "none", + ...(loading && shouldAnimate + ? {} + : { + "&::after": { + opacity: 0, + }, + }), + }, + ...(disabled && { + boxShadow: "none", + transform: "none", + }), + "& > *": { + position: "relative", + zIndex: 1, + }, + }; +}); + +export const AnimatedCircleButton = forwardRef< + HTMLButtonElement, + AnimatedCircleButtonProps +>((props, ref) => { + const { + loading = false, + disabled, + children, + startIcon, + endIcon, + ...rest + } = props; + + const effectiveStartIcon = loading ? ( + + ) : ( + startIcon + ); + + return ( + + {children} + + ); +}); diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 584c712..4728641 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -2,3 +2,4 @@ export * from "./TabPanel"; export * from "./BackButton"; export * from "./Modal"; export * from "./CoordinatesInput"; +export * from "./AnimatedCircleButton"; diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 38a9018..d174581 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/route/route-preview/web-gl/webglmap.tsx","./src/pages/route/route-preview/webgl-prototype/webglroutemapprototype.tsx","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/animatedcirclebutton.tsx","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e31cddf..6619cd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,7 +16,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz" integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.21.3", "@babel/core@^7.28.0": +"@babel/core@^7.21.3", "@babel/core@^7.28.0": version "7.28.5" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz" integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== @@ -170,6 +170,28 @@ resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz" integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== +"@emnapi/core@^1.5.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.0.tgz#135de4e8858763989112281bdf38ca02439db7c3" + integrity sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw== + dependencies: + "@emnapi/wasi-threads" "1.1.0" + tslib "^2.4.0" + +"@emnapi/runtime@^1.5.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.0.tgz#d7ef3832df8564fe5903bf0567aedbd19538ecbe" + integrity sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" + integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== + dependencies: + tslib "^2.4.0" + "@emotion/babel-plugin@^11.13.5": version "11.13.5" resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" @@ -215,7 +237,7 @@ resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== -"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.9.0": +"@emotion/react@^11.14.0": version "11.14.0" resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== @@ -245,7 +267,7 @@ resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== -"@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1": +"@emotion/styled@^11.14.0": version "11.14.1" resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz" integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw== @@ -277,11 +299,136 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== +"@esbuild/aix-ppc64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49" + integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg== + +"@esbuild/android-arm64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03" + integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ== + +"@esbuild/android-arm@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae" + integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg== + +"@esbuild/android-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6" + integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g== + "@esbuild/darwin-arm64@0.25.11": version "0.25.11" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz" integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w== +"@esbuild/darwin-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe" + integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ== + +"@esbuild/freebsd-arm64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a" + integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA== + +"@esbuild/freebsd-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb" + integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw== + +"@esbuild/linux-arm64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5" + integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA== + +"@esbuild/linux-arm@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f" + integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw== + +"@esbuild/linux-ia32@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b" + integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw== + +"@esbuild/linux-loong64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb" + integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw== + +"@esbuild/linux-mips64el@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5" + integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ== + +"@esbuild/linux-ppc64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74" + integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw== + +"@esbuild/linux-riscv64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273" + integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww== + +"@esbuild/linux-s390x@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263" + integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw== + +"@esbuild/linux-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910" + integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ== + +"@esbuild/netbsd-arm64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077" + integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg== + +"@esbuild/netbsd-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034" + integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A== + +"@esbuild/openbsd-arm64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad" + integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg== + +"@esbuild/openbsd-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2" + integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw== + +"@esbuild/openharmony-arm64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1" + integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ== + +"@esbuild/sunos-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244" + integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA== + +"@esbuild/win32-arm64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935" + integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q== + +"@esbuild/win32-ia32@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343" + integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA== + +"@esbuild/win32-x64@0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f" + integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA== + "@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": version "4.9.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz" @@ -332,7 +479,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^9.25.0", "@eslint/js@9.38.0": +"@eslint/js@9.38.0", "@eslint/js@^9.25.0": version "9.38.0" resolved "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz" integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A== @@ -442,7 +589,7 @@ dependencies: "@babel/runtime" "^7.28.4" -"@mui/material@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/material@^7.1.0", "@mui/material@^7.3.4": +"@mui/material@^7.1.0": version "7.3.4" resolved "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz" integrity sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw== @@ -481,7 +628,7 @@ csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system@^7.3.3": +"@mui/system@^7.3.3": version "7.3.3" resolved "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz" integrity sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q== @@ -546,6 +693,15 @@ "@mui/utils" "^7.3.3" "@mui/x-internals" "8.14.0" +"@napi-rs/wasm-runtime@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz#dcfea99a75f06209a235f3d941e3460a51e9b14c" + integrity sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw== + dependencies: + "@emnapi/core" "^1.5.0" + "@emnapi/runtime" "^1.5.0" + "@tybys/wasm-util" "^0.10.1" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -554,7 +710,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -572,7 +728,7 @@ resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz" integrity sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g== -"@photo-sphere-viewer/core@^5.13.2", "@photo-sphere-viewer/core@>=5.13.1": +"@photo-sphere-viewer/core@^5.13.2": version "5.14.0" resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.14.0.tgz" integrity sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A== @@ -624,7 +780,7 @@ utility-types "^3.11.0" zustand "^5.0.1" -"@react-three/fiber@^9.0.0", "@react-three/fiber@^9.1.2": +"@react-three/fiber@^9.1.2": version "9.4.0" resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz" integrity sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g== @@ -656,11 +812,116 @@ estree-walker "^2.0.2" picomatch "^4.0.2" +"@rollup/rollup-android-arm-eabi@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db" + integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ== + +"@rollup/rollup-android-arm64@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5" + integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA== + "@rollup/rollup-darwin-arm64@4.52.5": version "4.52.5" resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz" integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA== +"@rollup/rollup-darwin-x64@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956" + integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA== + +"@rollup/rollup-freebsd-arm64@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899" + integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA== + +"@rollup/rollup-freebsd-x64@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10" + integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c" + integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ== + +"@rollup/rollup-linux-arm-musleabihf@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00" + integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ== + +"@rollup/rollup-linux-arm64-gnu@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc" + integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg== + +"@rollup/rollup-linux-arm64-musl@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0" + integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q== + +"@rollup/rollup-linux-loong64-gnu@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2" + integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA== + +"@rollup/rollup-linux-ppc64-gnu@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5" + integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw== + +"@rollup/rollup-linux-riscv64-gnu@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994" + integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw== + +"@rollup/rollup-linux-riscv64-musl@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f" + integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg== + +"@rollup/rollup-linux-s390x-gnu@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b" + integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ== + +"@rollup/rollup-linux-x64-gnu@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278" + integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q== + +"@rollup/rollup-linux-x64-musl@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350" + integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg== + +"@rollup/rollup-openharmony-arm64@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30" + integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw== + +"@rollup/rollup-win32-arm64-msvc@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937" + integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w== + +"@rollup/rollup-win32-ia32-msvc@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50" + integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg== + +"@rollup/rollup-win32-x64-gnu@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3" + integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ== + +"@rollup/rollup-win32-x64-msvc@4.52.5": + version "4.52.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107" + integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg== + "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz" @@ -715,7 +976,7 @@ "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" "@svgr/babel-plugin-transform-svg-component" "8.0.0" -"@svgr/core@*", "@svgr/core@^8.1.0": +"@svgr/core@^8.1.0": version "8.1.0" resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== @@ -757,11 +1018,73 @@ source-map-js "^1.2.1" tailwindcss "4.1.16" +"@tailwindcss/oxide-android-arm64@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz#9bd16c0a08db20d7c93907a9bd1564e0255307eb" + integrity sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA== + "@tailwindcss/oxide-darwin-arm64@4.1.16": version "4.1.16" resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz" integrity sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA== +"@tailwindcss/oxide-darwin-x64@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz#6193bafbb1a885795702f12bbef9cc5eb4cc550b" + integrity sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg== + +"@tailwindcss/oxide-freebsd-x64@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz#0e2b064d71ba87a9001ac963be2752a8ddb64349" + integrity sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg== + +"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz#8e80c959eeda81a08ed955e23eb6d228287b9672" + integrity sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw== + +"@tailwindcss/oxide-linux-arm64-gnu@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz#d5f54910920fc5808122515f5208c5ecc1a40545" + integrity sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w== + +"@tailwindcss/oxide-linux-arm64-musl@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz#67cdb932230ac47bf3bf5415ccc92417b27020ee" + integrity sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ== + +"@tailwindcss/oxide-linux-x64-gnu@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz#80ae0cfd8ebc970f239060ecdfdd07f6f6b14dce" + integrity sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew== + +"@tailwindcss/oxide-linux-x64-musl@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz#524e5b87e8e79a712de3d9bbb94d2fc2fa44391c" + integrity sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw== + +"@tailwindcss/oxide-wasm32-wasi@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz#dc31d6bc1f6c1e8119a335ae3f28deb4d7c560f2" + integrity sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q== + dependencies: + "@emnapi/core" "^1.5.0" + "@emnapi/runtime" "^1.5.0" + "@emnapi/wasi-threads" "^1.1.0" + "@napi-rs/wasm-runtime" "^1.0.7" + "@tybys/wasm-util" "^0.10.1" + tslib "^2.4.0" + +"@tailwindcss/oxide-win32-arm64-msvc@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz#f1f810cdb49dae8071d5edf0db5cc0da2ec6a7e8" + integrity sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A== + +"@tailwindcss/oxide-win32-x64-msvc@4.1.16": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz#76dcda613578f06569c0a6015f39f12746a24dce" + integrity sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg== + "@tailwindcss/oxide@4.1.16": version "4.1.16" resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz" @@ -801,6 +1124,13 @@ resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz" integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== +"@tybys/wasm-util@^0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" + integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== + dependencies: + tslib "^2.4.0" + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" @@ -870,7 +1200,7 @@ dependencies: "@types/estree" "*" -"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@1.0.8": +"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6": version "1.0.8" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -904,7 +1234,7 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.15.24": +"@types/node@^22.15.24": version "22.18.13" resolved "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz" integrity sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A== @@ -951,7 +1281,7 @@ resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@*", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18.2.25 || ^19", "@types/react@^19.1.2", "@types/react@^19.2.0", "@types/react@>=16.8", "@types/react@>=18", "@types/react@>=18.0.0": +"@types/react@^19.1.2": version "19.2.2" resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz" integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA== @@ -970,7 +1300,7 @@ dependencies: "@types/estree" "*" -"@types/three@*", "@types/three@>=0.134.0": +"@types/three@*": version "0.180.0" resolved "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz" integrity sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg== @@ -1018,7 +1348,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@^8.46.2", "@typescript-eslint/parser@8.46.2": +"@typescript-eslint/parser@8.46.2": version "8.46.2" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz" integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g== @@ -1046,7 +1376,7 @@ "@typescript-eslint/types" "8.46.2" "@typescript-eslint/visitor-keys" "8.46.2" -"@typescript-eslint/tsconfig-utils@^8.46.2", "@typescript-eslint/tsconfig-utils@8.46.2": +"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2": version "8.46.2" resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz" integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag== @@ -1062,7 +1392,7 @@ debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@^8.46.2", "@typescript-eslint/types@8.46.2": +"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.46.2": version "8.46.2" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz" integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ== @@ -1145,7 +1475,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0: +acorn@^8.15.0: version "8.15.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -1249,7 +1579,7 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0, "browserslist@>= 4.21.0": +browserslist@^4.24.0: version "4.27.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz" integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw== @@ -1546,7 +1876,7 @@ earcut@^3.0.0, earcut@^3.0.2: resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz" integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ== -easymde@^2.20.0, "easymde@>= 2.0.0 < 3.0.0": +easymde@^2.20.0: version "2.20.0" resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz" integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ== @@ -1689,7 +2019,7 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.25.0, eslint@>=8.40: +eslint@^9.25.0: version "9.38.0" resolved "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz" integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw== @@ -1815,12 +2145,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fdir@^6.4.4: - version "6.5.0" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" - integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== - -fdir@^6.5.0: +fdir@^6.4.4, fdir@^6.5.0: version "6.5.0" resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== @@ -2284,7 +2609,7 @@ its-fine@^2.0.0: dependencies: "@types/react-reconciler" "^0.28.9" -jiti@*, jiti@^2.6.1, jiti@>=1.21.0: +jiti@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== @@ -2363,12 +2688,62 @@ lie@^3.0.2: dependencies: immediate "~3.0.5" +lightningcss-android-arm64@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz#6966b7024d39c94994008b548b71ab360eb3a307" + integrity sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A== + lightningcss-darwin-arm64@1.30.2: version "1.30.2" resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz" integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA== -lightningcss@^1.21.0, lightningcss@1.30.2: +lightningcss-darwin-x64@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz#5ce87e9cd7c4f2dcc1b713f5e8ee185c88d9b7cd" + integrity sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ== + +lightningcss-freebsd-x64@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz#6ae1d5e773c97961df5cff57b851807ef33692a5" + integrity sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA== + +lightningcss-linux-arm-gnueabihf@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz#62c489610c0424151a6121fa99d77731536cdaeb" + integrity sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA== + +lightningcss-linux-arm64-gnu@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz#2a3661b56fe95a0cafae90be026fe0590d089298" + integrity sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A== + +lightningcss-linux-arm64-musl@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz#d7ddd6b26959245e026bc1ad9eb6aa983aa90e6b" + integrity sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA== + +lightningcss-linux-x64-gnu@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz#5a89814c8e63213a5965c3d166dff83c36152b1a" + integrity sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w== + +lightningcss-linux-x64-musl@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz#808c2e91ce0bf5d0af0e867c6152e5378c049728" + integrity sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA== + +lightningcss-win32-arm64-msvc@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz#ab4a8a8a2e6a82a4531e8bbb6bf0ff161ee6625a" + integrity sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ== + +lightningcss-win32-x64-msvc@1.30.2: + version "1.30.2" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz#f01f382c8e0a27e1c018b0bee316d210eac43b6e" + integrity sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw== + +lightningcss@1.30.2: version "1.30.2" resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz" integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ== @@ -2812,7 +3187,7 @@ mobx-react-lite@^4.1.0: dependencies: use-sync-external-store "^1.4.0" -mobx@^6.13.7, mobx@^6.9.0: +mobx@^6.13.7: version "6.15.0" resolved "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz" integrity sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g== @@ -2993,12 +3368,12 @@ picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -"picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.3: +picomatch@^4.0.2, picomatch@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== -pixi.js@^8.10.1, pixi.js@^8.2.6: +pixi.js@^8.10.1: version "8.14.0" resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz" integrity sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw== @@ -3055,7 +3430,7 @@ promise-worker-transferable@^1.0.4: is-promise "^2.1.0" lie "^3.0.2" -prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -3116,19 +3491,14 @@ rbush@^4.0.0: dependencies: quickselect "^3.0.0" -"react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19, react-dom@^19.0.0, react-dom@^19.1.0, react-dom@>=16.0.0, react-dom@>=16.13, react-dom@>=16.6.0, react-dom@>=16.8.2, react-dom@>=18: +react-dom@^19.1.0: version "19.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz" integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ== dependencies: scheduler "^0.27.0" -react-is@^16.13.1: - version "16.13.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^16.7.0: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -3162,7 +3532,7 @@ react-photo-sphere-viewer@^6.2.3: dependencies: eventemitter3 "^5.0.1" -react-reconciler@^0.31.0, react-reconciler@0.31.0: +react-reconciler@0.31.0, react-reconciler@^0.31.0: version "0.31.0" resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz" integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ== @@ -3189,7 +3559,7 @@ react-router-dom@^7.6.1: dependencies: react-router "7.9.4" -react-router@^7.9.4, react-router@7.9.4: +react-router@7.9.4, react-router@^7.9.4: version "7.9.4" resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz" integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA== @@ -3226,12 +3596,12 @@ react-use-measure@^2.1.7: resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz" integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== -"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19", "react@^18.0 || ^19", "react@^18.0.0 || ^19.0.0", react@^19, react@^19.0.0, react@^19.1.0, react@^19.2.0, "react@>= 16.8.0", react@>=16.0.0, react@>=16.13, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.8.2, react@>=17.0, react@>=18, react@>=18.0.0, react@>=19.0.0: +react@^19.1.0: version "19.2.0" resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz" integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ== -redux@^5.0.0, redux@^5.0.1: +redux@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== @@ -3317,7 +3687,7 @@ rollup-plugin-visualizer@^6.0.5: source-map "^0.7.4" yargs "^17.5.1" -rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^4.34.9, "rollup@2.x || 3.x || 4.x": +rollup@^4.34.9: version "4.52.5" resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz" integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw== @@ -3503,7 +3873,7 @@ svg-parser@^2.0.4: resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -tailwindcss@^4.1.8, "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1", tailwindcss@4.1.16: +tailwindcss@4.1.16, tailwindcss@^4.1.8: version "4.1.16" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz" integrity sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA== @@ -3535,7 +3905,7 @@ three@^0.170.0: resolved "https://registry.npmjs.org/three/-/three-0.170.0.tgz" integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ== -three@^0.177.0, "three@>= 0.159.0", three@>=0.125.0, three@>=0.126.1, three@>=0.128.0, three@>=0.134.0, three@>=0.137, three@>=0.156, three@>=0.159: +three@^0.177.0: version "0.177.0" resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz" integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg== @@ -3605,9 +3975,9 @@ ts-api-utils@^2.1.0: resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.0.3: +tslib@^2.0.3, tslib@^2.4.0: version "2.8.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tunnel-rat@^0.1.2: @@ -3634,7 +4004,7 @@ typescript-eslint@^8.30.1: "@typescript-eslint/typescript-estree" "8.46.2" "@typescript-eslint/utils" "8.46.2" -typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@>=4.9.5, typescript@~5.8.3: +typescript@~5.8.3: version "5.8.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== @@ -3715,7 +4085,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0, use-sync-external-store@>=1.2.0: +use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== @@ -3770,7 +4140,7 @@ vite-plugin-svgr@^4.5.0: "@svgr/core" "^8.1.0" "@svgr/plugin-jsx" "^8.1.0" -"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^6.3.5, vite@>=2.6.0: +vite@^6.3.5: version "6.4.1" resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz" integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==