Merge pull request #4379 from dotCooCoo/fix/shape-builder-merge-off-ui-thread

Shape Builder: a large selection merges on another thread
This commit is contained in:
BFlat
2026-11-09 13:10:54 +08:00
committed by GitHub
40 changed files with 2215 additions and 154 deletions
@@ -1,5 +1,5 @@
use super::{
Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, SubPath, Touched, Value, b, bad, check_numbers,
Arc, Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, Touched, Value, b, bad, check_numbers,
f64_or, fill_param, finite, gap_param, ids_json, json, marquee_param, po, points_param, remnant, replace_leaves, replace_with_pieces, root_ids,
shape_node, switch_or,
};
@@ -41,6 +41,7 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
let gap = gap_param(p);
// The map the Shape Builder tool already has, which a click or drag of it is made over.
let shared = s.tool.shape_builder_map().filter(|(_, g)| *g == gap);
let prepared = s.tool.shape_builder_merge();
let label = if erase { "Shape Builder Delete" } else { "Shape Builder" };
let (ids, merged, touched, split) = s.edit(label, |d, sel| {
let (shapes, leaves) = b::shape_builder_shapes(d, &roots, open_filled_as_closed);
@@ -89,16 +90,28 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
touched.add(map.hit(q, Some(tol)));
return Ok((ids, None, touched, Some(split)));
}
let mut touched = Touched { regions: marquee.map(|(a, b)| map.regions_in_marquee(a, b)).unwrap_or_default(), ..Default::default() };
touched.extend(&map.builder_hits_along(&pts, piece_tol));
touched.edges = map.erased_edges(&touched);
let touched = map.merge_touched(&pts, marquee, piece_tol);
if touched.is_empty() {
return Err(EngineError::Other("Shape Builder: no region under the pointer".into()));
}
let cuts: Vec<&SubPath> = touched.edges.iter().filter_map(|&k| map.edges.get(k)?.0.path.subpaths.first()).collect();
let inputs = b::merge_inputs(&leaves.iter().collect::<Vec<_>>(), open_filled_as_closed);
// The tool computed this merge on another thread when it would come out the same.
let ready = prepared.as_ref().filter(|m| {
shared.as_ref().is_some_and(|(s, _)| Arc::ptr_eq(s, &m.map) && std::ptr::eq(s.as_ref(), map))
&& m.gap == gap
&& m.touched == touched
&& m.inputs == inputs
});
let computed;
let geometry = match ready {
Some(m) => &m.geometry,
None => {
computed = b::merge_geometry(map, &touched, gap, &inputs);
&computed
}
};
let picked: Vec<&po::Region> = touched.regions.iter().filter_map(|&i| map.regions.get(i).map(|r| &r.0)).collect();
let faces = po::FaceMerger::with_gap(&map.shapes, gap_param(p));
let union = if picked.is_empty() { PathData::default() } else { faces.merge(&picked) };
let union = &geometry.union;
// Pick Color From Artwork: the merged shape takes the look of the front-most object under the
// drag's first point, else under its last point, else of the front-most object. Color
// Swatches: the current fill, with the rest of the look of the front-most object over the
@@ -115,7 +128,7 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
// What goes in place of each object the map was made from (none: it stays as it is).
let mut outs: Vec<Option<Vec<Node>>> = vec![];
let mut merged_id = None;
for (i, (sh, leaf)) in map.shapes.iter().zip(&leaves).enumerate() {
for (i, leaf) in leaves.iter().take(map.shapes.len()).enumerate() {
let mut out = None;
let pieces: Vec<usize> = map.lines.iter().enumerate().filter(|(_, (l, _))| l.key == i as u64).map(|(k, _)| k).collect();
if !pieces.is_empty() {
@@ -131,41 +144,15 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
}
out = Some(kept);
}
} else {
} else if let Some(Some(rest)) = geometry.rests.get(i) {
// A shape loses the regions deleted or merged, then the edges deleted (each one on
// its outline, shared edges from every shape along them).
let in_union = picked.iter().any(|r| r.sources.contains(&i));
let mut rest = if in_union {
// What is left of it: its faces not merged or deleted, united. They share their
// edges with the merged faces exactly, where subtracting the merged shape left
// slivers of the outline along them (#569). A sliver goes in only where the
// shape covers it and the merged shape does not take it in.
let kept: Vec<&po::Region> = map
.regions
.iter()
.enumerate()
.filter(|(k, (r, ..))| r.sources.contains(&i) && !touched.regions.contains(k))
.map(|(_, (r, ..))| r)
.collect();
if kept.is_empty() { PathData::default() } else { faces.merge_rest(i, &kept, &picked) }
// its outline, shared edges from every shape along them). What is left of its own
// outline keeps its fill rule; united faces fill with non-zero.
out = Some(if rest.path.is_empty() {
vec![]
} else {
b::node_outline(leaf).map_or_else(|| sh.path.clone(), |o| o.0)
};
let fill_closes = open_filled_as_closed && !leaf.appearance.fill_paint().is_none();
// Edges come back from the planar map within a few hundredths of the outlines they
// follow, whatever else is selected.
let cut_tol = b::outline_snap(&sh.path);
let mut cut = false;
for piece in &cuts {
if let Some(r) = po::cut_out(&rest, piece, fill_closes, cut_tol) {
rest = r;
cut = true;
}
}
if in_union || cut {
// What is left of its own outline keeps its fill rule; united faces fill with non-zero.
out = Some(if rest.is_empty() { vec![] } else { vec![remnant(d, leaf, rest, in_union.then_some(FillRule::NonZero))] });
}
vec![remnant(d, leaf, rest.path.clone(), rest.united.then_some(FillRule::NonZero))]
});
}
// The merged shape goes in place of the object it takes its look from, so it stays in
// that object's group (a clip group too).
+2
View File
@@ -752,6 +752,8 @@ pub(super) fn encode_ai(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Res
/// file becomes the document's own; the live document gets them in [`SaveJob::finish`]). Bad
/// options fail here, before anything is encoded.
pub fn save_job(s: &mut Session, plan: SavePlan) -> Result<SaveJob> {
// A Shape Builder merge still being computed for a click is saved with the document.
s.finish_tool_edit_later()?;
// The web build, without a clock, leaves the dates unless given one.
let at = if plan.path.is_some() && plan.retargets() { clock_date(s, "modified", plan.modified) } else { None };
job_for_stamped(s, s.doc()?, plan, at)
+6
View File
@@ -678,6 +678,12 @@ impl Session {
for e in self.prefs.keep_folders(&mut p) {
log::warn!("{e}");
}
// The edit the active tool computes on another thread is applied first (the Preferences
// dialog applies its settings here, outside a command). A failure to apply it was the
// edit's own, and the preferences apply all the same.
if self.depth == 0 {
let _ = self.finish_tool_edit_later();
}
let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions;
let history_changed = p.history_states != self.prefs.history_states;
let tile_edge_changed = p.pattern_tile_edge_color != self.prefs.pattern_tile_edge_color;
+29 -1
View File
@@ -1,4 +1,4 @@
use super::{NodeId, Value, cmd};
use super::{NodeId, Value, cmd, find_command};
/// The commands that act on the active artboard (#693), which the app passes as their `artboard`:
/// Paste in Place, in Front and in Back paste onto it, Align to Artboard aligns to it, Distribute
@@ -78,6 +78,34 @@ pub(crate) fn strip_replay_only(id: &str, p: &mut Value) {
}
}
/// Queries that apply the edit the active tool is computing before they run, as edits do
/// ([`Session::finish_tool_edit`]): a batch runs edits, Find Objects with `select`, Select All
/// Instances and Lock Guides change the selection, and the proof views, the preferences and added
/// fonts give the documents new revisions (they draw again, and type lays out again).
const WAIT_FOR_TOOL_EDIT: &[&str] = &[
"command.batch",
"document.find",
"symbol.selectInstances",
"view.guides.lock",
"view.proofSetup",
"view.proofColors",
"view.overprintPreview",
"view.separationsPreview",
"prefs.set",
"prefs.reset",
"text.addFontFiles",
];
/// Does command `id`, run on its own, first apply the edit the active tool is computing on
/// another thread and the input held for it ([`Session::finish_tool_edit`])? Every command that
/// can edit, select, save or give a document a new revision does; queries that only read do not
/// (`commands_that_do_not_wait_for_a_tool_edit_change_no_document` in the command sweep checks
/// them). A frontend that adds params from
/// its own state (the active artboard) applies them first itself.
pub fn waits_for_tool_edit(id: &str) -> bool {
find_command(id).is_some_and(|spec| spec.journal || WAIT_FOR_TOOL_EDIT.contains(&id))
}
/// Journaled commands that write a file and change nothing in the document: with an object of a
/// dynamic symbol instance's art selected, they read the instance as queries do
/// ([`crate::Session::execute`]).
+14 -1
View File
@@ -62,6 +62,19 @@ pub struct Session {
/// While Cmd lends `tool` (a selection tool) for a drag: the tool it was lent to, which comes
/// back as it was at the release ([`Session::pointer`]).
pub(crate) lender: Option<Box<dyn Tool>>,
/// Pointer and key input that came while the tool computed an edit on another thread, each with
/// its number and view, made in order once the edit is applied.
held_input: std::collections::VecDeque<(u64, tooling::HeldInput, ViewInfo)>,
/// The number the next pointer or key event handed to the session gets.
input_count: u64,
/// The number of the event whose handling started the edit the tool is computing.
edit_started_by: Option<u64>,
/// Set while the tool's edit and the input held for it are applied: the commands that run then
/// do not start applying them again.
settling_tool_edit: bool,
/// Requests for the UI from making held input where nothing could take them (a command, a tool
/// switch), which the next [`Session::tool_poll`] returns.
held_ui: Vec<UiRequest>,
/// The active document (its uid) and its [`History::step`] when the tool last handled a pointer
/// event or drew its overlays: when they differ at the next one, the tool is told first
/// ([`vectorcraft_tools::Tool::history_moved`]). None until the first.
@@ -308,7 +321,7 @@ mod session;
pub(crate) use command_sets::SELECTS_GIVEN_IDS;
#[cfg(test)]
use command_sets::given_ids;
pub use command_sets::{FIT_ACTIVE_ARTBOARD, ON_ACTIVE_ARTBOARD};
pub use command_sets::{FIT_ACTIVE_ARTBOARD, ON_ACTIVE_ARTBOARD, waits_for_tool_edit};
pub use doc_state::{DocState, OpenRows, RecalledSelection};
pub use error::{EngineError, Result};
pub use history::{History, HistoryEntry, NamedStep, default_history_budget};
+1 -1
View File
@@ -1,4 +1,4 @@
use super::command_sets::{JOURNALED_READS, SELECTS_GIVEN_IDS, given_ids, strip_replay_only};
use super::command_sets::{JOURNALED_READS, SELECTS_GIVEN_IDS, given_ids, strip_replay_only, waits_for_tool_edit};
use super::doc_state::DocState;
use super::error::{EngineError, Result};
use super::history::NamedStep;
+8
View File
@@ -46,6 +46,11 @@ impl Session {
journal: Journal::default(),
tool: vectorcraft_tools::create("selection"),
lender: None,
held_input: Default::default(),
input_count: 0,
edit_started_by: None,
settling_tool_edit: false,
held_ui: vec![],
tool_step: None,
selection_at_lend: None,
lent_selection: None,
@@ -160,6 +165,9 @@ impl Session {
}
self.press_type_selection = None;
let view = self.last_view;
// The edit the tool computes on another thread and the input held for it belong to the
// document being left. The switch goes on whatever they did, as for the deactivation.
let _ = self.finish_tool_edit_later();
// The switch goes on whatever the lent tool's last actions did, as for the deactivation.
let _ = self.give_back_tool(view);
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
+5
View File
@@ -230,6 +230,11 @@ impl Session {
if label != "Typing" && cmd::typing_in_progress(self) {
cmd::settle_interaction(self)?;
}
// An edit the tool computes on another thread (a Shape Builder merge) is applied first too,
// so the snapshot the interaction previews from and goes back to holds it.
if !self.in_interaction() {
self.finish_tool_edit_later()?;
}
self.clear_deferred_overlay_cache();
let paint = self.paint.clone();
let st = self.doc_mut()?;
+7 -1
View File
@@ -1,6 +1,6 @@
use super::{
CommandSpec, EngineError, JOURNALED_READS, NodeId, Result, SELECTS_GIVEN_IDS, Selection, Session, Value, cmd, find_command, given_ids, guard,
strip_replay_only,
strip_replay_only, waits_for_tool_edit,
};
impl Session {
@@ -39,6 +39,12 @@ impl Session {
/// Execute a command by id. This is THE entry point for every frontend.
pub fn execute(&mut self, id: &str, params: &Value) -> Result<Value> {
let spec = find_command(id).ok_or_else(|| cmd::params::unknown_command(id))?;
// An edit the active tool is computing on another thread (a Shape Builder merge) is
// applied before a command that may edit, select, save or give a document a new revision,
// as the click came first. Queries that only read do not wait for it.
if self.depth == 0 && waits_for_tool_edit(id) {
self.finish_tool_edit_later()?;
}
let outer = std::mem::replace(&mut self.reading, !spec.journal || JOURNALED_READS.contains(&id));
let r = self.execute_spec(spec, id, params);
self.reading = outer;
+4
View File
@@ -207,6 +207,10 @@ impl Session {
/// [`Session::end_undo_scope`]: open an undo group there or, when one is open already, join it,
/// so that the edits are part of its step. `None` without a document.
pub fn begin_undo_scope(&mut self) -> Option<UndoScope> {
// An edit the tool computes on another thread is a step before the scope, so the scope's
// snapshot holds it. A failure to apply it was the edit's own, and the scope begins all the
// same.
let _ = self.finish_tool_edit_later();
let journal = self.journal.len();
let st = self.active()?;
let uid = st.uid;
+480
View File
@@ -607,6 +607,486 @@ fn shape_builder_merge_uses_the_map_the_tool_has() {
assert_eq!(maps_built(), built + 1, "another gap");
}
/// 40 squares 20 pt wide in a row, each 10 pt right of the one before (160 segments, which the
/// Shape Builder merges on another thread), all selected.
fn square_row(s: &mut Session) -> Vec<NodeId> {
let ids: Vec<NodeId> = (0..40).map(|k| rect(s, f64::from(k) * 10.0, 0.0, 20.0, 20.0)).collect();
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
ids
}
/// The active document of `s` without its dates, so documents made in two sessions at different
/// times compare equal when their art is.
fn undated(s: &Session) -> vectorcraft_doc::Document {
let mut d = (*s.doc().unwrap().doc).clone();
(d.metadata.created, d.metadata.modified) = (None, None);
d
}
/// The Shape Builder over [`square_row`], its map built, after a click at (15, 10): the merge is
/// being computed on another thread, and the document is as it was.
fn click_row(s: &mut Session, v: ViewInfo) {
s.select_tool("shapeBuilder", v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Move, 15.0, 10.0), v).unwrap();
s.tool_poll(v, true).unwrap();
let revision = s.doc().unwrap().revision;
s.pointer(&PointerEvent::new(PointerKind::Down, 15.0, 10.0), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Up, 15.0, 10.0), v).unwrap();
assert!(s.tool_editing() && s.tool_waiting());
assert_eq!(s.doc().unwrap().revision, revision, "the release returns before the merge is applied");
}
/// A click of the Shape Builder on a large selection merges on another thread, and the merge the
/// next poll applies is the command's own, as one undo step.
#[test]
fn shape_builder_merges_a_large_selection_on_another_thread() {
use vectorcraft_tools::builder::merges_computed;
let v = ViewInfo::default();
let mut s = session();
square_row(&mut s);
click_row(&mut s, v);
let computed = merges_computed();
s.tool_poll(v, true).unwrap();
assert!(!s.tool_editing());
assert_eq!(merges_computed(), computed, "the command takes the merge computed on another thread");
assert_eq!(s.journal.last().unwrap().0, "shapeBuilder.merge");
assert_eq!(layer_children(&s), 41, "the overlap and what is left of the two squares over it");
let mut direct = session();
square_row(&mut direct);
direct.execute("shapeBuilder.merge", &json!({"points": [[15, 10]]})).unwrap();
assert!(undated(&s) == undated(&direct), "the same document as the command gives");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(layer_children(&s), 40, "one undo step");
}
/// What comes after a click whose merge is being computed on another thread waits for the merge
/// and runs on the merged art, as when the merge ran on the calling thread: a command (an edit,
/// an undo, a selection, a save, a batch, Lock Guides, the proof views, a preference, Add Font
/// Files), an option change, another tool, a save made outside a command, preferences applied
/// outside a command, a dialog's preview, a scrubbed field's undo scope, a Cmd-click or Cmd-drag
/// with the selection tool. The merge is the one computed on the other thread.
#[test]
fn shape_builder_applies_a_merge_before_what_comes_next() {
use vectorcraft_tools::builder::merges_computed;
type Next = (&'static str, fn(&mut Session, &[NodeId], &std::path::Path));
let v = ViewInfo::default();
let next: Vec<Next> = vec![
("Select All Instances", |s, _, _| {
s.execute("symbol.selectInstances", &json!({"name": "S"})).unwrap();
assert_eq!(s.doc().unwrap().selection.objects.len(), 1);
}),
("an edit", |s, _, _| {
s.execute("object.move", &json!({"dx": 0, "dy": 100})).unwrap();
}),
("an undo", |s, _, _| {
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(layer_children(s), 40, "the undo takes the merge back");
s.execute("edit.redo", &json!({})).unwrap();
}),
("another selection", |s, _, _| {
s.execute("select.inverse", &json!({})).unwrap();
}),
("Select All", |s, _, _| {
s.execute("select.all", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().selection.objects.len(), 41);
}),
("a save", |s, _, dir| {
s.execute("file.saveAs", &json!({"path": dir.join("a.vectorcraft").to_string_lossy()})).unwrap();
assert!(!s.doc().unwrap().is_dirty(), "the file holds the merge");
}),
("a save outside a command", |s, _, dir| {
let p = json!({"path": dir.join("b.vectorcraft").to_string_lossy()});
let plan = cmd::fileio::save_plan(s, cmd::fileio::SaveMode::SaveAs, &p).unwrap();
assert_eq!(layer_children(s), 40, "planning a save leaves the document");
let job = cmd::fileio::save_job(s, plan).unwrap();
assert_eq!(layer_children(s), 41, "the snapshot is taken after the merge");
drop(job);
}),
("a batch", |s, _, _| {
s.execute("command.batch", &json!({"commands": [{"command": "object.move", "params": {"dx": 0, "dy": 50}}]})).unwrap();
}),
("an option change", |s, _, _| {
s.set_tool_option_cmd(&json!({"key": "gapDetection", "value": true})).unwrap();
}),
("another tool", |s, _, _| {
s.select_tool("selection", ViewInfo::default()).unwrap();
}),
("a closed document", |s, _, _| {
s.execute("file.close", &json!({})).unwrap();
}),
("Lock Guides", |s, _, _| {
s.execute("view.guides.lock", &json!({"locked": true})).unwrap();
}),
("an option set from a panel", |s, _, _| {
s.set_tool_option("gapDetection", &json!(true));
}),
("a dialog's preview, canceled", |s, _, _| {
s.begin_interaction("Move").unwrap();
s.preview("object.move", &json!({"dx": 7, "dy": 0})).unwrap();
s.preview("object.move", &json!({"dx": 9, "dy": 0})).unwrap();
s.cancel_interaction().unwrap();
assert_eq!(layer_children(s), 41, "canceling the preview keeps the merge");
}),
("a dialog's preview, kept", |s, _, _| {
s.begin_interaction("Move").unwrap();
s.preview("object.move", &json!({"dx": 7, "dy": 0})).unwrap();
s.commit_interaction().unwrap();
let labels: Vec<&str> = s.doc().unwrap().history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Move", "Shape Builder"]);
}),
("a scrubbed field, canceled", |s, _, _| {
let scope = s.begin_undo_scope().unwrap();
s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap();
s.end_undo_scope(scope, true);
assert_eq!(
s.doc().unwrap().history.undo.last().map(|e| e.label.as_str()),
Some("Shape Builder"),
"the scrub's step is undone, not the merge"
);
}),
("a held click, then another tool", |s, _, _| {
let v = ViewInfo::default();
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
}
assert!(s.tool_editing(), "the click is held");
s.select_tool("selection", v).unwrap();
let labels: Vec<&str> = s.doc().unwrap().history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Shape Builder", "Shape Builder"], "the held click merges with the Shape Builder first");
}),
("a held click, then another document", |s, _, _| {
let v = ViewInfo::default();
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
}
let first = s.active_index().unwrap();
s.add_document(vectorcraft_doc::Document::new(100.0, 100.0), None);
assert!(!s.tool_editing());
let labels: Vec<&str> = s.documents()[first].history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Shape Builder", "Shape Builder"], "the held click merges in its own document first");
s.set_active(first);
}),
("a Cmd-click", |s, _, _| {
let v = ViewInfo::default();
let cmd = Mods { cmd: true, ..Default::default() };
assert_eq!(s.cursor(Point::new(500.0, 300.0), cmd, v), vectorcraft_tools::Cursor::Busy, "Cmd shows the merge is computed");
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 500.0, 300.0).with_mods(cmd), v).unwrap();
}
assert!(s.tool_editing(), "the click is held");
while s.tool_waiting() {
s.tool_poll(v, true).unwrap();
}
assert!(s.doc().unwrap().selection.is_empty(), "the lent selection tool deselects after the merge");
assert_eq!(s.tool_id(), "shapeBuilder");
}),
("a Cmd-drag", |s, _, _| {
let v = ViewInfo::default();
let cmd = Mods { cmd: true, ..Default::default() };
for (kind, y) in [(PointerKind::Down, 10.0), (PointerKind::Drag, 60.0), (PointerKind::Drag, 110.0), (PointerKind::Up, 110.0)] {
s.pointer(&PointerEvent::new(kind, 305.0, y).with_mods(cmd), v).unwrap();
}
assert!(s.tool_editing(), "the drag is held");
while s.tool_waiting() {
s.tool_poll(v, true).unwrap();
}
let labels: Vec<&str> = s.doc().unwrap().history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Move", "Shape Builder"]);
}),
// The proof views redraw every document (a new revision); they are left off, as the
// other tests of the process-wide proof view expect.
("Separations Preview", |s, _, _| {
s.execute("view.separationsPreview", &json!({"on": false})).unwrap();
}),
("Overprint Preview", |s, _, _| {
s.execute("view.overprintPreview", &json!({"on": false})).unwrap();
}),
("Proof Colors", |s, _, _| {
s.execute("view.proofColors", &json!({"on": false})).unwrap();
}),
("Proof Setup", |s, _, _| {
s.execute("view.proofSetup", &json!({})).unwrap();
}),
("a preference", |s, _, _| {
let every = s.prefs.gridline_every;
s.execute("prefs.set", &json!({"key": "gridlineEvery", "value": every + 10.0})).unwrap();
}),
("Reset Preferences", |s, _, _| {
s.execute("prefs.reset", &json!({})).unwrap();
}),
("preferences applied outside a command", |s, _, _| {
let mut p = s.prefs.clone();
p.gridline_every += 10.0;
s.apply_prefs(p);
}),
("Add Font Files", |s, _, _| {
// No files: the command fails after the merge.
assert!(s.execute("text.addFontFiles", &json!({"files": []})).is_err());
}),
];
let _proof = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let dir = vectorcraft_testkit::temp_dir("shape-builder-merge-first");
for (what, then) in next {
let mut s = session();
if what == "Select All Instances" {
let far = rect(&mut s, 500.0, 500.0, 20.0, 20.0);
s.execute("symbol.new", &json!({"name": "S", "ids": [far.0]})).unwrap();
}
let ids = square_row(&mut s);
click_row(&mut s, v);
let computed = merges_computed();
let journal = s.journal.len();
then(&mut s, &ids, &dir);
assert!(!s.tool_editing(), "{what}");
assert_eq!(merges_computed(), computed, "{what}: the merge computed on another thread");
assert_eq!(s.journal.since(journal).first().map(|j| j.0.as_str()), Some("shapeBuilder.merge"), "{what}: the merge comes first");
if what != "a closed document" {
assert!(layer_children(&s) >= 41, "{what}: the merge stays");
}
}
}
/// Gestures and arrow keys that come while a merge is computed on another thread are held and
/// made after it, in order: the document is the one they make one after the other, and the
/// events count as made once they are and the merge they start is applied.
#[test]
fn shape_builder_holds_gestures_made_during_a_merge() {
type Gesture = fn(&mut Session, ViewInfo);
let cases: [(&str, bool, Gesture); 6] = [
("a second click", false, |s, v| {
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
}
}),
("a drag", false, |s, v| {
for (kind, x) in [(PointerKind::Down, 75.0), (PointerKind::Drag, 85.0), (PointerKind::Drag, 95.0), (PointerKind::Up, 95.0)] {
s.pointer(&PointerEvent::new(kind, x, 10.0), v).unwrap();
}
}),
("two clicks", false, |s, v| {
for x in [55.0, 135.0] {
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, x, 10.0), v).unwrap();
}
}
}),
("a Cmd-click", false, |s, v| {
let cmd = Mods { cmd: true, ..Default::default() };
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 500.0, 300.0).with_mods(cmd), v).unwrap();
}
}),
("a Cmd-drag and a click", false, |s, v| {
let cmd = Mods { cmd: true, ..Default::default() };
for (kind, y) in [(PointerKind::Down, 10.0), (PointerKind::Drag, 110.0), (PointerKind::Up, 110.0)] {
s.pointer(&PointerEvent::new(kind, 305.0, y).with_mods(cmd), v).unwrap();
}
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
}
}),
("an arrow key", true, |s, v| {
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
}),
];
let v = ViewInfo::default();
let start = |swatches: bool| {
let mut s = session();
if swatches {
s.select_tool("shapeBuilder", v).unwrap();
s.set_tool_option_cmd(&json!({"key": "pickColorFrom", "value": "swatches"})).unwrap();
}
square_row(&mut s);
click_row(&mut s, v);
s
};
for (what, swatches, send) in cases {
// One after the other: the merge applied before the gesture, and each merge after its own.
let mut one = start(swatches);
while one.tool_waiting() {
one.tool_poll(v, true).unwrap();
}
send(&mut one, v);
while one.tool_waiting() {
one.tool_poll(v, true).unwrap();
}
// While the merge is computed.
let mut s = start(swatches);
let revision = s.doc().unwrap().revision;
let from = s.input_count();
send(&mut s, v);
let to = s.input_count();
assert!(s.tool_editing() && !s.input_applied(from, to), "{what}: held");
assert_eq!(s.doc().unwrap().revision, revision, "{what}: nothing made before the merge");
while s.tool_waiting() {
s.tool_poll(v, true).unwrap();
}
assert!(!s.tool_editing() && s.input_applied(from, to), "{what}");
assert!(undated(&s) == undated(&one), "{what}: the document the gestures make one after the other");
assert_eq!(s.doc().unwrap().selection, one.doc().unwrap().selection, "{what}");
assert_eq!(s.paint.fill, one.paint.fill, "{what}");
let labels = |s: &Session| s.doc().unwrap().history.undo.iter().map(|e| e.label.clone()).collect::<Vec<_>>();
assert_eq!(labels(&s), labels(&one), "{what}");
// The click's merge alone.
let mut merged = start(swatches);
while merged.tool_waiting() {
merged.tool_poll(v, true).unwrap();
}
let changed = labels(&s).len() > labels(&merged).len()
|| s.paint.fill != merged.paint.fill
|| s.doc().unwrap().selection != merged.doc().unwrap().selection;
assert!(changed, "{what}: the gesture did something");
}
}
/// A press held while a merge is computed on another thread, then dropped (a long touch, a
/// canceled touch, a second finger), is never made: once the merge is applied, the tool is at
/// rest and the document is the one the same input makes, dropped the same way, after the merge.
/// A held click before the press is still made, and so is a perspective plane key held after it.
#[test]
fn shape_builder_drops_a_press_held_during_a_merge() {
use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane};
type Gesture = fn(&mut Session, ViewInfo);
fn press(s: &mut Session, v: ViewInfo, x: f64, mods: Mods) {
s.pointer(&PointerEvent::new(PointerKind::Down, x, 10.0).with_mods(mods), v).unwrap();
}
fn click(s: &mut Session, v: ViewInfo) {
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
}
}
// What a case sends, and whether the perspective grid is shown.
let cases: [(&str, bool, Gesture); 7] = [
("a press", false, |s, v| press(s, v, 75.0, Mods::default())),
("a drag", false, |s, v| {
press(s, v, 75.0, Mods::default());
s.pointer(&PointerEvent::new(PointerKind::Drag, 95.0, 10.0), v).unwrap();
}),
("a Cmd-press", false, |s, v| press(s, v, 305.0, Mods { cmd: true, ..Default::default() })),
("a press and a key", false, |s, v| {
press(s, v, 75.0, Mods::default());
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
}),
("a press and an arrow key", false, |s, v| {
press(s, v, 75.0, Mods::default());
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
}),
("a press and a plane key", true, |s, v| {
press(s, v, 75.0, Mods::default());
s.tool_key(ToolKey::Digit(3), Mods::default(), v).unwrap();
}),
("a click, then a press", false, |s, v| {
click(s, v);
press(s, v, 135.0, Mods::default());
}),
];
let v = ViewInfo::default();
let start = |grid: bool| {
let mut s = session();
s.select_tool("shapeBuilder", v).unwrap();
s.set_tool_option_cmd(&json!({"key": "pickColorFrom", "value": "swatches"})).unwrap();
if grid {
s.execute("perspective.grid.preset", &json!({"kind": 2})).unwrap();
}
square_row(&mut s);
click_row(&mut s, v);
s
};
// A tool left in the middle of a gesture keeps waiting: the polls stop after a few.
let settle = |s: &mut Session| {
for _ in 0..8 {
if !s.tool_waiting() {
return;
}
s.tool_poll(v, true).unwrap();
}
};
let labels = |s: &Session| s.doc().unwrap().history.undo.iter().map(|e| e.label.clone()).collect::<Vec<_>>();
let plane = |s: &Session| PerspectiveGrid::from_doc(&s.doc().unwrap().doc).unwrap().plane;
for (what, grid, send) in cases {
// The same input after the merge, dropped the same way.
let mut one = start(grid);
settle(&mut one);
send(&mut one, v);
one.drop_tool_press(v).unwrap();
settle(&mut one);
let mut s = start(grid);
if grid {
assert_ne!(plane(&s), Plane::Right, "{what}: the plane before the key");
}
let from = s.input_count();
send(&mut s, v);
let to = s.input_count();
assert!(s.tool_editing() && !s.input_applied(from, to), "{what}: held");
s.drop_tool_press(v).unwrap();
settle(&mut s);
assert!(!s.tool_waiting() && !s.tool_editing() && s.input_applied(from, to), "{what}");
assert!(!s.tool_busy(), "{what}: the tool is at rest");
assert_eq!(s.tool_id(), "shapeBuilder", "{what}: no tool is lent");
assert!(undated(&s) == undated(&one), "{what}: the document the input makes after the merge");
assert_eq!(labels(&s), labels(&one), "{what}");
assert_eq!(s.doc().unwrap().selection, one.doc().unwrap().selection, "{what}");
assert_eq!(s.paint.fill, one.paint.fill, "{what}");
if grid {
assert_eq!(plane(&one), Plane::Right, "{what}: the plane key is made after the merge");
assert_eq!(plane(&s), Plane::Right, "{what}: the plane key is made");
}
}
}
/// A new revision of the document made outside a command while a merge is computed on another
/// thread keeps the merge when the selected art is as it was (the next poll applies it, as the
/// command computes it), and drops it when the art changed.
#[test]
fn shape_builder_keeps_a_merge_over_a_new_revision_of_the_same_art() {
use vectorcraft_tools::builder::merges_computed;
let v = ViewInfo::default();
let mut s = session();
square_row(&mut s);
click_row(&mut s, v);
let computed = merges_computed();
s.active_mut().unwrap().revision += 1;
s.tool_poll(v, true).unwrap();
assert!(!s.tool_editing());
assert_eq!(merges_computed(), computed, "the merge computed on another thread");
assert_eq!(s.journal.last().unwrap().0, "shapeBuilder.merge");
let mut direct = session();
square_row(&mut direct);
direct.execute("shapeBuilder.merge", &json!({"points": [[15, 10]]})).unwrap();
assert!(undated(&s) == undated(&direct), "the same document as the command gives");
// The art changed: the merge is dropped.
let mut s = session();
let ids = square_row(&mut s);
click_row(&mut s, v);
let journal = s.journal.len();
let st = s.active_mut().unwrap();
Arc::make_mut(&mut st.doc).node_mut(ids[1]).unwrap().transform(Affine::translate((0.0, 50.0)), false);
st.revision += 1;
s.tool_poll(v, true).unwrap();
assert!(!s.tool_editing());
assert_eq!(s.journal.len(), journal, "nothing merged");
assert_eq!(layer_children(&s), 40);
}
/// A merge computed for the tool Cmd lent the selection tool's drag away from is applied before
/// a command too.
#[test]
fn shape_builder_merge_of_the_lending_tool_comes_first() {
let v = ViewInfo::default();
let mut s = session();
square_row(&mut s);
click_row(&mut s, v);
let lent = s.make_tool("selection");
s.lender = Some(std::mem::replace(&mut s.tool, lent));
assert!(s.tool_editing());
let journal = s.journal.len();
s.execute("object.move", &json!({"dx": 0, "dy": 100})).unwrap();
assert!(!s.tool_editing());
assert_eq!(s.journal.since(journal).first().map(|j| j.0.as_str()), Some("shapeBuilder.merge"));
assert_eq!(s.tool_id(), "selection", "the lent tool stays active until the release");
assert_eq!(s.lender.as_ref().map(|t| t.id()), Some("shapeBuilder"));
}
#[test]
fn shape_builder_tool_merges_the_narrow_shape_it_highlights_in_a_wide_selection() {
let mut s = session();
+273 -9
View File
@@ -83,6 +83,23 @@ pub enum UiRequest {
/// Place a file with these `file.place` params: a click or drag of the place cursor, when
/// [`Session::place_cursor_requests`] is set.
Place(Value),
/// A press held while the tool computed an edit on another thread was made now, at `pos` in the
/// document with [`crate::DocState::uid`] `doc`, with tool `tool` active (the selection tool Cmd
/// lent): the frontend does what it does after a press it hands over itself.
HeldPress {
doc: Option<u64>,
pos: Point,
tool: &'static str,
},
}
/// Pointer or key input that came while the active tool computed an edit on another thread
/// ([`Tool::editing`]), made in order once the edit is applied ([`Session::tool_poll`],
/// [`Session::finish_tool_edit`]).
#[derive(Clone, Copy, Debug)]
pub(crate) enum HeldInput {
Pointer(PointerEvent),
Key(ToolKey, Mods),
}
impl Session {
@@ -106,6 +123,7 @@ impl Session {
/// Choose a tool (the toolbar, its shortcut, MCP), finishing any pending tool work first.
pub fn select_tool(&mut self, id: &str, view: ViewInfo) -> Result<()> {
self.finish_tool_edit_later()?;
// Choosing a perspective tool shows the document's perspective grid; Hide Grid hides it
// again, the tool staying chosen.
if matches!(id, "perspectiveGrid" | "perspectiveSelection")
@@ -129,6 +147,8 @@ impl Session {
/// Switch to tool `id` without choosing it afresh (back from a temporary tool: a hidden
/// perspective grid stays hidden), finishing any pending tool work first.
pub fn switch_tool(&mut self, id: &str, view: ViewInfo) -> Result<()> {
// The edit the tool computes on another thread and the input held for it come first.
self.finish_tool_edit_later()?;
self.give_back_tool(view)?;
if self.tool.id() == id {
return Ok(());
@@ -430,8 +450,85 @@ impl Session {
self.tool_step = now;
}
/// Feed a pointer event to the active tool. Returns requests for the UI.
/// Feed a pointer event to the active tool. Returns requests for the UI. While the tool computes
/// an edit on another thread, a press and what follows it are held and made once the edit is
/// applied, in order ([`Session::tool_poll`]); moves of the pointer with no button held go to
/// the tool at once.
pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
let n = self.next_input();
if ev.kind != PointerKind::Move && (!self.held_input.is_empty() || self.tool_edit_pending()) {
self.held_input.push_back((n, HeldInput::Pointer(*ev), view));
return Ok(vec![]);
}
let r = self.pointer_now(ev, view);
self.note_edit_start(n);
r
}
/// The number of the next pointer or key event handed to the session.
fn next_input(&mut self) -> u64 {
let n = self.input_count;
self.input_count = n.wrapping_add(1);
n
}
/// How many pointer and key events have been handed to the session ([`Session::pointer`],
/// [`Session::tool_key`]); with [`Session::input_applied`], a frontend tells when the events it
/// handed over have been made.
pub fn input_count(&self) -> u64 {
self.input_count
}
/// Have the events numbered `from..to` ([`Session::input_count`] before and after handing them
/// over) been made, and the edit they started on another thread applied?
pub fn input_applied(&self, from: u64, to: u64) -> bool {
let ours = |n: &u64| (from..to).contains(n);
!self.held_input.iter().any(|(n, ..)| ours(n)) && !(self.tool_edit_pending() && self.edit_started_by.as_ref().is_some_and(ours))
}
/// After event `n` was made: the event that started the edit being computed, if one is.
fn note_edit_start(&mut self, n: u64) {
if !self.tool_edit_pending() {
self.edit_started_by = None;
} else if self.edit_started_by.is_none() {
self.edit_started_by = Some(n);
}
}
/// Is the active tool, or the tool Cmd lent the selection tool's drag away from, computing an
/// edit on another thread?
fn tool_edit_pending(&self) -> bool {
self.tool.editing() || self.lender.as_ref().is_some_and(|t| t.editing())
}
/// Makes the input held while the tool computed an edit, in order, until a held gesture starts
/// another such edit.
fn replay_held_input(&mut self) -> Result<Vec<UiRequest>> {
let mut ui = vec![];
let mut failed = None;
while !self.tool_edit_pending()
&& let Some((n, input, view)) = self.held_input.pop_front()
{
let r = match input {
HeldInput::Pointer(ev) => self.pointer_now(&ev, view).map(|mut r| {
if ev.kind == PointerKind::Down {
r.push(UiRequest::HeldPress { doc: self.active().map(|d| d.uid), pos: ev.pos, tool: self.tool.id() });
}
r
}),
HeldInput::Key(key, mods) => self.tool_key_now(key, mods, view),
};
self.note_edit_start(n);
match r {
Ok(r) => ui.extend(r),
// The input after it is still made, as it was when each event came on its own.
Err(e) => failed = failed.or(Some(e)),
}
}
failed.map_or(Ok(ui), Err)
}
fn pointer_now(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
self.pointer_event(ev, view, true)
}
@@ -442,8 +539,25 @@ impl Session {
/// nothing and a click opens no size dialog. The tool gets back the persistent options it had
/// when the press began, and none of the press's changes to them are stored. A dropped Type
/// press restores the prior selection. The freeform gradient point selected once the tool
/// took the press is selected again.
/// took the press is selected again. A press held while the tool computes an edit on another
/// thread has not reached the tool: it is removed from the held input with the pointer events
/// and tool keys held after it, and the tool is left as it is. A perspective plane key held
/// after it stays held, since the plane it picks outlasts a dropped press. Pointer events held
/// after the last held release belong to the press the tool has, and are removed with it.
pub fn drop_tool_press(&mut self, view: ViewInfo) -> Result<()> {
let (from, held) = self.held_press();
let keep: Vec<bool> = (self.held_input.iter().enumerate())
.map(|(j, (_, input, _))| match input {
_ if j < from => true,
HeldInput::Pointer(_) => false,
HeldInput::Key(key, _) => !held || self.plane_key(*key).is_some(),
})
.collect();
let mut keep = keep.into_iter();
self.held_input.retain(|_| keep.next().unwrap_or(true));
if held {
return Ok(());
}
// Nothing to roll back without a document; the tool still drops the press and gets the release.
let _ = self.cancel_interaction();
let before = self.press_options.take();
@@ -485,6 +599,19 @@ impl Session {
r.map(drop)
}
/// Where the input of the press in progress starts in the held input, and whether the press
/// itself is held: the last held press when no release is held after it; otherwise the input
/// after the last held release (all of it when none is held), which follows a press the tool
/// already has.
fn held_press(&self) -> (usize, bool) {
let ends = |kind: PointerKind| matches!(kind, PointerKind::Down | PointerKind::Up);
match self.held_input.iter().rposition(|(_, input, _)| matches!(input, HeldInput::Pointer(ev) if ends(ev.kind))) {
Some(i) if matches!(self.held_input.get(i), Some((_, HeldInput::Pointer(ev), _)) if ev.kind == PointerKind::Down) => (i, true),
Some(i) => (i + 1, false),
None => (0, false),
}
}
/// The options of `t` that [`Self::store_tool_settings`] stores.
fn persistent_options(t: &dyn Tool) -> Map<String, Value> {
let opts = t.options();
@@ -619,19 +746,148 @@ impl Session {
self.tool.wants_samples()
}
/// Is the active tool waiting for work it runs on another thread ([`Tool::waiting`])?
/// Is the active tool waiting for work it runs on another thread ([`Tool::waiting`]), is input
/// held until an edit it computes is applied, or are requests for the UI left from making it?
pub fn tool_waiting(&self) -> bool {
self.tool.waiting()
self.tool.waiting() || !self.held_input.is_empty() || !self.held_ui.is_empty()
}
/// Lets the active tool check on the work [`Session::tool_waiting`] reports, and with `wait`
/// wait for it to finish ([`Tool::poll`]).
pub fn tool_poll(&mut self, view: ViewInfo, wait: bool) -> Result<()> {
self.with_tool_cx(view, |t, cx| t.poll(cx, wait));
self.take_tool_panic()
/// wait for it to finish ([`Tool::poll`]); an edit the work made is applied, and then the input
/// held for it is made. Returns requests for the UI, those left from making held input where
/// nothing could take them first ([`Session::finish_tool_edit_later`]).
pub fn tool_poll(&mut self, view: ViewInfo, wait: bool) -> Result<Vec<UiRequest>> {
if self.settling_tool_edit {
return Ok(vec![]);
}
self.settling_tool_edit = true;
let r = self.tool_poll_now(view, wait);
self.settling_tool_edit = false;
// On an error the requests left wait for the next poll.
let r = r?;
let mut ui = std::mem::take(&mut self.held_ui);
ui.extend(r);
Ok(ui)
}
fn tool_poll_now(&mut self, view: ViewInfo, wait: bool) -> Result<Vec<UiRequest>> {
let acts = self.with_tool_cx(view, |t, cx| t.poll(cx, wait));
self.take_tool_panic()?;
let applied = !acts.is_empty();
let mut ui = self.apply_actions(acts)?;
if !self.tool_edit_pending() {
self.edit_started_by = None;
// The input held for an edit applied now is made at the next poll, so what waits for the
// edit alone (a `ui.pointer` reply) sees the document as the edit left it.
if !applied {
ui.extend(self.replay_held_input()?);
}
}
Ok(ui)
}
/// Is the active tool computing an edit on another thread ([`Tool::editing`]), or is input held
/// until it is applied?
pub fn tool_editing(&self) -> bool {
self.tool_edit_pending() || !self.held_input.is_empty()
}
/// Waits for the edit the active tool computes on another thread and applies it
/// ([`Tool::finish_edit`]), then makes the input held for it, waiting in turn for an edit that
/// starts, so what runs next sees the document as the gestures left it. Commands, saves, option
/// changes, interactions, undo scopes and tool or document switches call it first. Returns
/// requests for the UI, with those left from earlier ([`Session::finish_tool_edit_later`]); on
/// an error, the next [`Session::tool_poll`] returns them.
pub fn finish_tool_edit(&mut self) -> Result<Vec<UiRequest>> {
if self.settling_tool_edit {
return Ok(vec![]);
}
let mut ui = std::mem::take(&mut self.held_ui);
if self.tool_editing() {
self.settling_tool_edit = true;
let r = self.finish_tool_edit_now();
self.settling_tool_edit = false;
match r {
Ok(r) => ui.extend(r),
Err(e) => {
ui.append(&mut self.held_ui);
self.held_ui = ui;
return Err(e);
}
}
}
Ok(ui)
}
/// [`Session::finish_tool_edit`] for a caller that cannot hand requests to the UI (a command, a
/// tool switch): the next [`Session::tool_poll`] returns them, and [`Session::tool_waiting`]
/// holds until then.
pub(crate) fn finish_tool_edit_later(&mut self) -> Result<()> {
let ui = self.finish_tool_edit()?;
self.held_ui.extend(ui);
Ok(())
}
fn finish_tool_edit_now(&mut self) -> Result<Vec<UiRequest>> {
let mut ui = vec![];
let mut failed = None;
loop {
match self.finish_pending_edit() {
Ok(r) => ui.extend(r),
Err(e) => failed = failed.or(Some(e)),
}
// A tool that cannot finish its edit keeps the input held.
if self.tool_edit_pending() {
break;
}
self.edit_started_by = None;
if self.held_input.is_empty() {
break;
}
match self.replay_held_input() {
Ok(r) => ui.extend(r),
Err(e) => failed = failed.or(Some(e)),
}
}
match failed {
None => Ok(ui),
Some(e) => {
self.held_ui.extend(ui);
Err(e)
}
}
}
/// Waits for the edit being computed and applies it: the active tool's, or that of the tool
/// Cmd lent the selection tool's drag away from, which is active meanwhile.
fn finish_pending_edit(&mut self) -> Result<Vec<UiRequest>> {
if self.tool.editing() {
let acts = self.with_tool_cx(self.last_view, |t, cx| t.finish_edit(cx));
self.take_tool_panic()?;
return self.apply_actions(acts);
}
let Some(lender) = self.lender.take_if(|t| t.editing()) else { return Ok(vec![]) };
let lent = std::mem::replace(&mut self.tool, lender);
let acts = self.with_tool_cx(self.last_view, |t, cx| t.finish_edit(cx));
let r = self.take_tool_panic().and_then(|()| self.apply_actions(acts));
self.lender = Some(std::mem::replace(&mut self.tool, lent));
r
}
/// A key for the active tool. While the tool computes an edit on another thread, the key is held
/// and made once the edit is applied, after the input held before it.
pub fn tool_key(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> Result<Vec<UiRequest>> {
let n = self.next_input();
if self.tool_editing() {
self.held_input.push_back((n, HeldInput::Key(key, mods), view));
return Ok(vec![]);
}
let r = self.tool_key_now(key, mods, view);
self.note_edit_start(n);
r
}
fn tool_key_now(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> Result<Vec<UiRequest>> {
// 1–4 pick the active perspective plane while the grid is shown.
if let Some(plane) = self.plane_key(key) {
self.execute("perspective.plane.set", &serde_json::json!({ "plane": plane.id() }))?;
@@ -837,7 +1093,8 @@ impl Session {
/// it is on, makes the drawing tools' a crosshair, [`Cursor::precise`]). With Cmd held, the pointer of the selection tool a
/// press would borrow ([`Self::lend_selection_tool`]).
pub fn cursor(&mut self, p: Point, mods: Mods, view: ViewInfo) -> Cursor {
let lent = if mods.cmd && self.lender.is_none() { self.lent_tool(self.tool.id(), &mods) } else { None };
// While an edit is computed, a Cmd press is held for the active tool, which shows it is busy.
let lent = if mods.cmd && self.lender.is_none() && !self.tool_editing() { self.lent_tool(self.tool.id(), &mods) } else { None };
let c = match lent {
Some(id) => {
let lent = self.make_tool(id);
@@ -860,6 +1117,9 @@ impl Session {
self.tool.options()
}
pub fn set_tool_option(&mut self, key: &str, v: &Value) {
// The active tool's edit being computed is applied with the options it was started with. A
// failure to apply it was the edit's own, and the option is set all the same.
let _ = self.finish_tool_edit_later();
if key == "queue" && self.tool.id() == "place" {
self.note_place_queue_roots();
}
@@ -934,6 +1194,10 @@ impl Session {
{
return Err(format!("unknown tool `{t}`"));
}
// The active tool's merge being computed is applied with the options it was started with.
if tool.is_none_or(|t| t == self.tool.id()) {
self.finish_tool_edit_later().map_err(|e| e.to_string())?;
}
let mut values = p.get("values").and_then(Value::as_object).cloned().unwrap_or_default();
if let Some(k) = p.get("key").and_then(Value::as_str) {
values.insert(k.to_string(), p.get("value").cloned().unwrap_or(Value::Null));