diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 71c6da39..07700e8c 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -77,17 +77,19 @@ pub enum DocError { pub type DocResult = Result; -/// 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, std::sync::OnceLock); +pub struct Dirty(pub std::collections::BTreeSet, std::sync::OnceLock, std::sync::OnceLock); 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 { &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> { - let kids = &n.children; - let node = |i: usize| kids.get(i).and_then(|k| self.node(*k)); + let kids: Vec> = 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> { + 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 diff --git a/crates/render/src/bounds_memo_tests.rs b/crates/render/src/bounds_memo_tests.rs new file mode 100644 index 00000000..4d0b305b --- /dev/null +++ b/crates/render/src/bounds_memo_tests.rs @@ -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::>(), 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::>(), 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); + } + } + } +} diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs index 4e99afb0..63b38220 100644 --- a/crates/render/src/draw.rs +++ b/crates/render/src/draw.rs @@ -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>, + pub(crate) splat: std::cell::RefCell>, + nodes: std::cell::RefCell>>, +} + +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 = HashMap>; + +/// 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); + +/// 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) -> Rect { +pub(crate) fn local_paint_bounds(doc: &Document, n: &Node, depth: usize, greek_scale: Option) -> 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, + memo: Option<&Bounds<'a>>, +) -> (Rect, Option) { + // 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> = 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); } } } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 9969a597..2a911076 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -90,7 +90,7 @@ fn new_pixmap(w: u32, h: u32) -> Option { /// 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; diff --git a/crates/render/src/lod.rs b/crates/render/src/lod.rs index 6d742585..a5b3ddc1 100644 --- a/crates/render/src/lod.rs +++ b/crates/render/src/lod.rs @@ -60,18 +60,47 @@ fn text_paints<'a>(n: &'a Node, t: &'a TextData) -> impl Iterator 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) { 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); } } diff --git a/crates/render/src/lod_tests.rs b/crates/render/src/lod_tests.rs index de8b52b4..a573ee7c 100644 --- a/crates/render/src/lod_tests.rs +++ b/crates/render/src/lod_tests.rs @@ -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); diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index bfcf8e32..2489766c 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -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(|| {