mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 20:34:18 +00:00
Merge pull request #5042 from dotCooCoo/fix/issue-3526-docs
Fix #3526: the bounding box follows a resize drag
This commit is contained in:
@@ -253,9 +253,10 @@ 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>)>,
|
||||
/// Canvas feedback cached for the pending deferred preview ([`Session::overlays`],
|
||||
/// [`Session::deferred_transform_box`]); cleared when the preview params change or the
|
||||
/// interaction ends.
|
||||
pub(crate) deferred_overlay_cache: Option<tooling::DeferredFeedback>,
|
||||
/// The walker threads a folder search starts; `None`: [`cmd::findfiles::threads`]. Tests set
|
||||
/// it.
|
||||
pub search_threads: Option<usize>,
|
||||
|
||||
@@ -766,6 +766,11 @@ impl Session {
|
||||
self.tool.transforming()
|
||||
}
|
||||
|
||||
/// Is the active tool resizing the selection by a bounding-box handle?
|
||||
pub fn tool_resizing(&self) -> bool {
|
||||
self.tool.resizing()
|
||||
}
|
||||
|
||||
/// With Real-time Drawing off, how far a Selection-tool move drag has carried the selection
|
||||
/// so far: the document moves on release, and the bounding box shows at this offset meanwhile.
|
||||
pub fn deferred_move(&self) -> Option<vectorcraft_geom::Vec2> {
|
||||
@@ -1087,25 +1092,48 @@ 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();
|
||||
/// With Real-time Drawing off, the selection's transform box ([`Session::transform_box`]) as the
|
||||
/// pending preview of a handle resize leaves it, measured once per preview step; `None` when no
|
||||
/// such preview is pending, or when the preview fails.
|
||||
pub fn deferred_transform_box(&mut self) -> Option<vectorcraft_doc::OrientedBox> {
|
||||
if !self.tool.resizing() {
|
||||
return None;
|
||||
}
|
||||
let (cmd, p) = self.unapplied_interaction_preview()?;
|
||||
self.deferred_feedback(&cmd, &p)?.transform_box
|
||||
}
|
||||
|
||||
fn deferred_preview_overlays_from_command(&mut self, cmd: &str, p: &Value) -> Vec<Overlay> {
|
||||
self.deferred_feedback(cmd, p).map(|f| f.overlays.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Canvas feedback while Real-time Drawing is off: run the pending preview command once, take
|
||||
/// outlines from the objects it touched (and, during a handle resize, their transform box), then
|
||||
/// put the document back at the interaction snapshot.
|
||||
fn deferred_feedback(&mut self, cmd: &str, p: &Value) -> Option<&DeferredFeedback> {
|
||||
if !self.deferred_overlay_cache.as_ref().is_some_and(|f| f.cmd == cmd && f.params == *p) {
|
||||
let feedback = self.measure_deferred_preview(cmd, p)?;
|
||||
self.deferred_overlay_cache = Some(feedback);
|
||||
}
|
||||
self.deferred_overlay_cache.as_ref()
|
||||
}
|
||||
|
||||
fn measure_deferred_preview(&mut self, cmd: &str, p: &Value) -> Option<DeferredFeedback> {
|
||||
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![] };
|
||||
let st = self.active()?;
|
||||
let it = st.interaction.as_ref()?;
|
||||
if !std::sync::Arc::ptr_eq(&it.doc, &st.doc) {
|
||||
return vec![];
|
||||
return None;
|
||||
}
|
||||
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()
|
||||
let (overlays, transform_box) = if self.preview(cmd, p).is_ok() {
|
||||
let overlays = self.active().map(|st| outlines_for_selection(&st.doc, &st.selection)).unwrap_or_default();
|
||||
let objects = self.active().map(|st| st.selection.objects.clone()).unwrap_or_default();
|
||||
(overlays, if self.tool.resizing() { self.transform_box(&objects) } else { None })
|
||||
} else {
|
||||
vec![]
|
||||
(vec![], None)
|
||||
};
|
||||
if let Ok(st) = self.doc_mut() {
|
||||
st.doc = snapshot;
|
||||
@@ -1118,11 +1146,18 @@ impl Session {
|
||||
}
|
||||
}
|
||||
self.pending_paint = paint;
|
||||
self.deferred_overlay_cache = Some((cmd.to_string(), p.clone(), overlays.clone()));
|
||||
overlays
|
||||
Some(DeferredFeedback { cmd: cmd.to_string(), params: p.clone(), overlays, transform_box })
|
||||
}
|
||||
}
|
||||
|
||||
/// What the canvas shows for a deferred preview: see [`Session::deferred_transform_box`].
|
||||
pub(crate) struct DeferredFeedback {
|
||||
cmd: String,
|
||||
params: Value,
|
||||
overlays: Vec<Overlay>,
|
||||
transform_box: Option<vectorcraft_doc::OrientedBox>,
|
||||
}
|
||||
|
||||
/// The most points the outlines of [`outlines_for_selection`] are drawn for, the same number as the
|
||||
/// canvas's limit on the anchor points it outlines (`OVERLAY_MAX_ANCHORS` in
|
||||
/// `crates/ui-egui/src/canvas/outline.rs`). Past it the selection's bounds are drawn instead.
|
||||
|
||||
@@ -148,10 +148,15 @@ pub trait Tool: Send {
|
||||
false
|
||||
}
|
||||
/// Is a drag moving, scaling or rotating the selection (the canvas then hides the bounding box,
|
||||
/// so only the art is seen going with the pointer)?
|
||||
/// so only the art is seen going with the pointer, except during a handle resize)?
|
||||
fn transforming(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Is the transforming drag a resize by a bounding-box handle? The canvas then keeps drawing
|
||||
/// the bounding box at the changing bounds.
|
||||
fn resizing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Does the tool take `key` now, ahead of the command shortcuts bound to it (the Gradient tool
|
||||
/// with a stop selected takes Delete and the arrows)?
|
||||
fn claims_key(&self, _cx: &ToolContext, _key: ToolKey) -> bool {
|
||||
|
||||
@@ -822,6 +822,10 @@ impl Tool for SelectionTool {
|
||||
self.alt_hover
|
||||
}
|
||||
|
||||
fn resizing(&self) -> bool {
|
||||
matches!(self.state, State::Scaling { .. })
|
||||
}
|
||||
|
||||
fn transforming(&self) -> bool {
|
||||
match &self.state {
|
||||
State::Moving { began: true, .. } | State::Scaling { .. } | State::Rotating { .. } => true,
|
||||
|
||||
@@ -110,9 +110,10 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf:
|
||||
// The Selection tool's bounding box (rotated with the objects after a rotation), also shown by
|
||||
// the shape tools that keep a shape just drawn ready to adjust while they draw none (#5477). It
|
||||
// hides while a drag moves or resizes the selection, so only the art is seen going with the
|
||||
// pointer (#712).
|
||||
// pointer (#712). A handle resize keeps it at the changing bounds (#3526).
|
||||
// With Real-time Drawing off the art waits for the release of a move drag, and the box moves
|
||||
// with the pointer instead.
|
||||
let resizing = app.session.tool_resizing();
|
||||
let moved = app.session.deferred_move();
|
||||
let shape_tool = vectorcraft_tools::shape::shows_shape_widgets(app.session.tool_id());
|
||||
// A shape tool drawing a shape shows none of this; adjusting a widget, it shows the widgets.
|
||||
@@ -122,10 +123,18 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf:
|
||||
let held = super::drag::mods(p.ctx().input(|i| i.modifiers), false);
|
||||
let lends_selection = !app.session.tool_busy() && app.session.lent_tool_id(&held) == Some("selection");
|
||||
let show_box = (app.session.tool_id() == "selection" || lends_selection || (shape_tool && !app.session.tool_busy()))
|
||||
&& (!app.session.tool_transforming() || moved.is_some())
|
||||
&& (resizing || !app.session.tool_transforming() || moved.is_some())
|
||||
&& app.ui.view.bounding_box
|
||||
&& app.session.active().is_some_and(|st| !st.selection.is_empty() && st.selection.anchors.is_empty());
|
||||
let bbox = if show_box { app.selection_box() } else { None };
|
||||
// With Real-time Drawing off the document stays at the drag's snapshot, so a resize measures the
|
||||
// box from the pending preview; otherwise each step's revision measures it once (`selection_box`).
|
||||
let bbox = if !show_box {
|
||||
None
|
||||
} else if let Some(b) = resizing.then(|| app.session.deferred_transform_box()).flatten() {
|
||||
Some(b)
|
||||
} else {
|
||||
app.selection_box()
|
||||
};
|
||||
let bbox = bbox.map(|b| match moved {
|
||||
Some(d) => vectorcraft_doc::OrientedBox { rect: b.rect + b.to_local(d.to_point()).to_vec2(), ..b },
|
||||
None => b,
|
||||
|
||||
@@ -3931,6 +3931,96 @@ fn hidden_corner_widgets_do_not_answer_the_pointer() {
|
||||
assert!(radius(&app).is_some_and(|r| r > 0.0), "{:?}", radius(&app));
|
||||
}
|
||||
|
||||
/// One headless canvas frame → the bounding box drawn: its outline's corners on screen (the
|
||||
/// closed four-point line in the layer color; `None` when it is hidden) and the number of its
|
||||
/// handles (white 6 px squares).
|
||||
fn bounding_box_drawn(app: &mut VectorcraftApp, ctx: &egui::Context) -> (Option<Vec<Pos2>>, usize) {
|
||||
let st = app.session.active().unwrap();
|
||||
let layer = egui::epaint::ColorMode::Solid(c32(st.doc.layer_color(st.selection.objects[0])));
|
||||
let drawn = shapes(app, ctx);
|
||||
let outline = drawn.iter().find_map(|s| match s {
|
||||
Shape::Path(ps) if ps.closed && ps.points.len() == 4 && ps.stroke.color == layer => Some(ps.points.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let handle = |s: &&Shape| matches!(s, Shape::Rect(r) if r.rect.size() == vec2(6.0, 6.0) && r.fill == Color32::WHITE);
|
||||
(outline, drawn.iter().filter(handle).count())
|
||||
}
|
||||
|
||||
/// The selection's bounding box corners on screen, as measured from the document.
|
||||
fn box_corners(app: &mut VectorcraftApp) -> Vec<Pos2> {
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
app.selection_box().unwrap().corners().iter().map(|q| xf.to_screen(*q)).collect()
|
||||
}
|
||||
|
||||
/// Do two outlines have the same corners, to 0.01 px?
|
||||
fn same_corners(a: &[Pos2], b: &[Pos2]) -> bool {
|
||||
a.len() == b.len() && a.iter().zip(b).all(|(p, q)| p.distance(*q) < 0.01)
|
||||
}
|
||||
|
||||
/// #3526: a resize by a bounding-box handle keeps the bounding box and its handles drawn at the
|
||||
/// bounds the drag gives, with Real-time Drawing on and off (off, the document stays at the drag's
|
||||
/// snapshot until the release).
|
||||
#[test]
|
||||
fn a_resize_drag_keeps_the_bounding_box_at_the_new_bounds() {
|
||||
for real_time in [true, false] {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.session.execute("shape.ellipse", &json!({"x": 100, "y": 100, "width": 100, "height": 60})).unwrap();
|
||||
app.session.execute("prefs.set", &json!({"key": "realTimeDrawing", "value": real_time})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.select_tool("selection");
|
||||
let before = box_corners(&mut app);
|
||||
let (outline, handles) = bounding_box_drawn(&mut app, &ctx);
|
||||
assert!(outline.is_some_and(|o| same_corners(&o, &before)) && handles == 8, "real time {real_time}: the box at rest");
|
||||
let view = app.view_info();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 160.0), view).unwrap();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Drag, 263.0, 211.0), view).unwrap();
|
||||
assert!(app.session.tool_resizing(), "real time {real_time}: the handle was hit");
|
||||
let (during, handles) = bounding_box_drawn(&mut app, &ctx);
|
||||
assert_eq!(handles, 8, "real time {real_time}: the handles show during the resize");
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Up, 263.0, 211.0), view).unwrap();
|
||||
let after = box_corners(&mut app);
|
||||
assert!(!same_corners(&after, &before), "real time {real_time}: the drag resized the ellipse");
|
||||
assert!(during.as_ref().is_some_and(|o| same_corners(o, &after)), "real time {real_time}: {during:?}, resized to {after:?}");
|
||||
let (outline, handles) = bounding_box_drawn(&mut app, &ctx);
|
||||
assert!(outline.is_some_and(|o| same_corners(&o, &after)) && handles == 8, "real time {real_time}: the box after the release");
|
||||
}
|
||||
}
|
||||
|
||||
/// A move, a rotation and a corner-radius drag keep hiding the bounding box and its handles until
|
||||
/// the release (#712, #1229).
|
||||
#[test]
|
||||
fn a_move_rotate_or_corner_radius_drag_hides_the_bounding_box() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.select_tool("selection");
|
||||
let drag = |app: &mut VectorcraftApp, from: Point, to: Point| {
|
||||
let view = app.view_info();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Down, from.x, from.y), view).unwrap();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Drag, to.x, to.y), view).unwrap();
|
||||
assert!(app.session.tool_transforming() && !app.session.tool_resizing());
|
||||
let during = bounding_box_drawn(app, &ctx);
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Up, to.x, to.y), view).unwrap();
|
||||
during
|
||||
};
|
||||
assert_eq!(bounding_box_drawn(&mut app, &ctx).1, 8);
|
||||
assert_eq!(drag(&mut app, Point::new(150.0, 150.0), Point::new(160.0, 160.0)), (None, 0), "a move");
|
||||
assert_eq!(bounding_box_drawn(&mut app, &ctx).1, 8, "back after the move");
|
||||
let offset = 10.0 / app.view_info().zoom;
|
||||
assert_eq!(drag(&mut app, Point::new(210.0 + offset, 210.0 + offset), Point::new(260.0, 160.0)), (None, 0), "a rotation");
|
||||
assert_eq!(bounding_box_drawn(&mut app, &ctx).1, 8, "back after the rotation");
|
||||
let corner = {
|
||||
let st = app.session.active().unwrap();
|
||||
vectorcraft_tools::corners::CornerWidgets::of(&st.doc, &st.selection, app.view_info().zoom, false).unwrap().visible().next().unwrap()
|
||||
};
|
||||
assert_eq!(drag(&mut app, corner, corner + vectorcraft_geom::Vec2::new(10.0, 10.0)), (None, 0), "a corner-radius drag");
|
||||
assert_eq!(bounding_box_drawn(&mut app, &ctx).1, 8, "back after the corner-radius drag");
|
||||
}
|
||||
|
||||
/// View › Hide Edges hides the bounding box and the type widget beside it, and the Selection
|
||||
/// tool then answers neither: a double-click on the widget's place leaves point type as it is,
|
||||
/// and a drag from a corner handle's place does not scale. With edges shown, both answer.
|
||||
|
||||
Reference in New Issue
Block a user