From bb08a6acc970e7e1419867000c8f7529725b3f19 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:28:38 +0000 Subject: [PATCH] Work area: an edit never clamps with crossed bounds (#605 follow-up) A project saved with an inverted work area, or a comp shorter than a frame, made Tick::clamp panic on the next start or end edit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f --- crates/engine/src/commands/comp.rs | 5 +++-- crates/engine/src/tests_menu_cmds.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/commands/comp.rs b/crates/engine/src/commands/comp.rs index 2d7dafa8..bf1fd530 100644 --- a/crates/engine/src/commands/comp.rs +++ b/crates/engine/src/commands/comp.rs @@ -182,10 +182,11 @@ fn work_area(s: &mut Session, p: &Value) -> Result { _ => {} } if let Some(b) = begin { - c.work_area.0 = b.clamp(Tick::ZERO, c.duration - fd); + // min/max, not clamp: the bounds cross on a comp shorter than a frame or an inverted area. + c.work_area.0 = b.min(c.duration - fd).max(Tick::ZERO); } if let Some(e) = end { - c.work_area.1 = e.clamp(c.work_area.0 + fd, c.duration); + c.work_area.1 = e.max(c.work_area.0 + fd).min(c.duration); } // A start on its own can land after the current end. Pull it back so the interval // still covers one frame, the same rule as `set: "begin"`. diff --git a/crates/engine/src/tests_menu_cmds.rs b/crates/engine/src/tests_menu_cmds.rs index c20e0159..df0af369 100644 --- a/crates/engine/src/tests_menu_cmds.rs +++ b/crates/engine/src/tests_menu_cmds.rs @@ -196,6 +196,7 @@ fn separated_position_follows_canvas_resize_and_crop() { placed(&separated, b, 1.0, 50.0, 30.0); assert_eq!(opaque_bounds(&separated, Tick::ZERO), (70, 70, 25, 25, 34, 34, 100)); assert_eq!(opaque_bounds(&separated, Tick::from_seconds_f64(1.0)), (70, 70, 45, 25, 54, 34, 100)); +} /// Pixel-centre of the opaque samples, so a 10×10 solid centred on (11, 22) reports (11, 22). fn opaque_centroid(s: &Session) -> (f64, f64, u32) { @@ -399,6 +400,19 @@ fn lift_and_extract_work_area() { assert!((spans[1].0 - 2.0).abs() < 1e-6 && (spans[1].1 - 8.0).abs() < 1e-6, "{spans:?}"); } +/// A project saved with an inverted work area (by an older build) is edited without a panic: +/// `clamp` with crossed bounds would abort. +#[test] +fn an_inverted_saved_work_area_takes_an_end_edit() { + let mut s = comp(); + let cid = s.active_comp_id().unwrap(); + std::sync::Arc::make_mut(&mut s.project).comp_mut(cid).unwrap().work_area = (Tick::from_seconds_f64(9.0), Tick::from_seconds_f64(2.0)); + s.execute("comp.workArea", json!({"end": 1.0})).unwrap(); + s.execute("comp.workArea", json!({"start": 50.0})).unwrap(); + let c = s.active_comp().unwrap(); + assert!(c.work_area.0 >= Tick::ZERO && c.work_area.0 + c.frame_duration() <= c.work_area.1 && c.work_area.1 <= c.duration); +} + #[test] fn work_area_stays_ordered_when_the_start_passes_the_end() { let ordered = |s: &Session| {