Merge pull request #1386 from dotCooCoo/feat/symbol-instance-render-cache
Render: the placed art of symbol instances and inline graphics is kept between frames
This commit is contained in:
commit
ce55aaf2fe
|
|
@ -359,6 +359,21 @@ struct GeomEntry {
|
|||
type ClipEntry = (Arc<Node>, Option<Arc<(BezPath, FillRule)>>);
|
||||
/// A clipping path (kept alive) and what it paints: its fills and its strokes ([`Node::clip_paint`]).
|
||||
type ClipPaintEntry = (Arc<Node>, Option<Arc<Node>>, Option<Arc<Node>>);
|
||||
/// What symbol art placed by [`Renderer::placed_art`] is kept under: the art's address, the bits
|
||||
/// of the transform's coefficients, whether strokes scale with it, and the stain ([`stain_bits`]).
|
||||
type PlacedKey = (usize, [u64; 6], bool, [u32; 6]);
|
||||
|
||||
/// Symbol art placed by [`Renderer::placed_art`].
|
||||
struct PlacedEntry {
|
||||
/// The symbol's art, held weakly: the reference keeps the art's allocation, so no other art can
|
||||
/// get its address while the entry exists, and does not keep the art's content.
|
||||
art: std::sync::Weak<Node>,
|
||||
/// The art placed, or `None` for art placed again in every frame ([`depends_on_document`]).
|
||||
placed: Option<Arc<Node>>,
|
||||
/// The font generation ([`vectorcraft_text::FontDb::generation`]) the art was placed in, when
|
||||
/// it holds type: the art is placed again when the fonts change.
|
||||
fonts: Option<u64>,
|
||||
}
|
||||
|
||||
/// Reusable renderer (keeps the render context, geometry and glyph caches between frames; decoded
|
||||
/// images are shared by all renderers, see [`set_image_cache_budget`]).
|
||||
|
|
@ -407,6 +422,8 @@ pub struct Renderer {
|
|||
shape_of: usize,
|
||||
/// The art drawn for objects with effects that apply through it (see `fx::has_object_fx`).
|
||||
fx_arts: FrameCache<usize, (Arc<Node>, Arc<Node>)>,
|
||||
/// Symbol art as instances and inline graphics draw it (see [`Self::placed_art`]).
|
||||
placed_arts: FrameCache<PlacedKey, PlacedEntry>,
|
||||
/// Layers open around the drawing point in the current context: a non-isolated group can copy
|
||||
/// its backdrop only when there are none (see [`Self::group`]).
|
||||
nested: u32,
|
||||
|
|
@ -518,6 +535,7 @@ impl Renderer {
|
|||
knockout: false,
|
||||
shape_of: 0,
|
||||
fx_arts: FrameCache::new(lru::FX_ART_BUDGET, 256),
|
||||
placed_arts: FrameCache::new(lru::PLACED_ART_BUDGET, 256),
|
||||
nested: 0,
|
||||
backdrop: None,
|
||||
blends: PtrMap::default(),
|
||||
|
|
@ -621,6 +639,7 @@ impl Renderer {
|
|||
self.clips.sweep(g);
|
||||
self.clip_paints.sweep(g);
|
||||
self.fx_arts.sweep(g);
|
||||
self.placed_arts.sweep(g);
|
||||
self.brushes.entries.sweep(g);
|
||||
self.live.sweep(g);
|
||||
}
|
||||
|
|
@ -1146,8 +1165,7 @@ impl Renderer {
|
|||
NodeKind::Image(im) => self.draw_image(ctx, f, im),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
if let Some(sym) = f.doc.symbols.iter().find(|s| &s.name == symbol) {
|
||||
let mut art = brush_fx::instance_art(&sym.art, n);
|
||||
art.transform(*xf, false);
|
||||
let art = self.placed_art(&sym.art, *xf, false, Some(n));
|
||||
self.draw_node(ctx, f, &art, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1481,14 +1499,46 @@ impl Renderer {
|
|||
for ig in &g.inlines {
|
||||
let Some(art) = t.runs.get(ig.run).and_then(|r| r.inline.as_ref()) else { continue };
|
||||
let Some(sym) = f.doc.symbols.iter().find(|s| s.name == art.symbol) else { continue };
|
||||
let mut node = (*sym.art).clone();
|
||||
// Strokes scale with the art (it is sized to the type, as in the SVG `<use>`).
|
||||
node.transform(t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true);
|
||||
let node = self.placed_art(&sym.art, t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true, None);
|
||||
self.draw_node(ctx, f, &node, true);
|
||||
}
|
||||
self.inline_depth -= 1;
|
||||
}
|
||||
|
||||
/// Symbol art `art` as an instance or an inline graphic draws it ([`place`]). Art that is a
|
||||
/// group or a layer without an appearance of its own is kept between frames under the art's
|
||||
/// `Arc`, `xf`, `scale_strokes` and the stain of instance `inst`: its members keep their `Arc`s
|
||||
/// from frame to frame, and the caches keyed by a node's address (geometry, glyphs, live-effect
|
||||
/// geometry) hit for them. Other art is placed again in every frame, and so is art that holds
|
||||
/// symbol instances, type with inline graphics, images, placed documents, live blends,
|
||||
/// envelopes or repeats, or raster effects ([`depends_on_document`]). Art that holds type is
|
||||
/// placed again when the fonts change.
|
||||
fn placed_art(&mut self, art: &Arc<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> Arc<Node> {
|
||||
if !matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || fx::has_object_fx(art) {
|
||||
return place(art, xf, scale_strokes, inst);
|
||||
}
|
||||
let stain = stain_bits(inst.and_then(vectorcraft_brush::stain));
|
||||
let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), scale_strokes, stain);
|
||||
if let Some(e) = self.placed_arts.get(&key, self.stamp)
|
||||
&& std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art))
|
||||
&& e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
|
||||
{
|
||||
return match &e.placed {
|
||||
Some(placed) => placed.clone(),
|
||||
None => place(art, xf, scale_strokes, inst),
|
||||
};
|
||||
}
|
||||
let placed = place(art, xf, scale_strokes, inst);
|
||||
let kept = (!depends_on_document(art)).then(|| placed.clone());
|
||||
let fonts = (kept.is_some() && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation());
|
||||
// The bytes count the key and the entry, the art's allocation that the weak reference
|
||||
// keeps, and the kept art.
|
||||
let bytes = size_of::<(PlacedKey, PlacedEntry, Node)>() + kept.as_deref().map_or(0, lru::node_bytes);
|
||||
self.placed_arts.insert(key, PlacedEntry { art: Arc::downgrade(art), placed: kept, fonts }, bytes, self.stamp);
|
||||
placed
|
||||
}
|
||||
|
||||
/// Cached glyph geometry for a text node (keyed by Arc identity like paths).
|
||||
fn text_geom_of(&mut self, a: &Arc<Node>, t: &TextObject) -> Arc<TextGeom> {
|
||||
let fonts = vectorcraft_text::FontDb::global().generation();
|
||||
|
|
@ -1854,6 +1904,61 @@ fn painted_items(n: &Node) -> usize {
|
|||
.max(if n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !matches!(f.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None)) || matches!(i, AppearanceItem::Stroke(s) if s.visible && !matches!(s.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None))) { 2 } else { 0 })
|
||||
}
|
||||
|
||||
/// Symbol art `art` stained by instance `inst` ([`instance_art`]) and placed by `xf`, with
|
||||
/// strokes scaled by `xf` when `scale_strokes`.
|
||||
fn place(art: &Node, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> Arc<Node> {
|
||||
let mut placed = match inst {
|
||||
Some(i) => brush_fx::instance_art(art, i),
|
||||
None => art.clone(),
|
||||
};
|
||||
placed.transform(xf, scale_strokes);
|
||||
Arc::new(placed)
|
||||
}
|
||||
|
||||
/// Does `n` hold, in it or in the opacity masks of the objects in it, a symbol instance, type
|
||||
/// with an inline graphic, an image, a placed document, a blend, an envelope, a repeat, or an
|
||||
/// object or a fill or stroke with a visible raster effect?
|
||||
fn depends_on_document(n: &Node) -> bool {
|
||||
let raster = |fx: &[vectorcraft_doc::Effect]| fx.iter().any(|e| e.visible && effects::is_raster(&e.id));
|
||||
let mut depends = false;
|
||||
n.walk(&mut |c| {
|
||||
depends |= match &c.kind {
|
||||
NodeKind::SymbolInstance { .. }
|
||||
| NodeKind::Image(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Repeat(_) => true,
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| r.inline.is_some()),
|
||||
_ => false,
|
||||
} || raster(&c.appearance.effects)
|
||||
|| c.appearance.items.iter().any(|i| raster(i.effects()))
|
||||
|| c.extras().mask.as_ref().is_some_and(|m| depends_on_document(&m.art));
|
||||
});
|
||||
depends
|
||||
}
|
||||
|
||||
/// Does `n` hold type, in it or in the opacity masks of the objects in it?
|
||||
fn holds_type(n: &Node) -> bool {
|
||||
let mut holds = false;
|
||||
n.walk(&mut |c| holds |= matches!(c.kind, NodeKind::Text(_)) || c.extras().mask.as_ref().is_some_and(|m| holds_type(&m.art)));
|
||||
holds
|
||||
}
|
||||
|
||||
/// A symbol instance's stain ([`vectorcraft_brush::stain`]) as bits for a cache key: its color
|
||||
/// model, the color's components and the amount (all 0 without a stain).
|
||||
fn stain_bits(stain: Option<(vectorcraft_color::Color, f32)>) -> [u32; 6] {
|
||||
use vectorcraft_color::Color;
|
||||
let Some((color, amount)) = stain else { return [0; 6] };
|
||||
let (model, [p, q, r, s]) = match color {
|
||||
Color::Rgb { r, g, b } => (1, [r, g, b, 0.0]),
|
||||
Color::Cmyk { c, m, y, k } => (2, [c, m, y, k]),
|
||||
Color::Gray { k } => (3, [k, 0.0, 0.0, 0.0]),
|
||||
Color::Lab { l, a, b } => (4, [l, a, b, 0.0]),
|
||||
};
|
||||
[model, p.to_bits(), q.to_bits(), r.to_bits(), s.to_bits(), amount.to_bits()]
|
||||
}
|
||||
|
||||
/// Preferred rasterizer thread count set by the app (Preferences → Performance); negative = automatic.
|
||||
static THREADS_OVERRIDE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ pub(crate) const TEXT_BUDGET: usize = 128 << 20;
|
|||
pub(crate) const CLIP_BUDGET: usize = 32 << 20;
|
||||
/// The art drawn for objects with effects that apply through it (`Renderer::fx_art`).
|
||||
pub(crate) const FX_ART_BUDGET: usize = 64 << 20;
|
||||
/// Symbol art placed for instances and inline graphics (`Renderer::placed_art`).
|
||||
pub(crate) const PLACED_ART_BUDGET: usize = 64 << 20;
|
||||
/// Brush art per brushed stroke (`Renderer::draw_brush`).
|
||||
pub(crate) const BRUSH_BUDGET: usize = 64 << 20;
|
||||
/// Evaluated blends, envelopes and repeats (`Renderer::live_expanded`).
|
||||
|
|
|
|||
|
|
@ -39,3 +39,25 @@ fn type_redraws_when_its_font_becomes_available() {
|
|||
assert_ne!(before, after, "drawn in the font that arrived");
|
||||
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
|
||||
}
|
||||
|
||||
/// Type with a geometry effect in a symbol's art redraws in a font that becomes available: the art
|
||||
/// kept for the instance is placed again when the fonts change.
|
||||
#[test]
|
||||
fn symbol_type_redraws_when_its_font_becomes_available() {
|
||||
const FAMILY: &str = "Placed Serif 4";
|
||||
let mut d = Document::new(300.0, 150.0);
|
||||
let style = CharStyle { size: 60.0, font_family: FAMILY.into(), ..Default::default() };
|
||||
let mut t = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(-140.0, 25.0), "Hamburg", style))));
|
||||
t.appearance.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 0.0}), visible: true });
|
||||
let art = Node::group(d.alloc_id(), vec![Arc::new(t)]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Word".into(), art: Arc::new(art) });
|
||||
let (l, id) = (d.layers[0].id, d.alloc_id());
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Word".into(), xf: Affine::translate((150.0, 75.0)) })).unwrap();
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
|
||||
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(FAMILY)) > 0);
|
||||
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
|
||||
assert_ne!(before, after, "drawn in the font that arrived");
|
||||
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! What renderers keep between frames: decoded images shared by all of them, image mips, and
|
||||
//! caches swept by every kind of render.
|
||||
//! What renderers keep between frames: decoded images shared by all of them, image mips, symbol
|
||||
//! art placed for instances and inline graphics, and caches swept by every kind of render.
|
||||
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind};
|
||||
|
|
@ -174,6 +174,309 @@ fn swept_type_entries_give_back_their_bytes() {
|
|||
assert_eq!((r.texts.len(), r.texts.bytes()), (0, 0));
|
||||
}
|
||||
|
||||
/// A symbol's art inside the 20 × 20 square around the origin: a group of a square filled blue
|
||||
/// with a 2 pt black stroke and a black line.
|
||||
fn symbol_art(d: &mut Document) -> Node {
|
||||
use vectorcraft_doc::Appearance;
|
||||
let square = vectorcraft_geom::shapes::rectangle(Rect::new(-8.0, -8.0, 8.0, 8.0));
|
||||
let square = Node::path(d.alloc_id(), square, Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::solid(Color::BLACK), 2.0));
|
||||
let line = vectorcraft_geom::shapes::line((-8.0, 4.0).into(), (8.0, 4.0).into());
|
||||
let line = Node::path(d.alloc_id(), line, Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.5));
|
||||
Node::group(d.alloc_id(), vec![Arc::new(square), Arc::new(line)])
|
||||
}
|
||||
|
||||
/// Three instances of symbol `S` ([`symbol_art`]): two in a row, the second stained red, and a
|
||||
/// third turned and scaled by 1.5.
|
||||
fn symbol_doc() -> Document {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = symbol_art(&mut d);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
|
||||
let l = d.layers[0].id;
|
||||
let xfs = [
|
||||
Affine::translate((30.0, 30.0)),
|
||||
Affine::translate((80.0, 30.0)),
|
||||
Affine::translate((140.0, 60.0)) * Affine::rotate(0.5) * Affine::scale(1.5),
|
||||
];
|
||||
for (i, xf) in xfs.into_iter().enumerate() {
|
||||
let mut inst = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf });
|
||||
if i == 1 {
|
||||
inst.appearance.set_fill(Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
|
||||
}
|
||||
d.insert(Some(l), i, inst).unwrap();
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// The addresses of the nodes `r` keeps geometry for that are not the own nodes of any version
|
||||
/// of the document drawn (`docs`): the symbol art it placed for instances and inline graphics.
|
||||
fn placed_copies(r: &Renderer, docs: &[&Document]) -> std::collections::BTreeSet<usize> {
|
||||
let mut own = std::collections::HashSet::new();
|
||||
for d in docs {
|
||||
d.walk(|n| {
|
||||
own.insert(n as *const Node as usize);
|
||||
});
|
||||
}
|
||||
r.geom.values().map(|e| Arc::as_ptr(&e.node) as usize).filter(|p| !own.contains(p)).collect()
|
||||
}
|
||||
|
||||
/// A second frame of an unchanged document draws the symbol art placed for its instances in the
|
||||
/// first: it copies no art, and the geometry kept for the art's paths is used again. The renderer
|
||||
/// holds the symbol's art itself only weakly.
|
||||
#[test]
|
||||
fn symbol_instances_keep_their_placed_art_between_frames() {
|
||||
let d = symbol_doc();
|
||||
let opts = RenderOptions::default();
|
||||
let mut r = Renderer::new();
|
||||
let first = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let placed = placed_copies(&r, &[&d]);
|
||||
assert_eq!(placed.len(), 6, "the square and the line of three instances");
|
||||
assert!(r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == first);
|
||||
assert_eq!(placed_copies(&r, &[&d]), placed, "the second frame draws the art the first placed");
|
||||
assert_eq!(Arc::strong_count(&d.symbols[0].art), 1, "only the document holds the symbol's art");
|
||||
}
|
||||
|
||||
/// Moving an instance places its art again, and editing a symbol's art places the art of its
|
||||
/// instances again: the other instances keep theirs, and each frame draws what a new renderer
|
||||
/// draws.
|
||||
#[test]
|
||||
fn editing_an_instance_or_a_symbol_places_only_its_art_again() {
|
||||
let mut d = symbol_doc();
|
||||
// Symbol `T` is a green square, and one instance of it stands below the others.
|
||||
let green = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(0.0, 1.0, 0.0)), Paint::None, 0.0);
|
||||
let square = Node::path(d.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(-5.0, -5.0, 5.0, 5.0)), green);
|
||||
let t = Node::group(d.alloc_id(), vec![Arc::new(square)]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "T".into(), art: Arc::new(t) });
|
||||
let (l, id) = (d.layers[0].id, d.alloc_id());
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "T".into(), xf: Affine::translate((30.0, 80.0)) })).unwrap();
|
||||
let opts = RenderOptions::default();
|
||||
let frame = |r: &mut Renderer, d: &Document| r.render(d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let mut r = Renderer::new();
|
||||
frame(&mut r, &d);
|
||||
let before = placed_copies(&r, &[&d]);
|
||||
assert_eq!(before.len(), 7);
|
||||
// The first instance of `S` moves 5 pt right.
|
||||
let unmoved = d.clone();
|
||||
let first = d.layers[0].children().unwrap()[1].id;
|
||||
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(first).map(|n| &mut n.kind) {
|
||||
*xf = Affine::translate((5.0, 0.0)) * *xf;
|
||||
}
|
||||
assert!(frame(&mut r, &d) == frame(&mut Renderer::new(), &d));
|
||||
let moved = placed_copies(&r, &[&unmoved, &d]);
|
||||
assert!(before.is_subset(&moved) && moved.len() == before.len() + 2, "{} copies, then {}", before.len(), moved.len());
|
||||
// `S`'s square is repainted red: its instances show it, and `T`'s instance keeps its art.
|
||||
let mut art = (*d.symbols[0].art).clone();
|
||||
if let Some(square) = art.children_mut().and_then(|c| c.first_mut()) {
|
||||
Arc::make_mut(square).appearance.set_fill(Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
|
||||
}
|
||||
d.symbols[0].art = Arc::new(art);
|
||||
let red = frame(&mut r, &d);
|
||||
assert!(red == frame(&mut Renderer::new(), &d));
|
||||
let img = Rendered { width: 200, height: 100, pixels: red };
|
||||
assert_eq!(img.pixel(140, 60), [255, 0, 0, 255], "the turned instance's square");
|
||||
let edited = placed_copies(&r, &[&unmoved, &d]);
|
||||
assert!(moved.is_subset(&edited) && edited.len() == moved.len() + 6, "{} copies, then {}", moved.len(), edited.len());
|
||||
}
|
||||
|
||||
/// Draws `d` (200 × 100 points, on white), applies `edit` and draws it again with the same
|
||||
/// renderer: the frame changed, a new renderer draws the same, and only the document holds the
|
||||
/// symbols' art.
|
||||
fn edit_draws_as_new(mut d: Document, edit: impl FnOnce(&mut Document)) {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
let before = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
edit(&mut d);
|
||||
let after = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
assert!(after != before, "the edit changes the frame");
|
||||
assert!(after == Renderer::new().render(&d, 200, 100, Affine::IDENTITY, &opts).pixels, "a new renderer draws the same");
|
||||
assert!(d.symbols.iter().all(|s| Arc::strong_count(&s.art) == 1), "only the document holds the symbols' art");
|
||||
}
|
||||
|
||||
/// `d` with symbol `Holder` (a group of `members`) shown by one instance at (100, 50).
|
||||
fn holder_doc(mut d: Document, members: Vec<Node>) -> Document {
|
||||
let holder = Node::group(d.alloc_id(), members.into_iter().map(Arc::new).collect());
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Holder".into(), art: Arc::new(holder) });
|
||||
let (l, id) = (d.layers[0].id, d.alloc_id());
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Holder".into(), xf: Affine::translate((100.0, 50.0)) })).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// Symbol art that shows another symbol (type with an inline graphic of it, an instance of it with
|
||||
/// a Transform effect) is placed again in every frame: when that symbol's art changes, the next
|
||||
/// frame draws what a new renderer draws.
|
||||
#[test]
|
||||
fn art_showing_another_symbol_draws_its_edits() {
|
||||
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = symbol_art(&mut d);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
|
||||
let style = CharStyle { size: 24.0, ..Default::default() };
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(-60.0, 10.0), "A", style.clone());
|
||||
t.runs.push(TextRun::inline(InlineArt::new("S"), style.clone()));
|
||||
t.runs.push(TextRun::new("B", style));
|
||||
let text = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let mut moved = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((40.0, 0.0)) });
|
||||
moved.appearance.effects.push(vectorcraft_doc::Effect {
|
||||
id: "distort.transform".into(),
|
||||
params: serde_json::json!({"moveH": 4.0}),
|
||||
visible: true,
|
||||
});
|
||||
let d = holder_doc(d, vec![text, moved]);
|
||||
// `S` becomes a wide red bar: the inline graphic grows, and the type after it moves.
|
||||
edit_draws_as_new(d, |d| {
|
||||
let bar = vectorcraft_geom::shapes::rectangle(Rect::new(-20.0, -4.0, 20.0, 4.0));
|
||||
let red = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
d.symbols[0].art = Arc::new(Node::path(d.alloc_id(), bar, red));
|
||||
});
|
||||
}
|
||||
|
||||
/// Symbol art with raster effects is placed again in every frame: when a pattern under a blur or
|
||||
/// the document's raster resolution under De-Interlace changes, the next frame draws what a new
|
||||
/// renderer draws.
|
||||
#[test]
|
||||
fn art_with_raster_effects_draws_pattern_and_resolution_edits() {
|
||||
use vectorcraft_doc::pattern::{PatternDef, pattern_paint};
|
||||
let effect = |id: &str, params: serde_json::Value| vectorcraft_doc::Effect { id: id.into(), params, visible: true };
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
d.raster_effects_ppi = 72.0;
|
||||
// Pattern `Checks` is a red 10 × 10 square in a 20 × 20 tile.
|
||||
let tile = |c: Color| {
|
||||
let square = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0));
|
||||
vec![Arc::new(Node::path(NodeId(900), square, vectorcraft_doc::Appearance::basic(Paint::solid(c), Paint::None, 0.0)))]
|
||||
};
|
||||
let mut def = PatternDef::new("Checks", tile(Color::rgb(1.0, 0.0, 0.0)));
|
||||
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
|
||||
d.patterns.push(def);
|
||||
let band = vectorcraft_geom::shapes::rectangle(Rect::new(-90.0, -45.0, -20.0, -25.0));
|
||||
let mut blurred = Node::path(d.alloc_id(), band, vectorcraft_doc::Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0));
|
||||
blurred.appearance.effects.push(effect("blur.gaussian", serde_json::json!({"radius": 2.0})));
|
||||
// The path holds ten bars, 1 pt high and 2 pt apart.
|
||||
let mut bars = vectorcraft_geom::shapes::rectangle(Rect::new(20.0, -40.0, 90.0, -39.0));
|
||||
for k in 1..10 {
|
||||
let y = -40.0 + 2.0 * k as f64;
|
||||
bars.subpaths.extend(vectorcraft_geom::shapes::rectangle(Rect::new(20.0, y, 90.0, y + 1.0)).subpaths);
|
||||
}
|
||||
let mut striped = Node::path(d.alloc_id(), bars, vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
|
||||
striped.appearance.effects.push(effect("video.deinterlace", serde_json::json!({})));
|
||||
let d = holder_doc(d, vec![blurred, striped]);
|
||||
edit_draws_as_new(d, |d| {
|
||||
if let Some(def) = d.pattern_mut("Checks") {
|
||||
def.art = tile(Color::rgb(0.0, 0.0, 1.0));
|
||||
}
|
||||
d.raster_effects_ppi = 24.0;
|
||||
});
|
||||
}
|
||||
|
||||
/// Symbol art with a live object (an envelope that distorts a pattern fill) is placed again in
|
||||
/// every frame: when the pattern's tile type changes, the next frame draws what a new renderer
|
||||
/// draws.
|
||||
#[test]
|
||||
fn art_with_live_objects_draws_pattern_layout_edits() {
|
||||
use vectorcraft_doc::live::EnvelopeOptions;
|
||||
use vectorcraft_doc::pattern::{PatternDef, TileType, pattern_paint};
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
// Pattern `Checks` is a red 10 × 10 square in a 20 × 20 tile.
|
||||
let square = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0));
|
||||
let red = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
let mut def = PatternDef::new("Checks", vec![Arc::new(Node::path(NodeId(900), square, red))]);
|
||||
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
|
||||
d.patterns.push(def);
|
||||
let band = vectorcraft_geom::shapes::rectangle(Rect::new(-80.0, -40.0, 80.0, 40.0));
|
||||
let filled = Node::path(d.alloc_id(), band, vectorcraft_doc::Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0));
|
||||
let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
|
||||
let options = EnvelopeOptions { distort_appearance: true, distort_pattern_fills: true, ..Default::default() };
|
||||
let envelope = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Envelope {
|
||||
content: vec![Arc::new(filled)],
|
||||
kind: Box::new(kind),
|
||||
fidelity: 50.0,
|
||||
editing: false,
|
||||
options,
|
||||
frame: Box::new(Affine::IDENTITY),
|
||||
},
|
||||
);
|
||||
let d = holder_doc(d, vec![envelope]);
|
||||
edit_draws_as_new(d, |d| {
|
||||
if let Some(def) = d.pattern_mut("Checks") {
|
||||
def.tile_type = TileType::BrickByRow { offset: 0.5 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Symbol instances draw what groups of their art, stained and placed, draw: in every frame, when
|
||||
/// two instances placed alike are stained differently, when the art is a single path, and with a
|
||||
/// cache too small for all of the art. Art no longer drawn leaves the cache after a few frames.
|
||||
#[test]
|
||||
fn symbol_instances_draw_as_groups_of_their_placed_art() {
|
||||
let mut d = symbol_doc();
|
||||
let l = d.layers[0].id;
|
||||
// An instance placed like the first one lies over it, stained green.
|
||||
let mut over = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((30.0, 30.0)) });
|
||||
over.appearance.set_fill(Paint::solid(Color::rgb(0.0, 1.0, 0.0)));
|
||||
d.insert(Some(l), 1, over).unwrap();
|
||||
// Symbol `U` is a single path, a circle with a black stroke.
|
||||
let circle = vectorcraft_geom::shapes::ellipse(Rect::new(-6.0, -6.0, 6.0, 6.0));
|
||||
let ap = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.8, 0.0)), Paint::solid(Color::BLACK), 1.0);
|
||||
let circle = Node::path(d.alloc_id(), circle, ap);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "U".into(), art: Arc::new(circle) });
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: "U".into(), xf: Affine::translate((175.0, 25.0)) })).unwrap();
|
||||
// `groups` holds the same art in place of the instances: groups, and the circle as a path.
|
||||
let mut groups = d.clone();
|
||||
let placed = d.layers[0].children().unwrap().iter().map(|c| match &c.kind {
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
let sym = d.symbols.iter().find(|s| s.name == *symbol).unwrap();
|
||||
let mut art = instance_art(&sym.art, c);
|
||||
art.transform(*xf, false);
|
||||
Arc::new(art)
|
||||
}
|
||||
_ => c.clone(),
|
||||
});
|
||||
*groups.children_mut(Some(l)).unwrap() = placed.collect();
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let want = Renderer::new().render(&groups, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let mut r = Renderer::new();
|
||||
r.placed_arts = FrameCache::new(lru::PLACED_ART_BUDGET, 0);
|
||||
for frame in 0..3 {
|
||||
assert!(r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == want, "frame {frame}");
|
||||
}
|
||||
// A cache that holds half the bytes of that art keeps what fits and places the rest every frame.
|
||||
let all = r.placed_arts.bytes();
|
||||
let mut small = Renderer::new();
|
||||
small.placed_arts = FrameCache::new(all / 2, 0);
|
||||
for frame in 0..3 {
|
||||
assert!(small.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == want, "frame {frame} over the budget");
|
||||
assert!(!small.placed_arts.is_empty() && small.placed_arts.bytes() <= all / 2, "{} of {all} bytes", small.placed_arts.bytes());
|
||||
}
|
||||
let empty = Document::new(10.0, 10.0);
|
||||
for _ in 0..5 {
|
||||
r.render(&empty, 10, 10, Affine::IDENTITY, &opts);
|
||||
}
|
||||
assert_eq!((r.placed_arts.len(), r.placed_arts.bytes()), (0, 0));
|
||||
}
|
||||
|
||||
/// Inline graphics in type draw the art placed for them in an earlier frame, as instances do.
|
||||
#[test]
|
||||
fn inline_graphics_keep_their_placed_art_between_frames() {
|
||||
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = symbol_art(&mut d);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
|
||||
let style = CharStyle { size: 24.0, ..Default::default() };
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(10.0, 50.0), "A", style.clone());
|
||||
t.runs.push(TextRun::inline(InlineArt::new("S"), style.clone()));
|
||||
t.runs.push(TextRun::new("B", style));
|
||||
let (l, id) = (d.layers[0].id, d.alloc_id());
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap();
|
||||
let opts = RenderOptions::default();
|
||||
let mut r = Renderer::new();
|
||||
let first = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let placed = placed_copies(&r, &[&d]);
|
||||
assert_eq!(placed.len(), 2, "the square and the line of the inline graphic");
|
||||
assert!(r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == first);
|
||||
assert_eq!(placed_copies(&r, &[&d]), placed, "the second frame draws the art the first placed");
|
||||
}
|
||||
|
||||
/// Each run draws exactly the outlines it drew when kept as a path of its own: its glyphs and
|
||||
/// decorations in layout order (the whole text's path is kept once, the runs as spans of it).
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue