Source code for openrig.maya.core.solution

"""Base class for all rig solutions.

Every solution — from a single joint to a full IK/FK arm — implements the
same ``SolutionBase`` contract. The framework does not distinguish between
atomic and composite solutions.

Subclasses are auto-registered via ``__init_subclass__`` the moment their
module is imported. Adding a new solution type requires only creating a file
in ``maya/solutions/``.
"""

from abc import ABC, abstractmethod
from typing import ClassVar

from openrig.maya.core.context import BuildContext
from openrig.maya.core.ports import InputPort, OutputPort
from openrig.maya.core.registry import register
from openrig.maya.core.settings import SolutionSettings


[docs] class SolutionBase(ABC): """Contract every rig solution must implement. Subclasses must define: - ``solution_type`` — class variable used as the JSON registry key. - ``settings`` — instance attribute holding a ``SolutionSettings`` subclass with all configuration for this solution. - ``build(ctx)`` — the main build method (abstract). Subclasses are registered automatically when their module is imported. No manual registration is needed. Attributes: solution_type: Class-level type string used as the JSON key (e.g. ``"Arm"``). Must be unique across all registered solutions. solution_id: Instance-level unique identifier within a rig build (e.g. ``"arm_l"``). Set by the build pipeline before ``build()`` is called. settings: Configuration for this solution instance. Each subclass defines its own ``SolutionSettings`` subclass with the fields it needs. """ solution_type: ClassVar[str] solution_id: str settings: SolutionSettings def __init_subclass__(cls, **kwargs: object) -> None: super().__init_subclass__(**kwargs) solution_type = cls.__dict__.get("solution_type") if solution_type is not None: if not isinstance(solution_type, str) or not solution_type: raise TypeError( f"{cls.__qualname__}.solution_type must be a non-empty " f"string, got {solution_type!r}." ) register(solution_type, cls) # --- port declarations ---
[docs] @classmethod def input_ports(cls) -> dict[str, InputPort]: """Declare what this solution needs from other solutions. Override to declare input ports. The default returns an empty dict (no inputs required). Returns: A dict mapping port name to ``InputPort`` descriptor. """ return {}
[docs] @classmethod def output_ports(cls) -> dict[str, OutputPort]: """Declare what this solution produces for other solutions. Override to declare output ports. The default returns an empty dict (no outputs produced). Returns: A dict mapping port name to ``OutputPort`` descriptor. """ return {}
# --- build lifecycle ---
[docs] def create_guides(self, ctx: BuildContext) -> None: """Create guide objects in Maya for interactive positioning. Optional. The default implementation does nothing — the solution builds from ``settings`` alone. Override to place guide locators or geometry that the rigger can reposition before calling ``build()``. Args: ctx: The shared build context. """
[docs] @abstractmethod def build(self, ctx: BuildContext) -> None: """Build the solution in the Maya scene. Required. Called by the build pipeline in dependency order. Implementations must: - Read guide positions via ``ctx.get_guide()`` when guides exist, falling back to ``self.settings`` when they do not. - Create all necessary Maya nodes. - Register every declared output port via ``ctx.set_output()``. Args: ctx: The shared build context. """
[docs] def post_build(self, ctx: BuildContext) -> None: """Run connections that require all solutions to already exist. Optional. The default implementation does nothing. Override to make cross-solution connections (e.g. parenting arm to spine) that can only be established after every solution has completed ``build()``. Args: ctx: The shared build context. """