diff --git a/crates/engine/src/cmd/atn_cmds/path_tests.rs b/crates/engine/src/cmd/atn_cmds/path_tests.rs index 4b6907db91..25efc33c0d 100644 --- a/crates/engine/src/cmd/atn_cmds/path_tests.rs +++ b/crates/engine/src/cmd/atn_cmds/path_tests.rs @@ -148,7 +148,10 @@ fn source_wire_identity_is_retained_only_for_unchanged_geometry() { #[test] fn unknown_and_unrepresented_settings_are_not_silently_lost() { let base = fixture("Sbtr", false, false); - for d in [base.clone().with("extra", D::Boolean(true)), base.clone().with("T ", D::List(Vec::new())), fixture("Intr", false, false)] { + let mut empty_components = base.clone(); + let (_, components) = empty_components.items.iter_mut().find(|(key, _)| key.is("T ")).unwrap(); + *components = D::List(Vec::new()); + for d in [base.clone().with("extra", D::Boolean(true)), empty_components, fixture("Intr", false, false)] { assert!(matches!(from(&d), Mapped::Unsupported { harmless: false })); } let p = params(&base); diff --git a/crates/engine/src/jobs.rs b/crates/engine/src/jobs.rs index bffc99c62d..7deb6a006f 100644 --- a/crates/engine/src/jobs.rs +++ b/crates/engine/src/jobs.rs @@ -507,9 +507,9 @@ struct Running { label: String, params: Value, journal: bool, - /// The recording (`ActionState::take`) running when the job started, if any (set by - /// `dispatch`, once recording resumes). Only that recording gets the job's step: one started - /// later must not pick up an edit made before it. + /// The take active at dispatch, including strokes started with tool recording off. + started_in_take: Option, + /// The take eligible to receive this job's step under the mode at dispatch. recorded_by: Option, pre_selected: Option>, before: crate::actions_record::Before, @@ -863,6 +863,7 @@ fn new_job( label: label.to_string(), params: Value::Null, journal: true, + started_in_take: None, recorded_by: None, pre_selected: None, before: crate::actions_record::Before::capture(s), @@ -1031,9 +1032,9 @@ pub struct PendingPsdPreflight { } impl Session { - /// A recording mode change must not reclassify a painting job that is still in flight. + /// A recording mode change must not reclassify an in-flight stroke from this take. pub(crate) fn tool_stroke_job_running(&self) -> bool { - self.jobs.running.iter().any(|job| crate::actions_cmds::tool_stroke(&job.command)) + self.jobs.running.iter().any(|job| job.started_in_take == Some(self.actions.take) && crate::actions_cmds::tool_stroke(&job.command)) } /// Run the heavy part of inline (not background) job commands under `ctx`, so another thread @@ -1816,8 +1817,8 @@ impl Session { job.command = id.to_string(); job.params = params; job.journal = spec.journal; - job.recorded_by = - self.actions.recording.filter(|_| self.actions.record_tools || !crate::actions_cmds::tool_stroke(id)).map(|_| self.actions.take); + job.started_in_take = self.actions.recording.map(|_| self.actions.take); + job.recorded_by = job.started_in_take.filter(|_| self.actions.record_tools || !crate::actions_cmds::tool_stroke(id)); job.pre_selected = pre_selected.clone(); job.before = std::mem::take(&mut record_before); let jid = job.id; diff --git a/crates/engine/tests/it/actions_panel_menu.rs b/crates/engine/tests/it/actions_panel_menu.rs index eca36d62e3..6b65f2f08d 100644 --- a/crates/engine/tests/it/actions_panel_menu.rs +++ b/crates/engine/tests/it/actions_panel_menu.rs @@ -67,6 +67,32 @@ fn background_stroke_ownership_does_not_suppress_ordinary_filter_jobs() { } } +/// A Remove job started outside the current take neither blocks its toggle nor joins its steps. +#[test] +fn background_strokes_from_outside_the_current_take_do_not_block_the_toggle() { + for previous_take in [false, true] { + let mut s = session(); + stroke(&mut s, "#bb3300"); + if previous_take { + s.execute("actions.record", json!({"action":"A"})).unwrap(); + s.execute("actions.allowToolRecording", json!({"enabled":true})).unwrap(); + } + let Started::Job(job) = s.start("paint.remove", json!({"points":[[20,15],[21,15]],"size":3})).unwrap() else { + panic!("expected the real Remove job"); + }; + if previous_take { + s.execute("actions.stop", json!({})).unwrap(); + s.execute("actions.allowToolRecording", json!({"enabled":false})).unwrap(); + } + s.execute("actions.record", json!({"action":"A"})).unwrap(); + assert_eq!(s.execute("actions.allowToolRecording", json!({"enabled":true})).unwrap()["enabled"], true); + s.wait_job(job).unwrap(); + s.execute("actions.stop", json!({})).unwrap(); + assert!(s.actions.list[0].steps.is_empty()); + assert_eq!(s.journal.iter().filter(|step| step.0 == "paint.remove").count(), 1); + } +} + /// Interleaved membership and numeric calls expose incorrect folder placement or target rewriting. #[test] fn set_duplication_preserves_metadata_call_targets_and_empty_folder_order() {