diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 3cab26695..27e9ca01b 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -89,3 +89,5 @@ and the exact transfer revision are recorded in each vendored crate's SOURCE.jso | `crates/raw/docs/coverage/adobe-cameras.tsv` | Facts (maker, model, extension, profile, minimum Camera Raw version) parsed by LightCraft contributors from Adobe's published list | [Adobe Camera Raw supported cameras](https://helpx.adobe.com/camera-raw/desktop/dng-and-file-formats/camera-raw-plug-supported-cameras.html), Wayback capture 20261003133011; moved from [LightCraft `docs/raw`](https://github.com/storytold/lightcraft/tree/5ff567a4a2e067bef1b6bce152c11e7f727ccbaf/docs/raw) | Facts, not copyrightable expression; the file is MIT OR Apache-2.0 | | `crates/raw/docs/coverage/{coverage,coverage-sweep,evidence,usage-weights}.tsv`, `coverage.py` | LightCraft contributors | [LightCraft `docs/raw`](https://github.com/storytold/lightcraft/tree/5ff567a4a2e067bef1b6bce152c11e7f727ccbaf/docs/raw) | MIT OR Apache-2.0 | | `crates/raw/docs/raf-corpus.sha256` (hashes only; the files are not bundled) | The photographers who contributed each file to raw.pixls.us | [raw.pixls.us](https://raw.pixls.us/) | CC0-1.0 | +| `crates/drawingml/src/preset_text.txt` | DeckCraft contributors (Nikita Rakitin); generated by `crates/drawingml/tests/fixtures/make_preset_guides.py` from the guide formulas of ECMA-376 Part 1 `presetShapeDefinitions.xml` | [storytold/deckcraft#650](https://github.com/storytold/deckcraft/pull/650) | MIT OR Apache-2.0 (the formulas are the format's definition in ECMA-376) | +| `crates/drawingml/tests/fixtures/preset_table.txt` | DeckCraft contributors (Nikita Rakitin); generated by `crates/drawingml/tests/fixtures/make_preset_table.py` from ECMA-376 Part 1 `presetShapeDefinitions.xml` | [storytold/deckcraft#650](https://github.com/storytold/deckcraft/pull/650) | MIT OR Apache-2.0 (the formulas are the format's definition in ECMA-376) | diff --git a/crates/drawingml/src/color.rs b/crates/drawingml/src/color.rs index c72bd62bd..0dc59649f 100644 --- a/crates/drawingml/src/color.rs +++ b/crates/drawingml/src/color.rs @@ -20,6 +20,11 @@ impl Default for Rgba { } } +#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "rounding and clamping bound the cast to 0..=255; NaN maps to zero")] +fn channel_from_f64(value: f64) -> u8 { + value.round().clamp(0.0, 255.0) as u8 +} + impl Rgba { pub const BLACK: Rgba = Rgba::rgb(0, 0, 0); pub const WHITE: Rgba = Rgba::rgb(255, 255, 255); @@ -59,16 +64,18 @@ impl Rgba { Rgba { a, ..self } } pub fn to_f32(self) -> [f32; 4] { - [self.r as f32 / 255.0, self.g as f32 / 255.0, self.b as f32 / 255.0, self.a as f32 / 255.0] + [f32::from(self.r) / 255.0, f32::from(self.g) / 255.0, f32::from(self.b) / 255.0, f32::from(self.a) / 255.0] } - pub fn from_f64(r: f64, g: f64, b: f64, a: f64) -> Self { - let c = |v: f64| if v.is_finite() { (v.clamp(0.0, 1.0) * 255.0).round() as u8 } else { 0 }; - Rgba { r: c(r), g: c(g), b: c(b), a: c(a) } + pub fn from_f64(red: f64, green: f64, blue: f64, alpha: f64) -> Self { + let channel = |value: f64| { + if value.is_finite() { channel_from_f64(value * 255.0) } else { 0 } + }; + Rgba { r: channel(red), g: channel(green), b: channel(blue), a: channel(alpha) } } /// Relative luminance (WCAG), 0–1. pub fn luminance(self) -> f64 { let lin = |c: u8| { - let v = c as f64 / 255.0; + let v = f64::from(c) / 255.0; if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } }; 0.2126 * lin(self.r) + 0.7152 * lin(self.g) + 0.0722 * lin(self.b) @@ -80,60 +87,60 @@ impl Rgba { (hi + 0.05) / (lo + 0.05) } pub fn to_hsl(self) -> (f64, f64, f64) { - let r = self.r as f64 / 255.0; - let g = self.g as f64 / 255.0; - let b = self.b as f64 / 255.0; - let max = r.max(g).max(b); - let min = r.min(g).min(b); - let l = (max + min) / 2.0; + let red = f64::from(self.r) / 255.0; + let green = f64::from(self.g) / 255.0; + let blue = f64::from(self.b) / 255.0; + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + let lightness = f64::midpoint(max, min); if (max - min).abs() < 1e-12 { - return (0.0, 0.0, l); + return (0.0, 0.0, lightness); } - let d = max - min; - let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) }; - let h = if (max - r).abs() < 1e-12 { - (g - b) / d + if g < b { 6.0 } else { 0.0 } - } else if (max - g).abs() < 1e-12 { - (b - r) / d + 2.0 + let delta = max - min; + let saturation = if lightness > 0.5 { delta / (2.0 - max - min) } else { delta / (max + min) }; + let hue = if (max - red).abs() < 1e-12 { + (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } + } else if (max - green).abs() < 1e-12 { + (blue - red) / delta + 2.0 } else { - (r - g) / d + 4.0 + (red - green) / delta + 4.0 }; - (h * 60.0, s, l) + (hue * 60.0, saturation, lightness) } - pub fn from_hsl(h: f64, s: f64, l: f64, a: u8) -> Self { - let h = ((h % 360.0) + 360.0) % 360.0 / 360.0; - let s = s.clamp(0.0, 1.0); - let l = l.clamp(0.0, 1.0); - if s == 0.0 { - return Rgba::from_f64(l, l, l, a as f64 / 255.0); + pub fn from_hsl(hue: f64, saturation: f64, lightness: f64, alpha: u8) -> Self { + let hue = ((hue % 360.0) + 360.0) % 360.0 / 360.0; + let saturation = saturation.clamp(0.0, 1.0); + let lightness = lightness.clamp(0.0, 1.0); + if saturation == 0.0 { + return Rgba::from_f64(lightness, lightness, lightness, f64::from(alpha) / 255.0); } - let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }; - let p = 2.0 * l - q; - let f = |t: f64| { - let t = if t < 0.0 { - t + 1.0 - } else if t > 1.0 { - t - 1.0 + let upper = if lightness < 0.5 { lightness * (1.0 + saturation) } else { lightness + saturation - lightness * saturation }; + let lower = 2.0 * lightness - upper; + let channel = |position: f64| { + let position = if position < 0.0 { + position + 1.0 + } else if position > 1.0 { + position - 1.0 } else { - t + position }; - if t < 1.0 / 6.0 { - p + (q - p) * 6.0 * t - } else if t < 0.5 { - q - } else if t < 2.0 / 3.0 { - p + (q - p) * (2.0 / 3.0 - t) * 6.0 + if position < 1.0 / 6.0 { + lower + (upper - lower) * 6.0 * position + } else if position < 0.5 { + upper + } else if position < 2.0 / 3.0 { + lower + (upper - lower) * (2.0 / 3.0 - position) * 6.0 } else { - p + lower } }; - Rgba::from_f64(f(h + 1.0 / 3.0), f(h), f(h - 1.0 / 3.0), a as f64 / 255.0) + Rgba::from_f64(channel(hue + 1.0 / 3.0), channel(hue), channel(hue - 1.0 / 3.0), f64::from(alpha) / 255.0) } /// Linear interpolation (straight alpha). - pub fn lerp(self, o: Rgba, t: f64) -> Rgba { - let t = if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 }; - let m = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round().clamp(0.0, 255.0) as u8; - Rgba { r: m(self.r, o.r), g: m(self.g, o.g), b: m(self.b, o.b), a: m(self.a, o.a) } + pub fn lerp(self, other: Rgba, amount: f64) -> Rgba { + let amount = if amount.is_finite() { amount.clamp(0.0, 1.0) } else { 0.0 }; + let interpolate = |start: u8, end: u8| channel_from_f64(f64::from(start) + (f64::from(end) - f64::from(start)) * amount); + Rgba { r: interpolate(self.r, other.r), g: interpolate(self.g, other.g), b: interpolate(self.b, other.b), a: interpolate(self.a, other.a) } } } @@ -295,12 +302,12 @@ impl ColorScheme { ColorScheme { name: name.into(), colors, extra: Vec::new(), tints: BTreeMap::new(), computed_tints: true } } pub fn get(&self, slot: SchemeSlot) -> Rgba { - let i = match slot { + let index = match slot { SchemeSlot::Dk1 | SchemeSlot::Tx1 => 0, SchemeSlot::Lt1 | SchemeSlot::Bg1 => 1, SchemeSlot::Dk2 | SchemeSlot::Tx2 => 2, SchemeSlot::Lt2 | SchemeSlot::Bg2 => 3, - SchemeSlot::Accent1 => 4, + SchemeSlot::Accent1 | SchemeSlot::PhClr => 4, SchemeSlot::Accent2 => 5, SchemeSlot::Accent3 => 6, SchemeSlot::Accent4 => 7, @@ -308,9 +315,8 @@ impl ColorScheme { SchemeSlot::Accent6 => 9, SchemeSlot::Hlink => 10, SchemeSlot::FolHlink => 11, - SchemeSlot::PhClr => 4, }; - self.colors.get(i).copied().unwrap_or(Rgba::BLACK) + self.colors.get(index).copied().unwrap_or(Rgba::BLACK) } pub fn set(&mut self, slot: SchemeSlot, c: Rgba) { if let Some(i) = SchemeSlot::THEME.iter().position(|s| *s == slot) @@ -442,88 +448,88 @@ impl ColorTransform { /// Apply transforms to a base colour. pub fn apply(base: Rgba, transforms: &[ColorTransform]) -> Rgba { - let mut c = base; - for t in transforms { - c = apply_one(c, *t); + let mut color = base; + for transform in transforms { + color = apply_one(color, *transform); } - c + color } -fn pct(v: i32) -> f64 { - v as f64 / 100_000.0 +fn pct(value: i32) -> f64 { + f64::from(value) / 100_000.0 } -fn apply_one(c: Rgba, t: ColorTransform) -> Rgba { - match t { +fn apply_one(color: Rgba, transform: ColorTransform) -> Rgba { + match transform { // tint: towards white by (1 - value); shade: towards black by (1 - value). Office applies // these in linear light, which keeps mid-tones from washing out. - ColorTransform::Tint(v) => { - let k = pct(v).clamp(0.0, 1.0); - map_linear(c, |x| x * k + (1.0 - k)) + ColorTransform::Tint(value) => { + let factor = pct(value).clamp(0.0, 1.0); + map_linear(color, |channel| channel * factor + (1.0 - factor)) } - ColorTransform::Shade(v) => { - let k = pct(v).clamp(0.0, 1.0); - map_linear(c, |x| x * k) + ColorTransform::Shade(value) => { + let factor = pct(value).clamp(0.0, 1.0); + map_linear(color, |channel| channel * factor) } - ColorTransform::LumMod(v) => { - let (h, s, l) = c.to_hsl(); - Rgba::from_hsl(h, s, l * pct(v), c.a) + ColorTransform::LumMod(value) => { + let (hue, saturation, lightness) = color.to_hsl(); + Rgba::from_hsl(hue, saturation, lightness * pct(value), color.a) } - ColorTransform::LumOff(v) => { - let (h, s, l) = c.to_hsl(); - Rgba::from_hsl(h, s, l + pct(v), c.a) + ColorTransform::LumOff(value) => { + let (hue, saturation, lightness) = color.to_hsl(); + Rgba::from_hsl(hue, saturation, lightness + pct(value), color.a) } - ColorTransform::SatMod(v) => { - let (h, s, l) = c.to_hsl(); - Rgba::from_hsl(h, s * pct(v), l, c.a) + ColorTransform::SatMod(value) => { + let (hue, saturation, lightness) = color.to_hsl(); + Rgba::from_hsl(hue, saturation * pct(value), lightness, color.a) } - ColorTransform::SatOff(v) => { - let (h, s, l) = c.to_hsl(); - Rgba::from_hsl(h, s + pct(v), l, c.a) + ColorTransform::SatOff(value) => { + let (hue, saturation, lightness) = color.to_hsl(); + Rgba::from_hsl(hue, saturation + pct(value), lightness, color.a) } - ColorTransform::HueMod(v) => { - let (h, s, l) = c.to_hsl(); - Rgba::from_hsl(h * pct(v), s, l, c.a) + ColorTransform::HueMod(value) => { + let (hue, saturation, lightness) = color.to_hsl(); + Rgba::from_hsl(hue * pct(value), saturation, lightness, color.a) } - ColorTransform::HueOff(v) => { - let (h, s, l) = c.to_hsl(); - Rgba::from_hsl(h + v as f64 / 60_000.0, s, l, c.a) + ColorTransform::HueOff(value) => { + let (hue, saturation, lightness) = color.to_hsl(); + Rgba::from_hsl(hue + f64::from(value) / 60_000.0, saturation, lightness, color.a) } - ColorTransform::Alpha(v) => c.with_alpha((pct(v).clamp(0.0, 1.0) * 255.0).round() as u8), - ColorTransform::AlphaMod(v) => c.with_alpha((c.a as f64 * pct(v)).round().clamp(0.0, 255.0) as u8), - ColorTransform::AlphaOff(v) => c.with_alpha((c.a as f64 + pct(v) * 255.0).round().clamp(0.0, 255.0) as u8), + ColorTransform::Alpha(value) => color.with_alpha(channel_from_f64(pct(value) * 255.0)), + ColorTransform::AlphaMod(value) => color.with_alpha(channel_from_f64(f64::from(color.a) * pct(value))), + ColorTransform::AlphaOff(value) => color.with_alpha(channel_from_f64(f64::from(color.a) + pct(value) * 255.0)), ColorTransform::Comp => { - let (h, s, l) = c.to_hsl(); - Rgba::from_hsl(h + 180.0, s, l, c.a) + let (hue, saturation, lightness) = color.to_hsl(); + Rgba::from_hsl(hue + 180.0, saturation, lightness, color.a) } - ColorTransform::Inv => Rgba { r: 255 - c.r, g: 255 - c.g, b: 255 - c.b, a: c.a }, + ColorTransform::Inv => Rgba { r: 255 - color.r, g: 255 - color.g, b: 255 - color.b, a: color.a }, ColorTransform::Gray => { - let y = (0.299 * c.r as f64 + 0.587 * c.g as f64 + 0.114 * c.b as f64).round().clamp(0.0, 255.0) as u8; - Rgba { r: y, g: y, b: y, a: c.a } + let intensity = channel_from_f64(0.299 * f64::from(color.r) + 0.587 * f64::from(color.g) + 0.114 * f64::from(color.b)); + Rgba { r: intensity, g: intensity, b: intensity, a: color.a } } - ColorTransform::RedMod(v) => Rgba { r: (c.r as f64 * pct(v)).round().clamp(0.0, 255.0) as u8, ..c }, - ColorTransform::GreenMod(v) => Rgba { g: (c.g as f64 * pct(v)).round().clamp(0.0, 255.0) as u8, ..c }, - ColorTransform::BlueMod(v) => Rgba { b: (c.b as f64 * pct(v)).round().clamp(0.0, 255.0) as u8, ..c }, + ColorTransform::RedMod(value) => Rgba { r: channel_from_f64(f64::from(color.r) * pct(value)), ..color }, + ColorTransform::GreenMod(value) => Rgba { g: channel_from_f64(f64::from(color.g) * pct(value)), ..color }, + ColorTransform::BlueMod(value) => Rgba { b: channel_from_f64(f64::from(color.b) * pct(value)), ..color }, } } -fn to_lin(v: u8) -> f64 { - let v = v as f64 / 255.0; - if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } +fn to_lin(value: u8) -> f64 { + let value = f64::from(value) / 255.0; + if value <= 0.04045 { value / 12.92 } else { ((value + 0.055) / 1.055).powf(2.4) } } -fn from_lin(v: f64) -> f64 { - let v = v.clamp(0.0, 1.0); - if v <= 0.003_130_8 { v * 12.92 } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 } +fn from_lin(value: f64) -> f64 { + let value = value.clamp(0.0, 1.0); + if value <= 0.003_130_8 { value * 12.92 } else { 1.055 * value.powf(1.0 / 2.4) - 0.055 } } -fn map_linear(c: Rgba, f: impl Fn(f64) -> f64) -> Rgba { - Rgba::from_f64(from_lin(f(to_lin(c.r))), from_lin(f(to_lin(c.g))), from_lin(f(to_lin(c.b))), c.a as f64 / 255.0) +fn map_linear(color: Rgba, map: impl Fn(f64) -> f64) -> Rgba { + Rgba::from_f64(from_lin(map(to_lin(color.r))), from_lin(map(to_lin(color.g))), from_lin(map(to_lin(color.b))), f64::from(color.a) / 255.0) } /// The colour-picker grid shown under each theme colour: lighter 80/60/40%, darker 25/50% /// (for dark colours: lighter 90/75/50/25/10%). -pub fn theme_variants(c: Rgba) -> [(Vec, &'static str); 5] { - let (_, _, l) = c.to_hsl(); - if l < 0.2 { +pub fn theme_variants(color: Rgba) -> [(Vec, &'static str); 5] { + let (_, _, lightness) = color.to_hsl(); + if lightness < 0.2 { [ (vec![ColorTransform::LumMod(10000), ColorTransform::LumOff(90000)], "Lighter 90%"), (vec![ColorTransform::LumMod(25000), ColorTransform::LumOff(75000)], "Lighter 75%"), @@ -531,7 +537,7 @@ pub fn theme_variants(c: Rgba) -> [(Vec, &'static str); 5] { (vec![ColorTransform::LumMod(75000), ColorTransform::LumOff(25000)], "Lighter 25%"), (vec![ColorTransform::LumMod(90000), ColorTransform::LumOff(10000)], "Lighter 10%"), ] - } else if l > 0.8 { + } else if lightness > 0.8 { [ (vec![ColorTransform::LumMod(95000)], "Darker 5%"), (vec![ColorTransform::LumMod(85000)], "Darker 15%"), @@ -1029,12 +1035,14 @@ mod tests { #[test] fn hsl_roundtrip() { - for c in [Rgba::rgb(10, 200, 30), Rgba::rgb(255, 0, 0), Rgba::rgb(128, 128, 128), Rgba::rgb(21, 96, 130)] { - let (h, s, l) = c.to_hsl(); - let d = Rgba::from_hsl(h, s, l, 255); + for color in [Rgba::rgb(10, 200, 30), Rgba::rgb(255, 0, 0), Rgba::rgb(128, 128, 128), Rgba::rgb(21, 96, 130)] { + let (hue, saturation, lightness) = color.to_hsl(); + let converted = Rgba::from_hsl(hue, saturation, lightness, 255); assert!( - (d.r as i32 - c.r as i32).abs() <= 1 && (d.g as i32 - c.g as i32).abs() <= 1 && (d.b as i32 - c.b as i32).abs() <= 1, - "{c:?} {d:?}" + (i32::from(converted.r) - i32::from(color.r)).abs() <= 1 + && (i32::from(converted.g) - i32::from(color.g)).abs() <= 1 + && (i32::from(converted.b) - i32::from(color.b)).abs() <= 1, + "{color:?} {converted:?}" ); } } diff --git a/crates/drawingml/src/custgeom.rs b/crates/drawingml/src/custgeom.rs index d886d30b9..999c470db 100644 --- a/crates/drawingml/src/custgeom.rs +++ b/crates/drawingml/src/custgeom.rs @@ -16,6 +16,22 @@ use serde::{Deserialize, Serialize}; use crate::preset::FillMode; +/// A connection site of a custom geometry (`a:cxnLst`): where a connector end glues. `x` and `y` +/// are fractions of the shape's box (0 = left/top, 1 = right/bottom; outside that range is +/// allowed), so the site keeps its place when the shape is resized. +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct ConnectionSite { + pub x: f64, + pub y: f64, +} + +impl ConnectionSite { + pub const fn new(x: f64, y: f64) -> Self { + ConnectionSite { x, y } + } +} + /// One sub-path of a custom geometry: SVG-like commands in path units. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -459,11 +475,52 @@ pub fn parse_path(d: &str) -> Vec<(char, Vec)> { /// Write custom paths as `a:custGeom`. Coordinates are integers in the file, so small path spaces /// are scaled up. -pub fn write_cust_geom(w: &mut Writer, paths: &[CustomPath]) { +/// The extent a shape without a transform of its own (a placeholder) is read and written with, +/// in EMU: its sites keep their fractions through a round trip. +pub const INHERITED_EXTENT_EMU: f64 = 21600.0; + +/// `a:cxnLst`: the connection sites as fractions of the shape's box, in file order (connectors +/// refer to them by index, so a `cxn` without a `pos` keeps its place as the box's corner). +/// Positions are in the shape's own coordinate space, `w_emu` × `h_emu` (the extent, where the +/// guides are evaluated), not in a path's `w`/`h` space. +pub fn read_cxn_lst(cg: &El, w_emu: f64, h_emu: f64) -> Vec { + let Some(list) = cg.child(Local("cxnLst")) else { return vec![] }; + let mut g = Guides::new(w_emu, h_emu); + g.eval_list(cg.child(Local("avLst"))); + g.eval_list(cg.child(Local("gdLst"))); + let frac = |v: f64, size: f64| if size > 0.0 { finite(v / size) } else { 0.0 }; + list.children(Local("cxn")) + .take(1000) + .map(|c| { + c.child(Local("pos")) + .map(|pos| { + let (x, y) = pt(&g, pos); + ConnectionSite::new(frac(x, w_emu), frac(y, h_emu)) + }) + .unwrap_or_default() + }) + .collect() +} + +/// Write custom paths as `a:custGeom`. Coordinates are integers in the file, so small path spaces +/// are scaled up. `sites` go out as `a:cxnLst` in the shape's space `ext` (EMU). +pub fn write_cust_geom(w: &mut Writer, paths: &[CustomPath], sites: &[ConnectionSite], ext: (i64, i64)) { w.open("a:custGeom", &[]); - for list in ["a:avLst", "a:gdLst", "a:ahLst", "a:cxnLst"] { + for list in ["a:avLst", "a:gdLst", "a:ahLst"] { w.empty(list, &[]); } + if sites.is_empty() { + w.empty("a:cxnLst", &[]); + } else { + w.open("a:cxnLst", &[]); + let emu = |f: f64, size: i64| (finite(f) * size as f64).round().clamp(-2.0e9, 2.0e9) as i64; + for s in sites.iter().take(1000) { + w.tag("a:cxn").attr("ang", "0").open(); + w.tag("a:pos").attr("x", emu(s.x, ext.0)).attr("y", emu(s.y, ext.1)).empty(); + w.close(); + } + w.close(); + } w.empty("a:rect", &[("l", "l"), ("t", "t"), ("r", "r"), ("b", "b")]); w.open("a:pathLst", &[]); for p in paths { @@ -607,7 +664,7 @@ mod tests { fn write_then_read_path() { let p = CustomPath { w: 100.0, h: 50.0, d: "M 0 0 L 100 0 C 100 10 90 50 50 50 Q 0 50 0 0 Z".into(), fill: FillMode::Norm, stroke: true }; let mut w = Writer::new(); - write_cust_geom(&mut w, std::slice::from_ref(&p)); + write_cust_geom(&mut w, std::slice::from_ref(&p), &[], (0, 0)); let d = parse(w.as_str().as_bytes()).unwrap(); let back = read_cust_geom(&d, 1.0, 1.0); assert_eq!(back.len(), 1); @@ -705,8 +762,37 @@ mod tests { assert_eq!(p.d, "M 0 0 L 30 0 C 30 20 20 30 0 30 Z"); assert_eq!(p.fill, FillMode::None); let mut w = Writer::new(); - write_cust_geom(&mut w, std::slice::from_ref(&p)); + write_cust_geom(&mut w, std::slice::from_ref(&p), &[], (30, 30)); let back = read_cust_geom(&parse(w.finish().as_bytes()).unwrap(), 1.0, 1.0); assert_eq!(parse_path(&back[0].d).len(), 4); } + + /// `a:cxnLst` positions are in the shape's space (the extent, with its guides), not the + /// path's own `w`/`h`; they come back as fractions of the box and go out again in EMU. + #[test] + fn connection_sites_read_in_the_shape_space_and_write_back() { + let x = br#" + + + + + + "#; + let d = parse(x).unwrap(); + let sites = read_cxn_lst(&d, 1000.0, 500.0); + assert_eq!( + sites, + vec![ConnectionSite::new(0.0, 0.5), ConnectionSite::new(1.0, 0.5), ConnectionSite::new(0.0, 0.0), ConnectionSite::new(0.5, 1.25)], + "a cxn without a pos keeps its index (a connector may point past it)" + ); + assert!(read_cxn_lst(&d, 0.0, 0.0).iter().all(|s| s.x == 0.0 && s.y == 0.0), "a zero extent gives no division by zero"); + let paths = read_cust_geom(&d, 1000.0, 500.0); + let mut w = Writer::new(); + write_cust_geom(&mut w, &paths, &sites, (1000, 500)); + let out = w.finish(); + assert!(out.contains(r#""#), "{out}"); + let mut w = Writer::new(); + write_cust_geom(&mut w, &paths, &[], (1000, 500)); + assert!(w.finish().contains(""), "no sites: the empty list as before"); + } } diff --git a/crates/drawingml/src/lib.rs b/crates/drawingml/src/lib.rs index 0227bd836..1810a5555 100644 --- a/crates/drawingml/src/lib.rs +++ b/crates/drawingml/src/lib.rs @@ -31,6 +31,7 @@ pub mod diagram; pub mod numfmt; pub mod part; pub mod preset; +mod preset_text; pub mod read; pub mod style; pub mod table; diff --git a/crates/drawingml/src/preset.rs b/crates/drawingml/src/preset.rs index 3e3b5af5a..ffe563aa5 100644 --- a/crates/drawingml/src/preset.rs +++ b/crates/drawingml/src/preset.rs @@ -16,7 +16,7 @@ mod shapes; pub use build::build; use builder::{Corner, Dir, Pb, corner_rect, fill, fill_eo, fill_only, mid_sites, orient, shade, simple, stroke_only, vis}; -pub use catalog::{CATALOG, Category, FillMode, Geometry, Handle, PresetInfo, SubPath, info, is_line_like, mitred_by_default}; +pub use catalog::{CATALOG, Category, FillMode, Geometry, Handle, PresetInfo, SubPath, adjust_defaults, info, is_line_like, mitred_by_default}; mod extra; diff --git a/crates/drawingml/src/preset/build.rs b/crates/drawingml/src/preset/build.rs index 95b833efd..f8996a93c 100644 --- a/crates/drawingml/src/preset/build.rs +++ b/crates/drawingml/src/preset/build.rs @@ -2,7 +2,7 @@ use super::builder::{ Corner, Dir, Pb, arrow, corner_rect, double_arrow, ellipse_sites, fill, fill_eo, fill_only, len_thick, mid_sites, orient, polygon_regular, shade, simple, spec_pts, star, star_text_rect, stroke_only, tip_handle, vis, }; -use super::catalog::{FillMode, Geometry, Handle, info}; +use super::catalog::{FillMode, Geometry, Handle}; use super::shapes::{action_button, cloud, math, ribbon, scroll, wedge_rect}; use super::{Arc, BezPath, Point, Rect, Vec2, extra}; @@ -26,17 +26,26 @@ impl Ctx<'_> { /// Builds the geometry for preset `name` at size `w`×`h` with adjust values `adj` (missing /// values use the preset's defaults). Unknown names return `None`; the caller falls back to `rect`. pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option { + let mut g = build_outline(name, w, h, adj)?; + // Text goes where presetShapeDefinitions put it, from the standard's own guide formulas. + if let Some(t) = crate::preset_text::text_rect(name, w.max(0.0), h.max(0.0), adj) { + g.text_rect = t; + } + Some(g) +} + +fn build_outline(name: &str, w: f64, h: f64, adj: &[f64]) -> Option { if !(w.is_finite() && h.is_finite()) { return None; } - let defaults = info(name).map(|i| i.defaults).unwrap_or(&[]); + let defaults = super::catalog::adjust_defaults(name); let c = Ctx { name, w: w.max(0.0), h: h.max(0.0), adj, defaults }; rect_and_triangle_shapes(&c) .or_else(|| polygon_and_symbol_shapes(&c)) .or_else(|| bracket_and_arrow_shapes(&c)) .or_else(|| flowchart_and_star_shapes(&c)) .or_else(|| callout_wave_and_connector_shapes(&c)) - .or_else(|| extra::build(name, c.w, c.h, &|i| c.a(i))) + .or_else(|| extra::Name::from_str(name).map(|n| extra::build(n, c.w, c.h, &|i| c.a(i)))) } fn rect_and_triangle_shapes(c: &Ctx) -> Option { @@ -1186,16 +1195,60 @@ fn callout_wave_and_connector_shapes(c: &Ctx) -> Option { let l = Pb::new().m(0.0, 0.0).l(w, 0.0).l(w, h).take(); Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] } } - "bentConnector3" | "bentConnector4" | "bentConnector5" => { - let x = w * a(0) / 1e5; - let l = Pb::new().m(0.0, 0.0).l(x, 0.0).l(x, h).l(w, h).take(); + // The elbows of ECMA-376's presetShapeDefinitions: 3 has one middle leg, 4 adds a + // horizontal one at `adj2`, 5 a third vertical one at `adj3`. 4 and 5 were drawn as 3. + "bentConnector3" => { + let x1 = w * a(0) / 1e5; + let l = Pb::new().m(0.0, 0.0).l(x1, 0.0).l(x1, h).l(w, h).take(); let mut g = Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] }; g.handles.push(Handle::new(0, Point::new(0.0, h / 2.0), Vec2::new(w / 1e5, 0.0), a(0), -1e6, 1e6)); g } - "curvedConnector2" | "curvedConnector3" | "curvedConnector4" | "curvedConnector5" => { - let x = w * if name == "curvedConnector2" { 100000.0 } else { a(0) } / 1e5; - let l = Pb::new().m(0.0, 0.0).c(x / 2.0, 0.0, x, h / 4.0, x, h / 2.0).c(x, h * 0.75, (x + w) / 2.0, h, w, h).take(); + "bentConnector4" => { + let (x1, y2) = (w * a(0) / 1e5, h * a(1) / 1e5); + let l = Pb::new().m(0.0, 0.0).l(x1, 0.0).l(x1, y2).l(w, y2).l(w, h).take(); + let mut g = Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] }; + g.handles.push(Handle::new(0, Point::new(0.0, y2 / 2.0), Vec2::new(w / 1e5, 0.0), a(0), -1e6, 1e6)); + g.handles.push(Handle::new(1, Point::new((x1 + w) / 2.0, 0.0), Vec2::new(0.0, h / 1e5), a(1), -1e6, 1e6)); + g + } + "bentConnector5" => { + let (x1, y2, x3) = (w * a(0) / 1e5, h * a(1) / 1e5, w * a(2) / 1e5); + let l = Pb::new().m(0.0, 0.0).l(x1, 0.0).l(x1, y2).l(x3, y2).l(x3, h).l(w, h).take(); + let mut g = Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] }; + g.handles.push(Handle::new(0, Point::new(0.0, y2 / 2.0), Vec2::new(w / 1e5, 0.0), a(0), -1e6, 1e6)); + g.handles.push(Handle::new(1, Point::new((x1 + x3) / 2.0, 0.0), Vec2::new(0.0, h / 1e5), a(1), -1e6, 1e6)); + g.handles.push(Handle::new(2, Point::new(0.0, (y2 + h) / 2.0), Vec2::new(w / 1e5, 0.0), a(2), -1e6, 1e6)); + g + } + // The curved connectors follow the same legs with cubic Béziers. + "curvedConnector2" => { + let l = Pb::new().m(0.0, 0.0).c(w / 2.0, 0.0, w, h / 2.0, w, h).take(); + Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] } + } + "curvedConnector3" => { + let x2 = w * a(0) / 1e5; + let l = Pb::new().m(0.0, 0.0).c(x2 / 2.0, 0.0, x2, h / 4.0, x2, h / 2.0).c(x2, h * 0.75, (x2 + w) / 2.0, h, w, h).take(); + Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] } + } + "curvedConnector4" => { + let (x2, y4) = (w * a(0) / 1e5, h * a(1) / 1e5); + let (x1, x3) = (x2 / 2.0, (w + x2) / 2.0); + let (x4, x5) = ((x2 + x3) / 2.0, (x3 + w) / 2.0); + let y1 = y4 / 2.0; + let (y2, y3, y5) = (y1 / 2.0, (y1 + y4) / 2.0, (h + y4) / 2.0); + let l = Pb::new().m(0.0, 0.0).c(x1, 0.0, x2, y2, x2, y1).c(x2, y3, x4, y4, x3, y4).c(x5, y4, w, y5, w, h).take(); + Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] } + } + "curvedConnector5" => { + let (x3, y4, x6) = (w * a(0) / 1e5, h * a(1) / 1e5, w * a(2) / 1e5); + let x1 = (x3 + x6) / 2.0; + let (x2, x4, x5, x7) = (x3 / 2.0, (x3 + x1) / 2.0, (x6 + x1) / 2.0, (x6 + w) / 2.0); + let y1 = y4 / 2.0; + let (y2, y3, y5) = (y1 / 2.0, (y1 + y4) / 2.0, (h + y4) / 2.0); + let (y6, y7) = ((y5 + y4) / 2.0, (y5 + h) / 2.0); + let l = + Pb::new().m(0.0, 0.0).c(x2, 0.0, x3, y2, x3, y1).c(x3, y3, x4, y4, x1, y4).c(x5, y4, x6, y6, x6, y5).c(x6, y7, x7, h, w, h).take(); Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] } } _ => return None, diff --git a/crates/drawingml/src/preset/catalog.rs b/crates/drawingml/src/preset/catalog.rs index ca090f94d..84f17b332 100644 --- a/crates/drawingml/src/preset/catalog.rs +++ b/crates/drawingml/src/preset/catalog.rs @@ -345,6 +345,22 @@ pub fn info(name: &str) -> Option<&'static PresetInfo> { CATALOG.iter().find(|p| p.name == name) } +/// Adjust-value defaults of presets we draw but don't offer in the gallery: PowerPoint routes +/// connectors with them, and a file may give some of the values and rely on these for the rest +/// (ECMA-376 presetShapeDefinitions). +static UNLISTED_DEFAULTS: &[(&str, &[f64])] = &[ + ("bentConnector4", &[50000.0, 50000.0]), + ("bentConnector5", &[50000.0, 50000.0, 50000.0]), + ("curvedConnector4", &[50000.0, 50000.0]), + ("curvedConnector5", &[50000.0, 50000.0, 50000.0]), +]; + +/// The adjust values preset `name` takes when a file or the user gives none: its gallery entry's, +/// or for a preset outside the gallery the spec's. Empty for a preset without adjust values. +pub fn adjust_defaults(name: &str) -> &'static [f64] { + info(name).map(|i| i.defaults).or_else(|| UNLISTED_DEFAULTS.iter().find(|(n, _)| *n == name).map(|(_, d)| *d)).unwrap_or(&[]) +} + /// Whether PowerPoint joins this preset's outline with sharp (miter) corners when the line names /// no join (it rounds them otherwise). Observed per preset on its export: the plus, the right, /// up and down block arrows (not the left one), chevrons, stars from five points up, the diff --git a/crates/drawingml/src/preset/extra.rs b/crates/drawingml/src/preset/extra.rs index 8c61e5f11..ad9efb3bd 100644 --- a/crates/drawingml/src/preset/extra.rs +++ b/crates/drawingml/src/preset/extra.rs @@ -7,17 +7,154 @@ use super::{ stroke_only, vis, }; -/// Builds the presets this module knows; `None` for any other name. -pub(super) fn build(name: &str, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Option { +/// The preset outlines this module builds, named for their Office Open XML identifiers. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Name { + LineInv, + NonIsoscelesTrapezoid, + PieWedge, + FlowChartOfflineStorage, + ChartX, + ChartStar, + ChartPlus, + CornerTabs, + SquareTabs, + PlaqueTabs, + Callout1, + Callout2, + Callout3, + AccentCallout1, + AccentCallout2, + AccentCallout3, + BorderCallout2, + BorderCallout3, + AccentBorderCallout1, + AccentBorderCallout2, + AccentBorderCallout3, + LeftRightArrowCallout, + UpDownArrowCallout, + QuadArrowCallout, + CurvedRightArrow, + CurvedLeftArrow, + CurvedUpArrow, + CurvedDownArrow, + SwooshArrow, + EllipseRibbon, + EllipseRibbon2, + LeftRightRibbon, + Gear6, + Gear9, + Funnel, + CircularArrow, + LeftCircularArrow, + LeftRightCircularArrow, +} + +impl Name { + /// The Office Open XML identifier (as it appears in files and the catalog). Only the tests + /// need the reverse mapping; production code parses strings with [`Name::from_str`]. + #[cfg(test)] + pub(super) fn ooxml(self) -> &'static str { + match self { + Name::LineInv => "lineInv", + Name::NonIsoscelesTrapezoid => "nonIsoscelesTrapezoid", + Name::PieWedge => "pieWedge", + Name::FlowChartOfflineStorage => "flowChartOfflineStorage", + Name::ChartX => "chartX", + Name::ChartStar => "chartStar", + Name::ChartPlus => "chartPlus", + Name::CornerTabs => "cornerTabs", + Name::SquareTabs => "squareTabs", + Name::PlaqueTabs => "plaqueTabs", + Name::Callout1 => "callout1", + Name::Callout2 => "callout2", + Name::Callout3 => "callout3", + Name::AccentCallout1 => "accentCallout1", + Name::AccentCallout2 => "accentCallout2", + Name::AccentCallout3 => "accentCallout3", + Name::BorderCallout2 => "borderCallout2", + Name::BorderCallout3 => "borderCallout3", + Name::AccentBorderCallout1 => "accentBorderCallout1", + Name::AccentBorderCallout2 => "accentBorderCallout2", + Name::AccentBorderCallout3 => "accentBorderCallout3", + Name::LeftRightArrowCallout => "leftRightArrowCallout", + Name::UpDownArrowCallout => "upDownArrowCallout", + Name::QuadArrowCallout => "quadArrowCallout", + Name::CurvedRightArrow => "curvedRightArrow", + Name::CurvedLeftArrow => "curvedLeftArrow", + Name::CurvedUpArrow => "curvedUpArrow", + Name::CurvedDownArrow => "curvedDownArrow", + Name::SwooshArrow => "swooshArrow", + Name::EllipseRibbon => "ellipseRibbon", + Name::EllipseRibbon2 => "ellipseRibbon2", + Name::LeftRightRibbon => "leftRightRibbon", + Name::Gear6 => "gear6", + Name::Gear9 => "gear9", + Name::Funnel => "funnel", + Name::CircularArrow => "circularArrow", + Name::LeftCircularArrow => "leftCircularArrow", + Name::LeftRightCircularArrow => "leftRightCircularArrow", + } + } + + /// Parse an Office Open XML identifier into its variant; `None` if unknown. + pub(super) fn from_str(name: &str) -> Option { + let e = match name { + "lineInv" => Name::LineInv, + "nonIsoscelesTrapezoid" => Name::NonIsoscelesTrapezoid, + "pieWedge" => Name::PieWedge, + "flowChartOfflineStorage" => Name::FlowChartOfflineStorage, + "chartX" => Name::ChartX, + "chartStar" => Name::ChartStar, + "chartPlus" => Name::ChartPlus, + "cornerTabs" => Name::CornerTabs, + "squareTabs" => Name::SquareTabs, + "plaqueTabs" => Name::PlaqueTabs, + "callout1" => Name::Callout1, + "callout2" => Name::Callout2, + "callout3" => Name::Callout3, + "accentCallout1" => Name::AccentCallout1, + "accentCallout2" => Name::AccentCallout2, + "accentCallout3" => Name::AccentCallout3, + "borderCallout2" => Name::BorderCallout2, + "borderCallout3" => Name::BorderCallout3, + "accentBorderCallout1" => Name::AccentBorderCallout1, + "accentBorderCallout2" => Name::AccentBorderCallout2, + "accentBorderCallout3" => Name::AccentBorderCallout3, + "leftRightArrowCallout" => Name::LeftRightArrowCallout, + "upDownArrowCallout" => Name::UpDownArrowCallout, + "quadArrowCallout" => Name::QuadArrowCallout, + "curvedRightArrow" => Name::CurvedRightArrow, + "curvedLeftArrow" => Name::CurvedLeftArrow, + "curvedUpArrow" => Name::CurvedUpArrow, + "curvedDownArrow" => Name::CurvedDownArrow, + "swooshArrow" => Name::SwooshArrow, + "ellipseRibbon" => Name::EllipseRibbon, + "ellipseRibbon2" => Name::EllipseRibbon2, + "leftRightRibbon" => Name::LeftRightRibbon, + "gear6" => Name::Gear6, + "gear9" => Name::Gear9, + "funnel" => Name::Funnel, + "circularArrow" => Name::CircularArrow, + "leftCircularArrow" => Name::LeftCircularArrow, + "leftRightCircularArrow" => Name::LeftRightCircularArrow, + _ => return None, + }; + Some(e) + } +} + +/// Builds the geometry for a preset in this module. +pub(super) fn build(name: Name, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Geometry { let ss = w.min(h); let full = Rect::new(0.0, 0.0, w, h); let mut b = Pb::new(); - let g = match name { - "lineInv" => { + match name { + Name::LineInv => { let l = b.m(0.0, h).l(w, 0.0).take(); Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::new(0.0, h), Point::new(w, 0.0)] } } - "nonIsoscelesTrapezoid" => { + Name::NonIsoscelesTrapezoid => { let max = 50000.0 * w / ss.max(1e-9); let (v1, v2) = (a(0).clamp(0.0, max), a(1).clamp(0.0, max)); let (d1, d2) = (ss * v1 / 1e5, ss * v2 / 1e5); @@ -26,11 +163,11 @@ pub(super) fn build(name: &str, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Opt g.handles.push(Handle::new(1, Point::new(w, 0.0), Vec2::new(-ss / 1e5, 0.0), v2, 0.0, max)); g } - "pieWedge" => { + Name::PieWedge => { b.m(0.0, h).arc(w, h, 180.0, 90.0).l(w, h).z(); simple(b.take(), Rect::new(w * 0.29289, h * 0.29289, w, h), w, h) } - "flowChartOfflineStorage" => { + Name::FlowChartOfflineStorage => { let tri = b.poly(&[(0.0, 0.0), (w, 0.0), (w / 2.0, h)]).take(); let bar = Pb::new().m(w * 0.4, h * 0.8).l(w * 0.6, h * 0.8).take(); Geometry { @@ -40,37 +177,37 @@ pub(super) fn build(name: &str, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Opt sites: vec![Point::new(w / 2.0, 0.0), Point::new(w / 4.0, h / 2.0), Point::new(w / 2.0, h), Point::new(w * 0.75, h / 2.0)], } } - "chartX" | "chartStar" | "chartPlus" => { + Name::ChartX | Name::ChartStar | Name::ChartPlus => { let body = corner_rect(w, h, [Corner::Square; 4]); let mut l = Pb::new(); - if name != "chartPlus" { + if name != Name::ChartPlus { l.m(0.0, 0.0).l(w, h).m(0.0, h).l(w, 0.0); } - if name != "chartX" { + if name != Name::ChartX { l.m(w / 2.0, 0.0).l(w / 2.0, h); } - if name == "chartPlus" { + if name == Name::ChartPlus { l.m(0.0, h / 2.0).l(w, h / 2.0); } Geometry { paths: vec![fill_only(body), stroke_only(l.take())], text_rect: full, handles: vec![], sites: mid_sites(w, h) } } - "cornerTabs" | "squareTabs" | "plaqueTabs" => { + Name::CornerTabs | Name::SquareTabs | Name::PlaqueTabs => { // Each tab's size is a twentieth of the diagonal. let d = (w.hypot(h) / 20.0).min(w / 2.0).min(h / 2.0); match name { - "cornerTabs" => { + Name::CornerTabs => { b.poly(&[(0.0, 0.0), (d, 0.0), (0.0, d)]); b.poly(&[(w - d, 0.0), (w, 0.0), (w, d)]); b.poly(&[(w, h - d), (w, h), (w - d, h)]); b.poly(&[(0.0, h - d), (d, h), (0.0, h)]); } - "squareTabs" => { + Name::SquareTabs => { b.poly(&[(0.0, 0.0), (d, 0.0), (d, d), (0.0, d)]); b.poly(&[(w - d, 0.0), (w, 0.0), (w, d), (w - d, d)]); b.poly(&[(w - d, h - d), (w, h - d), (w, h), (w - d, h)]); b.poly(&[(0.0, h - d), (d, h - d), (d, h), (0.0, h)]); } - _ => { + Name::PlaqueTabs => { if d > 0.0 { b.m(0.0, 0.0).l(d, 0.0).arc(d, d, 0.0, 90.0).z(); b.m(w, 0.0).l(w, d).arc(d, d, 90.0, 90.0).z(); @@ -80,29 +217,53 @@ pub(super) fn build(name: &str, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Opt b.m(0.0, 0.0).z(); } } + // The outer match only reaches this arm for the three tab names above. + _ => {} } Geometry { paths: vec![fill(b.take())], text_rect: full.inset(-d), handles: vec![], sites: mid_sites(w, h) } } - "callout1" - | "callout2" - | "callout3" - | "accentCallout1" - | "accentCallout2" - | "accentCallout3" - | "borderCallout2" - | "borderCallout3" - | "accentBorderCallout1" - | "accentBorderCallout2" - | "accentBorderCallout3" => { - let n = match name.chars().last() { - Some('2') => 2, - Some('3') => 3, + Name::Callout1 + | Name::Callout2 + | Name::Callout3 + | Name::AccentCallout1 + | Name::AccentCallout2 + | Name::AccentCallout3 + | Name::BorderCallout2 + | Name::BorderCallout3 + | Name::AccentBorderCallout1 + | Name::AccentBorderCallout2 + | Name::AccentBorderCallout3 => { + let n = match name { + Name::Callout2 | Name::AccentCallout2 | Name::BorderCallout2 | Name::AccentBorderCallout2 => 2, + Name::Callout3 | Name::AccentCallout3 | Name::BorderCallout3 | Name::AccentBorderCallout3 => 3, _ => 1, }; - line_callout(w, h, a, n + 1, name.starts_with("accent"), name.contains("order")) + line_callout( + w, + h, + a, + n + 1, + matches!( + name, + Name::AccentCallout1 + | Name::AccentCallout2 + | Name::AccentCallout3 + | Name::AccentBorderCallout1 + | Name::AccentBorderCallout2 + | Name::AccentBorderCallout3 + ), + matches!( + name, + Name::BorderCallout2 + | Name::BorderCallout3 + | Name::AccentBorderCallout1 + | Name::AccentBorderCallout2 + | Name::AccentBorderCallout3 + ), + ) } - "leftRightArrowCallout" | "upDownArrowCallout" => { - let vertical = name == "upDownArrowCallout"; + Name::LeftRightArrowCallout | Name::UpDownArrowCallout => { + let vertical = name == Name::UpDownArrowCallout; let (l, t) = if vertical { (h, w) } else { (w, h) }; let ss = l.min(t); let head = (ss * a(1).max(0.0) / 1e5).min(t / 2.0); @@ -135,7 +296,7 @@ pub(super) fn build(name: &str, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Opt let tr = orient(d, w, h, &[(b0, 0.0), (b1, t)]); simple(b.poly(&pts).take(), Rect::from_points(Point::new(tr[0].0, tr[0].1), Point::new(tr[1].0, tr[1].1)).abs(), w, h) } - "quadArrowCallout" => { + Name::QuadArrowCallout => { let head = (ss * a(1).max(0.0) / 1e5).min(ss / 2.0); let shaft = (ss * a(0).max(0.0) / 1e5).min(head * 2.0); let hl = (ss * a(2).max(0.0) / 1e5).min(ss / 2.0); @@ -180,16 +341,14 @@ pub(super) fn build(name: &str, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Opt ]; simple(b.poly(&pts).take(), Rect::new(bx0, by0, bx1, by1), w, h) } - "curvedRightArrow" | "curvedLeftArrow" | "curvedUpArrow" | "curvedDownArrow" => curved_arrow(name, w, h, a(0), a(1), a(2)), - "swooshArrow" => swoosh(w, h, a(0), a(1)), - "ellipseRibbon" | "ellipseRibbon2" => ellipse_ribbon(name == "ellipseRibbon2", w, h, a(0), a(1), a(2)), - "leftRightRibbon" => left_right_ribbon(w, h, a(0), a(1), a(2)), - "gear6" | "gear9" => gear(if name == "gear6" { 6 } else { 9 }, w, h, a(0), a(1)), - "funnel" => funnel(w, h), - "circularArrow" | "leftCircularArrow" | "leftRightCircularArrow" => circular_arrow(name, w, h, [a(0), a(1), a(2), a(3), a(4)]), - _ => return None, - }; - Some(g) + Name::CurvedRightArrow | Name::CurvedLeftArrow | Name::CurvedUpArrow | Name::CurvedDownArrow => curved_arrow(name, w, h, a(0), a(1), a(2)), + Name::SwooshArrow => swoosh(w, h, a(0), a(1)), + Name::EllipseRibbon | Name::EllipseRibbon2 => ellipse_ribbon(name == Name::EllipseRibbon2, w, h, a(0), a(1), a(2)), + Name::LeftRightRibbon => left_right_ribbon(w, h, a(0), a(1), a(2)), + Name::Gear6 | Name::Gear9 => gear(if name == Name::Gear6 { 6 } else { 9 }, w, h, a(0), a(1)), + Name::Funnel => funnel(w, h), + Name::CircularArrow | Name::LeftCircularArrow | Name::LeftRightCircularArrow => circular_arrow(name, w, h, [a(0), a(1), a(2), a(3), a(4)]), + } } /// An ellipse-centred frame for the circular arrows: the band's middle runs on the ellipse @@ -261,7 +420,7 @@ fn turn(a: f64, b: f64, dir: f64) -> f64 { /// The circular arrows: a band along an ellipse with one head (`circularArrow` clockwise, /// `leftCircularArrow` anticlockwise) or a head at each end (`leftRightCircularArrow`). /// Adjusts: band thickness, head sweep (degrees), end angle, start angle, head half-width. -fn circular_arrow(name: &str, w: f64, h: f64, a: [f64; 5]) -> Geometry { +fn circular_arrow(name: Name, w: f64, h: f64, a: [f64; 5]) -> Geometry { let ss = w.min(h); if ss <= 0.0 { return simple(corner_rect(w, h, [Corner::Square; 4]), Rect::new(0.0, 0.0, w, h), w, h); @@ -283,7 +442,7 @@ fn circular_arrow(name: &str, w: f64, h: f64, a: [f64; 5]) -> Geometry { }; let outer_arc = |b: &mut Pb, from: Point, to: Point, dir: f64| edge_arc(b, from, to, half, dir); let inner_arc = |b: &mut Pb, from: Point, to: Point, dir: f64| edge_arc(b, from, to, -half, dir); - if name == "leftRightCircularArrow" { + if name == Name::LeftRightCircularArrow { let hs = ring.head(start, -sweep); let he = ring.head(end, sweep); b.m(hs[0].x, hs[0].y); @@ -292,7 +451,7 @@ fn circular_arrow(name: &str, w: f64, h: f64, a: [f64; 5]) -> Geometry { inner_arc(&mut b, he[4], hs[4], -1.0); b.l(hs[3].x, hs[3].y).l(hs[2].x, hs[2].y).l(hs[1].x, hs[1].y).z(); } else { - let dir = if name == "circularArrow" { 1.0 } else { -1.0 }; + let dir = if name == Name::CircularArrow { 1.0 } else { -1.0 }; let he = ring.head(end, sweep); let (o0, i0) = (ring.at(start, half), ring.at(start, -half)); b.m(o0.x, o0.y); @@ -488,8 +647,8 @@ fn swoosh(w: f64, h: f64, a0: f64, a1: f64) -> Geometry { /// head side's edge, its horizontal radius is the full length, and the band has a constant /// vertical thickness. The back half (shaded) starts flat against the head side; the front half /// runs to the head, centred on the band where it ends. -fn curved_arrow(name: &str, w: f64, h: f64, a0: f64, a1: f64, a2: f64) -> Geometry { - let along_x = matches!(name, "curvedRightArrow" | "curvedLeftArrow"); +fn curved_arrow(name: Name, w: f64, h: f64, a0: f64, a1: f64, a2: f64) -> Geometry { + let along_x = matches!(name, Name::CurvedRightArrow | Name::CurvedLeftArrow); let (cw, ch) = if along_x { (w, h) } else { (h, w) }; let ss = cw.min(ch); let head = (ss * a1.max(0.0) / 1e5).min(ch / 2.0); @@ -509,9 +668,11 @@ fn curved_arrow(name: &str, w: f64, h: f64, a0: f64, a1: f64, a2: f64) -> Geomet let yc = yb + th / 2.0; front.l(cw - hl, yc - hh).l(cw, 2.0 * r + th / 2.0).l(cw - hl, yc + hh).l(cw - hl, yb + th).arc(cw, r, te, 180.0 - te).z(); let m = match name { - "curvedLeftArrow" => kurbo::Affine::new([-1.0, 0.0, 0.0, 1.0, w, 0.0]), - "curvedUpArrow" => kurbo::Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, h]), - "curvedDownArrow" => kurbo::Affine::new([0.0, 1.0, 1.0, 0.0, 0.0, 0.0]), + Name::CurvedLeftArrow => kurbo::Affine::new([-1.0, 0.0, 0.0, 1.0, w, 0.0]), + Name::CurvedUpArrow => kurbo::Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, h]), + Name::CurvedDownArrow => kurbo::Affine::new([0.0, 1.0, 1.0, 0.0, 0.0, 0.0]), + // `curved_arrow` is only ever called with the four curved-arrow names; anything else + // falls back to the unrotated (right-pointing) construction. _ => kurbo::Affine::IDENTITY, }; let (mut back, mut front) = (back.take(), front.take()); @@ -549,6 +710,7 @@ fn line_callout(w: f64, h: f64, a: &dyn Fn(usize) -> f64, pts: usize, accent: bo #[cfg(test)] mod tests { use super::super::{CATALOG, FillMode, build, info}; + use super::Name; use kurbo::{BezPath, PathEl, Point, Shape}; const NEW: [&str; 38] = [ @@ -596,6 +758,101 @@ mod tests { build(name, w, h, adj).map(|g| g.outline()).unwrap_or_default() } + /// Adjust sets the digests were taken at: the defaults (an empty slice) and explicit values, + /// so every adjust branch runs. + const GOLD_ADJ: [&[f64]; 2] = [&[], &[25_000.0, 10_000.0, 50_000.0, 30_000.0, 70_000.0, 20_000.0, 60_000.0, 40_000.0, 90_000.0, 15_000.0]]; + /// Sizes the digests were taken at: wide, tall and square. + const GOLD_SIZES: [(f64, f64); 4] = [(120.0, 80.0), (300.0, 50.0), (64.0, 64.0), (50.0, 300.0)]; + + /// FNV-1a over each outline's path elements, for every adjust set and size. Coordinates are + /// printed to 3 decimal places so last-bit libm differences between platforms do not change it. + fn outline_digest(name: &str) -> u64 { + use std::fmt::Write; + let mut text = String::new(); + for adj in GOLD_ADJ { + for (w, h) in GOLD_SIZES { + let g = build(name, w, h, adj).unwrap_or_else(|| panic!("{name} builds no outline")); + for el in g.outline().elements() { + let _ = match *el { + PathEl::MoveTo(p) => write!(text, "M{:.3},{:.3}", p.x, p.y), + PathEl::LineTo(p) => write!(text, "L{:.3},{:.3}", p.x, p.y), + PathEl::QuadTo(a, p) => write!(text, "Q{:.3},{:.3},{:.3},{:.3}", a.x, a.y, p.x, p.y), + PathEl::CurveTo(a, b, p) => write!(text, "C{:.3},{:.3},{:.3},{:.3},{:.3},{:.3}", a.x, a.y, b.x, b.y, p.x, p.y), + PathEl::ClosePath => write!(text, "Z"), + }; + } + text.push('|'); + } + } + text.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |h, b| (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3)) + } + + /// Digests of every name's outlines as the string dispatch built them (craft-libs 2b35d243; + /// DeckCraft's fc35ad67 gave other digests for curvedRightArrow and curvedDownArrow), so + /// this refactor is proven output-identical: any changed outline fails here. + const GOLDEN: [(&str, u64); 38] = [ + ("lineInv", 0x4a58a16923e54a4f), + ("nonIsoscelesTrapezoid", 0x79eca0b36812e282), + ("pieWedge", 0x2a0d3ffe5e9a481d), + ("leftRightArrowCallout", 0x9cd88f0ab55f76e5), + ("upDownArrowCallout", 0x568bc91708855007), + ("quadArrowCallout", 0xca59015298b1584b), + ("circularArrow", 0x4227aa468cfe990e), + ("leftCircularArrow", 0x6fc3779ce4b08914), + ("leftRightCircularArrow", 0x9dd43d9ba80e47df), + ("curvedRightArrow", 0x83e7dd251401715d), + ("curvedLeftArrow", 0x82472848de4a9fec), + ("curvedUpArrow", 0xcfbd88c1aea4bf9d), + ("curvedDownArrow", 0xa6f75eaf0ee71d49), + ("swooshArrow", 0x63ff193fdf1d0427), + ("callout1", 0xe7e0410f96282e8d), + ("callout2", 0xcaa7730106a9ee1b), + ("callout3", 0x4bc625449c0dfb30), + ("accentCallout1", 0x64f4c3eb775542fb), + ("accentCallout2", 0x2e3caa5b473ab4c7), + ("accentCallout3", 0x6ca203b149a2618e), + ("borderCallout2", 0xcaa7730106a9ee1b), + ("borderCallout3", 0x4bc625449c0dfb30), + ("accentBorderCallout1", 0x64f4c3eb775542fb), + ("accentBorderCallout2", 0x2e3caa5b473ab4c7), + ("accentBorderCallout3", 0x6ca203b149a2618e), + ("ellipseRibbon", 0x7d259fa067028102), + ("ellipseRibbon2", 0x3536b31ad0c95cf5), + ("leftRightRibbon", 0x6adc78f12d1cab9c), + ("flowChartOfflineStorage", 0x36960029a3770971), + ("gear6", 0x62b46d43b67decea), + ("gear9", 0xa42f43c2ae80314b), + ("funnel", 0x9327e2c2e99ab7d3), + ("cornerTabs", 0xe236f19aac60a20d), + ("squareTabs", 0xc3aea5acb50d6ccd), + ("plaqueTabs", 0xc64ebb2d06640521), + ("chartX", 0x6b2a9ff342247551), + ("chartStar", 0x6ebb545c66f313dd), + ("chartPlus", 0x979031aae16250d7), + ]; + + /// Every name builds the same outline as before the `Name` enum, and unknown names build nothing. + #[test] + fn outlines_match_the_string_dispatch() { + for (name, want) in GOLDEN { + let have = outline_digest(name); + assert_eq!(have, want, "{name} outline changed"); + } + assert!(build("noSuchShape", 100.0, 100.0, &[]).is_none()); + } + + /// Every name this module builds round-trips through its OOXML identifier, and unknown + /// strings do not parse. + #[test] + fn name_round_trips_through_ooxml() { + for s in NEW { + let n = Name::from_str(s).unwrap_or_else(|| panic!("{s} missing from Name")); + assert_eq!(n.ooxml(), s, "{s} maps to a different OOXML id"); + } + assert!(Name::from_str("noSuchShape").is_none()); + assert!(Name::from_str("").is_none()); + } + /// Distance from `p` to the nearest point of the (flattened) outline. fn dist(path: &BezPath, p: Point) -> f64 { let mut best = f64::MAX; diff --git a/crates/drawingml/src/preset/tests.rs b/crates/drawingml/src/preset/tests.rs index dc18dcdb5..0427d1a3e 100644 --- a/crates/drawingml/src/preset/tests.rs +++ b/crates/drawingml/src/preset/tests.rs @@ -418,3 +418,71 @@ fn wedge_callout_mirrors_with_its_tip() { assert!(a.iter().any(|p| (p.x - x).abs() < 1e-6 && (p.y - h).abs() < 1e-6), "no wedge corner at ({x}, {h}): {a:?}"); } } + +/// The vertices, and the cubic segments' end points, of preset `name`'s path. +fn path_points(name: &str, w: f64, h: f64, adj: &[f64]) -> Vec<(f64, f64)> { + let g = build(name, w, h, adj).unwrap(); + g.paths[0].path.elements().iter().filter_map(|e| e.end_point()).map(|p| (p.x, p.y)).collect() +} + +// ECMA-376 presetShapeDefinitions: bentConnector4 runs l,t → x1,t → x1,y2 → r,y2 → r,b and +// bentConnector5 l,t → x1,t → x1,y2 → x3,y2 → x3,b → r,b, with x1 = w·adj1, y2 = h·adj2, +// x3 = w·adj3 (in 100000ths). Both were drawn as bentConnector3, with only adj1. +#[test] +fn the_three_and_four_bend_elbows_follow_their_adjust_values() { + assert_eq!(path_points("bentConnector4", 200.0, 100.0, &[]), [(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (200.0, 50.0), (200.0, 100.0)]); + assert_eq!( + path_points("bentConnector4", 200.0, 100.0, &[20000.0, 80000.0]), + [(0.0, 0.0), (40.0, 0.0), (40.0, 80.0), (200.0, 80.0), (200.0, 100.0)] + ); + // Outside 0..100000, as PowerPoint routes a connector around a shape. + assert_eq!( + path_points("bentConnector4", 200.0, 100.0, &[-40000.0, 120000.0]), + [(0.0, 0.0), (-80.0, 0.0), (-80.0, 120.0), (200.0, 120.0), (200.0, 100.0)] + ); + assert_eq!( + path_points("bentConnector5", 200.0, 100.0, &[]), + [(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (100.0, 50.0), (100.0, 100.0), (200.0, 100.0)] + ); + assert_eq!( + path_points("bentConnector5", 200.0, 100.0, &[25000.0, 75000.0, 60000.0]), + [(0.0, 0.0), (50.0, 0.0), (50.0, 75.0), (120.0, 75.0), (120.0, 100.0), (200.0, 100.0)] + ); + // The handles sit where the spec puts them: on the leg each value moves. + let g = build("bentConnector5", 200.0, 100.0, &[25000.0, 75000.0, 60000.0]).unwrap(); + let pos: Vec<(f64, f64)> = g.handles.iter().map(|h| (h.pos.x, h.pos.y)).collect(); + assert_eq!(pos, [(50.0, 37.5), (85.0, 75.0), (120.0, 87.5)]); +} + +// curvedConnector2 is one cubic (l,t)…(r,b) through (wd2,t) and (r,hd2); 4 and 5 bend at the same +// places as the elbows, three and four cubics. 2 was drawn as 3 at adj1 = 100%, 4 and 5 as 3. +#[test] +fn the_curved_connectors_follow_their_adjust_values() { + let g = build("curvedConnector2", 200.0, 100.0, &[]).unwrap(); + let els = g.paths[0].path.elements(); + assert_eq!(els.len(), 2, "a move and one cubic: {els:?}"); + assert_eq!(els[1], kurbo::PathEl::CurveTo(Point::new(100.0, 0.0), Point::new(200.0, 50.0), Point::new(200.0, 100.0))); + // the joints: (x2, y1), (x3, y4), (r, b) for 4; (x3, y1), (x1, y4), (x6, y5), (r, b) for 5 + assert_eq!(path_points("curvedConnector4", 200.0, 100.0, &[]), [(0.0, 0.0), (100.0, 25.0), (150.0, 50.0), (200.0, 100.0)]); + assert_eq!(path_points("curvedConnector4", 200.0, 100.0, &[20000.0, 80000.0]), [(0.0, 0.0), (40.0, 40.0), (120.0, 80.0), (200.0, 100.0)]); + assert_eq!(path_points("curvedConnector5", 200.0, 100.0, &[]), [(0.0, 0.0), (100.0, 25.0), (100.0, 50.0), (100.0, 75.0), (200.0, 100.0)]); + assert_eq!( + path_points("curvedConnector5", 200.0, 100.0, &[25000.0, 75000.0, 60000.0]), + [(0.0, 0.0), (50.0, 37.5), (85.0, 75.0), (120.0, 87.5), (200.0, 100.0)] + ); +} + +// None of these is in the gallery, so their defaults came from nowhere: a file giving no adjust +// values, or only some, drew them at 0. +#[test] +fn connectors_outside_the_gallery_take_the_spec_defaults() { + for (name, n) in [("bentConnector4", 2), ("bentConnector5", 3), ("curvedConnector4", 2), ("curvedConnector5", 3)] { + assert!(info(name).is_none(), "{name} is not in the gallery"); + assert_eq!(adjust_defaults(name), vec![50000.0; n].as_slice(), "{name}"); + for v in [f64::NAN, f64::INFINITY, -1e12, 1e12] { + assert!(build(name, 120.0, 80.0, &[v; 3]).is_some(), "{name} {v}"); + } + } + assert_eq!(adjust_defaults("bentConnector3"), [50000.0]); + assert!(adjust_defaults("rect").is_empty()); +} diff --git a/crates/drawingml/src/preset_text.rs b/crates/drawingml/src/preset_text.rs new file mode 100644 index 000000000..ba251eb7c --- /dev/null +++ b/crates/drawingml/src/preset_text.rs @@ -0,0 +1,134 @@ +//! Text rectangles of the preset geometries, evaluated from ECMA-376's presetShapeDefinitions +//! guide formulas (`preset_text.txt`, generated by `tests/fixtures/make_preset_guides.py`): each +//! preset's adjust values, the guides its `` uses, and the rectangle. + +use std::collections::HashMap; +use std::sync::OnceLock; + +use kurbo::Rect; + +struct Def { + adj: Vec<(&'static str, &'static str)>, + guides: Vec<(&'static str, &'static str)>, + rect: [&'static str; 4], +} + +fn defs() -> &'static HashMap<&'static str, Def> { + static DEFS: OnceLock> = OnceLock::new(); + DEFS.get_or_init(|| { + let pairs = |s: &'static str| -> Vec<(&'static str, &'static str)> { + s.split(';').filter(|x| !x.is_empty()).filter_map(|x| x.split_once('=')).collect() + }; + include_str!("preset_text.txt") + .lines() + .filter(|l| !l.starts_with('#')) + .filter_map(|l| { + let f: Vec<&'static str> = l.split('|').collect(); + let r: Vec<&'static str> = f.get(3)?.split_whitespace().collect(); + Some((*f.first()?, Def { adj: pairs(f.get(1)?), guides: pairs(f.get(2)?), rect: [r.first()?, r.get(1)?, r.get(2)?, r.get(3)?] })) + }) + .collect() + }) +} + +fn eval(f: &str, v: &HashMap<&str, f64>) -> f64 { + let mut p = f.split_whitespace(); + let op = p.next().unwrap_or("val"); + let a: Vec = p.map(|x| v.get(x).copied().or_else(|| x.parse().ok()).unwrap_or(0.0)).collect(); + let g = |i: usize| a.get(i).copied().unwrap_or(0.0); + let rad = |x: f64| (x / 60000.0).to_radians(); + let div = |n: f64, d: f64| if d != 0.0 { n / d } else { 0.0 }; + let r = match op { + "val" => g(0), + "*/" => div(g(0) * g(1), g(2)), + "+-" => g(0) + g(1) - g(2), + "+/" => div(g(0) + g(1), g(2)), + "?:" => { + if g(0) > 0.0 { + g(1) + } else { + g(2) + } + } + "abs" => g(0).abs(), + "at2" => g(1).atan2(g(0)).to_degrees() * 60000.0, + "cat2" => g(0) * g(2).atan2(g(1)).cos(), + "sat2" => g(0) * g(2).atan2(g(1)).sin(), + "cos" => g(0) * rad(g(1)).cos(), + "sin" => g(0) * rad(g(1)).sin(), + "tan" => g(0) * rad(g(1)).tan(), + "max" => g(0).max(g(1)), + "min" => g(0).min(g(1)), + "mod" => (g(0) * g(0) + g(1) * g(1) + g(2) * g(2)).sqrt(), + "pin" => { + if g(1) < g(0) { + g(0) + } else if g(1) > g(2) { + g(2) + } else { + g(1) + } + } + "sqrt" => g(0).max(0.0).sqrt(), + _ => 0.0, + }; + if r.is_finite() { r } else { 0.0 } +} + +/// The standard's text rectangle for preset `name` in a `w` × `h` box with adjust values `adj` +/// (in handle order; missing ones take the standard's defaults). `None` for presets it doesn't +/// define or without a rectangle. +pub(crate) fn text_rect(name: &str, w: f64, h: f64, adj: &[f64]) -> Option { + let d = defs().get(name)?; + let ss = w.min(h); + let mut v: HashMap<&str, f64> = HashMap::with_capacity(64); + for (k, x) in [ + ("w", w), + ("h", h), + ("l", 0.0), + ("t", 0.0), + ("r", w), + ("b", h), + ("ss", ss), + ("ls", w.max(h)), + ("hc", w / 2.0), + ("vc", h / 2.0), + ("cd2", 10_800_000.0), + ("cd4", 5_400_000.0), + ("cd8", 2_700_000.0), + ("3cd4", 16_200_000.0), + ("3cd8", 8_100_000.0), + ("5cd8", 13_500_000.0), + ("7cd8", 18_900_000.0), + ] { + v.insert(k, x); + } + const DIVS: [(&str, &str, &str, f64); 10] = [ + ("wd2", "hd2", "ssd2", 2.0), + ("wd3", "hd3", "ssd3", 3.0), + ("wd4", "hd4", "ssd4", 4.0), + ("wd5", "hd5", "ssd5", 5.0), + ("wd6", "hd6", "ssd6", 6.0), + ("wd8", "hd8", "ssd8", 8.0), + ("wd10", "hd10", "ssd10", 10.0), + ("wd12", "hd12", "ssd12", 12.0), + ("wd16", "hd16", "ssd16", 16.0), + ("wd32", "hd32", "ssd32", 32.0), + ]; + for (a, b, c, n) in DIVS { + v.insert(a, w / n); + v.insert(b, h / n); + v.insert(c, ss / n); + } + for (i, (k, f)) in d.adj.iter().enumerate() { + let x = adj.get(i).copied().filter(|x| x.is_finite()).unwrap_or_else(|| eval(f, &v)); + v.insert(k, x); + } + for (k, f) in &d.guides { + let x = eval(f, &v); + v.insert(k, x); + } + let get = |k: &str| v.get(k).copied().or_else(|| k.parse().ok()).unwrap_or(0.0); + let [l, t, r, b] = d.rect.map(get); + Some(Rect::new(l, t, r, b)) +} diff --git a/crates/drawingml/src/preset_text.txt b/crates/drawingml/src/preset_text.txt new file mode 100644 index 000000000..eeae4e0e9 --- /dev/null +++ b/crates/drawingml/src/preset_text.txt @@ -0,0 +1,183 @@ +# Generated by crates/drawingml/tests/fixtures/make_preset_guides.py from presetShapeDefinitions.xml. +accentBorderCallout1|adj1=val 18750;adj2=val -8333;adj3=val 112500;adj4=val -38333||l t r b +accentBorderCallout2|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 112500;adj6=val -46667||l t r b +accentBorderCallout3|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 100000;adj6=val -16667;adj7=val 112963;adj8=val -8333||l t r b +accentCallout1|adj1=val 18750;adj2=val -8333;adj3=val 112500;adj4=val -38333||l t r b +accentCallout2|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 112500;adj6=val -46667||l t r b +accentCallout3|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 100000;adj6=val -16667;adj7=val 112963;adj8=val -8333||l t r b +actionButtonBackPrevious|||l t r b +actionButtonBeginning|||l t r b +actionButtonBlank|||l t r b +actionButtonDocument|||l t r b +actionButtonEnd|||l t r b +actionButtonForwardNext|||l t r b +actionButtonHelp|||l t r b +actionButtonHome|||l t r b +actionButtonInformation|||l t r b +actionButtonMovie|||l t r b +actionButtonReturn|||l t r b +actionButtonSound|||l t r b +arc|adj1=val 16200000;adj2=val 0|stAng=pin 0 adj1 21599999;enAng=pin 0 adj2 21599999;sw11=+- enAng 0 stAng;sw12=+- sw11 21600000 0;swAng=?: sw11 sw11 sw12;wt1=sin wd2 stAng;ht1=cos hd2 stAng;dx1=cat2 wd2 ht1 wt1;dy1=sat2 hd2 ht1 wt1;wt2=sin wd2 enAng;ht2=cos hd2 enAng;dx2=cat2 wd2 ht2 wt2;dy2=sat2 hd2 ht2 wt2;x1=+- hc dx1 0;y1=+- vc dy1 0;x2=+- hc dx2 0;y2=+- vc dy2 0;sw0=+- 21600000 0 stAng;da1=+- swAng 0 sw0;g1=max x1 x2;ir=?: da1 r g1;sw1=+- cd4 0 stAng;sw2=+- 27000000 0 stAng;sw3=?: sw1 sw1 sw2;da2=+- swAng 0 sw3;g5=max y1 y2;ib=?: da2 b g5;sw4=+- cd2 0 stAng;sw5=+- 32400000 0 stAng;sw6=?: sw4 sw4 sw5;da3=+- swAng 0 sw6;g9=min x1 x2;il=?: da3 l g9;sw7=+- 3cd4 0 stAng;sw8=+- 37800000 0 stAng;sw9=?: sw7 sw7 sw8;da4=+- swAng 0 sw9;g13=min y1 y2;it=?: da4 t g13|il it ir ib +bentArrow|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 43750||l t r b +bentConnector2|||l t r b +bentConnector3|adj1=val 50000||l t r b +bentConnector4|adj1=val 50000;adj2=val 50000||l t r b +bentConnector5|adj1=val 50000;adj2=val 50000;adj3=val 50000||l t r b +bentUpArrow|adj1=val 25000;adj2=val 25000;adj3=val 25000|a1=pin 0 adj1 50000;a2=pin 0 adj2 50000;dx3=*/ ss a2 100000;x3=+- r 0 dx3;dx2=*/ ss a1 200000;x4=+- x3 dx2 0;dy2=*/ ss a1 100000;y2=+- b 0 dy2|l y2 x4 b +bevel|adj=val 12500|a=pin 0 adj 50000;x1=*/ ss a 100000;x2=+- r 0 x1;y2=+- b 0 x1|x1 x1 x2 y2 +blockArc|adj1=val 10800000;adj2=val 0;adj3=val 25000|stAng=pin 0 adj1 21599999;istAng=pin 0 adj2 21599999;a3=pin 0 adj3 50000;sw11=+- istAng 0 stAng;sw12=+- sw11 21600000 0;swAng=?: sw11 sw11 sw12;wt1=sin wd2 stAng;ht1=cos hd2 stAng;wt3=sin wd2 istAng;ht3=cos hd2 istAng;dx1=cat2 wd2 ht1 wt1;dy1=sat2 hd2 ht1 wt1;dx3=cat2 wd2 ht3 wt3;dy3=sat2 hd2 ht3 wt3;x1=+- hc dx1 0;y1=+- vc dy1 0;x3=+- hc dx3 0;y3=+- vc dy3 0;dr=*/ ss a3 100000;iwd2=+- wd2 0 dr;ihd2=+- hd2 0 dr;wt2=sin iwd2 istAng;ht2=cos ihd2 istAng;wt4=sin iwd2 stAng;ht4=cos ihd2 stAng;dx2=cat2 iwd2 ht2 wt2;dy2=sat2 ihd2 ht2 wt2;dx4=cat2 iwd2 ht4 wt4;dy4=sat2 ihd2 ht4 wt4;x2=+- hc dx2 0;y2=+- vc dy2 0;x4=+- hc dx4 0;y4=+- vc dy4 0;sw0=+- 21600000 0 stAng;da1=+- swAng 0 sw0;g1=max x1 x2;g2=max x3 x4;g3=max g1 g2;ir=?: da1 r g3;sw1=+- cd4 0 stAng;sw2=+- 27000000 0 stAng;sw3=?: sw1 sw1 sw2;da2=+- swAng 0 sw3;g5=max y1 y2;g6=max y3 y4;g7=max g5 g6;ib=?: da2 b g7;sw4=+- cd2 0 stAng;sw5=+- 32400000 0 stAng;sw6=?: sw4 sw4 sw5;da3=+- swAng 0 sw6;g9=min x1 x2;g10=min x3 x4;g11=min g9 g10;il=?: da3 l g11;sw7=+- 3cd4 0 stAng;sw8=+- 37800000 0 stAng;sw9=?: sw7 sw7 sw8;da4=+- swAng 0 sw9;g13=min y1 y2;g14=min y3 y4;g15=min g13 g14;it=?: da4 t g15|il it ir ib +borderCallout1|adj1=val 18750;adj2=val -8333;adj3=val 112500;adj4=val -38333||l t r b +borderCallout2|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 112500;adj6=val -46667||l t r b +borderCallout3|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 100000;adj6=val -16667;adj7=val 112963;adj8=val -8333||l t r b +bracePair|adj=val 8333|a=pin 0 adj 25000;x1=*/ ss a 100000;it=*/ x1 29289 100000;il=+- x1 it 0;ir=+- r 0 il;ib=+- b 0 it|il il ir ib +bracketPair|adj=val 16667|a=pin 0 adj 50000;x1=*/ ss a 100000;il=*/ x1 29289 100000;ir=+- r 0 il;ib=+- b 0 il|il il ir ib +callout1|adj1=val 18750;adj2=val -8333;adj3=val 112500;adj4=val -38333||l t r b +callout2|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 112500;adj6=val -46667||l t r b +callout3|adj1=val 18750;adj2=val -8333;adj3=val 18750;adj4=val -16667;adj5=val 100000;adj6=val -16667;adj7=val 112963;adj8=val -8333||l t r b +can|adj=val 25000|maxAdj=*/ 50000 h ss;a=pin 0 adj maxAdj;y1=*/ ss a 200000;y2=+- y1 y1 0;y3=+- b 0 y1|l y2 r y3 +chevron|adj=val 50000|maxAdj=*/ 100000 w ss;a=pin 0 adj maxAdj;x1=*/ ss a 100000;x2=+- r 0 x1;dx=+- x2 0 x1;il=?: dx x1 l;ir=?: dx x2 r|il t ir b +chord|adj1=val 2700000;adj2=val 16200000|idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +circularArrow|adj1=val 12500;adj2=val 1142319;adj3=val 20457681;adj4=val 10800000;adj5=val 12500|a5=pin 0 adj5 25000;maxAdj1=*/ a5 2 1;a1=pin 0 adj1 maxAdj1;th=*/ ss a1 100000;thh=*/ ss a5 100000;th2=*/ th 1 2;rw1=+- wd2 th2 thh;rh1=+- hd2 th2 thh;idx=cos rw1 2700000;idy=sin rh1 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +cloud||il=*/ w 2977 21600;it=*/ h 3262 21600;ir=*/ w 17087 21600;ib=*/ h 17337 21600|il it ir ib +cloudCallout|adj1=val -20833;adj2=val 62500|il=*/ w 2977 21600;it=*/ h 3262 21600;ir=*/ w 17087 21600;ib=*/ h 17337 21600|il it ir ib +corner|adj1=val 50000;adj2=val 50000|maxAdj1=*/ 100000 h ss;maxAdj2=*/ 100000 w ss;a1=pin 0 adj1 maxAdj1;a2=pin 0 adj2 maxAdj2;x1=*/ ss a2 100000;dy1=*/ ss a1 100000;y1=+- b 0 dy1;d=+- w 0 h;it=?: d y1 t;ir=?: d r x1|l it ir b +cornerTabs||md=mod w h 0;dx=*/ 1 md 20;y1=+- 0 b dx;x1=+- 0 r dx|dx dx x1 y1 +cube|adj=val 25000|a=pin 0 adj 100000;y1=*/ ss a 100000;x4=+- r 0 y1|l y1 x4 b +curvedConnector2|||l t r b +curvedConnector3|adj1=val 50000||l t r b +curvedConnector4|adj1=val 50000;adj2=val 50000||l t r b +curvedConnector5|adj1=val 50000;adj2=val 50000;adj3=val 50000||l t r b +curvedDownArrow|adj1=val 25000;adj2=val 50000;adj3=val 25000||l t r b +curvedLeftArrow|adj1=val 25000;adj2=val 50000;adj3=val 25000||l t r b +curvedRightArrow|adj1=val 25000;adj2=val 50000;adj3=val 25000||l t r b +curvedUpArrow|adj1=val 25000;adj2=val 50000;adj3=val 25000||l t r b +decagon|vf=val 105146|shd2=*/ hd2 vf 100000;dx1=cos wd2 2160000;x1=+- hc 0 dx1;x4=+- hc dx1 0;dy2=sin shd2 2160000;y2=+- vc 0 dy2;y3=+- vc dy2 0|x1 y2 x4 y3 +diagStripe|adj=val 50000|a=pin 0 adj 100000;x2=*/ w a 100000;x3=+/ x2 r 2;y2=*/ h a 100000;y3=+/ y2 b 2|l t x3 y3 +diamond||ir=*/ w 3 4;ib=*/ h 3 4|wd4 hd4 ir ib +dodecagon||x1=*/ w 2894 21600;x4=*/ w 18706 21600;y1=*/ h 2894 21600;y4=*/ h 18706 21600|x1 y1 x4 y4 +donut|adj=val 25000|idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +doubleWave|adj1=val 6250;adj2=val 0|a1=pin 0 adj1 12500;a2=pin -10000 adj2 10000;of2=*/ w a2 50000;dx2=?: of2 0 of2;x2=+- l 0 dx2;dx8=?: of2 of2 0;x8=+- r 0 dx8;x9=+- l dx8 0;x15=+- r dx2 0;il=max x2 x9;ir=min x8 x15;it=*/ h a1 50000;ib=+- b 0 it|il it ir ib +downArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 100000 h ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;dy1=*/ ss a2 100000;y1=+- b 0 dy1;dx1=*/ w a1 200000;x1=+- hc 0 dx1;x2=+- hc dx1 0;dy2=*/ x1 dy1 wd2;y2=+- y1 dy2 0|x1 t x2 y2 +downArrowCallout|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 64977|maxAdj3=*/ 100000 h ss;a3=pin 0 adj3 maxAdj3;q2=*/ a3 ss h;maxAdj4=+- 100000 0 q2;a4=pin 0 adj4 maxAdj4;y2=*/ h a4 100000|l t r y2 +ellipse||idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +ellipseRibbon|adj1=val 25000;adj2=val 50000;adj3=val 12500|a1=pin 0 adj1 100000;a2=pin 25000 adj2 75000;q10=+- 100000 0 a1;q11=*/ q10 1 2;q12=+- a1 0 q11;minAdj3=max 0 q12;a3=pin minAdj3 adj3 a1;dx2=*/ w a2 200000;x2=+- hc 0 dx2;x5=+- r 0 x2;dy1=*/ h a3 100000;f1=*/ 4 dy1 w;q1=*/ x3 x3 w;q1=*/ h a1 100000;dy3=+- q1 0 dy1;q3=*/ x2 x2 w;q4=+- x2 0 q3;q5=*/ f1 q4 1;y3=+- q5 dy3 0;rh=+- b 0 q1;y6=+- y3 rh 0|x2 q1 x5 y6 +ellipseRibbon2|adj1=val 25000;adj2=val 50000;adj3=val 12500|a1=pin 0 adj1 100000;a2=pin 25000 adj2 75000;q10=+- 100000 0 a1;q11=*/ q10 1 2;q12=+- a1 0 q11;minAdj3=max 0 q12;a3=pin minAdj3 adj3 a1;dx2=*/ w a2 200000;x2=+- hc 0 dx2;x5=+- r 0 x2;dy1=*/ h a3 100000;f1=*/ 4 dy1 w;q1=*/ x3 x3 w;q1=*/ h a1 100000;dy3=+- q1 0 dy1;q3=*/ x2 x2 w;q4=+- x2 0 q3;q5=*/ f1 q4 1;u3=+- q5 dy3 0;rh=+- b 0 q1;u6=+- u3 rh 0;y6=+- b 0 u6|x2 y6 x5 rh +flowChartAlternateProcess||il=*/ ssd6 29289 100000;ir=+- r 0 il;ib=+- b 0 il|il il ir ib +flowChartCollate||ir=*/ w 3 4;ib=*/ h 3 4|wd4 hd4 ir ib +flowChartConnector||idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +flowChartDecision||ir=*/ w 3 4;ib=*/ h 3 4|wd4 hd4 ir ib +flowChartDelay||idx=cos wd2 2700000;idy=sin hd2 2700000;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|l it ir ib +flowChartDisplay||x2=*/ w 5 6|wd6 t x2 b +flowChartDocument||y1=*/ h 17322 21600|l t r y1 +flowChartExtract||x2=*/ w 3 4|wd4 vc x2 b +flowChartInputOutput||x5=*/ w 4 5|wd5 t x5 b +flowChartInternalStorage|||wd8 hd8 r b +flowChartMagneticDisk||y3=*/ h 5 6|l hd3 r y3 +flowChartMagneticDrum||x2=*/ w 2 3|wd6 t x2 b +flowChartMagneticTape||idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +flowChartManualInput|||l hd5 r b +flowChartManualOperation||x3=*/ w 4 5|wd5 t x3 b +flowChartMerge||x2=*/ w 3 4|wd4 t x2 vc +flowChartMultidocument||y2=*/ h 3675 21600;y8=*/ h 20782 21600;x5=*/ w 18595 21600|l y2 x5 y8 +flowChartOfflineStorage||x4=*/ w 3 4|wd4 t x4 vc +flowChartOffpageConnector||y1=*/ h 4 5|l t r y1 +flowChartOnlineStorage||x2=*/ w 5 6|wd6 t x2 b +flowChartOr||idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +flowChartPredefinedProcess||x2=*/ w 7 8|wd8 t x2 b +flowChartPreparation||x2=*/ w 4 5|wd5 t x2 b +flowChartProcess|||l t r b +flowChartPunchedCard|||l hd5 r b +flowChartPunchedTape||ib=*/ h 4 5|l hd5 r ib +flowChartSort||ir=*/ w 3 4;ib=*/ h 3 4|wd4 hd4 ir ib +flowChartSummingJunction||idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +flowChartTerminator||il=*/ w 1018 21600;ir=*/ w 20582 21600;it=*/ h 3163 21600;ib=*/ h 18437 21600|il it ir ib +foldedCorner|adj=val 16667|a=pin 0 adj 50000;dy2=*/ ss a 100000;y2=+- b 0 dy2|l t r y2 +frame|adj1=val 12500|a1=pin 0 adj1 50000;x1=*/ ss a1 100000;x4=+- r 0 x1;y4=+- b 0 x1|x1 x1 x4 y4 +funnel|||l t r b +gear6|adj1=val 15000;adj2=val 3526|a1=pin 0 adj1 20000;a2=pin 0 adj2 5358;th=*/ ss a1 100000;lFD=*/ ss a2 100000;th2=*/ th 1 2;l2=*/ lFD 1 2;l3=+- th2 l2 0;rh=+- hd2 0 th;rw=+- wd2 0 th;dr=+- rw 0 rh;maxr=?: dr rh rw;ha=at2 maxr l3;aA1=+- 19800000 0 ha;aD1=+- 19800000 ha 0;ta11=cos rw aA1;ta12=sin rh aA1;bA1=at2 ta11 ta12;cta1=cos rh bA1;sta1=sin rw bA1;ma1=mod cta1 sta1 0;na1=*/ rw rh ma1;dxa1=cos na1 bA1;dya1=sin na1 bA1;xA1=+- hc dxa1 0;yA1=+- vc dya1 0;td11=cos rw aD1;td12=sin rh aD1;bD1=at2 td11 td12;ctd1=cos rh bD1;std1=sin rw bD1;md1=mod ctd1 std1 0;nd1=*/ rw rh md1;dxd1=cos nd1 bD1;dyd1=sin nd1 bD1;xD1=+- hc dxd1 0;yD1=+- vc dyd1 0;xAD1=+- xA1 0 xD1;yAD1=+- yA1 0 yD1;a1=at2 yAD1 xAD1;yD2=+- h 0 yA1;xD5=+- w 0 xA1|xD5 yA1 xA1 yD2 +gear9|adj1=val 10000;adj2=val 1763|a1=pin 0 adj1 20000;a2=pin 0 adj2 2679;th=*/ ss a1 100000;lFD=*/ ss a2 100000;th2=*/ th 1 2;l2=*/ lFD 1 2;l3=+- th2 l2 0;rh=+- hd2 0 th;rw=+- wd2 0 th;dr=+- rw 0 rh;maxr=?: dr rh rw;ha=at2 maxr l3;aA1=+- 18600000 0 ha;aD1=+- 18600000 ha 0;ta11=cos rw aA1;ta12=sin rh aA1;bA1=at2 ta11 ta12;cta1=cos rh bA1;sta1=sin rw bA1;ma1=mod cta1 sta1 0;na1=*/ rw rh ma1;dxa1=cos na1 bA1;dya1=sin na1 bA1;xA1=+- hc dxa1 0;yA1=+- vc dya1 0;td11=cos rw aD1;td12=sin rh aD1;bD1=at2 td11 td12;ctd1=cos rh bD1;std1=sin rw bD1;md1=mod ctd1 std1 0;nd1=*/ rw rh md1;dxd1=cos nd1 bD1;dyd1=sin nd1 bD1;xD1=+- hc dxd1 0;yD1=+- vc dyd1 0;xAD1=+- xA1 0 xD1;yAD1=+- yA1 0 yD1;a1=at2 yAD1 xAD1;aA2=+- 21000000 0 ha;aD2=+- 21000000 ha 0;ta21=cos rw aA2;ta22=sin rh aA2;bA2=at2 ta21 ta22;cta2=cos rh bA2;sta2=sin rw bA2;ma2=mod cta2 sta2 0;na2=*/ rw rh ma2;dxa2=cos na2 bA2;dya2=sin na2 bA2;xA2=+- hc dxa2 0;yA2=+- vc dya2 0;td21=cos rw aD2;td22=sin rh aD2;bD2=at2 td21 td22;ctd2=cos rh bD2;std2=sin rw bD2;md2=mod ctd2 std2 0;nd2=*/ rw rh md2;dxd2=cos nd2 bD2;dyd2=sin nd2 bD2;xD2=+- hc dxd2 0;yD2=+- vc dyd2 0;xAD2=+- xA2 0 xD2;yAD2=+- yA2 0 yD2;a2=at2 yAD2 xAD2;aD3=+- 1800000 ha 0;td31=cos rw aD3;td32=sin rh aD3;bD3=at2 td31 td32;ctd3=cos rh bD3;std3=sin rw bD3;md3=mod ctd3 std3 0;nd3=*/ rw rh md3;dyd3=sin nd3 bD3;yD3=+- vc dyd3 0;xA8=+- w 0 xD1|xA8 yD1 xD1 yD3 +halfFrame|adj1=val 33333;adj2=val 33333||l t r b +heart||il=*/ w 1 6;ir=*/ w 5 6;ib=*/ h 2 3|il hd4 ir ib +heptagon|hf=val 102572;vf=val 105210|swd2=*/ wd2 hf 100000;shd2=*/ hd2 vf 100000;svc=*/ vc vf 100000;dx2=*/ swd2 78183 100000;dy1=*/ shd2 62349 100000;x2=+- hc 0 dx2;x5=+- hc dx2 0;y1=+- svc 0 dy1;ib=+- b 0 y1|x2 y1 x5 ib +hexagon|adj=val 25000;vf=val 115470|maxAdj=*/ 50000 w ss;a=pin 0 adj maxAdj;q1=*/ maxAdj -1 2;q2=+- a q1 0;q3=?: q2 4 2;q4=?: q2 3 2;q5=?: q2 q1 0;q6=+/ a q5 q1;q7=*/ q6 q4 -1;q8=+- q3 q7 0;il=*/ w q8 24;it=*/ h q8 24;ir=+- r 0 il;ib=+- b 0 it|il it ir ib +homePlate|adj=val 50000|maxAdj=*/ 100000 w ss;a=pin 0 adj maxAdj;dx1=*/ ss a 100000;x1=+- r 0 dx1;ir=+/ x1 r 2|l t ir b +horizontalScroll|adj=val 12500|a=pin 0 adj 25000;ch=*/ ss a 100000;ch2=*/ ch 1 2;y6=+- b 0 ch;x4=+- r 0 ch2|ch ch x4 y6 +irregularSeal1||x5=*/ w 4627 21600;x21=*/ w 16702 21600;y3=*/ h 6320 21600;y9=*/ h 13937 21600|x5 y3 x21 y9 +irregularSeal2||x5=*/ w 5372 21600;x19=*/ w 14640 21600;y3=*/ h 6382 21600;y17=*/ h 15935 21600|x5 y3 x19 y17 +leftArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 100000 w ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;dx2=*/ ss a2 100000;x2=+- l dx2 0;dy1=*/ h a1 200000;y1=+- vc 0 dy1;y2=+- vc dy1 0;dx1=*/ y1 dx2 hd2;x1=+- x2 0 dx1|x1 y1 r y2 +leftArrowCallout|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 64977|maxAdj3=*/ 100000 w ss;a3=pin 0 adj3 maxAdj3;q2=*/ a3 ss w;maxAdj4=+- 100000 0 q2;a4=pin 0 adj4 maxAdj4;dx2=*/ w a4 100000;x2=+- r 0 dx2|x2 t r b +leftBrace|adj1=val 8333;adj2=val 50000|a2=pin 0 adj2 100000;q1=+- 100000 0 a2;q2=min q1 a2;q3=*/ q2 1 2;maxAdj1=*/ q3 h ss;a1=pin 0 adj1 maxAdj1;y1=*/ ss a1 100000;dx1=cos wd2 2700000;dy1=sin y1 2700000;il=+- r 0 dx1;it=+- y1 0 dy1;ib=+- b dy1 y1|il it r ib +leftBracket|adj=val 8333|maxAdj=*/ 50000 h ss;a=pin 0 adj maxAdj;y1=*/ ss a 100000;dx1=cos w 2700000;dy1=sin y1 2700000;il=+- r 0 dx1;it=+- y1 0 dy1;ib=+- b dy1 y1|il it r ib +leftCircularArrow|adj1=val 12500;adj2=val -1142319;adj3=val 1142319;adj4=val 10800000;adj5=val 12500|a5=pin 0 adj5 25000;maxAdj1=*/ a5 2 1;a1=pin 0 adj1 maxAdj1;th=*/ ss a1 100000;thh=*/ ss a5 100000;th2=*/ th 1 2;rw1=+- wd2 th2 thh;rh1=+- hd2 th2 thh;idx=cos rw1 2700000;idy=sin rh1 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +leftRightArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 50000 w ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;x2=*/ ss a2 100000;x3=+- r 0 x2;dy=*/ h a1 200000;y1=+- vc 0 dy;y2=+- vc dy 0;dx1=*/ y1 x2 hd2;x1=+- x2 0 dx1;x4=+- x3 dx1 0|x1 y1 x4 y2 +leftRightArrowCallout|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 48123|maxAdj3=*/ 50000 w ss;a3=pin 0 adj3 maxAdj3;q2=*/ a3 ss wd2;maxAdj4=+- 100000 0 q2;a4=pin 0 adj4 maxAdj4;dx2=*/ w a4 200000;x2=+- hc 0 dx2;x3=+- hc dx2 0|x2 t x3 b +leftRightCircularArrow|adj1=val 12500;adj2=val 1142319;adj3=val 20457681;adj4=val 11942319;adj5=val 12500|a5=pin 0 adj5 25000;maxAdj1=*/ a5 2 1;a1=pin 0 adj1 maxAdj1;th=*/ ss a1 100000;thh=*/ ss a5 100000;th2=*/ th 1 2;rw1=+- wd2 th2 thh;rh1=+- hd2 th2 thh;idx=cos rw1 2700000;idy=sin rh1 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +leftRightRibbon|adj1=val 50000;adj2=val 50000;adj3=val 16667|a3=pin 0 adj3 33333;maxAdj1=+- 100000 0 a3;a1=pin 0 adj1 maxAdj1;w1=+- wd2 0 wd32;maxAdj2=*/ 100000 w1 ss;a2=pin 0 adj2 maxAdj2;x1=*/ ss a2 100000;x4=+- r 0 x1;dy1=*/ h a1 200000;dy2=*/ h a3 -200000;ly1=+- vc dy2 dy1;ry4=+- vc dy1 dy2|x1 ly1 x4 ry4 +leftRightUpArrow|adj1=val 25000;adj2=val 25000;adj3=val 25000|a2=pin 0 adj2 50000;maxAdj1=*/ a2 2 1;a1=pin 0 adj1 maxAdj1;q1=+- 100000 0 maxAdj1;maxAdj3=*/ q1 1 2;a3=pin 0 adj3 maxAdj3;x1=*/ ss a3 100000;dx2=*/ ss a2 100000;dx3=*/ ss a1 200000;y4=+- b 0 dx2;y3=+- y4 0 dx3;y5=+- y4 dx3 0;il=*/ dx3 x1 dx2;ir=+- r 0 il|il y3 ir y5 +leftUpArrow|adj1=val 25000;adj2=val 25000;adj3=val 25000|a2=pin 0 adj2 50000;maxAdj1=*/ a2 2 1;a1=pin 0 adj1 maxAdj1;maxAdj3=+- 100000 0 maxAdj1;a3=pin 0 adj3 maxAdj3;x1=*/ ss a3 100000;dx4=*/ ss a2 100000;x4=+- r 0 dx4;y4=+- b 0 dx4;dx3=*/ ss a1 200000;y3=+- y4 0 dx3;y5=+- y4 dx3 0;il=*/ dx3 x1 dx4|il y3 x4 y5 +lightningBolt||x4=*/ w 8757 21600;x9=*/ w 13917 21600;y4=*/ h 7437 21600;y10=*/ h 14277 21600|x4 y4 x9 y10 +mathDivide|adj1=val 23520;adj2=val 5880;adj3=val 11760|a1=pin 1000 adj1 36745;dy1=*/ h a1 200000;dx1=*/ w 73490 200000;y3=+- vc 0 dy1;y4=+- vc dy1 0;x1=+- hc 0 dx1;x3=+- hc dx1 0|x1 y3 x3 y4 +mathEqual|adj1=val 23520;adj2=val 11760|a1=pin 0 adj1 36745;2a1=*/ a1 2 1;mAdj2=+- 100000 0 2a1;a2=pin 0 adj2 mAdj2;dy1=*/ h a1 100000;dy2=*/ h a2 200000;dx1=*/ w 73490 200000;y2=+- vc 0 dy2;y3=+- vc dy2 0;y1=+- y2 0 dy1;y4=+- y3 dy1 0;x1=+- hc 0 dx1;x2=+- hc dx1 0|x1 y1 x2 y4 +mathMinus|adj1=val 23520|a1=pin 0 adj1 100000;dy1=*/ h a1 200000;dx1=*/ w 73490 200000;y1=+- vc 0 dy1;y2=+- vc dy1 0;x1=+- hc 0 dx1;x2=+- hc dx1 0|x1 y1 x2 y2 +mathMultiply|adj1=val 23520|a1=pin 0 adj1 51965;th=*/ ss a1 100000;a=at2 w h;sa=sin 1 a;ca=cos 1 a;dl=mod w h 0;rw=*/ dl 51965 100000;lM=+- dl 0 rw;xM=*/ ca lM 2;yM=*/ sa lM 2;dxAM=*/ sa th 2;dyAM=*/ ca th 2;xA=+- xM 0 dxAM;yB=+- yM 0 dyAM;xE=+- r 0 xA;yH=+- b 0 yB|xA yB xE yH +mathNotEqual|adj1=val 23520;adj2=val 6600000;adj3=val 11760|a1=pin 0 adj1 50000;2a1=*/ a1 2 1;maxAdj3=+- 100000 0 2a1;a3=pin 0 adj3 maxAdj3;dy1=*/ h a1 100000;dy2=*/ h a3 200000;dx1=*/ w 73490 200000;x1=+- hc 0 dx1;x8=+- hc dx1 0;y2=+- vc 0 dy2;y3=+- vc dy2 0;y1=+- y2 0 dy1;y4=+- y3 dy1 0|x1 y1 x8 y4 +mathPlus|adj1=val 23520|a1=pin 0 adj1 73490;dx1=*/ w 73490 200000;dx2=*/ ss a1 200000;x1=+- hc 0 dx1;x4=+- hc dx1 0;y2=+- vc 0 dx2;y3=+- vc dx2 0|x1 y2 x4 y3 +moon|adj=val 50000|a=pin 0 adj 87500;g0=*/ ss a 100000;g0w=*/ g0 w ss;g12=*/ g0 9598 32768;g12w=*/ g12 w ss;g13=+- ss 0 g12;q1=*/ ss ss 1;q2=*/ g13 g13 1;q3=+- q1 0 q2;q4=sqrt q3;dy4=*/ q4 hd2 ss;g15h=+- vc 0 dy4;g16h=+- vc dy4 0|g12w g15h g0w g16h +nonIsoscelesTrapezoid|adj1=val 25000;adj2=val 25000|maxAdj=*/ 50000 w ss;a1=pin 0 adj1 maxAdj;a2=pin 0 adj2 maxAdj;il=*/ wd3 a1 maxAdj;adjm=max a1 a2;it=*/ hd3 adjm maxAdj;irt=*/ wd3 a2 maxAdj;ir=+- r 0 irt|il it ir b +noSmoking|adj=val 18750|idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +notchedRightArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 100000 w ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;dx2=*/ ss a2 100000;dy1=*/ h a1 200000;y1=+- vc 0 dy1;y2=+- vc dy1 0;x1=*/ dy1 dx2 hd2;x3=+- r 0 x1|x1 y1 x3 y2 +octagon|adj=val 29289|a=pin 0 adj 50000;x1=*/ ss a 100000;il=*/ x1 1 2;ir=+- r 0 il;ib=+- b 0 il|il il ir ib +parallelogram|adj=val 25000|maxAdj=*/ 100000 w ss;a=pin 0 adj maxAdj;il=*/ wd2 a maxAdj;q1=*/ 5 a maxAdj;q2=+/ 1 q1 12;il=*/ q2 w 1;it=*/ q2 h 1;ir=+- r 0 il;ib=+- b 0 it|il it ir ib +pentagon|hf=val 105146;vf=val 110557|swd2=*/ wd2 hf 100000;shd2=*/ hd2 vf 100000;svc=*/ vc vf 100000;dx1=cos swd2 1080000;dx2=cos swd2 18360000;dy1=sin shd2 1080000;dy2=sin shd2 18360000;x2=+- hc 0 dx2;x3=+- hc dx2 0;y1=+- svc 0 dy1;y2=+- svc 0 dy2;it=*/ y1 dx2 dx1|x2 it x3 y2 +pie|adj1=val 0;adj2=val 16200000|idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +pieWedge||g1=cos w 13500000;g2=sin h 13500000;x1=+- r g1 0;y1=+- b g2 0|x1 y1 r b +plaque|adj=val 16667|a=pin 0 adj 50000;x1=*/ ss a 100000;il=*/ x1 70711 100000;ir=+- r 0 il;ib=+- b 0 il|il il ir ib +plaqueTabs||md=mod w h 0;dx=*/ 1 md 20;y1=+- 0 b dx;x1=+- 0 r dx|dx dx x1 y1 +plus|adj=val 25000|a=pin 0 adj 50000;x1=*/ ss a 100000;x2=+- r 0 x1;y2=+- b 0 x1;d=+- w 0 h;il=?: d l x1;ir=?: d r x2;it=?: d x1 t;ib=?: d y2 b|il it ir ib +quadArrow|adj1=val 22500;adj2=val 22500;adj3=val 22500|a2=pin 0 adj2 50000;maxAdj1=*/ a2 2 1;a1=pin 0 adj1 maxAdj1;q1=+- 100000 0 maxAdj1;maxAdj3=*/ q1 1 2;a3=pin 0 adj3 maxAdj3;x1=*/ ss a3 100000;dx2=*/ ss a2 100000;dx3=*/ ss a1 200000;y3=+- vc 0 dx3;y4=+- vc dx3 0;il=*/ dx3 x1 dx2;ir=+- r 0 il|il y3 ir y4 +quadArrowCallout|adj1=val 18515;adj2=val 18515;adj3=val 18515;adj4=val 48123|a2=pin 0 adj2 50000;maxAdj1=*/ a2 2 1;a1=pin 0 adj1 maxAdj1;maxAdj3=+- 50000 0 a2;a3=pin 0 adj3 maxAdj3;q2=*/ a3 2 1;maxAdj4=+- 100000 0 q2;a4=pin a1 adj4 maxAdj4;dx1=*/ w a4 200000;dy1=*/ h a4 200000;x2=+- hc 0 dx1;x7=+- hc dx1 0;y2=+- vc 0 dy1;y7=+- vc dy1 0|x2 y2 x7 y7 +rect|||l t r b +ribbon|adj1=val 16667;adj2=val 50000|a1=pin 0 adj1 33333;a2=pin 25000 adj2 75000;dx2=*/ w a2 200000;x2=+- hc 0 dx2;x9=+- hc dx2 0;y2=*/ h a1 100000|x2 y2 x9 b +ribbon2|adj1=val 16667;adj2=val 50000|a1=pin 0 adj1 33333;a2=pin 25000 adj2 75000;dx2=*/ w a2 200000;x2=+- hc 0 dx2;x9=+- hc dx2 0;dy2=*/ h a1 100000;y2=+- b 0 dy2|x2 t x9 y2 +rightArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 100000 w ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;dx1=*/ ss a2 100000;x1=+- r 0 dx1;dy1=*/ h a1 200000;y1=+- vc 0 dy1;y2=+- vc dy1 0;dx2=*/ y1 dx1 hd2;x2=+- x1 dx2 0|l y1 x2 y2 +rightArrowCallout|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 64977|maxAdj3=*/ 100000 w ss;a3=pin 0 adj3 maxAdj3;q2=*/ a3 ss w;maxAdj4=+- 100000 0 q2;a4=pin 0 adj4 maxAdj4;x2=*/ w a4 100000|l t x2 b +rightBrace|adj1=val 8333;adj2=val 50000|a2=pin 0 adj2 100000;q1=+- 100000 0 a2;q2=min q1 a2;q3=*/ q2 1 2;maxAdj1=*/ q3 h ss;a1=pin 0 adj1 maxAdj1;y1=*/ ss a1 100000;dx1=cos wd2 2700000;dy1=sin y1 2700000;ir=+- l dx1 0;it=+- y1 0 dy1;ib=+- b dy1 y1|l it ir ib +rightBracket|adj=val 8333|maxAdj=*/ 50000 h ss;a=pin 0 adj maxAdj;y1=*/ ss a 100000;dx1=cos w 2700000;dy1=sin y1 2700000;ir=+- l dx1 0;it=+- y1 0 dy1;ib=+- b dy1 y1|l it ir ib +round1Rect|adj=val 16667|a=pin 0 adj 50000;dx1=*/ ss a 100000;idx=*/ dx1 29289 100000;ir=+- r 0 idx|l t ir b +round2DiagRect|adj1=val 16667;adj2=val 0|a1=pin 0 adj1 50000;a2=pin 0 adj2 50000;x1=*/ ss a1 100000;a=*/ ss a2 100000;dx1=*/ x1 29289 100000;dx2=*/ a 29289 100000;d=+- dx1 0 dx2;dx=?: d dx1 dx2;ir=+- r 0 dx;ib=+- b 0 dx|dx dx ir ib +round2SameRect|adj1=val 16667;adj2=val 0|a1=pin 0 adj1 50000;a2=pin 0 adj2 50000;tx1=*/ ss a1 100000;bx1=*/ ss a2 100000;d=+- tx1 0 bx1;tdx=*/ tx1 29289 100000;bdx=*/ bx1 29289 100000;il=?: d tdx bdx;ir=+- r 0 il;ib=+- b 0 bdx|il tdx ir ib +roundRect|adj=val 16667|a=pin 0 adj 50000;x1=*/ ss a 100000;il=*/ x1 29289 100000;ir=+- r 0 il;ib=+- b 0 il|il il ir ib +rtTriangle||it=*/ h 7 12;ir=*/ w 7 12;ib=*/ h 11 12|wd12 it ir ib +smileyFace|adj=val 4653|idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +snip1Rect|adj=val 16667|a=pin 0 adj 50000;dx1=*/ ss a 100000;x1=+- r 0 dx1;it=*/ dx1 1 2;ir=+/ x1 r 2|l it ir b +snip2DiagRect|adj1=val 0;adj2=val 16667|a1=pin 0 adj1 50000;a2=pin 0 adj2 50000;lx1=*/ ss a1 100000;rx1=*/ ss a2 100000;d=+- lx1 0 rx1;dx=?: d lx1 rx1;il=*/ dx 1 2;ir=+- r 0 il;ib=+- b 0 il|il il ir ib +snip2SameRect|adj1=val 16667;adj2=val 0|a1=pin 0 adj1 50000;a2=pin 0 adj2 50000;tx1=*/ ss a1 100000;bx1=*/ ss a2 100000;by1=+- b 0 bx1;d=+- tx1 0 bx1;dx=?: d tx1 bx1;il=*/ dx 1 2;ir=+- r 0 il;it=*/ tx1 1 2;ib=+/ by1 b 2|il it ir ib +snipRoundRect|adj1=val 16667;adj2=val 16667|a1=pin 0 adj1 50000;a2=pin 0 adj2 50000;x1=*/ ss a1 100000;dx2=*/ ss a2 100000;x2=+- r 0 dx2;il=*/ x1 29289 100000;ir=+/ x2 r 2|il il ir b +squareTabs||md=mod w h 0;dx=*/ 1 md 20;y1=+- 0 b dx;x1=+- 0 r dx|dx dx x1 y1 +star10|adj=val 42533;hf=val 105146|a=pin 0 adj 50000;swd2=*/ wd2 hf 100000;iwd2=*/ swd2 a 50000;ihd2=*/ hd2 a 50000;sdx1=*/ iwd2 80902 100000;sdy2=*/ ihd2 58779 100000;sx2=+- hc 0 sdx1;sx5=+- hc sdx1 0;sy2=+- vc 0 sdy2;sy3=+- vc sdy2 0|sx2 sy2 sx5 sy3 +star12|adj=val 37500|a=pin 0 adj 50000;iwd2=*/ wd2 a 50000;ihd2=*/ hd2 a 50000;sdx2=cos iwd2 2700000;sdy2=sin ihd2 2700000;sx2=+- hc 0 sdx2;sx5=+- hc sdx2 0;sy2=+- vc 0 sdy2;sy5=+- vc sdy2 0|sx2 sy2 sx5 sy5 +star16|adj=val 37500|a=pin 0 adj 50000;iwd2=*/ wd2 a 50000;ihd2=*/ hd2 a 50000;idx=cos iwd2 2700000;idy=sin ihd2 2700000;il=+- hc 0 idx;it=+- vc 0 idy;ir=+- hc idx 0;ib=+- vc idy 0|il it ir ib +star24|adj=val 37500|a=pin 0 adj 50000;iwd2=*/ wd2 a 50000;ihd2=*/ hd2 a 50000;idx=cos iwd2 2700000;idy=sin ihd2 2700000;il=+- hc 0 idx;it=+- vc 0 idy;ir=+- hc idx 0;ib=+- vc idy 0|il it ir ib +star32|adj=val 37500|a=pin 0 adj 50000;iwd2=*/ wd2 a 50000;ihd2=*/ hd2 a 50000;idx=cos iwd2 2700000;idy=sin ihd2 2700000;il=+- hc 0 idx;it=+- vc 0 idy;ir=+- hc idx 0;ib=+- vc idy 0|il it ir ib +star4|adj=val 12500|a=pin 0 adj 50000;iwd2=*/ wd2 a 50000;ihd2=*/ hd2 a 50000;sdx=cos iwd2 2700000;sdy=sin ihd2 2700000;sx1=+- hc 0 sdx;sx2=+- hc sdx 0;sy1=+- vc 0 sdy;sy2=+- vc sdy 0|sx1 sy1 sx2 sy2 +star5|adj=val 19098;hf=val 105146;vf=val 110557|a=pin 0 adj 50000;swd2=*/ wd2 hf 100000;shd2=*/ hd2 vf 100000;svc=*/ vc vf 100000;iwd2=*/ swd2 a 50000;ihd2=*/ shd2 a 50000;sdx1=cos iwd2 20520000;sdy1=sin ihd2 3240000;sx1=+- hc 0 sdx1;sx4=+- hc sdx1 0;sy1=+- svc 0 sdy1;sy3=+- svc ihd2 0|sx1 sy1 sx4 sy3 +star6|adj=val 28868;hf=val 115470|a=pin 0 adj 50000;swd2=*/ wd2 hf 100000;iwd2=*/ swd2 a 50000;ihd2=*/ hd2 a 50000;sx1=+- hc 0 iwd2;sx4=+- hc iwd2 0;sdy1=sin ihd2 3600000;sy1=+- vc 0 sdy1;sy2=+- vc sdy1 0|sx1 sy1 sx4 sy2 +star7|adj=val 34601;hf=val 102572;vf=val 105210|a=pin 0 adj 50000;swd2=*/ wd2 hf 100000;shd2=*/ hd2 vf 100000;svc=*/ vc vf 100000;iwd2=*/ swd2 a 50000;ihd2=*/ shd2 a 50000;sdx2=*/ iwd2 78183 100000;sx2=+- hc 0 sdx2;sx5=+- hc sdx2 0;sdy1=*/ ihd2 90097 100000;sdy3=*/ ihd2 62349 100000;sy1=+- svc 0 sdy1;sy3=+- svc sdy3 0|sx2 sy1 sx5 sy3 +star8|adj=val 37500|a=pin 0 adj 50000;iwd2=*/ wd2 a 50000;ihd2=*/ hd2 a 50000;sdx1=*/ iwd2 92388 100000;sdy1=*/ ihd2 92388 100000;sx1=+- hc 0 sdx1;sx4=+- hc sdx1 0;sy1=+- vc 0 sdy1;sy4=+- vc sdy1 0|sx1 sy1 sx4 sy4 +straightConnector1|||l t r b +stripedRightArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 84375 w ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;x4=*/ ss 5 32;dx5=*/ ss a2 100000;dy1=*/ h a1 200000;y1=+- vc 0 dy1;y2=+- vc dy1 0;dx6=*/ dy1 dx5 hd2;x6=+- r 0 dx6|x4 y1 x6 y2 +sun|adj=val 25000|a=pin 12500 adj 46875;g0=+- 50000 0 a;g7=*/ g0 23170 32768;g8=+- 50000 g7 0;g9=+- 50000 0 g7;x8=*/ w g8 100000;x9=*/ w g9 100000;y8=*/ h g8 100000;y9=*/ h g9 100000|x9 y9 x8 y8 +swooshArrow|adj1=val 25000;adj2=val 16667||l t r b +teardrop|adj=val 100000|idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +trapezoid|adj=val 25000|maxAdj=*/ 50000 w ss;a=pin 0 adj maxAdj;il=*/ wd3 a maxAdj;it=*/ hd3 a maxAdj;ir=+- r 0 il|il it ir b +triangle|adj=val 50000|a=pin 0 adj 100000;x1=*/ w a 200000;x3=+- x1 wd2 0|x1 vc x3 b +upArrowCallout|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 64977|maxAdj3=*/ 100000 h ss;a3=pin 0 adj3 maxAdj3;q2=*/ a3 ss h;maxAdj4=+- 100000 0 q2;a4=pin 0 adj4 maxAdj4;dy2=*/ h a4 100000;y2=+- b 0 dy2|l y2 r b +upDownArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 50000 h ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;y2=*/ ss a2 100000;y3=+- b 0 y2;dx1=*/ w a1 200000;x1=+- hc 0 dx1;x2=+- hc dx1 0;dy1=*/ x1 y2 wd2;y1=+- y2 0 dy1;y4=+- y3 dy1 0|x1 y1 x2 y4 +upArrow|adj1=val 50000;adj2=val 50000|maxAdj2=*/ 100000 h ss;a1=pin 0 adj1 100000;a2=pin 0 adj2 maxAdj2;dy2=*/ ss a2 100000;y2=+- t dy2 0;dx1=*/ w a1 200000;x1=+- hc 0 dx1;x2=+- hc dx1 0;dy1=*/ x1 dy2 wd2;y1=+- y2 0 dy1|x1 y1 x2 b +upDownArrowCallout|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 48123|maxAdj3=*/ 50000 h ss;a3=pin 0 adj3 maxAdj3;q2=*/ a3 ss hd2;maxAdj4=+- 100000 0 q2;a4=pin 0 adj4 maxAdj4;dy2=*/ h a4 200000;y2=+- vc 0 dy2;y3=+- vc dy2 0|l y2 r y3 +uturnArrow|adj1=val 25000;adj2=val 25000;adj3=val 25000;adj4=val 43750;adj5=val 75000||l t r b +verticalScroll|adj=val 12500|a=pin 0 adj 25000;ch=*/ ss a 100000;ch2=*/ ch 1 2;x6=+- r 0 ch;y4=+- b 0 ch2|ch ch x6 y4 +wave|adj1=val 12500;adj2=val 0|a1=pin 0 adj1 20000;a2=pin -10000 adj2 10000;of2=*/ w a2 50000;dx2=?: of2 0 of2;x2=+- l 0 dx2;dx5=?: of2 of2 0;x5=+- r 0 dx5;x6=+- l dx5 0;x10=+- r dx2 0;il=max x2 x6;ir=min x5 x10;it=*/ h a1 50000;ib=+- b 0 it|il it ir ib +wedgeEllipseCallout|adj1=val -20833;adj2=val 62500|idx=cos wd2 2700000;idy=sin hd2 2700000;il=+- hc 0 idx;ir=+- hc idx 0;it=+- vc 0 idy;ib=+- vc idy 0|il it ir ib +wedgeRectCallout|adj1=val -20833;adj2=val 62500||l t r b +wedgeRoundRectCallout|adj1=val -20833;adj2=val 62500;adj3=val 16667|u1=*/ ss adj3 100000;il=*/ u1 29289 100000;ir=+- r 0 il;ib=+- b 0 il|il il ir ib diff --git a/crates/drawingml/src/warp.rs b/crates/drawingml/src/warp.rs index acb453add..09e0d8e8d 100644 --- a/crates/drawingml/src/warp.rs +++ b/crates/drawingml/src/warp.rs @@ -107,7 +107,50 @@ impl Guide { let d = b - a; let l = d.hypot(); let t = if l > 1e-12 { d / l } else { Vec2::new(1.0, 0.0) }; - (a + t * (s - c0), t) + let u = if l > 1e-12 { (s - c0) / l } else { 0.0 }; + (a + t * (s - c0), self.blended(i, t, u)) + } + + /// The direction of the guide's segment `i` bent towards its neighbour's (the previous one in + /// the first half of the segment, the next in the second) so it turns continuously: `t` at the + /// middle of the segment, the mean of both at a vertex. A letter placed at a distance along + /// the normal would otherwise step by the turn of the segment at each vertex, which shows as + /// a ragged outline on tightly curved or large text. + fn blended(&self, i: usize, t: Vec2, u: f64) -> Vec2 { + let n = self.pts.len(); + let u = u.clamp(0.0, 1.0); + let (other, w) = if u < 0.5 { + ( + if i > 0 { + Some(i - 1) + } else if self.closed { + n.checked_sub(2) + } else { + None + }, + 0.5 - u, + ) + } else { + ( + if i + 2 < n { + Some(i + 1) + } else if self.closed { + Some(0) + } else { + None + }, + u - 0.5, + ) + }; + let unit = |j: usize| -> Option { + let v = *self.pts.get(j + 1)? - *self.pts.get(j)?; + let l = v.hypot(); + (l > 1e-12).then(|| v / l) + }; + let Some(o) = other.and_then(unit) else { return t }; + let m = t * (1.0 - w) + o * w; + let l = m.hypot(); + if l > 1e-12 { m / l } else { t } } /// The parallel curve `d` points away on the left of the direction of travel (above a @@ -992,6 +1035,22 @@ impl Mapper<'_> { self.zoom } + /// Map point `p` of a glyph of line `line` whose advance is centred on layout x `pivot`, for + /// the lines that follow a guide: PowerPoint sets such a glyph down whole, turned to the + /// guide's direction where its centre falls, instead of bending it along the guide. Other lines + /// map as [`Mapper::map`]. + pub fn map_rigid(&self, line: usize, pivot: f64, p: Point) -> Point { + let Some(&LineMap::Follow { band, guide, e0, s0, k, ref_y }) = self.lines.get(line) else { return self.map(line, p) }; + let path = match (guide.and_then(|g| self.offsets.get(g)), self.warp.bands.get(band)) { + (Some(g), _) => g, + (None, Some(Band::Follow { path, .. })) => path, + _ => return p, + }; + let (q, t) = path.at(s0 + (pivot - e0) * k); + let out = q + t * ((p.x - pivot) * k) + Vec2::new(t.y, -t.x) * ((ref_y - p.y) * k); + if out.x.is_finite() && out.y.is_finite() { out } else { p } + } + /// Map point `p` of line `line` (layout coordinates) onto the warp. pub fn map(&self, line: usize, p: Point) -> Point { let out = match self.lines.get(line) { diff --git a/crates/drawingml/src/warp/tests.rs b/crates/drawingml/src/warp/tests.rs index c4aa1bbd0..097467afb 100644 --- a/crates/drawingml/src/warp/tests.rs +++ b/crates/drawingml/src/warp/tests.rs @@ -320,6 +320,66 @@ fn arch_and_inflate_handles_follow_the_pointer() { assert!((more - 20_000.0).abs() < 1.0, "{more}"); } +#[test] +fn a_guides_direction_turns_continuously_across_its_vertices() { + // A coarse circle of radius 100: a vertex every 20 degrees, a 35 pt chord. + let mut path = BezPath::new(); + for k in 0..=18 { + let a = f64::from(k) * 20.0_f64.to_radians(); + let p = Point::new(100.0 * a.cos(), 100.0 * a.sin()); + if k == 0 { path.move_to(p) } else { path.line_to(p) } + } + let g = Guide::from_path(&path, false); + let chord = 2.0 * 100.0 * 10.0_f64.to_radians().sin(); + let angle = |s: f64| { + let (_, t) = g.at(s); + t.y.atan2(t.x) + }; + for k in 1..18 { + let s = f64::from(k) * chord; + let step = (angle(s + 1e-6) - angle(s - 1e-6)).abs(); + assert!(step < 1e-4, "the direction jumps by {step} rad at vertex {k}"); + } + // Along the guide, away from its two ends (which keep the end chords' direction), the direction follows the circle: + // the tangent is 90 degrees ahead of the radius, within a degree even in the middle of a chord. + for k in 10..350 { + let s = f64::from(k) * 0.05 * chord; + let (p, t) = g.at(s); + let radius = p.y.atan2(p.x); + let want = radius + std::f64::consts::FRAC_PI_2; + let diff = (t.y.atan2(t.x) - want + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI; + assert!(diff.abs() < 1.0_f64.to_radians(), "at {s}: the direction is {diff} rad off the circle's"); + } +} + +#[test] +fn a_glyph_on_a_follow_guide_is_set_down_whole_and_a_fill_warp_still_bends_it() { + let ink = Rect::new(100.0, 50.0, 300.0, 90.0); + let lines = one_line(ink); + // The corners of a 20 x 30 glyph whose advance is centred on x = 150, baseline at the line's bottom. + let glyph = [(140.0, 60.0), (160.0, 60.0), (160.0, 90.0), (140.0, 90.0)].map(|(x, y)| Point::new(x, y)); + let mapped = |name: &str, whole: bool| { + let w = warp(name, &[], rect()).unwrap(); + let m = w.mapper(&lines, rect().inset(-5.0)); + glyph.map(|p| if whole { m.map_rigid(0, 150.0, p) } else { m.map(0, p) }) + }; + let sides = |q: &[Point; 4]| { + [(q[0] - q[1]).hypot(), (q[1] - q[2]).hypot(), (q[2] - q[3]).hypot(), (q[3] - q[0]).hypot(), (q[0] - q[2]).hypot(), (q[1] - q[3]).hypot()] + }; + let want = sides(&glyph); + for name in ["textArchUp", "textArchDown", "textCircle", "textButton"] { + let whole = sides(&mapped(name, true)); + for (a, b) in whole.iter().zip(&want) { + assert!((a - b).abs() < 1e-6, "{name}: a glyph's size changed ({a} against {b})"); + } + // Mapped point by point along the same guide, the glyph is bent: its two sides differ. + let bent = sides(&mapped(name, false)); + assert!(bent.iter().zip(&want).any(|(a, b)| (a - b).abs() > 0.05), "{name}: bending left the glyph whole"); + } + // A fill warp bends the glyph either way. + assert_eq!(mapped("textInflate", true), mapped("textInflate", false)); +} + #[test] fn many_lines_on_a_follow_band_are_mapped_from_one_measurement() { // 50,000 lines on an arch: each line's scale and anchor come from the band measured once. diff --git a/crates/drawingml/tests/fixtures/make_preset_guides.py b/crates/drawingml/tests/fixtures/make_preset_guides.py new file mode 100644 index 000000000..7f4435151 --- /dev/null +++ b/crates/drawingml/tests/fixtures/make_preset_guides.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""From ECMA-376 presetShapeDefinitions.xml, write each preset's adjust values, the guides its +text rectangle depends on, and the rectangle, one preset per line (src/preset_text.txt): + name|adj=fmla;...|guide=fmla;...|l t r b +Usage: make_preset_guides.py presetShapeDefinitions.xml > ../../src/preset_text.txt""" +import sys, xml.etree.ElementTree as ET +A = '{http://schemas.openxmlformats.org/drawingml/2006/main}' +root = ET.parse(sys.argv[1]).getroot() +print('# Generated by crates/drawingml/tests/fixtures/make_preset_guides.py from presetShapeDefinitions.xml.') +for sh in root: + rect = sh.find(A + 'rect') + if rect is None: + continue + av = [(g.get('name'), g.get('fmla')) for g in list(sh.find(A + 'avLst') if sh.find(A + 'avLst') is not None else [])] + gds = [(g.get('name'), g.get('fmla')) for g in list(sh.find(A + 'gdLst') if sh.find(A + 'gdLst') is not None else [])] + names = {n for n, _ in gds} + need = set() + todo = [rect.get(k) for k in 'ltrb'] + deps = dict(gds) + while todo: + x = todo.pop() + if x in names and x not in need: + need.add(x) + todo += deps[x].split()[1:] + kept = [(n, f) for n, f in gds if n in need] + print('%s|%s|%s|%s' % (sh.tag, ';'.join(f'{n}={f}' for n, f in av), ';'.join(f'{n}={f}' for n, f in kept), + ' '.join(rect.get(k) for k in 'ltrb'))) diff --git a/crates/drawingml/tests/fixtures/make_preset_table.py b/crates/drawingml/tests/fixtures/make_preset_table.py new file mode 100644 index 000000000..9f4b93c46 --- /dev/null +++ b/crates/drawingml/tests/fixtures/make_preset_table.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Evaluate ECMA-376 presetShapeDefinitions.xml: for every preset, its default adjust values and +its text rectangle at a few sizes. Writes preset_table.txt, which the geometry tests compare +against. Usage: make_preset_table.py presetShapeDefinitions.xml > preset_table.txt +(The XML ships with LibreOffice's source: oox/source/drawingml/customshapes/.)""" +import math, sys, xml.etree.ElementTree as ET + +A = '{http://schemas.openxmlformats.org/drawingml/2006/main}' +SIZES = [(200.0, 100.0), (100.0, 200.0), (144.0, 144.0)] + +def builtins(w, h): + ss = min(w, h); ls = max(w, h) + v = {'w': w, 'h': h, 'l': 0.0, 't': 0.0, 'r': w, 'b': h, 'ss': ss, 'ls': ls, 'hc': w / 2, 'vc': h / 2, + 'cd2': 10800000.0, 'cd4': 5400000.0, 'cd8': 2700000.0, '3cd4': 16200000.0, '3cd8': 8100000.0, + '5cd8': 13500000.0, '7cd8': 18900000.0} + for n in (2, 3, 4, 5, 6, 8, 10, 12, 16, 32): + v[f'wd{n}'] = w / n; v[f'hd{n}'] = h / n; v[f'ssd{n}'] = ss / n + return v + +def ev(f, v): + p = f.split() + def g(x): + return v[x] if x in v else float(x) + op = p[0] + a = [g(x) for x in p[1:]] + rad = lambda x: x / 60000.0 * math.pi / 180.0 + if op == 'val': return a[0] + if op == '*/': return a[0] * a[1] / a[2] if a[2] else 0.0 + if op == '+-': return a[0] + a[1] - a[2] + if op == '+/': return (a[0] + a[1]) / a[2] if a[2] else 0.0 + if op == '?:': return a[1] if a[0] > 0 else a[2] + if op == 'abs': return abs(a[0]) + if op == 'at2': return math.degrees(math.atan2(a[1], a[0])) * 60000.0 + if op == 'cat2': return a[0] * math.cos(math.atan2(a[2], a[1])) + if op == 'sat2': return a[0] * math.sin(math.atan2(a[2], a[1])) + if op == 'cos': return a[0] * math.cos(rad(a[1])) + if op == 'sin': return a[0] * math.sin(rad(a[1])) + if op == 'tan': return a[0] * math.tan(rad(a[1])) + if op == 'max': return max(a[0], a[1]) + if op == 'min': return min(a[0], a[1]) + if op == 'mod': return math.sqrt(a[0] ** 2 + a[1] ** 2 + a[2] ** 2) + if op == 'pin': return a[0] if a[1] < a[0] else (a[2] if a[1] > a[2] else a[1]) + if op == 'sqrt': return math.sqrt(max(a[0], 0.0)) + raise ValueError(f) + +def main(): + root = ET.parse(sys.argv[1]).getroot() + print('# name | default adjust values | text rect l t r b at 200x100 ; 100x200 ; 144x144') + for sh in root: + av = sh.find(A + 'avLst'); gd = sh.find(A + 'gdLst'); rect = sh.find(A + 'rect') + avs = [] if av is None else [(g.get('name'), g.get('fmla')) for g in av] + defaults = [ev(f, {}) for _, f in avs] + rects = [] + for w, h in SIZES: + v = builtins(w, h) + for n, f in avs: + v[n] = ev(f, v) + for g in ([] if gd is None else gd): + v[g.get('name')] = ev(g.get('fmla'), v) + if rect is None: + rects.append('0 0 %g %g' % (w, h)) + else: + rects.append(' '.join('%.3f' % (v[rect.get(k)] if rect.get(k) in v else float(rect.get(k))) for k in 'ltrb')) + print('%s | %s | %s' % (sh.tag, ' '.join(repr(round(d, 3)).removesuffix('.0') for d in defaults), ' ; '.join(rects))) + +main() diff --git a/crates/drawingml/tests/fixtures/preset_table.txt b/crates/drawingml/tests/fixtures/preset_table.txt new file mode 100644 index 000000000..0a0e973f3 --- /dev/null +++ b/crates/drawingml/tests/fixtures/preset_table.txt @@ -0,0 +1,188 @@ +# name | default adjust values | text rect l t r b at 200x100 ; 100x200 ; 144x144 +accentBorderCallout1 | 18750 -8333 112500 -38333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +accentBorderCallout2 | 18750 -8333 18750 -16667 112500 -46667 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +accentBorderCallout3 | 18750 -8333 18750 -16667 100000 -16667 112963 -8333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +accentCallout1 | 18750 -8333 112500 -38333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +accentCallout2 | 18750 -8333 18750 -16667 112500 -46667 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +accentCallout3 | 18750 -8333 18750 -16667 100000 -16667 112963 -8333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonBackPrevious | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonBeginning | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonBlank | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonDocument | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonEnd | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonForwardNext | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonHelp | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonHome | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonInformation | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonMovie | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonReturn | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +actionButtonSound | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +arc | 16200000 0 | 100.000 0.000 200.000 50.000 ; 50.000 0.000 100.000 100.000 ; 72.000 0.000 144.000 72.000 +bentArrow | 25000 25000 25000 43750 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +bentConnector2 | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +bentConnector3 | 50000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +bentConnector4 | 50000 50000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +bentConnector5 | 50000 50000 50000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +bentUpArrow | 25000 25000 25000 | 0.000 75.000 187.500 100.000 ; 0.000 175.000 87.500 200.000 ; 0.000 108.000 126.000 144.000 +bevel | 12500 | 12.500 12.500 187.500 87.500 ; 12.500 12.500 87.500 187.500 ; 18.000 18.000 126.000 126.000 +blockArc | 10800000 0 25000 | 0.000 0.000 200.000 50.000 ; 0.000 0.000 100.000 100.000 ; 0.000 0.000 144.000 72.000 +borderCallout1 | 18750 -8333 112500 -38333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +borderCallout2 | 18750 -8333 18750 -16667 112500 -46667 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +borderCallout3 | 18750 -8333 18750 -16667 100000 -16667 112963 -8333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +bracePair | 8333 | 10.774 10.774 189.226 97.559 ; 10.774 10.774 89.226 197.559 ; 15.514 15.514 128.486 140.485 +bracketPair | 16667 | 4.882 4.882 195.118 95.118 ; 4.882 4.882 95.118 195.118 ; 7.030 7.030 136.970 136.970 +callout1 | 18750 -8333 112500 -38333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +callout2 | 18750 -8333 18750 -16667 112500 -46667 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +callout3 | 18750 -8333 18750 -16667 100000 -16667 112963 -8333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +can | 25000 | 0.000 25.000 200.000 87.500 ; 0.000 25.000 100.000 187.500 ; 0.000 36.000 144.000 126.000 +chartPlus | | 0 0 200 100 ; 0 0 100 200 ; 0 0 144 144 +chartStar | | 0 0 200 100 ; 0 0 100 200 ; 0 0 144 144 +chartX | | 0 0 200 100 ; 0 0 100 200 ; 0 0 144 144 +chevron | 50000 | 50.000 0.000 150.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +chord | 2700000 16200000 | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +circularArrow | 12500 1142319 20457681 10800000 12500 | 33.709 19.064 166.291 80.936 ; 19.064 33.709 80.936 166.291 ; 27.452 27.452 116.548 116.548 +cloud | | 27.565 15.102 158.213 80.264 ; 13.782 30.204 79.106 160.528 ; 19.847 21.747 113.913 115.580 +cloudCallout | -20833 62500 | 27.565 15.102 158.213 80.264 ; 13.782 30.204 79.106 160.528 ; 19.847 21.747 113.913 115.580 +corner | 50000 50000 | 0.000 50.000 200.000 100.000 ; 0.000 0.000 50.000 200.000 ; 0.000 0.000 72.000 144.000 +cornerTabs | | 11.180 11.180 188.820 88.820 ; 11.180 11.180 88.820 188.820 ; 10.182 10.182 133.818 133.818 +cube | 25000 | 0.000 25.000 175.000 100.000 ; 0.000 25.000 75.000 200.000 ; 0.000 36.000 108.000 144.000 +curvedConnector2 | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +curvedConnector3 | 50000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +curvedConnector4 | 50000 50000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +curvedConnector5 | 50000 50000 50000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +curvedDownArrow | 25000 50000 25000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +curvedLeftArrow | 25000 50000 25000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +curvedRightArrow | 25000 50000 25000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +curvedUpArrow | 25000 50000 25000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +decagon | 105146 | 19.098 19.098 180.902 80.902 ; 9.549 38.197 90.451 161.803 ; 13.751 27.502 130.249 116.498 +diagStripe | 50000 | 0.000 0.000 150.000 75.000 ; 0.000 0.000 75.000 150.000 ; 0.000 0.000 108.000 108.000 +diamond | | 50.000 25.000 150.000 75.000 ; 25.000 50.000 75.000 150.000 ; 36.000 36.000 108.000 108.000 +dodecagon | | 26.796 13.398 173.204 86.602 ; 13.398 26.796 86.602 173.204 ; 19.293 19.293 124.707 124.707 +donut | 25000 | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +doubleWave | 6250 0 | 0.000 12.500 200.000 87.500 ; 0.000 25.000 100.000 175.000 ; 0.000 18.000 144.000 126.000 +downArrow | 50000 50000 | 50.000 0.000 150.000 75.000 ; 25.000 0.000 75.000 175.000 ; 36.000 0.000 108.000 108.000 +downArrowCallout | 25000 25000 25000 64977 | 0.000 0.000 200.000 64.977 ; 0.000 0.000 100.000 129.954 ; 0.000 0.000 144.000 93.567 +ellipse | | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +ellipseRibbon | 25000 50000 12500 | 50.000 25.000 150.000 96.875 ; 25.000 50.000 75.000 193.750 ; 36.000 36.000 108.000 139.500 +ellipseRibbon2 | 25000 50000 12500 | 50.000 3.125 150.000 75.000 ; 25.000 6.250 75.000 150.000 ; 36.000 4.500 108.000 108.000 +flowChartAlternateProcess | | 4.882 4.882 195.119 95.118 ; 4.882 4.882 95.118 195.119 ; 7.029 7.029 136.971 136.971 +flowChartCollate | | 50.000 25.000 150.000 75.000 ; 25.000 50.000 75.000 150.000 ; 36.000 36.000 108.000 108.000 +flowChartConnector | | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +flowChartDecision | | 50.000 25.000 150.000 75.000 ; 25.000 50.000 75.000 150.000 ; 36.000 36.000 108.000 108.000 +flowChartDelay | | 0.000 14.645 170.711 85.355 ; 0.000 29.289 85.355 170.711 ; 0.000 21.088 122.912 122.912 +flowChartDisplay | | 33.333 0.000 166.667 100.000 ; 16.667 0.000 83.333 200.000 ; 24.000 0.000 120.000 144.000 +flowChartDocument | | 0.000 0.000 200.000 80.194 ; 0.000 0.000 100.000 160.389 ; 0.000 0.000 144.000 115.480 +flowChartExtract | | 50.000 50.000 150.000 100.000 ; 25.000 100.000 75.000 200.000 ; 36.000 72.000 108.000 144.000 +flowChartInputOutput | | 40.000 0.000 160.000 100.000 ; 20.000 0.000 80.000 200.000 ; 28.800 0.000 115.200 144.000 +flowChartInternalStorage | | 25.000 12.500 200.000 100.000 ; 12.500 25.000 100.000 200.000 ; 18.000 18.000 144.000 144.000 +flowChartMagneticDisk | | 0.000 33.333 200.000 83.333 ; 0.000 66.667 100.000 166.667 ; 0.000 48.000 144.000 120.000 +flowChartMagneticDrum | | 33.333 0.000 133.333 100.000 ; 16.667 0.000 66.667 200.000 ; 24.000 0.000 96.000 144.000 +flowChartMagneticTape | | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +flowChartManualInput | | 0.000 20.000 200.000 100.000 ; 0.000 40.000 100.000 200.000 ; 0.000 28.800 144.000 144.000 +flowChartManualOperation | | 40.000 0.000 160.000 100.000 ; 20.000 0.000 80.000 200.000 ; 28.800 0.000 115.200 144.000 +flowChartMerge | | 50.000 0.000 150.000 50.000 ; 25.000 0.000 75.000 100.000 ; 36.000 0.000 108.000 72.000 +flowChartMultidocument | | 0.000 17.014 172.176 96.213 ; 0.000 34.028 86.088 192.426 ; 0.000 24.500 123.967 138.547 +flowChartOfflineStorage | | 50.000 0.000 150.000 50.000 ; 25.000 0.000 75.000 100.000 ; 36.000 0.000 108.000 72.000 +flowChartOffpageConnector | | 0.000 0.000 200.000 80.000 ; 0.000 0.000 100.000 160.000 ; 0.000 0.000 144.000 115.200 +flowChartOnlineStorage | | 33.333 0.000 166.667 100.000 ; 16.667 0.000 83.333 200.000 ; 24.000 0.000 120.000 144.000 +flowChartOr | | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +flowChartPredefinedProcess | | 25.000 0.000 175.000 100.000 ; 12.500 0.000 87.500 200.000 ; 18.000 0.000 126.000 144.000 +flowChartPreparation | | 40.000 0.000 160.000 100.000 ; 20.000 0.000 80.000 200.000 ; 28.800 0.000 115.200 144.000 +flowChartProcess | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +flowChartPunchedCard | | 0.000 20.000 200.000 100.000 ; 0.000 40.000 100.000 200.000 ; 0.000 28.800 144.000 144.000 +flowChartPunchedTape | | 0.000 20.000 200.000 80.000 ; 0.000 40.000 100.000 160.000 ; 0.000 28.800 144.000 115.200 +flowChartSort | | 50.000 25.000 150.000 75.000 ; 25.000 50.000 75.000 150.000 ; 36.000 36.000 108.000 108.000 +flowChartSummingJunction | | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +flowChartTerminator | | 9.426 14.644 190.574 85.356 ; 4.713 29.287 95.287 170.713 ; 6.787 21.087 137.213 122.913 +foldedCorner | 16667 | 0.000 0.000 200.000 83.333 ; 0.000 0.000 100.000 183.333 ; 0.000 0.000 144.000 120.000 +frame | 12500 | 12.500 12.500 187.500 87.500 ; 12.500 12.500 87.500 187.500 ; 18.000 18.000 126.000 126.000 +funnel | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +gear6 | 15000 3526 | 39.711 25.327 160.289 74.673 ; 25.175 40.081 74.825 159.919 ; 36.252 36.472 107.748 107.528 +gear9 | 10000 1763 | 32.735 23.425 167.265 74.827 ; 20.104 40.205 79.896 155.860 ; 28.950 33.731 115.050 107.750 +halfFrame | 33333 33333 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +heart | | 33.333 25.000 166.667 66.667 ; 16.667 50.000 83.333 133.333 ; 24.000 36.000 120.000 96.000 +heptagon | 102572 105210 | 19.806 19.806 180.194 80.194 ; 9.903 39.613 90.097 160.387 ; 14.260 28.521 129.740 115.479 +hexagon | 25000 115470 | 25.000 12.500 175.000 87.500 ; 16.667 33.333 83.333 166.667 ; 24.000 24.000 120.000 120.000 +homePlate | 50000 | 0.000 0.000 175.000 100.000 ; 0.000 0.000 75.000 200.000 ; 0.000 0.000 108.000 144.000 +horizontalScroll | 12500 | 12.500 12.500 193.750 87.500 ; 12.500 12.500 93.750 187.500 ; 18.000 18.000 135.000 126.000 +irregularSeal1 | | 42.843 29.259 154.648 64.523 ; 21.421 58.519 77.324 129.046 ; 30.847 42.133 111.347 92.913 +irregularSeal2 | | 49.741 29.546 135.556 73.773 ; 24.870 59.093 67.778 147.546 ; 35.813 42.547 97.600 106.233 +leftArrow | 50000 50000 | 25.000 25.000 200.000 75.000 ; 25.000 50.000 100.000 150.000 ; 36.000 36.000 144.000 108.000 +leftArrowCallout | 25000 25000 25000 64977 | 70.046 0.000 200.000 100.000 ; 35.023 0.000 100.000 200.000 ; 50.433 0.000 144.000 144.000 +leftBrace | 8333 50000 | 129.289 2.441 200.000 97.559 ; 64.645 2.441 100.000 197.559 ; 93.088 3.515 144.000 140.485 +leftBracket | 8333 | 58.579 2.441 200.000 97.559 ; 29.289 2.441 100.000 197.559 ; 42.177 3.515 144.000 140.485 +leftCircularArrow | 12500 -1142319 1142319 10800000 12500 | 33.709 19.064 166.291 80.936 ; 19.064 33.709 80.936 166.291 ; 27.452 27.452 116.548 116.548 +leftRightArrow | 50000 50000 | 25.000 25.000 175.000 75.000 ; 25.000 50.000 75.000 150.000 ; 36.000 36.000 108.000 108.000 +leftRightArrowCallout | 25000 25000 25000 48123 | 51.877 0.000 148.123 100.000 ; 25.939 0.000 74.061 200.000 ; 37.351 0.000 106.649 144.000 +leftRightCircularArrow | 12500 1142319 20457681 11942319 12500 | 33.709 19.064 166.291 80.936 ; 19.064 33.709 80.936 166.291 ; 27.452 27.452 116.548 116.548 +leftRightRibbon | 50000 50000 16667 | 50.000 16.666 150.000 83.334 ; 46.875 33.333 53.125 166.667 ; 67.500 24.000 76.500 120.000 +leftRightUpArrow | 25000 25000 25000 | 12.500 62.500 187.500 87.500 ; 12.500 162.500 87.500 187.500 ; 18.000 90.000 126.000 126.000 +leftUpArrow | 25000 25000 25000 | 12.500 62.500 175.000 87.500 ; 12.500 162.500 75.000 187.500 ; 18.000 90.000 108.000 126.000 +lightningBolt | | 81.083 34.431 128.861 66.097 ; 40.542 68.861 64.431 132.194 ; 58.380 49.580 92.780 95.180 +line | | 0 0 200 100 ; 0 0 100 200 ; 0 0 144 144 +lineInv | | 0 0 200 100 ; 0 0 100 200 ; 0 0 144 144 +mathDivide | 23520 5880 11760 | 26.510 38.240 173.490 61.760 ; 13.255 76.480 86.745 123.520 ; 19.087 55.066 124.913 88.934 +mathEqual | 23520 11760 | 26.510 20.600 173.490 79.400 ; 13.255 41.200 86.745 158.800 ; 19.087 29.664 124.913 114.336 +mathMinus | 23520 | 26.510 38.240 173.490 61.760 ; 13.255 76.480 86.745 123.520 ; 19.087 55.066 124.913 88.934 +mathMultiply | 23520 | 42.776 13.499 157.224 86.501 ; 13.499 42.776 86.501 157.224 ; 22.611 22.611 121.389 121.389 +mathNotEqual | 23520 6600000 11760 | 26.510 20.600 173.490 79.400 ; 13.255 41.200 86.745 158.800 ; 19.087 29.664 124.913 114.336 +mathPlus | 23520 | 26.510 38.240 173.490 61.760 ; 13.255 88.240 86.745 111.760 ; 19.087 55.066 124.913 88.934 +moon | 50000 | 29.291 23.949 100.000 76.051 ; 14.645 47.898 50.000 152.102 ; 21.089 34.487 72.000 109.513 +nonIsoscelesTrapezoid | 25000 25000 | 16.667 8.333 183.333 100.000 ; 16.667 33.333 83.333 200.000 ; 24.000 24.000 120.000 144.000 +noSmoking | 18750 | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +notchedRightArrow | 50000 50000 | 25.000 25.000 175.000 75.000 ; 25.000 50.000 75.000 150.000 ; 36.000 36.000 108.000 108.000 +octagon | 29289 | 14.645 14.645 185.356 85.356 ; 14.645 14.645 85.356 185.356 ; 21.088 21.088 122.912 122.912 +parallelogram | 25000 | 27.083 13.542 172.917 86.458 ; 18.750 37.500 81.250 162.500 ; 27.000 27.000 117.000 117.000 +pentagon | 105146 110557 | 38.197 23.607 161.803 100.000 ; 19.098 47.213 80.902 199.999 ; 27.502 33.994 116.498 144.000 +pie | 0 16200000 | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +pieWedge | | 58.579 29.289 200.000 100.000 ; 29.289 58.579 100.000 200.000 ; 42.177 42.177 144.000 144.000 +plaque | 16667 | 11.785 11.785 188.215 88.215 ; 11.785 11.785 88.215 188.215 ; 16.971 16.971 127.029 127.029 +plaqueTabs | | 11.180 11.180 188.820 88.820 ; 11.180 11.180 88.820 188.820 ; 10.182 10.182 133.818 133.818 +plus | 25000 | 0.000 25.000 200.000 75.000 ; 25.000 0.000 75.000 200.000 ; 36.000 0.000 108.000 144.000 +quadArrow | 22500 22500 22500 | 11.250 38.750 188.750 61.250 ; 11.250 88.750 88.750 111.250 ; 16.200 55.800 127.800 88.200 +quadArrowCallout | 18515 18515 18515 48123 | 51.877 25.939 148.123 74.061 ; 25.939 51.877 74.061 148.123 ; 37.351 37.351 106.649 106.649 +rect | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +ribbon | 16667 50000 | 50.000 16.667 150.000 100.000 ; 25.000 33.334 75.000 200.000 ; 36.000 24.000 108.000 144.000 +ribbon2 | 16667 50000 | 50.000 0.000 150.000 83.333 ; 25.000 0.000 75.000 166.666 ; 36.000 0.000 108.000 120.000 +rightArrow | 50000 50000 | 0.000 25.000 175.000 75.000 ; 0.000 50.000 75.000 150.000 ; 0.000 36.000 108.000 108.000 +rightArrowCallout | 25000 25000 25000 64977 | 0.000 0.000 129.954 100.000 ; 0.000 0.000 64.977 200.000 ; 0.000 0.000 93.567 144.000 +rightBrace | 8333 50000 | 0.000 2.441 70.711 97.559 ; 0.000 2.441 35.355 197.559 ; 0.000 3.515 50.912 140.485 +rightBracket | 8333 | 0.000 2.441 141.421 97.559 ; 0.000 2.441 70.711 197.559 ; 0.000 3.515 101.823 140.485 +round1Rect | 16667 | 0.000 0.000 195.118 100.000 ; 0.000 0.000 95.118 200.000 ; 0.000 0.000 136.970 144.000 +round2DiagRect | 16667 0 | 4.882 4.882 195.118 95.118 ; 4.882 4.882 95.118 195.118 ; 7.030 7.030 136.970 136.970 +round2SameRect | 16667 0 | 4.882 4.882 195.118 100.000 ; 4.882 4.882 95.118 200.000 ; 7.030 7.030 136.970 144.000 +roundRect | 16667 | 4.882 4.882 195.118 95.118 ; 4.882 4.882 95.118 195.118 ; 7.030 7.030 136.970 136.970 +rtTriangle | | 16.667 58.333 116.667 91.667 ; 8.333 116.667 58.333 183.333 ; 12.000 84.000 84.000 132.000 +smileyFace | 4653 | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +snip1Rect | 16667 | 0.000 8.334 191.666 100.000 ; 0.000 8.334 91.666 200.000 ; 0.000 12.000 132.000 144.000 +snip2DiagRect | 0 16667 | 8.334 8.334 191.666 91.666 ; 8.334 8.334 91.666 191.666 ; 12.000 12.000 132.000 132.000 +snip2SameRect | 16667 0 | 8.334 8.334 191.666 100.000 ; 8.334 8.334 91.666 200.000 ; 12.000 12.000 132.000 144.000 +snipRoundRect | 16667 16667 | 4.882 4.882 191.666 100.000 ; 4.882 4.882 91.666 200.000 ; 7.030 7.030 132.000 144.000 +squareTabs | | 11.180 11.180 188.820 88.820 ; 11.180 11.180 88.820 188.820 ; 10.182 10.182 133.818 133.818 +star10 | 42533 105146 | 27.638 25.000 172.362 75.000 ; 13.819 49.999 86.181 150.001 ; 19.900 35.999 124.100 108.001 +star12 | 37500 | 46.967 23.483 153.033 76.517 ; 23.483 46.967 76.517 153.033 ; 33.816 33.816 110.184 110.184 +star16 | 37500 | 46.967 23.483 153.033 76.517 ; 23.483 46.967 76.517 153.033 ; 33.816 33.816 110.184 110.184 +star24 | 37500 | 46.967 23.483 153.033 76.517 ; 23.483 46.967 76.517 153.033 ; 33.816 33.816 110.184 110.184 +star32 | 37500 | 46.967 23.483 153.033 76.517 ; 23.483 46.967 76.517 153.033 ; 33.816 33.816 110.184 110.184 +star4 | 12500 | 82.322 41.161 117.678 58.839 ; 41.161 82.322 58.839 117.678 ; 59.272 59.272 84.728 84.728 +star5 | 19098 105146 110557 | 61.804 38.197 138.196 76.393 ; 30.902 76.394 69.098 152.785 ; 44.499 55.003 99.501 110.005 +star6 | 28868 115470 | 33.332 25.000 166.668 75.000 ; 16.666 49.999 83.334 150.001 ; 23.999 35.999 120.001 108.001 +star7 | 34601 102572 105210 | 44.504 19.806 155.496 75.302 ; 22.252 39.613 77.748 150.605 ; 32.043 28.521 111.957 108.435 +star8 | 37500 | 30.709 15.355 169.291 84.645 ; 15.355 30.709 84.645 169.291 ; 22.110 22.110 121.890 121.890 +straightConnector1 | | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +stripedRightArrow | 50000 50000 | 15.625 25.000 175.000 75.000 ; 15.625 50.000 75.000 150.000 ; 22.500 36.000 108.000 108.000 +sun | 25000 | 64.645 32.323 135.355 67.677 ; 32.323 64.645 67.677 135.355 ; 46.545 46.545 97.455 97.455 +swooshArrow | 25000 16667 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +teardrop | 100000 | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +trapezoid | 25000 | 16.667 8.333 183.333 100.000 ; 16.667 33.333 83.333 200.000 ; 24.000 24.000 120.000 144.000 +triangle | 50000 | 50.000 50.000 150.000 100.000 ; 25.000 100.000 75.000 200.000 ; 36.000 72.000 108.000 144.000 +upArrowCallout | 25000 25000 25000 64977 | 0.000 35.023 200.000 100.000 ; 0.000 70.046 100.000 200.000 ; 0.000 50.433 144.000 144.000 +upDownArrow | 50000 50000 | 50.000 25.000 150.000 75.000 ; 25.000 25.000 75.000 175.000 ; 36.000 36.000 108.000 108.000 +upArrow | 50000 50000 | 50.000 25.000 150.000 100.000 ; 25.000 25.000 75.000 200.000 ; 36.000 36.000 108.000 144.000 +upDownArrowCallout | 25000 25000 25000 48123 | 0.000 25.939 200.000 74.061 ; 0.000 51.877 100.000 148.123 ; 0.000 37.351 144.000 106.649 +uturnArrow | 25000 25000 25000 43750 75000 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +verticalScroll | 12500 | 12.500 12.500 187.500 93.750 ; 12.500 12.500 87.500 193.750 ; 18.000 18.000 126.000 135.000 +wave | 12500 0 | 0.000 25.000 200.000 75.000 ; 0.000 50.000 100.000 150.000 ; 0.000 36.000 144.000 108.000 +wedgeEllipseCallout | -20833 62500 | 29.289 14.645 170.711 85.355 ; 14.645 29.289 85.355 170.711 ; 21.088 21.088 122.912 122.912 +wedgeRectCallout | -20833 62500 | 0.000 0.000 200.000 100.000 ; 0.000 0.000 100.000 200.000 ; 0.000 0.000 144.000 144.000 +wedgeRoundRectCallout | -20833 62500 16667 | 4.882 4.882 195.118 95.118 ; 4.882 4.882 95.118 195.118 ; 7.030 7.030 136.970 136.970 diff --git a/crates/drawingml/tests/preset_table.rs b/crates/drawingml/tests/preset_table.rs new file mode 100644 index 000000000..b9a5b9ecd --- /dev/null +++ b/crates/drawingml/tests/preset_table.rs @@ -0,0 +1,46 @@ +//! Every preset against ECMA-376's presetShapeDefinitions.xml, evaluated by +//! `fixtures/make_preset_table.py`: default adjust values and the text rectangle at three sizes. + +use craft_drawingml::preset::{adjust_defaults as defaults, build, info}; + +#[test] +fn presets_follow_preset_shape_definitions() { + let table = include_str!("fixtures/preset_table.txt"); + let sizes = [(200.0, 100.0), (100.0, 200.0), (144.0, 144.0)]; + let mut bad = vec![]; + let mut checked = 0; + for line in table.lines().filter(|l| !l.starts_with('#') && !l.trim().is_empty()) { + let f: Vec<&str> = line.split(" | ").collect(); + let name = f[0]; + // Connectors the gallery doesn't list still build, with the standard's defaults. + if info(name).is_none() && !name.contains("Connector") { + bad.push(format!("{name}: not in the catalogue")); + continue; + } + let have = defaults(name); + checked += 1; + let want: Vec = f[1].split_whitespace().map(|v| v.parse().unwrap()).collect(); + // The standard lists stretch factors (star5's hf/vf…) after the adjust handles; the + // catalogue keeps handles only, so compare what both have. + for (k, (a, b)) in have.iter().zip(&want).enumerate() { + if (a - b).abs() > 0.5 { + bad.push(format!("{name}: adj{} default {a} (standard {b})", k + 1)); + } + } + let count_mismatch = if name.contains("Connector") { have.len() != want.len() } else { have.len() > want.len() }; + if count_mismatch { + bad.push(format!("{name}: {} defaults (standard {})", have.len(), want.len())); + } + for ((w, h), r) in sizes.iter().zip(f[2].split(" ; ")) { + let r: Vec = r.split_whitespace().map(|v| v.parse().unwrap()).collect(); + let g = build(name, *w, *h, &[]).unwrap_or_else(|| panic!("{name} builds")); + let t = g.text_rect; + let got = [t.x0, t.y0, t.x1, t.y1]; + if got.iter().zip(&r).any(|(a, b)| (a - b).abs() > 0.6) { + bad.push(format!("{name} {w}x{h}: text rect {got:.1?} (standard {r:.1?})")); + } + } + } + assert!(checked > 180, "only {checked} presets checked"); + assert!(bad.is_empty(), "{} mismatches:\n{}", bad.len(), bad.join("\n")); +} diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index 8ee87836f..22248a3a9 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -123,7 +123,9 @@ pub fn geometry(w: &mut Writer, g: &Geom) { w.empty("a:avLst", &[]); w.close(); } else { - craft_drawingml::custgeom::write_cust_geom(w, paths); + // `craft-pptx` does not yet retain custom-geometry connection sites. + // Zero extent keeps the previous path-only output until the model carries them. + craft_drawingml::custgeom::write_cust_geom(w, paths, &[], (0, 0)); } } }