文件内容
3d_modeling_skill/main.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
OpenClaw Super Skill: 3D Modeling & Model Processing
Version: 1.0.0
Author: OpenClaw Team
Description: Comprehensive 3D modeling, editing, rendering, and processing toolkit
"""
import os
import sys
import json
import numpy as np
from typing import Dict, List, Any, Optional, Tuple, Union
from dataclasses import dataclass, asdict
from enum import Enum
import logging
from pathlib import Path
import glob
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 3D Processing Libraries
try:
import trimesh
from trimesh import creation
from trimesh.smoothing import filter_humphrey
TRIMESH_AVAILABLE = True
except ImportError:
TRIMESH_AVAILABLE = False
logger.warning("trimesh not available, some features may be limited")
try:
import pyvista as pv
PYVISTA_AVAILABLE = True
except ImportError:
PYVISTA_AVAILABLE = False
logger.warning("pyvista not available, rendering features may be limited")
try:
from stl import mesh as stl_mesh
NUMPY_STL_AVAILABLE = True
except ImportError:
NUMPY_STL_AVAILABLE = False
logger.warning("numpy-stl not available, STL processing may be limited")
try:
from scipy.spatial import ConvexHull
SCIPY_AVAILABLE = True
except ImportError:
SCIPY_AVAILABLE = False
logger.warning("scipy not available, some advanced features may be limited")
class OperationType(Enum):
"""Supported 3D operation types"""
GENERATE_PRIMITIVE = "generate_primitive"
TRANSFORM = "transform"
BOOLEAN = "boolean"
MATERIAL = "material"
RENDER = "render"
CONVERT_FORMAT = "convert_format"
PARAMETRIC = "parametric"
PRINT_PREPROCESS = "print_preprocess"
MEASURE = "measure"
OPTIMIZE = "optimize"
BATCH_PROCESS = "batch_process"
IMPORT_EXPORT = "import_export"
VISUALIZE = "visualize"
class PrimitiveType(Enum):
"""Supported primitive geometry types"""
BOX = "box"
SPHERE = "sphere"
CYLINDER = "cylinder"
CONE = "cone"
TORUS = "torus"
ICOSPHERE = "icosphere"
TETRAHEDRON = "tetrahedron"
OCTAHEDRON = "octahedron"
ICOSAHEDRON = "icosahedron"
class BooleanOperation(Enum):
"""Boolean operation types"""
UNION = "union"
INTERSECTION = "intersection"
DIFFERENCE = "difference"
@dataclass
class SkillResult:
"""Standard OpenClaw Skill result structure"""
success: bool
message: str
data: Dict[str, Any] = None
error: str = None
output_files: List[str] = None
def to_dict(self):
return {
"success": self.success,
"message": self.message,
"data": self.data or {},
"error": self.error,
"output_files": self.output_files or []
}
class Modeling3DSkill:
"""
OpenClaw Super Skill: 3D Modeling & Model Processing
Provides comprehensive 3D modeling, editing, rendering, and processing capabilities
"""
def __init__(self):
self.name = "3D Modeling & Model Processing"
self.version = "1.0.0"
self.description = "Comprehensive 3D modeling toolkit with 12 core features"
self.author = "OpenClaw Team"
self.supported_formats = ['.stl', '.obj', '.ply', '.glb', '.gltf', '.3ds', '.fbx', '.dae', '.wrl']
self.current_mesh = None
self.mesh_cache = {}
self.output_dir = Path("./3d_output")
self.output_dir.mkdir(exist_ok=True)
def help(self) -> Dict[str, Any]:
"""Return skill help information"""
return {
"name": self.name,
"version": self.version,
"description": self.description,
"operations": [op.value for op in OperationType],
"supported_formats": self.supported_formats,
"features": [
"基础3D几何体生成",
"3D模型编辑",
"材质与纹理处理",
"3D场景渲染",
"3D格式转换",
"参数化建模",
"3D打印预处理",
"3D模型测量",
"模型优化",
"批量3D模型处理",
"3D模型导入导出",
"3D模型可视化预览"
]
}
def run(self, params: Dict[str, Any]) -> SkillResult:
"""
Main entry point for OpenClaw Skill
Args:
params: Dictionary containing operation parameters
Returns:
SkillResult with operation outcome
"""
try:
operation = params.get("operation")
if not operation:
return SkillResult(False, "Operation type is required", error="Missing operation parameter")
logger.info(f"Executing operation: {operation}")
# Route to appropriate handler
handlers = {
OperationType.GENERATE_PRIMITIVE.value: self._generate_primitive,
OperationType.TRANSFORM.value: self._transform_mesh,
OperationType.BOOLEAN.value: self._boolean_operation,
OperationType.MATERIAL.value: self._process_material,
OperationType.RENDER.value: self._render_scene,
OperationType.CONVERT_FORMAT.value: self._convert_format,
OperationType.PARAMETRIC.value: self._parametric_modeling,
OperationType.PRINT_PREPROCESS.value: self._3d_print_preprocess,
OperationType.MEASURE.value: self._measure_model,
OperationType.OPTIMIZE.value: self._optimize_model,
OperationType.BATCH_PROCESS.value: self._batch_process,
OperationType.IMPORT_EXPORT.value: self._import_export,
OperationType.VISUALIZE.value: self._visualize_model
}
handler = handlers.get(operation)
if not handler:
return SkillResult(False, f"Unknown operation: {operation}", error="Invalid operation type")
return handler(params)
except Exception as e:
logger.error(f"Operation failed: {str(e)}", exc_info=True)
return SkillResult(False, "Operation failed", error=str(e))
# =========================================================================
# 1. 基础3D几何体生成
# =========================================================================
def _generate_primitive(self, params: Dict[str, Any]) -> SkillResult:
"""Generate basic 3D geometric primitives"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required for this operation", error="Missing dependency")
primitive_type = params.get("primitive_type", PrimitiveType.BOX.value)
size = params.get("size", 1.0)
extents = params.get("extents", [size, size, size])
position = params.get("position", [0, 0, 0])
rotation = params.get("rotation", [0, 0, 0])
segments = params.get("segments", 32)
output_file = params.get("output_file")
try:
# Generate primitive based on type
if primitive_type == PrimitiveType.BOX.value:
mesh = creation.box(extents=extents)
elif primitive_type == PrimitiveType.SPHERE.value:
mesh = creation.icosphere(radius=size, subdivisions=3)
elif primitive_type == PrimitiveType.CYLINDER.value:
height = params.get("height", size * 2)
mesh = creation.cylinder(radius=size, height=height, sections=segments)
elif primitive_type == PrimitiveType.CONE.value:
height = params.get("height", size * 2)
mesh = creation.cone(radius=size, height=height, sections=segments)
elif primitive_type == PrimitiveType.TORUS.value:
major_radius = params.get("major_radius", size)
minor_radius = params.get("minor_radius", size * 0.3)
mesh = creation.torus(major_radius=major_radius, minor_radius=minor_radius, sections=segments)
elif primitive_type == PrimitiveType.ICOSPHERE.value:
subdivisions = params.get("subdivisions", 3)
mesh = creation.icosphere(radius=size, subdivisions=subdivisions)
elif primitive_type == PrimitiveType.TETRAHEDRON.value:
mesh = creation.tetrahedron()
mesh.apply_scale(size)
elif primitive_type == PrimitiveType.OCTAHEDRON.value:
mesh = creation.octahedron()
mesh.apply_scale(size)
elif primitive_type == PrimitiveType.ICOSAHEDRON.value:
mesh = creation.icosahedron()
mesh.apply_scale(size)
else:
return SkillResult(False, f"Unknown primitive type: {primitive_type}", error="Invalid primitive type")
# Apply transformations
mesh.apply_translation(position)
if any(rotation):
mesh.apply_euler(rotation, degrees=True)
# Store mesh
self.current_mesh = mesh
mesh_id = f"primitive_{primitive_type}_{id(mesh)}"
self.mesh_cache[mesh_id] = mesh
# Save if output file specified
output_files = []
if output_file:
output_path = self.output_dir / output_file
mesh.export(str(output_path))
output_files.append(str(output_path))
result_data = {
"mesh_id": mesh_id,
"primitive_type": primitive_type,
"vertices": len(mesh.vertices),
"faces": len(mesh.faces),
"bounds": mesh.bounds.tolist(),
"extents": mesh.extents.tolist()
}
return SkillResult(
True,
f"Successfully generated {primitive_type}",
data=result_data,
output_files=output_files
)
except Exception as e:
return SkillResult(False, f"Failed to generate primitive: {str(e)}", error=str(e))
# =========================================================================
# 2. 3D模型编辑 (平移、旋转、缩放、布尔运算等)
# =========================================================================
def _transform_mesh(self, params: Dict[str, Any]) -> SkillResult:
"""Apply transformations to mesh: translate, rotate, scale"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
mesh_id = params.get("mesh_id")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
try:
# Apply transformations
translation = params.get("translation")
if translation:
mesh.apply_translation(translation)
rotation = params.get("rotation")
if rotation:
mesh.apply_euler(rotation, degrees=params.get("degrees", True))
scale = params.get("scale")
if scale:
if isinstance(scale, (int, float)):
mesh.apply_scale(scale)
else:
mesh.apply_scale(scale)
output_file = params.get("output_file")
output_files = []
if output_file:
output_path = self.output_dir / output_file
mesh.export(str(output_path))
output_files.append(str(output_path))
return SkillResult(
True,
"Transformation applied successfully",
data={
"mesh_id": mesh_id,
"new_bounds": mesh.bounds.tolist(),
"new_extents": mesh.extents.tolist()
},
output_files=output_files
)
except Exception as e:
return SkillResult(False, f"Transformation failed: {str(e)}", error=str(e))
def _boolean_operation(self, params: Dict[str, Any]) -> SkillResult:
"""Perform boolean operations between two meshes"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
mesh1_id = params.get("mesh1_id")
mesh2_id = params.get("mesh2_id")
operation = params.get("boolean_type", BooleanOperation.UNION.value)
mesh1 = self._get_mesh(mesh1_id)
mesh2 = self._get_mesh(mesh2_id)
if mesh1 is None or mesh2 is None:
return SkillResult(False, "One or both meshes not found", error="Invalid mesh IDs")
try:
# Ensure meshes are watertight for boolean operations
if not mesh1.is_watertight:
mesh1.fill_holes()
if not mesh2.is_watertight:
mesh2.fill_holes()
if operation == BooleanOperation.UNION.value:
result = trimesh.boolean.union([mesh1, mesh2])
elif operation == BooleanOperation.INTERSECTION.value:
result = trimesh.boolean.intersection([mesh1, mesh2])
elif operation == BooleanOperation.DIFFERENCE.value:
result = trimesh.boolean.difference([mesh1, mesh2])
else:
return SkillResult(False, f"Unknown boolean operation: {operation}", error="Invalid operation")
result_id = f"boolean_{operation}_{id(result)}"
self.mesh_cache[result_id] = result
self.current_mesh = result
output_file = params.get("output_file")
output_files = []
if output_file:
output_path = self.output_dir / output_file
result.export(str(output_path))
output_files.append(str(output_path))
return SkillResult(
True,
f"Boolean {operation} completed successfully",
data={
"result_mesh_id": result_id,
"vertices": len(result.vertices),
"faces": len(result.faces),
"volume": result.volume
},
output_files=output_files
)
except Exception as e:
return SkillResult(False, f"Boolean operation failed: {str(e)}", error=str(e))
# =========================================================================
# 3. 材质与纹理处理
# =========================================================================
def _process_material(self, params: Dict[str, Any]) -> SkillResult:
"""Process material properties, UV mapping, and textures"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
mesh_id = params.get("mesh_id")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
try:
operation = params.get("material_operation", "set_color")
result_data = {}
if operation == "set_color":
# Set vertex colors
color = params.get("color", [200, 200, 200, 255])
if len(color) == 3:
color = list(color) + [255]
mesh.visual.vertex_colors = np.tile(color, (len(mesh.vertices), 1))
result_data["color_set"] = color
elif operation == "uv_unwrap":
# Simple UV unwrapping (spherical projection)
vertices = mesh.vertices
# Spherical UV mapping
x, y, z = vertices.T
r = np.sqrt(x**2 + y**2 + z**2)
theta = np.arccos(z / np.maximum(r, 1e-8))
phi = np.arctan2(y, x)
u = (phi + np.pi) / (2 * np.pi)
v = theta / np.pi
uv_coords = np.column_stack([u, v])
result_data["uv_coords"] = uv_coords.tolist()
result_data["uv_method"] = "spherical_projection"
elif operation == "set_material":
material_props = {
"ambient": params.get("ambient", [0.2, 0.2, 0.2]),
"diffuse": params.get("diffuse", [0.8, 0.8, 0.8]),
"specular": params.get("specular", [0.5, 0.5, 0.5]),
"shininess": params.get("shininess", 50.0),
"opacity": params.get("opacity", 1.0)
}
result_data["material_properties"] = material_props
output_file = params.get("output_file")
output_files = []
if output_file:
output_path = self.output_dir / output_file
mesh.export(str(output_path))
output_files.append(str(output_path))
return SkillResult(
True,
f"Material operation '{operation}' completed",
data=result_data,
output_files=output_files
)
except Exception as e:
return SkillResult(False, f"Material processing failed: {str(e)}", error=str(e))
# =========================================================================
# 4. 3D场景渲染
# =========================================================================
def _render_scene(self, params: Dict[str, Any]) -> SkillResult:
"""Render 3D scene with lighting and camera settings"""
if not PYVISTA_AVAILABLE:
return SkillResult(False, "pyvista library is required for rendering", error="Missing dependency")
mesh_id = params.get("mesh_id")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
try:
# Convert trimesh to pyvista
pv_mesh = pv.wrap(mesh.to_dict())
# Create plotter
off_screen = params.get("off_screen", True)
plotter = pv.Plotter(off_screen=off_screen)
# Add mesh with properties
color = params.get("color", "lightblue")
show_edges = params.get("show_edges", False)
edge_color = params.get("edge_color", "black")
opacity = params.get("opacity", 1.0)
plotter.add_mesh(
pv_mesh,
color=color,
show_edges=show_edges,
edge_color=edge_color,
opacity=opacity
)
# Camera settings
camera_position = params.get("camera_position")
if camera_position:
plotter.camera_position = camera_position
else:
plotter.camera.azimuth = params.get("azimuth", 45)
plotter.camera.elevation = params.get("elevation", 30)
plotter.camera.zoom(params.get("zoom", 1.0))
# Lighting
if params.get("enable_lighting", True):
plotter.enable_lightkit()
# Background
plotter.set_background(params.get("background_color", "white"))
# Add axes
if params.get("show_axes", True):
plotter.add_axes()
# Render and save
output_image = params.get("output_image", "render_output.png")
output_path = self.output_dir / output_image
window_size = params.get("window_size", [1920, 1080])
plotter.screenshot(str(output_path), window_size=window_size)
if not off_screen:
plotter.show()
else:
plotter.close()
return SkillResult(
True,
"Rendering completed successfully",
data={
"image_size": window_size,
"camera_position": plotter.camera_position,
"background": params.get("background_color", "white")
},
output_files=[str(output_path)]
)
except Exception as e:
return SkillResult(False, f"Rendering failed: {str(e)}", error=str(e))
# =========================================================================
# 5. 3D格式转换
# =========================================================================
def _convert_format(self, params: Dict[str, Any]) -> SkillResult:
"""Convert between different 3D file formats"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
input_file = params.get("input_file")
output_file = params.get("output_file")
if not input_file or not output_file:
return SkillResult(False, "Input and output files are required", error="Missing file parameters")
try:
# Load mesh
mesh = trimesh.load(input_file)
if isinstance(mesh, trimesh.Scene):
mesh = mesh.to_mesh()
# Export to target format
mesh.export(output_file)
# Store in cache
mesh_id = f"converted_{id(mesh)}"
self.mesh_cache[mesh_id] = mesh
input_ext = Path(input_file).suffix.lower()
output_ext = Path(output_file).suffix.lower()
return SkillResult(
True,
f"Successfully converted from {input_ext} to {output_ext}",
data={
"mesh_id": mesh_id,
"input_format": input_ext,
"output_format": output_ext,
"vertices": len(mesh.vertices),
"faces": len(mesh.faces)
},
output_files=[output_file]
)
except Exception as e:
return SkillResult(False, f"Format conversion failed: {str(e)}", error=str(e))
# =========================================================================
# 6. 参数化建模
# =========================================================================
def _parametric_modeling(self, params: Dict[str, Any]) -> SkillResult:
"""Parametric modeling with parameter-driven generation"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
model_type = params.get("model_type", "extrusion")
try:
if model_type == "extrusion":
# Extrude a 2D polygon
polygon_points = params.get("polygon_points", [[0, 0], [1, 0], [1, 1], [0, 1]])
height = params.get("height", 1.0)
# Create 3D vertices by extruding
vertices_2d = np.array(polygon_points)
n_points = len(vertices_2d)
# Create bottom and top vertices
bottom = np.column_stack([vertices_2d, np.zeros(n_points)])
top = np.column_stack([vertices_2d, np.full(n_points, height)])
vertices = np.vstack([bottom, top])
# Create faces
faces = []
# Bottom face
for i in range(1, n_points - 1):
faces.append([0, i, i + 1])
# Top face
for i in range(1, n_points - 1):
faces.append([n_points, n_points + i, n_points + i + 1])
# Side faces
for i in range(n_points):
next_i = (i + 1) % n_points
faces.append([i, next_i, n_points + next_i])
faces.append([i, n_points + next_i, n_points + i])
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
elif model_type == "revolution":
# Surface of revolution
profile_points = params.get("profile_points", [[0, 0], [1, 0], [1, 1], [0, 1]])
angle = params.get("angle", 360)
segments = params.get("segments", 32)
profile = np.array(profile_points)
mesh = creation.revolve(profile, angle=angle, sections=segments)
elif model_type == "sweep":
# Sweep along path
pass
else:
return SkillResult(False, f"Unknown parametric model type: {model_type}", error="Invalid model type")
mesh_id = f"parametric_{model_type}_{id(mesh)}"
self.mesh_cache[mesh_id] = mesh
self.current_mesh = mesh
output_file = params.get("output_file")
output_files = []
if output_file:
output_path = self.output_dir / output_file
mesh.export(str(output_path))
output_files.append(str(output_path))
return SkillResult(
True,
f"Parametric {model_type} modeling completed",
data={
"mesh_id": mesh_id,
"vertices": len(mesh.vertices),
"faces": len(mesh.faces),
"volume": mesh.volume
},
output_files=output_files
)
except Exception as e:
return SkillResult(False, f"Parametric modeling failed: {str(e)}", error=str(e))
# =========================================================================
# 7. 3D打印预处理
# =========================================================================
def _3d_print_preprocess(self, params: Dict[str, Any]) -> SkillResult:
"""3D printing preprocessing: repair, wall thickness check, supports"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
mesh_id = params.get("mesh_id")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
try:
operation = params.get("preprocess_operation", "repair")
result_data = {}
if operation == "repair":
# Full mesh repair
repair_steps = []
if not mesh.is_watertight:
holes_filled = mesh.fill_holes()
repair_steps.append(f"Filled {holes_filled} holes")
# Remove duplicate vertices
mesh.merge_vertices()
repair_steps.append("Merged duplicate vertices")
# Fix normals
mesh.fix_normals()
repair_steps.append("Fixed face normals")
# Remove degenerate faces
mesh.remove_degenerate_faces()
repair_steps.append("Removed degenerate faces")
result_data["repair_steps"] = repair_steps
result_data["is_watertight"] = mesh.is_watertight
elif operation == "wall_thickness_check":
# Check minimum wall thickness
min_thickness = params.get("min_thickness", 1.0)
thickness_issues = []
# Simple thickness check using edge lengths
edge_lengths = mesh.edges_unique_length
thin_edges = np.sum(edge_lengths < min_thickness)
result_data["min_thickness_required"] = min_thickness
result_data["thin_edges_count"] = int(thin_edges)
result_data["average_edge_length"] = float(np.mean(edge_lengths))
result_data["thickness_ok"] = thin_edges < len(edge_lengths) * 0.05
elif operation == "orient_for_printing":
# Orient model for best printing (largest face down)
# Find best orientation using bounding box
mesh.apply_obb()
result_data["orientation"] = "OBB oriented"
result_data["new_bounds"] = mesh.bounds.tolist()
elif operation == "slice_preview":
# Generate slice preview
layer_height = params.get("layer_height", 0.2)
z_min, z_max = mesh.bounds[:, 2]
num_layers = int((z_max - z_min) / layer_height)
result_data["layer_height"] = layer_height
result_data["estimated_layers"] = num_layers
result_data["print_height"] = float(z_max - z_min)
elif operation == "print_calculation":
# Calculate print parameters
volume = mesh.volume
density = params.get("material_density", 1.04) # g/cm³ for PLA
infill = params.get("infill_percent", 20) / 100
material_volume = volume * infill
material_mass = material_volume * density
result_data["model_volume_cm3"] = float(volume)
result_data["material_volume_cm3"] = float(material_volume)
result_data["estimated_mass_g"] = float(material_mass)
result_data["infill_percent"] = params.get("infill_percent", 20)
output_file = params.get("output_file")
output_files = []
if output_file:
output_path = self.output_dir / output_file
mesh.export(str(output_path))
output_files.append(str(output_path))
return SkillResult(
True,
f"3D print preprocessing '{operation}' completed",
data=result_data,
output_files=output_files
)
except Exception as e:
return SkillResult(False, f"3D print preprocessing failed: {str(e)}", error=str(e))
# =========================================================================
# 8. 3D模型测量
# =========================================================================
def _measure_model(self, params: Dict[str, Any]) -> SkillResult:
"""Measure model properties: dimensions, volume, surface area, center of mass"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
mesh_id = params.get("mesh_id")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
try:
measurements = {
# Bounding box
"bounds": mesh.bounds.tolist(),
"dimensions": mesh.extents.tolist(),
"width": float(mesh.extents[0]),
"depth": float(mesh.extents[1]),
"height": float(mesh.extents[2]),
# Volume and area
"volume": float(mesh.volume),
"surface_area": float(mesh.area),
# Center of mass
"center_of_mass": mesh.center_mass.tolist(),
"centroid": mesh.centroid.tolist(),
# Topology
"vertex_count": len(mesh.vertices),
"face_count": len(mesh.faces),
"edge_count": len(mesh.edges),
# Quality metrics
"is_watertight": mesh.is_watertight,
"is_convex": mesh.is_convex,
"euler_number": mesh.euler_number,
# Bounding sphere
"bounding_sphere_radius": float(mesh.bounding_sphere.primitive.radius)
}
# Additional measurements if requested
if params.get("calculate_bounding_box", True):
measurements["obb_dimensions"] = mesh.bounding_box_oriented.extents.tolist()
return SkillResult(
True,
"Model measurement completed",
data=measurements
)
except Exception as e:
return SkillResult(False, f"Measurement failed: {str(e)}", error=str(e))
# =========================================================================
# 9. 模型优化
# =========================================================================
def _optimize_model(self, params: Dict[str, Any]) -> SkillResult:
"""Optimize mesh: decimation, topology optimization, hole filling"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
mesh_id = params.get("mesh_id")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
try:
operation = params.get("optimization_type", "decimate")
original_vertices = len(mesh.vertices)
original_faces = len(mesh.faces)
result_data = {
"original_vertices": original_vertices,
"original_faces": original_faces
}
if operation == "decimate":
# Mesh decimation (simplification)
target_ratio = params.get("target_ratio", 0.5)
target_faces = int(original_faces * target_ratio)
simplified = mesh.simplify_quadric_decimation(target_faces)
mesh = simplified
result_data["optimization"] = "quadric_decimation"
result_data["target_ratio"] = target_ratio
result_data["result_vertices"] = len(mesh.vertices)
result_data["result_faces"] = len(mesh.faces)
result_data["reduction_percent"] = round((1 - len(mesh.faces) / original_faces) * 100, 2)
elif operation == "smooth":
# Mesh smoothing
iterations = params.get("iterations", 10)
filter_humphrey(mesh, iterations=iterations)
result_data["optimization"] = "humphrey_smoothing"
result_data["iterations"] = iterations
elif operation == "fill_holes":
# Fill holes
holes_filled = mesh.fill_holes()
result_data["optimization"] = "hole_filling"
result_data["holes_filled"] = holes_filled
elif operation == "fix_normals":
# Fix and unify normals
mesh.fix_normals()
result_data["optimization"] = "normal_fixing"
result_data["normals_fixed"] = True
elif operation == "remove_duplicates":
# Remove duplicate vertices and faces
mesh.merge_vertices()
mesh.remove_duplicate_faces()
result_data["optimization"] = "duplicate_removal"
# Update cache
self.mesh_cache[mesh_id] = mesh
self.current_mesh = mesh
output_file = params.get("output_file")
output_files = []
if output_file:
output_path = self.output_dir / output_file
mesh.export(str(output_path))
output_files.append(str(output_path))
return SkillResult(
True,
f"Model optimization '{operation}' completed",
data=result_data,
output_files=output_files
)
except Exception as e:
return SkillResult(False, f"Model optimization failed: {str(e)}", error=str(e))
# =========================================================================
# 10. 批量3D模型处理
# =========================================================================
def _batch_process(self, params: Dict[str, Any]) -> SkillResult:
"""Batch process multiple 3D models"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
input_dir = params.get("input_directory")
output_dir = params.get("output_directory", str(self.output_dir / "batch_output"))
operation = params.get("batch_operation", "convert_format")
target_format = params.get("target_format", ".stl")
if not input_dir:
return SkillResult(False, "Input directory is required", error="Missing input directory")
try:
os.makedirs(output_dir, exist_ok=True)
# Find all 3D files
input_path = Path(input_dir)
model_files = []
for ext in self.supported_formats:
model_files.extend(input_path.glob(f"*{ext}"))
if not model_files:
return SkillResult(False, "No 3D model files found in input directory", error="No files found")
results = []
processed_count = 0
failed_count = 0
for model_file in model_files:
try:
mesh = trimesh.load(str(model_file))
if isinstance(mesh, trimesh.Scene):
mesh = mesh.to_mesh()
if operation == "convert_format":
output_file = Path(output_dir) / f"{model_file.stem}{target_format}"
mesh.export(str(output_file))
results.append({
"file": model_file.name,
"status": "success",
"output": output_file.name
})
processed_count += 1
elif operation == "optimize":
# Batch optimization
simplified = mesh.simplify_quadric_decimation(int(len(mesh.faces) * 0.5))
output_file = Path(output_dir) / f"optimized_{model_file.name}"
simplified.export(str(output_file))
results.append({
"file": model_file.name,
"status": "success",
"output": output_file.name,
"original_faces": len(mesh.faces),
"optimized_faces": len(simplified.faces)
})
processed_count += 1
elif operation == "repair":
# Batch repair
if not mesh.is_watertight:
mesh.fill_holes()
mesh.merge_vertices()
mesh.fix_normals()
output_file = Path(output_dir) / f"repaired_{model_file.name}"
mesh.export(str(output_file))
results.append({
"file": model_file.name,
"status": "success",
"output": output_file.name
})
processed_count += 1
except Exception as e:
results.append({
"file": model_file.name,
"status": "failed",
"error": str(e)
})
failed_count += 1
return SkillResult(
True,
f"Batch processing completed: {processed_count} succeeded, {failed_count} failed",
data={
"total_files": len(model_files),
"processed": processed_count,
"failed": failed_count,
"operation": operation,
"details": results
}
)
except Exception as e:
return SkillResult(False, f"Batch processing failed: {str(e)}", error=str(e))
# =========================================================================
# 11. 3D模型导入导出
# =========================================================================
def _import_export(self, params: Dict[str, Any]) -> SkillResult:
"""Import and export 3D models"""
if not TRIMESH_AVAILABLE:
return SkillResult(False, "trimesh library is required", error="Missing dependency")
operation = params.get("io_operation", "import")
try:
if operation == "import":
input_file = params.get("input_file")
if not input_file:
return SkillResult(False, "Input file is required", error="Missing input file")
mesh = trimesh.load(input_file)
if isinstance(mesh, trimesh.Scene):
mesh = mesh.to_mesh()
mesh_id = f"imported_{id(mesh)}"
self.mesh_cache[mesh_id] = mesh
self.current_mesh = mesh
return SkillResult(
True,
f"Successfully imported: {Path(input_file).name}",
data={
"mesh_id": mesh_id,
"vertices": len(mesh.vertices),
"faces": len(mesh.faces),
"format": Path(input_file).suffix.lower(),
"is_watertight": mesh.is_watertight
}
)
elif operation == "export":
mesh_id = params.get("mesh_id")
output_file = params.get("output_file")
if not mesh_id or not output_file:
return SkillResult(False, "Mesh ID and output file are required", error="Missing parameters")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
mesh.export(output_file)
return SkillResult(
True,
f"Successfully exported to: {output_file}",
data={
"mesh_id": mesh_id,
"output_format": Path(output_file).suffix.lower(),
"file_size_bytes": os.path.getsize(output_file)
},
output_files=[output_file]
)
else:
return SkillResult(False, f"Unknown I/O operation: {operation}", error="Invalid operation")
except Exception as e:
return SkillResult(False, f"Import/Export failed: {str(e)}", error=str(e))
# =========================================================================
# 12. 3D模型可视化预览
# =========================================================================
def _visualize_model(self, params: Dict[str, Any]) -> SkillResult:
"""Interactive visualization of 3D models"""
if not PYVISTA_AVAILABLE:
return SkillResult(False, "pyvista library is required for visualization", error="Missing dependency")
mesh_id = params.get("mesh_id")
mesh = self._get_mesh(mesh_id)
if mesh is None:
return SkillResult(False, "Mesh not found", error="Invalid mesh_id")
try:
# Convert trimesh to pyvista
pv_mesh = pv.wrap(mesh.to_dict())
# Create interactive plotter
plotter = pv.Plotter()
# Visualization options
color = params.get("color", "lightblue")
show_edges = params.get("show_edges", True)
show_vertices = params.get("show_vertices", False)
style = params.get("style", "surface")
plotter.add_mesh(
pv_mesh,
color=color,
show_edges=show_edges,
style=style,
point_size=5 if show_vertices else 0
)
# Add helpful widgets
plotter.add_axes()
plotter.add_bounding_box(color="grey")
plotter.show_grid()
# Set background
plotter.set_background(params.get("background", "white"))
# Title
title = params.get("title", "3D Model Visualization")
plotter.add_text(title, font_size=12)
# Show interactive window
screenshot = params.get("screenshot_file")
if screenshot:
output_path = self.output_dir / screenshot
plotter.screenshot(str(output_path))
# Show interactive window (non-blocking if specified)
if not params.get("headless", False):
plotter.show()
plotter.close()
return SkillResult(
True,
"Visualization completed",
data={
"visualization_style": style,
"show_edges": show_edges,
"background": params.get("background", "white")
},
output_files=[str(self.output_dir / screenshot)] if screenshot else []
)
except Exception as e:
return SkillResult(False, f"Visualization failed: {str(e)}", error=str(e))
# =========================================================================
# Helper Methods
# =========================================================================
def _get_mesh(self, mesh_id: str) -> Optional[trimesh.Trimesh]:
"""Get mesh from cache or use current mesh"""
if mesh_id and mesh_id in self.mesh_cache:
return self.mesh_cache[mesh_id]
return self.current_mesh
def list_cached_meshes(self) -> List[Dict[str, Any]]:
"""List all cached meshes"""
result = []
for mesh_id, mesh in self.mesh_cache.items():
result.append({
"mesh_id": mesh_id,
"vertices": len(mesh.vertices),
"faces": len(mesh.faces),
"is_watertight": mesh.is_watertight
})
return result
def clear_cache(self) -> None:
"""Clear mesh cache"""
self.mesh_cache.clear()
self.current_mesh = None
# =============================================================================
# OpenClaw Skill Entry Point
# =============================================================================
def execute(params: Dict[str, Any]) -> Dict[str, Any]:
"""
Standard OpenClaw Skill entry point
Args:
params: Dictionary containing operation parameters
Returns:
Dictionary result
"""
skill = Modeling3DSkill()
# Handle help request
if params.get("help", False) or params.get("operation") == "help":
return skill.help()
# Execute operation
result = skill.run(params)
return result.to_dict()
def get_info() -> Dict[str, Any]:
"""Return skill metadata for OpenClaw registration"""
skill = Modeling3DSkill()
return {
"name": skill.name,
"version": skill.version,
"description": skill.description,
"author": skill.author,
"category": "3D Modeling & Design",
"tags": ["3D", "modeling", "rendering", "3D printing", "CAD", "mesh processing"],
"trigger_words": [
"3D建模", "生成3D模型", "3D打印", "模型处理",
"3D渲染", "格式转换", "模型优化", "几何体生成",
"布尔运算", "参数化建模", "模型测量", "3D可视化"
],
"dependencies": ["trimesh", "pyvista", "numpy-stl", "numpy", "scipy"],
"supported_platforms": ["Windows", "macOS", "Linux"]
}
if __name__ == "__main__":
# Test execution
print("3D Modeling Skill - Testing basic functionality")
print("=" * 60)
# Print skill info
info = get_info()
print(f"Skill: {info['name']} v{info['version']}")
print(f"Description: {info['description']}")
print(f"Trigger words: {', '.join(info['trigger_words'])}")
print()
# Test primitive generation
if len(sys.argv) > 1 and sys.argv[1] == "test":
print("Running basic functionality test...")
test_params = {
"operation": "generate_primitive",
"primitive_type": "sphere",
"size": 1.0,
"output_file": "test_sphere.stl"
}
result = execute(test_params)
print(f"Test result: {json.dumps(result, indent=2, ensure_ascii=False)}")