Browse Source

Revert "Feature/stl thumbnail"

MartinNYHC 3 months ago
parent
commit
221f9ffcc3

+ 0 - 5
=2.8.0

@@ -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

+ 0 - 220
backend/app/api/routes/library.py

@@ -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,
-    )

+ 0 - 29
backend/app/schemas/library.py

@@ -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]

+ 0 - 227
backend/app/services/stl_thumbnail.py

@@ -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

+ 0 - 326
backend/tests/unit/services/test_stl_thumbnail.py

@@ -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)

+ 0 - 15
frontend/src/api/client.ts

@@ -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
frontend/src/pages/FileManagerPage.tsx

@@ -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"

+ 0 - 4
requirements.txt

@@ -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