mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
Fix #2898: Up and Down change the corner kind while dragging a widget
Squashed from #5090 onto main.
This commit is contained in:
@@ -485,3 +485,70 @@ fn relative_corner_rounding_keeps_fraction_on_resize() {
|
||||
let c = LiveCorners::of(n).unwrap();
|
||||
assert!((c.drawn_radius(0) / drawn - 2.0).abs() < 0.05, "drawn radius doubled with the shape");
|
||||
}
|
||||
|
||||
/// #2898: while a corner widget is pressed or dragged, Up and Down step the corners' kind (round,
|
||||
/// inverted round, chamfer, in turn) along with the radius, as one undo step with the drag.
|
||||
#[test]
|
||||
fn arrows_while_dragging_a_corner_widget_change_the_corner_kind() {
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
let (mut s, id) = session();
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("selection", v).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let w = vectorcraft_tools::corners::CornerWidgets::showing(&st.doc, &st.selection, v.zoom, false, true, 180.0).expect("corner widgets show");
|
||||
let at = w.visible().next().expect("a widget");
|
||||
let steps = |s: &Session| s.doc().unwrap().history.undo.len();
|
||||
let ptr = |s: &mut Session, kind: PointerKind, x: f64, y: f64| {
|
||||
s.pointer(&PointerEvent::new(kind, x, y).with_mods(Mods::default()), v).unwrap();
|
||||
};
|
||||
let n = steps(&s);
|
||||
// Press, Up twice (round → inverted round → chamfer), drag inward, release.
|
||||
ptr(&mut s, PointerKind::Down, at.x, at.y);
|
||||
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
||||
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
||||
ptr(&mut s, PointerKind::Drag, at.x + 8.0, at.y + 8.0);
|
||||
ptr(&mut s, PointerKind::Up, at.x + 8.0, at.y + 8.0);
|
||||
let (radii, kinds, _) = corners(&s, id);
|
||||
assert_eq!(kinds, [CornerKind::Chamfer; 4], "every shown corner takes the kind");
|
||||
assert!(radii.iter().all(|r| *r > 0.0), "and the radius: {radii:?}");
|
||||
assert_eq!(steps(&s), n + 1, "one undo step");
|
||||
// A press and Down without a drag: the previous kind, the radius kept, one more step. A
|
||||
// pointer move within the drag threshold after the press leaves the radii alone too.
|
||||
let st = s.doc().unwrap();
|
||||
let w = vectorcraft_tools::corners::CornerWidgets::showing(&st.doc, &st.selection, v.zoom, false, true, 180.0).unwrap();
|
||||
let at = w.visible().next().unwrap();
|
||||
ptr(&mut s, PointerKind::Down, at.x, at.y);
|
||||
s.tool_key(ToolKey::Down, Mods::default(), v).unwrap();
|
||||
ptr(&mut s, PointerKind::Drag, at.x + 1.0, at.y + 1.0);
|
||||
ptr(&mut s, PointerKind::Up, at.x + 1.0, at.y + 1.0);
|
||||
let (after, kinds, _) = corners(&s, id);
|
||||
assert_eq!(kinds, [CornerKind::InvertedRound; 4]);
|
||||
assert_eq!(after, radii);
|
||||
assert_eq!(steps(&s), n + 2);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(corners(&s, id).1, [CornerKind::Chamfer; 4]);
|
||||
}
|
||||
|
||||
/// An arrow press without a drag changes only the corners' kind: corners with different radii
|
||||
/// keep each their own (#2898 review).
|
||||
#[test]
|
||||
fn an_arrow_press_without_a_drag_keeps_mixed_radii() {
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
let (mut s, id) = session();
|
||||
run(&mut s, json!({"id": id.0, "corners": [0], "radius": 5}));
|
||||
run(&mut s, json!({"id": id.0, "corners": [2], "radius": 15}));
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("selection", v).unwrap();
|
||||
let before = corners(&s, id).0;
|
||||
let st = s.doc().unwrap();
|
||||
let w = vectorcraft_tools::corners::CornerWidgets::showing(&st.doc, &st.selection, v.zoom, false, true, 180.0).unwrap();
|
||||
let at = w.visible().next().unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(Mods::default()), v).unwrap();
|
||||
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, at.x, at.y).with_mods(Mods::default()), v).unwrap();
|
||||
let (radii, kinds, _) = corners(&s, id);
|
||||
assert_eq!(radii, before, "each corner keeps its radius");
|
||||
assert!(kinds.iter().all(|k| *k == CornerKind::InvertedRound), "{kinds:?}");
|
||||
}
|
||||
|
||||
@@ -45,6 +45,15 @@ impl CornerKind {
|
||||
CornerKind::Chamfer => CornerKind::Round,
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind before this one (the Down arrow while dragging a corner widget).
|
||||
pub fn previous(self) -> Self {
|
||||
match self {
|
||||
CornerKind::Round => CornerKind::Chamfer,
|
||||
CornerKind::InvertedRound => CornerKind::Round,
|
||||
CornerKind::Chamfer => CornerKind::InvertedRound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The largest corner radius a `w` × `h` rectangle draws: half its shorter side.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! corner anchor: one that isn't smooth, where two sides, straight or curved, meet at an angle. Dragging one rounds (or
|
||||
//! sharpens) the corners whose widgets show together, on every selected path (#938): every corner
|
||||
//! of a path selected whole, those with a selected anchor when Direct Selection picked some. Alt-clicking a widget cycles their corner
|
||||
//! kind (round, inverted round, chamfer); double-clicking one opens the Corners dialog. The
|
||||
//! kind (round, inverted round, chamfer), as the Up and Down arrows do while one is dragged
|
||||
//! (#2898); double-clicking one opens the Corners dialog. The
|
||||
//! Selection and Direct Selection tools share this, and the canvas draws the widgets from the same
|
||||
//! geometry.
|
||||
|
||||
@@ -239,7 +240,10 @@ pub struct CornerDrag {
|
||||
/// Each shown path's outline, in the order of [`CornerWidgets`]' paths.
|
||||
outlines: Vec<Outline>,
|
||||
start: Point,
|
||||
/// The interaction began (a drag or an arrow press): the corners preview and commit.
|
||||
began: bool,
|
||||
/// The pointer moved past the drag threshold: the radius follows it.
|
||||
dragging: bool,
|
||||
alt: bool,
|
||||
radius: f64,
|
||||
/// The pointer went past fully round: the dragged corner is at its largest radius.
|
||||
@@ -247,6 +251,9 @@ pub struct CornerDrag {
|
||||
at: Point,
|
||||
/// Direct Selection: what a click (no drag) on the pressed widget selects.
|
||||
click: Option<CornerClick>,
|
||||
/// The corner kind the Up and Down arrows picked during the drag, which it sets with the
|
||||
/// radius (#2898); None: the corners keep theirs.
|
||||
kind: Option<CornerKind>,
|
||||
}
|
||||
|
||||
/// Direct Selection: the drawn anchors of a widget's corner, which a click on the widget selects
|
||||
@@ -288,7 +295,20 @@ impl CornerDrag {
|
||||
Some(Outline { corners: lc.corners.clone(), base: lc.base.into_owned(), radii, kinds, xf: w.xf })
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(Self { widgets, widget, outlines, start: p, began: false, alt: ev.mods.alt, radius, at_limit: false, at: p, click })
|
||||
Some(Self {
|
||||
widgets,
|
||||
widget,
|
||||
outlines,
|
||||
start: p,
|
||||
began: false,
|
||||
dragging: false,
|
||||
alt: ev.mods.alt,
|
||||
radius,
|
||||
at_limit: false,
|
||||
at: p,
|
||||
click,
|
||||
kind: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The start radius (as drawn: no larger than fits) plus the pointer's travel along the
|
||||
@@ -344,16 +364,54 @@ impl CornerDrag {
|
||||
|
||||
pub fn drag(&mut self, cx: &ToolContext, p: Point) -> Vec<Action> {
|
||||
let mut out = vec![];
|
||||
if !self.began {
|
||||
if !self.dragging {
|
||||
// The drag threshold holds after an arrow press too: until the pointer passes it, the
|
||||
// radius stays as it was.
|
||||
if p.distance(self.start) < cx.tol(3.0) {
|
||||
return out;
|
||||
}
|
||||
self.began = true;
|
||||
out.push(Action::Begin("Corner Radius".into()));
|
||||
self.dragging = true;
|
||||
if !self.began {
|
||||
self.began = true;
|
||||
out.push(Action::Begin("Corner Radius".into()));
|
||||
}
|
||||
}
|
||||
(self.radius, self.at_limit) = self.radius_at(p);
|
||||
self.at = p;
|
||||
out.push(Action::Preview("object.setLiveShape".into(), self.widgets.command("radius", json!(self.radius))));
|
||||
out.push(self.preview());
|
||||
out
|
||||
}
|
||||
|
||||
/// The corners as the gesture sets them: the radius once the pointer drags it, and the kind
|
||||
/// the arrows picked. An arrow press without a drag sends the kind alone, so the corners keep
|
||||
/// their own radii.
|
||||
fn preview(&self) -> Action {
|
||||
let p = match (self.dragging, self.kind) {
|
||||
(true, kind) => {
|
||||
let mut p = self.widgets.command("radius", json!(self.radius));
|
||||
if let Some(k) = kind {
|
||||
p["kind"] = json!(k);
|
||||
}
|
||||
p
|
||||
}
|
||||
(false, Some(k)) => self.widgets.command("kind", json!(k)),
|
||||
(false, None) => self.widgets.command("radius", json!(self.radius)),
|
||||
};
|
||||
Action::Preview("object.setLiveShape".into(), p)
|
||||
}
|
||||
|
||||
/// The Up (`up`) or Down arrow while the widget is pressed or dragged: the corners take the
|
||||
/// next (or previous) kind, round, inverted round and chamfer in turn, starting from the
|
||||
/// pressed corner's, and keep it when the drag ends (#2898).
|
||||
pub fn step_kind(&mut self, up: bool) -> Vec<Action> {
|
||||
let Some(from) = self.kind.or_else(|| self.widgets.widget(self.widget.0, self.widget.1).map(|w| w.kind)) else { return vec![] };
|
||||
self.kind = Some(if up { from.next() } else { from.previous() });
|
||||
let mut out = vec![];
|
||||
if !self.began {
|
||||
self.began = true;
|
||||
out.push(Action::Begin("Corner Radius".into()));
|
||||
}
|
||||
out.push(self.preview());
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -659,6 +659,10 @@ impl Tool for DirectSelectionTool {
|
||||
/// entirely inside it, whole, and E pressed again makes it select what it selects by default.
|
||||
/// The arrow keys nudge the selected frame anchors of area type.
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec<Action> {
|
||||
// Up and Down while a corner widget is pressed: the corners' kind (#2898).
|
||||
if let (ToolKey::Up | ToolKey::Down, State::Corner(c)) = (key, &mut self.state) {
|
||||
return c.step_kind(key == ToolKey::Up);
|
||||
}
|
||||
if let Some(out) = self.mesh_nudge(cx, key, mods) {
|
||||
return out;
|
||||
}
|
||||
@@ -688,7 +692,10 @@ impl Tool for DirectSelectionTool {
|
||||
if frame { vec![Action::Cancel, Action::Notify(FRAME_PICKED.into())] } else { vec![Action::Cancel] }
|
||||
}
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
self.claims_letter(key) || self.mesh_nudge(cx, key, Mods::default()).is_some() || self.frame_nudge(cx, key, Mods::default()).is_some()
|
||||
(matches!(self.state, State::Corner(_)) && matches!(key, ToolKey::Up | ToolKey::Down))
|
||||
|| self.claims_letter(key)
|
||||
|| self.mesh_nudge(cx, key, Mods::default()).is_some()
|
||||
|| self.frame_nudge(cx, key, Mods::default()).is_some()
|
||||
}
|
||||
fn notify(&mut self, cx: &ToolContext, what: &str) {
|
||||
if what == FRAME_PICKED {
|
||||
|
||||
@@ -617,6 +617,10 @@ impl Tool for SelectionTool {
|
||||
/// is left, and the tool waits for the next press. With the pointer loaded with text, Escape
|
||||
/// drops the text.
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
// Up and Down while a corner widget is pressed: the corners' kind (#2898).
|
||||
if let (ToolKey::Up | ToolKey::Down, State::Corner(c)) = (key, &mut self.state) {
|
||||
return c.step_kind(key == ToolKey::Up);
|
||||
}
|
||||
if key == ToolKey::Letter('e')
|
||||
&& let State::Marquee { enclosed, .. } = &mut self.state
|
||||
{
|
||||
@@ -652,6 +656,7 @@ impl Tool for SelectionTool {
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
match self.state {
|
||||
State::Marquee { .. } => key == ToolKey::Letter('e'),
|
||||
State::Corner(_) => matches!(key, ToolKey::Up | ToolKey::Down),
|
||||
State::Loaded { .. } | State::Mirror(_) | State::GridRepeat(_) => key == ToolKey::Escape,
|
||||
_ => false,
|
||||
}
|
||||
|
||||
@@ -2053,3 +2053,43 @@ fn cmd_enter_bound_to_an_effect_runs_the_effect_not_the_tool() {
|
||||
assert_eq!(d.fields.get("__effect").and_then(serde_json::Value::as_str), Some("distort.transform"), "the effect's, not Move: {:?}", d.kind);
|
||||
crate::shortcut_editor::sync(&crate::state::UiState::default());
|
||||
}
|
||||
|
||||
/// #2898 review: Up and Down change the corner kind only while a corner widget is pressed. With
|
||||
/// none pressed, a plain Up still nudges the selected rectangle and leaves its corners alone; with
|
||||
/// one pressed, Up steps the kind and the rectangle stays where it is.
|
||||
#[test]
|
||||
fn a_plain_up_nudges_unless_a_corner_widget_is_pressed() {
|
||||
use vectorcraft_doc::{LiveShape, NodeId, NodeKind};
|
||||
use vectorcraft_geom::shapes::CornerKind;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = app.session.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap();
|
||||
let id = NodeId(id);
|
||||
app.select_tool("selection");
|
||||
app.session.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
let state = |app: &VectorcraftApp| {
|
||||
let st = app.session.active().unwrap();
|
||||
let b = st.doc.bounds_of(&[id], false).unwrap();
|
||||
let NodeKind::Path { live: Some(l), .. } = &st.doc.node(id).unwrap().kind else { panic!("not live") };
|
||||
let LiveShape::Rectangle { kinds, .. } = &**l else { panic!("not a rectangle") };
|
||||
((b.x0, b.y0), kinds[0])
|
||||
};
|
||||
let ((x, y), kind) = state(&app);
|
||||
frame(&mut app, press(Key::ArrowUp, Modifiers::NONE));
|
||||
let ((x1, y1), kind1) = state(&app);
|
||||
assert_eq!(x1, x, "no sideways move");
|
||||
assert!((y1 - y).abs() > 0.5, "a plain Up nudges the rectangle: {y} → {y1}");
|
||||
assert_eq!(kind1, kind, "and leaves its corners alone");
|
||||
// Press a corner widget: now Up steps the corner kind and the rectangle stays put.
|
||||
let view = app.view_info();
|
||||
let st = app.session.active().unwrap();
|
||||
let w = vectorcraft_tools::corners::CornerWidgets::showing(&st.doc, &st.selection, view.zoom, false, true, 180.0).expect("corner widgets show");
|
||||
let at = w.visible().next().expect("a widget");
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Down, at.x, at.y), view).unwrap();
|
||||
frame(&mut app, press(Key::ArrowUp, Modifiers::NONE));
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Up, at.x, at.y), view).unwrap();
|
||||
let ((x2, y2), kind2) = state(&app);
|
||||
assert_eq!((x2, y2), (x1, y1), "no nudge while the widget is pressed");
|
||||
assert_eq!(kind2, CornerKind::InvertedRound, "Up stepped the kind");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user