fix: pixel-identical rendering (z2d stroker port) and last P3 items
Resolves the 3 P3 items in docs/rewrite/notes/review-rv12-codex.md (final-Z15.md): - Sprite strokes: tiny-skia is replaced by a port of z2d 0.10's stroke plotter (sprite/stroke.rs) plus z2d's arc segmentation and truncating integer compositing, feeding the ported 4x4 multisample rasterizer. All 36 reference sprite sheets now match Ghostty's byte for byte (test tolerance 0), and `just visual-parity` with a production build shows 0 differing pixels (max delta 0) against the installed Ghostty 1.3.1 for the test page (block and bar cursor) and the tabs+splits scenario, terminal area and chrome alike. tiny-skia dependency removed. - zig_error maps errno values per target via libc (Linux, Darwin/BSD) and Win32 codes the way Zig std does on Windows. - theme=. / theme=.. report "it is a directory" via a port of std.fs.path.basename, matching Ghostty. Test Plan: - cargo test --workspace --no-fail-fast (3498 passed, exit 0) - cargo build --profile production && just visual-parity - just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
1 parent
c9051641a5
commit
f68b11d469
10 files changed
+1339
-191
No files matched your search
@@ -151,6 +151,38 @@ fn check_regular(file: &File, theme: &str, diags: &mut DiagnosticList) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// `std.fs.path.basename` (last `ComponentIterator` component; `.` and `..`
|
||||
/// are ordinary names). The caller only passes non-absolute paths, so the root
|
||||
/// is empty on POSIX and an optional `X:` drive designator on Windows.
|
||||
fn zig_basename(path: &str) -> &str {
|
||||
let is_sep = |b: u8| b == b'/' || (cfg!(windows) && b == b'\\');
|
||||
let bytes = path.as_bytes();
|
||||
let root_end = if cfg!(windows)
|
||||
&& bytes.len() >= 2
|
||||
&& bytes[0].is_ascii_alphabetic()
|
||||
&& bytes[1] == b':'
|
||||
{
|
||||
2
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut end = bytes.len();
|
||||
loop {
|
||||
if end == root_end {
|
||||
return "";
|
||||
}
|
||||
if !is_sep(bytes[end - 1]) {
|
||||
break;
|
||||
}
|
||||
end -= 1;
|
||||
}
|
||||
let mut start = end;
|
||||
while start != root_end && !is_sep(bytes[start - 1]) {
|
||||
start -= 1;
|
||||
}
|
||||
&path[start..end]
|
||||
}
|
||||
|
||||
/// Open the given named theme. If there are any errors then messages
|
||||
/// will be appended to the given error list and None is returned. If
|
||||
/// a Some return value is returned, there are never any errors added.
|
||||
@@ -184,11 +216,7 @@ pub fn open(
|
||||
});
|
||||
}
|
||||
|
||||
let basename = theme_path
|
||||
.file_name()
|
||||
.map(EncodedOs::from_os)
|
||||
.unwrap_or_default();
|
||||
if *theme != basename {
|
||||
if theme_text != zig_basename(theme_text) {
|
||||
diag(
|
||||
diags,
|
||||
format!(
|
||||
@@ -335,6 +363,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zig_basename_matches_std() {
|
||||
assert_eq!(zig_basename(""), "");
|
||||
assert_eq!(zig_basename("."), ".");
|
||||
assert_eq!(zig_basename(".."), "..");
|
||||
assert_eq!(zig_basename("a/"), "a");
|
||||
assert_eq!(zig_basename("a/b"), "b");
|
||||
assert_eq!(zig_basename("a//"), "a");
|
||||
assert_eq!(zig_basename("./"), ".");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_theme_names_are_directories() {
|
||||
let dir = std::env::temp_dir().join(format!("ghostty-theme-dot-{}", std::process::id()));
|
||||
std::fs::create_dir_all(dir.join("themes")).unwrap();
|
||||
for name in [".", ".."] {
|
||||
let mut d = DiagnosticList::default();
|
||||
assert!(open(&e(name), Some(&EncodedOs::from_path(&dir)), &mut d).is_none());
|
||||
assert!(
|
||||
d.items()[0].message.ends_with("it is a directory"),
|
||||
"{}",
|
||||
d.items()[0].message
|
||||
);
|
||||
}
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_found_in_resources() {
|
||||
let dir = std::env::temp_dir().join(format!("ghostty-theme-test-{}", std::process::id()));
|
||||
|
||||
@@ -6,7 +6,6 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ghostty-os.workspace = true
|
||||
tiny-skia = "0.12.0"
|
||||
png = "0.18.1"
|
||||
thiserror = "2.0.21"
|
||||
log = "0.4.34"
|
||||
|
||||
@@ -4,12 +4,17 @@
|
||||
//! Ghostty uses the `z2d` library for antialiased path drawing. This port
|
||||
//! reproduces z2d's default `multisample_4x` rasterizer (4x4 samples per
|
||||
//! pixel, scanline spans, alpha `coverage * 16 - 1`) in [`Canvas::rasterize`];
|
||||
//! stroke outlines come from `tiny-skia`'s stroker.
|
||||
//! stroke outlines come from [`super::stroke`], a port of z2d's stroke plotter.
|
||||
|
||||
#![allow(
|
||||
clippy::float_cmp,
|
||||
clippy::excessive_precision,
|
||||
clippy::neg_cmp_op_on_partial_ord
|
||||
)]
|
||||
|
||||
use std::ops::Sub;
|
||||
|
||||
use tiny_skia::{LineCap, LineJoin, PathBuilder, PathSegment, Stroke};
|
||||
|
||||
use super::stroke;
|
||||
use crate::atlas::{self, Atlas, Format, Region};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
@@ -148,64 +153,129 @@ impl Path {
|
||||
return;
|
||||
}
|
||||
self.nodes.push(PathNode::ClosePath);
|
||||
// z2d adds an explicit `move_to` to the initial point after every
|
||||
// `close_path` node.
|
||||
if let Some((x, y)) = self.start {
|
||||
self.nodes.push(PathNode::MoveTo(x, y));
|
||||
}
|
||||
self.current = self.start;
|
||||
}
|
||||
|
||||
/// Add a circular arc centered at `(x, y)` from angle `a0` to `a1`
|
||||
/// (radians, clockwise in canvas space with +y down). If there is a
|
||||
/// current point, a line is drawn to the start of the arc, otherwise
|
||||
/// the arc starts a new subpath.
|
||||
/// (radians, increasing angles, clockwise in canvas space with +y down),
|
||||
/// like z2d's `Path.arc` (`internal/arc.zig`, Cairo's arc algorithm). If
|
||||
/// there is a current point, a line is drawn to the start of the arc,
|
||||
/// otherwise the arc starts a new subpath.
|
||||
pub fn arc(&mut self, x: f64, y: f64, radius: f64, a0: f64, a1: f64) {
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
|
||||
let (sx, sy) = (x + radius * a0.cos(), y + radius * a0.sin());
|
||||
if self.current.is_some() {
|
||||
self.line_to(sx, sy);
|
||||
} else {
|
||||
self.move_to(sx, sy);
|
||||
}
|
||||
|
||||
let mut sweep = a1 - a0;
|
||||
// Split into segments of at most a quarter turn.
|
||||
let segs = ((sweep.abs() / FRAC_PI_2).ceil() as usize).max(1);
|
||||
sweep /= segs as f64;
|
||||
let k = 4.0 / 3.0 * (sweep / 4.0).tan();
|
||||
let mut a = a0;
|
||||
for _ in 0..segs {
|
||||
let (s0, c0) = a.sin_cos();
|
||||
let (s1, c1) = (a + sweep).sin_cos();
|
||||
self.curve_to(
|
||||
x + radius * (c0 - k * s0),
|
||||
y + radius * (s0 + k * c0),
|
||||
x + radius * (c1 + k * s1),
|
||||
y + radius * (s1 - k * c1),
|
||||
x + radius * c1,
|
||||
y + radius * s1,
|
||||
);
|
||||
a += sweep;
|
||||
let mut a1 = a1;
|
||||
while a1 < a0 {
|
||||
a1 += std::f64::consts::TAU;
|
||||
}
|
||||
self.arc_in_direction(x, y, radius, a0, a1, 0.1);
|
||||
}
|
||||
|
||||
fn to_skia(&self, dx: f64, dy: f64) -> Option<tiny_skia::Path> {
|
||||
let mut pb = PathBuilder::new();
|
||||
let f = |v: f64, d: f64| (v + d) as f32;
|
||||
for n in &self.nodes {
|
||||
match *n {
|
||||
PathNode::MoveTo(x, y) => pb.move_to(f(x, dx), f(y, dy)),
|
||||
PathNode::LineTo(x, y) => pb.line_to(f(x, dx), f(y, dy)),
|
||||
PathNode::CurveTo(x1, y1, x2, y2, x3, y3) => pb.cubic_to(
|
||||
f(x1, dx),
|
||||
f(y1, dy),
|
||||
f(x2, dx),
|
||||
f(y2, dy),
|
||||
f(x3, dx),
|
||||
f(y3, dy),
|
||||
),
|
||||
PathNode::ClosePath => pb.close(),
|
||||
/// z2d `arc_line_to`: a no-op when already at the destination.
|
||||
fn arc_line_to(&mut self, x: f64, y: f64) {
|
||||
if self.current == Some((x, y)) {
|
||||
return;
|
||||
}
|
||||
self.line_to(x, y);
|
||||
}
|
||||
|
||||
fn arc_in_direction(&mut self, xc: f64, yc: f64, radius: f64, amin: f64, amax: f64, tol: f64) {
|
||||
use std::f64::consts::PI;
|
||||
let (mut amin, amax) = (amin, amax);
|
||||
if !(amax * amax >= 0.0) || !(amin * amin >= 0.0) {
|
||||
return;
|
||||
}
|
||||
// (The `max_full_circles` guard of z2d is irrelevant for sprites.)
|
||||
if amax - amin > PI {
|
||||
let amid = amin + (amax - amin) / 2.0;
|
||||
self.arc_in_direction(xc, yc, radius, amin, amid, tol);
|
||||
self.arc_in_direction(xc, yc, radius, amid, amax, tol);
|
||||
} else if amax != amin {
|
||||
// Identity CTM: the transformed circle's major axis is `radius`.
|
||||
let max_angle = arc_max_angle_for_tolerance_normalized(tol / radius);
|
||||
let mut segments = ((amax - amin).abs() / max_angle).ceil() as i32;
|
||||
let step = (amax - amin) / f64::from(segments);
|
||||
segments -= 1;
|
||||
self.arc_line_to(xc + radius * amin.cos(), yc + radius * amin.sin());
|
||||
for _ in 0..segments.max(0) {
|
||||
self.arc_segment(xc, yc, radius, amin, amin + step);
|
||||
amin += step;
|
||||
}
|
||||
self.arc_segment(xc, yc, radius, amin, amax);
|
||||
} else {
|
||||
self.arc_line_to(xc + radius * amin.cos(), yc + radius * amin.sin());
|
||||
}
|
||||
pb.finish()
|
||||
}
|
||||
|
||||
fn arc_segment(&mut self, xc: f64, yc: f64, radius: f64, a: f64, b: f64) {
|
||||
let (r_sin_a, r_cos_a) = (radius * a.sin(), radius * a.cos());
|
||||
let (r_sin_b, r_cos_b) = (radius * b.sin(), radius * b.cos());
|
||||
let h = 4.0 / 3.0 * ((b - a) / 4.0).tan();
|
||||
self.curve_to(
|
||||
xc + r_cos_a - h * r_sin_a,
|
||||
yc + r_sin_a + h * r_cos_a,
|
||||
xc + r_cos_b + h * r_sin_b,
|
||||
yc + r_sin_b - h * r_cos_b,
|
||||
xc + r_cos_b,
|
||||
yc + r_sin_b,
|
||||
);
|
||||
}
|
||||
|
||||
/// The path nodes translated by `(dx, dy)` (z2d applies the canvas
|
||||
/// transformation when nodes are added to the path).
|
||||
fn to_nodes(&self, dx: f64, dy: f64) -> Vec<stroke::Node> {
|
||||
let t = |x: f64, y: f64| (x + dx, y + dy);
|
||||
self.nodes
|
||||
.iter()
|
||||
.map(|n| match *n {
|
||||
PathNode::MoveTo(x, y) => stroke::Node::MoveTo(t(x, y)),
|
||||
PathNode::LineTo(x, y) => stroke::Node::LineTo(t(x, y)),
|
||||
PathNode::CurveTo(x1, y1, x2, y2, x3, y3) => {
|
||||
stroke::Node::CurveTo(t(x1, y1), t(x2, y2), t(x3, y3))
|
||||
}
|
||||
PathNode::ClosePath => stroke::Node::ClosePath,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// z2d `arc_error_normalized`.
|
||||
fn arc_error_normalized(angle: f64) -> f64 {
|
||||
2.0 / 27.0 * (angle / 4.0).sin().powf(6.0) / (angle / 4.0).cos().powf(2.0)
|
||||
}
|
||||
|
||||
/// z2d `arc_max_angle_for_tolerance_normalized`.
|
||||
fn arc_max_angle_for_tolerance_normalized(tolerance: f64) -> f64 {
|
||||
use std::f64::consts::PI;
|
||||
const TABLE: [(f64, f64); 11] = [
|
||||
(PI / 1.0, 0.0185185185185185036127),
|
||||
(PI / 2.0, 0.000272567143730179811158),
|
||||
(PI / 3.0, 2.38647043651461047433e-05),
|
||||
(PI / 4.0, 4.2455377443222443279e-06),
|
||||
(PI / 5.0, 1.11281001494389081528e-06),
|
||||
(PI / 6.0, 3.72662000942734705475e-07),
|
||||
(PI / 7.0, 1.47783685574284411325e-07),
|
||||
(PI / 8.0, 6.63240432022601149057e-08),
|
||||
(PI / 9.0, 3.2715520137536980553e-08),
|
||||
(PI / 10.0, 1.73863223499021216974e-08),
|
||||
(PI / 11.0, 9.81410988043554039085e-09),
|
||||
];
|
||||
for (angle, err) in TABLE {
|
||||
if err < tolerance {
|
||||
return angle;
|
||||
}
|
||||
}
|
||||
let mut angle = 0.0;
|
||||
for i in TABLE.len()..1000 {
|
||||
angle = PI / i as f64;
|
||||
if arc_error_normalized(angle) <= tolerance {
|
||||
break;
|
||||
}
|
||||
}
|
||||
angle
|
||||
}
|
||||
|
||||
/// Line cap modes (subset of z2d's).
|
||||
@@ -469,30 +539,35 @@ impl Canvas {
|
||||
/// with z2d's default multisample anti-aliasing.
|
||||
fn rasterize(&mut self, path: &Path, stroke: Option<StrokeOptions>) -> bool {
|
||||
self.cov.fill(0);
|
||||
let Some(p) = path.to_skia(f64::from(self.padding_x), f64::from(self.padding_y)) else {
|
||||
let nodes = path.to_nodes(f64::from(self.padding_x), f64::from(self.padding_y));
|
||||
if nodes.is_empty() {
|
||||
return false;
|
||||
};
|
||||
let p = match stroke {
|
||||
None => p,
|
||||
}
|
||||
let edges = match stroke {
|
||||
None => flatten_to_edges(&nodes),
|
||||
Some(opts) => {
|
||||
let s = Stroke {
|
||||
width: opts.line_width as f32,
|
||||
miter_limit: 10.0,
|
||||
line_cap: match opts.line_cap_mode {
|
||||
CapMode::Butt => LineCap::Butt,
|
||||
CapMode::Round => LineCap::Round,
|
||||
CapMode::Square => LineCap::Square,
|
||||
// z2d `painter.stroke`: thin lines revert to butt caps, and
|
||||
// there is a minimum line width.
|
||||
let params = stroke::StrokeParams {
|
||||
thickness: opts.line_width.max(0.00390625),
|
||||
cap_mode: if opts.line_width >= 2.0 {
|
||||
opts.line_cap_mode
|
||||
} else {
|
||||
CapMode::Butt
|
||||
},
|
||||
line_join: LineJoin::Miter,
|
||||
dash: None,
|
||||
miter_limit: 10.0,
|
||||
tolerance: 0.1,
|
||||
};
|
||||
match p.stroke(&s, 1.0) {
|
||||
Some(o) => o,
|
||||
None => return false,
|
||||
let mut edges = Vec::new();
|
||||
for (p0, p1) in stroke::plot(&nodes, params) {
|
||||
add_edge(&mut edges, p0, p1);
|
||||
}
|
||||
edges
|
||||
}
|
||||
};
|
||||
let edges = flatten_to_edges(&p);
|
||||
if edges.is_empty() {
|
||||
return false;
|
||||
}
|
||||
multisample_fill(
|
||||
&edges,
|
||||
self.sfc_width as usize,
|
||||
@@ -511,9 +586,11 @@ impl Canvas {
|
||||
if cov == 0 {
|
||||
continue;
|
||||
}
|
||||
let src = (cov * a + 127) / 255;
|
||||
// z2d integer compositing truncates: `dst_in` of the pattern with
|
||||
// the coverage, then `src_over`: `s + d - mul(s, d)`.
|
||||
let src = cov * a / 255;
|
||||
let d = u32::from(*dst);
|
||||
*dst = (src + (d * (255 - src) + 127) / 255).min(255) as u8;
|
||||
*dst = (src + d - src * d / 255).min(255) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +635,7 @@ impl Canvas {
|
||||
if self.rasterize(path, Some(mut_opts)) {
|
||||
let a = u32::from(color.0);
|
||||
for (d, px) in stroke.iter_mut().zip(self.cov.iter()) {
|
||||
*d = ((u32::from(*px) * a + 127) / 255) as u8;
|
||||
*d = (u32::from(*px) * a / 255) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,7 +643,7 @@ impl Canvas {
|
||||
for ((dst, f), s) in self.sfc.iter_mut().zip(&fill).zip(&stroke) {
|
||||
let m = (255.0 * (f64::from(*s) / 255.0) * (f64::from(*f) / 255.0)).round() as u32;
|
||||
let d = u32::from(*dst);
|
||||
*dst = (m + (d * (255 - m) + 127) / 255).min(255) as u8;
|
||||
*dst = (m + d - m * d / 255).min(255) as u8;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -728,9 +805,9 @@ fn flatten_cubic(
|
||||
out.push(d);
|
||||
}
|
||||
|
||||
/// Flatten a tiny-skia path into scaled polygon edges. Every subpath is
|
||||
/// Flatten path nodes into scaled polygon edges. Every subpath is
|
||||
/// implicitly closed (fill semantics).
|
||||
fn flatten_to_edges(p: &tiny_skia::Path) -> Vec<Edge> {
|
||||
fn flatten_to_edges(nodes: &[stroke::Node]) -> Vec<Edge> {
|
||||
let mut edges = Vec::new();
|
||||
let mut pts: Vec<(f64, f64)> = Vec::new();
|
||||
let flush = |pts: &mut Vec<(f64, f64)>, edges: &mut Vec<Edge>| {
|
||||
@@ -742,38 +819,23 @@ fn flatten_to_edges(p: &tiny_skia::Path) -> Vec<Edge> {
|
||||
}
|
||||
pts.clear();
|
||||
};
|
||||
let f = |p: tiny_skia::Point| (f64::from(p.x), f64::from(p.y));
|
||||
let mut cur = (0.0, 0.0);
|
||||
for seg in p.segments() {
|
||||
match seg {
|
||||
PathSegment::MoveTo(q) => {
|
||||
for node in nodes {
|
||||
match *node {
|
||||
stroke::Node::MoveTo(q) => {
|
||||
flush(&mut pts, &mut edges);
|
||||
cur = f(q);
|
||||
cur = q;
|
||||
pts.push(cur);
|
||||
}
|
||||
PathSegment::LineTo(q) => {
|
||||
cur = f(q);
|
||||
stroke::Node::LineTo(q) => {
|
||||
cur = q;
|
||||
pts.push(cur);
|
||||
}
|
||||
PathSegment::QuadTo(c, q) => {
|
||||
let (c, q) = (f(c), f(q));
|
||||
// Elevate to a cubic.
|
||||
let c1 = (
|
||||
cur.0 + 2.0 / 3.0 * (c.0 - cur.0),
|
||||
cur.1 + 2.0 / 3.0 * (c.1 - cur.1),
|
||||
);
|
||||
let c2 = (q.0 + 2.0 / 3.0 * (c.0 - q.0), q.1 + 2.0 / 3.0 * (c.1 - q.1));
|
||||
stroke::Node::CurveTo(c1, c2, q) => {
|
||||
flatten_cubic(cur, c1, c2, q, &mut pts);
|
||||
cur = q;
|
||||
}
|
||||
PathSegment::CubicTo(c1, c2, q) => {
|
||||
let q = f(q);
|
||||
flatten_cubic(cur, f(c1), f(c2), q, &mut pts);
|
||||
cur = q;
|
||||
}
|
||||
PathSegment::Close => {
|
||||
flush(&mut pts, &mut edges);
|
||||
}
|
||||
stroke::Node::ClosePath => flush(&mut pts, &mut edges),
|
||||
}
|
||||
}
|
||||
flush(&mut pts, &mut edges);
|
||||
|
||||
@@ -254,8 +254,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Per-pixel difference above which a pixel counts as a "big" difference.
|
||||
/// z2d and tiny-skia antialias differently, so small differences are expected
|
||||
/// along diagonals and curves.
|
||||
/// Kept for the diagnostics only: the sheets are expected to match exactly.
|
||||
const BIG: u8 = 96;
|
||||
|
||||
fn diff_sheet(test: &[u8], reference: &[u8]) -> Diff {
|
||||
@@ -321,13 +320,11 @@ mod tests {
|
||||
d.ink, d.differing, d.big, d.max
|
||||
);
|
||||
|
||||
// Tolerances: the multisample rasterizer matches z2d's coverage;
|
||||
// remaining differences come from stroke geometry (tiny-skia's
|
||||
// stroker) and are small. Practically no pixel may be off by a
|
||||
// lot, relative to the amount of ink on the sheet.
|
||||
// The multisample rasterizer, the stroker and the compositing are
|
||||
// ports of z2d's, so every sheet must match the reference exactly.
|
||||
let big_ratio = d.big as f64 / d.ink.max(1) as f64;
|
||||
let ok = big_ratio <= 0.001;
|
||||
if !ok || d.differing > 0 {
|
||||
let ok = d.differing == 0;
|
||||
if !ok {
|
||||
// Write a visual diff (red = reference, green = ours).
|
||||
let mut rgb = Vec::with_capacity(atlas.len() * 3);
|
||||
for (t, r) in atlas.iter().zip(&reference) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
pub mod canvas;
|
||||
pub mod draw;
|
||||
pub mod face;
|
||||
pub mod stroke;
|
||||
|
||||
pub use canvas::{Box, Canvas, Color, Point};
|
||||
pub use face::Face;
|
||||
|
||||
@@ -0,0 +1,874 @@
|
||||
//! Port of z2d 0.10's stroke geometry (`internal/tess/stroke_plotter.zig`,
|
||||
//! `Face.zig`, `Pen.zig`, `Slope.zig`, `Spline.zig`, `point_buffer.zig`,
|
||||
//! `Polygon.zig` contours) for the identity transformation and no dashes,
|
||||
//! which is all the sprite canvas uses.
|
||||
//!
|
||||
//! The result is a list of polygon edges (unscaled; the multisample
|
||||
//! rasterizer scales them) to be filled with the non-zero rule.
|
||||
|
||||
#![allow(clippy::float_cmp)] // exact comparisons, like z2d
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub type P = (f64, f64);
|
||||
|
||||
pub use super::canvas::CapMode;
|
||||
|
||||
/// A path node, as in z2d `path_nodes.zig`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Node {
|
||||
MoveTo(P),
|
||||
LineTo(P),
|
||||
CurveTo(P, P, P),
|
||||
ClosePath,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum JoinMode {
|
||||
Miter,
|
||||
Round,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Slope {
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
}
|
||||
|
||||
fn sign(x: f64) -> f64 {
|
||||
if x > 0.0 {
|
||||
1.0
|
||||
} else if x < 0.0 {
|
||||
-1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Slope {
|
||||
fn new(a: P, b: P) -> Slope {
|
||||
Slope {
|
||||
dx: b.0 - a.0,
|
||||
dy: b.1 - a.1,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize(&mut self) -> f64 {
|
||||
let (rdx, rdy, mag);
|
||||
if self.dx == 0.0 {
|
||||
rdx = 0.0;
|
||||
if self.dy > 0.0 {
|
||||
mag = self.dy;
|
||||
rdy = 1.0;
|
||||
} else {
|
||||
mag = -self.dy;
|
||||
rdy = -1.0;
|
||||
}
|
||||
} else if self.dy == 0.0 {
|
||||
rdy = 0.0;
|
||||
if self.dx > 0.0 {
|
||||
mag = self.dx;
|
||||
rdx = 1.0;
|
||||
} else {
|
||||
mag = -self.dx;
|
||||
rdx = -1.0;
|
||||
}
|
||||
} else {
|
||||
mag = self.dx.hypot(self.dy);
|
||||
rdx = self.dx / mag;
|
||||
rdy = self.dy / mag;
|
||||
}
|
||||
self.dx = rdx;
|
||||
self.dy = rdy;
|
||||
mag
|
||||
}
|
||||
|
||||
fn compare(a: Slope, b: Slope) -> i32 {
|
||||
let bdy = if (b.dy - a.dy).abs() > f64::EPSILON {
|
||||
b.dy
|
||||
} else {
|
||||
a.dy
|
||||
};
|
||||
let bdx = if (b.dx - a.dx).abs() > f64::EPSILON {
|
||||
b.dx
|
||||
} else {
|
||||
a.dx
|
||||
};
|
||||
let cmp = sign(a.dy * bdx - bdy * a.dx);
|
||||
if cmp != 0.0 {
|
||||
return cmp as i32;
|
||||
}
|
||||
if a.dx == 0.0 && a.dy == 0.0 && bdx == 0.0 && bdy == 0.0 {
|
||||
return 0;
|
||||
}
|
||||
if a.dx == 0.0 && a.dy == 0.0 {
|
||||
return 1;
|
||||
}
|
||||
if bdx == 0.0 && bdy == 0.0 {
|
||||
return -1;
|
||||
}
|
||||
if sign(a.dx) != sign(bdx) || sign(a.dy) != sign(bdy) {
|
||||
return if a.dx > 0.0 || (a.dx == 0.0 && a.dy > 0.0) {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn compare_for_miter_limit(in_slope: Slope, out_slope: Slope, miter_limit: f64) -> bool {
|
||||
let mut i = in_slope;
|
||||
i.normalize();
|
||||
let mut o = out_slope;
|
||||
o.normalize();
|
||||
let in_dot_out = i.dx * o.dx + i.dy * o.dy;
|
||||
2.0 <= miter_limit * miter_limit * (1.0 + in_dot_out)
|
||||
}
|
||||
}
|
||||
|
||||
/// z2d `Face` for the identity CTM.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Face {
|
||||
p1: P,
|
||||
dev_slope: Slope,
|
||||
half_width: f64,
|
||||
p0_cw: P,
|
||||
p0_ccw: P,
|
||||
p1_cw: P,
|
||||
p1_ccw: P,
|
||||
}
|
||||
|
||||
impl Face {
|
||||
fn new(p0: P, p1: P, thickness: f64) -> Face {
|
||||
let mut dev_slope = Slope::new(p0, p1);
|
||||
dev_slope.normalize();
|
||||
let half_width = thickness / 2.0;
|
||||
let ox = -dev_slope.dy * half_width;
|
||||
let oy = dev_slope.dx * half_width;
|
||||
Face {
|
||||
p1,
|
||||
dev_slope,
|
||||
half_width,
|
||||
p0_cw: (p0.0 + ox, p0.1 + oy),
|
||||
p0_ccw: (p0.0 + -ox, p0.1 + -oy),
|
||||
p1_cw: (p1.0 + ox, p1.1 + oy),
|
||||
p1_ccw: (p1.0 + -ox, p1.1 + -oy),
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect(&self, out: &Face, clockwise: bool) -> P {
|
||||
let in_point = if clockwise { self.p1_ccw } else { self.p1_cw };
|
||||
let out_point = if clockwise { out.p0_ccw } else { out.p0_cw };
|
||||
let mut in_slope = self.dev_slope;
|
||||
in_slope.normalize();
|
||||
let mut out_slope = out.dev_slope;
|
||||
out_slope.normalize();
|
||||
|
||||
let result_y = ((out_point.0 - in_point.0) * in_slope.dy * out_slope.dy
|
||||
- out_point.1 * out_slope.dx * in_slope.dy
|
||||
+ in_point.1 * in_slope.dx * out_slope.dy)
|
||||
/ (in_slope.dx * out_slope.dy - out_slope.dx * in_slope.dy);
|
||||
let result_x = if in_slope.dy.abs() >= out_slope.dy.abs() {
|
||||
(result_y - in_point.1) * in_slope.dx / in_slope.dy + in_point.0
|
||||
} else {
|
||||
(result_y - out_point.1) * out_slope.dx / out_slope.dy + out_point.0
|
||||
};
|
||||
(result_x, result_y)
|
||||
}
|
||||
|
||||
fn cap(&self, out: &mut dyn FnMut(P), mode: CapMode, clockwise: bool, pen: Option<&Pen>) {
|
||||
match mode {
|
||||
CapMode::Butt => {
|
||||
if clockwise {
|
||||
out(self.p1_ccw);
|
||||
out(self.p1_cw);
|
||||
} else {
|
||||
out(self.p1_cw);
|
||||
out(self.p1_ccw);
|
||||
}
|
||||
}
|
||||
CapMode::Square => {
|
||||
let ox = self.dev_slope.dx * self.half_width;
|
||||
let oy = self.dev_slope.dy * self.half_width;
|
||||
if clockwise {
|
||||
out(self.p1_ccw);
|
||||
out((self.p1_ccw.0 + ox, self.p1_ccw.1 + oy));
|
||||
out((self.p1_cw.0 + ox, self.p1_cw.1 + oy));
|
||||
out(self.p1_cw);
|
||||
} else {
|
||||
out(self.p1_cw);
|
||||
out((self.p1_cw.0 + ox, self.p1_cw.1 + oy));
|
||||
out((self.p1_ccw.0 + ox, self.p1_ccw.1 + oy));
|
||||
out(self.p1_ccw);
|
||||
}
|
||||
}
|
||||
CapMode::Round => {
|
||||
let pen = pen.expect("round caps need a pen");
|
||||
let mut vit = pen.vertex_iterator_for(
|
||||
self.dev_slope,
|
||||
Slope {
|
||||
dx: -self.dev_slope.dx,
|
||||
dy: -self.dev_slope.dy,
|
||||
},
|
||||
clockwise,
|
||||
);
|
||||
out(if clockwise { self.p1_ccw } else { self.p1_cw });
|
||||
while let Some(v) = vit.next() {
|
||||
out((self.p1.0 + v.point.0, self.p1.1 + v.point.1));
|
||||
}
|
||||
out(if clockwise { self.p1_cw } else { self.p1_ccw });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PenVertex {
|
||||
point: P,
|
||||
slope_cw: Slope,
|
||||
slope_ccw: Slope,
|
||||
}
|
||||
|
||||
struct Pen {
|
||||
vertices: Vec<PenVertex>,
|
||||
}
|
||||
|
||||
struct VertexIterator<'a> {
|
||||
pen: &'a Pen,
|
||||
end: usize,
|
||||
idx: usize,
|
||||
clockwise: bool,
|
||||
}
|
||||
|
||||
impl<'a> VertexIterator<'a> {
|
||||
fn next(&mut self) -> Option<&'a PenVertex> {
|
||||
if self.idx == self.end {
|
||||
return None;
|
||||
}
|
||||
let result = &self.pen.vertices[self.idx];
|
||||
if self.clockwise {
|
||||
self.idx += 1;
|
||||
if self.idx == self.pen.vertices.len() {
|
||||
self.idx = 0;
|
||||
}
|
||||
} else {
|
||||
if self.idx == 0 {
|
||||
self.idx = self.pen.vertices.len();
|
||||
}
|
||||
self.idx -= 1;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pen {
|
||||
fn new(thickness: f64, tolerance: f64) -> Pen {
|
||||
let radius = thickness / 2.0;
|
||||
// Identity CTM: the transformed major axis is the radius itself.
|
||||
let major_axis = radius;
|
||||
let num: i32 = if tolerance >= major_axis * 4.0 {
|
||||
1
|
||||
} else if tolerance >= major_axis {
|
||||
4
|
||||
} else {
|
||||
let delta = (1.0 - tolerance / major_axis).acos();
|
||||
if delta == 0.0 {
|
||||
4
|
||||
} else {
|
||||
let n = (2.0 * std::f64::consts::PI / delta).ceil() as i32;
|
||||
if n < 4 {
|
||||
4
|
||||
} else if n % 2 != 0 {
|
||||
n + 1
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
};
|
||||
let num = num.max(0) as usize;
|
||||
let mut pts = Vec::with_capacity(num);
|
||||
for i in 0..num {
|
||||
let theta = 2.0 * std::f64::consts::PI * i as f64 / num as f64;
|
||||
pts.push((radius * theta.cos(), radius * theta.sin()));
|
||||
}
|
||||
let mut vertices = Vec::with_capacity(num);
|
||||
for i in 0..num {
|
||||
let next = if i + 1 >= num { 0 } else { i + 1 };
|
||||
let prev = if i == 0 { num - 1 } else { i - 1 };
|
||||
vertices.push(PenVertex {
|
||||
point: pts[i],
|
||||
slope_cw: Slope::new(pts[prev], pts[i]),
|
||||
slope_ccw: Slope::new(pts[i], pts[next]),
|
||||
});
|
||||
}
|
||||
Pen { vertices }
|
||||
}
|
||||
|
||||
fn vertex_iterator_for(&self, from: Slope, to: Slope, clockwise: bool) -> VertexIterator<'_> {
|
||||
let n = self.vertices.len() as i32;
|
||||
let v = |i: i32| &self.vertices[i as usize];
|
||||
let (start, end);
|
||||
if clockwise {
|
||||
let (mut low, mut high) = (0i32, n);
|
||||
let mut i = (low + high) >> 1;
|
||||
while high - low > 1 {
|
||||
if Slope::compare(v(i).slope_cw, from) < 0 {
|
||||
low = i;
|
||||
} else {
|
||||
high = i;
|
||||
}
|
||||
i = (low + high) >> 1;
|
||||
}
|
||||
if Slope::compare(v(i).slope_cw, from) < 0 {
|
||||
i += 1;
|
||||
if i == n {
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
start = i;
|
||||
if Slope::compare(to, v(i).slope_ccw) >= 0 {
|
||||
low = i;
|
||||
high = i + n;
|
||||
i = (low + high) >> 1;
|
||||
while high - low > 1 {
|
||||
let j = if i >= n { i - n } else { i };
|
||||
if Slope::compare(v(j).slope_cw, to) > 0 {
|
||||
high = i;
|
||||
} else {
|
||||
low = i;
|
||||
}
|
||||
i = (low + high) >> 1;
|
||||
}
|
||||
if i >= n {
|
||||
i -= n;
|
||||
}
|
||||
}
|
||||
end = i;
|
||||
} else {
|
||||
let (mut low, mut high) = (0i32, n);
|
||||
let mut i = (low + high) >> 1;
|
||||
while high - low > 1 {
|
||||
if Slope::compare(from, v(i).slope_ccw) < 0 {
|
||||
low = i;
|
||||
} else {
|
||||
high = i;
|
||||
}
|
||||
i = (low + high) >> 1;
|
||||
}
|
||||
if Slope::compare(from, v(i).slope_ccw) < 0 {
|
||||
i += 1;
|
||||
if i == n {
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
start = i;
|
||||
if Slope::compare(v(i).slope_cw, to) <= 0 {
|
||||
low = i;
|
||||
high = i + n;
|
||||
i = (low + high) >> 1;
|
||||
while high - low > 1 {
|
||||
let j = if i >= n { i - n } else { i };
|
||||
if Slope::compare(to, v(j).slope_ccw) > 0 {
|
||||
high = i;
|
||||
} else {
|
||||
low = i;
|
||||
}
|
||||
i = (low + high) >> 1;
|
||||
}
|
||||
if i >= n {
|
||||
i -= n;
|
||||
}
|
||||
}
|
||||
end = i;
|
||||
}
|
||||
VertexIterator {
|
||||
pen: self,
|
||||
end: end.max(0) as usize,
|
||||
idx: start.max(0) as usize,
|
||||
clockwise,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `PointBuffer(2, 5)`.
|
||||
#[derive(Default)]
|
||||
struct PointBuffer {
|
||||
items: [P; 5],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl PointBuffer {
|
||||
fn add(&mut self, p: P) {
|
||||
if self.len < 5 {
|
||||
self.items[self.len] = p;
|
||||
self.len += 1;
|
||||
} else {
|
||||
for idx in 2..4 {
|
||||
self.items[idx] = self.items[idx + 1];
|
||||
}
|
||||
self.items[4] = p;
|
||||
}
|
||||
}
|
||||
fn head(&self, n: usize) -> P {
|
||||
self.items[n]
|
||||
}
|
||||
fn tail(&self, n: usize) -> P {
|
||||
self.items[self.len - n]
|
||||
}
|
||||
}
|
||||
|
||||
type Contour = VecDeque<P>;
|
||||
|
||||
/// Stroke options after z2d's `painter.stroke` clamping.
|
||||
pub struct StrokeParams {
|
||||
pub thickness: f64,
|
||||
pub cap_mode: CapMode,
|
||||
pub miter_limit: f64,
|
||||
pub tolerance: f64,
|
||||
}
|
||||
|
||||
struct Plotter {
|
||||
p: StrokeParams,
|
||||
join_mode: JoinMode,
|
||||
pen: Option<Pen>,
|
||||
points: PointBuffer,
|
||||
clockwise: Option<bool>,
|
||||
outer: Contour,
|
||||
inner: Contour,
|
||||
edges: Vec<(P, P)>,
|
||||
}
|
||||
|
||||
/// Plot the stroke polygon of `nodes`; returns the polygon as a list of
|
||||
/// directed edges (point pairs).
|
||||
pub fn plot(nodes: &[Node], p: StrokeParams) -> Vec<(P, P)> {
|
||||
let pen = if p.cap_mode == CapMode::Round {
|
||||
Some(Pen::new(p.thickness, p.tolerance))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut pl = Plotter {
|
||||
p,
|
||||
join_mode: JoinMode::Miter,
|
||||
pen,
|
||||
points: PointBuffer::default(),
|
||||
clockwise: None,
|
||||
outer: Contour::new(),
|
||||
inner: Contour::new(),
|
||||
edges: Vec::new(),
|
||||
};
|
||||
for n in nodes {
|
||||
match *n {
|
||||
Node::MoveTo(pt) => {
|
||||
if pl.points.len > 0 {
|
||||
pl.finish();
|
||||
}
|
||||
pl.points.len = 0;
|
||||
pl.points.add(pt);
|
||||
}
|
||||
Node::LineTo(pt) => pl.run_line_to(JoinMode::Miter, pt),
|
||||
Node::CurveTo(b, c, d) => pl.run_curve_to(b, c, d),
|
||||
Node::ClosePath => pl.run_close_path(),
|
||||
}
|
||||
}
|
||||
pl.finish();
|
||||
pl.edges
|
||||
}
|
||||
|
||||
fn add_contour_edges(edges: &mut Vec<(P, P)>, c: &Contour) {
|
||||
let mut last: Option<P> = None;
|
||||
for &pt in c {
|
||||
if let Some(l) = last {
|
||||
edges.push((l, pt));
|
||||
}
|
||||
last = Some(pt);
|
||||
}
|
||||
if let (Some(first), Some(l)) = (c.front(), last) {
|
||||
edges.push((l, *first));
|
||||
}
|
||||
}
|
||||
|
||||
impl Plotter {
|
||||
fn run_line_to(&mut self, mode: JoinMode, pt: P) {
|
||||
if self.points.len == 0 {
|
||||
return;
|
||||
}
|
||||
let cur = self.points.tail(1);
|
||||
if pt == cur {
|
||||
return;
|
||||
}
|
||||
self.points.add(pt);
|
||||
if self.points.len > 2 {
|
||||
self.join(
|
||||
mode,
|
||||
self.points.tail(3),
|
||||
self.points.tail(2),
|
||||
self.points.tail(1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_curve_to(&mut self, b: P, c: P, d: P) {
|
||||
if self.points.len == 0 {
|
||||
return;
|
||||
}
|
||||
let a = self.points.tail(1);
|
||||
if self.pen.is_none() {
|
||||
self.pen = Some(Pen::new(self.p.thickness, self.p.tolerance));
|
||||
}
|
||||
// z2d `Spline.decompose` with tolerance^2.
|
||||
if a == b && c == d {
|
||||
self.run_line_to(JoinMode::Round, d);
|
||||
return;
|
||||
}
|
||||
let tol = self.p.tolerance * self.p.tolerance;
|
||||
let mut k = [a, b, c, d];
|
||||
self.decompose_into(&mut k, a, tol);
|
||||
self.run_line_to(JoinMode::Round, d);
|
||||
}
|
||||
|
||||
fn decompose_into(&mut self, k: &mut [P; 4], start: P, tol: f64) {
|
||||
if knots_error_sq(k) < tol {
|
||||
if k[0] != start {
|
||||
self.run_line_to(JoinMode::Round, k[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut s2 = de_casteljau(k);
|
||||
self.decompose_into(k, start, tol);
|
||||
self.decompose_into(&mut s2, start, tol);
|
||||
}
|
||||
|
||||
fn run_close_path(&mut self) {
|
||||
match self.points.len {
|
||||
0 => {}
|
||||
1 => self.plot_dotted(self.points.head(0)),
|
||||
2 => self.plot_single(self.points.head(0), self.points.head(1)),
|
||||
_ => self.plot_closed_joined(
|
||||
self.points.head(0),
|
||||
self.points.head(1),
|
||||
self.points.tail(2),
|
||||
self.points.tail(1),
|
||||
),
|
||||
}
|
||||
self.points.len = 0;
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
match self.points.len {
|
||||
0 | 1 => {}
|
||||
2 => self.plot_single(self.points.head(0), self.points.head(1)),
|
||||
_ => self.plot_open_joined(
|
||||
self.points.head(0),
|
||||
self.points.head(1),
|
||||
self.points.tail(2),
|
||||
self.points.tail(1),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn plot_dotted(&mut self, point: P) {
|
||||
if self.p.cap_mode == CapMode::Round {
|
||||
let pen = self.pen.as_ref().expect("round caps need a pen");
|
||||
for v in &pen.vertices {
|
||||
self.outer
|
||||
.push_back((point.0 + v.point.0, point.1 + v.point.1));
|
||||
}
|
||||
add_contour_edges(&mut self.edges, &self.outer);
|
||||
self.outer.clear();
|
||||
self.clockwise = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn plot_single(&mut self, start: P, end: P) {
|
||||
let f = Face::new(start, end, self.p.thickness);
|
||||
let rev = Face::new(end, start, self.p.thickness);
|
||||
let mut sink = |pt: P| self.outer.push_back(pt);
|
||||
rev.cap(&mut sink, self.p.cap_mode, true, self.pen.as_ref());
|
||||
f.cap(&mut sink, self.p.cap_mode, true, self.pen.as_ref());
|
||||
add_contour_edges(&mut self.edges, &self.outer);
|
||||
self.outer.clear();
|
||||
self.clockwise = None;
|
||||
}
|
||||
|
||||
fn plot_open_joined(&mut self, start0: P, end0: P, start1: P, end1: P) {
|
||||
let t = self.p.thickness;
|
||||
let start_rev = Face::new(end0, start0, t);
|
||||
let end_face = Face::new(start1, end1, t);
|
||||
let clockwise = self.clockwise.unwrap_or(true);
|
||||
|
||||
// Start cap: inserted before the first corner of the outer contour.
|
||||
let mut cursor = 0usize;
|
||||
let outer = &mut self.outer;
|
||||
start_rev.cap(
|
||||
&mut |pt| {
|
||||
outer.insert(cursor, pt);
|
||||
cursor += 1;
|
||||
},
|
||||
self.p.cap_mode,
|
||||
clockwise,
|
||||
self.pen.as_ref(),
|
||||
);
|
||||
// End cap: appended.
|
||||
end_face.cap(
|
||||
&mut |pt| outer.push_back(pt),
|
||||
self.p.cap_mode,
|
||||
clockwise,
|
||||
self.pen.as_ref(),
|
||||
);
|
||||
// Concatenate the inner contour onto the outer.
|
||||
let inner = std::mem::take(&mut self.inner);
|
||||
self.outer.extend(inner);
|
||||
add_contour_edges(&mut self.edges, &self.outer);
|
||||
self.outer.clear();
|
||||
self.clockwise = None;
|
||||
}
|
||||
|
||||
fn plot_closed_joined(&mut self, initial0: P, initial1: P, p1: P, p2: P) {
|
||||
let mode = self.join_mode;
|
||||
if p2 == initial0 {
|
||||
self.join(mode, p1, initial0, initial1);
|
||||
} else {
|
||||
self.join(mode, p1, p2, initial0);
|
||||
self.join(mode, p2, initial0, initial1);
|
||||
}
|
||||
add_contour_edges(&mut self.edges, &self.outer);
|
||||
add_contour_edges(&mut self.edges, &self.inner);
|
||||
self.outer.clear();
|
||||
self.inner.clear();
|
||||
self.clockwise = None;
|
||||
}
|
||||
|
||||
fn join(&mut self, join_mode: JoinMode, p0: P, p1: P, p2: P) {
|
||||
if p0 == p1 || p1 == p2 {
|
||||
if self.clockwise.is_none() {
|
||||
self.clockwise = Some(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let t = self.p.thickness;
|
||||
let inf = Face::new(p0, p1, t);
|
||||
let out = Face::new(p1, p2, t);
|
||||
let join_clockwise = Slope::compare(inf.dev_slope, out.dev_slope) < 0;
|
||||
let poly_clockwise = self.clockwise.unwrap_or(join_clockwise);
|
||||
let switched = join_clockwise != poly_clockwise;
|
||||
|
||||
// `outer_joiner` plots to the outer contour (append) unless the
|
||||
// direction switched, in which case it plots to the inner (prepend).
|
||||
let plot_outer = |pl: &mut Plotter, pt: P, to_outer: bool| {
|
||||
if to_outer {
|
||||
pl.outer.push_back(pt);
|
||||
} else {
|
||||
pl.inner.push_front(pt);
|
||||
}
|
||||
};
|
||||
|
||||
if Slope::compare(inf.dev_slope, out.dev_slope) == 0 {
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
inf.p1_ccw
|
||||
} else {
|
||||
inf.p1_cw
|
||||
},
|
||||
!switched,
|
||||
);
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
inf.p1_cw
|
||||
} else {
|
||||
inf.p1_ccw
|
||||
},
|
||||
switched,
|
||||
);
|
||||
if self.clockwise.is_none() {
|
||||
self.clockwise = Some(poly_clockwise);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match join_mode {
|
||||
JoinMode::Miter => {
|
||||
if Slope::compare_for_miter_limit(inf.dev_slope, out.dev_slope, self.p.miter_limit)
|
||||
{
|
||||
let pt = inf.intersect(&out, join_clockwise);
|
||||
plot_outer(self, pt, !switched);
|
||||
} else {
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
inf.p1_ccw
|
||||
} else {
|
||||
inf.p1_cw
|
||||
},
|
||||
!switched,
|
||||
);
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
out.p0_ccw
|
||||
} else {
|
||||
out.p0_cw
|
||||
},
|
||||
!switched,
|
||||
);
|
||||
}
|
||||
}
|
||||
JoinMode::Round => {
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
inf.p1_ccw
|
||||
} else {
|
||||
inf.p1_cw
|
||||
},
|
||||
!switched,
|
||||
);
|
||||
let pen = self.pen.take().expect("round joins need a pen");
|
||||
let mut vit = pen.vertex_iterator_for(inf.dev_slope, out.dev_slope, join_clockwise);
|
||||
while let Some(v) = vit.next() {
|
||||
plot_outer(self, (p1.0 + v.point.0, p1.1 + v.point.1), !switched);
|
||||
}
|
||||
self.pen = Some(pen);
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
out.p0_ccw
|
||||
} else {
|
||||
out.p0_cw
|
||||
},
|
||||
!switched,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
inf.p1_cw
|
||||
} else {
|
||||
inf.p1_ccw
|
||||
},
|
||||
switched,
|
||||
);
|
||||
plot_outer(self, p1, switched);
|
||||
plot_outer(
|
||||
self,
|
||||
if join_clockwise {
|
||||
out.p0_cw
|
||||
} else {
|
||||
out.p0_ccw
|
||||
},
|
||||
switched,
|
||||
);
|
||||
if self.clockwise.is_none() {
|
||||
self.clockwise = Some(poly_clockwise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp_half(a: P, b: P) -> P {
|
||||
(a.0 + (b.0 - a.0) / 2.0, a.1 + (b.1 - a.1) / 2.0)
|
||||
}
|
||||
|
||||
/// z2d `Spline.Knots.errorSq`.
|
||||
#[allow(clippy::float_cmp)] // exact comparison, like z2d
|
||||
pub fn knots_error_sq(k: &[P; 4]) -> f64 {
|
||||
let [a, b, c, d] = *k;
|
||||
let (mut bx, mut by) = (b.0 - a.0, b.1 - a.1);
|
||||
let (mut cx, mut cy) = (c.0 - a.0, c.1 - a.1);
|
||||
if a.0 != d.0 || a.1 != d.1 {
|
||||
let (dx, dy) = (d.0 - a.0, d.1 - a.1);
|
||||
let dd = dx * dx + dy * dy;
|
||||
let bd = bx * dx + by * dy;
|
||||
if bd >= dd {
|
||||
bx -= dx;
|
||||
by -= dy;
|
||||
} else {
|
||||
bx -= bd / dd * dx;
|
||||
by -= bd / dd * dy;
|
||||
}
|
||||
let cd = cx * dx + cy * dy;
|
||||
if cd >= dd {
|
||||
cx -= dx;
|
||||
cy -= dy;
|
||||
} else {
|
||||
cx -= cd / dd * dx;
|
||||
cy -= cd / dd * dy;
|
||||
}
|
||||
}
|
||||
let (be, ce) = (bx * bx + by * by, cx * cx + cy * cy);
|
||||
if be > ce { be } else { ce }
|
||||
}
|
||||
|
||||
/// z2d `Spline.Knots.deCasteljau`: `k` becomes the first half, the second
|
||||
/// half is returned.
|
||||
pub fn de_casteljau(k: &mut [P; 4]) -> [P; 4] {
|
||||
let [a, b, c, d] = *k;
|
||||
let ab = lerp_half(a, b);
|
||||
let bc = lerp_half(b, c);
|
||||
let cd = lerp_half(c, d);
|
||||
let abbc = lerp_half(ab, bc);
|
||||
let bccd = lerp_half(bc, cd);
|
||||
let fin = lerp_half(abbc, bccd);
|
||||
*k = [a, ab, abbc, fin];
|
||||
[fin, bccd, cd, d]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn params() -> StrokeParams {
|
||||
StrokeParams {
|
||||
thickness: 2.0,
|
||||
cap_mode: CapMode::Butt,
|
||||
miter_limit: 10.0,
|
||||
tolerance: 0.01,
|
||||
}
|
||||
}
|
||||
|
||||
// Ports of the z2d `stroke_plotter` tests.
|
||||
#[test]
|
||||
fn degenerate_move_line_then_good_line() {
|
||||
let nodes = [
|
||||
Node::MoveTo((10.0, 10.0)),
|
||||
Node::LineTo((10.0, 10.0)),
|
||||
Node::LineTo((20.0, 20.0)),
|
||||
];
|
||||
assert_eq!(plot(&nodes, params()).len(), 4);
|
||||
let nodes = [
|
||||
Node::MoveTo((10.0, 10.0)),
|
||||
Node::LineTo((20.0, 20.0)),
|
||||
Node::LineTo((20.0, 20.0)),
|
||||
];
|
||||
assert_eq!(plot(&nodes, params()).len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slope_difference_below_epsilon_is_finite() {
|
||||
let nodes = [
|
||||
Node::MoveTo((2.8641015625e2, 1.43154296875e1)),
|
||||
Node::LineTo((2.822548828125e2, 1.6e1)),
|
||||
Node::LineTo((2.80990234375e2, 1.65126953125e1)),
|
||||
];
|
||||
let edges = plot(&nodes, params());
|
||||
assert_eq!(edges.len(), 6);
|
||||
for (a, b) in edges {
|
||||
assert!(a.0.is_finite() && a.1.is_finite() && b.0.is_finite() && b.1.is_finite());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_cap_dot_and_curve_do_not_panic() {
|
||||
let mut p = params();
|
||||
p.cap_mode = CapMode::Round;
|
||||
let nodes = [Node::MoveTo((5.0, 5.0)), Node::ClosePath];
|
||||
assert!(plot(&nodes, p).len() > 4);
|
||||
let nodes = [
|
||||
Node::MoveTo((0.0, 0.0)),
|
||||
Node::CurveTo((5.0, 0.0), (10.0, 5.0), (10.0, 10.0)),
|
||||
];
|
||||
assert!(plot(&nodes, params()).len() > 6);
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,133 @@
|
||||
//! `error.FileNotFound`), following Zig std's posix errno mapping for the
|
||||
//! calls Ghostty makes (`openat`, `fstat`, `realpath`, `execve`, ...).
|
||||
|
||||
/// Zig error name (without the `error.` prefix) for a Linux errno.
|
||||
/// Zig error name (without the `error.` prefix) for a platform error code:
|
||||
/// a POSIX `errno` on unix targets (values come from `libc`, so Linux and
|
||||
/// Darwin/BSD numbering are both right) or a Win32 `GetLastError` code on
|
||||
/// Windows (the `CreateProcessW` / file-open mapping of Zig std).
|
||||
#[must_use]
|
||||
#[cfg(unix)]
|
||||
pub fn errno_name(errno: i32) -> &'static str {
|
||||
use libc::{
|
||||
EACCES,
|
||||
EAGAIN,
|
||||
EBUSY,
|
||||
EEXIST,
|
||||
EFBIG,
|
||||
EILSEQ,
|
||||
EISDIR,
|
||||
ELOOP,
|
||||
EMFILE,
|
||||
ENAMETOOLONG,
|
||||
ENFILE,
|
||||
ENODEV,
|
||||
ENOENT,
|
||||
ENOMEM,
|
||||
ENOSPC,
|
||||
ENOTDIR,
|
||||
ENXIO,
|
||||
EOPNOTSUPP,
|
||||
EOVERFLOW,
|
||||
EPERM,
|
||||
EROFS,
|
||||
ESRCH,
|
||||
ETXTBSY,
|
||||
};
|
||||
match errno {
|
||||
1 => "PermissionDenied", // EPERM
|
||||
2 => "FileNotFound", // ENOENT
|
||||
3 => "ProcessNotFound", // ESRCH
|
||||
6 | 19 => "NoDevice", // ENXIO, ENODEV
|
||||
11 => "WouldBlock", // EAGAIN
|
||||
12 => "SystemResources", // ENOMEM
|
||||
13 => "AccessDenied", // EACCES
|
||||
16 => "DeviceBusy", // EBUSY
|
||||
17 => "PathAlreadyExists", // EEXIST
|
||||
20 => "NotDir", // ENOTDIR
|
||||
21 => "IsDir", // EISDIR
|
||||
23 => "SystemFdQuotaExceeded", // ENFILE
|
||||
24 => "ProcessFdQuotaExceeded", // EMFILE
|
||||
26 => "FileBusy", // ETXTBSY
|
||||
27 | 75 => "FileTooBig", // EFBIG, EOVERFLOW
|
||||
28 => "NoSpaceLeft", // ENOSPC
|
||||
30 => "ReadOnlyFileSystem", // EROFS
|
||||
36 => "NameTooLong", // ENAMETOOLONG
|
||||
40 => "SymLinkLoop", // ELOOP
|
||||
84 => "InvalidWtf8", // EILSEQ
|
||||
95 => "FileLocksNotSupported", // EOPNOTSUPP
|
||||
EPERM => "PermissionDenied",
|
||||
ENOENT => "FileNotFound",
|
||||
ESRCH => "ProcessNotFound",
|
||||
ENXIO | ENODEV => "NoDevice",
|
||||
EAGAIN => "WouldBlock",
|
||||
ENOMEM => "SystemResources",
|
||||
EACCES => "AccessDenied",
|
||||
EBUSY => "DeviceBusy",
|
||||
EEXIST => "PathAlreadyExists",
|
||||
ENOTDIR => "NotDir",
|
||||
EISDIR => "IsDir",
|
||||
ENFILE => "SystemFdQuotaExceeded",
|
||||
EMFILE => "ProcessFdQuotaExceeded",
|
||||
ETXTBSY => "FileBusy",
|
||||
EFBIG | EOVERFLOW => "FileTooBig",
|
||||
ENOSPC => "NoSpaceLeft",
|
||||
EROFS => "ReadOnlyFileSystem",
|
||||
ENAMETOOLONG => "NameTooLong",
|
||||
ELOOP => "SymLinkLoop",
|
||||
EILSEQ => "InvalidWtf8",
|
||||
EOPNOTSUPP => "FileLocksNotSupported",
|
||||
_ => "Unexpected",
|
||||
}
|
||||
}
|
||||
|
||||
/// See the unix variant; this is the Windows (Win32 error code) table.
|
||||
#[must_use]
|
||||
#[cfg(windows)]
|
||||
#[allow(non_upper_case_globals)] // windows-sys spells two constants `..._64k`
|
||||
pub fn errno_name(code: i32) -> &'static str {
|
||||
use windows_sys::Win32::Foundation::{
|
||||
ERROR_ACCESS_DENIED,
|
||||
ERROR_AUTODATASEG_EXCEEDS_64k,
|
||||
ERROR_BAD_EXE_FORMAT,
|
||||
ERROR_BAD_FORMAT,
|
||||
ERROR_COMMITMENT_LIMIT,
|
||||
ERROR_DIRECTORY,
|
||||
ERROR_DISK_FULL,
|
||||
ERROR_DYNLINK_FROM_INVALID_RING,
|
||||
ERROR_EXE_MACHINE_TYPE_MISMATCH,
|
||||
ERROR_EXE_MARKED_INVALID,
|
||||
ERROR_FILE_NOT_FOUND,
|
||||
ERROR_FILENAME_EXCED_RANGE,
|
||||
ERROR_INFLOOP_IN_RELOC_CHAIN,
|
||||
ERROR_INVALID_EXE_SIGNATURE,
|
||||
ERROR_INVALID_MINALLOCSIZE,
|
||||
ERROR_INVALID_MODULETYPE,
|
||||
ERROR_INVALID_NAME,
|
||||
ERROR_INVALID_SEGDPL,
|
||||
ERROR_INVALID_STACKSEG,
|
||||
ERROR_INVALID_STARTING_CODESEG,
|
||||
ERROR_IOPL_NOT_ENABLED,
|
||||
ERROR_ITERATED_DATA_EXCEEDS_64k,
|
||||
ERROR_PATH_NOT_FOUND,
|
||||
ERROR_RELOC_CHAIN_XEEDS_SEGLIM,
|
||||
ERROR_RING2SEG_MUST_BE_MOVABLE,
|
||||
ERROR_SHARING_VIOLATION,
|
||||
};
|
||||
match code as u32 {
|
||||
ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND | ERROR_DIRECTORY => "FileNotFound",
|
||||
ERROR_ACCESS_DENIED => "AccessDenied",
|
||||
ERROR_INVALID_NAME => "InvalidName",
|
||||
ERROR_FILENAME_EXCED_RANGE => "NameTooLong",
|
||||
ERROR_SHARING_VIOLATION => "FileBusy",
|
||||
ERROR_COMMITMENT_LIMIT => "SystemResources",
|
||||
ERROR_DISK_FULL => "NoSpaceLeft",
|
||||
ERROR_BAD_FORMAT
|
||||
| ERROR_INVALID_STARTING_CODESEG
|
||||
| ERROR_INVALID_STACKSEG
|
||||
| ERROR_INVALID_MODULETYPE
|
||||
| ERROR_INVALID_EXE_SIGNATURE
|
||||
| ERROR_EXE_MARKED_INVALID
|
||||
| ERROR_BAD_EXE_FORMAT
|
||||
| ERROR_ITERATED_DATA_EXCEEDS_64k
|
||||
| ERROR_INVALID_MINALLOCSIZE
|
||||
| ERROR_DYNLINK_FROM_INVALID_RING
|
||||
| ERROR_IOPL_NOT_ENABLED
|
||||
| ERROR_INVALID_SEGDPL
|
||||
| ERROR_AUTODATASEG_EXCEEDS_64k
|
||||
| ERROR_RING2SEG_MUST_BE_MOVABLE
|
||||
| ERROR_RELOC_CHAIN_XEEDS_SEGLIM
|
||||
| ERROR_INFLOOP_IN_RELOC_CHAIN
|
||||
| ERROR_EXE_MACHINE_TYPE_MISMATCH => "InvalidExe",
|
||||
_ => "Unexpected",
|
||||
}
|
||||
}
|
||||
|
||||
/// Targets without a native error-code table (wasm).
|
||||
#[must_use]
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub fn errno_name(_code: i32) -> &'static str {
|
||||
"Unexpected"
|
||||
}
|
||||
|
||||
/// `error.Name` text for an I/O error.
|
||||
#[must_use]
|
||||
pub fn io_error_name(err: &std::io::Error) -> String {
|
||||
@@ -45,19 +143,91 @@ pub fn io_error_name(err: &std::io::Error) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn n(e: i32) -> String {
|
||||
io_error_name(&std::io::Error::from_raw_os_error(e))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn names() {
|
||||
let n = |e| io_error_name(&std::io::Error::from_raw_os_error(e));
|
||||
assert_eq!(n(2), "error.FileNotFound");
|
||||
assert_eq!(n(20), "error.NotDir");
|
||||
assert_eq!(n(13), "error.AccessDenied");
|
||||
assert_eq!(n(21), "error.IsDir");
|
||||
assert_eq!(n(36), "error.NameTooLong");
|
||||
assert_eq!(n(40), "error.SymLinkLoop");
|
||||
fn libc_constants_map() {
|
||||
assert_eq!(n(libc::ENOENT), "error.FileNotFound");
|
||||
assert_eq!(n(libc::ENOTDIR), "error.NotDir");
|
||||
assert_eq!(n(libc::EACCES), "error.AccessDenied");
|
||||
assert_eq!(n(libc::EISDIR), "error.IsDir");
|
||||
assert_eq!(n(libc::ENAMETOOLONG), "error.NameTooLong");
|
||||
assert_eq!(n(libc::ELOOP), "error.SymLinkLoop");
|
||||
assert_eq!(n(libc::EAGAIN), "error.WouldBlock");
|
||||
assert_eq!(n(libc::EOPNOTSUPP), "error.FileLocksNotSupported");
|
||||
assert_eq!(n(9999), "error.Unexpected");
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn linux_table() {
|
||||
for (code, name) in [
|
||||
(1, "PermissionDenied"),
|
||||
(2, "FileNotFound"),
|
||||
(11, "WouldBlock"),
|
||||
(13, "AccessDenied"),
|
||||
(20, "NotDir"),
|
||||
(21, "IsDir"),
|
||||
(36, "NameTooLong"),
|
||||
(40, "SymLinkLoop"),
|
||||
(75, "FileTooBig"),
|
||||
(84, "InvalidWtf8"),
|
||||
(95, "FileLocksNotSupported"),
|
||||
] {
|
||||
assert_eq!(errno_name(code), name);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[test]
|
||||
fn darwin_table() {
|
||||
for (code, name) in [
|
||||
(1, "PermissionDenied"),
|
||||
(2, "FileNotFound"),
|
||||
(13, "AccessDenied"),
|
||||
(20, "NotDir"),
|
||||
(21, "IsDir"),
|
||||
(35, "WouldBlock"),
|
||||
(62, "SymLinkLoop"),
|
||||
(63, "NameTooLong"),
|
||||
(84, "FileTooBig"),
|
||||
(92, "InvalidWtf8"),
|
||||
(102, "FileLocksNotSupported"),
|
||||
] {
|
||||
assert_eq!(errno_name(code), name);
|
||||
}
|
||||
// Linux-only numbers must not leak through.
|
||||
assert_eq!(errno_name(36), "Unexpected");
|
||||
assert_eq!(errno_name(40), "Unexpected");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_table() {
|
||||
for (code, name) in [
|
||||
(2, "FileNotFound"),
|
||||
(3, "FileNotFound"),
|
||||
(5, "AccessDenied"),
|
||||
(32, "FileBusy"),
|
||||
(112, "NoSpaceLeft"),
|
||||
(123, "InvalidName"),
|
||||
(193, "InvalidExe"),
|
||||
(206, "NameTooLong"),
|
||||
(267, "FileNotFound"),
|
||||
(1455, "SystemResources"),
|
||||
(87, "Unexpected"),
|
||||
] {
|
||||
assert_eq!(errno_name(code), name);
|
||||
}
|
||||
let e = std::io::Error::from_raw_os_error(5);
|
||||
assert_eq!(io_error_name(&e), "error.AccessDenied");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user