forked from genewildish/Mainline
feat: Complete pipeline hot-rebuild implementation with acceptance tests
- Implements pipeline hot-rebuild with state preservation (issue #43) - Adds auto-injection of MVP stages for missing capabilities - Adds radial camera mode for polar coordinate scanning - Adds afterimage and motionblur effects using framebuffer history - Adds comprehensive acceptance tests for camera modes and pipeline rebuild - Updates presets.toml with new effect configurations Related to: #35 (Pipeline Mutation API epic) Closes: #43, #44, #45
This commit is contained in:
@@ -45,8 +45,19 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
print(f" \033[38;5;245mPreset: {preset.name} - {preset.description}\033[0m")
|
||||
|
||||
params = preset.to_params()
|
||||
params.viewport_width = 80
|
||||
params.viewport_height = 24
|
||||
# Use preset viewport if available, else default to 80x24
|
||||
params.viewport_width = getattr(preset, "viewport_width", 80)
|
||||
params.viewport_height = getattr(preset, "viewport_height", 24)
|
||||
|
||||
if "--viewport" in sys.argv:
|
||||
idx = sys.argv.index("--viewport")
|
||||
if idx + 1 < len(sys.argv):
|
||||
vp = sys.argv[idx + 1]
|
||||
try:
|
||||
params.viewport_width, params.viewport_height = map(int, vp.split("x"))
|
||||
except ValueError:
|
||||
print("Error: Invalid viewport format. Use WxH (e.g., 40x15)")
|
||||
sys.exit(1)
|
||||
|
||||
pipeline = Pipeline(
|
||||
config=PipelineConfig(
|
||||
@@ -156,25 +167,12 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
list_source = ListDataSource(items, name=preset.source)
|
||||
pipeline.add_stage("source", DataSourceStage(list_source, name=preset.source))
|
||||
|
||||
# Add FontStage for headlines/poetry (default for demo)
|
||||
if preset.source in ["headlines", "poetry"]:
|
||||
from engine.pipeline.adapters import FontStage, ViewportFilterStage
|
||||
|
||||
# Add viewport filter to prevent rendering all items
|
||||
pipeline.add_stage(
|
||||
"viewport_filter", ViewportFilterStage(name="viewport-filter")
|
||||
)
|
||||
pipeline.add_stage("font", FontStage(name="font"))
|
||||
else:
|
||||
# Fallback to simple conversion for other sources
|
||||
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
||||
|
||||
# Add camera stage if specified in preset (after font/render stage)
|
||||
# Add camera state update stage if specified in preset (must run before viewport filter)
|
||||
camera = None
|
||||
if preset.camera:
|
||||
from engine.camera import Camera
|
||||
from engine.pipeline.adapters import CameraStage
|
||||
from engine.pipeline.adapters import CameraClockStage, CameraStage
|
||||
|
||||
camera = None
|
||||
speed = getattr(preset, "camera_speed", 1.0)
|
||||
if preset.camera == "feed":
|
||||
camera = Camera.feed(speed=speed)
|
||||
@@ -190,9 +188,35 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
camera = Camera.floating(speed=speed)
|
||||
elif preset.camera == "bounce":
|
||||
camera = Camera.bounce(speed=speed)
|
||||
elif preset.camera == "radial":
|
||||
camera = Camera.radial(speed=speed)
|
||||
elif preset.camera == "static" or preset.camera == "":
|
||||
# Static camera: no movement, but provides camera_y=0 for viewport filter
|
||||
camera = Camera.scroll(speed=0.0) # Speed 0 = no movement
|
||||
camera.set_canvas_size(200, 200)
|
||||
|
||||
if camera:
|
||||
pipeline.add_stage("camera", CameraStage(camera, name=preset.camera))
|
||||
# Add camera update stage to ensure camera_y is available for viewport filter
|
||||
pipeline.add_stage(
|
||||
"camera_update", CameraClockStage(camera, name="camera-clock")
|
||||
)
|
||||
|
||||
# Add FontStage for headlines/poetry (default for demo)
|
||||
if preset.source in ["headlines", "poetry"]:
|
||||
from engine.pipeline.adapters import FontStage, ViewportFilterStage
|
||||
|
||||
# Add viewport filter to prevent rendering all items
|
||||
pipeline.add_stage(
|
||||
"viewport_filter", ViewportFilterStage(name="viewport-filter")
|
||||
)
|
||||
pipeline.add_stage("font", FontStage(name="font"))
|
||||
else:
|
||||
# Fallback to simple conversion for other sources
|
||||
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
||||
|
||||
# Add camera stage if specified in preset (after font/render stage)
|
||||
if camera:
|
||||
pipeline.add_stage("camera", CameraStage(camera, name=preset.camera))
|
||||
|
||||
for effect_name in preset.effects:
|
||||
effect = effect_registry.get(effect_name)
|
||||
@@ -451,7 +475,7 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
# Add camera if specified
|
||||
if new_preset.camera:
|
||||
from engine.camera import Camera
|
||||
from engine.pipeline.adapters import CameraStage
|
||||
from engine.pipeline.adapters import CameraClockStage, CameraStage
|
||||
|
||||
speed = getattr(new_preset, "camera_speed", 1.0)
|
||||
camera = None
|
||||
@@ -468,8 +492,19 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
camera = Camera.floating(speed=speed)
|
||||
elif cam_type == "bounce":
|
||||
camera = Camera.bounce(speed=speed)
|
||||
elif cam_type == "radial":
|
||||
camera = Camera.radial(speed=speed)
|
||||
elif cam_type == "static" or cam_type == "":
|
||||
# Static camera: no movement, but provides camera_y=0 for viewport filter
|
||||
camera = Camera.scroll(speed=0.0)
|
||||
camera.set_canvas_size(200, 200)
|
||||
|
||||
if camera:
|
||||
# Add camera update stage to ensure camera_y is available for viewport filter
|
||||
pipeline.add_stage(
|
||||
"camera_update",
|
||||
CameraClockStage(camera, name="camera-clock"),
|
||||
)
|
||||
pipeline.add_stage("camera", CameraStage(camera, name=cam_type))
|
||||
|
||||
# Add effects
|
||||
@@ -637,10 +672,11 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
ctx.set("pipeline_order", pipeline.execution_order)
|
||||
ctx.set("camera_y", 0)
|
||||
|
||||
current_width = 80
|
||||
current_height = 24
|
||||
current_width = params.viewport_width
|
||||
current_height = params.viewport_height
|
||||
|
||||
if hasattr(display, "get_dimensions"):
|
||||
# Only get dimensions from display if viewport wasn't explicitly set
|
||||
if "--viewport" not in sys.argv and hasattr(display, "get_dimensions"):
|
||||
current_width, current_height = display.get_dimensions()
|
||||
params.viewport_width = current_width
|
||||
params.viewport_height = current_height
|
||||
@@ -687,7 +723,7 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
display.clear_quit_request()
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
if hasattr(display, "get_dimensions"):
|
||||
if "--viewport" not in sys.argv and hasattr(display, "get_dimensions"):
|
||||
new_w, new_h = display.get_dimensions()
|
||||
if new_w != current_width or new_h != current_height:
|
||||
current_width, current_height = new_w, new_h
|
||||
|
||||
Reference in New Issue
Block a user