Eyedropper: color samples match the canvas and stay within their work limit

Squashed from #4979 onto main.
This commit is contained in:
dotCooCoo
2026-11-01 04:55:09 +07:00
committed by bflatastic
parent dc6440d523
commit 7211dc789f
+315 -26
View File
@@ -54,15 +54,25 @@ pub fn paint_color_at(paint: &Paint, bounds: Option<vectorcraft_geom::Rect>, p:
}
/// How many patterns and symbols deep a sample follows art inside art (tile art painted with a
/// pattern, a symbol whose art holds an instance of another).
/// pattern, a symbol whose art holds an instance of another). Art nested deeper gives no color.
const MAX_NESTING: usize = 16;
/// Most tiles of one pattern a sample looks at, front to back, for art that paints the point.
/// How many patterns deep the canvas draws tile art painted with a pattern. A pattern nested
/// deeper, like a pattern the document lacks, draws (and samples) as [`MISSING_PATTERN`].
const PATTERN_DEPTH: usize = 3;
/// The gray the canvas fills a pattern with when it does not draw its tiles.
const MISSING_PATTERN: [u8; 3] = [128, 128, 128];
/// Most tiles of one pattern a sample looks at, front to back, for art that paints the point. When
/// more tiles reach the point, the sample gives no color.
const MAX_TILES: usize = 1_000;
/// Most work one sample does, in objects looked at: each hit test after the first in a document
/// counts the objects of the document and of its symbols, each pattern tile or symbol instance
/// sampled counts the objects of its art, and every hundred tile positions checked count one.
/// Most work one sample does, in objects looked at: each hit test after the first in the sampled
/// document, and each one in the art of a tile or instance, counts the objects of the document it
/// tests and of the symbols, each pattern tile or symbol instance
/// sampled counts the objects of its art, and every hundred tile positions checked, or points of a
/// mesh's patches looked at, count one.
/// Art inside art multiplies them; a sample that would need more gives no color.
const MAX_WORK: usize = 1_000_000;
@@ -76,6 +86,8 @@ struct Sampler<'a> {
work: usize,
/// The work ran out: the sample gives no color.
exhausted: bool,
/// How many patterns the art being sampled is inside.
patterns: usize,
}
impl Sampler<'_> {
@@ -113,7 +125,7 @@ pub(crate) fn sample_color_at(cx: &ToolContext, p: Point) -> Option<Color> {
let (doc, opts, raster_sample) = (cx.doc, sample_options(cx), cx.raster_sample);
vectorcraft_doc::depth::with_deep_stack(|| {
let symbol_nodes = doc.symbols.iter().map(|s| s.art.count()).sum();
let mut s = Sampler { doc, opts, raster_sample, symbol_nodes, work: MAX_WORK, exhausted: false };
let mut s = Sampler { doc, opts, raster_sample, symbol_nodes, work: MAX_WORK, exhausted: false, patterns: 0 };
let mut levels: Vec<Option<Document>> = (0..MAX_NESTING).map(|_| None).collect();
let c = color_under(&mut s, doc, &mut levels, p, opts);
c.filter(|_| !s.exhausted)
@@ -128,10 +140,14 @@ fn color_under(s: &mut Sampler, doc: &Document, levels: &mut [Option<Document>],
let mut passed: HashSet<NodeId> = HashSet::new();
let mut size = None;
loop {
// The first hit test is the one a click makes anyway; each one after it may visit every
// object again.
let cost =
if passed.is_empty() { 1 } else { *size.get_or_insert_with(|| s.symbol_nodes + doc.layers.iter().map(|l| l.count()).sum::<usize>()) };
// The first hit test in the sampled document is the one a click makes anyway; each one
// after it, and every one in the art of a tile or instance, may visit every object again
// and measure the symbols' art.
let cost = if passed.is_empty() && std::ptr::eq(doc, s.doc) {
1
} else {
*size.get_or_insert_with(|| s.symbol_nodes + doc.layers.iter().map(|l| l.count()).sum::<usize>())
};
s.spend(cost)?;
let h = hit_test_visible_skipping(doc, p, opts, &|id| passed.contains(&id))?;
let id = paint_owner(doc, h.leaf);
@@ -168,6 +184,8 @@ fn node_color_at(s: &mut Sampler, levels: &mut [Option<Document>], n: &Node, on_
match &n.kind {
NodeKind::Image(_) => image_color_at(s, n, p),
NodeKind::Mesh(m) => {
// Finding the patch point looks at 17 × 17 points of every patch: a hundred count as one unit.
s.spend((m.rows as usize).saturating_mul(m.cols as usize).saturating_mul(17 * 17) / 100)?;
let (r, c, u, v) = m.locate(p)?;
Some(m.color_at(r, c, u, v).0)
}
@@ -201,7 +219,39 @@ pub(crate) fn has_sampleable_paint(cx: &ToolContext, id: vectorcraft_doc::NodeId
/// of its tile art at `p` ([`pattern_color_at`]).
fn paint_sample(s: &mut Sampler, levels: &mut [Option<Document>], n: &Node, on_stroke: bool, p: Point) -> Option<Color> {
let fill_none = n.proxy_paint(false, None).is_none_or(|(p, ..)| p.is_none());
let (paint, to_doc, bounds) = n.proxy_paint(on_stroke || fill_none, None)?;
let stroke = on_stroke || fill_none;
// Type takes its paint from its characters, and a colorized object from its colorize paint.
let items: Vec<usize> = n.appearance.items.iter().enumerate().filter(|(_, it)| it.is_fill() != stroke).map(|(i, _)| i).collect();
if items.is_empty() || matches!(n.kind, NodeKind::Text(_)) || (!stroke && n.extras().colorize.is_some()) {
let (paint, to_doc, bounds) = n.proxy_paint(stroke, None)?;
return paint_color(s, levels, paint, to_doc, bounds, p);
}
// The visible fills (or strokes) from the top: where one leaves a gap, the next shows. Of a
// path's strokes, only the ones that reach `p` count, when any does (a click within the hit
// tolerance of a stroke may reach none).
let mut visible: Vec<usize> = items.into_iter().filter(|&i| n.appearance.items.get(i).is_some_and(|it| it.visible())).collect();
if stroke && let (NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. }, Some(bp)) = (&n.kind, n.stroke_path()) {
let reach =
|i: &usize| matches!(n.appearance.items.get(*i), Some(vectorcraft_doc::AppearanceItem::Stroke(st)) if st.hits(&bp, *rule, p, 0.0));
if visible.iter().any(&reach) {
visible.retain(&reach);
}
}
for &i in visible.iter().rev() {
let Some((paint, to_doc, bounds)) = n.proxy_paint(stroke, Some(i)) else { continue };
if let Some(c) = paint_color(s, levels, paint, to_doc, bounds, p) {
return Some(c);
}
}
None
}
/// The color `paint`, in a space `to_doc` places in the document and with box `bounds` there,
/// shows at `p`. A pattern gives the color of its tile art at `p` ([`pattern_color_at`]).
fn paint_color(s: &mut Sampler, levels: &mut [Option<Document>], paint: &Paint, to_doc: Affine, bounds: Rect, p: Point) -> Option<Color> {
if paint.is_none() {
return None;
}
if let Paint::Pattern { pattern, xf } = paint {
return pattern_color_at(s, levels, pattern, to_doc * *xf, p);
}
@@ -210,13 +260,30 @@ fn paint_sample(s: &mut Sampler, levels: &mut [Option<Document>], n: &Node, on_s
}
/// The color pattern `name`, placed in the document by `xf`, shows at `p`: the color of the
/// front-most tile art there. A point the tiles leave empty has no color.
/// front-most tile art there. A point the tiles leave empty has no color. A pattern the document
/// lacks, or one more than [`PATTERN_DEPTH`] patterns deep, shows [`MISSING_PATTERN`].
fn pattern_color_at(s: &mut Sampler, levels: &mut [Option<Document>], name: &str, xf: Affine, p: Point) -> Option<Color> {
if xf.determinant().abs() < 1e-12 {
return None;
}
let [r, g, b] = MISSING_PATTERN;
let doc = s.doc;
let Some(def) = doc.patterns.iter().find(|d| d.name == name) else { return Some(Color::rgb8(r, g, b)) };
if def.art.is_empty() {
return None;
}
if s.patterns >= PATTERN_DEPTH {
return Some(Color::rgb8(r, g, b));
}
s.patterns += 1;
let c = tiles_color_at(s, levels, def, xf, p);
s.patterns -= 1;
c
}
/// The color the front-most tile art of pattern `def`, placed in the document by `xf`, shows at `p`.
fn tiles_color_at(s: &mut Sampler, levels: &mut [Option<Document>], def: &vectorcraft_doc::PatternDef, xf: Affine, p: Point) -> Option<Color> {
let doc = s.doc;
let def = doc.patterns.iter().find(|d| d.name == name)?;
// Sampled in pattern space, one tile at a time from the front, among the tiles whose art reaches
// the point (or meets it at an edge). The art moves by whole tiles, and the patterns it is
// painted with move with it.
@@ -235,10 +302,25 @@ fn pattern_color_at(s: &mut Sampler, levels: &mut [Option<Document>], name: &str
let bounds: Vec<Option<Rect>> = def.art.iter().map(|a| a.visual_bounds_with(&symbols)).collect();
let reaches = |m: Affine| bounds.iter().any(|b| b.is_none_or(|b| m.transform_rect_bbox(b).inflate(1e-6, 1e-6).contains(q)));
let scaling = vectorcraft_doc::Scaling { patterns: true, ..Default::default() };
// The tiles are listed as the canvas lists the ones it draws: by the art's bounds without its
// symbols. Listing them looks at every tile position near the point widened by those bounds (at
// most 250,000, past which it lists none): a hundred positions count as one unit, counted first.
let listed = def.art_bounds().unwrap_or(def.tile).union(def.tile);
let (w, h) = (def.width(), def.height());
let (sx, sy) = def.tile_type.steps(w, h);
let positions = ((near.width() + listed.width() + 2.0 * w) / sx + 3.0) * ((near.height() + listed.height() + 2.0 * h) / sy + 3.0);
if positions.is_finite() && positions > 0.0 {
s.spend(positions.min(250_000.0) as usize / 100)?;
}
let offsets = def.offsets_covering(near);
// Each tile position is checked against each piece of art: a hundred checks count as one unit.
s.spend(offsets.len().saturating_mul(bounds.len().max(1)) / 100)?;
for m in offsets.into_iter().rev().map(|o| def.instance_xf(o)).filter(|m| reaches(*m)).take(MAX_TILES) {
for (k, m) in offsets.into_iter().rev().map(|o| def.instance_xf(o)).filter(|m| reaches(*m)).enumerate() {
// A tile past the limit may paint the point: the sample gives no color.
if k == MAX_TILES {
s.exhausted = true;
return None;
}
s.spend(1 + art_nodes)?;
let art = def.art.iter().map(|a| {
let mut n = (**a).clone();
@@ -256,9 +338,12 @@ fn pattern_color_at(s: &mut Sampler, levels: &mut [Option<Document>], name: &str
/// as [`node_color_at`] samples an object, with no tolerance around outlines and strokes: only
/// what it paints at `p` counts. The art is hit in the first of `levels`, a scratch document with
/// the sampled document's symbols, made on first use and emptied for each new piece of art; art
/// inside it uses the levels after it. With no level left, the art gives no color.
/// inside it uses the levels after it. With no level left, the whole sample gives no color.
fn art_color_at(s: &mut Sampler, levels: &mut [Option<Document>], art: impl IntoIterator<Item = Node>, p: Point) -> Option<Color> {
let (slot, deeper) = levels.split_first_mut()?;
let Some((slot, deeper)) = levels.split_first_mut() else {
s.exhausted = true;
return None;
};
let doc = s.doc;
let scratch = slot.get_or_insert_with(|| {
let mut d = Document::new(1.0, 1.0);
@@ -755,6 +840,103 @@ mod tests {
assert_eq!(at(&d, 150.0, 155.0), "#00ff00");
}
/// A pattern nested more than three patterns deep, and a pattern the document lacks, sample as
/// the gray the canvas draws them with.
#[test]
fn deep_and_missing_patterns_sample_as_the_canvas_draws_them() {
use vectorcraft_doc::pattern::pattern_paint;
use vectorcraft_geom::Rect;
let (mut d, id) = doc_with_rect();
// "P0" to "P2": a 10 pt tile filled with the next; "P3" a green tile.
for i in 0..4 {
let fill = if i == 3 { Paint::solid(Color::rgb(0.0, 1.0, 0.0)) } else { pattern_paint(&format!("P{}", i + 1)) };
let mut def = vectorcraft_doc::PatternDef::new(&format!("P{i}"), vec![filled_rect(Rect::new(0.0, 0.0, 10.0, 10.0), fill)]);
def.tile = Rect::new(0.0, 0.0, 10.0, 10.0);
d.patterns.push(def);
}
let (s, p) = (Selection::default(), paint());
let mut at = |name: &str| {
d.node_mut(id).unwrap().appearance.set_fill(pattern_paint(name));
sample_color_at(&cx(&d, &s, &p), Point::new(150.5, 150.5)).map(|c| c.to_hex())
};
assert_eq!(at("P1").as_deref(), Some("#00ff00"), "three patterns deep");
assert_eq!(at("P0").as_deref(), Some("#808080"), "four patterns deep");
assert_eq!(at("Nope").as_deref(), Some("#808080"), "missing");
}
/// Art nested deeper than a sample follows gives no color, rather than the color of what lies
/// below it.
#[test]
fn art_nested_past_the_limit_gives_no_color() {
use std::sync::Arc;
use vectorcraft_doc::{NodeId, Symbol};
use vectorcraft_geom::{Affine, Rect};
let (mut d, _) = doc_with_rect();
// "Deep0" to "Deep19": one instance of the next; "Deep20" a green square over the white
// rectangle.
for i in 0..20 {
let next = Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: format!("Deep{}", i + 1), xf: Affine::IDENTITY }));
d.symbols.push(Symbol { dynamic: false, name: format!("Deep{i}"), art: next });
}
d.symbols.push(Symbol {
dynamic: false,
name: "Deep20".into(),
art: filled_rect(Rect::new(100.0, 100.0, 200.0, 200.0), Paint::solid(Color::rgb(0.0, 1.0, 0.0))),
});
let (l, inst) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), usize::MAX, Node::new(inst, NodeKind::SymbolInstance { symbol: "Deep0".into(), xf: Affine::IDENTITY })).unwrap();
let (s, p) = (Selection::default(), paint());
assert_eq!(sample_color_at(&cx(&d, &s, &p), Point::new(150.5, 150.5)).map(|c| c.to_hex()), None);
}
/// A pattern with more tiles over the point than a sample looks at gives no color, rather than
/// the color of what lies below it.
#[test]
fn too_many_tiles_over_the_point_give_no_color() {
use vectorcraft_doc::pattern::pattern_paint;
use vectorcraft_geom::{PathData, Rect, SubPath};
let (mut d, _) = doc_with_rect();
// An L 0.1 pt thick along the top and left of (0, 0)–(200, 200) on 1 pt tiles: thousands of
// tiles reach (150.5, 150.5), and none of them paints it.
let l: Vec<Point> = [(0.0, 0.0), (200.0, 0.0), (200.0, 0.1), (0.1, 0.1), (0.1, 200.0), (0.0, 200.0)].map(|(x, y)| Point::new(x, y)).into();
let mut el = (*filled_rect(Rect::new(0.0, 0.0, 1.0, 1.0), Paint::solid(Color::BLACK))).clone();
if let NodeKind::Path { path, .. } = &mut el.kind {
*path = PathData::single(SubPath::polyline(&l, true));
}
let mut def = vectorcraft_doc::PatternDef::new("Ls", vec![std::sync::Arc::new(el)]);
def.tile = Rect::new(0.0, 0.0, 1.0, 1.0);
d.patterns.push(def);
let (l, top) = (d.layers[0].id, d.alloc_id());
let mut over = (*filled_rect(Rect::new(100.0, 100.0, 200.0, 200.0), pattern_paint("Ls"))).clone();
over.id = top;
d.insert(Some(l), usize::MAX, over).unwrap();
let (s, p) = (Selection::default(), paint());
assert_eq!(sample_color_at(&cx(&d, &s, &p), Point::new(150.5, 150.5)).map(|c| c.to_hex()), None);
}
/// Symbol art in a tile is sampled only in the tiles the canvas draws, which it lists by the
/// art's bounds without its symbols.
#[test]
fn symbol_art_far_outside_its_tile_samples_as_the_canvas_draws_it() {
use std::sync::Arc;
use vectorcraft_doc::NodeId;
use vectorcraft_geom::{Affine, Rect};
let (mut d, id) = doc_with_rect();
// A 20 pt tile holding an instance of a 10 pt green square that sits 100 pt right of it.
// The canvas lists only the tile over a point, whose square lies 100 pt away, so it draws
// no green at x 165, where the square of the tile at x 60 would lie, or at x 175.
let green = Paint::solid(Color::rgb(0.0, 1.0, 0.0));
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Away".into(), art: filled_rect(Rect::new(100.0, 0.0, 110.0, 10.0), green) });
let away = Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Away".into(), xf: Affine::IDENTITY }));
let mut def = vectorcraft_doc::PatternDef::new("Away", vec![away]);
def.tile = Rect::new(0.0, 0.0, 20.0, 10.0);
d.patterns.push(def);
d.node_mut(id).unwrap().appearance.set_fill(vectorcraft_doc::pattern::pattern_paint("Away"));
let (s, p) = (Selection::default(), paint());
let at = |x: f64| sample_color_at(&cx(&d, &s, &p), Point::new(x, 155.0)).map(|c| c.to_hex());
assert_eq!((at(165.0), at(175.0)), (None, None));
}
/// Patterns whose tile art is painted with the next one, on tiles far smaller than their art,
/// symbols whose art holds four instances of the next, and symbols nested far deeper than a
/// sample follows: each sample stops within its work limit and gives no color, instead of
@@ -766,18 +948,27 @@ mod tests {
use vectorcraft_doc::{NodeId, Symbol};
use vectorcraft_geom::{Affine, Rect};
let (mut d, id) = doc_with_rect();
// "Nest0" to "Nest7": 300 pt art on 1 pt tiles, each painted with the next. "Nest8" doesn't
// exist, so no tile gives a color and every one is tried.
for i in 0..8 {
let mut def = vectorcraft_doc::PatternDef::new(
&format!("Nest{i}"),
vec![filled_rect(Rect::new(0.0, 0.0, 300.0, 300.0), pattern_paint(&format!("Nest{}", i + 1)))],
);
// "Gap": an L 0.1 pt thick along the top and left of (0, 0)–(20, 20) on 1 pt tiles, which
// reaches every point from hundreds of tiles and paints none at x.5, y.5.
let l: Vec<Point> = [(0.0, 0.0), (20.0, 0.0), (20.0, 0.1), (0.1, 0.1), (0.1, 20.0), (0.0, 20.0)].map(|(x, y)| Point::new(x, y)).into();
let mut el = (*filled_rect(Rect::new(0.0, 0.0, 1.0, 1.0), Paint::solid(Color::BLACK))).clone();
if let NodeKind::Path { path, .. } = &mut el.kind {
*path = vectorcraft_geom::PathData::single(vectorcraft_geom::SubPath::polyline(&l, true));
}
let mut gap = vectorcraft_doc::PatternDef::new("Gap", vec![Arc::new(el)]);
gap.tile = Rect::new(0.0, 0.0, 1.0, 1.0);
d.patterns.push(gap);
// "Nest0" and "Nest1": 300 pt art on 1 pt tiles, painted with the next ("Gap" after
// "Nest1"), so no tile gives a color and every one is tried.
for i in 0..2 {
let next = if i == 1 { "Gap".to_string() } else { format!("Nest{}", i + 1) };
let mut def =
vectorcraft_doc::PatternDef::new(&format!("Nest{i}"), vec![filled_rect(Rect::new(0.0, 0.0, 300.0, 300.0), pattern_paint(&next))]);
def.tile = Rect::new(0.0, 0.0, 1.0, 1.0);
d.patterns.push(def);
}
d.node_mut(id).unwrap().appearance.set_fill(pattern_paint("Nest0"));
// "Fan0" to "Fan11": four instances of the next symbol; "Fan12" a square filled with "Nest8".
// "Fan0" to "Fan11": four instances of the next symbol; "Fan12" a square filled with "Gap".
let instance = |name: String| Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: name, xf: Affine::IDENTITY }));
for i in 0..12 {
let art = Node::group(NodeId(0), (0..4).map(|_| instance(format!("Fan{}", i + 1))).collect());
@@ -786,7 +977,7 @@ mod tests {
d.symbols.push(Symbol {
dynamic: false,
name: "Fan12".into(),
art: filled_rect(Rect::new(300.0, 300.0, 400.0, 400.0), pattern_paint("Nest8")),
art: filled_rect(Rect::new(300.0, 300.0, 400.0, 400.0), pattern_paint("Gap")),
});
// "Deep0" to "Deep99": one instance of the next; "Deep100" a green square.
for i in 0..100 {
@@ -801,12 +992,92 @@ mod tests {
}
let (s, p) = (Selection::default(), paint());
let start = std::time::Instant::now();
for at in [Point::new(150.0, 150.0), Point::new(350.0, 350.0), Point::new(350.0, 50.0)] {
for at in [Point::new(150.5, 150.5), Point::new(350.5, 350.5), Point::new(350.5, 50.5)] {
assert_eq!(sample_color_at(&cx(&d, &s, &p), at), None, "{at:?}");
}
assert!(start.elapsed() < std::time::Duration::from_secs(60), "{:?}", start.elapsed());
}
/// Each hit test in a tile's art may measure the art of every symbol, so it counts the symbols'
/// objects, also when it is the first one in that art and hits nothing.
#[test]
fn hit_tests_in_tile_art_count_the_symbols_they_may_measure() {
use std::sync::Arc;
use vectorcraft_doc::pattern::pattern_paint;
use vectorcraft_doc::{NodeId, Symbol};
use vectorcraft_geom::{Affine, PathData, Rect, SubPath};
let (mut d, id) = doc_with_rect();
// Tile art: an L 0.1 pt thick along the top and left of (0, 0)–(200, 200), which no 1 pt
// tile places over (150.5, 150.5) though the art of a thousand tiles reaches it, and below
// it an instance of "Dots" (200 squares), which most of those tiles place away from it.
let l: Vec<Point> = [(0.0, 0.0), (200.0, 0.0), (200.0, 0.1), (0.1, 0.1), (0.1, 200.0), (0.0, 200.0)].map(|(x, y)| Point::new(x, y)).into();
let el = filled_rect(Rect::new(0.0, 0.0, 1.0, 1.0), Paint::solid(Color::BLACK));
let mut el = (*el).clone();
if let NodeKind::Path { path, .. } = &mut el.kind {
*path = PathData::single(SubPath::polyline(&l, true));
}
let green = || Paint::solid(Color::rgb(0.0, 1.0, 0.0));
let dots = (0..200).map(f64::from).map(|k| filled_rect(Rect::new(k, k, k + 0.1, k + 0.1), green())).collect();
d.symbols.push(Symbol { dynamic: false, name: "Dots".into(), art: Arc::new(Node::group(NodeId(0), dots)) });
let far = Affine::translate((0.0, -250.0));
let inst = Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Dots".into(), xf: far }));
let mut def = vectorcraft_doc::PatternDef::new("Dotted", vec![Arc::new(el), inst]);
def.tile = Rect::new(0.0, 0.0, 1.0, 1.0);
d.patterns.push(def);
d.node_mut(id).unwrap().appearance.set_fill(pattern_paint("Dotted"));
let (sel, p) = (Selection::default(), paint());
let cx = cx(&d, &sel, &p);
let opts = sample_options(&cx);
let symbol_nodes = d.symbols.iter().map(|s| s.art.count()).sum();
let mut s = Sampler { doc: &d, opts, raster_sample: cx.raster_sample, symbol_nodes, work: MAX_WORK, exhausted: false, patterns: 0 };
let mut levels: Vec<Option<Document>> = (0..MAX_NESTING).map(|_| None).collect();
color_under(&mut s, &d, &mut levels, Point::new(150.5, 150.5), opts);
// The symbols' objects count for each of at least a hundred tiles, not once for the pattern.
assert!(MAX_WORK - s.work >= 100 * symbol_nodes, "{} spent, {symbol_nodes} symbol objects", MAX_WORK - s.work);
}
/// A gap in an object's top pattern fill shows the object's own fill below it.
#[test]
fn a_gap_in_a_top_fill_samples_the_fill_below() {
use vectorcraft_doc::pattern::pattern_paint;
use vectorcraft_doc::{AppearanceItem, FillLayer};
use vectorcraft_geom::Rect;
// The rectangle (100, 100)–(200, 200) filled with red 10 pt squares 20 pt apart over blue.
let (mut d, id) = doc_with_rect();
let mut gappy =
vectorcraft_doc::PatternDef::new("Gappy", vec![filled_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)))]);
gappy.tile = Rect::new(0.0, 0.0, 20.0, 10.0);
d.patterns.push(gappy);
let ap = &mut d.node_mut(id).unwrap().appearance;
ap.set_fill(pattern_paint("Gappy"));
ap.items.insert(0, AppearanceItem::Fill(FillLayer::new(Paint::solid(Color::rgb(0.0, 0.0, 1.0)))));
let (s, p) = (Selection::default(), paint());
let at = |x: f64| sample_color_at(&cx(&d, &s, &p), Point::new(x, 155.0)).map(|c| c.to_hex());
// x 125 is 5 into a tile, on a red square; x 133 is 13 into one, between them.
assert_eq!((at(125.0), at(133.0)), (Some("#ff0000".to_string()), Some("#0000ff".to_string())));
// A hidden top fill is not drawn, so the fill below shows on the squares too.
let mut d = d.clone();
d.node_mut(id).unwrap().appearance.fill_mut().unwrap().visible = false;
let at = |x: f64| sample_color_at(&cx(&d, &s, &p), Point::new(x, 155.0)).map(|c| c.to_hex());
assert_eq!(at(125.0), Some("#0000ff".to_string()));
}
/// Of an object's strokes, the topmost one that reaches the point is sampled.
#[test]
fn the_topmost_stroke_that_reaches_the_point_is_sampled() {
use vectorcraft_doc::{AppearanceItem, StrokeLayer};
// The rectangle (100, 100)–(200, 200) with a 20 pt blue stroke under a 2 pt red one.
let (mut d, id) = doc_with_rect();
let ap = &mut d.node_mut(id).unwrap().appearance;
ap.items.retain(|it| it.is_fill());
ap.items.push(AppearanceItem::Stroke(StrokeLayer::new(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), 20.0)));
ap.items.push(AppearanceItem::Stroke(StrokeLayer::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 2.0)));
let (s, p) = (Selection::default(), paint());
let at = |x: f64| sample_color_at(&cx(&d, &s, &p), Point::new(x, 150.0)).map(|c| c.to_hex());
// x 100 is on both strokes; x 94 is on the blue one only.
assert_eq!((at(100.0), at(94.0)), (Some("#ff0000".to_string()), Some("#0000ff".to_string())));
}
/// A gap between a pattern's tile art shows what lies below it, in the document and in the tile
/// art of another pattern.
#[test]
@@ -867,6 +1138,24 @@ mod tests {
assert_eq!(sampled_hex(&a), "#404040");
}
/// Finding the point in a mesh counts the points it looks at in every patch: a 20 × 20 mesh
/// takes more than a thousand units of work.
#[test]
fn mesh_samples_count_the_work_of_finding_the_point() {
use vectorcraft_doc::{Document, GradientMesh};
let mut d = Document::new(500.0, 500.0);
let (l, id) = (d.layers[0].id, d.alloc_id());
let mesh = GradientMesh::from_surface(20, 20, &|u, v| Point::new(100.0 + 100.0 * u, 100.0 + 100.0 * v), &|_, _| Color::rgb(0.0, 1.0, 0.0));
d.insert(Some(l), 0, Node::new(id, NodeKind::Mesh(mesh))).unwrap();
let (sel, p) = (Selection::default(), paint());
let cx = cx(&d, &sel, &p);
let opts = sample_options(&cx);
let mut s = Sampler { doc: &d, opts, raster_sample: cx.raster_sample, symbol_nodes: 0, work: 1_000, exhausted: false, patterns: 0 };
let mut levels: Vec<Option<Document>> = (0..MAX_NESTING).map(|_| None).collect();
assert_eq!(color_under(&mut s, &d, &mut levels, Point::new(150.0, 150.0), opts).map(|c| c.to_hex()), None);
assert!(s.exhausted);
}
#[test]
fn gradient_sampled_along_vector() {
let g = vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient::default());