"""Adapter wrapping EffectPlugin as a Stage.""" from typing import Any from engine.pipeline.core import PipelineContext, Stage class EffectPluginStage(Stage): """Adapter wrapping EffectPlugin as a Stage. Supports capability-based dependencies through the dependencies parameter. """ def __init__( self, effect_plugin, name: str = "effect", dependencies: set[str] | None = None, ): self._effect = effect_plugin self.name = name self.category = "effect" self.optional = False self._dependencies = dependencies or set() @property def stage_type(self) -> str: """Return stage_type based on effect name. HUD effects are overlays. """ if self.name == "hud": return "overlay" return self.category @property def render_order(self) -> int: """Return render_order based on effect type. HUD effects have high render_order to appear on top. """ if self.name == "hud": return 100 # High order for overlays return 0 @property def is_overlay(self) -> bool: """Return True for HUD effects. HUD is an overlay - it composes on top of the buffer rather than transforming it for the next stage. """ return self.name == "hud" @property def capabilities(self) -> set[str]: return {f"effect.{self.name}"} @property def dependencies(self) -> set[str]: return self._dependencies @property def inlet_types(self) -> set: from engine.pipeline.core import DataType return {DataType.TEXT_BUFFER} @property def outlet_types(self) -> set: from engine.pipeline.core import DataType return {DataType.TEXT_BUFFER} def process(self, data: Any, ctx: PipelineContext) -> Any: """Process data through the effect.""" if data is None: return None from engine.effects.types import EffectContext, apply_param_bindings w = ctx.params.viewport_width if ctx.params else 80 h = ctx.params.viewport_height if ctx.params else 24 frame = ctx.params.frame_number if ctx.params else 0 effect_ctx = EffectContext( terminal_width=w, terminal_height=h, scroll_cam=0, ticker_height=h, camera_x=0, mic_excess=0.0, grad_offset=(frame * 0.01) % 1.0, frame_number=frame, has_message=False, items=ctx.get("items", []), ) # Copy sensor state from PipelineContext to EffectContext for key, value in ctx.state.items(): if key.startswith("sensor."): effect_ctx.set_state(key, value) # Copy metrics from PipelineContext to EffectContext if "metrics" in ctx.state: effect_ctx.set_state("metrics", ctx.state["metrics"]) # Copy pipeline_order from PipelineContext services to EffectContext state pipeline_order = ctx.get("pipeline_order") if pipeline_order: effect_ctx.set_state("pipeline_order", pipeline_order) # Apply sensor param bindings if effect has them if hasattr(self._effect, "param_bindings") and self._effect.param_bindings: bound_config = apply_param_bindings(self._effect, effect_ctx) self._effect.configure(bound_config) return self._effect.process(data, effect_ctx)