mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
469 lines
22 KiB
Rust
469 lines
22 KiB
Rust
//! Object › Intertwine (#1763), as a tutorial does it: two thick rings that overlap twice are
|
||
//! made into an Intertwine group, then one overlap is clicked so the rings pass over and under
|
||
//! each other. Driven through `Session::execute`; the art is checked on screen, in the saved file
|
||
//! and in the SVG and PDF output.
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_render::Rendered;
|
||
|
||
use crate::tests_clip::{hit, id_of, render, select, session};
|
||
use crate::*;
|
||
|
||
/// Ring A (red, centre 35,50) and ring B (blue, centre 65,50), radius 25, 8 pt strokes, no fill:
|
||
/// B is in front. The rings cross at (50, 30) and (50, 70).
|
||
fn rings(s: &mut Session) -> (NodeId, NodeId) {
|
||
let ring = |s: &mut Session, cx: f64, color: &str| {
|
||
let id = id_of(&s.execute("shape.ellipse", &json!({"x": cx - 25.0, "y": 25, "width": 50, "height": 50})).unwrap());
|
||
s.execute("paint.setFill", &json!({"ids": [id.0], "none": true})).unwrap();
|
||
s.execute("paint.setStroke", &json!({"ids": [id.0], "color": color})).unwrap();
|
||
s.execute("stroke.set", &json!({"ids": [id.0], "weight": 8})).unwrap();
|
||
id
|
||
};
|
||
let a = ring(s, 35.0, "#ff0000");
|
||
let b = ring(s, 65.0, "#0000ff");
|
||
(a, b)
|
||
}
|
||
|
||
/// The rings made into an Intertwine group: (group, a, b).
|
||
fn intertwined(s: &mut Session) -> (NodeId, NodeId, NodeId) {
|
||
let (a, b) = rings(s);
|
||
select(s, &[a, b]);
|
||
let g = id_of(&s.execute("object.intertwine.make", &json!({})).unwrap());
|
||
(g, a, b)
|
||
}
|
||
|
||
fn red(r: &Rendered, x: u32, y: u32) -> bool {
|
||
let p = r.pixel(x, y);
|
||
p[3] > 200 && p[0] > 200 && p[2] < 60
|
||
}
|
||
|
||
fn blue(r: &Rendered, x: u32, y: u32) -> bool {
|
||
let p = r.pixel(x, y);
|
||
p[3] > 200 && p[2] > 200 && p[0] < 60
|
||
}
|
||
|
||
fn arrange(s: &mut Session, p: Value) -> Value {
|
||
s.execute("object.intertwine.arrange", &p).unwrap()
|
||
}
|
||
|
||
fn area(s: &Session, id: NodeId) -> Option<vectorcraft_geom::PathData> {
|
||
s.doc().unwrap().doc.node(id).unwrap().extras().intertwine.as_deref().cloned()
|
||
}
|
||
|
||
#[test]
|
||
fn make_puts_the_objects_in_an_intertwine_group_drawn_as_before() {
|
||
let mut s = session();
|
||
let (g, a, b) = intertwined(&mut s);
|
||
let d = &s.doc().unwrap().doc;
|
||
let n = d.node(g).unwrap();
|
||
assert!(matches!(n.kind, NodeKind::Intertwine { .. }));
|
||
assert_eq!(n.kind_label(), "Intertwine");
|
||
assert_eq!(n.children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>(), [a, b], "the stacking order is kept");
|
||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||
let r = render(&s);
|
||
assert!(blue(&r, 50, 30) && blue(&r, 50, 70), "B is in front at both crossings until an area is arranged");
|
||
}
|
||
|
||
#[test]
|
||
fn clicking_an_overlap_brings_the_object_below_to_the_front_there_only() {
|
||
let mut s = session();
|
||
let (_, a, b) = intertwined(&mut s);
|
||
let v = arrange(&mut s, json!({"at": [50, 30]}));
|
||
assert_eq!(v["count"], 1);
|
||
assert_eq!(v["top"], json!([a.0]));
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 30), "A passes over B at the top crossing");
|
||
assert!(blue(&r, 50, 70), "and under it at the bottom one");
|
||
// Elsewhere each ring looks as it did.
|
||
assert!(red(&r, 10, 50) && blue(&r, 90, 50));
|
||
assert!(area(&s, a).is_some() && area(&s, b).is_none());
|
||
// A click there picks the ring drawn on top.
|
||
assert_eq!(hit(&s, 50.0, 30.0), Some(a));
|
||
assert_eq!(hit(&s, 50.0, 70.0), Some(b));
|
||
}
|
||
|
||
#[test]
|
||
fn clicking_the_same_overlap_again_swaps_back() {
|
||
let mut s = session();
|
||
let (_, a, b) = intertwined(&mut s);
|
||
arrange(&mut s, json!({"at": [50, 30]}));
|
||
let v = arrange(&mut s, json!({"at": [50, 30]}));
|
||
assert_eq!(v["top"], json!([b.0]));
|
||
assert!(area(&s, a).is_none() && area(&s, b).is_none(), "no area is left over");
|
||
assert!(blue(&render(&s), 50, 30));
|
||
}
|
||
|
||
#[test]
|
||
fn a_path_drawn_around_overlaps_arranges_every_overlap_inside_it() {
|
||
let mut s = session();
|
||
let (_, a, _) = intertwined(&mut s);
|
||
// Around the bottom crossing only.
|
||
let v = arrange(&mut s, json!({"area": [[40, 60], [60, 60], [60, 80], [40, 80]]}));
|
||
assert_eq!(v["count"], 1);
|
||
let r = render(&s);
|
||
assert!(blue(&r, 50, 30) && red(&r, 50, 70));
|
||
// Around both: each one swaps.
|
||
let v = arrange(&mut s, json!({"area": [[40, 20], [60, 20], [60, 80], [40, 80]]}));
|
||
assert_eq!(v["count"], 2);
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||
assert!(area(&s, a).is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn front_and_back_choose_the_member_where_three_objects_overlap() {
|
||
let mut s = session();
|
||
let (a, b) = rings(&mut s);
|
||
// A green bar across the top crossing, in front of both rings.
|
||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 46, "y": 20, "width": 8, "height": 20})).unwrap());
|
||
s.execute("paint.setFill", &json!({"ids": [c.0], "color": "#00ff00"})).unwrap();
|
||
s.execute("paint.setStroke", &json!({"ids": [c.0], "none": true})).unwrap();
|
||
select(&mut s, &[a, b, c]);
|
||
s.execute("object.intertwine.make", &json!({})).unwrap();
|
||
let green = |r: &Rendered| {
|
||
let p = r.pixel(50, 30);
|
||
p[1] > 200 && p[0] < 60 && p[2] < 60
|
||
};
|
||
assert!(green(&render(&s)));
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "front", "member": a.0}));
|
||
assert!(red(&render(&s), 50, 30), "A is brought to the front of all three");
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "back", "member": a.0}));
|
||
assert!(green(&render(&s)), "sent to the back, the others are over it again in their own order");
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "back"}));
|
||
assert!(blue(&render(&s), 50, 30), "back without a member sends the top one (C) back: B is on top");
|
||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 30], "order": "front", "member": 999})).unwrap_err();
|
||
assert!(e.to_string().contains("999"), "{e}");
|
||
}
|
||
|
||
/// The rings with a green bar across their top crossing, in front of both, made into an
|
||
/// Intertwine group: (group, a, b, c). A, B and C overlap at (50, 30); only A and B at (50, 70).
|
||
fn rings_and_bar(s: &mut Session) -> (NodeId, NodeId, NodeId, NodeId) {
|
||
let (a, b) = rings(s);
|
||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 46, "y": 20, "width": 8, "height": 20})).unwrap());
|
||
s.execute("paint.setFill", &json!({"ids": [c.0], "color": "#00ff00"})).unwrap();
|
||
s.execute("paint.setStroke", &json!({"ids": [c.0], "none": true})).unwrap();
|
||
select(s, &[a, b, c]);
|
||
let g = id_of(&s.execute("object.intertwine.make", &json!({})).unwrap());
|
||
(g, a, b, c)
|
||
}
|
||
|
||
/// The members of the overlap area of Intertwine group `g` at (x, y), in the order they are drawn
|
||
/// there, bottom first.
|
||
fn order_at(d: &Document, g: NodeId, x: f64, y: f64) -> Vec<NodeId> {
|
||
use vectorcraft_tools::intertwine::{drawn_order, face_at, faces};
|
||
let children = d.node(g).unwrap().children().unwrap();
|
||
let face = face_at(&faces(children), vectorcraft_geom::Point::new(x, y)).unwrap();
|
||
drawn_order(children, &face).into_iter().map(|i| children[i].id).collect()
|
||
}
|
||
|
||
/// Bring Forward and Send Backward move an object one place among the objects of one overlap
|
||
/// area, keeping the others' order, so every order of three objects can be set there (C, B, A
|
||
/// included); the other areas stay as they are.
|
||
#[test]
|
||
fn forward_and_backward_move_an_object_one_place_in_an_area() {
|
||
let mut s = session();
|
||
let (g, a, b, c) = rings_and_bar(&mut s);
|
||
let order = |s: &Session| order_at(&s.doc().unwrap().doc, g, 50.0, 30.0);
|
||
let v = arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": a.0}));
|
||
assert_eq!((&v["count"], &v["top"]), (&json!(1), &json!([c.0])));
|
||
assert_eq!(order(&s), [b, a, c]);
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "backward", "member": c.0}));
|
||
assert_eq!(order(&s), [b, c, a]);
|
||
assert!(red(&render(&s), 50, 30), "A is on top");
|
||
let v = arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": b.0}));
|
||
assert_eq!(v["top"], json!([a.0]));
|
||
assert_eq!(order(&s), [c, b, a], "the reverse of the group's order");
|
||
assert_eq!(hit(&s, 50.0, 30.0), Some(a), "a click picks the object drawn on top");
|
||
// Without a member: forward moves the object right below the top, backward the top one.
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "backward"}));
|
||
assert_eq!(order(&s), [c, a, b]);
|
||
assert!(blue(&render(&s), 50, 30));
|
||
assert_eq!(hit(&s, 50.0, 30.0), Some(b));
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "forward"}));
|
||
assert_eq!(order(&s), [c, b, a]);
|
||
// Bring to Front and Send to Back keep the order of the others.
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "front", "member": c.0}));
|
||
assert_eq!(order(&s), [b, a, c]);
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "back", "member": a.0}));
|
||
assert_eq!(order(&s), [a, b, c]);
|
||
// The crossing where only the rings overlap is as it was.
|
||
assert_eq!(order_at(&s.doc().unwrap().doc, g, 50.0, 70.0), [a, b]);
|
||
assert!(blue(&render(&s), 50, 70));
|
||
// There only the rings count: forward moves A over B, and C is not in that area.
|
||
let v = arrange(&mut s, json!({"at": [50, 70], "order": "forward"}));
|
||
assert_eq!(v["top"], json!([a.0]));
|
||
assert!(red(&render(&s), 50, 70));
|
||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 70], "order": "backward", "member": c.0})).unwrap_err();
|
||
assert!(e.to_string().contains("not in the overlap"), "{e}");
|
||
assert_eq!(order(&s), [a, b, c]);
|
||
}
|
||
|
||
/// An object already at the end it is moved to is not moved: no area changes and no undo step is
|
||
/// recorded. One move is one undo step, and undo, redo and a saved file keep the order.
|
||
#[test]
|
||
fn a_move_that_changes_nothing_records_nothing_and_orders_survive_undo_and_save() {
|
||
let mut s = session();
|
||
let (g, a, b, c) = rings_and_bar(&mut s);
|
||
let undo = |s: &Session| s.doc().unwrap().history.undo.len();
|
||
let before = undo(&s);
|
||
for (order, member) in [("forward", c), ("front", c), ("backward", a), ("back", a)] {
|
||
let v = arrange(&mut s, json!({"at": [50, 30], "order": order, "member": member.0}));
|
||
assert_eq!((&v["count"], &v["top"]), (&json!(0), &json!([])), "{order}");
|
||
}
|
||
assert_eq!(undo(&s), before, "nothing to undo");
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": b.0}));
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "backward", "member": c.0}));
|
||
arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": a.0}));
|
||
assert_eq!(undo(&s), before + 3, "one undo step a move");
|
||
let order = |s: &Session| order_at(&s.doc().unwrap().doc, g, 50.0, 30.0);
|
||
assert_eq!(order(&s), [c, b, a]);
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(order(&s), [c, a, b]);
|
||
s.execute("edit.redo", &json!({})).unwrap();
|
||
assert_eq!(order(&s), [c, b, a]);
|
||
let data = s.execute("document.export", &json!({"format": "vectorcraft"})).unwrap()["dataBase64"].clone();
|
||
let mut o = Session::new();
|
||
o.execute("document.open", &json!({"name": "rings.vectorcraft", "dataBase64": data})).unwrap();
|
||
assert_eq!(order_at(&o.doc().unwrap().doc, g, 50.0, 30.0), [c, b, a]);
|
||
assert!(red(&render(&o), 50, 30) && blue(&render(&o), 50, 70));
|
||
// Expand draws the same.
|
||
s.execute("object.expand", &json!({"fill": false, "stroke": false})).unwrap();
|
||
assert!(red(&render(&s), 50, 30) && blue(&render(&s), 50, 70));
|
||
}
|
||
|
||
/// Where three or more objects overlap, hovering highlights the outline of the object nearest to
|
||
/// the pointer.
|
||
#[test]
|
||
fn hovering_where_three_overlap_highlights_the_nearest_object() {
|
||
use kurbo::Shape;
|
||
let mut s = session();
|
||
let view = ViewInfo::default();
|
||
// Squares from x = 0, 20 and 40, 60 wide: all three overlap from x = 40 to 60.
|
||
let ids: Vec<NodeId> =
|
||
[0, 20, 40].iter().map(|x| id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 60, "height": 60})).unwrap())).collect();
|
||
select(&mut s, &ids);
|
||
s.execute("object.intertwine.make", &json!({})).unwrap();
|
||
let highlighted = |s: &mut Session, x: f64| {
|
||
s.pointer(&vectorcraft_tools::PointerEvent::new(vectorcraft_tools::PointerKind::Move, x, 30.0), view).unwrap();
|
||
let o = s.overlays(view);
|
||
let [vectorcraft_tools::Overlay::Path { path, .. }] = o.as_slice() else { panic!("one highlight: {o:?}") };
|
||
let r = path.bounding_box();
|
||
(r.x0, r.x1)
|
||
};
|
||
// Within the half point a 1 pt stroke reaches past each edge.
|
||
let near = |(x0, x1): (f64, f64), (e0, e1): (f64, f64)| (x0 - e0).abs() <= 0.6 && (x1 - e1).abs() <= 0.6;
|
||
// By the left edge of the third square, then by the right edge of the first one.
|
||
let h = highlighted(&mut s, 41.0);
|
||
assert!(near(h, (40.0, 100.0)), "{h:?}");
|
||
let h = highlighted(&mut s, 59.0);
|
||
assert!(near(h, (0.0, 60.0)), "{h:?}");
|
||
// Where two overlap, the area is highlighted.
|
||
let h = highlighted(&mut s, 30.0);
|
||
assert!(near(h, (20.0, 40.0)), "{h:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn release_gives_the_objects_back_as_they_were() {
|
||
let mut s = session();
|
||
let (g, a, b) = intertwined(&mut s);
|
||
arrange(&mut s, json!({"at": [50, 30]}));
|
||
let v = s.execute("object.intertwine.release", &json!({})).unwrap();
|
||
assert_eq!(v["ids"], json!([a.0, b.0]));
|
||
let d = &s.doc().unwrap().doc;
|
||
assert!(d.node(g).is_none());
|
||
assert!(d.node(a).unwrap().extras().intertwine.is_none());
|
||
assert!(blue(&render(&s), 50, 30));
|
||
// Undo brings the group back, arranged.
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert!(red(&render(&s), 50, 30));
|
||
}
|
||
|
||
/// #2540: Object › Expand turns the group into a plain group of what it draws (the rings, then the
|
||
/// part of A drawn over B as a clip group) in one undo step, looking the same; Ungroup then takes
|
||
/// it apart.
|
||
#[test]
|
||
fn expand_turns_the_group_into_plain_art_that_ungroups() {
|
||
let mut s = session();
|
||
let (g, a, b) = intertwined(&mut s);
|
||
arrange(&mut s, json!({"at": [50, 30]}));
|
||
assert_eq!(s.execute("object.expand.info", &json!({})).unwrap()["object"], json!(true));
|
||
let undo = s.doc().unwrap().history.undo.len();
|
||
s.execute("object.expand", &json!({"fill": false, "stroke": false})).unwrap();
|
||
let n = s.doc().unwrap().doc.node(g).unwrap().clone();
|
||
let NodeKind::Group { children, clip: false } = &n.kind else { panic!("a group: {}", n.kind_label()) };
|
||
assert_eq!(children.len(), 3, "the rings, then the part of A drawn over B");
|
||
assert_eq!((children[0].id, children[1].id), (a, b), "the rings keep their ids and order");
|
||
assert!(matches!(children[2].kind, NodeKind::Group { clip: true, .. }));
|
||
assert!(children.iter().all(|c| c.extras().intertwine.is_none()), "no area is left over");
|
||
let mut ids = vec![];
|
||
n.walk(&mut |c| ids.push(c.id));
|
||
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
||
assert!(unique.len() == ids.len() && !ids.contains(&NodeId(0)), "the generated art has ids of its own: {ids:?}");
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||
assert_eq!(s.execute("object.expand.info", &json!({})).unwrap()["object"], json!(false), "nothing live is left");
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 30) && blue(&r, 50, 70), "it looks as it did");
|
||
// Plain art: Ungroup takes it apart.
|
||
s.execute("object.ungroup", &json!({})).unwrap();
|
||
assert!(s.doc().unwrap().doc.node(g).is_none());
|
||
assert_eq!(s.doc().unwrap().selection.objects.len(), 3);
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||
// Undo brings the Intertwine group back.
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert!(matches!(s.doc().unwrap().doc.node(g).unwrap().kind, NodeKind::Intertwine { .. }));
|
||
}
|
||
|
||
/// Expand reaches an Intertwine group inside a selected group, and with every option on also
|
||
/// outlines the strokes of the rings and of the part drawn over the other one.
|
||
#[test]
|
||
fn expand_reaches_intertwine_groups_in_groups_and_outlines_their_strokes() {
|
||
let mut s = session();
|
||
let (g, ..) = intertwined(&mut s);
|
||
arrange(&mut s, json!({"at": [50, 30]}));
|
||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 5, "height": 5})).unwrap());
|
||
select(&mut s, &[g, c]);
|
||
let outer = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||
s.execute("object.expand", &json!({})).unwrap();
|
||
let (mut intertwines, mut strokes) = (0, 0);
|
||
s.doc().unwrap().doc.node(outer).unwrap().walk(&mut |n| {
|
||
intertwines += usize::from(matches!(n.kind, NodeKind::Intertwine { .. }));
|
||
strokes += usize::from(!n.appearance.stroke_paint().is_none());
|
||
});
|
||
assert_eq!((intertwines, strokes), (0, 0));
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 30) && blue(&r, 50, 70), "it looks as it did");
|
||
let info = s.execute("object.expand.info", &json!({})).unwrap();
|
||
assert_eq!((&info["object"], &info["stroke"]), (&json!(false), &json!(false)), "nothing is left to expand");
|
||
}
|
||
|
||
#[test]
|
||
fn the_arranged_areas_move_with_the_group() {
|
||
let mut s = session();
|
||
let (g, ..) = intertwined(&mut s);
|
||
arrange(&mut s, json!({"at": [50, 30]}));
|
||
select(&mut s, &[g]);
|
||
s.execute("object.move", &json!({"dx": 0, "dy": 10})).unwrap();
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 40) && blue(&r, 50, 80));
|
||
}
|
||
|
||
#[test]
|
||
fn the_woven_rings_survive_save_and_export() {
|
||
let mut s = session();
|
||
intertwined(&mut s);
|
||
arrange(&mut s, json!({"at": [50, 30]}));
|
||
for format in ["vectorcraft", "svg", "pdf"] {
|
||
let data = s.execute("document.export", &json!({"format": format})).unwrap()["dataBase64"].clone();
|
||
let mut o = Session::new();
|
||
o.execute("document.open", &json!({"name": format!("rings.{format}"), "dataBase64": data})).unwrap();
|
||
let r = render(&o);
|
||
assert!(red(&r, 50, 30) && blue(&r, 50, 70), "{format}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn what_cannot_be_intertwined_is_refused_with_a_reason() {
|
||
let mut s = session();
|
||
let (a, b) = rings(&mut s);
|
||
select(&mut s, &[a]);
|
||
let e = s.execute("object.intertwine.make", &json!({})).unwrap_err();
|
||
assert!(e.to_string().contains("two or more"), "{e}");
|
||
select(&mut s, &[a, b]);
|
||
let g = id_of(&s.execute("object.intertwine.make", &json!({})).unwrap());
|
||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 5, "height": 5})).unwrap());
|
||
let e = s.execute("object.intertwine.make", &json!({"ids": [g.0, c.0]})).unwrap_err();
|
||
assert!(e.to_string().contains("an Intertwine group can't") && !e.to_string().contains("already"), "{e}");
|
||
let e = s.execute("object.intertwine.make", &json!({"ids": [a.0, c.0]})).unwrap_err();
|
||
assert!(e.to_string().contains("already in an Intertwine group"), "{e}");
|
||
select(&mut s, &[g]);
|
||
let e = s.execute("object.intertwine.arrange", &json!({"at": [5, 50]})).unwrap_err();
|
||
assert!(e.to_string().contains("no overlap"), "{e}");
|
||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 30], "area": [[0, 0], [1, 0], [1, 1]]})).unwrap_err();
|
||
assert!(e.to_string().contains("either"), "{e}");
|
||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 30], "order": "sideways"})).unwrap_err();
|
||
assert!(e.to_string().contains("sideways"), "{e}");
|
||
let e = s.execute("object.intertwine.arrange", &json!({"area": [[0, 0], [1, 0]]})).unwrap_err();
|
||
assert!(e.to_string().contains("polygon"), "{e}");
|
||
let e = s.execute("object.intertwine.arrange", &json!({"id": c.0, "at": [1, 1]})).unwrap_err();
|
||
assert!(e.to_string().contains("not an Intertwine group"), "{e}");
|
||
}
|
||
|
||
// ---------- on the canvas ----------
|
||
|
||
fn press(s: &mut Session, pts: &[(f64, f64)], shift: bool) -> Vec<UiRequest> {
|
||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||
let view = ViewInfo::default();
|
||
let ev = |kind, (x, y): (f64, f64)| {
|
||
let mut e = PointerEvent::new(kind, x, y);
|
||
e.mods = Mods { shift, ..Mods::default() };
|
||
e
|
||
};
|
||
let (first, last) = (pts[0], pts[pts.len() - 1]);
|
||
s.pointer(&ev(PointerKind::Move, first), view).unwrap();
|
||
s.pointer(&ev(PointerKind::Down, first), view).unwrap();
|
||
for p in &pts[1..] {
|
||
s.pointer(&ev(PointerKind::Drag, *p), view).unwrap();
|
||
}
|
||
s.pointer(&ev(PointerKind::Up, last), view).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn make_chooses_the_intertwine_tool_and_a_click_on_an_overlap_arranges_it() {
|
||
let mut s = session();
|
||
let view = ViewInfo::default();
|
||
s.select_tool("ellipse", view).unwrap();
|
||
intertwined(&mut s);
|
||
assert_eq!(s.tool_id(), "intertwine");
|
||
// Hovering the overlap highlights it.
|
||
s.pointer(&vectorcraft_tools::PointerEvent::new(vectorcraft_tools::PointerKind::Move, 50.0, 30.0), view).unwrap();
|
||
assert_eq!(s.overlays(view).len(), 1);
|
||
press(&mut s, &[(50.0, 30.0)], false);
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||
// A click off any overlap changes nothing (and reports no error).
|
||
press(&mut s, &[(5.0, 5.0)], false);
|
||
assert!(red(&render(&s), 50, 30));
|
||
// Esc goes back to the tool chosen before Make.
|
||
let back = s.tool_key(vectorcraft_tools::ToolKey::Escape, Default::default(), view).unwrap();
|
||
assert_eq!(back, [UiRequest::SwitchTool("ellipse".into())]);
|
||
}
|
||
|
||
#[test]
|
||
fn a_drag_around_overlaps_arranges_them_and_shift_drags_a_rectangle() {
|
||
let mut s = session();
|
||
intertwined(&mut s);
|
||
// A loop around the bottom crossing.
|
||
press(&mut s, &[(40.0, 60.0), (60.0, 60.0), (60.0, 80.0), (40.0, 80.0), (40.0, 61.0)], false);
|
||
let r = render(&s);
|
||
assert!(blue(&r, 50, 30) && red(&r, 50, 70));
|
||
// A rectangle around both.
|
||
press(&mut s, &[(40.0, 20.0), (50.0, 50.0), (60.0, 80.0)], true);
|
||
let r = render(&s);
|
||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||
}
|
||
|
||
#[test]
|
||
fn edit_chooses_the_tool_for_the_group_of_a_selected_member() {
|
||
let mut s = session();
|
||
let (g, a, _) = intertwined(&mut s);
|
||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||
select(&mut s, &[a]);
|
||
let v = s.execute("object.intertwine.edit", &json!({})).unwrap();
|
||
assert_eq!(v["id"], g.0);
|
||
assert_eq!(s.tool_id(), "intertwine");
|
||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||
}
|
||
|
||
#[test]
|
||
fn arrange_with_explicit_id_works_without_active_selection() {
|
||
let mut s = session();
|
||
let (g, _, _) = intertwined(&mut s);
|
||
s.execute("select.none", &json!({})).unwrap();
|
||
assert!(s.doc().unwrap().selection.is_empty());
|
||
let out = s.execute("object.intertwine.arrange", &json!({"id": g.0, "at": [50.0, 30.0]})).unwrap();
|
||
assert_eq!(out["count"], 1);
|
||
}
|