mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
As the branch does for older tests: `panic!("…")` instead of
`unreachable!()` in test code (clippy.toml only allows unwrap, expect and
panic there), and the integration tests main added (CLI file I/O, render
gradient text and stroke geometry, SVG export suites) opt out of the
unwrap/expect/panic lints. No test logic changed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
70 lines
3.3 KiB
Rust
70 lines
3.3 KiB
Rust
//! Gradients on type: a vector across the text, and run strokes fitted like object strokes.
|
||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_color::Paint;
|
||
use vectorcraft_doc::{Document, NodeId, NodeKind, TextObject, appearance::stroke_paint_bounds};
|
||
use vectorcraft_geom::Rect;
|
||
use vectorcraft_testkit::fixtures::{exec, session_with};
|
||
use vectorcraft_testkit::raster::{Image, render_view};
|
||
|
||
/// Mean red and blue of the solid ink (no green: the ramp has none, the paper and anti-aliased
|
||
/// edges do) in columns `x0..x1`.
|
||
fn ink_tint(img: &Image, x0: u32, x1: u32) -> (f64, f64) {
|
||
let px: Vec<[u8; 4]> =
|
||
(x0..x1).flat_map(|x| (0..img.height).map(move |y| (x, y))).map(|(x, y)| img.pixel(x, y)).filter(|p| p[3] == 255 && p[1] < 40).collect();
|
||
assert!(px.len() > 20, "ink in {x0}..{x1}: {}", px.len());
|
||
let mean = |c: usize| px.iter().map(|p| p[c] as f64).sum::<f64>() / px.len() as f64;
|
||
(mean(0), mean(2))
|
||
}
|
||
|
||
#[test]
|
||
fn gradient_vector_runs_across_the_text() {
|
||
let mut s = session_with(300.0, 100.0);
|
||
exec(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "HHHHHH", "size": 60}));
|
||
exec(&mut s, "paint.setFill", json!({"gradient": {"stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]}}));
|
||
exec(&mut s, "paint.setGradientGeom", json!({"start": [10, 50], "end": [200, 50]}));
|
||
let img = render_view(&s.doc().unwrap().doc, 300, 100);
|
||
let (r0, b0) = ink_tint(&img, 10, 50);
|
||
let (r1, b1) = ink_tint(&img, 150, 200);
|
||
assert!(r0 > 180.0 && b0 < 75.0, "red where the vector starts: {r0} {b0}");
|
||
assert!(b1 > 160.0 && r1 < 95.0, "blue where it ends: {r1} {b1}");
|
||
}
|
||
|
||
fn text_mut(doc: &mut Document, id: NodeId) -> &mut TextObject {
|
||
match &mut doc.node_mut(id).unwrap().kind {
|
||
NodeKind::Text(t) => t,
|
||
k => panic!("not text: {k:?}"),
|
||
}
|
||
}
|
||
|
||
/// `doc` with every run's stroke gradient pinned to its fit on `b`.
|
||
fn pinned(doc: &Document, id: NodeId, b: Rect) -> Document {
|
||
let mut d = doc.clone();
|
||
for r in &mut text_mut(&mut d, id).runs {
|
||
if let Paint::Gradient(g) = &mut r.style.stroke {
|
||
g.pin(b);
|
||
}
|
||
}
|
||
d
|
||
}
|
||
|
||
#[test]
|
||
fn unplaced_run_stroke_gradients_fit_the_stroke_inflated_box() {
|
||
let mut s = session_with(300.0, 100.0);
|
||
exec(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "HHHH", "size": 60}));
|
||
exec(&mut s, "paint.setFill", json!({"none": true}));
|
||
exec(&mut s, "paint.setStroke", json!({"gradient": {"stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]}}));
|
||
let id = s.doc().unwrap().selection.objects[0];
|
||
let mut doc: Document = (*s.doc().unwrap().doc).clone();
|
||
let t = text_mut(&mut doc, id);
|
||
t.runs.iter_mut().for_each(|r| r.style.stroke_width = 8.0);
|
||
let lb = t.local_bounds();
|
||
let unplaced = render_view(&doc, 300, 100);
|
||
// Pinned to the fit on the layout box grown by half the 8 pt weight, it draws the same pixels;
|
||
// the bare layout box would not.
|
||
assert_eq!(render_view(&pinned(&doc, id, stroke_paint_bounds(lb, 8.0)), 300, 100), unplaced);
|
||
assert_ne!(render_view(&pinned(&doc, id, lb), 300, 100), unplaced);
|
||
}
|