Initial commit: RhinoMCP project structure
- MCP Server (Python) with full tool definitions for Rhino/Grasshopper - Rhino Plugin (C#) with HTTP server for command execution - Support for geometry creation, manipulation, boolean operations - Grasshopper integration: sliders, toggles, components, connections - MIT License, README with architecture documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
.env
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# C# / Visual Studio
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
.vs/
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.pdb
|
||||||
|
*.rhp
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
*.log
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Keep examples
|
||||||
|
!examples/*.gh
|
||||||
|
!examples/*.3dm
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 RhinoMCP Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
107
README.md
Normal file
107
README.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# RhinoMCP
|
||||||
|
|
||||||
|
Ein MCP-Server (Model Context Protocol) für die Integration von Claude AI mit Rhino 3D und Grasshopper.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Rhino-Geometrie erstellen** - Punkte, Linien, Kurven, Flächen, Volumenkörper
|
||||||
|
- **Grasshopper-Definitionen** - Komponenten erstellen, verbinden, Parameter setzen
|
||||||
|
- **GH-Dateien laden/modifizieren** - Bestehende Grasshopper-Definitionen bearbeiten
|
||||||
|
- **Geometrie analysieren** - Maße, Abstände, Flächen, Volumen berechnen
|
||||||
|
- **Export** - Verschiedene Formate (3DM, STEP, IGES, STL, etc.)
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Claude Code / Claude.ai │
|
||||||
|
└──────────────────┬──────────────────────────────────────┘
|
||||||
|
│ MCP Protocol (stdio/SSE)
|
||||||
|
┌──────────────────▼──────────────────────────────────────┐
|
||||||
|
│ MCP-Server (Python) │
|
||||||
|
│ - Tool-Definitionen │
|
||||||
|
│ - Request/Response Handling │
|
||||||
|
└──────────────────┬──────────────────────────────────────┘
|
||||||
|
│ HTTP (localhost:9000)
|
||||||
|
┌──────────────────▼──────────────────────────────────────┐
|
||||||
|
│ Rhino-Plugin (C# .rhp) │
|
||||||
|
│ ├─ RhinoCommon API (Geometrie) │
|
||||||
|
│ ├─ Grasshopper SDK (Komponenten, Verbindungen) │
|
||||||
|
│ └─ HTTP-Server (empfängt Befehle) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Rhino 8 oder Rhino 9 WIP
|
||||||
|
- Python 3.10+
|
||||||
|
- .NET Framework 4.8 / .NET 7.0 (für Rhino 8)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. MCP-Server (Python)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/mcp-server
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Rhino-Plugin
|
||||||
|
|
||||||
|
1. Öffne `src/rhino-plugin/RhinoMCP.sln` in Visual Studio
|
||||||
|
2. Build das Projekt
|
||||||
|
3. Kopiere `RhinoMCP.rhp` nach `%APPDATA%\McNeel\Rhinoceros\8.0\Plug-ins\`
|
||||||
|
4. Starte Rhino und aktiviere das Plugin
|
||||||
|
|
||||||
|
### 3. Claude Code Konfiguration
|
||||||
|
|
||||||
|
Füge in `~/.claude/claude_desktop_config.json` hinzu:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"rhino": {
|
||||||
|
"command": "python",
|
||||||
|
"args": ["C:/Entwicklung/Rhino-MCP/src/mcp-server/server.py"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
Nach der Installation kann Claude direkt mit Rhino kommunizieren:
|
||||||
|
|
||||||
|
```
|
||||||
|
"Erstelle einen Würfel mit 10mm Kantenlänge"
|
||||||
|
"Öffne die Grasshopper-Definition und setze den Slider 'Breite' auf 500"
|
||||||
|
"Exportiere die aktuelle Geometrie als STEP-Datei"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
RhinoMCP/
|
||||||
|
├── src/
|
||||||
|
│ ├── mcp-server/ # Python MCP-Server
|
||||||
|
│ │ ├── server.py # Hauptserver (stdio + SSE)
|
||||||
|
│ │ ├── tools/ # Tool-Implementierungen
|
||||||
|
│ │ └── requirements.txt
|
||||||
|
│ └── rhino-plugin/ # C# Rhino-Plugin
|
||||||
|
│ └── RhinoMCP/
|
||||||
|
│ ├── RhinoMCP.csproj
|
||||||
|
│ ├── Plugin.cs
|
||||||
|
│ └── HttpServer.cs
|
||||||
|
├── docs/ # Dokumentation
|
||||||
|
├── examples/ # Beispiel-Definitionen
|
||||||
|
├── LICENSE
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT License - siehe [LICENSE](LICENSE)
|
||||||
|
|
||||||
|
## Mitwirken
|
||||||
|
|
||||||
|
Beiträge sind willkommen! Bitte erstelle einen Issue oder Pull Request.
|
||||||
5
src/mcp-server/requirements.txt
Normal file
5
src/mcp-server/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mcp>=1.0.0
|
||||||
|
httpx>=0.25.0
|
||||||
|
pydantic>=2.0.0
|
||||||
|
uvicorn>=0.24.0
|
||||||
|
starlette>=0.32.0
|
||||||
705
src/mcp-server/server.py
Normal file
705
src/mcp-server/server.py
Normal file
@@ -0,0 +1,705 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
RhinoMCP - MCP Server for Rhino 3D and Grasshopper integration.
|
||||||
|
|
||||||
|
This server provides tools for Claude to interact with Rhino and Grasshopper
|
||||||
|
via the Model Context Protocol (MCP).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
from mcp.types import Tool, TextContent
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("rhino-mcp")
|
||||||
|
|
||||||
|
# Rhino Plugin HTTP endpoint
|
||||||
|
RHINO_ENDPOINT = "http://localhost:9000"
|
||||||
|
|
||||||
|
# Create MCP server instance
|
||||||
|
server = Server("rhino-mcp")
|
||||||
|
|
||||||
|
|
||||||
|
async def call_rhino(action: str, params: dict[str, Any] = None) -> dict[str, Any]:
|
||||||
|
"""Send a command to the Rhino plugin and return the response."""
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(
|
||||||
|
f"{RHINO_ENDPOINT}/execute",
|
||||||
|
json={"action": action, "params": params or {}}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except httpx.ConnectError:
|
||||||
|
return {"error": "Cannot connect to Rhino. Is the RhinoMCP plugin running?"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def list_tools() -> list[Tool]:
|
||||||
|
"""Return the list of available tools."""
|
||||||
|
return [
|
||||||
|
# === Rhino Document Tools ===
|
||||||
|
Tool(
|
||||||
|
name="rhino_get_document_info",
|
||||||
|
description="Get information about the current Rhino document (file path, units, layers, object count)",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_get_selected_objects",
|
||||||
|
description="Get information about currently selected objects in Rhino",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_get_layers",
|
||||||
|
description="Get all layers in the current Rhino document",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
# === Geometry Creation Tools ===
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_point",
|
||||||
|
description="Create a point in Rhino at the specified coordinates",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number", "description": "X coordinate"},
|
||||||
|
"y": {"type": "number", "description": "Y coordinate"},
|
||||||
|
"z": {"type": "number", "description": "Z coordinate", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_line",
|
||||||
|
description="Create a line between two points",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"start": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["start", "end"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_polyline",
|
||||||
|
description="Create a polyline from a list of points",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"points": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"description": "List of points defining the polyline"
|
||||||
|
},
|
||||||
|
"closed": {"type": "boolean", "default": False, "description": "Whether to close the polyline"}
|
||||||
|
},
|
||||||
|
"required": ["points"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_circle",
|
||||||
|
description="Create a circle with center point and radius",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"center": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"radius": {"type": "number", "description": "Circle radius"}
|
||||||
|
},
|
||||||
|
"required": ["center", "radius"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_rectangle",
|
||||||
|
description="Create a rectangle from corner point, width and height",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"corner": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"width": {"type": "number", "description": "Rectangle width"},
|
||||||
|
"height": {"type": "number", "description": "Rectangle height"}
|
||||||
|
},
|
||||||
|
"required": ["corner", "width", "height"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_sphere",
|
||||||
|
description="Create a sphere with center point and radius",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"center": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"radius": {"type": "number", "description": "Sphere radius"}
|
||||||
|
},
|
||||||
|
"required": ["center", "radius"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_box",
|
||||||
|
description="Create a box (cuboid) from corner point and dimensions",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"corner": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"width": {"type": "number", "description": "Box width (X)"},
|
||||||
|
"depth": {"type": "number", "description": "Box depth (Y)"},
|
||||||
|
"height": {"type": "number", "description": "Box height (Z)"}
|
||||||
|
},
|
||||||
|
"required": ["corner", "width", "depth", "height"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_cylinder",
|
||||||
|
description="Create a cylinder from base center, radius and height",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"base_center": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"radius": {"type": "number", "description": "Cylinder radius"},
|
||||||
|
"height": {"type": "number", "description": "Cylinder height"}
|
||||||
|
},
|
||||||
|
"required": ["base_center", "radius", "height"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_create_surface_from_curves",
|
||||||
|
description="Create a surface from boundary curves (planar surface or loft)",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"curve_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "List of curve GUIDs to create surface from"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["planar", "loft"],
|
||||||
|
"default": "planar",
|
||||||
|
"description": "Surface creation method"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["curve_ids"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_extrude_curve",
|
||||||
|
description="Extrude a curve along a direction to create a surface or solid",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"curve_id": {"type": "string", "description": "GUID of the curve to extrude"},
|
||||||
|
"direction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number", "default": 0},
|
||||||
|
"y": {"type": "number", "default": 0},
|
||||||
|
"z": {"type": "number", "default": 1}
|
||||||
|
},
|
||||||
|
"description": "Extrusion direction vector"
|
||||||
|
},
|
||||||
|
"distance": {"type": "number", "description": "Extrusion distance"},
|
||||||
|
"cap": {"type": "boolean", "default": True, "description": "Cap the ends to create a solid"}
|
||||||
|
},
|
||||||
|
"required": ["curve_id", "distance"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
# === Object Manipulation Tools ===
|
||||||
|
Tool(
|
||||||
|
name="rhino_move_object",
|
||||||
|
description="Move an object by a translation vector",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_id": {"type": "string", "description": "GUID of the object to move"},
|
||||||
|
"translation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["object_id", "translation"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_copy_object",
|
||||||
|
description="Copy an object with optional translation",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_id": {"type": "string", "description": "GUID of the object to copy"},
|
||||||
|
"translation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number", "default": 0},
|
||||||
|
"y": {"type": "number", "default": 0},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"description": "Optional translation for the copy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["object_id"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_rotate_object",
|
||||||
|
description="Rotate an object around an axis",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_id": {"type": "string", "description": "GUID of the object to rotate"},
|
||||||
|
"angle_degrees": {"type": "number", "description": "Rotation angle in degrees"},
|
||||||
|
"axis": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["x", "y", "z"],
|
||||||
|
"default": "z",
|
||||||
|
"description": "Rotation axis"
|
||||||
|
},
|
||||||
|
"center": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number", "default": 0},
|
||||||
|
"y": {"type": "number", "default": 0},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"description": "Rotation center point"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["object_id", "angle_degrees"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_scale_object",
|
||||||
|
description="Scale an object from a base point",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_id": {"type": "string", "description": "GUID of the object to scale"},
|
||||||
|
"scale_factor": {"type": "number", "description": "Uniform scale factor"},
|
||||||
|
"base_point": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number", "default": 0},
|
||||||
|
"y": {"type": "number", "default": 0},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"description": "Base point for scaling"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["object_id", "scale_factor"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_delete_object",
|
||||||
|
description="Delete an object by its GUID",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_id": {"type": "string", "description": "GUID of the object to delete"}
|
||||||
|
},
|
||||||
|
"required": ["object_id"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_set_layer",
|
||||||
|
description="Move an object to a specific layer",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_id": {"type": "string", "description": "GUID of the object"},
|
||||||
|
"layer_name": {"type": "string", "description": "Name of the target layer"}
|
||||||
|
},
|
||||||
|
"required": ["object_id", "layer_name"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
# === Analysis Tools ===
|
||||||
|
Tool(
|
||||||
|
name="rhino_get_object_info",
|
||||||
|
description="Get detailed information about an object (type, dimensions, area, volume)",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_id": {"type": "string", "description": "GUID of the object"}
|
||||||
|
},
|
||||||
|
"required": ["object_id"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_measure_distance",
|
||||||
|
description="Measure distance between two points",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"point1": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
},
|
||||||
|
"point2": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"},
|
||||||
|
"z": {"type": "number", "default": 0}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["point1", "point2"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
# === Boolean Operations ===
|
||||||
|
Tool(
|
||||||
|
name="rhino_boolean_union",
|
||||||
|
description="Create a boolean union of multiple objects",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "List of object GUIDs to union"
|
||||||
|
},
|
||||||
|
"delete_input": {"type": "boolean", "default": True, "description": "Delete input objects after union"}
|
||||||
|
},
|
||||||
|
"required": ["object_ids"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_boolean_difference",
|
||||||
|
description="Subtract one or more objects from a base object",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"base_id": {"type": "string", "description": "GUID of the base object"},
|
||||||
|
"subtract_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "List of object GUIDs to subtract"
|
||||||
|
},
|
||||||
|
"delete_input": {"type": "boolean", "default": True, "description": "Delete input objects after operation"}
|
||||||
|
},
|
||||||
|
"required": ["base_id", "subtract_ids"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="rhino_boolean_intersection",
|
||||||
|
description="Create a boolean intersection of objects",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"object_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "List of object GUIDs to intersect"
|
||||||
|
},
|
||||||
|
"delete_input": {"type": "boolean", "default": True, "description": "Delete input objects after operation"}
|
||||||
|
},
|
||||||
|
"required": ["object_ids"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
# === Export Tools ===
|
||||||
|
Tool(
|
||||||
|
name="rhino_export",
|
||||||
|
description="Export geometry to a file",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_path": {"type": "string", "description": "Output file path"},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["3dm", "step", "iges", "stl", "obj", "fbx", "dxf"],
|
||||||
|
"description": "Export format"
|
||||||
|
},
|
||||||
|
"object_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Optional: specific objects to export (empty = all)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["file_path", "format"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
# === Grasshopper Tools ===
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_open_definition",
|
||||||
|
description="Open a Grasshopper definition file",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_path": {"type": "string", "description": "Path to the .gh or .ghx file"}
|
||||||
|
},
|
||||||
|
"required": ["file_path"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_get_definition_info",
|
||||||
|
description="Get information about the current Grasshopper definition (components, parameters, connections)",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_set_slider_value",
|
||||||
|
description="Set the value of a number slider in Grasshopper",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"slider_name": {"type": "string", "description": "Name or nickname of the slider"},
|
||||||
|
"value": {"type": "number", "description": "New value for the slider"}
|
||||||
|
},
|
||||||
|
"required": ["slider_name", "value"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_set_toggle_value",
|
||||||
|
description="Set the value of a boolean toggle in Grasshopper",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"toggle_name": {"type": "string", "description": "Name or nickname of the toggle"},
|
||||||
|
"value": {"type": "boolean", "description": "New value for the toggle"}
|
||||||
|
},
|
||||||
|
"required": ["toggle_name", "value"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_set_panel_text",
|
||||||
|
description="Set the text content of a panel in Grasshopper",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"panel_name": {"type": "string", "description": "Name or nickname of the panel"},
|
||||||
|
"text": {"type": "string", "description": "New text content"}
|
||||||
|
},
|
||||||
|
"required": ["panel_name", "text"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_add_component",
|
||||||
|
description="Add a component to the Grasshopper canvas",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"component_name": {"type": "string", "description": "Name of the component (e.g., 'Circle', 'Move', 'Extrude')"},
|
||||||
|
"position": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number", "description": "X position on canvas"},
|
||||||
|
"y": {"type": "number", "description": "Y position on canvas"}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["component_name", "position"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_connect_components",
|
||||||
|
description="Connect two components in Grasshopper",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"source_component": {"type": "string", "description": "Name/ID of source component"},
|
||||||
|
"source_output": {"type": "string", "description": "Name of output parameter"},
|
||||||
|
"target_component": {"type": "string", "description": "Name/ID of target component"},
|
||||||
|
"target_input": {"type": "string", "description": "Name of input parameter"}
|
||||||
|
},
|
||||||
|
"required": ["source_component", "source_output", "target_component", "target_input"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_add_slider",
|
||||||
|
description="Add a number slider to the Grasshopper canvas",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string", "description": "Nickname for the slider"},
|
||||||
|
"min_value": {"type": "number", "description": "Minimum value"},
|
||||||
|
"max_value": {"type": "number", "description": "Maximum value"},
|
||||||
|
"current_value": {"type": "number", "description": "Current/default value"},
|
||||||
|
"position": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {"type": "number"},
|
||||||
|
"y": {"type": "number"}
|
||||||
|
},
|
||||||
|
"required": ["x", "y"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "min_value", "max_value", "current_value", "position"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_bake",
|
||||||
|
description="Bake Grasshopper geometry to Rhino",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"component_name": {"type": "string", "description": "Name of the component to bake (optional, bakes all if not specified)"},
|
||||||
|
"layer_name": {"type": "string", "description": "Target layer for baked geometry"}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_save_definition",
|
||||||
|
description="Save the current Grasshopper definition",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_path": {"type": "string", "description": "Path to save the file (optional, saves to current if not specified)"}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="grasshopper_recompute",
|
||||||
|
description="Force recompute of the Grasshopper definition",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
# === Rhino Commands ===
|
||||||
|
Tool(
|
||||||
|
name="rhino_run_command",
|
||||||
|
description="Run a Rhino command by name (advanced usage)",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {"type": "string", "description": "Rhino command string (e.g., '_Zoom _All')"}
|
||||||
|
},
|
||||||
|
"required": ["command"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
||||||
|
"""Handle tool calls by forwarding them to Rhino."""
|
||||||
|
logger.info(f"Tool called: {name} with arguments: {arguments}")
|
||||||
|
|
||||||
|
# Forward the tool call to Rhino
|
||||||
|
result = await call_rhino(name, arguments)
|
||||||
|
|
||||||
|
return [TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Run the MCP server."""
|
||||||
|
logger.info("Starting RhinoMCP server...")
|
||||||
|
|
||||||
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(
|
||||||
|
read_stream,
|
||||||
|
write_stream,
|
||||||
|
server.create_initialization_options()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
18
src/rhino-plugin/RhinoMCP.sln
Normal file
18
src/rhino-plugin/RhinoMCP.sln
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhinoMCP", "RhinoMCP\RhinoMCP.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
660
src/rhino-plugin/RhinoMCP/CommandHandler.cs
Normal file
660
src/rhino-plugin/RhinoMCP/CommandHandler.cs
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Rhino;
|
||||||
|
using Rhino.DocObjects;
|
||||||
|
using Rhino.Geometry;
|
||||||
|
|
||||||
|
namespace RhinoMCP
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles all commands from the MCP Server and executes them in Rhino.
|
||||||
|
/// </summary>
|
||||||
|
public static class CommandHandler
|
||||||
|
{
|
||||||
|
public static object Execute(string action, JObject parameters)
|
||||||
|
{
|
||||||
|
var doc = RhinoDoc.ActiveDoc;
|
||||||
|
if (doc == null && !action.StartsWith("rhino_get"))
|
||||||
|
return new { error = "No active Rhino document" };
|
||||||
|
|
||||||
|
return action switch
|
||||||
|
{
|
||||||
|
// Document Info
|
||||||
|
"rhino_get_document_info" => GetDocumentInfo(doc),
|
||||||
|
"rhino_get_selected_objects" => GetSelectedObjects(doc),
|
||||||
|
"rhino_get_layers" => GetLayers(doc),
|
||||||
|
|
||||||
|
// Geometry Creation
|
||||||
|
"rhino_create_point" => CreatePoint(doc, parameters),
|
||||||
|
"rhino_create_line" => CreateLine(doc, parameters),
|
||||||
|
"rhino_create_polyline" => CreatePolyline(doc, parameters),
|
||||||
|
"rhino_create_circle" => CreateCircle(doc, parameters),
|
||||||
|
"rhino_create_rectangle" => CreateRectangle(doc, parameters),
|
||||||
|
"rhino_create_sphere" => CreateSphere(doc, parameters),
|
||||||
|
"rhino_create_box" => CreateBox(doc, parameters),
|
||||||
|
"rhino_create_cylinder" => CreateCylinder(doc, parameters),
|
||||||
|
"rhino_create_surface_from_curves" => CreateSurfaceFromCurves(doc, parameters),
|
||||||
|
"rhino_extrude_curve" => ExtrudeCurve(doc, parameters),
|
||||||
|
|
||||||
|
// Object Manipulation
|
||||||
|
"rhino_move_object" => MoveObject(doc, parameters),
|
||||||
|
"rhino_copy_object" => CopyObject(doc, parameters),
|
||||||
|
"rhino_rotate_object" => RotateObject(doc, parameters),
|
||||||
|
"rhino_scale_object" => ScaleObject(doc, parameters),
|
||||||
|
"rhino_delete_object" => DeleteObject(doc, parameters),
|
||||||
|
"rhino_set_layer" => SetObjectLayer(doc, parameters),
|
||||||
|
|
||||||
|
// Analysis
|
||||||
|
"rhino_get_object_info" => GetObjectInfo(doc, parameters),
|
||||||
|
"rhino_measure_distance" => MeasureDistance(parameters),
|
||||||
|
|
||||||
|
// Boolean Operations
|
||||||
|
"rhino_boolean_union" => BooleanUnion(doc, parameters),
|
||||||
|
"rhino_boolean_difference" => BooleanDifference(doc, parameters),
|
||||||
|
"rhino_boolean_intersection" => BooleanIntersection(doc, parameters),
|
||||||
|
|
||||||
|
// Export
|
||||||
|
"rhino_export" => ExportGeometry(doc, parameters),
|
||||||
|
|
||||||
|
// Rhino Commands
|
||||||
|
"rhino_run_command" => RunCommand(parameters),
|
||||||
|
|
||||||
|
// Grasshopper (delegated to GrasshopperHandler)
|
||||||
|
var gh when gh.StartsWith("grasshopper_") => GrasshopperHandler.Execute(action, parameters),
|
||||||
|
|
||||||
|
_ => new { error = $"Unknown action: {action}" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Document Info
|
||||||
|
|
||||||
|
private static object GetDocumentInfo(RhinoDoc? doc)
|
||||||
|
{
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active document" };
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
file_path = doc.Path ?? "(unsaved)",
|
||||||
|
name = doc.Name ?? "Untitled",
|
||||||
|
units = doc.ModelUnitSystem.ToString(),
|
||||||
|
object_count = doc.Objects.Count,
|
||||||
|
layer_count = doc.Layers.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetSelectedObjects(RhinoDoc? doc)
|
||||||
|
{
|
||||||
|
if (doc == null)
|
||||||
|
return new { objects = Array.Empty<object>() };
|
||||||
|
|
||||||
|
var selected = doc.Objects.GetSelectedObjects(false, false);
|
||||||
|
var objects = selected.Select(obj => new
|
||||||
|
{
|
||||||
|
id = obj.Id.ToString(),
|
||||||
|
type = obj.ObjectType.ToString(),
|
||||||
|
name = obj.Name ?? "",
|
||||||
|
layer = doc.Layers[obj.Attributes.LayerIndex].Name
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new { objects, count = objects.Count };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetLayers(RhinoDoc? doc)
|
||||||
|
{
|
||||||
|
if (doc == null)
|
||||||
|
return new { layers = Array.Empty<object>() };
|
||||||
|
|
||||||
|
var layers = doc.Layers.Select(l => new
|
||||||
|
{
|
||||||
|
name = l.Name,
|
||||||
|
full_path = l.FullPath,
|
||||||
|
visible = l.IsVisible,
|
||||||
|
locked = l.IsLocked,
|
||||||
|
color = $"#{l.Color.R:X2}{l.Color.G:X2}{l.Color.B:X2}"
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new { layers };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Geometry Creation
|
||||||
|
|
||||||
|
private static Point3d ParsePoint(JObject? p)
|
||||||
|
{
|
||||||
|
if (p == null) return Point3d.Origin;
|
||||||
|
return new Point3d(
|
||||||
|
p["x"]?.Value<double>() ?? 0,
|
||||||
|
p["y"]?.Value<double>() ?? 0,
|
||||||
|
p["z"]?.Value<double>() ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3d ParseVector(JObject? v)
|
||||||
|
{
|
||||||
|
if (v == null) return Vector3d.ZAxis;
|
||||||
|
return new Vector3d(
|
||||||
|
v["x"]?.Value<double>() ?? 0,
|
||||||
|
v["y"]?.Value<double>() ?? 0,
|
||||||
|
v["z"]?.Value<double>() ?? 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreatePoint(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var pt = new Point3d(
|
||||||
|
p["x"]?.Value<double>() ?? 0,
|
||||||
|
p["y"]?.Value<double>() ?? 0,
|
||||||
|
p["z"]?.Value<double>() ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
|
var id = doc.Objects.AddPoint(pt);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString(), point = new { pt.X, pt.Y, pt.Z } };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateLine(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var start = ParsePoint(p["start"] as JObject);
|
||||||
|
var end = ParsePoint(p["end"] as JObject);
|
||||||
|
var line = new Line(start, end);
|
||||||
|
|
||||||
|
var id = doc.Objects.AddLine(line);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString(), length = line.Length };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreatePolyline(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var points = (p["points"] as JArray)?.Select(pt => ParsePoint(pt as JObject)).ToList();
|
||||||
|
if (points == null || points.Count < 2)
|
||||||
|
return new { error = "At least 2 points required" };
|
||||||
|
|
||||||
|
var closed = p["closed"]?.Value<bool>() ?? false;
|
||||||
|
if (closed && points.First() != points.Last())
|
||||||
|
points.Add(points.First());
|
||||||
|
|
||||||
|
var polyline = new Polyline(points);
|
||||||
|
var id = doc.Objects.AddPolyline(polyline);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString(), point_count = points.Count };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateCircle(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var center = ParsePoint(p["center"] as JObject);
|
||||||
|
var radius = p["radius"]?.Value<double>() ?? 1;
|
||||||
|
|
||||||
|
var circle = new Circle(center, radius);
|
||||||
|
var id = doc.Objects.AddCircle(circle);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString(), radius, circumference = circle.Circumference };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateRectangle(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var corner = ParsePoint(p["corner"] as JObject);
|
||||||
|
var width = p["width"]?.Value<double>() ?? 1;
|
||||||
|
var height = p["height"]?.Value<double>() ?? 1;
|
||||||
|
|
||||||
|
var plane = new Plane(corner, Vector3d.ZAxis);
|
||||||
|
var rect = new Rectangle3d(plane, width, height);
|
||||||
|
var id = doc.Objects.AddRectangle(rect);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString(), width, height, area = width * height };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateSphere(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var center = ParsePoint(p["center"] as JObject);
|
||||||
|
var radius = p["radius"]?.Value<double>() ?? 1;
|
||||||
|
|
||||||
|
var sphere = new Sphere(center, radius);
|
||||||
|
var id = doc.Objects.AddSphere(sphere);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString(), radius, volume = sphere.Volume() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateBox(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var corner = ParsePoint(p["corner"] as JObject);
|
||||||
|
var width = p["width"]?.Value<double>() ?? 1;
|
||||||
|
var depth = p["depth"]?.Value<double>() ?? 1;
|
||||||
|
var height = p["height"]?.Value<double>() ?? 1;
|
||||||
|
|
||||||
|
var plane = new Plane(corner, Vector3d.ZAxis);
|
||||||
|
var interval_x = new Interval(0, width);
|
||||||
|
var interval_y = new Interval(0, depth);
|
||||||
|
var interval_z = new Interval(0, height);
|
||||||
|
var box = new Box(plane, interval_x, interval_y, interval_z);
|
||||||
|
|
||||||
|
var brep = box.ToBrep();
|
||||||
|
var id = doc.Objects.AddBrep(brep);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString(), width, depth, height, volume = width * depth * height };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateCylinder(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var baseCenter = ParsePoint(p["base_center"] as JObject);
|
||||||
|
var radius = p["radius"]?.Value<double>() ?? 1;
|
||||||
|
var height = p["height"]?.Value<double>() ?? 1;
|
||||||
|
|
||||||
|
var plane = new Plane(baseCenter, Vector3d.ZAxis);
|
||||||
|
var circle = new Circle(plane, radius);
|
||||||
|
var cylinder = new Cylinder(circle, height);
|
||||||
|
|
||||||
|
var brep = cylinder.ToBrep(true, true);
|
||||||
|
var id = doc.Objects.AddBrep(brep);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
var volume = Math.PI * radius * radius * height;
|
||||||
|
return new { success = true, id = id.ToString(), radius, height, volume };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateSurfaceFromCurves(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var curveIds = (p["curve_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
|
||||||
|
if (curveIds == null || curveIds.Count == 0)
|
||||||
|
return new { error = "No curve IDs provided" };
|
||||||
|
|
||||||
|
var curves = curveIds.Select(id => doc.Objects.Find(id)?.Geometry as Curve).Where(c => c != null).ToList();
|
||||||
|
if (curves.Count == 0)
|
||||||
|
return new { error = "No valid curves found" };
|
||||||
|
|
||||||
|
var method = p["method"]?.ToString() ?? "planar";
|
||||||
|
|
||||||
|
Brep[]? breps = null;
|
||||||
|
if (method == "planar")
|
||||||
|
{
|
||||||
|
breps = Brep.CreatePlanarBreps(curves!, RhinoDoc.ActiveDoc.ModelAbsoluteTolerance);
|
||||||
|
}
|
||||||
|
else if (method == "loft")
|
||||||
|
{
|
||||||
|
var loftBreps = Brep.CreateFromLoft(curves!, Point3d.Unset, Point3d.Unset, LoftType.Normal, false);
|
||||||
|
breps = loftBreps;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breps == null || breps.Length == 0)
|
||||||
|
return new { error = "Failed to create surface" };
|
||||||
|
|
||||||
|
var ids = breps.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, ids, count = ids.Count };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ExtrudeCurve(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var curveId = Guid.Parse(p["curve_id"]?.ToString() ?? "");
|
||||||
|
var curve = doc.Objects.Find(curveId)?.Geometry as Curve;
|
||||||
|
if (curve == null)
|
||||||
|
return new { error = "Curve not found" };
|
||||||
|
|
||||||
|
var direction = ParseVector(p["direction"] as JObject);
|
||||||
|
var distance = p["distance"]?.Value<double>() ?? 1;
|
||||||
|
var cap = p["cap"]?.Value<bool>() ?? true;
|
||||||
|
|
||||||
|
var extrudeDir = direction * distance;
|
||||||
|
var surface = Surface.CreateExtrusion(curve, extrudeDir);
|
||||||
|
if (surface == null)
|
||||||
|
return new { error = "Failed to create extrusion" };
|
||||||
|
|
||||||
|
var brep = surface.ToBrep();
|
||||||
|
if (cap && curve.IsClosed)
|
||||||
|
{
|
||||||
|
brep = brep.CapPlanarHoles(doc.ModelAbsoluteTolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = doc.Objects.AddBrep(brep);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = id.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Object Manipulation
|
||||||
|
|
||||||
|
private static object MoveObject(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
|
||||||
|
var translation = ParseVector(p["translation"] as JObject);
|
||||||
|
|
||||||
|
var obj = doc.Objects.Find(objId);
|
||||||
|
if (obj == null)
|
||||||
|
return new { error = "Object not found" };
|
||||||
|
|
||||||
|
var xform = Transform.Translation(translation);
|
||||||
|
doc.Objects.Transform(objId, xform, true);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = objId.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CopyObject(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
|
||||||
|
var translation = ParseVector(p["translation"] as JObject);
|
||||||
|
|
||||||
|
var obj = doc.Objects.Find(objId);
|
||||||
|
if (obj == null)
|
||||||
|
return new { error = "Object not found" };
|
||||||
|
|
||||||
|
var xform = Transform.Translation(translation);
|
||||||
|
var newId = doc.Objects.Transform(objId, xform, false);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, original_id = objId.ToString(), new_id = newId.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object RotateObject(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
|
||||||
|
var angleDegrees = p["angle_degrees"]?.Value<double>() ?? 0;
|
||||||
|
var axis = p["axis"]?.ToString() ?? "z";
|
||||||
|
var center = ParsePoint(p["center"] as JObject);
|
||||||
|
|
||||||
|
var obj = doc.Objects.Find(objId);
|
||||||
|
if (obj == null)
|
||||||
|
return new { error = "Object not found" };
|
||||||
|
|
||||||
|
var axisVector = axis.ToLower() switch
|
||||||
|
{
|
||||||
|
"x" => Vector3d.XAxis,
|
||||||
|
"y" => Vector3d.YAxis,
|
||||||
|
_ => Vector3d.ZAxis
|
||||||
|
};
|
||||||
|
|
||||||
|
var angleRadians = angleDegrees * Math.PI / 180.0;
|
||||||
|
var xform = Transform.Rotation(angleRadians, axisVector, center);
|
||||||
|
doc.Objects.Transform(objId, xform, true);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = objId.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ScaleObject(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
|
||||||
|
var scaleFactor = p["scale_factor"]?.Value<double>() ?? 1;
|
||||||
|
var basePoint = ParsePoint(p["base_point"] as JObject);
|
||||||
|
|
||||||
|
var obj = doc.Objects.Find(objId);
|
||||||
|
if (obj == null)
|
||||||
|
return new { error = "Object not found" };
|
||||||
|
|
||||||
|
var xform = Transform.Scale(basePoint, scaleFactor);
|
||||||
|
doc.Objects.Transform(objId, xform, true);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = objId.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object DeleteObject(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
|
||||||
|
var success = doc.Objects.Delete(objId, true);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success, id = objId.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object SetObjectLayer(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
|
||||||
|
var layerName = p["layer_name"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
var obj = doc.Objects.Find(objId);
|
||||||
|
if (obj == null)
|
||||||
|
return new { error = "Object not found" };
|
||||||
|
|
||||||
|
var layerIndex = doc.Layers.FindByFullPath(layerName, -1);
|
||||||
|
if (layerIndex < 0)
|
||||||
|
layerIndex = doc.Layers.Add(layerName, System.Drawing.Color.Black);
|
||||||
|
|
||||||
|
var attributes = obj.Attributes.Duplicate();
|
||||||
|
attributes.LayerIndex = layerIndex;
|
||||||
|
doc.Objects.ModifyAttributes(objId, attributes, true);
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, id = objId.ToString(), layer = layerName };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Analysis
|
||||||
|
|
||||||
|
private static object GetObjectInfo(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
|
||||||
|
var obj = doc.Objects.Find(objId);
|
||||||
|
if (obj == null)
|
||||||
|
return new { error = "Object not found" };
|
||||||
|
|
||||||
|
var info = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["id"] = objId.ToString(),
|
||||||
|
["type"] = obj.ObjectType.ToString(),
|
||||||
|
["name"] = obj.Name ?? "",
|
||||||
|
["layer"] = doc.Layers[obj.Attributes.LayerIndex].Name
|
||||||
|
};
|
||||||
|
|
||||||
|
var geometry = obj.Geometry;
|
||||||
|
var bbox = geometry.GetBoundingBox(true);
|
||||||
|
info["bounding_box"] = new
|
||||||
|
{
|
||||||
|
min = new { bbox.Min.X, bbox.Min.Y, bbox.Min.Z },
|
||||||
|
max = new { bbox.Max.X, bbox.Max.Y, bbox.Max.Z },
|
||||||
|
diagonal = bbox.Diagonal.Length
|
||||||
|
};
|
||||||
|
|
||||||
|
if (geometry is Curve curve)
|
||||||
|
{
|
||||||
|
info["length"] = curve.GetLength();
|
||||||
|
info["is_closed"] = curve.IsClosed;
|
||||||
|
}
|
||||||
|
else if (geometry is Brep brep)
|
||||||
|
{
|
||||||
|
var area = AreaMassProperties.Compute(brep);
|
||||||
|
var volume = VolumeMassProperties.Compute(brep);
|
||||||
|
if (area != null) info["area"] = area.Area;
|
||||||
|
if (volume != null) info["volume"] = volume.Volume;
|
||||||
|
}
|
||||||
|
else if (geometry is Surface surface)
|
||||||
|
{
|
||||||
|
var area = AreaMassProperties.Compute(surface);
|
||||||
|
if (area != null) info["area"] = area.Area;
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object MeasureDistance(JObject p)
|
||||||
|
{
|
||||||
|
var pt1 = ParsePoint(p["point1"] as JObject);
|
||||||
|
var pt2 = ParsePoint(p["point2"] as JObject);
|
||||||
|
var distance = pt1.DistanceTo(pt2);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
distance,
|
||||||
|
point1 = new { pt1.X, pt1.Y, pt1.Z },
|
||||||
|
point2 = new { pt2.X, pt2.Y, pt2.Z }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Boolean Operations
|
||||||
|
|
||||||
|
private static object BooleanUnion(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objectIds = (p["object_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
|
||||||
|
var deleteInput = p["delete_input"]?.Value<bool>() ?? true;
|
||||||
|
|
||||||
|
if (objectIds == null || objectIds.Count < 2)
|
||||||
|
return new { error = "At least 2 objects required for union" };
|
||||||
|
|
||||||
|
var breps = objectIds
|
||||||
|
.Select(id => doc.Objects.Find(id)?.Geometry as Brep)
|
||||||
|
.Where(b => b != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (breps.Count < 2)
|
||||||
|
return new { error = "At least 2 valid breps required" };
|
||||||
|
|
||||||
|
var result = Brep.CreateBooleanUnion(breps!, doc.ModelAbsoluteTolerance);
|
||||||
|
if (result == null || result.Length == 0)
|
||||||
|
return new { error = "Boolean union failed" };
|
||||||
|
|
||||||
|
if (deleteInput)
|
||||||
|
{
|
||||||
|
foreach (var id in objectIds)
|
||||||
|
doc.Objects.Delete(id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIds = result.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, ids = newIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object BooleanDifference(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var baseId = Guid.Parse(p["base_id"]?.ToString() ?? "");
|
||||||
|
var subtractIds = (p["subtract_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
|
||||||
|
var deleteInput = p["delete_input"]?.Value<bool>() ?? true;
|
||||||
|
|
||||||
|
if (subtractIds == null || subtractIds.Count == 0)
|
||||||
|
return new { error = "No objects to subtract" };
|
||||||
|
|
||||||
|
var baseBrep = doc.Objects.Find(baseId)?.Geometry as Brep;
|
||||||
|
if (baseBrep == null)
|
||||||
|
return new { error = "Base object not found or not a brep" };
|
||||||
|
|
||||||
|
var subtractBreps = subtractIds
|
||||||
|
.Select(id => doc.Objects.Find(id)?.Geometry as Brep)
|
||||||
|
.Where(b => b != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var result = Brep.CreateBooleanDifference(baseBrep, subtractBreps!, doc.ModelAbsoluteTolerance);
|
||||||
|
if (result == null || result.Length == 0)
|
||||||
|
return new { error = "Boolean difference failed" };
|
||||||
|
|
||||||
|
if (deleteInput)
|
||||||
|
{
|
||||||
|
doc.Objects.Delete(baseId, true);
|
||||||
|
foreach (var id in subtractIds)
|
||||||
|
doc.Objects.Delete(id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIds = result.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, ids = newIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object BooleanIntersection(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var objectIds = (p["object_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
|
||||||
|
var deleteInput = p["delete_input"]?.Value<bool>() ?? true;
|
||||||
|
|
||||||
|
if (objectIds == null || objectIds.Count < 2)
|
||||||
|
return new { error = "At least 2 objects required for intersection" };
|
||||||
|
|
||||||
|
var breps = objectIds
|
||||||
|
.Select(id => doc.Objects.Find(id)?.Geometry as Brep)
|
||||||
|
.Where(b => b != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (breps.Count < 2)
|
||||||
|
return new { error = "At least 2 valid breps required" };
|
||||||
|
|
||||||
|
var result = Brep.CreateBooleanIntersection(breps![0], breps[1], doc.ModelAbsoluteTolerance);
|
||||||
|
if (result == null || result.Length == 0)
|
||||||
|
return new { error = "Boolean intersection failed" };
|
||||||
|
|
||||||
|
if (deleteInput)
|
||||||
|
{
|
||||||
|
foreach (var id in objectIds)
|
||||||
|
doc.Objects.Delete(id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIds = result.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
|
||||||
|
doc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, ids = newIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Export
|
||||||
|
|
||||||
|
private static object ExportGeometry(RhinoDoc doc, JObject p)
|
||||||
|
{
|
||||||
|
var filePath = p["file_path"]?.ToString() ?? "";
|
||||||
|
var format = p["format"]?.ToString() ?? "3dm";
|
||||||
|
var objectIds = (p["object_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(filePath))
|
||||||
|
return new { error = "File path required" };
|
||||||
|
|
||||||
|
// Select objects for export if specified
|
||||||
|
if (objectIds != null && objectIds.Count > 0)
|
||||||
|
{
|
||||||
|
doc.Objects.UnselectAll();
|
||||||
|
foreach (var id in objectIds)
|
||||||
|
{
|
||||||
|
var obj = doc.Objects.Find(id);
|
||||||
|
if (obj != null)
|
||||||
|
obj.Select(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var command = format.ToLower() switch
|
||||||
|
{
|
||||||
|
"3dm" => $"_-SaveAs \"{filePath}\"",
|
||||||
|
"step" => $"_-Export \"{filePath}\" _Enter",
|
||||||
|
"iges" => $"_-Export \"{filePath}\" _Enter",
|
||||||
|
"stl" => $"_-Export \"{filePath}\" _Enter",
|
||||||
|
"obj" => $"_-Export \"{filePath}\" _Enter",
|
||||||
|
"fbx" => $"_-Export \"{filePath}\" _Enter",
|
||||||
|
"dxf" => $"_-Export \"{filePath}\" _Enter",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (command == null)
|
||||||
|
return new { error = $"Unknown format: {format}" };
|
||||||
|
|
||||||
|
var result = RhinoApp.RunScript(command, false);
|
||||||
|
return new { success = result, file_path = filePath, format };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Rhino Commands
|
||||||
|
|
||||||
|
private static object RunCommand(JObject p)
|
||||||
|
{
|
||||||
|
var command = p["command"]?.ToString() ?? "";
|
||||||
|
if (string.IsNullOrEmpty(command))
|
||||||
|
return new { error = "No command specified" };
|
||||||
|
|
||||||
|
var result = RhinoApp.RunScript(command, true);
|
||||||
|
return new { success = result, command };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
527
src/rhino-plugin/RhinoMCP/GrasshopperHandler.cs
Normal file
527
src/rhino-plugin/RhinoMCP/GrasshopperHandler.cs
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using Grasshopper;
|
||||||
|
using Grasshopper.Kernel;
|
||||||
|
using Grasshopper.Kernel.Parameters;
|
||||||
|
using Grasshopper.Kernel.Special;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Rhino;
|
||||||
|
|
||||||
|
namespace RhinoMCP
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles all Grasshopper-related commands from the MCP Server.
|
||||||
|
/// </summary>
|
||||||
|
public static class GrasshopperHandler
|
||||||
|
{
|
||||||
|
public static object Execute(string action, JObject parameters)
|
||||||
|
{
|
||||||
|
return action switch
|
||||||
|
{
|
||||||
|
"grasshopper_open_definition" => OpenDefinition(parameters),
|
||||||
|
"grasshopper_get_definition_info" => GetDefinitionInfo(),
|
||||||
|
"grasshopper_set_slider_value" => SetSliderValue(parameters),
|
||||||
|
"grasshopper_set_toggle_value" => SetToggleValue(parameters),
|
||||||
|
"grasshopper_set_panel_text" => SetPanelText(parameters),
|
||||||
|
"grasshopper_add_component" => AddComponent(parameters),
|
||||||
|
"grasshopper_connect_components" => ConnectComponents(parameters),
|
||||||
|
"grasshopper_add_slider" => AddSlider(parameters),
|
||||||
|
"grasshopper_bake" => BakeGeometry(parameters),
|
||||||
|
"grasshopper_save_definition" => SaveDefinition(parameters),
|
||||||
|
"grasshopper_recompute" => Recompute(),
|
||||||
|
_ => new { error = $"Unknown Grasshopper action: {action}" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GH_Document? GetActiveDocument()
|
||||||
|
{
|
||||||
|
var editor = Instances.ActiveCanvas?.Document;
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object OpenDefinition(JObject p)
|
||||||
|
{
|
||||||
|
var filePath = p["file_path"]?.ToString() ?? "";
|
||||||
|
if (string.IsNullOrEmpty(filePath))
|
||||||
|
return new { error = "File path required" };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure Grasshopper is open
|
||||||
|
if (Instances.ActiveCanvas == null)
|
||||||
|
{
|
||||||
|
RhinoApp.RunScript("_Grasshopper", false);
|
||||||
|
System.Threading.Thread.Sleep(1000); // Wait for GH to open
|
||||||
|
}
|
||||||
|
|
||||||
|
var io = new GH_DocumentIO();
|
||||||
|
if (!io.Open(filePath))
|
||||||
|
return new { error = $"Failed to open file: {filePath}" };
|
||||||
|
|
||||||
|
var doc = io.Document;
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "Failed to load document" };
|
||||||
|
|
||||||
|
Instances.ActiveCanvas.Document = doc;
|
||||||
|
Instances.ActiveCanvas.Refresh();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
file_path = filePath,
|
||||||
|
component_count = doc.ObjectCount,
|
||||||
|
name = doc.DisplayName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new { error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetDefinitionInfo()
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
var components = new List<object>();
|
||||||
|
var sliders = new List<object>();
|
||||||
|
var toggles = new List<object>();
|
||||||
|
var panels = new List<object>();
|
||||||
|
|
||||||
|
foreach (var obj in doc.Objects)
|
||||||
|
{
|
||||||
|
var info = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["instance_guid"] = obj.InstanceGuid.ToString(),
|
||||||
|
["name"] = obj.Name ?? "",
|
||||||
|
["nickname"] = obj.NickName ?? "",
|
||||||
|
["category"] = obj.Category ?? "",
|
||||||
|
["subcategory"] = obj.SubCategory ?? "",
|
||||||
|
["position"] = new { x = obj.Attributes.Pivot.X, y = obj.Attributes.Pivot.Y }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (obj is GH_NumberSlider slider)
|
||||||
|
{
|
||||||
|
sliders.Add(new
|
||||||
|
{
|
||||||
|
info["instance_guid"],
|
||||||
|
info["nickname"],
|
||||||
|
info["position"],
|
||||||
|
current_value = slider.CurrentValue,
|
||||||
|
min_value = (double)slider.Slider.Minimum,
|
||||||
|
max_value = (double)slider.Slider.Maximum,
|
||||||
|
type = slider.Slider.Type.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (obj is GH_BooleanToggle toggle)
|
||||||
|
{
|
||||||
|
toggles.Add(new
|
||||||
|
{
|
||||||
|
info["instance_guid"],
|
||||||
|
info["nickname"],
|
||||||
|
info["position"],
|
||||||
|
value = toggle.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (obj is GH_Panel panel)
|
||||||
|
{
|
||||||
|
panels.Add(new
|
||||||
|
{
|
||||||
|
info["instance_guid"],
|
||||||
|
info["nickname"],
|
||||||
|
info["position"],
|
||||||
|
text = panel.UserText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (obj is IGH_Component component)
|
||||||
|
{
|
||||||
|
var inputs = component.Params.Input.Select(p => new
|
||||||
|
{
|
||||||
|
name = p.Name,
|
||||||
|
nickname = p.NickName,
|
||||||
|
type = p.TypeName,
|
||||||
|
source_count = p.SourceCount
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var outputs = component.Params.Output.Select(p => new
|
||||||
|
{
|
||||||
|
name = p.Name,
|
||||||
|
nickname = p.NickName,
|
||||||
|
type = p.TypeName,
|
||||||
|
recipient_count = p.Recipients.Count
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
components.Add(new
|
||||||
|
{
|
||||||
|
info["instance_guid"],
|
||||||
|
info["name"],
|
||||||
|
info["nickname"],
|
||||||
|
info["category"],
|
||||||
|
info["subcategory"],
|
||||||
|
info["position"],
|
||||||
|
inputs,
|
||||||
|
outputs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
name = doc.DisplayName,
|
||||||
|
file_path = doc.FilePath ?? "(unsaved)",
|
||||||
|
object_count = doc.ObjectCount,
|
||||||
|
components,
|
||||||
|
sliders,
|
||||||
|
toggles,
|
||||||
|
panels
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object SetSliderValue(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
var sliderName = p["slider_name"]?.ToString() ?? "";
|
||||||
|
var value = p["value"]?.Value<double>() ?? 0;
|
||||||
|
|
||||||
|
var slider = doc.Objects
|
||||||
|
.OfType<GH_NumberSlider>()
|
||||||
|
.FirstOrDefault(s => s.NickName == sliderName || s.Name == sliderName);
|
||||||
|
|
||||||
|
if (slider == null)
|
||||||
|
return new { error = $"Slider '{sliderName}' not found" };
|
||||||
|
|
||||||
|
// Clamp value to slider range
|
||||||
|
var min = (double)slider.Slider.Minimum;
|
||||||
|
var max = (double)slider.Slider.Maximum;
|
||||||
|
value = Math.Max(min, Math.Min(max, value));
|
||||||
|
|
||||||
|
slider.SetSliderValue((decimal)value);
|
||||||
|
slider.ExpireSolution(true);
|
||||||
|
|
||||||
|
return new { success = true, slider = sliderName, value, min, max };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object SetToggleValue(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
var toggleName = p["toggle_name"]?.ToString() ?? "";
|
||||||
|
var value = p["value"]?.Value<bool>() ?? false;
|
||||||
|
|
||||||
|
var toggle = doc.Objects
|
||||||
|
.OfType<GH_BooleanToggle>()
|
||||||
|
.FirstOrDefault(t => t.NickName == toggleName || t.Name == toggleName);
|
||||||
|
|
||||||
|
if (toggle == null)
|
||||||
|
return new { error = $"Toggle '{toggleName}' not found" };
|
||||||
|
|
||||||
|
toggle.Value = value;
|
||||||
|
toggle.ExpireSolution(true);
|
||||||
|
|
||||||
|
return new { success = true, toggle = toggleName, value };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object SetPanelText(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
var panelName = p["panel_name"]?.ToString() ?? "";
|
||||||
|
var text = p["text"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
var panel = doc.Objects
|
||||||
|
.OfType<GH_Panel>()
|
||||||
|
.FirstOrDefault(pnl => pnl.NickName == panelName || pnl.Name == panelName);
|
||||||
|
|
||||||
|
if (panel == null)
|
||||||
|
return new { error = $"Panel '{panelName}' not found" };
|
||||||
|
|
||||||
|
panel.UserText = text;
|
||||||
|
panel.ExpireSolution(true);
|
||||||
|
|
||||||
|
return new { success = true, panel = panelName, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object AddComponent(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
{
|
||||||
|
// Try to open Grasshopper
|
||||||
|
RhinoApp.RunScript("_Grasshopper", false);
|
||||||
|
System.Threading.Thread.Sleep(1000);
|
||||||
|
doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "Could not open Grasshopper" };
|
||||||
|
}
|
||||||
|
|
||||||
|
var componentName = p["component_name"]?.ToString() ?? "";
|
||||||
|
var position = p["position"] as JObject;
|
||||||
|
var x = position?["x"]?.Value<float>() ?? 0;
|
||||||
|
var y = position?["y"]?.Value<float>() ?? 0;
|
||||||
|
|
||||||
|
// Find component by name in the component server
|
||||||
|
var proxy = Instances.ComponentServer.FindObjectByName(componentName, true, true);
|
||||||
|
if (proxy == null)
|
||||||
|
return new { error = $"Component '{componentName}' not found" };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var obj = proxy.CreateInstance() as IGH_DocumentObject;
|
||||||
|
if (obj == null)
|
||||||
|
return new { error = $"Could not create instance of '{componentName}'" };
|
||||||
|
|
||||||
|
obj.CreateAttributes();
|
||||||
|
obj.Attributes.Pivot = new PointF(x, y);
|
||||||
|
|
||||||
|
doc.AddObject(obj, false);
|
||||||
|
Instances.ActiveCanvas?.Refresh();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
component = componentName,
|
||||||
|
instance_guid = obj.InstanceGuid.ToString(),
|
||||||
|
position = new { x, y }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new { error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ConnectComponents(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
var sourceId = p["source_component"]?.ToString() ?? "";
|
||||||
|
var sourceOutput = p["source_output"]?.ToString() ?? "";
|
||||||
|
var targetId = p["target_component"]?.ToString() ?? "";
|
||||||
|
var targetInput = p["target_input"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
// Find source component
|
||||||
|
var sourceObj = FindComponent(doc, sourceId);
|
||||||
|
if (sourceObj == null)
|
||||||
|
return new { error = $"Source component '{sourceId}' not found" };
|
||||||
|
|
||||||
|
// Find target component
|
||||||
|
var targetObj = FindComponent(doc, targetId);
|
||||||
|
if (targetObj == null)
|
||||||
|
return new { error = $"Target component '{targetId}' not found" };
|
||||||
|
|
||||||
|
// Get output parameter
|
||||||
|
IGH_Param? outputParam = null;
|
||||||
|
if (sourceObj is IGH_Component sourceComp)
|
||||||
|
{
|
||||||
|
outputParam = sourceComp.Params.Output.FirstOrDefault(o =>
|
||||||
|
o.Name == sourceOutput || o.NickName == sourceOutput);
|
||||||
|
}
|
||||||
|
else if (sourceObj is IGH_Param param)
|
||||||
|
{
|
||||||
|
outputParam = param;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputParam == null)
|
||||||
|
return new { error = $"Output '{sourceOutput}' not found on source component" };
|
||||||
|
|
||||||
|
// Get input parameter
|
||||||
|
IGH_Param? inputParam = null;
|
||||||
|
if (targetObj is IGH_Component targetComp)
|
||||||
|
{
|
||||||
|
inputParam = targetComp.Params.Input.FirstOrDefault(i =>
|
||||||
|
i.Name == targetInput || i.NickName == targetInput);
|
||||||
|
}
|
||||||
|
else if (targetObj is IGH_Param param)
|
||||||
|
{
|
||||||
|
inputParam = param;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputParam == null)
|
||||||
|
return new { error = $"Input '{targetInput}' not found on target component" };
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
inputParam.AddSource(outputParam);
|
||||||
|
doc.NewSolution(true);
|
||||||
|
Instances.ActiveCanvas?.Refresh();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
connection = new
|
||||||
|
{
|
||||||
|
source = new { component = sourceId, output = sourceOutput },
|
||||||
|
target = new { component = targetId, input = targetInput }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IGH_DocumentObject? FindComponent(GH_Document doc, string identifier)
|
||||||
|
{
|
||||||
|
// Try to find by GUID
|
||||||
|
if (Guid.TryParse(identifier, out var guid))
|
||||||
|
{
|
||||||
|
return doc.Objects.FirstOrDefault(o => o.InstanceGuid == guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find by name or nickname
|
||||||
|
return doc.Objects.FirstOrDefault(o =>
|
||||||
|
o.Name == identifier || o.NickName == identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object AddSlider(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
{
|
||||||
|
RhinoApp.RunScript("_Grasshopper", false);
|
||||||
|
System.Threading.Thread.Sleep(1000);
|
||||||
|
doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "Could not open Grasshopper" };
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = p["name"]?.ToString() ?? "Slider";
|
||||||
|
var minValue = p["min_value"]?.Value<decimal>() ?? 0;
|
||||||
|
var maxValue = p["max_value"]?.Value<decimal>() ?? 100;
|
||||||
|
var currentValue = p["current_value"]?.Value<decimal>() ?? 50;
|
||||||
|
var position = p["position"] as JObject;
|
||||||
|
var x = position?["x"]?.Value<float>() ?? 0;
|
||||||
|
var y = position?["y"]?.Value<float>() ?? 0;
|
||||||
|
|
||||||
|
var slider = new GH_NumberSlider();
|
||||||
|
slider.CreateAttributes();
|
||||||
|
slider.Attributes.Pivot = new PointF(x, y);
|
||||||
|
slider.NickName = name;
|
||||||
|
slider.Slider.Minimum = minValue;
|
||||||
|
slider.Slider.Maximum = maxValue;
|
||||||
|
slider.SetSliderValue(currentValue);
|
||||||
|
|
||||||
|
doc.AddObject(slider, false);
|
||||||
|
Instances.ActiveCanvas?.Refresh();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
slider = name,
|
||||||
|
instance_guid = slider.InstanceGuid.ToString(),
|
||||||
|
min = minValue,
|
||||||
|
max = maxValue,
|
||||||
|
value = currentValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object BakeGeometry(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
var componentName = p["component_name"]?.ToString();
|
||||||
|
var layerName = p["layer_name"]?.ToString() ?? "Baked";
|
||||||
|
|
||||||
|
var rhinoDoc = RhinoDoc.ActiveDoc;
|
||||||
|
if (rhinoDoc == null)
|
||||||
|
return new { error = "No active Rhino document" };
|
||||||
|
|
||||||
|
// Ensure layer exists
|
||||||
|
var layerIndex = rhinoDoc.Layers.FindByFullPath(layerName, -1);
|
||||||
|
if (layerIndex < 0)
|
||||||
|
layerIndex = rhinoDoc.Layers.Add(layerName, Color.Black);
|
||||||
|
|
||||||
|
var bakedCount = 0;
|
||||||
|
|
||||||
|
foreach (var obj in doc.Objects)
|
||||||
|
{
|
||||||
|
if (obj is IGH_Component component)
|
||||||
|
{
|
||||||
|
// Skip if we're looking for a specific component
|
||||||
|
if (!string.IsNullOrEmpty(componentName) &&
|
||||||
|
component.Name != componentName &&
|
||||||
|
component.NickName != componentName)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var output in component.Params.Output)
|
||||||
|
{
|
||||||
|
foreach (var item in output.VolatileData.AllData(true))
|
||||||
|
{
|
||||||
|
if (item is Grasshopper.Kernel.Types.IGH_BakeAwareData bakeAware)
|
||||||
|
{
|
||||||
|
var attributes = new Rhino.DocObjects.ObjectAttributes
|
||||||
|
{
|
||||||
|
LayerIndex = layerIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
Guid objId;
|
||||||
|
if (bakeAware.BakeGeometry(rhinoDoc, attributes, out objId))
|
||||||
|
bakedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rhinoDoc.Views.Redraw();
|
||||||
|
|
||||||
|
return new { success = true, baked_count = bakedCount, layer = layerName };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object SaveDefinition(JObject p)
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
var filePath = p["file_path"]?.ToString();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var io = new GH_DocumentIO(doc);
|
||||||
|
bool success;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(filePath))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(doc.FilePath))
|
||||||
|
return new { error = "No file path specified and document has no existing path" };
|
||||||
|
|
||||||
|
success = io.Save();
|
||||||
|
filePath = doc.FilePath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
success = io.SaveAs(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new { success, file_path = filePath };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new { error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object Recompute()
|
||||||
|
{
|
||||||
|
var doc = GetActiveDocument();
|
||||||
|
if (doc == null)
|
||||||
|
return new { error = "No active Grasshopper document" };
|
||||||
|
|
||||||
|
doc.NewSolution(true);
|
||||||
|
Instances.ActiveCanvas?.Refresh();
|
||||||
|
|
||||||
|
return new { success = true, message = "Definition recomputed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/rhino-plugin/RhinoMCP/HttpServer.cs
Normal file
110
src/rhino-plugin/RhinoMCP/HttpServer.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using EmbedIO;
|
||||||
|
using EmbedIO.Routing;
|
||||||
|
using EmbedIO.WebApi;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Rhino;
|
||||||
|
|
||||||
|
namespace RhinoMCP
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP Server that receives commands from the MCP Server.
|
||||||
|
/// </summary>
|
||||||
|
public class HttpServer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly int _port;
|
||||||
|
private WebServer? _server;
|
||||||
|
|
||||||
|
public HttpServer(int port)
|
||||||
|
{
|
||||||
|
_port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_server = new WebServer(o => o
|
||||||
|
.WithUrlPrefix($"http://localhost:{_port}/")
|
||||||
|
.WithMode(HttpListenerMode.EmbedIO))
|
||||||
|
.WithCors()
|
||||||
|
.WithWebApi("/", m => m.WithController<RhinoApiController>());
|
||||||
|
|
||||||
|
_server.StateChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
RhinoApp.WriteLine($"RhinoMCP: Server state changed to {e.NewState}");
|
||||||
|
};
|
||||||
|
|
||||||
|
_server.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_server?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Web API Controller handling all Rhino/Grasshopper commands.
|
||||||
|
/// </summary>
|
||||||
|
public class RhinoApiController : WebApiController
|
||||||
|
{
|
||||||
|
[Route(HttpVerbs.Get, "/health")]
|
||||||
|
public object GetHealth()
|
||||||
|
{
|
||||||
|
return new { status = "ok", version = "1.0.0", rhino_version = RhinoApp.Version.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route(HttpVerbs.Post, "/execute")]
|
||||||
|
public async Task<object> Execute()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var body = await HttpContext.GetRequestBodyAsStringAsync();
|
||||||
|
var request = JsonConvert.DeserializeObject<JObject>(body);
|
||||||
|
|
||||||
|
if (request == null)
|
||||||
|
return new { error = "Invalid request body" };
|
||||||
|
|
||||||
|
var action = request["action"]?.ToString();
|
||||||
|
var parameters = request["params"] as JObject ?? new JObject();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(action))
|
||||||
|
return new { error = "Missing action parameter" };
|
||||||
|
|
||||||
|
// Execute on Rhino's main thread
|
||||||
|
object? result = null;
|
||||||
|
Exception? exception = null;
|
||||||
|
|
||||||
|
RhinoApp.InvokeOnUiThread(new Action(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = CommandHandler.Execute(action, parameters);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
exception = ex;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wait a bit for UI thread execution
|
||||||
|
await Task.Delay(100);
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
return new { error = exception.Message };
|
||||||
|
|
||||||
|
return result ?? new { success = true };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new { error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/rhino-plugin/RhinoMCP/RhinoMCP.csproj
Normal file
33
src/rhino-plugin/RhinoMCP/RhinoMCP.csproj
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
|
||||||
|
<!-- Rhino Plugin Settings -->
|
||||||
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
|
||||||
|
<!-- Assembly Info -->
|
||||||
|
<AssemblyName>RhinoMCP</AssemblyName>
|
||||||
|
<RootNamespace>RhinoMCP</RootNamespace>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>RhinoMCP Contributors</Authors>
|
||||||
|
<Description>MCP Server integration for Rhino and Grasshopper</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Rhino 8 SDK -->
|
||||||
|
<PackageReference Include="RhinoCommon" Version="8.0.23304.9001" />
|
||||||
|
<PackageReference Include="Grasshopper" Version="8.0.23304.9001" />
|
||||||
|
|
||||||
|
<!-- HTTP Server -->
|
||||||
|
<PackageReference Include="EmbedIO" Version="3.5.2" />
|
||||||
|
|
||||||
|
<!-- JSON Handling -->
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
54
src/rhino-plugin/RhinoMCP/RhinoMcpPlugin.cs
Normal file
54
src/rhino-plugin/RhinoMCP/RhinoMcpPlugin.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using Rhino;
|
||||||
|
using Rhino.PlugIns;
|
||||||
|
|
||||||
|
namespace RhinoMCP
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// RhinoMCP Plugin - Provides HTTP API for Claude AI integration.
|
||||||
|
/// </summary>
|
||||||
|
public class RhinoMcpPlugin : PlugIn
|
||||||
|
{
|
||||||
|
private HttpServer? _httpServer;
|
||||||
|
|
||||||
|
public static RhinoMcpPlugin? Instance { get; private set; }
|
||||||
|
|
||||||
|
public RhinoMcpPlugin()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override LoadReturnCode OnLoad(ref string errorMessage)
|
||||||
|
{
|
||||||
|
RhinoApp.WriteLine("RhinoMCP: Plugin loading...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Start HTTP server on port 9000
|
||||||
|
_httpServer = new HttpServer(9000);
|
||||||
|
_httpServer.Start();
|
||||||
|
|
||||||
|
RhinoApp.WriteLine("RhinoMCP: HTTP server started on http://localhost:9000");
|
||||||
|
RhinoApp.WriteLine("RhinoMCP: Ready for Claude AI connections!");
|
||||||
|
|
||||||
|
return LoadReturnCode.Success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"RhinoMCP: Failed to start HTTP server: {ex.Message}";
|
||||||
|
RhinoApp.WriteLine(errorMessage);
|
||||||
|
return LoadReturnCode.ErrorShowDialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnShutdown()
|
||||||
|
{
|
||||||
|
RhinoApp.WriteLine("RhinoMCP: Plugin shutting down...");
|
||||||
|
|
||||||
|
_httpServer?.Stop();
|
||||||
|
_httpServer?.Dispose();
|
||||||
|
|
||||||
|
base.OnShutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user