diff --git a/crates/tools/src/xform/eyedropper.rs b/crates/tools/src/xform/eyedropper.rs index 46b66599b..f2f214935 100644 --- a/crates/tools/src/xform/eyedropper.rs +++ b/crates/tools/src/xform/eyedropper.rs @@ -54,15 +54,25 @@ pub fn paint_color_at(paint: &Paint, bounds: Option, 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 { 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> = (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], let mut passed: HashSet = 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::()) }; + // 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::()) + }; 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], 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], n: &Node, on_stroke: bool, p: Point) -> Option { 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 = 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 = 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], paint: &Paint, to_doc: Affine, bounds: Rect, p: Point) -> Option { + 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], 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], name: &str, xf: Affine, p: Point) -> Option { 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], def: &vectorcraft_doc::PatternDef, xf: Affine, p: Point) -> Option { 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], name: &str let bounds: Vec> = 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], 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], art: impl IntoIterator, p: Point) -> Option { - 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 = [(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 = [(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 = [(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> = (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> = (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());