#1723 follow-up: pattern tiles are drawn again after a brush edit or new Color Settings

A tile was kept while the pattern's art and what it shows were unchanged,
but a brush the art is stroked with lives in the document's other data, and
every tile is drawn through the color settings. A tile whose art reads more
than itself now also remembers the document's other data, and every tile
remembers the color settings it was drawn through (both weakly).
This commit is contained in:
bflatastic
2026-10-14 12:27:04 +07:00
parent f9b051d3d1
commit 6bc0222074
4 changed files with 80 additions and 10 deletions
+5
View File
@@ -19,6 +19,11 @@ impl<T> Shared<T> {
pub fn ptr_eq(a: &Self, b: &Self) -> bool {
Arc::ptr_eq(&a.0, &b.0)
}
/// A weak reference to the value: it keeps the allocation, so the value's address names it
/// while the reference is kept, but not the value.
pub fn downgrade(this: &Self) -> std::sync::Weak<T> {
Arc::downgrade(&this.0)
}
}
impl<T> std::ops::Deref for Shared<T> {
+35
View File
@@ -127,6 +127,41 @@ fn raster_effects_draw_through_new_color_settings() {
cms::set_active(&old).unwrap();
}
/// Pattern tiles follow the Color Settings: a fill of a pattern in a CMYK color is drawn through
/// the new working CMYK profile in the next frame, as a renderer on a new thread (with no tiles of
/// its own yet) draws it.
#[test]
fn pattern_tiles_draw_through_new_color_settings() {
use std::sync::Arc;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::pattern::{PatternDef, pattern_paint};
use vectorcraft_doc::{Appearance, Document, Node};
use vectorcraft_geom::Rect;
use vectorcraft_geom::shapes::rectangle;
let _g = settings();
let old = cms::active_settings();
let mut d = Document::new(200.0, 100.0);
let cyan = Appearance::basic(Paint::solid(Color::cmyk(1.0, 0.0, 0.0, 0.0)), Paint::None, 0.0);
let square = Node::path(d.alloc_id(), rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), cyan);
let mut def = PatternDef::new("Checks", vec![Arc::new(square)]);
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
d.patterns.push(def);
let checks = Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0);
let filled = Node::path(d.alloc_id(), rectangle(Rect::new(10.0, 10.0, 190.0, 90.0)), checks);
let layer = d.layers[0].id;
d.insert(Some(layer), 0, filled).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let frame = |r: &mut Renderer, d: &Document| r.render(d, 200, 100, Affine::IDENTITY, &opts).pixels;
let mut r = Renderer::new();
let before = frame(&mut r, &d);
session().execute("edit.colorSettings", &json!({"cmyk": cms::DEVICE_CMYK})).unwrap();
let warm = frame(&mut r, &d);
let fresh = std::thread::scope(|t| t.spawn(|| frame(&mut Renderer::new(), &d)).join().unwrap());
cms::set_active(&old).unwrap();
assert!(fresh != before, "the new profile changes the frame");
assert!(warm == fresh, "drawn as a renderer with no tiles draws it");
}
/// Live objects follow the Color Settings: in the next frame, a blend of an RGB red and a CMYK cyan,
/// the same blend in an envelope, an envelope that distorts the appearance of a CMYK cyan rectangle
/// with Revolve, and a gradient mesh in CMYK colors draw through the new working CMYK profile, and
+24 -8
View File
@@ -7,8 +7,9 @@
//! pattern name and raster scale (and ink plane, see [`crate::ink`]) within
//! [`lru::PATTERN_BUDGET`]. A tile is used while the pattern's definition is the one it was
//! rasterized from (art `Arc` identity, tile, tile type, overlap): the pattern's "revision", and,
//! when its art shows more than itself (a symbol, another pattern, an image, type), while what
//! the document holds of those is what it held then ([`Deps`]).
//! when its art shows more than itself (a symbol, another pattern, an image, type, a brush), while
//! what the document holds of those is what it held then ([`Deps`]), and while the color settings
//! are the ones it was drawn through.
//!
//! Pattern editing mode (`Document::pattern_edit`) draws only the temporary tile layer, with
//! dimmed copies of it at the neighbouring tile positions and the tile edge.
@@ -17,6 +18,7 @@ use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Weak};
use vectorcraft_color::cms::Cms;
use vectorcraft_doc::pattern::{Overlap, PatternDef, TileType};
use vectorcraft_doc::{Document, Node};
use vectorcraft_geom::{Affine, Rect, Shape};
@@ -35,13 +37,17 @@ const MAX_TILE_PX: f64 = 2048.0;
type TileKey = (String, u64);
/// What a tile was drawn from besides its pattern's own art, for art that shows more than itself:
/// the art of every pattern and symbol and the pixels of every image in the document, and the
/// fonts. Editing a symbol leaves the art of a pattern that shows it as it was, so the art alone
/// kept the tile from before (#1723). Held weakly: a weak reference keeps the allocation, so no
/// other value gets one of these addresses while the tile is kept.
/// the art of every pattern and symbol and the pixels of every image in the document, the
/// document's other data (which holds its brushes), and the fonts. Editing a symbol leaves the art
/// of a pattern that shows it as it was, so the art alone kept the tile from before (#1723). Held
/// weakly: a weak reference keeps the allocation, so no other value gets one of these addresses
/// while the tile is kept.
struct Deps {
art: Vec<Weak<Node>>,
pixels: Vec<Weak<Vec<u8>>>,
/// shortcut: any change to the document's other data draws the tile again, not only a brush
/// edit; compare the brush library alone if that ever costs a frame.
unknown: Weak<vectorcraft_brush::serde_json::Map<String, vectorcraft_brush::serde_json::Value>>,
fonts: u64,
}
@@ -58,6 +64,7 @@ impl Deps {
Self {
art: Self::art(doc).map(Arc::downgrade).collect(),
pixels: Self::pixels(doc).map(Arc::downgrade).collect(),
unknown: vectorcraft_doc::Shared::downgrade(&doc.unknown),
fonts: vectorcraft_text::FontDb::global().generation(),
}
}
@@ -65,6 +72,7 @@ impl Deps {
/// Does `doc` hold what the tile was drawn from?
fn current(&self, doc: &Document) -> bool {
self.fonts == vectorcraft_text::FontDb::global().generation()
&& std::ptr::eq(self.unknown.as_ptr(), &*doc.unknown)
&& self.art.iter().map(Weak::as_ptr).eq(Self::art(doc).map(Arc::as_ptr))
&& self.pixels.iter().map(Weak::as_ptr).eq(Self::pixels(doc).map(Arc::as_ptr))
}
@@ -74,6 +82,8 @@ struct TileEntry {
art: Vec<Arc<Node>>,
/// `None` for art that shows only itself.
deps: Option<Deps>,
/// The color settings the tile was drawn through, held weakly as [`Deps`] holds its values.
cms: Weak<Cms>,
params: (Rect, TileType, Overlap),
sx: f64,
sy: f64,
@@ -199,13 +209,19 @@ pub(crate) fn set_pattern_paint(ctx: &mut RenderContext, name: &str, xf: Affine,
let scale = raster_scale(s, (pw, ph));
let params = (def.tile, def.tile_type, def.overlap);
let key = (name.to_string(), scale.to_bits());
let cms = vectorcraft_color::cms::active();
let hit = CACHE.with(|c| {
let mut c = c.borrow_mut();
let tiles = &mut c.tiles[ink as usize];
let t = tiles.tick();
tiles
.get(&key, t)
.filter(|e| e.params == params && same_art(&e.art, &def.art) && e.deps.as_ref().is_none_or(|d| d.current(doc)))
.filter(|e| {
e.params == params
&& same_art(&e.art, &def.art)
&& std::ptr::eq(e.cms.as_ptr(), Arc::as_ptr(&cms))
&& e.deps.as_ref().is_none_or(|d| d.current(doc))
})
.map(|e| (e.pixmap.clone(), e.sx, e.sy))
});
let (pm, sx, sy) = match hit {
@@ -224,7 +240,7 @@ pub(crate) fn set_pattern_paint(ctx: &mut RenderContext, name: &str, xf: Affine,
let mut c = c.borrow_mut();
let tiles = &mut c.tiles[ink as usize];
let (t, bytes) = (tiles.tick(), size_of::<TileEntry>() + w as usize * h as usize * 4);
tiles.insert(key, TileEntry { art: def.art.clone(), deps, params, sx, sy, pixmap: pm.clone() }, bytes, t);
tiles.insert(key, TileEntry { art: def.art.clone(), deps, cms: Arc::downgrade(&cms), params, sx, sy, pixmap: pm.clone() }, bytes, t);
});
(pm, sx, sy)
}
+16 -2
View File
@@ -941,8 +941,8 @@ fn rasters_draw_pattern_edits() {
}
/// #1723: a pattern's tile is drawn again when what its art shows changes while the art itself
/// doesn't: a symbol an instance in it shows, or another pattern it is painted with. So is a
/// raster effect on an object painted with such a pattern.
/// doesn't: a symbol an instance in it shows, another pattern it is painted with, or a brush it is
/// stroked with. So is a raster effect on an object painted with such a pattern.
#[test]
fn pattern_tiles_draw_edits_to_what_their_art_shows() {
let _fonts = fonts_steady();
@@ -981,6 +981,20 @@ fn pattern_tiles_draw_edits_to_what_their_art_shows() {
edit_draws_as_new(doc("Stamps", blur()), new_symbol);
edit_draws_as_new(doc("Outer", vec![]), new_checks);
edit_draws_as_new(doc("Outer", blur()), new_checks);
// Pattern `Brushed` is a line stroked with a calligraphic brush; the brush gets wider.
let mut d = Document::new(200.0, 100.0);
let mut stroke = vectorcraft_doc::Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
stroke.stroke_mut().unwrap().brush = Some("3 pt. Round".into());
let brushed = Node::path(d.alloc_id(), vectorcraft_geom::shapes::line((2.0, 10.0).into(), (18.0, 10.0).into()), stroke);
d.patterns.push(tile("Brushed", brushed));
let painted = filled(&mut d, rectangle(Rect::new(10.0, 10.0, 190.0, 90.0)), pattern_paint("Brushed"), vec![]);
edit_draws_as_new(with_art(d, vec![painted]), |d| {
let mut lib = vectorcraft_brush::library(d);
if let vectorcraft_brush::BrushKind::Calligraphic(c) = &mut lib[0].kind {
c.size = 12.0;
}
vectorcraft_brush::store(d, &lib);
});
}
/// Raster effects are made again when a symbol shown inside another symbol changes (a group's drop