mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2482 from vangel76/fix/2432-dimension-shift-preview
Dimension tool: Shift preview, snap indicator, clicks snap onto lines and anchors (#2432, #2516)
This commit is contained in:
@@ -389,3 +389,66 @@ fn unknown_dimension_options_are_refused_by_commands_but_a_newer_file_still_open
|
||||
let doc = vectorcraft_format::load(&serde_json::to_vec(&v).unwrap()).unwrap();
|
||||
assert!(doc.node(NodeId(id)).unwrap().extras().dimension.is_some());
|
||||
}
|
||||
|
||||
/// With Shift down, the dashed line from the first picked point follows the 45-degree step the
|
||||
/// second click will pick (#2432), not the raw pointer.
|
||||
#[test]
|
||||
fn shift_shows_the_constrained_line_while_picking_the_second_point() {
|
||||
use vectorcraft_engine::tools::Overlay;
|
||||
let mut s = session();
|
||||
s.select_tool("dimension", ViewInfo::default()).unwrap();
|
||||
ev(&mut s, PointerKind::Down, 300.0, 300.0);
|
||||
ev(&mut s, PointerKind::Up, 300.0, 300.0);
|
||||
let end = |s: &mut Session, shift: bool| {
|
||||
let e = PointerEvent::new(PointerKind::Move, 372.0, 310.0).with_mods(Mods { shift, ..Mods::default() });
|
||||
s.pointer(&e, ViewInfo::default()).unwrap();
|
||||
s.overlays(ViewInfo::default()).into_iter().find_map(|o| if let Overlay::Line { b, .. } = o { Some(b) } else { None }).unwrap()
|
||||
};
|
||||
assert_eq!(end(&mut s, false).y, 310.0);
|
||||
assert!((end(&mut s, true).y - 300.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A click near a line, away from its anchors, snaps to the closest point of the line (#2432).
|
||||
#[test]
|
||||
fn clicking_near_a_line_snaps_the_measured_point_onto_it() {
|
||||
let mut s = session();
|
||||
edge(&mut s);
|
||||
s.select_tool("dimension", ViewInfo::default()).unwrap();
|
||||
for (x, y) in [(130.0, 103.0), (150.0, 97.0), (140.0, 130.0)] {
|
||||
ev(&mut s, PointerKind::Down, x, y);
|
||||
ev(&mut s, PointerKind::Up, x, y);
|
||||
}
|
||||
let id = s.doc().unwrap().selection.objects[0].0;
|
||||
assert_eq!(text(&s, id), "20.00 pt");
|
||||
}
|
||||
|
||||
/// Both picked points snap onto anchors a few pixels away, the rubber band and a ring show the
|
||||
/// snap before the click, and the dimension stays linked to the anchors (#2516).
|
||||
#[test]
|
||||
fn both_points_snap_onto_anchors_with_a_visible_indicator() {
|
||||
use vectorcraft_engine::tools::Overlay;
|
||||
let mut s = session();
|
||||
let line = edge(&mut s);
|
||||
s.select_tool("dimension", ViewInfo::default()).unwrap();
|
||||
ev(&mut s, PointerKind::Move, 103.0, 102.0);
|
||||
let ring = |s: &mut Session| {
|
||||
s.overlays(ViewInfo::default())
|
||||
.into_iter()
|
||||
.any(|o| matches!(o, Overlay::Anchor { p, size, .. } if size > 8.0 && p.x == 100.0 && p.y == 100.0))
|
||||
};
|
||||
assert!(ring(&mut s), "ring on the anchor before the first click");
|
||||
ev(&mut s, PointerKind::Down, 103.0, 102.0);
|
||||
ev(&mut s, PointerKind::Up, 103.0, 102.0);
|
||||
ev(&mut s, PointerKind::Move, 169.0, 98.0);
|
||||
let end = s.overlays(ViewInfo::default()).into_iter().find_map(|o| if let Overlay::Line { b, .. } = o { Some(b) } else { None }).unwrap();
|
||||
assert_eq!((end.x, end.y), (172.0, 100.0), "rubber band ends on the anchor");
|
||||
ev(&mut s, PointerKind::Down, 169.0, 98.0);
|
||||
ev(&mut s, PointerKind::Up, 169.0, 98.0);
|
||||
ev(&mut s, PointerKind::Down, 136.0, 130.0);
|
||||
ev(&mut s, PointerKind::Up, 136.0, 130.0);
|
||||
let id = s.doc().unwrap().selection.objects[0].0;
|
||||
assert_eq!(text(&s, id), "72.00 pt");
|
||||
run(&mut s, "select.set", json!({"ids":[line]}));
|
||||
run(&mut s, "object.scale", json!({"sx":200,"sy":100}));
|
||||
assert_eq!(text(&s, id), "144.00 pt", "linked to both anchors");
|
||||
}
|
||||
|
||||
@@ -60,9 +60,17 @@ pub struct DimensionTool {
|
||||
dragging: bool,
|
||||
hover: Option<Target>,
|
||||
at: Point,
|
||||
/// Whether Shift was down at the last pointer event: the picked line shows constrained.
|
||||
shift: bool,
|
||||
began: bool,
|
||||
}
|
||||
|
||||
/// `at` as a Shift click picks it: the direction from `last` snapped to 45° steps (from the
|
||||
/// constrain angle), the distance kept.
|
||||
fn constrained(cx: &ToolContext, last: Point, at: Point) -> Point {
|
||||
last + vectorcraft_geom::constrain_angle_from(at - last, 45.0, cx.constrain_angle)
|
||||
}
|
||||
|
||||
/// Nearest editable segment, with bounded work and no snapping to dimension artwork itself.
|
||||
pub fn pick(cx: &ToolContext, p: Point) -> Option<SegmentRef> {
|
||||
let mut best = None;
|
||||
@@ -111,6 +119,37 @@ fn anchor_near(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<(usize,
|
||||
.min_by(|(_, a), (_, b)| a.distance(p).total_cmp(&b.distance(p)))
|
||||
}
|
||||
|
||||
/// The point of `edge` closest to `p`.
|
||||
fn nearest_on(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<Point> {
|
||||
use vectorcraft_geom::kurbo::ParamCurveNearest;
|
||||
let sp = cx.doc.node(edge.node)?.path_data()?.subpaths.get(edge.subpath)?;
|
||||
if edge.segment >= sp.segment_count() {
|
||||
return None;
|
||||
}
|
||||
let c = sp.segment(edge.segment);
|
||||
let t = c.nearest(p, 1e-4).t;
|
||||
use vectorcraft_geom::kurbo::ParamCurve;
|
||||
let q = c.eval(t);
|
||||
(q.x.is_finite() && q.y.is_finite()).then_some(q)
|
||||
}
|
||||
|
||||
/// Where a Linear pick at `p` lands: on an anchor within reach (kept as a link to its path),
|
||||
/// else on the closest point of the picked line, else on the usual snap targets or `p` itself.
|
||||
fn snap_linear(cx: &ToolContext, p: Point) -> (Point, Option<PointRef>) {
|
||||
let (mut at, _) = crate::guides::snap_pick(cx, p);
|
||||
let mut reference = None;
|
||||
if let Some(edge) = pick(cx, p) {
|
||||
if let Some((anchor, point)) = anchor_near(cx, &edge, p) {
|
||||
at = point;
|
||||
reference = Some(PointRef { node: edge.node, subpath: edge.subpath, anchor });
|
||||
} else if let Some(point) = nearest_on(cx, &edge, p) {
|
||||
// Not on an anchor: snap to the closest point of the line (#2432).
|
||||
at = point;
|
||||
}
|
||||
}
|
||||
(at, reference)
|
||||
}
|
||||
|
||||
impl DimensionTool {
|
||||
fn target(&self, cx: &ToolContext, p: Point) -> Option<Target> {
|
||||
let edge = pick(cx, p)?;
|
||||
@@ -206,18 +245,11 @@ impl DimensionTool {
|
||||
fn click(&mut self, cx: &ToolContext, p: Point, target: Option<Target>, mods: Mods) -> Vec<Action> {
|
||||
match self.kind {
|
||||
Kind::Linear => {
|
||||
let (mut at, _) = crate::guides::snap_pick(cx, p);
|
||||
let mut reference = None;
|
||||
if let Some(edge) = pick(cx, p)
|
||||
&& let Some((anchor, point)) = anchor_near(cx, &edge, p)
|
||||
{
|
||||
at = point;
|
||||
reference = Some(PointRef { node: edge.node, subpath: edge.subpath, anchor });
|
||||
}
|
||||
let (mut at, mut reference) = snap_linear(cx, p);
|
||||
if mods.shift
|
||||
&& let Some((last, _)) = self.points.last()
|
||||
{
|
||||
at = *last + vectorcraft_geom::constrain_angle_from(at - *last, 45.0, cx.constrain_angle);
|
||||
at = constrained(cx, *last, at);
|
||||
reference = reference.filter(|r| r.position(cx.doc) == Some(at));
|
||||
}
|
||||
self.points.push((at, reference));
|
||||
@@ -293,6 +325,7 @@ impl Tool for DimensionTool {
|
||||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
self.at = ev.pos;
|
||||
self.shift = ev.mods.shift;
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
if self.pending.is_some() {
|
||||
@@ -392,8 +425,20 @@ impl Tool for DimensionTool {
|
||||
for edge in &self.lines {
|
||||
segment(edge, &mut out);
|
||||
}
|
||||
// Linear: show where the next click lands (anchor ring, or a dot on a line), and draw the
|
||||
// rubber band to that point, so the user sees the snap before clicking (#2516).
|
||||
let linear_pick = self.kind == Kind::Linear && self.pending.is_none() && self.lines.is_empty();
|
||||
let snapped = linear_pick.then(|| snap_linear(cx, self.at));
|
||||
if let Some((q, reference)) = &snapped
|
||||
&& (reference.is_some() || q.distance(self.at) > 1e-9)
|
||||
{
|
||||
let on_anchor = reference.is_some();
|
||||
out.push(Overlay::Anchor { p: *q, color, filled: !on_anchor, size: if on_anchor { 10.0 } else { 6.0 } });
|
||||
}
|
||||
if let Some((p, _)) = self.points.last() {
|
||||
out.push(Overlay::Line { a: *p, b: self.at, color, dashed: true });
|
||||
let target = snapped.map_or(self.at, |(q, _)| q);
|
||||
let b = if self.shift { constrained(cx, *p, target) } else { target };
|
||||
out.push(Overlay::Line { a: *p, b, color, dashed: true });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user