/ xpresso / bindings.py
bindings.py
  1  """
  2  DreamTalk Declarative Bindings (v2.0)
  3  
  4  A minimal, declarative syntax for expressing parameter relationships.
  5  Replaces XPresso with Python-native binding expressions.
  6  
  7  Usage (specify_relationships style):
  8      class MyObject(CustomObject):
  9          radius = ULength(default=100)
 10  
 11          def specify_parts(self):
 12              self.circle = Circle()
 13              self.parts = [self.circle]
 14  
 15          def specify_relationships(self):
 16              self.circle.radius << self.radius  # Identity
 17              self.circle.x << self.radius * 0.5  # Formula
 18  
 19  Usage (inline binding style - Phase 3):
 20      class MyObject(Holon):
 21          radius: Length = 100
 22  
 23          def specify_parts(self):
 24              # Bind circle radius to parent's radius parameter
 25              self.circle = Circle(radius=self.radius_parameter >> 100)
 26              self.parts = [self.circle]
 27  
 28  The << operator creates a Binding for relationships.
 29  The >> operator creates a BoundValue for inline bindings in constructors.
 30  """
 31  
 32  import math
 33  from typing import Any, List, Union, Optional
 34  
 35  
 36  class BindingExpression:
 37      """
 38      Represents a mathematical expression involving parameters.
 39      Supports operator overloading to build expression trees.
 40      """
 41  
 42      def __init__(self, expr_str: str, dependencies: List[str] = None):
 43          self.expr_str = expr_str
 44          self.dependencies = dependencies or []
 45  
 46      def __repr__(self):
 47          return f"Expr({self.expr_str})"
 48  
 49      # Arithmetic operators - build expression strings
 50      def __add__(self, other):
 51          other_expr, other_deps = self._unwrap(other)
 52          return BindingExpression(
 53              f"({self.expr_str} + {other_expr})",
 54              self.dependencies + other_deps
 55          )
 56  
 57      def __radd__(self, other):
 58          other_expr, other_deps = self._unwrap(other)
 59          return BindingExpression(
 60              f"({other_expr} + {self.expr_str})",
 61              other_deps + self.dependencies
 62          )
 63  
 64      def __sub__(self, other):
 65          other_expr, other_deps = self._unwrap(other)
 66          return BindingExpression(
 67              f"({self.expr_str} - {other_expr})",
 68              self.dependencies + other_deps
 69          )
 70  
 71      def __rsub__(self, other):
 72          other_expr, other_deps = self._unwrap(other)
 73          return BindingExpression(
 74              f"({other_expr} - {self.expr_str})",
 75              other_deps + self.dependencies
 76          )
 77  
 78      def __mul__(self, other):
 79          other_expr, other_deps = self._unwrap(other)
 80          return BindingExpression(
 81              f"({self.expr_str} * {other_expr})",
 82              self.dependencies + other_deps
 83          )
 84  
 85      def __rmul__(self, other):
 86          other_expr, other_deps = self._unwrap(other)
 87          return BindingExpression(
 88              f"({other_expr} * {self.expr_str})",
 89              other_deps + self.dependencies
 90          )
 91  
 92      def __truediv__(self, other):
 93          other_expr, other_deps = self._unwrap(other)
 94          return BindingExpression(
 95              f"({self.expr_str} / {other_expr})",
 96              self.dependencies + other_deps
 97          )
 98  
 99      def __rtruediv__(self, other):
