Merge pull request #179 from maziggy/revert-178-feature/stl-thumbnail

Revert "Feature/stl thumbnail"
This commit is contained in:
MartinNYHC
2026-01-29 15:52:58 +01:00
committed by GitHub
8 changed files with 1 additions and 886 deletions
-5
View File
@@ -1,5 +0,0 @@
Collecting PyJWT
Downloading PyJWT-2.10.1-py3-none-any.whl.metadata (4.0 kB)
Downloading PyJWT-2.10.1-py3-none-any.whl (22 kB)
Installing collected packages: PyJWT
Successfully installed PyJWT-2.10.1
-220
View File
@@ -25,9 +25,6 @@ from backend.app.schemas.library import (
AddToQueueRequest,
AddToQueueResponse,
AddToQueueResult,
BatchThumbnailRequest,
BatchThumbnailResponse,
BatchThumbnailResult,
BulkDeleteRequest,
BulkDeleteResponse,
FileDuplicate,
@@ -711,18 +708,6 @@ async def upload_file(
except Exception as e:
logger.warning(f"Failed to extract gcode thumbnail: {e}")
elif ext == ".stl":
# Generate thumbnail from STL file
try:
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
thumb_filename = f"{uuid.uuid4().hex}.png"
thumb_path = thumbnails_dir / thumb_filename
if generate_stl_thumbnail(file_path, thumb_path):
thumbnail_path = str(thumb_path)
except Exception as e:
logger.warning(f"Failed to generate STL thumbnail: {e}")
elif ext.lower() in IMAGE_EXTENSIONS:
# For image files, create a thumbnail from the image itself
thumbnail_path = create_image_thumbnail(file_path, thumbnails_dir)
@@ -953,17 +938,6 @@ async def extract_zip_file(
except Exception as e:
logger.warning(f"Failed to extract gcode thumbnail from ZIP: {e}")
elif ext == ".stl":
try:
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
thumb_filename = f"{uuid.uuid4().hex}.png"
thumb_path = thumbnails_dir / thumb_filename
if generate_stl_thumbnail(file_path, thumb_path):
thumbnail_path = str(thumb_path)
except Exception as e:
logger.warning(f"Failed to generate STL thumbnail from ZIP: {e}")
elif ext.lower() in IMAGE_EXTENSIONS:
thumbnail_path = create_image_thumbnail(file_path, thumbnails_dir)
@@ -1945,197 +1919,3 @@ async def get_library_stats(db: AsyncSession = Depends(get_db)):
"disk_total_bytes": disk_total_bytes,
"disk_used_bytes": disk_used_bytes,
}
# ============ Thumbnail Generation Endpoints ============
@router.post("/files/{file_id}/regenerate-thumbnail", response_model=FileResponseSchema)
async def regenerate_thumbnail(file_id: int, db: AsyncSession = Depends(get_db)):
"""Regenerate thumbnail for a specific file.
Works for STL, 3MF, gcode, and image files.
"""
result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
file = result.scalar_one_or_none()
if not file:
raise HTTPException(status_code=404, detail="File not found")
if not file.file_path or not os.path.exists(file.file_path):
raise HTTPException(status_code=404, detail="File not found on disk")
ext = os.path.splitext(file.filename)[1].lower()
thumbnails_dir = get_library_thumbnails_dir()
file_path = Path(file.file_path)
# Delete old thumbnail if exists
if file.thumbnail_path and os.path.exists(file.thumbnail_path):
try:
os.remove(file.thumbnail_path)
except Exception as e:
logger.warning(f"Failed to delete old thumbnail: {e}")
thumbnail_path = None
if ext == ".3mf":
try:
parser = ThreeMFParser(str(file_path))
raw_metadata = parser.parse()
thumbnail_data = raw_metadata.get("_thumbnail_data")
thumbnail_ext = raw_metadata.get("_thumbnail_ext", ".png")
if thumbnail_data:
thumb_filename = f"{uuid.uuid4().hex}{thumbnail_ext}"
thumb_path = thumbnails_dir / thumb_filename
with open(thumb_path, "wb") as f:
f.write(thumbnail_data)
thumbnail_path = str(thumb_path)
except Exception as e:
logger.warning(f"Failed to extract 3MF thumbnail: {e}")
elif ext == ".gcode":
try:
thumbnail_data = extract_gcode_thumbnail(file_path)
if thumbnail_data:
thumb_filename = f"{uuid.uuid4().hex}.png"
thumb_path = thumbnails_dir / thumb_filename
with open(thumb_path, "wb") as f:
f.write(thumbnail_data)
thumbnail_path = str(thumb_path)
except Exception as e:
logger.warning(f"Failed to extract gcode thumbnail: {e}")
elif ext == ".stl":
try:
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
thumb_filename = f"{uuid.uuid4().hex}.png"
thumb_path = thumbnails_dir / thumb_filename
if generate_stl_thumbnail(file_path, thumb_path):
thumbnail_path = str(thumb_path)
except Exception as e:
logger.warning(f"Failed to generate STL thumbnail: {e}")
elif ext.lower() in IMAGE_EXTENSIONS:
thumbnail_path = create_image_thumbnail(file_path, thumbnails_dir)
# Update database
file.thumbnail_path = thumbnail_path
await db.flush()
await db.refresh(file)
# Return full response
return await get_file(file_id, db)
@router.post("/generate-stl-thumbnails", response_model=BatchThumbnailResponse)
async def batch_generate_stl_thumbnails(
request: BatchThumbnailRequest,
db: AsyncSession = Depends(get_db),
):
"""Generate thumbnails for existing STL files.
Can target specific files, a folder, or all STL files missing thumbnails.
Args:
request: Batch request specifying which files to process
- file_ids: List of specific file IDs to process
- folder_id: Process all STL files in this folder
- all_missing: Process all STL files without thumbnails
"""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
results: list[BatchThumbnailResult] = []
thumbnails_dir = get_library_thumbnails_dir()
# Build query based on request parameters
query = select(LibraryFile).where(LibraryFile.file_type == "stl")
if request.file_ids:
# Specific files requested
query = query.where(LibraryFile.id.in_(request.file_ids))
elif request.folder_id is not None:
# All STL files in a folder
query = query.where(LibraryFile.folder_id == request.folder_id)
elif request.all_missing:
# All STL files without thumbnails
query = query.where(LibraryFile.thumbnail_path.is_(None))
else:
# No valid filter specified
raise HTTPException(
status_code=400,
detail="Must specify file_ids, folder_id, or all_missing=true",
)
result = await db.execute(query)
files = result.scalars().all()
succeeded = 0
failed = 0
for file in files:
if not file.file_path or not os.path.exists(file.file_path):
results.append(
BatchThumbnailResult(
file_id=file.id,
filename=file.filename,
success=False,
error="File not found on disk",
)
)
failed += 1
continue
try:
# Delete old thumbnail if exists
if file.thumbnail_path and os.path.exists(file.thumbnail_path):
try:
os.remove(file.thumbnail_path)
except Exception:
pass
# Generate new thumbnail
thumb_filename = f"{uuid.uuid4().hex}.png"
thumb_path = thumbnails_dir / thumb_filename
if generate_stl_thumbnail(file.file_path, thumb_path):
file.thumbnail_path = str(thumb_path)
results.append(
BatchThumbnailResult(
file_id=file.id,
filename=file.filename,
success=True,
)
)
succeeded += 1
else:
results.append(
BatchThumbnailResult(
file_id=file.id,
filename=file.filename,
success=False,
error="Thumbnail generation failed",
)
)
failed += 1
except Exception as e:
results.append(
BatchThumbnailResult(
file_id=file.id,
filename=file.filename,
success=False,
error=str(e),
)
)
failed += 1
await db.commit()
return BatchThumbnailResponse(
processed=len(results),
succeeded=succeeded,
failed=failed,
results=results,
)
-29
View File
@@ -262,32 +262,3 @@ class ZipExtractResponse(BaseModel):
folders_created: int
files: list[ZipExtractResult]
errors: list[ZipExtractError]
# ============ Batch Thumbnail Generation ============
class BatchThumbnailRequest(BaseModel):
"""Schema for batch STL thumbnail generation request."""
file_ids: list[int] | None = None # Specific file IDs to process
folder_id: int | None = None # Process all STL files in this folder
all_missing: bool = False # Process all STL files without thumbnails
class BatchThumbnailResult(BaseModel):
"""Result for a single file thumbnail generation."""
file_id: int
filename: str
success: bool
error: str | None = None
class BatchThumbnailResponse(BaseModel):
"""Schema for batch thumbnail generation response."""
processed: int
succeeded: int
failed: int
results: list[BatchThumbnailResult]
-227
View File
@@ -1,227 +0,0 @@
"""STL thumbnail generation service.
Generates PNG thumbnails from STL files using trimesh and matplotlib.
Supports both ASCII and binary STL formats, handles large meshes via simplification.
"""
import io
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
# Maximum vertices before simplification is applied
MAX_VERTICES = 100000
# Default thumbnail size
DEFAULT_SIZE = (256, 256)
def generate_stl_thumbnail(
stl_path: str | Path,
output_path: str | Path,
size: tuple[int, int] = DEFAULT_SIZE,
) -> bool:
"""Generate a PNG thumbnail from an STL file.
Args:
stl_path: Path to the input STL file
output_path: Path where the PNG thumbnail will be saved
size: Tuple of (width, height) for the output image
Returns:
True if thumbnail was generated successfully, False otherwise
"""
try:
import matplotlib
matplotlib.use("Agg") # Use non-interactive backend
import matplotlib.pyplot as plt
import numpy as np
import trimesh
# Load the STL file
mesh = trimesh.load(str(stl_path), file_type="stl")
if mesh is None or not hasattr(mesh, "vertices") or len(mesh.vertices) == 0:
logger.warning(f"Failed to load STL or empty mesh: {stl_path}")
return False
# Simplify if mesh is too large
if len(mesh.vertices) > MAX_VERTICES:
logger.info(f"Simplifying mesh with {len(mesh.vertices)} vertices to ~{MAX_VERTICES}")
# Calculate target face count based on vertex ratio
target_faces = int(len(mesh.faces) * (MAX_VERTICES / len(mesh.vertices)))
try:
mesh = mesh.simplify_quadric_decimation(target_faces)
except Exception as e:
logger.warning(f"Mesh simplification failed, using original: {e}")
# Create figure with transparent background
fig = plt.figure(figsize=(size[0] / 100, size[1] / 100), dpi=100)
ax = fig.add_subplot(111, projection="3d")
# Get mesh vertices and faces
vertices = mesh.vertices
faces = mesh.faces
# Center the mesh
center = vertices.mean(axis=0)
vertices = vertices - center
# Scale to fit in view
max_extent = np.abs(vertices).max()
if max_extent > 0:
vertices = vertices / max_extent
# Create triangles for plotting
triangles = vertices[faces]
# Plot the mesh with a nice color scheme
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
collection = Poly3DCollection(
triangles,
alpha=1.0,
facecolor="#00AE42", # Bambu green
edgecolor="#008833", # Darker green for edges
linewidths=0.1,
)
ax.add_collection3d(collection)
# Set axis limits
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
# Set viewing angle (isometric-ish)
ax.view_init(elev=25, azim=45)
# Remove axes for cleaner look
ax.set_axis_off()
# Set background color
ax.set_facecolor("#1a1a1a")
fig.patch.set_facecolor("#1a1a1a")
# Tight layout to minimize whitespace
plt.tight_layout(pad=0)
# Save the figure
plt.savefig(
str(output_path),
format="png",
dpi=100,
facecolor="#1a1a1a",
bbox_inches="tight",
pad_inches=0.05,
)
plt.close(fig)
logger.info(f"Generated STL thumbnail: {output_path}")
return True
except ImportError as e:
logger.error(f"Missing dependency for STL thumbnails: {e}")
return False
except Exception as e:
logger.error(f"Failed to generate STL thumbnail for {stl_path}: {e}")
return False
def generate_stl_thumbnail_bytes(
stl_data: bytes,
size: tuple[int, int] = DEFAULT_SIZE,
) -> bytes | None:
"""Generate a PNG thumbnail from STL data in memory.
Args:
stl_data: Raw STL file data (binary or ASCII)
size: Tuple of (width, height) for the output image
Returns:
PNG image data as bytes, or None on failure
"""
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import trimesh
# Load from bytes
mesh = trimesh.load(
file_obj=io.BytesIO(stl_data),
file_type="stl",
)
if mesh is None or not hasattr(mesh, "vertices") or len(mesh.vertices) == 0:
logger.warning("Failed to load STL from bytes or empty mesh")
return None
# Simplify if mesh is too large
if len(mesh.vertices) > MAX_VERTICES:
target_faces = int(len(mesh.faces) * (MAX_VERTICES / len(mesh.vertices)))
try:
mesh = mesh.simplify_quadric_decimation(target_faces)
except Exception:
pass # Use original if simplification fails
# Create figure
fig = plt.figure(figsize=(size[0] / 100, size[1] / 100), dpi=100)
ax = fig.add_subplot(111, projection="3d")
vertices = mesh.vertices
faces = mesh.faces
# Center and scale
center = vertices.mean(axis=0)
vertices = vertices - center
max_extent = np.abs(vertices).max()
if max_extent > 0:
vertices = vertices / max_extent
triangles = vertices[faces]
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
collection = Poly3DCollection(
triangles,
alpha=1.0,
facecolor="#00AE42",
edgecolor="#008833",
linewidths=0.1,
)
ax.add_collection3d(collection)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
ax.view_init(elev=25, azim=45)
ax.set_axis_off()
ax.set_facecolor("#1a1a1a")
fig.patch.set_facecolor("#1a1a1a")
plt.tight_layout(pad=0)
# Save to bytes buffer
buf = io.BytesIO()
plt.savefig(
buf,
format="png",
dpi=100,
facecolor="#1a1a1a",
bbox_inches="tight",
pad_inches=0.05,
)
plt.close(fig)
buf.seek(0)
return buf.read()
except ImportError as e:
logger.error(f"Missing dependency for STL thumbnails: {e}")
return None
except Exception as e:
logger.error(f"Failed to generate STL thumbnail from bytes: {e}")
return None
@@ -1,326 +0,0 @@
"""Unit tests for the STL thumbnail service."""
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
class TestSTLThumbnailService:
"""Tests for STL thumbnail generation."""
def test_generate_thumbnail_ascii_stl(self):
"""Test generating thumbnail from ASCII STL file."""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
# Create a simple ASCII STL cube
ascii_stl = """solid cube
facet normal 0 0 -1
outer loop
vertex 0 0 0
vertex 1 0 0
vertex 1 1 0
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 0 0 0
vertex 1 1 0
vertex 0 1 0
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 0 0 1
vertex 1 1 1
vertex 1 0 1
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 0 0 1
vertex 0 1 1
vertex 1 1 1
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 0 0
vertex 1 0 1
vertex 1 0 0
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 0 0
vertex 0 0 1
vertex 1 0 1
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 1 0 0
vertex 1 1 1
vertex 1 1 0
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 1 0 0
vertex 1 0 1
vertex 1 1 1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 1 0
vertex 1 1 0
vertex 1 1 1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 1 0
vertex 1 1 1
vertex 0 1 1
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 0 0
vertex 0 1 0
vertex 0 1 1
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 0 0
vertex 0 1 1
vertex 0 0 1
endloop
endfacet
endsolid cube
"""
with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="w") as stl_file:
stl_file.write(ascii_stl)
stl_path = stl_file.name
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
png_path = png_file.name
try:
result = generate_stl_thumbnail(stl_path, png_path)
assert result is True
assert os.path.exists(png_path)
# Check it's a valid PNG (starts with PNG magic bytes)
with open(png_path, "rb") as f:
header = f.read(8)
assert header[:4] == b"\x89PNG"
finally:
os.unlink(stl_path)
if os.path.exists(png_path):
os.unlink(png_path)
def test_generate_thumbnail_binary_stl(self):
"""Test generating thumbnail from binary STL file."""
import struct
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
# Create a simple binary STL cube (minimal version)
# Binary STL format:
# - 80 bytes header
# - 4 bytes number of triangles (uint32)
# - For each triangle:
# - 12 bytes normal (3 floats)
# - 36 bytes vertices (9 floats, 3 vertices x 3 coords)
# - 2 bytes attribute byte count (usually 0)
header = b"\x00" * 80 # Empty header
num_triangles = 12 # A cube has 12 triangles (2 per face)
# Define cube vertices
vertices = [
# Bottom face (z=0)
((0, 0, -1), [(0, 0, 0), (1, 0, 0), (1, 1, 0)]),
((0, 0, -1), [(0, 0, 0), (1, 1, 0), (0, 1, 0)]),
# Top face (z=1)
((0, 0, 1), [(0, 0, 1), (1, 1, 1), (1, 0, 1)]),
((0, 0, 1), [(0, 0, 1), (0, 1, 1), (1, 1, 1)]),
# Front face (y=0)
((0, -1, 0), [(0, 0, 0), (1, 0, 1), (1, 0, 0)]),
((0, -1, 0), [(0, 0, 0), (0, 0, 1), (1, 0, 1)]),
# Back face (y=1)
((0, 1, 0), [(0, 1, 0), (1, 1, 0), (1, 1, 1)]),
((0, 1, 0), [(0, 1, 0), (1, 1, 1), (0, 1, 1)]),
# Left face (x=0)
((-1, 0, 0), [(0, 0, 0), (0, 1, 0), (0, 1, 1)]),
((-1, 0, 0), [(0, 0, 0), (0, 1, 1), (0, 0, 1)]),
# Right face (x=1)
((1, 0, 0), [(1, 0, 0), (1, 1, 1), (1, 1, 0)]),
((1, 0, 0), [(1, 0, 0), (1, 0, 1), (1, 1, 1)]),
]
binary_data = header + struct.pack("<I", num_triangles)
for normal, verts in vertices:
binary_data += struct.pack("<fff", *normal) # Normal
for v in verts:
binary_data += struct.pack("<fff", *v) # Vertex
binary_data += struct.pack("<H", 0) # Attribute byte count
with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="wb") as stl_file:
stl_file.write(binary_data)
stl_path = stl_file.name
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
png_path = png_file.name
try:
result = generate_stl_thumbnail(stl_path, png_path)
assert result is True
assert os.path.exists(png_path)
finally:
os.unlink(stl_path)
if os.path.exists(png_path):
os.unlink(png_path)
def test_generate_thumbnail_invalid_file(self):
"""Test handling of invalid STL file."""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
# Create invalid STL content
with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="w") as stl_file:
stl_file.write("This is not valid STL content")
stl_path = stl_file.name
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
png_path = png_file.name
try:
result = generate_stl_thumbnail(stl_path, png_path)
# Should return False for invalid file
assert result is False
finally:
os.unlink(stl_path)
if os.path.exists(png_path):
os.unlink(png_path)
def test_generate_thumbnail_nonexistent_file(self):
"""Test handling of nonexistent file."""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
result = generate_stl_thumbnail("/nonexistent/path/file.stl", "/tmp/output.png")
assert result is False
def test_generate_thumbnail_bytes_ascii(self):
"""Test generating thumbnail from ASCII STL bytes."""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail_bytes
# Simple ASCII STL cube (same as above)
ascii_stl = b"""solid cube
facet normal 0 0 -1
outer loop
vertex 0 0 0
vertex 1 0 0
vertex 1 1 0
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 0 0 1
vertex 1 0 1
vertex 1 1 1
endloop
endfacet
endsolid cube
"""
result = generate_stl_thumbnail_bytes(ascii_stl)
assert result is not None
# Check it's a valid PNG
assert result[:4] == b"\x89PNG"
def test_generate_thumbnail_bytes_invalid(self):
"""Test handling of invalid STL bytes."""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail_bytes
result = generate_stl_thumbnail_bytes(b"not valid stl data")
assert result is None
def test_generate_thumbnail_custom_size(self):
"""Test generating thumbnail with custom size."""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
ascii_stl = """solid cube
facet normal 0 0 -1
outer loop
vertex 0 0 0
vertex 1 0 0
vertex 1 1 0
endloop
endfacet
endsolid cube
"""
with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="w") as stl_file:
stl_file.write(ascii_stl)
stl_path = stl_file.name
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
png_path = png_file.name
try:
result = generate_stl_thumbnail(stl_path, png_path, size=(128, 128))
assert result is True
assert os.path.exists(png_path)
finally:
os.unlink(stl_path)
if os.path.exists(png_path):
os.unlink(png_path)
class TestMeshSimplification:
"""Tests for mesh simplification with large files."""
def test_simplification_threshold(self):
"""Test that MAX_VERTICES constant is defined."""
from backend.app.services.stl_thumbnail import MAX_VERTICES
assert MAX_VERTICES == 100000
def test_large_mesh_handling(self):
"""Test that large meshes are simplified (mocked)."""
from backend.app.services.stl_thumbnail import generate_stl_thumbnail
# Create a mock mesh with many vertices
with patch("trimesh.load") as mock_load:
mock_mesh = MagicMock()
mock_mesh.vertices = MagicMock()
mock_mesh.vertices.__len__ = MagicMock(return_value=200000) # Over threshold
mock_mesh.faces = MagicMock()
mock_mesh.faces.__len__ = MagicMock(return_value=400000)
mock_simplified = MagicMock()
mock_simplified.vertices = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]
mock_simplified.faces = [[0, 1, 2]]
mock_mesh.simplify_quadric_decimation.return_value = mock_simplified
mock_load.return_value = mock_mesh
with tempfile.NamedTemporaryFile(suffix=".stl", delete=False) as stl_file:
stl_file.write(b"dummy")
stl_path = stl_file.name
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
png_path = png_file.name
try:
# This will fail but we can verify simplification was called
generate_stl_thumbnail(stl_path, png_path)
# The mock should have been called for simplification
mock_mesh.simplify_quadric_decimation.assert_called()
finally:
os.unlink(stl_path)
if os.path.exists(png_path):
os.unlink(png_path)
-15
View File
@@ -3382,21 +3382,6 @@ export interface ZipExtractResponse {
errors: ZipExtractError[];
}
// Batch Thumbnail Generation types
export interface BatchThumbnailResult {
file_id: number;
filename: string;
success: boolean;
error: string | null;
}
export interface BatchThumbnailResponse {
processed: number;
succeeded: number;
failed: number;
results: BatchThumbnailResult[];
}
// Library Queue types
export interface AddToQueueResult {
file_id: number;
+1 -60
View File
@@ -35,7 +35,6 @@ import {
Printer,
Pencil,
Play,
ImageIcon,
} from 'lucide-react';
import { api } from '../api/client';
import type {
@@ -833,10 +832,9 @@ interface FileCardProps {
onAddToQueue?: (id: number) => void;
onPrint?: (file: LibraryFileListItem) => void;
onRename?: (file: LibraryFileListItem) => void;
onRegenerateThumbnail?: (id: number) => void;
}
function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQueue, onPrint, onRename, onRegenerateThumbnail }: FileCardProps) {
function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQueue, onPrint, onRename }: FileCardProps) {
const [showActions, setShowActions] = useState(false);
return (
@@ -937,15 +935,6 @@ function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQue
Rename
</button>
)}
{onRegenerateThumbnail && ['stl', '3mf', 'gcode'].includes(file.file_type.toLowerCase()) && (
<button
className="w-full px-3 py-1.5 text-left text-sm text-white hover:bg-bambu-dark flex items-center gap-2"
onClick={() => { onRegenerateThumbnail(file.id); setShowActions(false); }}
>
<ImageIcon className="w-3.5 h-3.5" />
Regenerate Thumbnail
</button>
)}
<button
className="w-full px-3 py-1.5 text-left text-sm text-red-400 hover:bg-bambu-dark flex items-center gap-2"
onClick={() => { onDelete(file.id); setShowActions(false); }}
@@ -1283,30 +1272,6 @@ export function FileManagerPage() {
},
});
const regenerateThumbnailMutation = useMutation({
mutationFn: (fileId: number) => api.regenerateFileThumbnail(fileId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['library-files'] });
showToast('Thumbnail regenerated', 'success');
},
onError: (error: Error) => showToast(error.message, 'error'),
});
const batchGenerateThumbnailsMutation = useMutation({
mutationFn: () => api.batchGenerateStlThumbnails({ all_missing: true }),
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['library-files'] });
if (result.succeeded > 0) {
showToast(`Generated ${result.succeeded} thumbnail${result.succeeded > 1 ? 's' : ''}${result.failed > 0 ? `, ${result.failed} failed` : ''}`, 'success');
} else if (result.processed === 0) {
showToast('No STL files missing thumbnails', 'success');
} else {
showToast(`Failed to generate thumbnails: ${result.results[0]?.error || 'Unknown error'}`, 'error');
}
},
onError: (error: Error) => showToast(error.message, 'error'),
});
// Helper to check if a file is sliced (printable)
const isSlicedFile = useCallback((filename: string) => {
const lower = filename.toLowerCase();
@@ -1412,19 +1377,6 @@ export function FileManagerPage() {
<Upload className="w-4 h-4 mr-2" />
Upload
</Button>
<Button
variant="secondary"
onClick={() => batchGenerateThumbnailsMutation.mutate()}
disabled={batchGenerateThumbnailsMutation.isPending}
title="Generate thumbnails for STL files that don't have one"
>
{batchGenerateThumbnailsMutation.isPending ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<ImageIcon className="w-4 h-4 mr-2" />
)}
{batchGenerateThumbnailsMutation.isPending ? 'Generating...' : 'Generate Thumbnails'}
</Button>
</div>
</div>
@@ -1754,7 +1706,6 @@ export function FileManagerPage() {
onAddToQueue={(id) => addToQueueMutation.mutate([id])}
onPrint={setPrintFile}
onRename={(f) => setRenameItem({ type: 'file', id: f.id, name: f.filename })}
onRegenerateThumbnail={(id) => regenerateThumbnailMutation.mutate(id)}
/>
))}
</div>
@@ -1871,16 +1822,6 @@ export function FileManagerPage() {
>
<Pencil className="w-4 h-4" />
</button>
{['stl', '3mf', 'gcode'].includes(file.file_type.toLowerCase()) && (
<button
onClick={() => regenerateThumbnailMutation.mutate(file.id)}
className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-white transition-colors"
title="Regenerate Thumbnail"
disabled={regenerateThumbnailMutation.isPending}
>
<ImageIcon className="w-4 h-4" />
</button>
)}
<button
onClick={() => setDeleteConfirm({ type: 'file', id: file.id })}
className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors"
-4
View File
@@ -37,10 +37,6 @@ qrcode[pil]>=7.4.0
# System monitoring
psutil>=6.0.0
# STL thumbnail generation
trimesh>=4.0.0
matplotlib>=3.8.0
# Authentication
PyJWT>=2.8.0
passlib[bcrypt]>=1.7.4