mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 11:07:54 -06:00
Merge pull request #1091 from dotCooCoo/fix/eps-arc-angles
# Conflicts: # contributors/people.toml
This commit is contained in:
@@ -70,3 +70,7 @@ display_name = "nanox333"
|
||||
|
||||
[people.Nael-Nathanael]
|
||||
sync_github_name = true
|
||||
|
||||
[people.dotCooCoo]
|
||||
real_name = "Robert Lee"
|
||||
sync_github_name = true
|
||||
|
||||
+33
-11
@@ -343,6 +343,15 @@ fn matrix_val(m: Affine) -> V {
|
||||
array(m.as_coeffs().iter().map(|c| V::Num(*c)).collect())
|
||||
}
|
||||
|
||||
/// The angle from `a` counterclockwise to `b` (degrees, 0 to 360), worked out from what is left
|
||||
/// of each after whole turns.
|
||||
fn ccw_turn(a: f64, b: f64) -> f64 {
|
||||
(b % 360.0 - a % 360.0).rem_euclid(360.0)
|
||||
}
|
||||
|
||||
/// The most turns one `arc` or `arcn` draws.
|
||||
const MAX_ARC_TURNS: f64 = 1024.0;
|
||||
|
||||
/// The deepest procedure nesting run (deeper recursion stops the program).
|
||||
const MAX_CALL_DEPTH: usize = 256;
|
||||
|
||||
@@ -502,20 +511,33 @@ impl Interp {
|
||||
}
|
||||
|
||||
fn arc(&mut self, neg: bool) {
|
||||
let a2 = self.popn().to_radians();
|
||||
let a1 = self.popn().to_radians();
|
||||
let a2 = self.popn();
|
||||
let a1 = self.popn();
|
||||
let r = self.popn();
|
||||
let c = self.user_point();
|
||||
let mut sweep = a2 - a1;
|
||||
if neg {
|
||||
while sweep > 0.0 {
|
||||
sweep -= std::f64::consts::TAU;
|
||||
}
|
||||
// `arc` raises `a2` by multiples of 360 until it is at least `a1`, and `arcn` lowers it
|
||||
// until it is at most `a1` (PostScript Language Reference §8.2, `arc` and `arcn`). Those
|
||||
// sweeps, and the first endpoint, are worked out from what is left of each angle after
|
||||
// whole turns.
|
||||
let sweep = if !neg && a2 < a1 {
|
||||
ccw_turn(a1, a2)
|
||||
} else if neg && a2 > a1 {
|
||||
-ccw_turn(a2, a1)
|
||||
} else {
|
||||
while sweep < 0.0 {
|
||||
sweep += std::f64::consts::TAU;
|
||||
}
|
||||
}
|
||||
a2 - a1
|
||||
};
|
||||
// A sweep of more than `MAX_ARC_TURNS` turns is shortened by whole pairs of turns to
|
||||
// between `MAX_ARC_TURNS - 2` and `MAX_ARC_TURNS` turns, with a skip note. The arc still
|
||||
// ends at `a2`, and an even-odd fill or clip of it covers the same area as the full sweep.
|
||||
let max = MAX_ARC_TURNS * 360.0;
|
||||
let sweep = if sweep.abs() > max {
|
||||
self.b.skip("arc (too many turns)");
|
||||
let s = sweep.signum();
|
||||
s * (max - 720.0 + (s * (a2 % 720.0 - a1 % 720.0)).rem_euclid(720.0))
|
||||
} else {
|
||||
sweep
|
||||
};
|
||||
let (a1, sweep) = ((a1 % 360.0).to_radians(), sweep.to_radians());
|
||||
let start = Point::new(c.x + r * a1.cos(), c.y + r * a1.sin());
|
||||
if self.gs.cur.is_some() {
|
||||
self.line_to(start);
|
||||
|
||||
@@ -296,6 +296,69 @@ fn truncated_and_corrupt_eps_never_panic() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `body` after the header [`eps`] adds, on a thread of its own, and fails the test when
|
||||
/// parsing takes more than 20 s.
|
||||
fn eps_soon(body: &str) -> Doc {
|
||||
let mut b = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 100\n".to_vec();
|
||||
b.extend_from_slice(body.as_bytes());
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || tx.send(parse(&b)).ok());
|
||||
rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|_| panic!("parsing {body} did not finish")).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arcs_with_huge_angles_end_at_their_second_angle() {
|
||||
use kurbo::{Point, Rect, Shape as _};
|
||||
// `arc` raises its second angle by multiples of 360 until it is at least the first, and
|
||||
// `arcn` lowers it until it is at most the first (PostScript Language Reference §8.2),
|
||||
// however large the angles: the arc starts at the first angle's remainder after whole turns
|
||||
// and ends at the second angle, the new current point. 10^18 and 10^308 are written in
|
||||
// digits: the tokenizer reads no exponents.
|
||||
let (e18, e308) = (format!("1{}", "0".repeat(18)), format!("1{}", "0".repeat(308)));
|
||||
// `at(deg)` is the point at `deg` degrees on the circle of radius 10 around (50, 50), in
|
||||
// document pixels.
|
||||
let at = |deg: f64| Point::new(50.0 + 10.0 * deg.to_radians().cos(), 50.0 - 10.0 * deg.to_radians().sin());
|
||||
let near = |a: Rect, b: Rect| [(a.x0, b.x0), (a.y0, b.y0), (a.x1, b.x1), (a.y1, b.y1)].iter().all(|(x, y)| (x - y).abs() < 0.05);
|
||||
// Each program is listed with the remainders of its first and second angles after whole
|
||||
// turns, and whether its sweep is more than 1,024 turns (a skip note is recorded, and the arc
|
||||
// still goes around the whole circle).
|
||||
for (body, first, second, whole) in [
|
||||
(format!("50 50 10 {e18} 0 arc"), 280.0, 0.0, false),
|
||||
(format!("50 50 10 {e308} 0 arc"), 296.0, 0.0, false),
|
||||
(format!("50 50 10 0 {e18} arcn"), 0.0, 280.0, false),
|
||||
(format!("50 50 10 0 {e308} arcn"), 0.0, 296.0, false),
|
||||
(format!("50 50 10 0 {e18} arc"), 0.0, 280.0, true),
|
||||
(format!("50 50 10 0 {e308} arc"), 0.0, 296.0, true),
|
||||
(format!("50 50 10 {e18} 0 arcn"), 280.0, 0.0, true),
|
||||
(format!("50 50 10 {e308} 0 arcn"), 296.0, 0.0, true),
|
||||
] {
|
||||
let doc = eps_soon(&format!("newpath {body} currentpoint stroke 1 1 rectfill"));
|
||||
let skipped: &[&str] = if whole { &["arc (too many turns)"] } else { &[] };
|
||||
assert_eq!(doc.skipped, skipped, "{body}");
|
||||
let shapes = doc.flatten();
|
||||
assert_eq!(shapes.len(), 2, "{body}");
|
||||
let bounds = |i: usize| (shapes[i].0 * shapes[i].2.geom.to_path()).bounding_box();
|
||||
// An arc within one quarter of the circle is spanned by its endpoints.
|
||||
let (a, b) = (at(first), at(second));
|
||||
let arc = if whole { Rect::new(40.0, 40.0, 60.0, 60.0) } else { Rect::from_points(a, b) };
|
||||
assert!(near(bounds(0), arc), "{body}: {:?}", bounds(0));
|
||||
// The second shape is the square drawn at the current point.
|
||||
assert!(near(bounds(1), Rect::new(b.x, b.y - 1.0, b.x + 1.0, b.y)), "{body}: {:?}", bounds(1));
|
||||
}
|
||||
// Sweeps of up to 1,024 turns are drawn whole: five turns counterclockwise and three
|
||||
// clockwise wind around the disk twice, and a nonzero fill covers it.
|
||||
let doc = eps_soon("50 50 10 0 1800 arc 50 50 10 0 -1080 arcn fill");
|
||||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||||
let img = render(&doc);
|
||||
assert!(img.get(50, 50)[3] > 0.99, "{:?}", img.get(50, 50));
|
||||
// A longer sweep keeps the parity of its turns: 5 × 10^18 degrees is an even number of whole
|
||||
// turns and 320 degrees, so an even-odd fill covers the center of the disk.
|
||||
let doc = eps_soon("50 50 10 0 5000000000000000000 arc eofill");
|
||||
assert_eq!(doc.skipped, ["arc (too many turns)"]);
|
||||
let img = render(&doc);
|
||||
assert!(img.get(50, 50)[3] > 0.99, "{:?}", img.get(50, 50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_an_empty_path_is_a_no_op() {
|
||||
// "s", "b" and "b*" closed the path without a current point (kurbo asserts on that).
|
||||
|
||||
Reference in New Issue
Block a user