mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 21:26:48 -06:00
Snap: pen and anchor points
This commit is contained in:
committed by
Brandon Thomas
parent
e76570c875
commit
09c8dc3f06
@@ -237,7 +237,9 @@ fn pen_draws_and_direct_selection_edits() {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 90.0, 80.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 90.0, 80.0), v).unwrap();
|
||||
let a = s.doc().unwrap().doc.item(id).unwrap().path.subpaths[0].anchors[0].p;
|
||||
assert_eq!(a, designcraft_geom::Point::new(90.0, 80.0));
|
||||
// The pen placed this anchor on the baseline at y 96. Dragging toward (90, 80)
|
||||
// proposes y 76, and the baseline at 72 is inside the zone, so the anchor lands at (90, 72).
|
||||
assert_eq!(a, designcraft_geom::Point::new(90.0, 72.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+32
-9
@@ -17,6 +17,8 @@ pub struct PenTool {
|
||||
anchors: Vec<(Point, Point, Point)>,
|
||||
dragging: bool,
|
||||
hover: Point,
|
||||
/// Smart guides in canvas coordinates, as `snap_point` returned them.
|
||||
guides: Vec<Overlay>,
|
||||
}
|
||||
|
||||
fn anchor_json(a: &(Point, Point, Point)) -> Value {
|
||||
@@ -29,8 +31,20 @@ impl PenTool {
|
||||
self.spread = None;
|
||||
self.anchors.clear();
|
||||
self.dragging = false;
|
||||
self.guides.clear();
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Shift constrains first. The stored point is the snapped spread point. Guides are already canvas.
|
||||
fn snap_click(&mut self, cx: &ToolContext, spread: SpreadRef, mut p: Point, shift: bool) -> Point {
|
||||
if shift && let Some(last) = self.anchors.last() {
|
||||
p = last.0 + designcraft_geom::constrain_angle(p - last.0, 45.0);
|
||||
}
|
||||
let (p, guides) = crate::snap::snap_point(cx, spread, p);
|
||||
self.guides = guides;
|
||||
p
|
||||
}
|
||||
|
||||
fn commit_anchor(&mut self) -> Vec<Action> {
|
||||
let Some(sr) = self.spread else { return vec![] };
|
||||
let Some(last) = self.anchors.last() else { return vec![] };
|
||||
@@ -51,18 +65,27 @@ impl Tool for PenTool {
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let Some((sr, sp)) = cx.layout.spread_at(ev.pos) else { return vec![] };
|
||||
let Some((sr, sp)) = cx.layout.spread_at(ev.pos) else {
|
||||
self.guides.clear();
|
||||
return vec![];
|
||||
};
|
||||
self.hover = ev.pos;
|
||||
// The pen continues on the spread where the path started.
|
||||
let sp = match self.spread {
|
||||
Some(s) if s != sr => cx.layout.to_spread(s, ev.pos),
|
||||
_ => sp,
|
||||
};
|
||||
let snap_spread = self.spread.unwrap_or(sr);
|
||||
// Pick up the item id created by path.create (the newest selected path).
|
||||
if self.path.is_none() && self.anchors.len() >= 2 {
|
||||
self.path = cx.selection.items.last().map(|i| i.0);
|
||||
}
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
let p = self.snap_click(cx, snap_spread, sp, ev.mods.shift);
|
||||
self.hover = cx.layout.to_canvas(snap_spread, p);
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
// Close on the first anchor.
|
||||
if self.anchors.len() >= 2
|
||||
@@ -76,12 +99,7 @@ impl Tool for PenTool {
|
||||
self.finish();
|
||||
return out;
|
||||
}
|
||||
let mut p = sp;
|
||||
if ev.mods.shift
|
||||
&& let Some(last) = self.anchors.last()
|
||||
{
|
||||
p = last.0 + designcraft_geom::constrain_angle(p - last.0, 45.0);
|
||||
}
|
||||
let p = self.snap_click(cx, snap_spread, sp, ev.mods.shift);
|
||||
if self.anchors.is_empty() {
|
||||
self.spread = Some(sr);
|
||||
}
|
||||
@@ -120,9 +138,12 @@ impl Tool for PenTool {
|
||||
}
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let Some(sr) = self.spread else { return vec![] };
|
||||
let xf = cx.layout.xf(sr);
|
||||
let mut out = Vec::new();
|
||||
let Some(sr) = self.spread else {
|
||||
out.extend(self.guides.iter().cloned());
|
||||
return out;
|
||||
};
|
||||
let xf = cx.layout.xf(sr);
|
||||
if let Some(last) = self.anchors.last()
|
||||
&& !self.dragging
|
||||
{
|
||||
@@ -139,6 +160,8 @@ impl Tool for PenTool {
|
||||
out.push(Overlay::Line { a: xf * a.1, b: xf * a.2, color: [79, 153, 255], dashed: false });
|
||||
let _ = BezPath::new();
|
||||
}
|
||||
// Guides are already canvas. Do not run them through the spread transform again.
|
||||
out.extend(self.guides.iter().cloned());
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
+91
-11
@@ -5,7 +5,7 @@
|
||||
//! empty-canvas drags make a marquee, double-clicking a text frame switches to the Type tool.
|
||||
|
||||
use designcraft_doc::{ItemId, SpreadRef};
|
||||
use designcraft_geom::{Point, Rect, Vec2};
|
||||
use designcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{Action, Cursor, Gesture, Mods, Overlay, PointerEvent, PointerKind, SnapRequest, Tool, ToolContext, ToolKey, rect_json, spread_json};
|
||||
@@ -13,12 +13,42 @@ use crate::{Action, Cursor, Gesture, Mods, Overlay, PointerEvent, PointerKind, S
|
||||
#[derive(Clone, Debug)]
|
||||
enum Drag {
|
||||
None,
|
||||
Pending { start: Point, hit: bool },
|
||||
Move { start: Point, origin_spread: SpreadRef, bounds0: Option<Rect>, exclude: Vec<ItemId> },
|
||||
Resize { handle: usize, start: Point, from: Rect, spread: SpreadRef },
|
||||
Rotate { center: Point, start_angle: f64, start_rotation: Option<f64> },
|
||||
Marquee { start: Point, cur: Point },
|
||||
Anchor { id: u64, si: usize, ai: usize, handle: Option<&'static str>, start: Point },
|
||||
Pending {
|
||||
start: Point,
|
||||
hit: bool,
|
||||
},
|
||||
Move {
|
||||
start: Point,
|
||||
origin_spread: SpreadRef,
|
||||
bounds0: Option<Rect>,
|
||||
exclude: Vec<ItemId>,
|
||||
},
|
||||
Resize {
|
||||
handle: usize,
|
||||
start: Point,
|
||||
from: Rect,
|
||||
spread: SpreadRef,
|
||||
},
|
||||
Rotate {
|
||||
center: Point,
|
||||
start_angle: f64,
|
||||
start_rotation: Option<f64>,
|
||||
},
|
||||
Marquee {
|
||||
start: Point,
|
||||
cur: Point,
|
||||
},
|
||||
Anchor {
|
||||
id: u64,
|
||||
si: usize,
|
||||
ai: usize,
|
||||
handle: Option<&'static str>,
|
||||
start: Point,
|
||||
/// Spread and the anchor's spread position at pointer down.
|
||||
/// None when that point could not be read. The live document during a drag is the
|
||||
/// previous preview, so the snap must not read the anchor from it again.
|
||||
at: Option<(SpreadRef, Point)>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Anchor or handle of a selected item under `p` (canvas): (item, subpath, anchor, handle).
|
||||
@@ -49,6 +79,20 @@ pub fn anchor_at_in(cx: &ToolContext, ids: &[designcraft_doc::ItemId], p: Point)
|
||||
None
|
||||
}
|
||||
|
||||
/// Spread holding the anchor, and the anchor in that spread's coordinates.
|
||||
fn anchor_spread_point(cx: &ToolContext, id: u64, si: usize, ai: usize) -> Option<(SpreadRef, Point)> {
|
||||
let item_id = ItemId(id);
|
||||
let loc = cx.doc.find(item_id)?;
|
||||
let it = cx.doc.item(item_id)?;
|
||||
let local = it.path.subpaths.get(si)?.anchors.get(ai)?.p;
|
||||
Some((loc.spread, (cx.doc.parent_xf(&loc) * it.xf) * local))
|
||||
}
|
||||
|
||||
/// Spread-space correction as a canvas delta. Translation cancels, leaving the linear part of `xf`.
|
||||
fn spread_delta_to_canvas(xf: Affine, d: Vec2) -> Vec2 {
|
||||
(xf * Point::new(d.x, d.y)) - (xf * Point::ORIGIN)
|
||||
}
|
||||
|
||||
pub struct SelectionTool {
|
||||
direct: bool,
|
||||
drag: Drag,
|
||||
@@ -337,7 +381,8 @@ impl Tool for SelectionTool {
|
||||
if self.direct
|
||||
&& let Some((id, si, ai, handle)) = anchor_at(cx, p)
|
||||
{
|
||||
self.drag = Drag::Anchor { id, si, ai, handle, start: p };
|
||||
let at = anchor_spread_point(cx, id, si, ai);
|
||||
self.drag = Drag::Anchor { id, si, ai, handle, start: p, at };
|
||||
return vec![Action::Begin(if handle.is_some() { "Move Direction Handle".into() } else { "Move Anchor".into() })];
|
||||
}
|
||||
if !self.direct
|
||||
@@ -565,9 +610,44 @@ impl Tool for SelectionTool {
|
||||
// Screen y points down: a positive screen angle is clockwise.
|
||||
vec![Action::Preview("transform.rotate".into(), json!({"angle": -a}))]
|
||||
}
|
||||
Drag::Anchor { id, si, ai, handle, start } => {
|
||||
Drag::Anchor { id, si, ai, handle, start, at } => {
|
||||
let d = p - start;
|
||||
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": d.x, "dy": d.y});
|
||||
let mut dx = d.x;
|
||||
let mut dy = d.y;
|
||||
self.guides.clear();
|
||||
// Direction handles do not snap. An anchor excludes the path it is on.
|
||||
if handle.is_none()
|
||||
&& let Some((spread, origin)) = at
|
||||
{
|
||||
let proposed = origin + cx.layout.delta_to_spread(spread, d);
|
||||
let exclude = [ItemId(id)];
|
||||
let hit = crate::snap::snap(
|
||||
cx,
|
||||
SnapRequest {
|
||||
spread,
|
||||
gesture: Gesture::Point,
|
||||
rect: Rect::from_points(proposed, proposed),
|
||||
x_edges: [true, true, true],
|
||||
y_edges: [true, true, true],
|
||||
exclude: &exclude,
|
||||
copying: false,
|
||||
lengths: [None, None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: proposed,
|
||||
},
|
||||
);
|
||||
// dx and dy stay in canvas space. Snap.delta is spread space.
|
||||
let corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta);
|
||||
if corr.x.is_finite() {
|
||||
dx += corr.x;
|
||||
}
|
||||
if corr.y.is_finite() {
|
||||
dy += corr.y;
|
||||
}
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": dx, "dy": dy});
|
||||
if let Some(h) = handle {
|
||||
params["handle"] = json!(h);
|
||||
}
|
||||
@@ -640,7 +720,7 @@ impl Tool for SelectionTool {
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
match &self.drag {
|
||||
Drag::Marquee { start, cur } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))],
|
||||
Drag::Move { .. } | Drag::Resize { .. } | Drag::Rotate { .. } => self.guides.clone(),
|
||||
Drag::Move { .. } | Drag::Resize { .. } | Drag::Rotate { .. } | Drag::Anchor { .. } => self.guides.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use designcraft_compose::Cache;
|
||||
use designcraft_doc::build::NewDocument;
|
||||
use designcraft_doc::{Document, Item, ItemId, ParaFormat, Selection, Shape, SpreadRef};
|
||||
use designcraft_doc::{Document, Guide, Item, ItemId, Orientation, ParaFormat, Selection, Shape, SpreadRef};
|
||||
use designcraft_geom::shapes;
|
||||
use designcraft_geom::{Point, Rect, Unit};
|
||||
|
||||
@@ -521,3 +523,32 @@ fn add_rotated(doc: &mut Document, rect: Rect, degrees: f64) -> ItemId {
|
||||
doc.insert_item(SpreadRef::Doc(0), item, None).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_click_snaps_to_a_guide() {
|
||||
let mut d = Document::new(&NewDocument::default());
|
||||
Arc::make_mut(&mut d.spreads[0]).pages[0].guides.push(Guide {
|
||||
orientation: Orientation::Vertical,
|
||||
position: 100.0,
|
||||
spread: true,
|
||||
locked: false,
|
||||
layer: None,
|
||||
liquid: false,
|
||||
});
|
||||
let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, false));
|
||||
let mut cx = ctx(&d, &s, &c, &l);
|
||||
cx.snap = SnapView::FACTORY;
|
||||
let mut t = create("pen");
|
||||
let click = |t: &mut Box<dyn Tool>, cx: &ToolContext, x: f64, y: f64| {
|
||||
t.pointer(cx, &PointerEvent::new(PointerKind::Down, x, y));
|
||||
t.pointer(cx, &PointerEvent::new(PointerKind::Up, x, y))
|
||||
};
|
||||
assert!(click(&mut t, &cx, 102.0, 80.0).is_empty());
|
||||
let up = click(&mut t, &cx, 180.0, 80.0);
|
||||
let Action::Exec(cmd, p) = &up[0] else {
|
||||
panic!("expected path.create, got {up:?}");
|
||||
};
|
||||
assert_eq!(cmd, "path.create");
|
||||
let x = p["anchors"][0]["p"][0].as_f64().unwrap();
|
||||
assert!((x - 100.0).abs() < 1e-6, "anchor x {x}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user