mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Fix: a path moved by its stroke or edge lands exactly on a ruler guide
Cause: with Snap to Point, the point of a moved selection that snaps to a ruler guide is the grabbed point (#1534). Away from an anchor or center that was the raw pressed spot (select.rs: `grab` fell back to `p`; direct.rs: a straight segment's `grab: p`, MoveObject's `start`). A press on a stroke or within the pick tolerance of an edge lies a few pixels beside the path, so the pressed spot landed on the guide, "align" showed, and the path stopped that far beside it: 0.096 pt at 3,891% and 0.0099 pt at 28,757% in the #3701 screenshots, visible only when zoomed in. Change: `guides::hold_on_path` finds the nearest point of the path (on a straight segment exactly on its line). `select::hold_point` uses it for a press on the stroke or outline, or within 6 px of it. The Selection tool (`State::Moving::hold`), Direct and Group Selection moves (`State::MoveObject::hold`) and Direct Selection's straight-segment drag (`grab`) snap that point. The move delta still runs from the press, so nothing jumps. A press in the middle of a fill still snaps the pressed spot, as Snap to Point is documented to. Verification: tests_prefs::a_move_held_by_a_paths_stroke_lands_the_path_on_a_ruler_guide and tests_prefs::a_straight_segment_dragged_by_direct_selection_lands_on_a_ruler_guide fail without the change (edge 0.0257 pt and anchors -0.0052 pt off the guide) and pass with it (|gap| < 1e-9 pt). A headless egui/wgpu render at 3,891% and 28,757% puts the guide, the selection outline and the centre of the 1 pt stroke band in the same pixel column. cargo xtask ci: all 10 steps passed; 9429 tests passed, 0 failed.
This commit is contained in:
committed by
bflatastic
parent
f365468684
commit
970e62f27d
+16
-11
@@ -43,24 +43,27 @@ use crate::guides::{HandleSnap, Leave, PointSnap, Targets};
|
||||
use crate::meshedit::MeshEdit;
|
||||
use crate::pathtype::{self, BracketDrag, over_bracket};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
use crate::select::{MoveSnap, is_area_type, matrix_json, release_guide_at};
|
||||
use crate::select::{MoveSnap, hold_point, is_area_type, matrix_json, release_guide_at};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum State {
|
||||
Idle,
|
||||
/// Dragging the selected anchors, which move alike: `grab` is where the anchor pressed on (or
|
||||
/// the pointer on a segment) was, the point that snaps.
|
||||
/// the point of a straight segment nearest the pointer pressed on it) was, the point that
|
||||
/// snaps.
|
||||
MoveAnchors {
|
||||
start: Point,
|
||||
grab: Point,
|
||||
began: bool,
|
||||
},
|
||||
/// Moving (Alt: copying) the selected objects; `grab`: the anchor an Alt-press picked the
|
||||
/// path by, which snaps as a dragged anchor does.
|
||||
/// path by, which snaps as a dragged anchor does; else `hold`, where the press took hold of
|
||||
/// the object ([`hold_point`]), snaps as the Selection tool's does.
|
||||
MoveObject {
|
||||
start: Point,
|
||||
grab: Option<Point>,
|
||||
hold: Point,
|
||||
began: bool,
|
||||
/// Group Selection: Shift-click on a selected object toggles it off on release.
|
||||
deselect: Option<NodeId>,
|
||||
@@ -473,7 +476,7 @@ impl Tool for DirectSelectionTool {
|
||||
out.push(Action::Exec("select.set".into(), json!({"ids": [target.0]})));
|
||||
}
|
||||
}
|
||||
self.state = State::MoveObject { start: p, grab: None, began: false, deselect };
|
||||
self.state = State::MoveObject { start: p, grab: None, hold: hold_point(cx, &h, p), began: false, deselect };
|
||||
out
|
||||
}
|
||||
(PointerKind::DoubleClick, _) if self.group => release_guide_at(cx, p, ev.mods).into_iter().collect(),
|
||||
@@ -538,7 +541,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
// Alt picks the whole path (and Alt-drag copies it), as on a segment.
|
||||
if ev.mods.alt {
|
||||
self.state = State::MoveObject { start: p, grab: Some(grab), began: false, deselect: None };
|
||||
self.state = State::MoveObject { start: p, grab: Some(grab), hold: grab, began: false, deselect: None };
|
||||
let whole = cx.selection.contains(id) && cx.selection.partial(id).is_none();
|
||||
return if whole { vec![] } else { vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))] };
|
||||
}
|
||||
@@ -549,7 +552,7 @@ impl Tool for DirectSelectionTool {
|
||||
// Paths all selected whole move as objects, held by this anchor, so Alt can turn
|
||||
// the copy on and off during the drag as with the Selection tool.
|
||||
self.state = if already && cx.selection.anchors.is_empty() {
|
||||
State::MoveObject { start: p, grab: Some(grab), began: false, deselect: None }
|
||||
State::MoveObject { start: p, grab: Some(grab), hold: grab, began: false, deselect: None }
|
||||
} else {
|
||||
State::MoveAnchors { start: p, grab, began: false }
|
||||
};
|
||||
@@ -593,7 +596,9 @@ impl Tool for DirectSelectionTool {
|
||||
self.state = if s.curved {
|
||||
State::Segment { id: h.leaf, si: s.si, seg: s.seg, t: s.t, start: p, began: false }
|
||||
} else {
|
||||
State::MoveAnchors { start: p, grab: p, began: false }
|
||||
// Held by the segment itself, so the segment is what lands on a
|
||||
// ruler guide or lines up, not the pressed spot beside it (#3701).
|
||||
State::MoveAnchors { start: p, grab: hold_point(cx, &h, p), began: false }
|
||||
};
|
||||
if selected {
|
||||
return vec![];
|
||||
@@ -602,7 +607,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
}
|
||||
// Clicking the fill selects the whole leaf path (all anchors).
|
||||
self.state = State::MoveObject { start: p, grab: None, began: false, deselect: None };
|
||||
self.state = State::MoveObject { start: p, grab: None, hold: hold_point(cx, &h, p), began: false, deselect: None };
|
||||
if ev.mods.shift {
|
||||
return vec![Action::Exec("select.toggle".into(), json!({"id": h.leaf.0}))];
|
||||
}
|
||||
@@ -654,14 +659,14 @@ impl Tool for DirectSelectionTool {
|
||||
));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, State::MoveObject { start, grab, began, .. }) => {
|
||||
(PointerKind::Drag, State::MoveObject { start, grab, hold, began, .. }) => {
|
||||
let mut out = vec![];
|
||||
if !began {
|
||||
if p.distance(start) < cx.tol(3.0) {
|
||||
return out;
|
||||
}
|
||||
out.push(Action::Begin(if ev.mods.alt { "Copy".into() } else { "Move".into() }));
|
||||
self.state = State::MoveObject { start, grab, began: true, deselect: None };
|
||||
self.state = State::MoveObject { start, grab, hold, began: true, deselect: None };
|
||||
if grab.is_some() {
|
||||
// Alt can go down or up at any time: gather both while nothing has moved.
|
||||
self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection)));
|
||||
@@ -679,7 +684,7 @@ impl Tool for DirectSelectionTool {
|
||||
let (q, guides) = snap.snap_from(cx, grab + (p - start), shift.as_ref());
|
||||
(d, self.guides) = (q - grab, guides);
|
||||
} else if let Some(snap) = &self.move_snap {
|
||||
(d, self.guides) = if ev.mods.shift { snap.snap_along(cx, start, d, ev.mods.alt) } else { snap.snap(cx, start, d, ev.mods.alt) };
|
||||
(d, self.guides) = if ev.mods.shift { snap.snap_along(cx, hold, d, ev.mods.alt) } else { snap.snap(cx, hold, d, ev.mods.alt) };
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
out.push(Action::Preview(
|
||||
|
||||
@@ -1225,6 +1225,24 @@ fn touch_params(seg: &PathSeg, at: Point, perpendicular: bool) -> Vec<f64> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Where a press at `p` takes hold of `path` by its outline: the point of the path nearest `p`,
|
||||
/// when it is within `reach`, and on a straight segment exactly on its line. A path dragged by a
|
||||
/// press on its stroke or beside its edge snaps by that point of the path, so it is the path that
|
||||
/// lands on a ruler guide or lines up, not the pressed spot a few pixels beside it (#3701).
|
||||
pub(crate) fn hold_on_path(path: &vectorcraft_geom::PathData, p: Point, reach: f64) -> Option<Point> {
|
||||
let (si, seg, _, q, d) = path.nearest(p)?;
|
||||
if d > reach {
|
||||
return None;
|
||||
}
|
||||
let sp = path.subpaths.get(si)?;
|
||||
if !sp.segment_is_line(seg) {
|
||||
return Some(q);
|
||||
}
|
||||
let c = sp.segment(seg);
|
||||
let line = Line::new(c.p0, c.p3);
|
||||
Some(line.eval(line.nearest(p, 0.0).t))
|
||||
}
|
||||
|
||||
/// Snap `p` for a drawing tool when smart guides (or grid snapping) are on.
|
||||
pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec<Overlay>) {
|
||||
snap_with(cx, p, || Targets::collect(cx.doc, exclude, None))
|
||||
|
||||
@@ -27,13 +27,13 @@ use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::hit::{hit_test, marquee, objects_at};
|
||||
use vectorcraft_doc::hit::{Hit, HitKind, hit_test, marquee, objects_at};
|
||||
use vectorcraft_doc::{NodeId, OrientedBox};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
use crate::bbox::{Handle, hit_handle, in_rotate_zone, move_delta, rotate_for_drag, scale_for_drag};
|
||||
use crate::corners::{self, CornerDrag, over_widget};
|
||||
use crate::guides::{PointSnap, Targets};
|
||||
use crate::guides::{PointSnap, Targets, hold_on_path};
|
||||
use crate::pathtype::{self, BracketDrag, over_bracket};
|
||||
use crate::pie::{self, PieDrag};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
@@ -48,6 +48,9 @@ enum State {
|
||||
/// Pressed on an object; becomes Moving after the drag threshold.
|
||||
Moving {
|
||||
start: Point,
|
||||
/// The point that snaps, moving with the pointer: `start`, or where a press on a path's
|
||||
/// stroke or beside its edge took hold of the path ([`hold_point`]).
|
||||
hold: Point,
|
||||
began: bool,
|
||||
/// Shift-pressed on this selected object: released without a drag, it leaves the
|
||||
/// selection.
|
||||
@@ -397,6 +400,14 @@ pub(crate) fn bbox_handle_at(cx: &ToolContext, p: Point) -> bool {
|
||||
/// to label it while hovering.
|
||||
const HOVER_REACH: f64 = 6.0;
|
||||
|
||||
/// Where a press at `p` holds the object it hit (`h`) for a move: the nearest point of the path
|
||||
/// hit, when the press is on its stroke or outline or within [`HOVER_REACH`] of it, else `p`
|
||||
/// ([`hold_on_path`]). That point snaps, so the path itself lands on a ruler guide (#3701).
|
||||
pub(crate) fn hold_point(cx: &ToolContext, h: &Hit, p: Point) -> Point {
|
||||
let reach = if matches!(h.kind, HitKind::Stroke | HitKind::Outline) { f64::INFINITY } else { cx.tol(HOVER_REACH) };
|
||||
cx.doc.node(h.leaf).and_then(|n| n.path_data()).and_then(|pd| hold_on_path(pd, p, reach)).unwrap_or(p)
|
||||
}
|
||||
|
||||
impl SelectionTool {
|
||||
fn drag_threshold(cx: &ToolContext) -> f64 {
|
||||
cx.tol(3.0)
|
||||
@@ -560,7 +571,8 @@ impl Tool for SelectionTool {
|
||||
let m = ev.mods;
|
||||
// A press near a centre or an anchor grabs the object by it.
|
||||
self.hover = None;
|
||||
let grab = if matches!(ev.kind, PointerKind::Down) { Self::hover_point(cx, p).map_or(p, |(q, _)| q) } else { p };
|
||||
let grabbed = if matches!(ev.kind, PointerKind::Down) { Self::hover_point(cx, p).map(|(q, _)| q) } else { None };
|
||||
let grab = grabbed.unwrap_or(p);
|
||||
if matches!(ev.kind, PointerKind::Move) && matches!(self.state, State::Idle) {
|
||||
self.hover = Self::hover_point(cx, p);
|
||||
}
|
||||
@@ -658,7 +670,7 @@ impl Tool for SelectionTool {
|
||||
&& cx.select_behind
|
||||
&& let Some(behind) = object_behind(cx, p)
|
||||
{
|
||||
self.state = State::Moving { start: p, began: false, deselect: None, key: None };
|
||||
self.state = State::Moving { start: p, hold: p, began: false, deselect: None, key: None };
|
||||
return vec![Action::Exec("select.set".into(), json!({ "ids": [behind.0] }))];
|
||||
}
|
||||
match hit_test(cx.doc, p, cx.selection_hit_options()) {
|
||||
@@ -679,7 +691,10 @@ impl Tool for SelectionTool {
|
||||
} else if (cx.selection.objects.len() > 1 || !cx.selection.guides.is_empty()) && !m.cmd && !m.alt {
|
||||
key = Some(top);
|
||||
}
|
||||
self.state = State::Moving { start: grab, began: false, deselect, key };
|
||||
// Grabbed by no anchor or centre, a press on a path's stroke or beside its
|
||||
// edge holds the path there, and that point of it snaps (#3701).
|
||||
let hold = if grabbed.is_some() { grab } else { hold_point(cx, &h, p) };
|
||||
self.state = State::Moving { start: grab, hold, began: false, deselect, key };
|
||||
out
|
||||
}
|
||||
None => {
|
||||
@@ -688,7 +703,7 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
}
|
||||
}
|
||||
(PointerKind::Drag, State::Moving { start, began, .. }) => {
|
||||
(PointerKind::Drag, State::Moving { start, hold, began, .. }) => {
|
||||
let mut out = vec![];
|
||||
if !began {
|
||||
if p.distance(start) < Self::drag_threshold(cx) {
|
||||
@@ -700,11 +715,11 @@ impl Tool for SelectionTool {
|
||||
let mut d = move_delta(start, p, m.shift);
|
||||
let unsnapped = d;
|
||||
if let Some(snap) = &self.moving {
|
||||
(d, self.guides) = if m.shift { snap.snap_along(cx, start, d, m.alt) } else { snap.snap(cx, start, d, m.alt) };
|
||||
(d, self.guides) = if m.shift { snap.snap_along(cx, hold, d, m.alt) } else { snap.snap(cx, hold, d, m.alt) };
|
||||
}
|
||||
// Shift has already constrained `d`: any change is a snap.
|
||||
self.snapped = (d - unsnapped).hypot() > 1e-9;
|
||||
self.state = State::Moving { start, began: true, deselect: None, key: None };
|
||||
self.state = State::Moving { start, hold, began: true, deselect: None, key: None };
|
||||
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(d.x, d.y)));
|
||||
out.push(Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt })));
|
||||
out
|
||||
|
||||
Reference in New Issue
Block a user