/ xpresso / types.py
types.py
  1  """
  2  DreamTalk Parameter Types
  3  
  4  Semantic type classes for holon parameters.
  5  These serve as type hints AND default value containers.
  6  
  7  The library introspects class annotations to create UserData:
  8  
  9      class MindVirus(Holon):
 10          fold: Bipolar = 0
 11          color: Color = BLUE
 12          size: Length = 100
 13  
 14  These are translated to Cinema 4D UserData with appropriate
 15  type, range, and unit settings.
 16  
 17  Types:
 18  - Length: Distance/size (0 → ∞)
 19  - Angle: Rotation (0 → 2π)
 20  - Bipolar: Signed normalized (-1 → 1)
 21  - Completion: Progress/opacity (0 → 1)
 22  - Color: RGB color
 23  - Integer: Whole number
 24  - Bool: Boolean flag
 25  """
 26  
 27  from DreamTalk.constants import WHITE
 28  
 29  
 30  class ParameterType:
 31      """
 32      Base class for parameter types.
 33  
 34      Subclasses define the C4D type mapping and constraints.
 35      """
 36      c4d_type = None
 37      unit = None
 38      min_val = None
 39      max_val = None
 40  
 41      def __init__(self, default=None):
 42          self.default = default
 43  
 44      def __repr__(self):
 45          return f"{self.__class__.__name__}(default={self.default})"
 46  
 47  
 48  class Length(ParameterType):
 49      """
 50      Length parameter (0 → ∞).
 51  
 52      For sizes, distances, radii.
 53      Maps to C4D DTYPE_REAL with meter units.
 54      """
 55      c4d_type = "DTYPE_REAL"
 56      unit = "DESC_UNIT_METER"
 57      min_val = 0
 58  
 59      def __init__(self, default=100):
 60          super().__init__(default)
 61  
 62  
 63  class Angle(ParameterType):
 64      """
 65      Angle parameter (0 → 2π).
 66  
 67      For rotations in radians.
 68      Maps to C4D DTYPE_REAL with degree display.
 69      """
 70      c4d_type = "DTYPE_REAL"
 71      unit = "DESC_UNIT_DEGREE"
 72  
 73      def __init__(self, default=0):
 74          super().__init__(default)
 75  
 76  
 77  class Bipolar(ParameterType):
 78      """
 79      Bipolar parameter (-1 → 1).
 80  
 81      For signed normalized values like fold, blend, balance.
 82      Center (0) is neutral; extremes are -1 and 1.
 83      """
 84      c4d_type = "DTYPE_REAL"
 85      min_val = -1
 86      max_val = 1
 87  
 88      def __init__(self, default=0):
 89          super().__init__(default)
 90  
 91  
 92  class Completion(ParameterType):
 93      """
 94      Completion parameter (0 → 1).
 95  
 96      For progress, opacity, draw completion.
 97      Maps to C4D percent slider.
 98      """
 99      c4d_type = "DTYPE_REAL"
100      unit = "DESC_UNIT_PERCENT"
101      min_val = 0
102      max_val = 1
103  
104      def __init__(self, default=0):
105          super().__init__(default)
106  
107  
108  class Color(ParameterType):
109      """
110      Color parameter.
111  
112      For RGB colors.
113      Maps to C4D color picker.
114      """
115      c4d_type = "DTYPE_COLOR"
116  
117      def __init__(self, default=None):
118          if default is None:
119              default = WHITE
120          super().__init__(default)
121  
122  
123  class Integer(ParameterType):
124      """
125      Integer parameter.
126  
127      For counts, indices.
128      Maps to C4D DTYPE_LONG.
129      """
130      c4d_type = "DTYPE_LONG"
131  
132      def __init__(self, default=0):
133          super().__init__(default)
134  
135  
136  class Bool(ParameterType):
137      """
138      Boolean parameter.
139  
140      For on/off flags.
141      Maps to C4D checkbox.
142      """
143      c4d_type = "DTYPE_BOOL"
144  
145      def __init__(self, default=False):
146          super().__init__(default)
147  
148  
149  # =============================================================================
150  # Type checking utilities
151  # =============================================================================
152  
153  # Set of valid parameter type names for name-based type checking
154  # This handles module reload identity issues in Cinema 4D's persistent Python
155  PARAMETER_TYPE_NAMES = {
156      'Length', 'Angle', 'Bipolar', 'Completion', 'Color', 'Integer', 'Bool'
157  }
158  
159  
160  def is_parameter_type(obj):
161      """Check if an object is a parameter type instance or class."""
162      if isinstance(obj, type):
163          return issubclass(obj, ParameterType)
164      return isinstance(obj, ParameterType)
165  
166  
167  def _is_parameter_type_by_name(obj):
168      """Check if object is a ParameterType using name-based lookup."""
169      if isinstance(obj, type):
170          return obj.__name__ in PARAMETER_TYPE_NAMES
171      return obj.__class__.__name__ in PARAMETER_TYPE_NAMES
172  
173  
174  def get_default_value(type_hint, class_default):
175      """
176      Get the default value for a parameter.
177  
178      Args:
179          type_hint: The type annotation (class or instance)
180          class_default: The default value from the class attribute
181  
182      Returns:
183          The resolved default value
184      """
185      # If class_default is a ParameterType instance, get its default
186      # Use name-based check to handle module reload issues
187      if hasattr(class_default, 'default') and _is_parameter_type_by_name(class_default):
188          return class_default.default
189  
190      # If class_default is a raw value (int, float, etc.), use it
191      if class_default is not None:
192          return class_default
193  
194      # Fall back to type's default
195      if hasattr(type_hint, 'default') and _is_parameter_type_by_name(type_hint):
196          return type_hint.default
197      elif isinstance(type_hint, type) and type_hint.__name__ in PARAMETER_TYPE_NAMES:
198          return type_hint().default
199  
200      return None
201  
202  
203  def create_userdata_from_type(name, type_class, default_value):
204      """
205      Create a UserData object from a type hint.
206  
207      Args:
208          name: Parameter name
209          type_class: The ParameterType class (Length, Bipolar, etc.)
210          default_value: Default value for the parameter
211  
212      Returns:
213          A UData subclass instance (ULength, UBipolar, etc.)
214      """
215      from DreamTalk.xpresso.userdata import (
216          ULength, UAngle, UBipolar, UCompletion, UColor, UCount, UCheckBox
217      )
218  
219      # Map type class NAMES to UserData classes
220      # Using name-based lookup to handle module reload identity issues
221      type_name_to_userdata = {
222          'Length': ULength,
223          'Angle': UAngle,
224          'Bipolar': UBipolar,
225          'Completion': UCompletion,
226          'Color': UColor,
227          'Integer': UCount,  # UCount is the concrete integer class
228          'Bool': UCheckBox,
229      }
230  
231      # Get the UserData class for this type by name
232      type_name = type_class.__name__
233      userdata_class = type_name_to_userdata.get(type_name)
234      if userdata_class is None:
235          raise ValueError(f"Unknown parameter type: {type_class}")
236  
237      # Create and return the UserData instance
238      return userdata_class(name=name, default_value=default_value)