文件预览

main.py

查看 OpenClaw 3D建模与模型处理 SuperSkill V1.0.0 技能包中的文件内容。

文件内容

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)}")