Geom: lightning bolt, explosions, heart, cloud and document flowcharts as the standard defines them

These presets were hand-drawn approximations and looked visibly different
from PowerPoint's (the lightning bolt had 7 points instead of 11, the
explosions were different polygons, the cloud a ring of lobes without
creases). They now follow ECMA-376's preset geometry, checked against the
outlines PowerPoint gives for them (black-box: merging each preset with a
far-away square and reading the resulting freeform):
- lightningBolt, irregularSeal1/2: the standard's polygons (21600ths);
- heart: its two cubic curves;
- cloud (and cloudCallout's outline): the standard's eleven arcs drawn in
  the path's own 43200-unit square, plus the crease arcs, stretched to the
  box; arcs take DrawingML's angle convention (direction from the
  centre), new Pb::arc_spec;
- flowChartDocument, flowChartMultidocument (three pages, outlined where
  they show), flowChartPunchedTape (half-ellipse edges).

Wild synthetic set (per-shape mean diff, 1:1 gallery): lightningBolt
64.1 -> 2.8, irregularSeal1 41.1 -> 5.3, irregularSeal2 41.2 -> 4.8, cloud
28.2 -> 4.0, multidocument 30.9 -> 7.0, punched tape 27.7 -> 4.1, heart
20.9 -> 3.9, document 15.3 -> 3.8. Whole set 5.05 -> 4.84.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-13 08:14:09 -04:00
co-authored by Claude Opus 5.5
parent fa32300d67
commit ca1f6ab544
+277 -102
View File
@@ -6,7 +6,7 @@
//! geometry table. Adjust values use the file convention: ratios in 1/100 000 of the shorter side (or
//! of the width/height), angles in 1/60 000 degree.
use kurbo::{Arc, BezPath, Point, Rect, Shape as _, Vec2};
use kurbo::{Arc, BezPath, Point, Rect, Vec2};
use serde::{Deserialize, Serialize};
mod extra;
@@ -388,6 +388,29 @@ impl Pb {
self.cur = Point::new(c.x + wr * e.cos(), c.y + hr * e.sin());
self
}
/// A DrawingML `arcTo`: angles are where the ray from the ellipse's centre at that angle
/// meets it (not the ellipse's parameter), so elliptical arcs start and end where the
/// standard's shapes expect.
fn arc_spec(&mut self, wr: f64, hr: f64, st: f64, sw: f64) -> &mut Self {
let par = |a: f64| {
let a = a.to_radians();
(wr * a.sin()).atan2(hr * a.cos())
};
let t0 = par(st);
let mut d = par(st + sw) - t0;
let tau = std::f64::consts::TAU;
if sw.abs() >= 360.0 {
d = tau.copysign(sw);
} else {
while sw > 0.0 && d < 0.0 {
d += tau;
}
while sw < 0.0 && d > 0.0 {
d -= tau;
}
}
self.arc(wr, hr, t0.to_degrees(), d.to_degrees())
}
fn z(&mut self) -> &mut Self {
self.p.close_path();
self
@@ -605,6 +628,11 @@ fn double_arrow(vertical: bool, w: f64, h: f64, a1: f64, a2: f64) -> Geometry {
Geometry { paths: vec![fill(Pb::new().poly(&pts).take())], text_rect: r, handles: vec![], sites: mid_sites(w, h) }
}
/// Points given in 21600ths of the box (as ECMA-376 defines many fixed shapes), scaled to it.
fn spec_pts(pts: &[(i32, i32)], w: f64, h: f64) -> Vec<(f64, f64)> {
pts.iter().map(|(x, y)| (w * *x as f64 / 21600.0, h * *y as f64 / 21600.0)).collect()
}
fn simple(path: BezPath, text: Rect, w: f64, h: f64) -> Geometry {
Geometry { paths: vec![fill(path)], text_rect: text, handles: vec![], sites: mid_sites(w, h) }
}
@@ -1048,17 +1076,35 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
}
}
"heart" => {
b.m(w / 2.0, h * 0.25);
b.c(w * 0.62, h * -0.02, w, h * 0.0, w, h * 0.32);
b.c(w, h * 0.6, w * 0.62, h * 0.78, w / 2.0, h);
b.c(w * 0.38, h * 0.78, 0.0, h * 0.6, 0.0, h * 0.32);
b.c(0.0, h * 0.0, w * 0.38, h * -0.02, w / 2.0, h * 0.25);
b.z();
// ECMA-376 heart: two cubic curves from the notch at the top to the point.
let (dx1, dx2) = (w * 49.0 / 48.0, w * 10.0 / 48.0);
let y1 = -h / 3.0;
b.m(w / 2.0, h / 4.0)
.c(w / 2.0 + dx2, y1, w / 2.0 + dx1, h / 4.0, w / 2.0, h)
.c(w / 2.0 - dx1, h / 4.0, w / 2.0 - dx2, y1, w / 2.0, h / 4.0)
.z();
simple(b.take(), Rect::new(w * 0.2, h * 0.25, w * 0.8, h * 0.7), w, h)
}
"lightningBolt" => {
let pts = normalize(&[(0.36, 0.0), (0.68, 0.0), (0.53, 0.34), (0.79, 0.34), (0.24, 1.0), (0.39, 0.53), (0.15, 0.53)], w, h);
simple(b.poly(&pts).take(), Rect::new(w * 0.3, h * 0.35, w * 0.65, h * 0.6), w, h)
// ECMA-376 lightningBolt, in 21600ths of the box.
let pts = spec_pts(
&[
(8472, 0),
(12860, 6080),
(11050, 6797),
(16577, 12007),
(14767, 12877),
(21600, 21600),
(10012, 14915),
(12222, 13987),
(5022, 9705),
(7602, 8382),
(0, 3890),
],
w,
h,
);
simple(b.poly(&pts).take(), Rect::new(w * 8757.0 / 21600.0, h * 7637.0 / 21600.0, w * 13917.0 / 21600.0, h * 12963.0 / 21600.0), w, h)
}
"sun" => {
let k = a(0).clamp(12500.0, 46875.0) / 100000.0;
@@ -1378,18 +1424,45 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
}
}
"flowChartDocument" => {
b.m(0.0, 0.0).l(w, 0.0).l(w, h * 0.8).c(w * 0.7, h * 0.62, w * 0.35, h * 1.08, 0.0, h * 0.88).z();
simple(b.take(), Rect::new(0.0, 0.0, w, h * 0.78), w, h)
let u = |x: f64, y: f64| (w * x / 21600.0, h * y / 21600.0);
let (p1, p2, p3, p4) = (u(21600.0, 17322.0), u(10800.0, 17322.0), u(10800.0, 23922.0), u(0.0, 20172.0));
b.m(0.0, 0.0).l(w, 0.0).l(p1.0, p1.1).c(p2.0, p2.1, p3.0, p3.1, p4.0, p4.1).z();
simple(b.take(), Rect::new(0.0, 0.0, w, h * 17322.0 / 21600.0), w, h)
}
"flowChartMultidocument" => {
let (dx, dy) = (w * 0.1, h * 0.1);
let mut back = Pb::new();
back.poly(&[(2.0 * dx, 0.0), (w, 0.0), (w, h * 0.7), (w - dx * 0.5, h * 0.7), (w - dx * 0.5, dy * 0.5), (2.0 * dx, dy * 0.5)]);
back.poly(&[(dx, dy * 0.5), (w - dx * 0.5, dy * 0.5), (w - dx * 0.5, h * 0.78), (w - dx, h * 0.78), (w - dx, dy), (dx, dy)]);
let front = Pb::new().m(0.0, dy).l(w - dx, dy).l(w - dx, h * 0.84).c(w * 0.62, h * 0.7, w * 0.32, h * 1.08, 0.0, h * 0.9).z().take();
// ECMA-376 multidocument (21600ths): two back pages peeking out above and right of
// the front one; the back pages' edges are outlined only where they show.
let x = |v: f64| w * v / 21600.0;
let y = |v: f64| h * v / 21600.0;
let front = |b: &mut Pb| {
b.m(0.0, y(20782.0))
.c(x(9298.0), y(23542.0), x(9298.0), y(18022.0), x(18595.0), y(18022.0))
.l(x(18595.0), y(3675.0))
.l(0.0, y(3675.0))
.z();
};
let mid = |b: &mut Pb| {
b.m(x(1532.0), y(3675.0)).l(x(1532.0), y(1815.0)).l(x(20000.0), y(1815.0)).l(x(20000.0), y(16252.0));
b.c(x(19298.0), y(16252.0), x(18595.0), y(16352.0), x(18595.0), y(16352.0));
};
let back = |b: &mut Pb| {
b.m(x(2972.0), y(1815.0)).l(x(2972.0), 0.0).l(w, 0.0).l(w, y(14392.0));
b.c(x(20800.0), y(14392.0), x(20000.0), y(14467.0), x(20000.0), y(14467.0));
};
let mut area = Pb::new();
front(&mut area);
mid(&mut area);
area.l(x(18595.0), y(3675.0)).z();
back(&mut area);
area.l(x(20000.0), y(1815.0)).z();
let mut outline = Pb::new();
outline.m(0.0, y(3675.0)).l(x(18595.0), y(3675.0)).l(x(18595.0), y(18022.0));
outline.c(x(9298.0), y(18022.0), x(9298.0), y(23542.0), 0.0, y(20782.0)).z();
mid(&mut outline);
back(&mut outline);
Geometry {
paths: vec![fill(back.take()), fill(front)],
text_rect: Rect::new(0.0, dy, w - dx, h * 0.8),
paths: vec![fill_only(area.take()), stroke_only(outline.take())],
text_rect: Rect::new(0.0, y(3675.0), x(18595.0), y(17322.0)),
handles: vec![],
sites: mid_sites(w, h),
}
@@ -1407,10 +1480,11 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
simple(b.poly(&[(w * 0.2, 0.0), (w, 0.0), (w, h), (0.0, h), (0.0, h * 0.2)]).take(), Rect::new(0.0, h * 0.2, w, h), w, h)
}
"flowChartPunchedTape" => {
let a1 = h * 0.1;
b.m(0.0, a1).c(w * 0.25, a1 * 3.0, w * 0.25, -a1, w / 2.0, a1).c(w * 0.75, a1 * 3.0, w * 0.75, -a1, w, a1).l(w, h - a1);
b.c(w * 0.75, h - 3.0 * a1, w * 0.75, h + a1, w / 2.0, h - a1).c(w * 0.25, h - 3.0 * a1, w * 0.25, h + a1, 0.0, h - a1).z();
simple(b.take(), Rect::new(0.0, h * 0.2, w, h * 0.8), w, h)
// ECMA-376: each long edge is two half-ellipses a quarter of the width wide and a
// tenth of the height deep, one bulging in and one out.
let (rx, ry) = (w / 4.0, h / 10.0);
b.m(0.0, ry).arc(rx, ry, 180.0, -180.0).arc(rx, ry, 180.0, 180.0).l(w, h - ry).arc(rx, ry, 0.0, -180.0).arc(rx, ry, 0.0, 180.0).z();
simple(b.take(), Rect::new(0.0, h / 5.0, w, h * 0.8), w, h)
}
"flowChartSummingJunction" | "flowChartOr" => {
let e = Pb::new().ellipse(w / 2.0, h / 2.0, w / 2.0, h / 2.0).take();
@@ -1463,62 +1537,77 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
g.handles.push(Handle::new(0, Point::new(w / 2.0, h / 2.0), Vec2::new(0.0, -h / 2.0 / 50000.0), v, 0.0, 50000.0));
g
}
"irregularSeal1" | "irregularSeal2" => {
let pts: &[(f64, f64)] = if name == "irregularSeal1" {
"irregularSeal1" => {
// ECMA-376 irregularSeal1 ("Explosion: 8 Points"), in 21600ths of the box.
let pts = spec_pts(
&[
(0.48, 0.18),
(0.62, 0.0),
(0.64, 0.2),
(0.86, 0.1),
(0.78, 0.3),
(1.0, 0.36),
(0.82, 0.5),
(0.94, 0.72),
(0.7, 0.66),
(0.68, 0.9),
(0.54, 0.72),
(0.42, 1.0),
(0.36, 0.74),
(0.14, 0.86),
(0.22, 0.62),
(0.0, 0.6),
(0.18, 0.44),
(0.04, 0.24),
(0.3, 0.3),
(0.26, 0.06),
]
} else {
(10800, 5800),
(14522, 0),
(14155, 5325),
(18380, 4457),
(16702, 7315),
(21097, 8137),
(17607, 10475),
(21600, 13290),
(16837, 12942),
(18145, 18095),
(14020, 14457),
(13247, 19737),
(10532, 14935),
(8485, 21600),
(7715, 15627),
(4762, 17617),
(5667, 13937),
(135, 14587),
(3722, 11775),
(0, 8615),
(4627, 7617),
(370, 2295),
(7312, 6320),
(8352, 2295),
],
w,
h,
);
simple(b.poly(&pts).take(), Rect::new(w * 4627.0 / 21600.0, h * 6320.0 / 21600.0, w * 16702.0 / 21600.0, h * 13937.0 / 21600.0), w, h)
}
"irregularSeal2" => {
// ECMA-376 irregularSeal2 ("Explosion: 14 Points"), in 21600ths of the box.
let pts = spec_pts(
&[
(0.5, 0.16),
(0.58, 0.0),
(0.63, 0.18),
(0.78, 0.06),
(0.76, 0.24),
(0.96, 0.2),
(0.86, 0.38),
(1.0, 0.5),
(0.84, 0.58),
(0.92, 0.76),
(0.74, 0.72),
(0.76, 0.94),
(0.6, 0.8),
(0.5, 1.0),
(0.44, 0.8),
(0.28, 0.94),
(0.28, 0.74),
(0.08, 0.82),
(0.16, 0.62),
(0.0, 0.54),
(0.14, 0.44),
(0.02, 0.28),
(0.22, 0.3),
(0.2, 0.1),
(0.36, 0.22),
(0.4, 0.04),
]
};
let pts: Vec<(f64, f64)> = pts.iter().map(|(x, y)| (x * w, y * h)).collect();
simple(b.poly(&pts).take(), Rect::new(w * 0.25, h * 0.3, w * 0.75, h * 0.7), w, h)
(11462, 4342),
(14790, 0),
(14525, 5777),
(18007, 3172),
(16380, 6532),
(21600, 6645),
(16985, 9402),
(18270, 11290),
(16380, 12310),
(18877, 15632),
(14640, 14350),
(14942, 17370),
(12180, 15935),
(11612, 18842),
(9872, 17370),
(8700, 19712),
(7527, 18125),
(4917, 21600),
(4805, 18240),
(1285, 17825),
(3330, 15370),
(0, 12877),
(3935, 11592),
(1172, 8270),
(5372, 7817),
(4502, 3625),
(8550, 6382),
(9722, 1887),
],
w,
h,
);
simple(b.poly(&pts).take(), Rect::new(w * 5372.0 / 21600.0, h * 6382.0 / 21600.0, w * 14640.0 / 21600.0, h * 15935.0 / 21600.0), w, h)
}
"ribbon" | "ribbon2" => ribbon(name == "ribbon2", w, h, a(0), a(1)),
"verticalScroll" | "horizontalScroll" => scroll(name == "verticalScroll", w, h, a(0)),
@@ -1652,36 +1741,61 @@ fn wedge_rect(w: f64, h: f64, a0: f64, a1: f64, r: f64) -> Geometry {
g
}
/// ECMA-376 cloud outline arcs (radii in 43200ths of the box, angles in 60000ths of a degree,
/// as DrawingML `arcTo` takes them), from the start point (3900, 14370).
const CLOUD_ARCS: [(f64, f64, f64, f64); 11] = [
(6753.0, 9190.0, -11429249.0, 7426832.0),
(5333.0, 7267.0, -8646143.0, 5396714.0),
(4365.0, 5945.0, -8748475.0, 5983381.0),
(4857.0, 6595.0, -7859164.0, 7034504.0),
(5333.0, 7273.0, -4722533.0, 6541615.0),
(6775.0, 9220.0, -2776035.0, 7816140.0),
(5785.0, 7867.0, 37501.0, 6842000.0),
(6752.0, 9215.0, 1347096.0, 6910353.0),
(7720.0, 10543.0, 3974558.0, 4542661.0),
(4360.0, 5918.0, -16496525.0, 8804134.0),
(4345.0, 5945.0, -14809710.0, 9151131.0),
];
/// The cloud's creases: short arcs inside the outline, each from its own start point.
const CLOUD_CREASES: [(f64, f64, f64, f64, f64, f64); 11] = [
(4693.0, 26177.0, 4345.0, 5945.0, 5204520.0, 1585770.0),
(6928.0, 34899.0, 4360.0, 5918.0, 4416628.0, 686848.0),
(16478.0, 39090.0, 6752.0, 9215.0, 8257449.0, 844866.0),
(28827.0, 34751.0, 6752.0, 9215.0, 387196.0, 959901.0),
(34129.0, 22954.0, 5785.0, 7867.0, -4217541.0, 4255042.0),
(41798.0, 15354.0, 5333.0, 7273.0, 1819082.0, 1665090.0),
(38324.0, 5426.0, 4857.0, 6595.0, -824660.0, 891534.0),
(29078.0, 3952.0, 4857.0, 6595.0, -8950887.0, 1091722.0),
(22141.0, 4720.0, 4365.0, 5945.0, -9809656.0, 1061181.0),
(14000.0, 5192.0, 6753.0, 9190.0, -4002417.0, 739161.0),
(4127.0, 15789.0, 6753.0, 9190.0, 9459261.0, 711490.0),
];
fn cloud(name: &str, w: f64, h: f64, a0: f64, a1: f64) -> Geometry {
let n = 10;
let pts: Vec<Point> = (0..n)
.map(|i| {
let t = (i as f64 / n as f64 * 360.0 - 90.0).to_radians();
let wob = if i % 2 == 0 { 1.0 } else { 0.92 };
Point::new(0.5 + 0.40 * wob * t.cos(), 0.5 + 0.36 * wob * t.sin())
})
.collect();
// Drawn in the path's own 43200-unit square, then stretched to the box (as DrawingML lays out
// a path with its own w and h), so the arcs keep their shape's proportions at any aspect.
let to_box = kurbo::Affine::scale_non_uniform(w / 43200.0, h / 43200.0);
let deg = |v: f64| v / 60000.0;
let mut b = Pb::new();
let first = pts.first().copied().unwrap_or(Point::ZERO);
b.m(first.x, first.y);
for i in 0..n {
let p = pts.get(i).copied().unwrap_or(Point::ZERO);
let q = pts.get((i + 1) % n).copied().unwrap_or(Point::ZERO);
let mid = p.midpoint(q);
let out = (mid - Point::new(0.5, 0.5)).normalize();
let bulge = (q - p).hypot() * 0.75;
let c1 = p + (mid - p) * 0.2 + out * bulge;
let c2 = q + (mid - q) * 0.2 + out * bulge;
b.c(c1.x, c1.y, c2.x, c2.y, q.x, q.y);
b.m(3900.0, 14370.0);
for (wr, hr, st, sw) in CLOUD_ARCS {
b.arc_spec(wr, hr, deg(st), deg(sw));
}
b.z();
let mut path = b.take();
let bb = path.bounding_box();
let sx = if bb.width() > 0.0 { w / bb.width() } else { 1.0 };
let sy = if bb.height() > 0.0 { h / bb.height() } else { 1.0 };
path.apply_affine(kurbo::Affine::scale_non_uniform(sx, sy) * kurbo::Affine::translate(-bb.origin().to_vec2()));
let mut paths = vec![fill(path)];
let text_rect = Rect::new(w * 0.2, h * 0.22, w * 0.8, h * 0.78);
let mut creases = Pb::new();
for (x, y, wr, hr, st, sw) in CLOUD_CREASES {
creases.m(x, y).arc_spec(wr, hr, deg(st), deg(sw));
}
let mut outline = b.take();
outline.apply_affine(to_box);
let mut paths = vec![fill(outline)];
if name == "cloud" {
let mut c = creases.take();
c.apply_affine(to_box);
paths.push(stroke_only(c));
}
let text_rect = Rect::new(w * 2977.0 / 21600.0, h * 3262.0 / 21600.0, w * 17087.0 / 21600.0, h * 17337.0 / 21600.0);
let mut handles = vec![];
if name == "cloudCallout" {
let tip = Point::new(w / 2.0 + w * a0 / 1e5, h / 2.0 + h * a1 / 1e5);
@@ -1975,6 +2089,67 @@ mod tests {
assert!(!build("rect", 10.0, 10.0, &[]).unwrap().is_open());
}
fn on_path(g: &Geometry, p: Point, tol: f64) -> bool {
use kurbo::ParamCurveNearest;
g.paths.iter().any(|sp| sp.path.segments().any(|seg| seg.nearest(p, 1e-6).distance_sq <= tol * tol))
}
#[test]
fn fixed_shapes_follow_the_standards_coordinates() {
// Points of ECMA-376's definitions (in 21600ths), checked on a 216 × 108 box.
let (w, h) = (216.0, 108.0);
let at = |x: f64, y: f64| Point::new(w * x / 21600.0, h * y / 21600.0);
let g = build("lightningBolt", w, h, &[]).unwrap();
for (x, y) in [(8472.0, 0.0), (16577.0, 12007.0), (21600.0, 21600.0), (0.0, 3890.0)] {
assert!(on_path(&g, at(x, y), 1e-6), "lightningBolt ({x}, {y})");
}
let g = build("irregularSeal1", w, h, &[]).unwrap();
assert!(on_path(&g, at(14522.0, 0.0), 1e-6) && on_path(&g, at(8485.0, 21600.0), 1e-6));
let g = build("irregularSeal2", w, h, &[]).unwrap();
assert!(on_path(&g, at(14790.0, 0.0), 1e-6) && on_path(&g, at(4917.0, 21600.0), 1e-6));
// The heart's point is the middle of the bottom edge, its notch a quarter down.
let g = build("heart", w, h, &[]).unwrap();
assert!(on_path(&g, Point::new(w / 2.0, h), 1e-6) && on_path(&g, Point::new(w / 2.0, h / 4.0), 1e-6));
let g = build("flowChartDocument", w, h, &[]).unwrap();
assert!(on_path(&g, at(21600.0, 17322.0), 1e-6) && on_path(&g, at(0.0, 20172.0), 1e-6));
// Punched tape: the top edge dips to a fifth of the height a quarter of the way along.
let g = build("flowChartPunchedTape", w, h, &[]).unwrap();
assert!(on_path(&g, Point::new(w / 4.0, h / 5.0), 1e-3) && on_path(&g, Point::new(w * 0.75, 0.0), 1e-3));
let g = build("flowChartMultidocument", w, h, &[]).unwrap();
assert!(on_path(&g, at(2972.0, 0.0), 1e-6) && on_path(&g, at(18595.0, 18022.0), 1e-6));
}
#[test]
fn the_cloud_fills_its_box_at_any_aspect_and_has_creases() {
use kurbo::Shape as _;
for (w, h) in [(200.0, 200.0), (300.0, 100.0), (60.0, 180.0)] {
let g = build("cloud", w, h, &[]).unwrap();
let bb = g.paths[0].path.bounding_box();
assert!(bb.x0.abs() < w * 0.02 && (bb.x1 - w).abs() < w * 0.02, "{w}×{h}: {bb:?}");
assert!(bb.y0.abs() < h * 0.02 && (bb.y1 - h).abs() < h * 0.02, "{w}×{h}: {bb:?}");
assert_eq!(g.paths.len(), 2, "outline and creases");
assert!(g.paths[1].stroke && g.paths[1].fill == FillMode::None);
}
}
#[test]
fn spec_arcs_take_angles_as_directions_from_the_centre() {
// A DrawingML arc of a 2:1 ellipse from 45° to 90°: 45° is the direction (1, 1) from the
// centre, which on that ellipse is the point (r/√5)·(2, 1)… scaled so it lies on it.
let mut b = Pb::new();
b.m(0.0, 0.0).arc_spec(200.0, 100.0, 45.0, 45.0);
let p = b.take();
let end = p.elements().last().and_then(|e| match e {
kurbo::PathEl::CurveTo(_, _, q) => Some(*q),
_ => None,
});
// Centre: the start sits at direction 45°, i.e. at (k, k) from it with (k/200)² + (k/100)² = 1.
let k = 1.0 / (1.0f64 / 40000.0 + 1.0 / 10000.0).sqrt();
let c = Point::new(-k, -k);
let q = end.unwrap();
assert!((q.x - c.x).abs() < 1e-6 && (q.y - (c.y + 100.0)).abs() < 1e-6, "{q:?} vs centre {c:?}");
}
#[test]
fn brackets_and_braces_fill_out_to_their_straight_side() {
// PowerPoint fills a bracket or brace (when it has a fill) between its curve and the