mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3461 from lehni/update/canvas-partial-redraw
Canvas: an edit redraws only the area it changed
This commit is contained in:
@@ -935,6 +935,136 @@ type Places = BTreeMap<Vec<usize>, Vec<(usize, usize)>>;
|
||||
pub const FORMAT_VERSION: u32 = 1;
|
||||
|
||||
impl Document {
|
||||
/// Is everything but the layers the same in `other`, as the next version of the document after
|
||||
/// an edit of its art? The id counter and the id index are left out, and art outside the layers
|
||||
/// (symbols, patterns, graph designs) compares by identity, which an edit keeps for what it
|
||||
/// doesn't change: quick for every frame of an animation.
|
||||
pub fn same_outside_layers(&self, other: &Self) -> bool {
|
||||
let same_art = |a: &[Arc<Node>], b: &[Arc<Node>]| a.len() == b.len() && a.iter().zip(b).all(|(a, b)| Arc::ptr_eq(a, b));
|
||||
let same_pattern = |a: &PatternDef, b: &PatternDef| {
|
||||
same_art(&a.art, &b.art) && PatternDef { art: Vec::new(), ..a.clone() } == PatternDef { art: Vec::new(), ..b.clone() }
|
||||
};
|
||||
let Self {
|
||||
version,
|
||||
title,
|
||||
template,
|
||||
units,
|
||||
ruler_origin,
|
||||
rulers,
|
||||
artboard_ruler_origins,
|
||||
color_mode,
|
||||
artboards,
|
||||
layers: _,
|
||||
swatches,
|
||||
swatch_groups,
|
||||
graphic_styles,
|
||||
char_styles,
|
||||
para_styles,
|
||||
text_threads,
|
||||
symbols,
|
||||
graph_designs,
|
||||
guides,
|
||||
views,
|
||||
saved_selections,
|
||||
variables,
|
||||
grid,
|
||||
raster_effects_ppi,
|
||||
images,
|
||||
patterns,
|
||||
pattern_edit,
|
||||
mask_edit,
|
||||
next_id: _,
|
||||
unknown,
|
||||
page_isolate,
|
||||
page_knockout,
|
||||
spot_use_lab,
|
||||
setup,
|
||||
paste_remembers_layers,
|
||||
metadata,
|
||||
raster_effects,
|
||||
last_view,
|
||||
transparency_grid,
|
||||
layers_open,
|
||||
color_profiles,
|
||||
export_settings,
|
||||
extra,
|
||||
slices,
|
||||
slices_clip_to_artboard,
|
||||
print_setup,
|
||||
assets,
|
||||
svg_scripts,
|
||||
puppet,
|
||||
new_art,
|
||||
dimensions,
|
||||
index: _,
|
||||
} = self;
|
||||
*version == other.version
|
||||
&& *title == other.title
|
||||
&& *template == other.template
|
||||
&& *units == other.units
|
||||
&& *ruler_origin == other.ruler_origin
|
||||
&& *rulers == other.rulers
|
||||
&& *artboard_ruler_origins == other.artboard_ruler_origins
|
||||
&& *color_mode == other.color_mode
|
||||
&& *artboards == other.artboards
|
||||
&& *swatches == other.swatches
|
||||
&& *swatch_groups == other.swatch_groups
|
||||
&& *graphic_styles == other.graphic_styles
|
||||
&& *char_styles == other.char_styles
|
||||
&& *para_styles == other.para_styles
|
||||
&& *text_threads == other.text_threads
|
||||
&& symbols.len() == other.symbols.len()
|
||||
&& symbols.iter().zip(&other.symbols).all(|(a, b)| a.name == b.name && Arc::ptr_eq(&a.art, &b.art))
|
||||
&& graph_designs.len() == other.graph_designs.len()
|
||||
&& graph_designs.iter().zip(&other.graph_designs).all(|(a, b)| a.name == b.name && Arc::ptr_eq(&a.art, &b.art))
|
||||
&& *guides == other.guides
|
||||
&& *views == other.views
|
||||
&& *saved_selections == other.saved_selections
|
||||
&& *variables == other.variables
|
||||
&& *grid == other.grid
|
||||
&& *raster_effects_ppi == other.raster_effects_ppi
|
||||
&& *images == other.images
|
||||
&& patterns.len() == other.patterns.len()
|
||||
&& patterns.iter().zip(&other.patterns).all(|(a, b)| same_pattern(a, b))
|
||||
&& match (pattern_edit, &other.pattern_edit) {
|
||||
(Some(a), Some(b)) => {
|
||||
a.pattern == b.pattern
|
||||
&& a.layer == b.layer
|
||||
&& match (&a.original, &b.original) {
|
||||
(Some(a), Some(b)) => same_pattern(a, b),
|
||||
(a, b) => a.is_none() && b.is_none(),
|
||||
}
|
||||
}
|
||||
(a, b) => a.is_none() && b.is_none(),
|
||||
}
|
||||
&& *mask_edit == other.mask_edit
|
||||
&& *unknown == other.unknown
|
||||
&& *page_isolate == other.page_isolate
|
||||
&& *page_knockout == other.page_knockout
|
||||
&& *spot_use_lab == other.spot_use_lab
|
||||
&& *setup == other.setup
|
||||
&& *paste_remembers_layers == other.paste_remembers_layers
|
||||
&& *metadata == other.metadata
|
||||
&& *raster_effects == other.raster_effects
|
||||
&& *last_view == other.last_view
|
||||
&& *transparency_grid == other.transparency_grid
|
||||
&& *layers_open == other.layers_open
|
||||
&& *color_profiles == other.color_profiles
|
||||
&& *export_settings == other.export_settings
|
||||
&& *extra == other.extra
|
||||
&& *slices == other.slices
|
||||
&& *slices_clip_to_artboard == other.slices_clip_to_artboard
|
||||
&& *print_setup == other.print_setup
|
||||
&& *assets == other.assets
|
||||
&& *svg_scripts == other.svg_scripts
|
||||
&& match (puppet, &other.puppet) {
|
||||
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
|
||||
(a, b) => a.is_none() && b.is_none(),
|
||||
}
|
||||
&& *new_art == other.new_art
|
||||
&& *dimensions == other.dimensions
|
||||
}
|
||||
|
||||
/// Document Raster Effects Settings › Resolution in ppi, 1 to 2400 (72 when the stored value
|
||||
/// isn't a number).
|
||||
pub fn raster_ppi(&self) -> f64 {
|
||||
|
||||
@@ -1301,10 +1301,8 @@ impl Node {
|
||||
self.projected(Some(b)).map(|b| b.inflate(o, o))
|
||||
}
|
||||
NodeKind::Text(t) => {
|
||||
// The characters' strokes paint around the glyphs, scaled with the type.
|
||||
let k = Scaling::factor(t.xf).unwrap_or(1.0);
|
||||
let chars = t.runs.iter().map(|r| r.style.stroke_outset()).fold(0.0, f64::max) * k;
|
||||
let o = self.appearance.outset().max(chars);
|
||||
// The characters' strokes paint around the glyphs.
|
||||
let o = self.appearance.outset().max(t.char_stroke_outset());
|
||||
self.geometric(symbols).map(|b| b.inflate(o, o))
|
||||
}
|
||||
// An image is measured by its frame, without its own fills and strokes.
|
||||
|
||||
@@ -1710,6 +1710,12 @@ impl TextObject {
|
||||
TextKind::Point => Some(self.xf.transform_rect_bbox(self.cached_bounds.unwrap_or_else(|| self.estimate_bounds()))),
|
||||
}
|
||||
}
|
||||
/// How far the characters' strokes paint past their glyphs, in document space: they scale
|
||||
/// with the type.
|
||||
pub fn char_stroke_outset(&self) -> f64 {
|
||||
let k = crate::node::Scaling::factor(self.xf).unwrap_or(1.0);
|
||||
self.runs.iter().map(|r| r.style.stroke_outset()).fold(0.0, f64::max) * k
|
||||
}
|
||||
pub fn transform(&mut self, a: Affine) {
|
||||
self.xf = a * self.xf;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//! What draws differently after an edit, so a view can draw again only there.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{Document, Node, NodeKind, SymbolBoxes};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
/// What may draw differently in a document than in its version before an edit ([`changed`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Changed {
|
||||
Nothing,
|
||||
/// This area of the document, outside of which every pixel draws as before.
|
||||
Area(Rect),
|
||||
Everything,
|
||||
}
|
||||
|
||||
/// What may draw differently in `new` than in `old`, the version before an edit: the bounds of
|
||||
/// everything the objects that changed paint, before and after the edit ([`crate::painted_bounds`]),
|
||||
/// or everything when more than the art in the layers changed, or while a pattern is edited (its
|
||||
/// art also draws in the copies round it). Objects that an edit doesn't touch
|
||||
/// keep their identity, so this costs what changed, not the size of the document.
|
||||
pub fn changed(old: &Document, new: &Document) -> Changed {
|
||||
if new.pattern_edit.is_some() || !old.same_outside_layers(new) {
|
||||
return Changed::Everything;
|
||||
}
|
||||
let mut diff = Diff { old: old.symbol_boxes(), new: new.symbol_boxes(), area: None };
|
||||
diff.children(&old.layers, &new.layers);
|
||||
diff.area.map_or(Changed::Nothing, Changed::Area)
|
||||
}
|
||||
|
||||
/// The pixels of a `w`×`h` render through `view` that drawing `area` again covers, as x0, y0, x1,
|
||||
/// y1: its box in the view, grown by a pixel for the anti-aliased edges. None when it covers none.
|
||||
pub fn view_pixels(area: Rect, view: Affine, w: u32, h: u32) -> Option<[u32; 4]> {
|
||||
let r = view.transform_rect_bbox(area).inflate(1.0, 1.0);
|
||||
if ![r.x0, r.y0, r.x1, r.y1].iter().all(|c| c.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let (x0, y0) = (r.x0.floor().max(0.0), r.y0.floor().max(0.0));
|
||||
let (x1, y1) = (r.x1.ceil().min(f64::from(w)), r.y1.ceil().min(f64::from(h)));
|
||||
(x1 > x0 && y1 > y0).then_some([x0 as u32, y0 as u32, x1 as u32, y1 as u32])
|
||||
}
|
||||
|
||||
/// [`changed`] in progress: the symbols' boxes of both versions, and the area so far.
|
||||
struct Diff<'a> {
|
||||
old: SymbolBoxes<'a>,
|
||||
new: SymbolBoxes<'a>,
|
||||
area: Option<Rect>,
|
||||
}
|
||||
|
||||
impl Diff<'_> {
|
||||
/// Members in the same order compare one by one. Members added, removed or restacked count
|
||||
/// before and after, except those that kept their place unchanged.
|
||||
fn children(&mut self, old: &[Arc<Node>], new: &[Arc<Node>]) {
|
||||
if old.len() == new.len() && old.iter().zip(new).all(|(a, b)| a.id == b.id) {
|
||||
for (a, b) in old.iter().zip(new) {
|
||||
self.node(a, b);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for i in 0..old.len().max(new.len()) {
|
||||
match (old.get(i), new.get(i)) {
|
||||
(Some(a), Some(b)) if Arc::ptr_eq(a, b) => {}
|
||||
(a, b) => {
|
||||
self.add_old(a.map(|a| &**a));
|
||||
self.add_new(b.map(|b| &**b));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A group or layer that only its members changed in, without a clipping mask or effects of
|
||||
/// its own that draw from all of them, changed where they did. Any other object that changed
|
||||
/// counts whole, before and after.
|
||||
fn node(&mut self, old: &Arc<Node>, new: &Arc<Node>) {
|
||||
if Arc::ptr_eq(old, new) {
|
||||
return;
|
||||
}
|
||||
if let (Some(a), Some(b)) = (members(old), members(new))
|
||||
&& same_but_members(old, new)
|
||||
{
|
||||
self.children(a, b);
|
||||
} else {
|
||||
self.add_old(Some(old));
|
||||
self.add_new(Some(new));
|
||||
}
|
||||
}
|
||||
|
||||
fn add_old(&mut self, n: Option<&Node>) {
|
||||
self.add(n.and_then(|n| crate::brush_fx::cull_bounds(&self.old, n)));
|
||||
}
|
||||
|
||||
fn add_new(&mut self, n: Option<&Node>) {
|
||||
self.add(n.and_then(|n| crate::brush_fx::cull_bounds(&self.new, n)));
|
||||
}
|
||||
|
||||
fn add(&mut self, r: Option<Rect>) {
|
||||
self.area = vectorcraft_geom::union_opt(self.area, r);
|
||||
}
|
||||
}
|
||||
|
||||
/// The members of a group or layer whose members draw each on their own: no clipping mask, and no
|
||||
/// effects of its own.
|
||||
fn members(n: &Node) -> Option<&[Arc<Node>]> {
|
||||
match &n.kind {
|
||||
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } if !crate::fx::has_object_fx(n) => Some(children),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Are `a` and `b` the same but for their members?
|
||||
fn same_but_members(a: &Node, b: &Node) -> bool {
|
||||
let strip = |n: &Node| {
|
||||
let mut n = n.clone();
|
||||
if let Some(children) = n.children_mut() {
|
||||
children.clear();
|
||||
}
|
||||
n
|
||||
};
|
||||
strip(a) == strip(b)
|
||||
}
|
||||
+45
-12
@@ -478,12 +478,25 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Optio
|
||||
}
|
||||
_ => {
|
||||
let b = n.visual_bounds_with(symbols)?;
|
||||
let b = match &n.kind {
|
||||
NodeKind::Text(t) => vectorcraft_geom::union_opt(Some(b), glyphs_box(t, n.appearance.outset())).unwrap_or(b),
|
||||
_ => b,
|
||||
};
|
||||
let a = live_content_reach(symbols, n);
|
||||
Some(b.inflate(a, a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The box of type `t`'s glyphs as laid out (its layout bounds, else laid out now), grown by its
|
||||
/// characters' strokes and by `outset`: on a path they stand above it and run past its ends, in an
|
||||
/// area they can hang past its frame, where its own box ([`Node::visual_bounds_with`]) doesn't.
|
||||
fn glyphs_box(t: &vectorcraft_doc::TextObject, outset: f64) -> Option<Rect> {
|
||||
let b = t.cached_bounds.unwrap_or_else(|| vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).bounds);
|
||||
let o = outset.max(t.char_stroke_outset());
|
||||
(b.width() > 0.0 || b.height() > 0.0).then(|| t.xf.transform_rect_bbox(b).inflate(o, o))
|
||||
}
|
||||
|
||||
/// How far the content of live object `n` (a blend, envelope or repeat) can paint past its visual
|
||||
/// bounds: for each object in it, how far the effects in a symbol instance's art paint past that
|
||||
/// art ([`art_effects_reach`]) and how far brush art can paint ([`crate::brush_fx::pad`]), on the
|
||||
@@ -643,7 +656,7 @@ pub(crate) struct PixelEntry {
|
||||
/// For one cropped to the view, or made at a lower resolution, the view and the rect it was
|
||||
/// made for: it draws only there, so dragging other art over the object keeps it (a wide blur,
|
||||
/// or any blur zoomed in, reaches past the pixel budget).
|
||||
view: Option<ViewKey>,
|
||||
view: Option<ViewCrop>,
|
||||
made: Made,
|
||||
/// What the content read when the raster was made, with the channels the effects ran on and,
|
||||
/// under De-Interlace, the raster resolution.
|
||||
@@ -672,13 +685,29 @@ fn linear_key(view: Affine) -> [u64; 4] {
|
||||
[c[0].to_bits(), c[1].to_bits(), c[2].to_bits(), c[3].to_bits()]
|
||||
}
|
||||
|
||||
/// A view's six coefficients (its pan included) and the device-pixel rect a frame draws in, as a
|
||||
/// cache key: a raster cropped to that rect, or made at a lower resolution than the view's, draws
|
||||
/// only in that view of that rect ([`PixelEntry::view`], [`ShadowEntry::view`]).
|
||||
type ViewKey = ([u64; 6], [u64; 4]);
|
||||
/// The pan of the view a raster was cropped for, or made at a lower resolution than the view's, and
|
||||
/// the device-pixel rect of the frame it was made in ([`PixelEntry::view`], [`ShadowEntry::view`]).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ViewCrop {
|
||||
pan: Vec2,
|
||||
screen: Rect,
|
||||
}
|
||||
|
||||
fn view_key(view: Affine, screen: Rect) -> ViewKey {
|
||||
(view.as_coeffs().map(f64::to_bits), [screen.x0, screen.y0, screen.x1, screen.y1].map(f64::to_bits))
|
||||
impl ViewCrop {
|
||||
fn of(view: Affine, screen: Rect) -> Self {
|
||||
Self { pan: view.translation(), screen }
|
||||
}
|
||||
|
||||
/// Does the raster draw in a frame through `view` (of the same zoom and rotation) onto
|
||||
/// `screen`? When the view is panned by whole pixels and the frame's rect, panned back, lies
|
||||
/// within the one the raster was made in: the same pixels in the same places, as when a frame
|
||||
/// is drawn again only in a part of it.
|
||||
fn serves(&self, view: Affine, screen: Rect) -> bool {
|
||||
let d = view.translation() - self.pan;
|
||||
let whole = Vec2::new(d.x.round(), d.y.round());
|
||||
let s = screen - whole;
|
||||
(d - whole).hypot() < 1e-6 && s.x0 >= self.screen.x0 && s.y0 >= self.screen.y0 && s.x1 <= self.screen.x1 && s.y1 <= self.screen.y1
|
||||
}
|
||||
}
|
||||
|
||||
/// Most pixels a raster of [`Renderer::pixel_content`] or [`Renderer::with_filters`] holds while
|
||||
@@ -768,7 +797,7 @@ pub(crate) struct ShadowEntry {
|
||||
/// `None` for a raster of the whole silhouette's reach: it draws after a pan too. For one
|
||||
/// cropped to the view, or made over such a raster, the view and the rect it was made for: it
|
||||
/// draws only there ([`PixelEntry::view`]).
|
||||
view: Option<ViewKey>,
|
||||
view: Option<ViewCrop>,
|
||||
made: Made,
|
||||
/// What the silhouette read when the raster was made.
|
||||
inputs: Inputs,
|
||||
@@ -1084,9 +1113,9 @@ impl Renderer {
|
||||
// What the silhouette reads: as the raster kept for `n` found it, else found now.
|
||||
let reads = self.shadows.get(&key).filter(|e| e.node.is(n)).map_or_else(|| Reads::of(n), |e| e.inputs.reads);
|
||||
let inputs = self.raster_inputs(f, n, reads);
|
||||
let in_view = view_key(f.view, screen);
|
||||
let in_view = ViewCrop::of(f.view, screen);
|
||||
let exact = |e: &ShadowEntry| {
|
||||
e.linear == linear && e.node.is(n) && e.inputs == inputs && (interacting || !e.moved) && e.view.is_none_or(|v| v == in_view)
|
||||
e.linear == linear && e.node.is(n) && e.inputs == inputs && (interacting || !e.moved) && e.view.is_none_or(|v| v.serves(f.view, screen))
|
||||
};
|
||||
if interacting && !self.shadows.get(&key).is_some_and(exact) {
|
||||
self.move_shadow(f, n, key, fx, reach + Vec2::new(dx, dy), owner, &inputs, paint);
|
||||
@@ -1784,7 +1813,7 @@ impl Renderer {
|
||||
// The cached raster: of the whole content at full size at this zoom, else of this view.
|
||||
let linear = linear_key(f.view);
|
||||
let whole = fits && k >= 1.0;
|
||||
let in_view = (!whole).then(|| view_key(f.view, screen));
|
||||
let in_view = (!whole).then(|| ViewCrop::of(f.view, screen));
|
||||
let key = content.cache.map(|slot| (content.node as *const Node as usize, slot, f.ink));
|
||||
let stamp = self.stamp;
|
||||
let channels = match f.ink {
|
||||
@@ -1803,8 +1832,12 @@ impl Renderer {
|
||||
self.raster_inputs(f, content.node, reads).with((std::mem::discriminant(&channels), interlaced.then_some(ppi.to_bits())))
|
||||
});
|
||||
let interacting = f.opts.interacting;
|
||||
let in_place = |e: &PixelEntry| match e.view {
|
||||
None => whole,
|
||||
Some(v) => !whole && v.serves(f.view, screen),
|
||||
};
|
||||
let exact = |e: &PixelEntry| {
|
||||
e.linear == linear && e.view == in_view && e.node.is(content.node) && Some(&e.inputs) == inputs.as_ref() && (interacting || !e.moved)
|
||||
e.linear == linear && in_place(e) && e.node.is(content.node) && Some(&e.inputs) == inputs.as_ref() && (interacting || !e.moved)
|
||||
};
|
||||
if interacting
|
||||
&& whole
|
||||
|
||||
+17
-14
@@ -7,6 +7,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod brush_fx;
|
||||
mod changed;
|
||||
pub mod encode;
|
||||
mod freeform;
|
||||
mod fx;
|
||||
@@ -35,6 +36,7 @@ use ink::Ink;
|
||||
use lru::FrameCache;
|
||||
|
||||
pub use brush_fx::instance_art;
|
||||
pub use changed::{Changed, changed, view_pixels};
|
||||
pub use effects::stroke::width_outline;
|
||||
pub use images::{IMAGE_CACHE_MAX, image_cache_bytes, set_image_cache_budget};
|
||||
pub use live::expand_live;
|
||||
@@ -101,7 +103,7 @@ pub fn raster_size(region: Rect, scale: f64) -> Result<(u32, u32), String> {
|
||||
}
|
||||
|
||||
/// Rendering options.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RenderOptions {
|
||||
/// Outline (wireframe) view: 1 px black paths, no paint.
|
||||
pub outline: bool,
|
||||
@@ -444,8 +446,6 @@ pub struct Renderer {
|
||||
text_fonts: u64,
|
||||
/// Clip regions per clipping path (see [`Self::clip_of`]).
|
||||
clips: FrameCache<usize, ClipEntry>,
|
||||
/// Context reused when rendering single-threaded (`threads == 0`).
|
||||
ctx_st: Option<RenderContext>,
|
||||
/// Worker threads for the multithreaded rasterizer (0 = single-threaded).
|
||||
pub threads: u16,
|
||||
geom: PtrMap<usize, GeomEntry>,
|
||||
@@ -476,7 +476,10 @@ pub struct Renderer {
|
||||
stamp: u64,
|
||||
/// Opacity folded into paint alpha for the leaf being drawn (avoids a compositing layer).
|
||||
alpha: f32,
|
||||
ctx: Option<RenderContext>,
|
||||
/// The contexts kept for the next frames, with the threads they draw on, the last used last: a
|
||||
/// frame of the size of one reuses it instead of making a context and its threads. Two are
|
||||
/// kept, so a view's frames and the parts of them drawn again ([`changed`]) each keep theirs.
|
||||
contexts: Vec<(u16, RenderContext)>,
|
||||
resources: Resources,
|
||||
/// Statistics of the last frame.
|
||||
pub stats: FrameStats,
|
||||
@@ -613,7 +616,6 @@ impl Renderer {
|
||||
texts: FrameCache::new(lru::TEXT_BUDGET, 1024),
|
||||
text_fonts: 0,
|
||||
clips: FrameCache::new(lru::CLIP_BUDGET, 256),
|
||||
ctx_st: None,
|
||||
threads: default_threads(),
|
||||
geom: PtrMap::default(),
|
||||
symbols: vec![],
|
||||
@@ -625,7 +627,7 @@ impl Renderer {
|
||||
fx_geom: PtrMap::default(),
|
||||
stamp: 0,
|
||||
alpha: 1.0,
|
||||
ctx: None,
|
||||
contexts: Vec::new(),
|
||||
resources: Resources::new(),
|
||||
stats: FrameStats::default(),
|
||||
brushes: Default::default(),
|
||||
@@ -761,13 +763,13 @@ impl Renderer {
|
||||
/// Draw frame `f` (`w`×`h` pixels): the background, the artboards and the page's art.
|
||||
fn draw_frame(&mut self, f: &Frame, w: u16, h: u16) -> Vec<u8> {
|
||||
let (threads, opts) = (self.threads, f.opts);
|
||||
let slot = if threads == 0 { self.ctx_st.take() } else { self.ctx.take() };
|
||||
let mut ctx = match slot {
|
||||
Some(mut c) if c.width() == w && c.height() == h => {
|
||||
let kept = self.contexts.iter().position(|(t, c)| *t == threads && c.width() == w && c.height() == h);
|
||||
let mut ctx = match kept.map(|i| self.contexts.remove(i)) {
|
||||
Some((_, mut c)) => {
|
||||
c.reset();
|
||||
c
|
||||
}
|
||||
_ => RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: threads, ..Default::default() }),
|
||||
None => RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: threads, ..Default::default() }),
|
||||
};
|
||||
// `reset` keeps the threshold of the previous render: set it every time.
|
||||
ctx.set_aliasing_threshold(opts.anti_alias.threshold());
|
||||
@@ -800,10 +802,9 @@ impl Renderer {
|
||||
self.draw_page(&mut ctx, f);
|
||||
}
|
||||
let pm = self.finish(&mut ctx, self.raster);
|
||||
if threads == 0 {
|
||||
self.ctx_st = Some(ctx);
|
||||
} else {
|
||||
self.ctx = Some(ctx);
|
||||
self.contexts.push((threads, ctx));
|
||||
if self.contexts.len() > 2 {
|
||||
self.contexts.remove(0);
|
||||
}
|
||||
pm.data_as_u8_slice().to_vec()
|
||||
}
|
||||
@@ -2560,6 +2561,8 @@ mod tests_alternate;
|
||||
#[cfg(test)]
|
||||
mod tests_blend;
|
||||
#[cfg(test)]
|
||||
mod tests_changed;
|
||||
#[cfg(test)]
|
||||
mod tests_charstroke;
|
||||
#[cfg(test)]
|
||||
mod tests_clip;
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Drawing again only what an edit changed gives the pixels of drawing it all again.
|
||||
|
||||
use super::*;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::Appearance;
|
||||
use vectorcraft_geom::{Point, shapes};
|
||||
|
||||
const W: u32 = 160;
|
||||
const H: u32 = 120;
|
||||
|
||||
/// A view that doesn't line the art up with the pixels, so the edges are anti-aliased.
|
||||
fn view() -> Affine {
|
||||
Affine::translate((3.3, 2.7)) * Affine::scale(1.37)
|
||||
}
|
||||
|
||||
fn rect(d: &mut Document, r: Rect, fill: Color) -> Node {
|
||||
let id = d.alloc_id();
|
||||
Node::path(id, shapes::rectangle(r), Appearance::basic(Paint::solid(fill), Paint::solid(Color::BLACK), 2.0))
|
||||
}
|
||||
|
||||
fn shadow() -> vectorcraft_doc::Effect {
|
||||
vectorcraft_doc::Effect {
|
||||
id: "stylize.dropShadow".into(),
|
||||
params: serde_json::json!({"x": 6.0, "y": 6.0, "blur": 4.0, "opacity": 75.0}),
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer with a rectangle, a group at half opacity holding two, a clip group, and one with a
|
||||
/// drop shadow; the ids of the loose rectangle, a member of each group, and the shadowed one.
|
||||
fn document() -> (Document, [NodeId; 5]) {
|
||||
let mut d = Document::new(110.0, 80.0);
|
||||
let l = d.layers[0].id;
|
||||
let loose = rect(&mut d, Rect::new(5.0, 5.0, 25.0, 20.0), Color::rgb(1.0, 0.0, 0.0));
|
||||
let loose = d.insert(Some(l), 0, loose).unwrap();
|
||||
let (a, b) = (
|
||||
rect(&mut d, Rect::new(30.0, 5.0, 50.0, 25.0), Color::rgb(0.0, 0.6, 0.0)),
|
||||
rect(&mut d, Rect::new(40.0, 15.0, 60.0, 35.0), Color::rgb(0.0, 0.0, 1.0)),
|
||||
);
|
||||
let mut group = Node::new(d.alloc_id(), NodeKind::Group { children: vec![Arc::new(a), Arc::new(b)], clip: false });
|
||||
group.opacity = 0.5;
|
||||
let member = group.children().unwrap()[1].id;
|
||||
d.insert(Some(l), 1, group).unwrap();
|
||||
let (mask, art) =
|
||||
(rect(&mut d, Rect::new(5.0, 40.0, 30.0, 60.0), Color::WHITE), rect(&mut d, Rect::new(0.0, 45.0, 40.0, 55.0), Color::rgb(0.8, 0.4, 0.0)));
|
||||
let clipped = art.id;
|
||||
let clip = Node::new(d.alloc_id(), NodeKind::Group { children: vec![Arc::new(mask), Arc::new(art)], clip: true });
|
||||
d.insert(Some(l), 2, clip).unwrap();
|
||||
let mut shadowed = rect(&mut d, Rect::new(60.0, 45.0, 80.0, 60.0), Color::rgb(0.9, 0.9, 0.2));
|
||||
shadowed.appearance.effects.push(shadow());
|
||||
let shadowed = d.insert(Some(l), 3, shadowed).unwrap();
|
||||
let other = rect(&mut d, Rect::new(85.0, 10.0, 105.0, 30.0), Color::rgb(0.5, 0.0, 0.5));
|
||||
let other = d.insert(Some(l), 4, other).unwrap();
|
||||
(d, [loose, member, clipped, shadowed, other])
|
||||
}
|
||||
|
||||
/// Draws `new` again only where it changed from `old`, over `old` drawn whole, and checks that
|
||||
/// this is `new` drawn whole; returns what changed.
|
||||
fn redraw(old: &Document, new: &Document) -> Changed {
|
||||
let mut r = Renderer::new();
|
||||
let opts = RenderOptions::default();
|
||||
let before = r.render(old, W, H, view(), &opts);
|
||||
let after = r.render(new, W, H, view(), &opts);
|
||||
let change = changed(old, new);
|
||||
let mut patched = Rendered { width: W, height: H, pixels: before.pixels.clone() };
|
||||
match change {
|
||||
Changed::Everything => patched.pixels = after.pixels.clone(),
|
||||
Changed::Nothing => {}
|
||||
Changed::Area(area) => {
|
||||
if let Some([x0, y0, x1, y1]) = view_pixels(area, view(), W, H) {
|
||||
let part = r.render(new, x1 - x0, y1 - y0, Affine::translate((-f64::from(x0), -f64::from(y0))) * view(), &opts);
|
||||
for y in 0..y1 - y0 {
|
||||
let (from, to) = ((y * (x1 - x0) * 4) as usize, (((y0 + y) * W + x0) * 4) as usize);
|
||||
let n = ((x1 - x0) * 4) as usize;
|
||||
patched.pixels[to..to + n].copy_from_slice(&part.pixels[from..from + n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(patched.pixels == after.pixels, "{change:?}: drawn again only there, it differs from drawing it all");
|
||||
change
|
||||
}
|
||||
|
||||
fn area(c: Changed) -> Rect {
|
||||
match c {
|
||||
Changed::Area(a) => a,
|
||||
other => panic!("an area: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_an_object_changes_where_it_was_and_is() {
|
||||
let (old, [loose, ..]) = document();
|
||||
let mut new = old.clone();
|
||||
new.node_mut(loose).unwrap().transform(Affine::translate((7.0, 3.0)), false);
|
||||
let a = area(redraw(&old, &new));
|
||||
assert!(a.contains(Point::new(6.0, 6.0)) && a.contains(Point::new(31.0, 22.0)), "before and after: {a:?}");
|
||||
assert!(a.x1 < 35.0, "not the objects beside it: {a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recolouring_a_member_of_a_translucent_group_changes_only_the_member() {
|
||||
let (old, [_, member, ..]) = document();
|
||||
let mut new = old.clone();
|
||||
new.node_mut(member).unwrap().appearance.fill_mut().unwrap().paint = Paint::solid(Color::rgb(1.0, 0.5, 0.0));
|
||||
let a = area(redraw(&old, &new));
|
||||
assert!(a.x0 > 35.0, "not the group's other member: {a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_clipped_art_changes_its_whole_clip_group() {
|
||||
let (old, [_, _, clipped, ..]) = document();
|
||||
let mut new = old.clone();
|
||||
new.node_mut(clipped).unwrap().transform(Affine::translate((0.0, 4.0)), false);
|
||||
redraw(&old, &new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_a_shadowed_object_changes_where_its_shadow_falls() {
|
||||
let (old, [.., shadowed, _]) = document();
|
||||
let mut new = old.clone();
|
||||
new.node_mut(shadowed).unwrap().transform(Affine::translate((-5.0, 5.0)), false);
|
||||
let a = area(redraw(&old, &new));
|
||||
assert!(a.x1 > 86.0 && a.y1 > 70.0, "the shadow, before and after: {a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_removing_and_restacking_change_those_objects() {
|
||||
let (old, [loose, _, _, _, other]) = document();
|
||||
let mut added = old.clone();
|
||||
let n = rect(&mut added, Rect::new(70.0, 5.0, 90.0, 15.0), Color::BLACK);
|
||||
let l = added.layers[0].id;
|
||||
added.insert(Some(l), 5, n).unwrap();
|
||||
redraw(&old, &added);
|
||||
let mut removed = old.clone();
|
||||
removed.remove(loose).unwrap();
|
||||
redraw(&old, &removed);
|
||||
let mut restacked = old.clone();
|
||||
let n = restacked.remove(other).unwrap();
|
||||
restacked.insert(Some(l), 0, (*n).clone()).unwrap();
|
||||
redraw(&old, &restacked);
|
||||
}
|
||||
|
||||
/// A blur too wide for a raster of all of it is cropped to the frame. A part of the frame drawn
|
||||
/// again, during a drag too, draws it from that raster: the same pixels, and no preview at the
|
||||
/// drag's lower resolution.
|
||||
#[test]
|
||||
fn a_part_of_a_frame_draws_a_cropped_blur_from_the_frames_raster() {
|
||||
let (mut d, _) = document();
|
||||
let mut back = rect(&mut d, Rect::new(0.0, 0.0, 110.0, 80.0), Color::rgb(0.1, 0.3, 0.9));
|
||||
back.appearance.effects.push(vectorcraft_doc::Effect { id: "blur.gaussian".into(), params: serde_json::json!({"radius": 500.0}), visible: true });
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, back).unwrap();
|
||||
let view = Affine::scale(2.0) * Affine::translate((-20.0, -15.0));
|
||||
let mut r = Renderer::new();
|
||||
let whole = r.render(&d, W, H, view, &RenderOptions::default());
|
||||
let [x0, y0, x1, y1] = [40, 30, 100, 80];
|
||||
let dragging = RenderOptions { interacting: true, dragging: true, ..Default::default() };
|
||||
let part = r.render(&d, x1 - x0, y1 - y0, Affine::translate((-f64::from(x0), -f64::from(y0))) * view, &dragging);
|
||||
assert_eq!(r.stats.previewed, 0, "drawn from the frame's raster");
|
||||
for y in 0..y1 - y0 {
|
||||
for x in 0..x1 - x0 {
|
||||
assert_eq!(part.pixel(x, y), whole.pixel(x0 + x, y0 + y), "at {x}, {y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type on a path whose path a script reshapes: its letters stand above the path and run past
|
||||
/// where it ends, and they change where they were and where they are, outside the path's box.
|
||||
#[test]
|
||||
fn reshaping_the_path_of_type_on_a_path_changes_where_its_letters_were_and_are() {
|
||||
let mut old = Document::new(110.0, 80.0);
|
||||
let l = old.layers[0].id;
|
||||
let on = |y: f64| {
|
||||
let mut t = vectorcraft_doc::text::TextObject::point(
|
||||
Point::ZERO,
|
||||
"Text along a path",
|
||||
vectorcraft_doc::text::CharStyle { size: 14.0, ..Default::default() },
|
||||
);
|
||||
let line = kurbo::Line::new((10.0, 60.0), (90.0, y)).to_path(0.1);
|
||||
t.kind = vectorcraft_doc::TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&line), start: 0.0, end: None };
|
||||
t.xf = Affine::IDENTITY;
|
||||
// The engine lays out type that changed ([`vectorcraft_doc::TextObject::cached_bounds`]).
|
||||
t.cached_bounds = Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).bounds);
|
||||
t
|
||||
};
|
||||
let text = old.insert(Some(l), 0, Node::new(NodeId(0), NodeKind::Text(Box::new(on(55.0))))).unwrap();
|
||||
let mut new = old.clone();
|
||||
if let Some(NodeKind::Text(t)) = new.node_mut(text).map(|n| &mut n.kind) {
|
||||
**t = on(25.0);
|
||||
}
|
||||
redraw(&old, &new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unchanged_document_changes_nothing_and_a_new_symbol_everything() {
|
||||
let (old, _) = document();
|
||||
assert_eq!(redraw(&old, &old.clone()), Changed::Nothing);
|
||||
let mut new = old.clone();
|
||||
let art = Arc::new(rect(&mut new, Rect::new(0.0, 0.0, 5.0, 5.0), Color::BLACK));
|
||||
new.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art });
|
||||
assert_eq!(redraw(&old, &new), Changed::Everything);
|
||||
}
|
||||
|
||||
/// While a pattern is edited its art also draws in the copies round it: an edit of it changes
|
||||
/// everything.
|
||||
#[test]
|
||||
fn an_edit_while_a_pattern_is_edited_changes_everything() {
|
||||
let (mut old, [loose, ..]) = document();
|
||||
let l = old.layers[0].id;
|
||||
old.pattern_edit = Some(vectorcraft_doc::PatternEdit { pattern: "Tile".into(), layer: l, original: None });
|
||||
let mut new = old.clone();
|
||||
new.node_mut(loose).unwrap().transform(Affine::translate((7.0, 3.0)), false);
|
||||
assert_eq!(changed(&old, &new), Changed::Everything);
|
||||
}
|
||||
@@ -379,6 +379,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
{
|
||||
upload(app, ui.ctx(), &done.img, done.key.pixel.is_some());
|
||||
app.canvas.key = Some(done.key);
|
||||
app.canvas.shown = Some((done.doc, done.opts));
|
||||
app.perf.render_ms = done.ms;
|
||||
app.canvas.last_ms = done.ms;
|
||||
app.canvas.moved = done.moved > 0;
|
||||
@@ -453,13 +454,30 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let settling = app.canvas.moved && !key.interacting;
|
||||
let staying = key.interacting && app.canvas.from_worker;
|
||||
let heavy = (app.canvas.last_ms > 8.0 || settling || staying) && app.canvas.texture.is_some() && same_doc;
|
||||
match (&mut app.canvas.worker, heavy) {
|
||||
(Some(worker), true) => worker.submit(crate::render_worker::Job { key: key.clone(), doc: doc.clone(), w, h, view, opts }),
|
||||
let changed = changed_pixels(app, &key, &doc, &opts, view, w, h);
|
||||
match (&mut app.canvas.worker, heavy, changed) {
|
||||
// Only the document changed since the frame shown, in a part of it: that part is
|
||||
// rendered again, here, into the texture. `last_ms` stays the whole frame's.
|
||||
(_, _, Some(region)) => {
|
||||
let t0 = now_ms();
|
||||
if let (Some([x0, y0, x1, y1]), Some(tex)) = (region, &mut app.canvas.texture) {
|
||||
let part = app.canvas.renderer.render(&doc, x1 - x0, y1 - y0, Affine::translate((-f64::from(x0), -f64::from(y0))) * view, &opts);
|
||||
let color = color_image(&part);
|
||||
let options = if pixel.is_some() { egui::TextureOptions::NEAREST } else { egui::TextureOptions::LINEAR };
|
||||
tex.set_partial([x0 as usize, y0 as usize], color, options);
|
||||
app.canvas.moved |= app.canvas.renderer.stats.moved > 0;
|
||||
}
|
||||
app.canvas.key = Some(key.clone());
|
||||
app.canvas.shown = Some((doc.clone(), opts.clone()));
|
||||
app.perf.render_ms = now_ms() - t0;
|
||||
}
|
||||
(Some(worker), true, None) => worker.submit(crate::render_worker::Job { key: key.clone(), doc: doc.clone(), w, h, view, opts }),
|
||||
_ => {
|
||||
let t0 = now_ms();
|
||||
let img = app.canvas.renderer.render(&doc, w, h, view, &opts);
|
||||
upload(app, ui.ctx(), &img, pixel.is_some());
|
||||
app.canvas.key = Some(key.clone());
|
||||
app.canvas.shown = Some((doc.clone(), opts.clone()));
|
||||
app.perf.render_ms = now_ms() - t0;
|
||||
app.canvas.last_ms = app.perf.render_ms;
|
||||
app.canvas.moved = app.canvas.renderer.stats.moved > 0;
|
||||
@@ -3127,6 +3145,52 @@ pub(crate) fn color_image(img: &vectorcraft_render::Rendered) -> egui::ColorImag
|
||||
egui::ColorImage::new(size, pixels)
|
||||
}
|
||||
|
||||
/// The pixels of the `w`×`h` frame through `view` to render again for `doc`, when only the document
|
||||
/// changed since the frame shown (its key `key` but for the revision, and its render options
|
||||
/// `opts`), which this thread rendered, and no frame is on its way from the worker: those it
|
||||
/// changed in (`Some(None)`: none), grown to whole eighths of the frame, as long as they are at
|
||||
/// most half of it. None: render the whole frame. Frames the worker renders stay
|
||||
/// whole: its renderer keeps the rasters of the art's effects, this thread's doesn't.
|
||||
fn changed_pixels(
|
||||
app: &VectorcraftApp,
|
||||
key: &CacheKey,
|
||||
doc: &vectorcraft_doc::Document,
|
||||
opts: &vectorcraft_render::RenderOptions,
|
||||
view: Affine,
|
||||
w: u32,
|
||||
h: u32,
|
||||
) -> Option<Option<[u32; 4]>> {
|
||||
let (shown, _) = app.canvas.shown.as_ref().filter(|(_, o)| o == opts)?;
|
||||
// A viewed opacity mask shows its coverage wherever its art reaches, past its object's bounds.
|
||||
if opts.mask_view.is_some() {
|
||||
return None;
|
||||
}
|
||||
let same_view = app.canvas.key.as_ref().is_some_and(|k| *k == CacheKey { revision: k.revision, ..key.clone() });
|
||||
if !same_view || app.canvas.texture.is_none() || app.canvas.from_worker || app.canvas.worker.as_ref().is_some_and(|w| w.pending.is_some()) {
|
||||
return None;
|
||||
}
|
||||
match vectorcraft_render::changed(shown, doc) {
|
||||
vectorcraft_render::Changed::Nothing => Some(None),
|
||||
vectorcraft_render::Changed::Area(area) => match vectorcraft_render::view_pixels(area, view, w, h) {
|
||||
None => Some(None),
|
||||
Some([x0, y0, x1, y1]) => {
|
||||
let ((x0, x1), (y0, y1)) = (in_steps(x0, x1, w), in_steps(y0, y1, h));
|
||||
(u64::from(x1 - x0) * u64::from(y1 - y0) * 2 <= u64::from(w) * u64::from(h)).then_some(Some([x0, y0, x1, y1]))
|
||||
}
|
||||
},
|
||||
vectorcraft_render::Changed::Everything => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `a..b` of `0..n`, grown to whole eighths of `n` and moved back into `0..n`: the parts of a
|
||||
/// frame rendered again come in few sizes, which keep the contexts the renderer keeps.
|
||||
fn in_steps(a: u32, b: u32, n: u32) -> (u32, u32) {
|
||||
let step = n.div_ceil(8).max(1);
|
||||
let len = (b - a).max(1).div_ceil(step).saturating_mul(step).min(n);
|
||||
let a = a.min(n - len);
|
||||
(a, a + len)
|
||||
}
|
||||
|
||||
/// Upload the art's raster; Pixel Preview's document pixels keep hard edges when magnified.
|
||||
fn upload(app: &mut VectorcraftApp, ctx: &egui::Context, img: &vectorcraft_render::Rendered, pixel: bool) {
|
||||
let color = color_image(img);
|
||||
@@ -4691,6 +4755,69 @@ mod tests {
|
||||
(v, out.textures_delta)
|
||||
}
|
||||
|
||||
/// An edit of one object renders the canvas again only where it changed, into its texture; a
|
||||
/// change of the view renders all of it.
|
||||
#[test]
|
||||
fn an_edit_renders_only_its_part_of_the_canvas() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width":400,"height":300})).unwrap();
|
||||
app.ui.task_bar = false;
|
||||
app.ui.view.rulers = false;
|
||||
app.canvas.worker_started = true; // every frame on this thread, however long it takes
|
||||
let id = app.run("shape.rectangle", json!({"x":50,"y":50,"width":40,"height":30})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("shape.rectangle", json!({"x":250,"y":200,"width":40,"height":30})).unwrap();
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
// Where the canvas texture changed in a frame, and how much of it.
|
||||
let canvas = |app: &VectorcraftApp, mut delta: egui::TexturesDelta| {
|
||||
let tex = app.canvas.texture.as_ref().map(|t| t.id());
|
||||
let set = delta.set.iter().find(|(t, _)| tex == Some(**t)).and_then(|(_, d)| d.last()).map(|d| (d.pos, d.image.size()));
|
||||
delta.clear();
|
||||
set
|
||||
};
|
||||
let (_, delta) = frame_output(&mut app, &ctx);
|
||||
assert!(canvas(&app, delta).is_some_and(|(pos, _)| pos.is_none()), "the first frame renders all of it");
|
||||
app.run("paint.setFill", json!({"color": "#ff0000", "ids": [id]})).unwrap();
|
||||
let (_, delta) = frame_output(&mut app, &ctx);
|
||||
let (pos, size) = canvas(&app, delta).expect("the edit shows");
|
||||
let whole = app.canvas.key.as_ref().map(|k| (k.w as usize, k.h as usize)).unwrap();
|
||||
assert!(pos.is_some() && size[0] * size[1] * 10 < whole.0 * whole.1, "a small part of it: {pos:?} {size:?} of {whole:?}");
|
||||
// An edit with other render options than the frame shown's renders all of it.
|
||||
app.session.prefs.pattern_tile_edge_color = "#ff00ff".into();
|
||||
app.run("paint.setFill", json!({"color": "#00ff00", "ids": [id]})).unwrap();
|
||||
let (_, delta) = frame_output(&mut app, &ctx);
|
||||
assert!(canvas(&app, delta).is_some_and(|(pos, _)| pos.is_none()), "other options render all of it");
|
||||
app.run("view.setZoom", json!({"zoom": 2})).unwrap();
|
||||
let (_, delta) = frame_output(&mut app, &ctx);
|
||||
assert!(canvas(&app, delta).is_some_and(|(pos, _)| pos.is_none()), "a new view renders all of it");
|
||||
}
|
||||
|
||||
/// While an opacity mask is viewed, an edit renders all of the canvas: the mask view shows the
|
||||
/// mask's coverage wherever its art reaches, past its object's bounds.
|
||||
#[test]
|
||||
fn an_edit_while_a_mask_is_viewed_renders_all_of_the_canvas() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width":400,"height":300})).unwrap();
|
||||
app.ui.task_bar = false;
|
||||
app.ui.view.rulers = false;
|
||||
app.canvas.worker_started = true; // every frame on this thread, however long it takes
|
||||
let rect =
|
||||
|app: &mut VectorcraftApp, x: f64| app.run("shape.rectangle", json!({"x": x, "y": 50, "width": 60, "height": 40})).unwrap()["id"].clone();
|
||||
let (obj, mask) = (rect(&mut app, 50.0), rect(&mut app, 70.0));
|
||||
app.run("select.set", json!({"ids": [obj, mask]})).unwrap();
|
||||
app.run("transparency.makeOpacityMask", json!({})).unwrap();
|
||||
app.run("transparency.viewOpacityMask", json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let (_, mut delta) = frame_output(&mut app, &ctx);
|
||||
delta.clear();
|
||||
app.run("paint.setFill", json!({"color": "#ff0000", "ids": [obj]})).unwrap();
|
||||
let (_, mut delta) = frame_output(&mut app, &ctx);
|
||||
let tex = app.canvas.texture.as_ref().map(|t| t.id());
|
||||
let pos = delta.set.iter().find(|(t, _)| tex == Some(**t)).and_then(|(_, d)| d.last()).map(|d| d.pos);
|
||||
delta.clear();
|
||||
assert_eq!(pos, Some(None), "all of it");
|
||||
}
|
||||
|
||||
/// #1278: a segment click marks the edge, not its endpoint anchors or neighboring handles.
|
||||
#[test]
|
||||
fn a_segment_pick_has_hollow_anchors_and_only_its_own_handles() {
|
||||
@@ -5298,7 +5425,7 @@ mod tests {
|
||||
// Deliver the actual replacement and verify that it replaces the cached key.
|
||||
let img = app.canvas.renderer.render(&job.doc, job.w, job.h, job.view, &job.opts);
|
||||
let expected = job.key.clone();
|
||||
completed.send(crate::render_worker::Done { key: job.key, img, ms: 1.0, moved: 0 }).unwrap();
|
||||
completed.send(crate::render_worker::Done { key: job.key, doc: job.doc, opts: job.opts, img, ms: 1.0, moved: 0 }).unwrap();
|
||||
frame(&mut app, size, new_ppp);
|
||||
assert_eq!(app.canvas.key.as_ref(), Some(&expected), "the worker's frame arrived");
|
||||
}
|
||||
|
||||
@@ -321,6 +321,10 @@ pub struct CanvasCache {
|
||||
pub renderer: vectorcraft_render::Renderer,
|
||||
pub texture: Option<egui::TextureHandle>,
|
||||
pub key: Option<CacheKey>,
|
||||
/// The document the frame shown was rendered from, and the options it was rendered with, with
|
||||
/// [`Self::key`]: when only the document changes, the canvas renders again only where it
|
||||
/// changed ([`vectorcraft_render::changed`]).
|
||||
pub shown: Option<(std::sync::Arc<vectorcraft_doc::Document>, vectorcraft_render::RenderOptions)>,
|
||||
pub last_ms: f64,
|
||||
/// The frame shown drew raster effects from rasters made before their objects moved, or at a
|
||||
/// drag's lower resolution ([`vectorcraft_render::FrameStats::moved`]): the next frame outside
|
||||
@@ -565,6 +569,7 @@ impl VectorcraftApp {
|
||||
renderer: vectorcraft_render::Renderer::new(),
|
||||
texture: None,
|
||||
key: None,
|
||||
shown: None,
|
||||
last_ms: 0.0,
|
||||
moved: false,
|
||||
from_worker: false,
|
||||
|
||||
@@ -22,6 +22,9 @@ pub struct Job {
|
||||
|
||||
pub struct Done {
|
||||
pub key: CacheKey,
|
||||
/// The document rendered, and the options it was rendered with.
|
||||
pub doc: Arc<Document>,
|
||||
pub opts: RenderOptions,
|
||||
pub img: Rendered,
|
||||
pub ms: f64,
|
||||
/// [`vectorcraft_render::FrameStats::moved`] of the frame.
|
||||
@@ -53,7 +56,8 @@ impl Worker {
|
||||
}
|
||||
let t0 = crate::now_ms();
|
||||
let img = r.render(&job.doc, job.w, job.h, job.view, &job.opts);
|
||||
if done_tx.send(Done { key: job.key, img, ms: crate::now_ms() - t0, moved: r.stats.moved }).is_err() {
|
||||
if done_tx.send(Done { key: job.key, doc: job.doc, opts: job.opts, img, ms: crate::now_ms() - t0, moved: r.stats.moved }).is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
|
||||
Reference in New Issue
Block a user