//! Stroke panel reach: `stroke.set` on groups, what the panel shows for a selection (mixed values, //! Align Stroke), Units > Stroke, and the stroke options `document.inspect` reports. use serde_json::{Value, json}; use vectorcraft_doc::{NodeId, NodeKind, Unit}; use crate::*; fn session() -> Session { let mut s = Session::new(); s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); s } fn run(s: &mut Session, id: &str, p: Value) -> Value { s.execute(id, &p).unwrap_or_else(|e| panic!("{id}: {e}")) } fn rect(s: &mut Session, x: f64) -> NodeId { NodeId(run(s, "shape.rectangle", json!({"x": x, "y": 10, "width": 40, "height": 40}))["id"].as_u64().unwrap()) } fn select(s: &mut Session, ids: &[NodeId]) { run(s, "select.set", json!({ "ids": ids.iter().map(|i| i.0).collect::>() })); } #[test] fn stroke_set_on_a_group_skips_images_and_symbol_instances() { let mut s = session(); let (a, b, c) = (rect(&mut s, 10.0), rect(&mut s, 60.0), rect(&mut s, 110.0)); select(&mut s, &[b]); let image = NodeId(run(&mut s, "object.rasterize", json!({}))["id"].as_u64().unwrap()); select(&mut s, &[c]); let instance = NodeId(run(&mut s, "symbol.new", json!({"name": "Box"}))["id"].as_u64().unwrap()); select(&mut s, &[a, image, instance]); run(&mut s, "object.group", json!({})); run(&mut s, "stroke.set", json!({"weight": 5})); let d = &s.doc().unwrap().doc; assert_eq!(d.node(a).unwrap().appearance.stroke_width(), 5.0); for id in [image, instance] { let n = d.node(id).unwrap(); assert!(matches!(n.kind, NodeKind::Image(_) | NodeKind::SymbolInstance { .. })); assert!(n.appearance.stroke().is_none(), "{:?} gains no stroke", n.kind_label()); } } #[test] fn mixed_values_and_align_stroke_follow_the_selection() { let mut s = session(); let (a, b) = (rect(&mut s, 10.0), rect(&mut s, 60.0)); run(&mut s, "stroke.set", json!({"ids": [b.0], "weight": 3, "join": "round"})); select(&mut s, &[a, b]); let m = s.active().unwrap().stroke_mixed(); assert!(m.weight && m.join && !m.cap && !m.miter_limit && !m.align && !m.dash && m.can_align, "{m:?}"); // An open path in the selection can't take an inside or outside stroke; type can (its // characters' stroke goes around the glyphs). let line = NodeId(run(&mut s, "shape.line", json!({"x1": 10, "y1": 100, "x2": 90, "y2": 100}))["id"].as_u64().unwrap()); select(&mut s, &[a, line]); assert!(!s.active().unwrap().stroke_mixed().can_align); let text = NodeId(run(&mut s, "text.create", json!({"x": 10, "y": 200, "text": "Hi"}))["id"].as_u64().unwrap()); select(&mut s, &[text]); assert!(s.active().unwrap().stroke_mixed().can_align); select(&mut s, &[text, line]); assert!(!s.active().unwrap().stroke_mixed().can_align); // Grouped closed paths can. select(&mut s, &[a, b]); run(&mut s, "object.group", json!({})); let m = s.active().unwrap().stroke_mixed(); assert!(m.can_align && m.weight); assert_eq!(s.shown_stroke().map(|st| st.width), Some(1.0), "a group shows its first painted object's stroke"); } #[test] fn stroke_unit_follows_the_preference() { let mut s = session(); assert_eq!(s.stroke_unit(), Unit::Points); s.prefs.units_stroke = "millimeters".into(); assert_eq!(s.stroke_unit(), Unit::Millimeters); assert_eq!(s.stroke_unit().format(1.0), "0.3528 mm"); } #[test] fn inspect_reports_stroke_options() { let mut s = session(); let line = run(&mut s, "shape.line", json!({"x1": 10, "y1": 100, "x2": 90, "y2": 100}))["id"].as_u64().unwrap(); run( &mut s, "stroke.set", json!({"ids": [line], "cap": "round", "join": "bevel", "miterLimit": 4, "dash": [6, 3], "alignDashes": true, "endArrow": "Triangle", "arrowAlign": "tip", "profile": "lens"}), ); let doc = run(&mut s, "document.inspect", json!({})); let o = &doc["layers"][0]["children"][0]["strokeOptions"]; assert_eq!(o["cap"], "round"); assert_eq!(o["join"], "bevel"); assert_eq!(o["miterLimit"], 4.0); assert_eq!(o["align"], "center"); assert_eq!(o["dash"], json!([6.0, 3.0])); assert_eq!(o["alignDashes"], true); assert_eq!((o["startArrow"].clone(), o["endArrow"].clone(), o["arrowAlign"].clone()), (Value::Null, json!("Triangle"), json!("tip"))); assert_eq!(o["profile"], "lens"); // The options read back into stroke.set unchanged. let mut q = o.as_object().unwrap().clone(); q.insert("ids".into(), json!([line])); let before = s.doc().unwrap().doc.node(NodeId(line)).unwrap().appearance.clone(); run(&mut s, "stroke.set", Value::Object(q)); assert_eq!(s.doc().unwrap().doc.node(NodeId(line)).unwrap().appearance, before); // Type reports its characters' stroke. run(&mut s, "text.create", json!({"x": 10, "y": 200, "text": "Hi"})); run(&mut s, "stroke.set", json!({"join": "round"})); let doc = run(&mut s, "document.inspect", json!({})); let text = doc["layers"][0]["children"].as_array().unwrap().iter().find(|c| c["kind"] == "Text" || c.get("text").is_some()).unwrap().clone(); assert_eq!(text["strokeOptions"]["join"], "round", "{text}"); } /// Stroke Weight Up and Down (Cmd/Ctrl+Alt+. and ,) step each selected stroke from its own /// weight, a quarter point at a time below 1 pt and whole points above, in one undo step each. #[test] fn stroke_weight_up_and_down_step_each_selected_stroke() { let mut s = session(); let (a, b, bare) = (rect(&mut s, 10.0), rect(&mut s, 60.0), rect(&mut s, 110.0)); select(&mut s, &[a]); run(&mut s, "stroke.set", json!({"weight": 0.6})); select(&mut s, &[b]); run(&mut s, "stroke.set", json!({"weight": 3})); select(&mut s, &[bare]); run(&mut s, "paint.setStroke", json!({"none": true})); let has_stroke = |s: &Session| { s.doc().unwrap().doc.node(bare).unwrap().appearance.stroke().is_some_and(|st| !matches!(st.paint, vectorcraft_color::Paint::None)) }; let bare_before = has_stroke(&s); let w = |s: &Session, id| s.doc().unwrap().doc.node(id).unwrap().appearance.stroke_width(); select(&mut s, &[a, b, bare]); run(&mut s, "stroke.weightUp", json!({})); assert_eq!((w(&s, a), w(&s, b)), (0.75, 4.0)); run(&mut s, "stroke.weightUp", json!({})); assert_eq!((w(&s, a), w(&s, b)), (1.0, 5.0)); assert_eq!(has_stroke(&s), bare_before, "an object without a stroke takes none"); for _ in 0..6 { run(&mut s, "stroke.weightDown", json!({})); } // 1 → 0.75 → 0.5 → 0.25, where it stays; 5 → 4 → 3 → 2 → 1 → 0.75 → 0.5. assert_eq!((w(&s, a), w(&s, b)), (0.25, 0.5)); run(&mut s, "edit.undo", json!({})); assert_eq!((w(&s, a), w(&s, b)), (0.25, 0.75), "one undo step per press"); } /// With nothing selected the next object drawn takes the stepped weight. #[test] fn stroke_weight_up_with_nothing_selected_sets_up_new_art() { let mut s = session(); run(&mut s, "select.none", json!({})); run(&mut s, "stroke.set", json!({"weight": 2})); run(&mut s, "stroke.weightUp", json!({})); assert_eq!(s.paint.stroke_width, 3.0); let r = rect(&mut s, 10.0); assert_eq!(s.doc().unwrap().doc.node(r).unwrap().appearance.stroke_width(), 3.0); assert_eq!(crate::cmd::find_command("stroke.weightUp").unwrap().shortcut, Some("Cmd+Alt+.")); assert_eq!(crate::cmd::find_command("stroke.weightDown").unwrap().shortcut, Some("Cmd+Alt+,")); }