Scope Actions tool recording guard to the active take

This commit is contained in:
yoriswind committed 2026-11-06 13:40:05 -08:00
1 parent cfda1d893b
commit 2e210c0aae
3 files changed
+38 -8

No files matched your search

+4 -1
View File
@@ -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);
+8 -7
View File
@@ -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<u64>,
/// The take eligible to receive this job's step under the mode at dispatch.
recorded_by: Option<u64>,
pre_selected: Option<Vec<LayerId>>,
before: crate::actions_record::Before,
@@ -863,6 +863,7 @@ fn new_job<T: Send + 'static>(
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;
@@ -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() {