mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 20:06:53 -06:00
Culling a layer asked for its paint bounds, which walk its whole subtree, and then did the same for each child: every layer's subtree was walked again at each level above it, and once more in each parallel band. The bounds worked out during a render are now kept for it (layers with children only; bounds the 64-level cap cut short are not kept, and kept ones are read only where their levels still fit under the cap, so culling is exactly as before). Every render also looked through every layer of the document for the largest background blur, to size the overlap of its bands: 900 000 layers on one large file, on the main thread before the bands start. The document now works it out when first asked after a change, like whether it has masks. A 1440x900 render on a 900 000-layer file, page of 118 358 layers: at 100% 13.0 -> 0.6 ms, at 25% 16.5 -> 3.4 ms, fitted to the page 56 -> 31 ms; page of 246 204 layers: 12.6 -> 2.5 ms, 12.1 -> 2.0 ms, 69 -> 62 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
79 lines
3.1 KiB
Rust
79 lines
3.1 KiB
Rust
//! Pattern fills: the source layer drawn into one repeating cell at the resolution it shows at,
|
||
//! repeated by a tiling shader.
|
||
|
||
use std::cell::Cell;
|
||
use std::sync::Arc;
|
||
|
||
use figcraft_color::{PatternCell, PatternPaint};
|
||
use figcraft_doc::{Document, NodeId};
|
||
use figcraft_geom::Affine;
|
||
use tiny_skia::{FilterQuality, Pixmap, SpreadMode, Transform};
|
||
|
||
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.
|
||
const MAX_CELL_PX: f64 = 2048.0;
|
||
/// How deep patterns nest (a source filled with a pattern); deeper ones, and a source filled
|
||
/// with its own pattern, draw nothing.
|
||
pub const MAX_DEPTH: u32 = 2;
|
||
|
||
thread_local! {
|
||
static DEPTH: Cell<u32> = const { Cell::new(0) };
|
||
}
|
||
|
||
/// Run `f` one pattern level deeper; `None` past [`MAX_DEPTH`].
|
||
pub fn nested<T>(f: impl FnOnce() -> Option<T>) -> Option<T> {
|
||
let d = DEPTH.with(Cell::get);
|
||
if d >= MAX_DEPTH {
|
||
return None;
|
||
}
|
||
DEPTH.with(|c| c.set(d + 1));
|
||
let out = f();
|
||
DEPTH.with(|c| c.set(d));
|
||
out
|
||
}
|
||
|
||
/// Pattern `pt` on a `w`×`h` box drawn into one cell at about `scale` device pixels per unit and
|
||
/// `time` seconds into animated GIFs:
|
||
/// the pixmap, the cell and the pixmap's pixels per unit on each axis (whole pixels per cell, so
|
||
/// the repeat lines up exactly).
|
||
pub fn cell_pixmap(doc: &Document, pt: &PatternPaint, w: f64, h: f64, scale: f64, time: f64) -> Option<(Pixmap, PatternCell, (f64, f64))> {
|
||
let src = doc.node(NodeId(pt.source)).filter(|n| !n.is_page())?;
|
||
let cell = pt.cell(src.width, src.height, w, h)?;
|
||
let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
|
||
let k = scale.min(MAX_CELL_PX / cell.size.0.max(cell.size.1));
|
||
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,
|
||
outline_mode: false,
|
||
outline: crate::Outline::default(),
|
||
outlined: &[],
|
||
skip: &[],
|
||
lod: false,
|
||
under: None,
|
||
time,
|
||
bounds: &bounds,
|
||
};
|
||
let unrotated = src.transform.inverse();
|
||
nested(|| {
|
||
for (tx, ty) in &cell.tiles {
|
||
let xf = Affine::scale_non_uniform(kx, ky) * Affine::translate((*tx, *ty)) * Affine::scale(cell.scale) * unrotated;
|
||
draw_node(&ctx, &mut pix, src.id, xf, 0);
|
||
}
|
||
Some(())
|
||
})?;
|
||
Some((pix, cell, (kx, ky)))
|
||
}
|
||
|
||
/// The shader for pattern `pt` on a `w`×`h` box shown at `scale` device pixels per unit.
|
||
pub fn prepare(doc: &Document, pt: &PatternPaint, w: f64, h: f64, scale: f64, time: f64, opacity: f32) -> Option<Prepared> {
|
||
let (pix, cell, (kx, ky)) = cell_pixmap(doc, pt, w, h, scale, time)?;
|
||
let transform = Transform::from_row((1.0 / kx) as f32, 0.0, 0.0, (1.0 / ky) as f32, cell.origin.0 as f32, cell.origin.1 as f32);
|
||
Some(Prepared::Pattern { pixmap: Arc::new(pix), mode: SpreadMode::Repeat, quality: FilterQuality::Bilinear, opacity, transform })
|
||
}
|