diff --git a/README.md b/README.md
index af030d9..0b69775 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@

-# STLVault
+
+# STLVault


@@ -11,8 +12,6 @@

-
-
**STLVault** is a containerized 3D Model library manager and organizer, designed specifically for 3D printing enthusiasts. It provides a clean, modern web interface to manage your growing collection of STL, STEP, and 3MF files.
> **Note:** This project is currently in Beta. While the core functionality (importing, organizing, viewing) works, expect changes and improvements.
@@ -23,7 +22,7 @@
- **📂 Nestable Folders:** Organize your models into a deep hierarchy that makes sense to you.
- **🪄 Open in Slicer:** Let's you open the model direclty in your slicer.
-- **🔗 URL Import:** Import multiple files from Printables URL, with granular file selection. (Only models URL, not collections)
+- **🔗 URL Import:** Import multiple files from Printables URL, with granular file selection. (Only models URL)
- **🖱️ Drag n' Drop:** Seamlessly import new models or move files between folders.
- **📦 Bulk Actions:** Tag, move, delete, download, or upload multiple files at once.
- **👁️ 3D Preview:** Integrated web-based 3D viewer for STL and 3MF files.
@@ -38,7 +37,7 @@
- **Backend:** Python (FastAPI)
- **Database:** SQLite
- **Package Manager:** NPM, UV
-- **Containerization:** Docker & Docker Compose
+- **Containerization:** Docker & Docker Compose
---
@@ -49,17 +48,44 @@


