mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 05:42:38 +00:00
* M9: Boards: mind maps in the document and engine (#2226) Part of #2226, step 1 of 4. A mind map node is a board object of its own (BoardItem::MindNode: a frame hugging its text, no box) and a branch is a connector marked `branch`; the tree is read from the branches alone. `board.mindMap` places a first node with three children and `board.mindMapAdd` adds a child or a sibling, each one undo step. The normalize pass (`board::settle_mind_maps`, `figcraft_doc::mind_layout`) lays a touched map out again from its first node: 88 between columns, 48 between siblings, each stack centred on its parent; a branch that no longer joins two layers of its page is removed. Branches are curved, stroke 4, #757575, round caps. Numbers are from a FigJam map saved and decoded. No toolbar entry, shortcuts or import yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Fable 5.1 designed; Sonnet 5.5 wrote at medium * M12: Mind maps: a map around its own container stays put; a moved branch re-flows its old map (#2226) A map holding a layer and something inside it is skipped by the settle pass (it never settled). Every touched branch now stirs its old ends, so re-attaching a branch's start re-flows the map it left. Boards without branches return early, a map over MIND_MAX_NODES is left whole, shift skips non-finite moves, and the doc tests share one six-node helper. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Fable 5.1 reviewed; Sonnet 5.5 wrote at medium * M12: Mind maps: FigCraft's own starter texts (#2226) A new map's starter children read "Idea 1", "Idea 2" and "Idea 3", as the design specified, instead of the incumbent's wording, which the first commit had copied from the measurement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove and wrote * M12: Docs: the mind-map revision rows go in the revision history (#2226) The 2026-11-10 rows sat in ROADMAP.md's Headline numbers table and docs/roadmap.md's Alpha gate table; each now opens its file's revision history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove and wrote --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
314 lines
19 KiB
Rust
314 lines
19 KiB
Rust
//! Boards, continued: copies and pastes (within a file and from another), routes with named sides
|
|
//! and free points, connectors inside moving sections, styles and labels with their refusals, and
|
|
//! the shape, stamp, highlighter and connector tools with double-clicks.
|
|
|
|
use figcraft_doc::{BoardItem, BoardShape, End, NodeId, Side, Stamp, StrokeCap, board_color};
|
|
use figcraft_geom::Point;
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
use crate::board::route;
|
|
use crate::board_tests::{board, connector, drawn, id, near, node};
|
|
use crate::tools::{PointerEvent, PointerKind, ToolId};
|
|
|
|
/// The connector among `ids`, if any.
|
|
fn connector_in(s: &Session, ids: &[NodeId]) -> Option<figcraft_doc::Connector> {
|
|
let doc = s.doc().ok()?;
|
|
ids.iter().find_map(|i| match &doc.node(*i)?.board {
|
|
Some(BoardItem::Connector(k)) => Some(k.clone()),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn pasting_into_another_board_detaches_what_was_left_behind() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.sticky", &json!({"x": 0, "y": 0})).unwrap(), "id");
|
|
let b = id(&s.execute("board.sticky", &json!({"x": 400, "y": 0})).unwrap(), "id");
|
|
let r = s.execute("board.connector", &json!({"from": a, "to": b, "style": "straight", "label": "x"})).unwrap();
|
|
let (c, label) = (id(&r, "id"), id(&r, "label"));
|
|
let first = s.active;
|
|
// A second board whose own ids are the same small numbers as the first's.
|
|
let second = s.execute("file.newBoard", &json!({})).unwrap()["index"].clone();
|
|
for x in [0, 300, 600, 900] {
|
|
s.execute("board.sticky", &json!({"x": x, "y": 500})).unwrap();
|
|
}
|
|
let others: Vec<NodeId> = s.doc().unwrap().nodes().map(|n| n.id).collect();
|
|
s.execute("doc.switch", &json!({"index": first})).unwrap();
|
|
s.execute("select.set", &json!({"ids": [a, c, label]})).unwrap();
|
|
s.execute("edit.copy", &json!({})).unwrap();
|
|
s.execute("doc.switch", &json!({"index": second})).unwrap();
|
|
s.execute("edit.paste", &json!({})).unwrap();
|
|
let doc = s.doc().unwrap();
|
|
let pasted = doc.nodes().find(|n| matches!(n.board, Some(BoardItem::Connector(_))) && !others.contains(&n.id)).unwrap().clone();
|
|
let k = connector(&s, pasted.id);
|
|
let from = k.from.node().unwrap();
|
|
assert!(!others.contains(&from) && matches!(doc.node(from).unwrap().board, Some(BoardItem::Sticky { .. })), "joins the pasted sticky");
|
|
let to = drawn(&s, pasted.id).last().copied().unwrap();
|
|
assert!(matches!(k.to, End::Point { x, y } if near(Point::new(x, y), to)), "the end left behind stays where it is drawn: {k:?}");
|
|
let pasted_label = crate::board::label_of(doc, pasted.id).expect("the label came with its connector");
|
|
assert!(!others.contains(&pasted_label));
|
|
// A label copied without its connector doesn't come.
|
|
s.execute("doc.switch", &json!({"index": first})).unwrap();
|
|
s.execute("select.set", &json!({"ids": [label]})).unwrap();
|
|
s.execute("edit.copy", &json!({})).unwrap();
|
|
s.execute("doc.switch", &json!({"index": second})).unwrap();
|
|
let labels = |s: &Session| s.doc().unwrap().nodes().filter(|n| matches!(n.board, Some(BoardItem::Label { .. }))).count();
|
|
let before = labels(&s);
|
|
s.execute("edit.paste", &json!({})).unwrap();
|
|
assert_eq!(labels(&s), before);
|
|
// Within its own file, a connector pasted alone stays joined to the same layers.
|
|
s.execute("doc.switch", &json!({"index": first})).unwrap();
|
|
s.execute("select.set", &json!({"ids": [c]})).unwrap();
|
|
s.execute("edit.copy", &json!({})).unwrap();
|
|
s.execute("edit.paste", &json!({})).unwrap();
|
|
let k = connector_in(&s, &s.selection()).unwrap();
|
|
assert_eq!((k.from.node(), k.to.node()), (Some(a), Some(b)));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicating_and_alt_dragging_keep_connections_inside_copied_sections() {
|
|
let mut s = board();
|
|
let sec = id(&s.execute("node.create", &json!({"type": "section", "x": -50, "y": -50, "width": 400, "height": 400})).unwrap(), "id");
|
|
let a = id(&s.execute("board.sticky", &json!({"x": 50, "y": 50, "parent": sec})).unwrap(), "id");
|
|
let b = id(&s.execute("board.sticky", &json!({"x": 600, "y": 0})).unwrap(), "id");
|
|
let c = id(&s.execute("board.connector", &json!({"from": a, "to": b})).unwrap(), "id");
|
|
s.execute("edit.duplicate", &json!({"ids": [sec, c]})).unwrap();
|
|
let copies = s.selection();
|
|
let from = connector_in(&s, &copies).unwrap().from.node().unwrap();
|
|
let doc = s.doc().unwrap();
|
|
assert!(from != a && copies.iter().any(|r| doc.is_ancestor(*r, from)), "the copy joins the sticky inside the copied section");
|
|
// Alt-drag copies the same way.
|
|
s.execute("select.set", &json!({"ids": [b, c]})).unwrap();
|
|
let press = |k, x| PointerEvent { alt: true, ..PointerEvent::at(k, x, 100.0, 1.0) };
|
|
for (k, x) in [(PointerKind::Down, 700.0), (PointerKind::Move, 760.0), (PointerKind::Move, 800.0), (PointerKind::Up, 800.0)] {
|
|
s.pointer(&press(k, x)).unwrap();
|
|
}
|
|
let copies = s.selection();
|
|
assert_eq!(copies.len(), 2);
|
|
let k = connector_in(&s, &copies).unwrap();
|
|
assert!(copies.contains(&k.to.node().unwrap()), "{k:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn routes_take_named_sides_and_free_points() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.shape", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap(), "id");
|
|
let b = id(&s.execute("board.shape", &json!({"x": 300, "y": 300, "width": 100, "height": 100})).unwrap(), "id");
|
|
let doc = s.doc().unwrap();
|
|
let elbow =
|
|
|from, to| route(doc, &figcraft_doc::Connector { from, to, routing: figcraft_doc::Routing::Elbow, path: None, branch: false }).unwrap();
|
|
let side = |n, side| End::Node { node: n, side, at: None };
|
|
let p = |x, y| End::Point { x, y };
|
|
let pt = Point::new;
|
|
// Side to side: out (from END_GAP outside each side), across halfway, in.
|
|
assert_eq!(elbow(side(a, Side::Right), side(b, Side::Left)), vec![pt(104.0, 50.0), pt(200.0, 50.0), pt(200.0, 350.0), pt(296.0, 350.0)]);
|
|
// A side and a top or bottom: one turn.
|
|
assert_eq!(elbow(side(a, Side::Right), side(b, Side::Top)), vec![pt(104.0, 50.0), pt(350.0, 50.0), pt(350.0, 296.0)]);
|
|
assert_eq!(elbow(side(a, Side::Bottom), side(b, Side::Left)), vec![pt(50.0, 104.0), pt(50.0, 350.0), pt(296.0, 350.0)]);
|
|
// Two free points: the longer way first; in a line, no turns.
|
|
assert_eq!(elbow(p(0.0, 0.0), p(100.0, 40.0)), vec![pt(0.0, 0.0), pt(50.0, 0.0), pt(50.0, 40.0), pt(100.0, 40.0)]);
|
|
assert_eq!(elbow(p(0.0, 0.0), p(100.0, 0.0)), vec![pt(0.0, 0.0), pt(100.0, 0.0)]);
|
|
// An automatic side faces the other end.
|
|
let straight = route(
|
|
doc,
|
|
&figcraft_doc::Connector {
|
|
from: side(a, Side::Auto),
|
|
to: p(50.0, 500.0),
|
|
routing: figcraft_doc::Routing::Straight,
|
|
path: None,
|
|
branch: false,
|
|
},
|
|
);
|
|
assert_eq!(straight.unwrap(), vec![pt(50.0, 104.0), pt(50.0, 500.0)]);
|
|
assert_eq!(Side::from_id("TOP"), Some(Side::Top));
|
|
assert_eq!(Side::from_id("inside"), None);
|
|
// The same place twice is refused, as are a side that doesn't exist and a point without numbers.
|
|
assert!(s.execute("board.connector", &json!({"from": {"x": 5, "y": 5}, "to": {"x": 5, "y": 5}})).is_err());
|
|
assert!(s.execute("board.connector", &json!({"from": a, "to": b, "fromSide": "inside"})).is_err());
|
|
assert!(s.execute("board.connector", &json!({"from": a, "to": {"x": "a"}})).is_err());
|
|
assert!(s.execute("board.connector", &json!({"from": a})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn a_connector_follows_a_layer_whose_section_moves_and_ignores_unrelated_edits() {
|
|
let mut s = board();
|
|
let sec = id(&s.execute("node.create", &json!({"type": "section", "x": 0, "y": 0, "width": 300, "height": 300})).unwrap(), "id");
|
|
let a = id(&s.execute("board.sticky", &json!({"x": 50, "y": 50, "parent": sec})).unwrap(), "id");
|
|
let b = id(&s.execute("board.stamp", &json!({"x": 800, "y": 150})).unwrap(), "id");
|
|
let c = id(&s.execute("board.connector", &json!({"from": a, "to": b, "style": "straight"})).unwrap(), "id");
|
|
assert!(near(drawn(&s, c)[0], Point::new(254.0, 150.0)));
|
|
// Only the section is edited; the sticky inside it moves with it, and so does the connector.
|
|
s.execute("transform.move", &json!({"ids": [sec], "dx": 0, "dy": 100})).unwrap();
|
|
assert!(near(drawn(&s, c)[0], Point::new(254.0, 250.0)), "{:?}", drawn(&s, c));
|
|
// An edit that touches neither end leaves the connector as it was.
|
|
let before = s.doc().unwrap().node_arc(c).cloned().unwrap();
|
|
s.execute("board.sticky", &json!({"x": 0, "y": 900})).unwrap();
|
|
assert!(std::sync::Arc::ptr_eq(&before, s.doc().unwrap().node_arc(c).unwrap()));
|
|
}
|
|
|
|
#[test]
|
|
fn styles_labels_and_their_refusals() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.sticky", &json!({"x": 0, "y": 0, "author": false})).unwrap(), "id");
|
|
assert_eq!(node(&s, a).children.len(), 1, "no author line");
|
|
let b = id(&s.execute("board.sticky", &json!({"x": 400, "y": 0, "author": " Lin "})).unwrap(), "id");
|
|
assert!(matches!(node(&s, b).board, Some(BoardItem::Sticky { ref author }) if author == "Lin"));
|
|
let c = id(&s.execute("board.connector", &json!({"from": a, "to": b, "startCap": "circleFilled", "endCap": "none"})).unwrap(), "id");
|
|
assert_eq!((node(&s, c).stroke.cap_start, node(&s, c).stroke.cap_end), (StrokeCap::CircleFilled, StrokeCap::None));
|
|
// Labels: set, change, size, remove; named by the label itself too.
|
|
let l = id(&s.execute("board.label", &json!({"id": c, "text": "first"})).unwrap(), "label");
|
|
assert_eq!(id(&s.execute("board.label", &json!({"id": l, "text": "second"})).unwrap(), "label"), l, "the same label, new text");
|
|
let text = crate::board::text_of(s.doc().unwrap(), l).unwrap();
|
|
assert_eq!(node(&s, text).kind.text().unwrap().characters, "second");
|
|
s.execute("board.setStyle", &json!({"ids": [l], "fontSize": 36})).unwrap();
|
|
assert_eq!(node(&s, text).kind.text().unwrap().style.font_size, 36.0);
|
|
assert!(s.execute("board.label", &json!({"id": c, "text": ""})).unwrap()["label"].is_null());
|
|
assert!(!s.doc().unwrap().contains(l));
|
|
// What doesn't take a style or colour says so.
|
|
assert!(s.execute("board.setStyle", &json!({"ids": [a], "fontSize": 30})).is_err(), "stickies size their own text");
|
|
assert!(s.execute("board.setStyle", &json!({"ids": [c], "fontSize": 0})).is_err());
|
|
let st = id(&s.execute("board.stamp", &json!({"x": 0, "y": 0, "stamp": "check"})).unwrap(), "id");
|
|
assert!(s.execute("board.setColor", &json!({"ids": [st], "color": "blue"})).is_err());
|
|
assert!(s.execute("board.setColor", &json!({"ids": [a]})).is_err());
|
|
assert!(s.execute("board.label", &json!({"id": a, "text": "x"})).is_err());
|
|
// A palette colour on a line draws in its deeper shade; a hex colour as given.
|
|
s.execute("board.setColor", &json!({"ids": [c], "color": "blue"})).unwrap();
|
|
assert!(node(&s, c).strokes[0].solid_color().is_some() && node(&s, c).strokes[0].solid_color() != board_color("blue"));
|
|
s.execute("board.setColor", &json!({"ids": [c], "color": "#123456"})).unwrap();
|
|
assert_eq!(node(&s, c).strokes[0].solid_color().map(|c| c.to_hex()), Some("123456".into()));
|
|
// Next stickies on each side, and refusals.
|
|
for (side, dx, dy) in [("below", 0.0, 224.0), ("left", -224.0, 0.0), ("above", 0.0, -224.0)] {
|
|
let n = id(&s.execute("board.nextSticky", &json!({"id": a, "side": side})).unwrap(), "id");
|
|
let t = node(&s, n).transform.translation();
|
|
assert_eq!((t.x, t.y), (dx, dy), "{side}");
|
|
}
|
|
assert!(s.execute("board.nextSticky", &json!({"id": a, "side": "diagonal"})).is_err());
|
|
assert!(s.execute("board.nextSticky", &json!({"id": c})).is_err());
|
|
assert!(s.execute("board.ink", &json!({})).is_err());
|
|
assert!(s.execute("board.sticky", &json!({"x": 0, "y": 0, "parent": c})).is_err(), "a connector holds no layers");
|
|
}
|
|
|
|
#[test]
|
|
fn the_shape_stamp_highlighter_and_connector_tools_and_double_clicks() {
|
|
let mut s = board();
|
|
let click = |s: &mut Session, x, y| {
|
|
s.pointer(&PointerEvent::at(PointerKind::Down, x, y, 1.0)).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Up, x, y, 1.0)).unwrap();
|
|
};
|
|
s.execute("tool.shape", &json!({"shape": "roundedRectangle"})).unwrap();
|
|
click(&mut s, 100.0, 100.0);
|
|
let sh = s.selection()[0];
|
|
assert_eq!(node(&s, sh).board, Some(BoardItem::Shape { shape: BoardShape::RoundedRectangle }));
|
|
assert_eq!(s.tool.tool, ToolId::Move, "back to Move after placing");
|
|
s.execute("tool.stamp", &json!({"stamp": "question"})).unwrap();
|
|
click(&mut s, 400.0, 100.0);
|
|
click(&mut s, 450.0, 100.0);
|
|
let stamps = s.doc().unwrap().nodes().filter(|n| matches!(n.board, Some(BoardItem::Stamp { stamp: Stamp::Question, .. }))).count();
|
|
assert_eq!(stamps, 2);
|
|
assert_eq!(s.tool.tool, ToolId::Stamp, "stamps go on one after another");
|
|
assert!(s.execute("tool.stamp", &json!({"stamp": "skull"})).is_err());
|
|
s.execute("tool.highlighter", &json!({"color": "pink"})).unwrap();
|
|
assert_eq!(Some(s.tool.board.highlighter), board_color("pink"));
|
|
s.execute("tool.marker", &json!({"color": "#336699"})).unwrap();
|
|
assert_eq!(s.tool.board.marker.to_hex(), "336699");
|
|
// A connector drag that is only a click, or from an object to itself, makes nothing.
|
|
s.execute("tool.connector", &json!({"style": "straight"})).unwrap();
|
|
click(&mut s, 100.0, 100.0);
|
|
s.pointer(&PointerEvent::at(PointerKind::Down, 60.0, 120.0, 1.0)).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Move, 100.0, 140.0, 1.0)).unwrap();
|
|
assert!(!s.doc().unwrap().nodes().any(|n| matches!(n.board, Some(BoardItem::Connector(_)))), "nothing previewed: none would be made");
|
|
s.pointer(&PointerEvent::at(PointerKind::Up, 100.0, 140.0, 1.0)).unwrap();
|
|
assert!(!s.doc().unwrap().nodes().any(|n| matches!(n.board, Some(BoardItem::Connector(_)))));
|
|
// From the shape out to empty canvas: a connector to a free point.
|
|
s.pointer(&PointerEvent::at(PointerKind::Down, 80.0, 140.0, 1.0)).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Up, 80.0, 600.0, 1.0)).unwrap();
|
|
let c = s.doc().unwrap().nodes().find(|n| matches!(n.board, Some(BoardItem::Connector(_)))).map(|n| n.id).unwrap();
|
|
assert!(matches!(connector(&s, c).to, End::Point { .. }));
|
|
// Double-clicks: on the shape its text, on the connector a new label, both typed into.
|
|
s.execute("tool.move", &json!({})).unwrap();
|
|
s.execute("select.none", &json!({})).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::DoubleClick, 80.0, 140.0, 1.0)).unwrap();
|
|
let te = s.ds().unwrap().text_edit.clone().expect("typing in the shape");
|
|
assert_eq!(crate::board::owner_of_text(s.doc().unwrap(), te.id), Some(sh));
|
|
crate::textedit::end(&mut s).unwrap();
|
|
let line = drawn(&s, c);
|
|
let m = line[0].midpoint(line[1]);
|
|
s.pointer(&PointerEvent::at(PointerKind::DoubleClick, m.x, m.y, 1.0)).unwrap();
|
|
let label = crate::board::label_of(s.doc().unwrap(), c).expect("a label was added");
|
|
let te = s.ds().unwrap().text_edit.clone().expect("typing in the label");
|
|
assert_eq!(crate::board::owner_of_text(s.doc().unwrap(), te.id), Some(label));
|
|
}
|
|
|
|
#[test]
|
|
fn an_ink_stroke_ends_where_the_button_comes_up() {
|
|
let mut s = board();
|
|
s.execute("tool.marker", &json!({})).unwrap();
|
|
// Down and up with no move between: the stroke still runs from the press to the release.
|
|
s.pointer(&PointerEvent::at(PointerKind::Down, 0.0, 0.0, 1.0)).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Up, 150.0, 0.0, 1.0)).unwrap();
|
|
let doc = s.doc().unwrap();
|
|
let ink = doc.nodes().find(|n| matches!(n.board, Some(BoardItem::Ink))).map(|n| n.id).expect("a stroke");
|
|
let b = doc.world_bounds(ink).unwrap();
|
|
assert!(b.x0.abs() < 6.0 && (b.x1 - 150.0).abs() < 6.0, "from 0 to 150 (stroke width aside): {b:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_second_sticky_ends_the_first_ones_typing_and_undoes_alone() {
|
|
let mut s = board();
|
|
s.execute("tool.sticky", &json!({})).unwrap();
|
|
s.prefs.keep_tool = true;
|
|
s.pointer(&PointerEvent::at(PointerKind::Down, 100.0, 100.0, 1.0)).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Up, 100.0, 100.0, 1.0)).unwrap();
|
|
let text = s.ds().unwrap().text_edit.as_ref().expect("typing in the new sticky").id;
|
|
crate::textedit::insert(&mut s, "first note").unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Down, 500.0, 100.0, 1.0)).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Up, 500.0, 100.0, 1.0)).unwrap();
|
|
let stickies = |s: &Session| s.doc().unwrap().nodes().filter(|n| matches!(n.board, Some(BoardItem::Sticky { .. }))).count();
|
|
let typed = |s: &Session| s.doc().unwrap().node(text).unwrap().kind.text().unwrap().characters.clone();
|
|
assert_eq!(stickies(&s), 2);
|
|
crate::textedit::end(&mut s).unwrap();
|
|
s.undo().unwrap();
|
|
assert_eq!(stickies(&s), 1, "the second sticky is its own undo step");
|
|
assert_eq!(typed(&s), "first note", "the first sticky keeps what was typed in it");
|
|
}
|
|
|
|
#[test]
|
|
fn a_board_tool_left_on_presses_as_move_in_a_design_file() {
|
|
let mut s = board();
|
|
s.execute("tool.sticky", &json!({})).unwrap();
|
|
s.execute("file.new", &json!({})).unwrap();
|
|
let r = id(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap(), "id");
|
|
s.execute("select.none", &json!({})).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Down, 50.0, 50.0, 1.0)).unwrap();
|
|
s.pointer(&PointerEvent::at(PointerKind::Up, 50.0, 50.0, 1.0)).unwrap();
|
|
assert_eq!(s.tool.tool, ToolId::Move);
|
|
assert_eq!(s.selection(), vec![r], "the press selected as Move does");
|
|
}
|
|
|
|
/// #1916: a connector's start, END_GAP outside the layer it leaves, is a snap line for other layers
|
|
/// but not for that layer, nor for a section holding it (both carry the connector along), and nor
|
|
/// is the connector's label.
|
|
#[test]
|
|
fn a_connector_joined_to_a_moving_layer_or_its_section_is_no_snap_target() {
|
|
let mut s = board();
|
|
let sec = id(&s.execute("node.create", &json!({"type": "section", "x": 0, "y": 0, "width": 300, "height": 300})).unwrap(), "id");
|
|
let a = id(&s.execute("board.sticky", &json!({"x": 50, "y": 50, "parent": sec})).unwrap(), "id");
|
|
let b = id(&s.execute("board.stamp", &json!({"x": 800, "y": 150})).unwrap(), "id");
|
|
let c = id(&s.execute("board.connector", &json!({"from": a, "to": b, "style": "straight", "label": "to"})).unwrap(), "id");
|
|
// A layer the connector has nothing to do with.
|
|
let d = id(&s.execute("board.sticky", &json!({"x": 0, "y": 900})).unwrap(), "id");
|
|
let page = s.page().unwrap();
|
|
let start = drawn(&s, c)[0].x;
|
|
let label = crate::board::label_of(s.doc().unwrap(), c).unwrap();
|
|
let lx = s.doc().unwrap().world_bounds(label).unwrap().x0;
|
|
// A box whose left edge is 1 past a line snaps back by 1 when the line is a target.
|
|
let pull = |s: &Session, moving: &[NodeId], x: f64| {
|
|
let t = crate::snap::Targets::gather(s.doc().unwrap(), page, moving);
|
|
t.snap_rect(figcraft_geom::Rect::new(x + 1.0, 600.0, x + 9.0, 610.0), 4.0).0
|
|
};
|
|
assert_eq!((pull(&s, &[d], start), pull(&s, &[d], lx)), (-1.0, -1.0), "for an unrelated layer, the connector and its label snap");
|
|
assert_eq!(pull(&s, &[b], start), 0.0, "not for the layer it arrives at");
|
|
assert_eq!((pull(&s, &[sec], start), pull(&s, &[sec], lx)), (0.0, 0.0), "not for the section the connector leaves from");
|
|
}
|