100          other_expr, other_deps = self._unwrap(other)
101          return BindingExpression(
102              f"({other_expr} / {self.expr_str})",
103              other_deps + self.dependencies
104          )
105  
106      def __neg__(self):
107          return BindingExpression(f"(-{self.expr_str})", self.dependencies)
108  
109      def __pow__(self, other):
110          other_expr, other_deps = self._unwrap(other)
111          return BindingExpression(
112              f"({self.expr_str} ** {other_expr})",
113              self.dependencies + other_deps
114          )
115  
116      def __rshift__(self, default_value):
117          """
118          Create a BoundValue for inline binding syntax.
119  
120          Usage:
121              Circle(radius=(self.size_parameter * 0.5) >> 50)
122  
123          This allows binding expressions (not just parameters) to be
124          used with inline binding syntax.
125          """
126          return BoundValue(expression=self, default=default_value)
127  
128      def _unwrap(self, other):
129          """Extract expression string and dependencies from any value."""
130          if isinstance(other, BindingExpression):
131              return other.expr_str, other.dependencies
132          elif isinstance(other, ParameterRef):
133              return other.to_expr().expr_str, other.to_expr().dependencies
134          else:
135              return str(other), []
136  
137  
138  class ParameterRef:
139      """
140      Reference to a UserData parameter on the parent object.
141      Created when accessing a parameter in a relationship context.
142      """
143  
144      def __init__(self, name: str):
145          self.name = name
146  
147      def to_expr(self) -> BindingExpression:
148          """Convert to expression that reads this parameter."""
149          return BindingExpression(
150              f'get_userdata_by_name(op, "{self.name}")',
151              [self.name]
152          )
153  
154      def __repr__(self):
155          return f"Param({self.name})"
156  
157      # >> operator for inline bindings: self.param >> default_value
158      def __rshift__(self, default_value):
159          """
160          Create a BoundValue for inline binding syntax.
161  
162          Usage in part constructors:
163              Circle(radius=self.radius_parameter >> 100)
164  
165          This means: bind the circle's radius to the parent's radius parameter,
166          using 100 as the default/initial value.
167          """
168          return BoundValue(expression=self.to_expr(), default=default_value, param_name=self.name)
169  
170      # Delegate arithmetic to BindingExpression
171      def __add__(self, other):
172          return self.to_expr() + other
173  
174      def __radd__(self, other):
175          return other + self.to_expr()
176  
177      def __sub__(self, other):
178          return self.to_expr() - other
179  
180      def __rsub__(self, other):
181          return other - self.to_expr()
182  
183      def __mul__(self, other):
184          return self.to_expr() * other
185  
186      def __rmul__(self, other):
187          return other * self.to_expr()
188  
189      def __truediv__(self, other):
190          return self.to_expr() / other
191  
192      def __rtruediv__(self, other):
193          return other / self.to_expr()
194  
195      def __neg__(self):
196          return -self.to_expr()
197  
198      def __pow__(self, other):
199          return self.to_expr() ** other
200  
201  
202  class BoundValue:
203      """
204      A value bound to a parameter expression, for inline binding in constructors.
205  
206      Created via the >> operator:
207          Circle(radius=self.radius_parameter >> 100)
208  
209      This stores both the binding expression and the default value.
210      When parts are instantiated, ProtoObject detects BoundValues and:
211      1. Uses the default value for initialization
212      2. Stores the binding for later collection by the parent Holon
213  
214      Attributes:
215          expression: The BindingExpression that computes the value
216          default: The default/initial value to use
217          param_name: Name of the source parameter (for debugging)
218          target_property: Set by the part constructor to record what property this binds to
219      """
220  
221      def __init__(self, expression: BindingExpression, default, param_name: str = None):
222          self.expression = expression
223          self.default = default
224          self.param_name = param_name
225          self.target_property = None  # Set when binding is collected
226  
227      def __repr__(self):
228          return f"BoundValue({self.param_name} >> {self.default})"
229  
230      # Support arithmetic operations to allow expressions like:
231      # Circle(radius=(self.size_parameter * 0.5) >> 50)
232      def __add__(self, other):
233          other_expr, other_deps = self._unwrap(other)
234          new_expr = BindingExpression(
235              f"({self.expression.expr_str} + {other_expr})",
236              self.expression.dependencies + other_deps
237          )
238          return BoundValue(new_expr, self.default + other if isinstance(other, (int, float)) else self.default, self.param_name)
239  
240      def __mul__(self, other):
241          other_expr, other_deps = self._unwrap(other)
242          new_expr = BindingExpression(
243              f"({self.expression.expr_str} * {other_expr})",
244              self.expression.dependencies + other_deps
245          )
246          return BoundValue(new_expr, self.default * other if isinstance(other, (int, float)) else self.default, self.param_name)
247  
248      def __rmul__(self, other):
249          return self.__mul__(other)
250  
251      def _unwrap(self, other):
252          """Extract expression string and dependencies from any value."""
253          if isinstance(other, BindingExpression):
254              return other.expr_str, other.dependencies
255          elif isinstance(other, ParameterRef):
256              return other.to_expr().expr_str, other.to_expr().dependencies
257          elif isinstance(other, BoundValue):
258              return other.expression.expr_str, other.expression.dependencies
259          else:
260              return str(other), []
261  
262  
263  class PropertyTarget:
264      """
265      Target for a binding - a property on a child object.
266      Receives values via the << operator.
267      """
268  
269      # Class-level collector for auto-registration
270      _active_collector = None
271  
272      def __init__(self, part_name: str, property_name: str, c4d_attr: str = None,
273                   vector_component: str = None, setter_template: str = None):
274          self.part_name = part_name
275          self.property_name = property_name
276          self.c4d_attr = c4d_attr  # e.g., "c4d.PRIM_CIRCLE_RADIUS"
277          self.vector_component = vector_component  # e.g., "x", "y", "z" for position
278          self.setter_template = setter_template  # Code template with {value} placeholder
279          self._binding = None
280  
281      def __lshift__(self, expr):
282          """The << operator - creates a binding from expression to this target."""
283          if isinstance(expr, ParameterRef):
284              expr = expr.to_expr()
285          elif isinstance(expr, (int, float)):
286              expr = BindingExpression(str(expr), [])
287  
288          binding = Binding(target=self, expression=expr)
289          self._binding = binding
290  
291          # Auto-register with active collector if one exists
292          if PropertyTarget._active_collector is not None:
293              PropertyTarget._active_collector.add_binding(binding)
294  
295          return binding
296  
297      def __repr__(self):
298          return f"Target({self.part_name}.{self.property_name})"
299  
300      @classmethod
301      def set_collector(cls, collector):
302          """Set the active collector for auto-registration."""
303          cls._active_collector = collector
304  
305      @classmethod
306      def clear_collector(cls):
307          """Clear the active collector."""
308          cls._active_collector = None
309  
310  
311  class Binding:
312      """
313      A complete binding: target property <- expression.
314      Collected by CustomObject to generate code.
315      """
316  
317      def __init__(self, target: PropertyTarget, expression: BindingExpression):
318          self.target = target
319          self.expression = expression
320  
321      def __repr__(self):
322          return f"Binding({self.target} << {self.expression})"
323  
324      def to_code(self, indent: int = 4) -> str:
325          """Generate the Python code for this binding."""
326          ind = " " * indent
327          comment = f"{ind}# {self.target.part_name}.{self.target.property_name} << ..."
328  
329          # Use setter template if available (for position/rotation vector components)
330          if self.target.setter_template:
331              setter_code = self.target.setter_template.format(value=self.expression.expr_str)
332              return f'''{comment}
333  {ind}child = find_child_by_name(op, "{self.target.part_name}")
334  {ind}if child:
335  {ind}    {setter_code}'''
336  
337          # Handle UserData parameters
338          if self.target.c4d_attr and self.target.c4d_attr.startswith('userdata:'):
339              param_name = self.target.c4d_attr.split(':')[1]
340              return f'''{comment}
341  {ind}child = find_child_by_name(op, "{self.target.part_name}")
342  {ind}if child:
343  {ind}    set_userdata_by_name(child, "{param_name}", {self.expression.expr_str})'''
344  
345          # Default: direct property access
346          return f'''{comment}
347  {ind}child = find_child_by_name(op, "{self.target.part_name}")
348  {ind}if child:
349  {ind}    child[{self.target.c4d_attr}] = {self.expression.expr_str}'''
350  
351  
352  class PartProxy:
353      """
354      Proxy object for accessing part properties in relationship definitions.
355      Created when accessing self.<part_name> in specify_relationships().
356      """
357  
358      # Common property mappings to C4D attributes
359      # Format: property_name -> (c4d_constant, is_vector_component, setter_code_template)
360      # Note: For primitive properties on LineObjects/SolidObjects, we need to find the nested
361      # spline/mesh inside the StrokeGen wrapper. The setter template handles this.
362      PROPERTY_MAP = {
363          # Position (vector components need special handling)
364          'x': ('c4d.ID_BASEOBJECT_POSITION', 'x', 'pos = child.GetRelPos(); pos.x = {value}; child.SetRelPos(pos)'),
365          'y': ('c4d.ID_BASEOBJECT_POSITION', 'y', 'pos = child.GetRelPos(); pos.y = {value}; child.SetRelPos(pos)'),
366          'z': ('c4d.ID_BASEOBJECT_POSITION', 'z', 'pos = child.GetRelPos(); pos.z = {value}; child.SetRelPos(pos)'),
367          # Rotation
368          'h': ('c4d.ID_BASEOBJECT_ROTATION', 'x', 'rot = child.GetRelRot(); rot.x = {value}; child.SetRelRot(rot)'),
369          'p': ('c4d.ID_BASEOBJECT_ROTATION', 'y', 'rot = child.GetRelRot(); rot.y = {value}; child.SetRelRot(rot)'),
370          'b': ('c4d.ID_BASEOBJECT_ROTATION', 'z', 'rot = child.GetRelRot(); rot.z = {value}; child.SetRelRot(rot)'),
371          # Scale
372          'scale_x': ('c4d.ID_BASEOBJECT_SCALE', 'x', 'scale = child.GetRelScale(); scale.x = {value}; child.SetRelScale(scale)'),
373          'scale_y': ('c4d.ID_BASEOBJECT_SCALE', 'y', 'scale = child.GetRelScale(); scale.y = {value}; child.SetRelScale(scale)'),
374          'scale_z': ('c4d.ID_BASEOBJECT_SCALE', 'z', 'scale = child.GetRelScale(); scale.z = {value}; child.SetRelScale(scale)'),
375          # Circle primitive - look for nested spline if child is a generator
376          'radius': ('c4d.PRIM_CIRCLE_RADIUS', None, 'spline = child.GetDown() if child.GetType() == 1023866 else child; spline[c4d.PRIM_CIRCLE_RADIUS] = {value}'),
377          # Rectangle primitive
378          'width': ('c4d.PRIM_RECTANGLE_WIDTH', None, 'spline = child.GetDown() if child.GetType() == 1023866 else child; spline[c4d.PRIM_RECTANGLE_WIDTH] = {value}'),
379          'height': ('c4d.PRIM_RECTANGLE_HEIGHT', None, 'spline = child.GetDown() if child.GetType() == 1023866 else child; spline[c4d.PRIM_RECTANGLE_HEIGHT] = {value}'),
380          # Sphere primitive
381          'sphere_radius': ('c4d.PRIM_SPHERE_RAD', None, 'mesh = child.GetDown() if child.GetType() == 1023866 else child; mesh[c4d.PRIM_SPHERE_RAD] = {value}'),
382          # Camera
383          'zoom': ('c4d.CAMERA_ZOOM', None, 'child[c4d.CAMERA_ZOOM] = {value}'),
384      }
385  
386      def __init__(self, part_name: str, part_obj=None):
387          self.part_name = part_name
388          self.part_obj = part_obj
389          self._bindings = []
390  
391      def __getattr__(self, name: str) -> 'PropertyTarget':
392          """Access a property on this part, returning a bindable target."""
393          if name.startswith('_'):
394              raise AttributeError(name)
395  
396          prop_info = self.PROPERTY_MAP.get(name)
397  
398          if prop_info:
399              c4d_attr, vector_comp, setter_template = prop_info
400          else:
401              # Check if part has desc_ids
402              if self.part_obj and hasattr(self.part_obj, 'desc_ids') and name in self.part_obj.desc_ids:
403                  c4d_attr = f"self.{self.part_name}.desc_ids['{name}']"
404              else:
405                  # Fallback - assume it's a UserData parameter on the child
406                  c4d_attr = f'userdata:{name}'
407              vector_comp = None
408              setter_template = None
409  
410          target = PropertyTarget(self.part_name, name, c4d_attr, vector_comp, setter_template)
411          return target
412  
413  
414  # Math functions that work with BindingExpressions
415  def sin(x) -> BindingExpression:
416      if isinstance(x, BindingExpression):
417          return BindingExpression(f"math.sin({x.expr_str})", x.dependencies)
418      elif isinstance(x, ParameterRef):
419          expr = x.to_expr()
420          return BindingExpression(f"math.sin({expr.expr_str})", expr.dependencies)
421      else:
422          return BindingExpression(f"math.sin({x})", [])
423  
424  def cos(x) -> BindingExpression:
425      if isinstance(x, BindingExpression):
426          return BindingExpression(f"math.cos({x.expr_str})", x.dependencies)
427      elif isinstance(x, ParameterRef):
428          expr = x.to_expr()
429          return BindingExpression(f"math.cos({expr.expr_str})", expr.dependencies)
430      else:
431          return BindingExpression(f"math.cos({x})", [])
432  
433  def sqrt(x) -> BindingExpression:
434      if isinstance(x, BindingExpression):
435          return BindingExpression(f"math.sqrt({x.expr_str})", x.dependencies)
436      elif isinstance(x, ParameterRef):
437          expr = x.to_expr()
438          return BindingExpression(f"math.sqrt({expr.expr_str})", expr.dependencies)
439      else:
440          return BindingExpression(f"math.sqrt({x})", [])
441  
442  
443  class BindingCollector:
444      """
445      Collects bindings created during specify_relationships().
446      Used by CustomObject to gather bindings and compile them to generator code.
447      """
448  
449      def __init__(self):
450          self.bindings: List[Binding] = []
451          self._part_proxies = {}
452          self._param_refs = {}
453  
454      def get_part_proxy(self, name: str, part_obj=None) -> PartProxy:
455          """Get or create a proxy for a part."""
456          if name not in self._part_proxies:
457              self._part_proxies[name] = PartProxy(name, part_obj)
458          return self._part_proxies[name]
459  
460      def get_param_ref(self, name: str) -> ParameterRef:
461          """Get or create a reference to a parameter."""
462          if name not in self._param_refs:
463              self._param_refs[name] = ParameterRef(name)
464          return self._param_refs[name]
465  
466      def add_binding(self, binding: Binding):
467          """Add a binding to the collection."""
468          self.bindings.append(binding)
469  
470      def compile_to_generator_code(self) -> str:
471          """Compile collected bindings into generator Python code."""
472          if not self.bindings:
473              return None
474  
475          # Collect all parameter dependencies
476          all_params = set()
477          for binding in self.bindings:
478              all_params.update(binding.expression.dependencies)
479  
480          # Generate parameter reading code
481          param_code = []
482          for param in sorted(all_params):
483              # Use lowercase variable name for the local
484              var_name = param.lower().replace(' ', '_')
485              param_code.append(f'    {var_name} = get_userdata_by_name(op, "{param}") or 0.0')
486  
487          # Generate binding code
488          binding_code = []
489          for binding in self.bindings:
490              binding_code.append(binding.to_code(indent=4))
491  
492          code = '''def main():
493      # Read parameters
494  '''
495          if param_code:
496              code += '\n'.join(param_code) + '\n'
497  
498          code += '''
499      # Apply bindings
500  '''
501          code += '\n'.join(binding_code)
502          code += '''
503  
504      return None
505  '''
506          return code
507  
508  
509  class RelationshipContext:
510      """
511      Context for defining relationships between parameters and parts.
512  
513      Provides a clean namespace where:
514      - Accessing self.<param> returns a ParameterRef
515      - Accessing self.<part> returns a PartProxy
516      - Using << creates bindings that get collected
517  
518      Usage in CustomObject:
519          def specify_relationships(self):
520              self.circle.radius << self.radius
521              self.circle.x << self.distance * cos(PI/6)
522      """
523  
524      def __init__(self, custom_object, collector: BindingCollector):
525          object.__setattr__(self, '_custom_object', custom_object)
526          object.__setattr__(self, '_collector', collector)
527          object.__setattr__(self, '_part_names', set())
528          object.__setattr__(self, '_param_names', set())
529  
530          # Discover parts (objects with .obj attribute that are children)
531          for attr_name in dir(custom_object):
532              if attr_name.startswith('_'):
533                  continue
534              try:
535                  attr = getattr(custom_object, attr_name)
536                  # Check if it's a part (has .obj attribute - is a DreamTalk object)
537                  if hasattr(attr, 'obj'):
538                      self._part_names.add(attr_name)
539                  # Check if it's a parameter (has .desc_id or is a parameter class)
540                  elif hasattr(attr, 'desc_id') or (hasattr(attr, 'name') and hasattr(attr, 'default_value')):
541                      self._param_names.add(attr_name)
542              except:
543                  pass
544  
545          # Also check the parameters list
546          if hasattr(custom_object, 'parameters'):
547              for param in custom_object.parameters:
548                  if hasattr(param, 'name'):
549                      # Store by name for lookup
550                      self._param_names.add(param.name)
551  
552      def __getattr__(self, name: str):
553          """Return a PartProxy or ParameterRef based on what's being accessed."""
554          custom_obj = object.__getattribute__(self, '_custom_object')
555          collector = object.__getattribute__(self, '_collector')
556          part_names = object.__getattribute__(self, '_part_names')
557          param_names = object.__getattribute__(self, '_param_names')
558  
559          # Check if it's a known part
560          if name in part_names:
561              part_obj = getattr(custom_obj, name, None)
562              return collector.get_part_proxy(name, part_obj)
563  
564          # Check if it's a known parameter (by attribute name)
565          if name in param_names:
566              # Get the actual parameter name from the parameter object
567              param_obj = getattr(custom_obj, name, None)
568              if hasattr(param_obj, 'name'):
569                  return collector.get_param_ref(param_obj.name)
570              return collector.get_param_ref(name)
571  
572          # Check if accessing a parameter by its name directly
573          # e.g., self.Radius where the parameter's name is "Radius"
574          if hasattr(custom_obj, 'parameters'):
575              for param in custom_obj.parameters:
576                  if hasattr(param, 'name') and param.name == name:
577                      return collector.get_param_ref(name)
578  
579          # Fall back to actual attribute on custom object
580          return getattr(custom_obj, name)
581  
582  
583  # Convenience: PI constant that works in expressions
584  PI = math.pi
585  
586  
587  def collect_relationships(custom_object, specify_relationships_func) -> str:
588      """
589      Execute specify_relationships() and collect bindings into generator code.
590  
591      The key insight is that we can't easily intercept `self.part.prop << self.param`
592      inside the method. Instead, we temporarily replace the object's attributes
593      with proxies before calling the method.
594  
595      Args:
596          custom_object: The CustomObject instance
597          specify_relationships_func: The specify_relationships method (bound)
598  
599      Returns:
600          Generated Python code string for the generator, or None if no bindings
601      """
602      collector = BindingCollector()
603  
604      # Set up the collector for auto-registration
605      PropertyTarget.set_collector(collector)
606  
607      # Store original attributes so we can restore them
608      original_attrs = {}
609  
610      try:
611          # Discover and replace parts with proxies
612          for attr_name in list(vars(custom_object).keys()):
613              if attr_name.startswith('_'):
614                  continue
615              attr = getattr(custom_object, attr_name)
616              # Check if it's a part (has .obj attribute - is a DreamTalk object)
617              if hasattr(attr, 'obj'):
618                  original_attrs[attr_name] = attr
619                  # Get the actual name the part uses in the hierarchy
620                  part_name = attr.obj.GetName() if hasattr(attr.obj, 'GetName') else attr_name
621                  setattr(custom_object, attr_name, collector.get_part_proxy(part_name, attr))
622  
623          # Replace parameters with refs
624          for attr_name in list(vars(custom_object).keys()):
625              if attr_name.startswith('_'):
626                  continue
627              attr = original_attrs.get(attr_name) or getattr(custom_object, attr_name, None)
628              # Check if it's a parameter
629              if hasattr(attr, 'name') and hasattr(attr, 'default_value'):
630                  if attr_name not in original_attrs:
631                      original_attrs[attr_name] = attr
632                  setattr(custom_object, attr_name, collector.get_param_ref(attr.name))
633  
634          # Now call the method - it will use our proxies
635          specify_relationships_func()
636  
637      finally:
638          # Restore original attributes
639          for attr_name, attr in original_attrs.items():
640              setattr(custom_object, attr_name, attr)
641  
642          # Always clean up
643          PropertyTarget.clear_collector()
644  
645      return collector.compile_to_generator_code()
646  
647  
648  def extract_bound_values(kwargs: dict) -> tuple:
649      """
650      Extract BoundValues from constructor kwargs.
651  
652      Returns:
653          (clean_kwargs, bindings_list)
654          - clean_kwargs: dict with BoundValues replaced by their defaults
655          - bindings_list: list of (property_name, BoundValue) tuples
656  
657      Usage in part constructors:
658          def __init__(self, radius=100, **kwargs):
659              kwargs, bindings = extract_bound_values({'radius': radius, **kwargs})
660              radius = kwargs.pop('radius', 100)
661              # ... use radius as the value
662              # Store bindings on self._pending_bindings for collection
663      """
664      clean_kwargs = {}
665      bindings = []
666  
667      for key, value in kwargs.items():
668          if isinstance(value, BoundValue):
669              # Replace with default value
670              clean_kwargs[key] = value.default
671              # Record the binding
672              value.target_property = key
673              bindings.append((key, value))
674          else:
675              clean_kwargs[key] = value
676  
677      return clean_kwargs, bindings
678  
679  
680  def collect_inline_bindings(holon, parts: list) -> str:
681      """
682      Collect inline bindings from parts and compile them to generator code.
683  
684      Called by CustomObject after specify_parts() to gather any bindings
685      that were created via the >> inline syntax.
686  
687      Args:
688          holon: The parent Holon/CustomObject
689          parts: List of parts to scan for bindings
690  
691      Returns:
692          Generated Python code string, or None if no bindings
693      """
694      collector = BindingCollector()
695  
696      for part in parts:
697          # Check if part has pending bindings
698          pending = getattr(part, '_pending_bindings', [])
699          for prop_name, bound_value in pending:
700              # Get the part's name in the hierarchy
701              part_name = part.obj.GetName() if hasattr(part, 'obj') and hasattr(part.obj, 'GetName') else part.__class__.__name__
702  
703              # Look up property mapping
704              prop_info = PartProxy.PROPERTY_MAP.get(prop_name)
705              if prop_info:
706                  c4d_attr, vector_comp, setter_template = prop_info
707              else:
708                  # Assume UserData
709                  c4d_attr = f'userdata:{prop_name}'
710                  vector_comp = None
711                  setter_template = None
712  
713              # Create PropertyTarget and Binding
714              target = PropertyTarget(part_name, prop_name, c4d_attr, vector_comp, setter_template)
715              binding = Binding(target=target, expression=bound_value.expression)
716              collector.add_binding(binding)
717  
718      return collector.compile_to_generator_code()