mcp_server_plugin.pyp
1 """ 2 Cinema 4D MCP Server Plugin 3 Updated for Cinema 4D R2025 compatibility 4 Version 0.1.8 - Context awareness 5 """ 6 7 import c4d 8 from c4d import gui 9 import socket 10 import threading 11 import json 12 import time 13 import math 14 import queue 15 import os 16 import sys 17 import base64 18 import traceback 19 20 PLUGIN_ID = 1057843 # Unique plugin ID for SpecialEventAdd 21 22 # Add DreamTalk parent to Python path for introspection module access 23 # Resolve symlink to find the real plugin location, then navigate to DreamTalk's parent 24 # Plugin is at: DreamTalk/mcp-servers/cinema4d-mcp/c4d_plugin/mcp_server_plugin.pyp 25 # We need: /Users/davidrug/ProjectLiminality (parent of DreamTalk) 26 _plugin_path = os.path.realpath(__file__) 27 _dreamtalk_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(_plugin_path)))) 28 _dreamtalk_parent = os.path.dirname(_dreamtalk_root) # Go up one more level 29 if _dreamtalk_parent not in sys.path: 30 sys.path.insert(0, _dreamtalk_parent) 31 print(f"[C4D MCP] Added to path: {_dreamtalk_parent}") 32 33 # Check Cinema 4D version and log compatibility info 34 C4D_VERSION = c4d.GetC4DVersion() 35 C4D_VERSION_MAJOR = C4D_VERSION // 1000 36 C4D_VERSION_MINOR = (C4D_VERSION // 100) % 10 37 print(f"[C4D MCP] Running on Cinema 4D R{C4D_VERSION_MAJOR}{C4D_VERSION_MINOR}") 38 print(f"[C4D MCP] Python version: {sys.version}") 39 40 # Warn if using unsupported version 41 if C4D_VERSION_MAJOR < 20: 42 print( 43 "[C4D MCP] ## Warning ##: This plugin is in development for Cinema 4D 2025 or later with plans to futher support earlier versions. Some features may not work correctly." 44 ) 45 46 47 class C4DSocketServer(threading.Thread): 48 """Socket Server running in a background thread, sending logs & status via queue.""" 49 50 def __init__(self, msg_queue, host="127.0.0.1", port=5555): 51 super(C4DSocketServer, self).__init__() 52 self.host = host 53 self.port = port 54 self.socket = None 55 self.running = False 56 self.msg_queue = msg_queue # Queue to communicate with UI 57 self.daemon = True # Ensures cleanup on shutdown 58 59 # --- ADDED FOR CONTEXT AWARENESS --- 60 self._object_name_registry = ( 61 {} 62 ) # OLDs? --Maps GUID -> requested_name AND requested_name -> GUID (less robust) 63 64 self._name_to_guid_registry = ( 65 {} 66 ) # Maps requested_name.lower() or actual_name.lower() -> guid 67 self._guid_to_name_registry = ( 68 {} 69 ) # Maps guid -> {'requested_name': str, 'actual_name': str} 70 71 def log(self, message): 72 """Send log messages to UI via queue and trigger an event.""" 73 self.msg_queue.put(("LOG", message)) 74 c4d.SpecialEventAdd(PLUGIN_ID) # Notify UI thread 75 76 def update_status(self, status): 77 """Update status via queue and trigger an event.""" 78 self.msg_queue.put(("STATUS", status)) 79 c4d.SpecialEventAdd(PLUGIN_ID) 80 81 def execute_on_main_thread(self, func, args=None, kwargs=None, _timeout=None): 82 """Execute a function on the main thread using a thread-safe queue and special event. 83 84 Since CallMainThread is not available in the Python SDK (R2025), we use 85 a thread-safe approach by queuing the function and triggering it via SpecialEventAdd. 86 87 Args: 88 func: The function to execute on the main thread 89 *args: Arguments to pass to the function 90 **kwargs: Keyword arguments to pass to the function 91 Special keyword '_timeout': Override default timeout (in seconds) 92 93 Returns: 94 The result of executing the function on the main thread 95 """ 96 args = args or () 97 kwargs = kwargs or {} 98 99 # Extract the timeout parameter if provided, or use default 100 timeout = kwargs.pop("_timeout", None) 101 102 # Set appropriate timeout based on operation type 103 if timeout is None: 104 # Use different default timeouts based on the function name 105 func_name = func.__name__ if hasattr(func, "__name__") else str(func) 106 107 if "render" in func_name.lower(): 108 timeout = 120 # 2 minutes for rendering 109 self.log(f"[C4D] Using extended timeout (120s) for rendering operation") 110 elif "save" in func_name.lower(): 111 timeout = 60 # 1 minute for saving 112 self.log(f"[C4D] Using extended timeout (60s) for save operation") 113 elif "field" in func_name.lower(): 114 timeout = 30 # 30 seconds for field operations 115 self.log(f"[C4D] Using extended timeout (30s) for field operation") 116 else: 117 timeout = 15 # Default timeout increased to 15 seconds 118 119 self.log(f"[C4D] Main thread execution will timeout after {timeout}s") 120 121 # Create a thread-safe container for the result 122 result_container = {"result": None, "done": False} 123 124 # Define a wrapper that will be executed on the main thread 125 def main_thread_exec(): 126 try: 127 self.log( 128 f"[C4D] Starting main thread execution of {func.__name__ if hasattr(func, '__name__') else 'function'}" 129 ) 130 start_time = time.time() 131 result_container["result"] = func(*args, **kwargs) 132 execution_time = time.time() - start_time 133 self.log( 134 f"[C4D] Main thread execution completed in {execution_time:.2f}s" 135 ) 136 except Exception as e: 137 self.log( 138 f"[**ERROR**] Error executing function on main thread: {str(e)}" 139 ) 140 result_container["result"] = {"error": str(e)} 141 finally: 142 result_container["done"] = True 143 return True 144 145 # Queue the request and signal the main thread 146 self.log("[C4D] Queueing function for main thread execution") 147 self.msg_queue.put(("EXEC", main_thread_exec)) 148 c4d.SpecialEventAdd(PLUGIN_ID) # Notify UI thread 149 150 # Wait for the function to complete (with timeout) 151 start_time = time.time() 152 poll_interval = 0.01 # Small sleep to prevent CPU overuse 153 progress_interval = 1.0 # Log progress every second 154 last_progress = 0 155 156 while not result_container["done"]: 157 time.sleep(poll_interval) 158 159 # Calculate elapsed time 160 elapsed = time.time() - start_time 161 162 # Log progress periodically for long-running operations 163 if int(elapsed) > last_progress: 164 if elapsed > 5: # Only start logging after 5 seconds 165 self.log( 166 f"[C4D] Waiting for main thread execution ({elapsed:.1f}s elapsed)" 167 ) 168 last_progress = int(elapsed) 169 170 # Check for timeout 171 if elapsed > timeout: 172 self.log(f"[C4D] Main thread execution timed out after {elapsed:.2f}s") 173 return {"error": f"Execution on main thread timed out after {timeout}s"} 174 175 # Improved result handling 176 if result_container["result"] is None: 177 self.log( 178 "[C4D] ## Warning ##: Function execution completed but returned None" 179 ) 180 # Return a structured response instead of None 181 return { 182 "status": "completed", 183 "result": None, 184 "warning": "Function returned None", 185 } 186 187 return result_container["result"] 188 189 def run(self): 190 """Main server loop""" 191 try: 192 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 193 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 194 self.socket.bind((self.host, self.port)) 195 self.socket.listen(5) 196 self.running = True 197 self.update_status("Online") 198 self.log(f"[C4D] Server started on {self.host}:{self.port}") 199 200 while self.running: 201 client, addr = self.socket.accept() 202 self.log(f"[C4D] Client connected from {addr}") 203 threading.Thread(target=self.handle_client, args=(client,)).start() 204 205 except Exception as e: 206 self.log(f"[C4D] Server Error: {str(e)}") 207 self.update_status("Offline") 208 self.running = False 209 210 def handle_client(self, client): 211 """Handle incoming client connections.""" 212 buffer = "" 213 try: 214 while self.running: 215 data = client.recv(4096) 216 if not data: 217 break 218 219 # Add received data to buffer 220 buffer += data.decode("utf-8") 221 222 # Process complete messages (separated by newlines) 223 while "\n" in buffer: 224 message, buffer = buffer.split("\n", 1) 225 self.log(f"[C4D] Received: {message}") 226 227 try: 228 # Parse the command 229 command = json.loads(message) 230 command_type = command.get("command", "") 231 232 # Scene info & execution 233 if command_type == "get_scene_info": 234 response = self.handle_get_scene_info() 235 elif command_type == "list_objects": 236 response = self.handle_list_objects() 237 elif command_type == "group_objects": 238 response = self.handle_group_objects(command) 239 elif command_type == "execute_python": 240 response = self.handle_execute_python(command) 241 elif command_type == "save_scene": 242 response = self.handle_save_scene(command) 243 elif command_type == "load_scene": 244 response = self.handle_load_scene(command) 245 elif command_type == "set_keyframe": 246 response = self.handle_set_keyframe(command) 247 # Object creation & modification 248 elif command_type == "add_primitive": 249 response = self.handle_add_primitive(command) 250 elif command_type == "modify_object": 251 response = self.handle_modify_object(command) 252 elif command_type == "create_abstract_shape": 253 response = self.handle_create_abstract_shape(command) 254 # Materials & shaders 255 elif command_type == "create_material": 256 response = self.handle_create_material(command) 257 elif command_type == "apply_material": 258 response = self.handle_apply_material(command) 259 elif command_type == "apply_shader": 260 response = self.handle_apply_shader(command) 261 elif command_type == "validate_redshift_materials": 262 response = self.handle_validate_redshift_materials(command) 263 # Rendering & preview 264 elif command_type == "render_frame": 265 response = self.handle_render_frame(command) 266 elif command_type == "render_preview": 267 response = self.handle_render_preview_base64() 268 elif command_type == "snapshot_scene": 269 response = self.handle_snapshot_scene(command) 270 # Camera & light handling 271 elif command_type == "create_camera": 272 response = self.handle_create_camera(command) 273 elif command_type == "animate_camera": 274 response = self.handle_animate_camera(command) 275 elif command_type == "create_light": 276 response = self.handle_create_light(command) 277 # MoGraph/dynamics 278 elif command_type == "create_mograph_cloner": 279 response = self.handle_create_mograph_cloner(command) 280 elif command_type == "add_effector": 281 response = self.handle_add_effector(command) 282 elif command_type == "apply_mograph_fields": 283 response = self.handle_apply_mograph_fields(command) 284 elif command_type == "create_soft_body": 285 response = self.handle_create_soft_body(command) 286 elif command_type == "apply_dynamics": 287 response = self.handle_apply_dynamics(command) 288 elif command_type == "describe_hierarchy": 289 response = self.handle_describe_hierarchy(command) 290 elif command_type == "inspect_object": 291 response = self.handle_inspect_object(command) 292 elif command_type == "inspect_materials": 293 response = self.handle_inspect_materials(command) 294 elif command_type == "inspect_animation": 295 response = self.handle_inspect_animation(command) 296 elif command_type == "validate_scene": 297 response = self.handle_validate_scene(command) 298 elif command_type == "inspect_xpresso": 299 response = self.handle_inspect_xpresso(command) 300 elif command_type == "diff_scene": 301 response = self.handle_diff_scene(command) 302 elif command_type == "reset_snapshot": 303 response = self.handle_reset_snapshot(command) 304 elif command_type == "viewport_preview": 305 response = self.handle_viewport_preview(command) 306 elif command_type == "rendered_preview": 307 response = self.handle_rendered_preview(command) 308 elif command_type == "describe_scene": 309 response = self.handle_describe_scene(command) 310 elif command_type == "run_dreamtalk": 311 response = self.handle_run_dreamtalk(command) 312 else: 313 response = {"error": f"Unknown command: {command_type}"} 314 315 # Send the response as JSON 316 response_json = json.dumps(response) + "\n" 317 client.sendall(response_json.encode("utf-8")) 318 self.log(f"[C4D] Sent response for {command_type}") 319 320 except json.JSONDecodeError: 321 error_response = {"error": "Invalid JSON format"} 322 client.sendall( 323 (json.dumps(error_response) + "\n").encode("utf-8") 324 ) 325 except Exception as e: 326 error_response = { 327 "error": f"Error processing command: {str(e)}" 328 } 329 client.sendall( 330 (json.dumps(error_response) + "\n").encode("utf-8") 331 ) 332 self.log(f"[**ERROR**] Error processing command: {str(e)}") 333 334 except Exception as e: 335 self.log(f"[C4D] Client error: {str(e)}") 336 finally: 337 client.close() 338 self.log("[C4D] Client disconnected") 339 340 def stop(self): 341 """Stop the server.""" 342 self.running = False 343 if self.socket: 344 self.socket.close() 345 self.update_status("Offline") 346 self.log("[C4D] Server stopped") 347 348 # Basic commands 349 def handle_get_scene_info(self): 350 """Handle get_scene_info command.""" 351 doc = c4d.documents.GetActiveDocument() 352 353 # Get scene information 354 scene_info = { 355 "filename": doc.GetDocumentName() or "Untitled", 356 "object_count": self.count_objects(doc), 357 "polygon_count": self.count_polygons(doc), 358 "material_count": len(doc.GetMaterials()), 359 "current_frame": doc.GetTime().GetFrame(doc.GetFps()), 360 "fps": doc.GetFps(), 361 "frame_start": doc.GetMinTime().GetFrame(doc.GetFps()), 362 "frame_end": doc.GetMaxTime().GetFrame(doc.GetFps()), 363 } 364 365 return {"scene_info": scene_info} 366 367 def count_objects(self, doc): 368 """Count all objects in the document.""" 369 count = 0 370 obj = doc.GetFirstObject() 371 while obj: 372 count += 1 373 obj = obj.GetNext() 374 return count 375 376 def count_polygons(self, doc): 377 """Count all polygons in the document.""" 378 count = 0 379 obj = doc.GetFirstObject() 380 while obj: 381 if obj.GetType() == c4d.Opolygon: 382 count += obj.GetPolygonCount() 383 obj = obj.GetNext() 384 return count 385 386 def get_object_type_name(self, obj): 387 """Get a human-readable object type name.""" 388 type_id = obj.GetType() 389 390 # Expanded type map including MoGraph objects 391 type_map = { 392 c4d.Ocube: "Cube", 393 c4d.Osphere: "Sphere", 394 c4d.Ocone: "Cone", 395 c4d.Ocylinder: "Cylinder", 396 c4d.Odisc: "Disc", 397 c4d.Ocapsule: "Capsule", 398 c4d.Otorus: "Torus", 399 c4d.Otube: "Tube", 400 c4d.Oplane: "Plane", 401 c4d.Olight: "Light", 402 c4d.Ocamera: "Camera", 403 c4d.Onull: "Null", 404 c4d.Opolygon: "Polygon Object", 405 c4d.Ospline: "Spline", 406 c4d.Omgcloner: "MoGraph Cloner", # MoGraph Cloner 407 } 408 409 # Check for MoGraph objects using ranges 410 if 1018544 <= type_id <= 1019544: # MoGraph objects general range 411 if type_id == c4d.Omgcloner: 412 return "MoGraph Cloner" 413 elif type_id == c4d.Omgtext: 414 return "MoGraph Text" 415 elif type_id == c4d.Omgtracer: 416 return "MoGraph Tracer" 417 elif type_id == c4d.Omgmatrix: 418 return "MoGraph Matrix" 419 else: 420 return "MoGraph Object" 421 422 # MoGraph Effectors 423 if 1019544 <= type_id <= 1019644: 424 if type_id == c4d.Omgrandom: 425 return "Random Effector" 426 elif type_id == c4d.Omgstep: 427 return "Step Effector" 428 elif type_id == c4d.Omgformula: 429 return "Formula Effector" 430 else: 431 return "MoGraph Effector" 432 433 # Fields (newer Cinema 4D versions) 434 if 1039384 <= type_id <= 1039484: 435 field_types = { 436 1039384: "Spherical Field", 437 1039385: "Box Field", 438 1039386: "Cylindrical Field", 439 1039387: "Torus Field", 440 1039388: "Cone Field", 441 1039389: "Linear Field", 442 1039390: "Radial Field", 443 1039394: "Noise Field", 444 } 445 return field_types.get(type_id, "Field") 446 447 return type_map.get(type_id, f"Object (Type: {type_id})") 448 449 def find_object_by_name(self, doc, name_or_guid, use_guid=False): 450 """Find object by GUID (preferred) or name, using local registry first. FIX for recursion and GUID format check.""" 451 if not name_or_guid: 452 self.log("[C4D FIND] Cannot find object: No name or GUID provided.") 453 return None 454 if not doc: 455 self.log("[C4D FIND] ## Error ##: No document provided for search.") 456 return None 457 if not hasattr(self, "_name_to_guid_registry"): 458 self._name_to_guid_registry = {} 459 if not hasattr(self, "_guid_to_name_registry"): 460 self._guid_to_name_registry = {} 461 462 search_term = str(name_or_guid).strip() 463 self.log( 464 f"[C4D FIND] Attempting to find: '{search_term}' (Treat as GUID: {use_guid})" 465 ) 466 467 # --- GUID Search Logic --- 468 if use_guid: 469 guid_to_find = search_term 470 # --- FIXED: GUID format check --- 471 # C4D GUIDs converted with str() are typically long numbers (sometimes negative). 472 # Check if it's likely numeric and long enough. Hyphen is NOT required. 473 is_valid_guid_format = False 474 if guid_to_find: # Check if not empty 475 try: 476 int(guid_to_find) # Check if it can be interpreted as an integer 477 if len(guid_to_find) > 10: # Check if it's reasonably long 478 is_valid_guid_format = True 479 except ValueError: 480 is_valid_guid_format = False # Not purely numeric 481 # --- END FIXED --- 482 483 if not is_valid_guid_format: 484 self.log( 485 f"[C4D FIND] ## Warning ##: Invalid format/length for GUID search: '{guid_to_find}'. Treating as name." 486 ) 487 use_guid = False # Fallback to name search 488 else: 489 # 1. Try direct C4D SearchObject 490 obj_from_search = doc.SearchObject(guid_to_find) 491 if obj_from_search: 492 self.log( 493 f"[C4D FIND] Success (GUID Scene Search): Found '{obj_from_search.GetName()}' (GUID: {guid_to_find})" 494 ) 495 current_actual_name = obj_from_search.GetName() 496 reg_entry = self._guid_to_name_registry.get(guid_to_find) 497 if ( 498 not reg_entry 499 or reg_entry.get("actual_name") != current_actual_name 500 ): 501 req_name = ( 502 reg_entry.get("requested_name", current_actual_name) 503 if reg_entry 504 else current_actual_name 505 ) 506 self.register_object_name(obj_from_search, req_name) 507 return obj_from_search 508 509 # 2. Manual iteration fallback 510 self.log( 511 f"[C4D FIND] Info: doc.SearchObject failed for GUID {guid_to_find}. Iterating manually..." 512 ) 513 all_objects = self._get_all_objects(doc) 514 found_obj_manual = None 515 for obj_iter in all_objects: 516 try: 517 iter_guid = str(obj_iter.GetGUID()) 518 if iter_guid == guid_to_find: 519 self.log( 520 f"[C4D FIND] Success (GUID Manual Iteration): Found '{obj_iter.GetName()}' (GUID: {guid_to_find})" 521 ) 522 found_obj_manual = obj_iter 523 break 524 except Exception as e_iter: 525 self.log( 526 f"[C4D FIND] Error checking GUID during iteration for '{obj_iter.GetName()}': {e_iter}" 527 ) 528 529 if found_obj_manual: 530 current_actual_name = found_obj_manual.GetName() 531 reg_entry = self._guid_to_name_registry.get(guid_to_find) 532 if ( 533 reg_entry 534 and reg_entry.get("actual_name") != current_actual_name 535 ): 536 req_name = reg_entry.get("requested_name", current_actual_name) 537 self.register_object_name(found_obj_manual, req_name) 538 elif not reg_entry: 539 self.register_object_name( 540 found_obj_manual, found_obj_manual.GetName() 541 ) 542 return found_obj_manual 543 544 # 3. If both failed, cleanup registry 545 self.log( 546 f"[C4D FIND] Failed (GUID): Object with GUID '{guid_to_find}' not found by SearchObject or Manual Iteration." 547 ) 548 if guid_to_find in self._guid_to_name_registry: 549 self.log( 550 f"[C4D FIND] Cleaning registry for supposedly existing but unfound GUID {guid_to_find}." 551 ) 552 reg_entry = self._guid_to_name_registry.pop(guid_to_find, None) 553 if reg_entry: 554 req_name_lower = reg_entry.get("requested_name", "").lower() 555 act_name_lower = reg_entry.get("actual_name", "").lower() 556 if req_name_lower: 557 self._name_to_guid_registry.pop(req_name_lower, None) 558 if act_name_lower and act_name_lower != req_name_lower: 559 self._name_to_guid_registry.pop(act_name_lower, None) 560 return None 561 562 # --- Name Search Logic (Keep as is from previous correction) --- 563 name_to_find_lower = search_term.lower() 564 565 # 1. Check registry by name -> GUID -> Object 566 guid_from_registry = self._name_to_guid_registry.get(name_to_find_lower) 567 if guid_from_registry: 568 obj_from_guid_lookup = self.find_object_by_name( 569 doc, guid_from_registry, use_guid=True 570 ) 571 if obj_from_guid_lookup: 572 stored_names = self._guid_to_name_registry.get(guid_from_registry, {}) 573 actual_name_reg = stored_names.get("actual_name", "").lower() 574 requested_name_reg = stored_names.get("requested_name", "").lower() 575 found_name_actual = obj_from_guid_lookup.GetName().lower() 576 if name_to_find_lower in [ 577 actual_name_reg, 578 requested_name_reg, 579 found_name_actual, 580 ]: 581 self.log( 582 f"[C4D FIND] Success (Registry Name '{search_term}' -> GUID {guid_from_registry}): Found '{obj_from_guid_lookup.GetName()}'" 583 ) 584 if found_name_actual != actual_name_reg: 585 self.register_object_name( 586 obj_from_guid_lookup, 587 stored_names.get("requested_name", search_term), 588 ) 589 return obj_from_guid_lookup 590 else: 591 self.log( 592 f"[C4D FIND] ## Warning ## Registry inconsistency for name '{search_term}'. Continuing search." 593 ) 594 else: 595 self.log( 596 f"[C4D FIND] ## Warning ## Name '{search_term}' maps to non-existent GUID. Cleaning registry." 597 ) 598 self._name_to_guid_registry.pop(name_to_find_lower, None) 599 reg_entry = self._guid_to_name_registry.pop(guid_from_registry, None) 600 if reg_entry: 601 other_name_key = ( 602 "actual_name" 603 if name_to_find_lower 604 == reg_entry.get("requested_name", "").lower() 605 else "requested_name" 606 ) 607 other_name_val = reg_entry.get(other_name_key, "").lower() 608 if other_name_val: 609 self._name_to_guid_registry.pop(other_name_val, None) 610 611 # 2. Direct name search 612 all_objects_name = self._get_all_objects(doc) 613 for obj in all_objects_name: 614 if obj.GetName().strip().lower() == name_to_find_lower: 615 self.log( 616 f"[C4D FIND] Success (Direct Name Search): Found '{obj.GetName()}'" 617 ) 618 self.register_object_name(obj, search_term) 619 return obj 620 621 # 3. Comment Tag Search 622 self.log(f"[C4D FIND] Trying comment tag search for '{search_term}'") 623 if hasattr(c4d, "Tcomment"): 624 for obj in all_objects_name: 625 for tag in obj.GetTags(): 626 if tag.GetType() == c4d.Tcomment: 627 try: 628 tag_text = tag[c4d.COMMENTTAG_TEXT] 629 if tag_text and tag_text.startswith("MCP_NAME:"): 630 tagged_name = tag_text[9:].strip() 631 if tagged_name.lower() == name_to_find_lower: 632 self.log( 633 f"[C4D FIND] Success (Comment Tag): Found '{obj.GetName()}'" 634 ) 635 self.register_object_name(obj, search_term) 636 return obj 637 except Exception as e: 638 self.log(f"Error reading comment tag: {e}") 639 640 # 4. User Data Search 641 self.log(f"[C4D FIND] Trying user data search for '{search_term}'") 642 for obj in all_objects_name: 643 try: 644 userdata = obj.GetUserDataContainer() 645 if userdata: 646 for i in range(len(userdata)): 647 desc_id_tuple = obj.GetUserDataContainer()[i] 648 if ( 649 isinstance(desc_id_tuple, tuple) 650 and len(desc_id_tuple) > c4d.DESC_NAME 651 ): 652 if desc_id_tuple[c4d.DESC_NAME] == "mcp_original_name": 653 desc_id = desc_id_tuple[c4d.DESC_ID] 654 if obj[desc_id].strip().lower() == name_to_find_lower: 655 self.log( 656 f"[C4D FIND] Success (User Data): Found '{obj.GetName()}'" 657 ) 658 self.register_object_name(obj, search_term) 659 return obj 660 except Exception as e: 661 self.log(f"Error checking user data for '{obj.GetName()}': {e}") 662 663 # 5. Fuzzy Name Search 664 self.log(f"[C4D FIND] Trying fuzzy name matching for '{search_term}'") 665 similar_objects = [] 666 for obj in all_objects_name: 667 obj_name_lower = obj.GetName().strip().lower() 668 if ( 669 name_to_find_lower in obj_name_lower 670 or obj_name_lower in name_to_find_lower 671 or obj_name_lower.startswith(name_to_find_lower) 672 or name_to_find_lower.startswith(obj_name_lower) 673 ): 674 similarity = abs(len(obj_name_lower) - len(name_to_find_lower)) 675 similar_objects.append((obj, similarity)) 676 if similar_objects: 677 similar_objects.sort(key=lambda pair: pair[1]) 678 closest_match = similar_objects[0][0] 679 self.log( 680 f"[C4D FIND] Success (Fuzzy Fallback): Using '{closest_match.GetName()}' for '{search_term}'" 681 ) 682 self.register_object_name(closest_match, search_term) 683 return closest_match 684 685 # Final failure 686 self.log( 687 f"[C4D FIND] Failed: Object '{search_term}' not found after all checks." 688 ) 689 return None 690 691 def _get_all_objects(self, doc): 692 """Recursively collects all objects in the scene into a flat list.""" 693 result = [] 694 695 def collect_recursive(obj): 696 while obj: 697 result.append(obj) 698 if obj.GetDown(): 699 collect_recursive(obj.GetDown()) 700 obj = obj.GetNext() 701 702 first_obj = doc.GetFirstObject() 703 if first_obj: 704 collect_recursive(first_obj) 705 706 return result 707 708 def get_all_objects_comprehensive(self, doc): 709 """Get all objects in the document using multiple methods to ensure complete coverage. 710 711 This method is specifically designed to catch objects that might be missed by 712 standard GetFirstObject()/GetNext() iteration, particularly MoGraph objects. 713 714 Args: 715 doc: The Cinema 4D document to search 716 717 Returns: 718 List of all objects found 719 """ 720 all_objects = [] 721 found_ids = set() 722 723 # Method 1: Standard traversal using GetFirstObject/GetNext/GetDown 724 self.log("[C4D] Comprehensive search - using standard traversal") 725 726 def traverse_hierarchy(obj): 727 while obj: 728 try: 729 obj_id = str(obj.GetGUID()) 730 if obj_id not in found_ids: 731 all_objects.append(obj) 732 found_ids.add(obj_id) 733 734 # Check children 735 child = obj.GetDown() 736 if child: 737 traverse_hierarchy(child) 738 except Exception as e: 739 self.log(f"[**ERROR**] Error in hierarchy traversal: {str(e)}") 740 741 # Move to next sibling 742 obj = obj.GetNext() 743 744 # Start traversal from the first object 745 first_obj = doc.GetFirstObject() 746 if first_obj: 747 traverse_hierarchy(first_obj) 748 749 # Method 2: Use GetObjects() for flat list (catches some objects) 750 try: 751 self.log("[C4D] Comprehensive search - using GetObjects()") 752 flat_objects = doc.GetObjects() 753 for obj in flat_objects: 754 obj_id = str(obj.GetGUID()) 755 if obj_id not in found_ids: 756 all_objects.append(obj) 757 found_ids.add(obj_id) 758 except Exception as e: 759 self.log(f"[**ERROR**] Error in GetObjects search: {str(e)}") 760 761 # Method 3: Special handling for MoGraph objects 762 try: 763 self.log("[C4D] Comprehensive search - direct access for MoGraph") 764 765 # Direct check for Cloners 766 if hasattr(c4d, "Omgcloner"): 767 # Try using FindObjects if available (R20+) 768 if hasattr(c4d.BaseObject, "FindObjects"): 769 cloners = c4d.BaseObject.FindObjects(doc, c4d.Omgcloner) 770 for cloner in cloners: 771 obj_id = str(cloner.GetGUID()) 772 if obj_id not in found_ids: 773 all_objects.append(cloner) 774 found_ids.add(obj_id) 775 self.log( 776 f"[C4D] Found cloner using FindObjects: {cloner.GetName()}" 777 ) 778 779 # Check for other MoGraph objects if needed 780 # (Add specific searches here if certain objects are still missed) 781 782 except Exception as e: 783 self.log(f"[**ERROR**] Error in MoGraph direct search: {str(e)}") 784 785 self.log( 786 f"[C4D] Comprehensive object search complete, found {len(all_objects)} objects" 787 ) 788 return all_objects 789 790 def handle_group_objects(self, command): 791 """Handle group_objects command with GUID support.""" 792 doc = c4d.documents.GetActiveDocument() 793 if not doc: 794 return {"error": "No active document"} 795 796 requested_group_name = command.get("group_name", "Group") 797 object_identifiers = command.get("object_names", []) 798 position = command.get("position", None) 799 center = command.get("center", False) 800 keep_world_pos = command.get("keep_world_position", True) 801 802 objects_to_group = [] 803 identifiers_found_guids = set() 804 identifiers_not_found = [] 805 806 if object_identifiers: 807 self.log(f"[GROUP] Received identifiers: {object_identifiers}") 808 for identifier in object_identifiers: 809 if not identifier: 810 continue 811 812 identifier_str = str(identifier).strip() 813 # --- REVISED: Detect GUID format correctly --- 814 use_current_id_as_guid = False 815 if "-" in identifier_str and len(identifier_str) > 30: 816 use_current_id_as_guid = True 817 elif identifier_str.isdigit() or ( 818 identifier_str.startswith("-") and identifier_str[1:].isdigit() 819 ): 820 if len(identifier_str) > 10: 821 use_current_id_as_guid = True 822 # --- END REVISED --- 823 824 self.log( 825 f"[GROUP] Finding object by identifier: '{identifier_str}' (Treat as GUID: {use_current_id_as_guid})" 826 ) 827 # --- Pass the correct flag to find_object_by_name --- 828 obj = self.find_object_by_name( 829 doc, identifier_str, use_guid=use_current_id_as_guid 830 ) 831 832 if obj: 833 obj_guid = str(obj.GetGUID()) 834 if obj_guid not in identifiers_found_guids: 835 objects_to_group.append(obj) 836 identifiers_found_guids.add(obj_guid) 837 self.log( 838 f"[GROUP] Found object: '{obj.GetName()}' (GUID: {obj_guid})" 839 ) 840 else: 841 self.log( 842 f"[GROUP] Info: Object '{obj.GetName()}' (GUID: {obj_guid}) already added." 843 ) 844 else: 845 self.log( 846 f"[GROUP] ## Warning ##: Object identifier not found: '{identifier_str}' (Searched as GUID: {use_current_id_as_guid})" 847 ) 848 identifiers_not_found.append(identifier_str) 849 else: 850 objects_to_group = doc.GetActiveObjects( 851 c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER 852 | c4d.GETACTIVEOBJECTFLAGS_TOPLEVEL 853 ) 854 if not objects_to_group: 855 return { 856 "error": "No objects selected (top-level) or specified via 'object_names'." 857 } 858 self.log( 859 f"[GROUP] Fallback: Grouping {len(objects_to_group)} selected top-level objects." 860 ) 861 862 if not objects_to_group: 863 error_msg = "No valid objects found to group." 864 if identifiers_not_found: 865 error_msg += f" Identifiers not found: {identifiers_not_found}" 866 return {"error": error_msg} 867 868 # --- Grouping Logic --- 869 group_null = None 870 try: 871 doc.StartUndo() 872 group_null = c4d.BaseObject(c4d.Onull) 873 group_null.SetName(requested_group_name) 874 doc.InsertObject(group_null, None, None) 875 doc.AddUndo(c4d.UNDOTYPE_NEW, group_null) 876 877 grouped_actual_names = [] 878 grouped_guids = [] 879 original_matrices = {} 880 881 # Calculate center 882 group_center_pos = c4d.Vector(0) 883 # ... (keep centering logic as before) ... 884 if center: 885 min_vec, max_vec = c4d.Vector(float("inf")), c4d.Vector(float("-inf")) 886 count = 0 887 for obj in objects_to_group: 888 try: 889 rad, mp = obj.GetRad(), obj.GetMp() 890 min_vec.x, min_vec.y, min_vec.z = ( 891 min(min_vec.x, mp.x - rad.x), 892 min(min_vec.y, mp.y - rad.y), 893 min(min_vec.z, mp.z - rad.z), 894 ) 895 max_vec.x, max_vec.y, max_vec.z = ( 896 max(max_vec.x, mp.x + rad.x), 897 max(max_vec.y, mp.y + rad.y), 898 max(max_vec.z, mp.z + rad.z), 899 ) 900 count += 1 901 except Exception as e_bounds: 902 self.log( 903 f"[GROUP] Warning: Error getting bounds for '{obj.GetName()}': {e_bounds}" 904 ) 905 if count > 0: 906 group_center_pos = (min_vec + max_vec) * 0.5 907 self.log(f"[GROUP] Calculated center for null: {group_center_pos}") 908 else: 909 center = False 910 self.log( 911 "[GROUP] Warning: Could not calculate center, disabling centering." 912 ) 913 914 # Reparent 915 for obj in reversed(objects_to_group): 916 try: 917 obj_name = obj.GetName() 918 obj_guid = str(obj.GetGUID()) 919 grouped_actual_names.append(obj_name) 920 grouped_guids.append(obj_guid) 921 if keep_world_pos: 922 original_matrices[obj_guid] = obj.GetMg() 923 obj.Remove() 924 obj.InsertUnder(group_null) 925 doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj) 926 except Exception as e_reparent: 927 self.log( 928 f"[**ERROR**] Failed to reparent object '{obj_name}': {e_reparent}" 929 ) 930 931 # Set Position 932 if isinstance(position, list) and len(position) == 3: 933 try: 934 target_pos = c4d.Vector( 935 float(position[0]), float(position[1]), float(position[2]) 936 ) 937 group_null.SetAbsPos(target_pos) 938 doc.AddUndo(c4d.UNDOTYPE_CHANGE, group_null) 939 except (ValueError, TypeError) as e_pos: 940 self.log( 941 f"[GROUP] Warning: Invalid position value '{position}': {e_pos}" 942 ) 943 elif center: 944 group_null.SetAbsPos(group_center_pos) 945 doc.AddUndo(c4d.UNDOTYPE_CHANGE, group_null) 946 947 # Adjust children 948 if keep_world_pos: 949 null_mg_inv = ~group_null.GetMg() 950 for child in group_null.GetChildren(): 951 child_guid = str(child.GetGUID()) 952 if child_guid in original_matrices: 953 new_ml = null_mg_inv * original_matrices[child_guid] 954 child.SetMl(new_ml) 955 doc.AddUndo(c4d.UNDOTYPE_CHANGE, child) 956 else: 957 self.log( 958 f"[GROUP] ## Warning ## Original matrix not found for child '{child.GetName()}'." 959 ) 960 961 doc.EndUndo() 962 c4d.EventAdd() 963 964 # --- Contextual Return --- 965 actual_group_name = group_null.GetName() 966 group_guid = str(group_null.GetGUID()) 967 pos_vector = group_null.GetAbsPos() 968 self.register_object_name(group_null, requested_group_name) 969 response = { 970 "group": { 971 "requested_name": requested_group_name, 972 "actual_name": actual_group_name, 973 "guid": group_guid, 974 "children_actual_names": grouped_actual_names, 975 "children_guids": grouped_guids, 976 "position": [pos_vector.x, pos_vector.y, pos_vector.z], 977 "centered": center, 978 "kept_world_position": keep_world_pos, 979 } 980 } 981 if identifiers_not_found: 982 response["warnings"] = [ 983 f"Object identifier not found: '{idf}'" 984 for idf in identifiers_not_found 985 ] 986 return response 987 988 except Exception as e: 989 doc.EndUndo() 990 error_msg = f"Error during grouping: {str(e)}" 991 self.log(f"[**ERROR**] {error_msg}\n{traceback.format_exc()}") 992 if group_null and group_null.GetDown() is None: 993 try: 994 doc.AddUndo(c4d.UNDOTYPE_DELETE, group_null) 995 group_null.Remove() 996 except: 997 pass 998 return {"error": error_msg, "traceback": traceback.format_exc()} 999 1000 def handle_add_primitive(self, command): 1001 """Handle add_primitive command.""" 1002 doc = c4d.documents.GetActiveDocument() 1003 if not doc: 1004 return {"error": "No active document"} # Added check 1005 1006 primitive_type = command.get("primitive_type") or command.get("type") or "cube" 1007 primitive_type = primitive_type.lower() 1008 1009 # Use provided name or generate one 1010 requested_name = ( 1011 command.get("name") 1012 or command.get("object_name") 1013 or f"MCP_{primitive_type.capitalize()}_{int(time.time()) % 1000}" # Generate unique name 1014 ) 1015 1016 position_list = command.get("position", [0, 0, 0]) 1017 size_list = command.get("size", [50, 50, 50]) # Default size 1018 1019 # --- Safely parse position and size --- 1020 position = [0.0, 0.0, 0.0] 1021 if isinstance(position_list, list) and len(position_list) >= 3: 1022 try: 1023 position = [float(p) for p in position_list[:3]] 1024 except (ValueError, TypeError): 1025 self.log(f"Warning: Invalid position data {position_list}") 1026 else: 1027 self.log(f"Warning: Position data not a list of 3: {position_list}") 1028 1029 size = [50.0, 50.0, 50.0] 1030 if isinstance(size_list, list) and len(size_list) > 0: 1031 try: 1032 size_raw = [float(s) for s in size_list if s is not None] 1033 if not size_raw: 1034 raise ValueError("Empty size list after filtering None") 1035 sx = size_raw[0] 1036 sy = size_raw[1] if len(size_raw) > 1 else sx 1037 sz = size_raw[2] if len(size_raw) > 2 else sx 1038 size = [sx, sy, sz] 1039 except (ValueError, TypeError): 1040 self.log(f"Warning: Invalid size data {size_list}") 1041 elif isinstance(size_list, (int, float)): # Allow single size value 1042 size = [float(size_list)] * 3 1043 else: 1044 self.log(f"Warning: Size data not a list or number: {size_list}") 1045 # --- End safe parse --- 1046 1047 obj = None 1048 try: # Wrap object creation/setting in try-except 1049 # Create the appropriate primitive object 1050 if primitive_type == "cube": 1051 obj = c4d.BaseObject(c4d.Ocube) 1052 obj[c4d.PRIM_CUBE_LEN] = c4d.Vector(*size) 1053 elif primitive_type == "sphere": 1054 obj = c4d.BaseObject(c4d.Osphere) 1055 obj[c4d.PRIM_SPHERE_RAD] = size[0] / 2.0 # Use float division 1056 elif primitive_type == "cone": 1057 obj = c4d.BaseObject(c4d.Ocone) 1058 obj[c4d.PRIM_CONE_TRAD] = 0 1059 obj[c4d.PRIM_CONE_BRAD] = size[0] / 2.0 1060 obj[c4d.PRIM_CONE_HEIGHT] = size[1] 1061 elif primitive_type == "cylinder": 1062 obj = c4d.BaseObject(c4d.Ocylinder) 1063 obj[c4d.PRIM_CYLINDER_RADIUS] = size[0] / 2.0 1064 obj[c4d.PRIM_CYLINDER_HEIGHT] = size[1] 1065 elif primitive_type == "plane": 1066 obj = c4d.BaseObject(c4d.Oplane) 1067 obj[c4d.PRIM_PLANE_WIDTH] = size[0] 1068 obj[c4d.PRIM_PLANE_HEIGHT] = size[1] 1069 elif primitive_type == "pyramid": 1070 obj = c4d.BaseObject(c4d.Opyramid) 1071 if hasattr(c4d, "PRIM_PYRAMID_LEN"): 1072 obj[c4d.PRIM_PYRAMID_LEN] = c4d.Vector(*size) 1073 else: 1074 if hasattr(c4d, "PRIM_PYRAMID_WIDTH"): 1075 obj[c4d.PRIM_PYRAMID_WIDTH] = size[0] 1076 if hasattr(c4d, "PRIM_PYRAMID_HEIGHT"): 1077 obj[c4d.PRIM_PYRAMID_HEIGHT] = size[1] 1078 if hasattr(c4d, "PRIM_PYRAMID_DEPTH"): 1079 obj[c4d.PRIM_PYRAMID_DEPTH] = size[2] 1080 elif primitive_type == "disc": 1081 obj = c4d.BaseObject(c4d.Odisc) 1082 # Use ORAD/IRAD for disc 1083 obj[c4d.PRIM_DISC_ORAD] = size[0] / 2.0 1084 obj[c4d.PRIM_DISC_IRAD] = 0 # Default inner radius 1085 elif primitive_type == "tube": 1086 obj = c4d.BaseObject(c4d.Otube) 1087 obj[c4d.PRIM_TUBE_RADIUS] = size[0] / 2.0 1088 obj[c4d.PRIM_TUBE_IRADIUS] = size[1] / 2.0 1089 obj[c4d.PRIM_TUBE_HEIGHT] = size[2] 1090 elif primitive_type == "torus": 1091 obj = c4d.BaseObject(c4d.Otorus) 1092 # Use RINGRAD/PIPERAD for Torus 1093 obj[c4d.PRIM_TORUS_RINGRAD] = size[0] / 2.0 1094 obj[c4d.PRIM_TORUS_PIPERAD] = size[1] / 2.0 1095 elif primitive_type == "platonic": 1096 obj = c4d.BaseObject(c4d.Oplatonic) 1097 obj[c4d.PRIM_PLATONIC_TYPE] = c4d.PRIM_PLATONIC_TYPE_TETRA 1098 obj[c4d.PRIM_PLATONIC_RAD] = size[0] / 2.0 1099 else: 1100 self.log( 1101 f"Unknown primitive_type: {primitive_type}, defaulting to cube." 1102 ) 1103 obj = c4d.BaseObject(c4d.Ocube) 1104 obj[c4d.PRIM_CUBE_LEN] = c4d.Vector(*size) 1105 1106 if obj is None: # Check if object creation failed 1107 return { 1108 "error": f"Failed to create base object for type '{primitive_type}'" 1109 } 1110 1111 # Set common properties 1112 obj.SetName(requested_name) 1113 obj.SetAbsPos(c4d.Vector(*position)) 1114 1115 # Add to doc and finalize 1116 doc.InsertObject(obj) 1117 doc.AddUndo(c4d.UNDOTYPE_NEW, obj) # Add Undo step 1118 doc.SetActiveObject(obj) # Make it active 1119 c4d.EventAdd() 1120 1121 # --- MODIFIED FOR CONTEXT --- 1122 actual_name = obj.GetName() 1123 guid = str(obj.GetGUID()) 1124 pos_vec = obj.GetAbsPos() 1125 obj_type_name = self.get_object_type_name(obj) 1126 1127 # Register the object 1128 self.register_object_name(obj, requested_name) 1129 1130 # Return contextual information 1131 return { 1132 "object": { 1133 "requested_name": requested_name, 1134 "actual_name": actual_name, 1135 "guid": guid, 1136 "type": obj_type_name, 1137 "type_id": obj.GetType(), 1138 "position": [pos_vec.x, pos_vec.y, pos_vec.z], 1139 } 1140 } 1141 # --- END MODIFIED --- 1142 1143 except Exception as e: 1144 # Catch errors during object creation or property setting 1145 self.log( 1146 f"[**ERROR**] Error adding primitive '{requested_name}': {str(e)}\n{traceback.format_exc()}" 1147 ) 1148 # Clean up object if created but not inserted 1149 if obj and not obj.GetDocument(): 1150 try: 1151 obj.Remove() 1152 except: 1153 pass 1154 return { 1155 "error": f"Failed to add primitive: {str(e)}", 1156 "traceback": traceback.format_exc(), 1157 } 1158 1159 def register_object_name(self, obj, requested_name): 1160 """Register object GUID, actual name, and requested name for context tracking.""" 1161 if not obj or not isinstance(obj, c4d.BaseObject): 1162 self.log("[C4D REG] Invalid object provided for registration.") 1163 return 1164 # Ensure registries exist (redundant if __init__ is correct, but safe) 1165 if not hasattr(self, "_name_to_guid_registry"): 1166 self._name_to_guid_registry = {} 1167 if not hasattr(self, "_guid_to_name_registry"): 1168 self._guid_to_name_registry = {} 1169 # Keep original for compatibility if needed 1170 if not hasattr(self, "_object_name_registry"): 1171 self._object_name_registry = {} 1172 1173 try: 1174 # Ensure the object is part of a document before getting GUID 1175 if not obj.GetDocument(): 1176 self.log( 1177 f"[C4D REG] ## Warning ##: Object '{obj.GetName()}' not in document, cannot get reliable GUID." 1178 ) 1179 return # Skip registration if not in doc 1180 1181 obj_id = str(obj.GetGUID()) 1182 actual_name = obj.GetName() 1183 1184 if not obj_id or len(obj_id) < 10: # Basic check for non-empty GUID 1185 self.log( 1186 f"[C4D REG] ## Warning ##: Got potentially invalid GUID '{obj_id}' for object '{actual_name}'. Cannot register." 1187 ) 1188 return 1189 1190 if not requested_name: 1191 self.log( 1192 f"[C4D REG] ## Warning ## Empty requested name provided for '{actual_name}', using actual name." 1193 ) 1194 requested_name = actual_name 1195 1196 # Prepare names for registry check (lower case) 1197 req_name_lower = requested_name.lower() 1198 act_name_lower = actual_name.lower() 1199 1200 # Clean up potentially stale entries for these names in _name_to_guid_registry 1201 for name_lower in {req_name_lower, act_name_lower}: 1202 old_guid = self._name_to_guid_registry.pop(name_lower, None) 1203 if old_guid and old_guid != obj_id: 1204 self.log( 1205 f"[C4D REG] Cleaning old name->guid mapping: '{name_lower}' pointed to {old_guid}, now points to {obj_id}" 1206 ) 1207 # Also remove the reverse mapping for the old GUID if it exists 1208 old_reg_entry = self._guid_to_name_registry.get(old_guid) 1209 if old_reg_entry: 1210 old_req_lower_check = old_reg_entry.get( 1211 "requested_name", "" 1212 ).lower() 1213 old_act_lower_check = old_reg_entry.get( 1214 "actual_name", "" 1215 ).lower() 1216 if ( 1217 old_req_lower_check == name_lower 1218 or old_act_lower_check == name_lower 1219 ): 1220 self._guid_to_name_registry.pop(old_guid, None) 1221 self.log( 1222 f"[C4D REG] Removed stale guid->name entry for {old_guid}" 1223 ) 1224 1225 # Clean up potentially stale entry for this GUID in _guid_to_name_registry 1226 old_name_entry = self._guid_to_name_registry.pop(obj_id, None) 1227 if old_name_entry: 1228 # Remove old name mappings associated with this GUID from _name_to_guid_registry 1229 self._name_to_guid_registry.pop( 1230 old_name_entry.get("requested_name", "").lower(), None 1231 ) 1232 self._name_to_guid_registry.pop( 1233 old_name_entry.get("actual_name", "").lower(), None 1234 ) 1235 self.log( 1236 f"[C4D REG] Cleaning old guid->name mapping for {obj_id} (was pointing to '{old_name_entry.get('actual_name')}')." 1237 ) 1238 1239 # Add the new mappings to the *new* registries 1240 self._name_to_guid_registry[req_name_lower] = obj_id 1241 if ( 1242 act_name_lower != req_name_lower 1243 ): # Avoid duplicate key if names are same 1244 self._name_to_guid_registry[act_name_lower] = obj_id 1245 1246 self._guid_to_name_registry[obj_id] = { 1247 "requested_name": requested_name, 1248 "actual_name": actual_name, 1249 } 1250 1251 # --- Keep Original Registry Logic (Optional - for strict backward compatibility) --- 1252 # If you need the old registry structure for some reason, keep these lines. 1253 # Otherwise, they can be removed once find_object_by_name is fully updated. 1254 self._object_name_registry[obj_id] = requested_name 1255 self._object_name_registry[requested_name] = obj_id 1256 # --- End Optional Original Registry --- 1257 1258 self.log( 1259 f"[C4D REG] Registered: Req='{requested_name}', Act='{actual_name}', GUID={obj_id}" 1260 ) 1261 1262 # User Data part from original (keep as is) 1263 try: 1264 has_tag = False 1265 userdata = obj.GetUserDataContainer() 1266 if userdata: 1267 for data_index in range(len(userdata)): 1268 desc_entry = userdata[data_index] 1269 # Check if it's a valid description element before accessing DESC_NAME 1270 if ( 1271 isinstance(desc_entry, tuple) 1272 and len(desc_entry) > c4d.DESC_NAME 1273 ): # Handle tuple structure 1274 if desc_entry[c4d.DESC_NAME] == "mcp_original_name": 1275 has_tag = True 1276 break 1277 elif hasattr(desc_entry, "__getitem__") and c4d.DESC_NAME < len( 1278 desc_entry 1279 ): # Handle potential sequence access 1280 if desc_entry[c4d.DESC_NAME] == "mcp_original_name": 1281 has_tag = True 1282 break 1283 1284 if not has_tag: 1285 bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_STRING) 1286 if bc: 1287 bc[c4d.DESC_NAME] = "mcp_original_name" 1288 bc[c4d.DESC_SHORT_NAME] = "MCP Name" 1289 element = obj.AddUserData(bc) 1290 if element: 1291 # Make sure element is a DescID before using it as an index 1292 if isinstance(element, c4d.DescID): 1293 obj[element] = requested_name 1294 self.log( 1295 f"[C4D] Stored original name '{requested_name}' in object user data" 1296 ) 1297 else: 1298 # Handle case where AddUserData returns index directly (older C4D?) 1299 try: 1300 descid_from_index = obj.GetUserDataContainer()[ 1301 element 1302 ][c4d.DESC_ID] 1303 obj[descid_from_index] = requested_name 1304 self.log( 1305 f"[C4D] Stored original name '{requested_name}' in object user data (via index)" 1306 ) 1307 except Exception as e_ud_index: 1308 self.log( 1309 f"[C4D] ## Warning ##: AddUserData returned unexpected value '{element}', cannot set user data: {e_ud_index}" 1310 ) 1311 1312 except Exception as e: 1313 # Catch potential errors during GetUserDataContainer or AddUserData 1314 self.log( 1315 f"[C4D] ## Warning ##: Could not add/check user data for original name: {str(e)}\n{traceback.format_exc()}" 1316 ) 1317 1318 except Exception as e: 1319 # Catch potential errors during GetGUID, GetName etc. 1320 failed_name = requested_name or (obj.GetName() if obj else "UnknownObject") 1321 self.log( 1322 f"[**ERROR**] Failed to register object '{failed_name}': {e}\n{traceback.format_exc()}" 1323 ) 1324 1325 def handle_render_preview_base64(self, frame=0, width=640, height=360): 1326 """SDK 2025-compliant base64 renderer with error resolution""" 1327 import c4d 1328 import base64 1329 import traceback 1330 1331 def _execute_render(): 1332 try: 1333 doc = c4d.documents.GetActiveDocument() 1334 if not doc: 1335 return {"error": "No active document"} 1336 1337 # 1. Camera Validation (Critical Fix) 1338 if not doc.GetActiveBaseDraw().GetSceneCamera(doc): 1339 return {"error": "No active camera (create camera first)"} 1340 1341 # 2. RenderData Protocol Fix (SDK §9.1.3) 1342 original_rd = doc.GetActiveRenderData() 1343 if not original_rd: 1344 return {"error": "No render settings configured"} 1345 1346 rd_clone = original_rd.GetClone(c4d.COPYFLAGS_NONE) 1347 if not rd_clone: 1348 return {"error": "RenderData clone failed"} 1349 1350 try: 1351 doc.InsertRenderData(rd_clone) 1352 doc.SetActiveRenderData(rd_clone) # Required activation 1353 1354 # 3. 2025-Specific Configuration 1355 settings = rd_clone.GetData() 1356 settings[c4d.RDATA_XRES] = width 1357 settings[c4d.RDATA_YRES] = height 1358 settings[c4d.RDATA_FRAMESEQUENCE] = ( 1359 c4d.RDATA_FRAMESEQUENCE_CURRENTFRAME 1360 ) 1361 # Force Standard renderer (required for Sketch & Toon, avoids Redshift memory issues) 1362 settings[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD 1363 1364 # 4. Mandatory Flags (SDK §9.4.5) 1365 render_flags = ( 1366 c4d.RENDERFLAGS_EXTERNAL 1367 | c4d.RENDERFLAGS_SHOWERRORS 1368 | 0x00040000 # EMBREE_STREAMING 1369 | c4d.RENDERFLAGS_NODOCUMENTCLONE 1370 ) 1371 1372 # 5. Bitmap Initialization - Use BaseBitmap for reliable Sketch & Toon rendering 1373 bmp = c4d.bitmaps.BaseBitmap() 1374 bmp.Init(width, height, 24) # 24-bit RGB works reliably with Sketch & Toon 1375 1376 # 6. Frame Synchronization 1377 doc.SetTime(c4d.BaseTime(frame, doc.GetFps())) 1378 doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) 1379 1380 # 7. Core Render Execution 1381 result = c4d.documents.RenderDocument( 1382 doc, settings, bmp, render_flags 1383 ) 1384 if result != c4d.RENDERRESULT_OK: 1385 return { 1386 "error": f"Render failed: {self._render_code_to_str(result)}" 1387 } 1388 1389 # 8. MemoryFile Handling Fix 1390 mem_file = c4d.storage.MemoryFileStruct() 1391 mem_file.SetMemoryWriteMode() 1392 if bmp.Save(mem_file, c4d.FILTER_PNG) != c4d.IMAGERESULT_OK: 1393 return {"error": "PNG encoding failed"} 1394 1395 data, _ = mem_file.GetData() 1396 return { 1397 "success": True, 1398 "image_base64": f"data:image/png;base64,{base64.b64encode(data).decode()}", 1399 } 1400 1401 finally: 1402 # 9. Correct Resource Cleanup (SDK §9.1.4) 1403 if rd_clone: 1404 rd_clone.Remove() # Fixed removal method 1405 if "bmp" in locals(): 1406 bmp.FlushAll() 1407 c4d.EventAdd() 1408 1409 except Exception as e: 1410 return {"error": f"Render failure: {str(e)}"} 1411 1412 return self.execute_on_main_thread(_execute_render, _timeout=120) 1413 1414 def _render_code_to_str(self, code): 1415 """Convert Cinema4D render result codes to human-readable strings""" 1416 codes = { 1417 0: "Success", 1418 1: "Out of memory", 1419 2: "Command canceled", 1420 3: "Missing assets", 1421 4: "Rendering in progress", 1422 5: "Invalid document", 1423 6: "Version mismatch", 1424 7: "Network error", 1425 8: "Invalid parameters", 1426 9: "IO error", 1427 } 1428 return codes.get(code, f"Unknown error ({code})") 1429 1430 def handle_modify_object(self, command): 1431 """Handle modify_object command with full property support, GUID option, and Camera params.""" 1432 doc = c4d.documents.GetActiveDocument() 1433 if not doc: 1434 return {"error": "No active document"} 1435 1436 properties = command.get("properties", {}) 1437 if not properties: 1438 return {"error": "No properties provided to modify."} 1439 1440 # --- Identifier Detection --- 1441 identifier = None 1442 use_guid = False 1443 if command.get("guid"): 1444 identifier = command.get("guid") 1445 use_guid = True 1446 self.log(f"[MODIFY] Using GUID identifier: '{identifier}'") 1447 elif command.get("object_name"): 1448 identifier = command.get("object_name") 1449 identifier_str = str(identifier) 1450 if "-" in identifier_str and len(identifier_str) > 30: 1451 use_guid = True 1452 self.log(f"[MODIFY] Identifier '{identifier}' looks like GUID.") 1453 else: 1454 use_guid = False 1455 self.log(f"[MODIFY] Using Name identifier: '{identifier}'") 1456 elif command.get("name"): 1457 identifier = command.get("name") 1458 use_guid = False 1459 self.log(f"[MODIFY] Using 'name' key as Name identifier: '{identifier}'") 1460 else: 1461 return { 1462 "error": "No object identifier ('guid', 'object_name', or 'name') provided." 1463 } 1464 1465 # Find the object using the determined identifier and flag 1466 obj = self.find_object_by_name(doc, identifier, use_guid=use_guid) 1467 if obj is None: 1468 search_type = "GUID" if use_guid else "Name" 1469 return { 1470 "error": f"Object '{identifier}' (searched by {search_type}) not found." 1471 } 1472 1473 # Apply modifications 1474 modified = {} 1475 name_before = obj.GetName() 1476 something_changed = False 1477 obj_type = obj.GetType() # Get type for specific param handling 1478 1479 try: 1480 doc.StartUndo() # Start undo block 1481 1482 # Position 1483 pos_val = properties.get("position") 1484 if isinstance(pos_val, list) and len(pos_val) >= 3: 1485 try: 1486 new_pos = c4d.Vector( 1487 float(pos_val[0]), float(pos_val[1]), float(pos_val[2]) 1488 ) 1489 if obj.GetAbsPos() != new_pos: 1490 obj.SetAbsPos(new_pos) 1491 modified["position"] = [new_pos.x, new_pos.y, new_pos.z] 1492 something_changed = True 1493 except (ValueError, TypeError) as e: 1494 self.log(f"Warning: Invalid position value '{pos_val}': {e}") 1495 1496 # Rotation 1497 rot_val = properties.get("rotation") 1498 if isinstance(rot_val, list) and len(rot_val) >= 3: 1499 try: 1500 new_rot_deg = [float(r) for r in rot_val[:3]] 1501 new_rot_rad = c4d.Vector( 1502 *[c4d.utils.DegToRad(r) for r in new_rot_deg] 1503 ) 1504 obj.SetAbsRot(new_rot_rad) 1505 modified["rotation"] = new_rot_deg 1506 something_changed = True 1507 except (ValueError, TypeError) as e: 1508 self.log(f"Warning: Invalid rotation value '{rot_val}': {e}") 1509 1510 # Scale 1511 scale_val = properties.get("scale") 1512 if isinstance(scale_val, list) and len(scale_val) >= 3: 1513 try: 1514 new_scale = c4d.Vector( 1515 float(scale_val[0]), float(scale_val[1]), float(scale_val[2]) 1516 ) 1517 if obj.GetAbsScale() != new_scale: 1518 obj.SetAbsScale(new_scale) 1519 modified["scale"] = [new_scale.x, new_scale.y, new_scale.z] 1520 something_changed = True 1521 except (ValueError, TypeError) as e: 1522 self.log(f"Warning: Invalid scale value '{scale_val}': {e}") 1523 1524 # Color 1525 color_val = properties.get("color") 1526 if isinstance(color_val, list) and len(color_val) >= 3: 1527 try: 1528 new_color = c4d.Vector( 1529 max(0.0, min(1.0, float(color_val[0]))), 1530 max(0.0, min(1.0, float(color_val[1]))), 1531 max(0.0, min(1.0, float(color_val[2]))), 1532 ) 1533 if ( 1534 obj.IsCorrectType(c4d.Opoint) 1535 or obj.IsCorrectType(c4d.Opolygon) 1536 or obj.IsCorrectType(c4d.Ospline) 1537 or obj.IsCorrectType(c4d.Onull) 1538 ): 1539 if ( 1540 obj.GetParameter(c4d.DescID(c4d.ID_BASEOBJECT_COLOR))[1] 1541 != new_color 1542 ): # Safer comparison 1543 obj[c4d.ID_BASEOBJECT_USECOLOR] = ( 1544 c4d.ID_BASEOBJECT_USECOLOR_ON 1545 ) 1546 obj[c4d.ID_BASEOBJECT_COLOR] = new_color 1547 modified["color"] = [new_color.x, new_color.y, new_color.z] 1548 something_changed = True 1549 else: 1550 self.log( 1551 f"Warning: Cannot set display color for object type {obj.GetType()} ('{name_before}')" 1552 ) 1553 except (ValueError, TypeError, AttributeError) as e: 1554 self.log(f"Warning: Error setting color for '{name_before}': {e}") 1555 1556 # Primitive Size 1557 size = properties.get("size") 1558 if isinstance(size, list) and len(size) > 0: 1559 obj_type = obj.GetType() 1560 size_applied = False 1561 new_size_applied = [] 1562 try: 1563 safe_size = [float(s) for s in size if s is not None] 1564 if not safe_size: 1565 raise ValueError("No valid numeric sizes") 1566 sx, sy, sz = ( 1567 safe_size[0], 1568 safe_size[1] if len(safe_size) > 1 else safe_size[0], 1569 safe_size[2] if len(safe_size) > 2 else safe_size[0], 1570 ) 1571 1572 if obj_type == c4d.Ocube: 1573 new_val = c4d.Vector(sx, sy, sz) 1574 current = obj[c4d.PRIM_CUBE_LEN] 1575 setter = lambda v: obj.SetParameter( 1576 c4d.DescID(c4d.PRIM_CUBE_LEN), v, c4d.DESCFLAGS_SET_NONE 1577 ) 1578 params = [sx, sy, sz] 1579 1580 elif obj_type == c4d.Osphere: 1581 new_val = sx / 2.0 1582 current = obj[c4d.PRIM_SPHERE_RAD] 1583 setter = lambda v: obj.SetParameter( 1584 c4d.DescID(c4d.PRIM_SPHERE_RAD), v, c4d.DESCFLAGS_SET_NONE 1585 ) 1586 params = [sx] 1587 1588 elif obj_type == c4d.Ocone: 1589 new_val = (sx / 2.0, sy) 1590 current = (obj[c4d.PRIM_CONE_BRAD], obj[c4d.PRIM_CONE_HEIGHT]) 1591 setter = lambda v: obj.SetParameters( 1592 { 1593 c4d.DescID(c4d.PRIM_CONE_BRAD): v[0], 1594 c4d.DescID(c4d.PRIM_CONE_HEIGHT): v[1], 1595 } 1596 ) 1597 params = [sx, sy] 1598 1599 elif obj_type == c4d.Ocylinder: 1600 new_val = (sx / 2.0, sy) 1601 current = ( 1602 obj[c4d.PRIM_CYLINDER_RADIUS], 1603 obj[c4d.PRIM_CYLINDER_HEIGHT], 1604 ) 1605 setter = lambda v: obj.SetParameters( 1606 { 1607 c4d.DescID(c4d.PRIM_CYLINDER_RADIUS): v[0], 1608 c4d.DescID(c4d.PRIM_CYLINDER_HEIGHT): v[1], 1609 } 1610 ) 1611 params = [sx, sy] 1612 1613 elif obj_type == c4d.Oplane: 1614 new_val = (sx, sy) 1615 current = ( 1616 obj[c4d.PRIM_PLANE_WIDTH], 1617 obj[c4d.PRIM_PLANE_HEIGHT], 1618 ) 1619 setter = lambda v: obj.SetParameters( 1620 { 1621 c4d.DescID(c4d.PRIM_PLANE_WIDTH): v[0], 1622 c4d.DescID(c4d.PRIM_PLANE_HEIGHT): v[1], 1623 } 1624 ) 1625 params = [sx, sy] 1626 # Add other primitives here if needed... 1627 else: 1628 new_val = None 1629 current = None 1630 setter = None 1631 params = None # Indicate not applicable 1632 1633 if setter and new_val is not None and current != new_val: 1634 setter(new_val) 1635 size_applied = True 1636 new_size_applied = params 1637 1638 if size_applied: 1639 modified["size"] = new_size_applied 1640 something_changed = True 1641 elif size: 1642 self.log( 1643 f"Info: 'size' prop not applicable to type {obj_type} ('{name_before}')" 1644 ) 1645 except Exception as e_size: 1646 self.log( 1647 f"Warning: Error modifying size for {name_before}: {e_size}" 1648 ) 1649 1650 # --- NEW: Camera Specific Properties --- 1651 elif obj_type == c4d.Ocamera: 1652 bc = obj.GetDataInstance() 1653 if bc: 1654 focal_length = properties.get("focal_length") 1655 if focal_length is not None: 1656 try: 1657 val = float(focal_length) 1658 focus_id = getattr( 1659 c4d, "CAMERAOBJECT_FOCUS", c4d.CAMERA_FOCUS 1660 ) 1661 if bc[focus_id] != val: 1662 bc[focus_id] = val 1663 modified["focal_length"] = val 1664 something_changed = True 1665 except (ValueError, TypeError, AttributeError) as e: 1666 self.log( 1667 f"Warning: Failed to set focal_length '{focal_length}': {e}" 1668 ) 1669 1670 focus_distance = properties.get("focus_distance") 1671 if focus_distance is not None: 1672 try: 1673 val = float(focus_distance) 1674 dist_id = getattr( 1675 c4d, "CAMERAOBJECT_TARGETDISTANCE", None 1676 ) # ID for focus distance 1677 if dist_id and bc[dist_id] != val: 1678 bc[dist_id] = val 1679 modified["focus_distance"] = val 1680 something_changed = True 1681 elif not dist_id: 1682 self.log( 1683 "Warning: CAMERAOBJECT_TARGETDISTANCE parameter not found." 1684 ) 1685 except (ValueError, TypeError, AttributeError) as e: 1686 self.log( 1687 f"Warning: Failed to set focus_distance '{focus_distance}': {e}" 1688 ) 1689 else: 1690 self.log( 1691 f"Warning: Could not get BaseContainer for camera '{name_before}'" 1692 ) 1693 1694 # Rename - process *after* other properties in case identifier was 'name' 1695 requested_new_name = properties.get("name") 1696 if isinstance(requested_new_name, str): 1697 new_name_stripped = requested_new_name.strip() 1698 if new_name_stripped and new_name_stripped != name_before: 1699 self.log( 1700 f"[MODIFY] Renaming '{name_before}' to '{new_name_stripped}'" 1701 ) 1702 obj.SetName(new_name_stripped) 1703 name_after_rename = obj.GetName() 1704 modified["name"] = { 1705 "from": name_before, 1706 "requested": new_name_stripped, 1707 "to": name_after_rename, 1708 } 1709 something_changed = True 1710 self.register_object_name( 1711 obj, new_name_stripped 1712 ) # Register with requested new name 1713 1714 # Finalize 1715 if something_changed: 1716 doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj) 1717 c4d.EventAdd() 1718 else: 1719 self.log(f"No modifications applied to '{name_before}'") 1720 1721 doc.EndUndo() # End undo block 1722 1723 # Contextual Return 1724 final_name = obj.GetName() 1725 guid = str(obj.GetGUID()) 1726 pos_vec = obj.GetAbsPos() 1727 rot_vec_rad = obj.GetAbsRot() 1728 scale_vec = obj.GetAbsScale() 1729 1730 if "name" not in modified and final_name != name_before: 1731 self.log( 1732 f"Warning: Object name changed unexpectedly from '{name_before}' to '{final_name}'. Updating registry." 1733 ) 1734 self.register_object_name(obj, name_before) 1735 1736 return { 1737 "object": { 1738 "requested_identifier": identifier, 1739 "was_guid": use_guid, 1740 "actual_name": final_name, 1741 "guid": guid, 1742 "name_before": name_before, 1743 "modified_properties": modified, 1744 "current_position": [pos_vec.x, pos_vec.y, pos_vec.z], 1745 "current_rotation": [ 1746 c4d.utils.RadToDeg(r) 1747 for r in [rot_vec_rad.x, rot_vec_rad.y, rot_vec_rad.z] 1748 ], 1749 "current_scale": [scale_vec.x, scale_vec.y, scale_vec.z], 1750 } 1751 } 1752 1753 except Exception as e: 1754 if doc and doc.IsUndoEnabled(): 1755 doc.EndUndo() # Ensure undo ended 1756 error_msg = f"Unexpected error modifying object '{name_before}': {str(e)}" 1757 self.log(f"[**ERROR**] {error_msg}\n{traceback.format_exc()}") 1758 return {"error": error_msg, "traceback": traceback.format_exc()} 1759 1760 def handle_apply_material(self, command): 1761 """Handle apply_material command with GUID support.""" 1762 doc = c4d.documents.GetActiveDocument() 1763 if not doc: 1764 return {"error": "No active document"} 1765 1766 material_name = command.get("material_name", "") 1767 identifier = None 1768 use_guid = False 1769 1770 # --- GUID Detection Improved --- 1771 if command.get("guid"): 1772 identifier = command.get("guid") 1773 use_guid = True 1774 self.log(f"[APPLY MAT] Using GUID identifier: '{identifier}'") 1775 elif command.get("object_name"): 1776 identifier = command.get("object_name") 1777 if "-" in str(identifier) and len(str(identifier)) > 30: 1778 self.log( 1779 f"[APPLY MAT] Identifier '{identifier}' looks like GUID, treating as GUID." 1780 ) 1781 use_guid = True 1782 else: 1783 use_guid = False 1784 self.log(f"[APPLY MAT] Using Name identifier: '{identifier}'") 1785 else: 1786 return {"error": "No object identifier ('guid' or 'object_name') provided."} 1787 1788 # Find object 1789 obj = self.find_object_by_name(doc, identifier, use_guid=use_guid) 1790 if obj is None: 1791 search_type = "GUID" if use_guid else "Name" 1792 return { 1793 "error": f"Object '{identifier}' (searched by {search_type}) not found." 1794 } 1795 1796 # Find material 1797 mat = self._find_material_by_name(doc, material_name) 1798 if mat is None: 1799 return {"error": f"Material not found: {material_name}"} 1800 1801 material_type = command.get("material_type", "standard").lower() 1802 projection_type = command.get("projection_type", "cubic").lower() 1803 auto_uv = command.get("auto_uv", False) 1804 procedural = command.get("procedural", False) 1805 1806 try: 1807 doc.StartUndo() 1808 1809 # Create and configure texture tag 1810 tag = c4d.TextureTag() 1811 if not tag: 1812 raise RuntimeError("Failed to create TextureTag") 1813 tag.SetMaterial(mat) 1814 1815 proj_map = { 1816 "cubic": c4d.TEXTURETAG_PROJECTION_CUBIC, 1817 "spherical": c4d.TEXTURETAG_PROJECTION_SPHERICAL, 1818 "flat": c4d.TEXTURETAG_PROJECTION_FLAT, 1819 "cylindrical": c4d.TEXTURETAG_PROJECTION_CYLINDRICAL, 1820 "frontal": c4d.TEXTURETAG_PROJECTION_FRONTAL, 1821 "uvw": c4d.TEXTURETAG_PROJECTION_UVW, 1822 } 1823 tag[c4d.TEXTURETAG_PROJECTION] = proj_map.get( 1824 projection_type, c4d.TEXTURETAG_PROJECTION_UVW 1825 ) 1826 1827 obj.InsertTag(tag) 1828 doc.AddUndo(c4d.UNDOTYPE_NEW, tag) 1829 1830 # Auto UV generation 1831 if auto_uv: 1832 self.log( 1833 f"[APPLY MAT] Attempting auto UV generation for '{obj.GetName()}'" 1834 ) 1835 try: 1836 if obj.IsInstanceOf(c4d.Opolygon): 1837 uvw_tag = obj.GetTag(c4d.Tuvw) 1838 if not uvw_tag: 1839 uvw_tag = obj.MakeTag(c4d.Tuvw) 1840 if uvw_tag: 1841 doc.AddUndo(c4d.UNDOTYPE_NEW, uvw_tag) 1842 else: 1843 self.log("Warning: Failed to create UVW tag.") 1844 1845 if uvw_tag: 1846 c4d.plugins.CallCommand(12205) # Optimal Cubic Mapping 1847 self.log("Executed Optimal (Cubic) UV mapping command.") 1848 else: 1849 self.log( 1850 "Warning: Could not get or create UVW tag for auto UV." 1851 ) 1852 else: 1853 self.log( 1854 f"Warning: Auto UV skipped, object '{obj.GetName()}' not a polygon." 1855 ) 1856 except Exception as e_uv: 1857 self.log( 1858 f"[**ERROR**] Error during auto UV generation: {str(e_uv)}" 1859 ) 1860 1861 # Handle Redshift 1862 if ( 1863 material_type == "redshift" 1864 and hasattr(c4d, "modules") 1865 and hasattr(c4d.modules, "redshift") 1866 ): 1867 self.log( 1868 f"[APPLY MAT] Checking Redshift setup for material '{mat.GetName()}'" 1869 ) 1870 try: 1871 redshift = c4d.modules.redshift 1872 rs_id = getattr(c4d, "ID_REDSHIFT_MATERIAL", 1036224) 1873 1874 if mat.GetType() != rs_id: 1875 self.log( 1876 f"Converting material '{mat.GetName()}' to Redshift (ID: {rs_id})" 1877 ) 1878 rs_mat = c4d.BaseMaterial(rs_id) 1879 if not rs_mat: 1880 raise RuntimeError("Failed to create Redshift material") 1881 1882 rs_mat.SetName(f"RS_{mat.GetName()}") 1883 doc.InsertMaterial(rs_mat) 1884 doc.AddUndo(c4d.UNDOTYPE_NEW, rs_mat) 1885 1886 try: 1887 if hasattr(c4d, "REDSHIFT_MATERIAL_DIFFUSE_COLOR"): 1888 rs_mat[c4d.REDSHIFT_MATERIAL_DIFFUSE_COLOR] = mat[ 1889 c4d.MATERIAL_COLOR_COLOR 1890 ] 1891 except Exception as e_color_copy: 1892 self.log( 1893 f"Warning: Could not copy color during RS conversion: {e_color_copy}" 1894 ) 1895 1896 try: 1897 import maxon 1898 1899 ns_id = maxon.Id( 1900 "com.redshift3d.redshift4c4d.class.nodespace" 1901 ) 1902 node_rs_mat = c4d.NodeMaterial(rs_mat) 1903 if node_rs_mat and not node_rs_mat.HasSpace(ns_id): 1904 node_rs_mat.CreateDefaultGraph(ns_id) 1905 self.log("Created default Redshift node graph.") 1906 except Exception as e_graph: 1907 self.log( 1908 f"Warning: Failed to create Redshift graph: {e_graph}" 1909 ) 1910 1911 if procedural: 1912 try: 1913 node_space = redshift.GetRSMaterialNodeSpace(rs_mat) 1914 root = redshift.GetRSMaterialRootShader(rs_mat) 1915 if node_space and root: 1916 tex_node = ( 1917 redshift.RSMaterialNodeCreator.CreateNode( 1918 node_space, 1919 redshift.RSMaterialNodeType.TEXTURE, 1920 "RS::TextureNode", 1921 ) 1922 ) 1923 if tex_node: 1924 tex_node[redshift.TEXTURE_TYPE] = ( 1925 redshift.TEXTURE_NOISE 1926 ) 1927 redshift.CreateConnectionBetweenNodes( 1928 node_space, 1929 tex_node, 1930 "outcolor", 1931 root, 1932 "diffuse_color", 1933 ) 1934 self.log( 1935 "Connected procedural Noise node to diffuse color." 1936 ) 1937 else: 1938 self.log( 1939 "Warning: Failed to create procedural texture node." 1940 ) 1941 except Exception as e_proc: 1942 self.log( 1943 f"Warning: Error setting up procedural RS nodes: {e_proc}" 1944 ) 1945 1946 tag.SetMaterial(rs_mat) 1947 mat = rs_mat 1948 doc.AddUndo(c4d.UNDOTYPE_CHANGE, tag) 1949 self.log( 1950 f"Swapped tag to use new Redshift material '{rs_mat.GetName()}'" 1951 ) 1952 1953 except Exception as e_rs_setup: 1954 self.log( 1955 f"[**ERROR**] Error during Redshift setup: {str(e_rs_setup)}" 1956 ) 1957 1958 doc.EndUndo() 1959 c4d.EventAdd() 1960 1961 return { 1962 "success": True, 1963 "message": f"Applied material '{mat.GetName()}' to object '{obj.GetName()}'.", 1964 "object_name": obj.GetName(), 1965 "object_guid": str(obj.GetGUID()), 1966 "material_name": mat.GetName(), 1967 "material_type_id": mat.GetType(), 1968 "projection": projection_type, 1969 "auto_uv_attempted": auto_uv, 1970 } 1971 1972 except Exception as e: 1973 doc.EndUndo() 1974 err = f"Error applying material '{material_name}' to '{obj.GetName()}': {str(e)}" 1975 self.log(f"[**ERROR**] {err}\n{traceback.format_exc()}") 1976 return {"error": err, "traceback": traceback.format_exc()} 1977 1978 # def handle_render_to_file(self, doc, frame, width, height, output_path=None): 1979 # """Render a frame to file, with optional base64 and fallback output path.""" 1980 # import os 1981 # import tempfile 1982 # import time 1983 # import base64 1984 # import c4d.storage 1985 # import traceback 1986 1987 # try: 1988 # start_time = time.time() 1989 1990 # # Clone active render settings 1991 # render_data = doc.GetActiveRenderData() 1992 # if not render_data: 1993 # return {"error": "No active RenderData found"} 1994 1995 # rd_clone = render_data.GetClone() 1996 # if not rd_clone: 1997 # return {"error": "Failed to clone render settings"} 1998 1999 # # Update render settings 2000 # settings = rd_clone.GetData() 2001 # settings[c4d.RDATA_XRES] = float(width) 2002 # settings[c4d.RDATA_YRES] = float(height) 2003 # settings[c4d.RDATA_PATH] = output_path or os.path.join( 2004 # tempfile.gettempdir(), "temp_render_output.png" 2005 # ) 2006 2007 # settings[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD 2008 # settings[c4d.RDATA_FRAMESEQUENCE] = c4d.RDATA_FRAMESEQUENCE_CURRENTFRAME 2009 # settings[c4d.RDATA_SAVEIMAGE] = False 2010 2011 # # render_data.SetData(settings) 2012 # # Create temp RenderData container 2013 # # Insert actual RenderData object into the scene with settings 2014 # temp_rd = c4d.documents.RenderData() 2015 # temp_rd.SetData(settings) 2016 # doc.InsertRenderData(temp_rd) 2017 2018 # # Update document time/frame 2019 # if isinstance(frame, dict): 2020 # frame = frame.get("frame", 0) 2021 # doc.SetTime(c4d.BaseTime(frame, doc.GetFps())) 2022 2023 # doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) 2024 2025 # # Create target bitmap 2026 # bmp = c4d.bitmaps.BaseBitmap() 2027 # if not bmp.Init(int(width), int(height)): 2028 # return {"error": "Failed to initialize bitmap"} 2029 2030 # self.log(f"[RENDER] Rendering frame {frame} at {width}x{height}...") 2031 # self.log(f"[RENDER DEBUG] Using RenderData name: {temp_rd.GetName()}") 2032 2033 # self.log( 2034 # f"[RENDER DEBUG] Width: {settings[c4d.RDATA_XRES]}, Height: {settings[c4d.RDATA_YRES]}" 2035 # ) 2036 2037 # # Render to bitmap 2038 # result = c4d.documents.RenderDocument( 2039 # doc, 2040 # temp_rd.GetData(), 2041 # bmp, 2042 # c4d.RENDERFLAGS_EXTERNAL | c4d.RENDERFLAGS_NODOCUMENTCLONE, 2043 # None, 2044 # ) 2045 2046 # if not result: 2047 # self.log("[RENDER] RenderDocument returned False") 2048 # return {"error": "RenderDocument failed"} 2049 2050 # # Fallback path if needed 2051 # if not output_path: 2052 # doc_name = doc.GetDocumentName() or "untitled" 2053 # if doc_name.lower().endswith(".c4d"): 2054 # doc_name = doc_name[:-4] 2055 # base_dir = doc.GetDocumentPath() or tempfile.gettempdir() 2056 # output_path = os.path.join(base_dir, f"{doc_name}_snapshot_{frame}.png") 2057 2058 # # Choose format based on extension 2059 # ext = os.path.splitext(output_path)[1].lower() 2060 # format_map = { 2061 # ".png": c4d.FILTER_PNG, 2062 # ".jpg": c4d.FILTER_JPG, 2063 # ".jpeg": c4d.FILTER_JPG, 2064 # ".tif": c4d.FILTER_TIF, 2065 # ".tiff": c4d.FILTER_TIF, 2066 # } 2067 # format_id = format_map.get(ext, c4d.FILTER_PNG) 2068 2069 # # Save image to file 2070 # if not bmp.Save(output_path, format_id): 2071 # self.log(f"[RENDER] Failed to save bitmap to file: {output_path}") 2072 # return {"error": f"Failed to save image to: {output_path}"} 2073 2074 # # Optionally encode to base64 if PNG 2075 # image_base64 = None 2076 # if format_id == c4d.FILTER_PNG: 2077 # mem_file = c4d.storage.MemoryFileWrite() 2078 # if mem_file.Open(1024 * 1024): 2079 # if bmp.Save(mem_file, c4d.FILTER_PNG): 2080 # raw_bytes = mem_file.GetValue() 2081 # image_base64 = base64.b64encode(raw_bytes).decode("utf-8") 2082 # self.log("[RENDER] Base64 preview generated") 2083 # mem_file.Close() 2084 2085 # elapsed = round(time.time() - start_time, 3) 2086 2087 # return { 2088 # "success": True, 2089 # "frame": frame, 2090 # "resolution": f"{width}x{height}", 2091 # "output_path": output_path, 2092 # "file_exists": os.path.exists(output_path), 2093 # "image_base64": image_base64, 2094 # "render_time": elapsed, 2095 # } 2096 2097 # except Exception as e: 2098 # self.log("[RENDER ] Exception during render_to_file") 2099 # self.log(traceback.format_exc()) 2100 2101 # return {"error": f"Exception during render: {str(e)}"} 2102 2103 def handle_snapshot_scene(self, command=None): 2104 """ 2105 Generates a snapshot: object list + base64 preview render. 2106 Uses the corrected core render logic via handle_render_preview_base64. 2107 """ 2108 doc = c4d.documents.GetActiveDocument() 2109 if not doc: 2110 return {"error": "No active document for snapshot."} 2111 2112 frame = doc.GetTime().GetFrame(doc.GetFps()) 2113 width, height = 640, 360 2114 2115 self.log(f"[C4D SNAPSHOT] Generating snapshot for frame {frame}...") 2116 2117 # 1. List objects 2118 object_data = self.handle_list_objects() # Runs via execute_on_main_thread 2119 objects = object_data.get("objects", []) 2120 2121 # 2. Render preview - uses handle_render_preview_base64 which now uses corrected core logic 2122 render_command = {"width": width, "height": height, "frame": frame} 2123 render_result = self.handle_render_preview_base64( 2124 **render_command 2125 ) # Runs via execute_on_main_thread 2126 2127 render_info = {} 2128 if render_result and render_result.get("success"): 2129 render_info = { 2130 "frame": render_result.get("frame", frame), 2131 "resolution": f"{render_result.get('width', width)}x{render_result.get('height', height)}", 2132 "image_base64": render_result.get("image_base64"), 2133 "render_time": render_result.get("render_time", 0.0), 2134 "format": render_result.get("format", "png"), 2135 "success": True, 2136 } 2137 self.log(f"[C4D SNAPSHOT] Render successful.") 2138 else: 2139 error_msg = render_result.get("error", "Unknown rendering error") 2140 render_info = {"error": error_msg, "success": False} 2141 self.log(f"[C4D SNAPSHOT] Render failed: {error_msg}") 2142 # Include traceback from render result if available 2143 if isinstance(render_result, dict) and "traceback" in render_result: 2144 render_info["traceback"] = render_result["traceback"] 2145 2146 # 3. Return combined result 2147 return { 2148 "objects": objects, 2149 "render": render_info, 2150 } 2151 2152 def handle_set_keyframe(self, command): 2153 """Set a keyframe on an object, supporting both GUID and name lookup.""" 2154 doc = c4d.documents.GetActiveDocument() 2155 if not doc: 2156 return {"error": "No active document"} 2157 2158 # --- Identifier Detection --- 2159 identifier = None 2160 use_guid = False 2161 if command.get("guid"): 2162 identifier = command.get("guid") 2163 use_guid = True 2164 self.log(f"[KEYFRAME] Using GUID identifier: '{identifier}'") 2165 elif command.get("object_name"): 2166 identifier = command.get("object_name") 2167 if "-" in str(identifier) and len(str(identifier)) > 30: 2168 use_guid = True 2169 self.log( 2170 f"[KEYFRAME] Identifier '{identifier}' looks like GUID, treating as GUID." 2171 ) 2172 else: 2173 use_guid = False 2174 self.log(f"[KEYFRAME] Using Name identifier: '{identifier}'") 2175 else: 2176 identifier = command.get("name") 2177 if identifier: 2178 use_guid = False 2179 self.log( 2180 f"[KEYFRAME] Using 'name' key as Name identifier: '{identifier}'" 2181 ) 2182 else: 2183 return { 2184 "error": "No object identifier ('guid', 'object_name', or 'name') provided." 2185 } 2186 2187 # Find object 2188 obj = self.find_object_by_name(doc, identifier, use_guid=use_guid) 2189 if obj is None: 2190 search_type = "GUID" if use_guid else "Name" 2191 return { 2192 "error": f"Object '{identifier}' (searched by {search_type}) not found for keyframing." 2193 } 2194 2195 # --- Property, Frame, and Value --- 2196 property_type = ( 2197 command.get("property_type") or command.get("property") or "position" 2198 ).lower() 2199 frame = command.get("frame", doc.GetTime().GetFrame(doc.GetFps())) 2200 value = command.get("value") 2201 if value is None: 2202 return {"error": "No 'value' provided for keyframe."} 2203 2204 try: 2205 frame = int(frame) 2206 except (ValueError, TypeError): 2207 return {"error": f"Invalid frame: {frame}"} 2208 2209 try: 2210 # --- Handle Different Property Types --- 2211 if "." in property_type: 2212 # Vector component property (e.g., position.x) 2213 parts = property_type.split(".") 2214 if len(parts) != 2: 2215 return { 2216 "error": f"Invalid property format: '{property_type}'. Use 'position.x' etc." 2217 } 2218 2219 base_property, component = parts 2220 property_map = { 2221 "position": c4d.ID_BASEOBJECT_POSITION, 2222 "rotation": c4d.ID_BASEOBJECT_ROTATION, 2223 "scale": c4d.ID_BASEOBJECT_SCALE, 2224 "color": c4d.LIGHT_COLOR if obj.GetType() == c4d.Olight else None, 2225 } 2226 component_map = { 2227 "x": c4d.VECTOR_X, 2228 "y": c4d.VECTOR_Y, 2229 "z": c4d.VECTOR_Z, 2230 "r": c4d.VECTOR_X, 2231 "g": c4d.VECTOR_Y, 2232 "b": c4d.VECTOR_Z, 2233 } 2234 2235 if ( 2236 base_property not in property_map 2237 or property_map[base_property] is None 2238 ): 2239 return { 2240 "error": f"Unsupported/invalid base property '{base_property}' for object type." 2241 } 2242 if component not in component_map: 2243 return { 2244 "error": f"Unsupported component '{component}'. Use x, y, z, r, g, or b." 2245 } 2246 2247 if isinstance(value, list): 2248 value = value[0] if value else 0.0 2249 2250 result = self._set_vector_component_keyframe( 2251 obj, 2252 frame, 2253 property_map[base_property], 2254 component_map[component], 2255 float(value), 2256 base_property, 2257 component, 2258 ) 2259 if not result: 2260 return {"error": f"Failed to set {property_type} keyframe"} 2261 2262 elif property_type in ["position", "rotation", "scale"]: 2263 # Full vector properties 2264 property_ids = { 2265 "position": c4d.ID_BASEOBJECT_POSITION, 2266 "rotation": c4d.ID_BASEOBJECT_ROTATION, 2267 "scale": c4d.ID_BASEOBJECT_SCALE, 2268 } 2269 2270 if isinstance(value, (int, float)): 2271 value = [float(value)] * 3 2272 elif isinstance(value, list): 2273 if len(value) == 1: 2274 value = [float(value[0])] * 3 2275 elif len(value) == 2: 2276 value = [float(value[0]), float(value[1]), 0.0] 2277 elif len(value) > 3: 2278 value = [float(v) for v in value[:3]] 2279 else: 2280 value = [float(v) for v in value] 2281 else: 2282 return { 2283 "error": f"{property_type.capitalize()} value must be a number or a list [x,y,z]." 2284 } 2285 2286 if len(value) != 3: 2287 return { 2288 "error": f"{property_type.capitalize()} value must have 3 components." 2289 } 2290 2291 result = self._set_vector_keyframe( 2292 obj, frame, property_ids[property_type], value, property_type 2293 ) 2294 if not result: 2295 return {"error": f"Failed to set {property_type} keyframe"} 2296 2297 elif obj.GetType() == c4d.Olight and property_type in [ 2298 "intensity", 2299 "color", 2300 ]: 2301 if property_type == "intensity": 2302 if isinstance(value, list): 2303 value = value[0] if value else 0.0 2304 result = self._set_scalar_keyframe( 2305 obj, 2306 frame, 2307 c4d.LIGHT_BRIGHTNESS, 2308 c4d.DTYPE_REAL, 2309 float(value) / 100.0, 2310 "intensity", 2311 ) 2312 if not result: 2313 return {"error": "Failed to set intensity keyframe"} 2314 2315 elif property_type == "color": 2316 if not isinstance(value, list) or len(value) < 3: 2317 return {"error": "Color must be a list [r,g,b]."} 2318 result = self._set_vector_keyframe( 2319 obj, frame, c4d.LIGHT_COLOR, value[:3], "color" 2320 ) 2321 if not result: 2322 return {"error": "Failed to set color keyframe"} 2323 2324 else: 2325 return { 2326 "error": f"Unsupported property type '{property_type}' for object '{obj.GetName()}'." 2327 } 2328 2329 # --- Success --- 2330 return { 2331 "keyframe_set": { 2332 "object_name": obj.GetName(), 2333 "object_guid": str(obj.GetGUID()), 2334 "property": property_type, 2335 "value_set": value, 2336 "frame": frame, 2337 "success": True, 2338 } 2339 } 2340 2341 except Exception as e: 2342 self.log( 2343 f"[**ERROR**] Error setting keyframe: {str(e)}\n{traceback.format_exc()}" 2344 ) 2345 return { 2346 "error": f"Error setting keyframe: {str(e)}", 2347 "traceback": traceback.format_exc(), 2348 } 2349 2350 def _set_position_keyframe(self, obj, frame, position): 2351 """Set a position keyframe for an object at a specific frame. 2352 2353 Args: 2354 obj: The Cinema 4D object to keyframe 2355 frame: The frame number 2356 position: A list of [x, y, z] coordinates 2357 2358 Returns: 2359 True if successful, False otherwise 2360 """ 2361 if not obj or not isinstance(position, list) or len(position) < 3: 2362 self.log(f"[C4D KEYFRAME] Invalid object or position for keyframe") 2363 return False 2364 2365 try: 2366 # Get the active document and time 2367 doc = c4d.documents.GetActiveDocument() 2368 2369 # Log what we're doing 2370 self.log( 2371 f"[C4D KEYFRAME] Setting position keyframe for {obj.GetName()} at frame {frame} to {position}" 2372 ) 2373 2374 # Create the position vector from the list 2375 pos = c4d.Vector(position[0], position[1], position[2]) 2376 2377 # Set the object's position 2378 obj.SetAbsPos(pos) 2379 2380 # Create track or get existing track for position 2381 track_x = obj.FindCTrack( 2382 c4d.DescID( 2383 c4d.DescLevel(c4d.ID_BASEOBJECT_POSITION, c4d.DTYPE_VECTOR, 0), 2384 c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0), 2385 ) 2386 ) 2387 if track_x is None: 2388 track_x = c4d.CTrack( 2389 obj, 2390 c4d.DescID( 2391 c4d.DescLevel(c4d.ID_BASEOBJECT_POSITION, c4d.DTYPE_VECTOR, 0), 2392 c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0), 2393 ), 2394 ) 2395 obj.InsertTrackSorted(track_x) 2396 2397 track_y = obj.FindCTrack( 2398 c4d.DescID( 2399 c4d.DescLevel(c4d.ID_BASEOBJECT_POSITION, c4d.DTYPE_VECTOR, 0), 2400 c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0), 2401 ) 2402 ) 2403 if track_y is None: 2404 track_y = c4d.CTrack( 2405 obj, 2406 c4d.DescID( 2407 c4d.DescLevel(c4d.ID_BASEOBJECT_POSITION, c4d.DTYPE_VECTOR, 0), 2408 c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0), 2409 ), 2410 ) 2411 obj.InsertTrackSorted(track_y) 2412 2413 track_z = obj.FindCTrack( 2414 c4d.DescID( 2415 c4d.DescLevel(c4d.ID_BASEOBJECT_POSITION, c4d.DTYPE_VECTOR, 0), 2416 c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0), 2417 ) 2418 ) 2419 if track_z is None: 2420 track_z = c4d.CTrack( 2421 obj, 2422 c4d.DescID( 2423 c4d.DescLevel(c4d.ID_BASEOBJECT_POSITION, c4d.DTYPE_VECTOR, 0), 2424 c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0), 2425 ), 2426 ) 2427 obj.InsertTrackSorted(track_z) 2428 2429 # Create time object for the keyframe 2430 time = c4d.BaseTime(frame, doc.GetFps()) 2431 2432 # Set the keyframes for each axis 2433 curve_x = track_x.GetCurve() 2434 key_x = curve_x.AddKey(time) 2435 if key_x is not None and key_x["key"] is not None: 2436 key_x["key"].SetValue(curve_x, position[0]) 2437 2438 curve_y = track_y.GetCurve() 2439 key_y = curve_y.AddKey(time) 2440 if key_y is not None and key_y["key"] is not None: 2441 key_y["key"].SetValue(curve_y, position[1]) 2442 2443 curve_z = track_z.GetCurve() 2444 key_z = curve_z.AddKey(time) 2445 if key_z is not None and key_z["key"] is not None: 2446 key_z["key"].SetValue(curve_z, position[2]) 2447 2448 # Update the document 2449 c4d.EventAdd() 2450 2451 self.log( 2452 f"[C4D KEYFRAME] Successfully set keyframe for {obj.GetName()} at frame {frame}" 2453 ) 2454 return True 2455 2456 except Exception as e: 2457 self.log(f"[C4D KEYFRAME] Error setting position keyframe: {str(e)}") 2458 return False 2459 2460 def _set_vector_keyframe(self, obj, frame, property_id, value, property_name): 2461 """Set a keyframe for a vector property of an object. 2462 2463 Args: 2464 obj: The Cinema 4D object to keyframe 2465 frame: The frame number 2466 property_id: The ID of the property (e.g., c4d.ID_BASEOBJECT_POSITION) 2467 value: A list of [x, y, z] values 2468 property_name: Name of the property for logging 2469 2470 Returns: 2471 True if successful, False otherwise 2472 """ 2473 if not obj or not isinstance(value, list) or len(value) < 3: 2474 self.log( 2475 f"[C4D KEYFRAME] Invalid object or {property_name} value for keyframe" 2476 ) 2477 return False 2478 2479 try: 2480 # Get the active document and time 2481 doc = c4d.documents.GetActiveDocument() 2482 2483 # Log what we're doing 2484 self.log( 2485 f"[C4D KEYFRAME] Setting {property_name} keyframe for {obj.GetName()} at frame {frame} to {value}" 2486 ) 2487 2488 # Create the vector from the list 2489 vec = c4d.Vector(value[0], value[1], value[2]) 2490 2491 # Set the object's property value based on property type 2492 if property_id == c4d.ID_BASEOBJECT_POSITION: 2493 obj.SetAbsPos(vec) 2494 elif property_id == c4d.ID_BASEOBJECT_ROTATION: 2495 # Convert degrees to radians for rotation 2496 rot_rad = c4d.Vector( 2497 c4d.utils.DegToRad(value[0]), 2498 c4d.utils.DegToRad(value[1]), 2499 c4d.utils.DegToRad(value[2]), 2500 ) 2501 obj.SetRotation(rot_rad) 2502 elif property_id == c4d.ID_BASEOBJECT_SCALE: 2503 obj.SetScale(vec) 2504 elif property_id == c4d.LIGHT_COLOR: 2505 obj[c4d.LIGHT_COLOR] = vec 2506 2507 # Component IDs for vector properties 2508 component_ids = [c4d.VECTOR_X, c4d.VECTOR_Y, c4d.VECTOR_Z] 2509 component_names = ["X", "Y", "Z"] 2510 2511 # Create tracks and set keyframes for each component 2512 for i, component_id in enumerate(component_ids): 2513 # Create or get track for this component 2514 track = obj.FindCTrack( 2515 c4d.DescID( 2516 c4d.DescLevel(property_id, c4d.DTYPE_VECTOR, 0), 2517 c4d.DescLevel(component_id, c4d.DTYPE_REAL, 0), 2518 ) 2519 ) 2520 2521 if track is None: 2522 track = c4d.CTrack( 2523 obj, 2524 c4d.DescID( 2525 c4d.DescLevel(property_id, c4d.DTYPE_VECTOR, 0), 2526 c4d.DescLevel(component_id, c4d.DTYPE_REAL, 0), 2527 ), 2528 ) 2529 obj.InsertTrackSorted(track) 2530 2531 # Create time object for the keyframe 2532 time = c4d.BaseTime(frame, doc.GetFps()) 2533 2534 # Set the keyframe 2535 curve = track.GetCurve() 2536 key = curve.AddKey(time) 2537 2538 # Convert rotation values from degrees to radians if necessary 2539 component_value = value[i] 2540 if property_id == c4d.ID_BASEOBJECT_ROTATION: 2541 component_value = c4d.utils.DegToRad(component_value) 2542 2543 if key is not None and key["key"] is not None: 2544 key["key"].SetValue(curve, component_value) 2545 self.log( 2546 f"[C4D KEYFRAME] Set {property_name}.{component_names[i]} keyframe to {value[i]}" 2547 ) 2548 2549 # Update the document 2550 c4d.EventAdd() 2551 2552 self.log( 2553 f"[C4D KEYFRAME] Successfully set {property_name} keyframe for {obj.GetName()} at frame {frame}" 2554 ) 2555 2556 return True 2557 except Exception as e: 2558 self.log(f"[C4D KEYFRAME] Error setting {property_name} keyframe: {str(e)}") 2559 return False 2560 2561 def _set_scalar_keyframe( 2562 self, obj, frame, property_id, data_type, value, property_name 2563 ): 2564 """Set a keyframe for a scalar property of an object. 2565 2566 Args: 2567 obj: The Cinema 4D object to keyframe 2568 frame: The frame number 2569 property_id: The ID of the property (e.g., c4d.LIGHT_BRIGHTNESS) 2570 data_type: The data type of the property (e.g., c4d.DTYPE_REAL) 2571 value: The scalar value 2572 property_name: Name of the property for logging 2573 2574 Returns: 2575 True if successful, False otherwise 2576 """ 2577 if not obj: 2578 self.log(f"[C4D KEYFRAME] Invalid object for {property_name} keyframe") 2579 return False 2580 2581 try: 2582 # Get the active document and time 2583 doc = c4d.documents.GetActiveDocument() 2584 2585 # Log what we're doing 2586 self.log( 2587 f"[C4D KEYFRAME] Setting {property_name} keyframe for {obj.GetName()} at frame {frame} to {value}" 2588 ) 2589 2590 # Set the object's property value 2591 obj[property_id] = value 2592 2593 # Create or get track for this property 2594 track = obj.FindCTrack(c4d.DescID(c4d.DescLevel(property_id, data_type, 0))) 2595 2596 if track is None: 2597 track = c4d.CTrack( 2598 obj, c4d.DescID(c4d.DescLevel(property_id, data_type, 0)) 2599 ) 2600 obj.InsertTrackSorted(track) 2601 2602 # Create time object for the keyframe 2603 time = c4d.BaseTime(frame, doc.GetFps()) 2604 2605 # Set the keyframe 2606 curve = track.GetCurve() 2607 key = curve.AddKey(time) 2608 2609 if key is not None and key["key"] is not None: 2610 key["key"].SetValue(curve, value) 2611 2612 # Update the document 2613 c4d.EventAdd() 2614 2615 self.log( 2616 f"[C4D KEYFRAME] Successfully set {property_name} keyframe for {obj.GetName()} at frame {frame}" 2617 ) 2618 2619 return True 2620 except Exception as e: 2621 self.log(f"[C4D KEYFRAME] Error setting {property_name} keyframe: {str(e)}") 2622 return False 2623 2624 def _set_vector_component_keyframe( 2625 self, 2626 obj, 2627 frame, 2628 property_id, 2629 component_id, 2630 value, 2631 property_name, 2632 component_name, 2633 ): 2634 """Set a keyframe for a single component of a vector property. 2635 2636 Args: 2637 obj: The Cinema 4D object to keyframe 2638 frame: The frame number 2639 property_id: The ID of the property (e.g., c4d.ID_BASEOBJECT_POSITION) 2640 component_id: The ID of the component (e.g., c4d.VECTOR_X) 2641 value: The scalar value for the component 2642 property_name: Name of the property for logging 2643 component_name: Name of the component for logging 2644 2645 Returns: 2646 True if successful, False otherwise 2647 """ 2648 if not obj: 2649 self.log( 2650 f"[C4D KEYFRAME] Invalid object for {property_name}.{component_name} keyframe" 2651 ) 2652 return False 2653 2654 try: 2655 # Get the active document and time 2656 doc = c4d.documents.GetActiveDocument() 2657 2658 # Log what we're doing 2659 self.log( 2660 f"[C4D KEYFRAME] Setting {property_name}.{component_name} keyframe for {obj.GetName()} at frame {frame} to {value}" 2661 ) 2662 2663 # Get the current vector value 2664 current_vec = None 2665 if property_id == c4d.ID_BASEOBJECT_POSITION: 2666 current_vec = obj.GetAbsPos() 2667 elif property_id == c4d.ID_BASEOBJECT_ROTATION: 2668 current_vec = obj.GetRotation() 2669 # For rotation, convert the input value from degrees to radians 2670 value = c4d.utils.DegToRad(value) 2671 elif property_id == c4d.ID_BASEOBJECT_SCALE: 2672 current_vec = obj.GetScale() 2673 elif property_id == c4d.LIGHT_COLOR: 2674 current_vec = obj[c4d.LIGHT_COLOR] 2675 2676 if current_vec is None: 2677 self.log(f"[C4D KEYFRAME] Could not get current {property_name} value") 2678 return False 2679 2680 # Update the specific component 2681 if component_id == c4d.VECTOR_X: 2682 current_vec.x = value 2683 elif component_id == c4d.VECTOR_Y: 2684 current_vec.y = value 2685 elif component_id == c4d.VECTOR_Z: 2686 current_vec.z = value 2687 2688 # Set the updated vector back to the object 2689 if property_id == c4d.ID_BASEOBJECT_POSITION: 2690 obj.SetAbsPos(current_vec) 2691 elif property_id == c4d.ID_BASEOBJECT_ROTATION: 2692 obj.SetRotation(current_vec) 2693 elif property_id == c4d.ID_BASEOBJECT_SCALE: 2694 obj.SetScale(current_vec) 2695 elif property_id == c4d.LIGHT_COLOR: 2696 obj[c4d.LIGHT_COLOR] = current_vec 2697 2698 # Create or get track for this component 2699 track = obj.FindCTrack( 2700 c4d.DescID( 2701 c4d.DescLevel(property_id, c4d.DTYPE_VECTOR, 0), 2702 c4d.DescLevel(component_id, c4d.DTYPE_REAL, 0), 2703 ) 2704 ) 2705 2706 if track is None: 2707 track = c4d.CTrack( 2708 obj, 2709 c4d.DescID( 2710 c4d.DescLevel(property_id, c4d.DTYPE_VECTOR, 0), 2711 c4d.DescLevel(component_id, c4d.DTYPE_REAL, 0), 2712 ), 2713 ) 2714 obj.InsertTrackSorted(track) 2715 2716 # Create time object for the keyframe 2717 time = c4d.BaseTime(frame, doc.GetFps()) 2718 2719 # Set the keyframe 2720 curve = track.GetCurve() 2721 key = curve.AddKey(time) 2722 2723 if key is not None and key["key"] is not None: 2724 key["key"].SetValue(curve, value) 2725 2726 # Update the document 2727 c4d.EventAdd() 2728 2729 self.log( 2730 f"[C4D KEYFRAME] Successfully set {property_name}.{component_name} keyframe for {obj.GetName()} at frame {frame}" 2731 ) 2732 2733 return True 2734 except Exception as e: 2735 self.log( 2736 f"[C4D KEYFRAME] Error setting {property_name}.{component_name} keyframe: {str(e)}" 2737 ) 2738 return False 2739 2740 def handle_save_scene(self, command): 2741 """Handle save_scene command.""" 2742 file_path = command.get("file_path", "") 2743 if not file_path: 2744 return {"error": "No file path provided"} 2745 2746 # Log the save request 2747 self.log(f"[C4D SAVE] Saving scene to: {file_path}") 2748 2749 # Define function to execute on main thread 2750 def save_scene_on_main_thread(doc, file_path): 2751 try: 2752 # Ensure the directory exists 2753 directory = os.path.dirname(file_path) 2754 if directory and not os.path.exists(directory): 2755 os.makedirs(directory) 2756 2757 # Check file extension 2758 _, extension = os.path.splitext(file_path) 2759 if not extension: 2760 file_path += ".c4d" # Add default extension 2761 elif extension.lower() != ".c4d": 2762 file_path = file_path[: -len(extension)] + ".c4d" 2763 2764 # Save document 2765 self.log(f"[C4D SAVE] Saving to: {file_path}") 2766 if not c4d.documents.SaveDocument( 2767 doc, 2768 file_path, 2769 c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST, 2770 c4d.FORMAT_C4DEXPORT, 2771 ): 2772 return {"error": f"Failed to save document to {file_path}"} 2773 2774 # R2025.1 fix: Update document name and path to fix "Untitled-1" issue 2775 try: 2776 # Update the document name 2777 doc.SetDocumentName(os.path.basename(file_path)) 2778 2779 # Update document path 2780 doc.SetDocumentPath(os.path.dirname(file_path)) 2781 2782 # Ensure UI is updated 2783 c4d.EventAdd() 2784 self.log( 2785 f"[C4D SAVE] Updated document name and path for {file_path}" 2786 ) 2787 except Exception as e: 2788 self.log( 2789 f"[C4D SAVE] ## Warning ##: Could not update document name/path: {str(e)}" 2790 ) 2791 2792 return { 2793 "success": True, 2794 "file_path": file_path, 2795 "message": f"Scene saved to {file_path}", 2796 } 2797 except Exception as e: 2798 return {"error": f"Error saving scene: {str(e)}"} 2799 2800 # Execute the save function on the main thread with extended timeout 2801 doc = c4d.documents.GetActiveDocument() 2802 result = self.execute_on_main_thread( 2803 save_scene_on_main_thread, args=(doc, file_path), _timeout=60 2804 ) 2805 return result 2806 2807 def handle_load_scene(self, command): 2808 """Handle load_scene command with improved path handling.""" 2809 file_path = command.get("file_path", "") 2810 if not file_path: 2811 return {"error": "No file path provided"} 2812 2813 # Normalize path to handle different path formats 2814 file_path = os.path.normpath(os.path.expanduser(file_path)) 2815 2816 # Log the normalized path 2817 self.log(f"[C4D LOAD] Normalized file path: {file_path}") 2818 2819 # If path is not absolute, try to resolve it relative to current directory 2820 if not os.path.isabs(file_path): 2821 current_doc_path = c4d.documents.GetActiveDocument().GetDocumentPath() 2822 if current_doc_path: 2823 possible_path = os.path.join(current_doc_path, file_path) 2824 self.log( 2825 f"[C4D LOAD] Trying path relative to current document: {possible_path}" 2826 ) 2827 if os.path.exists(possible_path): 2828 file_path = possible_path 2829 2830 # Check if file exists 2831 if not os.path.exists(file_path): 2832 # Try to find the file in common locations 2833 common_dirs = [ 2834 os.path.expanduser("~/Documents"), 2835 os.path.expanduser("~/Desktop"), 2836 "/Users/Shared/", 2837 ".", 2838 # Add the current working directory 2839 os.getcwd(), 2840 # Add the directory containing the plugin 2841 os.path.dirname(os.path.abspath(__file__)), 2842 # Add parent directory of plugin 2843 os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 2844 ] 2845 2846 # Try with different extensions 2847 filename = os.path.basename(file_path) 2848 basename, ext = os.path.splitext(filename) 2849 if not ext: 2850 filenames_to_try = [filename, filename + ".c4d"] 2851 else: 2852 filenames_to_try = [filename] 2853 2854 # Report search paths 2855 self.log( 2856 f"[C4D LOAD] Searching for file '{filename}' in multiple locations" 2857 ) 2858 2859 # Try each directory and filename combination 2860 for directory in common_dirs: 2861 for fname in filenames_to_try: 2862 possible_path = os.path.join(directory, fname) 2863 self.log(f"[C4D LOAD] Trying path: {possible_path}") 2864 if os.path.exists(possible_path): 2865 file_path = possible_path 2866 self.log(f"[C4D LOAD] Found file at: {file_path}") 2867 break 2868 else: 2869 continue # Continue to next directory if file not found 2870 break # Break main loop if file found 2871 else: 2872 # Try a case-insensitive search as a last resort 2873 for directory in common_dirs: 2874 if os.path.exists(directory): 2875 for file in os.listdir(directory): 2876 if file.lower() == filename.lower(): 2877 file_path = os.path.join(directory, file) 2878 self.log( 2879 f"[C4D LOAD] Found file with case-insensitive match: {file_path}" 2880 ) 2881 break 2882 else: 2883 continue # Continue to next directory if file not found 2884 break # Break main loop if file found 2885 else: 2886 return {"error": f"File not found: {file_path}"} 2887 2888 # Log the load request 2889 self.log(f"[C4D LOAD] Loading scene from: {file_path}") 2890 2891 # Define function to execute on main thread 2892 def load_scene_on_main_thread(file_path): 2893 try: 2894 # Load the document 2895 new_doc = c4d.documents.LoadDocument(file_path, c4d.SCENEFILTER_NONE) 2896 if not new_doc: 2897 return {"error": f"Failed to load document from {file_path}"} 2898 2899 # Set the new document as active 2900 c4d.documents.SetActiveDocument(new_doc) 2901 2902 # Add the document to the documents list 2903 # (only needed if the document wasn't loaded by the document manager) 2904 c4d.documents.InsertBaseDocument(new_doc) 2905 2906 # Update Cinema 4D 2907 c4d.EventAdd() 2908 2909 return { 2910 "success": True, 2911 "file_path": file_path, 2912 "message": f"Scene loaded from {file_path}", 2913 } 2914 except Exception as e: 2915 return {"error": f"Error loading scene: {str(e)}"} 2916 2917 # Execute the load function on the main thread with extended timeout 2918 result = self.execute_on_main_thread( 2919 load_scene_on_main_thread, file_path, _timeout=60 2920 ) 2921 return result 2922 2923 def handle_execute_python(self, command): 2924 """Handle execute_python command with improved output capturing and error handling.""" 2925 code = command.get("code", "") 2926 if not code: 2927 # Try alternative parameter names 2928 code = command.get("script", "") 2929 if not code: 2930 self.log( 2931 "[C4D PYTHON] Error: No Python code provided in 'code' or 'script' parameters" 2932 ) 2933 return {"error": "No Python code provided"} 2934 2935 # For security, limit available modules 2936 allowed_imports = [ 2937 "c4d", 2938 "math", 2939 "random", 2940 "time", 2941 "json", 2942 "os.path", 2943 "sys", 2944 ] 2945 2946 # Check for potentially harmful imports or functions 2947 for banned_keyword in [ 2948 "os.system", 2949 "subprocess", 2950 "exec(", 2951 "eval(", 2952 "import os", 2953 "from os import", 2954 ]: 2955 if banned_keyword in code: 2956 return { 2957 "error": f"Security: Banned keyword found in code: {banned_keyword}" 2958 } 2959 2960 self.log(f"[C4D PYTHON] Executing Python code") 2961 2962 # Prepare improved capture function with thread-safe collection 2963 captured_output = [] 2964 import sys 2965 import traceback 2966 from io import StringIO 2967 2968 # Execute the code on the main thread 2969 def execute_code(): 2970 # Save original stdout 2971 original_stdout = sys.stdout 2972 # Create a StringIO object to capture output 2973 string_io = StringIO() 2974 2975 try: 2976 # Redirect stdout to our capture object 2977 sys.stdout = string_io 2978 2979 # Create a new namespace with limited globals 2980 sandbox = { 2981 "c4d": c4d, 2982 "math": __import__("math"), 2983 "random": __import__("random"), 2984 "time": __import__("time"), 2985 "json": __import__("json"), 2986 "doc": c4d.documents.GetActiveDocument(), 2987 } 2988 2989 # Print startup message 2990 print("[C4D PYTHON] Starting script execution") 2991 2992 # Execute the code 2993 exec(code, sandbox) 2994 2995 # Print completion message 2996 print("[C4D PYTHON] Script execution completed") 2997 2998 # Get any variables that were set in the code 2999 result_vars = { 3000 k: v 3001 for k, v in sandbox.items() 3002 if not k.startswith("__") 3003 and k not in ["c4d", "math", "random", "time", "json", "doc"] 3004 } 3005 3006 # Get captured output 3007 full_output = string_io.getvalue() 3008 3009 # Feed captured output to console log for delta tracking 3010 if full_output: 3011 try: 3012 from DreamTalk.introspection.hierarchy import add_console_message 3013 for line in full_output.strip().split('\n'): 3014 if line.strip(): 3015 add_console_message(line.strip()) 3016 except ImportError: 3017 pass # Console tracking not available 3018 3019 # Process variables to make them serializable 3020 processed_vars = {} 3021 for k, v in result_vars.items(): 3022 try: 3023 # Try to make the value JSON-serializable 3024 if hasattr(v, "__dict__"): 3025 processed_vars[k] = f"<{type(v).__name__} object>" 3026 else: 3027 processed_vars[k] = str(v) 3028 except: 3029 processed_vars[k] = f"<{type(v).__name__} object>" 3030 3031 # Return results 3032 return { 3033 "success": True, 3034 "output": full_output, 3035 "variables": processed_vars, 3036 } 3037 3038 except Exception as e: 3039 error_msg = f"Python execution error: {str(e)}" 3040 self.log(f"[C4D PYTHON] {error_msg}") 3041 3042 # Get traceback info 3043 tb = traceback.format_exc() 3044 3045 # Get any output captured before the error 3046 captured = string_io.getvalue() 3047 3048 # Feed captured output and error to console log 3049 try: 3050 from DreamTalk.introspection.hierarchy import add_console_message 3051 if captured: 3052 for line in captured.strip().split('\n'): 3053 if line.strip(): 3054 add_console_message(line.strip()) 3055 add_console_message(f"ERROR: {str(e)}") 3056 except ImportError: 3057 pass # Console tracking not available 3058 3059 # Return error with details 3060 return { 3061 "error": error_msg, 3062 "traceback": tb, 3063 "output": captured, 3064 } 3065 finally: 3066 # Restore original stdout 3067 sys.stdout = original_stdout 3068 3069 # Close the StringIO object 3070 string_io.close() 3071 3072 # Execute on main thread with extended timeout 3073 result = self.execute_on_main_thread(execute_code, _timeout=30) 3074 3075 # Check for empty output and add warning 3076 if result.get("success") and not result.get("output").strip(): 3077 self.log( 3078 "[C4D PYTHON] ## Warning ##: Script executed successfully but produced no output" 3079 ) 3080 result["warning"] = "Script executed but produced no output" 3081 3082 return result 3083 3084 def handle_create_mograph_cloner(self, command): 3085 """Handle create_mograph_cloner command with context and fixed parameter names.""" 3086 doc = c4d.documents.GetActiveDocument() 3087 if not doc: 3088 return {"error": "No active document"} 3089 3090 requested_name = command.get("cloner_name", "MoGraph Cloner") 3091 mode = command.get("mode", "grid").lower() 3092 object_identifier = command.get("object_name", None) 3093 3094 use_child_guid = False 3095 clone_child_provided = object_identifier is not None 3096 if clone_child_provided: 3097 identifier_str = str(object_identifier) 3098 if "-" in identifier_str and len(identifier_str) > 30: 3099 use_child_guid = True 3100 elif identifier_str.isdigit() or ( 3101 identifier_str.startswith("-") and identifier_str[1:].isdigit() 3102 ): 3103 if len(identifier_str) > 10: 3104 use_child_guid = True 3105 3106 # Count Parsing (robust version from previous step) 3107 default_count = [3, 1, 3] if mode == "grid" else 10 3108 raw_count = command.get("count", default_count) 3109 count_vec = None 3110 count_scalar = None 3111 reported_count = raw_count 3112 try: 3113 if mode == "grid": 3114 if isinstance(raw_count, list) and len(raw_count) >= 3: 3115 count_vec = c4d.Vector( 3116 int(raw_count[0]), int(raw_count[1]), int(raw_count[2]) 3117 ) 3118 reported_count = [int(c) for c in count_vec] 3119 elif isinstance(raw_count, (int, float)): 3120 count_vec = c4d.Vector(int(raw_count), 1, 1) 3121 reported_count = [int(raw_count), 1, 1] 3122 else: 3123 self.log( 3124 f"[CLONER] ## Warning ## Invalid count '{raw_count}' for grid. Using defaults." 3125 ) 3126 count_vec = c4d.Vector(3, 1, 3) 3127 reported_count = [3, 1, 3] 3128 elif mode in ["linear", "radial", "object", "spline", "honeycomb"]: 3129 if isinstance(raw_count, list) and len(raw_count) >= 1: 3130 count_scalar = int(raw_count[0]) 3131 reported_count = count_scalar 3132 elif isinstance(raw_count, (int, float)): 3133 count_scalar = int(raw_count) 3134 reported_count = count_scalar 3135 else: 3136 self.log( 3137 f"[CLONER] ## Warning ## Invalid count '{raw_count}' for {mode}. Using default 10." 3138 ) 3139 count_scalar = 10 3140 reported_count = 10 3141 else: 3142 self.log( 3143 f"[CLONER] ## Warning ## Unsupported mode '{mode}'. Using default grid." 3144 ) 3145 mode = "grid" 3146 count_vec = c4d.Vector(3, 1, 3) 3147 reported_count = [3, 1, 3] 3148 except (ValueError, TypeError) as e: 3149 self.log( 3150 f"[CLONER] ## Warning ## Error parsing count '{raw_count}': {e}. Using defaults." 3151 ) 3152 if mode == "grid": 3153 count_vec = c4d.Vector(3, 1, 3) 3154 reported_count = [3, 1, 3] 3155 else: 3156 count_scalar = 10 3157 reported_count = 10 3158 3159 self.log( 3160 f"[C4D CLONER] Creating: Name='{requested_name}', Mode='{mode}', Count='{reported_count}', Source='{object_identifier}' (GUID: {use_child_guid})" 3161 ) 3162 3163 clone_obj_target = None 3164 child_obj_details = {"source": "Default Cube"} 3165 child_guid = None 3166 child_actual_name = "Default Cube" 3167 if clone_child_provided: 3168 clone_obj_target = self.find_object_by_name( 3169 doc, object_identifier, use_guid=use_child_guid 3170 ) 3171 if not clone_obj_target: 3172 search_type = "GUID" if use_child_guid else "Name" 3173 return { 3174 "error": f"Object '{object_identifier}' (searched by {search_type}) not found to clone." 3175 } 3176 else: 3177 target_name = clone_obj_target.GetName() 3178 target_guid = str(clone_obj_target.GetGUID()) 3179 child_obj_details["source"] = ( 3180 f"Existing Object: '{target_name}' (GUID: {target_guid})" 3181 ) 3182 self.log( 3183 f"[CLONER] Found clone object: '{target_name}' (GUID: {target_guid})" 3184 ) 3185 3186 def create_mograph_cloner_safe( 3187 doc, name, mode, count, count_vec, found_clone_object 3188 ): 3189 nonlocal child_guid, child_actual_name 3190 try: 3191 cloner = c4d.BaseObject(c4d.Omgcloner) 3192 if not cloner: 3193 raise RuntimeError("Failed to create Cloner object") 3194 cloner.SetName(name) 3195 3196 mode_ids = { 3197 "linear": 0, 3198 "radial": 2, 3199 "grid": 1, 3200 "object": 3, 3201 "spline": 4, 3202 "honeycomb": 5, 3203 } 3204 mode_id = mode_ids.get(mode, 1) 3205 3206 doc.StartUndo() 3207 doc.InsertObject(cloner) 3208 doc.AddUndo(c4d.UNDOTYPE_NEW, cloner) 3209 cloner[c4d.ID_MG_MOTIONGENERATOR_MODE] = mode_id 3210 3211 if found_clone_object: 3212 child_obj = found_clone_object.GetClone() 3213 else: 3214 child_obj = c4d.BaseObject(c4d.Ocube) 3215 child_obj.SetName("Default Cube") 3216 child_obj.SetAbsScale(c4d.Vector(0.5, 0.5, 0.5)) 3217 if not child_obj: 3218 raise RuntimeError("Failed to create/clone child object") 3219 3220 doc.InsertObject(child_obj) 3221 doc.AddUndo(c4d.UNDOTYPE_NEW, child_obj) 3222 child_actual_name = child_obj.GetName() 3223 child_guid = str(child_obj.GetGUID()) 3224 child_obj.InsertUnderLast(cloner) 3225 self.register_object_name( 3226 child_obj, 3227 ( 3228 found_clone_object.GetName() 3229 if found_clone_object 3230 else "Default Cube" 3231 ), 3232 ) 3233 3234 mg_bc = cloner.GetDataInstance() 3235 if not mg_bc: 3236 raise RuntimeError("Failed to get MoGraph BaseContainer") 3237 3238 # --- FIXED: Use getattr for potentially missing constants --- 3239 if mode == "linear": 3240 mg_bc[c4d.MG_LINEAR_COUNT] = count 3241 # Use getattr for MG_LINEAR_PERSTEP, provide default vector if missing 3242 perstep_id = getattr(c4d, "MG_LINEAR_PERSTEP", None) 3243 mode_id_param = getattr(c4d, "MG_LINEAR_MODE", None) 3244 perstep_mode_val = getattr( 3245 c4d, "MG_LINEAR_MODE_PERSTEP", 0 3246 ) # Default to 0 if missing 3247 3248 if perstep_id: 3249 mg_bc[perstep_id] = c4d.Vector(0, 50, 0) 3250 else: 3251 self.log("[CLONER] ## Warning ## MG_LINEAR_PERSTEP not found.") 3252 if mode_id_param: 3253 mg_bc[mode_id_param] = perstep_mode_val 3254 else: 3255 self.log("[CLONER] ## Warning ## MG_LINEAR_MODE not found.") 3256 self.log(f"[C4D CLONER] Set linear count: {count}") 3257 # --- END FIXED --- 3258 elif mode == "grid": 3259 version = c4d.GetC4DVersion() 3260 try: 3261 if version >= 2025000 and hasattr(c4d, "MGGRIDARRAY_MODE"): 3262 mg_bc[c4d.MGGRIDARRAY_MODE] = c4d.MGGRIDARRAY_MODE_ENDPOINT 3263 mg_bc[c4d.MGGRIDARRAY_RESOLUTION] = count_vec 3264 mg_bc[c4d.MGGRIDARRAY_SIZE] = c4d.Vector(200, 200, 200) 3265 self.log( 3266 f"[C4D CLONER] Using 2025+ MGGRIDARRAY_*; resolution: {count_vec}" 3267 ) 3268 else: 3269 if ( 3270 hasattr(c4d, "MG_GRID_COUNT") 3271 and hasattr(c4d, "MG_GRID_MODE") 3272 and hasattr(c4d, "MG_GRID_SIZE") 3273 ): 3274 mg_bc[c4d.MG_GRID_COUNT] = count_vec 3275 mg_bc[c4d.MG_GRID_MODE] = c4d.MG_GRID_MODE_PERSTEP 3276 mg_bc[c4d.MG_GRID_SIZE] = c4d.Vector(100, 100, 100) 3277 self.log( 3278 f"[C4D CLONER] Using legacy MG_GRID_COUNT: {count_vec}, Mode: Per Step" 3279 ) 3280 else: 3281 if all( 3282 hasattr(c4d, attr) 3283 for attr in [ 3284 "MG_GRID_COUNT_X", 3285 "MG_GRID_COUNT_Y", 3286 "MG_GRID_COUNT_Z", 3287 "MG_CLONER_SIZE", 3288 ] 3289 ): 3290 mg_bc[c4d.MG_GRID_COUNT_X] = int(count_vec.x) 3291 mg_bc[c4d.MG_GRID_COUNT_Y] = int(count_vec.y) 3292 mg_bc[c4d.MG_GRID_COUNT_Z] = int(count_vec.z) 3293 mg_bc[c4d.MG_CLONER_SIZE] = c4d.Vector( 3294 200, 200, 200 3295 ) 3296 self.log( 3297 f"[C4D CLONER] Using legacy MG_GRID_COUNT_X/Y/Z: {count_vec}, Size: 200" 3298 ) 3299 else: 3300 self.log( 3301 "[C4D CLONER] ## Warning ##: Could not find suitable grid parameters." 3302 ) 3303 except Exception as e_grid: 3304 self.log( 3305 f"[C4D CLONER] ## Warning ## Grid mode config failed: {e_grid}" 3306 ) 3307 elif mode == "radial": 3308 if hasattr(c4d, "MG_POLY_COUNT") and hasattr(c4d, "MG_POLY_RADIUS"): 3309 mg_bc[c4d.MG_POLY_COUNT] = count 3310 mg_bc[c4d.MG_POLY_RADIUS] = 200 3311 self.log(f"[C4D CLONER] Set radial count: {count}, Radius: 200") 3312 else: 3313 self.log( 3314 "[C4D CLONER] ## Warning ##: Radial parameters not found." 3315 ) 3316 elif mode == "object": 3317 self.log("[C4D CLONER] Object mode selected, requires linking.") 3318 if not hasattr(c4d, "MG_OBJECT_LINK"): 3319 self.log( 3320 "[C4D CLONER] ## Warning ##: Object link parameter not found." 3321 ) 3322 3323 if hasattr(c4d, "MGCLONER_MODE"): 3324 cloner[c4d.MGCLONER_MODE] = c4d.MGCLONER_MODE_ITERATE 3325 3326 doc.EndUndo() 3327 c4d.EventAdd() 3328 3329 actual_cloner_name = cloner.GetName() 3330 cloner_guid = str(cloner.GetGUID()) 3331 pos_vec = cloner.GetAbsPos() 3332 self.register_object_name(cloner, name) # Use 'name' (requested name) 3333 3334 return { 3335 "cloner": { 3336 "requested_name": name, 3337 "actual_name": actual_cloner_name, 3338 "guid": cloner_guid, 3339 "type": mode, 3340 "count_set": reported_count, 3341 "position": [pos_vec.x, pos_vec.y, pos_vec.z], 3342 "child_object": { 3343 "source": child_obj_details["source"], 3344 "actual_name": child_actual_name, 3345 "guid": child_guid, 3346 }, 3347 } 3348 } 3349 except Exception as e: 3350 doc.EndUndo() 3351 self.log( 3352 f"[**ERROR**] Exception during cloner creation safe wrapper: {str(e)}\n{traceback.format_exc()}" 3353 ) 3354 return { 3355 "error": f"Exception during cloner creation: {str(e)}", 3356 "traceback": traceback.format_exc(), 3357 } 3358 3359 try: 3360 self.log("[C4D CLONER] Dispatching cloner creation to main thread") 3361 result = self.execute_on_main_thread( 3362 create_mograph_cloner_safe, 3363 args=( 3364 doc, 3365 requested_name, 3366 mode, 3367 count_scalar, 3368 count_vec, 3369 clone_obj_target, 3370 ), 3371 _timeout=30, 3372 ) 3373 if isinstance(result, dict) and "error" in result: 3374 self.log(f"[C4D CLONER] Error: {result['error']}") 3375 return result 3376 return result 3377 except Exception as e: 3378 self.log( 3379 f"[**ERROR**] Exception in cloner handler dispatch: {str(e)}\n{traceback.format_exc()}" 3380 ) 3381 return { 3382 "error": f"Exception dispatching cloner handler: {str(e)}", 3383 "traceback": traceback.format_exc(), 3384 } 3385 3386 def handle_list_objects(self): 3387 """Handle list_objects command with comprehensive object detection including MoGraph objects.""" 3388 doc = c4d.documents.GetActiveDocument() 3389 objects = [] 3390 found_ids = set() # Track object IDs to avoid duplicates 3391 3392 # Function to recursively get all objects including children with improved traversal 3393 def get_objects_recursive(start_obj, depth=0): 3394 current_obj = start_obj 3395 while current_obj: 3396 try: 3397 # Get object ID to avoid duplicates 3398 obj_id = str(current_obj.GetGUID()) 3399 3400 # Skip if we've already processed this object 3401 if obj_id in found_ids: 3402 current_obj = current_obj.GetNext() 3403 continue 3404 3405 found_ids.add(obj_id) 3406 3407 # Get object name and type 3408 obj_name = current_obj.GetName() 3409 obj_type_id = current_obj.GetType() 3410 3411 # Get basic object info with enhanced MoGraph detection 3412 obj_type = self.get_object_type_name(current_obj) 3413 3414 # Additional properties dictionary for specific object types 3415 additional_props = {} 3416 3417 # MoGraph Cloner enhanced detection - explicitly check for cloner type 3418 if obj_type_id == c4d.Omgcloner: 3419 obj_type = "MoGraph Cloner" 3420 try: 3421 # Get the cloner mode 3422 mode_id = current_obj[c4d.ID_MG_MOTIONGENERATOR_MODE] 3423 modes = { 3424 0: "Linear", 3425 1: "Grid", 3426 2: "Radial", 3427 3: "Object", 3428 } 3429 mode_name = modes.get(mode_id, f"Mode {mode_id}") 3430 additional_props["cloner_mode"] = mode_name 3431 3432 # Add counts based on mode - using R2025.1 constant paths 3433 try: 3434 # Try R2025.1 module path first 3435 if mode_id == 0: # Linear 3436 if hasattr(c4d, "MG_LINEAR_COUNT"): 3437 additional_props["count"] = current_obj[ 3438 c4d.MG_LINEAR_COUNT 3439 ] 3440 elif mode_id == 1: # Grid 3441 if hasattr(c4d, "MGGRIDARRAY_RESOLUTION"): 3442 resolution = current_obj[ 3443 c4d.MGGRIDARRAY_RESOLUTION 3444 ] 3445 additional_props["count_x"] = int(resolution.x) 3446 additional_props["count_y"] = int(resolution.y) 3447 additional_props["count_z"] = int(resolution.z) 3448 # Fallback to legacy MG_GRID_COUNT_* if available 3449 elif all( 3450 hasattr(c4d, attr) 3451 for attr in [ 3452 "MG_GRID_COUNT_X", 3453 "MG_GRID_COUNT_Y", 3454 "MG_GRID_COUNT_Z", 3455 ] 3456 ): 3457 additional_props["count_x"] = int( 3458 current_obj[c4d.MG_GRID_COUNT_X] 3459 ) 3460 additional_props["count_y"] = int( 3461 current_obj[c4d.MG_GRID_COUNT_Y] 3462 ) 3463 additional_props["count_z"] = int( 3464 current_obj[c4d.MG_GRID_COUNT_Z] 3465 ) 3466 else: 3467 self.log( 3468 "[C4D CLONER WARNING] No valid grid count parameters found" 3469 ) 3470 elif mode_id == 2: # Radial 3471 if hasattr(c4d, "MG_POLY_COUNT"): 3472 additional_props["count"] = current_obj[ 3473 c4d.MG_POLY_COUNT 3474 ] 3475 except Exception as e: 3476 self.log( 3477 f"[C4D CLONER] Error getting cloner counts: {str(e)}" 3478 ) 3479 3480 self.log( 3481 f"[C4D CLONER] Detected MoGraph Cloner: {obj_name}, Mode: {mode_name}" 3482 ) 3483 except Exception as e: 3484 self.log( 3485 f"[C4D CLONER] Error getting cloner details: {str(e)}" 3486 ) 3487 3488 # MoGraph Effector enhanced detection 3489 elif 1019544 <= obj_type_id <= 1019644: 3490 if obj_type_id == c4d.Omgrandom: 3491 obj_type = "Random Effector" 3492 elif obj_type_id == c4d.Omgformula: 3493 obj_type = "Formula Effector" 3494 elif hasattr(c4d, "Omgstep") and obj_type_id == c4d.Omgstep: 3495 obj_type = "Step Effector" 3496 else: 3497 obj_type = "MoGraph Effector" 3498 3499 # Try to get effector strength 3500 try: 3501 if hasattr(c4d, "ID_MG_BASEEFFECTOR_STRENGTH"): 3502 additional_props["strength"] = current_obj[ 3503 c4d.ID_MG_BASEEFFECTOR_STRENGTH 3504 ] 3505 except: 3506 pass 3507 3508 # Field objects enhanced detection 3509 elif 1039384 <= obj_type_id <= 1039484: 3510 field_types = { 3511 1039384: "Spherical Field", 3512 1039385: "Box Field", 3513 1039386: "Cylindrical Field", 3514 1039387: "Torus Field", 3515 1039388: "Cone Field", 3516 1039389: "Linear Field", 3517 1039390: "Radial Field", 3518 1039394: "Noise Field", 3519 } 3520 obj_type = field_types.get(obj_type_id, "Field") 3521 3522 # Try to get field strength 3523 try: 3524 if hasattr(c4d, "FIELD_STRENGTH"): 3525 additional_props["strength"] = current_obj[ 3526 c4d.FIELD_STRENGTH 3527 ] 3528 except: 3529 pass 3530 3531 # Base object info 3532 obj_info = { 3533 "id": obj_id, 3534 "name": obj_name, 3535 "type": obj_type, 3536 "type_id": obj_type_id, 3537 "level": depth, 3538 **additional_props, # Include any additional properties 3539 } 3540 3541 # Position 3542 if hasattr(current_obj, "GetAbsPos"): 3543 pos = current_obj.GetAbsPos() 3544 obj_info["position"] = [pos.x, pos.y, pos.z] 3545 3546 # Rotation (converted to degrees) 3547 if hasattr(current_obj, "GetRelRot"): 3548 rot = current_obj.GetRelRot() 3549 obj_info["rotation"] = [ 3550 c4d.utils.RadToDeg(rot.x), 3551 c4d.utils.RadToDeg(rot.y), 3552 c4d.utils.RadToDeg(rot.z), 3553 ] 3554 3555 # Scale 3556 if hasattr(current_obj, "GetAbsScale"): 3557 scale = current_obj.GetAbsScale() 3558 obj_info["scale"] = [scale.x, scale.y, scale.z] 3559 3560 # Add to the list 3561 objects.append(obj_info) 3562 3563 # Recurse children 3564 if current_obj.GetDown(): 3565 get_objects_recursive(current_obj.GetDown(), depth + 1) 3566 3567 # Move to next object 3568 current_obj = current_obj.GetNext() 3569 except Exception as e: 3570 self.log(f"[C4D CLONER] Error processing object: {str(e)}") 3571 if current_obj: 3572 current_obj = current_obj.GetNext() 3573 3574 def get_all_root_objects(): 3575 # Start with standard objects 3576 get_objects_recursive(doc.GetFirstObject()) 3577 3578 # Also check for MoGraph objects that might not be in main hierarchy 3579 # (This is more for thoroughness as get_objects_recursive should find everything) 3580 try: 3581 if hasattr(c4d, "GetMoData"): 3582 mograph_data = c4d.GetMoData(doc) 3583 if mograph_data: 3584 for i in range(mograph_data.GetCount()): 3585 obj = mograph_data.GetObject(i) 3586 if obj and obj.GetType() == c4d.Omgcloner: 3587 if str(obj.GetGUID()) not in found_ids: 3588 get_objects_recursive(obj) 3589 except Exception as e: 3590 self.log(f"[**ERROR**] Error checking MoGraph objects: {str(e)}") 3591 3592 # Get all objects starting from the root level 3593 get_all_root_objects() 3594 3595 self.log( 3596 f"[C4D] Comprehensive object search complete, found {len(objects)} objects" 3597 ) 3598 return {"objects": objects} 3599 3600 def handle_add_effector(self, command): 3601 """Adds a MoGraph effector and optionally links it to a cloner, returns context.""" 3602 doc = c4d.documents.GetActiveDocument() 3603 if not doc: 3604 return {"error": "No active document"} 3605 3606 type_name = command.get("effector_type", "random").lower() 3607 # --- Use 'target' preferentially, fallback to 'cloner_name' --- 3608 cloner_identifier = command.get("target") or command.get("cloner_name") or "" 3609 properties = command.get("properties", {}) 3610 requested_name = ( 3611 command.get("name") 3612 or command.get("effector_name") 3613 or f"{type_name.capitalize()} Effector" 3614 ) 3615 3616 # --- Detect if the cloner_identifier looks like a GUID --- 3617 use_cloner_guid = False 3618 if cloner_identifier: # Check only if identifier exists 3619 identifier_str = str(cloner_identifier) 3620 if "-" in identifier_str and len(identifier_str) > 30: 3621 use_cloner_guid = True 3622 elif identifier_str.isdigit() or ( 3623 identifier_str.startswith("-") and identifier_str[1:].isdigit() 3624 ): 3625 if len(identifier_str) > 10: 3626 use_cloner_guid = True 3627 # --- End GUID detection --- 3628 3629 effector = None 3630 try: 3631 self.log( 3632 f"[C4D EFFECTOR] Creating {type_name} effector named '{requested_name}'" 3633 ) 3634 if cloner_identifier: 3635 self.log( 3636 f"[C4D EFFECTOR] Will attempt to apply to cloner '{cloner_identifier}' (Treat as GUID: {use_cloner_guid})" 3637 ) 3638 3639 # (Effector type mapping and creation remains the same) 3640 effector_types = { 3641 "random": c4d.Omgrandom, 3642 "formula": c4d.Omgformula, 3643 "step": c4d.Omgstep, 3644 "target": getattr( 3645 c4d, "Omgtarget", getattr(c4d, "Omgeffectortarget", None) 3646 ), 3647 "time": c4d.Omgtime, 3648 "sound": c4d.Omgsound, 3649 "plain": c4d.Omgplain, 3650 "delay": c4d.Omgdelay, 3651 "spline": c4d.Omgspline, 3652 "python": c4d.Omgpython, 3653 "shader": c4d.Omgshader, 3654 "volume": c4d.Omgvolume, 3655 } 3656 if hasattr(c4d, "Omgfalloff"): 3657 effector_types["falloff"] = c4d.Omgfalloff 3658 effector_id = effector_types.get(type_name) 3659 if effector_id is None: 3660 return {"error": f"Unsupported effector type: {type_name}"} 3661 3662 doc.StartUndo() 3663 effector = c4d.BaseObject(effector_id) 3664 if effector is None: 3665 raise RuntimeError(f"Failed to create {type_name} effector BaseObject") 3666 effector.SetName(requested_name) 3667 3668 # (Property setting remains the same) 3669 bc = effector.GetDataInstance() 3670 if bc: 3671 if "strength" in properties and isinstance( 3672 properties["strength"], (int, float) 3673 ): 3674 try: 3675 bc[c4d.ID_MG_BASEEFFECTOR_STRENGTH] = ( 3676 float(properties["strength"]) / 100.0 3677 ) 3678 except Exception as e_prop: 3679 self.log(f"Warning: Could not set strength: {e_prop}") 3680 if "position_mode" in properties and isinstance( 3681 properties["position_mode"], bool 3682 ): 3683 try: 3684 bc[c4d.ID_MG_BASEEFFECTOR_POSITION_ACTIVE] = properties[ 3685 "position_mode" 3686 ] 3687 except Exception as e_prop: 3688 self.log(f"Warning: Could not set position_mode: {e_prop}") 3689 if "rotation_mode" in properties and isinstance( 3690 properties["rotation_mode"], bool 3691 ): 3692 try: 3693 bc[c4d.ID_MG_BASEEFFECTOR_ROTATION_ACTIVE] = properties[ 3694 "rotation_mode" 3695 ] 3696 except Exception as e_prop: 3697 self.log(f"Warning: Could not set rotation_mode: {e_prop}") 3698 if "scale_mode" in properties and isinstance( 3699 properties["scale_mode"], bool 3700 ): 3701 try: 3702 bc[c4d.ID_MG_BASEEFFECTOR_SCALE_ACTIVE] = properties[ 3703 "scale_mode" 3704 ] 3705 except Exception as e_prop: 3706 self.log(f"Warning: Could not set scale_mode: {e_prop}") 3707 else: 3708 self.log( 3709 f"Warning: Could not get BaseContainer for effector '{requested_name}'" 3710 ) 3711 3712 doc.InsertObject(effector) 3713 doc.AddUndo(c4d.UNDOTYPE_NEW, effector) 3714 3715 # --- Linking logic (remains the same, but find_object_by_name call uses correct flag now) --- 3716 cloner_applied_to_name = "None" 3717 cloner_applied_to_guid = None 3718 cloner_found = None 3719 3720 if cloner_identifier: 3721 # Pass the use_cloner_guid flag correctly 3722 cloner_found = self.find_object_by_name( 3723 doc, cloner_identifier, use_guid=use_cloner_guid 3724 ) 3725 3726 if cloner_found is None: 3727 search_type = "GUID" if use_cloner_guid else "Name" 3728 self.log( 3729 f"[C4D EFFECTOR] ## Warning ##: Cloner '{cloner_identifier}' (searched by {search_type}) not found, effector created but not linked." 3730 ) 3731 else: 3732 if cloner_found.GetType() != c4d.Omgcloner: 3733 self.log( 3734 f"[C4D EFFECTOR] ## Warning ##: Target '{cloner_found.GetName()}' is not a MoGraph Cloner (Type: {cloner_found.GetType()})" 3735 ) 3736 else: 3737 try: 3738 effector_list = None 3739 try: 3740 effector_list = cloner_found[ 3741 c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST 3742 ] 3743 except: 3744 self.log( 3745 f"[C4D EFFECTOR] Creating new effector list for cloner '{cloner_found.GetName()}'" 3746 ) 3747 if not isinstance(effector_list, c4d.InExcludeData): 3748 effector_list = c4d.InExcludeData() 3749 3750 effector_list.InsertObject(effector, 1) 3751 cloner_found[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] = ( 3752 effector_list 3753 ) 3754 doc.AddUndo(c4d.UNDOTYPE_CHANGE, cloner_found) 3755 cloner_applied_to_name = cloner_found.GetName() 3756 cloner_applied_to_guid = str(cloner_found.GetGUID()) 3757 self.log( 3758 f"[C4D EFFECTOR] Successfully applied effector to cloner '{cloner_applied_to_name}'" 3759 ) 3760 except Exception as e_apply: 3761 self.log( 3762 f"[**ERROR**] Error applying effector to cloner '{cloner_found.GetName()}': {str(e_apply)}" 3763 ) 3764 3765 doc.EndUndo() 3766 c4d.EventAdd() 3767 3768 # --- Contextual Return (remains the same) --- 3769 actual_effector_name = effector.GetName() 3770 effector_guid = str(effector.GetGUID()) 3771 pos_vec = effector.GetAbsPos() 3772 3773 self.register_object_name(effector, requested_name) 3774 3775 return { 3776 "effector": { 3777 "requested_name": requested_name, 3778 "actual_name": actual_effector_name, 3779 "guid": effector_guid, 3780 "type": type_name, 3781 "position": [pos_vec.x, pos_vec.y, pos_vec.z], 3782 "applied_to_cloner_name": cloner_applied_to_name, 3783 "applied_to_cloner_guid": cloner_applied_to_guid, 3784 } 3785 } 3786 3787 except Exception as e: 3788 doc.EndUndo() 3789 self.log( 3790 f"[**ERROR**] Error creating effector: {str(e)}\n{traceback.format_exc()}" 3791 ) 3792 if effector and not effector.GetDocument(): 3793 try: 3794 effector.Remove() 3795 except: 3796 pass 3797 return { 3798 "error": f"Failed to create effector: {str(e)}", 3799 "traceback": traceback.format_exc(), 3800 } 3801 3802 def handle_apply_mograph_fields(self, command): 3803 """Applies a MoGraph field (as a child) to a MoGraph effector, returns context.""" 3804 doc = c4d.documents.GetActiveDocument() 3805 if not doc: 3806 return {"error": "No active document"} 3807 3808 field_type = command.get("field_type", "spherical").lower() 3809 requested_name = command.get("field_name", f"{field_type.capitalize()} Field") 3810 target_identifier = command.get("target_name", "") 3811 parameters = command.get("parameters", {}) 3812 3813 # --- REVISED: Detect if target_identifier is likely a GUID --- 3814 use_target_guid = False 3815 if target_identifier: 3816 identifier_str = str(target_identifier) 3817 if "-" in identifier_str and len(identifier_str) > 30: 3818 use_target_guid = True 3819 elif identifier_str.isdigit() or ( 3820 identifier_str.startswith("-") and identifier_str[1:].isdigit() 3821 ): 3822 if len(identifier_str) > 10: 3823 use_target_guid = True 3824 # --- END REVISED --- 3825 3826 field = None 3827 try: 3828 self.log( 3829 f"[C4D FIELDS] Request: Field='{requested_name}' Type='{field_type}' Target='{target_identifier}' (Treat as GUID: {use_target_guid})" 3830 ) 3831 3832 target = self.find_object_by_name( 3833 doc, target_identifier, use_guid=use_target_guid 3834 ) 3835 if not target: 3836 search_type = "GUID" if use_target_guid else "Name" 3837 return { 3838 "error": f"Target effector '{target_identifier}' (searched by {search_type}) not found" 3839 } 3840 3841 valid_effector_types = { 3842 c4d.Omgplain, 3843 c4d.Omgrandom, 3844 c4d.Omgstep, 3845 c4d.Omgdelay, 3846 c4d.Omgformula, 3847 c4d.Omgtime, 3848 c4d.Omgsound, 3849 c4d.Omgpython, 3850 c4d.Omgshader, 3851 c4d.Omgvolume, 3852 getattr(c4d, "Omgtarget", getattr(c4d, "Omgeffectortarget", None)), 3853 } 3854 if target.GetType() not in valid_effector_types: 3855 return { 3856 "error": f"Target '{target.GetName()}' is not a supported effector type (Type: {target.GetType()})" 3857 } 3858 3859 target_name = target.GetName() 3860 target_guid = str(target.GetGUID()) 3861 3862 field_type_map = { 3863 "spherical": getattr(c4d, "Fspherical", 440000243), 3864 "box": getattr(c4d, "Fbox", 440000244), 3865 "radial": getattr(c4d, "Fradial", 440000245), 3866 "linear": getattr(c4d, "Flinear", 440000246), 3867 "noise": 440000248, 3868 "cylinder": getattr(c4d, "Fcylinder", 1039386), 3869 "cone": getattr(c4d, "Fcone", 1039388), 3870 "torus": getattr(c4d, "Ftorus", 1039387), 3871 "formula": getattr(c4d, "Fformula", 1040830), 3872 "random": getattr(c4d, "Frandom", 1040831), 3873 "step": getattr(c4d, "Fstep", 1040832), 3874 } 3875 field_type_id = field_type_map.get(field_type) 3876 if not field_type_id: 3877 return {"error": f"Unsupported field type: '{field_type}'"} 3878 3879 doc.StartUndo() 3880 field = c4d.BaseObject(field_type_id) 3881 if not field: 3882 raise RuntimeError("Failed to create field BaseObject") 3883 field.SetName(requested_name) 3884 3885 bc = field.GetDataInstance() 3886 if bc: 3887 if ( 3888 "position" in parameters 3889 and isinstance(parameters["position"], list) 3890 and len(parameters["position"]) >= 3 3891 ): 3892 try: 3893 field.SetAbsPos( 3894 c4d.Vector(*[float(p) for p in parameters["position"][:3]]) 3895 ) 3896 except (ValueError, TypeError): 3897 self.log( 3898 f"Warning: Invalid field position {parameters['position']}" 3899 ) 3900 if ( 3901 "scale" in parameters 3902 and isinstance(parameters["scale"], list) 3903 and len(parameters["scale"]) >= 3 3904 ): 3905 try: 3906 field.SetAbsScale( 3907 c4d.Vector(*[float(p) for p in parameters["scale"][:3]]) 3908 ) 3909 except (ValueError, TypeError): 3910 self.log(f"Warning: Invalid field scale {parameters['scale']}") 3911 if ( 3912 "rotation" in parameters 3913 and isinstance(parameters["rotation"], list) 3914 and len(parameters["rotation"]) >= 3 3915 ): 3916 try: 3917 hpb_rad = [ 3918 c4d.utils.DegToRad(float(angle)) 3919 for angle in parameters["rotation"][:3] 3920 ] 3921 field.SetAbsRot(c4d.Vector(*hpb_rad)) 3922 except (ValueError, TypeError): 3923 self.log( 3924 f"Warning: Invalid field rotation {parameters['rotation']}" 3925 ) 3926 if field_type == "spherical" and "radius" in parameters: 3927 radius_id = getattr( 3928 c4d, "FIELD_SIZE", getattr(c4d, "FIELDSPHERICAL_RADIUS", None) 3929 ) 3930 if radius_id: 3931 try: 3932 bc[radius_id] = float(parameters["radius"]) 3933 except (ValueError, TypeError): 3934 self.log( 3935 f"Warning: Invalid radius value {parameters['radius']}" 3936 ) 3937 else: 3938 self.log( 3939 "Warning: Could not find radius parameter ID for spherical field." 3940 ) 3941 else: 3942 self.log( 3943 f"Warning: Could not get BaseContainer for field '{requested_name}'" 3944 ) 3945 3946 doc.InsertObject(field) 3947 doc.AddUndo(c4d.UNDOTYPE_NEW, field) 3948 field.InsertUnder(target) 3949 doc.AddUndo(c4d.UNDOTYPE_CHANGE, target) 3950 doc.EndUndo() 3951 c4d.EventAdd() 3952 self.log( 3953 f"[C4D FIELDS] Linked field '{field.GetName()}' to effector '{target_name}'" 3954 ) 3955 3956 actual_field_name = field.GetName() 3957 field_guid = str(field.GetGUID()) 3958 pos_vec = field.GetAbsPos() 3959 self.register_object_name(field, requested_name) 3960 3961 return { 3962 "field": { 3963 "requested_name": requested_name, 3964 "actual_name": actual_field_name, 3965 "guid": field_guid, 3966 "type": field_type, 3967 "target_name": target_name, 3968 "target_guid": target_guid, 3969 "position": [pos_vec.x, pos_vec.y, pos_vec.z], 3970 } 3971 } 3972 3973 except Exception as e: 3974 doc.EndUndo() 3975 self.log(f"[**ERROR**] Error applying field: {e}\n{traceback.format_exc()}") 3976 if field and not field.GetDocument(): 3977 try: 3978 field.Remove() 3979 except: 3980 pass 3981 return { 3982 "error": f"Exception occurred applying field: {str(e)}", 3983 "traceback": traceback.format_exc(), 3984 } 3985 3986 def handle_create_soft_body(self, command): 3987 """Handle create_soft_body command with GUID support.""" 3988 doc = c4d.documents.GetActiveDocument() 3989 if not doc: 3990 return {"error": "No active document"} 3991 3992 # --- MODIFIED: Identify target object --- 3993 identifier = None 3994 use_guid = False 3995 if command.get("guid"): 3996 identifier = command.get("guid") 3997 use_guid = True 3998 self.log(f"[SOFT BODY] Using GUID identifier: '{identifier}'") 3999 elif command.get("object_name"): 4000 identifier = command.get("object_name") 4001 use_guid = False 4002 self.log(f"[SOFT BODY] Using Name identifier: '{identifier}'") 4003 else: 4004 return {"error": "No object identifier ('guid' or 'object_name') provided."} 4005 4006 # Find target object using the determined method 4007 obj = self.find_object_by_name(doc, identifier, use_guid=use_guid) 4008 if obj is None: 4009 search_type = "GUID" if use_guid else "Name" 4010 return { 4011 "error": f"Object '{identifier}' (searched by {search_type}) not found for soft body." 4012 } 4013 # --- END MODIFIED --- 4014 4015 # Get parameters (using original logic) 4016 name = command.get( 4017 "name", f"{obj.GetName()} Soft Body" 4018 ) # Default name based on object 4019 stiffness = command.get("stiffness", 50) 4020 mass = command.get("mass", 1.0) 4021 4022 # Define safe wrapper (using original logic, but noting potential RIGID_BODY_SOFTBODY issue) 4023 def create_soft_body_safe( 4024 target_obj, tag_name, stiff_val, mass_val, obj_actual_name 4025 ): 4026 self.log( 4027 f"[C4D SBODY] Creating soft body dynamics tag '{tag_name}' for object '{obj_actual_name}'" 4028 ) 4029 dynamics_tag_id = 180000102 # Standard Dynamics Body Tag ID 4030 4031 tag = c4d.BaseTag(dynamics_tag_id) 4032 if tag is None: 4033 self.log( 4034 f"[C4D SBODY] Error: Failed to create Dynamics Body tag with ID {dynamics_tag_id}" 4035 ) 4036 raise RuntimeError("Failed to create Dynamics Body tag") 4037 4038 tag.SetName(tag_name) 4039 self.log(f"[C4D SBODY] Successfully created dynamics tag: {tag_name}") 4040 4041 # --- Potential Issue Area --- 4042 # RIGID_BODY_SOFTBODY might be deprecated in newer C4D versions. 4043 # This might need adjustment based on testing with the target C4D version. 4044 # A more modern approach uses RIGID_BODY_TYPE = 2 4045 try: 4046 # Try modern approach first 4047 if hasattr(c4d, "RIGID_BODY_TYPE"): 4048 tag[c4d.RIGID_BODY_TYPE] = getattr( 4049 c4d, "RIGID_BODY_TYPE_SOFTBODY", 2 4050 ) # Use constant or fallback value 2 4051 self.log( 4052 f"[C4D SBODY] Set RIGID_BODY_TYPE to Soft Body ({tag[c4d.RIGID_BODY_TYPE]})" 4053 ) 4054 elif hasattr(c4d, "RIGID_BODY_SOFTBODY"): 4055 # Fallback to older attribute if modern one doesn't exist 4056 tag[c4d.RIGID_BODY_SOFTBODY] = True 4057 self.log("[C4D SBODY] Set RIGID_BODY_SOFTBODY to True (legacy)") 4058 else: 4059 self.log( 4060 "[C4D SBODY] ## Warning ##: Cannot find suitable parameter to enable Soft Body mode." 4061 ) 4062 4063 # Common properties (assuming these IDs are stable) 4064 tag[c4d.RIGID_BODY_DYNAMIC] = 1 # Enable dynamics 4065 tag[c4d.RIGID_BODY_MASS] = float(mass_val) 4066 4067 # Stiffness might also have changed ID, add check 4068 softbody_stiffness_id = getattr( 4069 c4d, "RIGID_BODY_SOFTBODY_STIFFNESS", 1110 4070 ) # Example ID 1110 4071 if tag.HasParameter(softbody_stiffness_id): 4072 tag[softbody_stiffness_id] = ( 4073 float(stiff_val) / 100.0 4074 ) # Assume 0-100 input 4075 self.log( 4076 f"[C4D SBODY] Set stiffness parameter ID {softbody_stiffness_id}" 4077 ) 4078 else: 4079 self.log( 4080 f"[C4D SBODY] ## Warning ##: Stiffness parameter ID {softbody_stiffness_id} not found." 4081 ) 4082 4083 except AttributeError as ae: 4084 self.log( 4085 f"[**ERROR**] Missing Dynamics attribute: {ae}. Dynamics setup might be incomplete." 4086 ) 4087 # Don't raise, just log, tag might still be useful partially 4088 except Exception as e_tag: 4089 self.log(f"[**ERROR**] Error setting dynamics parameters: {e_tag}") 4090 # Don't raise, try inserting tag anyway 4091 4092 target_obj.InsertTag(tag) 4093 doc.AddUndo(c4d.UNDOTYPE_NEW, tag) 4094 c4d.EventAdd() 4095 4096 # Return context 4097 return { 4098 "object_name": obj_actual_name, 4099 "object_guid": str(target_obj.GetGUID()), # Added GUID 4100 "tag_name": tag.GetName(), 4101 "stiffness_set": float(stiff_val), # Report value requested 4102 "mass_set": float(mass_val), # Report value requested 4103 } 4104 4105 # Execute on main thread 4106 try: 4107 result = self.execute_on_main_thread( 4108 create_soft_body_safe, 4109 args=(obj, name, stiffness, mass, obj.GetName()), 4110 ) 4111 # Check result structure from execute_on_main_thread 4112 if isinstance(result, dict) and "error" in result: 4113 return result # Propagate error 4114 elif isinstance(result, dict) and result.get("status") == "completed_none": 4115 return { 4116 "error": "Soft body creation function returned None unexpectedly." 4117 } 4118 else: 4119 return {"soft_body": result} # Wrap successful result 4120 4121 except Exception as e: 4122 # Catch errors related to execute_on_main_thread itself 4123 self.log( 4124 f"[**ERROR**] Failed to execute soft body creation via main thread: {e}\n{traceback.format_exc()}" 4125 ) 4126 return {"error": f"Failed to queue/execute Soft Body creation: {str(e)}"} 4127 4128 def handle_apply_dynamics(self, command): 4129 """Handle apply_dynamics command with GUID support.""" 4130 doc = c4d.documents.GetActiveDocument() 4131 if not doc: 4132 return {"error": "No active document"} 4133 4134 # --- MODIFIED: Identify target object --- 4135 identifier = None 4136 use_guid = False 4137 if command.get("guid"): 4138 identifier = command.get("guid") 4139 use_guid = True 4140 self.log(f"[DYNAMICS] Using GUID identifier: '{identifier}'") 4141 elif command.get("object_name"): 4142 identifier = command.get("object_name") 4143 use_guid = False 4144 self.log(f"[DYNAMICS] Using Name identifier: '{identifier}'") 4145 else: 4146 return {"error": "No object identifier ('guid' or 'object_name') provided."} 4147 4148 # Find target object using the determined method 4149 obj = self.find_object_by_name(doc, identifier, use_guid=use_guid) 4150 if obj is None: 4151 search_type = "GUID" if use_guid else "Name" 4152 return { 4153 "error": f"Object '{identifier}' (searched by {search_type}) not found for dynamics." 4154 } 4155 # --- END MODIFIED --- 4156 4157 tag_type = command.get("tag_type", "rigid_body").lower() 4158 params = command.get("parameters", {}) 4159 tag_name = command.get( 4160 "tag_name", f"{obj.GetName()} {tag_type.replace('_',' ').title()}" 4161 ) # Default name 4162 4163 try: 4164 # Use Tdynamicsbody if available, fallback to old ID 4165 dynamics_tag_id = getattr(c4d, "Tdynamicsbody", 180000102) 4166 self.log( 4167 f"[DYNAMICS] Using Dynamics Tag ID: {dynamics_tag_id} for type '{tag_type}'" 4168 ) 4169 4170 doc.StartUndo() # Start undo block 4171 tag = obj.MakeTag(dynamics_tag_id) # Use MakeTag for safer insertion 4172 if tag is None: 4173 raise RuntimeError( 4174 f"Failed to create Dynamics tag (ID: {dynamics_tag_id}) on '{obj.GetName()}'" 4175 ) 4176 4177 tag.SetName(tag_name) 4178 bc = tag.GetDataInstance() 4179 if not bc: 4180 raise RuntimeError("Failed to get BaseContainer for dynamics tag") 4181 4182 # Map tag_type string to RIGID_BODY_TYPE enum value 4183 type_map = { 4184 "rigid_body": getattr(c4d, "RIGID_BODY_TYPE_RIGIDBODY", 1), # Usually 1 4185 "collider": getattr(c4d, "RIGID_BODY_TYPE_COLLIDER", 0), # Usually 0 4186 "ghost": getattr(c4d, "RIGID_BODY_TYPE_GHOST", 3), # Usually 3 4187 "soft_body": getattr(c4d, "RIGID_BODY_TYPE_SOFTBODY", 2), # Usually 2 4188 } 4189 dynamics_type = type_map.get(tag_type) 4190 if dynamics_type is None: 4191 self.log( 4192 f"Warning: Unknown dynamics tag_type '{tag_type}'. Defaulting to Collider." 4193 ) 4194 dynamics_type = type_map["collider"] 4195 4196 # Set dynamics type and enable 4197 bc[c4d.RIGID_BODY_TYPE] = dynamics_type 4198 bc[c4d.RIGID_BODY_ENABLED] = True 4199 4200 # Apply common parameters from the 'params' dictionary safely 4201 if "mass" in params: 4202 try: 4203 bc[c4d.RIGID_BODY_MASS_TYPE] = getattr( 4204 c4d, "RIGID_BODY_MASS_TYPE_CUSTOM", 1 4205 ) 4206 bc[c4d.RIGID_BODY_MASS_CUSTOM] = float(params["mass"]) 4207 except (ValueError, TypeError, AttributeError) as e: 4208 self.log( 4209 f"Warning: Invalid/unsupported mass value '{params['mass']}': {e}" 4210 ) 4211 if "friction" in params: 4212 try: 4213 bc[c4d.RIGID_BODY_FRICTION] = float(params["friction"]) 4214 except (ValueError, TypeError, AttributeError) as e: 4215 self.log( 4216 f"Warning: Invalid/unsupported friction value '{params['friction']}': {e}" 4217 ) 4218 # Use BOUNCE as it's the more common ID name than ELASTICITY 4219 if "bounce" in params or "elasticity" in params: 4220 bounce_val = params.get( 4221 "bounce", params.get("elasticity") 4222 ) # Accept either key 4223 try: 4224 bc[c4d.RIGID_BODY_BOUNCE] = float(bounce_val) 4225 except (ValueError, TypeError, AttributeError) as e: 4226 self.log( 4227 f"Warning: Invalid/unsupported bounce/elasticity value '{bounce_val}': {e}" 4228 ) 4229 if "collision_shape" in params: 4230 shape_map = { 4231 "auto": getattr(c4d, "RIGID_BODY_SHAPE_AUTO", 0), 4232 "box": getattr(c4d, "RIGID_BODY_SHAPE_BOX", 1), 4233 "sphere": getattr(c4d, "RIGID_BODY_SHAPE_SPHERE", 2), 4234 "capsule": getattr(c4d, "RIGID_BODY_SHAPE_CAPSULE", 3), 4235 "cylinder": getattr(c4d, "RIGID_BODY_SHAPE_CYLINDER", 4), 4236 "cone": getattr(c4d, "RIGID_BODY_SHAPE_CONE", 5), 4237 "static_mesh": getattr(c4d, "RIGID_BODY_SHAPE_STATICMESH", 7), 4238 "moving_mesh": getattr(c4d, "RIGID_BODY_SHAPE_MOVINGMESH", 8), 4239 } 4240 shape_val = shape_map.get(str(params["collision_shape"]).lower()) 4241 if shape_val is not None: 4242 try: 4243 bc[c4d.RIGID_BODY_SHAPE] = shape_val 4244 except AttributeError as e: 4245 self.log(f"Warning: Collision shape parameter not found: {e}") 4246 else: 4247 self.log( 4248 f"Warning: Invalid collision_shape value '{params['collision_shape']}'" 4249 ) 4250 else: # Default collision shape if not specified 4251 try: 4252 bc[c4d.RIGID_BODY_SHAPE] = getattr(c4d, "RIGID_BODY_SHAPE_AUTO", 0) 4253 except AttributeError as e: 4254 self.log( 4255 f"Warning: Default collision shape parameter not found: {e}" 4256 ) 4257 4258 # No need for obj.InsertTag(tag) because MakeTag already inserts it 4259 doc.AddUndo(c4d.UNDOTYPE_NEW, tag) # Add undo for the new tag 4260 doc.EndUndo() # End undo block 4261 c4d.EventAdd() 4262 4263 # --- MODIFIED: Contextual Return --- 4264 return { 4265 "dynamics": { 4266 "object_name": obj.GetName(), 4267 "object_guid": str(obj.GetGUID()), 4268 "tag_name": tag.GetName(), 4269 "tag_type_applied": tag_type, 4270 "parameters_received": params, # Echo back received params for verification 4271 } 4272 } 4273 # --- END MODIFIED --- 4274 4275 except Exception as e: 4276 doc.EndUndo() # Ensure undo is ended on error 4277 self.log( 4278 f"[**ERROR**] Error applying dynamics: {e}\n{traceback.format_exc()}" 4279 ) 4280 return { 4281 "error": f"Failed to apply Dynamics tag: {str(e)}", 4282 "traceback": traceback.format_exc(), 4283 } 4284 4285 def handle_create_abstract_shape(self, command): 4286 """Handle create_abstract_shape command with context and C4D 2025 compatibility.""" 4287 doc = c4d.documents.GetActiveDocument() 4288 if not doc: 4289 return {"error": "No active document"} 4290 4291 shape_type = command.get("shape_type", "metaball").lower() 4292 # Accept both "name" and "object_name" 4293 requested_name = command.get("name") or command.get( 4294 "object_name", f"{shape_type.capitalize()}" 4295 ) 4296 position_list = command.get("position", [0, 0, 0]) 4297 4298 # Safely parse position 4299 position = [0.0, 0.0, 0.0] 4300 if isinstance(position_list, list) and len(position_list) >= 3: 4301 try: 4302 position = [float(p) for p in position_list[:3]] 4303 except (ValueError, TypeError): 4304 self.log(f"Warning: Invalid position data {position_list}") 4305 4306 self.log( 4307 f"[C4D ABSTRCTSHAPE] Creating abstract shape '{shape_type}' with requested name: '{requested_name}'" 4308 ) 4309 4310 shape = None # Initialize shape variable 4311 try: 4312 shape_types = { 4313 "metaball": 5125, 4314 "blob": 5119, 4315 "loft": 5107, 4316 "sweep": 5118, 4317 "atom": 5168, 4318 "platonic": 5170, 4319 "cloth": 5186, 4320 "landscape": 5119, 4321 "extrude": 5116, 4322 } 4323 shape_type_id = shape_types.get(shape_type, shape_types["metaball"]) 4324 self.log( 4325 f"[C4D ABSTRCTSHAPE] Creating abstract shape of type: {shape_type} (ID: {shape_type_id})" 4326 ) 4327 4328 doc.StartUndo() # Start undo block 4329 shape = c4d.BaseObject(shape_type_id) 4330 if shape is None: 4331 raise RuntimeError(f"Failed to create {shape_type} object") 4332 4333 shape.SetName(requested_name) 4334 shape.SetAbsPos(c4d.Vector(*position)) 4335 4336 child_objects_context = {} # Store context for children 4337 4338 # Add children based on type (using original logic) 4339 if shape_type in ["metaball", "blob"]: 4340 self.log(f"[C4D ABSTRCTSHAPE] Creating child sphere for {shape_type}") 4341 sphere = c4d.BaseObject(c4d.Osphere) 4342 if sphere: 4343 child_req_name = ( 4344 f"{requested_name}_Sphere" # Use requested name of parent 4345 ) 4346 sphere.SetName(child_req_name) 4347 sphere.SetAbsScale(c4d.Vector(2.0, 2.0, 2.0)) # Use floats 4348 bc = sphere.GetDataInstance() 4349 if bc: 4350 bc.SetFloat(c4d.PRIM_SPHERE_RAD, 50.0) # Use floats 4351 sphere.InsertUnder(shape) 4352 doc.AddUndo(c4d.UNDOTYPE_NEW, sphere) 4353 # Add child context 4354 child_actual_name = sphere.GetName() 4355 child_guid = str(sphere.GetGUID()) 4356 child_objects_context["sphere"] = { 4357 "requested_name": child_req_name, 4358 "actual_name": child_actual_name, 4359 "guid": child_guid, 4360 } 4361 self.register_object_name(sphere, child_req_name) # Register child 4362 else: 4363 self.log(f"Warning: Failed to create child sphere for {shape_type}") 4364 4365 elif shape_type in ("loft", "sweep"): 4366 self.log( 4367 f"[C4D ABSTRCTSHAPE] Creating profile and path splines for {shape_type}" 4368 ) 4369 spline = c4d.BaseObject(c4d.Osplinecircle) 4370 path = c4d.BaseObject(c4d.Osplinenside) 4371 4372 if spline: 4373 child_req_name = f"{requested_name}_Profile" 4374 spline.SetName(child_req_name) 4375 spline.InsertUnder(shape) 4376 doc.AddUndo(c4d.UNDOTYPE_NEW, spline) 4377 child_actual_name = spline.GetName() 4378 child_guid = str(spline.GetGUID()) 4379 child_objects_context["profile"] = { 4380 "requested_name": child_req_name, 4381 "actual_name": child_actual_name, 4382 "guid": child_guid, 4383 } 4384 self.register_object_name(spline, child_req_name) 4385 else: 4386 self.log("Warning: Failed to create profile spline") 4387 4388 if path: 4389 child_req_name = f"{requested_name}_Path" 4390 path.SetName(child_req_name) 4391 path.SetAbsPos(c4d.Vector(0, 50, 0)) 4392 path.InsertUnder(shape) 4393 doc.AddUndo(c4d.UNDOTYPE_NEW, path) 4394 child_actual_name = path.GetName() 4395 child_guid = str(path.GetGUID()) 4396 child_objects_context["path"] = { 4397 "requested_name": child_req_name, 4398 "actual_name": child_actual_name, 4399 "guid": child_guid, 4400 } 4401 self.register_object_name(path, child_req_name) 4402 else: 4403 self.log("Warning: Failed to create path spline") 4404 4405 # Insert the main shape object 4406 doc.InsertObject(shape) 4407 doc.AddUndo(c4d.UNDOTYPE_NEW, shape) 4408 doc.EndUndo() # End undo block 4409 c4d.EventAdd() 4410 4411 # --- MODIFIED: Contextual Return --- 4412 actual_name = shape.GetName() 4413 guid = str(shape.GetGUID()) 4414 pos_vec = shape.GetAbsPos() 4415 shape_type_name = self.get_object_type_name(shape) # Get user friendly name 4416 4417 # Register the main shape object 4418 self.register_object_name(shape, requested_name) 4419 4420 return { 4421 "shape": { 4422 "requested_name": requested_name, 4423 "actual_name": actual_name, 4424 "guid": guid, 4425 "type": shape_type_name, # User friendly type 4426 "type_id": shape.GetType(), # C4D ID 4427 "position": [pos_vec.x, pos_vec.y, pos_vec.z], 4428 "child_objects": child_objects_context, # Include context of children 4429 } 4430 } 4431 # --- END MODIFIED --- 4432 4433 except Exception as e: 4434 doc.EndUndo() # Ensure undo is ended on error 4435 self.log( 4436 f"[**ERROR**] Error creating abstract shape '{requested_name}': {str(e)}\n{traceback.format_exc()}" 4437 ) 4438 # Clean up shape if created but not inserted 4439 if shape and not shape.GetDocument(): 4440 try: 4441 shape.Remove() 4442 except: 4443 pass 4444 return { 4445 "error": f"Failed to create abstract shape: {str(e)}", 4446 "traceback": traceback.format_exc(), 4447 } 4448 4449 def _find_by_guid_recursive(self, start_obj, guid): 4450 """Recursively search for an object with a specific GUID.""" 4451 current_obj = start_obj 4452 while current_obj: 4453 if str(current_obj.GetGUID()) == guid: 4454 return current_obj 4455 4456 # Check children recursively 4457 child = current_obj.GetDown() 4458 if child: 4459 result = self._find_by_guid_recursive(child, guid) 4460 if result: 4461 return result 4462 4463 current_obj = current_obj.GetNext() 4464 return None 4465 4466 def _get_all_objects(self, doc): 4467 """Get all objects in the document for efficient searching. 4468 4469 This method uses optimal strategies for Cinema 4D 2025 to collect all objects 4470 in the scene without missing anything. 4471 """ 4472 all_objects = [] 4473 found_ids = set() # To avoid duplicates 4474 4475 # Method 1: Standard hierarchy traversal 4476 def collect_recursive(obj): 4477 if obj is None: 4478 return 4479 4480 obj_id = str(obj.GetGUID()) 4481 if obj_id not in found_ids: 4482 all_objects.append(obj) 4483 found_ids.add(obj_id) 4484 4485 # Get children 4486 child = obj.GetDown() 4487 if child: 4488 collect_recursive(child) 4489 4490 # Get siblings 4491 next_obj = obj.GetNext() 4492 if next_obj: 4493 collect_recursive(next_obj) 4494 4495 # Start collection from root 4496 collect_recursive(doc.GetFirstObject()) 4497 4498 # Method 2: Use GetObjects API if available in this version 4499 try: 4500 if hasattr(doc, "GetObjects"): 4501 objects = doc.GetObjects() 4502 for obj in objects: 4503 obj_id = str(obj.GetGUID()) 4504 if obj_id not in found_ids: 4505 all_objects.append(obj) 4506 found_ids.add(obj_id) 4507 except Exception as e: 4508 self.log(f"[**ERROR**] Error using GetObjects API: {str(e)}") 4509 4510 # Method 3: Check for any missed MoGraph objects 4511 try: 4512 # Direct check for Cloners 4513 if hasattr(c4d, "Omgcloner"): 4514 # Use object type filtering to find cloners 4515 for obj in all_objects[:]: # Use a copy to avoid modification issues 4516 if ( 4517 obj.GetType() == c4d.Omgcloner 4518 and str(obj.GetGUID()) not in found_ids 4519 ): 4520 all_objects.append(obj) 4521 found_ids.add(str(obj.GetGUID())) 4522 except Exception as e: 4523 self.log(f"[**ERROR**] Error checking for MoGraph objects: {str(e)}") 4524 4525 self.log(f"[C4D] Found {len(all_objects)} objects in document") 4526 return all_objects 4527 4528 def handle_create_light(self, command): 4529 """Light creation with context and EXACT 2025.0 SDK parameters""" 4530 doc = c4d.documents.GetActiveDocument() 4531 if not doc: 4532 return {"error": "No active document"} 4533 4534 light_type = command.get("type", "spot").lower() 4535 # Use requested name or generate one 4536 requested_name = ( 4537 command.get("name") 4538 or command.get("object_name") 4539 or f"MCP_{light_type.capitalize()}Light_{int(time.time()) % 1000}" 4540 ) 4541 # Handle test harness name if provided 4542 if not requested_name and command.get("from_test_harness"): 4543 requested_name = "Test_Light" 4544 4545 position_list = command.get("position", [0, 100, 0]) 4546 color_list = command.get("color", [1, 1, 1]) 4547 intensity = command.get("intensity", 100) 4548 temperature = command.get("temperature", 6500) 4549 width = command.get("width", 200) 4550 height = command.get("height", 200) 4551 4552 LIGHT_TYPE_MAP = {"point": 0, "spot": 1, "area": 8, "infinite": 3} 4553 if light_type not in LIGHT_TYPE_MAP: 4554 valid_types = ", ".join(LIGHT_TYPE_MAP.keys()) 4555 return { 4556 "error": f"Invalid light type: '{light_type}'. Valid: {valid_types}" 4557 } 4558 4559 light = None # Initialize light variable 4560 try: 4561 doc.StartUndo() # Start undo block 4562 light = c4d.BaseObject(c4d.Olight) 4563 if not light: 4564 raise RuntimeError("Light creation failed") 4565 4566 light_code = LIGHT_TYPE_MAP[light_type] 4567 light[c4d.LIGHT_TYPE] = light_code 4568 light.SetName(requested_name) 4569 self.log( 4570 f"[C4D LIGHT] Set requested name '{requested_name}' before insertion." 4571 ) # Log name set 4572 4573 # Safely set position, color, brightness 4574 try: 4575 light.SetAbsPos(c4d.Vector(*[float(x) for x in position_list[:3]])) 4576 except (ValueError, TypeError): 4577 self.log(f"Warning: Invalid light position {position_list}") 4578 try: 4579 light[c4d.LIGHT_COLOR] = c4d.Vector( 4580 *[max(0.0, min(1.0, float(c))) for c in color_list[:3]] 4581 ) # Clamp color 0-1 4582 except (ValueError, TypeError): 4583 self.log(f"Warning: Invalid light color {color_list}") 4584 try: 4585 light[c4d.LIGHT_BRIGHTNESS] = max( 4586 0.0, float(intensity) / 100.0 4587 ) # Clamp brightness >= 0 4588 except (ValueError, TypeError): 4589 self.log(f"Warning: Invalid light intensity {intensity}") 4590 4591 # Temperature handling 4592 if hasattr(c4d, "LIGHT_TEMPERATURE"): 4593 try: 4594 light[c4d.LIGHT_TEMPERATURE] = int(float(temperature)) 4595 except (TypeError, ValueError): 4596 self.log(f"Warning: Invalid temperature '{temperature}'") 4597 4598 # Area light parameters 4599 if light_code == 8: # Area light 4600 try: 4601 light[c4d.LIGHT_AREADETAILS_SIZEX] = max( 4602 0.0, float(width) 4603 ) # Ensure non-negative 4604 except (ValueError, TypeError): 4605 self.log(f"Warning: Invalid area light width {width}") 4606 try: 4607 light[c4d.LIGHT_AREADETAILS_SIZEY] = max( 4608 0.0, float(height) 4609 ) # Ensure non-negative 4610 except (ValueError, TypeError): 4611 self.log(f"Warning: Invalid area light height {height}") 4612 try: 4613 light[c4d.LIGHT_AREADETAILS_SHAPE] = 0 # Rectangle 4614 except AttributeError: 4615 pass # Ignore if param doesn't exist 4616 4617 # Shadow parameters 4618 if hasattr(c4d, "LIGHT_SHADOWTYPE"): 4619 try: 4620 light[c4d.LIGHT_SHADOWTYPE] = 1 # Soft shadows 4621 except AttributeError: 4622 pass # Ignore if param doesn't exist 4623 4624 doc.InsertObject(light) 4625 doc.AddUndo(c4d.UNDOTYPE_NEW, light) # Add undo for new light 4626 doc.EndUndo() # End undo block 4627 c4d.EventAdd() 4628 4629 # --- MODIFIED: Contextual Return --- 4630 actual_name = light.GetName() 4631 guid = str(light.GetGUID()) 4632 pos_vec = light.GetAbsPos() 4633 light_type_name = self.get_object_type_name(light) # Get user friendly name 4634 4635 # Register the light object 4636 self.register_object_name(light, requested_name) 4637 4638 return { 4639 "light": { # Changed key from 'object' to 'light' for clarity 4640 "requested_name": requested_name, 4641 "actual_name": actual_name, 4642 "guid": guid, 4643 "type": light_type_name, # User friendly type name 4644 "type_id": light.GetType(), # C4D ID 4645 "position": [pos_vec.x, pos_vec.y, pos_vec.z], 4646 # Optionally return other set properties for context 4647 "color_set": [ 4648 light[c4d.LIGHT_COLOR].x, 4649 light[c4d.LIGHT_COLOR].y, 4650 light[c4d.LIGHT_COLOR].z, 4651 ], 4652 "intensity_set": light[c4d.LIGHT_BRIGHTNESS] * 100.0, 4653 } 4654 } 4655 # --- END MODIFIED --- 4656 4657 except Exception as e: 4658 doc.EndUndo() # Ensure undo is ended on error 4659 self.log( 4660 f"[**ERROR**] Error creating light '{requested_name}': {str(e)}\n{traceback.format_exc()}" 4661 ) 4662 # Clean up light if created but not inserted 4663 if light and not light.GetDocument(): 4664 try: 4665 light.Remove() 4666 except: 4667 pass 4668 return { 4669 "error": f"Light creation failed: {str(e)}", 4670 "traceback": traceback.format_exc(), 4671 } 4672 4673 def handle_create_camera(self, command): 4674 """Create a new camera, optionally pointing it towards a target.""" 4675 doc = c4d.documents.GetActiveDocument() 4676 if not doc: 4677 return {"error": "No active document"} 4678 4679 requested_name = command.get("name", "Camera") 4680 position_list = command.get("position", [0, 0, 0]) 4681 properties = command.get( 4682 "properties", {} 4683 ) # Includes focal_length, aperture, target_position etc. 4684 4685 # Safely parse position 4686 position = [0.0, 0.0, 0.0] 4687 if isinstance(position_list, list) and len(position_list) >= 3: 4688 try: 4689 position = [float(p) for p in position_list[:3]] 4690 except (ValueError, TypeError): 4691 self.log(f"Warning: Invalid camera position data {position_list}") 4692 4693 camera = None 4694 try: 4695 doc.StartUndo() 4696 camera = c4d.BaseObject(c4d.Ocamera) 4697 if not camera: 4698 raise RuntimeError("Failed to create camera object") 4699 4700 camera.SetName(requested_name) 4701 cam_pos_vec = c4d.Vector(*position) 4702 camera.SetAbsPos(cam_pos_vec) 4703 4704 # --- Apply standard camera properties --- 4705 applied_properties = {} 4706 bc = camera.GetDataInstance() 4707 if bc: 4708 if "focal_length" in properties: 4709 try: 4710 val = float(properties["focal_length"]) 4711 focus_id = getattr(c4d, "CAMERAOBJECT_FOCUS", c4d.CAMERA_FOCUS) 4712 bc[focus_id] = val 4713 applied_properties["focal_length"] = val 4714 except (ValueError, TypeError, AttributeError) as e: 4715 self.log(f"Warning: Failed to set focal_length: {e}") 4716 if "aperture" in properties: 4717 try: 4718 val = float(properties["aperture"]) 4719 bc[c4d.CAMERAOBJECT_APERTURE] = val 4720 applied_properties["aperture"] = val 4721 except (ValueError, TypeError, AttributeError) as e: 4722 self.log(f"Warning: Failed to set aperture: {e}") 4723 # Add other properties like film offset here if needed... 4724 4725 # --- NEW: Handle Target Position --- 4726 target_pos = None 4727 target_list = properties.get( 4728 "target_position" 4729 ) # Expect key "target_position" 4730 if isinstance(target_list, list) and len(target_list) >= 3: 4731 try: 4732 target_pos = c4d.Vector(*[float(p) for p in target_list[:3]]) 4733 except (ValueError, TypeError): 4734 self.log(f"Warning: Invalid target_position data {target_list}") 4735 else: 4736 # Default target to world origin if not specified 4737 target_pos = c4d.Vector(0, 0, 0) 4738 self.log( 4739 f"No target_position provided, defaulting camera target to world origin." 4740 ) 4741 4742 if target_pos is not None: 4743 try: 4744 # Calculate direction vector 4745 direction = target_pos - cam_pos_vec 4746 direction.Normalize() 4747 4748 # Calculate HPB rotation in radians 4749 hpb = c4d.utils.VectorToHPB(direction) 4750 4751 # Apply rotation (SetAbsRot expects radians) 4752 camera.SetAbsRot(hpb) 4753 applied_properties["rotation_set_to_target"] = [ 4754 c4d.utils.RadToDeg(a) for a in [hpb.x, hpb.y, hpb.z] 4755 ] # Report degrees 4756 self.log( 4757 f"Pointed camera '{camera.GetName()}' towards target {target_list or '[0,0,0]'}" 4758 ) 4759 except Exception as e_rot: 4760 self.log( 4761 f"Warning: Failed to calculate or set camera rotation towards target: {e_rot}" 4762 ) 4763 # --- END NEW TARGET HANDLING --- 4764 4765 doc.InsertObject(camera) 4766 doc.AddUndo(c4d.UNDOTYPE_NEW, camera) 4767 doc.SetActiveObject(camera) 4768 doc.EndUndo() 4769 c4d.EventAdd() 4770 4771 self.log(f"[C4D] Created camera '{camera.GetName()}' at {position}") 4772 4773 # --- Contextual Return --- 4774 actual_name = camera.GetName() 4775 guid = str(camera.GetGUID()) 4776 pos_vec = camera.GetAbsPos() 4777 rot_vec_rad = camera.GetAbsRot() # Get final rotation 4778 camera_type_name = self.get_object_type_name(camera) 4779 self.register_object_name(camera, requested_name) 4780 4781 return { 4782 "camera": { 4783 "requested_name": requested_name, 4784 "actual_name": actual_name, 4785 "guid": guid, 4786 "type": camera_type_name, 4787 "type_id": camera.GetType(), 4788 "position": [pos_vec.x, pos_vec.y, pos_vec.z], 4789 "rotation": [ 4790 c4d.utils.RadToDeg(a) 4791 for a in [rot_vec_rad.x, rot_vec_rad.y, rot_vec_rad.z] 4792 ], # Return final rotation in degrees 4793 "properties_applied": applied_properties, 4794 } 4795 } 4796 4797 except Exception as e: 4798 if doc and doc.IsUndoEnabled(): 4799 doc.EndUndo() # Ensure undo ended 4800 self.log( 4801 f"[**ERROR**] Error creating camera '{requested_name}': {str(e)}\n{traceback.format_exc()}" 4802 ) 4803 if camera and not camera.GetDocument(): 4804 try: 4805 camera.Remove() 4806 except: 4807 pass 4808 return { 4809 "error": f"Failed to create camera: {str(e)}", 4810 "traceback": traceback.format_exc(), 4811 } 4812 4813 def handle_animate_camera(self, command): 4814 """Handle animate_camera command with context.""" 4815 doc = c4d.documents.GetActiveDocument() 4816 if not doc: 4817 return {"error": "No active document"} 4818 4819 # --- MODIFIED: Identify target camera --- 4820 identifier = None 4821 use_guid = False 4822 if command.get("guid"): # Check for GUID first 4823 identifier = command.get("guid") 4824 use_guid = True 4825 self.log(f"[ANIM CAM] Using GUID identifier: '{identifier}'") 4826 elif command.get("camera_name"): 4827 identifier = command.get("camera_name") 4828 use_guid = False 4829 self.log(f"[ANIM CAM] Using Name identifier: '{identifier}'") 4830 # --- END MODIFIED --- 4831 4832 path_type = command.get("path_type", "linear").lower() 4833 positions = command.get("positions", []) 4834 frames = command.get("frames", []) 4835 create_camera = command.get("create_camera", False) 4836 camera_properties = command.get("camera_properties", {}) # e.g., focal_length 4837 4838 camera = None 4839 camera_created = False 4840 requested_name = ( 4841 identifier if identifier else "Animated Camera" 4842 ) # Use identifier as requested name if provided 4843 4844 # Find existing camera if identifier provided and not creating new 4845 if identifier and not create_camera: 4846 camera = self.find_object_by_name(doc, identifier, use_guid=use_guid) 4847 if camera and camera.GetType() != c4d.Ocamera: 4848 self.log( 4849 f"Warning: Object '{identifier}' found but is not a camera. Will create new." 4850 ) 4851 camera = None # Force creation 4852 elif camera is None: 4853 search_type = "GUID" if use_guid else "Name" 4854 self.log( 4855 f"Info: Camera '{identifier}' (searched by {search_type}) not found, will create a new one." 4856 ) 4857 4858 # Create camera if needed 4859 if camera is None: 4860 doc.StartUndo() # Start undo if creating 4861 camera = c4d.BaseObject(c4d.Ocamera) 4862 if not camera: 4863 return {"error": "Failed to create camera object"} 4864 4865 camera.SetName(requested_name) # Use requested name 4866 self.log(f"[ANIM CAM] Created new camera: {camera.GetName()}") 4867 camera_created = True 4868 4869 # Apply properties if provided (using original logic, ensure safety) 4870 applied_properties = {} 4871 bc = camera.GetDataInstance() 4872 if bc: 4873 if "focal_length" in camera_properties: 4874 try: 4875 val = float(camera_properties["focal_length"]) 4876 focus_id = getattr(c4d, "CAMERAOBJECT_FOCUS", c4d.CAMERA_FOCUS) 4877 bc[focus_id] = val 4878 applied_properties["focal_length"] = val 4879 except (ValueError, TypeError, AttributeError) as e: 4880 self.log(f"Warning: Failed to set focal_length: {e}") 4881 if "aperture" in camera_properties: 4882 try: 4883 val = float(camera_properties["aperture"]) 4884 bc[c4d.CAMERAOBJECT_APERTURE] = val 4885 applied_properties["aperture"] = val 4886 except (ValueError, TypeError, AttributeError) as e: 4887 self.log(f"Warning: Failed to set aperture: {e}") 4888 # Add other properties as needed... 4889 4890 doc.InsertObject(camera) 4891 doc.AddUndo(c4d.UNDOTYPE_NEW, camera) 4892 doc.SetActiveObject(camera) # Make active 4893 # Register the newly created camera 4894 self.register_object_name(camera, requested_name) 4895 doc.EndUndo() # End undo block for creation 4896 else: 4897 self.log(f"[ANIM CAM] Using existing camera: '{camera.GetName()}'") 4898 4899 # --- Animation Logic --- 4900 try: 4901 doc.StartUndo() # Start undo for animation changes 4902 4903 # Add default frames if only positions are provided 4904 if positions and not frames: 4905 frames = list(range(len(positions))) # Simple frame sequence 4906 4907 if not positions or not frames or len(positions) != len(frames): 4908 # Allow animation types without positions/frames? e.g. wiggle? 4909 if path_type not in ["wiggle"]: # Add other position-less types here 4910 doc.EndUndo() # End undo as nothing happened yet 4911 return { 4912 "error": f"Invalid positions/frames data for animation type '{path_type}'. They must be arrays of equal length." 4913 } 4914 else: 4915 self.log( 4916 f"Info: No position/frame data provided for '{path_type}', proceeding if type supports it." 4917 ) 4918 4919 keyframe_count = 0 4920 frame_range_set = [] 4921 4922 # Set keyframes for camera positions if provided 4923 if positions and frames: 4924 for pos, frame in zip(positions, frames): 4925 if isinstance(pos, list) and len(pos) >= 3: 4926 # Use internal helper which already includes AddUndo 4927 if self._set_position_keyframe(camera, frame, pos): 4928 keyframe_count += 1 4929 else: 4930 self.log( 4931 f"Warning: Skipping invalid position data {pos} for frame {frame}" 4932 ) 4933 if frames: 4934 frame_range_set = [min(frames), max(frames)] 4935 4936 # Handle spline path if requested and positions available 4937 path_guid = None # Store GUID of created path 4938 if path_type in ["spline", "spline_oriented"] and len(positions) > 1: 4939 self.log("[ANIM CAM] Creating spline path and alignment tag.") 4940 path = c4d.BaseObject(c4d.Ospline) 4941 path.SetName(f"{camera.GetName()} Path") 4942 points = [ 4943 c4d.Vector(p[0], p[1], p[2]) 4944 for p in positions 4945 if isinstance(p, list) and len(p) >= 3 4946 ] 4947 if not points: 4948 self.log("Warning: No valid points for spline path creation.") 4949 else: 4950 path.ResizeObject(len(points)) 4951 for i, pt in enumerate(points): 4952 path.SetPoint(i, pt) 4953 4954 doc.InsertObject(path) # Insert path into scene 4955 doc.AddUndo(c4d.UNDOTYPE_NEW, path) 4956 path_guid = str(path.GetGUID()) # Get GUID after insertion 4957 self.register_object_name( 4958 path, path.GetName() 4959 ) # Register the path spline 4960 4961 # Create and apply Align to Spline tag 4962 align_tag = camera.MakeTag(c4d.Talignspline) # Use Talignspline 4963 if align_tag: 4964 align_tag[c4d.ALIGNTOSPLINETAG_LINK] = ( 4965 path # Link the path object 4966 ) 4967 # Set Tangential if spline_oriented? Check specific tag params if needed. 4968 # align_tag[c4d.ALIGNTOSPLINETAG_TANGENTIAL] = (path_type == "spline_oriented") 4969 doc.AddUndo(c4d.UNDOTYPE_NEW, align_tag) # Add undo for new tag 4970 self.log("Applied Align to Spline tag.") 4971 else: 4972 self.log("Warning: Failed to create Align to Spline tag.") 4973 4974 # Handle other animation types (like wiggle) if needed here... 4975 # elif path_type == "wiggle": 4976 # # Apply wiggle expression or tag... (Requires specific implementation) 4977 # self.log("Info: Wiggle animation type not fully implemented in this version.") 4978 4979 doc.EndUndo() # End undo block for animation changes 4980 c4d.EventAdd() 4981 4982 # --- MODIFIED: Contextual Return --- 4983 actual_camera_name = camera.GetName() 4984 camera_guid = str(camera.GetGUID()) 4985 4986 response_data = { 4987 "requested_name": requested_name, # Name used to find/create 4988 "actual_name": actual_camera_name, # Final name 4989 "guid": camera_guid, 4990 "camera_created": camera_created, # Was it created by this call? 4991 "path_type": path_type, 4992 "keyframe_count": keyframe_count, 4993 "frame_range_set": frame_range_set, # Frames actually keyframed 4994 "spline_path_guid": path_guid, # GUID of path spline if created 4995 # "properties_applied": applied_properties if camera_created else {}, # Properties set during creation 4996 } 4997 4998 return {"camera_animation": response_data} # Keep original top-level key 4999 # --- END MODIFIED --- 5000 5001 except Exception as e: 5002 doc.EndUndo() # Ensure undo ended 5003 self.log( 5004 f"[**ERROR**] Error animating camera '{requested_name}': {str(e)}\n{traceback.format_exc()}" 5005 ) 5006 # Clean up camera if created but not inserted 5007 if camera and not camera.GetDocument(): 5008 try: 5009 camera.Remove() 5010 except: 5011 pass 5012 return { 5013 "error": f"Failed to animate camera: {str(e)}", 5014 "traceback": traceback.format_exc(), 5015 } 5016 5017 def _get_redshift_material_id(self): 5018 """Detect Redshift material ID by examining existing materials. 5019 5020 This function scans the active document for materials with type IDs 5021 in the range typical for Redshift materials (over 1,000,000). 5022 5023 Returns: 5024 A BaseMaterial with the detected Redshift material type or None if not found 5025 """ 5026 doc = c4d.documents.GetActiveDocument() 5027 5028 # Look for existing Redshift materials to detect the proper ID 5029 for mat in doc.GetMaterials(): 5030 mat_type = mat.GetType() 5031 if mat_type >= 1000000: 5032 self.log( 5033 f"[C4D RS] Found existing Redshift material with type ID: {mat_type}" 5034 ) 5035 # Try to create a material with this ID 5036 try: 5037 rs_mat = c4d.BaseMaterial(mat_type) 5038 if rs_mat and rs_mat.GetType() == mat_type: 5039 self.log( 5040 f"[C4D RS] Successfully created Redshift material using detected ID: {mat_type}" 5041 ) 5042 return rs_mat 5043 except: 5044 pass 5045 5046 # If Python scripting can create Redshift materials, try this method 5047 try: 5048 # Execute a Python script to create a Redshift material 5049 script = """ 5050 import c4d 5051 doc = c4d.documents.GetActiveDocument() 5052 # Try with known Redshift ID 5053 rs_mat = c4d.BaseMaterial(1036224) 5054 if rs_mat: 5055 rs_mat.SetName("TempRedshiftMaterial") 5056 doc.InsertMaterial(rs_mat) 5057 c4d.EventAdd() 5058 """ 5059 # Only try script-based approach if explicitly allowed 5060 if ( 5061 hasattr(c4d, "modules") 5062 and hasattr(c4d.modules, "net") 5063 and hasattr(c4d.modules.net, "Execute") 5064 ): 5065 # Execute in a controlled way that won't affect normal operation 5066 import tempfile, os 5067 5068 script_path = None 5069 try: 5070 with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f: 5071 f.write(script.encode("utf-8")) 5072 script_path = f.name 5073 5074 # Try to execute this script 5075 self.execute_on_main_thread( 5076 lambda: c4d.modules.net.Execute(script_path) 5077 ) 5078 finally: 5079 # Always clean up the temp file 5080 if script_path and os.path.exists(script_path): 5081 try: 5082 os.unlink(script_path) 5083 except: 5084 pass 5085 5086 # Now look for the material we created 5087 temp_mat = self._find_material_by_name(doc, "TempRedshiftMaterial") 5088 if temp_mat and temp_mat.GetType() >= 1000000: 5089 self.log( 5090 f"[C4D RS] Created Redshift material via script with type ID: {temp_mat.GetType()}" 5091 ) 5092 # Clean up the temporary material 5093 doc.RemoveMaterial(temp_mat) 5094 c4d.EventAdd() 5095 # Create a fresh material with this ID 5096 return c4d.BaseMaterial(temp_mat.GetType()) 5097 except Exception as e: 5098 self.log( 5099 f"[C4D RS] Script-based Redshift material creation failed: {str(e)}" 5100 ) 5101 5102 # No Redshift materials found 5103 return None 5104 5105 def _find_material_by_name(self, doc, name): 5106 """Find a material by name in the document. 5107 5108 Args: 5109 doc: The active Cinema 4D document 5110 name: The name of the material to find 5111 5112 Returns: 5113 The material if found, None otherwise 5114 """ 5115 if not name: 5116 self.log(f"[C4D] ## Warning ##: Empty material name provided") 5117 return None 5118 5119 # Get all materials in the document 5120 materials = doc.GetMaterials() 5121 5122 # First pass: exact match 5123 for mat in materials: 5124 if mat.GetName() == name: 5125 return mat 5126 5127 # Second pass: case-insensitive match 5128 name_lower = name.lower() 5129 closest_match = None 5130 for mat in materials: 5131 if mat.GetName().lower() == name_lower: 5132 closest_match = mat 5133 self.log( 5134 f"[C4D] Found case-insensitive match for material '{name}': '{mat.GetName()}'" 5135 ) 5136 break 5137 5138 if closest_match: 5139 return closest_match 5140 5141 self.log(f"[C4D] Material not found: '{name}'") 5142 5143 # If material not found, list available materials to aid debugging 5144 if materials: 5145 material_names = [mat.GetName() for mat in materials] 5146 self.log(f"[C4D] Available materials: {', '.join(material_names)}") 5147 5148 return None 5149 5150 def handle_validate_redshift_materials(self, command): 5151 """Validate Redshift node materials in the scene and fix issues when possible.""" 5152 import maxon 5153 5154 warnings = [] 5155 fixes = [] 5156 doc = c4d.documents.GetActiveDocument() 5157 5158 try: 5159 # Advanced Redshift detection diagnostics 5160 self.log(f"[C4D] DIAGNOSTIC: Cinema 4D version: {c4d.GetC4DVersion()}") 5161 self.log(f"[C4D] DIAGNOSTIC: Python version: {sys.version}") 5162 5163 # Check for Redshift modules more comprehensively 5164 redshift_module_exists = hasattr(c4d, "modules") and hasattr( 5165 c4d.modules, "redshift" 5166 ) 5167 self.log( 5168 f"[C4D] DIAGNOSTIC: Redshift module exists: {redshift_module_exists}" 5169 ) 5170 5171 if redshift_module_exists: 5172 redshift = c4d.modules.redshift 5173 self.log( 5174 f"[C4D] DIAGNOSTIC: Redshift module dir contents: {dir(redshift)}" 5175 ) 5176 5177 # Check for common Redshift module attributes 5178 for attr in [ 5179 "Mmaterial", 5180 "MATERIAL_TYPE", 5181 "GetRSMaterialNodeSpace", 5182 ]: 5183 has_attr = hasattr(redshift, attr) 5184 self.log( 5185 f"[C4D] DIAGNOSTIC: Redshift module has '{attr}': {has_attr}" 5186 ) 5187 5188 # Check if Redshift ID_REDSHIFT_MATERIAL constant exists 5189 has_rs_constant = hasattr(c4d, "ID_REDSHIFT_MATERIAL") 5190 self.log( 5191 f"[C4D] DIAGNOSTIC: c4d.ID_REDSHIFT_MATERIAL exists: {has_rs_constant}" 5192 ) 5193 if has_rs_constant: 5194 self.log( 5195 f"[C4D] DIAGNOSTIC: c4d.ID_REDSHIFT_MATERIAL value: {c4d.ID_REDSHIFT_MATERIAL}" 5196 ) 5197 5198 # Check all installed plugins 5199 plugins = c4d.plugins.FilterPluginList(c4d.PLUGINTYPE_MATERIAL, True) 5200 self.log(f"[C4D] DIAGNOSTIC: Found {len(plugins)} material plugins") 5201 for plugin in plugins: 5202 plugin_name = plugin.GetName() 5203 plugin_id = plugin.GetID() 5204 self.log( 5205 f"[C4D] DIAGNOSTIC: Material plugin: {plugin_name} (ID: {plugin_id})" 5206 ) 5207 5208 # Continue with normal validation 5209 # Get the Redshift node space ID 5210 redshift_ns = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace") 5211 5212 # Log all relevant Redshift material IDs for debugging 5213 self.log(f"[C4D] Standard material ID: {c4d.Mmaterial}") 5214 self.log( 5215 f"[C4D] Redshift material ID (c4d.ID_REDSHIFT_MATERIAL): {c4d.ID_REDSHIFT_MATERIAL}" 5216 ) 5217 5218 # Check if Redshift module has its own material type constant 5219 if hasattr(c4d, "modules") and hasattr(c4d.modules, "redshift"): 5220 redshift = c4d.modules.redshift 5221 rs_material_id = getattr(redshift, "Mmaterial", None) 5222 if rs_material_id is not None: 5223 self.log(f"[C4D] Redshift module material ID: {rs_material_id}") 5224 rs_material_type = getattr(redshift, "MATERIAL_TYPE", None) 5225 if rs_material_type is not None: 5226 self.log(f"[C4D] Redshift MATERIAL_TYPE: {rs_material_type}") 5227 5228 # Count of materials by type 5229 mat_stats = { 5230 "total": 0, 5231 "redshift": 0, 5232 "standard": 0, 5233 "fixed": 0, 5234 "issues": 0, 5235 "material_types": {}, 5236 } 5237 5238 # Validate all materials in the document 5239 for mat in doc.GetMaterials(): 5240 mat_stats["total"] += 1 5241 name = mat.GetName() 5242 5243 # Track all material types encountered 5244 mat_type = mat.GetType() 5245 if mat_type not in mat_stats["material_types"]: 5246 mat_stats["material_types"][mat_type] = 1 5247 else: 5248 mat_stats["material_types"][mat_type] += 1 5249 5250 # Check if it's a Redshift node material (should be c4d.ID_REDSHIFT_MATERIAL) 5251 is_rs_material = mat_type == c4d.ID_REDSHIFT_MATERIAL 5252 5253 # Also check for alternative Redshift material type IDs 5254 if not is_rs_material and mat_type >= 1000000: 5255 # This is likely a Redshift material with a different ID 5256 self.log( 5257 f"[C4D] Found possible Redshift material with ID {mat_type}: {name}" 5258 ) 5259 is_rs_material = True 5260 5261 if not is_rs_material: 5262 warnings.append( 5263 f"ℹ️ '{name}': Not a Redshift node material (type: {mat.GetType()})." 5264 ) 5265 mat_stats["standard"] += 1 5266 5267 # Auto-fix option: convert standard materials to Redshift if requested 5268 if command.get("auto_convert", False): 5269 try: 5270 # Create new Redshift material 5271 rs_mat = c4d.BaseMaterial(c4d.ID_REDSHIFT_MATERIAL) 5272 rs_mat.SetName(f"RS_{name}") 5273 5274 # Copy basic properties 5275 color = mat[c4d.MATERIAL_COLOR_COLOR] 5276 5277 # Set up default graph using CreateDefaultGraph 5278 try: 5279 rs_mat.CreateDefaultGraph(redshift_ns) 5280 except Exception as e: 5281 warnings.append( 5282 f"⚠️ Error creating default graph for '{name}': {str(e)}" 5283 ) 5284 # Continue anyway and try to work with what we have 5285 5286 # Get the graph and root 5287 graph = rs_mat.GetGraph(redshift_ns) 5288 root = graph.GetRoot() 5289 5290 # Find the Standard Surface output 5291 for node in graph.GetNodes(): 5292 if "StandardMaterial" in node.GetId(): 5293 # Set diffuse color 5294 try: 5295 node.SetParameter( 5296 maxon.nodes.ParameterID("base_color"), 5297 maxon.Color(color.x, color.y, color.z), 5298 maxon.PROPERTYFLAGS_NONE, 5299 ) 5300 except: 5301 pass 5302 break 5303 5304 # Insert the new material 5305 doc.InsertMaterial(rs_mat) 5306 5307 # Find and update texture tags 5308 if command.get("update_references", False): 5309 obj = doc.GetFirstObject() 5310 while obj: 5311 tag = obj.GetFirstTag() 5312 while tag: 5313 if tag.GetType() == c4d.Ttexture: 5314 if tag[c4d.TEXTURETAG_MATERIAL] == mat: 5315 tag[c4d.TEXTURETAG_MATERIAL] = rs_mat 5316 tag = tag.GetNext() 5317 obj = obj.GetNext() 5318 5319 fixes.append( 5320 f"✅ Converted '{name}' to Redshift node material." 5321 ) 5322 mat_stats["fixed"] += 1 5323 except Exception as e: 5324 warnings.append(f"❌ Failed to convert '{name}': {str(e)}") 5325 5326 continue 5327 5328 # For Redshift materials, continue with validation 5329 if is_rs_material: 5330 # It's a confirmed Redshift material 5331 mat_stats["redshift"] += 1 5332 5333 # Check if it's using the Redshift node space 5334 if ( 5335 hasattr(mat, "GetNodeMaterialSpace") 5336 and mat.GetNodeMaterialSpace() != redshift_ns 5337 ): 5338 warnings.append( 5339 f"⚠️ '{name}': Redshift material but not using correct node space." 5340 ) 5341 mat_stats["issues"] += 1 5342 continue 5343 else: 5344 # Skip further validation for non-Redshift materials 5345 continue 5346 5347 # Validate the node graph 5348 graph = mat.GetGraph(redshift_ns) 5349 if not graph: 5350 warnings.append(f"❌ '{name}': No node graph.") 5351 mat_stats["issues"] += 1 5352 5353 # Try to fix by creating a default graph 5354 if command.get("auto_fix", False): 5355 try: 5356 mat.CreateDefaultGraph(redshift_ns) 5357 fixes.append(f"✅ Created default graph for '{name}'.") 5358 mat_stats["fixed"] += 1 5359 except Exception as e: 5360 warnings.append( 5361 f"❌ Could not create default graph for '{name}': {str(e)}" 5362 ) 5363 5364 continue 5365 5366 # Check the root node connections 5367 root = graph.GetRoot() 5368 if not root: 5369 warnings.append(f"❌ '{name}': No root node in graph.") 5370 mat_stats["issues"] += 1 5371 continue 5372 5373 # Check if we have inputs 5374 inputs = root.GetInputs() 5375 if not inputs or len(inputs) == 0: 5376 warnings.append(f"❌ '{name}': Root has no input ports.") 5377 mat_stats["issues"] += 1 5378 continue 5379 5380 # Check the output connection 5381 output_port = inputs[0] # First input is typically the main output 5382 output_node = output_port.GetDestination() 5383 5384 if not output_node: 5385 warnings.append(f"⚠️ '{name}': Output not connected.") 5386 mat_stats["issues"] += 1 5387 5388 # Try to fix by creating a Standard Surface node 5389 if command.get("auto_fix", False): 5390 try: 5391 # Create Standard Surface node 5392 standard_surface = graph.CreateNode( 5393 maxon.nodes.IdAndVersion( 5394 "com.redshift3d.redshift4c4d.nodes.core.standardmaterial" 5395 ) 5396 ) 5397 5398 # Connect to output 5399 graph.CreateConnection( 5400 standard_surface.GetOutputs()[0], # Surface output 5401 root.GetInputs()[0], # Surface input on root 5402 ) 5403 5404 fixes.append(f"✅ Added Standard Surface node to '{name}'.") 5405 mat_stats["fixed"] += 1 5406 except Exception as e: 5407 warnings.append( 5408 f"❌ Could not add Standard Surface to '{name}': {str(e)}" 5409 ) 5410 5411 continue 5412 5413 # Check that the output is connected to a Redshift Material node (Standard Surface, etc.) 5414 if ( 5415 "StandardMaterial" not in output_node.GetId() 5416 and "Material" not in output_node.GetId() 5417 ): 5418 warnings.append( 5419 f"❌ '{name}': Output not connected to a Redshift Material node." 5420 ) 5421 mat_stats["issues"] += 1 5422 continue 5423 5424 # Now check specific material inputs 5425 rs_mat_node = output_node 5426 5427 # Check diffuse/base color 5428 base_color = None 5429 for input_port in rs_mat_node.GetInputs(): 5430 port_id = input_port.GetId() 5431 if "diffuse_color" in port_id or "base_color" in port_id: 5432 base_color = input_port 5433 break 5434 5435 if base_color is None: 5436 warnings.append(f"⚠️ '{name}': No diffuse/base color input found.") 5437 mat_stats["issues"] += 1 5438 continue 5439 5440 if not base_color.GetDestination(): 5441 warnings.append( 5442 f"ℹ️ '{name}': Diffuse/base color input not connected." 5443 ) 5444 # This is not necessarily an issue, just informational 5445 else: 5446 source_node = base_color.GetDestination().GetNode() 5447 source_type = "unknown" 5448 5449 # Identify the type of source 5450 if "ColorTexture" in source_node.GetId(): 5451 source_type = "texture" 5452 elif "Noise" in source_node.GetId(): 5453 source_type = "noise" 5454 elif "Checker" in source_node.GetId(): 5455 source_type = "checker" 5456 elif "Gradient" in source_node.GetId(): 5457 source_type = "gradient" 5458 elif "ColorConstant" in source_node.GetId(): 5459 source_type = "color" 5460 5461 warnings.append( 5462 f"✅ '{name}': Diffuse/base color connected to {source_type} node." 5463 ) 5464 5465 # Check for common issues in other ports 5466 # Detect if there's a fresnel node present 5467 has_fresnel = False 5468 for node in graph.GetNodes(): 5469 if "Fresnel" in node.GetId(): 5470 has_fresnel = True 5471 5472 # Verify the Fresnel node has proper connections 5473 inputs_valid = True 5474 for input_port in node.GetInputs(): 5475 port_id = input_port.GetId() 5476 if "ior" in port_id and not input_port.GetDestination(): 5477 inputs_valid = False 5478 warnings.append( 5479 f"⚠️ '{name}': Fresnel node missing IOR input." 5480 ) 5481 mat_stats["issues"] += 1 5482 5483 outputs_valid = False 5484 for output_port in node.GetOutputs(): 5485 if output_port.GetSource(): 5486 outputs_valid = True 5487 break 5488 5489 if not outputs_valid: 5490 warnings.append( 5491 f"⚠️ '{name}': Fresnel node has no output connections." 5492 ) 5493 mat_stats["issues"] += 1 5494 5495 if has_fresnel: 5496 warnings.append( 5497 f"ℹ️ '{name}': Contains Fresnel shader (check for potential issues)." 5498 ) 5499 5500 # Summary stats 5501 summary = ( 5502 f"Material validation complete. Found {mat_stats['total']} materials: " 5503 + f"{mat_stats['redshift']} Redshift, {mat_stats['standard']} Standard, " 5504 + f"{mat_stats['issues']} with issues, {mat_stats['fixed']} fixed." 5505 ) 5506 5507 # Update the document to apply any changes 5508 c4d.EventAdd() 5509 5510 # Format material_types for better readability 5511 material_types_formatted = {} 5512 for type_id, count in mat_stats["material_types"].items(): 5513 if type_id == c4d.Mmaterial: 5514 name = "Standard Material" 5515 elif type_id == c4d.ID_REDSHIFT_MATERIAL: 5516 name = "Redshift Material (using c4d.ID_REDSHIFT_MATERIAL)" 5517 elif type_id == 1036224: 5518 name = "Redshift Material (1036224)" 5519 elif type_id >= 1000000: 5520 name = f"Possible Redshift Material ({type_id})" 5521 else: 5522 name = f"Unknown Type ({type_id})" 5523 5524 material_types_formatted[name] = count 5525 5526 # Replace the original dictionary with the formatted one 5527 mat_stats["material_types"] = material_types_formatted 5528 5529 return { 5530 "status": "ok", 5531 "warnings": warnings, 5532 "fixes": fixes, 5533 "summary": summary, 5534 "stats": mat_stats, 5535 "ids": { 5536 "standard_material": c4d.Mmaterial, 5537 "redshift_material": c4d.ID_REDSHIFT_MATERIAL, 5538 }, 5539 } 5540 5541 except Exception as e: 5542 return { 5543 "status": "error", 5544 "message": f"Error validating materials: {str(e)}", 5545 "warnings": warnings, 5546 } 5547 5548 def handle_create_material(self, command): 5549 """Handle create_material command with context and proper NodeMaterial support for Redshift.""" 5550 doc = c4d.documents.GetActiveDocument() 5551 if not doc: 5552 return {"error": "No active document"} 5553 5554 requested_name = ( 5555 command.get("name") or command.get("material_name") or "New Material" 5556 ) 5557 color_list = command.get("color", [1.0, 1.0, 1.0]) # Default to white 5558 properties = command.get("properties", {}) 5559 material_type = command.get( 5560 "material_type", "standard" 5561 ).lower() # standard, redshift 5562 procedural = command.get( 5563 "procedural", False 5564 ) # Currently only affects Redshift in this example 5565 shader_type = command.get( 5566 "shader_type", "noise" 5567 ) # Used if procedural=True for Redshift 5568 5569 # Safely parse color 5570 color = [1.0, 1.0, 1.0] 5571 if isinstance(color_list, list) and len(color_list) >= 3: 5572 try: 5573 color = [ 5574 max(0.0, min(1.0, float(c))) for c in color_list[:3] 5575 ] # Clamp 0-1 5576 except (ValueError, TypeError): 5577 self.log(f"Warning: Invalid color data {color_list}") 5578 5579 self.log( 5580 f"[C4D] Starting material creation: Name='{requested_name}', Type='{material_type}'" 5581 ) 5582 5583 mat = None 5584 has_redshift = False 5585 redshift_plugin_id = None 5586 rs_mat_id_used = None # Store the ID actually used for RS material 5587 5588 try: 5589 # Check for Redshift plugin 5590 plugins = c4d.plugins.FilterPluginList(c4d.PLUGINTYPE_MATERIAL, True) 5591 for plugin in plugins: 5592 if "redshift" in plugin.GetName().lower(): 5593 has_redshift = True 5594 redshift_plugin_id = plugin.GetID() 5595 self.log( 5596 f"[C4D] Found Redshift plugin: {plugin.GetName()} (ID: {plugin_id})" 5597 ) 5598 break # Found it 5599 5600 if material_type == "redshift" and not has_redshift: 5601 self.log( 5602 "[C4D] ## Warning ##: Redshift requested but not found. Using standard material." 5603 ) 5604 material_type = "standard" 5605 5606 doc.StartUndo() # Start undo block 5607 5608 # Create material based on type 5609 if material_type == "redshift": 5610 self.log("[C4D] Attempting Redshift material creation...") 5611 try: 5612 # Determine the Redshift material ID to use 5613 rs_id = getattr( 5614 c4d, "ID_REDSHIFT_MATERIAL", redshift_plugin_id or 1036224 5615 ) # Prefer constant, then plugin, then default 5616 rs_mat_id_used = rs_id # Store the ID we are trying 5617 self.log(f"[C4D] Using Redshift Material ID: {rs_id}") 5618 5619 mat = c4d.BaseMaterial(rs_id) 5620 if not mat or mat.GetType() != rs_id: 5621 raise RuntimeError(f"Failed to create material with ID {rs_id}") 5622 5623 mat.SetName(requested_name) 5624 self.log(f"[C4D] Base Redshift material created: '{mat.GetName()}'") 5625 5626 # Setup node graph using maxon API (R20+) 5627 try: 5628 import maxon 5629 5630 redshift_ns = maxon.Id( 5631 "com.redshift3d.redshift4c4d.class.nodespace" 5632 ) 5633 node_mat = c4d.NodeMaterial(mat) # Wrap in NodeMaterial 5634 if not node_mat: 5635 raise RuntimeError("Failed to create NodeMaterial wrapper") 5636 5637 # Create default graph if it doesn't exist 5638 if not node_mat.HasSpace(redshift_ns): 5639 graph = node_mat.CreateDefaultGraph(redshift_ns) 5640 self.log("[C4D] Created default Redshift node graph") 5641 else: 5642 graph = node_mat.GetGraph(redshift_ns) 5643 self.log("[C4D] Using existing Redshift node graph") 5644 5645 if not graph: 5646 raise RuntimeError( 5647 "Failed to get or create Redshift node graph" 5648 ) 5649 5650 # Find StandardMaterial node and set base color 5651 standard_mat_node = None 5652 for node in graph.GetNodes(): 5653 if "StandardMaterial" in node.GetId(): 5654 standard_mat_node = node 5655 break 5656 5657 if standard_mat_node: 5658 try: 5659 standard_mat_node.SetParameter( 5660 maxon.nodes.ParameterID("base_color"), 5661 maxon.Color(*color), 5662 maxon.PROPERTYFLAGS_NONE, 5663 ) 5664 self.log(f"[C4D] Set Redshift base_color to {color}") 5665 except Exception as e_node: 5666 self.log( 5667 f"Warning: Failed to set Redshift base_color: {e_node}" 5668 ) 5669 else: 5670 self.log( 5671 "Warning: Could not find StandardMaterial node in Redshift graph to set color." 5672 ) 5673 5674 except ImportError: 5675 self.log( 5676 "Warning: 'maxon' module not found, cannot configure Redshift nodes." 5677 ) 5678 except Exception as e_node_setup: 5679 self.log( 5680 f"Warning: Error setting up Redshift node graph: {e_node_setup}" 5681 ) 5682 5683 except Exception as e_rs: 5684 self.log( 5685 f"[**ERROR**] Redshift material creation failed: {e_rs}\n{traceback.format_exc()}. Falling back to standard." 5686 ) 5687 material_type = "standard" # Fallback flag 5688 mat = None # Reset mat so standard creation runs 5689 5690 # Create a standard material if needed (or if RS failed) 5691 if material_type == "standard": 5692 self.log("[C4D] Creating standard material") 5693 mat = c4d.BaseMaterial(c4d.Mmaterial) 5694 if not mat: 5695 raise RuntimeError("Failed to create standard material") 5696 mat.SetName(requested_name) 5697 5698 # Set standard material properties 5699 mat[c4d.MATERIAL_COLOR_COLOR] = c4d.Vector(*color) # Set color 5700 5701 # Apply additional standard properties if provided 5702 if ( 5703 "specular" in properties 5704 and isinstance(properties["specular"], list) 5705 and len(properties["specular"]) >= 3 5706 ): 5707 try: 5708 mat[c4d.MATERIAL_SPECULAR_COLOR] = c4d.Vector( 5709 *[float(s) for s in properties["specular"][:3]] 5710 ) 5711 except (ValueError, TypeError): 5712 self.log( 5713 f"Warning: Invalid specular color value {properties['specular']}" 5714 ) 5715 if "reflection" in properties: 5716 try: 5717 mat[c4d.MATERIAL_REFLECTION_BRIGHTNESS] = max( 5718 0.0, float(properties["reflection"]) 5719 ) # Clamp >= 0 5720 except (ValueError, TypeError): 5721 self.log( 5722 f"Warning: Invalid reflection value {properties['reflection']}" 5723 ) 5724 5725 if not mat: # Final check if creation failed completely 5726 raise RuntimeError("Material creation failed for unknown reason.") 5727 5728 # Insert material into document 5729 doc.InsertMaterial(mat) 5730 doc.AddUndo(c4d.UNDOTYPE_NEW, mat) # Add undo step 5731 doc.EndUndo() # End undo block 5732 c4d.EventAdd() 5733 5734 # --- MODIFIED: Contextual Return --- 5735 actual_name = mat.GetName() 5736 mat_type_id = mat.GetType() 5737 final_material_type = ( 5738 "redshift" if mat_type_id == rs_mat_id_used else "standard" 5739 ) # Determine final type based on actual ID 5740 5741 # Get final color (might differ if RS nodes failed) 5742 final_color = color # Default to requested 5743 if final_material_type == "standard": 5744 try: 5745 final_color = [ 5746 mat[c4d.MATERIAL_COLOR_COLOR].x, 5747 mat[c4d.MATERIAL_COLOR_COLOR].y, 5748 mat[c4d.MATERIAL_COLOR_COLOR].z, 5749 ] 5750 except: 5751 pass # Keep requested color if read fails 5752 5753 self.log( 5754 f"[C4D] Material created successfully: Name='{actual_name}', Type='{final_material_type}', ID={mat_type_id}" 5755 ) 5756 5757 # Note: Materials don't have GUIDs in the same way as objects, so we don't register them. 5758 # We return info based on the final state. 5759 return { 5760 "material": { 5761 "requested_name": requested_name, 5762 "actual_name": actual_name, 5763 "type": final_material_type, # Report actual type created 5764 "color_set": final_color, # Report the final color state if possible 5765 "type_id": mat_type_id, 5766 "redshift_available": has_redshift, 5767 # Add any other relevant context about properties set 5768 } 5769 } 5770 # --- END MODIFIED --- 5771 5772 except Exception as e: 5773 doc.EndUndo() # Ensure undo ended 5774 error_msg = f"Failed to create material '{requested_name}': {str(e)}" 5775 self.log(f"[**ERROR**] {error_msg}\n{traceback.format_exc()}") 5776 # Clean up material if created but not inserted 5777 if mat and not mat.GetDocument(): 5778 try: 5779 mat.Remove() 5780 except: 5781 pass 5782 return {"error": error_msg, "traceback": traceback.format_exc()} 5783 5784 def handle_render_frame( 5785 self, command 5786 ): # Renamed from handle_render_to_file to match command key 5787 """Render the current frame to a file, using adapted core logic.""" 5788 doc = c4d.documents.GetActiveDocument() 5789 if not doc: 5790 return {"error": "No active document"} 5791 5792 output_path = command.get("output_path") 5793 width = int(command.get("width", 640)) 5794 height = int(command.get("height", 360)) 5795 # Frame handling - default to current frame if not specified 5796 frame = command.get("frame") 5797 if frame is None: 5798 frame = doc.GetTime().GetFrame(doc.GetFps()) 5799 else: 5800 try: 5801 frame = int(frame) 5802 except (ValueError, TypeError): 5803 self.log(f"Warning: Invalid frame value '{frame}', using current.") 5804 frame = doc.GetTime().GetFrame(doc.GetFps()) 5805 5806 self.log( 5807 f"[RENDER FRAME] Request: frame={frame}, size={width}x{height}, path={output_path}" 5808 ) 5809 5810 # Ensure output path is valid and directory exists 5811 if not output_path: 5812 doc_name = os.path.splitext(doc.GetDocumentName() or "Untitled")[0] 5813 fallback_dir = doc.GetDocumentPath() or os.path.join( 5814 os.path.expanduser("~"), "Desktop" 5815 ) # Fallback to desktop 5816 output_path = os.path.join( 5817 fallback_dir, f"{doc_name}_render_{frame:04d}.png" 5818 ) 5819 self.log( 5820 f"[RENDER FRAME] No output path provided, using fallback: {output_path}" 5821 ) 5822 else: 5823 output_path = os.path.normpath(os.path.expanduser(output_path)) 5824 5825 output_dir = os.path.dirname(output_path) 5826 try: 5827 os.makedirs(output_dir, exist_ok=True) 5828 except OSError as e: 5829 return {"error": f"Cannot create output directory '{output_dir}': {e}"} 5830 5831 # Determine format from extension (Default to PNG) 5832 ext = os.path.splitext(output_path)[1].lower() 5833 format_map = { 5834 ".png": c4d.FILTER_PNG, 5835 ".jpg": c4d.FILTER_JPG, 5836 ".jpeg": c4d.FILTER_JPG, 5837 ".tif": c4d.FILTER_TIF, 5838 ".tiff": c4d.FILTER_TIF, 5839 } 5840 format_id = format_map.get(ext, c4d.FILTER_PNG) 5841 if format_id == c4d.FILTER_PNG and ext not in format_map: 5842 output_path = os.path.splitext(output_path)[0] + ".png" 5843 format_id = c4d.FILTER_PNG 5844 self.log( 5845 f"Warning: Unsupported output extension '{ext}', defaulting to PNG: {output_path}" 5846 ) 5847 5848 # --- Execute render task on main thread --- 5849 def render_task(): 5850 bmp = None 5851 render_duration = 0.0 5852 original_rd = None # Keep track of original RD 5853 rd_clone = None # Keep track of clone RD 5854 temp_rd_inserted = False 5855 try: 5856 # --- Start Core Logic Adaptation --- 5857 if not doc: 5858 return {"error": "No active document (in render_task)"} 5859 active_draw = doc.GetActiveBaseDraw() 5860 if not active_draw: 5861 return {"error": "No active BaseDraw (in render_task)"} 5862 active_camera = ( 5863 active_draw.GetSceneCamera(doc) or active_draw.GetEditorCamera() 5864 ) 5865 if not active_camera: 5866 return {"error": "No active camera (in render_task)"} 5867 5868 original_rd = doc.GetActiveRenderData() 5869 if not original_rd: 5870 return {"error": "No active RenderData (in render_task)"} 5871 rd_clone = original_rd.GetClone(c4d.COPYFLAGS_NONE) 5872 if not rd_clone: 5873 return {"error": "RenderData clone failed (in render_task)"} 5874 5875 settings = rd_clone.GetDataInstance() 5876 if not settings: 5877 raise RuntimeError("Failed to get settings instance") 5878 5879 settings[c4d.RDATA_XRES] = float(width) 5880 settings[c4d.RDATA_YRES] = float(height) 5881 settings[c4d.RDATA_FRAMESEQUENCE] = c4d.RDATA_FRAMESEQUENCE_CURRENTFRAME 5882 settings[c4d.RDATA_SAVEIMAGE] = False # Render to bitmap, not auto-save 5883 # Force Standard renderer (required for Sketch & Toon, avoids Redshift memory issues) 5884 settings[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD 5885 5886 doc.InsertRenderData(rd_clone) 5887 temp_rd_inserted = True 5888 doc.SetActiveRenderData(rd_clone) 5889 5890 target_time = c4d.BaseTime(frame, doc.GetFps()) 5891 doc.SetTime(target_time) 5892 # --- FIXED ExecutePasses Call --- 5893 doc.ExecutePasses( 5894 None, True, True, True, c4d.BUILDFLAGS_NONE 5895 ) # Use None instead of active_draw 5896 # --- END FIXED --- 5897 5898 bmp = c4d.bitmaps.BaseBitmap() # Use BaseBitmap 5899 if ( 5900 not bmp 5901 or bmp.Init(int(width), int(height), 24) != c4d.IMAGERESULT_OK 5902 ): # Use int() for dimensions 5903 raise MemoryError(f"Bitmap Init failed ({width}x{height})") 5904 5905 render_flags = ( 5906 c4d.RENDERFLAGS_EXTERNAL 5907 | c4d.RENDERFLAGS_SHOWERRORS 5908 | c4d.RENDERFLAGS_NODOCUMENTCLONE 5909 | 0x00040000 5910 ) 5911 start_time = time.time() 5912 result_code = c4d.documents.RenderDocument( 5913 doc, settings, bmp, render_flags, None 5914 ) 5915 render_duration = time.time() - start_time 5916 5917 if result_code != c4d.RENDERRESULT_OK: 5918 err_str = self._render_code_to_str(result_code) 5919 last_c4d_err = c4d.GetLastError() 5920 if last_c4d_err: 5921 err_str += f" (GetLastError: {last_c4d_err})" 5922 raise RuntimeError(f"RenderDocument failed: {err_str}") 5923 # --- End Core Logic Adaptation --- 5924 5925 # Save the resulting bitmap to file 5926 self.log( 5927 f"[RENDER FRAME] Saving bitmap to: {output_path} (Format ID: {format_id})" 5928 ) 5929 save_result = bmp.Save(output_path, format_id) 5930 if save_result == c4d.IMAGERESULT_OK: 5931 self.log(f"[RENDER FRAME] Bitmap saved successfully.") 5932 return { 5933 "success": True, 5934 "output_path": output_path, 5935 "width": width, 5936 "height": height, 5937 "frame": frame, 5938 "render_time": render_duration, 5939 "file_exists": os.path.exists(output_path), 5940 } 5941 else: 5942 return { 5943 "error": f"Failed to save bitmap (Error code: {save_result})" 5944 } 5945 5946 except Exception as e_render: 5947 tb = traceback.format_exc() 5948 self.log( 5949 f"[**ERROR**][RENDER FRAME] Error during render task: {e_render}\n{tb}" 5950 ) 5951 return { 5952 "error": f"Exception during render/save: {str(e_render)}", 5953 "traceback": tb, 5954 } 5955 finally: 5956 # Cleanup render data clone 5957 if temp_rd_inserted and original_rd: 5958 try: 5959 doc.SetActiveRenderData(original_rd) 5960 if rd_clone: 5961 rd_clone.Remove() 5962 except Exception as e_cleanup: 5963 self.log(f"Warning: Error during RD cleanup: {e_cleanup}") 5964 # Cleanup bitmap 5965 if bmp: 5966 try: 5967 bmp.FlushAll() 5968 except: 5969 pass 5970 c4d.EventAdd() 5971 5972 # Execute the task on the main thread 5973 response = self.execute_on_main_thread(render_task, _timeout=180) 5974 5975 # Structure the final response for the tool 5976 if response and response.get("success"): 5977 return { 5978 "render_info": response 5979 } # Return nested structure expected by server tool 5980 else: 5981 # Ensure error structure is consistent if render_task itself returns an error dict 5982 if isinstance(response, dict) and "error" in response: 5983 return response 5984 # Handle cases where execute_on_main_thread returned an error (like timeout) 5985 elif isinstance(response, dict) and "error" in response: 5986 return response 5987 else: # Fallback for unexpected scenarios 5988 return {"error": "Unknown error during render frame execution."} 5989 5990 def handle_apply_shader(self, command): 5991 """Handle apply_shader command with improved Redshift/Fresnel support and context.""" 5992 doc = c4d.documents.GetActiveDocument() 5993 if not doc: 5994 return {"error": "No active document"} 5995 5996 material_name = command.get("material_name", "") 5997 # --- MODIFIED: Identify target object --- 5998 identifier = None 5999 use_guid = False 6000 object_specified = False 6001 if command.get("guid"): # Check for GUID first 6002 identifier = command.get("guid") 6003 use_guid = True 6004 object_specified = True 6005 self.log(f"[APPLY SHADER] Using GUID identifier for object: '{identifier}'") 6006 elif command.get("object_name"): 6007 identifier = command.get("object_name") 6008 use_guid = False 6009 object_specified = True 6010 self.log(f"[APPLY SHADER] Using Name identifier for object: '{identifier}'") 6011 # --- END MODIFIED --- 6012 6013 shader_type = command.get("shader_type", "noise").lower() 6014 channel = command.get("channel", "color").lower() 6015 parameters = command.get("parameters", {}) 6016 6017 self.log( 6018 f"[APPLY SHADER] Request: Shader='{shader_type}', Channel='{channel}', Material='{material_name}', Object='{identifier}'" 6019 ) 6020 6021 mat = None 6022 created_new_material = False 6023 obj_to_apply = None 6024 6025 try: 6026 doc.StartUndo() # Start undo block 6027 6028 # Find or create material 6029 if material_name: 6030 mat = self._find_material_by_name(doc, material_name) 6031 if mat is None: 6032 default_mat_name = ( 6033 material_name 6034 if material_name 6035 else f"{shader_type.capitalize()} Material" 6036 ) 6037 mat = c4d.BaseMaterial(c4d.Mmaterial) # Create standard by default 6038 if not mat: 6039 raise RuntimeError("Failed to create new material") 6040 mat.SetName(default_mat_name) 6041 doc.InsertMaterial(mat) 6042 doc.AddUndo(c4d.UNDOTYPE_NEW, mat) 6043 created_new_material = True 6044 material_name = mat.GetName() # Use actual name 6045 self.log(f"[APPLY SHADER] Created new material: '{material_name}'") 6046 6047 # Find object if specified 6048 if object_specified: 6049 obj_to_apply = self.find_object_by_name( 6050 doc, identifier, use_guid=use_guid 6051 ) 6052 if obj_to_apply is None: 6053 search_type = "GUID" if use_guid else "Name" 6054 self.log( 6055 f"Warning: Object '{identifier}' (searched by {search_type}) not found for shader application." 6056 ) 6057 # Don't error out, just won't apply tag later 6058 6059 # Determine if material is Redshift 6060 is_redshift_material = False 6061 rs_mat_id = getattr( 6062 c4d, "ID_REDSHIFT_MATERIAL", 1036224 6063 ) # Get RS ID safely 6064 if mat.GetType() == rs_mat_id: 6065 is_redshift_material = True 6066 elif mat.GetType() >= 1000000: # General check for other RS types 6067 is_redshift_material = True 6068 self.log( 6069 f"Info: Material '{material_name}' has high ID ({mat.GetType()}), treating as Redshift." 6070 ) 6071 6072 if is_redshift_material: 6073 self.log( 6074 f"[APPLY SHADER] Applying shader to Redshift material '{material_name}'..." 6075 ) 6076 # --- Redshift Node Graph Logic --- 6077 try: 6078 import maxon 6079 6080 redshift_ns = maxon.Id( 6081 "com.redshift3d.redshift4c4d.class.nodespace" 6082 ) 6083 node_mat = c4d.NodeMaterial(mat) 6084 if node_mat and node_mat.HasSpace(redshift_ns): 6085 graph = node_mat.GetGraph(redshift_ns) 6086 if graph: 6087 with graph.BeginTransaction() as transaction: 6088 # Find output node... (Simplified for brevity - assumes StandardMaterial exists) 6089 material_output = None 6090 for node in graph.GetNodes(): 6091 if "StandardMaterial" in node.GetId(): 6092 material_output = node 6093 break 6094 6095 if material_output: 6096 # Create shader node... 6097 shader_node = None 6098 shader_node_id_str = "" 6099 if shader_type == "noise": 6100 shader_node_id_str = "com.redshift3d.redshift4c4d.nodes.core.texturesampler" 6101 elif shader_type == "fresnel": 6102 shader_node_id_str = "com.redshift3d.redshift4c4d.nodes.core.fresnel" 6103 # Add more shader types here... 6104 6105 if shader_node_id_str: 6106 shader_node = graph.AddChild( 6107 maxon.Id(), maxon.Id(shader_node_id_str) 6108 ) 6109 if ( 6110 shader_node and shader_type == "noise" 6111 ): # Configure noise specific 6112 shader_node.SetParameter( 6113 maxon.nodes.ParameterID("tex0_tex"), 6114 4, 6115 maxon.PROPERTYFLAGS_NONE, 6116 ) # 4=Noise 6117 if "scale" in parameters: 6118 shader_node.SetParameter( 6119 maxon.nodes.ParameterID( 6120 "noise_scale" 6121 ), 6122 float(parameters["scale"]), 6123 maxon.PROPERTYFLAGS_NONE, 6124 ) 6125 6126 # Connect shader node... 6127 if shader_node: 6128 # Find target port... (Simplified) 6129 target_port_id_str = ( 6130 "base_color" 6131 if channel == "color" 6132 else "refl_color" 6133 ) # Example mapping 6134 target_port = material_output.GetInputs().Find( 6135 maxon.Id(target_port_id_str) 6136 ) 6137 6138 # Find source port... (Simplified) 6139 source_port_id_str = ( 6140 "outcolor" 6141 if shader_type != "fresnel" 6142 else "out" 6143 ) 6144 source_port = shader_node.GetOutputs().Find( 6145 maxon.Id(source_port_id_str) 6146 ) 6147 6148 if target_port and source_port: 6149 graph.CreateConnection( 6150 source_port, target_port 6151 ) 6152 self.log( 6153 f"Connected RS {shader_type} node to {channel}" 6154 ) 6155 else: 6156 self.log( 6157 "Warning: Could not find source/target ports for RS shader connection." 6158 ) 6159 else: 6160 self.log( 6161 f"Warning: Failed to create RS {shader_type} node." 6162 ) 6163 else: 6164 self.log( 6165 "Warning: Could not find RS StandardMaterial output node." 6166 ) 6167 transaction.Commit() 6168 else: 6169 self.log("Warning: Could not get RS node graph.") 6170 else: 6171 self.log( 6172 "Warning: Material is not a Redshift Node Material or lacks RS space." 6173 ) 6174 except ImportError: 6175 self.log("Warning: 'maxon' module not found, cannot edit RS nodes.") 6176 except Exception as e_rs: 6177 self.log(f"Error applying shader to RS material: {e_rs}") 6178 # Fallthrough to standard shader application is NOT intended here. If it's RS, we try nodes. 6179 6180 else: 6181 # --- Standard Shader Logic (from original) --- 6182 self.log( 6183 f"[APPLY SHADER] Applying shader to Standard material '{material_name}'..." 6184 ) 6185 shader_types = { 6186 "noise": 5832, 6187 "gradient": 5825, 6188 "fresnel": 5837, 6189 "layer": 5685, 6190 "checkerboard": 5831, 6191 } 6192 channel_map = { 6193 "color": c4d.MATERIAL_COLOR_SHADER, 6194 "luminance": c4d.MATERIAL_LUMINANCE_SHADER, 6195 "transparency": c4d.MATERIAL_TRANSPARENCY_SHADER, 6196 "reflection": c4d.MATERIAL_REFLECTION_SHADER, 6197 "bump": c4d.MATERIAL_BUMP_SHADER, 6198 } # Added bump 6199 shader_type_id = shader_types.get(shader_type, 5832) 6200 channel_id = channel_map.get(channel) 6201 6202 if channel_id is None: 6203 raise ValueError(f"Unsupported standard channel: {channel}") 6204 6205 shader = c4d.BaseShader(shader_type_id) 6206 if shader is None: 6207 raise RuntimeError( 6208 f"Failed to create standard {shader_type} shader" 6209 ) 6210 6211 # Apply parameters (example for noise) 6212 if shader_type == "noise" and hasattr(c4d, "SLA_NOISE_SCALE"): 6213 if "scale" in parameters: 6214 shader[c4d.SLA_NOISE_SCALE] = float( 6215 parameters.get("scale", 1.0) 6216 ) 6217 if "octaves" in parameters: 6218 shader[c4d.SLA_NOISE_OCTAVES] = int( 6219 parameters.get("octaves", 3) 6220 ) 6221 # Add more parameter settings for other standard shader types here... 6222 6223 mat[channel_id] = shader 6224 6225 # Enable the channel 6226 enable_map = { 6227 "color": c4d.MATERIAL_USE_COLOR, 6228 "luminance": c4d.MATERIAL_USE_LUMINANCE, 6229 "transparency": c4d.MATERIAL_USE_TRANSPARENCY, 6230 "reflection": c4d.MATERIAL_USE_REFLECTION, 6231 "bump": c4d.MATERIAL_USE_BUMP, 6232 } 6233 if channel in enable_map: 6234 try: 6235 mat[enable_map[channel]] = True 6236 except AttributeError: 6237 self.log( 6238 f"Warning: Could not find enable parameter for channel '{channel}'" 6239 ) 6240 6241 mat.Update(True, True) 6242 doc.AddUndo(c4d.UNDOTYPE_CHANGE, mat) # Add undo for material change 6243 6244 # Apply material to object if found 6245 applied_to_name = "None" 6246 applied_to_guid = None 6247 if obj_to_apply: 6248 try: 6249 # Check if object already has a texture tag for this material 6250 existing_tag = None 6251 for tag in obj_to_apply.GetTags(): 6252 if tag.GetType() == c4d.Ttexture and tag.GetMaterial() == mat: 6253 existing_tag = tag 6254 self.log( 6255 f"Found existing texture tag for material '{material_name}' on '{obj_to_apply.GetName()}'" 6256 ) 6257 break 6258 6259 if not existing_tag: 6260 tag = obj_to_apply.MakeTag( 6261 c4d.Ttexture 6262 ) # Use MakeTag for safer insertion 6263 if tag: 6264 tag.SetMaterial(mat) 6265 doc.AddUndo(c4d.UNDOTYPE_NEW, tag) 6266 applied_to_name = obj_to_apply.GetName() 6267 applied_to_guid = str(obj_to_apply.GetGUID()) 6268 self.log( 6269 f"[APPLY SHADER] Applied material '{material_name}' to object '{applied_to_name}'" 6270 ) 6271 else: 6272 self.log( 6273 f"Warning: Failed to create texture tag on '{obj_to_apply.GetName()}'" 6274 ) 6275 else: 6276 # Material already applied via existing tag 6277 applied_to_name = obj_to_apply.GetName() 6278 applied_to_guid = str(obj_to_apply.GetGUID()) 6279 self.log( 6280 f"Material '{material_name}' was already applied to object '{applied_to_name}'" 6281 ) 6282 6283 except Exception as e_tag: 6284 self.log( 6285 f"[**ERROR**] Error applying material tag to '{obj_to_apply.GetName()}': {str(e_tag)}" 6286 ) 6287 6288 doc.EndUndo() # End undo block 6289 c4d.EventAdd() 6290 6291 # --- MODIFIED: Contextual Return --- 6292 return { 6293 "shader_application": { # Changed key for clarity 6294 "material_name": material_name, 6295 "material_type_id": mat.GetType(), 6296 "shader_type": shader_type, 6297 "channel": channel, 6298 "applied_to_object_name": applied_to_name, # Name or "None" 6299 "applied_to_object_guid": applied_to_guid, # GUID or None 6300 "created_new_material": created_new_material, 6301 "is_redshift_material": is_redshift_material, 6302 } 6303 } 6304 # --- END MODIFIED --- 6305 6306 except Exception as e: 6307 doc.EndUndo() # Ensure undo ended 6308 self.log( 6309 f"[**ERROR**] Error applying shader: {str(e)}\n{traceback.format_exc()}" 6310 ) 6311 return { 6312 "error": f"Failed to apply shader: {str(e)}", 6313 "traceback": traceback.format_exc(), 6314 } 6315 6316 def handle_describe_hierarchy(self, command): 6317 """ 6318 Handle describe_hierarchy command - returns semantic scene description. 6319 6320 Tries to use DreamTalk introspection module if available, falls back 6321 to basic C4D object listing otherwise. 6322 """ 6323 doc = c4d.documents.GetActiveDocument() 6324 format_type = command.get("format", "markdown") 6325 6326 try: 6327 # Try to use DreamTalk introspection if available 6328 from DreamTalk.introspection import describe_hierarchy, format_for_ai 6329 result = describe_hierarchy(doc) 6330 description = format_for_ai(result, format_type) 6331 self.log("[C4D] Used DreamTalk introspection for hierarchy description") 6332 except ImportError: 6333 # Fallback to basic description 6334 description = self._basic_hierarchy_description(doc, format_type) 6335 self.log("[C4D] Used fallback hierarchy description (DreamTalk not available)") 6336 except Exception as e: 6337 self.log(f"[**ERROR**] Error in describe_hierarchy: {str(e)}") 6338 return {"error": f"Failed to describe hierarchy: {str(e)}"} 6339 6340 return {"description": description} 6341 6342 def _basic_hierarchy_description(self, doc, format_type="markdown"): 6343 """ 6344 Fallback hierarchy description when DreamTalk is not available. 6345 6346 Returns basic C4D object listing. 6347 """ 6348 lines = [] 6349 lines.append(f"# Scene: {doc.GetDocumentName() or 'Untitled'}") 6350 lines.append("") 6351 6352 def describe_obj(obj, depth=0): 6353 indent = " " * depth 6354 prefix = "└── " if depth > 0 else "" 6355 pos = obj.GetAbsPos() 6356 name = obj.GetName() 6357 obj_type = obj.GetTypeName() 6358 lines.append(f"{indent}{prefix}{name} ({obj_type}) @ ({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f})") 6359 6360 child = obj.GetDown() 6361 while child: 6362 describe_obj(child, depth + 1) 6363 child = child.GetNext() 6364 6365 obj = doc.GetFirstObject() 6366 count = 0 6367 while obj: 6368 describe_obj(obj) 6369 count += 1 6370 obj = obj.GetNext() 6371 6372 lines.insert(1, f"**{count} root object(s)**") 6373 lines.insert(2, "") 6374 6375 if format_type == "json": 6376 import json 6377 return json.dumps({"description": "\n".join(lines), "object_count": count}) 6378 6379 return "\n".join(lines) 6380 6381 def handle_inspect_object(self, command): 6382 """Handle inspect_object command - deep dive into single object.""" 6383 object_name = command.get("object_name") 6384 if not object_name: 6385 return {"error": "object_name is required"} 6386 6387 doc = c4d.documents.GetActiveDocument() 6388 6389 try: 6390 from DreamTalk.introspection import inspect_object 6391 from DreamTalk.introspection.formatters import format_inspect_object 6392 result = inspect_object(object_name, doc) 6393 description = format_inspect_object(result) 6394 self.log(f"[C4D] Inspected object: {object_name}") 6395 except ImportError: 6396 description = f"DreamTalk introspection not available. Object '{object_name}' exists but cannot be inspected." 6397 self.log("[C4D] DreamTalk introspection not available") 6398 except Exception as e: 6399 return {"error": f"Failed to inspect object: {str(e)}"} 6400 6401 return {"description": description} 6402 6403 def handle_inspect_materials(self, command): 6404 """Handle inspect_materials command - describe all materials.""" 6405 doc = c4d.documents.GetActiveDocument() 6406 6407 try: 6408 from DreamTalk.introspection import inspect_materials 6409 from DreamTalk.introspection.formatters import format_inspect_materials 6410 result = inspect_materials(doc) 6411 description = format_inspect_materials(result) 6412 self.log("[C4D] Inspected materials") 6413 except ImportError: 6414 # Fallback 6415 mat = doc.GetFirstMaterial() 6416 count = 0 6417 names = [] 6418 while mat: 6419 names.append(mat.GetName()) 6420 count += 1 6421 mat = mat.GetNext() 6422 description = f"# Materials ({count})\n\n" + "\n".join([f"- {n}" for n in names]) 6423 self.log("[C4D] Used fallback materials inspection") 6424 except Exception as e: 6425 return {"error": f"Failed to inspect materials: {str(e)}"} 6426 6427 return {"description": description} 6428 6429 def handle_inspect_animation(self, command): 6430 """Handle inspect_animation command - describe animation keyframes.""" 6431 doc = c4d.documents.GetActiveDocument() 6432 start_frame = command.get("start_frame") 6433 end_frame = command.get("end_frame") 6434 6435 try: 6436 from DreamTalk.introspection import inspect_animation 6437 from DreamTalk.introspection.formatters import format_inspect_animation 6438 result = inspect_animation(start_frame, end_frame, doc) 6439 description = format_inspect_animation(result) 6440 self.log("[C4D] Inspected animation") 6441 except ImportError: 6442 fps = doc.GetFps() 6443 doc_start = doc.GetMinTime().GetFrame(fps) 6444 doc_end = doc.GetMaxTime().GetFrame(fps) 6445 description = f"# Animation\n\nFrame range: {doc_start} - {doc_end}\nFPS: {fps}\n\n(DreamTalk introspection not available for detailed analysis)" 6446 self.log("[C4D] Used fallback animation inspection") 6447 except Exception as e: 6448 return {"error": f"Failed to inspect animation: {str(e)}"} 6449 6450 return {"description": description} 6451 6452 def handle_validate_scene(self, command): 6453 """Handle validate_scene command - pre-render sanity checks.""" 6454 doc = c4d.documents.GetActiveDocument() 6455 6456 try: 6457 from DreamTalk.introspection import validate_scene 6458 from DreamTalk.introspection.formatters import format_validate_scene 6459 result = validate_scene(doc) 6460 description = format_validate_scene(result) 6461 self.log("[C4D] Validated scene") 6462 except ImportError: 6463 description = "# Scene Validation\n\n(DreamTalk introspection not available)" 6464 self.log("[C4D] DreamTalk introspection not available for validation") 6465 except Exception as e: 6466 return {"error": f"Failed to validate scene: {str(e)}"} 6467 6468 return {"description": description} 6469 6470 def handle_inspect_xpresso(self, command): 6471 """Handle inspect_xpresso command - deep XPresso tag inspection.""" 6472 object_name = command.get("object_name") 6473 if not object_name: 6474 return {"error": "object_name is required"} 6475 6476 doc = c4d.documents.GetActiveDocument() 6477 6478 try: 6479 from DreamTalk.introspection import inspect_xpresso 6480 result = inspect_xpresso(object_name) 6481 6482 # Format as readable description 6483 lines = [f"# XPresso Inspection: {object_name}", ""] 6484 6485 if "error" in result: 6486 return {"error": result["error"]} 6487 6488 for tag_info in result.get("xpresso_tags", []): 6489 lines.append(f"## XPresso Tag: {tag_info['name']}") 6490 lines.append("") 6491 6492 for group in tag_info.get("groups", []): 6493 lines.append(f"### {group['name']}") 6494 for node in group.get("nodes", []): 6495 inputs = ", ".join(node.get("inputs", [])) or "none" 6496 outputs = ", ".join(node.get("outputs", [])) or "none" 6497 lines.append(f"- **{node['name']}**: in=[{inputs}] → out=[{outputs}]") 6498 lines.append("") 6499 6500 if not result.get("xpresso_tags"): 6501 lines.append("No XPresso tags found on this object.") 6502 6503 description = "\n".join(lines) 6504 self.log(f"[C4D] Inspected XPresso on {object_name}") 6505 6506 except ImportError as e: 6507 description = f"# XPresso Inspection\n\n(DreamTalk introspection not available: {e})" 6508 self.log("[C4D] DreamTalk introspection not available for XPresso") 6509 except Exception as e: 6510 return {"error": f"Failed to inspect XPresso: {str(e)}"} 6511 6512 return {"description": description} 6513 6514 def handle_diff_scene(self, command): 6515 """Handle diff_scene command - compare scene state to last snapshot.""" 6516 doc = c4d.documents.GetActiveDocument() 6517 6518 try: 6519 from DreamTalk.introspection import diff_scene 6520 result = diff_scene(doc) 6521 6522 # Format as readable description 6523 if result.get("status") == "first_snapshot": 6524 description = f"# Scene Diff\n\n{result['message']}\n\nObjects tracked: {result['objects_tracked']}" 6525 else: 6526 lines = ["# Scene Diff", ""] 6527 lines.append(f"**Summary**: {result['summary']}") 6528 lines.append("") 6529 6530 changes = result.get("changes", {}) 6531 6532 if changes.get("modified"): 6533 lines.append("## Modified") 6534 for obj, params in changes["modified"].items(): 6535 for param, vals in params.items(): 6536 lines.append(f"- `{obj}.{param}`: {vals['old']} → **{vals['new']}**") 6537 lines.append("") 6538 6539 if changes.get("added"): 6540 lines.append("## Added Objects") 6541 for obj in changes["added"]: 6542 lines.append(f"- {obj}") 6543 lines.append("") 6544 6545 if changes.get("removed"): 6546 lines.append("## Removed Objects") 6547 for obj in changes["removed"]: 6548 lines.append(f"- {obj}") 6549 lines.append("") 6550 6551 if result["total_changes"] == 0: 6552 lines.append("No changes detected since last snapshot.") 6553 6554 description = "\n".join(lines) 6555 6556 self.log("[C4D] Performed scene diff") 6557 6558 except ImportError as e: 6559 description = f"# Scene Diff\n\n(DreamTalk introspection not available: {e})" 6560 self.log("[C4D] DreamTalk introspection not available for diff") 6561 except Exception as e: 6562 return {"error": f"Failed to diff scene: {str(e)}"} 6563 6564 return {"description": description} 6565 6566 def handle_reset_snapshot(self, command): 6567 """Handle reset_snapshot command - clear scene snapshot.""" 6568 try: 6569 from DreamTalk.introspection import reset_snapshot 6570 result = reset_snapshot() 6571 description = f"# Snapshot Reset\n\n{result['message']}" 6572 self.log("[C4D] Reset scene snapshot") 6573 except ImportError as e: 6574 description = f"# Snapshot Reset\n\n(DreamTalk introspection not available: {e})" 6575 except Exception as e: 6576 return {"error": f"Failed to reset snapshot: {str(e)}"} 6577 6578 return {"description": description} 6579 6580 def handle_viewport_preview(self, command): 6581 """ 6582 Fast viewport preview using hardware OpenGL renderer (~100-200ms per frame). 6583 6584 PRIMARY FEEDBACK TOOL - use this 95% of the time for geometry, positioning, 6585 hierarchy checks. Does NOT show Sketch & Toon lines or post-effects. 6586 6587 Args: 6588 command: dict with optional 'frames' (int or list), 'width', 'height' 6589 6590 Returns: 6591 dict with 'success', 'paths' (list), timing info 6592 """ 6593 width = command.get("width", 640) 6594 height = command.get("height", 360) 6595 frames_input = command.get("frames", None) 6596 6597 # Normalize frames to list 6598 if frames_input is None: 6599 frames = None # Use current frame 6600 elif isinstance(frames_input, (int, float)): 6601 frames = [int(frames_input)] 6602 else: 6603 frames = [int(f) for f in frames_input] 6604 6605 def capture_task(): 6606 rd_temp = None 6607 paths = [] 6608 total_time = 0 6609 try: 6610 doc = c4d.documents.GetActiveDocument() 6611 if not doc: 6612 return {"error": "No active document"} 6613 6614 fps = doc.GetFps() 6615 6616 # Create a CLEAN temporary RenderData without any VideoPost effects 6617 rd_temp = c4d.documents.RenderData() 6618 rd_temp[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_PREVIEWHARDWARE 6619 rd_temp[c4d.RDATA_XRES] = width 6620 rd_temp[c4d.RDATA_YRES] = height 6621 rd_temp[c4d.RDATA_FRAMESEQUENCE] = c4d.RDATA_FRAMESEQUENCE_CURRENTFRAME 6622 doc.InsertRenderData(rd_temp) 6623 6624 # Determine which frames to render 6625 if frames is None: 6626 frame_list = [doc.GetTime().GetFrame(fps)] 6627 else: 6628 frame_list = frames 6629 6630 for frame in frame_list: 6631 # Set frame 6632 doc.SetTime(c4d.BaseTime(frame, fps)) 6633 doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) 6634 6635 # Create bitmap 6636 bmp = c4d.bitmaps.BaseBitmap() 6637 if bmp.Init(width, height, 24) != c4d.IMAGERESULT_OK: 6638 return {"error": f"Failed to initialize bitmap ({width}x{height})"} 6639 6640 # Render 6641 start_time = time.time() 6642 result = c4d.documents.RenderDocument( 6643 doc, rd_temp.GetData(), bmp, 6644 c4d.RENDERFLAGS_EXTERNAL | c4d.RENDERFLAGS_NODOCUMENTCLONE 6645 ) 6646 total_time += time.time() - start_time 6647 6648 if result != c4d.RENDERRESULT_OK: 6649 return {"error": f"Hardware preview failed at frame {frame}: {self._render_code_to_str(result)}"} 6650 6651 # Save 6652 if len(frame_list) == 1: 6653 path = "/tmp/c4d_viewport_preview.png" 6654 else: 6655 path = f"/tmp/c4d_viewport_preview_{frame:04d}.png" 6656 6657 save_result = bmp.Save(path, c4d.FILTER_PNG) 6658 if save_result != c4d.IMAGERESULT_OK: 6659 return {"error": f"Failed to save image: {save_result}"} 6660 paths.append(path) 6661 bmp.FlushAll() 6662 6663 return { 6664 "success": True, 6665 "paths": paths, 6666 "frames": frame_list, 6667 "width": width, 6668 "height": height, 6669 "total_time_ms": int(total_time * 1000), 6670 "renderer": "hardware_preview" 6671 } 6672 6673 except Exception as e: 6674 import traceback 6675 return {"error": f"Viewport preview failed: {str(e)}", "traceback": traceback.format_exc()} 6676 finally: 6677 if rd_temp: 6678 try: 6679 rd_temp.Remove() 6680 except: 6681 pass 6682 c4d.EventAdd() 6683 6684 return self.execute_on_main_thread(capture_task, _timeout=60) 6685 6686 def handle_rendered_preview(self, command): 6687 """ 6688 Full rendered preview with Sketch & Toon and proper materials (~2-10s per frame). 6689 6690 SECONDARY FEEDBACK TOOL - use when you need to verify line drawing, 6691 creation animations, or final look. Much slower than viewport_preview. 6692 6693 Uses Standard renderer with Sketch & Toon VideoPost (matches DreamTalk library). 6694 6695 Args: 6696 command: dict with optional 'frames' (int or list), 'width', 'height' 6697 6698 Returns: 6699 dict with 'success', 'paths' (list), timing info 6700 """ 6701 width = command.get("width", 640) 6702 height = command.get("height", 360) 6703 frames_input = command.get("frames", None) 6704 6705 # Normalize frames to list 6706 if frames_input is None: 6707 frames = None 6708 elif isinstance(frames_input, (int, float)): 6709 frames = [int(frames_input)] 6710 else: 6711 frames = [int(f) for f in frames_input] 6712 6713 def render_task(): 6714 rd_temp = None 6715 paths = [] 6716 total_time = 0 6717 original_rd = None 6718 try: 6719 doc = c4d.documents.GetActiveDocument() 6720 if not doc: 6721 return {"error": "No active document"} 6722 6723 fps = doc.GetFps() 6724 original_rd = doc.GetActiveRenderData() 6725 6726 # Create render data matching DreamTalk library settings 6727 rd_temp = c4d.documents.RenderData() 6728 rd_temp[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD 6729 rd_temp[c4d.RDATA_XRES] = width 6730 rd_temp[c4d.RDATA_YRES] = height 6731 rd_temp[c4d.RDATA_FRAMESEQUENCE] = c4d.RDATA_FRAMESEQUENCE_CURRENTFRAME 6732 rd_temp[c4d.RDATA_SAVEIMAGE] = False 6733 6734 # Add Sketch & Toon VideoPost (matching DreamTalk scene.py settings) 6735 sketch_vp = c4d.documents.BaseVideoPost(1011015) 6736 sketch_vp[c4d.OUTLINEMAT_SHADING_BACK] = False 6737 sketch_vp[c4d.OUTLINEMAT_SHADING_OBJECT] = False 6738 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_INDEPENDENT] = True 6739 sketch_vp[c4d.OUTLINEMAT_EDLINES_SHOWLINES] = True 6740 sketch_vp[c4d.OUTLINEMAT_EDLINES_LINE_DRAW] = 1 6741 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_INDEPENDENT_MODE] = 1 6742 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_BASEW] = 1080 6743 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_BASEH] = 1080 6744 sketch_vp[c4d.OUTLINEMAT_EDLINES_REDRAW_FULL] = True 6745 sketch_vp[c4d.OUTLINEMAT_LINE_SPLINES] = True 6746 rd_temp.InsertVideoPost(sketch_vp) 6747 6748 doc.InsertRenderData(rd_temp) 6749 doc.SetActiveRenderData(rd_temp) 6750 6751 # Determine which frames to render 6752 if frames is None: 6753 frame_list = [doc.GetTime().GetFrame(fps)] 6754 else: 6755 frame_list = frames 6756 6757 for frame in frame_list: 6758 # Set frame 6759 doc.SetTime(c4d.BaseTime(frame, fps)) 6760 doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) 6761 6762 # Create bitmap - 24-bit for Sketch & Toon compatibility 6763 bmp = c4d.bitmaps.BaseBitmap() 6764 if bmp.Init(width, height, 24) != c4d.IMAGERESULT_OK: 6765 return {"error": f"Failed to initialize bitmap ({width}x{height})"} 6766 6767 # Render with Standard renderer 6768 start_time = time.time() 6769 result = c4d.documents.RenderDocument( 6770 doc, rd_temp.GetData(), bmp, 6771 c4d.RENDERFLAGS_EXTERNAL | c4d.RENDERFLAGS_NODOCUMENTCLONE 6772 ) 6773 total_time += time.time() - start_time 6774 6775 if result != c4d.RENDERRESULT_OK: 6776 return {"error": f"Render failed at frame {frame}: {self._render_code_to_str(result)}"} 6777 6778 # Save 6779 if len(frame_list) == 1: 6780 path = "/tmp/c4d_rendered_preview.png" 6781 else: 6782 path = f"/tmp/c4d_rendered_preview_{frame:04d}.png" 6783 6784 save_result = bmp.Save(path, c4d.FILTER_PNG) 6785 if save_result != c4d.IMAGERESULT_OK: 6786 return {"error": f"Failed to save image: {save_result}"} 6787 paths.append(path) 6788 bmp.FlushAll() 6789 6790 return { 6791 "success": True, 6792 "paths": paths, 6793 "frames": frame_list, 6794 "width": width, 6795 "height": height, 6796 "total_time_ms": int(total_time * 1000), 6797 "renderer": "standard_sketch_toon" 6798 } 6799 6800 except Exception as e: 6801 import traceback 6802 return {"error": f"Rendered preview failed: {str(e)}", "traceback": traceback.format_exc()} 6803 finally: 6804 if rd_temp: 6805 try: 6806 if original_rd: 6807 doc.SetActiveRenderData(original_rd) 6808 rd_temp.Remove() 6809 except: 6810 pass 6811 c4d.EventAdd() 6812 6813 # Longer timeout for rendered preview 6814 return self.execute_on_main_thread(render_task, _timeout=180) 6815 6816 def handle_describe_scene(self, command): 6817 """ 6818 Universal scene introspection with automatic change detection. 6819 6820 Combines all introspection into one call: 6821 - Scene metadata (fps, frame range, current frame) 6822 - Full object hierarchy with DreamTalk semantics 6823 - All UserData parameters 6824 - Materials and assignments 6825 - Animation keyframes 6826 - Validation warnings 6827 - Changes since last call (auto-diffing) 6828 """ 6829 def describe_task(): 6830 doc = c4d.documents.GetActiveDocument() 6831 if not doc: 6832 return {"error": "No active document"} 6833 6834 try: 6835 from DreamTalk.introspection import describe_scene, format_describe_scene 6836 result = describe_scene(doc) 6837 description = format_describe_scene(result) 6838 self.log("[C4D] Universal scene introspection completed") 6839 return {"description": description, "raw": result} 6840 except ImportError as e: 6841 self.log(f"[C4D] DreamTalk introspection not available: {e}") 6842 # Fallback to basic scene info 6843 fps = doc.GetFps() 6844 current_frame = doc.GetTime().GetFrame(fps) 6845 doc_start = doc.GetMinTime().GetFrame(fps) 6846 doc_end = doc.GetMaxTime().GetFrame(fps) 6847 6848 # Count objects 6849 obj_count = 0 6850 def count_objects(obj): 6851 nonlocal obj_count 6852 while obj: 6853 obj_count += 1 6854 count_objects(obj.GetDown()) 6855 obj = obj.GetNext() 6856 count_objects(doc.GetFirstObject()) 6857 6858 # Count materials 6859 mat_count = 0 6860 mat = doc.GetFirstMaterial() 6861 while mat: 6862 mat_count += 1 6863 mat = mat.GetNext() 6864 6865 description = f"""# Scene: {doc.GetDocumentName() or 'Untitled'} 6866 Frame {current_frame}/{doc_end} @ {fps}fps 6867 6868 *DreamTalk introspection not available - showing basic info* 6869 6870 ## Summary 6871 - Objects: {obj_count} 6872 - Materials: {mat_count} 6873 - Frame range: {doc_start} - {doc_end} 6874 """ 6875 return {"description": description} 6876 except Exception as e: 6877 import traceback 6878 return {"error": f"Scene introspection failed: {str(e)}", "traceback": traceback.format_exc()} 6879 6880 return self.execute_on_main_thread(describe_task, _timeout=30) 6881 6882 def handle_run_dreamtalk(self, command): 6883 """ 6884 Canonical DreamTalk script execution. 6885 6886 Clears the scene and runs the specified DreamTalk .py file as __main__. 6887 This triggers the `if __name__ == "__main__"` block which contains 6888 the canonical standalone scene for that DreamNode. 6889 6890 Args (in command): 6891 path: Absolute path to the DreamTalk .py file 6892 clear_scene: Whether to clear scene before running (default: True) 6893 """ 6894 path = command.get("path") 6895 if not path: 6896 return {"error": "path is required"} 6897 6898 clear_scene = command.get("clear_scene", True) 6899 6900 def run_task(): 6901 import runpy 6902 import os 6903 import sys 6904 from io import StringIO 6905 6906 if not os.path.exists(path): 6907 return {"error": f"File not found: {path}"} 6908 6909 doc = c4d.documents.GetActiveDocument() 6910 if not doc: 6911 return {"error": "No active document"} 6912 6913 # Capture stdout to feed into console log 6914 original_stdout = sys.stdout 6915 captured = StringIO() 6916 6917 try: 6918 # Clear scene if requested 6919 if clear_scene: 6920 while doc.GetFirstObject(): 6921 doc.GetFirstObject().Remove() 6922 while doc.GetFirstMaterial(): 6923 doc.GetFirstMaterial().Remove() 6924 c4d.EventAdd() 6925 self.log("[C4D] Scene cleared") 6926 6927 # Redirect stdout to capture print statements 6928 sys.stdout = captured 6929 6930 # Add the script's parent directory to sys.path so local 6931 # submodules (MindVirus/, DreamTalk/) are found before standalone copies 6932 script_dir = os.path.dirname(os.path.abspath(path)) 6933 path_inserted = False 6934 if script_dir not in sys.path: 6935 sys.path.insert(0, script_dir) 6936 path_inserted = True 6937 6938 # Execute the DreamTalk script as __main__ 6939 self.log(f"[C4D] Executing DreamTalk: {path}") 6940 try: 6941 runpy.run_path(path, run_name='__main__') 6942 finally: 6943 # Restore sys.path 6944 if path_inserted and script_dir in sys.path: 6945 sys.path.remove(script_dir) 6946 6947 c4d.EventAdd() 6948 self.log(f"[C4D] DreamTalk execution completed: {path}") 6949 6950 # Get captured output and feed to console log 6951 sys.stdout = original_stdout 6952 output = captured.getvalue() 6953 if output: 6954 try: 6955 from DreamTalk.introspection.hierarchy import add_console_message 6956 for line in output.strip().split('\n'): 6957 if line.strip(): 6958 add_console_message(line.strip()) 6959 except ImportError: 6960 pass # Console tracking not available 6961 6962 # Get fresh document reference (script may have changed it) 6963 doc = c4d.documents.GetActiveDocument() 6964 6965 # Count resulting objects 6966 obj_count = 0 6967 def count_objects(obj): 6968 nonlocal obj_count 6969 while obj: 6970 obj_count += 1 6971 count_objects(obj.GetDown()) 6972 obj = obj.GetNext() 6973 if doc: 6974 count_objects(doc.GetFirstObject()) 6975 6976 # Auto-snapshot for change detection on next describe_scene 6977 try: 6978 from DreamTalk.introspection.hierarchy import get_scene_snapshot 6979 import DreamTalk.introspection.hierarchy as _hier 6980 _hier._last_snapshot = get_scene_snapshot(doc) 6981 self.log("[C4D] Auto-snapshot captured after run_dreamtalk") 6982 except Exception as snap_err: 6983 self.log(f"[C4D] Auto-snapshot failed: {snap_err}") 6984 6985 return { 6986 "success": True, 6987 "path": path, 6988 "objects_created": obj_count, 6989 "message": f"Executed {os.path.basename(path)}, created {obj_count} object(s)", 6990 "output": output if output else None 6991 } 6992 6993 except Exception as e: 6994 import traceback 6995 # Restore stdout before logging 6996 sys.stdout = original_stdout 6997 6998 # Capture any output before the error and feed to console log 6999 output = captured.getvalue() 7000 if output: 7001 try: 7002 from DreamTalk.introspection.hierarchy import add_console_message 7003 for line in output.strip().split('\n'): 7004 if line.strip(): 7005 add_console_message(line.strip()) 7006 # Also add the error 7007 add_console_message(f"ERROR: {str(e)}") 7008 except ImportError: 7009 pass 7010 7011 self.log(f"[**ERROR**] DreamTalk execution failed: {str(e)}") 7012 return { 7013 "error": f"DreamTalk execution failed: {str(e)}", 7014 "traceback": traceback.format_exc(), 7015 "output": output if output else None 7016 } 7017 finally: 7018 sys.stdout = original_stdout 7019 captured.close() 7020 7021 return self.execute_on_main_thread(run_task, _timeout=60) 7022 7023 7024 class SocketServerDialog(gui.GeDialog): 7025 """GUI Dialog to control the server and display logs.""" 7026 7027 def __init__(self): 7028 super(SocketServerDialog, self).__init__() 7029 self.server = None 7030 self.msg_queue = queue.Queue() # Thread-safe queue 7031 self.SetTimer(100) # Update UI at 10 Hz 7032 7033 def CreateLayout(self): 7034 self.SetTitle("Socket Server Control") 7035 7036 self.status_text = self.AddStaticText( 7037 1002, c4d.BFH_SCALEFIT, name="Server: Offline" 7038 ) 7039 7040 self.GroupBegin(1010, c4d.BFH_SCALEFIT, 2, 1) 7041 self.AddButton(1011, c4d.BFH_SCALE, name="Start Server") 7042 self.AddButton(1012, c4d.BFH_SCALE, name="Stop Server") 7043 self.GroupEnd() 7044 7045 self.log_box = self.AddMultiLineEditText( 7046 1004, 7047 c4d.BFH_SCALEFIT, 7048 initw=400, 7049 inith=250, 7050 style=c4d.DR_MULTILINE_READONLY, 7051 ) 7052 7053 self.Enable(1012, False) # Disable "Stop" button initially 7054 return True 7055 7056 def CoreMessage(self, id, msg): 7057 """Handles UI updates and main thread execution triggered by SpecialEventAdd().""" 7058 if id == PLUGIN_ID: 7059 try: 7060 # Process all pending messages in the queue 7061 while not self.msg_queue.empty(): 7062 try: 7063 # Get next message from queue with timeout to avoid potential deadlocks 7064 msg_type, msg_value = self.msg_queue.get(timeout=0.1) 7065 7066 # Process based on message type 7067 if msg_type == "STATUS": 7068 self.UpdateStatusText(msg_value) 7069 elif msg_type == "LOG": 7070 self.AppendLog(msg_value) 7071 elif msg_type == "EXEC": 7072 # Execute function on main thread 7073 if callable(msg_value): 7074 try: 7075 msg_value() 7076 except Exception as e: 7077 error_msg = f"[**ERROR**] Error in main thread execution: {str(e)}" 7078 self.AppendLog(error_msg) 7079 print( 7080 error_msg 7081 ) # Also print to console for debugging 7082 else: 7083 self.AppendLog( 7084 f"[C4D] ## Warning ##: Non-callable value received: {type(msg_value)}" 7085 ) 7086 else: 7087 self.AppendLog( 7088 f"[C4D] ## Warning ##: Unknown message type: {msg_type}" 7089 ) 7090 except queue.Empty: 7091 # Queue timeout - break the loop to prevent blocking 7092 break 7093 except Exception as e: 7094 # Handle any other exceptions during message processing 7095 error_msg = f"[**ERROR**] Error processing message: {str(e)}" 7096 self.AppendLog(error_msg) 7097 print(error_msg) # Also print to console for debugging 7098 except Exception as e: 7099 # Catch all exceptions to prevent Cinema 4D from crashing 7100 error_msg = f"[C4D] Critical error in message processing: {str(e)}" 7101 print(error_msg) # Print to console as UI might be unstable 7102 try: 7103 self.AppendLog(error_msg) 7104 except: 7105 pass # Ignore if we can't even log to UI 7106 7107 return True 7108 7109 def Timer(self, msg): 7110 """Periodic UI update in case SpecialEventAdd() missed something.""" 7111 if self.server: 7112 if not self.server.running: # Detect unexpected crashes 7113 self.UpdateStatusText("Offline") 7114 self.Enable(1011, True) 7115 self.Enable(1012, False) 7116 return True 7117 7118 def UpdateStatusText(self, status): 7119 """Update server status UI.""" 7120 self.SetString(1002, f"Server: {status}") 7121 self.Enable(1011, status == "Offline") 7122 self.Enable(1012, status == "Online") 7123 7124 def AppendLog(self, message): 7125 """Append log messages to UI.""" 7126 existing_text = self.GetString(1004) 7127 new_text = (existing_text + "\n" + message).strip() 7128 self.SetString(1004, new_text) 7129 7130 def Command(self, id, msg): 7131 if id == 1011: # Start Server button 7132 self.StartServer() 7133 return True 7134 elif id == 1012: # Stop Server button 7135 self.StopServer() 7136 return True 7137 return False 7138 7139 def StartServer(self): 7140 """Start the socket server thread.""" 7141 if not self.server: 7142 self.server = C4DSocketServer(msg_queue=self.msg_queue) 7143 self.server.start() 7144 self.Enable(1011, False) 7145 self.Enable(1012, True) 7146 7147 def StopServer(self): 7148 """Stop the socket server.""" 7149 if self.server: 7150 self.server.stop() 7151 self.server = None 7152 self.Enable(1011, True) 7153 self.Enable(1012, False) 7154 7155 7156 class SocketServerPlugin(c4d.plugins.CommandData): 7157 """Cinema 4D Plugin Wrapper""" 7158 7159 PLUGIN_ID = 1057843 7160 PLUGIN_NAME = "Socket Server Plugin" 7161 7162 def __init__(self): 7163 self.dialog = None 7164 7165 def Execute(self, doc): 7166 if self.dialog is None: 7167 self.dialog = SocketServerDialog() 7168 return self.dialog.Open( 7169 dlgtype=c4d.DLG_TYPE_ASYNC, 7170 pluginid=self.PLUGIN_ID, 7171 defaultw=400, 7172 defaulth=300, 7173 ) 7174 7175 def GetState(self, doc): 7176 return c4d.CMD_ENABLED 7177 7178 7179 if __name__ == "__main__": 7180 c4d.plugins.RegisterCommandPlugin( 7181 SocketServerPlugin.PLUGIN_ID, 7182 SocketServerPlugin.PLUGIN_NAME, 7183 0, 7184 None, 7185 None, 7186 SocketServerPlugin(), 7187 )