diff --git a/crates/doc/src/para.rs b/crates/doc/src/para.rs index 1266b7d4f..9add66c65 100644 --- a/crates/doc/src/para.rs +++ b/crates/doc/src/para.rs @@ -311,7 +311,14 @@ pub enum ShapeKind { RightTriangle, Diamond, Line, + /// A block arrow pointing right (`rightArrow`; also stands for the double-headed arrows). Arrow, + /// A block arrow pointing left (`leftArrow`). + ArrowLeft, + /// A block arrow pointing up (`upArrow`). + ArrowUp, + /// A block arrow pointing down (`downArrow`). + ArrowDown, Star, Heart, /// A text box (rectangle with a text story). diff --git a/crates/doc/src/wrap.rs b/crates/doc/src/wrap.rs index add3b5d32..1c235d73e 100644 --- a/crates/doc/src/wrap.rs +++ b/crates/doc/src/wrap.rs @@ -92,11 +92,7 @@ pub fn shape_outline(kind: ShapeKind, w: f32, h: f32, corner: Option) -> Op ShapeKind::Triangle => vec![(0.5, 0.0), (1.0, 1.0), (0.0, 1.0)], ShapeKind::RightTriangle => vec![(0.0, 0.0), (1.0, 1.0), (0.0, 1.0)], ShapeKind::Diamond => vec![(0.5, 0.0), (1.0, 0.5), (0.5, 1.0), (0.0, 0.5)], - ShapeKind::Arrow => { - // As drawn: the head is half the height long, the shaft 40% of the height thick. - let head = if w > 0.0 { (0.5 * h / w).min(1.0) } else { 0.5 }; - vec![(0.0, 0.3), (1.0 - head, 0.3), (1.0 - head, 0.0), (1.0, 0.5), (1.0 - head, 1.0), (1.0 - head, 0.7), (0.0, 0.7)] - } + ShapeKind::Arrow | ShapeKind::ArrowLeft | ShapeKind::ArrowUp | ShapeKind::ArrowDown => arrow_outline(kind, w, h), ShapeKind::Star => (0..10) .map(|i| { let a = PI * i as f32 / 5.0 - FRAC_PI_2; @@ -126,6 +122,26 @@ pub fn shape_outline(kind: ShapeKind, w: f32, h: f32, corner: Option) -> Op Some(pts) } +/// A block arrow's outline in unit coordinates (0..1 across a `w` × `h` frame), pointing the way +/// `kind` says (right for anything but the left, up and down arrows). The head is half the arrow's +/// thickness long, the shaft 40% of the thickness thick. +pub fn arrow_outline(kind: ShapeKind, w: f32, h: f32) -> Vec<(f32, f32)> { + let vertical = matches!(kind, ShapeKind::ArrowUp | ShapeKind::ArrowDown); + let (len, thick) = if vertical { (h, w) } else { (w, h) }; + let head = if len > 0.0 { (0.5 * thick / len).min(1.0) } else { 0.5 }; + // Pointing right: `u` runs along the arrow from its tail, `v` across it. + let right = [(0.0, 0.3), (1.0 - head, 0.3), (1.0 - head, 0.0), (1.0, 0.5), (1.0 - head, 1.0), (1.0 - head, 0.7), (0.0, 0.7)]; + right + .iter() + .map(|&(u, v)| match kind { + ShapeKind::ArrowLeft => (1.0 - u, v), + ShapeKind::ArrowUp => (v, 1.0 - u), + ShapeKind::ArrowDown => (v, u), + _ => (u, v), + }) + .collect() +} + /// Largest side of the grid [`alpha_outline`] looks at. pub const ALPHA_GRID: usize = 48; @@ -214,6 +230,16 @@ mod tests { assert!(WrapPolygon::from_unit(false, &[(f32::NAN, 0.0), (0.0, 0.0), (1.0, 0.0), (0.5, 1.0)]).is_some_and(|p| p.points.len() == 3)); } + /// #3919: the tip of each block arrow lies on the side it points to. + #[test] + fn arrow_outlines_point_the_right_way() { + let tip = |kind| shape_outline(kind, 100.0, 100.0, None).unwrap()[3]; + assert_eq!(tip(ShapeKind::Arrow), (1.0, 0.5)); + assert_eq!(tip(ShapeKind::ArrowLeft), (0.0, 0.5)); + assert_eq!(tip(ShapeKind::ArrowUp), (0.5, 0.0)); + assert_eq!(tip(ShapeKind::ArrowDown), (0.5, 1.0)); + } + #[test] fn shape_outlines_stay_in_the_frame() { for kind in [ @@ -223,6 +249,9 @@ mod tests { ShapeKind::RightTriangle, ShapeKind::Diamond, ShapeKind::Arrow, + ShapeKind::ArrowLeft, + ShapeKind::ArrowUp, + ShapeKind::ArrowDown, ShapeKind::Star, ShapeKind::Heart, ] { diff --git a/crates/docbin/src/drawing.rs b/crates/docbin/src/drawing.rs index 43e351189..461d6b372 100644 --- a/crates/docbin/src/drawing.rs +++ b/crates/docbin/src/drawing.rs @@ -190,7 +190,10 @@ fn leaf(sp: &Sp, w: f32, h: f32, float: Float, b: &Build, pics: &mut Pictures) - 5 => ShapeKind::Triangle, 6 => ShapeKind::RightTriangle, 12 | 187 => ShapeKind::Star, - 13 | 66 | 67 | 68 | 69 => ShapeKind::Arrow, + 13 | 69 => ShapeKind::Arrow, + 66 => ShapeKind::ArrowLeft, + 67 => ShapeKind::ArrowDown, + 68 => ShapeKind::ArrowUp, 20 | 32 => ShapeKind::Line, 74 => ShapeKind::Heart, 202 => ShapeKind::TextBox, diff --git a/crates/docx/src/read/shape_style.rs b/crates/docx/src/read/shape_style.rs index 180774eff..e8ccee919 100644 --- a/crates/docx/src/read/shape_style.rs +++ b/crates/docx/src/read/shape_style.rs @@ -41,9 +41,10 @@ pub(crate) fn preset_kind(prst: &str) -> ShapeKind { "straightConnector1" => ShapeKind::StraightConnector, "bentConnector2" | "bentConnector3" | "bentConnector4" | "bentConnector5" => ShapeKind::ElbowConnector, "curvedConnector2" | "curvedConnector3" | "curvedConnector4" | "curvedConnector5" => ShapeKind::CurvedConnector, - "rightArrow" | "leftArrow" | "upArrow" | "downArrow" | "leftRightArrow" | "upDownArrow" | "notchedRightArrow" | "stripedRightArrow" => { - ShapeKind::Arrow - } + "rightArrow" | "leftRightArrow" | "upDownArrow" | "notchedRightArrow" | "stripedRightArrow" => ShapeKind::Arrow, + "leftArrow" => ShapeKind::ArrowLeft, + "upArrow" => ShapeKind::ArrowUp, + "downArrow" => ShapeKind::ArrowDown, "star4" | "star5" | "star6" | "star7" | "star8" | "star10" | "star12" | "star16" | "star24" | "star32" => ShapeKind::Star, "heart" => ShapeKind::Heart, _ => ShapeKind::Rectangle, @@ -469,6 +470,15 @@ mod tests { assert_eq!(preset_kind("flowChartDecision"), ShapeKind::Diamond); } + /// #3919: Word's up, down and left block arrows were all read as the right-pointing arrow. + #[test] + fn block_arrows_keep_their_direction() { + assert_eq!(preset_kind("rightArrow"), ShapeKind::Arrow); + assert_eq!(preset_kind("leftArrow"), ShapeKind::ArrowLeft); + assert_eq!(preset_kind("upArrow"), ShapeKind::ArrowUp); + assert_eq!(preset_kind("downArrow"), ShapeKind::ArrowDown); + } + #[test] fn vml_presets_map_like_drawingml_presets() { assert_eq!(vml_preset_kind(13), Some(ShapeKind::Arrow)); @@ -477,7 +487,7 @@ mod tests { assert_eq!(vml_preset_kind(202), Some(ShapeKind::TextBox)); // The nearest shape, as `preset_kind` gives it: left arrow, right triangle, four-point // star, rounded callout, elbow connector, a bevel (its box). - assert_eq!(vml_preset_kind(66), Some(ShapeKind::Arrow)); + assert_eq!(vml_preset_kind(66), Some(ShapeKind::ArrowLeft)); assert_eq!(vml_preset_kind(6), Some(ShapeKind::RightTriangle)); assert_eq!(vml_preset_kind(187), Some(ShapeKind::Star)); assert_eq!(vml_preset_kind(62), Some(ShapeKind::RoundedRectangle)); diff --git a/crates/docx/src/write/story.rs b/crates/docx/src/write/story.rs index 131a3fbc6..184fe0f23 100644 --- a/crates/docx/src/write/story.rs +++ b/crates/docx/src/write/story.rs @@ -1069,6 +1069,9 @@ impl Writer<'_> { ShapeKind::ElbowConnector => "bentConnector3", ShapeKind::CurvedConnector => "curvedConnector3", ShapeKind::Arrow => "rightArrow", + ShapeKind::ArrowLeft => "leftArrow", + ShapeKind::ArrowUp => "upArrow", + ShapeKind::ArrowDown => "downArrow", ShapeKind::Star => "star5", ShapeKind::Heart => "heart", }; diff --git a/crates/docx/tests/it/fixtures.rs b/crates/docx/tests/it/fixtures.rs index 20c61483d..eabdd38a9 100644 --- a/crates/docx/tests/it/fixtures.rs +++ b/crates/docx/tests/it/fixtures.rs @@ -2406,7 +2406,7 @@ fn vml_preset_shapes_are_drawn() { assert_eq!(kind(15), Some((ShapeKind::Rectangle, false))); assert_eq!(kind(16), Some((ShapeKind::Diamond, false))); assert_eq!(kind(17), Some((ShapeKind::Ellipse, false))); - assert_eq!(shape(18), Some((ShapeKind::Arrow, 60.0, 20.0, Some(Rgb(0xFF, 0xA5, 0x00)), black, 0.75, true, false))); + assert_eq!(shape(18), Some((ShapeKind::ArrowLeft, 60.0, 20.0, Some(Rgb(0xFF, 0xA5, 0x00)), black, 0.75, true, false))); assert_eq!(kind(19), Some((ShapeKind::Rectangle, false))); assert_eq!(kind(20), Some((ShapeKind::Star, false))); assert_eq!(shape(21), None); @@ -2477,12 +2477,12 @@ fn vml_nearest_preset_shape_survives_saving() { let mut z = zip::ZipArchive::new(std::io::Cursor::new(saved.as_slice())).unwrap(); let mut xml = String::new(); std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap(); - assert!(xml.contains(r#"prst="rightArrow""#), "{xml}"); + assert!(xml.contains(r#"prst="leftArrow""#), "{xml}"); let again = wordcraft_docx::read(&saved).unwrap(); assert!( matches!( ¶s(&again)[0].objects[..], - [InlineObject::Shape { kind: ShapeKind::Arrow, w: 60.0, h: 20.0, fill: Some(Fill::Solid { color: Rgb(0xFF, 0xA5, 0x00), .. }), .. }] + [InlineObject::Shape { kind: ShapeKind::ArrowLeft, w: 60.0, h: 20.0, fill: Some(Fill::Solid { color: Rgb(0xFF, 0xA5, 0x00), .. }), .. }] ), "{:?}", paras(&again)[0].objects diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index e6072e91d..c4898c3cf 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -692,20 +692,10 @@ fn shape_path(kind: ShapeKind, r: &Rect, corner: Option) -> Option { pb.move_to(x0, y0); pb.line_to(x1, y1); } - ShapeKind::Arrow => { - let h = r.h; - poly( - &mut pb, - &[ - (x0, cy - h * 0.2), - (x1 - h * 0.5, cy - h * 0.2), - (x1 - h * 0.5, y0), - (x1, cy), - (x1 - h * 0.5, y1), - (x1 - h * 0.5, cy + h * 0.2), - (x0, cy + h * 0.2), - ], - ) + ShapeKind::Arrow | ShapeKind::ArrowLeft | ShapeKind::ArrowUp | ShapeKind::ArrowDown => { + let pts: Vec<(f32, f32)> = + wordcraft_doc::wrap::arrow_outline(kind, r.w, r.h).iter().map(|&(u, v)| (x0 + u * r.w, y0 + v * r.h)).collect(); + poly(&mut pb, &pts) } ShapeKind::Star => { let pts: Vec<(f32, f32)> = (0..10) diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 92315bc59..146a6c0c3 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -426,6 +426,9 @@ fn shapes_have_paths() { ShapeKind::Star, ShapeKind::Heart, ShapeKind::Arrow, + ShapeKind::ArrowLeft, + ShapeKind::ArrowUp, + ShapeKind::ArrowDown, ShapeKind::Line, ShapeKind::Diamond, ShapeKind::Triangle, diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index f264db557..a67f91447 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1188,17 +1188,13 @@ pub fn shape_path(kind: ShapeKind, r: kurbo::Rect, corner: Option) -> BezPa p.line_to((r.x1, r.y1)); p } - ShapeKind::Arrow => { - let h = r.height(); - poly(&[ - (r.x0, cy - h * 0.2), - (r.x1 - h * 0.5, cy - h * 0.2), - (r.x1 - h * 0.5, r.y0), - (r.x1, cy), - (r.x1 - h * 0.5, r.y1), - (r.x1 - h * 0.5, cy + h * 0.2), - (r.x0, cy + h * 0.2), - ]) + ShapeKind::Arrow | ShapeKind::ArrowLeft | ShapeKind::ArrowUp | ShapeKind::ArrowDown => { + let (w, h) = (r.width(), r.height()); + let pts: Vec<(f64, f64)> = wordcraft_doc::wrap::arrow_outline(kind, w as f32, h as f32) + .iter() + .map(|&(u, v)| (r.x0 + f64::from(u) * w, r.y0 + f64::from(v) * h)) + .collect(); + poly(&pts) } ShapeKind::Star => { let mut pts = Vec::new(); @@ -2192,9 +2188,18 @@ mod tests { #[test] fn shapes_have_paths() { let r = kurbo::Rect::new(0.0, 0.0, 10.0, 10.0); - for k in - [ShapeKind::Rectangle, ShapeKind::Ellipse, ShapeKind::FlowchartOr, ShapeKind::Star, ShapeKind::Heart, ShapeKind::Arrow, ShapeKind::Line] - { + for k in [ + ShapeKind::Rectangle, + ShapeKind::Ellipse, + ShapeKind::FlowchartOr, + ShapeKind::Star, + ShapeKind::Heart, + ShapeKind::Arrow, + ShapeKind::ArrowLeft, + ShapeKind::ArrowUp, + ShapeKind::ArrowDown, + ShapeKind::Line, + ] { assert!(!shape_path(k, r, None).elements().is_empty()); } }