mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Direct Selection: a marquee selects area type's frame anchors
Squashed from #4435 onto main.
This commit is contained in:
@@ -96,7 +96,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Reshape Type Area",
|
||||
[],
|
||||
None,
|
||||
"{id, anchors: [[subpath, anchor]…], dx, dy} move anchors of area type's frame (the type area) by dx, dy points, with their handles; the text reflows at its size, through its thread too (what Direct Selection does dragging a frame corner or edge)",
|
||||
"{id, anchors: [[subpath, anchor]…], dx, dy} or {items: [{id, anchors}…], dx, dy} move anchors of area type's frame (the type area) by dx, dy points, with their handles; the text reflows at its size, through its thread too (what Direct Selection does dragging a frame corner or edge, or the frame anchors its marquee selected, and the arrow keys nudging those); with items, every listed frame moves in one step",
|
||||
has_doc,
|
||||
reshape_area
|
||||
),
|
||||
@@ -1063,22 +1063,45 @@ fn area_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn reshape_area(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.reshapeArea";
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
|
||||
let mut refs = super::select::checked_refs(p.get("anchors"), C)?;
|
||||
refs.sort_unstable();
|
||||
refs.dedup();
|
||||
if refs.is_empty() {
|
||||
return Err(bad(C, "missing anchors [[subpath, anchor]…]"));
|
||||
// One area type (`id`, `anchors`), or several (`items`: [{id, anchors}…]).
|
||||
let raw: Vec<&Value> = match p.get("items") {
|
||||
Some(items) => {
|
||||
if p.get("id").is_some() || p.get("anchors").is_some() {
|
||||
return Err(bad(C, "give `id` and `anchors`, or `items`, not both"));
|
||||
}
|
||||
let items = items.as_array().ok_or_else(|| bad(C, "`items` must be an array of {id, anchors}"))?;
|
||||
if items.is_empty() {
|
||||
return Err(bad(C, "`items` is empty"));
|
||||
}
|
||||
items.iter().collect()
|
||||
}
|
||||
None => vec![p],
|
||||
};
|
||||
let mut items = Vec::with_capacity(raw.len());
|
||||
for it in raw {
|
||||
let id = id_param(it, "id").ok_or_else(|| bad(C, "missing id"))?;
|
||||
let mut refs = super::select::checked_refs(it.get("anchors"), C)?;
|
||||
refs.sort_unstable();
|
||||
refs.dedup();
|
||||
if refs.is_empty() {
|
||||
return Err(bad(C, "missing anchors [[subpath, anchor]…]"));
|
||||
}
|
||||
if items.iter().any(|(other, _)| *other == id) {
|
||||
return Err(bad(C, format!("area type {} is named twice", id.0)));
|
||||
}
|
||||
let node = s.doc()?.doc.node(id);
|
||||
if area_text(node).is_none() || node.is_some_and(|n| n.extras().perspective.is_some()) {
|
||||
return Err(bad(C, "not area type (text in a frame) outside perspective"));
|
||||
}
|
||||
items.push((id, refs));
|
||||
}
|
||||
let d = Vec2::new(f64_req(p, "dx", C)?, f64_req(p, "dy", C)?);
|
||||
let node = s.doc()?.doc.node(id);
|
||||
if area_text(node).is_none() || node.is_some_and(|n| n.extras().perspective.is_some()) {
|
||||
return Err(bad(C, "not area type (text in a frame) outside perspective"));
|
||||
}
|
||||
s.edit("Reshape Type Area", |doc, _| {
|
||||
let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) else { return Err(EngineError::NoNode(id)) };
|
||||
if !reshape_area_with(t, |t| t.move_area_anchors(&refs, d)) {
|
||||
return Err(bad(C, "no such frame anchor, or the frame would reach past the canvas"));
|
||||
for (id, refs) in &items {
|
||||
let Some(NodeKind::Text(t)) = doc.node_mut(*id).map(|n| &mut n.kind) else { return Err(EngineError::NoNode(*id)) };
|
||||
if !reshape_area_with(t, |t| t.move_area_anchors(refs, d)) {
|
||||
return Err(bad(C, "no such frame anchor, or the frame would reach past the canvas"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use vectorcraft_doc::{NodeKind, TextKind, TextObject};
|
||||
use vectorcraft_geom::{Affine, Point, Rect};
|
||||
use vectorcraft_text::TextLayout;
|
||||
use vectorcraft_tools::bbox::Handle;
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -194,6 +194,87 @@ fn direct_selection_drags_a_frame_corner_or_edge() {
|
||||
assert_eq!(text(&s, id).runs[0].style.size, 12.0);
|
||||
}
|
||||
|
||||
/// Direct Selection's marquee selects the frame anchors of area type inside it: a drag of one moves
|
||||
/// only those, the arrow keys nudge them, and Shift-click takes one out.
|
||||
#[test]
|
||||
fn direct_selection_marquee_selects_frame_anchors() {
|
||||
let mut s = session();
|
||||
let id = area(&mut s, 40.0, 40.0, 120.0, 40.0, STORY);
|
||||
let other = area(&mut s, 300.0, 40.0, 120.0, 40.0, STORY);
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
// A marquee round the right-hand corners, (160, 40) and (160, 80).
|
||||
drag(&mut s, "directSelection", Point::new(150.0, 20.0), Point::new(180.0, 100.0));
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![id]);
|
||||
let n = undo_len(&s);
|
||||
drag(&mut s, "directSelection", Point::new(160.0, 80.0), Point::new(180.0, 100.0));
|
||||
assert_eq!(undo_len(&s), n + 1, "one step");
|
||||
assert_eq!(last_step(&s), "Reshape Type Area");
|
||||
assert_eq!(corners(&s, id), vec![Point::ZERO, Point::new(140.0, 20.0), Point::new(140.0, 60.0), Point::new(0.0, 40.0)]);
|
||||
// The arrow keys nudge them, each press one step, and Shift-click takes (179, 60) out.
|
||||
assert!(s.tool_claims_key(ToolKey::Left, v));
|
||||
s.tool_key(ToolKey::Left, Mods::default(), v).unwrap();
|
||||
assert_eq!(undo_len(&s), n + 2);
|
||||
assert_eq!(corners(&s, id), vec![Point::ZERO, Point::new(139.0, 20.0), Point::new(139.0, 60.0), Point::new(0.0, 40.0)]);
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 179.0, 60.0).with_mods(shift), v).unwrap();
|
||||
}
|
||||
s.tool_key(ToolKey::Down, shift, v).unwrap();
|
||||
assert_eq!(corners(&s, id), vec![Point::ZERO, Point::new(139.0, 20.0), Point::new(139.0, 70.0), Point::new(0.0, 40.0)]);
|
||||
// A marquee across both frames' facing corners moves both frames in one step.
|
||||
drag(&mut s, "directSelection", Point::new(170.0, 10.0), Point::new(310.0, 65.0));
|
||||
assert_eq!(s.doc().unwrap().selection.objects.len(), 2);
|
||||
let n = undo_len(&s);
|
||||
drag(&mut s, "directSelection", Point::new(300.0, 40.0), Point::new(300.0, 30.0));
|
||||
assert_eq!(undo_len(&s), n + 1, "one step");
|
||||
assert_eq!(corners(&s, id)[1], Point::new(139.0, 10.0));
|
||||
assert_eq!(corners(&s, other), vec![Point::new(0.0, -10.0), Point::new(120.0, 0.0), Point::new(120.0, 40.0), Point::new(0.0, 40.0)]);
|
||||
// Shift-click on the other frame's only selected corner deselects that type; the corner of the
|
||||
// first frame still nudges.
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 300.0, 30.0).with_mods(shift), v).unwrap();
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![id]);
|
||||
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
||||
assert_eq!(corners(&s, id)[1], Point::new(139.0, 9.0));
|
||||
// Deselect, then the same two selected again: they are selected whole, so the arrow keys move
|
||||
// them as objects.
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
sel(&mut s, &[id, other]);
|
||||
assert!(!s.tool_claims_key(ToolKey::Left, v));
|
||||
// A click on empty canvas deselects; the arrow keys are then no longer the tool's.
|
||||
drag(&mut s, "directSelection", Point::new(600.0, 500.0), Point::new(600.0, 500.0));
|
||||
assert!(!s.tool_claims_key(ToolKey::Left, v));
|
||||
}
|
||||
|
||||
/// `text.reshapeArea` with `items` moves several frames' anchors in one step, and fails whole.
|
||||
#[test]
|
||||
fn reshape_area_moves_the_anchors_of_several_frames() {
|
||||
let mut s = session();
|
||||
let a = area(&mut s, 40.0, 40.0, 120.0, 40.0, STORY);
|
||||
let b = area(&mut s, 300.0, 40.0, 120.0, 40.0, STORY);
|
||||
let n = undo_len(&s);
|
||||
let items = json!([{"id": a.0, "anchors": [[0, 2]]}, {"id": b.0, "anchors": [[0, 0], [0, 3]]}]);
|
||||
s.execute("text.reshapeArea", &json!({"items": items, "dx": -10, "dy": 5})).unwrap();
|
||||
assert_eq!(undo_len(&s), n + 1, "one step");
|
||||
assert_eq!(corners(&s, a)[2], Point::new(110.0, 45.0));
|
||||
assert_eq!(corners(&s, b), vec![Point::new(-10.0, 5.0), Point::new(120.0, 0.0), Point::new(120.0, 40.0), Point::new(-10.0, 45.0)]);
|
||||
for bad in [
|
||||
json!({"items": [], "dx": 1, "dy": 1}),
|
||||
json!({"items": {"id": a.0}, "dx": 1, "dy": 1}),
|
||||
json!({"items": [{"id": a.0, "anchors": [[0, 2]]}], "id": a.0, "dx": 1, "dy": 1}),
|
||||
json!({"items": [{"id": a.0, "anchors": [[0, 2]]}, {"id": a.0, "anchors": [[0, 1]]}], "dx": 1, "dy": 1}),
|
||||
// A bad item fails the whole step: the good one does not move either.
|
||||
json!({"items": [{"id": a.0, "anchors": [[0, 2]]}, {"id": b.0, "anchors": [[0, 9]]}], "dx": 1, "dy": 1}),
|
||||
json!({"items": [{"id": a.0, "anchors": [[0, 2]]}, {"anchors": [[0, 1]]}], "dx": 1, "dy": 1}),
|
||||
] {
|
||||
assert!(s.execute("text.reshapeArea", &bad).is_err(), "{bad}");
|
||||
}
|
||||
assert_eq!(undo_len(&s), n + 1, "nothing more changed");
|
||||
assert_eq!(corners(&s, a)[2], Point::new(110.0, 45.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resized_frame_reflows_its_thread() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user