mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-14 06:12:28 +00:00
Review: the drop target is the topmost sticky or shape under the pointer, sections and frames looked into (board::sticky_or_shape_at, instead of the stamp's any-layer hit test on the page's children); the drop point is placed in the parent's coordinates; the connector leaves by the handle's side (as the preview draws it) and arrives by the facing side; a handle let go off the canvas makes nothing; the parity rows on quick-create handles agree. Tests: dropping_a_handle_on_a_shape_inside_a_frame_joins_it, letting_a_handle_go_over_a_panel_makes_nothing, the_next_one_goes_where_it_is_dropped (frame case). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
135 lines
7.3 KiB
Rust
135 lines
7.3 KiB
Rust
//! `board.addConnected` (#1597): the next sticky or shape on a side of the selected one, of the
|
|
//! same kind, size and colour, joined to it by a connector, in one undo step.
|
|
|
|
use figcraft_doc::{BoardItem, BoardShape, Connector, End, NodeId, Side};
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
use crate::board::CONNECTED_GAP;
|
|
use crate::board_tests::{board, id, node};
|
|
|
|
fn connector(s: &Session, c: NodeId) -> Connector {
|
|
match &node(s, c).board {
|
|
Some(BoardItem::Connector(k)) => k.clone(),
|
|
other => panic!("not a connector: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn at(s: &Session, n: NodeId) -> (f64, f64) {
|
|
let t = node(s, n).transform.translation();
|
|
(t.x, t.y)
|
|
}
|
|
|
|
#[test]
|
|
fn a_shape_gets_the_next_one_joined_on_each_side() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.shape", &json!({"shape": "diamond", "x": 0, "y": 0, "width": 200, "height": 120, "color": "blue"})).unwrap(), "id");
|
|
let colour = |s: &Session, n: NodeId| node(s, node(s, n).children[0]).fills[0].solid_color();
|
|
let g = CONNECTED_GAP;
|
|
for (side, want, ends) in [
|
|
("right", (200.0 + g, 0.0), (Side::Right, Side::Left)),
|
|
("below", (0.0, 120.0 + g), (Side::Bottom, Side::Top)),
|
|
("left", (-200.0 - g, 0.0), (Side::Left, Side::Right)),
|
|
("above", (0.0, -120.0 - g), (Side::Top, Side::Bottom)),
|
|
] {
|
|
let r = s.execute("board.addConnected", &json!({"id": a, "side": side})).unwrap();
|
|
let (b, c) = (id(&r, "id"), id(&r, "connector"));
|
|
assert_eq!(at(&s, b), want, "{side}");
|
|
let nb = node(&s, b);
|
|
assert_eq!((nb.width, nb.height), (200.0, 120.0), "{side}: the same size");
|
|
assert!(matches!(nb.board, Some(BoardItem::Shape { shape: BoardShape::Diamond })), "{side}: the same shape");
|
|
assert_eq!(colour(&s, b), colour(&s, a), "{side}: the same colour");
|
|
let k = connector(&s, c);
|
|
assert_eq!((k.from, k.to), (End::Node { node: a, side: ends.0, at: None }, End::Node { node: b, side: ends.1, at: None }), "{side}");
|
|
assert_eq!(k.routing, s.tool.board.routing, "{side}: as the connector tool makes one");
|
|
assert_eq!(s.selection(), vec![b], "{side}: the new one is selected");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_sticky_gets_a_sticky_of_its_colour_and_typing_goes_on_in_it() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.sticky", &json!({"x": 0, "y": 0, "color": "pink", "text": "First"})).unwrap(), "id");
|
|
s.ui_requests.clear();
|
|
// The selection is the default, and editing is asked for.
|
|
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
|
let r = s.execute("board.addConnected", &json!({"side": "below", "edit": true})).unwrap();
|
|
let b = id(&r, "id");
|
|
assert!(matches!(node(&s, b).board, Some(BoardItem::Sticky { .. })));
|
|
assert_eq!(node(&s, b).fills[0].solid_color(), node(&s, a).fills[0].solid_color());
|
|
assert_eq!(at(&s, b).1, node(&s, a).height + CONNECTED_GAP);
|
|
assert!(s.ui_requests.iter().any(|q| q.kind == "board.editText" && q.data["id"] == json!(b)));
|
|
}
|
|
|
|
#[test]
|
|
fn it_is_one_undo_step_and_refuses_what_it_cant_join() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.shape", &json!({"shape": "ellipse", "x": 0, "y": 0})).unwrap(), "id");
|
|
let before = s.doc().unwrap().ids().count();
|
|
let r = s.execute("board.addConnected", &json!({"id": a})).unwrap();
|
|
assert_eq!(at(&s, id(&r, "id")).0, 160.0 + CONNECTED_GAP, "right by default");
|
|
assert!(s.doc().unwrap().ids().count() > before);
|
|
s.execute("edit.undo", &json!({})).unwrap();
|
|
assert_eq!(s.doc().unwrap().ids().count(), before, "the shape and its connector go together");
|
|
// Not a sticky or shape, not a side, nothing selected.
|
|
let c = id(&s.execute("board.stamp", &json!({"x": 400, "y": 0, "stamp": "star"})).unwrap(), "id");
|
|
assert!(s.execute("board.addConnected", &json!({"id": c})).is_err());
|
|
assert!(s.execute("board.addConnected", &json!({"id": a, "side": "diagonal"})).is_err());
|
|
s.execute("select.set", &json!({"ids": []})).unwrap();
|
|
assert!(s.execute("board.addConnected", &json!({})).is_err());
|
|
// Not on a design file.
|
|
let mut d = Session::new();
|
|
assert!(d.execute("board.addConnected", &json!({})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn the_next_sticky_shortcut_joins_the_next_shape_and_leaves_stickies_as_they_were() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.shape", &json!({"shape": "rectangle", "x": 0, "y": 0, "width": 200, "height": 120})).unwrap(), "id");
|
|
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
|
let undo = s.ds().unwrap().history.undo.len();
|
|
// Shift+Cmd/Ctrl+Enter on a shape: the next shape on the right, joined by a connector (#1597).
|
|
let r = s.execute("board.nextSticky", &json!({})).unwrap();
|
|
let (b, c) = (id(&r, "id"), id(&r, "connector"));
|
|
assert!(matches!(node(&s, b).board, Some(BoardItem::Shape { shape: BoardShape::Rectangle })));
|
|
assert_eq!(at(&s, b), (200.0 + CONNECTED_GAP, 0.0));
|
|
assert_eq!(connector(&s, c).from, End::Node { node: a, side: Side::Right, at: None });
|
|
assert_eq!(s.ds().unwrap().history.undo.len(), undo + 1, "one undo step");
|
|
// Another side, as the command takes one.
|
|
let r = s.execute("board.nextSticky", &json!({"id": a, "side": "below"})).unwrap();
|
|
assert_eq!(at(&s, id(&r, "id")), (0.0, 120.0 + CONNECTED_GAP));
|
|
// A sticky still gets a sticky beside it, without a connector.
|
|
let st = id(&s.execute("board.sticky", &json!({"x": 1000, "y": 0})).unwrap(), "id");
|
|
let r = s.execute("board.nextSticky", &json!({"id": st})).unwrap();
|
|
assert!(r.get("connector").is_none(), "{r}");
|
|
assert!(matches!(node(&s, id(&r, "id")).board, Some(BoardItem::Sticky { .. })));
|
|
}
|
|
|
|
/// `at` (#1919; "drag the preview to any location on your board", Build faster with quick create):
|
|
/// the next one is centred on that point, wherever it is, and the connector takes the sides facing
|
|
/// each other; `at` must be two finite numbers.
|
|
#[test]
|
|
fn the_next_one_goes_where_it_is_dropped() {
|
|
let mut s = board();
|
|
let a = id(&s.execute("board.shape", &json!({"shape": "diamond", "x": 0, "y": 0, "width": 200, "height": 120})).unwrap(), "id");
|
|
let r = s.execute("board.addConnected", &json!({"id": a, "side": "below", "at": [700, 60]})).unwrap();
|
|
let (b, c) = (id(&r, "id"), id(&r, "connector"));
|
|
assert_eq!(at(&s, b), (600.0, 0.0), "centred on the point");
|
|
let k = connector(&s, c);
|
|
assert_eq!(
|
|
(k.from, k.to),
|
|
(End::Node { node: a, side: Side::Bottom, at: None }, End::Node { node: b, side: Side::Auto, at: None }),
|
|
"leaves by the handle's side"
|
|
);
|
|
// Inside a frame at (1000, 500): the page point lands in the frame's coordinates.
|
|
let f = id(&s.execute("frame.create", &json!({"x": 1000, "y": 500, "width": 800, "height": 600})).unwrap(), "id");
|
|
let inner = id(&s.execute("board.shape", &json!({"shape": "ellipse", "x": 0, "y": 0, "width": 200, "height": 120, "parent": f})).unwrap(), "id");
|
|
let r = s.execute("board.addConnected", &json!({"id": inner, "at": [1300, 700]})).unwrap();
|
|
let placed = id(&r, "id");
|
|
assert_eq!(node(&s, placed).parent, Some(f), "beside it, in the frame");
|
|
assert_eq!(at(&s, placed), (200.0, 140.0), "centred on the page point, in the frame's coordinates");
|
|
for wrong in [json!([1]), json!("x"), json!([0, "y"]), json!({"x": 1, "y": 2})] {
|
|
assert!(s.execute("board.addConnected", &json!({"id": a, "at": wrong})).is_err(), "{wrong}");
|
|
}
|
|
}
|