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.
- {codimate-0.1.3 → codimate-0.1.4}/Cargo.lock +1 -1
- {codimate-0.1.3 → codimate-0.1.4}/PKG-INFO +2 -1
- {codimate-0.1.3 → codimate-0.1.4}/README.md +1 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-py/Cargo.toml +1 -1
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-reconcile/src/lib.rs +203 -2
- {codimate-0.1.3 → codimate-0.1.4}/pyproject.toml +1 -1
- {codimate-0.1.3 → codimate-0.1.4}/python/codimate/__init__.py +1 -1
- {codimate-0.1.3 → codimate-0.1.4}/python/codimate/scene.py +30 -0
- {codimate-0.1.3 → codimate-0.1.4}/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/animation.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/parallel.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/playable.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/sequence.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-animation/tests/stagger.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/builder.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/easing.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/glyph.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/manim_palette.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/path.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/circle.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/connection.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/mod.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/path.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/primitive.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/pulse.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/rect.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/text.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/scene/transform.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/src/value.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/anchors.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/animated.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/circle.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/connection.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/easing.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/manim_palette.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/path.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/ports.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/pulse.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/rect.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/scene.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/scene_effect_helpers.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-core/tests/tween.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/tests/export.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-export/tests/png.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/build.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/fonts/DejaVuSansMono.ttf +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/fonts/DroidSansFallbackFull.ttf +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/fonts/NotoSansKhmer-Regular.ttf +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-fonts/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-glyph/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-glyph/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-layout/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-layout/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-layout/tests/layout.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-math/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-math/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-py/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-reconcile/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/Cargo.toml +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/src/command.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/src/lib.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/src/raster.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/tests/raster.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/crates/codimate-render/tests/render.rs +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/python/codimate/explain.py +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/python/codimate/layout.py +0 -0
- {codimate-0.1.3 → codimate-0.1.4}/python/codimate/trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codimate
|
|
3
|
-
Version: 0.1.
|
|
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=)
|
|
@@ -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;
|
|
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");
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|