mirror of
https://github.com/penpot/penpot.git
synced 2026-06-26 09:12:06 +00:00
♻️ Refactor render structures
This commit is contained in:
parent
9805d97e45
commit
ddf9b4637b
@ -923,7 +923,7 @@ pub extern "C" fn get_shape_extrect(a: u32, b: u32, c: u32, d: u32) -> Result<*m
|
||||
let Some(shape) = state.shapes.get(&id) else {
|
||||
return Err(Error::CriticalError("Shape not found".to_string()));
|
||||
};
|
||||
let extrect = get_render_state().get_cached_extrect(shape, &state.shapes, 1.0);
|
||||
let extrect = shape.extrect(&state.shapes, 1.0);
|
||||
let mut buf = Vec::with_capacity(16);
|
||||
buf.extend_from_slice(&extrect.x().to_le_bytes());
|
||||
buf.extend_from_slice(&extrect.y().to_le_bytes());
|
||||
|
||||
@ -333,11 +333,7 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec<Segment> {
|
||||
let mut last_end = (first_bezier.end.x as f32, first_bezier.end.y as f32);
|
||||
let mut cur_end = first_bezier.end;
|
||||
|
||||
loop {
|
||||
let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src)
|
||||
else {
|
||||
break;
|
||||
};
|
||||
while let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src) {
|
||||
push_bezier(&mut result, &next_bezier);
|
||||
last_end = (next_bezier.end.x as f32, next_bezier.end.y as f32);
|
||||
cur_end = next_bezier.end;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
91
render-wasm/src/render/background_blur.rs
Normal file
91
render-wasm/src/render/background_blur.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use super::{RenderState, SurfaceId};
|
||||
use crate::shapes::{radius_to_sigma, Shape, Type};
|
||||
use skia_safe::{self as skia, RRect};
|
||||
|
||||
pub fn render_background_blur(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
target_surface: SurfaceId,
|
||||
) {
|
||||
if render_state.options.is_fast_mode() {
|
||||
return;
|
||||
}
|
||||
if matches!(shape.shape_type, Type::Text(_)) || matches!(shape.shape_type, Type::SVGRaw(_)) {
|
||||
return;
|
||||
}
|
||||
let blur = match shape
|
||||
.blur
|
||||
.filter(|b| !b.hidden && b.blur_type == crate::shapes::BlurType::BackgroundBlur)
|
||||
{
|
||||
Some(blur) => blur,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let scale = render_state.get_scale();
|
||||
let scaled_sigma = radius_to_sigma(blur.value * scale);
|
||||
let sigma = if render_state.export_context.is_some() {
|
||||
scaled_sigma
|
||||
} else {
|
||||
let margin = render_state.surfaces.margins().width as f32;
|
||||
let max_sigma = margin / 3.0;
|
||||
scaled_sigma.min(max_sigma)
|
||||
};
|
||||
|
||||
let blur_filter =
|
||||
match skia::image_filters::blur((sigma, sigma), skia::TileMode::Clamp, None, None) {
|
||||
Some(filter) => filter,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let target_surface_snapshot = render_state.surfaces.snapshot(target_surface);
|
||||
let translation = render_state
|
||||
.surfaces
|
||||
.get_render_context_translation(render_state.render_area, scale);
|
||||
|
||||
let center = shape.center();
|
||||
let mut matrix = shape.transform;
|
||||
matrix.post_translate(center);
|
||||
matrix.pre_translate(-center);
|
||||
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
canvas.save();
|
||||
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
canvas.concat(&matrix);
|
||||
|
||||
match &shape.shape_type {
|
||||
Type::Rect(data) if data.corners.is_some() => {
|
||||
let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap());
|
||||
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
Type::Frame(data) if data.corners.is_some() => {
|
||||
let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap());
|
||||
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
Type::Rect(_) | Type::Frame(_) => {
|
||||
canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
Type::Circle => {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.add_oval(shape.selrect, None, None);
|
||||
canvas.clip_path(&pb.detach(), skia::ClipOp::Intersect, true);
|
||||
}
|
||||
_ => {
|
||||
if let Some(path) = shape.get_skia_path() {
|
||||
canvas.clip_path(&path, skia::ClipOp::Intersect, true);
|
||||
} else {
|
||||
canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas.reset_matrix();
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_image_filter(blur_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::Src);
|
||||
canvas.draw_image(&target_surface_snapshot, (0, 0), Some(&paint));
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
148
render-wasm/src/render/cache.rs
Normal file
148
render-wasm/src/render/cache.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::tiles;
|
||||
|
||||
pub fn apply_render_to_final_canvas(render_state: &mut crate::render::RenderState) -> Result<()> {
|
||||
if render_state.options.is_interactive_transform() {
|
||||
let tile_rect = render_state.get_current_aligned_tile_bounds()?;
|
||||
render_state.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
render_state.background_color,
|
||||
super::surfaces::DrawOnCache::No,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if render_state.viewer_masked_pass() {
|
||||
let tile_rect = render_state.get_current_tile_bounds()?;
|
||||
render_state.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
render_state.background_color,
|
||||
super::surfaces::DrawOnCache::No,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let fast_mode = render_state.options.is_fast_mode();
|
||||
if !fast_mode && !render_state.cache_cleared_this_render {
|
||||
render_state
|
||||
.surfaces
|
||||
.clear_cache(render_state.background_color);
|
||||
render_state.cache_cleared_this_render = true;
|
||||
}
|
||||
let tile_rect = render_state.get_current_aligned_tile_bounds()?;
|
||||
|
||||
let current_tile = *render_state
|
||||
.current_tile
|
||||
.as_ref()
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
|
||||
render_state.surfaces.draw_current_tile_into_tile_atlas(
|
||||
&render_state.tile_viewbox,
|
||||
¤t_tile,
|
||||
&tile_rect,
|
||||
fast_mode,
|
||||
render_state.render_area,
|
||||
);
|
||||
|
||||
let rect = render_state.get_current_tile_bounds()?;
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_cached_tile_into_backbuffer(current_tile, &rect);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_from_cache(render_state: &mut crate::render::RenderState, shapes: ShapesPoolRef) {
|
||||
let _start = crate::performance::begin_timed_log!("render_from_cache");
|
||||
crate::performance::begin_measure!("render_from_cache");
|
||||
let bg_color = render_state.background_color;
|
||||
|
||||
if render_state.options.is_fast_mode() && !render_state.surfaces.atlas.is_empty() {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_atlas_to_backbuffer(render_state.viewbox, bg_color);
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
crate::performance::end_measure!("render_from_cache");
|
||||
crate::performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
|
||||
if render_state.cached_viewbox.area.width() > 0.0 {
|
||||
let navigate_zoom = render_state.viewbox.zoom / render_state.cached_viewbox.zoom;
|
||||
|
||||
let interest = render_state.options.dpr_viewport_interest_area_threshold;
|
||||
let tiles::TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(&render_state.cached_viewbox, interest);
|
||||
let offset_x = render_state.viewbox.area.left
|
||||
* render_state.cached_viewbox.zoom
|
||||
* render_state.options.dpr;
|
||||
let offset_y = render_state.viewbox.area.top
|
||||
* render_state.cached_viewbox.zoom
|
||||
* render_state.options.dpr;
|
||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||
|
||||
let zooming_out = render_state.viewbox.zoom < render_state.cached_viewbox.zoom;
|
||||
if zooming_out {
|
||||
let cache_dim = render_state.surfaces.cache_dimensions();
|
||||
let cache_w = cache_dim.width as f32;
|
||||
let cache_h = cache_dim.height as f32;
|
||||
|
||||
let vw = render_state.viewbox.dpr_width().max(1.0);
|
||||
let vh = render_state.viewbox.dpr_height().max(1.0);
|
||||
|
||||
let inv = if navigate_zoom.abs() > f32::EPSILON {
|
||||
1.0 / navigate_zoom
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let cx0 = -translate_x;
|
||||
let cy0 = -translate_y;
|
||||
let cx1 = (vw * inv) - translate_x;
|
||||
let cy1 = (vh * inv) - translate_y;
|
||||
|
||||
let min_x = cx0.min(cx1);
|
||||
let min_y = cy0.min(cy1);
|
||||
let max_x = cx0.max(cx1);
|
||||
let max_y = cy0.max(cy1);
|
||||
|
||||
let cache_covers = min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
|
||||
if !cache_covers && !render_state.surfaces.atlas.is_empty() {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_atlas_to_backbuffer(render_state.viewbox, bg_color);
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
crate::performance::end_measure!("render_from_cache");
|
||||
crate::performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
render_state.surfaces.draw_cache_to_backbuffer();
|
||||
|
||||
if !render_state.zoom_changed() {
|
||||
let visible_rect = tiles::get_tiles_for_viewbox(&render_state.viewbox);
|
||||
let offset = render_state.viewbox.get_offset();
|
||||
for tx in visible_rect.x1()..=visible_rect.x2() {
|
||||
for ty in visible_rect.y1()..=visible_rect.y2() {
|
||||
let tile = tiles::Tile::from(tx, ty);
|
||||
if render_state.surfaces.has_cached_tile_surface(tile) {
|
||||
let rect = tile.get_rect_with_offset(&offset);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_cached_tile_into_backbuffer(tile, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
}
|
||||
|
||||
crate::performance::end_measure!("render_from_cache");
|
||||
crate::performance::end_timed_log!("render_from_cache", _start);
|
||||
}
|
||||
@ -163,6 +163,10 @@ pub fn render_debug_shape(
|
||||
shape_selrect: Option<skia::Rect>,
|
||||
shape_extrect: Option<skia::Rect>,
|
||||
) {
|
||||
if shape_selrect.is_none() && shape_extrect.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
|
||||
330
render-wasm/src/render/drag_crop.rs
Normal file
330
render-wasm/src/render/drag_crop.rs
Normal file
@ -0,0 +1,330 @@
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
use super::RenderState;
|
||||
use super::SurfaceId;
|
||||
use crate::get_gpu_state;
|
||||
use crate::math;
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
pub struct InteractiveDragCrop {
|
||||
pub src_doc_bounds: Rect,
|
||||
pub src_selrect: Rect,
|
||||
/// Viewbox origin (doc-space) at capture time.
|
||||
pub capture_vb_left: f32,
|
||||
pub capture_vb_top: f32,
|
||||
/// Backbuffer pixel origin used for `snapshot_rect` (so we can do 1:1 blits).
|
||||
pub capture_src_left: i32,
|
||||
pub capture_src_top: i32,
|
||||
pub image: skia::Image,
|
||||
}
|
||||
|
||||
/// Chooses a window inside the full workspace-pixel crop `[0, out_w) × [0, out_h)` with each side
|
||||
/// at most `max_side_px` (**without scaling**): centered on the projection of
|
||||
/// `viewport_doc ∩ src_doc_bounds`, or on the full crop if that intersection is empty.
|
||||
/// `max_side_px` should match [`GpuState::max_texture_size`] (same budget as the atlas).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn drag_crop_snapshot_window_px(
|
||||
max_side_px: i32,
|
||||
out_w: i32,
|
||||
out_h: i32,
|
||||
viewport_doc: Rect,
|
||||
vb_left: f32,
|
||||
vb_top: f32,
|
||||
scale: f32,
|
||||
src_left_px: i32,
|
||||
src_top_px: i32,
|
||||
src_doc_bounds: Rect,
|
||||
) -> (i32, i32, i32, i32) {
|
||||
let cap = max_side_px.max(1);
|
||||
if out_w <= cap && out_h <= cap {
|
||||
return (0, 0, out_w, out_h);
|
||||
}
|
||||
let win_w = out_w.min(cap);
|
||||
let win_h = out_h.min(cap);
|
||||
|
||||
let mut vis = viewport_doc;
|
||||
let has_vis = vis.intersect(src_doc_bounds);
|
||||
let (cx, cy) = if !has_vis || vis.is_empty() {
|
||||
(out_w as f32 * 0.5, out_h as f32 * 0.5)
|
||||
} else {
|
||||
let lx0 = (vis.left - vb_left) * scale - src_left_px as f32;
|
||||
let ly0 = (vis.top - vb_top) * scale - src_top_px as f32;
|
||||
let lx1 = (vis.right - vb_left) * scale - src_left_px as f32;
|
||||
let ly1 = (vis.bottom - vb_top) * scale - src_top_px as f32;
|
||||
((lx0 + lx1) * 0.5, (ly0 + ly1) * 0.5)
|
||||
};
|
||||
|
||||
let mut ox = (cx - win_w as f32 * 0.5).round() as i32;
|
||||
let mut oy = (cy - win_h as f32 * 0.5).round() as i32;
|
||||
ox = ox.clamp(0, out_w - win_w);
|
||||
oy = oy.clamp(0, out_h - win_h);
|
||||
(ox, oy, win_w, win_h)
|
||||
}
|
||||
|
||||
/// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an
|
||||
/// interactive transform (drag/resize/rotate).
|
||||
///
|
||||
/// We only reuse cached pixels when it is safe and visually correct:
|
||||
/// - **Top-level only**: cache entries are built for direct children of the root.
|
||||
/// - **Moved node**: only allow cache reuse for *pure translations* (no scale/rotate/skew),
|
||||
/// because other transforms would require resampling and can diverge from the live render.
|
||||
/// - **Other cached nodes**: if the moving bounds overlap this cached crop, invalidate it so
|
||||
/// we don't show stale content while something moves over/inside it.
|
||||
pub fn should_use_cached_top_level_during_interactive(
|
||||
render_state: &mut RenderState,
|
||||
node_id: Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
moved_ids: &[Uuid],
|
||||
moved_bounds: Option<Rect>,
|
||||
) -> bool {
|
||||
if !render_state.backbuffer_crop_cache.contains_key(&node_id) {
|
||||
return false;
|
||||
}
|
||||
let Some(raw) = tree.get_raw(&node_id) else {
|
||||
return false;
|
||||
};
|
||||
if raw.parent_id != Some(Uuid::nil()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this top-level shape itself is being moved, always allow using its cached pixels.
|
||||
// BUT only for pure translations. For non-translation transforms (scale/rotate/skew),
|
||||
// cached pixels won't match the live result (and may require resampling), so render live.
|
||||
if moved_ids.contains(&node_id) {
|
||||
let Some(m) = tree.get_modifier(&node_id) else {
|
||||
return false;
|
||||
};
|
||||
// Only allow using the cached pixels for pure translations.
|
||||
// For non-translation transforms (scale/rotate/skew), cached pixels won't match.
|
||||
// If the transform is the identity means a reflow, we need to redraw as well.
|
||||
if math::identitish(m) || !math::is_move_only_matrix(m) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additionally require this node to be safe to serve from a rectangular backbuffer
|
||||
// crop while moving; otherwise it must be rendered live (e.g. text, overflow frames).
|
||||
return tree
|
||||
.get(&node_id)
|
||||
.is_some_and(|s| s.is_safe_for_drag_crop_cache(tree));
|
||||
}
|
||||
|
||||
// If the moving content overlaps this cached crop, do not use the cached pixels
|
||||
// for this frame. We intentionally keep the cache entry: overlap is typically
|
||||
// transient during drag, and once the moving content leaves the area the crop
|
||||
// becomes valid again (stationary shape unchanged).
|
||||
if let Some(moved) = moved_bounds {
|
||||
let intersects = render_state
|
||||
.backbuffer_crop_cache
|
||||
.get(&node_id)
|
||||
.is_some_and(|crop| moved.intersects(crop.src_doc_bounds));
|
||||
|
||||
if intersects {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn rebuild_backbuffer_crop_cache(render_state: &mut RenderState, tree: ShapesPoolRef) {
|
||||
render_state.backbuffer_crop_cache.clear();
|
||||
|
||||
// Collect candidate shapes that are "recortable" and visible in the current viewport.
|
||||
|
||||
// This is intentionally conservative; we only cache shapes that do not overlap with
|
||||
// ANY other candidate to guarantee the pixels under their bounds belong exclusively
|
||||
// to that shape in Backbuffer.
|
||||
let viewport = render_state.viewbox.area;
|
||||
let scale = render_state.get_scale();
|
||||
let mut candidates: Vec<(Uuid, Rect, Rect)> = Vec::new(); // (id, doc_bounds, selrect)
|
||||
|
||||
let root_ids: Vec<Uuid> = match tree.get(&Uuid::nil()) {
|
||||
Some(root) => root.children_ids(false),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
for shape_id in root_ids {
|
||||
let Some(shape) = tree.get(&shape_id) else {
|
||||
continue;
|
||||
};
|
||||
if shape.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let doc_bounds = shape.extrect(tree, 1.0);
|
||||
if !doc_bounds.intersects(viewport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also require selrect to be visible; used for drag delta placement.
|
||||
let selrect = shape.selrect();
|
||||
if !selrect.intersects(viewport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push((shape.id, doc_bounds, selrect));
|
||||
}
|
||||
|
||||
// Filter out any candidate that overlaps with any other candidate.
|
||||
// Sort by left edge so the inner loop can break early once no further
|
||||
// x-overlap is possible, reducing comparisons from O(N²) to O(N log N)
|
||||
// in typical layouts where shapes are spread out.
|
||||
candidates.sort_unstable_by(|a, b| {
|
||||
a.1.left
|
||||
.partial_cmp(&b.1.left)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let n = candidates.len();
|
||||
let mut is_overlapping = vec![false; n];
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
if candidates[j].1.left >= candidates[i].1.right {
|
||||
break; // sorted: no further x-overlap possible for i
|
||||
}
|
||||
if is_overlapping[i] && is_overlapping[j] {
|
||||
continue; // both already excluded, skip check
|
||||
}
|
||||
if candidates[i].1.intersects(candidates[j].1) {
|
||||
is_overlapping[i] = true;
|
||||
is_overlapping[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let non_overlapping: Vec<(Uuid, Rect, Rect)> = candidates
|
||||
.iter()
|
||||
.zip(is_overlapping.iter())
|
||||
.filter_map(|((id, bounds, selrect), ov)| {
|
||||
if !ov {
|
||||
Some((*id, *bounds, *selrect))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let vb_left = render_state.viewbox.area.left;
|
||||
let vb_top = render_state.viewbox.area.top;
|
||||
let (bb_w, bb_h) = render_state.surfaces.surface_size(SurfaceId::Backbuffer);
|
||||
let max_snap_px = get_gpu_state().max_texture_size();
|
||||
|
||||
// Snapshot the atlas once for the whole pass so that all shapes sharing
|
||||
// the tile/atlas fallback path reuse the same GPU image rather than each
|
||||
// triggering a separate `image_snapshot` flush.
|
||||
let atlas_snap = render_state.surfaces.atlas.snapshot_for_drag_crop();
|
||||
|
||||
// Scratch surface reused across all shapes that need the tile/atlas
|
||||
// fallback — avoids one WebGL texture allocation per shape.
|
||||
// Created lazily on first use and grown if a later shape needs more space.
|
||||
let mut scratch_surface: Option<skia::Surface> = None;
|
||||
|
||||
for (id, doc_bounds, selrect) in non_overlapping {
|
||||
let left = ((doc_bounds.left - vb_left) * scale).floor() as i32;
|
||||
let top = ((doc_bounds.top - vb_top) * scale).floor() as i32;
|
||||
let right = ((doc_bounds.right - vb_left) * scale).ceil() as i32;
|
||||
let bottom = ((doc_bounds.bottom - vb_top) * scale).ceil() as i32;
|
||||
if right <= left || bottom <= top {
|
||||
continue;
|
||||
}
|
||||
let src_irect = skia::IRect::new(left, top, right, bottom);
|
||||
|
||||
let src_doc_bounds = Rect::new(
|
||||
src_irect.left as f32 / scale + vb_left,
|
||||
src_irect.top as f32 / scale + vb_top,
|
||||
src_irect.right as f32 / scale + vb_left,
|
||||
src_irect.bottom as f32 / scale + vb_top,
|
||||
);
|
||||
|
||||
let full_w = src_irect.width();
|
||||
let full_h = src_irect.height();
|
||||
let (win_ox, win_oy, win_w, win_h) = drag_crop_snapshot_window_px(
|
||||
max_snap_px,
|
||||
full_w,
|
||||
full_h,
|
||||
viewport,
|
||||
vb_left,
|
||||
vb_top,
|
||||
scale,
|
||||
src_irect.left,
|
||||
src_irect.top,
|
||||
src_doc_bounds,
|
||||
);
|
||||
let window_irect = skia::IRect::new(
|
||||
src_irect.left + win_ox,
|
||||
src_irect.top + win_oy,
|
||||
src_irect.left + win_ox + win_w,
|
||||
src_irect.top + win_oy + win_h,
|
||||
);
|
||||
|
||||
let src_doc_window = Rect::new(
|
||||
window_irect.left as f32 / scale + vb_left,
|
||||
window_irect.top as f32 / scale + vb_top,
|
||||
window_irect.right as f32 / scale + vb_left,
|
||||
window_irect.bottom as f32 / scale + vb_top,
|
||||
);
|
||||
|
||||
let in_backbuffer = window_irect.left >= 0
|
||||
&& window_irect.top >= 0
|
||||
&& window_irect.right <= bb_w
|
||||
&& window_irect.bottom <= bb_h;
|
||||
|
||||
let backbuffer_snap = if in_backbuffer {
|
||||
render_state
|
||||
.surfaces
|
||||
.snapshot_rect(SurfaceId::Backbuffer, window_irect)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let image = if let Some(img) = backbuffer_snap {
|
||||
img
|
||||
} else {
|
||||
// Ensure the scratch surface is large enough for this window.
|
||||
// Grow (reallocate) only when necessary so that the common case
|
||||
// of similarly-sized shapes pays zero extra allocation cost.
|
||||
let needs_alloc = scratch_surface
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.width() < win_w || s.height() < win_h);
|
||||
if needs_alloc {
|
||||
scratch_surface = get_gpu_state()
|
||||
.create_surface_with_isize(
|
||||
"drag_crop_scratch".to_string(),
|
||||
skia::ISize::new(win_w, win_h),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
let Some(scratch) = scratch_surface.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let Some(img) = render_state
|
||||
.surfaces
|
||||
.try_snapshot_doc_rect_from_tiles_and_atlas(
|
||||
scratch,
|
||||
atlas_snap.as_ref(),
|
||||
src_doc_window,
|
||||
window_irect,
|
||||
win_w,
|
||||
win_h,
|
||||
vb_left,
|
||||
vb_top,
|
||||
scale,
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
img
|
||||
};
|
||||
|
||||
render_state.backbuffer_crop_cache.insert(
|
||||
id,
|
||||
InteractiveDragCrop {
|
||||
src_doc_bounds: src_doc_window,
|
||||
src_selrect: selrect,
|
||||
capture_vb_left: vb_left,
|
||||
capture_vb_top: vb_top,
|
||||
capture_src_left: window_irect.left,
|
||||
capture_src_top: window_irect.top,
|
||||
image,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
357
render-wasm/src/render/drop_shadow.rs
Normal file
357
render-wasm/src/render/drop_shadow.rs
Normal file
@ -0,0 +1,357 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use skia_safe as skia;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::shapes::{radius_to_sigma, Blur, Fill, Shadow, Shape, SolidColor, Type};
|
||||
use crate::state::ShapesPoolRef;
|
||||
|
||||
use super::{
|
||||
filters, get_simplified_children, layer_blur, ClipStack, NodeRenderState, RenderState,
|
||||
SurfaceId,
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_drop_black_shadow(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
shape_bounds: &skia::Rect,
|
||||
shadow: &Shadow,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
scale: f32,
|
||||
extra_layer_blur: Option<Blur>,
|
||||
target_surface: SurfaceId,
|
||||
) -> Result<()> {
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().offset = (0.0, 0.0);
|
||||
transformed_shadow.to_mut().color = skia::Color::BLACK;
|
||||
|
||||
let mut plain_shape = Cow::Borrowed(shape);
|
||||
let combined_blur = layer_blur::combine_blur_values(
|
||||
layer_blur::combined_layer_blur(
|
||||
&render_state.nested_blurs,
|
||||
&mut render_state.cached_layer_blur,
|
||||
shape.blur,
|
||||
),
|
||||
extra_layer_blur,
|
||||
);
|
||||
let blur_filter = combined_blur.and_then(|blur| {
|
||||
let sigma = blur.sigma();
|
||||
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||
});
|
||||
|
||||
let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none();
|
||||
let mut transform_matrix = shape.transform;
|
||||
let center = shape.center();
|
||||
transform_matrix.post_translate(center);
|
||||
transform_matrix.pre_translate(-center);
|
||||
|
||||
let mapped = transform_matrix.map_vector((shadow.offset.0, shadow.offset.1));
|
||||
let world_offset = (mapped.x, mapped.y);
|
||||
|
||||
let plain_shape_mut = plain_shape.to_mut();
|
||||
plain_shape_mut.clear_fills();
|
||||
if shape.has_fills() {
|
||||
plain_shape_mut.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK)));
|
||||
}
|
||||
|
||||
for stroke in plain_shape_mut.strokes.iter_mut() {
|
||||
stroke.fill = Fill::Solid(SolidColor(skia::Color::BLACK));
|
||||
}
|
||||
|
||||
plain_shape_mut.clear_shadows();
|
||||
plain_shape_mut.blur = None;
|
||||
|
||||
plain_shape_mut.clip_content = false;
|
||||
|
||||
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
|
||||
bounds.offset(world_offset);
|
||||
if !bounds.intersects(render_state.render_area_with_margins)
|
||||
&& target_surface != SurfaceId::Export
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if scale > 1.0 && shadow.blur <= 0.0 {
|
||||
let drop_canvas = render_state.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save();
|
||||
|
||||
render_state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let blur_only_filter = if transformed_shadow.blur > 0.0 {
|
||||
let sigma = radius_to_sigma(transformed_shadow.blur);
|
||||
Some(skia::image_filters::blur((sigma, sigma), None, None, None))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
if let Some(blur_filter) = blur_only_filter {
|
||||
shadow_paint.set_image_filter(blur_filter);
|
||||
}
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
|
||||
if use_low_zoom_path {
|
||||
let drop_canvas = render_state.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save_layer(&layer_rec);
|
||||
|
||||
render_state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let blur_downscale_threshold: f32 = render_state.options.blur_downscale_threshold;
|
||||
let min_blur_downscale: f32 = 1.0 / blur_downscale_threshold;
|
||||
let blur_downscale = if shadow.blur > blur_downscale_threshold {
|
||||
(blur_downscale_threshold / shadow.blur).max(min_blur_downscale)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let filter_result = filters::render_into_filter_surface(
|
||||
render_state,
|
||||
bounds,
|
||||
blur_downscale,
|
||||
|state, temp_surface| {
|
||||
let canvas = state.surfaces.canvas(temp_surface);
|
||||
canvas.save_layer(&layer_rec);
|
||||
|
||||
state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
|
||||
state.surfaces.canvas(temp_surface).restore();
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Some((mut surface, filter_scale)) = filter_result {
|
||||
let drop_canvas = render_state.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save();
|
||||
let mut drop_paint = skia::Paint::default();
|
||||
drop_paint.set_image_filter(blur_filter.clone());
|
||||
|
||||
if filter_scale < 1.0 {
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale));
|
||||
drop_canvas.translate((bounds.left * filter_scale, bounds.top * filter_scale));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
render_state.sampling_options,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
} else {
|
||||
drop_canvas.save();
|
||||
drop_canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
render_state.sampling_options,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
}
|
||||
drop_canvas.restore();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_element_drop_shadows_and_composite(
|
||||
render_state: &mut RenderState,
|
||||
element: &Shape,
|
||||
tree: ShapesPoolRef,
|
||||
extrect: &mut Option<skia::Rect>,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
scale: f32,
|
||||
node_render_state: &NodeRenderState,
|
||||
target_surface: SurfaceId,
|
||||
) -> Result<()> {
|
||||
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
||||
let inherited_layer_blur = match element.shape_type {
|
||||
Type::Frame(_) | Type::Group(_) => element.blur,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
render_drop_black_shadow(
|
||||
render_state,
|
||||
element,
|
||||
element_extrect,
|
||||
shadow,
|
||||
clip_bounds.clone(),
|
||||
scale,
|
||||
None,
|
||||
target_surface,
|
||||
)?;
|
||||
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
let mut shadow_children = Vec::new();
|
||||
if element.is_recursive() {
|
||||
get_simplified_children(tree, element, &mut shadow_children);
|
||||
}
|
||||
|
||||
for shadow_shape_id in shadow_children.iter() {
|
||||
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
|
||||
continue;
|
||||
};
|
||||
if shadow_shape.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nested_clip_bounds =
|
||||
node_render_state.get_nested_shadow_clip_bounds(element, shadow);
|
||||
|
||||
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
||||
render_drop_black_shadow(
|
||||
render_state,
|
||||
shadow_shape,
|
||||
&shadow_shape.extrect(tree, scale),
|
||||
shadow,
|
||||
nested_clip_bounds,
|
||||
scale,
|
||||
inherited_layer_blur,
|
||||
target_surface,
|
||||
)?;
|
||||
} else {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().color = skia::Color::BLACK;
|
||||
transformed_shadow.to_mut().blur = transformed_shadow.blur;
|
||||
transformed_shadow.to_mut().spread = transformed_shadow.spread;
|
||||
|
||||
let mut new_shadow_paint = skia::Paint::default();
|
||||
new_shadow_paint.set_image_filter(transformed_shadow.get_drop_shadow_filter());
|
||||
new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
render_state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
shadow_shape,
|
||||
nested_clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
true,
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
None,
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(shadow.color);
|
||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.draw_paint(&paint);
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
}
|
||||
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
let antialias = !render_state.options.is_fast_mode()
|
||||
&& element.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
render_state.surfaces.canvas(target_surface).save();
|
||||
render_state.clip_target_surface_to_stack(clips, target_surface, scale, antialias);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
render_state.surfaces.canvas(target_surface).restore();
|
||||
} else {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
}
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
Ok(())
|
||||
}
|
||||
198
render-wasm/src/render/enter_exit.rs
Normal file
198
render-wasm/src/render/enter_exit.rs
Normal file
@ -0,0 +1,198 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use skia_safe as skia;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::shapes::{radius_to_sigma, Shape, StrokeKind, Type};
|
||||
|
||||
use super::layer_blur;
|
||||
use super::{ClipStack, NodeRenderState, RenderState, SurfaceId};
|
||||
|
||||
pub fn render_shape_enter(
|
||||
render_state: &mut RenderState,
|
||||
element: &Shape,
|
||||
mask: bool,
|
||||
clip_bounds: Option<&ClipStack>,
|
||||
target_surface: SurfaceId,
|
||||
) {
|
||||
if let Type::Group(group) = element.shape_type {
|
||||
let fills = &element.fills;
|
||||
let shadows = &element.shadows;
|
||||
render_state.nested_fills.push(fills.to_vec());
|
||||
render_state.nested_shadows.push(shadows.to_vec());
|
||||
|
||||
if group.masked {
|
||||
let mask_group_blur = element.masked_group_layer_blur().is_some();
|
||||
if mask_group_blur {
|
||||
render_state.surfaces.canvas(target_surface).save();
|
||||
if let Some(clips) = clip_bounds {
|
||||
let scale = render_state.get_scale();
|
||||
let antialias = !render_state.options.is_fast_mode()
|
||||
&& element
|
||||
.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
render_state.clip_target_surface_to_stack(
|
||||
clips,
|
||||
target_surface,
|
||||
scale,
|
||||
antialias,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
if !render_state.options.is_fast_mode() {
|
||||
if let Some(blur) = element.masked_group_layer_blur() {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(blur.value * scale);
|
||||
if let Some(filter) =
|
||||
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||
{
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(target_surface)
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
}
|
||||
|
||||
if let Type::Frame(_) = element.shape_type {
|
||||
render_state.nested_fills.push(Vec::new());
|
||||
}
|
||||
|
||||
if mask {
|
||||
let mut mask_paint = skia::Paint::default();
|
||||
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
||||
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(target_surface)
|
||||
.save_layer(&mask_rec);
|
||||
}
|
||||
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
if needs_layer {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
if !render_state.options.is_fast_mode() {
|
||||
if let Some(frame_blur) = layer_blur::frame_clip_layer_blur(element) {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(frame_blur.value * scale);
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(target_surface)
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
|
||||
render_state.focus_mode.enter(&element.id);
|
||||
}
|
||||
|
||||
pub fn render_shape_exit(
|
||||
render_state: &mut RenderState,
|
||||
element: &Shape,
|
||||
visited_mask: bool,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
target_surface: SurfaceId,
|
||||
) -> Result<()> {
|
||||
if visited_mask {
|
||||
if let Type::Group(group) = element.shape_type {
|
||||
if group.masked {
|
||||
render_state.surfaces.canvas(target_surface).restore();
|
||||
}
|
||||
}
|
||||
} else if let Type::Group(group) = element.shape_type {
|
||||
if group.masked {
|
||||
render_state.pending_nodes.push(NodeRenderState {
|
||||
id: element.id,
|
||||
visited_children: true,
|
||||
clip_bounds: None,
|
||||
visited_mask: true,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
});
|
||||
if let Some(&mask_id) = element.mask_id() {
|
||||
render_state.pending_nodes.push(NodeRenderState {
|
||||
id: mask_id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: true,
|
||||
flattened: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match element.shape_type {
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
render_state.nested_fills.pop();
|
||||
render_state.nested_blurs.pop();
|
||||
render_state.cached_layer_blur = None;
|
||||
render_state.nested_shadows.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let needs_exit_strokes = render_state.focus_mode.is_active()
|
||||
&& (element.clip()
|
||||
|| (matches!(element.shape_type, Type::Frame(_)) && element.has_inner_stroke()));
|
||||
|
||||
if needs_exit_strokes {
|
||||
let mut element_strokes: Cow<Shape> = Cow::Borrowed(element);
|
||||
element_strokes.to_mut().clear_fills();
|
||||
element_strokes.to_mut().clear_shadows();
|
||||
element_strokes.to_mut().clip_content = false;
|
||||
|
||||
if !element.clip() {
|
||||
let is_open = element.is_open();
|
||||
element_strokes
|
||||
.to_mut()
|
||||
.strokes
|
||||
.retain(|s| s.render_kind(is_open) == StrokeKind::Inner);
|
||||
}
|
||||
|
||||
if layer_blur::frame_clip_layer_blur(element).is_some() {
|
||||
element_strokes.to_mut().set_blur(None);
|
||||
}
|
||||
render_state.render_shape(
|
||||
&element_strokes,
|
||||
clip_bounds,
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
SurfaceId::TextDropShadows,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
target_surface,
|
||||
)?;
|
||||
}
|
||||
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
if needs_layer {
|
||||
render_state.surfaces.canvas(target_surface).restore();
|
||||
}
|
||||
|
||||
if visited_mask && element.masked_group_layer_blur().is_some() {
|
||||
render_state.surfaces.canvas(target_surface).restore();
|
||||
}
|
||||
|
||||
render_state.focus_mode.exit(&element.id);
|
||||
Ok(())
|
||||
}
|
||||
98
render-wasm/src/render/export.rs
Normal file
98
render-wasm/src/render/export.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use crate::error::Result;
|
||||
use crate::get_gpu_state;
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
use skia_safe as skia;
|
||||
|
||||
use super::{NodeRenderState, RenderState, SurfaceId};
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
render_state: &mut RenderState,
|
||||
id: &Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
timestamp: i32,
|
||||
) -> Result<(Vec<u8>, i32, i32)> {
|
||||
let target_surface = SurfaceId::Export;
|
||||
|
||||
let saved_focus_mode = render_state.focus_mode.clone();
|
||||
let saved_export_context = render_state.export_context;
|
||||
let saved_render_area = render_state.render_area;
|
||||
let saved_render_area_with_margins = render_state.render_area_with_margins;
|
||||
let saved_current_tile = render_state.current_tile;
|
||||
let saved_pending_nodes = std::mem::take(&mut render_state.pending_nodes);
|
||||
let saved_nested_fills = std::mem::take(&mut render_state.nested_fills);
|
||||
let saved_nested_blurs = std::mem::take(&mut render_state.nested_blurs);
|
||||
let saved_nested_shadows = std::mem::take(&mut render_state.nested_shadows);
|
||||
let saved_ignore_nested_blurs = render_state.ignore_nested_blurs;
|
||||
let saved_preview_mode = render_state.preview_mode;
|
||||
|
||||
render_state.focus_mode.clear();
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(target_surface)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
if tree.len() != 0 {
|
||||
let Some(shape) = tree.get(id) else {
|
||||
return Ok((Vec::new(), 0, 0));
|
||||
};
|
||||
let mut extrect = shape.extrect(tree, scale);
|
||||
render_state.export_context = Some((extrect, scale));
|
||||
let margins = render_state.surfaces.margins;
|
||||
extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale));
|
||||
|
||||
render_state.surfaces.resize_export_surface(scale, extrect);
|
||||
render_state.render_area = extrect;
|
||||
render_state.render_area_with_margins = extrect;
|
||||
render_state.surfaces.update_render_context(extrect, scale);
|
||||
|
||||
render_state.pending_nodes.push(NodeRenderState {
|
||||
id: *id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
});
|
||||
render_state.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
|
||||
}
|
||||
|
||||
render_state.export_context = None;
|
||||
|
||||
render_state.surfaces.flush_and_submit(target_surface);
|
||||
|
||||
let image = render_state.surfaces.snapshot(target_surface);
|
||||
let data = image
|
||||
.encode(
|
||||
Some(&mut get_gpu_state().context),
|
||||
skia::EncodedImageFormat::PNG,
|
||||
100,
|
||||
)
|
||||
.expect("PNG encode failed");
|
||||
let skia::ISize { width, height } = image.dimensions();
|
||||
|
||||
render_state.focus_mode = saved_focus_mode;
|
||||
render_state.export_context = saved_export_context;
|
||||
render_state.render_area = saved_render_area;
|
||||
render_state.render_area_with_margins = saved_render_area_with_margins;
|
||||
render_state.current_tile = saved_current_tile;
|
||||
render_state.pending_nodes = saved_pending_nodes;
|
||||
render_state.nested_fills = saved_nested_fills;
|
||||
render_state.nested_blurs = saved_nested_blurs;
|
||||
render_state.nested_shadows = saved_nested_shadows;
|
||||
render_state.ignore_nested_blurs = saved_ignore_nested_blurs;
|
||||
render_state.preview_mode = saved_preview_mode;
|
||||
|
||||
let workspace_scale = render_state.get_scale();
|
||||
if let Some(tile) = render_state.current_tile {
|
||||
render_state.update_render_context(tile);
|
||||
} else if !render_state.render_area.is_empty() {
|
||||
render_state
|
||||
.surfaces
|
||||
.update_render_context(render_state.render_area, workspace_scale);
|
||||
}
|
||||
|
||||
Ok((data.as_bytes().to_vec(), width, height))
|
||||
}
|
||||
@ -27,13 +27,18 @@ fn draw_image_fill(
|
||||
|
||||
let mut image_paint = skia::Paint::default();
|
||||
image_paint.set_anti_alias(antialias);
|
||||
if let Some(filter) = shape.image_filter(1.) {
|
||||
let filter = shape.image_filter(1.);
|
||||
if let Some(ref filter) = filter {
|
||||
image_paint.set_image_filter(filter.clone());
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
|
||||
// Save the current canvas state
|
||||
canvas.save_layer(&layer_rec);
|
||||
let has_image_filter = filter.is_some();
|
||||
if has_image_filter {
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
} else {
|
||||
canvas.save();
|
||||
}
|
||||
|
||||
// Set the clipping rectangle to the container bounds
|
||||
match &shape.shape_type {
|
||||
@ -87,7 +92,6 @@ fn draw_image_fill(
|
||||
paint,
|
||||
);
|
||||
|
||||
// Restore the canvas to remove the clipping
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
|
||||
@ -119,11 +119,13 @@ where
|
||||
|
||||
{
|
||||
let canvas = render_state.surfaces.canvas(filter_id);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
canvas.save();
|
||||
// Apply scale first, then translate
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate((-bounds.left, -bounds.top));
|
||||
|
||||
let scaled_bounds = Rect::new(bounds.left, bounds.top, bounds.right, bounds.bottom);
|
||||
canvas.clip_rect(scaled_bounds, skia::ClipOp::Intersect, false);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
}
|
||||
|
||||
draw_fn(render_state, filter_id)?;
|
||||
|
||||
53
render-wasm/src/render/focus_mode.rs
Normal file
53
render-wasm/src/render/focus_mode.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FocusMode {
|
||||
shapes: HashSet<Uuid>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl FocusMode {
|
||||
pub fn new() -> Self {
|
||||
FocusMode {
|
||||
shapes: HashSet::new(),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.shapes.clear();
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
pub fn set_shapes(&mut self, shapes: Vec<Uuid>) {
|
||||
self.shapes = shapes.into_iter().collect();
|
||||
}
|
||||
|
||||
/// Returns `true` if the given shape ID should be focused.
|
||||
/// If the `shapes` list is empty, focus applies to all shapes.
|
||||
pub fn should_focus(&self, id: &Uuid) -> bool {
|
||||
self.shapes.is_empty() || self.shapes.contains(id)
|
||||
}
|
||||
|
||||
pub fn enter(&mut self, id: &Uuid) {
|
||||
if !self.active && self.should_focus(id) {
|
||||
self.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit(&mut self, id: &Uuid) {
|
||||
if self.active && self.should_focus(id) {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ pub struct FontStore {
|
||||
debug_font: Font,
|
||||
ui_font: Font,
|
||||
fallback_fonts: HashSet<String>,
|
||||
registered_families: HashSet<String>,
|
||||
}
|
||||
|
||||
impl FontStore {
|
||||
@ -55,6 +56,7 @@ impl FontStore {
|
||||
debug_font,
|
||||
ui_font,
|
||||
fallback_fonts: HashSet::new(),
|
||||
registered_families: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -105,7 +107,7 @@ impl FontStore {
|
||||
};
|
||||
|
||||
self.font_provider.register_typeface(typeface, font_name);
|
||||
self.font_collection.clear_caches();
|
||||
self.registered_families.insert(font_name.to_string());
|
||||
|
||||
if is_fallback {
|
||||
self.fallback_fonts.insert(alias);
|
||||
@ -121,13 +123,17 @@ impl FontStore {
|
||||
} else {
|
||||
alias.as_str()
|
||||
};
|
||||
self.font_provider.family_names().any(|x| x == font_name)
|
||||
self.registered_families.contains(font_name)
|
||||
}
|
||||
|
||||
pub fn get_fallback(&self) -> &HashSet<String> {
|
||||
&self.fallback_fonts
|
||||
}
|
||||
|
||||
pub fn flush_caches(&mut self) {
|
||||
self.font_collection.clear_caches();
|
||||
}
|
||||
|
||||
pub fn get_emoji_font(&self, _size: f32) -> Option<Font> {
|
||||
None
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ use crate::error::Result;
|
||||
use crate::get_gpu_state;
|
||||
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
|
||||
use skia_safe::{self as skia, Codec, ISize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type Image = skia::Image;
|
||||
@ -60,8 +61,8 @@ enum StoredImage {
|
||||
}
|
||||
|
||||
pub struct ImageStore {
|
||||
images: HashMap<(Uuid, bool), StoredImage>,
|
||||
context: Box<DirectContext>,
|
||||
images: RefCell<HashMap<(Uuid, bool), StoredImage>>,
|
||||
context: RefCell<Box<DirectContext>>,
|
||||
}
|
||||
|
||||
/// Creates a Skia image from an existing WebGL texture.
|
||||
@ -148,8 +149,8 @@ impl ImageStore {
|
||||
let gpu_state = get_gpu_state();
|
||||
let context = &gpu_state.context;
|
||||
Self {
|
||||
images: HashMap::with_capacity(2048),
|
||||
context: Box::new(context.clone()),
|
||||
images: RefCell::new(HashMap::new()),
|
||||
context: RefCell::new(Box::new(context.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,16 +162,18 @@ impl ImageStore {
|
||||
) -> crate::error::Result<()> {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
if self.images.borrow().contains_key(&key) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let raw_data = image_data.to_vec();
|
||||
|
||||
if let Some(gpu_image) = decode_image(&mut self.context, &raw_data) {
|
||||
self.images.insert(key, StoredImage::Gpu(gpu_image));
|
||||
if let Some(gpu_image) = decode_image(&mut self.context.borrow_mut(), image_data) {
|
||||
self.images
|
||||
.borrow_mut()
|
||||
.insert(key, StoredImage::Gpu(gpu_image));
|
||||
} else {
|
||||
self.images.insert(key, StoredImage::Raw(raw_data));
|
||||
self.images
|
||||
.borrow_mut()
|
||||
.insert(key, StoredImage::Raw(image_data.to_vec()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -188,51 +191,49 @@ impl ImageStore {
|
||||
) -> Result<()> {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
if self.images.borrow().contains_key(&key) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a Skia image from the existing GL texture
|
||||
let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?;
|
||||
self.images.insert(key, StoredImage::Gpu(image));
|
||||
let image = create_image_from_gl_texture(
|
||||
&mut self.context.borrow_mut(),
|
||||
texture_id,
|
||||
width,
|
||||
height,
|
||||
)?;
|
||||
self.images
|
||||
.borrow_mut()
|
||||
.insert(key, StoredImage::Gpu(image));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contains(&self, id: &Uuid, is_thumbnail: bool) -> bool {
|
||||
self.images.contains_key(&(*id, is_thumbnail))
|
||||
self.images.borrow().contains_key(&(*id, is_thumbnail))
|
||||
}
|
||||
|
||||
pub fn get(&mut self, id: &Uuid) -> Option<&Image> {
|
||||
// Try to get full image first, fallback to thumbnail
|
||||
let has_full = self.images.contains_key(&(*id, false));
|
||||
if has_full {
|
||||
self.get_internal(id, false)
|
||||
} else {
|
||||
self.get_internal(id, true)
|
||||
}
|
||||
pub fn get(&mut self, id: &Uuid) -> Option<Image> {
|
||||
self.get_internal(id, false)
|
||||
.or_else(|| self.get_internal(id, true))
|
||||
}
|
||||
|
||||
pub fn get_cpu_image(&mut self, id: &Uuid) -> Option<Image> {
|
||||
let gpu_image = self.get(id)?.clone();
|
||||
gpu_image.make_non_texture_image(self.context.as_mut())
|
||||
let gpu_image = self.get(id)?;
|
||||
gpu_image.make_non_texture_image(self.context.borrow_mut().as_mut())
|
||||
}
|
||||
|
||||
fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> {
|
||||
fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<Image> {
|
||||
let key = (*id, is_thumbnail);
|
||||
// Use entry API to mutate the HashMap in-place if needed
|
||||
if let Some(entry) = self.images.get_mut(&key) {
|
||||
let mut images = self.images.borrow_mut();
|
||||
if let Some(entry) = images.get_mut(&key) {
|
||||
match entry {
|
||||
StoredImage::Gpu(ref img) => Some(img),
|
||||
StoredImage::Gpu(ref img) => Some(img.clone()),
|
||||
StoredImage::Raw(raw_data) => {
|
||||
let gpu_image = decode_image(&mut self.context, raw_data)?;
|
||||
let gpu_image = decode_image(&mut self.context.borrow_mut(), raw_data)?;
|
||||
let clone = gpu_image.clone();
|
||||
*entry = StoredImage::Gpu(gpu_image);
|
||||
|
||||
if let StoredImage::Gpu(ref img) = entry {
|
||||
Some(img)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(clone)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
58
render-wasm/src/render/layer_blur.rs
Normal file
58
render-wasm/src/render/layer_blur.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use crate::shapes::{Blur, BlurType, Shape};
|
||||
|
||||
/// Combines every visible layer blur currently active (ancestors + shape)
|
||||
/// into a single equivalent blur. Layer blur radii compound by adding their
|
||||
/// variances (σ² = radius²), so we:
|
||||
/// 1. Convert each blur radius into variance via `blur_variance`.
|
||||
/// 2. Sum all variances.
|
||||
/// 3. Convert the total variance back to a radius with `blur_from_variance`.
|
||||
///
|
||||
/// This keeps blur math consistent everywhere we need to merge blur sources.
|
||||
pub fn combined_layer_blur(
|
||||
nested_blurs: &[Option<Blur>],
|
||||
cached_layer_blur: &mut Option<Option<Blur>>,
|
||||
shape_blur: Option<Blur>,
|
||||
) -> Option<Blur> {
|
||||
if let Some(ref cached) = cached_layer_blur {
|
||||
return *cached;
|
||||
}
|
||||
let mut total = 0.;
|
||||
|
||||
for nested_blur in nested_blurs.iter().flatten() {
|
||||
total += blur_variance(Some(*nested_blur));
|
||||
}
|
||||
|
||||
total += blur_variance(shape_blur);
|
||||
|
||||
let result = blur_from_variance(total);
|
||||
*cached_layer_blur = Some(result);
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns the variance (radius²) for a visible layer blur, or zero if the
|
||||
/// blur is hidden/absent. Working in variance space lets us add multiple
|
||||
/// blur radii correctly.
|
||||
pub fn blur_variance(blur: Option<Blur>) -> f32 {
|
||||
match blur {
|
||||
Some(blur) if !blur.hidden && blur.blur_type == BlurType::LayerBlur => blur.value.powi(2),
|
||||
_ => 0.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a blur from an accumulated variance value. If no variance was
|
||||
/// contributed, we return `None`; otherwise the equivalent single radius is
|
||||
/// `sqrt(total)`.
|
||||
pub fn blur_from_variance(total: f32) -> Option<Blur> {
|
||||
(total > 0.).then(|| Blur::new(BlurType::LayerBlur, false, total.sqrt()))
|
||||
}
|
||||
|
||||
/// Convenience helper to merge two optional layer blurs using the same
|
||||
/// variance math as `combined_layer_blur`.
|
||||
pub fn combine_blur_values(base: Option<Blur>, extra: Option<Blur>) -> Option<Blur> {
|
||||
let total = blur_variance(base) + blur_variance(extra);
|
||||
blur_from_variance(total)
|
||||
}
|
||||
|
||||
pub fn frame_clip_layer_blur(shape: &Shape) -> Option<Blur> {
|
||||
shape.frame_clip_layer_blur()
|
||||
}
|
||||
@ -297,13 +297,14 @@ pub(super) fn handle_stroke_caps(
|
||||
blur: Option<&ImageFilter>,
|
||||
_antialias: bool,
|
||||
) {
|
||||
// Closed shapes don't have caps
|
||||
if !is_open {
|
||||
return;
|
||||
}
|
||||
|
||||
// When both ends share the same simple line cap, Skia already drew it
|
||||
// natively via `PaintCap` on the stroke paint, so skip the manual overlay.
|
||||
if stroke.cap_start.is_none() && stroke.cap_end.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if stroke.to_skia_linecap().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -167,8 +167,10 @@ impl DocAtlas {
|
||||
new_bottom = new_bottom.max(doc_rect.bottom.ceil());
|
||||
}
|
||||
|
||||
// Add padding to reduce realloc frequency.
|
||||
let pad = tiles::TILE_SIZE;
|
||||
// Geometric over-allocation: pad by 25% of extent to reduce realloc frequency.
|
||||
let doc_extent_w = new_right - new_left;
|
||||
let doc_extent_h = new_bottom - new_top;
|
||||
let pad = (doc_extent_w.min(doc_extent_h) * 0.25_f32).max(TILE_SIZE as f32);
|
||||
new_left -= pad;
|
||||
new_top -= pad;
|
||||
new_right += pad;
|
||||
@ -770,7 +772,11 @@ impl Surfaces {
|
||||
if ids & SurfaceId::Export as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Export));
|
||||
}
|
||||
performance::begin_measure!("apply_mut::flags");
|
||||
performance::end_measure!("apply_mut::flags");
|
||||
}
|
||||
|
||||
pub fn apply_one(&mut self, surface_id: SurfaceId, mut f: impl FnMut(&mut skia::Surface)) {
|
||||
f(self.get_mut(surface_id));
|
||||
}
|
||||
|
||||
pub fn get_render_context_translation(
|
||||
@ -801,13 +807,6 @@ impl Surfaces {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
});
|
||||
|
||||
// Mark all render surfaces as dirty so they get redrawn
|
||||
self.mark_dirty(SurfaceId::Fills);
|
||||
self.mark_dirty(SurfaceId::Strokes);
|
||||
self.mark_dirty(SurfaceId::InnerShadows);
|
||||
self.mark_dirty(SurfaceId::TextDropShadows);
|
||||
self.mark_dirty(SurfaceId::DropShadows);
|
||||
|
||||
// Update transformations
|
||||
self.apply_mut(surface_ids, |s| {
|
||||
let canvas = s.canvas();
|
||||
@ -1191,13 +1190,14 @@ impl Surfaces {
|
||||
self.atlas.tile_doc_rects.insert(*tile, tile_doc_rect);
|
||||
|
||||
// Draws current tile into tile atlas
|
||||
let tile_ref = self.tiles.add(tile_viewbox, tile);
|
||||
self.tile_atlas.canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
None,
|
||||
tile_ref.rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
if let Some(tile_ref) = self.tiles.add(tile_viewbox, tile) {
|
||||
self.tile_atlas.canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
None,
|
||||
tile_ref.rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1324,9 +1324,7 @@ impl Surfaces {
|
||||
}
|
||||
|
||||
pub fn get_tile_image_from_tile_atlas(&mut self, tile: Tile) -> Option<skia::Image> {
|
||||
let Some(tile_ref) = self.tiles.get(tile) else {
|
||||
panic!("Tile not found {}:{}", tile.0, tile.1);
|
||||
};
|
||||
let tile_ref = self.tiles.get(tile)?;
|
||||
|
||||
let rect = IRect::from_ltrb(
|
||||
tile_ref.rect.left as i32,
|
||||
@ -1456,10 +1454,7 @@ impl Surfaces {
|
||||
self.shape_strokes = surface;
|
||||
}
|
||||
|
||||
if let Some(surface) = self
|
||||
.shape_strokes
|
||||
.new_surface_with_dimensions((max_w, max_h))
|
||||
{
|
||||
if let Some(surface) = self.shape_fills.new_surface_with_dimensions((max_w, max_h)) {
|
||||
self.shape_fills = surface;
|
||||
}
|
||||
}
|
||||
@ -1564,8 +1559,9 @@ impl TileTextureCache {
|
||||
|
||||
fn gc(&mut self) {
|
||||
// Make a real remove
|
||||
for tile in self.removed.iter() {
|
||||
if let Some(tile_ref) = self.grid.remove(tile) {
|
||||
let removed = std::mem::take(&mut self.removed);
|
||||
for tile in removed {
|
||||
if let Some(tile_ref) = self.grid.remove(&tile) {
|
||||
self.provider.deallocate(tile_ref);
|
||||
}
|
||||
}
|
||||
@ -1652,7 +1648,7 @@ impl TileTextureCache {
|
||||
self.grid.contains_key(&tile) && !self.removed.contains(&tile)
|
||||
}
|
||||
|
||||
pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> TileAtlasTextureRef {
|
||||
pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> Option<TileAtlasTextureRef> {
|
||||
if self.grid.len() > TEXTURES_CACHE_CAPACITY {
|
||||
// First we try to remove the obsolete tiles.
|
||||
self.gc();
|
||||
@ -1665,9 +1661,7 @@ impl TileTextureCache {
|
||||
self.gc_non_visible(tile_viewbox);
|
||||
}
|
||||
|
||||
let Some(tile_ref) = self.provider.allocate() else {
|
||||
panic!("Tile texture allocation failed {}:{}", tile.0, tile.1);
|
||||
};
|
||||
let tile_ref = self.provider.allocate()?;
|
||||
|
||||
self.grid.insert(*tile, tile_ref.clone());
|
||||
|
||||
@ -1676,7 +1670,7 @@ impl TileTextureCache {
|
||||
}
|
||||
|
||||
self.is_updated = true;
|
||||
tile_ref.clone()
|
||||
Some(tile_ref)
|
||||
}
|
||||
|
||||
pub fn get(&mut self, tile: Tile) -> Option<&TileAtlasTextureRef> {
|
||||
|
||||
@ -28,24 +28,26 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
let mut group_layer_opacity: Option<f32> = None;
|
||||
|
||||
for paragraph in text_content.paragraphs() {
|
||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
||||
std::collections::HashMap::new();
|
||||
let mut stroke_paragraphs: Vec<ParagraphBuilder> = Vec::new();
|
||||
let (stroke_paints, stroke_layer_opacity) =
|
||||
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
|
||||
|
||||
if group_layer_opacity.is_none() {
|
||||
group_layer_opacity = stroke_layer_opacity;
|
||||
}
|
||||
|
||||
for span in paragraph.children().iter() {
|
||||
let (stroke_paints, stroke_layer_opacity) =
|
||||
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
|
||||
|
||||
if group_layer_opacity.is_none() {
|
||||
group_layer_opacity = stroke_layer_opacity;
|
||||
}
|
||||
|
||||
let text: String = span.apply_text_transform();
|
||||
|
||||
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
||||
let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| {
|
||||
if stroke_paragraphs.len() < stroke_paints.len() {
|
||||
for _stroke_paint in stroke_paints.iter() {
|
||||
let paragraph_style = paragraph.paragraph_to_style();
|
||||
ParagraphBuilder::new(¶graph_style, fonts)
|
||||
});
|
||||
stroke_paragraphs.push(ParagraphBuilder::new(¶graph_style, fonts));
|
||||
}
|
||||
}
|
||||
|
||||
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
||||
let builder = &mut stroke_paragraphs[paint_idx];
|
||||
let stroke_paint = stroke_paint.clone();
|
||||
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
|
||||
let stroke_style = span.to_stroke_style(
|
||||
@ -59,10 +61,6 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
}
|
||||
}
|
||||
|
||||
let stroke_paragraphs: Vec<ParagraphBuilder> = (0..stroke_paragraphs_map.len())
|
||||
.filter_map(|i| stroke_paragraphs_map.remove(&i))
|
||||
.collect();
|
||||
|
||||
paragraph_group.push(stroke_paragraphs);
|
||||
}
|
||||
|
||||
@ -555,7 +553,7 @@ fn draw_text(
|
||||
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
} else {
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
canvas.save();
|
||||
}
|
||||
|
||||
paint_text_with_emoji_overlay(canvas, shape, paragraph_builder_groups, overlay_emoji);
|
||||
|
||||
@ -3,7 +3,6 @@ use skia_safe::{self as skia, Color4f};
|
||||
use super::{RenderState, ShapesPoolRef, SurfaceId};
|
||||
use crate::globals::get_ui_state;
|
||||
use crate::render::{grid_layout, rulers};
|
||||
use crate::shapes::{Layout, Type};
|
||||
pub mod guides;
|
||||
|
||||
pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
@ -30,28 +29,13 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
}
|
||||
|
||||
// Render overlays for empty grid frames
|
||||
for shape in shapes.iter() {
|
||||
if shape.id.is_nil() || !shape.children.is_empty() {
|
||||
let empty_grid_ids: std::collections::HashSet<crate::uuid::Uuid> =
|
||||
std::mem::take(&mut render_state.empty_grid_frame_ids);
|
||||
for id in &empty_grid_ids {
|
||||
if show_grid_id == Some(*id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if show_grid_id == Some(shape.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Type::Frame(frame) = &shape.shape_type else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !matches!(frame.layout, Some(Layout::GridLayout(_, _))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if shape.deleted() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(shape) = shapes.get(&shape.id) {
|
||||
if let Some(shape) = shapes.get(id) {
|
||||
grid_layout::render_overlay(
|
||||
zoom,
|
||||
render_state.options.antialias_threshold,
|
||||
@ -61,6 +45,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
);
|
||||
}
|
||||
}
|
||||
render_state.empty_grid_frame_ids = empty_grid_ids;
|
||||
|
||||
let viewbox = render_state.viewbox;
|
||||
let ruler_state = render_state.rulers;
|
||||
|
||||
73
render-wasm/src/render/view_mode.rs
Normal file
73
render-wasm/src/render/view_mode.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::render::RenderState;
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
pub fn viewer_masked_pass(include_filter: &Option<HashSet<Uuid>>) -> bool {
|
||||
include_filter.is_some()
|
||||
}
|
||||
|
||||
pub fn reset_viewer_masked_surfaces(render_state: &mut RenderState) {
|
||||
render_state
|
||||
.surfaces
|
||||
.clear_backbuffer(render_state.background_color);
|
||||
render_state.surfaces.clear_tile_atlas();
|
||||
}
|
||||
|
||||
/// Precompute the set of all ancestor ids that are visible for the viewer
|
||||
/// masked pass, avoiding recursive checks on the hot path.
|
||||
pub fn precompute_viewer_visible_set(render_state: &mut RenderState, tree: ShapesPoolRef) {
|
||||
render_state.viewer_visible_set = None;
|
||||
let Some(ref include) = render_state.include_filter else {
|
||||
return;
|
||||
};
|
||||
let mut visible = include.clone();
|
||||
for id in include.iter() {
|
||||
let mut current_id = id;
|
||||
while let Some(raw) = tree.get_raw(current_id) {
|
||||
match raw.parent_id {
|
||||
Some(ref parent_id) => {
|
||||
visible.insert(*parent_id);
|
||||
current_id = parent_id;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
render_state.viewer_visible_set = Some(visible);
|
||||
}
|
||||
|
||||
/// True when the shape or any descendant is whitelisted.
|
||||
pub fn shape_visible_in_include_filter(
|
||||
viewer_visible_set: &Option<HashSet<Uuid>>,
|
||||
shape_id: &Uuid,
|
||||
) -> bool {
|
||||
match viewer_visible_set {
|
||||
Some(visible) => visible.contains(shape_id),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// When an include whitelist is active, only those ids are painted.
|
||||
pub fn shape_should_paint_for_viewer_layer(
|
||||
include_filter: &Option<HashSet<Uuid>>,
|
||||
shape_id: &Uuid,
|
||||
) -> bool {
|
||||
match include_filter {
|
||||
Some(include) => include.contains(shape_id),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Viewer layer mask: traverse whitelisted subtrees; paint only listed ids.
|
||||
pub fn shape_visible_for_viewer_layer(
|
||||
viewer_render_root: &Option<Uuid>,
|
||||
viewer_visible_set: &Option<HashSet<Uuid>>,
|
||||
shape_id: &Uuid,
|
||||
) -> bool {
|
||||
if viewer_render_root.as_ref() == Some(shape_id) {
|
||||
return true;
|
||||
}
|
||||
shape_visible_in_include_filter(viewer_visible_set, shape_id)
|
||||
}
|
||||
232
render-wasm/src/render/walk.rs
Normal file
232
render-wasm/src/render/walk.rs
Normal file
@ -0,0 +1,232 @@
|
||||
use crate::shapes::{Corners, Shadow, Shape, Type};
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
use skia_safe::{Matrix, Rect};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type ClipStack = Vec<(Rect, Option<Corners>, Matrix, Matrix)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRenderState {
|
||||
pub id: Uuid,
|
||||
pub(crate) visited_children: bool,
|
||||
pub(crate) clip_bounds: Option<ClipStack>,
|
||||
pub(crate) visited_mask: bool,
|
||||
pub(crate) mask: bool,
|
||||
pub(crate) flattened: bool,
|
||||
}
|
||||
|
||||
/// Get simplified children of a container, flattening nested flattened containers
|
||||
pub fn get_simplified_children<'a>(
|
||||
tree: ShapesPoolRef<'a>,
|
||||
shape: &'a Shape,
|
||||
result: &mut Vec<Uuid>,
|
||||
) {
|
||||
for child_id in shape.children_ids_iter(false) {
|
||||
if let Some(child) = tree.get(child_id) {
|
||||
if child.can_flatten() {
|
||||
get_simplified_children(tree, child, result);
|
||||
} else {
|
||||
result.push(*child_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeRenderState {
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.id.is_nil()
|
||||
}
|
||||
|
||||
/// Calculates the clip bounds for child elements of a given shape.
|
||||
///
|
||||
/// This function determines the clipping region that should be applied to child elements
|
||||
/// when rendering. It takes into account the element's selection rectangle, transform.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `element` - The shape element for which to calculate clip bounds
|
||||
/// * `offset` - Optional offset (x, y) to adjust the bounds position. When provided,
|
||||
/// the bounds are translated by the negative of this offset, effectively moving
|
||||
/// the clipping region to compensate for coordinate system transformations.
|
||||
/// This is useful for nested coordinate systems or when elements are grouped
|
||||
/// and need relative positioning adjustments.
|
||||
fn append_clip(
|
||||
clip_stack: Option<ClipStack>,
|
||||
clip: (Rect, Option<Corners>, Matrix, Matrix),
|
||||
) -> Option<ClipStack> {
|
||||
match clip_stack {
|
||||
Some(mut stack) => {
|
||||
stack.push(clip);
|
||||
Some(stack)
|
||||
}
|
||||
None => Some(vec![clip]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_children_clip_bounds(
|
||||
&self,
|
||||
element: &Shape,
|
||||
offset: Option<(f32, f32)>,
|
||||
clip_inset: Option<f32>,
|
||||
) -> Option<ClipStack> {
|
||||
if self.id.is_nil() || !element.clip() {
|
||||
return self.clip_bounds.clone();
|
||||
}
|
||||
|
||||
let mut bounds = element.selrect();
|
||||
if let Some(offset) = offset {
|
||||
let x = bounds.x() - offset.0;
|
||||
let y = bounds.y() - offset.1;
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
bounds.set_xywh(x, y, width, height);
|
||||
}
|
||||
let mut transform = element.transform;
|
||||
transform.post_translate(bounds.center());
|
||||
transform.pre_translate(-bounds.center());
|
||||
|
||||
let corners = match &element.shape_type {
|
||||
Type::Rect(data) => data.corners,
|
||||
Type::Frame(data) => data.corners,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(clip_inset) = clip_inset.filter(|&e| e > 0.0) {
|
||||
bounds.inset((clip_inset, clip_inset));
|
||||
}
|
||||
|
||||
Self::append_clip(
|
||||
self.clip_bounds.clone(),
|
||||
(
|
||||
bounds,
|
||||
corners,
|
||||
transform,
|
||||
transform.invert().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Calculates the clip bounds for shadow rendering of a given shape.
|
||||
///
|
||||
/// This function determines the clipping region that should be applied when rendering a
|
||||
/// shadow for a shape element. For frames, it uses the shadow bounds to clip nested
|
||||
/// shadows. For groups, it returns the existing clip bounds since groups should not
|
||||
/// constrain nested shadows based on their selection rectangle bounds.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `element` - The shape element for which to calculate shadow clip bounds
|
||||
/// * `shadow` - The shadow configuration containing blur, offset, and other properties
|
||||
pub fn get_nested_shadow_clip_bounds(
|
||||
&self,
|
||||
element: &Shape,
|
||||
shadow: &Shadow,
|
||||
) -> Option<ClipStack> {
|
||||
if self.id.is_nil() {
|
||||
return self.clip_bounds.clone();
|
||||
}
|
||||
|
||||
// Assert that the shape is either a Frame or Group
|
||||
assert!(
|
||||
matches!(element.shape_type, Type::Frame(_) | Type::Group(_)),
|
||||
"Shape must be a Frame or Group for nested shadow clip bounds calculation"
|
||||
);
|
||||
|
||||
match &element.shape_type {
|
||||
Type::Frame(_) => {
|
||||
let mut bounds = element.get_selrect_shadow_bounds(shadow);
|
||||
let blur_inset = (shadow.blur * 2.).max(0.0);
|
||||
if blur_inset > 0.0 {
|
||||
let max_inset_x = (bounds.width() * 0.5).max(0.0);
|
||||
let max_inset_y = (bounds.height() * 0.5).max(0.0);
|
||||
// Clamp the inset so we never shrink more than half of the width/height;
|
||||
// otherwise the rect could end up inverted on small frames.
|
||||
let inset_x = blur_inset.min(max_inset_x);
|
||||
let inset_y = blur_inset.min(max_inset_y);
|
||||
if inset_x > 0.0 || inset_y > 0.0 {
|
||||
bounds.inset((inset_x, inset_y));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transform = element.transform;
|
||||
transform.post_translate(element.center());
|
||||
transform.pre_translate(-element.center());
|
||||
|
||||
let corners = match &element.shape_type {
|
||||
Type::Frame(data) => data.corners,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Self::append_clip(
|
||||
self.clip_bounds.clone(),
|
||||
(
|
||||
bounds,
|
||||
corners,
|
||||
transform,
|
||||
transform.invert().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => self.clip_bounds.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sort by z_index descending (higher z renders on top).
|
||||
* The sort is stable so if the values are equal the index for the children
|
||||
* has preference.
|
||||
* When changing this method check the benchmark
|
||||
*/
|
||||
pub fn sort_z_index(tree: ShapesPoolRef, element: &Shape, children_ids: Vec<Uuid>) -> Vec<Uuid> {
|
||||
if element.has_layout() {
|
||||
let mut ids = children_ids;
|
||||
ids.sort_by_cached_key(|id| {
|
||||
std::cmp::Reverse(tree.get(id).map(|s| s.z_index()).unwrap_or(0))
|
||||
});
|
||||
if element.is_flex() && !element.is_flex_reverse() {
|
||||
ids.reverse();
|
||||
}
|
||||
ids
|
||||
} else {
|
||||
children_ids
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderStats {
|
||||
pub counts: HashMap<Uuid, i32>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl RenderStats {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
counts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&mut self, id: Uuid) -> i32 {
|
||||
let counter = self.counts.entry(id).or_insert(0);
|
||||
*counter += 1;
|
||||
*counter
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.counts.clear();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get(&self, id: &Uuid) -> Option<&i32> {
|
||||
self.counts.get(id)
|
||||
}
|
||||
|
||||
pub(crate) fn print(&self) {
|
||||
let mut sum: i32 = 0;
|
||||
for (&id, &count) in self.counts.iter() {
|
||||
println!("{}: {}", id, count);
|
||||
sum += count;
|
||||
}
|
||||
println!("{}: {}", self.counts.len(), sum);
|
||||
}
|
||||
}
|
||||
@ -315,8 +315,6 @@ fn set_flex_multi_span(
|
||||
for track in tracks[start..end].iter_mut() {
|
||||
if track.track_type == GridTrackType::Flex {
|
||||
let new_size = alloc.clamp(track.size, track.max_size);
|
||||
let aloc = new_size - track.size;
|
||||
dist -= aloc;
|
||||
track.size = new_size;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ pub fn uuid_from_u32(id: [u32; 4]) -> Uuid {
|
||||
uuid_from_u32_quartet(id[0], id[1], id[2], id[3])
|
||||
}
|
||||
|
||||
pub fn get_image(image_id: &Uuid) -> Option<&Image> {
|
||||
pub fn get_image(image_id: &Uuid) -> Option<Image> {
|
||||
get_render_state().images.get(image_id)
|
||||
}
|
||||
|
||||
|
||||
@ -70,3 +70,8 @@ pub extern "C" fn is_font_uploaded(
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn flush_font_caches() {
|
||||
get_render_state().fonts_mut().flush_caches();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user