mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2942 from nrq6993-a11y/stream/vc-feat3-g12-real-time-drawing
Honor Preferences › Performance › Real-time Drawing and Editing during tool drags
This commit is contained in:
@@ -1415,6 +1415,9 @@ pub struct Session {
|
||||
/// Where folder searches may go; `None`: this computer's rules ([`cmd::findfiles::Rules`]).
|
||||
/// Tests set it.
|
||||
pub search_rules: Option<cmd::findfiles::Rules>,
|
||||
/// Canvas feedback cached for the pending deferred preview ([`Session::overlays`]); cleared
|
||||
/// when the preview params change or the interaction ends.
|
||||
pub(crate) deferred_overlay_cache: Option<(String, Value, Vec<vectorcraft_tools::Overlay>)>,
|
||||
/// The walker threads a folder search starts; `None`: [`cmd::findfiles::threads`]. Tests set
|
||||
/// it.
|
||||
pub search_threads: Option<usize>,
|
||||
@@ -1489,6 +1492,7 @@ impl Session {
|
||||
font_search: None,
|
||||
link_search: None,
|
||||
search_rules: None,
|
||||
deferred_overlay_cache: None,
|
||||
search_threads: None,
|
||||
link_stamps: Default::default(),
|
||||
link_scan: None,
|
||||
@@ -1941,12 +1945,23 @@ impl Session {
|
||||
|
||||
// ---------- interactions (live drags) ----------
|
||||
|
||||
pub(crate) fn clear_deferred_overlay_cache(&mut self) {
|
||||
self.deferred_overlay_cache = None;
|
||||
}
|
||||
|
||||
/// The interaction's latest preview command when the document is still its snapshot (deferred
|
||||
/// Real-time Drawing), regardless of the preference at commit or overlay time.
|
||||
pub(crate) fn unapplied_interaction_preview(&self) -> Option<(String, Value)> {
|
||||
self.active().and_then(|d| d.interaction.as_ref().filter(|it| std::sync::Arc::ptr_eq(&it.doc, &d.doc)).and_then(|it| it.preview.clone()))
|
||||
}
|
||||
|
||||
pub fn begin_interaction(&mut self, label: &str) -> Result<()> {
|
||||
// Typing in progress is kept first: a drag or a dialog's preview would otherwise start
|
||||
// from the snapshot taken before the typing, and drop it (#1881).
|
||||
if label != "Typing" && cmd::typing_in_progress(self) {
|
||||
cmd::settle_interaction(self)?;
|
||||
}
|
||||
self.clear_deferred_overlay_cache();
|
||||
let st = self.doc_mut()?;
|
||||
if st.interaction.is_some() {
|
||||
return Ok(());
|
||||
@@ -2000,6 +2015,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub fn commit_interaction(&mut self) -> Result<()> {
|
||||
self.clear_deferred_overlay_cache();
|
||||
self.remember_pending_paint();
|
||||
let st = self.doc_mut()?;
|
||||
let Some(mut it) = st.interaction.take() else { return Ok(()) };
|
||||
@@ -2052,6 +2068,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub fn cancel_interaction(&mut self) -> Result<()> {
|
||||
self.clear_deferred_overlay_cache();
|
||||
self.pending_paint = None;
|
||||
let guides_locked = self.menu.guides_locked;
|
||||
let st = self.doc_mut()?;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Preferences: `prefs.get` / `prefs.set` / `prefs.reset` / `prefs.list`.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeKind;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use super::*;
|
||||
use crate::cmd::prefscmds::{PREF_CATEGORIES, PREF_GROUPS, PREF_SPECS, validate};
|
||||
@@ -1393,6 +1395,114 @@ fn highlight_alternate_glyphs_preference_is_off_by_default() {
|
||||
assert!(!s.prefs.highlight_alternate_glyphs);
|
||||
}
|
||||
|
||||
/// Preferences › Performance › Real-time Drawing and Editing (#394): off, the document isn't
|
||||
/// previewed while dragging but the pending shape still shows as a canvas overlay until commit.
|
||||
#[test]
|
||||
fn real_time_drawing_off_skips_tool_previews_until_commit() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("rectangle", v).unwrap();
|
||||
set_pref(&mut s, "realTimeDrawing", json!(false));
|
||||
let paths = |s: &Session| {
|
||||
s.doc()
|
||||
.unwrap()
|
||||
.doc
|
||||
.layers
|
||||
.first()
|
||||
.and_then(|l| l.children())
|
||||
.map_or(0, |c| c.iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).count())
|
||||
};
|
||||
let rev = s.doc().unwrap().revision;
|
||||
assert_eq!(paths(&s), 0);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 50.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 150.0, 150.0), v).unwrap();
|
||||
assert_eq!(s.doc().unwrap().revision, rev, "no preview steps");
|
||||
assert_eq!(paths(&s), 0, "the rectangle isn't in the document yet");
|
||||
assert!(s.overlays(v).iter().any(|o| matches!(o, vectorcraft_tools::Overlay::Path { .. })), "the deferred rectangle shows as an outline");
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 150.0, 150.0), v).unwrap();
|
||||
assert!(s.doc().unwrap().revision > rev, "commit applies the shape");
|
||||
assert_eq!(paths(&s), 1);
|
||||
set_pref(&mut s, "realTimeDrawing", json!(true));
|
||||
let rev = s.doc().unwrap().revision;
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 50.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 280.0, 120.0), v).unwrap();
|
||||
assert!(s.doc().unwrap().revision > rev, "on again: previews run while dragging");
|
||||
}
|
||||
|
||||
/// Real-time Drawing turned on mid-drag still applies the deferred preview on commit.
|
||||
#[test]
|
||||
fn real_time_drawing_turned_on_mid_drag_still_commits_deferred_preview() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("rectangle", v).unwrap();
|
||||
set_pref(&mut s, "realTimeDrawing", json!(false));
|
||||
let paths = |s: &Session| {
|
||||
s.doc()
|
||||
.unwrap()
|
||||
.doc
|
||||
.layers
|
||||
.first()
|
||||
.and_then(|l| l.children())
|
||||
.map_or(0, |c| c.iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).count())
|
||||
};
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 50.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 150.0, 150.0), v).unwrap();
|
||||
assert_eq!(paths(&s), 0);
|
||||
set_pref(&mut s, "realTimeDrawing", json!(true));
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 150.0, 150.0), v).unwrap();
|
||||
assert_eq!(paths(&s), 1, "the deferred rectangle is committed");
|
||||
}
|
||||
|
||||
/// Repeated overlay redraws with Real-time Drawing off reuse the cached deferred feedback.
|
||||
#[test]
|
||||
fn deferred_preview_overlays_are_cached_across_redraws() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("rectangle", v).unwrap();
|
||||
set_pref(&mut s, "realTimeDrawing", json!(false));
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 50.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 150.0, 150.0), v).unwrap();
|
||||
let rev = s.doc().unwrap().revision;
|
||||
let count = |s: &mut Session| s.overlays(v).iter().filter(|o| matches!(o, vectorcraft_tools::Overlay::Path { .. })).count();
|
||||
let first = count(&mut s);
|
||||
assert!(first > 0);
|
||||
assert_eq!(s.doc().unwrap().revision, rev);
|
||||
let second = count(&mut s);
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(s.doc().unwrap().revision, rev, "the preview command is not run again");
|
||||
}
|
||||
|
||||
/// A deferred commit that fails its preview cancels the interaction instead of leaving it open.
|
||||
#[test]
|
||||
fn deferred_commit_preview_failure_cancels_interaction() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
set_pref(&mut s, "realTimeDrawing", json!(false));
|
||||
s.begin_interaction("Bad").unwrap();
|
||||
s.doc_mut().unwrap().interaction.as_mut().unwrap().preview = Some(("no.such.command".into(), json!({})));
|
||||
s.apply_actions(vec![vectorcraft_tools::Action::Commit]).unwrap();
|
||||
assert!(s.doc().unwrap().interaction.is_none());
|
||||
}
|
||||
|
||||
/// A deferred commit applies preview warnings to the UI, as live previews do.
|
||||
#[test]
|
||||
fn deferred_commit_forwards_preview_warnings() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let rect = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 200})).unwrap();
|
||||
let text = s.execute("text.create", &json!({"x": 300, "y": 200, "text": "Hi"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [rect["id"], text["id"]]})).unwrap();
|
||||
set_pref(&mut s, "realTimeDrawing", json!(false));
|
||||
s.begin_interaction("Bloat").unwrap();
|
||||
let p = json!({"tool": "bloat", "points": [[290.0, 200.0]], "width": 200.0, "height": 200.0});
|
||||
s.doc_mut().unwrap().interaction.as_mut().unwrap().preview = Some(("object.liquify".into(), p));
|
||||
let ui = s.apply_actions(vec![vectorcraft_tools::Action::Commit]).unwrap();
|
||||
assert!(ui.iter().any(|r| matches!(r, UiRequest::Status(m) if m.contains("type"))), "{ui:?}");
|
||||
}
|
||||
|
||||
/// File Handling › Use Low Resolution Proxy for Linked EPS (#394): off by default; toggling it does
|
||||
/// not replace full-resolution pixels already in the document.
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! Hosting the active tool: pointer/key events → actions → commands.
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use vectorcraft_doc::Selection;
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_tools::distort::perspective::widget::WidgetPlace;
|
||||
use vectorcraft_tools::draw2::FEEDBACK;
|
||||
use vectorcraft_tools::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, settings};
|
||||
|
||||
use crate::{EngineError, Prefs, Result, Session};
|
||||
@@ -450,11 +452,15 @@ impl Session {
|
||||
v.extend(self.with_tool_cx(view, |_, cx| g.overlays(cx)));
|
||||
self.ruler_guide = Some(g);
|
||||
}
|
||||
let deferred_preview = self.unapplied_interaction_preview();
|
||||
if let Some(d) = self.active() {
|
||||
let w = self.prefs.perspective_widget;
|
||||
let place = w.show.then_some(WidgetPlace { screen: view.screen.as_ref(), corner: w.position });
|
||||
v.splice(0..0, vectorcraft_tools::distort::perspective::grid_overlays_in(&d.doc, 1.0 / view.zoom.max(1e-9), place));
|
||||
}
|
||||
if let Some((cmd, p)) = deferred_preview {
|
||||
v.extend(self.deferred_preview_overlays_from_command(&cmd, &p));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
@@ -562,19 +568,49 @@ impl Session {
|
||||
for a in acts {
|
||||
match a {
|
||||
Action::Begin(label) => self.begin_interaction(&label)?,
|
||||
Action::Preview(cmd, p) => match self.preview(&cmd, &p) {
|
||||
Ok(v) => {
|
||||
if let Some(w) = v.get("warning").and_then(Value::as_str)
|
||||
&& !ui.iter().any(|r| matches!(r, UiRequest::Status(s) if s == w))
|
||||
{
|
||||
ui.push(UiRequest::Status(w.to_string()));
|
||||
Action::Preview(cmd, p) => {
|
||||
// Performance › Real-time Drawing and Editing (#394): off, remember the latest
|
||||
// params and apply them once on Commit instead of every move.
|
||||
if !self.prefs.real_time_drawing {
|
||||
self.clear_deferred_overlay_cache();
|
||||
let st = self.doc_mut()?;
|
||||
if let Some(it) = &mut st.interaction {
|
||||
it.preview = Some((cmd, p));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(e) => log::warn!("preview {cmd}: {e}"),
|
||||
},
|
||||
self.clear_deferred_overlay_cache();
|
||||
match self.preview(&cmd, &p) {
|
||||
Ok(v) => {
|
||||
if let Some(w) = v.get("warning").and_then(Value::as_str)
|
||||
&& !ui.iter().any(|r| matches!(r, UiRequest::Status(s) if s == w))
|
||||
{
|
||||
ui.push(UiRequest::Status(w.to_string()));
|
||||
}
|
||||
}
|
||||
Err(e) => log::warn!("preview {cmd}: {e}"),
|
||||
}
|
||||
}
|
||||
// The drag is over: the dabs of a Liquify stroke have no further use.
|
||||
Action::Commit => {
|
||||
self.liquify_stroke = None;
|
||||
let deferred = self.unapplied_interaction_preview();
|
||||
if let Some((cmd, p)) = deferred {
|
||||
match self.preview(&cmd, &p) {
|
||||
Ok(v) => {
|
||||
if let Some(w) = v.get("warning").and_then(Value::as_str)
|
||||
&& !ui.iter().any(|r| matches!(r, UiRequest::Status(s) if s == w))
|
||||
{
|
||||
ui.push(UiRequest::Status(w.to_string()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("preview {cmd}: {e}");
|
||||
self.cancel_interaction()?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.commit_interaction()?
|
||||
}
|
||||
Action::Cancel => {
|
||||
@@ -598,4 +634,50 @@ impl Session {
|
||||
}
|
||||
Ok(ui)
|
||||
}
|
||||
|
||||
/// Canvas feedback while Real-time Drawing is off: run the pending preview command once, take
|
||||
/// outlines from the objects it touched, then put the document back at the interaction snapshot.
|
||||
fn deferred_preview_overlays_from_command(&mut self, cmd: &str, p: &Value) -> Vec<Overlay> {
|
||||
if self.deferred_overlay_cache.as_ref().is_some_and(|(c, params, _)| c == cmd && params == p) {
|
||||
return self.deferred_overlay_cache.as_ref().map(|(_, _, o)| o.clone()).unwrap_or_default();
|
||||
}
|
||||
let (snapshot, selection, revision, paint, interaction_fields) = {
|
||||
let Some(st) = self.active() else { return vec![] };
|
||||
let Some(it) = st.interaction.as_ref() else { return vec![] };
|
||||
if !std::sync::Arc::ptr_eq(&it.doc, &st.doc) {
|
||||
return vec![];
|
||||
}
|
||||
let interaction_fields = (it.preview.clone(), it.copies.clone(), it.perspective_again.clone());
|
||||
(st.doc.clone(), st.selection.clone(), st.revision, self.pending_paint.clone(), interaction_fields)
|
||||
};
|
||||
let overlays = if self.preview(cmd, p).is_ok() {
|
||||
self.active().map(|st| outlines_for_selection(&st.doc, &st.selection)).unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
if let Ok(st) = self.doc_mut() {
|
||||
st.doc = snapshot;
|
||||
st.selection = selection;
|
||||
st.revision = revision;
|
||||
if let Some(it) = &mut st.interaction {
|
||||
it.preview = interaction_fields.0;
|
||||
it.copies = interaction_fields.1;
|
||||
it.perspective_again = interaction_fields.2;
|
||||
}
|
||||
}
|
||||
self.pending_paint = paint;
|
||||
self.deferred_overlay_cache = Some((cmd.to_string(), p.clone(), overlays.clone()));
|
||||
overlays
|
||||
}
|
||||
}
|
||||
|
||||
fn outlines_for_selection(doc: &vectorcraft_doc::Document, selection: &Selection) -> Vec<Overlay> {
|
||||
selection
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|&id| {
|
||||
let n = doc.node(id)?;
|
||||
vectorcraft_render::drawn_outline(doc, n).map(|(path, _)| Overlay::Path { path, color: FEEDBACK, width: 1.0, dashed: false })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user