mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Merge pull request #3973 from vecteza/feat/dimension-smart-guides
Dimension tool: Linear picks show and snap to Smart Guides
This commit is contained in:
@@ -452,6 +452,38 @@ fn both_points_snap_onto_anchors_with_a_visible_indicator() {
|
||||
assert_eq!(text(&s, id), "144.00 pt", "linked to both anchors");
|
||||
}
|
||||
|
||||
/// Smart Guides for the Linear picks: hovering away from any line, the first point slides into
|
||||
/// line with another object's corner and its guides show before the click; the second slides onto
|
||||
/// the construction guide through the first. The dimension measures between the snapped points.
|
||||
#[test]
|
||||
fn linear_picks_show_and_snap_to_smart_guides() {
|
||||
use vectorcraft_engine::tools::Overlay;
|
||||
let mut s = session();
|
||||
run(&mut s, "shape.rectangle", json!({"x":300,"y":200,"width":80,"height":60}));
|
||||
run(&mut s, "select.none", json!({}));
|
||||
s.select_tool("dimension", ViewInfo::default()).unwrap();
|
||||
let overlays = |s: &mut Session| s.overlays(ViewInfo::default());
|
||||
// In line with the rectangle's top edge, 2.5 pt off it: the pick lands on its level.
|
||||
ev(&mut s, PointerKind::Move, 250.0, 202.5);
|
||||
let o = overlays(&mut s);
|
||||
assert!(o.iter().any(|o| matches!(o, Overlay::Anchor { p, .. } if p.x == 250.0 && p.y == 200.0)), "the pick shows on the guide: {o:?}");
|
||||
assert!(o.iter().any(|o| matches!(o, Overlay::Line { .. } | Overlay::Label { .. })), "its guides show before the click: {o:?}");
|
||||
ev(&mut s, PointerKind::Down, 250.0, 202.5);
|
||||
ev(&mut s, PointerKind::Up, 250.0, 202.5);
|
||||
// Straight below the first point, 1.5 pt off: the rubber band ends on the vertical through it.
|
||||
ev(&mut s, PointerKind::Move, 251.5, 260.0);
|
||||
let end = overlays(&mut s).into_iter().find_map(|o| if let Overlay::Line { b, dashed: true, .. } = o { Some(b) } else { None }).unwrap();
|
||||
assert_eq!((end.x, end.y), (250.0, 260.0), "on the construction guide");
|
||||
ev(&mut s, PointerKind::Down, 251.5, 260.0);
|
||||
ev(&mut s, PointerKind::Up, 251.5, 260.0);
|
||||
ev(&mut s, PointerKind::Move, 220.0, 230.0);
|
||||
assert!(!overlays(&mut s).iter().any(|o| matches!(o, Overlay::Label { .. })), "placing: the guides are gone");
|
||||
ev(&mut s, PointerKind::Down, 220.0, 230.0);
|
||||
ev(&mut s, PointerKind::Up, 220.0, 230.0);
|
||||
let id = s.doc().unwrap().selection.objects[0].0;
|
||||
assert_eq!(text(&s, id), "60.00 pt");
|
||||
}
|
||||
|
||||
/// The page content streams of exported PDF `v`, inflated.
|
||||
fn pdf_content(v: &Value) -> String {
|
||||
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
|
||||
|
||||
@@ -63,6 +63,10 @@ pub struct DimensionTool {
|
||||
/// Whether Shift was down at the last pointer event: the picked line shows constrained.
|
||||
shift: bool,
|
||||
began: bool,
|
||||
/// Smart Guides for the Linear picks, hovering too: the guides show before each click.
|
||||
snap: crate::guides::DrawSnap,
|
||||
/// Where the next Linear pick lands, as last worked out for the pointer (for the overlays).
|
||||
snapped: Option<(Point, Option<PointRef>)>,
|
||||
}
|
||||
|
||||
/// `at` as a Shift click picks it: the direction from `last` snapped to 45° steps (from the
|
||||
@@ -133,24 +137,27 @@ fn nearest_on(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<Point> {
|
||||
(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 {
|
||||
/// Where a Linear pick at `p` lands: on an anchor of the line under the pointer, within reach
|
||||
/// (kept as a link to its path); else where Smart Guides put it (another object's anchor or
|
||||
/// centre, in line with one, or for the second point on a construction guide through the
|
||||
/// first), its guides showing; else on the closest point of that line; else `p` itself.
|
||||
fn snap_linear(&mut self, cx: &ToolContext, p: Point) -> (Point, Option<PointRef>) {
|
||||
let leave = self.points.last().map(|(first, _)| crate::guides::Leave::segment(cx, *first, false));
|
||||
let guided = self.snap.hover(cx, p, &[], leave.as_ref());
|
||||
let edge = pick(cx, p);
|
||||
if let Some(edge) = &edge
|
||||
&& let Some((anchor, point)) = anchor_near(cx, edge, p)
|
||||
{
|
||||
self.snap.clear();
|
||||
return (point, Some(PointRef { node: edge.node, subpath: edge.subpath, anchor }));
|
||||
}
|
||||
if guided.distance(p) > 1e-9 {
|
||||
return (guided, None);
|
||||
}
|
||||
// Not on an anchor or a guide: snap to the closest point of the line (#2432).
|
||||
(edge.and_then(|e| nearest_on(cx, &e, p)).unwrap_or(p), None)
|
||||
}
|
||||
fn target(&self, cx: &ToolContext, p: Point) -> Option<Target> {
|
||||
let edge = pick(cx, p)?;
|
||||
match self.kind {
|
||||
@@ -240,12 +247,14 @@ impl DimensionTool {
|
||||
self.lines.clear();
|
||||
self.dragging = false;
|
||||
self.began = false;
|
||||
self.snap.clear();
|
||||
self.snapped = None;
|
||||
}
|
||||
/// A plain click: picks a point or a line, or measures a circle's radius.
|
||||
fn click(&mut self, cx: &ToolContext, p: Point, target: Option<Target>, mods: Mods) -> Vec<Action> {
|
||||
match self.kind {
|
||||
Kind::Linear => {
|
||||
let (mut at, mut reference) = snap_linear(cx, p);
|
||||
let (mut at, mut reference) = self.snap_linear(cx, p);
|
||||
if mods.shift
|
||||
&& let Some((last, _)) = self.points.last()
|
||||
{
|
||||
@@ -329,9 +338,16 @@ impl Tool for DimensionTool {
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
if self.pending.is_some() {
|
||||
// Placing: the picks are done, their guides go.
|
||||
self.snap.clear();
|
||||
self.snapped = None;
|
||||
return self.preview(cx, ev.pos);
|
||||
}
|
||||
self.hover = self.target(cx, ev.pos);
|
||||
self.snapped = (self.kind == Kind::Linear && self.lines.is_empty()).then(|| self.snap_linear(cx, ev.pos));
|
||||
if self.snapped.is_none() {
|
||||
self.snap.clear();
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
@@ -430,7 +446,7 @@ impl Tool for DimensionTool {
|
||||
// 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));
|
||||
let snapped = self.snapped.clone().filter(|_| linear_pick);
|
||||
if let Some((q, reference)) = &snapped
|
||||
&& (reference.is_some() || q.distance(self.at) > 1e-9)
|
||||
{
|
||||
@@ -442,6 +458,10 @@ impl Tool for DimensionTool {
|
||||
let b = if self.shift { constrained(cx, *p, target) } else { target };
|
||||
out.push(Overlay::Line { a: *p, b, color, dashed: true });
|
||||
}
|
||||
// The Smart Guides of the point the next click picks (after the rubber band).
|
||||
if linear_pick {
|
||||
out.extend(self.snap.guides().iter().cloned());
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -5,7 +5,9 @@ The **Dimension Tool** is in the Modify section of the toolbar. Its strip offers
|
||||
|
||||
- **Linear:** hover a path segment and drag outwards: releasing places the dimension. Or click two
|
||||
points (anchors snap, Shift constrains to 45°), then click to place. A curved segment is measured
|
||||
from end to end.
|
||||
from end to end. With Smart Guides on, both points also snap into line with other objects'
|
||||
anchors and centres, and the second onto the construction guides through the first; the guides
|
||||
show while hovering, before each click.
|
||||
- **Angular:** hover a corner where two straight segments meet and drag outwards. Or click two
|
||||
straight lines that meet (or would, extended), then click to place.
|
||||
- **Radial:** hover a circle, or a segment that is a perfect arc, and drag: outwards gives its
|
||||
|
||||
Reference in New Issue
Block a user