/ legacy / abstract_objects_legacy.py
abstract_objects_legacy.py
   1  import importlib
   2  import DreamTalk.materials
   3  importlib.reload(DreamTalk.materials)
   4  from DreamTalk.materials import FillMaterial, SketchMaterial
   5  from DreamTalk.tags import FillTag, SketchTag, XPressoTag, AlignToSplineTag
   6  from DreamTalk.objects.stroke_objects import StrokeGen, StrokeMaterial, SilhouetteSplineGen, MeshStroke
   7  from DreamTalk.constants import WHITE, SCALE_X, SCALE_Y, SCALE_Z
   8  from DreamTalk.animation.animation import VectorAnimation, ScalarAnimation, ColorAnimation
   9  from DreamTalk.xpresso.userdata import *
  10  from DreamTalk.xpresso.xpressions import XRelation, XIdentity, XSplineLength, XBoundingBox, XAction, Movement
  11  # Lazy imports to avoid circular dependencies - these modules import from this file
  12  # Import them inside methods that need them instead of at module level
  13  # import DreamTalk.objects.effect_objects as effect_objects
  14  # import DreamTalk.objects.helper_objects as helper_objects
  15  # import DreamTalk.objects.custom_objects as custom_objects
  16  from abc import ABC, abstractmethod
  17  import c4d.utils
  18  import c4d
  19  
  20  
  21  def _get_effect_objects():
  22      """Lazy import to avoid circular dependency"""
  23      import DreamTalk.objects.effect_objects as effect_objects
  24      return effect_objects
  25  
  26  
  27  def _get_helper_objects():
  28      """Lazy import to avoid circular dependency"""
  29      import DreamTalk.objects.helper_objects as helper_objects
  30      return helper_objects
  31  
  32  
  33  def _get_custom_objects():
  34      """Lazy import to avoid circular dependency"""
  35      import DreamTalk.objects.custom_objects as custom_objects
  36      return custom_objects
  37  
  38  
  39  class ProtoObject(ABC):
  40  
  41      def __init__(self, name=None, x=0, y=0, z=0, h=0, p=0, b=0, scale=1, position=None, rotation=None, plane="xy"):
  42          self.document = c4d.documents.GetActiveDocument()  # get document
  43          self.specify_object()
  44          self.set_xpresso_tags()
  45          self.set_unique_desc_ids()
  46          self.insert_to_document()
  47          self.set_name(name=name)
  48          self.set_position(x=x, y=y, z=z, position=position)
  49          self.set_rotation(h=h, p=p, b=b, rotation=rotation)
  50          self.set_scale(scale=scale)
  51          self.set_object_properties()
  52          self.plane = plane
  53          self.set_plane()
  54          self.relations = []
  55          self.actions = []
  56          self.xpressions = {}  # keeps track of animators, composers etc.
  57          self.accessed_parameters = {}  # keeps track which parameters have AccessControl
  58          self.helper_objects = {}  # keeps track of helper objects created by Animators
  59          self.parent = None
  60  
  61      def __repr__(self):
  62          """sets the string representation for printing"""
  63          return self.name
  64  
  65      @abstractmethod
  66      def specify_object(self):
  67          pass
  68  
  69      def set_xpresso_tags(self):
  70          """initializes the necessary xpresso tags on the object"""
  71          # the composition tags hold the hierarchy of compositions and ensure execution from highest to lowest
  72          #self.composition_tags = []
  73          # the animator tag holds the acting of the animators on the actual parameters
  74          # set priority to be executed last
  75          # self.animator_tag = XPressoTag(
  76          #    target=self, name="AnimatorTag", priority=1, priority_mode="expression")
  77          # the freeze tag holds the freezing xpressions that are executed before the animators
  78          # set priority to be executed after compositions and before animators
  79          # self.freeze_tag = XPressoTag(
  80          #    target=self, name="FreezeTag", priority=0, priority_mode="animation")
  81          # inserts an xpresso tag used for custom xpressions
  82          self.custom_tag = XPressoTag(
  83              target=self, name="CustomTag", priority_mode="expression")
  84  
  85      def add_composition_tag(self):
  86          """adds another layer to the composition hierarchy"""
  87          # set priority according to position in composition hierarchy
  88          tag_name = "CompositionTag" + str(len(self.composition_tags))
  89          tag_priority = -len(self.composition_tags)
  90          composition_tag = XPressoTag(
  91              target=self, name=tag_name, priority=tag_priority, priority_mode="initial")
  92          self.composition_tags.append(composition_tag)
  93          return composition_tag.obj
  94  
  95      def set_unique_desc_ids(self):
  96          """optional method to make unique descIds easily accessible"""
  97          pass
  98  
  99      def set_name(self, name=None):
 100          if name is None:
 101              self.name = self.__class__.__name__
 102          else:
 103              self.name = name
 104          self.obj.SetName(self.name)
 105  
 106      def set_plane(self):
 107          """sets the plane of the custom object"""
 108          if self.plane == "xy":
 109              self.rotate(rotation=(0, 0, 0))
 110          elif self.plane == "yz":
 111              self.rotate(rotation=(PI / 2, 0, 0))
 112          elif self.plane == "xz":
 113              self.rotate(rotation=(0, -PI / 2, 0))
 114  
 115      def specify_parameters(self):
 116          """specifies optional parameters for the custom object"""
 117          pass
 118  
 119      def insert_parameters(self):
 120          """inserts the specified parameters as userdata"""
 121          if self.parameters:
 122              self.parameters_u_group = UGroup(
 123                  *self.parameters, target=self.obj, name=self.name + "Parameters")
 124  
 125      def specify_relations(self):
 126          """specifies the relations between the part's parameters using xpresso"""
 127          pass
 128  
 129      def set_position(self, x=0, y=0, z=0, position=None, relative=False):
 130          if position is None:
 131              position = c4d.Vector(x, y, z)
 132          elif type(position) is not c4d.Vector:
 133              position = c4d.Vector(*position)
 134          if relative:
 135              self.obj[c4d.ID_BASEOBJECT_POSITION] += position
 136          else:
 137              self.obj[c4d.ID_BASEOBJECT_POSITION] = position
 138  
 139      def set_rotation(self, h=0, p=0, b=0, rotation=None, relative=False):
 140          if rotation is None:
 141              rotation = c4d.Vector(h, p, b)
 142          elif type(rotation) is not c4d.Vector:
 143              rotation = c4d.Vector(*rotation)
 144          if relative:
 145              self.obj[c4d.ID_BASEOBJECT_ROTATION] += rotation
 146          else:
 147              self.obj[c4d.ID_BASEOBJECT_ROTATION] = rotation
 148  
 149      def set_frozen_rotation(self, h=0, p=0, b=0, rotation=None, relative=False):
 150          if rotation is None:
 151              rotation = c4d.Vector(h, p, b)
 152          if relative:
 153              self.obj[c4d.ID_BASEOBJECT_FROZEN_ROTATION] += rotation
 154          else:
 155              self.obj[c4d.ID_BASEOBJECT_FROZEN_ROTATION] = rotation
 156  
 157      def set_scale(self, scale=1, relative=False):
 158          if relative:
 159              self.obj[c4d.ID_BASEOBJECT_SCALE] *= scale
 160          else:
 161              scale = c4d.Vector(scale, scale, scale)
 162              self.obj[c4d.ID_BASEOBJECT_SCALE] = scale
 163  
 164      def move(self, x=0, y=0, z=0, position=None, relative=True):
 165          if position is None:
 166              position = c4d.Vector(x, y, z)
 167          elif type(position) is not c4d.Vector:
 168              position = c4d.Vector(*position)
 169          descriptor = c4d.ID_BASEOBJECT_POSITION
 170          animation = VectorAnimation(
 171              target=self, descriptor=descriptor, vector=position, relative=relative)
 172          if relative:
 173              self.obj[descriptor] += position
 174          else:
 175              self.obj[descriptor] = position
 176          return animation
 177  
 178      def rotate(self, h=0, p=0, b=0, rotation=None):
 179          if rotation is None:
 180              rotation = c4d.Vector(h, p, b)
 181          elif type(rotation) is not c4d.Vector:
 182              rotation = c4d.Vector(*rotation)
 183          descriptor = c4d.ID_BASEOBJECT_ROTATION
 184          animation = VectorAnimation(
 185              target=self, descriptor=descriptor, vector=rotation, relative=True)
 186          self.obj[descriptor] += rotation
 187          return animation
 188  
 189      def scale(self, x=0, y=0, z=0, scale=None):
 190          if scale is None:
 191              scale = c4d.Vector(x, y, z)
 192          elif type(scale) in (tuple, list):
 193              scale = c4d.Vector(*scale)
 194          elif type(scale) in (int, float):
 195              scale = c4d.Vector(scale, scale, scale)
 196          descriptor = c4d.ID_BASEOBJECT_SCALE
 197          animation = VectorAnimation(
 198              target=self, descriptor=descriptor, vector=scale, relative=True, multiplicative=True)
 199          self.obj[descriptor] += scale
 200          return animation
 201  
 202      def insert_to_document(self):
 203          self.document.InsertObject(self.obj)
 204  
 205      def get_segment_count(self):
 206          # returns the length of the spline or a specific segment
 207          spline_help = c4d.utils.SplineHelp()
 208          spline_help.InitSplineWith(self.obj)
 209          segment_count = spline_help.GetSegmentCount()
 210          return segment_count
 211  
 212      def get_length(self, segment=None):
 213          # returns the length of the spline or a specific segment
 214          spline_help = c4d.utils.SplineHelp()
 215          spline_help.InitSplineWith(self.obj)
 216  
 217          if segment:
 218              segment_length = spline_help.GetSegmentLength(segment)
 219              return segment_length
 220          else:
 221              spline_length = spline_help.GetSplineLength()
 222              return spline_length
 223  
 224      def get_spline_segment_lengths(self):
 225          # get the length of each segment
 226          segment_lengths = []
 227          for i in range(self.get_segment_count()):
 228              segment_lengths.append(self.get_length(segment=i))
 229          return segment_lengths
 230  
 231      def set_object_properties(self):
 232          """used to set the unique properties of a specific object"""
 233          pass
 234  
 235      def sort_relations_by_priority(self):
 236          """sorts the relations by priority"""
 237  
 238          # right now it oly ensures that the actions are inserted above the relations
 239          # in the future we will implement priority and sorting by sub-xpression dependencies
 240  
 241          # get node master
 242          master = self.custom_tag.obj.GetNodeMaster()
 243          parent = master.GetRoot()
 244  
 245          """
 246          # resort by parent reference
 247          for relation in self.relations:
 248              if relation.parent:
 249                  self.relations.remove(relation)
 250                  parent_idx = self.relations.index(relation.parent)
 251                  self.relations.insert(parent_idx, relation)
 252          for action in self.actions:
 253              if action.parent:
 254                  self.actions.remove(action)
 255                  parent_idx = self.actions.index(action.parent)
 256                  self.actions.insert(parent_idx, action)
 257          """
 258          if self.relations:
 259              for relation in self.relations:
 260                  master.InsertFirst(parent, relation.obj)
 261          if self.actions:
 262              for action in self.actions:
 263                  master.InsertFirst(parent, action.obj)
 264  
 265  
 266  class VisibleObject(ProtoObject):
 267      # visible objects
 268  
 269      def __init__(self, visible=True, creation=0, **kwargs):
 270          super().__init__(**kwargs)
 271          self.creation = creation
 272          self.visible = visible
 273          self.specify_visibility_parameter()
 274          self.insert_visibility_parameter()
 275          self.specify_visibility_relation()
 276          self.specify_live_bounding_box_parameters()
 277          self.insert_live_bounding_box_parameters()
 278          self.specify_live_bounding_box_relation()
 279          self.add_bounding_box_information()
 280  
 281      def specify_action_parameters(self):
 282          pass
 283  
 284      def specify_creation_parameter(self):
 285          self.creation_parameter = UCompletion(
 286              name="Creation", default_value=self.creation)
 287          self.action_parameters += [self.creation_parameter]
 288  
 289      def insert_action_parameters(self):
 290          """inserts the specified action_parameters as userdata"""
 291          if self.action_parameters:
 292              self.actions_u_group = UGroup(
 293                  *self.action_parameters, target=self.obj, name=self.name + "Actions")
 294  
 295      def specify_actions(self):
 296          """specifies actions that coordinate parameters"""
 297          pass
 298  
 299      def specify_creation(self):
 300          """specifies the creation action"""
 301          pass
 302  
 303      def get_clone(self):
 304          """clones an object and inserts it into the scene"""
 305          clone = self.obj.GetClone()
 306          self.document.InsertObject(clone)
 307          return clone
 308  
 309      def get_editable(self):
 310          """returns an editable clone of the object"""
 311          clone = self.get_clone()
 312          editable_clone = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_MAKEEDITABLE, list=[
 313              clone], mode=c4d.MODELINGCOMMANDMODE_ALL, doc=self.document)[0]
 314          return editable_clone
 315  
 316      def attach_to(self, target, direction="front", offset=0):
 317          """places the object such that the bounding boxes touch along a given direction and makes object child of target"""
 318          bounding_box = self.obj.GetRad()
 319          bounding_box_position = self.obj.GetMp()
 320          bounding_box_target = target.obj.GetRad()
 321          bounding_box_position_target = target.obj.GetMp()
 322          new_position = bounding_box_position_target - bounding_box_position
 323          if direction == "top":
 324              new_position.y += bounding_box_target.y + bounding_box.y + offset
 325          if direction == "bottom":
 326              new_position.y -= bounding_box_target.y + bounding_box.y + offset
 327          if direction == "left":
 328              new_position.x -= bounding_box_target.x + bounding_box.x + offset
 329          if direction == "right":
 330              new_position.x += bounding_box_target.x + bounding_box.x + offset
 331          if direction == "front":
 332              new_position.z -= bounding_box_target.z + bounding_box.z + offset
 333          if direction == "back":
 334              new_position.z += bounding_box_target.z + bounding_box.z + offset
 335          if direction == "center":
 336              new_position = bounding_box_position_target - bounding_box_position
 337          self.obj.InsertUnder(target.obj)
 338          self.set_position(position=new_position)
 339  
 340      def specify_visibility_parameter(self):
 341          """specifies visibility parameter"""
 342          self.visibility_parameter = UCheckBox(
 343              name="Visibility", default_value=self.visible)
 344  
 345      def insert_visibility_parameter(self):
 346          """inserts the visibility parameter as userdata"""
 347          self.visibility_u_group = UGroup(
 348              self.visibility_parameter, target=self.obj, name="Visibility")
 349  
 350      def specify_visibility_relation(self):
 351          """link parameter to visibility"""
 352          visibility_relation = XRelation(part=self, whole=self, desc_ids=[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR, c4d.ID_BASEOBJECT_VISIBILITY_RENDER],
 353                                          parameters=[self.visibility_parameter], formula=f"1-{self.visibility_parameter.name}")
 354  
 355      def specify_live_bounding_box_parameters(self):
 356          """specifies bounding box parameters"""
 357          self.width_parameter = ULength(name="Width")
 358          self.height_parameter = ULength(name="Height")
 359          self.depth_parameter = ULength(name="Depth")
 360          self.center_parameter = UVector(name="Center")
 361          self.center_x_parameter = ULength(name="CenterX")
 362          self.center_y_parameter = ULength(name="CenterY")
 363          self.center_z_parameter = ULength(name="CenterZ")
 364          self.live_bounding_box_parameters = [self.width_parameter,
 365                                               self.height_parameter,
 366                                               self.depth_parameter,
 367                                               self.center_parameter,
 368                                               self.center_x_parameter,
 369                                               self.center_y_parameter,
 370                                               self.center_z_parameter]
 371  
 372      def insert_live_bounding_box_parameters(self):
 373          """inserts the bounding box parameters as userdata"""
 374          self.live_bounding_box_u_group = UGroup(
 375              *self.live_bounding_box_parameters, target=self.obj, name="LiveBoundingBox")
 376  
 377      def specify_live_bounding_box_relation(self):
 378          """feed bounding box information into parameters"""
 379          live_bounding_box_relation = XBoundingBox(self, target=self, width_parameter=self.width_parameter, height_parameter=self.height_parameter,
 380                                                    depth_parameter=self.depth_parameter, center_parameter=self.center_parameter,
 381                                                    center_x_parameter=self.center_x_parameter, center_y_parameter=self.center_y_parameter, center_z_parameter=self.center_z_parameter)
 382  
 383      def add_bounding_box_information(self):
 384          bounding_box_center, bounding_radius = c4d.utils.GetBBox(
 385              self.obj, self.obj.GetMg())
 386          self.width = bounding_radius.x * 2
 387          self.height = bounding_radius.y * 2
 388          self.depth = bounding_radius.z * 2
 389          self.center = bounding_box_center
 390  
 391      def get_center(self):
 392          # returns the center position from the live bounding box information
 393          center_position = self.obj.GetMp() * self.obj.GetMg()
 394          return center_position
 395  
 396      def get_radius(self):
 397          # returns the radius from the live bounding box information
 398          __, radius = c4d.utils.GetBBox(self.obj, self.obj.GetMg())
 399          return radius
 400  
 401      def get_diameter(self):
 402          # returns the diameter from the longest radius dimension
 403          radius = self.get_radius()
 404          diameter = max(radius.x, radius.y, radius.z) * 2
 405          return diameter
 406  
 407      def register_connections(self, connections):
 408          # saves the connections for later functionality of UnConnect
 409          self.connections = connections
 410  
 411      def create(self, completion=1):
 412          """specifies the creation animation"""
 413          desc_id = self.creation_parameter.desc_id
 414          animation = ScalarAnimation(
 415              target=self, descriptor=desc_id, value_fin=completion)
 416          self.obj[desc_id] = completion
 417          return animation
 418  
 419      def un_create(self, completion=0):
 420          """specifies the uncreation animation"""
 421          desc_id = self.creation_parameter.desc_id
 422          animation = ScalarAnimation(
 423              target=self, descriptor=desc_id, value_fin=completion)
 424          self.obj[desc_id] = completion
 425          return animation
 426  
 427      def align_to_spline(self, spline=None):
 428          self.align_to_spline_tag = AlignToSplineTag(target=self, spline=spline)
 429  
 430      def wrap_around(self, target=None):
 431          helper_objects = _get_helper_objects()
 432          self.shrink_wrap = helper_objects.ShrinkWrap(target=target)
 433          self.shrink_wrap.obj.InsertUnder(self.obj)
 434  
 435      def project_to_surface(self, projection_surface=None, **kwargs):
 436          helper_objects = _get_helper_objects()
 437          self.projection = helper_objects.Projection(target=self, projection_surface=projection_surface, **kwargs)
 438  
 439      def move_axis(self, position=(0, 0, 0)):
 440          """moves the axis without moving the geometry"""
 441          vec = c4d.Vector(*position)
 442          print(vec)
 443          print(position)
 444          points = self.obj.GetAllPoints()
 445          for i, point in enumerate(points):
 446              points[i] = point - vec
 447              if i < 3:
 448                  print(points[i] - point)
 449          self.obj.SetAbsPos(self.obj.GetAbsPos() + vec)
 450          self.obj.SetAllPoints(points)
 451          self.obj.Message(c4d.MSG_UPDATE)
 452          c4d.EventAdd()
 453  
 454  
 455  class CustomObject(VisibleObject):
 456      """this class is used to create custom objects that are basically
 457      groups with coupling of the childrens parameters through xpresso
 458  
 459      Supports two modes:
 460      - Standard mode (default): Uses XPresso for parameter relationships
 461      - Generator mode (generator_mode=True): Uses Python Generator for MoGraph compatibility
 462      """
 463  
 464      def __init__(self, diameter=None, generator_mode=False, **kwargs):
 465          self.generator_mode = generator_mode
 466          super().__init__(**kwargs)
 467          self.parts = []
 468          self.specify_parts()
 469          self.insert_parts()
 470          self.parameters = []
 471          self.specify_parameters()
 472          self.insert_parameters()
 473          self.relations = []  # Initialize relations list before specify_relations
 474          self.specify_relations()
 475  
 476          # Skip XPresso setup in generator mode
 477          if not self.generator_mode:
 478              self.action_parameters = []
 479              self.specify_action_parameters()
 480              self.specify_creation_parameter()
 481              self.insert_action_parameters()
 482              self.specify_actions()
 483              self.specify_creation()
 484              self.diameter = diameter
 485              self.add_bounding_box_information()
 486              self.specify_bounding_box_parameters()
 487              self.insert_bounding_box_parameters()
 488              self.specify_bounding_box_relations()
 489              self.specify_visibility_inheritance_relations()
 490              self.specify_position_inheritance()
 491              self.sort_relations_by_priority()
 492          else:
 493              # Generator mode: still need creation_parameter for keyframe animations
 494              self.action_parameters = []
 495              self.specify_creation_parameter()
 496              self.diameter = diameter
 497              self.insert_action_parameters()
 498              # Note: Subclasses with custom _convert_to_generator (like FoldableCube)
 499              # will override this. Otherwise, use the generic GeneratorMixin conversion.
 500              # The conversion happens AFTER all parameters are set up.
 501              self._convert_to_generator()
 502  
 503      def _convert_to_generator(self):
 504          """Convert this CustomObject to a Python Generator for MoGraph compatibility.
 505  
 506          This replaces self.obj (a Null) with a Python Generator that:
 507          - Has the same UserData parameters
 508          - Contains the same children (recursively converted if they have GeneratorMixin)
 509          - Uses Python code instead of XPresso for relationships
 510          """
 511          if not hasattr(self, 'create_as_generator'):
 512              # No GeneratorMixin - can't convert
 513              return
 514  
 515          # Store old obj reference
 516          old_obj = self.obj
 517          old_parent = old_obj.GetUp()
 518          old_pred = old_obj.GetPred()
 519  
 520          # Create the generator version
 521          gen = self.create_as_generator(recursive=True)
 522  
 523          # Replace old_obj with gen in the document
 524          if old_parent:
 525              if old_pred:
 526                  gen.InsertAfter(old_pred)
 527              else:
 528                  gen.InsertUnder(old_parent)
 529          else:
 530              # Was at root level
 531              doc = c4d.documents.GetActiveDocument()
 532              doc.InsertObject(gen)
 533  
 534          old_obj.Remove()
 535  
 536          # Update self.obj to point to the generator
 537          self.obj = gen
 538  
 539      def specify_creation(self):
 540          """used to specify the unique creation animation for each individual custom object"""
 541          pass
 542  
 543      def inherit_creation(self):
 544          """inheriting creation from parts"""
 545          movements = [Movement(part.creation_parameter, (0, 1), part=part, easing=False)
 546                       for part in self.parts]
 547          self.creation_action = XAction(*movements,
 548                                         target=self, completion_parameter=self.creation_parameter, name="Creation")
 549  
 550      def specify_position_inheritance(self):
 551          """used to specify how the position should be determined"""
 552          pass
 553  
 554      @abstractmethod
 555      def specify_parts(self):
 556          """save parts as attributes and write them to self.parts"""
 557          pass
 558  
 559      def insert_parts(self):
 560          """inserts the parts as children"""
 561          for part in self.parts:
 562              # check if part is not already child so existing hierarchies won't be disturbed
 563              if not part.obj.GetUp():
 564                  part.obj.InsertUnder(self.obj)
 565                  part.parent = self
 566              # check for membranes
 567              if hasattr(part, "membrane"):
 568                  part.membrane.obj.InsertUnder(self.obj)
 569                  part.membrane.parent = self
 570  
 571      def specify_object(self):
 572          self.obj = c4d.BaseObject(c4d.Onull)
 573  
 574      def specify_visibility_inheritance_relations(self):
 575          """inherits visibility to parts"""
 576          effect_objects = _get_effect_objects()
 577          visibility_relations = []
 578          for part in self.parts:
 579              if hasattr(part, "visibility_parameter") and not isinstance(part, effect_objects.Morpher):
 580                  visibility_relation = XIdentity(
 581                      part=part, whole=self, desc_ids=[part.visibility_parameter.desc_id], parameter=self.visibility_parameter, name="VisibilityInheritance")
 582                  visibility_relations.append(visibility_relation)
 583  
 584      def specify_bounding_box_parameters(self):
 585          """specifies bounding box parameters"""
 586          default_diameter = self.diameter if self.diameter else max(
 587              self.width, self.height, self.depth)
 588          self.diameter_parameter = ULength(
 589              name="Diameter", default_value=default_diameter)
 590          self.default_width_parameter = ULength(
 591              name="DefaultWidth", default_value=self.width)
 592          self.default_height_parameter = ULength(
 593              name="DefaultHeight", default_value=self.height)
 594          self.default_depth_parameter = ULength(
 595              name="DefaultDepth", default_value=self.depth)
 596          self.bounding_box_parameters = [self.diameter_parameter, self.default_width_parameter,
 597                                          self.default_height_parameter, self.default_depth_parameter]
 598  
 599      def insert_bounding_box_parameters(self):
 600          """inserts the bounding box parameters"""
 601          self.bounding_box_u_group = UGroup(
 602              *self.bounding_box_parameters, target=self.obj, name="BoundingBox")
 603  
 604      def specify_bounding_box_relations(self):
 605          """gives the custom object basic control over the bounding box diameter"""
 606          diameter_relation = XRelation(part=self, whole=self, desc_ids=[SCALE_X, SCALE_Y, SCALE_Z], parameters=[self.diameter_parameter, self.default_width_parameter, self.default_height_parameter, self.default_depth_parameter],
 607                                        formula=f"{self.diameter_parameter.name}/max({self.default_width_parameter.name};max({self.default_height_parameter.name};{self.default_depth_parameter.name}))")
 608  
 609  
 610  class LineObject(VisibleObject):
 611      """line objects consist of splines and only require a sketch material
 612  
 613      Supports two rendering modes:
 614      - Sketch mode (default): Uses Sketch & Toon post-effect
 615      - Stroke mode (stroke_mode=True): Uses geometry-based strokes (MoGraph compatible, faster)
 616      """
 617  
 618      def __init__(self, color=WHITE, plane="xy", arrow_start=False, arrow_end=False, draw_completion=0, opacity=1, helper_mode=False, draw_order="long_to_short", filled=False, fill_color=None, stroke_width=None, stroke_mode=False, **kwargs):
 619          self.stroke_width = stroke_width if stroke_width is not None else 3.0
 620          self.stroke_mode = stroke_mode
 621          super().__init__(**kwargs)
 622          self.color = color
 623          self.plane = plane
 624          self.arrow_start = arrow_start
 625          self.arrow_end = arrow_end
 626          self.draw_completion = draw_completion
 627          self.opacity = opacity
 628          self.draw_order = draw_order
 629          if not helper_mode:
 630              if self.stroke_mode:
 631                  # Geometry-based stroke rendering
 632                  self._setup_stroke_mode()
 633                  self.sketch_parameter_setup()  # Sets up Draw, Opacity, Color parameters
 634              else:
 635                  # Traditional Sketch & Toon rendering
 636                  self.set_sketch_material()
 637                  self.set_sketch_tag()
 638                  self.sketch_parameter_setup()
 639              self.set_plane()
 640              self.spline_length_parameter_setup()
 641              self.parameters = []
 642              self.specify_parameters()
 643              self.insert_parameters()
 644              self.specify_relations()
 645              self.action_parameters = []
 646              self.specify_action_parameters()
 647              self.specify_creation_parameter()
 648              self.insert_action_parameters()
 649              self.specify_actions()
 650              self.specify_creation()
 651              self.sort_relations_by_priority()
 652          self.filled = filled
 653          self.fill_color = fill_color
 654          if self.filled:
 655              self.create_membrane()
 656  
 657      def create_membrane(self):
 658          custom_objects = _get_custom_objects()
 659          if self.fill_color:
 660              color = self.fill_color
 661          else:
 662              color = self.color
 663  
 664          self.membrane = custom_objects.Membrane(
 665              self, name=self.name + "Membrane", creation=True, color=color)
 666  
 667      def _setup_stroke_mode(self):
 668          """Set up geometry-based stroke rendering using StrokeGen.
 669  
 670          In stroke mode:
 671          - self.spline holds the original spline object
 672          - self.obj becomes the StrokeGen (for position/rotation/UserData compatibility)
 673          - The spline is a child of the stroke generator
 674  
 675          This maintains the mental model that self.obj is "the thing" while
 676          enabling geometry-based stroke rendering.
 677          """
 678          from DreamTalk.objects.stroke_objects import STROKE_GEN_CODE, StrokeMaterial
 679  
 680          # Store spline reference
 681          self.spline = self.obj
 682  
 683          # Create the stroke generator
 684          self.stroke_gen = c4d.BaseObject(1023866)  # Python Generator
 685          self.stroke_gen[c4d.OPYTHON_CODE] = STROKE_GEN_CODE
 686          self.stroke_gen[c4d.OPYTHON_OPTIMIZE] = False  # Critical for MoGraph!
 687          self.stroke_gen.SetName(self.name)
 688  
 689          # Add Stroke Width UserData
 690          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
 691          bc[c4d.DESC_NAME] = "Stroke Width"
 692          bc[c4d.DESC_DEFAULT] = 3.0
 693          bc[c4d.DESC_MIN] = 0.1
 694          bc[c4d.DESC_MAX] = 100.0
 695          bc[c4d.DESC_STEP] = 0.5
 696          bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_METER
 697          self.stroke_width_id = self.stroke_gen.AddUserData(bc)
 698          self.stroke_gen[self.stroke_width_id] = self.stroke_width
 699  
 700          # Add Draw UserData (0-1 completion for animation)
 701          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
 702          bc[c4d.DESC_NAME] = "Draw"
 703          bc[c4d.DESC_DEFAULT] = 1.0
 704          bc[c4d.DESC_MIN] = 0.0
 705          bc[c4d.DESC_MAX] = 1.0
 706          bc[c4d.DESC_STEP] = 0.01
 707          bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
 708          self.stroke_draw_id = self.stroke_gen.AddUserData(bc)
 709          self.stroke_gen[self.stroke_draw_id] = self.draw_completion
 710  
 711          # Insert stroke gen into document
 712          self.document.InsertObject(self.stroke_gen)
 713  
 714          # Move spline under stroke gen
 715          self.spline.Remove()
 716          self.spline.InsertUnder(self.stroke_gen)
 717  
 718          # Create and apply stroke material
 719          self.stroke_material = StrokeMaterial(
 720              color=self.color,
 721              opacity=self.opacity,
 722              name=f"{self.name}_StrokeMat"
 723          )
 724          tag = self.stroke_gen.MakeTag(c4d.Ttexture)
 725          tag[c4d.TEXTURETAG_MATERIAL] = self.stroke_material.obj
 726  
 727          # Swap obj to be the stroke_gen - this is the "main" object now
 728          self.obj = self.stroke_gen
 729  
 730      def _stroke_sketch_parameter_setup(self):
 731          """Set up parameters for stroke mode that mirror sketch parameters.
 732  
 733          In stroke mode, Draw/Opacity/Color UserData already exists on
 734          the stroke generator (self.obj). We just need to create parameter
 735          wrappers that reference the existing desc_ids.
 736          """
 737          # Draw parameter - already on stroke_gen via _setup_stroke_mode
 738          self.draw_parameter = UCompletion(
 739              name="Draw", default_value=self.draw_completion)
 740          self.draw_parameter.desc_id = self.stroke_draw_id  # Point to existing
 741  
 742          # For Opacity and Color, we use the material directly
 743          # Create parameter objects but they'll control the material
 744          self.opacity_parameter = UCompletion(
 745              name="Opacity", default_value=self.opacity)
 746          self.color_parameter = UColor(
 747              name="Color", default_value=self.color)
 748  
 749          self.sketch_parameters = [self.draw_parameter,
 750                                    self.opacity_parameter, self.color_parameter]
 751  
 752      def spline_length_parameter_setup(self):
 753          self.specify_spline_length_parameter()
 754          self.insert_spline_length_parameter()
 755          self.specify_spline_length_relation()
 756  
 757      def sketch_parameter_setup(self):
 758          if self.stroke_mode:
 759              self._stroke_sketch_parameter_setup()
 760              self._insert_stroke_sketch_parameters()
 761              self._specify_stroke_sketch_relations()
 762          else:
 763              self.specify_sketch_parameters()
 764              self.insert_sketch_parameters()
 765              self.specify_sketch_relations()
 766  
 767      def _insert_stroke_sketch_parameters(self):
 768          """Insert additional UserData on stroke_gen for Opacity and Color.
 769  
 770          Draw is already on the generator (read by generator code).
 771          Opacity and Color are added for animation compatibility but
 772          actually control the material programmatically.
 773          """
 774          # Opacity UserData (for animation keyframing)
 775          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
 776          bc[c4d.DESC_NAME] = "Opacity"
 777          bc[c4d.DESC_DEFAULT] = 1.0
 778          bc[c4d.DESC_MIN] = 0.0
 779          bc[c4d.DESC_MAX] = 1.0
 780          bc[c4d.DESC_STEP] = 0.01
 781          bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
 782          self.stroke_opacity_id = self.obj.AddUserData(bc)
 783          self.obj[self.stroke_opacity_id] = self.opacity
 784          self.opacity_parameter.desc_id = self.stroke_opacity_id
 785  
 786          # Color UserData (for animation keyframing)
 787          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_COLOR)
 788          bc[c4d.DESC_NAME] = "Color"
 789          self.stroke_color_id = self.obj.AddUserData(bc)
 790          self.obj[self.stroke_color_id] = self.color
 791          self.color_parameter.desc_id = self.stroke_color_id
 792  
 793      def _specify_stroke_sketch_relations(self):
 794          """Link UserData to stroke material.
 795  
 796          In stroke mode:
 797          - Draw is handled by the generator code directly (reads UserData by name)
 798          - Opacity and Color are set at construction time on the material
 799  
 800          For animated opacity/color, use set_opacity() and set_color() methods
 801          or animate via Python Generator code.
 802          """
 803          # No XPresso relations needed - generator reads Draw directly
 804          # Opacity/Color are set on material at construction time
 805          pass
 806  
 807      def specify_creation(self):
 808          """specifies the creation action"""
 809          if self.stroke_mode:
 810              # In stroke mode, creation animation is simpler - just animate the Draw UserData
 811              # XAction uses XPresso which is complex; stroke_mode uses direct UserData access
 812              # The Create animator will animate the creation_parameter which maps to draw
 813              pass
 814          else:
 815              creation_action = XAction(
 816                  Movement(self.draw_parameter, (0, 1)),
 817                  target=self, completion_parameter=self.creation_parameter, name="Creation")
 818              self.actions.append(creation_action)
 819  
 820      def set_sketch_material(self):
 821          self.sketch_material = SketchMaterial(
 822              name=self.__class__.__name__, draw_order=self.draw_order, color=self.color, arrow_start=self.arrow_start, arrow_end=self.arrow_end, stroke_width=self.stroke_width)
 823  
 824      def set_sketch_tag(self):
 825          self.sketch_tag = SketchTag(target=self, material=self.sketch_material)
 826  
 827      def specify_sketch_parameters(self):
 828          self.draw_parameter = UCompletion(
 829              name="Draw", default_value=self.draw_completion)
 830          self.opacity_parameter = UCompletion(
 831              name="Opacity", default_value=self.opacity)
 832          self.color_parameter = UColor(
 833              name="Color", default_value=self.color)
 834          self.sketch_parameters = [self.draw_parameter,
 835                                    self.opacity_parameter, self.color_parameter]
 836  
 837      def insert_sketch_parameters(self):
 838          self.draw_u_group = UGroup(
 839              *self.sketch_parameters, target=self.obj, name="Sketch")
 840  
 841      def specify_sketch_relations(self):
 842          draw_relation = XIdentity(part=self.sketch_material, whole=self, desc_ids=[self.sketch_material.desc_ids["draw_completion"]],
 843                                    parameter=self.draw_parameter)
 844          opacity_relation = XIdentity(part=self.sketch_material, whole=self, desc_ids=[self.sketch_material.desc_ids["opacity"]],
 845                                       parameter=self.opacity_parameter)
 846          color_relation = XIdentity(part=self.sketch_material, whole=self, desc_ids=[self.sketch_material.desc_ids["color"]],
 847                                     parameter=self.color_parameter)
 848  
 849      def set_plane(self):
 850          planes = {"xy": 0, "zy": 1, "xz": 2}
 851          self.obj[c4d.PRIM_PLANE] = planes[self.plane]
 852  
 853      def specify_spline_length_parameter(self):
 854          self.spline_length_parameter = ULength(name="SplineLength")
 855  
 856      def insert_spline_length_parameter(self):
 857          self.spline_length_u_group = UGroup(
 858              self.spline_length_parameter, target=self.obj, name="Spline")
 859  
 860      def specify_spline_length_relation(self):
 861          self.spline_length_relation = XSplineLength(
 862              spline=self, whole=self, parameter=self.spline_length_parameter)
 863  
 864      def draw(self, completion=1):
 865          """specifies the draw animation"""
 866          desc_id = self.draw_parameter.desc_id
 867          animation = ScalarAnimation(
 868              target=self, descriptor=desc_id, value_fin=completion)
 869          self.obj[desc_id] = completion
 870          return animation
 871  
 872      def un_draw(self, completion=0):
 873          """specifies the undraw animation"""
 874          desc_id = self.draw_parameter.desc_id
 875          animation = ScalarAnimation(
 876              target=self, descriptor=desc_id, value_fin=completion)
 877          self.obj[desc_id] = completion
 878          return animation
 879  
 880      def fade_in(self, completion=1):
 881          """specifies the fade in animation"""
 882          desc_id = self.opacity_parameter.desc_id
 883          animation = ScalarAnimation(
 884              target=self, descriptor=desc_id, value_fin=completion)
 885          self.obj[desc_id] = completion
 886          return animation
 887  
 888      def fade_out(self, completion=0):
 889          """specifies the fade out animation"""
 890          desc_id = self.opacity_parameter.desc_id
 891          animation = ScalarAnimation(
 892              target=self, descriptor=desc_id, value_fin=completion)
 893          self.obj[desc_id] = completion
 894          return animation
 895  
 896      def change_color(self, color):
 897          """specifies the color change animation"""
 898          desc_id = self.color_parameter.desc_id
 899          animation = ColorAnimation(
 900              target=self, descriptor=desc_id, vector=color)
 901          self.obj[desc_id] = color
 902          return animation
 903  
 904  
 905  class SolidObject(VisibleObject):
 906      """solid objects only require a fill material
 907  
 908      Supports two rendering modes:
 909      - Sketch mode (default): Uses Sketch & Toon post-effect for silhouette lines
 910      - Stroke mode (stroke_mode=True): Uses geometry-based strokes (MoGraph compatible, faster)
 911      """
 912  
 913      def __init__(self, filled=0, glowing=0, color=WHITE, fill_color=None, sketch_color=None, draw_order="long_to_short", draw_completion=0, arrow_start=False, arrow_end=False, fill_opacity=1, sketch_opacity=1, hidden_material=True, stroke_width=None, sketch_outline=True, sketch_folds=False, sketch_creases=True, sketch_border=False, sketch_contour=True, sketch_splines=True, stroke_mode=False, **kwargs):
 914          """
 915          Base class for 3D solid objects with fill and sketch materials.
 916  
 917          Args:
 918              filled: Fill visibility (0-1)
 919              glowing: Glow intensity (0-1)
 920              color: Base color (used for both fill and sketch if not specified)
 921              fill_color: Override fill color
 922              sketch_color: Override sketch color
 923              draw_order: Sketch draw order ("long_to_short" or "short_to_long")
 924              draw_completion: Initial sketch draw completion (0-1)
 925              arrow_start: Add arrow at spline start
 926              arrow_end: Add arrow at spline end
 927              fill_opacity: Fill opacity (0-1)
 928              sketch_opacity: Sketch opacity (0-1)
 929              hidden_material: Control hidden line rendering:
 930                  - True (default): Same material for hidden lines (solid look)
 931                  - False/None: No hidden material (X-ray see-through effect)
 932                  - Material object: Custom hidden line material
 933              stroke_width: Override sketch line thickness
 934              sketch_outline: Enable outline rendering (default True)
 935              sketch_folds: Enable fold lines (default False)
 936              sketch_creases: Enable crease lines (default True)
 937              sketch_border: Enable border lines (default False)
 938              sketch_contour: Enable contour lines (default True)
 939              sketch_splines: Enable spline rendering (default True)
 940              stroke_mode: Use geometry-based strokes instead of Sketch & Toon (default False)
 941              **kwargs: Parent class arguments
 942          """
 943          self.stroke_mode = stroke_mode
 944          self.stroke_width = stroke_width if stroke_width is not None else 3.0
 945          self.filled = filled
 946          self.glowing = glowing
 947          self.color = color
 948          self.fill_color = fill_color
 949          self.sketch_color = sketch_color
 950          self.draw_order = draw_order
 951          self.draw_completion = draw_completion
 952          self.arrow_start = arrow_start
 953          self.arrow_end = arrow_end
 954          self.sketch_opacity = sketch_opacity
 955          self.fill_opacity = fill_opacity
 956          self.hidden_material = hidden_material
 957          self.sketch_outline = sketch_outline
 958          self.sketch_folds = sketch_folds
 959          self.sketch_creases = sketch_creases
 960          self.sketch_border = sketch_border
 961          self.sketch_contour = sketch_contour
 962          self.sketch_splines = sketch_splines
 963          super().__init__(**kwargs)
 964          self.derive_colors()
 965          # fill setup
 966          self.set_fill_material()
 967          self.set_fill_tag()
 968          self.fill_parameter_setup()
 969          # sketch setup
 970          if self.stroke_mode:
 971              # Geometry-based stroke rendering
 972              self._setup_stroke_mode()
 973              self.sketch_parameter_setup()  # Sets up Draw, Opacity, Color parameters
 974          else:
 975              # Traditional Sketch & Toon rendering
 976              self.set_sketch_material()
 977              self.set_sketch_tag()
 978              self.sketch_parameter_setup()
 979          self.parameters = []
 980          self.specify_parameters()
 981          self.insert_parameters()
 982          self.specify_relations()
 983          self.action_parameters = []
 984          self.specify_action_parameters()
 985          self.specify_creation_parameter()
 986          self.insert_action_parameters()
 987          self.specify_actions()
 988          self.specify_creation()
 989          self.sort_relations_by_priority()
 990  
 991      def derive_colors(self):
 992          if not self.fill_color:
 993              self.fill_color = self.color
 994          if not self.sketch_color:
 995              self.sketch_color = self.color
 996  
 997      def _setup_stroke_mode(self):
 998          """Set up geometry-based stroke rendering using MeshStroke.
 999  
1000          In stroke mode for solids:
1001          - self.mesh holds the original mesh object
1002          - self.obj becomes the MeshStroke wrapper (for position/rotation/UserData compatibility)
1003          - The mesh is nested inside: StrokeGen > SilhouetteSplineGen > Mesh
1004  
1005          This extracts silhouette edges and renders them as geometry-based strokes.
1006          """
1007          from DreamTalk.objects.stroke_objects import STROKE_GEN_CODE, SILHOUETTE_SPLINE_GEN_CODE, StrokeMaterial
1008  
1009          # Store mesh reference
1010          self.mesh = self.obj
1011  
1012          # Create the silhouette spline generator
1013          self.silhouette_gen = c4d.BaseObject(1023866)  # Python Generator
1014          self.silhouette_gen[c4d.OPYTHON_CODE] = SILHOUETTE_SPLINE_GEN_CODE
1015          self.silhouette_gen[c4d.OPYTHON_OPTIMIZE] = False  # Critical for MoGraph!
1016          self.silhouette_gen.SetName("SilhouetteSpline")
1017  
1018          # Create the stroke generator
1019          self.stroke_gen = c4d.BaseObject(1023866)  # Python Generator
1020          self.stroke_gen[c4d.OPYTHON_CODE] = STROKE_GEN_CODE
1021          self.stroke_gen[c4d.OPYTHON_OPTIMIZE] = False  # Critical for MoGraph!
1022          self.stroke_gen.SetName(self.name)
1023  
1024          # Add Stroke Width UserData to stroke gen
1025          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
1026          bc[c4d.DESC_NAME] = "Stroke Width"
1027          bc[c4d.DESC_DEFAULT] = 3.0
1028          bc[c4d.DESC_MIN] = 0.1
1029          bc[c4d.DESC_MAX] = 100.0
1030          bc[c4d.DESC_STEP] = 0.5
1031          bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_METER
1032          self.stroke_width_id = self.stroke_gen.AddUserData(bc)
1033          self.stroke_gen[self.stroke_width_id] = self.stroke_width
1034  
1035          # Add Draw UserData (0-1 completion for animation)
1036          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
1037          bc[c4d.DESC_NAME] = "Draw"
1038          bc[c4d.DESC_DEFAULT] = 1.0
1039          bc[c4d.DESC_MIN] = 0.0
1040          bc[c4d.DESC_MAX] = 1.0
1041          bc[c4d.DESC_STEP] = 0.01
1042          bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
1043          self.stroke_draw_id = self.stroke_gen.AddUserData(bc)
1044          self.stroke_gen[self.stroke_draw_id] = self.draw_completion
1045  
1046          # Insert stroke gen into document
1047          self.document.InsertObject(self.stroke_gen)
1048  
1049          # Build hierarchy: StrokeGen > SilhouetteSplineGen > Mesh
1050          self.silhouette_gen.InsertUnder(self.stroke_gen)
1051          self.mesh.Remove()
1052          self.mesh.InsertUnder(self.silhouette_gen)
1053  
1054          # Create and apply stroke material
1055          self.stroke_material = StrokeMaterial(
1056              color=self.sketch_color,
1057              opacity=self.sketch_opacity,
1058              name=f"{self.name}_StrokeMat"
1059          )
1060          tag = self.stroke_gen.MakeTag(c4d.Ttexture)
1061          tag[c4d.TEXTURETAG_MATERIAL] = self.stroke_material.obj
1062  
1063          # Swap obj to be the stroke_gen - this is the "main" object now
1064          self.obj = self.stroke_gen
1065  
1066      def sketch_parameter_setup(self):
1067          if self.stroke_mode:
1068              self._stroke_sketch_parameter_setup()
1069              self._insert_stroke_sketch_parameters()
1070              self._specify_stroke_sketch_relations()
1071          else:
1072              self.specify_sketch_parameters()
1073              self.insert_sketch_parameters()
1074              self.specify_sketch_relations()
1075  
1076      def _stroke_sketch_parameter_setup(self):
1077          """Set up parameters for stroke mode that mirror sketch parameters."""
1078          # Draw parameter - already on stroke_gen via _setup_stroke_mode
1079          self.draw_parameter = UCompletion(
1080              name="Draw", default_value=self.draw_completion)
1081          self.draw_parameter.desc_id = self.stroke_draw_id  # Point to existing
1082  
1083          # For Opacity and Color, we use the material directly
1084          self.opacity_parameter = UCompletion(
1085              name="SketchOpacity", default_value=self.sketch_opacity)
1086          self.color_parameter = UColor(
1087              name="Color", default_value=self.sketch_color)
1088  
1089          self.sketch_parameters = [self.draw_parameter,
1090                                    self.opacity_parameter, self.color_parameter]
1091  
1092      def _insert_stroke_sketch_parameters(self):
1093          """Insert additional UserData on stroke_gen for Opacity and Color."""
1094          # Opacity UserData (for animation keyframing)
1095          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
1096          bc[c4d.DESC_NAME] = "SketchOpacity"
1097          bc[c4d.DESC_DEFAULT] = 1.0
1098          bc[c4d.DESC_MIN] = 0.0
1099          bc[c4d.DESC_MAX] = 1.0
1100          bc[c4d.DESC_STEP] = 0.01
1101          bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
1102          self.stroke_opacity_id = self.obj.AddUserData(bc)
1103          self.obj[self.stroke_opacity_id] = self.sketch_opacity
1104          self.opacity_parameter.desc_id = self.stroke_opacity_id
1105  
1106          # Color UserData (for animation keyframing)
1107          bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_COLOR)
1108          bc[c4d.DESC_NAME] = "Color"
1109          self.stroke_color_id = self.obj.AddUserData(bc)
1110          self.obj[self.stroke_color_id] = self.sketch_color
1111          self.color_parameter.desc_id = self.stroke_color_id
1112  
1113      def _specify_stroke_sketch_relations(self):
1114          """Link UserData to stroke material.
1115  
1116          In stroke mode:
1117          - Draw is handled by the generator code directly (reads UserData by name)
1118          - Opacity and Color are set at construction time on the material
1119          """
1120          # No XPresso relations needed - generator reads Draw directly
1121          pass
1122  
1123      def specify_creation(self):
1124          """specifies the creation action"""
1125          if self.stroke_mode:
1126              # In stroke mode, skip XAction - use direct UserData access
1127              # Create animator will animate the creation_parameter directly
1128              pass
1129          else:
1130              creation_action = XAction(
1131                  Movement(self.fill_parameter, (1 / 3, 1),
1132                           output=(0, self.fill_opacity)),
1133                  Movement(self.draw_parameter, (0, 2 / 3)),
1134                  target=self, completion_parameter=self.creation_parameter, name="Creation")
1135  
1136      def fill_parameter_setup(self):
1137          self.specify_fill_parameter()
1138          self.insert_fill_parameter()
1139          self.specify_fill_relation()
1140  
1141      def set_fill_material(self):
1142          self.fill_material = FillMaterial(
1143              name=self.name, fill=self.filled, color=self.fill_color)
1144  
1145      def set_fill_tag(self):
1146          self.fill_tag = FillTag(target=self, material=self.fill_material)
1147  
1148      def specify_fill_parameter(self):
1149          self.fill_parameter = UCompletion(
1150              name="Fill", default_value=self.filled)
1151          self.glow_parameter = UCompletion(
1152              name="Glow", default_value=self.glowing)
1153          self.fill_parameters = [self.fill_parameter, self.glow_parameter]
1154  
1155      def insert_fill_parameter(self):
1156          self.fill_u_group = UGroup(
1157              *self.fill_parameters, target=self.obj, name="Solid")
1158  
1159      def specify_fill_relation(self):
1160          self.fill_relation = XRelation(part=self.fill_material, whole=self, desc_ids=[self.fill_material.desc_ids["transparency"]],
1161                                         parameters=[self.fill_parameter], formula=f"1-{self.fill_parameter.name}")
1162          self.glow_relation = XRelation(part=self.fill_material, whole=self, desc_ids=[self.fill_material.desc_ids["glow_brightness"]],
1163                                         parameters=[self.glow_parameter], formula=f"{self.glow_parameter.name}")
1164  
1165      def fill(self, completion=1):
1166          """specifies the fill animation"""
1167          desc_id = self.fill_parameter.desc_id
1168          animation = ScalarAnimation(
1169              target=self, descriptor=desc_id, value_fin=completion)
1170          self.obj[desc_id] = completion
1171          return animation
1172  
1173      def un_fill(self, completion=0):
1174          """specifies the unfill animation"""
1175          desc_id = self.fill_parameter.desc_id
1176          animation = ScalarAnimation(
1177              target=self, descriptor=desc_id, value_fin=completion)
1178          self.obj[desc_id] = completion
1179          return animation
1180  
1181      def set_sketch_material(self):
1182          self.sketch_material = SketchMaterial(
1183              name=self.__class__.__name__, draw_order=self.draw_order, color=self.sketch_color, arrow_start=self.arrow_start, arrow_end=self.arrow_end, stroke_width=self.stroke_width)
1184  
1185      def set_sketch_tag(self):
1186          self.sketch_tag = SketchTag(
1187              target=self,
1188              material=self.sketch_material,
1189              outline=self.sketch_outline,
1190              folds=self.sketch_folds,
1191              creases=self.sketch_creases,
1192              border=self.sketch_border,
1193              contour=self.sketch_contour,
1194              splines=self.sketch_splines,
1195              hidden_material=self.hidden_material
1196          )
1197  
1198      def specify_sketch_parameters(self):
1199          self.draw_parameter = UCompletion(
1200              name="Draw", default_value=self.draw_completion)
1201          self.opacity_parameter = UCompletion(
1202              name="SketchOpacity", default_value=self.sketch_opacity)
1203          self.color_parameter = UColor(
1204              name="Color", default_value=self.sketch_color)
1205          self.sketch_parameters = [self.draw_parameter,
1206                                    self.opacity_parameter, self.color_parameter]
1207  
1208      def insert_sketch_parameters(self):
1209          self.draw_u_group = UGroup(
1210              *self.sketch_parameters, target=self.obj, name="Sketch")
1211  
1212      def specify_sketch_relations(self):
1213          draw_relation = XIdentity(part=self.sketch_material, whole=self, desc_ids=[self.sketch_material.desc_ids["draw_completion"]],
1214                                    parameter=self.draw_parameter)
1215          opacity_relation = XIdentity(part=self.sketch_material, whole=self, desc_ids=[self.sketch_material.desc_ids["opacity"]],
1216                                       parameter=self.opacity_parameter)
1217          color_relation = XIdentity(part=self.sketch_material, whole=self, desc_ids=[self.sketch_material.desc_ids["color"]],
1218                                     parameter=self.color_parameter)
1219  
1220      def draw(self, completion=1):
1221          """specifies the draw animation"""
1222          desc_id = self.draw_parameter.desc_id
1223          animation = ScalarAnimation(
1224              target=self, descriptor=desc_id, value_fin=completion)
1225          self.obj[desc_id] = completion
1226          return animation
1227  
1228      def un_draw(self, completion=0):
1229          """specifies the undraw animation"""
1230          desc_id = self.draw_parameter.desc_id
1231          animation = ScalarAnimation(
1232              target=self, descriptor=desc_id, value_fin=completion)
1233          self.obj[desc_id] = completion
1234          return animation
1235  
1236      def glow(self, completion=1):
1237          """specifies the glow animation"""
1238          desc_id = self.glow_parameter.desc_id
1239          animation = ScalarAnimation(
1240              target=self, descriptor=desc_id, value_fin=completion)
1241          self.obj[desc_id] = completion
1242          return animation
1243  
1244      def un_glow(self, completion=0):
1245          """specifies the unglow animation"""
1246          desc_id = self.glow_parameter.desc_id
1247          animation = ScalarAnimation(
1248              target=self, descriptor=desc_id, value_fin=completion)
1249          self.obj[desc_id] = completion
1250          return animation