mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
co-authored by
Claude Opus 5.5
parent
5a4012bab8
commit
4a71ee5d83
+70
-70
@@ -782,51 +782,51 @@ impl SelectionTool {
|
||||
&& let Some((id, si, ai, handle)) = anchor_at(cx, p)
|
||||
{
|
||||
let at = anchor_spread_point(cx, id, si, ai);
|
||||
let mut out = vec![];
|
||||
let mut others = vec![];
|
||||
if handle.is_none() {
|
||||
// InDesign's anchor point selection: a click selects the point, Shift+click
|
||||
// adds it or takes it away, and dragging a selected point moves all of them.
|
||||
let me = (ItemId(id), si, ai);
|
||||
let mut points = cx.selection.anchors.clone();
|
||||
let was = points.contains(&me);
|
||||
if ev.mods.shift && was {
|
||||
// Kept selected so a Shift-drag can constrain it; a click still takes it
|
||||
// away, on release, as for objects.
|
||||
self.anchor_release = Some(me);
|
||||
} else if ev.mods.shift {
|
||||
points.push(me);
|
||||
} else if !was {
|
||||
points = vec![me];
|
||||
}
|
||||
if points != cx.selection.anchors {
|
||||
out.push(Action::Exec("selection.set".into(), anchor_selection(cx, &points, ev.mods.shift)));
|
||||
}
|
||||
others = points.iter().filter(|a| **a != me).map(|(i, s, a)| (i.0, *s, *a)).collect();
|
||||
}
|
||||
let label = if handle.is_some() {
|
||||
"Move Direction Handle"
|
||||
} else if others.is_empty() {
|
||||
"Move Anchor"
|
||||
} else {
|
||||
"Move Anchors"
|
||||
};
|
||||
self.drag = Drag::Anchor { id, si, ai, handle, start: p, at, others };
|
||||
out.push(Action::Begin(label.into()));
|
||||
return out;
|
||||
let mut out = vec![];
|
||||
let mut others = vec![];
|
||||
if handle.is_none() {
|
||||
// InDesign's anchor point selection: a click selects the point, Shift+click
|
||||
// adds it or takes it away, and dragging a selected point moves all of them.
|
||||
let me = (ItemId(id), si, ai);
|
||||
let mut points = cx.selection.anchors.clone();
|
||||
let was = points.contains(&me);
|
||||
if ev.mods.shift && was {
|
||||
// Kept selected so a Shift-drag can constrain it; a click still takes it
|
||||
// away, on release, as for objects.
|
||||
self.anchor_release = Some(me);
|
||||
} else if ev.mods.shift {
|
||||
points.push(me);
|
||||
} else if !was {
|
||||
points = vec![me];
|
||||
}
|
||||
// As in InDesign, the Direct Selection tool pressed on a corner or anchor of an object
|
||||
// that isn't selected selects it and moves just that point (Chris Nielsen's "The
|
||||
// InDesign Toolbox", 10:10: click outside, then pull a frame's corner).
|
||||
if self.direct
|
||||
&& !ev.mods.shift
|
||||
&& let Some((_, hit)) = cx.hit(p)
|
||||
&& !cx.selection.contains(hit)
|
||||
&& let Some((id, si, ai, None)) = anchor_at_in(cx, &[hit], p)
|
||||
{
|
||||
let at = anchor_spread_point(cx, id, si, ai);
|
||||
self.drag = Drag::Anchor { id, si, ai, handle: None, start: p, at, others: vec![] };
|
||||
return vec![Action::Exec("selection.set".into(), json!({"ids": [id], "content": true})), Action::Begin("Move Anchor".into())];
|
||||
if points != cx.selection.anchors {
|
||||
out.push(Action::Exec("selection.set".into(), anchor_selection(cx, &points, ev.mods.shift)));
|
||||
}
|
||||
others = points.iter().filter(|a| **a != me).map(|(i, s, a)| (i.0, *s, *a)).collect();
|
||||
}
|
||||
let label = if handle.is_some() {
|
||||
"Move Direction Handle"
|
||||
} else if others.is_empty() {
|
||||
"Move Anchor"
|
||||
} else {
|
||||
"Move Anchors"
|
||||
};
|
||||
self.drag = Drag::Anchor { id, si, ai, handle, start: p, at, others };
|
||||
out.push(Action::Begin(label.into()));
|
||||
return out;
|
||||
}
|
||||
// As in InDesign, the Direct Selection tool pressed on a corner or anchor of an object
|
||||
// that isn't selected selects it and moves just that point (Chris Nielsen's "The
|
||||
// InDesign Toolbox", 10:10: click outside, then pull a frame's corner).
|
||||
if self.direct
|
||||
&& !ev.mods.shift
|
||||
&& let Some((_, hit)) = cx.hit(p)
|
||||
&& !cx.selection.contains(hit)
|
||||
&& let Some((id, si, ai, None)) = anchor_at_in(cx, &[hit], p)
|
||||
{
|
||||
let at = anchor_spread_point(cx, id, si, ai);
|
||||
self.drag = Drag::Anchor { id, si, ai, handle: None, start: p, at, others: vec![] };
|
||||
return vec![Action::Exec("selection.set".into(), json!({"ids": [id], "content": true})), Action::Begin("Move Anchor".into())];
|
||||
}
|
||||
// A selected placed graphic's own handles resize it inside its frame (the
|
||||
// frame's anchors, under Direct Selection, come first).
|
||||
@@ -1165,18 +1165,18 @@ impl 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, at, others } => {
|
||||
let mut d = p - start;
|
||||
if d.hypot() >= cx.tol(3.0) {
|
||||
self.anchor_release = None;
|
||||
} else if self.anchor_release.is_some() {
|
||||
// A Shift-press on a selected point is still a click: nothing moves yet.
|
||||
return vec![];
|
||||
}
|
||||
// Shift: horizontal, vertical or at 45°.
|
||||
if ev.mods.shift {
|
||||
d = constrain_45(d);
|
||||
}
|
||||
Drag::Anchor { id, si, ai, handle, start, at, others } => {
|
||||
let mut d = p - start;
|
||||
if d.hypot() >= cx.tol(3.0) {
|
||||
self.anchor_release = None;
|
||||
} else if self.anchor_release.is_some() {
|
||||
// A Shift-press on a selected point is still a click: nothing moves yet.
|
||||
return vec![];
|
||||
}
|
||||
// Shift: horizontal, vertical or at 45°.
|
||||
if ev.mods.shift {
|
||||
d = constrain_45(d);
|
||||
}
|
||||
let mut dx = d.x;
|
||||
let mut dy = d.y;
|
||||
self.guides.clear();
|
||||
@@ -1204,25 +1204,25 @@ impl SelectionTool {
|
||||
},
|
||||
);
|
||||
// dx and dy stay in canvas space. Snap.delta is spread space.
|
||||
let mut corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta);
|
||||
corr = Vec2::new(if corr.x.is_finite() { corr.x } else { 0.0 }, if corr.y.is_finite() { corr.y } else { 0.0 });
|
||||
// Shift: the snap moves the points along the constrained direction only.
|
||||
if ev.mods.shift && d.hypot() > 1e-9 {
|
||||
let dir = d / d.hypot();
|
||||
corr = dir * corr.dot(dir);
|
||||
let mut corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta);
|
||||
corr = Vec2::new(if corr.x.is_finite() { corr.x } else { 0.0 }, if corr.y.is_finite() { corr.y } else { 0.0 });
|
||||
// Shift: the snap moves the points along the constrained direction only.
|
||||
if ev.mods.shift && d.hypot() > 1e-9 {
|
||||
let dir = d / d.hypot();
|
||||
corr = dir * corr.dot(dir);
|
||||
}
|
||||
dx += corr.x;
|
||||
dy += corr.y;
|
||||
dx += corr.x;
|
||||
dy += corr.y;
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
// The command moves in spread space: each point by the drag in its own spread.
|
||||
let own = at.map_or(Vec2::new(dx, dy), |(spread, _)| cx.layout.delta_to_spread(spread, Vec2::new(dx, dy)));
|
||||
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": own.x, "dy": own.y});
|
||||
// The command moves in spread space: each point by the drag in its own spread.
|
||||
let own = at.map_or(Vec2::new(dx, dy), |(spread, _)| cx.layout.delta_to_spread(spread, Vec2::new(dx, dy)));
|
||||
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": own.x, "dy": own.y});
|
||||
if let Some(h) = handle {
|
||||
params["handle"] = json!(h);
|
||||
} else if !others.is_empty() {
|
||||
let points = std::iter::once((id, si, ai)).chain(others).map(|(i, s, a)| (ItemId(i), s, a));
|
||||
params = json!({"points": moved_points(cx, points, Vec2::new(dx, dy))});
|
||||
} else if !others.is_empty() {
|
||||
let points = std::iter::once((id, si, ai)).chain(others).map(|(i, s, a)| (ItemId(i), s, a));
|
||||
params = json!({"points": moved_points(cx, points, Vec2::new(dx, dy))});
|
||||
}
|
||||
vec![Action::Preview("path.moveAnchors".into(), params)]
|
||||
}
|
||||
|
||||
@@ -786,82 +786,82 @@ fn apply_to_text_colours_a_text_frames_text_not_the_frame() {
|
||||
assert_eq!(doc.item(designcraft_doc::ids::ItemId(id)).unwrap().fill.swatch, "[None]", "the frame isn't");
|
||||
}
|
||||
|
||||
/// Points selected with Direct Selection: InDesign's "Anchor Point(s)" panel, with Convert
|
||||
/// Points and the Delete Point(s) quick action, as in the Zero2Graphic tutorial (1:25).
|
||||
#[test]
|
||||
fn selected_points_show_convert_points_and_delete_points() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("frame.create", json!({"rect": [200, 72, 300, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0], [b, 0, 0]]})).unwrap();
|
||||
assert_eq!(selection_title(&app, &super::super::sel_info(&app)), "Anchor Point(s)");
|
||||
let mut h = panel(app);
|
||||
// The four point types (Appearance has a Corner row too).
|
||||
for tip in ["Plain", "Corner", "Smooth", "Symmetrical"] {
|
||||
assert!(h.get_all_by_label(tip).next().is_some(), "{tip}");
|
||||
}
|
||||
assert!(h.query_by_label("Align").is_none(), "only the sections InDesign shows for points");
|
||||
h.get_by_label("Delete Point(s)").click();
|
||||
h.run_steps(3);
|
||||
let d = &h.state().session.active().unwrap().doc;
|
||||
for id in [a, b] {
|
||||
assert_eq!(d.item(designcraft_doc::ids::ItemId(id)).unwrap().path.subpaths[0].anchors.len(), 3, "one point gone from each");
|
||||
}
|
||||
/// Points selected with Direct Selection: InDesign's "Anchor Point(s)" panel, with Convert
|
||||
/// Points and the Delete Point(s) quick action, as in the Zero2Graphic tutorial (1:25).
|
||||
#[test]
|
||||
fn selected_points_show_convert_points_and_delete_points() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("frame.create", json!({"rect": [200, 72, 300, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0], [b, 0, 0]]})).unwrap();
|
||||
assert_eq!(selection_title(&app, &super::super::sel_info(&app)), "Anchor Point(s)");
|
||||
let mut h = panel(app);
|
||||
// The four point types (Appearance has a Corner row too).
|
||||
for tip in ["Plain", "Corner", "Smooth", "Symmetrical"] {
|
||||
assert!(h.get_all_by_label(tip).next().is_some(), "{tip}");
|
||||
}
|
||||
assert!(h.query_by_label("Align").is_none(), "only the sections InDesign shows for points");
|
||||
h.get_by_label("Delete Point(s)").click();
|
||||
h.run_steps(3);
|
||||
let d = &h.state().session.active().unwrap().doc;
|
||||
for id in [a, b] {
|
||||
assert_eq!(d.item(designcraft_doc::ids::ItemId(id)).unwrap().path.subpaths[0].anchors.len(), 3, "one point gone from each");
|
||||
}
|
||||
}
|
||||
|
||||
/// With points selected, Transform measures and moves the points: one corner of a frame shows
|
||||
/// that corner, not the frame, and X moves the corner alone; W spreads two points apart.
|
||||
#[test]
|
||||
fn transform_measures_and_moves_the_selected_points() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
let corner = |app: &DesignApp, i: usize| {
|
||||
let d = &app.session.active().unwrap().doc;
|
||||
let it = d.item(designcraft_doc::ids::ItemId(a)).unwrap();
|
||||
it.xf * it.path.subpaths[0].anchors[i].p
|
||||
};
|
||||
let first = corner(&app, 0);
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0]]})).unwrap();
|
||||
let i = super::super::sel_info(&app).unwrap();
|
||||
assert_eq!((i.page_rect.width(), i.page_rect.height()), (0.0, 0.0), "a point, not the frame");
|
||||
app.ui.ref_point = 0;
|
||||
transform_anchors(&mut app, &json!({"x": i.page_rect.x0 + 10.0})).unwrap();
|
||||
assert_eq!(corner(&app, 0), first + designcraft_geom::Vec2::new(10.0, 0.0), "the corner moved");
|
||||
assert_eq!(corner(&app, 2), designcraft_geom::Point::new(172.0, 172.0), "the opposite corner stayed");
|
||||
// Two points 100 pt apart, W 200 from the left: the right one goes 100 further.
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 2], [a, 0, 3]]})).unwrap();
|
||||
let (p2, p3) = (corner(&app, 2), corner(&app, 3));
|
||||
let w = super::super::sel_info(&app).unwrap().page_rect.width();
|
||||
transform_anchors(&mut app, &json!({"width": w * 2.0})).unwrap();
|
||||
let (left, right) = if p2.x < p3.x { (2, 3) } else { (3, 2) };
|
||||
let (pl, pr) = if left == 2 { (p2, p3) } else { (p3, p2) };
|
||||
assert_eq!(corner(&app, left), pl, "the reference side stays");
|
||||
assert!((corner(&app, right).x - (pr.x + w)).abs() < 1e-9, "spread to twice the width");
|
||||
}
|
||||
/// With points selected, Transform measures and moves the points: one corner of a frame shows
|
||||
/// that corner, not the frame, and X moves the corner alone; W spreads two points apart.
|
||||
#[test]
|
||||
fn transform_measures_and_moves_the_selected_points() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
let corner = |app: &DesignApp, i: usize| {
|
||||
let d = &app.session.active().unwrap().doc;
|
||||
let it = d.item(designcraft_doc::ids::ItemId(a)).unwrap();
|
||||
it.xf * it.path.subpaths[0].anchors[i].p
|
||||
};
|
||||
let first = corner(&app, 0);
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0]]})).unwrap();
|
||||
let i = super::super::sel_info(&app).unwrap();
|
||||
assert_eq!((i.page_rect.width(), i.page_rect.height()), (0.0, 0.0), "a point, not the frame");
|
||||
app.ui.ref_point = 0;
|
||||
transform_anchors(&mut app, &json!({"x": i.page_rect.x0 + 10.0})).unwrap();
|
||||
assert_eq!(corner(&app, 0), first + designcraft_geom::Vec2::new(10.0, 0.0), "the corner moved");
|
||||
assert_eq!(corner(&app, 2), designcraft_geom::Point::new(172.0, 172.0), "the opposite corner stayed");
|
||||
// Two points 100 pt apart, W 200 from the left: the right one goes 100 further.
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 2], [a, 0, 3]]})).unwrap();
|
||||
let (p2, p3) = (corner(&app, 2), corner(&app, 3));
|
||||
let w = super::super::sel_info(&app).unwrap().page_rect.width();
|
||||
transform_anchors(&mut app, &json!({"width": w * 2.0})).unwrap();
|
||||
let (left, right) = if p2.x < p3.x { (2, 3) } else { (3, 2) };
|
||||
let (pl, pr) = if left == 2 { (p2, p3) } else { (p3, p2) };
|
||||
assert_eq!(corner(&app, left), pl, "the reference side stays");
|
||||
assert!((corner(&app, right).x - (pr.x + w)).abs() < 1e-9, "spread to twice the width");
|
||||
}
|
||||
|
||||
/// One point has no size: W and H show its frame's, greyed out, as InDesign; two points
|
||||
/// measure (and set) their own.
|
||||
#[test]
|
||||
fn one_selected_point_shows_its_frames_size_greyed() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 222]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0]]})).unwrap();
|
||||
assert_eq!(super::super::sel_info(&app).unwrap().object_size, (100.0, 150.0));
|
||||
let mut h = panel(app);
|
||||
h.run_steps(2);
|
||||
let greyed = |h: &Harness<'_, DesignApp>| {
|
||||
h.query_all_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_disabled()).filter_map(|n| n.value()).collect::<Vec<_>>()
|
||||
};
|
||||
let u = units(h.state());
|
||||
let f = |v| designcraft_geom::units::format_measure(v, u);
|
||||
assert_eq!(greyed(&h), [f(100.0), f(150.0)], "the frame's W and H, greyed");
|
||||
h.state_mut().run("selection.set", json!({"anchors": [[a, 0, 0], [a, 0, 2]]})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert!(greyed(&h).is_empty(), "two points have a size of their own");
|
||||
}
|
||||
/// One point has no size: W and H show its frame's, greyed out, as InDesign; two points
|
||||
/// measure (and set) their own.
|
||||
#[test]
|
||||
fn one_selected_point_shows_its_frames_size_greyed() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 222]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0]]})).unwrap();
|
||||
assert_eq!(super::super::sel_info(&app).unwrap().object_size, (100.0, 150.0));
|
||||
let mut h = panel(app);
|
||||
h.run_steps(2);
|
||||
let greyed = |h: &Harness<'_, DesignApp>| {
|
||||
h.query_all_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_disabled()).filter_map(|n| n.value()).collect::<Vec<_>>()
|
||||
};
|
||||
let u = units(h.state());
|
||||
let f = |v| designcraft_geom::units::format_measure(v, u);
|
||||
assert_eq!(greyed(&h), [f(100.0), f(150.0)], "the frame's W and H, greyed");
|
||||
h.state_mut().run("selection.set", json!({"anchors": [[a, 0, 0], [a, 0, 2]]})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert!(greyed(&h).is_empty(), "two points have a size of their own");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_flips_and_rotates_from_properties() {
|
||||
|
||||
Reference in New Issue
Block a user