fix: fix upload bug 3d
This commit is contained in:
98
src/shared/lib/gltfCacheManager.ts
Normal file
98
src/shared/lib/gltfCacheManager.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Утилита для управления кешем GLTF и blob URL
|
||||
*/
|
||||
|
||||
// Динамический импорт useGLTF для избежания проблем с SSR
|
||||
let useGLTF: any = null;
|
||||
|
||||
const initializeUseGLTF = async () => {
|
||||
if (!useGLTF) {
|
||||
try {
|
||||
const drei = await import("@react-three/drei");
|
||||
useGLTF = drei.useGLTF;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"⚠️ GLTFCacheManager: Не удалось импортировать useGLTF",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
return useGLTF;
|
||||
};
|
||||
|
||||
/**
|
||||
* Очищает кеш GLTF для конкретного URL
|
||||
*/
|
||||
export const clearGLTFCacheForUrl = async (url: string) => {
|
||||
try {
|
||||
const gltf = await initializeUseGLTF();
|
||||
if (gltf && gltf.clear) {
|
||||
gltf.clear(url);
|
||||
console.log("🧹 GLTFCacheManager: Очистка кеша для URL", { url });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ GLTFCacheManager: Ошибка при очистке кеша для URL", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Очищает весь кеш GLTF
|
||||
*/
|
||||
export const clearAllGLTFCache = async () => {
|
||||
try {
|
||||
const gltf = await initializeUseGLTF();
|
||||
if (gltf && gltf.clear) {
|
||||
gltf.clear();
|
||||
console.log("🧹 GLTFCacheManager: Очистка всего кеша GLTF");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ GLTFCacheManager: Ошибка при очистке всего кеша", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Очищает blob URL из памяти браузера
|
||||
*/
|
||||
export const revokeBlobURL = (url: string) => {
|
||||
if (url && url.startsWith("blob:")) {
|
||||
try {
|
||||
URL.revokeObjectURL(url);
|
||||
console.log("🧹 GLTFCacheManager: Отзыв blob URL", { url });
|
||||
} catch (error) {
|
||||
console.warn("⚠️ GLTFCacheManager: Ошибка при отзыве blob URL", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Комплексная очистка: blob URL + кеш GLTF
|
||||
*/
|
||||
export const clearBlobAndGLTFCache = async (url: string) => {
|
||||
// Сначала отзываем blob URL
|
||||
revokeBlobURL(url);
|
||||
|
||||
// Затем очищаем кеш GLTF
|
||||
await clearGLTFCacheForUrl(url);
|
||||
|
||||
console.log("🧹 GLTFCacheManager: Комплексная очистка выполнена", { url });
|
||||
};
|
||||
|
||||
/**
|
||||
* Очистка при смене медиа (для предотвращения конфликтов)
|
||||
*/
|
||||
export const clearMediaTransitionCache = async (
|
||||
previousMediaId: string | number | null,
|
||||
newMediaId: string | number | null,
|
||||
newMediaType?: number
|
||||
) => {
|
||||
console.log("🔄 GLTFCacheManager: Очистка кеша при смене медиа", {
|
||||
previousMediaId,
|
||||
newMediaId,
|
||||
newMediaType,
|
||||
});
|
||||
|
||||
// Если переключаемся с/на 3D модель, очищаем весь кеш
|
||||
if (newMediaType === 6 || previousMediaId) {
|
||||
await clearAllGLTFCache();
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./mui/theme";
|
||||
export * from "./DecodeJWT";
|
||||
export * from "./gltfCacheManager";
|
||||
|
||||
/**
|
||||
* Генерирует название медиа по умолчанию в разных форматах
|
||||
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
MEDIA_TYPE_VALUES,
|
||||
editSightStore,
|
||||
generateDefaultMediaName,
|
||||
clearBlobAndGLTFCache,
|
||||
} from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@@ -82,18 +83,41 @@ export const UploadMediaDialog = observer(
|
||||
[]
|
||||
);
|
||||
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
|
||||
const previousMediaUrlRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialFile) {
|
||||
// Очищаем предыдущий blob URL если он существует
|
||||
if (
|
||||
previousMediaUrlRef.current &&
|
||||
previousMediaUrlRef.current.startsWith("blob:")
|
||||
) {
|
||||
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
||||
}
|
||||
|
||||
setMediaFile(initialFile);
|
||||
setMediaFilename(initialFile.name);
|
||||
setAvailableMediaTypes([2]);
|
||||
setMediaType(2);
|
||||
setMediaUrl(URL.createObjectURL(initialFile));
|
||||
const newBlobUrl = URL.createObjectURL(initialFile);
|
||||
setMediaUrl(newBlobUrl);
|
||||
previousMediaUrlRef.current = newBlobUrl;
|
||||
setMediaName(initialFile.name.replace(/\.[^/.]+$/, ""));
|
||||
}
|
||||
}, [initialFile]);
|
||||
|
||||
// Очистка blob URL при размонтировании компонента
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (
|
||||
previousMediaUrlRef.current &&
|
||||
previousMediaUrlRef.current.startsWith("blob:")
|
||||
) {
|
||||
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
||||
}
|
||||
};
|
||||
}, []); // Пустой массив зависимостей - выполняется только при размонтировании
|
||||
|
||||
useEffect(() => {
|
||||
if (fileToUpload) {
|
||||
setMediaFile(fileToUpload);
|
||||
@@ -211,10 +235,24 @@ export const UploadMediaDialog = observer(
|
||||
|
||||
useEffect(() => {
|
||||
if (mediaFile) {
|
||||
setMediaUrl(URL.createObjectURL(mediaFile as Blob));
|
||||
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
|
||||
// Очищаем предыдущий blob URL и кеш GLTF если он существует
|
||||
if (
|
||||
previousMediaUrlRef.current &&
|
||||
previousMediaUrlRef.current.startsWith("blob:")
|
||||
) {
|
||||
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
||||
}
|
||||
}, [mediaFile]);
|
||||
|
||||
const newBlobUrl = URL.createObjectURL(mediaFile as Blob);
|
||||
setMediaUrl(newBlobUrl);
|
||||
previousMediaUrlRef.current = newBlobUrl; // Сохраняем новый URL в ref
|
||||
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
|
||||
console.log("🆕 UploadMediaDialog: Создан новый blob URL", {
|
||||
newBlobUrl,
|
||||
fileName: mediaFile.name,
|
||||
});
|
||||
}
|
||||
}, [mediaFile]); // Убираем mediaUrl из зависимостей чтобы избежать зацикливания
|
||||
|
||||
// const fileFormat = useEffect(() => {
|
||||
// const handleKeyPress = (event: KeyboardEvent) => {
|
||||
@@ -263,8 +301,20 @@ export const UploadMediaDialog = observer(
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Очищаем blob URL и кеш GLTF при закрытии диалога
|
||||
if (
|
||||
previousMediaUrlRef.current &&
|
||||
previousMediaUrlRef.current.startsWith("blob:")
|
||||
) {
|
||||
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
setMediaUrl(null);
|
||||
setMediaFile(null);
|
||||
setIsPreviewLoaded(false);
|
||||
previousMediaUrlRef.current = null; // Очищаем ref
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
101
src/shared/store/ModelLoadingStore/index.ts
Normal file
101
src/shared/store/ModelLoadingStore/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export interface ModelLoadingState {
|
||||
isLoading: boolean;
|
||||
progress: number;
|
||||
modelId: string | null;
|
||||
error?: string;
|
||||
startTime?: number;
|
||||
}
|
||||
|
||||
class ModelLoadingStore {
|
||||
private loadingStates: Map<string, ModelLoadingState> = new Map();
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
// Начать отслеживание загрузки модели
|
||||
startLoading(modelId: string) {
|
||||
this.loadingStates.set(modelId, {
|
||||
isLoading: true,
|
||||
progress: 0,
|
||||
modelId,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Обновить прогресс загрузки
|
||||
updateProgress(modelId: string, progress: number) {
|
||||
const state = this.loadingStates.get(modelId);
|
||||
if (state) {
|
||||
state.progress = Math.min(100, Math.max(0, progress));
|
||||
}
|
||||
}
|
||||
|
||||
// Завершить загрузку модели
|
||||
finishLoading(modelId: string) {
|
||||
const state = this.loadingStates.get(modelId);
|
||||
if (state) {
|
||||
state.isLoading = false;
|
||||
state.progress = 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Остановить загрузку (в случае ошибки)
|
||||
stopLoading(modelId: string) {
|
||||
this.loadingStates.delete(modelId);
|
||||
}
|
||||
|
||||
// Обработать ошибку загрузки
|
||||
handleError(modelId: string, error?: string) {
|
||||
const state = this.loadingStates.get(modelId);
|
||||
if (state) {
|
||||
state.isLoading = false;
|
||||
state.error = error || "Ошибка загрузки модели";
|
||||
}
|
||||
}
|
||||
|
||||
// Получить состояние загрузки для конкретной модели
|
||||
getLoadingState(modelId: string): ModelLoadingState | undefined {
|
||||
return this.loadingStates.get(modelId);
|
||||
}
|
||||
|
||||
// Проверить, загружается ли какая-либо модель
|
||||
get isAnyModelLoading(): boolean {
|
||||
return Array.from(this.loadingStates.values()).some(
|
||||
(state) => state.isLoading
|
||||
);
|
||||
}
|
||||
|
||||
// Получить все загружающиеся модели
|
||||
get loadingModels(): ModelLoadingState[] {
|
||||
return Array.from(this.loadingStates.values()).filter(
|
||||
(state) => state.isLoading
|
||||
);
|
||||
}
|
||||
|
||||
// Получить общий прогресс всех загружающихся моделей
|
||||
get overallProgress(): number {
|
||||
const loadingModels = this.loadingModels;
|
||||
if (loadingModels.length === 0) return 100;
|
||||
|
||||
const totalProgress = loadingModels.reduce(
|
||||
(sum, model) => sum + model.progress,
|
||||
0
|
||||
);
|
||||
return Math.round(totalProgress / loadingModels.length);
|
||||
}
|
||||
|
||||
// Проверить, заблокировано ли сохранение (есть ли загружающиеся модели)
|
||||
get isSaveBlocked(): boolean {
|
||||
return this.isAnyModelLoading;
|
||||
}
|
||||
|
||||
// Очистить все состояния загрузки
|
||||
clearAll() {
|
||||
this.loadingStates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const modelLoadingStore = new ModelLoadingStore();
|
||||
143
src/shared/ui/LoadingSpinner/index.tsx
Normal file
143
src/shared/ui/LoadingSpinner/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
} from "@mui/material";
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
progress?: number;
|
||||
message?: string;
|
||||
size?: number;
|
||||
color?: "primary" | "secondary" | "error" | "info" | "success" | "warning";
|
||||
variant?: "circular" | "linear";
|
||||
showPercentage?: boolean;
|
||||
thickness?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
||||
progress,
|
||||
message = "Загрузка...",
|
||||
size = 40,
|
||||
color = "primary",
|
||||
variant = "circular",
|
||||
showPercentage = true,
|
||||
thickness = 4,
|
||||
className,
|
||||
}) => {
|
||||
if (variant === "linear") {
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 2,
|
||||
padding: 3,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: "100%", maxWidth: 300 }}>
|
||||
<LinearProgress
|
||||
variant={progress !== undefined ? "determinate" : "indeterminate"}
|
||||
value={progress}
|
||||
color={color}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showPercentage && progress !== undefined && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: "block",
|
||||
textAlign: "center",
|
||||
mt: 1,
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(progress)}%`}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{message && (
|
||||
<Typography variant="body2" color="text.secondary" textAlign="center">
|
||||
{message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 2,
|
||||
padding: 3,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: "relative", display: "inline-flex" }}>
|
||||
<CircularProgress
|
||||
size={size}
|
||||
color={color}
|
||||
variant={progress !== undefined ? "determinate" : "indeterminate"}
|
||||
value={progress}
|
||||
thickness={thickness}
|
||||
sx={{
|
||||
"& .MuiCircularProgress-circle": {
|
||||
strokeLinecap: "round",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showPercentage && progress !== undefined && (
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: size * 0.25,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(progress)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{message && (
|
||||
<Typography variant="body2" color="text.secondary" textAlign="center">
|
||||
{message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
196
src/shared/ui/ModelLoadingIndicator/index.tsx
Normal file
196
src/shared/ui/ModelLoadingIndicator/index.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
|
||||
interface ModelLoadingIndicatorProps {
|
||||
progress?: number;
|
||||
message?: string;
|
||||
isVisible?: boolean;
|
||||
variant?: "overlay" | "inline";
|
||||
size?: "small" | "medium" | "large";
|
||||
showDetails?: boolean;
|
||||
}
|
||||
|
||||
export const ModelLoadingIndicator: React.FC<ModelLoadingIndicatorProps> = ({
|
||||
progress = 0,
|
||||
message = "Загрузка 3D модели...",
|
||||
isVisible = true,
|
||||
variant = "overlay",
|
||||
size = "medium",
|
||||
showDetails = true,
|
||||
}) => {
|
||||
const sizeConfig = {
|
||||
small: {
|
||||
spinnerSize: 32,
|
||||
fontSize: "0.75rem",
|
||||
progressBarWidth: 150,
|
||||
padding: 2,
|
||||
},
|
||||
medium: {
|
||||
spinnerSize: 48,
|
||||
fontSize: "0.875rem",
|
||||
progressBarWidth: 200,
|
||||
padding: 3,
|
||||
},
|
||||
large: {
|
||||
spinnerSize: 64,
|
||||
fontSize: "1rem",
|
||||
progressBarWidth: 250,
|
||||
padding: 4,
|
||||
},
|
||||
};
|
||||
|
||||
const currentSize = sizeConfig[size];
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
const getProgressStage = (progress: number): string => {
|
||||
if (progress < 20) return "Инициализация...";
|
||||
if (progress < 40) return "Загрузка геометрии...";
|
||||
if (progress < 60) return "Обработка материалов...";
|
||||
if (progress < 80) return "Загрузка текстур...";
|
||||
if (progress < 95) return "Финализация...";
|
||||
if (progress === 100) return "Готово!";
|
||||
return "Загрузка...";
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 2,
|
||||
padding: currentSize.padding,
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Крутяшка с процентами */}
|
||||
<Box sx={{ position: "relative", display: "inline-flex" }}>
|
||||
<CircularProgress
|
||||
size={currentSize.spinnerSize}
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
thickness={4}
|
||||
sx={{
|
||||
color: "primary.main",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: currentSize.spinnerSize * 0.25,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(progress)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Линейный прогресс бар */}
|
||||
<Box sx={{ width: "100%", maxWidth: currentSize.progressBarWidth }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
color="primary"
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Основное сообщение */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: currentSize.fontSize,
|
||||
fontWeight: 500,
|
||||
maxWidth: 280,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Typography>
|
||||
|
||||
{/* Детальная информация о прогрессе */}
|
||||
{showDetails && progress > 0 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.disabled"
|
||||
sx={{
|
||||
fontSize: "0.75rem",
|
||||
opacity: 0.8,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{getProgressStage(progress)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (variant === "overlay") {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 1000,
|
||||
borderRadius: 1,
|
||||
border: "1px solid rgba(0, 0, 0, 0.05)",
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 200,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.02)",
|
||||
borderRadius: 2,
|
||||
border: "1px dashed",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,78 @@ import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
||||
import { useEffect, Suspense } from "react";
|
||||
import { Box, CircularProgress, Typography } from "@mui/material";
|
||||
|
||||
// Утилита для очистки кеша GLTF
|
||||
const clearGLTFCache = (url?: string) => {
|
||||
try {
|
||||
if (url) {
|
||||
// Если это blob URL, очищаем его из кеша
|
||||
if (url.startsWith("blob:")) {
|
||||
useGLTF.clear(url);
|
||||
console.log("🧹 clearGLTFCache: Очистка blob URL из кеша GLTF", {
|
||||
url,
|
||||
});
|
||||
} else {
|
||||
useGLTF.clear(url);
|
||||
console.log("🧹 clearGLTFCache: Очистка кеша для URL", { url });
|
||||
}
|
||||
} else {
|
||||
// Очищаем весь кеш GLTF
|
||||
useGLTF.clear();
|
||||
console.log("🧹 clearGLTFCache: Очистка всего кеша GLTF");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ clearGLTFCache: Ошибка при очистке кеша", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Утилита для проверки типа файла
|
||||
const isValid3DFile = (url: string): boolean => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname.toLowerCase();
|
||||
const searchParams = urlObj.searchParams;
|
||||
|
||||
// Проверяем расширение файла в пути
|
||||
const validExtensions = [".glb", ".gltf"];
|
||||
const hasValidExtension = validExtensions.some((ext) =>
|
||||
pathname.endsWith(ext)
|
||||
);
|
||||
|
||||
// Проверяем параметры запроса на наличие типа файла
|
||||
const fileType = searchParams.get("type") || searchParams.get("format");
|
||||
const hasValidType =
|
||||
fileType && ["glb", "gltf"].includes(fileType.toLowerCase());
|
||||
|
||||
// Если это blob URL, считаем его валидным (пользователь выбрал файл)
|
||||
const isBlobUrl = url.startsWith("blob:");
|
||||
|
||||
// Если это URL с токеном и нет явного расширения, считаем валидным
|
||||
// (предполагаем что сервер вернет правильный файл)
|
||||
const hasToken = searchParams.has("token");
|
||||
const isServerUrl = hasToken && !hasValidExtension;
|
||||
|
||||
const isValid =
|
||||
hasValidExtension || hasValidType || isBlobUrl || isServerUrl;
|
||||
|
||||
console.log("🔍 isValid3DFile: Проверка файла", {
|
||||
url,
|
||||
pathname,
|
||||
hasValidExtension,
|
||||
fileType,
|
||||
hasValidType,
|
||||
isBlobUrl,
|
||||
isServerUrl,
|
||||
isValid,
|
||||
});
|
||||
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error);
|
||||
// В случае ошибки парсинга URL, считаем валидным (пусть useGLTF сам разберется)
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
type ModelViewerProps = {
|
||||
width?: string;
|
||||
fileUrl: string;
|
||||
@@ -10,6 +82,18 @@ type ModelViewerProps = {
|
||||
};
|
||||
|
||||
const Model = ({ fileUrl }: { fileUrl: string }) => {
|
||||
// Очищаем кеш перед загрузкой новой модели
|
||||
useEffect(() => {
|
||||
// Очищаем кеш для текущего URL
|
||||
clearGLTFCache(fileUrl);
|
||||
}, [fileUrl]);
|
||||
|
||||
// Проверяем валидность файла перед загрузкой (только для blob URL)
|
||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||
console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl });
|
||||
throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`);
|
||||
}
|
||||
|
||||
const { scene } = useGLTF(fileUrl);
|
||||
return <primitive object={scene} />;
|
||||
};
|
||||
@@ -49,11 +133,26 @@ export const ThreeView = ({
|
||||
height = "100%",
|
||||
width = "100%",
|
||||
}: ModelViewerProps) => {
|
||||
// Очищаем кеш при размонтировании
|
||||
// Проверяем валидность файла (только для blob URL)
|
||||
useEffect(() => {
|
||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||
console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl });
|
||||
}
|
||||
}, [fileUrl]);
|
||||
|
||||
// Очищаем кеш при размонтировании и при смене URL
|
||||
useEffect(() => {
|
||||
// Очищаем кеш сразу при монтировании компонента
|
||||
clearGLTFCache(fileUrl);
|
||||
console.log("🧹 ThreeView: Очистка кеша модели при монтировании", {
|
||||
fileUrl,
|
||||
});
|
||||
|
||||
return () => {
|
||||
useGLTF.clear(fileUrl);
|
||||
console.log("🧹 ThreeView: Очистка кеша модели", { fileUrl });
|
||||
clearGLTFCache(fileUrl);
|
||||
console.log("🧹 ThreeView: Очистка кеша модели при размонтировании", {
|
||||
fileUrl,
|
||||
});
|
||||
};
|
||||
}, [fileUrl]);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
|
||||
import { ThreeView } from "./ThreeView";
|
||||
import { ThreeViewErrorBoundary } from "./ThreeViewErrorBoundary";
|
||||
import { clearMediaTransitionCache } from "../../shared/lib/gltfCacheManager";
|
||||
|
||||
export interface MediaData {
|
||||
id: string | number;
|
||||
@@ -28,6 +29,30 @@ export function MediaViewer({
|
||||
}>) {
|
||||
const token = localStorage.getItem("token");
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
const [previousMediaId, setPreviousMediaId] = useState<
|
||||
string | number | null
|
||||
>(null);
|
||||
|
||||
// Сбрасываем ключ при смене медиа
|
||||
useEffect(() => {
|
||||
if (media?.id !== previousMediaId) {
|
||||
console.log("🔄 MediaViewer: Смена медиа, сброс ключа", {
|
||||
previousMediaId,
|
||||
newMediaId: media?.id,
|
||||
mediaType: media?.media_type,
|
||||
});
|
||||
|
||||
// Используем новый cache manager для очистки кеша
|
||||
clearMediaTransitionCache(
|
||||
previousMediaId,
|
||||
media?.id || null,
|
||||
media?.media_type
|
||||
);
|
||||
|
||||
setResetKey(0);
|
||||
setPreviousMediaId(media?.id || null);
|
||||
}
|
||||
}, [media?.id, media?.media_type, previousMediaId]);
|
||||
|
||||
const handleReset = () => {
|
||||
console.log("🔄 MediaViewer: handleReset вызван", {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user