"""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 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.
"""