stl_thumbnail.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """STL Thumbnail Generation Service.
  2. Generates thumbnail images from STL files using trimesh and matplotlib.
  3. """
  4. import logging
  5. import uuid
  6. from pathlib import Path
  7. logger = logging.getLogger(__name__)
  8. # Bambu green color for rendering
  9. BAMBU_GREEN = "#00AE42"
  10. BACKGROUND_COLOR = "#1a1a1a"
  11. # Maximum vertices before simplification
  12. MAX_VERTICES = 100000
  13. def generate_stl_thumbnail(
  14. stl_path: Path,
  15. thumbnails_dir: Path,
  16. size: int = 256,
  17. ) -> str | None:
  18. """Generate a thumbnail image from an STL file.
  19. Args:
  20. stl_path: Path to the STL file
  21. thumbnails_dir: Directory to save the thumbnail
  22. size: Thumbnail size in pixels (default 256x256)
  23. Returns:
  24. Path to the generated thumbnail, or None on failure
  25. """
  26. try:
  27. import matplotlib
  28. import trimesh
  29. # Use Agg backend for headless rendering
  30. matplotlib.use("Agg")
  31. import matplotlib.pyplot as plt
  32. from mpl_toolkits.mplot3d import Axes3D # noqa: F401
  33. from mpl_toolkits.mplot3d.art3d import Poly3DCollection
  34. # Load the STL file
  35. mesh = trimesh.load(str(stl_path), force="mesh")
  36. if mesh is None or not hasattr(mesh, "vertices") or len(mesh.vertices) == 0:
  37. logger.warning("Failed to load STL or empty mesh: %s", stl_path)
  38. return None
  39. # Simplify large meshes for performance
  40. if len(mesh.vertices) > MAX_VERTICES:
  41. logger.info("Simplifying mesh from %s vertices", len(mesh.vertices))
  42. try:
  43. # Calculate reduction ratio (0-1 range)
  44. # e.g., 124633 vertices -> 100000 means keep ~80%, so reduce by ~20%
  45. keep_ratio = MAX_VERTICES / len(mesh.vertices)
  46. target_reduction = 1.0 - keep_ratio
  47. # Clamp to valid range (0.01 to 0.99)
  48. target_reduction = max(0.01, min(0.99, target_reduction))
  49. mesh = mesh.simplify_quadric_decimation(target_reduction)
  50. logger.info("Simplified mesh to %s vertices", len(mesh.vertices))
  51. except Exception as e:
  52. logger.warning("Mesh simplification failed, using original: %s", e)
  53. # Get mesh bounds and center it
  54. vertices = mesh.vertices
  55. bounds_min = vertices.min(axis=0)
  56. bounds_max = vertices.max(axis=0)
  57. center = (bounds_min + bounds_max) / 2
  58. vertices_centered = vertices - center
  59. # Scale to fit in view
  60. max_extent = (bounds_max - bounds_min).max()
  61. if max_extent > 0:
  62. scale = 1.0 / max_extent
  63. vertices_scaled = vertices_centered * scale
  64. else:
  65. vertices_scaled = vertices_centered
  66. # Create figure with dark background
  67. fig = plt.figure(figsize=(size / 100, size / 100), dpi=100)
  68. fig.patch.set_facecolor(BACKGROUND_COLOR)
  69. ax = fig.add_subplot(111, projection="3d")
  70. ax.set_facecolor(BACKGROUND_COLOR)
  71. # Create polygon collection from mesh faces
  72. faces = mesh.faces
  73. poly3d = [[vertices_scaled[vertex] for vertex in face] for face in faces]
  74. collection = Poly3DCollection(
  75. poly3d,
  76. facecolors=BAMBU_GREEN,
  77. edgecolors=BAMBU_GREEN,
  78. linewidths=0.1,
  79. alpha=0.9,
  80. )
  81. ax.add_collection3d(collection)
  82. # Set axis limits
  83. ax.set_xlim(-0.6, 0.6)
  84. ax.set_ylim(-0.6, 0.6)
  85. ax.set_zlim(-0.6, 0.6)
  86. # Set view angle (isometric-ish)
  87. ax.view_init(elev=25, azim=45)
  88. # Remove axes and grid
  89. ax.set_axis_off()
  90. ax.grid(False)
  91. # Remove margins
  92. plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
  93. # Save thumbnail
  94. thumb_filename = f"{uuid.uuid4().hex}.png"
  95. thumb_path = thumbnails_dir / thumb_filename
  96. fig.savefig(
  97. thumb_path,
  98. format="png",
  99. facecolor=BACKGROUND_COLOR,
  100. edgecolor="none",
  101. bbox_inches="tight",
  102. pad_inches=0.05,
  103. dpi=100,
  104. )
  105. plt.close(fig)
  106. logger.info("Generated STL thumbnail: %s", thumb_path)
  107. return str(thumb_path)
  108. except ImportError as e:
  109. logger.warning("STL thumbnail generation unavailable (missing dependencies): %s", e)
  110. return None
  111. except Exception as e:
  112. logger.warning("Failed to generate STL thumbnail for %s: %s", stl_path, e)
  113. return None