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