mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-02 16:30:05 -06:00
Shapes: arrowheads keep their kind and size and are drawn at Word's sizes (#5192)
Word draws an arrowhead 2, 3 or 5 times the line's width long and wide (small, medium, large; `a:headEnd`/`a:tailEnd` `len`/`w`), a line under 2 points as a 2-point one. Every arrowhead was drawn one size and saved as a medium triangle: the kind, width and length are now read (DrawingML and VML), kept and written back, and the size is drawn as Word does. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
499fc6289e
commit
8a191fbf11
+3
-1
File diff suppressed because one or more lines are too long
@@ -48,6 +48,131 @@ impl LineCap {
|
||||
}
|
||||
}
|
||||
|
||||
/// An arrowhead's width across its line or length along it (ST_LineEndWidth and
|
||||
/// ST_LineEndLength, ECMA-376 §20.1.10.39–40): 2, 3 or 5 times the line's width, as Word draws it.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ArrowSize {
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl ArrowSize {
|
||||
/// Read a DrawingML size (`sm`, `med`, `lg`); absent or unknown values are medium.
|
||||
pub fn from_ooxml(value: &str) -> Self {
|
||||
match value {
|
||||
"sm" => Self::Small,
|
||||
"lg" => Self::Large,
|
||||
_ => Self::Medium,
|
||||
}
|
||||
}
|
||||
|
||||
/// The DrawingML spelling.
|
||||
pub fn ooxml(self) -> &'static str {
|
||||
match self {
|
||||
Self::Small => "sm",
|
||||
Self::Medium => "med",
|
||||
Self::Large => "lg",
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a VML width (`narrow`, `medium`, `wide`) or length (`short`, `medium`, `long`).
|
||||
pub fn from_vml(value: &str) -> Self {
|
||||
match value {
|
||||
"narrow" | "short" => Self::Small,
|
||||
"wide" | "long" => Self::Large,
|
||||
_ => Self::Medium,
|
||||
}
|
||||
}
|
||||
|
||||
/// How many [`arrow_unit`]s the arrowhead measures.
|
||||
pub fn factor(self) -> f32 {
|
||||
match self {
|
||||
Self::Small => 2.0,
|
||||
Self::Medium => 3.0,
|
||||
Self::Large => 5.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of an arrowhead (ST_LineEndType, ECMA-376 §20.1.10.33), kept for saving; every kind
|
||||
/// is drawn as a triangle.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ArrowKind {
|
||||
#[default]
|
||||
Triangle,
|
||||
Stealth,
|
||||
Diamond,
|
||||
Oval,
|
||||
Arrow,
|
||||
}
|
||||
|
||||
impl ArrowKind {
|
||||
/// Read a DrawingML end type other than `none`; unknown values are triangles.
|
||||
pub fn from_ooxml(value: &str) -> Self {
|
||||
match value {
|
||||
"stealth" => Self::Stealth,
|
||||
"diamond" => Self::Diamond,
|
||||
"oval" => Self::Oval,
|
||||
"arrow" => Self::Arrow,
|
||||
_ => Self::Triangle,
|
||||
}
|
||||
}
|
||||
|
||||
/// The DrawingML spelling.
|
||||
pub fn ooxml(self) -> &'static str {
|
||||
match self {
|
||||
Self::Triangle => "triangle",
|
||||
Self::Stealth => "stealth",
|
||||
Self::Diamond => "diamond",
|
||||
Self::Oval => "oval",
|
||||
Self::Arrow => "arrow",
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a VML arrow (`block`, `classic`, `diamond`, `oval`, `open`) other than `none`.
|
||||
pub fn from_vml(value: &str) -> Self {
|
||||
match value {
|
||||
"classic" => Self::Stealth,
|
||||
"diamond" => Self::Diamond,
|
||||
"oval" => Self::Oval,
|
||||
"open" => Self::Arrow,
|
||||
_ => Self::Triangle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An arrowhead's kind and size (`a:headEnd` / `a:tailEnd`). The default is a medium triangle.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct ArrowStyle {
|
||||
pub kind: ArrowKind,
|
||||
pub width: ArrowSize,
|
||||
pub length: ArrowSize,
|
||||
}
|
||||
|
||||
impl ArrowStyle {
|
||||
pub fn is_default(&self) -> bool {
|
||||
*self == Self::default()
|
||||
}
|
||||
|
||||
/// The arrowhead's length along its line and width across it, for a line `line_width` wide.
|
||||
pub fn dims(self, line_width: f32) -> (f32, f32) {
|
||||
let unit = arrow_unit(line_width);
|
||||
(self.length.factor() * unit, self.width.factor() * unit)
|
||||
}
|
||||
}
|
||||
|
||||
/// What an arrowhead's size is a multiple of: the line's width, but at least 2 points (Word draws
|
||||
/// a thinner line's arrowheads as for a 2-point line).
|
||||
pub fn arrow_unit(line_width: f32) -> f32 {
|
||||
let w = if line_width.is_finite() { line_width.clamp(0.25, 200.0) } else { 0.75 };
|
||||
w.max(2.0)
|
||||
}
|
||||
|
||||
/// The rest of a shape's properties. Empty (the default) for most shapes.
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
@@ -69,6 +194,11 @@ pub struct ShapeExtra {
|
||||
pub arrow_start: bool,
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub arrow_end: bool,
|
||||
/// The kind and size of the start and end arrowheads, when there are any.
|
||||
#[serde(skip_serializing_if = "ArrowStyle::is_default")]
|
||||
pub start_style: ArrowStyle,
|
||||
#[serde(skip_serializing_if = "ArrowStyle::is_default")]
|
||||
pub end_style: ArrowStyle,
|
||||
/// A rounded rectangle's corner radius (`roundRect`'s `adj`, ECMA-376 §20.1.10.56): in
|
||||
/// 100000ths of the shorter side, at most [`MAX_CORNER`]; `None` draws [`DEFAULT_CORNER`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -499,10 +499,26 @@ impl Reader<'_> {
|
||||
.and_then(|g| preset_adj(g, "adj"))
|
||||
.and_then(|v| u32::try_from(v.clamp(0, i64::from(wordcraft_doc::connector::MAX_CORNER))).ok());
|
||||
}
|
||||
// Arrowheads: any end type but `none` (ECMA-376 §20.1.10.33) is drawn as a triangle.
|
||||
let arrow = |tag: &str| ln.and_then(|l| l.child(tag)).and_then(|e| e.attr("type")).is_some_and(|t| t != "none");
|
||||
extra.arrow_start = arrow("a:headEnd");
|
||||
extra.arrow_end = arrow("a:tailEnd");
|
||||
// Arrowheads: any end type but `none` (ECMA-376 §20.1.10.33) is drawn as a triangle of its
|
||||
// width and length.
|
||||
let arrow = |tag: &str| {
|
||||
let e = ln.and_then(|l| l.child(tag))?;
|
||||
let kind = e.attr("type").filter(|t| *t != "none")?;
|
||||
let size = |name: &str| wordcraft_doc::connector::ArrowSize::from_ooxml(e.attr(name).unwrap_or("med"));
|
||||
Some(wordcraft_doc::connector::ArrowStyle {
|
||||
kind: wordcraft_doc::connector::ArrowKind::from_ooxml(kind),
|
||||
width: size("w"),
|
||||
length: size("len"),
|
||||
})
|
||||
};
|
||||
if let Some(style) = arrow("a:headEnd") {
|
||||
extra.arrow_start = true;
|
||||
extra.start_style = style;
|
||||
}
|
||||
if let Some(style) = arrow("a:tailEnd") {
|
||||
extra.arrow_end = true;
|
||||
extra.end_style = style;
|
||||
}
|
||||
// A connector's glued ends (ECMA-376 §20.1.2.2.36, §20.1.2.2.13).
|
||||
if let Some(cn) = wsp.child("wps:cNvCnPr") {
|
||||
let end = |tag: &str| {
|
||||
|
||||
@@ -296,10 +296,24 @@ impl Reader<'_> {
|
||||
let kind = kind?;
|
||||
let mut extra = ShapeExtra { cap, ..Default::default() };
|
||||
if kind == ShapeKind::Line {
|
||||
// Arrowheads (`v:stroke` startarrow / endarrow): any but `none` is drawn.
|
||||
let arrow = |name: &str| shape.child("v:stroke").and_then(|s| s.attr(name)).is_some_and(|v| v != "none");
|
||||
extra.arrow_start = arrow("startarrow");
|
||||
extra.arrow_end = arrow("endarrow");
|
||||
// Arrowheads (`v:stroke` startarrow / endarrow, with their width and length): any but
|
||||
// `none` is drawn.
|
||||
use wordcraft_doc::connector::{ArrowKind, ArrowSize, ArrowStyle};
|
||||
let stroke = shape.child("v:stroke");
|
||||
let arrow = |end: &str| {
|
||||
let s = stroke?;
|
||||
let kind = s.attr(&format!("{end}arrow")).filter(|v| *v != "none")?;
|
||||
let size = |what: &str| ArrowSize::from_vml(s.attr(&format!("{end}arrow{what}")).unwrap_or("medium"));
|
||||
Some(ArrowStyle { kind: ArrowKind::from_vml(kind), width: size("width"), length: size("length") })
|
||||
};
|
||||
if let Some(style) = arrow("start") {
|
||||
extra.arrow_start = true;
|
||||
extra.start_style = style;
|
||||
}
|
||||
if let Some(style) = arrow("end") {
|
||||
extra.arrow_end = true;
|
||||
extra.end_style = style;
|
||||
}
|
||||
}
|
||||
if shape.local() == "line" {
|
||||
// A line runs `from` → `to` (points, offsets from where its style places it). A preset
|
||||
|
||||
@@ -382,11 +382,10 @@ fn line_dash(w: &mut W, dash: &wordcraft_doc::line_dash::LineDash) {
|
||||
/// A shape's `a:effectLst` (ECMA-376 Part 1 §20.1.8.26), children in schema order.
|
||||
/// A line's arrowheads (`a:headEnd`, `a:tailEnd`, ECMA-376 §20.1.8.38 / §20.1.8.57).
|
||||
fn arrow_ends(w: &mut W, extra: &wordcraft_doc::connector::ShapeExtra) {
|
||||
if extra.arrow_start {
|
||||
w.empty("a:headEnd", &[("type", "triangle")]);
|
||||
}
|
||||
if extra.arrow_end {
|
||||
w.empty("a:tailEnd", &[("type", "triangle")]);
|
||||
for (on, tag, style) in [(extra.arrow_start, "a:headEnd", extra.start_style), (extra.arrow_end, "a:tailEnd", extra.end_style)] {
|
||||
if on {
|
||||
w.empty(tag, &[("type", style.kind.ooxml()), ("w", style.width.ooxml()), ("len", style.length.ooxml())]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2475,6 +2475,52 @@ fn vml_lines_and_rectangles() {
|
||||
));
|
||||
}
|
||||
|
||||
/// An arrowhead's kind, width and length (DrawingML `a:headEnd`/`a:tailEnd` `type`/`w`/`len`, VML
|
||||
/// `startarrow`/`startarrowwidth`/`startarrowlength`) are read and saved: they set the size Word
|
||||
/// draws it at. An end without `w`/`len` is medium.
|
||||
#[test]
|
||||
fn arrowheads_keep_their_kind_and_size() {
|
||||
use wordcraft_doc::connector::{ArrowKind, ArrowSize, ArrowStyle};
|
||||
let style = |kind, width, length| ArrowStyle { kind, width, length };
|
||||
let body = [
|
||||
anchored_wsp(
|
||||
"Line",
|
||||
1270000,
|
||||
0,
|
||||
r#"<a:prstGeom prst="line"><a:avLst/></a:prstGeom><a:ln w="12700"><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:headEnd type="stealth" w="sm" len="lg"/><a:tailEnd type="diamond" w="lg" len="sm"/></a:ln>"#,
|
||||
"",
|
||||
"<wps:cNvCnPr/>",
|
||||
),
|
||||
anchored_wsp("Plain", 1270000, 0, r#"<a:prstGeom prst="line"><a:avLst/></a:prstGeom><a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:tailEnd type="triangle"/></a:ln>"#, "", "<wps:cNvCnPr/>"),
|
||||
r#"<w:r><w:pict><v:line style="position:absolute" from="10pt,20pt" to="40pt,20pt"><v:stroke startarrow="classic" startarrowwidth="narrow" startarrowlength="long" endarrow="oval" endarrowwidth="wide" endarrowlength="short"/></v:line></w:pict></w:r>"#.to_string(),
|
||||
]
|
||||
.concat();
|
||||
let ends = |d: &Document| -> Vec<(bool, ArrowStyle, bool, ArrowStyle)> {
|
||||
paras(d)[0]
|
||||
.objects
|
||||
.iter()
|
||||
.map(|o| match o {
|
||||
InlineObject::Shape { extra, .. } => (extra.arrow_start, extra.start_style, extra.arrow_end, extra.end_style),
|
||||
o => panic!("{o:?}"),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let expected = vec![
|
||||
(true, style(ArrowKind::Stealth, ArrowSize::Small, ArrowSize::Large), true, style(ArrowKind::Diamond, ArrowSize::Large, ArrowSize::Small)),
|
||||
(false, ArrowStyle::default(), true, ArrowStyle::default()),
|
||||
(true, style(ArrowKind::Stealth, ArrowSize::Small, ArrowSize::Large), true, style(ArrowKind::Oval, ArrowSize::Large, ArrowSize::Small)),
|
||||
];
|
||||
let d = read_body(&format!("<w:p>{body}</w:p>"));
|
||||
assert_eq!(ends(&d), expected);
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut xml = String::new();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes.as_slice())).unwrap();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
assert!(xml.contains(r#"<a:headEnd type="stealth" w="sm" len="lg"/>"#), "{xml}");
|
||||
assert!(xml.contains(r#"<a:tailEnd type="diamond" w="lg" len="sm"/>"#), "{xml}");
|
||||
assert_eq!(ends(&wordcraft_docx::read(&bytes).unwrap()), expected);
|
||||
}
|
||||
|
||||
/// The right arrow's shapetype as Word writes it (ECMA-376 Part 4 `v:shapetype`, `o:spt` 13).
|
||||
const VML_RIGHT_ARROW_TYPE: &str = r#"<v:shapetype id="_x0000_t13" coordsize="21600,21600" o:spt="13" adj="16200,5400" path="m@0,l@0@1,0@1,0@2@0@2@0,21600,21600,10800xe"><v:stroke joinstyle="miter"/><v:formulas><v:f eqn="val #0"/><v:f eqn="val #1"/></v:formulas><v:path o:connecttype="custom"/></v:shapetype>"#;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::options::DisplayOptions;
|
||||
use super::paint::{Paint, paint_for};
|
||||
use super::{BezPath, Document, Draw, FaceRef, Graphic, GraphicItem, GraphicKind, InlineObject, PathSeg, Rect, Rgb, ShapeKind, Spin, TextAlign};
|
||||
use wordcraft_doc::connector::ArrowStyle;
|
||||
|
||||
/// The draws of a chart or diagram's items inside `rect` (page coordinates). The items were built
|
||||
/// for the size `g.w` × `g.h` (0: `rect`'s own size), so they are scaled to `rect`; strokes and text
|
||||
@@ -137,7 +138,7 @@ pub(super) fn shape_draws(rect: Rect, look: &ShapeLook, opts: &DisplayOptions, o
|
||||
}
|
||||
if !closed
|
||||
&& extra.arrow_end
|
||||
&& let Some(arrow) = path_arrowhead(&pts, color, width)
|
||||
&& let Some(arrow) = path_arrowhead(&pts, color, width, extra.end_style)
|
||||
{
|
||||
out.push(arrow);
|
||||
}
|
||||
@@ -166,7 +167,7 @@ pub(super) fn shape_draws(rect: Rect, look: &ShapeLook, opts: &DisplayOptions, o
|
||||
&& let Some(color) = stroke
|
||||
{
|
||||
let points: Vec<(f32, f32)> = pts.iter().map(|[x, y]| (*x, *y)).collect();
|
||||
if let Some(arrow) = path_arrowhead(&points, color, width) {
|
||||
if let Some(arrow) = path_arrowhead(&points, color, width, extra.end_style) {
|
||||
out.push(arrow);
|
||||
}
|
||||
}
|
||||
@@ -179,10 +180,10 @@ fn path_stroke_width(width: f32) -> f32 {
|
||||
}
|
||||
|
||||
/// Make a filled triangular arrowhead at the last point, in the points' coordinate space.
|
||||
/// The last distinct segment supplies its direction and the bounded stroke width sets its size.
|
||||
/// The last distinct segment supplies its direction; `style` and the stroke width set its size.
|
||||
/// Returns `None` if there is no usable final segment. The triangle has no outline, so the path's
|
||||
/// cap setting does not affect its appearance.
|
||||
fn path_arrowhead(points: &[(f32, f32)], color: Rgb, stroke_width: f32) -> Option<Draw> {
|
||||
fn path_arrowhead(points: &[(f32, f32)], color: Rgb, stroke_width: f32, style: ArrowStyle) -> Option<Draw> {
|
||||
let tip = *points.last()?;
|
||||
let (prev_x, prev_y) = points.iter().rev().skip(1).copied().find(|p| *p != tip)?;
|
||||
let dx = tip.0 - prev_x;
|
||||
@@ -191,13 +192,17 @@ fn path_arrowhead(points: &[(f32, f32)], color: Rgb, stroke_width: f32) -> Optio
|
||||
if !length.is_finite() || length <= f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
let (dx, dy) = (dx / length, dy / length);
|
||||
let width = if stroke_width.is_finite() { stroke_width.clamp(0.25, 200.0) } else { 0.75 };
|
||||
let len = (width * 3.5).max(7.0);
|
||||
Some(arrowhead(tip, (dx / length, dy / length), stroke_width, style, color))
|
||||
}
|
||||
|
||||
/// A filled triangular arrowhead with its tip at `tip`, pointing along unit vector `dir`: as long
|
||||
/// and as wide as `style` makes it for a `stroke_width` line ([`ArrowStyle::dims`]).
|
||||
fn arrowhead(tip: (f32, f32), (dx, dy): (f32, f32), stroke_width: f32, style: ArrowStyle, color: Rgb) -> Draw {
|
||||
let (len, wide) = style.dims(stroke_width);
|
||||
let (bx, by) = (tip.0 - dx * len, tip.1 - dy * len);
|
||||
let (nx, ny) = (-dy * len * 0.45, dx * len * 0.45);
|
||||
let (nx, ny) = (-dy * wide / 2.0, dx * wide / 2.0);
|
||||
let segs = vec![PathSeg::Move(tip.0, tip.1), PathSeg::Line(bx + nx, by + ny), PathSeg::Line(bx - nx, by - ny), PathSeg::Close];
|
||||
Some(Draw::Path { segs, fill: Some(Paint::solid(color)), stroke: None, stroke_width: 0.0, dash: Vec::new(), cap: Default::default() })
|
||||
Draw::Path { segs, fill: Some(Paint::solid(color)), stroke: None, stroke_width: 0.0, dash: Vec::new(), cap: Default::default() }
|
||||
}
|
||||
|
||||
/// A line or connector at `rect` (its unflipped frame: flips are the frame's turn), with its
|
||||
@@ -243,17 +248,10 @@ fn line_draws(
|
||||
// to the bottom-right.
|
||||
let (start, end) =
|
||||
if kind == ShapeKind::Line { ((rect.x, rect.bottom()), (rect.right(), rect.y)) } else { ((rect.x, rect.y), (rect.right(), rect.bottom())) };
|
||||
let width = if stroke_width.is_finite() { stroke_width.clamp(0.25, 200.0) } else { 0.75 };
|
||||
let len = (width * 3.5).max(7.0);
|
||||
for (on, tip, at_end) in [(extra.arrow_start, start, false), (extra.arrow_end, end, true)] {
|
||||
if !on {
|
||||
continue;
|
||||
for (on, tip, at_end, style) in [(extra.arrow_start, start, false, extra.start_style), (extra.arrow_end, end, true, extra.end_style)] {
|
||||
if on {
|
||||
out.push(arrowhead(tip, end_direction(kind, rect.w, rect.h, at_end), stroke_width, style, color));
|
||||
}
|
||||
let (dx, dy) = end_direction(kind, rect.w, rect.h, at_end);
|
||||
let (bx, by) = (tip.0 - dx * len, tip.1 - dy * len);
|
||||
let (nx, ny) = (-dy * len * 0.45, dx * len * 0.45);
|
||||
let segs = vec![PathSeg::Move(tip.0, tip.1), PathSeg::Line(bx + nx, by + ny), PathSeg::Line(bx - nx, by - ny), PathSeg::Close];
|
||||
out.push(Draw::Path { segs, fill: Some(Paint::solid(color)), stroke: None, stroke_width: 0.0, dash: Vec::new(), cap: Default::default() });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3285,6 +3285,61 @@ fn freeform_arrow_end_draws_at_the_last_point() {
|
||||
assert_eq!(arrow_tip, endpoint, "the arrowhead tip meets the custom path's final point");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_arrowhead_is_as_long_and_wide_as_its_sizes_make_it() {
|
||||
// Word draws an arrowhead 2, 3 or 5 times the line's width long and wide (small, medium,
|
||||
// large), and a line thinner than 2 points as if it were 2 points (measured in Word's PDFs).
|
||||
use wordcraft_doc::connector::{ArrowKind, ArrowSize, ArrowStyle, ShapeExtra};
|
||||
use wordcraft_doc::graphic::PathSeg;
|
||||
use wordcraft_doc::para::{Float, ShapeKind};
|
||||
use wordcraft_doc::props::Rgb;
|
||||
|
||||
let style = |width, length| ArrowStyle { kind: ArrowKind::Triangle, width, length };
|
||||
let cases = [
|
||||
(0.75, style(ArrowSize::Small, ArrowSize::Large), style(ArrowSize::Large, ArrowSize::Small), (10.0, 4.0), (4.0, 10.0)),
|
||||
(0.75, ArrowStyle::default(), ArrowStyle::default(), (6.0, 6.0), (6.0, 6.0)),
|
||||
(3.0, style(ArrowSize::Medium, ArrowSize::Large), style(ArrowSize::Small, ArrowSize::Medium), (15.0, 9.0), (9.0, 6.0)),
|
||||
(6.0, style(ArrowSize::Large, ArrowSize::Large), style(ArrowSize::Small, ArrowSize::Small), (30.0, 30.0), (12.0, 12.0)),
|
||||
];
|
||||
for (line_width, start_style, end_style, start_dims, end_dims) in cases {
|
||||
let shape = InlineObject::Shape {
|
||||
kind: ShapeKind::Line,
|
||||
w: 100.0,
|
||||
h: 0.0,
|
||||
fill: None,
|
||||
stroke: Some(Rgb::BLACK),
|
||||
stroke_width: line_width,
|
||||
float: Float::default(),
|
||||
story: None,
|
||||
freeform: None,
|
||||
effects: Default::default(),
|
||||
extra: ShapeExtra { arrow_start: true, arrow_end: true, start_style, end_style, ..Default::default() },
|
||||
};
|
||||
let mut doc = Document::from_text("Scan");
|
||||
doc.insert_object(&Pos::body(0, 0), shape, &Default::default()).unwrap();
|
||||
let draws = flat(display::page_display(&doc, &lay(&doc).pages[0], &Default::default()));
|
||||
// Each arrowhead: its length (tip to the middle of its base) and width (across its base),
|
||||
// the start's first.
|
||||
let heads: Vec<(f32, f32)> = draws
|
||||
.iter()
|
||||
.filter_map(|draw| match draw {
|
||||
display::Draw::Path { fill: Some(display::Paint::Solid(Rgb::BLACK, _)), segs, .. } => match segs.as_slice() {
|
||||
[PathSeg::Move(tx, ty), PathSeg::Line(ax, ay), PathSeg::Line(bx, by), PathSeg::Close] => {
|
||||
let (mx, my) = ((ax + bx) / 2.0, (ay + by) / 2.0);
|
||||
Some(((tx - mx).hypot(ty - my), (ax - bx).hypot(ay - by)))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(heads.len(), 2, "{line_width}: {heads:?}");
|
||||
for ((got, want), end) in heads.iter().zip([start_dims, end_dims]).zip(["start", "end"]) {
|
||||
assert!((got.0 - want.0).abs() < 0.01 && (got.1 - want.1).abs() < 0.01, "{line_width} pt line, {end}: got {got:?}, want {want:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_drawings_are_neither_drawn_nor_take_room() {
|
||||
// A hidden floating shape (VML `visibility:hidden`, `wp:docPr/@hidden`): no wrap area, nothing
|
||||
|
||||
File diff suppressed because one or more lines are too long
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user