From 250cfa3cab99a880c4c03ccbbc5499c210d065de Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 07:35:50 -0400 Subject: [PATCH] Geom: moon, magnetic disk and drum by the standard - The magnetic disk's and drum's end ellipses were half the size of PowerPoint's: ECMA-376 makes them h/6 high (disk) and w/6 across (drum); measured on PowerPoint's outlines. - The moon's inner edge was a half ellipse of the box's height; the standard's is an arc of a larger ellipse through the right corners and (adj * w, middle) whose centre lies right of that point (radius g18w across, dy1 down), and so it is fuller. Text rectangle from the standard too. Wild synthetic set: preset galleries 3.76 -> 3.47, 3.45 -> 3.21, 4.31 -> 4.00 and others slightly. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/geom/src/preset.rs | 56 +++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/crates/geom/src/preset.rs b/crates/geom/src/preset.rs index b93f94f..6a29535 100644 --- a/crates/geom/src/preset.rs +++ b/crates/geom/src/preset.rs @@ -933,7 +933,8 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option { g } "can" | "flowChartMagneticDisk" => { - let v = if name == "can" { a(0).clamp(0.0, 50000.0 * h / ss.max(1e-9)) } else { 33333.0 * h / ss.max(1e-9) / 2.0 }; + // The disk's ends are a third of its height (ECMA-376: ellipses of radius h/6). + let v = if name == "can" { a(0).clamp(0.0, 50000.0 * h / ss.max(1e-9)) } else { 100000.0 / 3.0 * h / ss.max(1e-9) }; let eh = ss * v / 100000.0; let r = eh / 2.0; let body = Pb::new().m(0.0, r).l(0.0, h - r).arc(w / 2.0, r, 180.0, -180.0).l(w, r).arc(w / 2.0, r, 0.0, -180.0).z().take(); @@ -950,7 +951,8 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option { g } "flowChartMagneticDrum" => { - let r = w / 6.0 / 2.0; + // ECMA-376: end ellipses of radius w/6 across. + let r = w / 6.0; let body = Pb::new().m(r, 0.0).l(w - r, 0.0).arc(r, h / 2.0, 270.0, 180.0).l(r, h).arc(r, h / 2.0, 90.0, 180.0).z().take(); let side = Pb::new().m(w - r, 0.0).arc(r, h / 2.0, 270.0, -180.0).take(); Geometry { @@ -1184,10 +1186,36 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option { g } "moon" => { + // ECMA-376 moon: the outer edge is the left half of an ellipse w × h/2 centred on the + // right edge; the inner edge an arc through the corners and (adj · w, middle) of a + // larger ellipse (rx = g18, ry = dy1) whose centre lies right of that point. let k = a(0).clamp(0.0, 87500.0) / 100000.0; - let g0 = w * k; - b.m(w, 0.0).arc(w, h / 2.0, 270.0, -180.0).arc((w - g0).max(0.0), h / 2.0, 90.0, 180.0).z(); - simple(b.take(), Rect::new(w * 0.1, h * 0.25, w * (1.0 - k).max(0.2) * 0.8, h * 0.75), w, h) + let ssf = ss.max(1e-9); + let g0 = ss * k; + let g1 = (ss - g0).max(1e-9); + let g2 = g0 * g0 / g1; + let g3 = ss * ss / g1; + let g5 = 2.0 * g3 - g2; + let g6 = g5 - g0; + let dy1 = (g5 / 2.0 - g0) * (h / 2.0) / ssf; + let (g0w, g6w) = (g0 * w / ssf, g6 * w / ssf); + let rx = (g6w - g0w) / 2.0; + let cx = g0w + rx; + b.m(w, 0.0).arc(w, h / 2.0, 270.0, -180.0); + if rx > 1e-9 && dy1 > 1e-9 { + // From the bottom-right corner back up to the top-right through (g0w, h/2). + let t0 = ((h / 2.0) / dy1).clamp(-1.0, 1.0).atan2((w - cx) / rx); + let t1 = (-(h / 2.0) / dy1).clamp(-1.0, 1.0).atan2((w - cx) / rx) + std::f64::consts::TAU; + let arc = Arc::new(Point::new(cx, h / 2.0), Vec2::new(rx, dy1), t0, t1 - t0, 0.0); + arc.to_cubic_beziers(0.05, |p1, p2, p| { + b.p.curve_to(p1, p2, p); + }); + } + b.z(); + let g12 = g0 * 9598.0 / 32768.0; + let g13 = ss - g12; + let dy4 = (ss * ss - g13 * g13).max(0.0).sqrt() * (h / 2.0) / ssf; + simple(b.take(), Rect::new(g12 * w / ssf, h / 2.0 - dy4, g0w, h / 2.0 + dy4), w, h) } "cloud" | "cloudCallout" => cloud(name, w, h, a(0), a(1)), "bracketPair" => { @@ -2298,6 +2326,24 @@ mod tests { assert!((bb.x0).abs() < 0.5 && (bb.x1 - k).abs() < 0.5, "{bb:?}"); } + #[test] + fn moon_disk_and_drum_match_powerpoints_outlines() { + let k = 216.0; + // Moon, default: the inner edge runs through (0.5, 0.5) and (0.625, 0.2266) of the box. + let g = build("moon", k, k, &[]).unwrap(); + for (x, y) in [(0.5, 0.5), (0.626, 0.2266), (0.0, 0.5), (1.0, 0.0)] { + assert!(on_path(&g, Point::new(k * x, k * y), 0.5), "moon ({x}, {y})"); + } + // 300 × 120 at 30%: its inner edge reaches 30% of the width in. + let g = build("moon", 300.0, 120.0, &[30000.0]).unwrap(); + assert!(on_path(&g, Point::new(90.0, 60.0), 0.5)); + // Magnetic disk ends are ellipses h/6 high, drum ends w/6 across. + let g = build("flowChartMagneticDisk", k, k, &[]).unwrap(); + assert!(on_path(&g, Point::new(k, k / 6.0), 0.01) && on_path(&g, Point::new(k / 2.0, k / 3.0), 0.5)); + let g = build("flowChartMagneticDrum", k, k, &[]).unwrap(); + assert!(on_path(&g, Point::new(k / 6.0, 0.0), 0.01) && on_path(&g, Point::new(0.0, k / 2.0), 0.01)); + } + #[test] fn the_cloud_fills_its_box_at_any_aspect_and_has_creases() { use kurbo::Shape as _;