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
7dd54833b9
commit
6befc09cc8
10 files changed
+1333
-185
No files matched your search
Generated
-39
@@ -88,12 +88,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayref"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
@@ -1549,7 +1543,6 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"png",
|
"png",
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
"tiny-skia",
|
|
||||||
"yeslogic-fontconfig-sys",
|
"yeslogic-fontconfig-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3861,12 +3854,6 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strict-num"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -4148,32 +4135,6 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tiny-skia"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"arrayvec",
|
|
||||||
"bytemuck",
|
|
||||||
"cfg-if",
|
|
||||||
"log",
|
|
||||||
"png",
|
|
||||||
"tiny-skia-path",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tiny-skia-path"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"bytemuck",
|
|
||||||
"strict-num",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.8+spec-1.1.0"
|
version = "1.1.8+spec-1.1.0"
|
||||||
|
|||||||
@@ -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
|
/// 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
|
/// 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.
|
/// a Some return value is returned, there are never any errors added.
|
||||||
@@ -184,11 +216,7 @@ pub fn open(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let basename = theme_path
|
if theme_text != zig_basename(theme_text) {
|
||||||
.file_name()
|
|
||||||
.map(EncodedOs::from_os)
|
|
||||||
.unwrap_or_default();
|
|
||||||
if *theme != basename {
|
|
||||||
diag(
|
diag(
|
||||||
diags,
|
diags,
|
||||||
format!(
|
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]
|
#[test]
|
||||||
fn theme_found_in_resources() {
|
fn theme_found_in_resources() {
|
||||||
let dir = std::env::temp_dir().join(format!("ghostty-theme-test-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("ghostty-theme-test-{}", std::process::id()));
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ license.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ghostty-os.workspace = true
|
ghostty-os.workspace = true
|
||||||
tiny-skia = "0.12.0"
|
|
||||||
png = "0.18.1"
|
png = "0.18.1"
|
||||||
thiserror = "2.0.21"
|
thiserror = "2.0.21"
|
||||||
log = "0.4.34"
|
log = "0.4.34"
|
||||||
|
|||||||
@@ -4,12 +4,17 @@
|
|||||||
//! Ghostty uses the `z2d` library for antialiased path drawing. This port
|
//! Ghostty uses the `z2d` library for antialiased path drawing. This port
|
||||||
//! reproduces z2d's default `multisample_4x` rasterizer (4x4 samples per
|
//! reproduces z2d's default `multisample_4x` rasterizer (4x4 samples per
|
||||||
//! pixel, scanline spans, alpha `coverage * 16 - 1`) in [`Canvas::rasterize`];
|
//! 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 std::ops::Sub;
|
||||||
|
|
||||||
use tiny_skia::{LineCap, LineJoin, PathBuilder, PathSegment, Stroke};
|
use super::stroke;
|
||||||
|
|
||||||
use crate::atlas::{self, Atlas, Format, Region};
|
use crate::atlas::{self, Atlas, Format, Region};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
@@ -148,65 +153,130 @@ impl Path {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.nodes.push(PathNode::ClosePath);
|
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;
|
self.current = self.start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a circular arc centered at `(x, y)` from angle `a0` to `a1`
|
/// 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
|
/// (radians, increasing angles, clockwise in canvas space with +y down),
|
||||||
/// current point, a line is drawn to the start of the arc, otherwise
|
/// like z2d's `Path.arc` (`internal/arc.zig`, Cairo's arc algorithm). If
|
||||||
/// the arc starts a new subpath.
|
/// 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) {
|
pub fn arc(&mut self, x: f64, y: f64, radius: f64, a0: f64, a1: f64) {
|
||||||
use std::f64::consts::FRAC_PI_2;
|
let mut a1 = a1;
|
||||||
|
while a1 < a0 {
|
||||||
|
a1 += std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
self.arc_in_direction(x, y, radius, a0, a1, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
let (sx, sy) = (x + radius * a0.cos(), y + radius * a0.sin());
|
/// z2d `arc_line_to`: a no-op when already at the destination.
|
||||||
if self.current.is_some() {
|
fn arc_line_to(&mut self, x: f64, y: f64) {
|
||||||
self.line_to(sx, sy);
|
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 {
|
} else {
|
||||||
self.move_to(sx, sy);
|
self.arc_line_to(xc + radius * amin.cos(), yc + radius * amin.sin());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut sweep = a1 - a0;
|
fn arc_segment(&mut self, xc: f64, yc: f64, radius: f64, a: f64, b: f64) {
|
||||||
// Split into segments of at most a quarter turn.
|
let (r_sin_a, r_cos_a) = (radius * a.sin(), radius * a.cos());
|
||||||
let segs = ((sweep.abs() / FRAC_PI_2).ceil() as usize).max(1);
|
let (r_sin_b, r_cos_b) = (radius * b.sin(), radius * b.cos());
|
||||||
sweep /= segs as f64;
|
let h = 4.0 / 3.0 * ((b - a) / 4.0).tan();
|
||||||
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(
|
self.curve_to(
|
||||||
x + radius * (c0 - k * s0),
|
xc + r_cos_a - h * r_sin_a,
|
||||||
y + radius * (s0 + k * c0),
|
yc + r_sin_a + h * r_cos_a,
|
||||||
x + radius * (c1 + k * s1),
|
xc + r_cos_b + h * r_sin_b,
|
||||||
y + radius * (s1 - k * c1),
|
yc + r_sin_b - h * r_cos_b,
|
||||||
x + radius * c1,
|
xc + r_cos_b,
|
||||||
y + radius * s1,
|
yc + r_sin_b,
|
||||||
);
|
);
|
||||||
a += sweep;
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_skia(&self, dx: f64, dy: f64) -> Option<tiny_skia::Path> {
|
/// z2d `arc_error_normalized`.
|
||||||
let mut pb = PathBuilder::new();
|
fn arc_error_normalized(angle: f64) -> f64 {
|
||||||
let f = |v: f64, d: f64| (v + d) as f32;
|
2.0 / 27.0 * (angle / 4.0).sin().powf(6.0) / (angle / 4.0).cos().powf(2.0)
|
||||||
for n in &self.nodes {
|
}
|
||||||
match *n {
|
|
||||||
PathNode::MoveTo(x, y) => pb.move_to(f(x, dx), f(y, dy)),
|
/// z2d `arc_max_angle_for_tolerance_normalized`.
|
||||||
PathNode::LineTo(x, y) => pb.line_to(f(x, dx), f(y, dy)),
|
fn arc_max_angle_for_tolerance_normalized(tolerance: f64) -> f64 {
|
||||||
PathNode::CurveTo(x1, y1, x2, y2, x3, y3) => pb.cubic_to(
|
use std::f64::consts::PI;
|
||||||
f(x1, dx),
|
const TABLE: [(f64, f64); 11] = [
|
||||||
f(y1, dy),
|
(PI / 1.0, 0.0185185185185185036127),
|
||||||
f(x2, dx),
|
(PI / 2.0, 0.000272567143730179811158),
|
||||||
f(y2, dy),
|
(PI / 3.0, 2.38647043651461047433e-05),
|
||||||
f(x3, dx),
|
(PI / 4.0, 4.2455377443222443279e-06),
|
||||||
f(y3, dy),
|
(PI / 5.0, 1.11281001494389081528e-06),
|
||||||
),
|
(PI / 6.0, 3.72662000942734705475e-07),
|
||||||
PathNode::ClosePath => pb.close(),
|
(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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pb.finish()
|
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).
|
/// Line cap modes (subset of z2d's).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
@@ -469,30 +539,35 @@ impl Canvas {
|
|||||||
/// with z2d's default multisample anti-aliasing.
|
/// with z2d's default multisample anti-aliasing.
|
||||||
fn rasterize(&mut self, path: &Path, stroke: Option<StrokeOptions>) -> bool {
|
fn rasterize(&mut self, path: &Path, stroke: Option<StrokeOptions>) -> bool {
|
||||||
self.cov.fill(0);
|
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;
|
return false;
|
||||||
};
|
}
|
||||||
let p = match stroke {
|
let edges = match stroke {
|
||||||
None => p,
|
None => flatten_to_edges(&nodes),
|
||||||
Some(opts) => {
|
Some(opts) => {
|
||||||
let s = Stroke {
|
// z2d `painter.stroke`: thin lines revert to butt caps, and
|
||||||
width: opts.line_width as f32,
|
// there is a minimum line width.
|
||||||
miter_limit: 10.0,
|
let params = stroke::StrokeParams {
|
||||||
line_cap: match opts.line_cap_mode {
|
thickness: opts.line_width.max(0.00390625),
|
||||||
CapMode::Butt => LineCap::Butt,
|
cap_mode: if opts.line_width >= 2.0 {
|
||||||
CapMode::Round => LineCap::Round,
|
opts.line_cap_mode
|
||||||
CapMode::Square => LineCap::Square,
|
} else {
|
||||||
|
CapMode::Butt
|
||||||
},
|
},
|
||||||
line_join: LineJoin::Miter,
|
miter_limit: 10.0,
|
||||||
dash: None,
|
tolerance: 0.1,
|
||||||
};
|
};
|
||||||
match p.stroke(&s, 1.0) {
|
let mut edges = Vec::new();
|
||||||
Some(o) => o,
|
for (p0, p1) in stroke::plot(&nodes, params) {
|
||||||
None => return false,
|
add_edge(&mut edges, p0, p1);
|
||||||
}
|
}
|
||||||
|
edges
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let edges = flatten_to_edges(&p);
|
if edges.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
multisample_fill(
|
multisample_fill(
|
||||||
&edges,
|
&edges,
|
||||||
self.sfc_width as usize,
|
self.sfc_width as usize,
|
||||||
@@ -511,9 +586,11 @@ impl Canvas {
|
|||||||
if cov == 0 {
|
if cov == 0 {
|
||||||
continue;
|
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);
|
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)) {
|
if self.rasterize(path, Some(mut_opts)) {
|
||||||
let a = u32::from(color.0);
|
let a = u32::from(color.0);
|
||||||
for (d, px) in stroke.iter_mut().zip(self.cov.iter()) {
|
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) {
|
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 m = (255.0 * (f64::from(*s) / 255.0) * (f64::from(*f) / 255.0)).round() as u32;
|
||||||
let d = u32::from(*dst);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -728,9 +805,9 @@ fn flatten_cubic(
|
|||||||
out.push(d);
|
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).
|
/// 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 edges = Vec::new();
|
||||||
let mut pts: Vec<(f64, f64)> = Vec::new();
|
let mut pts: Vec<(f64, f64)> = Vec::new();
|
||||||
let flush = |pts: &mut Vec<(f64, f64)>, edges: &mut Vec<Edge>| {
|
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();
|
pts.clear();
|
||||||
};
|
};
|
||||||
let f = |p: tiny_skia::Point| (f64::from(p.x), f64::from(p.y));
|
|
||||||
let mut cur = (0.0, 0.0);
|
let mut cur = (0.0, 0.0);
|
||||||
for seg in p.segments() {
|
for node in nodes {
|
||||||
match seg {
|
match *node {
|
||||||
PathSegment::MoveTo(q) => {
|
stroke::Node::MoveTo(q) => {
|
||||||
flush(&mut pts, &mut edges);
|
flush(&mut pts, &mut edges);
|
||||||
cur = f(q);
|
cur = q;
|
||||||
pts.push(cur);
|
pts.push(cur);
|
||||||
}
|
}
|
||||||
PathSegment::LineTo(q) => {
|
stroke::Node::LineTo(q) => {
|
||||||
cur = f(q);
|
cur = q;
|
||||||
pts.push(cur);
|
pts.push(cur);
|
||||||
}
|
}
|
||||||
PathSegment::QuadTo(c, q) => {
|
stroke::Node::CurveTo(c1, c2, 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));
|
|
||||||
flatten_cubic(cur, c1, c2, q, &mut pts);
|
flatten_cubic(cur, c1, c2, q, &mut pts);
|
||||||
cur = q;
|
cur = q;
|
||||||
}
|
}
|
||||||
PathSegment::CubicTo(c1, c2, q) => {
|
stroke::Node::ClosePath => flush(&mut pts, &mut edges),
|
||||||
let q = f(q);
|
|
||||||
flatten_cubic(cur, f(c1), f(c2), q, &mut pts);
|
|
||||||
cur = q;
|
|
||||||
}
|
|
||||||
PathSegment::Close => {
|
|
||||||
flush(&mut pts, &mut edges);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.
|
/// Per-pixel difference above which a pixel counts as a "big" difference.
|
||||||
/// z2d and tiny-skia antialias differently, so small differences are expected
|
/// Kept for the diagnostics only: the sheets are expected to match exactly.
|
||||||
/// along diagonals and curves.
|
|
||||||
const BIG: u8 = 96;
|
const BIG: u8 = 96;
|
||||||
|
|
||||||
fn diff_sheet(test: &[u8], reference: &[u8]) -> Diff {
|
fn diff_sheet(test: &[u8], reference: &[u8]) -> Diff {
|
||||||
@@ -321,13 +320,11 @@ mod tests {
|
|||||||
d.ink, d.differing, d.big, d.max
|
d.ink, d.differing, d.big, d.max
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tolerances: the multisample rasterizer matches z2d's coverage;
|
// The multisample rasterizer, the stroker and the compositing are
|
||||||
// remaining differences come from stroke geometry (tiny-skia's
|
// ports of z2d's, so every sheet must match the reference exactly.
|
||||||
// stroker) and are small. Practically no pixel may be off by a
|
|
||||||
// lot, relative to the amount of ink on the sheet.
|
|
||||||
let big_ratio = d.big as f64 / d.ink.max(1) as f64;
|
let big_ratio = d.big as f64 / d.ink.max(1) as f64;
|
||||||
let ok = big_ratio <= 0.001;
|
let ok = d.differing == 0;
|
||||||
if !ok || d.differing > 0 {
|
if !ok {
|
||||||
// Write a visual diff (red = reference, green = ours).
|
// Write a visual diff (red = reference, green = ours).
|
||||||
let mut rgb = Vec::with_capacity(atlas.len() * 3);
|
let mut rgb = Vec::with_capacity(atlas.len() * 3);
|
||||||
for (t, r) in atlas.iter().zip(&reference) {
|
for (t, r) in atlas.iter().zip(&reference) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
pub mod canvas;
|
pub mod canvas;
|
||||||
pub mod draw;
|
pub mod draw;
|
||||||
pub mod face;
|
pub mod face;
|
||||||
|
pub mod stroke;
|
||||||
|
|
||||||
pub use canvas::{Box, Canvas, Color, Point};
|
pub use canvas::{Box, Canvas, Color, Point};
|
||||||
pub use face::Face;
|
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
|
//! `error.FileNotFound`), following Zig std's posix errno mapping for the
|
||||||
//! calls Ghostty makes (`openat`, `fstat`, `realpath`, `execve`, ...).
|
//! 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]
|
#[must_use]
|
||||||
|
#[cfg(unix)]
|
||||||
pub fn errno_name(errno: i32) -> &'static str {
|
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 {
|
match errno {
|
||||||
1 => "PermissionDenied", // EPERM
|
EPERM => "PermissionDenied",
|
||||||
2 => "FileNotFound", // ENOENT
|
ENOENT => "FileNotFound",
|
||||||
3 => "ProcessNotFound", // ESRCH
|
ESRCH => "ProcessNotFound",
|
||||||
6 | 19 => "NoDevice", // ENXIO, ENODEV
|
ENXIO | ENODEV => "NoDevice",
|
||||||
11 => "WouldBlock", // EAGAIN
|
EAGAIN => "WouldBlock",
|
||||||
12 => "SystemResources", // ENOMEM
|
ENOMEM => "SystemResources",
|
||||||
13 => "AccessDenied", // EACCES
|
EACCES => "AccessDenied",
|
||||||
16 => "DeviceBusy", // EBUSY
|
EBUSY => "DeviceBusy",
|
||||||
17 => "PathAlreadyExists", // EEXIST
|
EEXIST => "PathAlreadyExists",
|
||||||
20 => "NotDir", // ENOTDIR
|
ENOTDIR => "NotDir",
|
||||||
21 => "IsDir", // EISDIR
|
EISDIR => "IsDir",
|
||||||
23 => "SystemFdQuotaExceeded", // ENFILE
|
ENFILE => "SystemFdQuotaExceeded",
|
||||||
24 => "ProcessFdQuotaExceeded", // EMFILE
|
EMFILE => "ProcessFdQuotaExceeded",
|
||||||
26 => "FileBusy", // ETXTBSY
|
ETXTBSY => "FileBusy",
|
||||||
27 | 75 => "FileTooBig", // EFBIG, EOVERFLOW
|
EFBIG | EOVERFLOW => "FileTooBig",
|
||||||
28 => "NoSpaceLeft", // ENOSPC
|
ENOSPC => "NoSpaceLeft",
|
||||||
30 => "ReadOnlyFileSystem", // EROFS
|
EROFS => "ReadOnlyFileSystem",
|
||||||
36 => "NameTooLong", // ENAMETOOLONG
|
ENAMETOOLONG => "NameTooLong",
|
||||||
40 => "SymLinkLoop", // ELOOP
|
ELOOP => "SymLinkLoop",
|
||||||
84 => "InvalidWtf8", // EILSEQ
|
EILSEQ => "InvalidWtf8",
|
||||||
95 => "FileLocksNotSupported", // EOPNOTSUPP
|
EOPNOTSUPP => "FileLocksNotSupported",
|
||||||
_ => "Unexpected",
|
_ => "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.
|
/// `error.Name` text for an I/O error.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn io_error_name(err: &std::io::Error) -> String {
|
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 {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn n(e: i32) -> String {
|
||||||
|
io_error_name(&std::io::Error::from_raw_os_error(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn names() {
|
fn libc_constants_map() {
|
||||||
let n = |e| io_error_name(&std::io::Error::from_raw_os_error(e));
|
assert_eq!(n(libc::ENOENT), "error.FileNotFound");
|
||||||
assert_eq!(n(2), "error.FileNotFound");
|
assert_eq!(n(libc::ENOTDIR), "error.NotDir");
|
||||||
assert_eq!(n(20), "error.NotDir");
|
assert_eq!(n(libc::EACCES), "error.AccessDenied");
|
||||||
assert_eq!(n(13), "error.AccessDenied");
|
assert_eq!(n(libc::EISDIR), "error.IsDir");
|
||||||
assert_eq!(n(21), "error.IsDir");
|
assert_eq!(n(libc::ENAMETOOLONG), "error.NameTooLong");
|
||||||
assert_eq!(n(36), "error.NameTooLong");
|
assert_eq!(n(libc::ELOOP), "error.SymLinkLoop");
|
||||||
assert_eq!(n(40), "error.SymLinkLoop");
|
assert_eq!(n(libc::EAGAIN), "error.WouldBlock");
|
||||||
|
assert_eq!(n(libc::EOPNOTSUPP), "error.FileLocksNotSupported");
|
||||||
assert_eq!(n(9999), "error.Unexpected");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Z15: RV12 P3 closures
|
||||||
|
|
||||||
|
## 1. Platform error codes (`ghostty-os/src/zig_error.rs`)
|
||||||
|
`errno_name` is now per target: unix uses `libc::E*` constants (Linux and Darwin/BSD numbering both
|
||||||
|
correct, e.g. Darwin `ELOOP=62`, `ENAMETOOLONG=63`, `EAGAIN=35`); Windows maps Win32 codes the way
|
||||||
|
Zig std's `windowsCreateProcess` / file-open paths do (FILE_NOT_FOUND, PATH_NOT_FOUND, DIRECTORY ->
|
||||||
|
FileNotFound; ACCESS_DENIED -> AccessDenied; INVALID_NAME, FILENAME_EXCED_RANGE -> NameTooLong;
|
||||||
|
SHARING_VIOLATION -> FileBusy; COMMITMENT_LIMIT -> SystemResources; DISK_FULL -> NoSpaceLeft; the
|
||||||
|
ENOEXEC family -> InvalidExe), using `windows-sys` constants. Other targets (wasm): `Unexpected`.
|
||||||
|
Tests are cfg'd per platform (libc constants everywhere, literal tables for Linux/Apple/Windows) and
|
||||||
|
compile-checked with `just check-macos` / `just check-windows`.
|
||||||
|
|
||||||
|
## 2. Dot theme names (`ghostty-config/src/theme.rs`)
|
||||||
|
`zig_basename` ports `std.fs.path.basename` (ComponentIterator.last: trailing separators stripped,
|
||||||
|
`.`/`..` are ordinary names; `X:` drive root on Windows) instead of `Path::file_name`. `theme=.` and
|
||||||
|
`theme=..` now pass the basename check and report Zig's `not reading theme from ".": it is a
|
||||||
|
directory`. Differentially checked against /usr/bin/ghostty (`.`, `..`, `a/`, `a/b`, `./`: identical).
|
||||||
|
|
||||||
|
## 3. Sprite stroking (`ghostty-font/src/sprite/{stroke,canvas}.rs`)
|
||||||
|
tiny-skia is gone (dependency removed). New `stroke.rs` ports z2d 0.10 `stroke_plotter`, `Face`, `Pen`,
|
||||||
|
`Slope`, `Spline` (curve flattening with round joins), `PointBuffer` and Contour/Polygon edge building
|
||||||
|
(identity CTM, no dashes). `canvas.rs` now builds z2d-style nodes (`close` appends the implicit
|
||||||
|
`move_to`), fills via direct node flattening in f64 (no f32 skia round trip), ports z2d's Cairo-style
|
||||||
|
`arc` (segment count from the tolerance table), and uses z2d's truncating integer `src_over`
|
||||||
|
(`s + d - s*d/255`, `dst_in` `cov*a/255`) instead of rounding.
|
||||||
|
Sprite-sheet reference test: every one of the 36 sheets is now bit-exact (was 1.2% differing, max 86);
|
||||||
|
the test now requires 0 differing pixels.
|
||||||
|
`just visual-parity` (iteration build of the production profile): page, page-bar, tabs and chrome all
|
||||||
|
0 differing pixels (was 196 px / 0.020% for page).
|
||||||
@@ -30,13 +30,13 @@
|
|||||||
|
|
||||||
| scenario | before | after |
|
| scenario | before | after |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| page (full frame) | 0.130% (1250 px, max 93) | 0.020% (196 px, max 86; 0.001% > 32 levels) |
|
| page (full frame) | 0.130% (1250 px, max 93) | 0.000% (0 px; was 0.020% / 196 px before Z15) |
|
||||||
| page-bar | same as page | 0.020% |
|
| page-bar | same as page | 0.000% |
|
||||||
| tabs + splits | 44.6% (a whole pane blank, bug) | 0.000% (bit-identical) |
|
| tabs + splits | 44.6% (a whole pane blank, bug) | 0.000% (bit-identical) |
|
||||||
| chrome (headerbar, tab bar) | 0.000% | 0.000% |
|
| chrome (headerbar, tab bar) | 0.000% | 0.000% |
|
||||||
|
|
||||||
Sprite sheets (`ghostty-font` tests, Zig reference PNGs): differing pixels 5.76% -> 1.20%, pixels off by
|
Sprite sheets (`ghostty-font` tests, Zig reference PNGs): differing pixels 5.76% -> 1.20% -> 0 (bit-exact after Z15;
|
||||||
more than 96 levels 74 -> 0 (test tolerance tightened from 4% to 0.1% big diffs).
|
test tolerance is now zero).
|
||||||
|
|
||||||
Font metrics, cell size, baseline, glyph rasterization, colors, cursor, underline positions, padding,
|
Font metrics, cell size, baseline, glyph rasterization, colors, cursor, underline positions, padding,
|
||||||
GTK CSS/headerbar/tab bar were already pixel-identical; no change was needed there.
|
GTK CSS/headerbar/tab bar were already pixel-identical; no change was needed there.
|
||||||
@@ -51,12 +51,12 @@ GTK CSS/headerbar/tab bar were already pixel-identical; no change was needed the
|
|||||||
2. **Sprite antialiasing** (`ghostty-font/src/sprite/canvas.rs`): replaced tiny-skia's coverage with a port of
|
2. **Sprite antialiasing** (`ghostty-font/src/sprite/canvas.rs`): replaced tiny-skia's coverage with a port of
|
||||||
z2d 0.10's default `multisample_4x` rasterizer (4x4 samples, scanline spans with edge x rounded at
|
z2d 0.10's default `multisample_4x` rasterizer (4x4 samples, scanline spans with edge x rounded at
|
||||||
sample row centers, non-zero filter, alpha = `coverage * 16 - 1`, full = 255, z2d's Bezier
|
sample row centers, non-zero filter, alpha = `coverage * 16 - 1`, full = 255, z2d's Bezier
|
||||||
flattening with tolerance 0.1). Stroke outlines still come from tiny-skia's stroker.
|
flattening with tolerance 0.1). (Stroke outlines were still tiny-skia's until Z15, which ported z2d's stroker.)
|
||||||
|
|
||||||
## Remaining differences (page: 196 px, all <= 86 levels)
|
## Remaining differences
|
||||||
|
|
||||||
- Curly underline: first/last pixels and peaks differ slightly. z2d builds its own stroke polygon (pen/joins)
|
None (Z15). z2d's stroke plotter (pen, joins, caps, spline flattening), Cairo-style arc segmentation and
|
||||||
for curves; tiny-skia's stroker offsets the Bezier differently.
|
z2d's truncating integer compositing were ported (`ghostty-font/src/sprite/stroke.rs`, `canvas.rs`;
|
||||||
- Diagonal box glyphs (`U+2571..2573`) have a few edge pixels off by up to ~38 levels (same cause).
|
see `final-Z15.md`). Final `just visual-parity`: page, page-bar and tabs are 0 differing pixels on the
|
||||||
- Porting z2d's stroker (`tess/stroke_plotter`, `Pen`) would remove the rest; judged not worth it
|
full frame, the terminal area and the chrome (max delta 0). All sprite sheets in the `ghostty-font`
|
||||||
(0.02% of pixels, imperceptible).
|
reference test are bit-exact (tolerance now zero).
|
||||||
Reference in new issue
Block a user