/ mcp-servers / cinema4d-mcp / c4d_plugin / mcp_server_plugin.pyp
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      )