mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Merge pull request #304 from theironchef/fix/space-moves-shape-while-drawing
Shape tools: holding Space while drawing moves the shape
This commit is contained in:
4 files changed
+99
-12
No files matched your search
+1
-1
@@ -105,7 +105,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Tools:**
|
||||
- **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso.
|
||||
- **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join.
|
||||
- **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools.
|
||||
- **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size.
|
||||
- **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes).
|
||||
- **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres), Reshape, Free Transform (free and perspective distort and shear, switched by the keys held during a handle drag).
|
||||
- **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout. Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Line Segment family drag tools: Arc, Spiral, Rectangular Grid and Polar Grid.
|
||||
//!
|
||||
//! Drag draws (Shift = equal axes / square, Alt = from the centre for arc and grids); a click
|
||||
//! without dragging asks the UI for the options dialog. While dragging, ↑/↓ change the spiral's
|
||||
//! segments, the grid rows or the concentric dividers; ←/→ change grid columns / radial dividers.
|
||||
//! Drag draws (Shift = equal axes / square, Alt = from the centre for arc and grids, Space held
|
||||
//! moves the shape being drawn); a click without dragging asks the UI for the options dialog.
|
||||
//! While dragging, ↑/↓ change the spiral's segments, the grid rows or the concentric dividers; ←/→
|
||||
//! change grid columns / radial dividers.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Rect, Vec2};
|
||||
@@ -120,8 +121,9 @@ impl Tool for FamilyTool {
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some(s) = self.start else { return vec![] };
|
||||
self.last = ev.pos;
|
||||
let Some(mut s) = self.start else { return vec![] };
|
||||
crate::shape::space_moves(&mut s, &mut self.last, ev, self.began);
|
||||
self.start = Some(s);
|
||||
self.mods = ev.mods;
|
||||
let mut out = vec![];
|
||||
if !self.began {
|
||||
@@ -238,7 +240,13 @@ mod tests {
|
||||
);
|
||||
let a = t.key(&cx, ToolKey::Right, Mods::default());
|
||||
assert!(matches!(&a[0], Action::Preview(_, v) if v["columns"] == 6));
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 60.0, 40.0)), vec![Action::Commit]);
|
||||
// Space held moves the grid at its size; let go, it grows again from there.
|
||||
let space = Mods { space: true, ..Mods::default() };
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 80.0, 50.0).with_mods(space));
|
||||
assert!(matches!(&a[..], [Action::Preview(_, v)] if v["x"] == 30.0 && v["y"] == 20.0 && v["width"] == 50.0 && v["height"] == 30.0), "{a:?}");
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 60.0));
|
||||
assert!(matches!(&a[..], [Action::Preview(_, v)] if v["x"] == 30.0 && v["width"] == 60.0 && v["height"] == 40.0), "{a:?}");
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 90.0, 60.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Shape tools: Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment.
|
||||
//!
|
||||
//! Drag draws (Shift constrains to square/circle/45°, Alt draws from the centre); a click without a
|
||||
//! drag asks the UI for the size dialog (like Illustrator). ↑/↓ during a polygon/star drag change the
|
||||
//! side/point count.
|
||||
//! Drag draws (Shift constrains to square/circle/45°, Alt draws from the centre, Space held moves
|
||||
//! the shape being drawn); a click without a drag asks the UI for the size dialog (like
|
||||
//! Illustrator). ↑/↓ during a polygon/star drag change the side/point count.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
@@ -92,6 +92,15 @@ impl ShapeTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Space held once a shape drag has begun moves the shape being drawn instead of sizing it: `start`
|
||||
/// moves as far as the pointer did since `last`, which becomes the pointer.
|
||||
pub(crate) fn space_moves(start: &mut Point, last: &mut Point, ev: &PointerEvent, began: bool) {
|
||||
if ev.mods.space && began {
|
||||
*start += ev.pos - *last;
|
||||
}
|
||||
*last = ev.pos;
|
||||
}
|
||||
|
||||
/// The rectangle a drag from `start` to `p` draws: Shift makes it a square, Alt draws it from its
|
||||
/// centre.
|
||||
pub(crate) fn drag_rect(start: Point, p: Point, m: Mods) -> Rect {
|
||||
@@ -120,11 +129,12 @@ impl Tool for ShapeTool {
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some(s) = self.start else { return vec![] };
|
||||
let Some(mut s) = self.start else { return vec![] };
|
||||
let (pos, g) = crate::guides::snap_draw(cx, ev.pos, &[]);
|
||||
self.guides = g;
|
||||
let ev = &PointerEvent { pos, ..*ev };
|
||||
self.last = ev.pos;
|
||||
space_moves(&mut s, &mut self.last, ev, self.began);
|
||||
self.start = Some(s);
|
||||
self.mods = ev.mods;
|
||||
let mut out = vec![];
|
||||
if !self.began {
|
||||
@@ -239,6 +249,31 @@ mod tests {
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 50.0, 30.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_moves_the_shape_being_drawn() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let space = Mods { space: true, ..Default::default() };
|
||||
let mut t = ShapeTool::new("rectangle");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 120.0));
|
||||
// Space held: the 100 × 70 rectangle follows the pointer.
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 170.0).with_mods(space));
|
||||
assert_eq!(a.last(), Some(&Action::Preview("shape.rectangle".into(), json!({"x": 150.0, "y": 100.0, "width": 100.0, "height": 70.0}))));
|
||||
// Let go of Space: it grows again, from where it was moved to.
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 270.0, 190.0));
|
||||
assert_eq!(a.last(), Some(&Action::Preview("shape.rectangle".into(), json!({"x": 150.0, "y": 100.0, "width": 120.0, "height": 90.0}))));
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 270.0, 190.0)), vec![Action::Commit]);
|
||||
// A star (drawn from its centre) moves its centre.
|
||||
let mut t = ShapeTool::new("star");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 80.0, 50.0));
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 60.0).with_mods(space));
|
||||
assert!(matches!(a.last(), Some(Action::Preview(_, v)) if v["cx"] == 60.0 && v["cy"] == 60.0), "{a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_square_alt_center() {
|
||||
let t = ShapeTool::new("ellipse");
|
||||
|
||||
@@ -1655,6 +1655,50 @@ mod tests {
|
||||
assert!((after.center.x - (before.center.x - 30.0 / before.zoom)).abs() < 1e-6, "{before:?} → {after:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_pressed_mid_drag_moves_what_the_tool_draws() {
|
||||
use egui::{Event, Modifiers};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.ui.view.smart_guides = false;
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let space = |pressed| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE };
|
||||
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
// Press at `a`, drag to `b`, then hold Space while moving on by `by` and let go of both.
|
||||
let draw = |app: &mut VectorcraftApp, a: Pos2, b: Pos2, by: egui::Vec2| {
|
||||
frame(app, &ctx, vec![Event::PointerMoved(a)]);
|
||||
frame(app, &ctx, vec![button(a, true)]);
|
||||
frame(app, &ctx, vec![Event::PointerMoved(b)]);
|
||||
frame(app, &ctx, vec![space(true)]);
|
||||
frame(app, &ctx, vec![Event::PointerMoved(b + by)]);
|
||||
frame(app, &ctx, vec![space(false)]);
|
||||
frame(app, &ctx, vec![button(b + by, false)]);
|
||||
};
|
||||
let near = |p: Point, q: Point| p.distance(q) < 1e-3;
|
||||
let at = app.canvas_rect.unwrap().center() - vec2(100.0, 60.0);
|
||||
let center = app.view().unwrap().center;
|
||||
// The rectangle moves at its size instead of growing, and the view doesn't pan.
|
||||
app.select_tool("rectangle");
|
||||
draw(&mut app, at, at + vec2(80.0, 40.0), vec2(30.0, 20.0));
|
||||
assert_eq!(app.view().unwrap().center, center, "Space during a drag doesn't pan");
|
||||
let st = app.session.active().unwrap();
|
||||
let r = st.doc.node(st.selection.objects[0]).unwrap().path_data().unwrap().bounds().unwrap();
|
||||
assert!(
|
||||
near(Point::new(r.x0, r.y0), xf.to_doc(at + vec2(30.0, 20.0))) && near(Point::new(r.x1, r.y1), xf.to_doc(at + vec2(110.0, 60.0))),
|
||||
"{r:?}"
|
||||
);
|
||||
// The Pen's anchor being dragged out moves, handles and all.
|
||||
app.session.execute("select.none", &json!({})).unwrap();
|
||||
app.select_tool("pen");
|
||||
let a = at + vec2(0.0, 120.0);
|
||||
draw(&mut app, a, a + vec2(40.0, 0.0), vec2(20.0, 10.0));
|
||||
let st = app.session.active().unwrap();
|
||||
let anchor = st.doc.node(st.selection.objects[0]).unwrap().path_data().unwrap().subpaths[0].anchors[0];
|
||||
assert!(near(anchor.p, xf.to_doc(a + vec2(20.0, 10.0))) && near(anchor.h_out, xf.to_doc(a + vec2(60.0, 10.0))), "{anchor:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_type_with_the_selection_tool_puts_the_caret_there() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
||||
Reference in new issue
Block a user