//! xfig's object edit panel as commands (W10 group 3): geometry rows, //! text flags, arc type, arrow thickness, comments of a compound. #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use rustfig_engine::Engine; use rustfig_model::{ObjectKind, SplineKind}; use serde_json::json; const FIG: &str = "#FIG 3.2\nLandscape\nCenter\nInches\nLetter\n100.00\nSingle\n-2\n1200 2\n\ 2 2 0 1 0 7 50 -1 -1 0.000 0 0 -1 0 0 5\n\t 0 0 600 0 600 600 0 600 0 0\n\ 1 3 0 1 0 7 50 -1 -1 0.000 1 0.0000 3000 3000 500 500 3000 3000 3500 3500\n\ 2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 0 0 3\n\t 0 0 100 100 200 0\n\ 5 1 0 1 0 7 50 -1 -1 0.000 0 1 0 0 1000 0 1500 0 1000 500 500 0\n\ 3 2 0 1 0 7 50 -1 -1 0.000 0 0 0 4\n\t 0 400 100 500 200 400 300 500\n\t 0.0 -1.0 -1.0 0.0\n\ 4 0 0 50 -1 0 12 0.0000 4 150 600 5000 5000 Hi\\001\n\ 6 0 0 600 600\n2 2 0 1 0 7 50 -1 -1 0.000 0 0 -1 0 0 5\n\t 0 0 600 0 600 600 0 600 0 0\n-6\n"; fn engine() -> Engine { let mut e = Engine::new(); e.execute("file.open_data", &json!({"data": FIG})).unwrap(); e } fn obj<'a>(e: &'a Engine, ty: &str) -> &'a rustfig_model::Object { e.doc() .objects .iter() .find(|o| o.type_name() == ty) .map(|o| &**o) .unwrap() } fn id_of(e: &Engine, ty: &str) -> u64 { obj(e, ty).id.0 } #[test] fn box_corners_are_set_in_xfigs_vertex_order() { let mut e = engine(); let id = id_of(&e, "box"); e.execute( "object.set_geometry", &json!({"id": id, "points": [[100, 200], [700, 900]]}), ) .unwrap(); let ObjectKind::Polyline(p) = &obj(&e, "box").kind else { panic!() }; let v: Vec<(f64, f64)> = p.points.iter().map(|q| (q.x, q.y)).collect(); assert_eq!(v, vec![(100.0, 200.0), (700.0, 200.0), (700.0, 900.0), (100.0, 900.0)]); } #[test] fn circle_by_diameter_keeps_start_and_end_like_xfig() { let mut e = engine(); let id = id_of(&e, "circle"); e.execute( "object.set_geometry", &json!({"id": id, "points": [[1000, 1000]], "radii": [300, 999]}), ) .unwrap(); let ObjectKind::Ellipse(el) = &obj(&e, "circle").kind else { panic!() }; assert_eq!((el.center.x, el.radii.x, el.radii.y), (1000.0, 300.0, 300.0)); assert_eq!((el.end.x, el.end.y), (1300.0, 1000.0)); } #[test] fn polyline_point_count_cannot_change_and_failure_changes_nothing() { let mut e = engine(); let id = id_of(&e, "polyline"); let before = obj(&e, "polyline").clone(); assert!(e .execute("object.set_geometry", &json!({"id": id, "points": [[0, 0], [1, 1]]})) .is_err()); assert_eq!(*obj(&e, "polyline"), before); e.execute( "object.set_geometry", &json!({"id": id, "points": [[0, 0], [5, 5], [9, 9]]}), ) .unwrap(); let ObjectKind::Polyline(p) = &obj(&e, "polyline").kind else { panic!() }; assert_eq!(p.points[2].x, 9.0); } #[test] fn collinear_arc_points_are_refused() { let mut e = engine(); let id = id_of(&e, "arc"); assert!(e .execute( "object.set_geometry", &json!({"id": id, "points": [[0, 0], [1, 1], [2, 2]]}) ) .is_err()); e.execute("object.set_attributes", &json!({"ids": [id], "pie": true})) .unwrap(); let ObjectKind::Arc(a) = &obj(&e, "arc").kind else { panic!() }; assert!(a.pie); } #[test] fn spline_shape_factor_turns_an_interpolated_spline_into_an_xspline() { let mut e = engine(); let id = id_of(&e, "spline"); e.execute( "object.set_geometry", &json!({"id": id, "shapes": [0.0, 0.3, -1.0, 0.0]}), ) .unwrap(); let ObjectKind::Spline(s) = &obj(&e, "spline").kind else { panic!() }; assert_eq!(s.kind, SplineKind::X); assert_eq!(s.shapes, vec![0.0, 0.3, -1.0, 0.0]); } #[test] fn text_flags_origin_and_compound_comment() { let mut e = engine(); let t = id_of(&e, "text"); e.execute( "object.set_attributes", &json!({"ids": [t], "rigid": true, "special": true, "hidden": false}), ) .unwrap(); e.execute("object.set_geometry", &json!({"id": t, "points": [[111, 222]]})) .unwrap(); let ObjectKind::Text(x) = &obj(&e, "text").kind else { panic!() }; assert!(x.flags & 1 != 0 && x.flags & 2 != 0); assert_eq!((x.origin.x, x.origin.y), (111.0, 222.0)); let c = id_of(&e, "compound"); e.execute("object.set_attributes", &json!({"ids": [c], "comment": "group note"})) .unwrap(); let o = obj(&e, "compound"); assert_eq!(o.comments, vec!["group note".to_string()]); let ObjectKind::Compound(cc) = &o.kind else { panic!() }; assert!(cc.objects[0].comments.is_empty()); } #[test] fn attributes_and_geometry_in_one_undo_step() { let mut e = engine(); let id = id_of(&e, "box"); let before = obj(&e, "box").clone(); e.execute( "object.set_attributes", &json!({"ids": [id], "thickness": 7, "geometry": {"points": [[0, 0], [50, 50]]}}), ) .unwrap(); assert_ne!(*obj(&e, "box"), before); e.execute("edit.undo", &json!({})).unwrap(); assert_eq!(*obj(&e, "box"), before); } fn press(e: &mut Engine, x: f64, y: f64, button: &str) { e.execute( "tool.event", &json!({"type": "press", "x": x, "y": y, "button": button}), ) .unwrap(); e.execute( "tool.event", &json!({"type": "release", "x": x, "y": y, "button": button}), ) .unwrap(); } #[test] fn edit_mode_buttons_follow_xfig() { use rustfig_engine::UiRequest; let mut e = engine(); e.execute("tool.edit", &json!({})).unwrap(); // Middle button: the whole figure's comments, wherever it is. press(&mut e, 9000.0, 9000.0, "middle"); assert_eq!(e.take_requests(), vec![UiRequest::EditFigureComments]); // Right button on an interior spline point: its shape-factor panel. let sid = obj(&e, "spline").id; press(&mut e, 100.0, 500.0, "right"); assert_eq!( e.take_requests(), vec![UiRequest::EditSplinePoint { id: sid, index: 1 }] ); // On an end point or a non-spline point: a message, no panel. press(&mut e, 0.0, 400.0, "right"); assert!(e.take_requests().is_empty()); assert!(e.status().contains("end-points")); press(&mut e, 600.0, 600.0, "right"); assert!(e.take_requests().is_empty()); assert!(e.status().contains("Only spline")); } #[test] fn degenerate_geometry_survives_save_and_open() { let mut e = engine(); for ty in ["box", "polyline", "circle", "spline", "arc"] { let id = id_of(&e, ty); let pts = match ty { "polyline" => json!([[0, 0], [0, 0], [0, 0]]), "spline" => json!([[0, 0], [0, 0], [0, 0], [0, 0]]), "arc" => json!([[0, 0], [0, 0], [0, 0]]), "circle" => json!([[0, 0]]), _ => json!([[0, 0], [0, 0]]), }; let _ = e.execute( "object.set_geometry", &json!({"id": id, "points": pts, "radii": [0, 0]}), ); } let n = e.doc().objects.len(); assert!(e .execute( "object.set_geometry", &json!({"id": id_of(&e, "box"), "points": [[5, 5], [5, 9]]}) ) .is_err()); let data = e.execute("file.save_data", &json!({})).unwrap()["data"] .as_str() .unwrap() .to_string(); let mut e2 = Engine::new(); e2.execute("file.open_data", &json!({"data": data})).unwrap(); assert_eq!(e2.doc().objects.len(), n); } fn first_poly_points(e: &Engine) -> Vec<(f64, f64)> { let ObjectKind::Compound(c) = &obj(e, "compound").kind else { panic!() }; let ObjectKind::Polyline(p) = &c.objects[0].kind else { panic!() }; p.points.iter().map(|q| (q.x, q.y)).collect() } #[test] fn compound_corners_reposition_and_rescale_like_xfig() { let mut e = engine(); let id = id_of(&e, "compound"); // Move the top-left corner: the compound keeps its size. e.execute( "object.set_geometry", &json!({"id": id, "points": [[1000, 2000], [1600, 2600]]}), ) .unwrap(); let v = first_poly_points(&e); // (the corners are the geometry's, without line width) assert!((v.iter().map(|q| q.0).fold(f64::MAX, f64::min) - 1000.0).abs() < 1.0); assert!((v.iter().map(|q| q.1).fold(f64::MAX, f64::min) - 2000.0).abs() < 1.0); // Rescale about the top-left corner (width and height, Return). e.execute( "object.set_geometry", &json!({"id": id, "points": [[1000, 2000], [2200, 2600]]}), ) .unwrap(); let v = first_poly_points(&e); let max_x = v.iter().map(|q| q.0).fold(f64::MIN, f64::max); let max_y = v.iter().map(|q| q.1).fold(f64::MIN, f64::max); assert!((max_x - 2200.0).abs() < 40.0, "{max_x}"); assert!((max_y - 2600.0).abs() < 40.0, "{max_y}"); // A zero size is refused and leaves the document alone. assert!(e .execute( "object.set_geometry", &json!({"id": id, "points": [[1000, 2000], [1000, 2600]]}) ) .is_err()); assert!(e.execute("object.set_geometry", &json!({"id": id})).is_err()); assert_eq!(first_poly_points(&e), v); } #[test] fn compound_texts_and_collapsed_depths() { let mut e = Engine::new(); e.execute( "file.open_data", &json!({"data": "#FIG 3.2\nLandscape\nCenter\nInches\nLetter\n100.00\nSingle\n-2\n1200 2\n\ 6 0 0 3000 3000\n4 0 0 40 -1 0 12 0.0000 4 150 600 100 500 One\\001\n\ 4 0 0 60 -1 0 12 0.0000 4 150 600 100 900 Two\\001\n\ 2 2 0 1 0 7 70 -1 -1 0.000 0 0 -1 0 0 5\n\t 0 0 600 0 600 600 0 600 0 0\n-6\n"}), ) .unwrap(); let id = id_of(&e, "compound"); e.execute( "object.set_attributes", &json!({"ids": [id], "texts": ["Uno", "Dos"], "depth": 40}), ) .unwrap(); let ObjectKind::Compound(c) = &obj(&e, "compound").kind else { panic!() }; let texts: Vec<&str> = c .objects .iter() .filter_map(|o| match &o.kind { ObjectKind::Text(t) => Some(t.text.as_str()), _ => None, }) .collect(); assert_eq!(texts, ["Uno", "Dos"]); assert!(c.objects.iter().all(|o| o.depth() == 40)); assert!(e .execute("object.set_attributes", &json!({"ids": [id], "texts": [1]})) .is_err()); } #[test] fn point_table_resizes_lines_and_splines() { let mut e = engine(); let id = id_of(&e, "polyline"); // The count may only change with `resize`. let pts = json!([[0, 0], [100, 100], [150, 50], [200, 0]]); assert!(e .execute("object.set_geometry", &json!({"id": id, "points": pts})) .is_err()); e.execute("object.set_geometry", &json!({"id": id, "points": pts, "resize": true})) .unwrap(); let ObjectKind::Polyline(p) = &obj(&e, "polyline").kind else { panic!() }; assert_eq!(p.points.len(), 4); let sid = id_of(&e, "spline"); e.execute( "object.set_geometry", &json!({"id": sid, "points": [[0, 400], [100, 500], [250, 450], [200, 400], [300, 500]], "shapes": [0.0, -1.0, 1.0, -1.0, 0.0], "resize": true}), ) .unwrap(); let ObjectKind::Spline(s) = &obj(&e, "spline").kind else { panic!() }; assert_eq!((s.points.len(), s.shapes.len()), (5, 5)); assert_eq!(s.shapes[2], 1.0); // Without shapes the new points get the kind's own factor. e.execute( "object.set_geometry", &json!({"id": sid, "points": [[0, 400], [100, 500], [300, 500]], "resize": true}), ) .unwrap(); let ObjectKind::Spline(s) = &obj(&e, "spline").kind else { panic!() }; assert_eq!((s.points.len(), s.shapes.len()), (3, 3)); // Too few points are refused. assert!(e .execute( "object.set_geometry", &json!({"id": sid, "points": [[0, 400]], "resize": true}) ) .is_err()); } .