Source code for linkforge.blender.adapters.mesh_io

"""Mesh export utilities for LinkForge.

Export Blender mesh objects to STL, OBJ, and GLB files for URDF.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import bpy
from linkforge.core import get_logger
from linkforge.core._utils.string_utils import sanitize_name
from linkforge.core.constants import (
    EPSILON,
)
from mathutils import Matrix, Vector

from ..constants import (
    FORMAT_GLB,
    FORMAT_OBJ,
    FORMAT_STL,
    PURPOSE_COLLISION,
)

logger = get_logger(__name__)


[docs] def export_mesh_stl(obj: Any, filepath: Path) -> bool: """Export a Blender object to an STL file. This function utilizes the modern Blender WM STL exporter, ensuring correct axis orientations (Y-forward, Z-up) for ROS 2 compatibility. Args: obj: The Blender mesh object to export. filepath: Target filesystem path for the STL file. Returns: True if the export completed successfully, False otherwise. """ if obj is None: return False # Ensure object is visible before selection for reliable Blender context. was_hidden = obj.hide_viewport # Ensure parent directory exists try: filepath.parent.mkdir(parents=True, exist_ok=True) # Deselect all and select only target object obj.hide_viewport = False bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) if bpy.context.view_layer is not None: bpy.context.view_layer.objects.active = obj # Export to STL bpy.ops.wm.stl_export( filepath=str(filepath), export_selected_objects=True, apply_modifiers=True, forward_axis="Y", up_axis="Z", ) except (RuntimeError, OSError) as e: logger.warning(f"STL export failed: {e}") # Restore visibility if failed obj.hide_viewport = was_hidden return False except (TypeError, AttributeError, KeyError) as e: logger.error(f"Unexpected error during STL export: {e}", exc_info=True) raise except Exception as e: logger.critical(f"Critical unexpected error during STL export: {e}", exc_info=True) raise finally: # Restore visibility state obj.hide_viewport = was_hidden return True
[docs] def export_mesh_obj(obj: Any, filepath: Path) -> bool: """Export a Blender object to an OBJ file with associated MTL materials. This function ensures that materials are correctly exported alongside the geometry, maintaining visual fidelity in the target URDF. Args: obj: The Blender mesh object to export. filepath: Target filesystem path for the OBJ file. Returns: True if the export completed successfully, False otherwise. """ if obj is None: return False # Store visibility state before modifying was_hidden = obj.hide_viewport # Ensure parent directory exists try: filepath.parent.mkdir(parents=True, exist_ok=True) # Deselect all and select only target object # Selection requires object visibility obj.hide_viewport = False bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) if bpy.context.view_layer is not None: bpy.context.view_layer.objects.active = obj # Export to OBJ bpy.ops.wm.obj_export( filepath=str(filepath), export_selected_objects=True, apply_modifiers=True, export_materials=True, forward_axis="Y", up_axis="Z", ) except (RuntimeError, OSError) as e: logger.warning(f"OBJ export failed: {e}") obj.hide_viewport = was_hidden return False except (TypeError, AttributeError, KeyError) as e: logger.error(f"Unexpected error during OBJ export: {e}", exc_info=True) raise except Exception as e: logger.critical(f"Critical unexpected error during OBJ export: {e}", exc_info=True) raise finally: # Restore visibility state obj.hide_viewport = was_hidden return True
[docs] def create_simplified_mesh(obj: Any, decimation_ratio: float) -> Any | None: """Create a simplified mesh copy using Blender's Decimate modifier. This function is primarily used to generate lightweight collision geometry from high-fidelity visual meshes, reducing physics computation overhead. Args: obj: The source Blender mesh object. decimation_ratio: The target triangle count ratio (0.0 to 1.0). Returns: A new Blender object with the simplified mesh, or None if failed. """ if obj is None or obj.type != "MESH": return None # Use data-level copy instead of high-level duplicate operator to ensure # operation succeeds regardless of viewport visibility (hide_viewport state). simplified_obj = obj.copy() simplified_obj.data = obj.data.copy() # Ensure temporary object is visible for modifier application simplified_obj.hide_viewport = False # Link to the same collections as the original for col in obj.users_collection: col.objects.link(simplified_obj) # Add Decimate modifier decimate_mod = simplified_obj.modifiers.new(name="Decimate", type="DECIMATE") decimate_mod.ratio = decimation_ratio decimate_mod.decimate_type = "COLLAPSE" # Apply the modifier bpy.ops.object.select_all(action="DESELECT") simplified_obj.select_set(True) if bpy.context.view_layer is not None: bpy.context.view_layer.objects.active = simplified_obj bpy.ops.object.modifier_apply(modifier=decimate_mod.name) return simplified_obj
[docs] def get_mesh_filename( link_name: str, geometry_type: str, mesh_format: str, suffix: str = "" ) -> str: """Generate mesh filename based on link and geometry type. Args: link_name: Name of the robot link geometry_type: "visual" or "collision" (use PURPOSE_VISUAL, PURPOSE_COLLISION) mesh_format: "STL", "OBJ", or "GLB" (use FORMAT_STL, etc.) suffix: Optional unique suffix (e.g., index or name) Returns: Filename string (e.g., "base_link_visual_0.stl"). """ ext = mesh_format.lower() # Sanitize both link_name and suffix for URDF/filesystem compatibility clean_link = sanitize_name(link_name) clean_suffix = sanitize_name(suffix) if suffix else "" return f"{clean_link}_{geometry_type}{clean_suffix}.{ext}"
[docs] def export_mesh_glb(obj: Any, filepath: Path) -> bool: """Export Blender object to GLB (glTF Binary) file. Args: obj: Blender Object to export filepath: Path where GLB file should be saved Returns: True if export succeeded, False otherwise """ if obj is None: return False # Store visibility state before modifying was_hidden = obj.hide_viewport # Ensure parent directory exists try: filepath.parent.mkdir(parents=True, exist_ok=True) # Deselect all and select only target object # Selection requires object visibility obj.hide_viewport = False bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) if bpy.context.view_layer is not None: bpy.context.view_layer.objects.active = obj # Export to GLB bpy.ops.export_scene.gltf( filepath=str(filepath), export_format="GLB", use_selection=True, export_apply=True, # We want Y-up for standard conventions, usually handled by glTF exporter automatically # but Blender Z-up to glTF Y-up conversion is standard. ) except (RuntimeError, OSError) as e: logger.warning(f"GLB export failed: {e}") obj.hide_viewport = was_hidden return False except (TypeError, AttributeError, KeyError) as e: logger.error(f"Unexpected error during GLB export: {e}", exc_info=True) raise except Exception as e: logger.critical(f"Critical unexpected error during GLB export: {e}", exc_info=True) raise finally: # Restore visibility state obj.hide_viewport = was_hidden return True