mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
* Tools: snap Gap tool drags to active snapping settings (#2637) * Tools: reject unreachable Gap tool snap targets beyond resize clamp * Tools: preserve raw Gap tool delta when no reachable snap matches
This commit is contained in:
1 parent
cf4efa1833
commit
dbdc5b697a
5 files changed
+253
-41
No files matched your search
@@ -28,6 +28,22 @@ pub struct ItemLoc {
|
||||
pub path: ItemPath,
|
||||
}
|
||||
|
||||
/// A gap between adjacent items (or between an item and the page edge) on a spread.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Gap {
|
||||
/// True for a vertical gap (between left and right neighbours, moving along X);
|
||||
/// false for a horizontal gap (between top and bottom neighbours, moving along Y).
|
||||
pub vertical: bool,
|
||||
/// Leading edge of the gap along its movement axis.
|
||||
pub a: f64,
|
||||
/// Trailing edge of the gap along its movement axis.
|
||||
pub z: f64,
|
||||
/// Items ending at `a` that resize when the gap moves.
|
||||
pub before: Vec<ItemId>,
|
||||
/// Items starting at `z` that resize when the gap moves.
|
||||
pub after: Vec<ItemId>,
|
||||
}
|
||||
|
||||
impl ItemLoc {
|
||||
/// Top-level index in the spread.
|
||||
pub fn top(&self) -> usize {
|
||||
@@ -648,6 +664,38 @@ impl Document {
|
||||
.map(|it| it.id)
|
||||
}
|
||||
|
||||
/// The gap at `p` (spread space) on spread `sr`: between left and right neighbours (vertical
|
||||
/// gap moving along X), or above and below (horizontal gap moving along Y).
|
||||
pub fn find_gap(&self, sr: SpreadRef, p: Point) -> Option<Gap> {
|
||||
let sp = self.spread(sr)?;
|
||||
let page = sp.page_at_x(p.x).and_then(|i| sp.pages.get(i)).map(|pg| pg.bounds())?;
|
||||
let items: Vec<(ItemId, Rect)> = sp
|
||||
.items
|
||||
.iter()
|
||||
.filter(|it| !it.hidden && !it.locked && self.layer(it.layer).is_none_or(|l| !l.locked && l.visible))
|
||||
.map(|it| (it.id, it.bounds()))
|
||||
.collect();
|
||||
if items.iter().any(|(_, b)| b.contains(p)) {
|
||||
return None;
|
||||
}
|
||||
// Between left and right neighbours (a vertical gap), or above and below (horizontal).
|
||||
let side = |vertical: bool| {
|
||||
let (pos, lo, hi) = if vertical { (p.x, page.x0, page.x1) } else { (p.y, page.y0, page.y1) };
|
||||
let across = |b: &Rect| if vertical { b.y0 <= p.y && p.y <= b.y1 } else { b.x0 <= p.x && p.x <= b.x1 };
|
||||
let end = |b: &Rect| if vertical { b.x1 } else { b.y1 };
|
||||
let start = |b: &Rect| if vertical { b.x0 } else { b.y0 };
|
||||
let a = items.iter().filter(|(_, b)| across(b) && end(b) <= pos).map(|(_, b)| end(b)).fold(lo, f64::max);
|
||||
let z = items.iter().filter(|(_, b)| across(b) && start(b) >= pos).map(|(_, b)| start(b)).fold(hi, f64::min);
|
||||
let before: Vec<ItemId> = items.iter().filter(|(_, b)| across(b) && (end(b) - a).abs() < 0.5).map(|(i, _)| *i).collect();
|
||||
let after: Vec<ItemId> = items.iter().filter(|(_, b)| across(b) && (start(b) - z).abs() < 0.5).map(|(i, _)| *i).collect();
|
||||
(!before.is_empty() || !after.is_empty()).then_some(Gap { vertical, a, z, before, after })
|
||||
};
|
||||
match (side(true), side(false)) {
|
||||
(Some(v), Some(h)) => Some(if v.z - v.a <= h.z - h.a { v } else { h }),
|
||||
(v, h) => v.or(h),
|
||||
}
|
||||
}
|
||||
|
||||
/// The first item on a document or parent spread whose geometry isn't finite
|
||||
/// ([`Item::geometry_is_finite`]).
|
||||
pub fn non_finite_item(&self) -> Option<ItemId> {
|
||||
|
||||
@@ -52,7 +52,7 @@ pub use datamerge::{
|
||||
};
|
||||
pub use designcraft_color as color;
|
||||
pub use designcraft_geom as geom;
|
||||
pub use edit::{ItemLoc, ItemPath, SpreadRef, hit_descendant, item_hit as edit_hit};
|
||||
pub use edit::{Gap, ItemLoc, ItemPath, SpreadRef, hit_descendant, item_hit as edit_hit};
|
||||
pub use endnotes::{ENDNOTE_REF, EditorialNote, EndnoteOptions, NOTE_MARK};
|
||||
pub use file_info::FileInfo;
|
||||
pub use flex::{FlexAlign, FlexDirection, FlexJustify, FlexLayout, FlexSize};
|
||||
|
||||
@@ -3101,37 +3101,6 @@ fn apply_custom_frame_grid(
|
||||
}
|
||||
}
|
||||
|
||||
/// The gap at `p` on spread `sr`: (vertical gap?, edge a, edge b, ids ending at a, ids starting at b).
|
||||
pub(crate) fn find_gap(d: &Document, sr: designcraft_doc::SpreadRef, p: Point) -> Option<(bool, f64, f64, Vec<ItemId>, Vec<ItemId>)> {
|
||||
let sp = d.spread(sr)?;
|
||||
let page = sp.page_at_x(p.x).and_then(|i| sp.pages.get(i)).map(|pg| pg.bounds())?;
|
||||
let items: Vec<(ItemId, Rect)> = sp
|
||||
.items
|
||||
.iter()
|
||||
.filter(|it| !it.hidden && !it.locked && d.layer(it.layer).is_none_or(|l| !l.locked && l.visible))
|
||||
.map(|it| (it.id, it.bounds()))
|
||||
.collect();
|
||||
if items.iter().any(|(_, b)| b.contains(p)) {
|
||||
return None;
|
||||
}
|
||||
// Between left and right neighbours (a vertical gap), or above and below (horizontal).
|
||||
let side = |vertical: bool| {
|
||||
let (pos, lo, hi) = if vertical { (p.x, page.x0, page.x1) } else { (p.y, page.y0, page.y1) };
|
||||
let across = |b: &Rect| if vertical { b.y0 <= p.y && p.y <= b.y1 } else { b.x0 <= p.x && p.x <= b.x1 };
|
||||
let end = |b: &Rect| if vertical { b.x1 } else { b.y1 };
|
||||
let start = |b: &Rect| if vertical { b.x0 } else { b.y0 };
|
||||
let a = items.iter().filter(|(_, b)| across(b) && end(b) <= pos).map(|(_, b)| end(b)).fold(lo, f64::max);
|
||||
let z = items.iter().filter(|(_, b)| across(b) && start(b) >= pos).map(|(_, b)| start(b)).fold(hi, f64::min);
|
||||
let before: Vec<ItemId> = items.iter().filter(|(_, b)| across(b) && (end(b) - a).abs() < 0.5).map(|(i, _)| *i).collect();
|
||||
let after: Vec<ItemId> = items.iter().filter(|(_, b)| across(b) && (start(b) - z).abs() < 0.5).map(|(i, _)| *i).collect();
|
||||
(!before.is_empty() || !after.is_empty()).then_some((vertical, a, z, before, after))
|
||||
};
|
||||
match (side(true), side(false)) {
|
||||
(Some(v), Some(h)) => Some(if v.2 - v.1 <= h.2 - h.1 { v } else { h }),
|
||||
(v, h) => v.or(h),
|
||||
}
|
||||
}
|
||||
|
||||
fn gap_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const ID: &str = "gap.move";
|
||||
let at = p
|
||||
@@ -3140,19 +3109,20 @@ fn gap_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.and_then(|a| Some(Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)))
|
||||
.ok_or_else(|| bad(ID, "missing at"))?;
|
||||
let sr = spread_param(p, ID)?;
|
||||
let (vertical, a, z, before, after) = find_gap(&s.doc()?.doc, sr, at).ok_or_else(|| bad(ID, "no gap there"))?;
|
||||
let gap = s.doc()?.doc.find_gap(sr, at).ok_or_else(|| bad(ID, "no gap there"))?;
|
||||
// `delta` along the gap's axis, or a drag (dx, dy) of which the axis part counts.
|
||||
let delta =
|
||||
p.get("delta").and_then(Value::as_f64).unwrap_or_else(|| p.get(if vertical { "dx" } else { "dy" }).and_then(Value::as_f64).unwrap_or(0.0));
|
||||
let _ = (a, z);
|
||||
let delta = p
|
||||
.get("delta")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or_else(|| p.get(if gap.vertical { "dx" } else { "dy" }).and_then(Value::as_f64).unwrap_or(0.0));
|
||||
s.edit(|d, _| {
|
||||
let mut n = 0;
|
||||
for (ids, is_before) in [(&before, true), (&after, false)] {
|
||||
for (ids, is_before) in [(&gap.before, true), (&gap.after, false)] {
|
||||
for id in ids {
|
||||
let Some(it) = d.item_mut(*id) else { continue };
|
||||
let b = it.bounds();
|
||||
let mut to = b;
|
||||
match (vertical, is_before) {
|
||||
match (gap.vertical, is_before) {
|
||||
(true, true) => to.x1 = (b.x1 + delta).max(b.x0 + 1.0),
|
||||
(true, false) => to.x0 = (b.x0 + delta).min(b.x1 - 1.0),
|
||||
(false, true) => to.y1 = (b.y1 + delta).max(b.y0 + 1.0),
|
||||
@@ -3170,7 +3140,7 @@ fn gap_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(json!({"axis": if vertical { "x" } else { "y" }, "changed": n}))
|
||||
Ok(json!({"axis": if gap.vertical { "x" } else { "y" }, "changed": n}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+107
-2
@@ -1,6 +1,6 @@
|
||||
//! Hand (H) and Zoom (Z) tools.
|
||||
|
||||
use designcraft_geom::Point;
|
||||
use designcraft_geom::{Point, Rect};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
@@ -573,13 +573,70 @@ impl Tool for PathTypeTool {
|
||||
}
|
||||
}
|
||||
|
||||
struct GapDrag {
|
||||
vertical: bool,
|
||||
a: f64,
|
||||
z: f64,
|
||||
cross_lo: f64,
|
||||
cross_hi: f64,
|
||||
min_delta: f64,
|
||||
max_delta: f64,
|
||||
x_edges: [bool; 3],
|
||||
y_edges: [bool; 3],
|
||||
exclude: Vec<designcraft_doc::ItemId>,
|
||||
}
|
||||
|
||||
impl GapDrag {
|
||||
fn from_doc(doc: &designcraft_doc::Document, sr: designcraft_doc::SpreadRef, p: Point) -> Option<Self> {
|
||||
let g = doc.find_gap(sr, p)?;
|
||||
let mut exclude = Vec::with_capacity(g.before.len() + g.after.len());
|
||||
exclude.extend_from_slice(&g.before);
|
||||
exclude.extend_from_slice(&g.after);
|
||||
let (mut cross_lo, mut cross_hi) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for id in &exclude {
|
||||
if let Some(it) = doc.item(*id) {
|
||||
let b = it.bounds();
|
||||
let (lo, hi) = if g.vertical { (b.y0, b.y1) } else { (b.x0, b.x1) };
|
||||
cross_lo = cross_lo.min(lo);
|
||||
cross_hi = cross_hi.max(hi);
|
||||
}
|
||||
}
|
||||
if !cross_lo.is_finite() || !cross_hi.is_finite() {
|
||||
let c = if g.vertical { p.y } else { p.x };
|
||||
cross_lo = c;
|
||||
cross_hi = c;
|
||||
}
|
||||
let mut min_delta = f64::NEG_INFINITY;
|
||||
for id in &g.before {
|
||||
if let Some(it) = doc.item(*id) {
|
||||
let b = it.bounds();
|
||||
let limit = if g.vertical { b.x0 + 1.0 - b.x1 } else { b.y0 + 1.0 - b.y1 };
|
||||
min_delta = min_delta.max(limit);
|
||||
}
|
||||
}
|
||||
let mut max_delta = f64::INFINITY;
|
||||
for id in &g.after {
|
||||
if let Some(it) = doc.item(*id) {
|
||||
let b = it.bounds();
|
||||
let limit = if g.vertical { b.x1 - 1.0 - b.x0 } else { b.y1 - 1.0 - b.y0 };
|
||||
max_delta = max_delta.min(limit);
|
||||
}
|
||||
}
|
||||
let flags = [!g.before.is_empty(), !g.before.is_empty() && !g.after.is_empty(), !g.after.is_empty()];
|
||||
let (x_edges, y_edges) = if g.vertical { (flags, [false; 3]) } else { ([false; 3], flags) };
|
||||
Some(Self { vertical: g.vertical, a: g.a, z: g.z, cross_lo, cross_hi, min_delta, max_delta, x_edges, y_edges, exclude })
|
||||
}
|
||||
}
|
||||
|
||||
/// Gap tool (U): drag the space between objects (or between an object and the page edge); the
|
||||
/// objects on both sides resize so the gap moves.
|
||||
#[derive(Default)]
|
||||
pub struct GapTool {
|
||||
/// Spread, its spread → canvas transform and the press point in spread coordinates.
|
||||
start: Option<(designcraft_doc::SpreadRef, designcraft_geom::Affine, Point)>,
|
||||
gap: Option<GapDrag>,
|
||||
active: bool,
|
||||
guides: Vec<Overlay>,
|
||||
}
|
||||
|
||||
impl Tool for GapTool {
|
||||
@@ -590,15 +647,18 @@ impl Tool for GapTool {
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
self.guides.clear();
|
||||
self.gap = None;
|
||||
if let Some((sr, p)) = cx.layout.spread_at(ev.pos) {
|
||||
self.start = Some((sr, cx.layout.xf(sr), p));
|
||||
self.gap = GapDrag::from_doc(cx.doc, sr, p);
|
||||
}
|
||||
self.active = false;
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some((sr, xf, p)) = self.start else { return vec![] };
|
||||
let d = (xf.inverse() * ev.pos) - p;
|
||||
let mut d = (xf.inverse() * ev.pos) - p;
|
||||
let mut out = vec![];
|
||||
if !self.active {
|
||||
if d.hypot() < cx.tol(2.0) {
|
||||
@@ -607,6 +667,45 @@ impl Tool for GapTool {
|
||||
self.active = true;
|
||||
out.push(Action::Begin("Move Gap".into()));
|
||||
}
|
||||
self.guides.clear();
|
||||
if !ev.mods.cmd
|
||||
&& cx.snap.any()
|
||||
&& let Some(gap) = &self.gap
|
||||
{
|
||||
let raw = if gap.vertical { d.x } else { d.y };
|
||||
let proposed = if gap.vertical {
|
||||
Rect::new(gap.a + raw, gap.cross_lo, gap.z + raw, gap.cross_hi)
|
||||
} else {
|
||||
Rect::new(gap.cross_lo, gap.a + raw, gap.cross_hi, gap.z + raw)
|
||||
};
|
||||
let hit = crate::snap::snap(
|
||||
cx,
|
||||
crate::SnapRequest {
|
||||
spread: sr,
|
||||
gesture: crate::Gesture::Resize,
|
||||
rect: proposed,
|
||||
x_edges: gap.x_edges,
|
||||
y_edges: gap.y_edges,
|
||||
exclude: &gap.exclude,
|
||||
exclude_guides: &[],
|
||||
copying: false,
|
||||
lengths: [None, None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: p + d,
|
||||
},
|
||||
);
|
||||
let snap_delta = if gap.vertical { hit.delta.x } else { hit.delta.y };
|
||||
let snapped = raw + snap_delta;
|
||||
if snap_delta.is_finite() && snapped >= gap.min_delta - 1e-6 && snapped <= gap.max_delta + 1e-6 {
|
||||
if gap.vertical {
|
||||
d.x = snapped;
|
||||
} else {
|
||||
d.y = snapped;
|
||||
}
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
}
|
||||
out.push(Action::Preview(
|
||||
"gap.move".into(),
|
||||
serde_json::json!({"spread": crate::spread_json(sr), "at": [p.x, p.y], "dx": d.x, "dy": d.y}),
|
||||
@@ -615,12 +714,18 @@ impl Tool for GapTool {
|
||||
}
|
||||
PointerKind::Up => {
|
||||
self.start = None;
|
||||
self.gap = None;
|
||||
self.guides.clear();
|
||||
if std::mem::take(&mut self.active) { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
self.guides.clone()
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
}
|
||||
|
||||
@@ -2416,3 +2416,92 @@ fn direct_selection_marquee_selects_points_of_several_frames_and_drags_them_toge
|
||||
assert!(want.x.abs() < 1e-9 && (want.y.abs() - inc).abs() < 1e-9, "a quarter turn: {want:?}");
|
||||
assert!(moves(p).iter().all(|&(x, y)| (x - want.x).abs() < 1e-9 && (y - want.y).abs() < 1e-9), "{p}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_tool_follows_snapping_settings() {
|
||||
let mut d = Document::new(&NewDocument::default());
|
||||
add_rect(&mut d, Rect::new(100.0, 100.0, 180.0, 200.0));
|
||||
add_rect(&mut d, Rect::new(200.0, 100.0, 300.0, 200.0));
|
||||
add_rect(&mut d, Rect::new(250.0, 250.0, 320.0, 350.0));
|
||||
Arc::make_mut(&mut d.spreads[0]).pages[0].guides.push(Guide {
|
||||
orientation: Orientation::Vertical,
|
||||
position: 225.0,
|
||||
spread: false,
|
||||
locked: false,
|
||||
layer: None,
|
||||
liquid: false,
|
||||
});
|
||||
Arc::make_mut(&mut d.spreads[0]).pages[0].guides.push(Guide {
|
||||
orientation: Orientation::Vertical,
|
||||
position: 310.0,
|
||||
spread: false,
|
||||
locked: false,
|
||||
layer: None,
|
||||
liquid: false,
|
||||
});
|
||||
let sel = Selection::default();
|
||||
let c = Cache::new();
|
||||
let l = CanvasLayout::new(&d, false);
|
||||
let p0 = spread_pt(&l, 190.0, 150.0);
|
||||
|
||||
let drag_dx = |snap: SnapView, mods: Mods, raw_dx: f64| -> (f64, Vec<Overlay>) {
|
||||
let mut t = create("gap");
|
||||
let cx = ToolContext { snap, ..ctx(&d, &sel, &c, &l) };
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, p0.x, p0.y));
|
||||
let drag = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, p0.x + raw_dx, p0.y).with_mods(mods));
|
||||
let Some(Action::Preview(cmd, p)) = drag.last() else { panic!("{drag:?}") };
|
||||
assert_eq!(cmd, "gap.move");
|
||||
let overlays = t.overlays(&cx);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, p0.x + raw_dx, p0.y).with_mods(mods));
|
||||
(p["dx"].as_f64().unwrap(), overlays)
|
||||
};
|
||||
|
||||
// Ruler guide at x = 225: gap right edge is 200 + 23 = 223, snaps +2 pt to 225.
|
||||
let (dx, _) = drag_dx(SnapView::FACTORY, Mods::default(), 23.0);
|
||||
assert!((dx - 25.0).abs() < 1e-6, "expected snap to guide (dx=25), got {dx}");
|
||||
|
||||
// Snapping off: stays at raw 23.0.
|
||||
let (dx, overlays) = drag_dx(SnapView::OFF, Mods::default(), 23.0);
|
||||
assert!((dx - 23.0).abs() < 1e-6, "expected unsnapped dx=23, got {dx}");
|
||||
assert!(overlays.is_empty(), "expected no overlays when snap is off: {overlays:?}");
|
||||
|
||||
// Cmd suspends snapping: stays at raw 23.0.
|
||||
let (dx, overlays) = drag_dx(SnapView::FACTORY, Mods { cmd: true, ..Default::default() }, 23.0);
|
||||
assert!((dx - 23.0).abs() < 1e-6, "expected unsnapped dx=23 with Cmd, got {dx}");
|
||||
assert!(overlays.is_empty(), "expected no overlays with Cmd: {overlays:?}");
|
||||
|
||||
// Smart guide alignment to third item's left edge (x = 250): gap left edge 180 + 68 = 248, snaps to 250 (dx = 70).
|
||||
let (dx, overlays) = drag_dx(SnapView::FACTORY, Mods::default(), 68.0);
|
||||
assert!((dx - 70.0).abs() < 1e-6, "expected smart-guide snap (dx=70), got {dx}");
|
||||
assert!(overlays.iter().any(|o| matches!(o, Overlay::Guide { .. })), "expected alignment guide overlay: {overlays:?}");
|
||||
|
||||
// Smart guides disabled: no snap to third item.
|
||||
let no_smart = SnapView { smart_guides: false, ..SnapView::FACTORY };
|
||||
let (dx, overlays) = drag_dx(no_smart, Mods::default(), 68.0);
|
||||
assert!((dx - 68.0).abs() < 1e-6, "expected no smart-guide snap when disabled, got {dx}");
|
||||
assert!(overlays.is_empty(), "expected no guide overlay when smart_guides=false: {overlays:?}");
|
||||
|
||||
// Ruler guide beyond gap_move's minimum-width clamp (right item [200..300] clamps at x=299, max_delta=99):
|
||||
// dragging 108 pt reaches x=308 near guide x=310 (dx=110), which must be rejected as unreachable.
|
||||
let (dx, overlays) = drag_dx(no_smart, Mods::default(), 108.0);
|
||||
assert!((dx - 108.0).abs() < 1e-6, "expected unreachable snap target to be rejected, got {dx}");
|
||||
assert!(overlays.is_empty(), "expected no guide overlay for unreachable snap target: {overlays:?}");
|
||||
|
||||
// With two left neighbours of different widths ([100..180] and [140..180]), dragging left by 60 pt
|
||||
// without a snap match preserves the raw delta (-60) instead of clamping it to the narrower item (-39).
|
||||
let mut d2 = Document::new(&NewDocument::default());
|
||||
add_rect(&mut d2, Rect::new(100.0, 100.0, 180.0, 150.0));
|
||||
add_rect(&mut d2, Rect::new(140.0, 150.0, 180.0, 200.0));
|
||||
add_rect(&mut d2, Rect::new(200.0, 100.0, 300.0, 200.0));
|
||||
let l2 = CanvasLayout::new(&d2, false);
|
||||
let q0 = spread_pt(&l2, 190.0, 150.0);
|
||||
let mut t2 = create("gap");
|
||||
let cx2 = ToolContext { snap: SnapView::FACTORY, ..ctx(&d2, &sel, &c, &l2) };
|
||||
t2.pointer(&cx2, &PointerEvent::new(PointerKind::Down, q0.x, q0.y));
|
||||
let drag2 = t2.pointer(&cx2, &PointerEvent::new(PointerKind::Drag, q0.x - 60.0, q0.y));
|
||||
let Some(Action::Preview(cmd2, p2)) = drag2.last() else { panic!("{drag2:?}") };
|
||||
assert_eq!(cmd2, "gap.move");
|
||||
assert!((p2["dx"].as_f64().unwrap() - (-60.0)).abs() < 1e-6, "expected raw dx=-60 preserved, got {p2}");
|
||||
assert!(t2.overlays(&cx2).is_empty(), "expected no guide overlay without snap match");
|
||||
t2.pointer(&cx2, &PointerEvent::new(PointerKind::Up, q0.x - 60.0, q0.y));
|
||||
}
|
||||
Reference in new issue
Block a user