-
-
---
## 🚀 Deployment
The recommended way to deploy STLVault is using **Docker Compose** or via a container management tool like **Portainer**.
-**Images are available on docker-hub, just replace the build steps with the image tag in the docker-compose. (you will need to set the API Host in the app settings)**
-
-### Option 1: Docker Compose (CLI)
+## Docker Compose with Images
+
+```
+services:
+ stlvbackend:
+ image: moddroid94/stlvault-backend:latest
+ pull_policy: build
+ environment:
+ - FILE_STORAGE=/app/uploads
+ - DB_PATH=/app/data/data.db
+ - WEBUI_URL=http://192.168.178.21:8999
+ ports:
+ - '8998:8080'
+ volumes:
+ - YOUR_FOLDER_PATH:/app/uploads
+ - YOUR_FOLDER_PATH:/app/data
+ restart: always
+ stlvfrontend:
+ image: moddroid94/stlvault-frontend:latest
+ pull_policy: build
+ volumes:
+ - node_modules:/app/node_modules
+ ports:
+ - '8999:5173'
+ depends_on:
+ - stlvbackend
+ restart: always
+volumes:
+ node_modules: null
+```
+
+### Docker Compose (CLI)
1. **Clone the repository:**
@@ -88,7 +114,7 @@ The recommended way to deploy STLVault is using **Docker Compose** or via a cont
4. **Access the App:**
Open your browser and navigate to `http://localhost:8989` (or the port you configured).
-### Option 2: Portainer or GitOps (Recommended)
+### Portainer
You can deploy STLVault directly from Portainer using the repository as a stack source.
diff --git a/backend/app.py b/backend/app.py
index b4a24ce..0ed3ae4 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -3,6 +3,7 @@
import time
import shutil
import sqlite3
+import base64
from fastapi import (
FastAPI,
UploadFile,
@@ -396,7 +397,7 @@ def replace_model_file(
pass
filename_str = file.filename or ".stl"
- ext = os.path.splitext(filename_str)[1] or ".stl"
+ ext = os.path.splitext(filename_str)[-1] or ".stl"
filename = f"{model_id}{ext}"
path = os.path.join(UPLOAD_DIR, filename)
size = save_upload_file(file, path)
@@ -411,6 +412,38 @@ def replace_model_file(
return row_to_model(row)
+@app.put("/api/models/{model_id}/thumbnail")
+def replace_model_thumbnail(
+ model_id: str, file: UploadFile = File(...)
+):
+ filename_str = file.filename
+ ext = os.path.splitext(filename_str)[-1]
+ if not ext:
+ raise HTTPException(status_code=429, detail="File not Valid, Extension not found")
+
+ filebytes = file.file.read()
+ encoded_string = base64.b64encode(filebytes)
+ baseext = ext[1:]
+ thumbnail = "data:image/" + baseext + ";base64," + encoded_string.decode()
+
+ conn = get_db_conn()
+ cur = conn.cursor()
+
+ m = cur.execute("SELECT * FROM models WHERE id=?", (model_id,)).fetchone()
+ if not m:
+ conn.close()
+ raise HTTPException(status_code=404, detail="Model not found")
+
+ cur.execute(
+ "UPDATE models SET thumbnail=? WHERE id=?",
+ (thumbnail, model_id),
+ )
+ conn.commit()
+ row = cur.execute("SELECT * FROM models WHERE id=?", (model_id,)).fetchone()
+ conn.close()
+ return row_to_model(row)
+
+
@app.get("/api/storage-stats")
def storage_stats():
used = 0
diff --git a/frontend/App.tsx b/frontend/App.tsx
index 6a3823e..0ffe3b6 100644
--- a/frontend/App.tsx
+++ b/frontend/App.tsx
@@ -342,7 +342,8 @@ const App = () => {
model.name,
model.parentId,
model.previewPath,
- importFolderId
+ importFolderId,
+ model.typeName
);
setUploadQueue((prev) => prev - 1);
setModels((prev) => [newModel, ...prev]);
@@ -987,8 +988,8 @@ const App = () => {
{/* File List */}
900 ? "h-[700px]" : "h-[400px]"
- }`}
+ visualViewport.height > 900 ? "h-[700px]" : "h-[400px]"
+ }`}
>
{Array.from(folderOptions).map((f) => (
diff --git a/frontend/components/DetailPanel.tsx b/frontend/components/DetailPanel.tsx
index 21545b1..611233d 100644
--- a/frontend/components/DetailPanel.tsx
+++ b/frontend/components/DetailPanel.tsx
@@ -1,11 +1,23 @@
+import React, { useState, useCallback, useRef } from "react";
+import { STLModel } from "../types";
+import Viewer3D from "./Viewer3D";
+import {
+ X,
+ Download,
+ Tag as TagIcon,
+ Sparkles,
+ Save,
+ Edit,
+ Trash2,
+ Calendar,
+ HardDrive,
+ FileUp,
+ RefreshCw,
+ AlertTriangle,
+} from "lucide-react";
-import React, { useState, useCallback, useRef } from 'react';
-import { STLModel } from '../types';
-import Viewer3D from './Viewer3D';
-import { X, Download, Tag as TagIcon, Sparkles, Save, Edit, Trash2, Calendar, HardDrive, FileUp, RefreshCw, AlertTriangle } from 'lucide-react';
-
-import { generateThumbnail } from '../services/thumbnailGenerator';
-import { api } from '../services/api';
+import { generateThumbnail } from "../services/thumbnailGenerator";
+import { api } from "../services/api";
interface DetailPanelProps {
model: STLModel | null;
@@ -14,14 +26,21 @@ interface DetailPanelProps {
onDelete: (id: string) => void;
}
-const DetailPanel: React.FC
= ({ model, onClose, onUpdate, onDelete }) => {
-
+const DetailPanel: React.FC = ({
+ model,
+ onClose,
+ onUpdate,
+ onDelete,
+}) => {
const [isReplacing, setIsReplacing] = useState(false);
const [isEditing, setIsEditing] = useState(false);
- const [editName, setEditName] = useState('');
- const [editDesc, setEditDesc] = useState('');
- const [editTags, setEditTags] = useState('');
- const [errorState, setErrorState] = useState<{ show: boolean; message: string }>({ show: false, message: '' });
+ const [editName, setEditName] = useState("");
+ const [editDesc, setEditDesc] = useState("");
+ const [editTags, setEditTags] = useState("");
+ const [errorState, setErrorState] = useState<{
+ show: boolean;
+ message: string;
+ }>({ show: false, message: "" });
const fileInputRef = useRef(null);
@@ -29,31 +48,32 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
React.useEffect(() => {
if (model) {
setEditName(model.name);
- setEditDesc(model.description || '');
- setEditTags(model.tags.join(', '));
+ setEditDesc(model.description || "");
+ setEditTags(model.tags.join(", "));
setIsEditing(false);
- setErrorState({ show: false, message: '' });
+ setErrorState({ show: false, message: "" });
}
}, [model]);
- const handleModelLoaded = useCallback((dimensions: { x: number; y: number; z: number }) => {
- if (model && !model.dimensions) {
- onUpdate(model.id, { dimensions });
- }
- }, [model, onUpdate]);
+ const handleModelLoaded = useCallback(
+ (dimensions: { x: number; y: number; z: number }) => {
+ if (model && !model.dimensions) {
+ onUpdate(model.id, { dimensions });
+ }
+ },
+ [model, onUpdate]
+ );
if (!model) return null;
-
-
const handleReplaceFile = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (!file || !model) return;
// Extension check
const getExtension = (filename: string) => {
- const parts = filename.split('.');
- return parts.length > 1 ? parts.pop()?.toLowerCase() : '';
+ const parts = filename.split(".");
+ return parts.length > 1 ? parts.pop()?.toLowerCase() : "";
};
const currentExt = getExtension(model.name);
@@ -62,9 +82,9 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
if (currentExt && newExt && currentExt !== newExt) {
setErrorState({
show: true,
- message: `You cannot replace a .${currentExt} file with a .${newExt} file.`
+ message: `You cannot replace a .${currentExt} file with a .${newExt} file.`,
});
- if (fileInputRef.current) fileInputRef.current.value = '';
+ if (fileInputRef.current) fileInputRef.current.value = "";
return;
}
@@ -82,7 +102,7 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
onUpdate(model.id, {
url: updated.url,
size: updated.size,
- thumbnail: updated.thumbnail
+ thumbnail: updated.thumbnail,
});
// Note: The name and other metadata are preserved unless the user explicitly changes them in the text fields
} catch (e) {
@@ -90,16 +110,43 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
alert("Failed to replace file");
} finally {
setIsReplacing(false);
- if (fileInputRef.current) fileInputRef.current.value = '';
+ if (fileInputRef.current) fileInputRef.current.value = "";
+ }
+ };
+
+ const handleReplaceThumbnail = async (
+ e: React.ChangeEvent
+ ) => {
+ const file = e.target.files?.[0];
+ if (!file || !model) return;
+
+ setIsReplacing(true);
+ try {
+ const updated = await api.replaceModelThumbnail(model.id, file);
+ onUpdate(model.id, {
+ url: updated.url,
+ size: updated.size,
+ thumbnail: updated.thumbnail,
+ });
+ // Note: The name and other metadata are preserved unless the user explicitly changes them in the text fields
+ } catch (e) {
+ console.error("Failed to replace", e);
+ alert("Failed to replace file");
+ } finally {
+ setIsReplacing(false);
+ if (fileInputRef.current) fileInputRef.current.value = "";
}
};
const handleSave = () => {
- const newTags = editTags.split(',').map(t => t.trim()).filter(t => t.length > 0);
+ const newTags = editTags
+ .split(",")
+ .map((t) => t.trim())
+ .filter((t) => t.length > 0);
onUpdate(model.id, {
name: editName,
description: editDesc,
- tags: newTags
+ tags: newTags,
});
setIsEditing(false);
};
@@ -109,7 +156,10 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
{/* Header */}
Model Details
-
@@ -117,7 +167,12 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
{/* Viewer */}
-
+
{/* Actions */}
@@ -142,9 +197,14 @@ const DetailPanel: React.FC
= ({ model, onClose, onUpdate, onD
{/* Info Form */}
-
+
{!isEditing && (
-
setIsEditing(true)} className="text-xs text-blue-400 hover:underline flex items-center gap-1">
+ setIsEditing(true)}
+ className="text-xs text-blue-400 hover:underline flex items-center gap-1"
+ >
Edit
)}
@@ -176,14 +236,20 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
{/* File Replacement Section (Edit Mode Only) */}
{isEditing && (
-
Source File
+
+ Source File
+
fileInputRef.current?.click()}
disabled={isReplacing}
className="flex items-center gap-2 bg-vault-800 border border-vault-600 hover:bg-vault-700 text-slate-300 px-3 py-2 rounded-md text-xs font-medium transition-colors w-full justify-center"
>
- {isReplacing ? : }
+ {isReplacing ? (
+
+ ) : (
+
+ )}
{isReplacing ? "Uploading..." : "Replace 3D Model File"}
= ({ model, onClose, onUpdate, onD
onChange={handleReplaceFile}
/>
-
Replaces geometry but keeps name/desc unless changed.
+
+ Replaces geometry but keeps name/desc unless changed.
+
+
+
+ Thumbnail
+
+
+
+

+
+
fileInputRef.current?.click()}
+ disabled={isReplacing}
+ className="flex items-center gap-2 bg-vault-800 border border-vault-600 hover:bg-vault-700 text-slate-300 px-3 py-2 rounded-md text-xs font-medium transition-colors w-full justify-center"
+ >
+ {isReplacing ? (
+
+ ) : (
+
+ )}
+ {isReplacing ? "Uploading..." : "Replace Thumbnail"}
+
+
+
+
+ Replaces Thumbnail.
+
)}
- Filename
+
+ Filename
+
{isEditing ? (
= ({ model, onClose, onUpdate, onD
-
Description
+
+ Description
+
{isEditing ? (
@@ -238,22 +349,37 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
/>
) : (
- {model.tags.length > 0 ? model.tags.map(tag => (
-
-
- {tag}
+ {model.tags.length > 0 ? (
+ model.tags.map((tag) => (
+
+
+ {tag}
+
+ ))
+ ) : (
+
+ No tags
- )) : No tags}
+ )}
)}
{isEditing && (
-
+
Save Changes
- setIsEditing(false)} className="flex-1 bg-vault-700 hover:bg-vault-600 text-slate-200 py-1.5 rounded text-xs font-medium">
+ setIsEditing(false)}
+ className="flex-1 bg-vault-700 hover:bg-vault-600 text-slate-200 py-1.5 rounded text-xs font-medium"
+ >
Cancel
@@ -262,7 +388,10 @@ const DetailPanel: React.FC
= ({ model, onClose, onUpdate, onD
- onDelete(model.id)} className="w-full border border-red-900/50 text-red-500 hover:bg-red-900/20 py-2 rounded-md text-sm transition-colors flex items-center justify-center gap-2">
+ onDelete(model.id)}
+ className="w-full border border-red-900/50 text-red-500 hover:bg-red-900/20 py-2 rounded-md text-sm transition-colors flex items-center justify-center gap-2"
+ >
Delete Model
@@ -277,10 +406,12 @@ const DetailPanel: React.FC = ({ model, onClose, onUpdate, onD
File Mismatch
-
{errorState.message}
+
+ {errorState.message}
+
setErrorState({ show: false, message: '' })}
+ onClick={() => setErrorState({ show: false, message: "" })}
className="w-full mt-2 py-2 bg-vault-700 hover:bg-vault-600 text-white rounded-lg text-sm font-medium transition-colors"
>
Okay, got it
diff --git a/frontend/components/ModelList.tsx b/frontend/components/ModelList.tsx
index 40a09f2..4210458 100644
--- a/frontend/components/ModelList.tsx
+++ b/frontend/components/ModelList.tsx
@@ -436,7 +436,7 @@ const ModelList: React.FC = ({
) : (
<>
@@ -450,13 +450,13 @@ const ModelList: React.FC = ({
{model.tags.slice(0, 2).map((tag) => (
{tag}
))}
{model.tags.length > 2 && (
-
+
+{model.tags.length - 2}
)}
@@ -464,7 +464,7 @@ const ModelList: React.FC = ({
{/* File Type Badge */}
-
+
{model.name.split(".").pop()}
diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx
index f1eb30d..132bda4 100644
--- a/frontend/components/Sidebar.tsx
+++ b/frontend/components/Sidebar.tsx
@@ -9,8 +9,6 @@ import {
Check,
X,
ChevronRight,
- ChevronDown,
- FolderOpen,
Settings,
} from "lucide-react";
import { Folder, STLModel, StorageStats } from "../types";
@@ -119,7 +117,7 @@ const FolderNode: React.FC = ({
{/* Folder Row */}
= ({
: "text-slate-400 hover:bg-vault-800 hover:text-slate-200"
}
`}
- style={{ paddingLeft: `${level * 16 + 12}px` }}
+ style={{ paddingLeft: `${level * 15 + 5}px` }}
onDragOver={(e) => onDragOver(e, folder.id)}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, folder.id)}
>
{/* Expand Toggle */}
{
e.stopPropagation();
onToggleExpand(folder.id);
}}
>
{hasChildren ? (
- isExpanded ? (
-
- ) : (
-
- )
+
) : (
// Placeholder to keep alignment
@@ -188,18 +186,14 @@ const FolderNode: React.FC
= ({
className="flex-1 flex items-center gap-2 min-w-0 cursor-pointer py-1.5"
onClick={() => onSelect(folder.id)}
>
- {isExpanded || isDropTarget ? (
-
- ) : (
-
- )}
+
{folder.name}
{/* Hover Actions */}
-
+
{
e.stopPropagation();
@@ -235,9 +229,9 @@ const FolderNode: React.FC = ({
{/* Count Badge */}
{count}
diff --git a/frontend/components/Viewer3D.tsx b/frontend/components/Viewer3D.tsx
index 7f25f61..7fa1992 100644
--- a/frontend/components/Viewer3D.tsx
+++ b/frontend/components/Viewer3D.tsx
@@ -59,6 +59,7 @@ class ErrorBoundary extends Component {
interface Viewer3DProps {
url: string;
filename: string;
+ thumbnail;
color?: string;
onLoaded?: (dimensions: { x: number; y: number; z: number }) => void;
}
@@ -66,6 +67,7 @@ interface Viewer3DProps {
const Model = ({
url,
filename,
+ thumbnail,
color = "#3b82f6",
onLoaded,
}: Viewer3DProps) => {
@@ -149,16 +151,20 @@ const Model = ({
);
};
-const Viewer3D: React.FC = ({ url, filename, onLoaded }) => {
+const Viewer3D: React.FC = ({
+ url,
+ filename,
+ thumbnail,
+ onLoaded,
+}) => {
const [error, setError] = useState(false);
const containerRef = useRef(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const unsupportedFormat = useMemo(() => {
const lower = filename.toLowerCase();
- if (lower.endsWith(".step") || lower.endsWith(".stp")) return "STEP";
- // 3MF is now supported
- return null;
+ if (lower.endsWith(".stl") || lower.endsWith(".3mf")) return null;
+ return "UNSUPPORTED";
}, [filename]);
// Reset error when url changes
@@ -199,13 +205,8 @@ const Viewer3D: React.FC = ({ url, filename, onLoaded }) => {
if (unsupportedFormat) {
return (
-
-
-
Preview not available
-
- {unsupportedFormat} files cannot be previewed in the browser directly.
- Please download to view.
-
+
+
);
}
@@ -239,7 +240,12 @@ const Viewer3D: React.FC
= ({ url, filename, onLoaded }) => {
setError(true)}>
-
+
diff --git a/frontend/services/api.ts b/frontend/services/api.ts
index d4fcaf5..d64f1fd 100644
--- a/frontend/services/api.ts
+++ b/frontend/services/api.ts
@@ -181,12 +181,13 @@ export const api = {
name: string,
parentId: string,
previewPath: string,
- folderId: string
+ folderId: string,
+ typeName: string,
): Promise => {
const res = await fetch(`${API_BASE_URL}/printables/importid`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ id, name, parentId, previewPath, folderId }),
+ body: JSON.stringify({ id, name, parentId, previewPath, folderId, typeName }),
});
if (!res.ok) throw new Error("Import failed");
return res.json();
@@ -210,6 +211,22 @@ export const api = {
return res.json();
},
+ // 14a. REPLACE FILE
+ replaceModelThumbnail: async (
+ id: string,
+ file: File,
+ ): Promise => {
+ const formData = new FormData();
+ formData.append("file", file);
+
+ const res = await fetch(`${API_BASE_URL}/models/${id}/thumbnail`, {
+ method: "PUT",
+ body: formData,
+ });
+ if (!res.ok) throw new Error("File replacement failed");
+ return res.json();
+ },
+
// 15. GET Storage Stats
getStorageStats: async (): Promise => {
const res = await fetch(`${API_BASE_URL}/storage-stats`);
diff --git a/frontend/services/thumbnailGenerator.ts b/frontend/services/thumbnailGenerator.ts
index 0aeefcd..77d98fb 100644
--- a/frontend/services/thumbnailGenerator.ts
+++ b/frontend/services/thumbnailGenerator.ts
@@ -1,16 +1,16 @@
-import * as THREE from 'three';
-import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
-import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';
+import * as THREE from "three";
+import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
+import { ThreeMFLoader } from "three/examples/jsm/loaders/3MFLoader.js";
export const generateThumbnail = async (file: File): Promise => {
return new Promise((resolve, reject) => {
- const is3MF = file.name.toLowerCase().endsWith('.3mf');
- const isSTL = file.name.toLowerCase().endsWith('.stl');
+ const is3MF = file.name.toLowerCase().endsWith(".3mf");
+ const isSTL = file.name.toLowerCase().endsWith(".stl");
if (!isSTL && !is3MF) {
- // Skip unsupported
- reject(new Error("Unsupported file type for thumbnail"));
- return;
+ // Skip unsupported
+ reject(new Error("Unsupported file type for thumbnail"));
+ return;
}
const reader = new FileReader();
@@ -18,40 +18,40 @@ export const generateThumbnail = async (file: File): Promise => {
reader.onload = (event) => {
try {
if (!event.target?.result) {
- reject(new Error("File read failed"));
- return;
+ reject(new Error("File read failed"));
+ return;
}
const contents = event.target.result as ArrayBuffer;
-
+
let object: THREE.Object3D;
if (is3MF) {
- const loader = new ThreeMFLoader();
- // 3MFLoader parse returns a Group
- object = loader.parse(contents);
+ const loader = new ThreeMFLoader();
+ // 3MFLoader parse returns a Group
+ object = loader.parse(contents);
} else {
- const loader = new STLLoader();
- const geometry = loader.parse(contents);
- const material = new THREE.MeshStandardMaterial({
- color: 0x3b82f6,
- roughness: 0.5,
- metalness: 0.2
- });
- object = new THREE.Mesh(geometry, material);
+ const loader = new STLLoader();
+ const geometry = loader.parse(contents);
+ const material = new THREE.MeshStandardMaterial({
+ color: 0x3b82f6,
+ roughness: 0.5,
+ metalness: 0.2,
+ });
+ object = new THREE.Mesh(geometry, material);
}
// Setup scene for snapshot
const scene = new THREE.Scene();
// Transparent background
- scene.background = null;
+ scene.background = null;
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
-
+
// Center and scale
const box = new THREE.Box3().setFromObject(object);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
-
+
// Move object to center
object.position.sub(center);
scene.add(object);
@@ -65,11 +65,11 @@ export const generateThumbnail = async (file: File): Promise => {
camera.position.y = cameraZ * 0.4; // Slight angle
camera.position.x = cameraZ * 0.4; // Slight angle
camera.lookAt(0, 0, 0);
-
+
// Add lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
-
+
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
@@ -79,17 +79,21 @@ export const generateThumbnail = async (file: File): Promise => {
scene.add(backLight);
// Render
- const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true });
+ const renderer = new THREE.WebGLRenderer({
+ alpha: true,
+ antialias: true,
+ preserveDrawingBuffer: true,
+ });
renderer.setSize(300, 300);
renderer.render(scene, camera);
- const dataUrl = renderer.domElement.toDataURL('image/png');
-
+ const dataUrl = renderer.domElement.toDataURL("image/png");
+
// Clean up
if (!is3MF) {
- // Dispose geometry/material created manually for STL
- (object as THREE.Mesh).geometry.dispose();
- ((object as THREE.Mesh).material as THREE.Material).dispose();
+ // Dispose geometry/material created manually for STL
+ (object as THREE.Mesh).geometry.dispose();
+ ((object as THREE.Mesh).material as THREE.Material).dispose();
}
renderer.dispose();
@@ -103,4 +107,4 @@ export const generateThumbnail = async (file: File): Promise => {
reader.onerror = (e) => reject(e);
reader.readAsArrayBuffer(file);
});
-};
\ No newline at end of file
+};