Add STL 3D viewer preview support
All checks were successful
Automated Container Build / build-and-push (push) Successful in 29s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 29s
This commit is contained in:
parent
bfe55e2bfc
commit
cd540499ca
4 changed files with 462 additions and 2 deletions
|
|
@ -10,6 +10,7 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|||
import { formatSize, getFileDisplayName } from '@/lib/utils';
|
||||
|
||||
const DynamicPdfViewer = dynamic(() => import('./PdfViewer'), { ssr: false });
|
||||
const DynamicStlPreview = dynamic(() => import('./StlPreview'), { ssr: false });
|
||||
|
||||
interface FilePreviewProps {
|
||||
file: {
|
||||
|
|
@ -85,7 +86,7 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
|||
exit={{ scale: 0.95, opacity: 0, y: 20 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`relative flex flex-col max-w-[95vw] max-h-[95vh] rounded-2xl overflow-hidden shadow-2xl ${
|
||||
(previewType === 'image' || previewType === 'video' || previewType === 'pdf' || previewType === 'audio') ? 'w-fit' : 'w-full'
|
||||
(previewType === 'image' || previewType === 'video' || previewType === 'pdf' || previewType === 'stl' || previewType === 'audio') ? 'w-fit' : 'w-full'
|
||||
} ${
|
||||
(previewType === 'image' || previewType === 'video' || previewType === 'audio') ? 'h-fit' : 'h-full'
|
||||
}`}
|
||||
|
|
@ -160,6 +161,16 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
|||
)
|
||||
)}
|
||||
|
||||
{previewType === 'stl' && (
|
||||
!downloadToken ? (
|
||||
<div className="flex items-center justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-accent" /></div>
|
||||
) : (
|
||||
<div className="w-[80vw] h-[80vh] max-w-[1200px] max-h-[800px]">
|
||||
<DynamicStlPreview url={downloadUrl} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{(previewType === 'text' || previewType === 'code') && (
|
||||
<div className="absolute inset-0 p-4">
|
||||
{loading ? (
|
||||
|
|
@ -234,12 +245,13 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
|||
|
||||
// --- Helpers ---
|
||||
|
||||
type PreviewType = 'image' | 'video' | 'audio' | 'text' | 'code' | 'pdf' | 'unsupported';
|
||||
type PreviewType = 'image' | 'video' | 'audio' | 'text' | 'code' | 'pdf' | 'stl' | 'unsupported';
|
||||
|
||||
function getPreviewType(file: { name: string; mime_type: string }): PreviewType {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
|
||||
if (ext === 'pdf' || file.mime_type === 'application/pdf') return 'pdf';
|
||||
if (ext === 'stl' || file.mime_type === 'model/stl') return 'stl';
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext)) return 'image';
|
||||
if (['mp4', 'webm', 'ogg'].includes(ext)) return 'video'; // Only browser-supported videos
|
||||
if (['mp3', 'wav', 'flac', 'm4a', 'aac'].includes(ext)) return 'audio';
|
||||
|
|
|
|||
50
frontend/src/components/StlPreview.tsx
Normal file
50
frontend/src/components/StlPreview.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React, { useState } from 'react';
|
||||
import { StlViewer } from 'react-19-stl-viewer';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface StlPreviewProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function StlPreview({ url }: StlPreviewProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
{loading && !error && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center z-10" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
<Loader2 className="w-8 h-8 animate-spin mb-4" style={{ color: 'var(--color-accent)' }} />
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--color-text-secondary)' }}>Loading 3D Model...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center z-10" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
<svg className="w-12 h-12 mb-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-red-500">Failed to load STL file</span>
|
||||
<span className="text-xs mt-2" style={{ color: 'var(--color-text-tertiary)' }}>{error.message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`w-full h-full transition-opacity duration-300 ${loading ? 'opacity-0' : 'opacity-100'}`}>
|
||||
<StlViewer
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
orbitControls
|
||||
shadows
|
||||
url={url}
|
||||
onFinishLoading={() => setLoading(false)}
|
||||
onError={(err) => {
|
||||
console.error('STL Load Error:', err);
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
}}
|
||||
modelProps={{ color: '#007BFF' }}
|
||||
floorProps={{ gridWidth: 100, gridLength: 100 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in a new issue