vectorcraft/crates/engine/src/tests_envelope_edit.rs

137 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Editing envelopes on the canvas: Edit Contents hit-testing, mesh points and handles.
use serde_json::{Value, json};
use vectorcraft_doc::hit::{HitOptions, hit_test};
use vectorcraft_doc::live::{self, EnvelopeMap};
use vectorcraft_doc::{EnvelopeKind, Node, NodeKind};
use vectorcraft_geom::{Affine, Point, Vec2};
use vectorcraft_tools::{PointerEvent, PointerKind};
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
s
}
fn id_of(v: &Value) -> NodeId {
NodeId(v["id"].as_u64().unwrap())
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).unwrap().clone()
}
/// A 200×100 rectangle at (100, 100) enveloped by `cmd` → (rectangle, envelope).
fn enveloped(s: &mut Session, cmd: &str, params: Value) -> (NodeId, NodeId) {
let a = id_of(&s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 100})).unwrap());
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
(a, id_of(&s.execute(cmd, &params).unwrap()))
}
fn mesh(s: &Session, e: NodeId) -> (u32, u32, Vec<Point>, Vec<[Vec2; 4]>) {
match node(s, e).kind {
NodeKind::Envelope { kind: EnvelopeKind::Mesh { rows, cols, points, handles }, .. } => (rows, cols, points, handles),
k => panic!("{k:?}"),
}
}
#[test]
fn edit_contents_clicks_reach_the_content_where_it_is() {
let mut s = session();
let (a, e) = enveloped(&mut s, "object.envelope.makeWithWarp", json!({"style": "arch", "bend": 50}));
let doc = |s: &Session| s.doc().unwrap().doc.clone();
// Inside the rectangle near its bottom middle, which the arch lifted away.
let below = Point::new(200.0, 195.0);
let h = hit_test(&doc(&s), below, HitOptions::default()).unwrap();
assert_eq!((h.leaf, h.top_object(None), h.contents_of), (e, e, None), "the envelope is one object");
s.execute("object.envelope.editContents", &json!({"editing": true})).unwrap();
let h = hit_test(&doc(&s), below, HitOptions::default()).unwrap();
assert_eq!((h.leaf, h.top_object(None), h.contents_of), (a, a, Some(e)), "Edit Contents: the content is the object clicked");
// The Selection tool picks it.
s.execute("select.none", &json!({})).unwrap();
s.select_tool("selection", ViewInfo::default()).unwrap();
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 200.0, 195.0), ViewInfo::default()).unwrap();
}
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
}
#[test]
fn mesh_handles_start_from_the_smooth_mesh() {
let mut s = session();
let (_, e) = enveloped(&mut s, "object.envelope.makeWithMesh", json!({"rows": 2, "cols": 2}));
s.execute("object.envelope.setMeshPoint", &json!({"id": e.0, "index": 4, "x": 210, "y": 130})).unwrap();
assert!(mesh(&s, e).3.is_empty(), "moving a point keeps the smooth mesh");
let smooth = |s: &Session, u: f64, v: f64| EnvelopeMap::of(&node(s, e)).unwrap().at(u, v);
let before: Vec<Point> = (0..=8).flat_map(|i| [(i as f64 / 8.0, 0.5), (0.5, i as f64 / 8.0)]).map(|(u, v)| smooth(&s, u, v)).collect();
// Placing a handle where it already is fills in the handles: the grid lines don't move.
let (rows, cols, points, _) = mesh(&s, e);
let h = live::smooth_handles(rows, cols, &points)[4][live::H_RIGHT];
let end = points[4] + h;
s.execute("object.envelope.setMeshPoint", &json!({"id": e.0, "index": 4, "x": end.x, "y": end.y, "handle": 0})).unwrap();
assert_eq!(mesh(&s, e).3.len(), 9);
let after: Vec<Point> = (0..=8).flat_map(|i| [(i as f64 / 8.0, 0.5), (0.5, i as f64 / 8.0)]).map(|(u, v)| smooth(&s, u, v)).collect();
for (p, q) in before.iter().zip(&after) {
assert!(p.distance(*q) < 1e-6, "{p:?} vs {q:?}");
}
// Now the handle bends the row line up right of the point.
let y0 = smooth(&s, 0.62, 0.5).y;
s.execute("object.envelope.setMeshPoint", &json!({"id": e.0, "index": 4, "x": end.x, "y": end.y - 40.0, "handle": 0})).unwrap();
assert!(smooth(&s, 0.62, 0.5).y < y0 - 1.0, "{} vs {y0}", smooth(&s, 0.62, 0.5).y);
assert!(s.execute("object.envelope.setMeshPoint", &json!({"id": e.0, "index": 4, "x": 0, "y": 0, "handle": 7})).is_err());
}
#[test]
fn mesh_commands_edit_mesh_envelopes() {
let mut s = session();
let (_, e) = enveloped(&mut s, "object.envelope.makeWithMesh", json!({"rows": 1, "cols": 1}));
let r = s.execute("object.mesh.addLine", &json!({"id": e.0, "x": 150, "y": 175})).unwrap();
let (rows, cols, points, handles) = mesh(&s, e);
assert_eq!((rows, cols, handles.len()), (2, 2, 9));
let i = r["index"].as_u64().unwrap() as usize;
assert!(points[i].distance(Point::new(150.0, 175.0)) < 0.5);
s.execute("object.mesh.movePoint", &json!({"id": e.0, "index": i, "x": 160, "y": 160})).unwrap();
assert!(mesh(&s, e).2[i].distance(Point::new(160.0, 160.0)) < 1e-9);
s.execute("object.mesh.movePoint", &json!({"id": e.0, "index": i, "x": 200, "y": 160, "handle": 0})).unwrap();
assert!((mesh(&s, e).3[i][0] - Vec2::new(40.0, 0.0)).hypot() < 1e-9);
s.execute("object.mesh.deletePoint", &json!({"id": e.0, "index": i})).unwrap();
assert_eq!(mesh(&s, e).0, 1);
assert!(s.execute("object.mesh.setPointColor", &json!({"id": e.0, "index": 0, "color": "#ff0000"})).is_err(), "envelope points have no colour");
// One undo step per edit.
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(mesh(&s, e).0, 2);
}
#[test]
fn mesh_handles_turn_with_the_envelope_and_round_trip() {
let mut s = session();
let (_, e) = enveloped(&mut s, "object.envelope.makeWithMesh", json!({"rows": 1, "cols": 1}));
s.execute("object.envelope.setMeshPoint", &json!({"id": e.0, "index": 0, "x": 150, "y": 100, "handle": 0})).unwrap();
s.execute("object.rotate", &json!({"angle": 90, "origin": [200, 150]})).unwrap();
let h = mesh(&s, e).3[0][0];
let want = Affine::rotate(-std::f64::consts::FRAC_PI_2) * Point::new(50.0, 0.0);
assert!((h - want.to_vec2()).hypot() < 1e-9, "{h:?}");
let d = &s.doc().unwrap().doc;
let back = vectorcraft_format::load(&vectorcraft_format::save(d, false)).unwrap();
assert_eq!(back.node(e), d.node(e));
// Mesh envelopes saved before handles existed load as smooth meshes.
let mut old = serde_json::to_value(node(&s, e)).unwrap();
old["kind"]["kind"].as_object_mut().unwrap().remove("handles");
let n: Node = serde_json::from_value(old).unwrap();
assert!(matches!(n.kind, NodeKind::Envelope { kind: EnvelopeKind::Mesh { handles, .. }, .. } if handles.is_empty()));
}
#[test]
fn reset_with_mesh_keeps_the_warp_shape_along_the_grid() {
let mut s = session();
let (_, e) = enveloped(&mut s, "object.envelope.makeWithWarp", json!({"style": "arc", "bend": 60}));
let warp = EnvelopeMap::of(&node(&s, e)).unwrap().surface_mesh(2, 2, vectorcraft_color::Color::BLACK);
s.execute("object.envelope.resetWithMesh", &json!({"rows": 2, "cols": 2})).unwrap();
let (_, _, points, handles) = mesh(&s, e);
for (q, (p, h)) in warp.points.iter().zip(points.iter().zip(&handles)) {
assert!(q.p.distance(*p) < 1e-9 && q.handles == *h);
}
}