codimate 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {codimate-0.1.3 → codimate-0.1.4}/Cargo.lock +1 -1
  2. {codimate-0.1.3 → codimate-0.1.4}/PKG-INFO +2 -1
  3. {codimate-0.1.3 → codimate-0.1.4}/README.md +1 -0
  4. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-py/Cargo.toml +1 -1
  5. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-reconcile/src/lib.rs +203 -2
  6. {codimate-0.1.3 → codimate-0.1.4}/pyproject.toml +1 -1
  7. {codimate-0.1.3 → codimate-0.1.4}/python/codimate/__init__.py +1 -1
  8. {codimate-0.1.3 → codimate-0.1.4}/python/codimate/scene.py +30 -0
  9. {codimate-0.1.3 → codimate-0.1.4}/Cargo.toml +0 -0
  10. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/Cargo.toml +0 -0
  11. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/src/lib.rs +0 -0
  12. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/animation.rs +0 -0
  13. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/parallel.rs +0 -0
  14. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/playable.rs +0 -0
  15. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/sequence.rs +0 -0
  16. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/stagger.rs +0 -0
  17. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/Cargo.toml +0 -0
  18. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/builder.rs +0 -0
  19. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/easing.rs +0 -0
  20. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/glyph.rs +0 -0
  21. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/lib.rs +0 -0
  22. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/manim_palette.rs +0 -0
  23. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/path.rs +0 -0
  24. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/circle.rs +0 -0
  25. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/connection.rs +0 -0
  26. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/mod.rs +0 -0
  27. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/path.rs +0 -0
  28. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/primitive.rs +0 -0
  29. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/pulse.rs +0 -0
  30. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/rect.rs +0 -0
  31. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/text.rs +0 -0
  32. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/transform.rs +0 -0
  33. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/value.rs +0 -0
  34. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/anchors.rs +0 -0
  35. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/animated.rs +0 -0
  36. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/circle.rs +0 -0
  37. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/connection.rs +0 -0
  38. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/easing.rs +0 -0
  39. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/manim_palette.rs +0 -0
  40. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/path.rs +0 -0
  41. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/ports.rs +0 -0
  42. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/pulse.rs +0 -0
  43. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/rect.rs +0 -0
  44. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/scene.rs +0 -0
  45. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/scene_effect_helpers.rs +0 -0
  46. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/tween.rs +0 -0
  47. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/Cargo.toml +0 -0
  48. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/src/lib.rs +0 -0
  49. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/tests/export.rs +0 -0
  50. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/tests/png.rs +0 -0
  51. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/Cargo.toml +0 -0
  52. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/build.rs +0 -0
  53. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/fonts/DejaVuSansMono.ttf +0 -0
  54. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/fonts/DroidSansFallbackFull.ttf +0 -0
  55. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/fonts/NotoSansKhmer-Regular.ttf +0 -0
  56. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/src/lib.rs +0 -0
  57. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-glyph/Cargo.toml +0 -0
  58. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-glyph/src/lib.rs +0 -0
  59. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-layout/Cargo.toml +0 -0
  60. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-layout/src/lib.rs +0 -0
  61. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-layout/tests/layout.rs +0 -0
  62. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-math/Cargo.toml +0 -0
  63. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-math/src/lib.rs +0 -0
  64. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-py/src/lib.rs +0 -0
  65. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-reconcile/Cargo.toml +0 -0
  66. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/Cargo.toml +0 -0
  67. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/src/command.rs +0 -0
  68. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/src/lib.rs +0 -0
  69. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/src/raster.rs +0 -0
  70. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/tests/raster.rs +0 -0
  71. {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/tests/render.rs +0 -0
  72. {codimate-0.1.3 → codimate-0.1.4}/python/codimate/explain.py +0 -0
  73. {codimate-0.1.3 → codimate-0.1.4}/python/codimate/layout.py +0 -0
  74. {codimate-0.1.3 → codimate-0.1.4}/python/codimate/trace.py +0 -0
@@ -217,7 +217,7 @@ dependencies = [
217
217
 
218
218
  [[package]]
219
219
  name = "codimate-py"
220
- version = "0.1.3"
220
+ version = "0.1.4"
221
221
  dependencies = [
222
222
  "codimate-animation",
223
223
  "codimate-core",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codimate
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Topic :: Multimedia :: Video
@@ -182,6 +182,7 @@ scene.circle(name, r=, at=)
182
182
  scene.text(name, content, size=, at=)
183
183
  scene.polygon(name, points) # cm.ngon, cm.star
184
184
  scene.curve(name, points, w=) # smooth, through the points
185
+ scene.arc(name, r=, sweep=(0, 120)) # an arc, a dial, a pie slice
185
186
  scene.svg(name, "logo.svg", size=) # vector art, as real geometry
186
187
  scene.image(name, "photo.jpg", size=) # a picture: PNG or JPEG
187
188
  scene.line(name, start=slot_or_point, end=slot_or_point, w=)
@@ -170,6 +170,7 @@ scene.circle(name, r=, at=)
170
170
  scene.text(name, content, size=, at=)
171
171
  scene.polygon(name, points) # cm.ngon, cm.star
172
172
  scene.curve(name, points, w=) # smooth, through the points
173
+ scene.arc(name, r=, sweep=(0, 120)) # an arc, a dial, a pie slice
173
174
  scene.svg(name, "logo.svg", size=) # vector art, as real geometry
174
175
  scene.image(name, "photo.jpg", size=) # a picture: PNG or JPEG
175
176
  scene.line(name, start=slot_or_point, end=slot_or_point, w=)
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "codimate-py"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -85,8 +85,8 @@ pub struct Shape {
85
85
 
86
86
  /// Every `kind` Python may send. An unknown kind is a Python `ValueError`,
87
87
  /// never a silently missing shape.
88
- pub const KINDS: [&str; 9] = [
89
- "rect", "circle", "text", "line", "formula", "polygon", "curve", "svg", "image",
88
+ pub const KINDS: [&str; 10] = [
89
+ "rect", "circle", "text", "line", "formula", "polygon", "curve", "svg", "image", "arc",
90
90
  ];
91
91
 
92
92
  /// A rectangle with rounded corners, in local space, centred on the anchor.
@@ -235,6 +235,79 @@ fn curve_path(s: &Shape) -> Path {
235
235
  Path { segments, closed }
236
236
  }
237
237
 
238
+ /// How many cubics an arc is built from, whatever it sweeps.
239
+ ///
240
+ /// Fixed rather than "one per 90 degrees", because two arcs only tween if they
241
+ /// have the same path structure — the rule `polygon` follows for corners. A
242
+ /// fixed count is what lets a pie fill or an angle mark grow smoothly, which is
243
+ /// the whole reason this kind exists rather than a `curve` through points on a
244
+ /// circle. Eight keeps every span under 45 degrees even at a full turn, where
245
+ /// the cubic approximation is good to a few thousandths of a radius.
246
+ const ARC_SPANS: usize = 8;
247
+
248
+ /// An arc or a sector in local space.
249
+ ///
250
+ /// `w`/`h` are the bounding box, so an ellipse costs nothing extra; `x2`/`y2`
251
+ /// are the start and end angle in degrees; `r` above zero closes it back to the
252
+ /// centre, making a pie slice rather than an open curve.
253
+ ///
254
+ /// Angles run clockwise from twelve o'clock, matching `cm.ngon`, whose first
255
+ /// corner also points straight up.
256
+ /// The same arc, from plain numbers — so the tween can interpolate the
257
+ /// *angles* and build a true arc at every instant.
258
+ ///
259
+ /// Interpolating the path's control points instead looks right for an open
260
+ /// arc and is wrong for a sector: halfway between a collapsed slice and a
261
+ /// 300-degree one is not a 150-degree slice, it is a self-crossing shape that
262
+ /// fills as a sliver and a half-disc. Caught by rendering one.
263
+ fn arc_at(rx: f32, ry: f32, from_deg: f32, to_deg: f32, sector: bool) -> Path {
264
+ // Screen y grows downward, so subtracting a quarter turn puts zero at the
265
+ // top and makes a rising angle read as clockwise.
266
+ let at = |deg: f32| {
267
+ let a = deg.to_radians() - std::f32::consts::FRAC_PI_2;
268
+ (Vec2::new(rx * a.cos(), ry * a.sin()), a)
269
+ };
270
+
271
+ let (start, theta0) = at(from_deg);
272
+ let (_, theta1) = at(to_deg);
273
+ let step = (theta1 - theta0) / ARC_SPANS as f32;
274
+ // The classic cubic fit to a circular span: exact at both ends and at the
275
+ // midpoint, which is why the error stays small for spans under a quarter
276
+ // turn.
277
+ let k = 4.0 / 3.0 * (step / 4.0).tan();
278
+
279
+ let mut segments = Vec::with_capacity(ARC_SPANS + 3);
280
+ if sector {
281
+ segments.push(Segment::MoveTo(Vec2::new(0.0, 0.0)));
282
+ segments.push(Segment::Line(Vec2::new(0.0, 0.0), start));
283
+ } else {
284
+ segments.push(Segment::MoveTo(start));
285
+ }
286
+
287
+ let mut from = start;
288
+ let mut a = theta0;
289
+ for _ in 0..ARC_SPANS {
290
+ let b = a + step;
291
+ let to = Vec2::new(rx * b.cos(), ry * b.sin());
292
+ // The tangent at each end, scaled by the span — the control points sit
293
+ // along it, which is what keeps the join smooth.
294
+ let c1 = Vec2::new(from.x - k * rx * a.sin(), from.y + k * ry * a.cos());
295
+ let c2 = Vec2::new(to.x + k * rx * b.sin(), to.y - k * ry * b.cos());
296
+ segments.push(Segment::Cubic(from, c1, c2, to));
297
+ from = to;
298
+ a = b;
299
+ }
300
+
301
+ if sector {
302
+ segments.push(Segment::Line(from, Vec2::new(0.0, 0.0)));
303
+ }
304
+
305
+ Path {
306
+ segments,
307
+ closed: sector,
308
+ }
309
+ }
310
+
238
311
  /// A line in local space: from the anchor to the far end.
239
312
  fn line_path(s: &Shape) -> Path {
240
313
  Path {
@@ -272,6 +345,29 @@ impl Shape {
272
345
  }
273
346
  }
274
347
 
348
+ // The angles tween, and the arc is rebuilt at every instant — so
349
+ // a dial sweeps and a pie fills through shapes that are all
350
+ // genuinely arcs. This is the point of the kind: a `curve` through
351
+ // points on a circle changes its point count when the sweep
352
+ // changes, and then it snaps instead of sweeping.
353
+ "arc" => {
354
+ let (rx0, ry0) = (self.w / 2.0, self.h / 2.0);
355
+ let (rx1, ry1) = (other.w / 2.0, other.h / 2.0);
356
+ let (a0, b0) = (self.x2, self.y2);
357
+ let (a1, b1) = (other.x2, other.y2);
358
+ let sector = other.r > 0.0;
359
+ Geometry::path(Animated::new(move |t| {
360
+ let mix = |x: f32, y: f32| x + (y - x) * t;
361
+ arc_at(
362
+ mix(rx0, rx1),
363
+ mix(ry0, ry1),
364
+ mix(a0, a1),
365
+ mix(b0, b1),
366
+ sector,
367
+ )
368
+ }))
369
+ }
370
+
275
371
  // Same rule as a polygon: matching sample counts interpolate, and
276
372
  // otherwise the later shape stands for the whole segment.
277
373
  "curve" => {
@@ -1408,6 +1504,9 @@ fn bounds(shape: &Shape) -> (f32, f32, f32, f32) {
1408
1504
  ys.iter().cloned().fold(f32::MIN, f32::max),
1409
1505
  );
1410
1506
  }
1507
+ // The whole ellipse, not the swept part: an arc that grows would
1508
+ // otherwise resize the camera as it sweeps.
1509
+ "arc" => (shape.w, shape.h),
1411
1510
  "line" => {
1412
1511
  let (x0, x1) = (shape.x.min(shape.x2), shape.x.max(shape.x2));
1413
1512
  let (y0, y1) = (shape.y.min(shape.y2), shape.y.max(shape.y2));
@@ -1855,6 +1954,108 @@ mod tests {
1855
1954
 
1856
1955
  /// An SVG on disk, because `svg_art` reads a path rather than a string —
1857
1956
  /// the payload carries a file name, so the cache can key on it.
1957
+ fn arc_path(s: &Shape) -> Path {
1958
+ arc_at(s.w / 2.0, s.h / 2.0, s.x2, s.y2, s.r > 0.0)
1959
+ }
1960
+
1961
+ fn arc_shape(r: f32, from: f32, to: f32, sector: bool) -> Shape {
1962
+ Shape {
1963
+ item: "arc".into(),
1964
+ kind: "arc".into(),
1965
+ w: r * 2.0,
1966
+ h: r * 2.0,
1967
+ x2: from,
1968
+ y2: to,
1969
+ r: if sector { 1.0 } else { 0.0 },
1970
+ color: "white".into(),
1971
+ opacity: 1.0,
1972
+ ..Default::default()
1973
+ }
1974
+ }
1975
+
1976
+ /// Every point the arc visits is on the circle it claims to be — the
1977
+ /// eight-span cubic fit is an approximation, so this is what says how good.
1978
+ #[test]
1979
+ fn an_arc_stays_on_its_circle() {
1980
+ for (from, to) in [(0.0, 90.0), (0.0, 270.0), (0.0, 360.0), (45.0, -120.0)] {
1981
+ let path = arc_path(&arc_shape(100.0, from, to, false));
1982
+ for seg in &path.segments {
1983
+ let ends = match *seg {
1984
+ Segment::Cubic(a, _, _, b) => vec![a, b],
1985
+ Segment::MoveTo(a) => vec![a],
1986
+ _ => vec![],
1987
+ };
1988
+ for p in ends {
1989
+ let radius = (p.x * p.x + p.y * p.y).sqrt();
1990
+ assert!(
1991
+ (radius - 100.0).abs() < 0.5,
1992
+ "{from}..{to}: a point sits at {radius}, not 100"
1993
+ );
1994
+ }
1995
+ }
1996
+ }
1997
+ }
1998
+
1999
+ /// A sector closes back to its centre and an open arc does not — the
2000
+ /// difference between a pie slice and a curved line.
2001
+ #[test]
2002
+ fn a_sector_returns_to_the_centre() {
2003
+ let open = arc_path(&arc_shape(50.0, 0.0, 120.0, false));
2004
+ let pie = arc_path(&arc_shape(50.0, 0.0, 120.0, true));
2005
+
2006
+ assert!(!open.closed && pie.closed);
2007
+ // Distance from the centre, not `x`: an arc starting at zero degrees
2008
+ // begins at the top, where `x` is zero.
2009
+ assert!(
2010
+ matches!(open.segments.first(), Some(Segment::MoveTo(p)) if p.x.hypot(p.y) > 1.0),
2011
+ "an open arc starts on its circle, not at the centre"
2012
+ );
2013
+ assert!(
2014
+ matches!(pie.segments.first(), Some(Segment::MoveTo(p)) if p.x.abs() < 1e-6 && p.y.abs() < 1e-6),
2015
+ "a pie starts at its centre"
2016
+ );
2017
+ assert!(
2018
+ matches!(pie.segments.last(), Some(Segment::Line(_, p)) if p.x.abs() < 1e-6 && p.y.abs() < 1e-6),
2019
+ "and comes back to it"
2020
+ );
2021
+ }
2022
+
2023
+ /// The reason this kind exists rather than a `curve` through points on a
2024
+ /// circle, and the bug the first version had: halfway through a sweep the
2025
+ /// shape must be a real arc at the halfway angle.
2026
+ ///
2027
+ /// Interpolating the path's control points gives a self-crossing sliver
2028
+ /// instead, which looks fine on an open arc and obviously wrong on a pie.
2029
+ #[test]
2030
+ fn a_sweep_passes_through_real_arcs() {
2031
+ let closed = arc_shape(100.0, 0.0, 0.0, true);
2032
+ let open_to_300 = arc_shape(100.0, 0.0, 300.0, true);
2033
+
2034
+ let geometry = closed.geometry(&open_to_300);
2035
+ let codimate_core::ConcreteGeometry::Path { path: halfway } = geometry.resolve(0.5) else {
2036
+ panic!("an arc resolves to a path");
2037
+ };
2038
+ let expected = arc_path(&arc_shape(100.0, 0.0, 150.0, true));
2039
+
2040
+ let ends = |p: &Path| -> Vec<Vec2> {
2041
+ p.segments
2042
+ .iter()
2043
+ .filter_map(|s| match *s {
2044
+ Segment::Cubic(_, _, _, b) => Some(b),
2045
+ _ => None,
2046
+ })
2047
+ .collect()
2048
+ };
2049
+ let (got, want) = (ends(&halfway), ends(&expected));
2050
+ assert_eq!(got.len(), want.len());
2051
+ for (a, b) in got.iter().zip(&want) {
2052
+ assert!(
2053
+ (a.x - b.x).abs() < 0.01 && (a.y - b.y).abs() < 0.01,
2054
+ "halfway through a 0..300 sweep should be the 150 arc: {a:?} vs {b:?}"
2055
+ );
2056
+ }
2057
+ }
2058
+
1858
2059
  fn svg_on_disk(name: &str, body: &str) -> String {
1859
2060
  let path = std::env::temp_dir().join(format!("codimate-test-{name}.svg"));
1860
2061
  std::fs::write(&path, body).expect("temp file");
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "codimate"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Turn a running algorithm into an explainer video"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -20,7 +20,7 @@ encoding — happens in Rust (ADR 0008).
20
20
 
21
21
  ## What a moment looks like
22
22
 
23
- - `Scene` — one picture: `rect`, `circle`, `polygon`, `curve`, `arrow`,
23
+ - `Scene` — one picture: `rect`, `circle`, `arc`, `polygon`, `curve`, `arrow`,
24
24
  `text`, `line`, `formula`, `svg`, `image`
25
25
  - `ngon`, `star` — corners for a polygon, so you do not compute them
26
26
  - `Group` — several shapes that move together
@@ -290,6 +290,36 @@ class Group:
290
290
  return self._place(key, "formula", x=x, y=y, text=latex, size=size,
291
291
  layer=10, r=1.0)
292
292
 
293
+ def arc(self, key: Hashable, *, r, sweep, at=None) -> "Handle":
294
+ """A slice of a circle — an angle mark, a pie, a dial, an orbit.
295
+
296
+ scene.arc("angle", r=90, sweep=(0, 50)).fill("none", edge="cyan",
297
+ edge_w=3)
298
+ scene.arc("slice", r=120, sweep=(0, 120)).fill("orange").round(1)
299
+
300
+ `sweep` is `(start, end)` in degrees, clockwise from twelve o'clock —
301
+ the same zero `cm.ngon` uses. `r` is the radius, or `(rx, ry)` for an
302
+ ellipse.
303
+
304
+ Open by default, so it draws as a curved line. `.round(1)` closes it
305
+ back to the centre and makes a pie slice you can fill.
306
+
307
+ Two arcs always tween, however far apart their angles are, so an angle
308
+ mark grows and a pie fills smoothly. That is what this cannot be done
309
+ with `curve`: a curve through points on a circle changes its point
310
+ count when the sweep changes, and then it snaps instead of sweeping.
311
+ """
312
+ rx, ry = (r, r) if isinstance(r, (int, float)) else r
313
+ start, end = sweep
314
+ x, y = self._where(at, 0.0)
315
+ return self._place(
316
+ key, "arc", x=x, y=y,
317
+ # The bounding box, so an ellipse costs nothing extra.
318
+ w=float(rx) * 2, h=float(ry) * 2,
319
+ # `x2`/`y2` are the line's endpoints elsewhere and free here.
320
+ x2=float(start), y2=float(end),
321
+ )
322
+
293
323
  def image(self, key: Hashable, file, *, size=None, at=None) -> "Handle":
294
324
  """A picture — a photo, a screenshot, a figure — drawn into the frame.
295
325
 
File without changes