mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Make and Edit choose the Intertwine tool: hovering highlights the overlap area under the pointer, a click swaps which object is on top there, and a path drawn around overlaps (Shift: a rectangle) arranges every overlap inside it; Esc or Enter goes back to the tool chosen before. The overlap faces move to vectorcraft-tools, shared by the tool and the command. Clicks inside a raised area pick the member drawn on top there, the Object menu gets the Intertwine submenu, and the new labels and the predicate message are translated in the complete catalogs. docs/gaps.md records what landed under G13.
271 lines
11 KiB
Rust
271 lines
11 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 super::tests_clip::{hit, id_of, render, select, session};
|
||
use super::*;
|
||
|
||
/// 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}");
|
||
}
|
||
|
||
#[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));
|
||
}
|
||
|
||
#[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]);
|
||
}
|