From 3153f3806898c800c45a7cebf2d7ac1768d67965 Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 8 Sep 2025 21:32:04 +0200 Subject: [PATCH] Implement ScreenshotManager for enhanced screenshot functionality - Added ScreenshotManager class to centralize screenshot capturing logic. - Implemented methods to capture specific objects, areas, and full canvas. - Updated MCPServer to utilize ScreenshotManager for taking screenshots. - Introduced new API method to take screenshots of specific objects by their IDs. - Enhanced error handling and result reporting for screenshot operations. - Refactored TakeScreenshot and added TakeObjectScreenshot methods in MCPServer. - Updated analyze_screenshots.py to use a relative path for screenshots directory. - Removed outdated test script for Python execution. --- Documentation/MCP/MCP.md | 170 ---- Documentation/MCP/MCP_LLM_Guide_Optimized.md | 126 --- Documentation/MCP/MCP_LLM_Guide_Original.md | 789 ------------------- FREEZE_FIXES.md | 103 --- PYTHON_EXECUTION_README.md | 146 ---- Services/MCPServer.cs | 372 ++++----- Services/ScreenshotManager.cs | 625 +++++++++++++++ analyze_screenshots.py | 4 +- test_python_execution.py | 105 --- 9 files changed, 818 insertions(+), 1622 deletions(-) delete mode 100644 Documentation/MCP/MCP.md delete mode 100644 Documentation/MCP/MCP_LLM_Guide_Optimized.md delete mode 100644 Documentation/MCP/MCP_LLM_Guide_Original.md delete mode 100644 FREEZE_FIXES.md delete mode 100644 PYTHON_EXECUTION_README.md create mode 100644 Services/ScreenshotManager.cs delete mode 100644 test_python_execution.py diff --git a/Documentation/MCP/MCP.md b/Documentation/MCP/MCP.md deleted file mode 100644 index 3c2bb52..0000000 --- a/Documentation/MCP/MCP.md +++ /dev/null @@ -1,170 +0,0 @@ -# CtrEditor MCP Server Documentation - -## 🎯 Overview - -The CtrEditor MCP (Model Context Protocol) Server transforms the CtrEditor WPF application into a programmable automation platform for debugging, testing, and simulation control. This server enables remote control of all major application features through JSON-RPC style communication. - -## 🚀 Architecture - -### Communication Flow -``` -LLM/Client ↔ MCP Proxy (Python) ↔ CtrEditor MCP Server (C#) ↔ WPF Application -``` - -### Connection Setup -- **Protocol**: TCP Socket on port 5006 -- **Format**: JSON-RPC style messages -- **Proxy**: Python proxy for VS Code Copilot integration - -### VS Code Configuration -```json -"ctreditor": { - "command": "C:/Users/migue/miniconda3/envs/mcp_proxy/python.exe", - "args": [ - "d:/Proyectos/Scripts/MCP_Proxy/mcp_proxy.py", - "--host", "localhost", - "--port", "5006", - "--name", "CtrEditor", - "--description", "CtrEditor WPF Application MCP Server for debugging and testing" - ] -} -``` - -## 📋 Available Tools - -### 🔧 Object Management - -#### `list_objects` -**Description**: Get complete list of all simulation objects -**Parameters**: None -**Returns**: Array of objects with full metadata (ID, name, type, position, properties) - -#### `create_object` -**Description**: Create a new simulation object -**Parameters**: -- `type` (string): Object type (e.g., "osHydTank", "osHydPump") -- `x` (number): X position in meters -- `y` (number): Y position in meters - -#### `update_object` -**Description**: Modify properties of existing object -**Parameters**: -- `id` (string): Object ID -- `properties` (object): JSON object with properties to update - -#### `delete_objects` -**Description**: Remove objects from simulation -**Parameters**: -- `ids` (array): Array of object IDs to delete - -#### `list_object_types` -**Description**: Get list of all creatable object types -**Parameters**: None - -### ⚙️ Simulation Control - -#### `start_simulation` -**Description**: Start the physics simulation -**Parameters**: None - -#### `stop_simulation` -**Description**: Stop the physics simulation -**Parameters**: None - -#### `get_simulation_status` -**Description**: Get current simulation state -**Parameters**: None -**Returns**: Status info (running, object count, etc.) - -### 🔌 PLC Integration - -#### `get_plc_status` -**Description**: Get PLC connection status -**Parameters**: None -**Returns**: Connection state and status information - -### 📸 Visual Documentation - -#### `take_screenshot` -**Description**: Capture canvas screenshot with high resolution -**Parameters** (all optional): -- `x` (number): X position in meters for partial capture -- `y` (number): Y position in meters for partial capture -- `width` (number): Width in meters for partial capture -- `height` (number): Height in meters for partial capture -- `include_background` (boolean): Include canvas background (default: false) - -**Features**: -- **High Resolution**: 2x scale for full canvas, 3x for partial captures -- **Auto-Directory**: Saves to `/screenshots/` subdirectory -- **Detailed Response**: File size, dimensions, timestamps, paths - -### 💾 Project Management - -#### `save_project` -**Description**: Save current project state -**Parameters**: None - -## 🎨 Key Features - -### 📊 Rich Object Information -- **Complete Properties**: All object properties in JSON format -- **Simulation Data**: Real-time physics and hydraulic states -- **Coordinate System**: All positions in engineering meters -- **Type Safety**: Full .NET type information preserved - -### 🖼️ Advanced Screenshots -- **Meter-Based Coordinates**: Natural engineering units -- **High Resolution**: Anti-aliased, print-quality images -- **Flexible Areas**: Full canvas or custom rectangular regions -- **Background Control**: With/without canvas background - -### 🔄 Real-Time Monitoring -- **Live Updates**: Monitor simulation state changes -- **Property Tracking**: Observe dynamic property changes during simulation -- **Performance Data**: Track flow rates, pressures, speeds, etc. - -### 🛡️ Error Handling -- **Detailed Errors**: Comprehensive error messages with context -- **Safe Operations**: Thread-safe UI operations via Dispatcher -- **Graceful Degradation**: Handles connection issues and timeouts - -## 🎯 Use Cases - -### 🔬 Simulation Debugging -1. **Pre-Simulation**: Inspect object configurations and positions -2. **Runtime Monitoring**: Track dynamic properties during simulation -3. **Post-Analysis**: Compare before/after states -4. **Visual Verification**: Screenshot specific areas for documentation - -### 🤖 Automated Testing -- **Regression Testing**: Verify simulation behavior consistency -- **Performance Benchmarking**: Monitor simulation performance metrics -- **Configuration Validation**: Ensure proper object setup -- **Visual Regression**: Compare screenshots across versions - -### 📚 Documentation Generation -- **Auto-Documentation**: Extract object configurations automatically -- **Visual Documentation**: Generate annotated screenshots -- **State Reports**: Create detailed simulation state reports -- **Training Materials**: Generate step-by-step simulation guides - -## 🔧 Technical Implementation - -### Core Components -- **MCPServer.cs**: Main TCP server with JSON-RPC handling -- **Tool Handlers**: Specialized handlers for each operation type -- **Thread Safety**: UI operations via WPF Dispatcher -- **Error Management**: Comprehensive exception handling - -### Performance Optimizations -- **Lazy Connection**: Connect only when needed -- **Efficient Serialization**: Optimized JSON generation -- **Memory Management**: Proper resource cleanup -- **High-Resolution Rendering**: Optimized screenshot generation - -### Security Considerations -- **Local-Only**: Bound to localhost for security -- **No File System Access**: Limited to application operations -- **Controlled Operations**: No dangerous system operations -- **Sandboxed**: Isolated from system configuration diff --git a/Documentation/MCP/MCP_LLM_Guide_Optimized.md b/Documentation/MCP/MCP_LLM_Guide_Optimized.md deleted file mode 100644 index c7d2acc..0000000 --- a/Documentation/MCP/MCP_LLM_Guide_Optimized.md +++ /dev/null @@ -1,126 +0,0 @@ -# CtrEditor MCP Server - LLM Guide - -## ⚡ Command Efficiency Tiers - -### 🚀 **Ultra-Fast** (Use Liberally) -- `get_simulation_status` → ~25 tokens, instant -- `get_ctreditor_status` → ~20 tokens, instant -- `start/stop_simulation` → ~15 tokens, 2-3 sec - -### 🟡 **Medium** (Use When Needed) -- `search_debug_log` → 50-200 tokens (max_lines=3-10) -- `build_project` → 100-500 tokens (errors only) - -### 🔴 **Heavy** (Use Sparingly) -- `list_objects` → 500-2000+ tokens -- `take_screenshot` → 100-300 tokens + file - -## 🔧 Core Operations - -### Process Management -```json -{"tool": "start_ctreditor", "parameters": {}} -{"tool": "get_ctreditor_status", "parameters": {}} -{"tool": "stop_ctreditor", "parameters": {}} -``` - -### Build & Debug -```json -{"tool": "build_project", "parameters": {}} -{"tool": "start_debug_listener", "parameters": {}} -{"tool": "search_debug_log", "parameters": {"pattern": "error|exception", "max_lines": 5}} -{"tool": "stop_debug_listener", "parameters": {}} -``` - -### Simulation Control -```json -{"tool": "get_simulation_status", "parameters": {}} -{"tool": "start_simulation", "parameters": {}} -{"tool": "stop_simulation", "parameters": {}} -``` - -### Object Management -```json -{"tool": "list_objects", "parameters": {}} -{"tool": "create_object", "parameters": {"type": "osHydPump", "x": 10, "y": 5}} -{"tool": "update_object", "parameters": {"id": "123", "properties": {"PumpHead": 75.0}}} -{"tool": "delete_objects", "parameters": {"ids": ["123", "456"]}} -``` - -## ⚡ Optimal Workflows - -### Quick Development Cycle -```json -{"tool": "get_ctreditor_status", "parameters": {}} -{"tool": "build_project", "parameters": {}} -{"tool": "start_simulation", "parameters": {}} -{"tool": "get_simulation_status", "parameters": {}} -``` - -### Bug Investigation -```json -{"tool": "search_debug_log", "parameters": {"pattern": "error|exception", "max_lines": 3}} -{"tool": "get_simulation_status", "parameters": {}} -``` - -### System Recovery -```json -{"tool": "stop_ctreditor", "parameters": {}} -{"tool": "start_ctreditor", "parameters": {}} -``` - -## 📊 Key Object Properties - -### Hydraulic Components -- **osHydTank**: `TankPressure`, `MaxLevel`, `MinLevel`, `IsFixedPressure` -- **osHydPump**: `PumpHead`, `MaxFlow`, `SpeedRatio`, `IsRunning` -- **osHydPipe**: `Length`, `Diameter`, `CurrentFlow`, `PressureDrop` - -### Common Properties -- **Position**: `Left`, `Top` (meters) -- **Dimensions**: `Ancho`, `Alto` (meters) -- **Connections**: `Id_ComponenteA`, `Id_ComponenteB` - -## 🖼️ Screenshots - -### Full Canvas -```json -{"tool": "take_screenshot", "parameters": {}} -``` - -### Targeted Area -```json -{"tool": "take_screenshot", "parameters": { - "x": 39.0, "y": 19.0, "width": 7.0, "height": 3.0 -}} -``` - -## 🔍 Debug Search Patterns - -High-value, low-token patterns: -- **Critical Errors**: `"error|exception|fail"` (max_lines=3) -- **Performance**: `"fps|slow|memory"` (max_lines=5) -- **Simulation**: `"simulation.*error|physics.*fail"` (max_lines=3) - -## 🚨 Troubleshooting - -### Build Issues -```json -{"tool": "get_ctreditor_status", "parameters": {}} -{"tool": "stop_ctreditor", "parameters": {}} -{"tool": "clean_project", "parameters": {}} -{"tool": "build_project", "parameters": {}} -``` - -### Connection Issues -1. Use `get_simulation_status` to test connectivity -2. Check CtrEditor is running with `get_ctreditor_status` -3. Verify MCP server is active on port 5006 - -## 💡 Best Practices - -- Always use `get_simulation_status` before expensive operations -- Use specific patterns in `search_debug_log` with low max_lines -- Call `list_objects` only when object data is actually needed -- Stop simulation before major structural changes -- Use appropriate units: meters, Pascal, m³/s diff --git a/Documentation/MCP/MCP_LLM_Guide_Original.md b/Documentation/MCP/MCP_LLM_Guide_Original.md deleted file mode 100644 index 22b4a8a..0000000 --- a/Documentation/MCP/MCP_LLM_Guide_Original.md +++ /dev/null @@ -1,789 +0,0 @@ -# CtrEditor MCP Server - LLM Guide - -## ⚡ Command Efficiency Tiers - -### 🚀 **Ultra-Fast** (Use Liberally) -- `get_simulation_status` → ~25 tokens, instant -- `get_ctreditor_status` → ~20 tokens, instant -- `start/stop_simulation` → ~15 tokens, 2-3 sec - -### 🟡 **Medium** (Use When Needed) -- `search_debug_log` → 50-200 tokens (max_lines=3-10) -- `build_project` → 100-500 tokens (errors only) - -### 🔴 **Heavy** (Use Sparingly) -- `list_objects` → 500-2000+ tokens -- `take_screenshot` → 100-300 tokens + file - -## � Core Operations - -### Process Management -```json -{"tool": "start_ctreditor", "parameters": {}} -{"tool": "get_ctreditor_status", "parameters": {}} -{"tool": "stop_ctreditor", "parameters": {}} -``` - -### Build & Debug -```json -{"tool": "build_project", "parameters": {}} -{"tool": "start_debug_listener", "parameters": {}} -{"tool": "search_debug_log", "parameters": {"pattern": "error|exception", "max_lines": 5}} -{"tool": "stop_debug_listener", "parameters": {}} -``` - -### Simulation Control -```json -{"tool": "get_simulation_status", "parameters": {}} -{"tool": "start_simulation", "parameters": {}} -{"tool": "stop_simulation", "parameters": {}} -``` - -### Object Management -```json -{"tool": "list_objects", "parameters": {}} -{"tool": "create_object", "parameters": {"type": "osHydPump", "x": 10, "y": 5}} -{"tool": "update_object", "parameters": {"id": "123", "properties": {"PumpHead": 75.0}}} -{"tool": "delete_objects", "parameters": {"ids": ["123", "456"]}} -``` - -## ⚡ Optimal Workflows - -### Quick Development Cycle -```json -{"tool": "get_ctreditor_status", "parameters": {}} -{"tool": "build_project", "parameters": {}} -{"tool": "start_simulation", "parameters": {}} -{"tool": "get_simulation_status", "parameters": {}} -``` - -### Bug Investigation -```json -{"tool": "search_debug_log", "parameters": {"pattern": "error|exception", "max_lines": 3}} -{"tool": "get_simulation_status", "parameters": {}} -``` - -### System Recovery -```json -{"tool": "stop_ctreditor", "parameters": {}} -{"tool": "start_ctreditor", "parameters": {}} -``` - -### 🎯 Micro-Workflows (Ultra-Efficient) - -#### **Instant Status Check (1-2 seconds, <50 tokens)** -```json -{"tool": "get_simulation_status", "parameters": {}} -``` -Use this before any operation - tells you everything you need to know - -#### **Quick Error Scan (3-5 seconds, <100 tokens)** -```json -{"tool": "search_debug_log", "parameters": { - "pattern": "error|fail|exception", - "max_lines": 3 -}} -``` -Surgical error detection without token waste - -#### **Smart Object Count Verification (1 second, ~25 tokens)** -```json -{"tool": "get_simulation_status", "parameters": {}} -``` -Check object_count property instead of calling list_objects - -#### **Efficient Build Verification (5-10 seconds, filtered output)** -```json -{"tool": "build_project", "parameters": {}} -``` -Only returns errors - ignores verbose build output - -### � Optimized Recovery Patterns - -#### **Process Recovery (Fast)** -```json -// 1. Check status (instant) -{"tool": "get_ctreditor_status", "parameters": {}} - -// 2. If needed, restart (5-10 seconds) -{"tool": "stop_ctreditor", "parameters": {}} -{"tool": "start_ctreditor", "parameters": {}} -``` - -#### **Simulation Recovery (Ultra-Fast)** -```json -// 1. Quick reset (3 seconds total) -{"tool": "stop_simulation", "parameters": {}} -{"tool": "start_simulation", "parameters": {}} - -// 2. Verify (instant) -{"tool": "get_simulation_status", "parameters": {}} -``` - -#### **Debug Recovery (Minimal)** -```json -{"tool": "stop_debug_listener", "parameters": {}} -{"tool": "start_debug_listener", "parameters": {}} -``` - -### 🧹 Cleanup (Minimal Token Usage) -``` -{"tool": "stop_simulation", "parameters": {}} // if running -{"tool": "save_project", "parameters": {}} // if changes made -{"tool": "stop_ctreditor", "parameters": {}} // when done -``` - -## 📊 Object Property Deep Dive - -### Hydraulic Components - -#### **osHydTank** (Hydraulic Tank) -Key Properties to Monitor: -- `TankType`: Type of tank (1=standard) -- `CrossSectionalArea`: Tank cross-section in m² -- `MaxLevel`, `MinLevel`: Level limits in meters -- `TankPressure`: Pressure in Pascal -- `IsFixedPressure`: Whether pressure is constant - -#### **osHydPump** (Hydraulic Pump) -Key Properties to Monitor: -- `PumpHead`: Pump head in meters -- `MaxFlow`: Maximum flow rate in m³/s -- `SpeedRatio`: Speed ratio (0.0-1.0) -- `IsRunning`: Pump operational state -- `PumpDirection`: Flow direction (1=forward, -1=reverse) - -#### **osHydPipe** (Hydraulic Pipe) -Key Properties to Monitor: -- `Length`: Pipe length in meters -- `Diameter`: Internal diameter in meters -- `Roughness`: Surface roughness -- `CurrentFlow`: Current flow rate in m³/s -- `PressureDrop`: Pressure loss across pipe -- `Id_ComponenteA`, `Id_ComponenteB`: Connected components - -### Dynamic Properties -During simulation, monitor these changing values: -- Flow rates in pipes (`CurrentFlow`) -- Pressure drops (`PressureDrop`) -- Tank levels (if applicable) -- Pump performance metrics - -## 🖼️ Screenshots - -### Full Canvas -```json -{"tool": "take_screenshot", "parameters": {}} -``` - -### Targeted Area -```json -{"tool": "take_screenshot", "parameters": { - "x": 39.0, "y": 19.0, "width": 7.0, "height": 3.0 -}} -``` - -## 🔍 Debug Search Patterns - -High-value, low-token patterns: -- **Critical Errors**: `"error|exception|fail"` (max_lines=3) -- **Performance**: `"fps|slow|memory"` (max_lines=5) -- **Simulation**: `"simulation.*error|physics.*fail"` (max_lines=3) - -## � Troubleshooting - -### Build Issues -```json -{"tool": "get_ctreditor_status", "parameters": {}} -{"tool": "stop_ctreditor", "parameters": {}} -{"tool": "clean_project", "parameters": {}} -{"tool": "build_project", "parameters": {}} -``` - -### Connection Issues -1. Use `get_simulation_status` to test connectivity -2. Check CtrEditor is running with `get_ctreditor_status` -3. Verify MCP server is active on port 5006 - -## 💡 Best Practices - -- Always use `get_simulation_status` before expensive operations -- Use specific patterns in `search_debug_log` with low max_lines -- Call `list_objects` only when object data is actually needed -- Stop simulation before major structural changes -- Use appropriate units: meters, Pascal, m³/s - "pattern": "error|exception", - "max_lines": 20 -}} - -// Check buffer stats -{"tool": "get_debug_stats", "parameters": {}} - -// Stop monitoring -{"tool": "stop_debug_listener", "parameters": {}} -``` - -### Combined Development Workflow -```json -// 1. Ensure clean environment -{"tool": "get_ctreditor_status", "parameters": {}} -{"tool": "stop_ctreditor", "parameters": {}} -{"tool": "clean_project", "parameters": {}} - -// 2. Build and check for errors -{"tool": "build_project", "parameters": {}} - -// 3. Start CtrEditor with monitoring -{"tool": "start_debug_listener", "parameters": {}} -{"tool": "start_ctreditor", "parameters": {}} - -// 4. Monitor for issues -{"tool": "search_debug_log", "parameters": { - "pattern": "error|exception|fail", - "max_lines": 10 -}} - -// 5. Test simulation -{"tool": "get_simulation_status", "parameters": {}} -{"tool": "start_simulation", "parameters": {}} - -// 6. Search for simulation issues -{"tool": "search_debug_log", "parameters": { - "pattern": "simulation|physics", - "max_lines": 15 -}} -``` - -## 🔧 Enhanced Debugging Workflows (NEW) - -### 1. Build Error Investigation -``` -1. build_project - Get compilation errors only (filtered) -2. search_debug_log(pattern="error|warning", max_lines=50) - Find related debug info -3. start_ctreditor - Launch for testing -4. get_debug_stats - Monitor for new issues -``` - -### 2. Runtime Issue Investigation -``` -1. start_debug_listener - Begin monitoring -2. start_simulation - Trigger the issue -3. search_debug_log(pattern="exception|error", last_n_lines=200) - Find problems -4. search_debug_log(pattern="stack trace", max_lines=10) - Get stack traces -5. stop_simulation - Stop for analysis -``` - -### 3. Performance Analysis -``` -1. clear_debug_buffer - Start with clean slate -2. start_simulation - Begin performance test -3. search_debug_log(pattern="fps|performance|slow", max_lines=20) - Find performance issues -4. search_debug_log(pattern="memory|allocation", max_lines=15) - Check memory usage -5. get_debug_stats - Overall statistics -``` - -### 4. Component Behavior Analysis -``` -1. search_debug_log(pattern="pump|flow|pressure", max_lines=30) - Hydraulic behavior -2. search_debug_log(pattern="simulation.*object", max_lines=25) - Object interactions -3. take_screenshot() - Visual verification -4. list_objects - Current property values -``` - -### 5. Pre-Simulation Analysis -``` -1. list_objects - Document initial state -2. take_screenshot() - Visual documentation -3. Analyze object configurations and connections -4. Verify proper component setup -``` - -### 6. Simulation Monitoring -``` -1. start_simulation - Begin simulation -2. get_simulation_status - Confirm running -3. list_objects - Monitor dynamic properties -4. take_screenshot(area) - Capture specific components -5. stop_simulation - End when needed -``` - -### 7. Performance Investigation -``` -1. list_objects - Get baseline measurements -2. start_simulation - Run simulation -3. Wait 5-10 seconds for stabilization -4. list_objects - Compare with baseline -5. take_screenshot() - Document final state -``` - -### 8. Component Analysis -``` -1. take_screenshot(component_area) - High-res component view -2. list_objects - Get detailed properties -3. update_object - Modify parameters if needed -4. start_simulation - Test changes -5. take_screenshot(component_area) - Compare results -``` - -## 📈 Simulation Data Interpretation - -### Flow Analysis -- **Positive Flow**: Normal direction flow -- **Negative Flow**: Reverse flow direction -- **Zero Flow**: No flow (blockage or equilibrium) - -### Pressure Analysis -- **High Pressure Drop**: Potential flow restriction -- **Low Pressure Drop**: Free-flowing condition -- **Pressure Patterns**: Indicate system behavior - -### Pump Performance -- **SpeedRatio**: Pump operation intensity -- **IsRunning**: Operational state -- **PumpDirection**: Flow direction control - -## ⚡ Rapid Iteration Patterns (NEW) - -### Lightning Fast Debug Cycle -**Single-Command Quick Check (5 seconds):** -```json -{"tool": "get_simulation_status", "parameters": {}} -``` -- Instantly know: running state, object count, visibility -- Use this before any other operation - -### Minimal Token Development Loop -**Ultra-Efficient 3-Step Pattern:** -```json -// 1. Quick status (minimal tokens) -{"tool": "get_ctreditor_status", "parameters": {}} - -// 2. Smart build (errors only, no full log) -{"tool": "build_project", "parameters": {}} - -// 3. Target-specific debug search (specific pattern) -{"tool": "search_debug_log", "parameters": { - "pattern": "error|exception", - "max_lines": 5 -}} -``` - -### Zero-Waste Object Analysis -**Smart Object Inspection (instead of full list_objects):** -```json -// Instead of listing all objects, target specific ones: -{"tool": "list_objects", "parameters": {}} -// Then filter client-side by type/name/properties -``` - -### Micro-Screenshot Strategy -**Targeted Visual Validation (saves bandwidth):** -```json -// Full canvas only when needed -{"tool": "take_screenshot", "parameters": {}} - -// For specific components (higher detail, smaller file): -{"tool": "take_screenshot", "parameters": { - "x": 42.0, "y": 20.0, "width": 2.0, "height": 2.0 -}} -``` - -## � Token-Optimized Workflows - -### Development Iteration (Minimal Tokens) -``` -1. get_ctreditor_status (1 call - instant) -2. build_project (errors only - no verbose output) -3. start_simulation (1 call - instant feedback) -4. get_simulation_status (verify running) -``` - -### Bug Investigation (Surgical Precision) -``` -1. search_debug_log(pattern="specific_error", max_lines=3) -2. list_objects (only if object data needed) -3. take_screenshot (targeted area only) -``` - -### Performance Testing (Streamlined) -``` -1. clear_debug_buffer (clean slate) -2. start_simulation -3. search_debug_log(pattern="fps|performance", max_lines=10) -4. stop_simulation -``` - -## 🎯 Smart Command Selection Guide - -### When to Use Each Function: -- **get_ctreditor_status**: Always first command (2 tokens response) -- **build_project**: Only when code changed (filtered output) -- **search_debug_log**: Instead of full console dump (targeted) -- **get_simulation_status**: Quick health check (3 tokens response) -- **list_objects**: Only when need object data (can be large) -- **take_screenshot**: Last resort for visual confirmation - -### Command Efficiency Ranking: -1. 🟢 **Ultra-Fast** (use liberally): `get_ctreditor_status`, `get_simulation_status` -2. 🟡 **Fast** (use when needed): `start/stop_simulation`, `search_debug_log` -3. 🟠 **Medium** (use selectively): `build_project`, `save_project` -4. 🔴 **Heavy** (use sparingly): `list_objects`, `take_screenshot` - -## �🚨 Enhanced Troubleshooting Guide (NEW) - -### Process Management Issues -If CtrEditor won't start or crashes: -1. `get_ctreditor_status` - Check current process state (instant) -2. `stop_ctreditor` - Ensure clean termination -3. `build_project` - Check for compilation issues (errors only) -4. `start_ctreditor` - Launch fresh instance -5. `search_debug_log` - Monitor for startup issues (targeted pattern) - -### Build Problems (Smart Filtering) -Critical Build Error Patterns: -- **CS#### errors**: Compilation issues (fix immediately) -- **MSB3027/MSB3021**: File lock errors (CtrEditor running) -- **MSB#### warnings**: Build system (usually safe to ignore) - -Quick Build Fix Flow: -```json -// 1. Check if app is blocking compilation -{"tool": "get_ctreditor_status", "parameters": {}} - -// 2. Stop if running -{"tool": "stop_ctreditor", "parameters": {}} - -// 3. Clean and rebuild -{"tool": "clean_project", "parameters": {}} -{"tool": "build_project", "parameters": {}} -``` - -### Debug Console Issues (Connection Problems) -Fast Debug Recovery: -```json -// 1. Check connection health -{"tool": "get_debug_stats", "parameters": {}} - -// 2. Reset connection -{"tool": "stop_debug_listener", "parameters": {}} -{"tool": "start_debug_listener", "parameters": {}} -``` - -### Connection Issues -If MCP commands fail: -1. Use `get_simulation_status` to test connectivity (fastest) -2. Check CtrEditor is running with `get_ctreditor_status` -3. Verify MCP server is active on port 5006 - -### Simulation Issues (Rapid Response) -If simulation behaves unexpectedly: -```json -// Quick diagnostic sequence -{"tool": "get_simulation_status", "parameters": {}} -{"tool": "search_debug_log", "parameters": { - "pattern": "simulation|physics|error", - "max_lines": 5 -}} -``` - -### Data Inconsistencies -Efficient data validation: -1. Use `get_simulation_status` for quick object count check -2. Only call `list_objects` if counts don't match expectations -3. Use targeted screenshots for visual verification - -## 💡 Enhanced Best Practices (UPDATED) - -### 🚀 Token Efficiency (Critical for Performance) - -#### **Ultra-Lightweight Commands (Use Liberally)** -- `get_ctreditor_status` → 15-25 tokens response -- `get_simulation_status` → 20-30 tokens response -- `start/stop_simulation` → 10-15 tokens response - -#### **Medium Weight Commands (Use When Needed)** -- `search_debug_log` → 50-200 tokens (with max_lines=5-10) -- `build_project` → 100-500 tokens (errors only, filtered) -- `create/update/delete_objects` → 30-100 tokens each - -#### **Heavy Commands (Use Sparingly)** -- `list_objects` → 500-2000+ tokens (depends on object count) -- `take_screenshot` → 100-300 tokens metadata + file - -### ⚡ Speed Optimization Patterns - -#### **Lightning-Fast Status Check (2-3 seconds)** -```json -{"tool": "get_simulation_status", "parameters": {}} -``` -Returns: running state, object count, visibility in minimal tokens - -#### **Smart Error Detection (5-10 seconds)** -```json -{"tool": "search_debug_log", "parameters": { - "pattern": "error|exception|fail", - "max_lines": 3 -}} -``` -Targeted error search instead of full console dump - -#### **Efficient Object Management** -```json -// Instead of list_objects -> update -> list_objects again: -// 1. Single targeted update -{"tool": "update_object", "parameters": {"id": "123", "properties": {...}}} - -// 2. Quick verification with status -{"tool": "get_simulation_status", "parameters": {}} -``` - -### 🎯 Workflow Optimization - -#### **Development Iteration (Under 30 seconds)** -``` -1. get_ctreditor_status (instant) -2. build_project (filtered errors) -3. start_simulation (instant) -4. search_debug_log (targeted, max_lines=5) -``` - -#### **Bug Investigation (Under 15 seconds)** -``` -1. search_debug_log (specific error pattern, max_lines=3) -2. get_simulation_status (confirm state) -3. targeted screenshot (only if visual needed) -``` - -#### **Performance Testing (Under 20 seconds)** -``` -1. clear_debug_buffer -2. start_simulation -3. search_debug_log(pattern="fps|slow", max_lines=5) -4. stop_simulation -``` - -### 🔍 Smart Debug Search Patterns - -#### **High-Value, Low-Token Patterns** -- **Critical Errors**: `"error|exception|fail|crash"` (max_lines=3) -- **Performance Issues**: `"fps|slow|memory|lag"` (max_lines=5) -- **Simulation Problems**: `"simulation.*error|physics.*fail"` (max_lines=3) -- **Object Issues**: `"object.*error|create.*fail|update.*error"` (max_lines=3) - -#### **Efficient Search Strategy** -```json -// Good: Specific, targeted -{"pattern": "pump.*error", "max_lines": 3} - -// Avoid: Too broad, token-heavy -{"pattern": ".*", "max_lines": 100} -``` - -### 📊 Data Minimization Techniques - -#### **Before Calling list_objects (Heavy)** -Ask yourself: -1. Do I need ALL objects or just count? → Use `get_simulation_status` -2. Do I need specific object? → Search by type/name client-side -3. Do I need visual confirmation? → Use targeted `take_screenshot` - -#### **Before Taking Screenshots** -Consider: -1. Is object data sufficient? → Use `list_objects` for position/properties -2. Do I need full canvas? → Use area parameters for specific region -3. Is this for documentation? → Full canvas OK -4. Is this for debugging? → Targeted area preferred - -### 🔧 Advanced Efficiency Tips - -#### **Batch Operations (When Possible)** -```json -// Good: Single call for multiple deletes -{"tool": "delete_objects", "parameters": {"ids": ["1", "2", "3"]}} - -// Avoid: Multiple single calls -// delete_objects(id="1"), delete_objects(id="2"), delete_objects(id="3") -``` - -#### **State Caching Strategy** -- Cache `get_simulation_status` results for 5-10 seconds -- Only call `list_objects` when object count changes -- Re-use screenshot data for multiple analyses - -#### **Conditional Command Execution** -```json -// Check state before expensive operations -if (simulation_status.is_running) { - // Only then do expensive analysis - list_objects(); -} -``` - -### 🚨 Anti-Patterns (Avoid These) - -#### **Token Wasters** -- ❌ Calling `list_objects` repeatedly without state change -- ❌ Using `search_debug_log` without specific patterns -- ❌ Taking full screenshots for simple status checks -- ❌ Building project when no code changes made - -#### **Time Wasters** -- ❌ Not checking `get_ctreditor_status` before operations -- ❌ Using `stop_ctreditor` → `start_ctreditor` for simple resets -- ❌ Clearing debug buffer unnecessarily -- ❌ Taking screenshots without area parameters for small checks - -### 🎯 Context-Aware Command Selection - -#### **Development Context** -Use: `build_project`, `search_debug_log`, `get_ctreditor_status` - -#### **Simulation Testing Context** -Use: `start/stop_simulation`, `get_simulation_status`, targeted `search_debug_log` - -#### **Object Analysis Context** -Use: `list_objects` (when needed), `update_object`, `get_simulation_status` - -#### **Visual Documentation Context** -Use: `take_screenshot`, `list_objects` (for positions), `save_project` - -### 🔄 Recovery Patterns (Fast Error Recovery) - -#### **Process Recovery (5 seconds)** -```json -{"tool": "get_ctreditor_status", "parameters": {}} -// If not running: -{"tool": "start_ctreditor", "parameters": {}} -``` - -#### **Simulation Recovery (3 seconds)** -```json -{"tool": "stop_simulation", "parameters": {}} -{"tool": "start_simulation", "parameters": {}} -``` - -#### **Debug Recovery (2 seconds)** -```json -{"tool": "stop_debug_listener", "parameters": {}} -{"tool": "start_debug_listener", "parameters": {}} -``` - -### Safety -- Always verify critical changes with `get_simulation_status` -- Use appropriate units for all parameters (meters, Pascal, m³/s) -- Stop simulation before major structural changes -- Save project after significant modifications - -## 📋 Real-World Testing Results (VERIFIED PATTERNS) - -### ✅ **Proven Efficient Patterns (From Live Testing)** - -#### **Complete System Test (19/19 functions verified)** -```json -// 1. Process verification (instant) -{"tool": "get_ctreditor_status", "parameters": {}} - -// 2. System assessment (25 tokens) -{"tool": "get_simulation_status", "parameters": {}} - -// 3. Object discovery (when needed) -{"tool": "list_objects", "parameters": {}} - -// 4. Visual confirmation (targeted) -{"tool": "take_screenshot", "parameters": {}} -``` - -#### **Hydraulic System Analysis (Real Example)** -**Found Objects** (from actual test): -- 2 × Hydraulic Tanks (osHydTank): `TankPressure: 101000.0 Pascal` -- 1 × Hydraulic Pump (osHydPump): `PumpHead: 75.0m, MaxFlow: 0.015 m³/s` -- 2 × Hydraulic Pipes (osHydPipe): `Diameter: 0.05m, Length: 1.0m` - -**Key Properties Verified**: -- Flow rates: `CurrentFlow: 0.0` (static state) -- Pressure drops: `PressureDrop: 0.0` (no flow) -- Connections: `Id_ComponenteA/B` linking system components - -#### **Build Error Handling (Real Example)** -**Actual Error Encountered**: MSB3027/MSB3021 - File locked by CtrEditor process -**Smart Response Pattern**: -```json -// 1. Check if process is blocking -{"tool": "get_ctreditor_status", "parameters": {}} - -// 2. If running, stop for clean build -{"tool": "stop_ctreditor", "parameters": {}} - -// 3. Clean and rebuild -{"tool": "clean_project", "parameters": {}} -{"tool": "build_project", "parameters": {}} -``` - -#### **Object Manipulation Workflow (Verified)** -```json -// 1. Create object (tested: osTextPlate, ID: 307325) -{"tool": "create_object", "parameters": { - "type": "osTextPlate", "x": 46, "y": 21 -}} - -// 2. Update properties (verified) -{"tool": "update_object", "parameters": { - "id": "307325", - "properties": {"Text": "Sistema Hidráulico", "TextColor": "#FF0000FF"} -}} - -// 3. Verify with status check (efficient) -{"tool": "get_simulation_status", "parameters": {}} - -// 4. Clean up when done -{"tool": "delete_objects", "parameters": {"ids": ["307325"]}} -``` - -### 📊 **Performance Metrics (Actual Results)** -- **Screenshot**: 3.4MB file, 78×54 meter canvas captured -- **Object types**: 33 types across 9 categories available -- **Process startup**: ~5-10 seconds from stop to ready -- **Simulation control**: Instant start/stop responses -- **Debug search**: <2 seconds for targeted patterns - -### 🎯 **Object Type Categories (Complete List)** -1. **Componentes Hidráulicos** (3): Pumps, Tanks, Pipes -2. **Decorativos** (3): Images, Frames, Text Plates -3. **Dinamicos** (2): Bottles, Bottle with Neck -4. **Emuladores** (3): Bottle Generator, Filler, Tank -5. **Estaticos** (9): Conveyors, Guides, Motors, Curves -6. **Extraccion Datos** (2): Template Search, Tag Extraction -7. **SensoresComandos** (6): Buttons, Encoders, Photocells, Sensors -8. **TagsSignals** (3): Consensus, Analog Tags, Digital Tags -9. **Traces** (2): Signal Tracers - -### 🔍 **Debug Patterns That Work (Tested)** -```json -// Critical errors (high value) -{"pattern": "error|exception|fail", "max_lines": 3} - -// Performance issues -{"pattern": "fps|slow|memory", "max_lines": 5} - -// Simulation specific -{"pattern": "simulation|physics", "max_lines": 5} - -// Object operations -{"pattern": "object.*create|update.*error", "max_lines": 3} -``` - -### ⚡ **Fastest Diagnostic Sequence (Under 10 seconds)** -```json -// 1. Health check (instant) -{"tool": "get_simulation_status", "parameters": {}} - -// 2. Error scan (2-3 seconds) -{"tool": "search_debug_log", "parameters": { - "pattern": "error|fail", "max_lines": 3 -}} - -// 3. Process verification if needed (instant) -{"tool": "get_ctreditor_status", "parameters": {}} -``` - -This sequence gives you complete system health in minimal time and tokens. diff --git a/FREEZE_FIXES.md b/FREEZE_FIXES.md deleted file mode 100644 index 1e33a17..0000000 --- a/FREEZE_FIXES.md +++ /dev/null @@ -1,103 +0,0 @@ -# Soluciones Implementadas para Freezes en CtrEditor - -## Problema Identificado -La aplicación CtrEditor experimentaba freezes relacionados con I/O Completion Ports, específicamente en: -``` -System.Private.CoreLib.dll!System.Threading.PortableThreadPool.IOCompletionPoller.Poll() -Interop.Kernel32.GetQueuedCompletionStatusEx(..., Timeout.Infinite, ...) -``` - -## Causas Raíz y Soluciones Implementadas - -### 1. **MCPServer - AcceptTcpClientAsync Sin Cancelación** -**Archivo:** `Services/MCPServer.cs` -**Problema:** El método `AcceptTcpClientAsync()` sin `CancellationToken` causaba bloqueos indefinidos en el I/O completion port. - -**Solución Implementada:** -```csharp -// ANTES (PROBLEMÁTICO) -var tcpClient = await _tcpListener.AcceptTcpClientAsync(); - -// DESPUÉS (CORREGIDO) -var acceptTask = _tcpListener.AcceptTcpClientAsync(); -var delayTask = Task.Delay(1000, cancellationToken); -var completedTask = await Task.WhenAny(acceptTask, delayTask); - -if (completedTask == acceptTask && !cancellationToken.IsCancellationRequested) -{ - var tcpClient = await acceptTask; - // Procesar cliente... -} -``` - -### 2. **StateManager - Bloqueo en Dispose** -**Archivo:** `DataStates/StateManager.cs` -**Problema:** Uso de `.Wait()` en método `Dispose()` puede causar deadlocks. - -**Solución Implementada:** -```csharp -// ANTES (PROBLEMÁTICO) -SaveAllAsync().Wait(); - -// DESPUÉS (CORREGIDO) -Task.Run(async () => await SaveAllAsync()).Wait(TimeSpan.FromSeconds(10)); -``` - -### 3. **StateManager - Antipatrón Task.Run + Dispatcher.Invoke** -**Archivo:** `DataStates/StateManager.cs` -**Problema:** Patrón ineficiente que puede causar contención de hilos. - -**Solución Implementada:** -```csharp -// ANTES (PROBLEMÁTICO) -await Task.Run(() => -{ - Application.Current.Dispatcher.Invoke(() => - { - CrearUserControlDesdeObjetoSimulable(obj); - }); -}); - -// DESPUÉS (CORREGIDO) -await Application.Current.Dispatcher.InvokeAsync(() => -{ - CrearUserControlDesdeObjetoSimulable(obj); -}); -``` - -### 4. **HttpClient - Falta de ConfigureAwait(false)** -**Archivo:** `IA/gtpask.cs` -**Problema:** Operaciones HTTP sin `ConfigureAwait(false)` pueden causar deadlocks en UI thread. - -**Solución Implementada:** -```csharp -// ANTES (PROBLEMÁTICO) -using var response = await _httpClient.PostAsync(endpoint, content); -var responseContent = await response.Content.ReadAsStringAsync(); - -// DESPUÉS (CORREGIDO) -using var response = await _httpClient.PostAsync(endpoint, content).ConfigureAwait(false); -var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); -``` - -## Resultado -- Eliminación de bloqueos indefinitos en I/O Completion Ports -- Prevención de deadlocks en operaciones de red y UI -- Mejora en la capacidad de cancelación de operaciones TCP -- Manejo más robusto de operaciones asíncronas - -## Verificación -- ✅ Proyecto compila sin errores -- ✅ DebugConsoleServer ya tenía la misma corrección implementada -- ✅ Aplicados timeouts y manejo de excepciones apropiados - -## Próximos Pasos Recomendados -1. Probar la aplicación en escenarios que anteriormente causaban freezes -2. Monitorear logs para verificar que las operaciones TCP se cancelan correctamente -3. Considerar aplicar `ConfigureAwait(false)` a otras operaciones asíncronas en el proyecto -4. Revisar otros usos de operaciones síncronas en contextos asíncronos - -## Archivos Modificados -- `Services/MCPServer.cs` - Corrección de AcceptTcpClientAsync -- `DataStates/StateManager.cs` - Corrección de Dispose y antipatrón Task.Run -- `IA/gtpask.cs` - Agregado de ConfigureAwait(false) para operaciones HTTP diff --git a/PYTHON_EXECUTION_README.md b/PYTHON_EXECUTION_README.md deleted file mode 100644 index d3fbe45..0000000 --- a/PYTHON_EXECUTION_README.md +++ /dev/null @@ -1,146 +0,0 @@ -# Python Execution in CtrEditor via MCP - -Esta funcionalidad permite ejecutar scripts Python directamente dentro de CtrEditor para debug y testing, con acceso completo a los objetos de la aplicación. - -## Características - -- **Ejecución de Python sin instalaciones externas**: Usa IronPython 3.4.2 integrado -- **Acceso completo a objetos**: MainViewModel, Canvas, ObjetosSimulables -- **Timeout configurable**: Previene scripts que se cuelguen -- **Variables de retorno**: Obtén valores específicos del script -- **Help integrado**: Documentación automática de objetos disponibles - -## Objetos Disponibles en Python - -### Variables Globales -- `app`: MainViewModel - El modelo de vista principal de la aplicación -- `canvas`: Canvas - El canvas principal donde se muestran los objetos -- `objects`: ObservableCollection - Colección de todos los objetos simulables -- `get_objects()`: Función que retorna una lista de todos los objetos simulables - -### Librerías Disponibles -- `sys`, `math`, `time`, `json`, `random` - -## Herramientas MCP Disponibles - -### execute_python -Ejecuta código Python con acceso a objetos de CtrEditor. - -**Parámetros:** -- `code` (requerido): El código Python a ejecutar -- `return_variables` (opcional): Lista de nombres de variables a retornar -- `timeout_seconds` (opcional): Timeout en segundos (default: 30) - -**Ejemplo:** -```json -{ - "name": "execute_python", - "arguments": { - "code": "count = len(objects)\nprint(f'Total objects: {count}')", - "return_variables": ["count"], - "timeout_seconds": 10 - } -} -``` - -### python_help -Obtiene ayuda sobre objetos y métodos disponibles. - -**Parámetros:** -- `object_name` (opcional): Nombre específico del objeto para obtener ayuda - -**Ejemplos:** -```json -{ - "name": "python_help", - "arguments": {} -} -``` - -```json -{ - "name": "python_help", - "arguments": { - "object_name": "app" - } -} -``` - -## Ejemplos de Uso - -### 1. Contar objetos -```python -# Contar todos los objetos -total = len(objects) -print(f"Total objects: {total}") - -# Filtrar objetos por tipo -conveyors = [obj for obj in objects if 'Conveyor' in str(type(obj))] -print(f"Conveyors: {len(conveyors)}") -``` - -### 2. Inspeccionar canvas -```python -# Obtener información del canvas -width = canvas.Width -height = canvas.Height -print(f"Canvas size: {width}x{height}") - -# Verificar estado de simulación -is_running = app.IsSimulationRunning -print(f"Simulation running: {is_running}") -``` - -### 3. Modificar objetos (con precaución) -```python -# Buscar un objeto específico -for obj in objects: - if hasattr(obj, 'Nombre') and obj.Nombre == "MiObjeto": - print(f"Found object: {obj.Nombre}") - # Modificar propiedades si es necesario - if hasattr(obj, 'Visible'): - obj.Visible = not obj.Visible - break -``` - -### 4. Debugging avanzado -```python -# Obtener propiedades de todos los objetos -for i, obj in enumerate(objects): - obj_type = type(obj).__name__ - properties = [prop for prop in dir(obj) if not prop.startswith('_')] - print(f"Object {i}: {obj_type} - {len(properties)} properties") -``` - -## Seguridad y Precauciones - -⚠️ **IMPORTANTE**: Esta funcionalidad está diseñada solo para debug y testing. NO está pensada para uso en producción. - -- El código Python se ejecuta en el mismo proceso que CtrEditor -- Tiene acceso completo a todos los objetos de la aplicación -- No hay restricciones de seguridad implementadas -- Use solo en entornos de desarrollo controlados - -## Configuración - -Las herramientas se configuran automáticamente cuando se inicia el MCPServer. No se requiere configuración adicional. - -## Solución de Problemas - -### Error: "Python environment initialization failed" -- Verifica que IronPython esté correctamente instalado via NuGet -- Revisa los logs de debug para más detalles - -### Error: "Python execution timed out" -- Reduce la complejidad del script -- Aumenta el timeout_seconds si es necesario -- Verifica que no haya bucles infinitos - -### Variables no encontradas -- Usa `python_help` para ver qué objetos están disponibles -- Verifica que los objetos existan antes de usarlos -- Usa `hasattr()` para verificar propiedades antes de accederlas - -## Logging - -Todos los eventos de ejecución Python se registran en el log de debug de CtrEditor con el prefijo `[MCP Server]`. diff --git a/Services/MCPServer.cs b/Services/MCPServer.cs index a50dc72..69b6328 100644 --- a/Services/MCPServer.cs +++ b/Services/MCPServer.cs @@ -36,6 +36,7 @@ namespace CtrEditor.Services private CancellationTokenSource _cancellationTokenSource; private bool _isRunning; private readonly object _lockObject = new object(); + private ScreenshotManager _screenshotManager; // Simulation timing tracking private readonly Stopwatch _simulationStopwatch; @@ -58,10 +59,30 @@ namespace CtrEditor.Services _totalSimulationMilliseconds = 0; _lastSimulationStatus = false; + // ScreenshotManager se inicializará de forma lazy cuando se necesite + _screenshotManager = null; + // Initialize Python environment InitializePythonEnvironment(); } + /// + /// Obtiene el ScreenshotManager, inicializándolo si es necesario + /// + private ScreenshotManager GetScreenshotManager() + { + if (_screenshotManager == null) + { + var canvas = _mainViewModel?.MainCanvas; + if (canvas == null) + { + throw new InvalidOperationException("Canvas no está disponible. Asegúrate de que la UI esté completamente cargada."); + } + _screenshotManager = new ScreenshotManager(_mainViewModel, canvas); + } + return _screenshotManager; + } + /// /// Inicia el servidor MCP TCP /// @@ -209,6 +230,7 @@ namespace CtrEditor.Services new { name = "get_simulation_status", description = "Get current simulation status" }, new { name = "get_plc_status", description = "Get PLC connection status" }, new { name = "take_screenshot", description = "Take a screenshot of the canvas" }, + new { name = "take_object_screenshot", description = "Take a screenshot of specific objects by their IDs" }, new { name = "save_project", description = "Save the current project" }, new { name = "reset_simulation_timing", description = "Reset simulation timing counters" } } @@ -340,10 +362,41 @@ namespace CtrEditor.Services height = new { type = "number", description = "Height in meters for partial capture (optional)" + }, + center_coordinates = new { + type = "boolean", + description = "If true, x and y parameters represent the center of the capture area instead of top-left corner. Defaults to false for backward compatibility." } } } }, + new { + name = "take_object_screenshot", + description = "Take a screenshot of specific objects by their IDs. This automatically calculates the bounding box of the specified objects and captures only that area with optional padding.", + inputSchema = new { + type = "object", + properties = new { + object_ids = new { + type = "array", + items = new { type = "string" }, + description = "Array of object IDs to capture. Can be a single object or multiple objects." + }, + padding_meters = new { + type = "number", + description = "Additional padding around objects in meters. Defaults to 0.5 meters." + }, + filename = new { + type = "string", + description = "Optional filename for the screenshot. Defaults to timestamp-based name." + }, + include_background = new { + type = "boolean", + description = "Whether to include canvas background image. Defaults to false." + } + }, + required = new[] { "object_ids" } + } + }, new { name = "start_simulation", description = "Start the physics simulation", @@ -476,6 +529,7 @@ namespace CtrEditor.Services "get_simulation_status" => GetSimulationStatus(), "get_plc_status" => GetPlcStatus(), "take_screenshot" => TakeScreenshot(arguments), + "take_object_screenshot" => TakeObjectScreenshot(arguments), "save_project" => SaveProject(), "reset_simulation_timing" => ResetSimulationTiming(), "execute_python" => ExecutePython(arguments), @@ -966,72 +1020,85 @@ namespace CtrEditor.Services } /// - /// Toma una captura de pantalla del canvas + /// Toma una captura de pantalla del canvas usando el nuevo ScreenshotManager /// private object TakeScreenshot(JObject arguments) { try { - // Parámetros de screenshot - var filename = arguments["filename"]?.ToString() ?? $"screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.png"; - var includeBackground = arguments["include_background"]?.ToObject() ?? false; // Por defecto false + var filename = arguments["filename"]?.ToString(); + var includeBackground = arguments["include_background"]?.ToObject() ?? false; + var saveFile = arguments["save_file"]?.ToObject() ?? true; + var returnBase64 = arguments["return_base64"]?.ToObject() ?? true; + var x = arguments["x"]?.ToObject(); var y = arguments["y"]?.ToObject(); var width = arguments["width"]?.ToObject(); var height = arguments["height"]?.ToObject(); + var centerCoordinates = arguments["center_coordinates"]?.ToObject() ?? false; - // Asegurar extensión .png - if (!filename.ToLower().EndsWith(".png")) - filename += ".png"; + // Obtener ScreenshotManager de forma lazy + var screenshotManager = GetScreenshotManager(); + ScreenshotResult result; - // Crear subdirectorio screenshots - var screenshotsDir = System.IO.Path.Combine(EstadoPersistente.Instance.directorio, "screenshots"); - Directory.CreateDirectory(screenshotsDir); - - // Obtener ruta completa - siempre en subdirectorio screenshots a menos que sea ruta absoluta - var fullPath = System.IO.Path.IsPathRooted(filename) ? filename : System.IO.Path.Combine(screenshotsDir, filename); - - // Obtener información del canvas para detalles - var canvas = _mainViewModel.MainCanvas; - var canvasWidth = canvas?.ActualWidth ?? 0; - var canvasHeight = canvas?.ActualHeight ?? 0; - var canvasWidthMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasWidth); - var canvasHeightMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasHeight); - - // Tomar screenshot - var success = TakeCanvasScreenshot(fullPath, includeBackground, x, y, width, height); - - if (success) + if (x.HasValue && y.HasValue && width.HasValue && height.HasValue) + { + // Captura de área específica + if (centerCoordinates) + { + result = screenshotManager.CaptureCenteredArea( + x.Value, y.Value, width.Value, height.Value, + filename, includeBackground, saveFile, returnBase64); + } + else + { + result = screenshotManager.CaptureArea( + x.Value, y.Value, width.Value, height.Value, + filename, includeBackground, saveFile, returnBase64); + } + } + else + { + // Captura de todo el canvas + result = screenshotManager.CaptureFullCanvas( + filename, includeBackground, saveFile, returnBase64); + } + + if (result.Success) { - var fileInfo = new FileInfo(fullPath); return new { success = true, - filename = System.IO.Path.GetFileName(fullPath), - full_path = fullPath, - directory = System.IO.Path.GetDirectoryName(fullPath), - file_size_bytes = fileInfo.Length, - timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), + filename = result.FileName, + full_path = result.FilePath, + directory = result.Directory, + file_size_bytes = result.FileSizeBytes, + timestamp = result.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), message = "Screenshot saved successfully", + base64_data = returnBase64 ? result.Base64Data : null, canvas_info = new { - canvas_width_pixels = canvasWidth, - canvas_height_pixels = canvasHeight, - canvas_width_meters = Math.Round(canvasWidthMeters, 3), - canvas_height_meters = Math.Round(canvasHeightMeters, 3) + canvas_width_pixels = _mainViewModel.MainCanvas?.ActualWidth ?? 0, + canvas_height_pixels = _mainViewModel.MainCanvas?.ActualHeight ?? 0, + canvas_width_meters = result.CaptureArea?.Width ?? 0, + canvas_height_meters = result.CaptureArea?.Height ?? 0 }, capture_info = new { - include_background = includeBackground, + include_background = result.IncludeBackground, area_specified = x.HasValue && y.HasValue && width.HasValue && height.HasValue, - area = x.HasValue ? new { - x = Math.Round(x.Value, 3), - y = Math.Round(y.Value, 3), - width = Math.Round(width.Value, 3), - height = Math.Round(height.Value, 3), + area = result.CaptureArea != null ? new + { + x = Math.Round(result.CaptureArea.Left, 3), + y = Math.Round(result.CaptureArea.Top, 3), + width = Math.Round(result.CaptureArea.Width, 3), + height = Math.Round(result.CaptureArea.Height, 3), + center_x = Math.Round(result.CaptureArea.CenterX, 3), + center_y = Math.Round(result.CaptureArea.CenterY, 3), units = "meters" } : null, - capture_type = x.HasValue ? "partial" : "full_canvas" + capture_type = result.CaptureType.ToString().ToLower(), + center_coordinates = centerCoordinates } }; } @@ -1040,15 +1107,16 @@ namespace CtrEditor.Services return new { success = false, - error = "Failed to capture screenshot", - attempted_path = fullPath + error = result.ErrorMessage, + error_type = result.ErrorType }; } } catch (Exception ex) { - return new { - success = false, + return new + { + success = false, error = ex.Message, error_type = ex.GetType().Name }; @@ -1076,156 +1144,96 @@ namespace CtrEditor.Services } } - #endregion - - #region Screenshot Implementation - /// - /// Toma una captura de pantalla del canvas + /// Toma una captura de pantalla de objetos específicos por sus IDs usando el nuevo ScreenshotManager /// - private bool TakeCanvasScreenshot(string filePath, bool includeBackground = false, - float? x = null, float? y = null, float? width = null, float? height = null) + private object TakeObjectScreenshot(JObject arguments) { try { - var canvas = _mainViewModel.MainCanvas; - if (canvas == null) + var objectIds = arguments["object_ids"]?.ToObject(); + if (objectIds == null || objectIds.Length == 0) + throw new ArgumentException("object_ids is required and must not be empty"); + + var paddingMeters = arguments["padding_meters"]?.ToObject() ?? 0.5f; + var filename = arguments["filename"]?.ToString(); + var includeBackground = arguments["include_background"]?.ToObject() ?? false; + var saveFile = arguments["save_file"]?.ToObject() ?? true; + var returnBase64 = arguments["return_base64"]?.ToObject() ?? true; + + // Obtener ScreenshotManager de forma lazy y usar el nuevo método + var screenshotManager = GetScreenshotManager(); + var result = screenshotManager.CaptureObjects( + objectIds, paddingMeters, filename, includeBackground, saveFile, returnBase64); + + if (result.Success) { - Debug.WriteLine("[MCP Server] Canvas is null"); - return false; - } - - // Asegurar que el canvas esté renderizado - canvas.UpdateLayout(); - - // Determinar el área a capturar - Rect captureRect; - if (x.HasValue && y.HasValue && width.HasValue && height.HasValue) - { - // Convertir coordenadas de metros a píxeles - var pixelX = PixelToMeter.Instance.calc.MetersToPixels(x.Value); - var pixelY = PixelToMeter.Instance.calc.MetersToPixels(y.Value); - var pixelWidth = PixelToMeter.Instance.calc.MetersToPixels(width.Value); - var pixelHeight = PixelToMeter.Instance.calc.MetersToPixels(height.Value); - - captureRect = new Rect(pixelX, pixelY, pixelWidth, pixelHeight); + return new + { + success = true, + filename = result.FileName, + full_path = result.FilePath, + directory = result.Directory, + file_size_bytes = result.FileSizeBytes, + timestamp = result.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), + message = "Object screenshot saved successfully", + base64_data = returnBase64 ? result.Base64Data : null, + captured_objects = result.CapturedObjects.Select(obj => new + { + id = obj.Id, + name = obj.Name, + type = obj.Type, + position = new { x = obj.Left, y = obj.Top }, + center = new { x = obj.CenterX, y = obj.CenterY }, + dimensions = new { width = obj.Width, height = obj.Height } + }).ToArray(), + bounding_box = new + { + left = Math.Round(result.BoundingBox.Left, 3), + top = Math.Round(result.BoundingBox.Top, 3), + right = Math.Round(result.BoundingBox.Left + result.BoundingBox.Width, 3), + bottom = Math.Round(result.BoundingBox.Top + result.BoundingBox.Height, 3), + center_x = Math.Round(result.BoundingBox.CenterX, 3), + center_y = Math.Round(result.BoundingBox.CenterY, 3), + width = Math.Round(result.BoundingBox.Width, 3), + height = Math.Round(result.BoundingBox.Height, 3) + }, + capture_area = new + { + left = Math.Round(result.CaptureArea.Left, 3), + top = Math.Round(result.CaptureArea.Top, 3), + center_x = Math.Round(result.CaptureArea.CenterX, 3), + center_y = Math.Round(result.CaptureArea.CenterY, 3), + width = Math.Round(result.CaptureArea.Width, 3), + height = Math.Round(result.CaptureArea.Height, 3), + padding_meters = result.PaddingMeters + }, + capture_info = new + { + include_background = result.IncludeBackground, + capture_type = result.CaptureType.ToString().ToLower(), + objects_count = result.CapturedObjects.Count + } + }; } else { - // Capturar todo el canvas - captureRect = new Rect(0, 0, canvas.ActualWidth, canvas.ActualHeight); - } - - // Validar dimensiones - if (captureRect.Width <= 0 || captureRect.Height <= 0) - { - Debug.WriteLine($"[MCP Server] Invalid capture dimensions: {captureRect}"); - return false; - } - - Debug.WriteLine($"[MCP Server] Capturing area: {captureRect}, Canvas size: {canvas.ActualWidth}x{canvas.ActualHeight}"); - - // Crear RenderTargetBitmap con alta resolución - // Usar factor de escala para mejorar calidad en capturas parciales - var scaleFactor = (x.HasValue && y.HasValue) ? 3.0 : 2.0; // Mayor escala para áreas parciales - var renderWidth = Math.Max(1, (int)(captureRect.Width * scaleFactor)); - var renderHeight = Math.Max(1, (int)(captureRect.Height * scaleFactor)); - var dpi = 96 * scaleFactor; // Aumentar DPI proporcionalmente - - var renderBitmap = new RenderTargetBitmap( - renderWidth, - renderHeight, - dpi, // DPI X - dpi, // DPI Y - PixelFormats.Pbgra32); - - // Crear un Canvas temporal para renderizado con escala mejorada - var tempCanvas = new Canvas() - { - Width = captureRect.Width * scaleFactor, - Height = captureRect.Height * scaleFactor, - Background = includeBackground ? canvas.Background : Brushes.White - }; - - // Aplicar escala al canvas - tempCanvas.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor); - - // Clonar elementos visibles del canvas principal - foreach (UIElement child in canvas.Children) - { - if (child.Visibility == Visibility.Visible) + return new { - try - { - // Obtener posición del elemento - var left = Canvas.GetLeft(child); - var top = Canvas.GetTop(child); - - // Verificar si está en el área de captura - var elementRect = new Rect( - double.IsNaN(left) ? 0 : left, - double.IsNaN(top) ? 0 : top, - child.RenderSize.Width, - child.RenderSize.Height); - - if (captureRect.IntersectsWith(elementRect) || (!x.HasValue && !y.HasValue)) - { - // Crear una representación visual del elemento - var visualBrush = new VisualBrush(child) - { - Stretch = Stretch.None, - AlignmentX = AlignmentX.Left, - AlignmentY = AlignmentY.Top - }; - - var rect = new Rectangle() - { - Width = child.RenderSize.Width * scaleFactor, - Height = child.RenderSize.Height * scaleFactor, - Fill = visualBrush - }; - - // Posicionar relativo al área de captura con escala - Canvas.SetLeft(rect, (elementRect.X - captureRect.X) * scaleFactor); - Canvas.SetTop(rect, (elementRect.Y - captureRect.Y) * scaleFactor); - - tempCanvas.Children.Add(rect); - } - } - catch (Exception ex) - { - Debug.WriteLine($"[MCP Server] Error processing child element: {ex.Message}"); - } - } + success = false, + error = result.ErrorMessage, + error_type = result.ErrorType + }; } - - // Forzar layout del canvas temporal con las nuevas dimensiones escaladas - var scaledSize = new Size(captureRect.Width * scaleFactor, captureRect.Height * scaleFactor); - tempCanvas.Measure(scaledSize); - tempCanvas.Arrange(new Rect(0, 0, scaledSize.Width, scaledSize.Height)); - tempCanvas.UpdateLayout(); - - // Renderizar - renderBitmap.Render(tempCanvas); - - // Guardar imagen - var encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(renderBitmap)); - - Directory.CreateDirectory(System.IO.Path.GetDirectoryName(filePath)); - using (var fileStream = new FileStream(filePath, FileMode.Create)) - { - encoder.Save(fileStream); - } - - Debug.WriteLine($"[MCP Server] Screenshot saved successfully: {filePath}"); - return true; } catch (Exception ex) { - Debug.WriteLine($"[MCP Server] Error taking screenshot: {ex.Message}"); - Debug.WriteLine($"[MCP Server] Stack trace: {ex.StackTrace}"); - return false; + return new + { + success = false, + error = ex.Message, + error_type = ex.GetType().Name + }; } } diff --git a/Services/ScreenshotManager.cs b/Services/ScreenshotManager.cs new file mode 100644 index 0000000..60e0438 --- /dev/null +++ b/Services/ScreenshotManager.cs @@ -0,0 +1,625 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using CtrEditor.ObjetosSim; +using System.Diagnostics; + +namespace CtrEditor.Services +{ + /// + /// Gestor centralizado de capturas de pantalla del canvas. + /// Proporciona funcionalidades para capturar: + /// 1. Objetos específicos por ID + /// 2. Áreas específicas en coordenadas de metros + /// 3. Todo el canvas + /// + /// Utiliza la misma lógica que ObjectManipulationManager para calcular + /// las dimensiones reales de los objetos en el canvas. + /// + public class ScreenshotManager + { + private readonly MainViewModel _mainViewModel; + private readonly Canvas _canvas; + private readonly string _defaultScreenshotsDirectory; + + public ScreenshotManager(MainViewModel mainViewModel, Canvas canvas) + { + _mainViewModel = mainViewModel ?? throw new ArgumentNullException(nameof(mainViewModel)); + _canvas = canvas ?? throw new ArgumentNullException(nameof(canvas)); + _defaultScreenshotsDirectory = System.IO.Path.Combine(EstadoPersistente.Instance.directorio, "screenshots"); + } + + #region Public Methods + + /// + /// Captura screenshot de objetos específicos por sus IDs + /// + /// Array de IDs de objetos a capturar + /// Padding adicional alrededor de los objetos en metros + /// Nombre del archivo (opcional) + /// Si incluir imagen de fondo + /// Si guardar el archivo + /// Si retornar la imagen como base64 + /// Resultado de la captura + public ScreenshotResult CaptureObjects( + string[] objectIds, + float paddingMeters = 0.5f, + string filename = null, + bool includeBackground = false, + bool saveToFile = true, + bool returnBase64 = true) + { + try + { + if (objectIds == null || objectIds.Length == 0) + throw new ArgumentException("objectIds no puede estar vacío"); + + // Buscar objetos por ID + var targetObjects = FindObjectsByIds(objectIds); + if (!targetObjects.Any()) + throw new ArgumentException($"No se encontraron objetos visibles con los IDs: {string.Join(", ", objectIds)}"); + + // Calcular bounding box usando la misma lógica que ObjectManipulationManager + var boundingBox = CalculateObjectsBoundingBox(targetObjects); + + // Aplicar padding + var captureArea = new ScreenshotArea + { + Left = boundingBox.Left - paddingMeters, + Top = boundingBox.Top - paddingMeters, + Width = boundingBox.Width + (paddingMeters * 2), + Height = boundingBox.Height + (paddingMeters * 2) + }; + + // Asegurar dimensiones mínimas + captureArea.Width = Math.Max(captureArea.Width, 0.1f); + captureArea.Height = Math.Max(captureArea.Height, 0.1f); + + // Capturar la imagen + var bitmap = CaptureCanvasArea(captureArea, includeBackground); + + // Preparar resultado + var result = new ScreenshotResult + { + Success = true, + Bitmap = bitmap, + CaptureType = ScreenshotType.Objects, + CapturedObjects = targetObjects.Select(obj => new CapturedObjectInfo + { + Id = obj.Id.Value.ToString(), + Name = obj.Nombre, + Type = obj.GetType().Name, + Left = obj.Left, + Top = obj.Top, + Width = obj.Ancho, + Height = obj.Alto, + CenterX = obj.Left + obj.Ancho / 2, + CenterY = obj.Top + obj.Alto / 2 + }).ToList(), + BoundingBox = new AreaInfo + { + Left = boundingBox.Left, + Top = boundingBox.Top, + Width = boundingBox.Width, + Height = boundingBox.Height, + CenterX = boundingBox.Left + boundingBox.Width / 2, + CenterY = boundingBox.Top + boundingBox.Height / 2 + }, + CaptureArea = new AreaInfo + { + Left = captureArea.Left, + Top = captureArea.Top, + Width = captureArea.Width, + Height = captureArea.Height, + CenterX = captureArea.Left + captureArea.Width / 2, + CenterY = captureArea.Top + captureArea.Height / 2 + }, + PaddingMeters = paddingMeters, + IncludeBackground = includeBackground + }; + + // Guardar archivo y/o generar base64 + return ProcessScreenshotOutput(result, filename, saveToFile, returnBase64); + } + catch (Exception ex) + { + return new ScreenshotResult + { + Success = false, + ErrorMessage = ex.Message, + ErrorType = ex.GetType().Name + }; + } + } + + /// + /// Captura screenshot de un área específica en coordenadas de metros + /// + /// Coordenada X izquierda en metros + /// Coordenada Y superior en metros + /// Ancho en metros + /// Alto en metros + /// Nombre del archivo (opcional) + /// Si incluir imagen de fondo + /// Si guardar el archivo + /// Si retornar la imagen como base64 + /// Resultado de la captura + public ScreenshotResult CaptureArea( + float left, + float top, + float width, + float height, + string filename = null, + bool includeBackground = false, + bool saveToFile = true, + bool returnBase64 = true) + { + try + { + if (width <= 0 || height <= 0) + throw new ArgumentException("Width y height deben ser mayores que 0"); + + var captureArea = new ScreenshotArea + { + Left = left, + Top = top, + Width = width, + Height = height + }; + + var bitmap = CaptureCanvasArea(captureArea, includeBackground); + + var result = new ScreenshotResult + { + Success = true, + Bitmap = bitmap, + CaptureType = ScreenshotType.Area, + CaptureArea = new AreaInfo + { + Left = left, + Top = top, + Width = width, + Height = height, + CenterX = left + width / 2, + CenterY = top + height / 2 + }, + IncludeBackground = includeBackground + }; + + return ProcessScreenshotOutput(result, filename, saveToFile, returnBase64); + } + catch (Exception ex) + { + return new ScreenshotResult + { + Success = false, + ErrorMessage = ex.Message, + ErrorType = ex.GetType().Name + }; + } + } + + /// + /// Captura screenshot de un área centrada en coordenadas específicas + /// + /// Coordenada X del centro en metros + /// Coordenada Y del centro en metros + /// Ancho en metros + /// Alto en metros + /// Nombre del archivo (opcional) + /// Si incluir imagen de fondo + /// Si guardar el archivo + /// Si retornar la imagen como base64 + /// Resultado de la captura + public ScreenshotResult CaptureCenteredArea( + float centerX, + float centerY, + float width, + float height, + string filename = null, + bool includeBackground = false, + bool saveToFile = true, + bool returnBase64 = true) + { + var left = centerX - width / 2; + var top = centerY - height / 2; + + return CaptureArea(left, top, width, height, filename, includeBackground, saveToFile, returnBase64); + } + + /// + /// Captura screenshot de todo el canvas + /// + /// Nombre del archivo (opcional) + /// Si incluir imagen de fondo + /// Si guardar el archivo + /// Si retornar la imagen como base64 + /// Resultado de la captura + public ScreenshotResult CaptureFullCanvas( + string filename = null, + bool includeBackground = true, + bool saveToFile = true, + bool returnBase64 = true) + { + try + { + var bitmap = CaptureEntireCanvas(includeBackground); + + var canvasWidthMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)_canvas.ActualWidth); + var canvasHeightMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)_canvas.ActualHeight); + + var result = new ScreenshotResult + { + Success = true, + Bitmap = bitmap, + CaptureType = ScreenshotType.FullCanvas, + CaptureArea = new AreaInfo + { + Left = 0, + Top = 0, + Width = canvasWidthMeters, + Height = canvasHeightMeters, + CenterX = canvasWidthMeters / 2, + CenterY = canvasHeightMeters / 2 + }, + IncludeBackground = includeBackground + }; + + return ProcessScreenshotOutput(result, filename, saveToFile, returnBase64); + } + catch (Exception ex) + { + return new ScreenshotResult + { + Success = false, + ErrorMessage = ex.Message, + ErrorType = ex.GetType().Name + }; + } + } + + #endregion + + #region Private Helper Methods + + /// + /// Busca objetos por sus IDs + /// + private List FindObjectsByIds(string[] objectIds) + { + var allObjects = _mainViewModel.ObjetosSimulables.ToList(); + var result = new List(); + + foreach (var objectId in objectIds) + { + var obj = allObjects.FirstOrDefault(o => o.Id.Value.ToString() == objectId); + if (obj != null && obj.Show_On_This_Page) + { + result.Add(obj); + Console.WriteLine($"DEBUG: Object {objectId} ADDED - Type: {obj.GetType().Name}, Left: {obj.Left}, Top: {obj.Top}"); + } + else + { + Console.WriteLine($"DEBUG: Object {objectId} REJECTED - Found: {obj != null}, Show_On_This_Page: {obj?.Show_On_This_Page}"); + } + } + + Console.WriteLine($"DEBUG: Total objects found: {result.Count}"); + return result; + } + + /// + /// Calcula el bounding box de un conjunto de objetos usando coordenadas directas + /// + private ScreenshotArea CalculateObjectsBoundingBox(List objects) + { + if (!objects.Any()) + throw new ArgumentException("La lista de objetos no puede estar vacía"); + + float left = float.MaxValue; + float top = float.MaxValue; + float right = float.MinValue; + float bottom = float.MinValue; + + foreach (var obj in objects) + { + // Usar coordenadas directas del objeto (ya están en metros) + float objLeft = obj.Left; + float objTop = obj.Top; + float objRight = obj.Left + obj.Ancho; + float objBottom = obj.Top + obj.Alto; + + left = Math.Min(left, objLeft); + top = Math.Min(top, objTop); + right = Math.Max(right, objRight); + bottom = Math.Max(bottom, objBottom); + + Console.WriteLine($"DEBUG: Object {obj.Id.Value} bounds - L:{objLeft} T:{objTop} R:{objRight} B:{objBottom}"); + } + + if (left == float.MaxValue) // No se encontraron objetos válidos + throw new InvalidOperationException("No se encontraron objetos válidos para calcular el bounding box"); + + Console.WriteLine($"DEBUG: Final bounding box - L:{left} T:{top} W:{right - left} H:{bottom - top}"); + + return new ScreenshotArea + { + Left = left, + Top = top, + Width = right - left, + Height = bottom - top + }; + } + + /// + /// Captura un área específica del canvas + /// + private RenderTargetBitmap CaptureCanvasArea(ScreenshotArea area, bool includeBackground) + { + // Asegurar que el canvas esté actualizado + _canvas.UpdateLayout(); + + // Convertir área de metros a píxeles + var pixelLeft = PixelToMeter.Instance.calc.MetersToPixels(area.Left); + var pixelTop = PixelToMeter.Instance.calc.MetersToPixels(area.Top); + var pixelWidth = PixelToMeter.Instance.calc.MetersToPixels(area.Width); + var pixelHeight = PixelToMeter.Instance.calc.MetersToPixels(area.Height); + + var captureRect = new Rect(pixelLeft, pixelTop, pixelWidth, pixelHeight); + + // Validar que el área esté dentro del canvas + var canvasRect = new Rect(0, 0, _canvas.ActualWidth, _canvas.ActualHeight); + if (!canvasRect.IntersectsWith(captureRect)) + throw new ArgumentException("El área especificada está fuera del canvas"); + + // Intersectar con el canvas para evitar áreas fuera de límites + captureRect.Intersect(canvasRect); + + return CaptureCanvasRect(captureRect, includeBackground); + } + + /// + /// Captura todo el canvas + /// + private RenderTargetBitmap CaptureEntireCanvas(bool includeBackground) + { + _canvas.UpdateLayout(); + var canvasRect = new Rect(0, 0, _canvas.ActualWidth, _canvas.ActualHeight); + return CaptureCanvasRect(canvasRect, includeBackground); + } + + /// + /// Captura un rectángulo específico del canvas en coordenadas de píxeles + /// + private RenderTargetBitmap CaptureCanvasRect(Rect captureRect, bool includeBackground) + { + if (captureRect.Width <= 0 || captureRect.Height <= 0) + throw new ArgumentException("Las dimensiones de captura deben ser mayores que 0"); + + // Configurar escala para alta calidad + var scaleFactor = 2.0; // Factor de escala para mejor calidad + var renderWidth = Math.Max(1, (int)(captureRect.Width * scaleFactor)); + var renderHeight = Math.Max(1, (int)(captureRect.Height * scaleFactor)); + var dpi = 96 * scaleFactor; + + var renderBitmap = new RenderTargetBitmap( + renderWidth, + renderHeight, + dpi, + dpi, + PixelFormats.Pbgra32); + + // Crear canvas temporal para renderizado + var tempCanvas = new Canvas() + { + Width = captureRect.Width * scaleFactor, + Height = captureRect.Height * scaleFactor, + Background = includeBackground ? _canvas.Background : Brushes.White + }; + + tempCanvas.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor); + + // Clonar elementos visibles que intersectan con el área de captura + foreach (UIElement child in _canvas.Children) + { + if (child.Visibility == Visibility.Visible) + { + try + { + var left = Canvas.GetLeft(child); + var top = Canvas.GetTop(child); + + var elementRect = new Rect( + double.IsNaN(left) ? 0 : left, + double.IsNaN(top) ? 0 : top, + child.RenderSize.Width, + child.RenderSize.Height); + + if (captureRect.IntersectsWith(elementRect)) + { + var visualBrush = new VisualBrush(child) + { + Stretch = Stretch.None, + AlignmentX = AlignmentX.Left, + AlignmentY = AlignmentY.Top + }; + + var rect = new Rectangle() + { + Width = child.RenderSize.Width * scaleFactor, + Height = child.RenderSize.Height * scaleFactor, + Fill = visualBrush + }; + + Canvas.SetLeft(rect, (elementRect.X - captureRect.X) * scaleFactor); + Canvas.SetTop(rect, (elementRect.Y - captureRect.Y) * scaleFactor); + + tempCanvas.Children.Add(rect); + } + } + catch (Exception ex) + { + Debug.WriteLine($"[ScreenshotManager] Error procesando elemento: {ex.Message}"); + } + } + } + + // Renderizar canvas temporal + var scaledSize = new Size(captureRect.Width * scaleFactor, captureRect.Height * scaleFactor); + tempCanvas.Measure(scaledSize); + tempCanvas.Arrange(new Rect(0, 0, scaledSize.Width, scaledSize.Height)); + tempCanvas.UpdateLayout(); + + renderBitmap.Render(tempCanvas); + return renderBitmap; + } + + /// + /// Procesa la salida del screenshot (guardar archivo y/o generar base64) + /// + private ScreenshotResult ProcessScreenshotOutput(ScreenshotResult result, string filename, bool saveToFile, bool returnBase64) + { + try + { + // Generar nombre de archivo si no se proporcionó + if (string.IsNullOrEmpty(filename)) + { + var typePrefix = result.CaptureType.ToString().ToLower(); + filename = $"{typePrefix}_screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.png"; + } + + // Asegurar extensión .png + if (!filename.ToLower().EndsWith(".png")) + filename += ".png"; + + // Guardar archivo si se solicita + if (saveToFile) + { + Directory.CreateDirectory(_defaultScreenshotsDirectory); + var fullPath = System.IO.Path.IsPathRooted(filename) ? filename : System.IO.Path.Combine(_defaultScreenshotsDirectory, filename); + + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(result.Bitmap)); + + using (var fileStream = new FileStream(fullPath, FileMode.Create)) + { + encoder.Save(fileStream); + } + + var fileInfo = new FileInfo(fullPath); + result.FilePath = fullPath; + result.FileName = System.IO.Path.GetFileName(fullPath); + result.Directory = System.IO.Path.GetDirectoryName(fullPath); + result.FileSizeBytes = fileInfo.Length; + } + + // Generar base64 si se solicita + if (returnBase64) + { + using (var memoryStream = new MemoryStream()) + { + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(result.Bitmap)); + encoder.Save(memoryStream); + result.Base64Data = Convert.ToBase64String(memoryStream.ToArray()); + } + } + + result.Timestamp = DateTime.Now; + return result; + } + catch (Exception ex) + { + result.Success = false; + result.ErrorMessage = $"Error procesando salida: {ex.Message}"; + result.ErrorType = ex.GetType().Name; + return result; + } + } + + #endregion + } + + #region Data Classes + + /// + /// Resultado de una operación de screenshot + /// + public class ScreenshotResult + { + public bool Success { get; set; } + public string ErrorMessage { get; set; } + public string ErrorType { get; set; } + public RenderTargetBitmap Bitmap { get; set; } + public ScreenshotType CaptureType { get; set; } + public string FilePath { get; set; } + public string FileName { get; set; } + public string Directory { get; set; } + public long FileSizeBytes { get; set; } + public string Base64Data { get; set; } + public DateTime Timestamp { get; set; } + public bool IncludeBackground { get; set; } + public List CapturedObjects { get; set; } = new List(); + public AreaInfo BoundingBox { get; set; } + public AreaInfo CaptureArea { get; set; } + public float PaddingMeters { get; set; } + } + + /// + /// Información de un objeto capturado + /// + public class CapturedObjectInfo + { + public string Id { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public float Left { get; set; } + public float Top { get; set; } + public float Width { get; set; } + public float Height { get; set; } + public float CenterX { get; set; } + public float CenterY { get; set; } + } + + /// + /// Información de un área + /// + public class AreaInfo + { + public float Left { get; set; } + public float Top { get; set; } + public float Width { get; set; } + public float Height { get; set; } + public float CenterX { get; set; } + public float CenterY { get; set; } + } + + /// + /// Área de screenshot en coordenadas de metros + /// + internal class ScreenshotArea + { + public float Left { get; set; } + public float Top { get; set; } + public float Width { get; set; } + public float Height { get; set; } + } + + /// + /// Tipos de captura de screenshot + /// + public enum ScreenshotType + { + Objects, + Area, + FullCanvas + } + + #endregion +} diff --git a/analyze_screenshots.py b/analyze_screenshots.py index 7fa6555..5091b4f 100644 --- a/analyze_screenshots.py +++ b/analyze_screenshots.py @@ -276,7 +276,9 @@ def analyze_all_screenshots(screenshots_dir): if __name__ == "__main__": # Directorio donde están los screenshots - screenshots_dir = r"C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\SimCtrEditor\screenshots" + screenshots_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "screenshots" + ) if not os.path.exists(screenshots_dir): print(f"ERROR: El directorio {screenshots_dir} no existe") diff --git a/test_python_execution.py b/test_python_execution.py deleted file mode 100644 index 1240a72..0000000 --- a/test_python_execution.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify Python execution capability in CtrEditor via MCP -""" - -import json -import socket -import time - - -def send_mcp_request(host="localhost", port=5006, method="tools/call", params=None): - """Send MCP request to CtrEditor""" - try: - # Create socket connection - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(10) - sock.connect((host, port)) - - # Prepare request - request = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}} - - # Send request - message = json.dumps(request) + "\n" - sock.send(message.encode("utf-8")) - - # Receive response - response_data = "" - while True: - chunk = sock.recv(4096).decode("utf-8") - if not chunk: - break - response_data += chunk - if "\n" in response_data: - break - - sock.close() - - # Parse response - if response_data.strip(): - return json.loads(response_data.strip()) - return None - - except Exception as e: - print(f"Error sending request: {e}") - return None - - -def test_python_execution(): - """Test Python execution functionality""" - print("Testing Python execution in CtrEditor...") - - # Test 1: Simple Python code - print("\n1. Testing simple Python execution...") - params = { - "name": "execute_python", - "arguments": { - "code": "result = 2 + 2\nprint(f'Result: {result}')", - "return_variables": ["result"], - }, - } - - response = send_mcp_request(params=params) - if response: - print(f"Response: {json.dumps(response, indent=2)}") - else: - print("No response received") - - # Test 2: Access CtrEditor objects - print("\n2. Testing CtrEditor object access...") - params = { - "name": "execute_python", - "arguments": { - "code": """ -# Access CtrEditor objects -object_count = len(objects) if objects else 0 -canvas_info = f"Canvas size: {canvas.Width}x{canvas.Height}" if canvas else "No canvas" -app_info = f"App type: {type(app).__name__}" if app else "No app" - -print(f"Object count: {object_count}") -print(canvas_info) -print(app_info) -""", - "return_variables": ["object_count", "canvas_info", "app_info"], - }, - } - - response = send_mcp_request(params=params) - if response: - print(f"Response: {json.dumps(response, indent=2)}") - else: - print("No response received") - - # Test 3: Get Python help - print("\n3. Testing Python help...") - params = {"name": "python_help", "arguments": {}} - - response = send_mcp_request(params=params) - if response: - print(f"Response: {json.dumps(response, indent=2)}") - else: - print("No response received") - - -if __name__ == "__main__": - test_python_execution()