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)