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.
This commit is contained in:
Miguel 2025-09-08 21:32:04 +02:00
parent 181a3db41c
commit 3153f38068
9 changed files with 818 additions and 1622 deletions

View File

@ -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

View File

@ -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

View File

@ -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
## <20> 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
### <20> 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)
## <20> 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
}}
```
## <20> 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`
## <20>🚨 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.

View File

@ -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

View File

@ -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]`.

View File

@ -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();
}
/// <summary>
/// Obtiene el ScreenshotManager, inicializándolo si es necesario
/// </summary>
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;
}
/// <summary>
/// Inicia el servidor MCP TCP
/// </summary>
@ -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
}
/// <summary>
/// Toma una captura de pantalla del canvas
/// Toma una captura de pantalla del canvas usando el nuevo ScreenshotManager
/// </summary>
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<bool>() ?? false; // Por defecto false
var filename = arguments["filename"]?.ToString();
var includeBackground = arguments["include_background"]?.ToObject<bool>() ?? false;
var saveFile = arguments["save_file"]?.ToObject<bool>() ?? true;
var returnBase64 = arguments["return_base64"]?.ToObject<bool>() ?? true;
var x = arguments["x"]?.ToObject<float?>();
var y = arguments["y"]?.ToObject<float?>();
var width = arguments["width"]?.ToObject<float?>();
var height = arguments["height"]?.ToObject<float?>();
var centerCoordinates = arguments["center_coordinates"]?.ToObject<bool>() ?? 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
/// <summary>
/// Toma una captura de pantalla del canvas
/// Toma una captura de pantalla de objetos específicos por sus IDs usando el nuevo ScreenshotManager
/// </summary>
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<string[]>();
if (objectIds == null || objectIds.Length == 0)
throw new ArgumentException("object_ids is required and must not be empty");
var paddingMeters = arguments["padding_meters"]?.ToObject<float>() ?? 0.5f;
var filename = arguments["filename"]?.ToString();
var includeBackground = arguments["include_background"]?.ToObject<bool>() ?? false;
var saveFile = arguments["save_file"]?.ToObject<bool>() ?? true;
var returnBase64 = arguments["return_base64"]?.ToObject<bool>() ?? 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
};
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// Captura screenshot de objetos específicos por sus IDs
/// </summary>
/// <param name="objectIds">Array de IDs de objetos a capturar</param>
/// <param name="paddingMeters">Padding adicional alrededor de los objetos en metros</param>
/// <param name="filename">Nombre del archivo (opcional)</param>
/// <param name="includeBackground">Si incluir imagen de fondo</param>
/// <param name="saveToFile">Si guardar el archivo</param>
/// <param name="returnBase64">Si retornar la imagen como base64</param>
/// <returns>Resultado de la captura</returns>
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
};
}
}
/// <summary>
/// Captura screenshot de un área específica en coordenadas de metros
/// </summary>
/// <param name="left">Coordenada X izquierda en metros</param>
/// <param name="top">Coordenada Y superior en metros</param>
/// <param name="width">Ancho en metros</param>
/// <param name="height">Alto en metros</param>
/// <param name="filename">Nombre del archivo (opcional)</param>
/// <param name="includeBackground">Si incluir imagen de fondo</param>
/// <param name="saveToFile">Si guardar el archivo</param>
/// <param name="returnBase64">Si retornar la imagen como base64</param>
/// <returns>Resultado de la captura</returns>
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
};
}
}
/// <summary>
/// Captura screenshot de un área centrada en coordenadas específicas
/// </summary>
/// <param name="centerX">Coordenada X del centro en metros</param>
/// <param name="centerY">Coordenada Y del centro en metros</param>
/// <param name="width">Ancho en metros</param>
/// <param name="height">Alto en metros</param>
/// <param name="filename">Nombre del archivo (opcional)</param>
/// <param name="includeBackground">Si incluir imagen de fondo</param>
/// <param name="saveToFile">Si guardar el archivo</param>
/// <param name="returnBase64">Si retornar la imagen como base64</param>
/// <returns>Resultado de la captura</returns>
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);
}
/// <summary>
/// Captura screenshot de todo el canvas
/// </summary>
/// <param name="filename">Nombre del archivo (opcional)</param>
/// <param name="includeBackground">Si incluir imagen de fondo</param>
/// <param name="saveToFile">Si guardar el archivo</param>
/// <param name="returnBase64">Si retornar la imagen como base64</param>
/// <returns>Resultado de la captura</returns>
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
/// <summary>
/// Busca objetos por sus IDs
/// </summary>
private List<osBase> FindObjectsByIds(string[] objectIds)
{
var allObjects = _mainViewModel.ObjetosSimulables.ToList();
var result = new List<osBase>();
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;
}
/// <summary>
/// Calcula el bounding box de un conjunto de objetos usando coordenadas directas
/// </summary>
private ScreenshotArea CalculateObjectsBoundingBox(List<osBase> 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
};
}
/// <summary>
/// Captura un área específica del canvas
/// </summary>
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);
}
/// <summary>
/// Captura todo el canvas
/// </summary>
private RenderTargetBitmap CaptureEntireCanvas(bool includeBackground)
{
_canvas.UpdateLayout();
var canvasRect = new Rect(0, 0, _canvas.ActualWidth, _canvas.ActualHeight);
return CaptureCanvasRect(canvasRect, includeBackground);
}
/// <summary>
/// Captura un rectángulo específico del canvas en coordenadas de píxeles
/// </summary>
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;
}
/// <summary>
/// Procesa la salida del screenshot (guardar archivo y/o generar base64)
/// </summary>
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
/// <summary>
/// Resultado de una operación de screenshot
/// </summary>
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<CapturedObjectInfo> CapturedObjects { get; set; } = new List<CapturedObjectInfo>();
public AreaInfo BoundingBox { get; set; }
public AreaInfo CaptureArea { get; set; }
public float PaddingMeters { get; set; }
}
/// <summary>
/// Información de un objeto capturado
/// </summary>
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; }
}
/// <summary>
/// Información de un área
/// </summary>
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; }
}
/// <summary>
/// Área de screenshot en coordenadas de metros
/// </summary>
internal class ScreenshotArea
{
public float Left { get; set; }
public float Top { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
/// <summary>
/// Tipos de captura de screenshot
/// </summary>
public enum ScreenshotType
{
Objects,
Area,
FullCanvas
}
#endregion
}

View File

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

View File

@ -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()