mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-26 19:46:58 -06:00
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:
co-authored by
Claude Opus 5.5
parent
fa32300d67
commit
ca1f6ab544
+277
-102
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user