Skip to main content
Glama

3D-MCP

by team-plask
monitor_atomic.py29.8 kB
# Generated blender implementation for monitor atomic tools # This file is generated - DO NOT EDIT DIRECTLY import bpy from typing import Dict, Any, Optional, List, Union, Tuple, Literal, Callable import mathutils import os import time from pathlib import Path import traceback import subprocess import sys # try: # from PIL import Image # except: # import sysconfig # import platform # python_exe = sys.executable # This works on macOS/Linux/Windows # try: # subprocess.call([python_exe, "-m", "ensurepip"]) # subprocess.call( # [python_exe, "-m", "pip", "install", "--upgrade", "pip"]) # subprocess.call([python_exe, "-m", "pip", "install", "pillow"]) # except Exception as install_error: # print(f"Failed to install Pillow: {install_error}") def getSceneGraph() -> Dict[str, Any]: """ Get the scene graph of the current scene. Args: No parameters Returns: success (bool): Operation success status scene_graph (Dict[str, Any] with keys {"name": str, "children": List[Dict[str, Any] with keys {"id": str, "name": str, "metadata": Dict[str, Any], "position": List[float], "rotation": List[float], "scale": List[float], "parentId": str, "childIds": List[str]}]}): Scene graph of the current scene """ tool_name = "getSceneGraph" # Define tool name for logging params = {} # Create params dict for logging print(f"Executing {tool_name} in Blender with params: {params}") try: def build_scene_graph(obj): return { "id": obj.name, "name": obj.name, "metadata": { "type": obj.type, "visible": obj.visible_get(), }, "position": list(obj.location), "rotation": list(obj.rotation_euler), "scale": list(obj.scale), "parentId": obj.parent.name if obj.parent else None, "childIds": [child.name for child in obj.children], } scene_graph = { "name": bpy.context.scene.name, "children": [build_scene_graph(obj) for obj in bpy.context.scene.objects], } return { "success": True, "scene_graph": scene_graph, } except Exception as e: print(f"Error in {tool_name}: {str(e)}") return {"success": False, "error": str(e)} # === NEWLY GENERATED === def getCameraView( shading_mode: Optional[str] = None, name_visibility_predicate: Optional[str] = None, auto_adjust_camera: Optional[bool] = None, export_width: int = 960, export_height: int = 540, perspective: Optional[Union[str, Dict[str, Any]]] = None, ) -> Dict[str, Any]: """ Get a customizable view of the 3D scene from any camera angle. Args: shading_mode (str): Rendering style for the viewport (WIREFRAME: line rendering, SOLID: basic shading, MATERIAL: with materials, RENDERED: fully rendered) name_visibility_predicate (str): Python lambda function that takes an object as input and returns display settings (e.g., 'lambda obj: {"show_name": obj.type == "MESH"}') auto_adjust_camera (bool): When true, automatically positions the camera to frame all scene objects export_width (int): Width of the exported image in pixels export_height (int): Height of the exported image in pixels perspective (str or dict): Predefined view angle (TOP/FRONT/RIGHT/PERSP) or custom camera configuration Returns: success (bool): Operation success status image_path (List[str]): File paths to the generated image """ tool_name = "getCameraView" # Define tool name for logging params = { "shading_mode": shading_mode, "name_visibility_predicate": name_visibility_predicate, "auto_adjust_camera": auto_adjust_camera, "export_width": export_width, "export_height": export_height, "perspective": perspective, } # Create params dict for logging print(f"Executing {tool_name} in Blender with params: {params}") def create_view_screenshots( base_filepath=None, shading_mode="WIREFRAME", auto_adjust_camera=True, export_width=960, export_height=540, name_visibility_predicate=None, perspective="FRONT", ): """ Create a screenshot from the specified perspective view. The screenshot will be taken in fullscreen mode with consistent size. Args: base_filepath (str, optional): Base path for saving the screenshot. shading_mode (str, optional): The shading mode to use for the viewport. auto_adjust_camera (bool, optional): Whether to automatically adjust the camera for better framing. export_width (int, optional): Width of the exported image in pixels. export_height (int, optional): Height of the exported image in pixels. name_visibility_predicate (callable, optional): Function that takes an object as input and returns display settings. perspective (str or dict, optional): Specific view perspective or custom camera parameters. """ # Default filepath if not base_filepath: home = Path.home() base_filepath = home / "Downloads" / "blender_view" else: base_filepath = Path(base_filepath) # Default name visibility predicate if name_visibility_predicate is None: # Default behavior: don't show any names def name_visibility_predicate(obj): return {"show_name": False} # Store original state original_state = store_original_state() try: # Apply name visibility settings based on predicate apply_name_visibility(name_visibility_predicate) # Create temporary screen and configure 3D view temp_screen_name = "Temp_View_Layout" setup_temp_screen(temp_screen_name) # Set render resolution bpy.context.scene.render.resolution_x = export_width bpy.context.scene.render.resolution_y = export_height bpy.context.scene.render.resolution_percentage = 10 # Take screenshots image_paths = take_view_screenshots( base_filepath, temp_screen_name, shading_mode, auto_adjust_camera, perspective, ) return image_paths except Exception as ex: print("=== EXCEPTION OCCURRED ===") traceback.print_exc() return [] finally: # Restore original state restore_original_state(original_state) def store_original_state() -> Dict[str, Any]: """Store the original state of the Blender scene for later restoration.""" original_state = { "object_settings": {}, "mode": bpy.context.mode if hasattr(bpy.context, "mode") else "OBJECT", } # Safely get screen name if hasattr(bpy.context, "screen") and bpy.context.screen is not None: original_state["screen_name"] = bpy.context.screen.name else: # Get the first window's screen as a fallback if len(bpy.data.screens) > 0: original_state["screen_name"] = bpy.data.screens[0].name else: original_state["screen_name"] = None # Safely get window if hasattr(bpy.context, "window") and bpy.context.window is not None: original_state["window"] = bpy.context.window else: if ( hasattr(bpy.context, "window_manager") and len(bpy.context.window_manager.windows) > 0 ): original_state["window"] = bpy.context.window_manager.windows[0] else: original_state["window"] = None # Store object display settings for obj in bpy.context.scene.objects: obj_settings = {"show_name": obj.show_name} # Store armature-specific settings if obj.type == "ARMATURE": obj_settings.update( { "show_names": obj.data.show_names, "display_type": obj.data.display_type, } ) original_state["object_settings"][obj.name] = obj_settings return original_state def restore_original_state(original_state: Dict[str, Any]): """Restore the original state of the Blender scene.""" # Ensure we're in object mode when restoring settings if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") # Restore object settings for obj_name, settings in original_state["object_settings"].items(): obj = bpy.data.objects.get(obj_name) if obj: obj.show_name = settings["show_name"] # Restore armature-specific settings if obj.type == "ARMATURE" and "show_names" in settings: obj.data.show_names = settings["show_names"] obj.data.display_type = settings["display_type"] # Restore original screen try: # Get a valid window to use for screen switching window = original_state.get("window") if window is None or not hasattr(bpy.context, "window"): if ( hasattr(bpy.context, "window_manager") and len(bpy.context.window_manager.windows) > 0 ): window = bpy.context.window_manager.windows[0] else: print("No valid window found to restore screen") return # Switch back to the original screen if possible screen_name = original_state.get("screen_name") if screen_name and screen_name in bpy.data.screens: window.screen = bpy.data.screens[screen_name] # Delete the temporary screen if "Temp_View_Layout" in bpy.data.screens: try: window.screen = bpy.data.screens["Temp_View_Layout"] bpy.ops.screen.delete() except Exception as e: print(f"Could not delete temporary screen: {e}") print("You may need to delete it manually.") print("Original layout restored") except Exception as e: print(f"Error restoring original layout: {e}") print("You may need to manually restore your desired layout") def apply_name_visibility(predicate: Callable): """Apply name visibility settings based on the predicate function.""" for obj in bpy.context.scene.objects: # Get visibility settings from predicate settings = predicate(obj) # Apply basic object name visibility if "show_name" in settings: obj.show_name = settings["show_name"] # Apply armature-specific settings if obj.type == "ARMATURE": # Show bone names if "show_bones" in settings and settings["show_bones"]: obj.data.show_names = True obj.data.display_type = "STICK" # Better for seeing bone names # Select and activate the armature for better display bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj # Toggle pose mode to ensure bone visibility bpy.ops.object.mode_set(mode="POSE") bpy.ops.object.mode_set(mode="OBJECT") elif "show_bones" in settings and not settings["show_bones"]: obj.data.show_names = False def setup_temp_screen(temp_screen_name: str) -> None: """Set up a temporary screen for taking screenshots.""" original_window = bpy.context.window # Delete existing temp screen if it exists for screen in bpy.data.screens: if screen.name == temp_screen_name: context_override = {"window": original_window} try: with bpy.context.temp_override(**context_override): bpy.context.window.screen = screen bpy.ops.screen.delete() except (AttributeError, TypeError): bpy.context.window.screen = screen bpy.ops.screen.delete(context_override) # Create new screen bpy.ops.screen.new() temp_screen = bpy.context.screen temp_screen.name = temp_screen_name # Find or create 3D view area main_3d_view = None for area in temp_screen.areas: if area.type == "VIEW_3D": main_3d_view = area break if not main_3d_view: largest_area = max(temp_screen.areas, key=lambda a: a.width * a.height) largest_area.type = "VIEW_3D" main_3d_view = largest_area def calculate_scene_bounds() -> Tuple[mathutils.Vector, mathutils.Vector]: """Calculate bounds of all visible objects for auto-fit.""" min_co = mathutils.Vector((float("inf"), float("inf"), float("inf"))) max_co = mathutils.Vector( (float("-inf"), float("-inf"), float("-inf"))) has_objects = False for obj in bpy.context.view_layer.objects: if obj.visible_get() and obj.type in { "MESH", "CURVE", "SURFACE", "META", "FONT", "ARMATURE", "LATTICE", }: has_objects = True # Get world matrix matrix_world = obj.matrix_world if obj.type == "MESH" and len(obj.data.vertices) > 0: for vertex in obj.data.vertices: world_co = matrix_world @ vertex.co min_co.x = min(min_co.x, world_co.x) min_co.y = min(min_co.y, world_co.y) min_co.z = min(min_co.z, world_co.z) max_co.x = max(max_co.x, world_co.x) max_co.y = max(max_co.y, world_co.y) max_co.z = max(max_co.z, world_co.z) elif obj.type == "ARMATURE": # For armatures, include all bones for bone in obj.data.bones: head_world = matrix_world @ bone.head_local tail_world = matrix_world @ bone.tail_local # Check head and tail positions for pos in [head_world, tail_world]: min_co.x = min(min_co.x, pos.x) min_co.y = min(min_co.y, pos.y) min_co.z = min(min_co.z, pos.z) max_co.x = max(max_co.x, pos.x) max_co.y = max(max_co.y, pos.y) max_co.z = max(max_co.z, pos.z) else: # For non-mesh objects, use bounding box bbox_corners = [ matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box ] for corner in bbox_corners: min_co.x = min(min_co.x, corner.x) min_co.y = min(min_co.y, corner.y) min_co.z = min(min_co.z, corner.z) max_co.x = max(max_co.x, corner.x) max_co.y = max(max_co.y, corner.y) max_co.z = max(max_co.z, corner.z) if not has_objects: # Default values if no objects are found min_co = mathutils.Vector((-5.0, -5.0, -5.0)) max_co = mathutils.Vector((5.0, 5.0, 5.0)) # Calculate center and dimensions center = (min_co + max_co) / 2 dimensions = max_co - min_co # Ensure minimum size to avoid empty/tiny bounds min_size = 1.0 for i in range(3): if dimensions[i] < min_size: dimensions[i] = min_size return center, dimensions def configure_3d_view(area, shading_mode: str, show_names: bool = False) -> None: """Configure a 3D view with the specified settings.""" valid_shading_modes = ["WIREFRAME", "SOLID", "MATERIAL", "RENDERED"] if shading_mode not in valid_shading_modes: raise ValueError( f"Invalid shading mode: {shading_mode}. Must be one of {valid_shading_modes}." ) for space in area.spaces: if space.type == "VIEW_3D": # Set shading mode space.shading.type = shading_mode # Settings for better name display if show_names: space.overlay.show_overlays = True space.overlay.show_text = True if hasattr(space.overlay, "show_extras"): space.overlay.show_extras = True if hasattr(space.overlay, "show_relationship_lines"): space.overlay.show_relationship_lines = True if hasattr(space.overlay, "show_bones"): space.overlay.show_bones = True if hasattr(space.overlay, "show_bone_names"): space.overlay.show_bone_names = True def take_view_screenshots( base_filepath: Union[str, Path], temp_screen_name: str, shading_mode: str, auto_adjust_camera: bool, perspective: Union[str, Dict[str, Any]], ) -> List[str]: """Take a screenshot based on the specified perspective.""" original_window = bpy.context.window # Find the 3D view area main_3d_view = None for area in bpy.context.screen.areas: if area.type == "VIEW_3D": main_3d_view = area configure_3d_view(area, shading_mode, show_names=True) break if not main_3d_view: raise ValueError("No 3D view area found.") # Maximize the 3D view with bpy.context.temp_override(window=original_window, area=main_3d_view): bpy.ops.screen.screen_full_area() # Calculate scene bounds scene_center, scene_dimensions = calculate_scene_bounds() max_dimension = max( scene_dimensions.x, scene_dimensions.y, scene_dimensions.z ) # Define standard view configurations view_configs = { "TOP": { "perspective": "ORTHO", # Quaternion for top view "rotation": (1.0, 0.0, 0.0, 0.0), "dimension_func": lambda d: max(d.x, d.y), "filename": f"{base_filepath}_top.png", }, "FRONT": { "perspective": "ORTHO", "rotation": ( 0.7071068, 0.7071068, 0.0, 0.0, ), # Quaternion for front view "dimension_func": lambda d: max(d.x, d.z), "filename": f"{base_filepath}_front.png", }, "RIGHT": { "perspective": "ORTHO", # Quaternion for right view "rotation": (0.5, 0.5, 0.5, 0.5), "dimension_func": lambda d: max(d.y, d.z), "filename": f"{base_filepath}_right.png", }, "PERSP": { "perspective": "PERSP", # Default perspective "rotation": (0.8205, 0.4306, 0.1714, 0.3312), "dimension_func": lambda d: max(d.x, d.y, d.z), "filename": f"{base_filepath}_persp.png", }, } # Determine which views to render views_to_render = [] if isinstance(perspective, dict): # Custom perspective configuration custom_view = { "perspective": perspective.get("type", "PERSP"), "rotation": tuple(perspective.get("rotation")), "location": perspective.get("location"), "dimension_func": lambda d: max(d.x, d.y, d.z), "filename": f"{base_filepath}_custom.png", } views_to_render = [("CUSTOM", custom_view)] elif perspective == "ALL": views_to_render = list(view_configs.items()) elif perspective in view_configs: views_to_render = [(perspective, view_configs[perspective])] else: raise ValueError(f"Invalid perspective: {perspective}") # Take screenshots for each selected view image_paths = [] for view_name, config in views_to_render: for area in bpy.context.screen.areas: if area.type == "VIEW_3D": space = area.spaces[0] region_3d = space.region_3d # Configure the view region_3d.view_perspective = config["perspective"] region_3d.view_rotation = mathutils.Quaternion( config["rotation"] ) if auto_adjust_camera: # Set view location based on config or use scene_center if "location" in config and config["location"] is not None: region_3d.view_location = mathutils.Vector( config["location"] ) else: region_3d.view_location = scene_center if region_3d.view_perspective == "ORTHO": padding = 1.2 region_3d.view_distance = 0 region_3d.view_camera_zoom = 0 # Use the dimension function from the config if "dimension_func" in config: ortho_dimension = config["dimension_func"]( scene_dimensions ) region_3d.view_distance = max_dimension * padding else: region_3d.view_distance = max_dimension * padding else: padding = 1.5 region_3d.view_distance = max_dimension * padding region_3d.view_camera_zoom = 0 else: region_3d.view_distance = 7.0 region_3d.view_location = (0.0, 0.0, 0.0) # Force redraw and wait for UI update bpy.ops.wm.redraw_timer( type="DRAW_WIN_SWAP", iterations=1) bpy.context.view_layer.update() time.sleep(0.3) # Get the filename from config or generate one view_filename = config.get( "filename", f"{base_filepath}_{view_name.lower()}.png" ) print(f"Taking {view_name} view screenshot...") context_override = { "window": original_window, "area": area} try: with bpy.context.temp_override(**context_override): bpy.ops.screen.screenshot_area("EXEC_DEFAULT", filepath=str(view_filename), show_multiview=True ) pass except (AttributeError, TypeError): bpy.ops.screen.screenshot_area( context_override, filepath=str(view_filename), show_multiview=True, ) print(f"Screenshot saved to: {view_filename}") # Resize the image to half its size try: # Create resized filename filename_parts = os.path.splitext(view_filename) resized_filename = ( f"{filename_parts[0]}_resized{filename_parts[1]}" ) # Open and resize the image from PIL import Image img = Image.open(view_filename) width, height = img.size resized_img = img.resize( (width // 2, height // 2), Image.Resampling.LANCZOS ) resized_img.save(resized_filename) # Replace the original file with the resized one os.replace(resized_filename, view_filename) print( f"Image resized to half resolution: {view_filename}") image_paths.append(view_filename) except Exception as e: print(f"Error resizing image: {e}") # If resizing fails, use the original image image_paths.append(view_filename) time.sleep(0.2) break # Exit fullscreen mode try: for area in bpy.context.screen.areas: if area.type == "VIEW_3D": context_override = { "window": original_window, "area": area} try: with bpy.context.temp_override(**context_override): bpy.ops.screen.screen_full_area() except (AttributeError, TypeError): bpy.ops.screen.screen_full_area(context_override) break except Exception as e: print(f"Error exiting fullscreen: {e}") return image_paths try: # Validate enum values for shading_mode if shading_mode is not None and shading_mode not in [ "WIREFRAME", "RENDERED", "SOLID", "MATERIAL", ]: raise ValueError( f"Parameter 'shading_mode' must be one of ['WIREFRAME','RENDERED','SOLID','MATERIAL'], got {shading_mode}" ) # Set default values if shading_mode is None: shading_mode = "WIREFRAME" if auto_adjust_camera is None: auto_adjust_camera = True if perspective is None: perspective = "FRONT" # Changed default from "ALL" to "FRONT" # Use Desktop directory instead of temp directory desktop_dir = os.path.expanduser("~/Desktop") base_filepath = os.path.join( desktop_dir, "blender_camera_view" ) # Updated filename base print(f"Saving camera view image to: {base_filepath}_*.png") # Parse name_visibility_predicate if provided visibility_func = None if name_visibility_predicate: try: # Try to evaluate the string as Python code visibility_func = eval( f"lambda obj: {name_visibility_predicate}") except Exception as e: print( f"Error evaluating name_visibility_predicate: {name_visibility_predicate} - {str(e)}" ) print("Using default visibility settings") # Create view screenshots image_paths = create_view_screenshots( base_filepath=base_filepath, shading_mode=shading_mode, auto_adjust_camera=auto_adjust_camera, export_width=export_width, export_height=export_height, name_visibility_predicate=visibility_func, perspective=perspective, ) # Verify files exist existing_paths = [path for path in image_paths if os.path.exists(path)] if not existing_paths: raise FileNotFoundError("No screenshot files were generated") return { "content": { "path": existing_paths, "type": "image", } } except Exception as e: print(f"Error in {tool_name}: {str(e)}") return {"success": False, "error": str(e)}

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/team-plask/3d-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server