mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Merge fix/667 into batch 13
This commit is contained in:
+19
-6
@@ -77,17 +77,19 @@ pub enum DocError {
|
||||
|
||||
pub type DocResult<T> = Result<T, DocError>;
|
||||
|
||||
/// The set of touched node ids, and whether any layer is a mask (worked out when first asked
|
||||
/// after a change: hit testing skips its mask checks without one). Never part of equality or
|
||||
/// The set of touched node ids, whether any layer is a mask (hit testing skips its mask checks
|
||||
/// without one) and the largest background blur radius (renders keep that much overlap between
|
||||
/// their bands), both worked out when first asked after a change. Never part of equality or
|
||||
/// serialisation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Dirty(pub std::collections::BTreeSet<NodeId>, std::sync::OnceLock<bool>);
|
||||
pub struct Dirty(pub std::collections::BTreeSet<NodeId>, std::sync::OnceLock<bool>, std::sync::OnceLock<f64>);
|
||||
|
||||
impl Dirty {
|
||||
/// Record node `id` as touched (edited, added or removed).
|
||||
fn touch(&mut self, id: NodeId) {
|
||||
self.0.insert(id);
|
||||
self.1 = std::sync::OnceLock::new();
|
||||
self.2 = std::sync::OnceLock::new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +256,11 @@ impl Document {
|
||||
*self.dirty.1.get_or_init(|| self.nodes.values().any(|n| n.is_mask))
|
||||
}
|
||||
|
||||
/// The largest background blur or glass radius of any layer's effects (0 without any).
|
||||
pub fn max_backdrop_radius(&self) -> f64 {
|
||||
*self.dirty.2.get_or_init(|| self.nodes.values().flat_map(|n| n.effects.iter()).map(Effect::backdrop_radius).fold(0.0, f64::max))
|
||||
}
|
||||
|
||||
/// Node ids touched so far (without clearing).
|
||||
pub fn dirty(&self) -> &std::collections::BTreeSet<NodeId> {
|
||||
&self.dirty.0
|
||||
@@ -1117,8 +1124,14 @@ impl Document {
|
||||
/// mask with the layers above it up to the next mask, or a single layer. An auto layout frame
|
||||
/// whose first child paints on top paints the runs in reverse (see [`Node::paint_order`]).
|
||||
pub fn paint_runs(&self, n: &Node) -> Vec<std::ops::Range<usize>> {
|
||||
let kids = &n.children;
|
||||
let node = |i: usize| kids.get(i).and_then(|k| self.node(*k));
|
||||
let kids: Vec<Option<&Node>> = n.children.iter().map(|k| self.node(*k)).collect();
|
||||
Self::paint_runs_of(&kids, n.layout.as_ref().is_some_and(|l| l.reverse_z))
|
||||
}
|
||||
|
||||
/// [`Self::paint_runs`] of children already looked up (`None`: not in the document), last run
|
||||
/// first when `reverse_z`.
|
||||
pub fn paint_runs_of(kids: &[Option<&Node>], reverse_z: bool) -> Vec<std::ops::Range<usize>> {
|
||||
let node = |i: usize| kids.get(i).copied().flatten();
|
||||
let mut runs = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < kids.len() {
|
||||
@@ -1131,7 +1144,7 @@ impl Document {
|
||||
runs.push(i..j);
|
||||
i = j;
|
||||
}
|
||||
if n.layout.as_ref().is_some_and(|l| l.reverse_z) {
|
||||
if reverse_z {
|
||||
runs.reverse();
|
||||
}
|
||||
runs
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Paint bounds worked out once per render: the bounds kept for a layer are the ones worked out
|
||||
//! without keeping any, and the largest background blur follows the document's edits.
|
||||
|
||||
use super::*;
|
||||
use figcraft_doc::{Effect, EffectKind};
|
||||
|
||||
#[test]
|
||||
fn kept_paint_bounds_are_the_bounds_worked_out_afresh() {
|
||||
// Unclipped frames nested past the 64-level walk, a rectangle at the bottom: bounds asked for
|
||||
// deep first and then from the top, and the other way round, are those worked out alone.
|
||||
let mut d = Document::new("deep");
|
||||
let p = d.first_page().unwrap();
|
||||
let mut chain = Vec::new();
|
||||
let mut parent = p;
|
||||
for k in 0..80 {
|
||||
let f = d.create(parent, NodeKind::Frame, &format!("F{k}"), 1.0, 1.0, 10.0, 10.0).unwrap();
|
||||
d.node_mut(f).unwrap().clip = false;
|
||||
chain.push(f);
|
||||
parent = f;
|
||||
}
|
||||
d.create(parent, NodeKind::Rectangle, "R", 500.0, 0.0, 10.0, 10.0).unwrap();
|
||||
for order in [chain.iter().rev().copied().collect::<Vec<_>>(), chain.clone()] {
|
||||
let memo = draw::Bounds::default();
|
||||
for id in &order {
|
||||
let n = d.node(*id).unwrap();
|
||||
for depth in [0, 20] {
|
||||
let kept = draw::bounds_within(&d, n, depth, None, Some(&memo)).0;
|
||||
assert_eq!(kept, draw::local_paint_bounds(&d, n, depth, None), "{} at depth {depth}", n.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_largest_background_blur_follows_edits() {
|
||||
let mut d = Document::new("blur");
|
||||
let p = d.first_page().unwrap();
|
||||
let r = d.create(p, NodeKind::Rectangle, "R", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||||
assert_eq!(max_backdrop_blur(&d), 0.0);
|
||||
let mut blur = Effect::background_blur();
|
||||
if let EffectKind::BackgroundBlur(b) = &mut blur.kind {
|
||||
b.radius = 12.0;
|
||||
}
|
||||
d.node_mut(r).unwrap().effects.push(blur);
|
||||
assert_eq!(max_backdrop_blur(&d), 12.0);
|
||||
// A copy of the document keeps it, and an edit of the copy changes only the copy's.
|
||||
let mut copy = d.clone();
|
||||
if let Some(EffectKind::BackgroundBlur(b)) = copy.node_mut(r).unwrap().effects.first_mut().map(|e| &mut e.kind) {
|
||||
b.radius = 30.0;
|
||||
}
|
||||
assert_eq!(max_backdrop_blur(©), 30.0);
|
||||
assert_eq!(max_backdrop_blur(&d), 12.0);
|
||||
copy.node_mut(r).unwrap().effects.clear();
|
||||
assert_eq!(max_backdrop_blur(©), 0.0);
|
||||
// A deleted layer's blur no longer counts.
|
||||
d.remove(r).unwrap();
|
||||
assert_eq!(max_backdrop_blur(&d), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kept_splat_answers_are_the_answers_worked_out_afresh() {
|
||||
use figcraft_color::{ImageScaleMode, Paint};
|
||||
// Groups nested past the 64-level walk, with an image-filled rectangle at the bottom of one
|
||||
// branch and a plain one at the bottom of the other: answers asked for deep-first and
|
||||
// top-first, at depths 0 and 20, are those worked out alone.
|
||||
let mut d = Document::new("deep");
|
||||
let p = d.first_page().unwrap();
|
||||
let mut chain = Vec::new();
|
||||
for image in [false, true] {
|
||||
let mut parent = p;
|
||||
for k in 0..80 {
|
||||
let g = d.create(parent, NodeKind::Frame, &format!("{image} {k}"), 0.0, 0.0, 10.0, 10.0).unwrap();
|
||||
d.node_mut(g).unwrap().clip = false;
|
||||
chain.push(g);
|
||||
parent = g;
|
||||
}
|
||||
let r = d.create(parent, NodeKind::Rectangle, "R", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||||
if image {
|
||||
d.node_mut(r).unwrap().fills = vec![Paint::image("missing", ImageScaleMode::Fill)];
|
||||
}
|
||||
}
|
||||
for order in [chain.iter().rev().copied().collect::<Vec<_>>(), chain.clone()] {
|
||||
let memo = draw::Bounds::default();
|
||||
for id in &order {
|
||||
let n = d.node(*id).unwrap();
|
||||
for depth in [0, 20] {
|
||||
let kept = lod::splattable_within(&d, n, depth, Some(&memo)).0;
|
||||
assert_eq!(kept, lod::splattable(&d, n, depth), "{} at depth {depth}", n.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
-21
@@ -16,8 +16,9 @@ use crate::{blur, grain, lod, shadow};
|
||||
|
||||
/// Shared state for one render.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Ctx<'a> {
|
||||
pub doc: &'a Document,
|
||||
/// `'d`: the document's (layers looked up for the render are kept that long), `'a`: the rest.
|
||||
pub struct Ctx<'a, 'd> {
|
||||
pub doc: &'d Document,
|
||||
/// Device pixels per document unit (for effect radii).
|
||||
pub scale: f64,
|
||||
pub outline_mode: bool,
|
||||
@@ -33,8 +34,67 @@ pub struct Ctx<'a> {
|
||||
pub under: Option<&'a Under<'a>>,
|
||||
/// Seconds into animated GIFs (0: their first frame; only the presenter plays them).
|
||||
pub time: f64,
|
||||
/// Paint bounds already worked out in this render.
|
||||
pub bounds: &'a Bounds<'d>,
|
||||
}
|
||||
|
||||
/// What one render works out about layers with children, which a walk down their subtree finds:
|
||||
/// their local paint bounds by greeking scale, and whether they can be splatted (see
|
||||
/// [`lod::splattable`]). Culling asks for a node's and then for each of its children's, so without
|
||||
/// them every layer's subtree was walked again at each level above it. Each answer keeps how many
|
||||
/// levels its walk went down.
|
||||
/// It also keeps the layers it looked up: the walks for culling, splatting and drawing go down the
|
||||
/// same layers, and in a large document a lookup is a good part of a layer's cost.
|
||||
#[derive(Default)]
|
||||
pub struct Bounds<'a> {
|
||||
paint: std::cell::RefCell<IdMap<BoundsKey, (Rect, usize)>>,
|
||||
pub(crate) splat: std::cell::RefCell<IdMap<NodeId, (bool, usize)>>,
|
||||
nodes: std::cell::RefCell<IdMap<NodeId, Option<&'a Node>>>,
|
||||
}
|
||||
|
||||
impl<'a> Bounds<'a> {
|
||||
/// Node `id` of `doc`, looked up once for the render.
|
||||
pub(crate) fn node(&self, doc: &'a Document, id: NodeId) -> Option<&'a Node> {
|
||||
if let Some(n) = self.nodes.borrow().get(&id) {
|
||||
return *n;
|
||||
}
|
||||
let n = doc.node(id);
|
||||
self.nodes.borrow_mut().insert(id, n);
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
/// A hash map keyed by node ids, which need no SipHash: they are counters (or a peer's range of
|
||||
/// them), and SipHash took a good part of a render.
|
||||
type IdMap<K, V> = HashMap<K, V, std::hash::BuildHasherDefault<IdHash>>;
|
||||
|
||||
/// Mixes each word in with a multiply (Fibonacci hashing), so ids that differ in low bits spread.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct IdHash(u64);
|
||||
|
||||
impl Hasher for IdHash {
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
for b in bytes {
|
||||
self.write_u64(u64::from(*b));
|
||||
}
|
||||
}
|
||||
fn write_u64(&mut self, v: u64) {
|
||||
self.0 = (self.0.rotate_left(5) ^ v).wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||
}
|
||||
fn write_usize(&mut self, v: usize) {
|
||||
self.write_u64(v as u64);
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer, and the scale its text is greeked at (none: drawn in full).
|
||||
type BoundsKey = (NodeId, Option<u64>);
|
||||
|
||||
/// Deepest nesting the paint bounds walk follows.
|
||||
const MAX_BOUNDS_DEPTH: usize = 64;
|
||||
|
||||
pub fn ts_transform(a: Affine) -> Transform {
|
||||
let c = a.as_coeffs();
|
||||
Transform::from_row(c[0] as f32, c[1] as f32, c[2] as f32, c[3] as f32, c[4] as f32, c[5] as f32)
|
||||
@@ -279,32 +339,63 @@ fn text_box_bounds(n: &Node) -> Rect {
|
||||
/// Local bounds a node paints into (its box grown by stroke/effect reach, plus unclipped children).
|
||||
/// Text that overflows its box counts where its glyphs are, so it isn't culled while visible;
|
||||
/// text greeked at `greek_scale` (see [`lod::greeked`]) paints only inside [`text_box_bounds`].
|
||||
fn local_paint_bounds(doc: &Document, n: &Node, depth: usize, greek_scale: Option<f64>) -> Rect {
|
||||
pub(crate) fn local_paint_bounds(doc: &Document, n: &Node, depth: usize, greek_scale: Option<f64>) -> Rect {
|
||||
bounds_within(doc, n, depth, greek_scale, None).0
|
||||
}
|
||||
|
||||
/// [`local_paint_bounds`], kept in and read from `memo` when there is one. Also returns how many
|
||||
/// levels the walk went down, or `None` when the nesting cap cut it short: such bounds depend on
|
||||
/// the depth they were asked at and aren't kept, and kept bounds are read only where their levels
|
||||
/// still fit under the cap.
|
||||
pub(crate) fn bounds_within<'a>(
|
||||
doc: &'a Document,
|
||||
n: &Node,
|
||||
depth: usize,
|
||||
greek_scale: Option<f64>,
|
||||
memo: Option<&Bounds<'a>>,
|
||||
) -> (Rect, Option<usize>) {
|
||||
// A layer without children is quicker to work out again than to look up.
|
||||
let memo = memo.filter(|_| !n.children.is_empty());
|
||||
let key = (n.id, greek_scale.map(f64::to_bits));
|
||||
if let Some((r, levels)) = memo.and_then(|m| m.paint.borrow().get(&key).copied())
|
||||
&& depth + levels <= MAX_BOUNDS_DEPTH
|
||||
{
|
||||
return (r, Some(levels));
|
||||
}
|
||||
let reach = n.paint_reach();
|
||||
let mut r = n.local_rect().inflate(reach, reach);
|
||||
let mut levels = Some(0);
|
||||
let clips = n.clip && n.kind.is_frame_like();
|
||||
if (!clips || n.kind.bounds_follow_children()) && depth < 64 && !n.hides_contents() {
|
||||
for c in n.children.iter().filter_map(|c| doc.node(*c)) {
|
||||
if !c.visible {
|
||||
continue;
|
||||
if (!clips || n.kind.bounds_follow_children()) && !n.hides_contents() {
|
||||
if depth < MAX_BOUNDS_DEPTH {
|
||||
let mut below = Some(0);
|
||||
for c in n.children.iter().filter_map(|c| memo.map_or_else(|| doc.node(*c), |m| m.node(doc, *c))) {
|
||||
if !c.visible {
|
||||
continue;
|
||||
}
|
||||
let (cb, c_levels) = bounds_within(doc, c, depth + 1, greek_scale, memo);
|
||||
below = below.zip(c_levels).map(|(b, l)| b.max(l));
|
||||
r = r.union(figcraft_geom::transform_rect_bbox(c.transform, cb));
|
||||
}
|
||||
let cb = local_paint_bounds(doc, c, depth + 1, greek_scale);
|
||||
r = r.union(figcraft_geom::transform_rect_bbox(c.transform, cb));
|
||||
levels = below.map(|b| b + 1);
|
||||
} else if !n.children.is_empty() {
|
||||
levels = None;
|
||||
}
|
||||
}
|
||||
// Text can overflow its box.
|
||||
if let NodeKind::Text(_) = n.kind {
|
||||
r = r.union(text_box_bounds(n).inflate(reach, reach));
|
||||
if greek_scale.is_some_and(|s| lod::greeked(n, s)) {
|
||||
return r;
|
||||
}
|
||||
if let Some((l, dy)) = figcraft_text::layout_node(n)
|
||||
if !greek_scale.is_some_and(|s| lod::greeked(n, s))
|
||||
&& let Some((l, dy)) = figcraft_text::layout_node(n)
|
||||
&& let Some(ink) = l.ink_bounds()
|
||||
{
|
||||
r = r.union(ink + figcraft_geom::kurbo::Vec2::new(0.0, dy));
|
||||
}
|
||||
}
|
||||
r
|
||||
if let (Some(m), Some(l)) = (memo, levels) {
|
||||
m.paint.borrow_mut().insert(key, (r, l));
|
||||
}
|
||||
(r, levels)
|
||||
}
|
||||
|
||||
/// How far (document units) effects that read outside a node's paint bounds reach: blurs, glass,
|
||||
@@ -367,10 +458,17 @@ const MAX_EFFECT_MARGIN_PX: f64 = 2048.0;
|
||||
|
||||
/// Draw node `id` onto `pix`, where `parent_xf` maps the parent's space to `pix` pixels.
|
||||
pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, depth: usize) {
|
||||
if let Some(n) = ctx.doc.node(id) {
|
||||
draw_layer(ctx, pix, n, parent_xf, depth);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`draw_node`] for a node already looked up.
|
||||
fn draw_layer(ctx: &Ctx, pix: &mut Pixmap, n: &Node, parent_xf: Affine, depth: usize) {
|
||||
let id = n.id;
|
||||
if depth > crate::MAX_EXPORT_DEPTH || ctx.skip.contains(&id) {
|
||||
return;
|
||||
}
|
||||
let Some(n) = ctx.doc.node(id) else { return };
|
||||
let outline = ctx.outline_mode || ctx.outlined.contains(&id);
|
||||
if n.is_mask || n.opacity <= 0.0 && n.visible {
|
||||
return;
|
||||
@@ -385,7 +483,7 @@ pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, dep
|
||||
}
|
||||
let xf = parent_xf * n.transform;
|
||||
let (pw, ph) = (pix.width(), pix.height());
|
||||
let local = local_paint_bounds(ctx.doc, n, 0, ctx.lod.then_some(ctx.scale));
|
||||
let local = bounds_within(ctx.doc, n, 0, ctx.lod.then_some(ctx.scale), Some(ctx.bounds)).0;
|
||||
let dev = device_rect(xf, local);
|
||||
// Effects that read outside (blur) need a margin when culling. Absurd effects (a spread or
|
||||
// blur of millions of pixels) keep a sane layer: what lies further off reads as empty.
|
||||
@@ -398,7 +496,7 @@ pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, dep
|
||||
draw_outline(&Ctx { outline_mode: true, ..*ctx }, pix, n, xf, depth);
|
||||
return;
|
||||
}
|
||||
if ctx.lod && dev.width().max(dev.height()) < lod::TINY_PX && lod::splattable(ctx.doc, n, 0) {
|
||||
if ctx.lod && dev.width().max(dev.height()) < lod::TINY_PX && lod::splattable_within(ctx.doc, n, 0, Some(ctx.bounds)).0 {
|
||||
lod::splat_subtree(ctx, pix, n, xf, None, 1.0, depth);
|
||||
return;
|
||||
}
|
||||
@@ -672,13 +770,15 @@ fn draw_content(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, depth: usize,
|
||||
|
||||
fn draw_children(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, depth: usize) {
|
||||
let kids = &n.children;
|
||||
for run in ctx.doc.paint_runs(n) {
|
||||
let Some(&c) = kids.get(run.start) else { continue };
|
||||
if ctx.doc.node(c).is_some_and(|cn| cn.is_mask && cn.visible) {
|
||||
// Each child looked up once: in a large document a lookup is a good part of a layer's cost.
|
||||
let nodes: Vec<Option<&Node>> = kids.iter().map(|c| ctx.bounds.node(ctx.doc, *c)).collect();
|
||||
for run in Document::paint_runs_of(&nodes, n.layout.as_ref().is_some_and(|l| l.reverse_z)) {
|
||||
let (Some(&c), Some(Some(cn))) = (kids.get(run.start), nodes.get(run.start)) else { continue };
|
||||
if cn.is_mask && cn.visible {
|
||||
// Everything above the mask (until the next mask) is clipped by its alpha.
|
||||
draw_masked(ctx, pix, c, kids.get(run.start + 1..run.end).unwrap_or(&[]), xf, depth);
|
||||
} else {
|
||||
draw_node(ctx, pix, c, xf, depth + 1);
|
||||
draw_layer(ctx, pix, cn, xf, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ fn new_pixmap(w: u32, h: u32) -> Option<Pixmap> {
|
||||
/// The largest backdrop radius (background blur, glass: see [`figcraft_doc::Effect::backdrop_radius`])
|
||||
/// in the document (0 without any).
|
||||
pub(crate) fn max_backdrop_blur(doc: &Document) -> f64 {
|
||||
doc.nodes().flat_map(|n| n.effects.iter()).map(figcraft_doc::Effect::backdrop_radius).fold(0.0, f64::max)
|
||||
doc.max_backdrop_radius()
|
||||
}
|
||||
|
||||
/// How far (device pixels) a background blur of `radius` reads its backdrop at `scale`.
|
||||
@@ -170,6 +170,7 @@ pub fn render_view_single(doc: &Document, page: NodeId, view: &View, opts: &Opti
|
||||
pix.fill(paint::ts_color(bg));
|
||||
}
|
||||
let lod = opts.lod && !opts.outline_mode && view.scale <= lod::MAX_SCALE;
|
||||
let bounds = draw::Bounds::default();
|
||||
let ctx = draw::Ctx {
|
||||
doc,
|
||||
scale: view.scale,
|
||||
@@ -180,6 +181,7 @@ pub fn render_view_single(doc: &Document, page: NodeId, view: &View, opts: &Opti
|
||||
lod,
|
||||
under: None,
|
||||
time: 0.0,
|
||||
bounds: &bounds,
|
||||
};
|
||||
let xf = view.transform();
|
||||
let Some(p) = doc.node(page) else { return Some(pix) };
|
||||
@@ -201,6 +203,7 @@ pub fn render_nodes_into(doc: &Document, ids: &[NodeId], view: &View, pix: &mut
|
||||
|
||||
/// [`render_nodes_into`] `time` seconds into animated GIFs.
|
||||
fn nodes_into(doc: &Document, ids: &[NodeId], view: &View, pix: &mut Pixmap, time: f64) {
|
||||
let bounds = draw::Bounds::default();
|
||||
let ctx = draw::Ctx {
|
||||
doc,
|
||||
scale: view.scale,
|
||||
@@ -211,6 +214,7 @@ fn nodes_into(doc: &Document, ids: &[NodeId], view: &View, pix: &mut Pixmap, tim
|
||||
lod: false,
|
||||
under: None,
|
||||
time,
|
||||
bounds: &bounds,
|
||||
};
|
||||
for id in ids {
|
||||
draw::draw_node(&ctx, pix, *id, view.transform() * parent_transform(doc, *id), 0);
|
||||
@@ -434,6 +438,8 @@ mod band_tests;
|
||||
#[cfg(test)]
|
||||
mod boolean_cache_tests;
|
||||
#[cfg(test)]
|
||||
mod bounds_memo_tests;
|
||||
#[cfg(test)]
|
||||
mod effect_order_tests;
|
||||
#[cfg(test)]
|
||||
mod effects_behaviour_tests;
|
||||
|
||||
@@ -60,18 +60,47 @@ fn text_paints<'a>(n: &'a Node, t: &'a TextData) -> impl Iterator<Item = &'a Pai
|
||||
/// Whether the subtree of `n` can be splatted: no image or pattern paints (their colours aren't known
|
||||
/// without sampling them) and no masks (which change what their siblings show).
|
||||
pub fn splattable(doc: &Document, n: &Node, depth: usize) -> bool {
|
||||
splattable_within(doc, n, depth, None).0
|
||||
}
|
||||
|
||||
/// [`splattable`], kept in and read from `memo` when there is one (layers with children only).
|
||||
/// Also returns how many levels the walk went down, or `None` when the nesting cap decided it: such
|
||||
/// answers depend on the depth they were asked at and aren't kept, and kept answers are read only
|
||||
/// where their levels still fit under the cap.
|
||||
pub(crate) fn splattable_within<'a>(doc: &'a Document, n: &Node, depth: usize, memo: Option<&crate::draw::Bounds<'a>>) -> (bool, Option<usize>) {
|
||||
if depth > MAX_DEPTH {
|
||||
return false;
|
||||
return (false, None);
|
||||
}
|
||||
let memo = memo.filter(|_| !n.children.is_empty());
|
||||
if let Some((ok, levels)) = memo.and_then(|m| m.splat.borrow().get(&n.id).copied())
|
||||
&& depth + levels <= MAX_DEPTH
|
||||
{
|
||||
return (ok, Some(levels));
|
||||
}
|
||||
let image = |p: &Paint| p.visible && matches!(p.kind, PaintKind::Image(_) | PaintKind::Pattern(_));
|
||||
let own = match &n.kind {
|
||||
NodeKind::Text(t) => text_paints(n, t).any(image),
|
||||
_ => n.fills.iter().any(image),
|
||||
};
|
||||
if own || n.strokes.iter().any(image) {
|
||||
return false;
|
||||
let (mut ok, mut below) = (!(own || n.strokes.iter().any(image)), Some(0));
|
||||
if ok {
|
||||
for c in n.children.iter().filter_map(|c| memo.map_or_else(|| doc.node(*c), |m| m.node(doc, *c))).filter(|c| c.visible) {
|
||||
if c.is_mask {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
let (c_ok, c_levels) = splattable_within(doc, c, depth + 1, memo);
|
||||
below = below.zip(c_levels).map(|(b, l)| b.max(l + 1));
|
||||
if !c_ok {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
n.children.iter().filter_map(|c| doc.node(*c)).filter(|c| c.visible).all(|c| !c.is_mask && splattable(doc, c, depth + 1))
|
||||
if let (Some(m), Some(l)) = (memo, below) {
|
||||
m.splat.borrow_mut().insert(n.id, (ok, l));
|
||||
}
|
||||
(ok, below)
|
||||
}
|
||||
|
||||
/// A paint's average colour as straight RGBA (alpha times the paint's opacity); `None` for
|
||||
@@ -199,7 +228,7 @@ pub fn splat_subtree(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, clip: Op
|
||||
} else {
|
||||
clip
|
||||
};
|
||||
for c in n.paint_order().filter_map(|c| ctx.doc.node(c)) {
|
||||
for c in n.paint_order().filter_map(|c| ctx.bounds.node(ctx.doc, c)) {
|
||||
splat_subtree(ctx, pix, c, xf * c.transform, clip, alpha, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ fn exports_never_use_lod() {
|
||||
crate::render_nodes_into(&d, &[card], &v, &mut exact);
|
||||
assert_eq!(png.data(), exact.data());
|
||||
let mut preview = Pixmap::new(v.width, v.height).unwrap();
|
||||
let bounds = crate::draw::Bounds::default();
|
||||
let ctx = crate::draw::Ctx {
|
||||
doc: &d,
|
||||
scale,
|
||||
@@ -164,6 +165,7 @@ fn exports_never_use_lod() {
|
||||
lod: true,
|
||||
under: None,
|
||||
time: 0.0,
|
||||
bounds: &bounds,
|
||||
};
|
||||
let parent = d.world_transform(p);
|
||||
crate::draw::draw_node(&ctx, &mut preview, card, v.transform() * parent, 0);
|
||||
|
||||
@@ -9,7 +9,7 @@ use figcraft_doc::{Document, NodeId};
|
||||
use figcraft_geom::Affine;
|
||||
use tiny_skia::{FilterQuality, Pixmap, SpreadMode, Transform};
|
||||
|
||||
use crate::draw::{Ctx, draw_node};
|
||||
use crate::draw::{Bounds, Ctx, draw_node};
|
||||
use crate::paint::Prepared;
|
||||
|
||||
/// The largest side of a cell's pixmap; a cell showing larger is drawn coarser.
|
||||
@@ -46,6 +46,7 @@ pub fn cell_pixmap(doc: &Document, pt: &PatternPaint, w: f64, h: f64, scale: f64
|
||||
let (pw, ph) = ((cell.size.0 * k).ceil().clamp(1.0, MAX_CELL_PX), (cell.size.1 * k).ceil().clamp(1.0, MAX_CELL_PX));
|
||||
let (kx, ky) = (pw / cell.size.0, ph / cell.size.1);
|
||||
let mut pix = Pixmap::new(pw as u32, ph as u32)?;
|
||||
let bounds = Bounds::default();
|
||||
let ctx = Ctx {
|
||||
doc,
|
||||
scale: kx.max(ky) * cell.scale,
|
||||
@@ -56,6 +57,7 @@ pub fn cell_pixmap(doc: &Document, pt: &PatternPaint, w: f64, h: f64, scale: f64
|
||||
lod: false,
|
||||
under: None,
|
||||
time,
|
||||
bounds: &bounds,
|
||||
};
|
||||
let unrotated = src.transform.inverse();
|
||||
nested(|| {
|
||||
|
||||
Reference in New Issue
Block a user