diff --git a/engine/app/pipeline_runner.py b/engine/app/pipeline_runner.py index 61594da..d18b6b0 100644 --- a/engine/app/pipeline_runner.py +++ b/engine/app/pipeline_runner.py @@ -349,6 +349,9 @@ def run_pipeline_mode(preset_name: str = "demo"): print(f" \033[38;5;245mSwitching to preset: {preset_name}\033[0m") + # Save current UI panel state before rebuild + ui_state = ui_panel.save_state() if ui_panel else None + try: # Clean up old pipeline pipeline.cleanup() @@ -558,6 +561,10 @@ def run_pipeline_mode(preset_name: str = "demo"): ) stage_control.effect = effect # type: ignore[attr-defined] + # Restore UI panel state if it was saved + if ui_state: + ui_panel.restore_state(ui_state) + if ui_panel.stages: first_stage = next(iter(ui_panel.stages)) ui_panel.select_stage(first_stage) diff --git a/engine/pipeline/adapters/camera.py b/engine/pipeline/adapters/camera.py index 68068fd..2c5dd6e 100644 --- a/engine/pipeline/adapters/camera.py +++ b/engine/pipeline/adapters/camera.py @@ -16,6 +16,58 @@ class CameraStage(Stage): self.optional = True self._last_frame_time: float | None = None + def save_state(self) -> dict[str, Any]: + """Save camera state for restoration after pipeline rebuild. + + Returns: + Dictionary containing camera state that can be restored + """ + return { + "x": self._camera.x, + "y": self._camera.y, + "mode": self._camera.mode.value + if hasattr(self._camera.mode, "value") + else self._camera.mode, + "speed": self._camera.speed, + "zoom": self._camera.zoom, + "canvas_width": self._camera.canvas_width, + "canvas_height": self._camera.canvas_height, + "_x_float": getattr(self._camera, "_x_float", 0.0), + "_y_float": getattr(self._camera, "_y_float", 0.0), + "_time": getattr(self._camera, "_time", 0.0), + } + + def restore_state(self, state: dict[str, Any]) -> None: + """Restore camera state from saved state. + + Args: + state: Dictionary containing camera state from save_state() + """ + from engine.camera import CameraMode + + self._camera.x = state.get("x", 0) + self._camera.y = state.get("y", 0) + + # Restore mode - handle both enum value and direct enum + mode_value = state.get("mode", 0) + if isinstance(mode_value, int): + self._camera.mode = CameraMode(mode_value) + else: + self._camera.mode = mode_value + + self._camera.speed = state.get("speed", 1.0) + self._camera.zoom = state.get("zoom", 1.0) + self._camera.canvas_width = state.get("canvas_width", 200) + self._camera.canvas_height = state.get("canvas_height", 200) + + # Restore internal state + if hasattr(self._camera, "_x_float"): + self._camera._x_float = state.get("_x_float", 0.0) + if hasattr(self._camera, "_y_float"): + self._camera._y_float = state.get("_y_float", 0.0) + if hasattr(self._camera, "_time"): + self._camera._time = state.get("_time", 0.0) + @property def stage_type(self) -> str: return "camera" diff --git a/engine/pipeline/adapters/display.py b/engine/pipeline/adapters/display.py index 7808a84..7fa885c 100644 --- a/engine/pipeline/adapters/display.py +++ b/engine/pipeline/adapters/display.py @@ -13,6 +13,39 @@ class DisplayStage(Stage): self.name = name self.category = "display" self.optional = False + self._initialized = False + self._init_width = 80 + self._init_height = 24 + + def save_state(self) -> dict[str, Any]: + """Save display state for restoration after pipeline rebuild. + + Returns: + Dictionary containing display state that can be restored + """ + return { + "initialized": self._initialized, + "init_width": self._init_width, + "init_height": self._init_height, + "width": getattr(self._display, "width", 80), + "height": getattr(self._display, "height", 24), + } + + def restore_state(self, state: dict[str, Any]) -> None: + """Restore display state from saved state. + + Args: + state: Dictionary containing display state from save_state() + """ + self._initialized = state.get("initialized", False) + self._init_width = state.get("init_width", 80) + self._init_height = state.get("init_height", 24) + + # Restore display dimensions if the display supports it + if hasattr(self._display, "width"): + self._display.width = state.get("width", 80) + if hasattr(self._display, "height"): + self._display.height = state.get("height", 24) @property def capabilities(self) -> set[str]: @@ -37,7 +70,17 @@ class DisplayStage(Stage): def init(self, ctx: PipelineContext) -> bool: w = ctx.params.viewport_width if ctx.params else 80 h = ctx.params.viewport_height if ctx.params else 24 - result = self._display.init(w, h, reuse=False) + + # Try to reuse display if already initialized + reuse = self._initialized + result = self._display.init(w, h, reuse=reuse) + + # Update initialization state + if result is not False: + self._initialized = True + self._init_width = w + self._init_height = h + return result is not False def process(self, data: Any, ctx: PipelineContext) -> Any: diff --git a/engine/pipeline/controller.py b/engine/pipeline/controller.py index c867e26..e34184b 100644 --- a/engine/pipeline/controller.py +++ b/engine/pipeline/controller.py @@ -204,6 +204,15 @@ class Pipeline: if hasattr(old_stage, "_enabled"): new_stage._enabled = old_stage._enabled + # Preserve camera state + if hasattr(old_stage, "save_state") and hasattr(new_stage, "restore_state"): + try: + state = old_stage.save_state() + new_stage.restore_state(state) + except Exception: + # If state preservation fails, continue without it + pass + def _rebuild(self) -> None: """Rebuild execution order after mutation without full reinitialization.""" self._capability_map = self._build_capability_map() diff --git a/engine/pipeline/ui.py b/engine/pipeline/ui.py index 7876e67..8d206ec 100644 --- a/engine/pipeline/ui.py +++ b/engine/pipeline/ui.py @@ -78,6 +78,58 @@ class UIPanel: self._show_panel: bool = True # UI panel visibility self._preset_scroll_offset: int = 0 # Scroll in preset list + def save_state(self) -> dict[str, Any]: + """Save UI panel state for restoration after pipeline rebuild. + + Returns: + Dictionary containing UI panel state that can be restored + """ + # Save stage control states (enabled, params, etc.) + stage_states = {} + for name, ctrl in self.stages.items(): + stage_states[name] = { + "enabled": ctrl.enabled, + "selected": ctrl.selected, + "params": dict(ctrl.params), # Copy params dict + } + + return { + "stage_states": stage_states, + "scroll_offset": self.scroll_offset, + "selected_stage": self.selected_stage, + "_focused_param": self._focused_param, + "_show_panel": self._show_panel, + "_show_preset_picker": self._show_preset_picker, + "_preset_scroll_offset": self._preset_scroll_offset, + } + + def restore_state(self, state: dict[str, Any]) -> None: + """Restore UI panel state from saved state. + + Args: + state: Dictionary containing UI panel state from save_state() + """ + # Restore stage control states + stage_states = state.get("stage_states", {}) + for name, stage_state in stage_states.items(): + if name in self.stages: + ctrl = self.stages[name] + ctrl.enabled = stage_state.get("enabled", True) + ctrl.selected = stage_state.get("selected", False) + # Restore params + saved_params = stage_state.get("params", {}) + for param_name, param_value in saved_params.items(): + if param_name in ctrl.params: + ctrl.params[param_name] = param_value + + # Restore UI panel state + self.scroll_offset = state.get("scroll_offset", 0) + self.selected_stage = state.get("selected_stage") + self._focused_param = state.get("_focused_param") + self._show_panel = state.get("_show_panel", True) + self._show_preset_picker = state.get("_show_preset_picker", False) + self._preset_scroll_offset = state.get("_preset_scroll_offset", 0) + def register_stage(self, stage: Any, enabled: bool = True) -> StageControl: """Register a stage for UI control.