mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-03 08:49:37 -06:00
M12: a connector end on any part of an object with Cmd/Ctrl (#1733)
This commit is contained in:
@@ -40,7 +40,7 @@ fn a_shape_gets_the_next_one_joined_on_each_side() {
|
||||
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 }, End::Node { node: b, side: ends.1 }), "{side}");
|
||||
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");
|
||||
}
|
||||
@@ -93,7 +93,7 @@ fn the_next_sticky_shortcut_joins_the_next_shape_and_leaves_stickies_as_they_wer
|
||||
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 });
|
||||
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();
|
||||
|
||||
@@ -25,7 +25,7 @@ fn three() -> (Session, [figcraft_doc::NodeId; 4]) {
|
||||
fn reattach_moves_one_end_and_undoes() {
|
||||
let (mut s, [a, b, c, k]) = three();
|
||||
s.execute("board.reattach", &json!({"id": k, "end": "to", "target": c, "side": "top"})).unwrap();
|
||||
assert_eq!(connector(&s, k).to, End::Node { node: c, side: Side::Top });
|
||||
assert_eq!(connector(&s, k).to, End::Node { node: c, side: Side::Top, at: None });
|
||||
assert_eq!(connector(&s, k).from.node(), Some(a));
|
||||
assert!(near(*drawn(&s, k).last().unwrap(), Point::new(650.0, 0.0)), "arrives on c's top: {:?}", drawn(&s, k));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
@@ -257,7 +257,7 @@ fn an_end_dropped_along_a_side_attaches_to_that_side() {
|
||||
let want = (side != Side::Auto).then(|| side.point(bounds)).map(|p| [p.x, p.y]);
|
||||
assert_eq!(lit, want.into_iter().collect::<Vec<_>>(), "{drop:?}");
|
||||
s.pointer(&ev(PointerKind::Up, drop)).unwrap();
|
||||
assert_eq!(connector(&s, k).from, End::Node { node: a, side }, "{drop:?}");
|
||||
assert_eq!(connector(&s, k).from, End::Node { node: a, side, at: None }, "{drop:?}");
|
||||
if side != Side::Auto {
|
||||
assert_eq!(drawn(&s, k)[0], side.point(bounds), "the route leaves from that side");
|
||||
}
|
||||
@@ -277,7 +277,10 @@ fn the_connector_tool_takes_the_side_it_starts_and_ends_on() {
|
||||
s.pointer(&ev(PointerKind::Move, 590.0, 330.0)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, 590.0, 330.0)).unwrap();
|
||||
let k = the_connector(&s).unwrap().id;
|
||||
assert_eq!((connector(&s, k).from, connector(&s, k).to), (End::Node { node: a, side: Side::Bottom }, End::Node { node: b, side: Side::Right }));
|
||||
assert_eq!(
|
||||
(connector(&s, k).from, connector(&s, k).to),
|
||||
(End::Node { node: a, side: Side::Bottom, at: None }, End::Node { node: b, side: Side::Right, at: None })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -300,6 +303,9 @@ fn a_connector_end_snaps_to_the_attach_point_under_the_pointer() {
|
||||
s.pointer(&ev(PointerKind::Move, 352.0, 204.0)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, 352.0, 204.0)).unwrap();
|
||||
let k = the_connector(&s).unwrap().id;
|
||||
assert_eq!((connector(&s, k).from, connector(&s, k).to), (End::Node { node: a, side: Side::Right }, End::Node { node: b, side: Side::Top }));
|
||||
assert_eq!(
|
||||
(connector(&s, k).from, connector(&s, k).to),
|
||||
(End::Node { node: a, side: Side::Right, at: None }, End::Node { node: b, side: Side::Top, at: None })
|
||||
);
|
||||
assert_eq!(drawn(&s, k), vec![Point::new(100.0, 50.0), Point::new(350.0, 200.0)]);
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ fn a_reshaped_connector_keeps_the_sides_it_left_by() {
|
||||
let (portion, at) = handles[handles.len() / 2];
|
||||
reshape(&mut s, k, portion as f64, at.x + 30.0, at.y + 30.0).unwrap();
|
||||
let c = connector(&s, k);
|
||||
assert!(matches!(c.from, End::Node { node, side } if node == a && side != Side::Auto), "{c:?}");
|
||||
assert!(matches!(c.to, End::Node { node, side } if node == b && side != Side::Auto), "{c:?}");
|
||||
assert!(matches!(c.from, End::Node { node, side, .. } if node == a && side != Side::Auto), "{c:?}");
|
||||
assert!(matches!(c.to, End::Node { node, side, .. } if node == b && side != Side::Auto), "{c:?}");
|
||||
}
|
||||
|
||||
/// "For the start and end portions of a connector, dragging can add an extra portion to the path."
|
||||
|
||||
@@ -201,7 +201,12 @@ fn connectors_follow_what_they_join_and_undo_with_it() {
|
||||
fn routes_are_the_same_for_the_same_document() {
|
||||
let mut s = board();
|
||||
let a = id(&s.execute("board.shape", &json!({"x": 0, "y": 0})).unwrap(), "id");
|
||||
let c = Connector { from: End::Node { node: a, side: Side::Bottom }, to: End::Point { x: 300.0, y: 500.0 }, routing: Routing::Elbow, path: None };
|
||||
let c = Connector {
|
||||
from: End::Node { node: a, side: Side::Bottom, at: None },
|
||||
to: End::Point { x: 300.0, y: 500.0 },
|
||||
routing: Routing::Elbow,
|
||||
path: None,
|
||||
};
|
||||
let doc = s.doc().unwrap();
|
||||
let r = route(doc, &c).unwrap();
|
||||
assert_eq!(r, route(&doc.clone(), &c).unwrap());
|
||||
|
||||
@@ -97,7 +97,7 @@ fn routes_take_named_sides_and_free_points() {
|
||||
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 }).unwrap();
|
||||
let side = |n, side| End::Node { node: n, side };
|
||||
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, across halfway, in.
|
||||
|
||||
@@ -128,14 +128,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"board.connector",
|
||||
"Connector",
|
||||
"{from, to (each a layer id or {x, y} on the page), fromSide?, toSide? (auto|top|right|bottom|left), style?: straight|elbow|curved, startCap?, endCap? (none|lineArrow|triangleArrow|circleFilled|diamondFilled…; default an arrow at the end), label?} → {id, label?} a line that follows the layers it joins",
|
||||
"{from, to (each a layer id or {x, y} on the page), fromSide?, toSide? (auto|top|right|bottom|left), fromAt?, toAt? ([x, y] on the layer, fractions of its box), style?: straight|elbow|curved, startCap?, endCap? (none|lineArrow|triangleArrow|circleFilled|diamondFilled…; default an arrow at the end), label?} → {id, label?} a line that follows the layers it joins",
|
||||
true,
|
||||
connector_cmd
|
||||
),
|
||||
spec!(
|
||||
"board.reattach",
|
||||
"Reattach connector",
|
||||
"{id? (a connector; default the selection), end: from|to, target (a layer id, or {x, y} on the page to leave the end free), side? (auto|top|right|bottom|left)} → {id} moves one end of a connector",
|
||||
"{id? (a connector; default the selection), end: from|to, target (a layer id, or {x, y} on the page to leave the end free), side? (auto|top|right|bottom|left), at? ([x, y] on the layer, fractions of its box)} → {id} moves one end of a connector",
|
||||
true,
|
||||
reattach
|
||||
),
|
||||
|
||||
@@ -6,8 +6,21 @@ use super::{
|
||||
VectorNetwork, bad, cap_of, get, id_of, json, num, req_targets, require, routing_of, string,
|
||||
};
|
||||
|
||||
/// One connector end from the params: a layer on this page, or a point.
|
||||
fn end_param(cmd: &str, s: &Session, p: &Value, key: &str, side_key: &str) -> Result<End> {
|
||||
/// Where on a layer a connector end sits, from the params: `[x, y]`, fractions of its box.
|
||||
fn at_param(cmd: &str, p: &Value, key: &str) -> Result<Option<[f64; 2]>> {
|
||||
let Some(v) = get(p, key).filter(|v| !v.is_null()) else { return Ok(None) };
|
||||
match v.as_array().map(Vec::as_slice) {
|
||||
Some([x, y]) => match (x.as_f64().filter(|x| x.is_finite()), y.as_f64().filter(|y| y.is_finite())) {
|
||||
(Some(x), Some(y)) => Ok(Some([x.clamp(0.0, 1.0), y.clamp(0.0, 1.0)])),
|
||||
_ => Err(bad(cmd, format!("`{key}` is [x, y], fractions of the layer's box"))),
|
||||
},
|
||||
_ => Err(bad(cmd, format!("`{key}` is [x, y], fractions of the layer's box"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// One connector end from the params (`keys`: its target, side and place on the layer): a layer on
|
||||
/// this page, or a point.
|
||||
fn end_param(cmd: &str, s: &Session, p: &Value, [key, side_key, at_key]: [&str; 3]) -> Result<End> {
|
||||
let side = match string(p, side_key) {
|
||||
Some(v) => Side::from_id(&v).ok_or_else(|| bad(cmd, format!("`{side_key}` is auto, top, right, bottom or left")))?,
|
||||
None => Side::Auto,
|
||||
@@ -30,7 +43,7 @@ fn end_param(cmd: &str, s: &Session, p: &Value, key: &str, side_key: &str) -> Re
|
||||
if matches!(n.board, Some(BoardItem::Connector(_))) {
|
||||
return Err(bad(cmd, format!("`{key}` can't be another connector")));
|
||||
}
|
||||
Ok(End::Node { node, side })
|
||||
Ok(End::Node { node, side, at: at_param(cmd, p, at_key)? })
|
||||
}
|
||||
None => Err(bad(cmd, format!("`{key}` is required"))),
|
||||
}
|
||||
@@ -39,8 +52,8 @@ fn end_param(cmd: &str, s: &Session, p: &Value, key: &str, side_key: &str) -> Re
|
||||
pub(super) fn connector_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "board.connector";
|
||||
require(s, cmd)?;
|
||||
let from = end_param(cmd, s, p, "from", "fromSide")?;
|
||||
let to = end_param(cmd, s, p, "to", "toSide")?;
|
||||
let from = end_param(cmd, s, p, ["from", "fromSide", "fromAt"])?;
|
||||
let to = end_param(cmd, s, p, ["to", "toSide", "toAt"])?;
|
||||
if from.node().is_some() && from.node() == to.node() {
|
||||
return Err(bad(cmd, "connect two different layers"));
|
||||
}
|
||||
@@ -106,7 +119,7 @@ pub(super) fn reattach(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some("to") => false,
|
||||
_ => return Err(bad(cmd, "`end` is from or to")),
|
||||
};
|
||||
let end = end_param(cmd, s, p, "target", "side")?;
|
||||
let end = end_param(cmd, s, p, ["target", "side", "at"])?;
|
||||
let other = if from { c.to } else { c.from };
|
||||
if end.node().is_some() && end.node() == other.node() {
|
||||
return Err(bad(cmd, "connect two different layers"));
|
||||
@@ -141,6 +154,14 @@ pub(super) fn side_json(e: End) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a connector end sits on its layer, as the commands take it.
|
||||
pub(super) fn at_json(e: End) -> Value {
|
||||
match e {
|
||||
End::Node { at: Some(at), .. } => json!(at),
|
||||
_ => Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// The end of connector `id` that stays while its other one (`from`: its start) is dragged.
|
||||
pub(super) fn other_end(doc: &Document, id: NodeId, from: bool) -> Option<End> {
|
||||
match &doc.node(id)?.board {
|
||||
|
||||
@@ -297,7 +297,10 @@ pub fn route(doc: &Document, c: &Connector) -> Option<Vec<Point>> {
|
||||
pub(super) fn route_ends(doc: &Document, c: &Connector) -> Option<(RouteEnd, RouteEnd)> {
|
||||
let end = |e: &End| match e {
|
||||
End::Point { x, y } => Some(RouteEnd::Point(Point::new(*x, *y))),
|
||||
End::Node { node, side } => doc.world_bounds(*node).map(|b| RouteEnd::Box(b, *side)),
|
||||
End::Node { node, side, at } => doc.world_bounds(*node).map(|b| match at {
|
||||
Some([x, y]) => RouteEnd::On(b, *side, Point::new(b.x0 + x * b.width(), b.y0 + y * b.height())),
|
||||
None => RouteEnd::Box(b, *side),
|
||||
}),
|
||||
};
|
||||
Some((end(&c.from)?, end(&c.to)?))
|
||||
}
|
||||
|
||||
@@ -232,7 +232,12 @@ pub(super) fn add_connected(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => make_sticky(doc, parent, to_at, "", color, &author)?,
|
||||
Some(shape) => make_shape(doc, parent, to_at, next, shape, "", color)?,
|
||||
};
|
||||
let connector = Connector { from: End::Node { node: from, side: from_side }, to: End::Node { node: id, side: to_side }, routing, path: None };
|
||||
let connector = Connector {
|
||||
from: End::Node { node: from, side: from_side, at: None },
|
||||
to: End::Node { node: id, side: to_side, at: None },
|
||||
routing,
|
||||
path: None,
|
||||
};
|
||||
let line = make(doc, page, connector, CAPS)?;
|
||||
s.set_selection(vec![id]);
|
||||
if boolean(p, "edit") == Some(true) {
|
||||
|
||||
@@ -16,7 +16,7 @@ const MIN_RUN_PX: f64 = 28.0;
|
||||
/// `end` with the side it leaves by named: a reshaped path holds only as long as its ends do.
|
||||
fn pinned(end: End, side: Option<Side>) -> End {
|
||||
match (end, side) {
|
||||
(End::Node { node, .. }, Some(side)) => End::Node { node, side },
|
||||
(End::Node { node, at, .. }, Some(side)) => End::Node { node, side, at },
|
||||
_ => end,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::connectors::{CAPS, end_json, make, other_end, side_json};
|
||||
use super::connectors::{CAPS, at_json, end_json, make, other_end, side_json};
|
||||
use super::derived::{ends_now, route};
|
||||
use super::edit::label_of;
|
||||
use super::items::edit_text;
|
||||
@@ -10,6 +10,11 @@ use super::{
|
||||
/// How near (screen pixels) the pointer must be to the middle of an object's side for a connector
|
||||
/// end to attach there, however small the object is on screen.
|
||||
const ATTACH_PX: f64 = 10.0;
|
||||
|
||||
/// Point `p` as fractions of box `b` (`None`: a box with no area).
|
||||
fn part_at(b: figcraft_geom::Rect, p: Point) -> Option<[f64; 2]> {
|
||||
(b.width() > 0.0 && b.height() > 0.0).then(|| [((p.x - b.x0) / b.width()).clamp(0.0, 1.0), ((p.y - b.y0) / b.height()).clamp(0.0, 1.0)])
|
||||
}
|
||||
/// The sides an object takes a connector end at (their middles are its attach points).
|
||||
const SIDES: [Side; 4] = [Side::Top, Side::Right, Side::Bottom, Side::Left];
|
||||
|
||||
@@ -92,7 +97,8 @@ impl Session {
|
||||
}
|
||||
|
||||
/// What a connector end at the pointer attaches to: the object there (at the side whose middle
|
||||
/// is within [`ATTACH_PX`], else the side [`side_at`] the pointer), or the point.
|
||||
/// is within [`ATTACH_PX`], else the side [`side_at`] the pointer; with Cmd/Ctrl held, right
|
||||
/// there), or the point.
|
||||
fn end_at(&self, ev: &PointerEvent) -> End {
|
||||
let zoom = ev.zoom.max(1e-6);
|
||||
let hit = (|| {
|
||||
@@ -109,7 +115,10 @@ impl Session {
|
||||
let far = |s: &Side| b.map(|b| (s.point(b) - ev.p()).hypot() * zoom).filter(|d| *d <= ATTACH_PX);
|
||||
let near = SIDES.iter().filter_map(|s| Some((*s, far(s)?))).min_by(|x, y| x.1.total_cmp(&y.1)).map(|(s, _)| s);
|
||||
let side = near.or(b.map(|b| side_at(b, ev.p()))).unwrap_or(Side::Auto);
|
||||
Some(End::Node { node: t, side })
|
||||
match b.and_then(|b| part_at(b, ev.p())).filter(|_| ev.cmd) {
|
||||
Some(at) => Some(End::Node { node: t, side: Side::Auto, at: Some(at) }),
|
||||
None => Some(End::Node { node: t, side, at: None }),
|
||||
}
|
||||
})();
|
||||
hit.unwrap_or(End::Point { x: ev.x, y: ev.y })
|
||||
}
|
||||
@@ -128,7 +137,7 @@ impl Session {
|
||||
/// (`at`, else its named side's) lit.
|
||||
fn show_attach(&mut self, end: Option<End>, at: Option<Point>) {
|
||||
let attach = match end {
|
||||
Some(End::Node { node, side }) => self.doc().ok().and_then(|d| d.world_bounds(node)).map(|b| {
|
||||
Some(End::Node { node, side, .. }) => self.doc().ok().and_then(|d| d.world_bounds(node)).map(|b| {
|
||||
let on = at.or_else(|| (side != Side::Auto).then(|| side.point(b)));
|
||||
SIDES.map(|s| s.point(b)).map(|p| ([p.x, p.y], on.is_some_and(|o| (o - p).hypot() < 1e-6))).to_vec()
|
||||
}),
|
||||
@@ -179,7 +188,8 @@ impl Session {
|
||||
self.cancel_interaction()?;
|
||||
let Some(to) = self.drag_end(ev, from, start) else { return Ok(()) };
|
||||
let style = self.tool.board.routing.id();
|
||||
let p = json!({"from": end_json(from), "fromSide": side_json(from), "to": end_json(to), "toSide": side_json(to), "style": style});
|
||||
let (from_at, to_at) = (at_json(from), at_json(to));
|
||||
let p = json!({"from": end_json(from), "fromSide": side_json(from), "fromAt": from_at, "to": end_json(to), "toSide": side_json(to), "toAt": to_at, "style": style});
|
||||
self.execute("board.connector", &p)?;
|
||||
self.board_placed();
|
||||
Ok(())
|
||||
@@ -214,7 +224,7 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
let end = if from { "from" } else { "to" };
|
||||
self.execute("board.reattach", &json!({"id": id, "end": end, "target": end_json(target), "side": side_json(target)}))?;
|
||||
self.execute("board.reattach", &json!({"id": id, "end": end, "target": end_json(target), "side": side_json(target), "at": at_json(target)}))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//! A connector end placed on any part of an object: holding Cmd/Ctrl while moving it puts it right
|
||||
//! where the pointer is, and it stays at that part of the object when the object moves or resizes.
|
||||
|
||||
use figcraft_doc::{End, NodeId, Side};
|
||||
use figcraft_geom::Point;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
use crate::board_tests::{board, connector, drawn, id, near};
|
||||
use crate::tools::{PointerEvent, PointerKind};
|
||||
|
||||
/// A board with two 200 × 100 shapes and a straight connector between them: (a, b, connector).
|
||||
fn scene() -> (Session, NodeId, NodeId, NodeId) {
|
||||
let mut s = board();
|
||||
let mut shape = |x: f64| id(&s.execute("board.shape", &json!({"x": x, "y": 0, "width": 200, "height": 100})).unwrap(), "id");
|
||||
let (a, b) = (shape(0.0), shape(500.0));
|
||||
let k = id(&s.execute("board.connector", &json!({"from": a, "to": b, "style": "straight"})).unwrap(), "id");
|
||||
s.execute("select.set", &json!({"ids": [k]})).unwrap();
|
||||
(s, a, b, k)
|
||||
}
|
||||
|
||||
/// Drag the connector's end at `from` to `to`, with Cmd/Ctrl held or not.
|
||||
fn drag_end(s: &mut Session, from: Point, to: Point, cmd: bool) {
|
||||
let ev = |kind, p: Point| PointerEvent { cmd, ..PointerEvent::at(kind, p.x, p.y, 1.0) };
|
||||
s.pointer(&ev(PointerKind::Down, from)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, Point::new(300.0, 300.0))).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, to)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, to)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_puts_an_end_on_any_part_of_the_object() {
|
||||
let (mut s, _, b, k) = scene();
|
||||
let end = *drawn(&s, k).last().unwrap();
|
||||
drag_end(&mut s, end, Point::new(650.0, 25.0), true);
|
||||
assert_eq!(connector(&s, k).to, End::Node { node: b, side: Side::Auto, at: Some([0.75, 0.25]) });
|
||||
assert!(near(*drawn(&s, k).last().unwrap(), Point::new(650.0, 25.0)), "{:?}", drawn(&s, k));
|
||||
// It undoes as one step, back to the side.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(connector(&s, k).to, End::Node { node: b, side: Side::Auto, at: None });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_cmd_an_end_keeps_snapping_to_a_side() {
|
||||
let (mut s, _, b, k) = scene();
|
||||
let end = *drawn(&s, k).last().unwrap();
|
||||
drag_end(&mut s, end, Point::new(650.0, 25.0), false);
|
||||
assert_eq!(connector(&s, k).to, End::Node { node: b, side: Side::Right, at: None });
|
||||
assert!(near(*drawn(&s, k).last().unwrap(), Point::new(700.0, 50.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_end_stays_on_that_part_when_the_object_moves_or_resizes() {
|
||||
let (mut s, _, b, k) = scene();
|
||||
s.execute("board.reattach", &json!({"id": k, "end": "to", "target": b, "at": [0.5, 0.5]})).unwrap();
|
||||
assert!(near(*drawn(&s, k).last().unwrap(), Point::new(600.0, 50.0)));
|
||||
s.execute("node.set", &json!({"id": b, "props": {"x": 600, "y": 100, "width": 400, "height": 200}})).unwrap();
|
||||
assert!(near(*drawn(&s, k).last().unwrap(), Point::new(800.0, 200.0)), "{:?}", drawn(&s, k));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_end_moved_back_without_cmd_leaves_its_place() {
|
||||
let (mut s, _, b, k) = scene();
|
||||
s.execute("board.reattach", &json!({"id": k, "end": "to", "target": b, "at": [0.5, 0.5]})).unwrap();
|
||||
s.execute("board.reattach", &json!({"id": k, "end": "to", "target": b, "side": "top"})).unwrap();
|
||||
assert_eq!(connector(&s, k).to, End::Node { node: b, side: Side::Top, at: None });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_commands_take_a_place_and_refuse_junk() {
|
||||
let (mut s, a, b, _) = scene();
|
||||
let k = id(&s.execute("board.connector", &json!({"from": a, "to": b, "fromAt": [0.25, 0.5], "toAt": [9, -1]})).unwrap(), "id");
|
||||
assert!(matches!(connector(&s, k).from, End::Node { at: Some([0.25, 0.5]), .. }));
|
||||
assert!(matches!(connector(&s, k).to, End::Node { at: Some([1.0, 0.0]), .. }), "kept on the object");
|
||||
for junk in [json!("middle"), json!([1]), json!([1, 2, 3]), json!(["x", 0]), json!({"x": 0, "y": 0})] {
|
||||
assert!(s.execute("board.reattach", &json!({"id": k, "end": "to", "target": b, "at": junk})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reshaped_path_keeps_the_end_where_it_is() {
|
||||
let (mut s, a, b, _) = scene();
|
||||
let k = id(&s.execute("board.connector", &json!({"from": a, "to": b, "style": "curved", "toAt": [0.5, 0.5]})).unwrap(), "id");
|
||||
s.execute("board.reshape", &json!({"id": k, "portion": 0, "x": 350, "y": 200})).unwrap();
|
||||
assert!(matches!(connector(&s, k).to, End::Node { at: Some([0.5, 0.5]), .. }));
|
||||
assert!(near(*drawn(&s, k).last().unwrap(), Point::new(600.0, 50.0)), "{:?}", drawn(&s, k));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_saves_only_when_placed_by_hand() {
|
||||
let (mut s, _, b, k) = scene();
|
||||
let saved = |s: &Session| figcraft_format::to_json(s.doc().unwrap()).unwrap().to_string();
|
||||
assert!(!saved(&s).contains("\"at\":"), "older files stay as they are");
|
||||
s.execute("board.reattach", &json!({"id": k, "end": "to", "target": b, "at": [0.5, 0.25]})).unwrap();
|
||||
assert!(saved(&s).contains("\"at\":[0.5,0.25]"));
|
||||
let bytes = figcraft_format::save_bytes(s.doc().unwrap(), None).unwrap();
|
||||
let (open, _) = figcraft_format::load_bytes(&bytes).unwrap();
|
||||
let kept = open.node(k).and_then(|n| n.board.clone());
|
||||
assert!(matches!(kept, Some(figcraft_doc::BoardItem::Connector(c)) if matches!(c.to, End::Node { at: Some([0.5, 0.25]), .. })));
|
||||
}
|
||||
@@ -263,6 +263,8 @@ mod components_behaviour_tests;
|
||||
#[cfg(test)]
|
||||
mod components_scale_tests;
|
||||
#[cfg(test)]
|
||||
mod connector_anywhere_tests;
|
||||
#[cfg(test)]
|
||||
mod convert_container_tests;
|
||||
#[cfg(test)]
|
||||
mod cursor_tests;
|
||||
|
||||
Reference in New Issue
Block a user