From a1299e17976def35ac540bd3fc9b723ad0ae0853 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Mon, 29 Jun 2026 18:23:50 +0200 Subject: [PATCH] :sparkles: Add vector render path for PDF export --- .../src/app/main/data/exports/assets.cljs | 15 ++-- render-wasm/src/globals.rs | 29 +++++++- render-wasm/src/main.rs | 4 +- render-wasm/src/render.rs | 59 ++++----------- render-wasm/src/render/debug.rs | 15 ++-- render-wasm/src/render/fills.rs | 5 +- render-wasm/src/render/filters.rs | 5 +- render-wasm/src/render/images.rs | 54 +++++++++++--- render-wasm/src/render/options.rs | 1 + render-wasm/src/render/pdf.rs | 8 +-- render-wasm/src/render/resources.rs | 31 ++++++++ render-wasm/src/render/strokes.rs | 5 +- render-wasm/src/render/surfaces.rs | 1 + render-wasm/src/render/ui.rs | 3 +- render-wasm/src/render/vector.rs | 72 +++++++------------ render-wasm/src/shapes/text_paths.rs | 4 +- render-wasm/src/state.rs | 6 +- render-wasm/src/utils.rs | 6 +- render-wasm/src/wasm/fills/image.rs | 9 ++- render-wasm/src/wasm/fonts.rs | 10 ++- 20 files changed, 195 insertions(+), 147 deletions(-) create mode 100644 render-wasm/src/render/resources.rs diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs index 24d72e5e92..83a7cbb5fa 100644 --- a/frontend/src/app/main/data/exports/assets.cljs +++ b/frontend/src/app/main/data/exports/assets.cljs @@ -179,6 +179,15 @@ (and (wasm-export-enabled? state) (contains? wasm-export-types (:type export)))) +(defn- request-simple-export-wasm + [export] + (ptk/reify ::request-simple-export-wasm + ptk/EffectEvent + (effect [_ _ _] + (case (:type export) + :pdf (wasm.exports/export-pdf export) + (wasm.exports/export-image export))))) + (defn request-simple-export [{:keys [export]}] (ptk/reify ::request-simple-export @@ -191,11 +200,7 @@ ptk/WatchEvent (watch [_ state _] (if (use-wasm-export? state export) - (do - (case (:type export) - :pdf (wasm.exports/export-pdf export) - (wasm.exports/export-image export)) - (rx/empty)) + (rx/of (request-simple-export-wasm export)) (let [profile-id (:profile-id state) params {:exports [export] :profile-id profile-id diff --git a/render-wasm/src/globals.rs b/render-wasm/src/globals.rs index 64e7d8c94f..06d28bbcc3 100644 --- a/render-wasm/src/globals.rs +++ b/render-wasm/src/globals.rs @@ -4,7 +4,7 @@ use macros::wasm_error; use crate::emscripten::init_gl; use crate::mem; -use crate::render::{gpu_state::GpuState, RenderState}; +use crate::render::{gpu_state::GpuState, RenderResources, RenderState}; use crate::state::{State, TextEditorState, UIState}; static mut DESIGN_STATE: *mut State = std::ptr::null_mut(); @@ -23,7 +23,12 @@ static mut GPU_STATE: *mut GpuState = std::ptr::null_mut(); #[inline(always)] pub(crate) fn get_gpu_state() -> &'static mut GpuState { unsafe { - debug_assert!(!GPU_STATE.is_null(), "GPU State is null"); + // `assert!` (not `debug_assert!`): a headless instance never inits GPU + // state, so an interactive call must fail-fast rather than deref null. + assert!( + !GPU_STATE.is_null(), + "GPU State is null (headless instance?)" + ); &mut *GPU_STATE } } @@ -39,6 +44,17 @@ pub(crate) fn get_render_state() -> &'static mut RenderState { } } +/// GPU-free resources for the headless export path +static mut RENDER_RESOURCES: *mut RenderResources = std::ptr::null_mut(); + +#[inline(always)] +pub(crate) fn get_resources() -> &'static mut RenderResources { + unsafe { + debug_assert!(!RENDER_RESOURCES.is_null(), "Render Resources is null"); + &mut *RENDER_RESOURCES + } +} + /// Text Editor State static mut TEXT_EDITOR_STATE: *mut TextEditorState = std::ptr::null_mut(); @@ -113,6 +129,14 @@ fn render_init(width: i32, height: i32) { } } +/// Initializes the interactive RenderResources (GPU image store). +fn resources_init() { + unsafe { + let resources = RenderResources::try_new().expect("Cannot initialize RenderResources"); + RENDER_RESOURCES = Box::into_raw(Box::new(resources)); + } +} + /// Initializes DesignState. fn design_init() { unsafe { @@ -143,6 +167,7 @@ pub extern "C" fn init(width: i32, height: i32) -> Result<()> { #[cfg(target_arch = "wasm32")] init_gl!(); gpu_init(); + resources_init(); render_init(width, height); text_editor_init(); design_init(); diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 0aa3fae930..d0acfda5bd 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -22,7 +22,7 @@ use std::collections::HashMap; use crate::error::{Error, Result}; use crate::render::{FrameType, RenderFlag}; -use globals::{get_design_state, get_gpu_state, get_render_state}; +use globals::{get_design_state, get_gpu_state, get_render_state, get_resources}; use macros::wasm_error; use math::{Bounds, Matrix}; @@ -720,7 +720,7 @@ pub extern "C" fn is_image_cached( is_thumbnail: bool, ) -> Result { let id = uuid_from_u32_quartet(a, b, c, d); - let result = get_render_state().has_image(&id, is_thumbnail); + let result = get_resources().images.contains(&id, is_thumbnail); Ok(result) } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ee4ebe3bd0..55a9832f6d 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -7,6 +7,7 @@ pub mod grid_layout; mod images; mod options; pub mod pdf; +mod resources; pub mod rulers; mod shadows; pub mod shape_renderer; @@ -35,10 +36,11 @@ use crate::tiles::{self, PendingTiles, TileRect}; use crate::uuid::Uuid; use crate::view::Viewbox; use crate::wapi; -use crate::{get_gpu_state, performance}; +use crate::{get_gpu_state, get_resources, performance}; pub use fonts::*; pub use images::*; +pub(crate) use resources::RenderResources; type ClipStack = Vec<(Rect, Option, Matrix)>; @@ -349,15 +351,12 @@ pub(crate) struct RenderState { pub options: RenderOptions, stats: RenderStats, pub surfaces: Surfaces, - pub fonts: FontStore, pub viewbox: Viewbox, pub cached_viewbox: Viewbox, - pub images: ImageStore, pub background_color: skia::Color, // Stack of nodes pending to be rendered. pending_nodes: Vec, pub current_tile: Option, - pub sampling_options: skia::SamplingOptions, pub render_area: Rect, // render_area expanded by surface margins — used for visibility checks so that // shapes in the margin zone are rendered (needed for background blur sampling). @@ -538,19 +537,18 @@ impl RenderState { pub fn try_new(width: i32, height: i32) -> Result { // This needs to be done once per WebGL context. - let sampling_options = - skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest); + let sampling_options = get_resources().sampling_options; - let fonts = FontStore::try_new()?; let surfaces = Surfaces::try_new( (width, height), sampling_options, tiles::get_tile_dimensions(), )?; - // This is used multiple times everywhere so instead of creating new instances every - // time we reuse this one. + Self::assemble(width, height, surfaces) + } + fn assemble(width: i32, height: i32, surfaces: Surfaces) -> Result { let viewbox = Viewbox::new(width as f32, height as f32); let tiles = tiles::TileHashMap::new(); let options = RenderOptions::default(); @@ -559,14 +557,11 @@ impl RenderState { options, stats: RenderStats::new(), surfaces, - fonts, viewbox, cached_viewbox: Viewbox::new(0., 0.), - images: ImageStore::new(), background_color: skia::Color::TRANSPARENT, pending_nodes: vec![], current_tile: None, - sampling_options, render_area: Rect::new_empty(), render_area_with_margins: Rect::new_empty(), tiles, @@ -770,35 +765,6 @@ impl RenderState { Ok(result) } - pub fn fonts(&self) -> &FontStore { - &self.fonts - } - - pub fn fonts_mut(&mut self) -> &mut FontStore { - &mut self.fonts - } - - pub fn add_image(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<()> { - self.images.add(id, is_thumbnail, image_data) - } - - /// Adds an image from an existing WebGL texture, avoiding re-decoding - pub fn add_image_from_gl_texture( - &mut self, - id: Uuid, - is_thumbnail: bool, - texture_id: u32, - width: i32, - height: i32, - ) -> Result<()> { - self.images - .add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height) - } - - pub fn has_image(&self, id: &Uuid, is_thumbnail: bool) -> bool { - self.images.contains(id, is_thumbnail) - } - pub fn set_debug_flags(&mut self, debug: u32) { self.options.flags = debug; } @@ -813,7 +779,7 @@ impl RenderState { self.viewbox.width().floor() as i32, self.viewbox.height().floor() as i32, )?; - self.fonts.set_scale_debug_font(dpr); + get_resources().fonts.set_scale_debug_font(dpr); self.viewbox.set_dpr(dpr); self.surfaces.set_dpr(dpr); } @@ -961,7 +927,7 @@ impl RenderState { text_paint.set_color(skia::Color::GRAY); text_paint.set_anti_alias(true); - let font = self.fonts.debug_font(); + let font = get_resources().fonts.debug_font(); // FIXME let text = "Loading…"; let (text_width, _) = font.measure_str(text, None); @@ -1398,7 +1364,8 @@ impl RenderState { if let Some(svg) = shape.svg.as_ref() { svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id)); } else { - let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone()); + let font_manager = + skia::FontMgr::from(get_resources().fonts.font_provider().clone()); let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager); match dom_result { Ok(dom) => { @@ -2938,7 +2905,7 @@ impl RenderState { surface.draw( drop_canvas, (0.0, 0.0), - self.sampling_options, + get_resources().sampling_options, Some(&drop_paint), ); drop_canvas.restore(); @@ -2948,7 +2915,7 @@ impl RenderState { surface.draw( drop_canvas, (0.0, 0.0), - self.sampling_options, + get_resources().sampling_options, Some(&drop_paint), ); drop_canvas.restore(); diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 0574e3ab28..d661fd2bd1 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -6,6 +6,7 @@ use macros::wasm_error; #[cfg(target_arch = "wasm32")] use crate::get_render_state; +use crate::get_resources; use skia_safe::{self as skia, Rect}; @@ -62,16 +63,16 @@ pub fn render_wasm_label(render_state: &mut RenderState) { "WebGL rendering" }; let dpr = render_state.options.dpr; - let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None); + let (scalar, _) = get_resources().fonts.debug_font().measure_str(str, None); let mut p = skia::Point::new(canvas_width - (20.0 * dpr) - scalar, 50.0 * dpr); - let debug_font = render_state.fonts.debug_font(); + let debug_font = get_resources().fonts.debug_font(); canvas.draw_str(str, p, debug_font, &paint); if render_state.options.is_text_editor_v3() { str = "Text Editor v3"; - let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None); + let (scalar, _) = get_resources().fonts.debug_font().measure_str(str, None); p.x = canvas_width - (20.0 * dpr) - scalar; p.y += 15.0 * dpr; canvas.draw_str(str, p, debug_font, &paint); @@ -85,7 +86,7 @@ pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) { paint.set_color(skia::Color::RED); let str_rect = format!("{} {} {} {}", sx, sy, ex, ey); - let debug_font = render_state.fonts.debug_font(); + let debug_font = get_resources().fonts.debug_font(); canvas.draw_str(str_rect, skia::Point::new(100.0, 150.0), debug_font, &paint); } @@ -104,7 +105,7 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) { let str_rect = format!("{} {} {} {}", sx, sy, ex, ey); - let debug_font = render_state.fonts.debug_font(); + let debug_font = get_resources().fonts.debug_font(); canvas.draw_str(str_rect, skia::Point::new(100.0, 100.0), debug_font, &paint); let tile_size = tiles::get_tile_size(scale); @@ -114,7 +115,7 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) { let debug_rect = get_debug_rect(rect); let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.); let str = format!("{}:{}", x, y); - let debug_font = render_state.fonts.debug_font(); + let debug_font = get_resources().fonts.debug_font(); paint.set_style(skia::PaintStyle::Fill); canvas.draw_str(str, p, debug_font, &paint); canvas.draw_rect(debug_rect, &paint); @@ -153,7 +154,7 @@ pub fn render_workspace_current_tile( let tile_position_origin = skia::Point::new(rect.x() + 10., rect.y() + 20.); p.set_style(skia::PaintStyle::Fill); let str = format!("{prefix} {}:{}", tile.x(), tile.y()); - let mut debug_font = render_state.fonts.debug_font().clone(); + let mut debug_font = get_resources().fonts.debug_font().clone(); debug_font.set_size(16.); canvas.draw_str(str, tile_position_origin, &debug_font, &p); } diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 45f8fc3048..cb41522a2d 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -2,6 +2,7 @@ use skia_safe::{self as skia, Paint, RRect}; use super::{filters, RenderState, SurfaceId}; use crate::error::Result; +use crate::get_resources; use crate::render::get_source_rect; use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type}; @@ -13,7 +14,7 @@ fn draw_image_fill( antialias: bool, surface_id: SurfaceId, ) { - let Some(image) = render_state.images.get(&image_fill.id()) else { + let Some(image) = get_resources().images.get(&image_fill.id()) else { return; }; @@ -83,7 +84,7 @@ fn draw_image_fill( image, Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), dest_rect, - render_state.sampling_options, + get_resources().sampling_options, paint, ); diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index 34b33f403d..b4313a043d 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -2,6 +2,7 @@ use skia_safe::{self as skia, ImageFilter, Rect}; use super::{RenderState, SurfaceId}; use crate::error::Result; +use crate::get_resources; /// Composes two image filters, returning a combined filter if both are present, /// or the individual filter if only one is present, or None if neither is present. @@ -51,12 +52,12 @@ where canvas.save(); canvas.scale((1.0 / scale, 1.0 / scale)); canvas.translate((bounds.left * scale, bounds.top * scale)); - surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None); + surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None); canvas.restore(); } else { canvas.save(); canvas.translate((bounds.left, bounds.top)); - surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None); + surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None); canvas.restore(); } Ok(true) diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index b567829ab7..0de0f3f7f8 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -61,7 +61,8 @@ enum StoredImage { pub struct ImageStore { images: HashMap<(Uuid, bool), StoredImage>, - context: Box, + /// gpu-only + context: Option>, } /// Creates a Skia image from an existing WebGL texture. @@ -149,7 +150,7 @@ impl ImageStore { let context = &gpu_state.context; Self { images: HashMap::with_capacity(2048), - context: Box::new(context.clone()), + context: Some(Box::new(context.clone())), } } @@ -167,10 +168,18 @@ impl ImageStore { 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)); - } else { - self.images.insert(key, StoredImage::Raw(raw_data)); + match self.context.as_mut() { + Some(context) => { + if let Some(gpu_image) = decode_image(context, &raw_data) { + self.images.insert(key, StoredImage::Gpu(gpu_image)); + } else { + self.images.insert(key, StoredImage::Raw(raw_data)); + } + } + // GPU-free: keep the encoded bytes; decoded on the CPU at draw time. + None => { + self.images.insert(key, StoredImage::Raw(raw_data)); + } } Ok(()) } @@ -193,7 +202,12 @@ impl ImageStore { } // Create a Skia image from the existing GL texture - let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?; + let Some(context) = self.context.as_mut() else { + return Err(crate::error::Error::CriticalError( + "Cannot register a GL texture without a GPU context".to_string(), + )); + }; + let image = create_image_from_gl_texture(context, texture_id, width, height)?; self.images.insert(key, StoredImage::Gpu(image)); Ok(()) @@ -214,8 +228,27 @@ impl ImageStore { } pub fn get_cpu_image(&mut self, id: &Uuid) -> Option { - let gpu_image = self.get(id)?.clone(); - gpu_image.make_non_texture_image(self.context.as_mut()) + // GPU path: promote to a texture, then copy to a CPU image. + if self.context.is_some() { + let gpu_image = self.get(id)?.clone(); + let context = self.context.as_mut()?; + return gpu_image.make_non_texture_image(context.as_mut()); + } + // Headless (no GPU context): decode the stored encoded bytes directly to + // a CPU image, which draws fine on a raster/PDF canvas. Try full first, + // then thumbnail. + self.decode_raw_cpu_image(id, false) + .or_else(|| self.decode_raw_cpu_image(id, true)) + } + + fn decode_raw_cpu_image(&self, id: &Uuid, is_thumbnail: bool) -> Option { + match self.images.get(&(*id, is_thumbnail))? { + StoredImage::Raw(raw_data) => { + let data = unsafe { skia::Data::new_bytes(raw_data) }; + Image::from_encoded(&data) + } + StoredImage::Gpu(img) => Some(img.clone()), + } } fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> { @@ -225,7 +258,8 @@ impl ImageStore { match entry { StoredImage::Gpu(ref img) => Some(img), StoredImage::Raw(raw_data) => { - let gpu_image = decode_image(&mut self.context, raw_data)?; + let context = self.context.as_mut()?; + let gpu_image = decode_image(context, raw_data)?; *entry = StoredImage::Gpu(gpu_image); if let StoredImage::Gpu(ref img) = entry { diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index 7a6a3a3d39..fed66505fe 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -69,6 +69,7 @@ impl RenderOptions { self.fast_mode = enabled; } + #[cfg(target_arch = "wasm32")] pub fn set_capture_frames(&mut self, capture_frames: i32) { self.capture_frames = capture_frames; } diff --git a/render-wasm/src/render/pdf.rs b/render-wasm/src/render/pdf.rs index 551a451c92..7862ec7048 100644 --- a/render-wasm/src/render/pdf.rs +++ b/render-wasm/src/render/pdf.rs @@ -4,8 +4,8 @@ use crate::error::Result; use crate::state::ShapesPoolRef; use crate::uuid::Uuid; -use super::vector::{self, VectorTarget}; -use super::RenderState; +use super::vector; +use super::RenderResources; /// Renders a shape tree to a PDF document and returns the raw PDF bytes. /// @@ -16,7 +16,7 @@ use super::RenderState; /// pixel-based (blur, shadows with blur) are rasterised internally by Skia's /// PDF backend pub fn render_to_pdf( - shared: &mut RenderState, + shared: &mut RenderResources, id: &Uuid, tree: ShapesPoolRef, scale: f32, @@ -45,7 +45,7 @@ pub fn render_to_pdf( let page_canvas = on_page.canvas(); page_canvas.scale((scale, scale)); page_canvas.translate((-bounds.left(), -bounds.top())); - vector::render_tree(shared, page_canvas, id, tree, scale, VectorTarget::Pdf)?; + vector::render_tree(shared, page_canvas, id, tree, scale)?; } let document = on_page.end_page(); diff --git a/render-wasm/src/render/resources.rs b/render-wasm/src/render/resources.rs new file mode 100644 index 0000000000..7c154bf304 --- /dev/null +++ b/render-wasm/src/render/resources.rs @@ -0,0 +1,31 @@ +use skia_safe as skia; + +use crate::error::Result; + +use super::{FontStore, ImageStore}; + +/// Render dependencies that the export (PDF) path needs independently of the +/// surface/tile/GPU machinery in [`RenderState`]: the font store, the +/// decoded-image store, and the default sampling options. +/// +/// Splitting these out keeps the vector/PDF export path (which takes +/// `&mut RenderResources`) decoupled from the interactive renderer, and gives a +/// single source of truth reached through `get_resources()`. +pub(crate) struct RenderResources { + pub fonts: FontStore, + pub images: ImageStore, + pub sampling_options: skia::SamplingOptions, +} + +impl RenderResources { + pub fn try_new() -> Result { + Ok(Self { + fonts: FontStore::try_new()?, + images: ImageStore::new(), + sampling_options: skia::SamplingOptions::new( + skia::FilterMode::Linear, + skia::MipmapMode::Nearest, + ), + }) + } +} diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 8221cad4b6..b5a5a93518 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -8,6 +8,7 @@ use skia_safe::{self as skia, ImageFilter, RRect}; use super::{filters, RenderState, SurfaceId}; use crate::error::{Error, Result}; +use crate::get_resources; use crate::render::filters::compose_filters; use crate::render::{get_dest_rect, get_source_rect}; @@ -467,7 +468,7 @@ fn draw_image_stroke_in_container( surface_id: SurfaceId, ) -> Result<()> { let scale = render_state.get_scale(); - let Some(image) = render_state.images.get(&image_fill.id()) else { + let Some(image) = get_resources().images.get(&image_fill.id()) else { return Ok(()); }; @@ -575,7 +576,7 @@ fn draw_image_stroke_in_container( image, Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), dest_rect, - render_state.sampling_options, + get_resources().sampling_options, &image_paint, ); diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index dc91011abc..e68c0df0c3 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -106,6 +106,7 @@ impl DocAtlas { }) } + // TODO: delete one docatlas is optional pub fn is_empty(&self) -> bool { self.size.width <= 0 || self.size.height <= 0 } diff --git a/render-wasm/src/render/ui.rs b/render-wasm/src/render/ui.rs index cf3f2d7963..72ba82c11c 100644 --- a/render-wasm/src/render/ui.rs +++ b/render-wasm/src/render/ui.rs @@ -1,6 +1,7 @@ use skia_safe::{self as skia, Color4f}; use super::{RenderState, ShapesPoolRef, SurfaceId}; +use crate::get_resources; use crate::globals::get_ui_state; use crate::render::{grid_layout, rulers}; use crate::shapes::{Layout, Type}; @@ -64,7 +65,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { let viewbox = render_state.viewbox; let ruler_state = render_state.rulers; - rulers::render(canvas, viewbox, &render_state.fonts, &ruler_state); + rulers::render(canvas, viewbox, &get_resources().fonts, &ruler_state); // Width of the ruler bars in document coordinates. // Note that when rulers are hidden, guides are not shown either, so we diff --git a/render-wasm/src/render/vector.rs b/render-wasm/src/render/vector.rs index 721a02fbc5..e3e0fd33ed 100644 --- a/render-wasm/src/render/vector.rs +++ b/render-wasm/src/render/vector.rs @@ -9,19 +9,9 @@ use crate::uuid::Uuid; use super::shape_renderer::ShapeRenderer; use super::text; -use super::RenderState; +use super::RenderResources; use super::{get_dest_rect, get_source_rect}; -// --------------------------------------------------------------------------- -// VectorTarget — vector export backend selector -// --------------------------------------------------------------------------- - -/// Vector export backend selector (PDF today; SVG could be added as a variant). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum VectorTarget { - Pdf, -} - // --------------------------------------------------------------------------- // VectorRenderer — implements ShapeRenderer for canvas-based vector export // --------------------------------------------------------------------------- @@ -29,23 +19,16 @@ pub(super) enum VectorTarget { /// Canvas-based vector render backend (CPU Skia canvas, no GPU surfaces). pub(super) struct VectorRenderer<'a> { canvas: &'a Canvas, - shared: &'a mut RenderState, + shared: &'a mut RenderResources, scale: f32, - _target: VectorTarget, } impl<'a> VectorRenderer<'a> { - pub fn new( - canvas: &'a Canvas, - shared: &'a mut RenderState, - scale: f32, - target: VectorTarget, - ) -> Self { + pub fn new(canvas: &'a Canvas, shared: &'a mut RenderResources, scale: f32) -> Self { Self { canvas, shared, scale, - _target: target, } } } @@ -348,12 +331,11 @@ impl ShapeRenderer for VectorRenderer<'_> { /// Depth-first render of the shape tree rooted at `id`. pub(super) fn render_tree( - shared: &mut RenderState, + shared: &mut RenderResources, canvas: &Canvas, id: &Uuid, tree: ShapesPoolRef, scale: f32, - target: VectorTarget, ) -> Result<()> { let Some(element) = tree.get(id) else { return Ok(()); @@ -365,10 +347,10 @@ pub(super) fn render_tree( match &element.shape_type { Type::Group(group) => { - render_group(shared, canvas, element, group.masked, tree, scale, target)?; + render_group(shared, canvas, element, group.masked, tree, scale)?; } Type::Frame(_) => { - render_frame(shared, canvas, element, tree, scale, target)?; + render_frame(shared, canvas, element, tree, scale)?; } // Leaf types listed explicitly (no `_`) so a new Type must be handled. Type::Rect(_) @@ -377,7 +359,7 @@ pub(super) fn render_tree( | Type::Bool(_) | Type::Text(_) | Type::SVGRaw(_) => { - render_leaf(shared, canvas, element, scale, target)?; + render_leaf(shared, canvas, element, scale)?; } } @@ -389,13 +371,12 @@ pub(super) fn render_tree( // --------------------------------------------------------------------------- fn render_group( - shared: &mut RenderState, + shared: &mut RenderResources, canvas: &Canvas, element: &Shape, masked: bool, tree: ShapesPoolRef, scale: f32, - target: VectorTarget, ) -> Result<()> { // A group has no geometry of its own and does NOT propagate a transform to // its children: child shapes are stored in absolute coordinates and each @@ -404,7 +385,7 @@ fn render_group( canvas.save(); // Group drop shadow: subtree silhouette, below the opacity/clip layer. - render_container_drop_shadows(shared, canvas, element, tree, scale, target, false)?; + render_container_drop_shadows(shared, canvas, element, tree, scale, false)?; // Layer for opacity / blend mode (and group-level layer blur) let needs_layer = element.needs_layer(); @@ -437,21 +418,21 @@ fn render_group( canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint)); for child_id in &children { - render_tree(shared, canvas, child_id, tree, scale, target)?; + render_tree(shared, canvas, child_id, tree, scale)?; } if let Some(mask_id) = element.mask_id() { let mut mask_paint = Paint::default(); mask_paint.set_blend_mode(skia::BlendMode::DstIn); canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&mask_paint)); - render_tree(shared, canvas, mask_id, tree, scale, target)?; + render_tree(shared, canvas, mask_id, tree, scale)?; canvas.restore(); // mask layer } canvas.restore(); // composition layer } else { for child_id in &children { - render_tree(shared, canvas, child_id, tree, scale, target)?; + render_tree(shared, canvas, child_id, tree, scale)?; } } @@ -467,12 +448,11 @@ fn render_group( // --------------------------------------------------------------------------- fn render_frame( - shared: &mut RenderState, + shared: &mut RenderResources, canvas: &Canvas, element: &Shape, tree: ShapesPoolRef, scale: f32, - target: VectorTarget, ) -> Result<()> { // A frame's own geometry (background, clip, strokes) is placed by its // `centered_transform`, but — like groups — it does NOT propagate that @@ -485,7 +465,7 @@ fn render_frame( // Frame drop shadow: background + subtree silhouette, below the clip layer // so it extends outside the frame bounds. - render_container_drop_shadows(shared, canvas, element, tree, scale, target, true)?; + render_container_drop_shadows(shared, canvas, element, tree, scale, true)?; let needs_layer = element.needs_layer(); @@ -524,7 +504,7 @@ fn render_frame( if !element.fills.is_empty() { canvas.save(); canvas.concat(&matrix); - let mut renderer = VectorRenderer::new(canvas, shared, scale, target); + let mut renderer = VectorRenderer::new(canvas, shared, scale); renderer.draw_fills(element, &element.fills)?; renderer.draw_fill_inner_shadows(element)?; canvas.restore(); @@ -533,7 +513,7 @@ fn render_frame( // Children (absolute coords, no frame transform). let children: Vec = element.children_ids_iter_forward(false).copied().collect(); for child_id in &children { - render_tree(shared, canvas, child_id, tree, scale, target)?; + render_tree(shared, canvas, child_id, tree, scale)?; } // Strokes over children (clipped frames), in the frame's space. @@ -541,7 +521,7 @@ fn render_frame( if !visible_strokes.is_empty() { canvas.save(); canvas.concat(&matrix); - let mut renderer = VectorRenderer::new(canvas, shared, scale, target); + let mut renderer = VectorRenderer::new(canvas, shared, scale); renderer.draw_strokes(element, &visible_strokes)?; canvas.restore(); } @@ -557,12 +537,11 @@ fn render_frame( /// layer (its alpha becomes the shadow). `draw_fills` includes the frame /// background in the silhouette. fn render_container_drop_shadows( - shared: &mut RenderState, + shared: &mut RenderResources, canvas: &Canvas, element: &Shape, tree: ShapesPoolRef, scale: f32, - target: VectorTarget, draw_fills: bool, ) -> Result<()> { for shadow in element.drop_shadows_visible() { @@ -574,13 +553,13 @@ fn render_container_drop_shadows( canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint)); if draw_fills && !element.fills.is_empty() { - let mut renderer = VectorRenderer::new(canvas, shared, scale, target); + let mut renderer = VectorRenderer::new(canvas, shared, scale); renderer.draw_fills(element, &element.fills)?; } let children: Vec = element.children_ids_iter_forward(false).copied().collect(); for child_id in &children { - render_tree(shared, canvas, child_id, tree, scale, target)?; + render_tree(shared, canvas, child_id, tree, scale)?; } canvas.restore(); @@ -593,11 +572,10 @@ fn render_container_drop_shadows( // --------------------------------------------------------------------------- fn render_leaf( - shared: &mut RenderState, + shared: &mut RenderResources, canvas: &Canvas, element: &Shape, scale: f32, - target: VectorTarget, ) -> Result<()> { let needs_layer = element.needs_layer(); @@ -615,7 +593,7 @@ fn render_leaf( canvas.save_layer(&layer_rec); } - let mut renderer = VectorRenderer::new(canvas, shared, scale, target); + let mut renderer = VectorRenderer::new(canvas, shared, scale); // Layer blur (non-text shapes) let blur_layer = if !matches!(element.shape_type, Type::Text(_)) { @@ -677,7 +655,7 @@ fn render_leaf_content(renderer: &mut R, shape: &Shap // --------------------------------------------------------------------------- fn draw_image_fill( - shared: &mut RenderState, + shared: &mut RenderResources, canvas: &Canvas, shape: &Shape, image_fill: &crate::shapes::ImageFill, @@ -719,7 +697,7 @@ fn draw_image_fill( fn draw_single_stroke( canvas: &Canvas, - shared: &mut RenderState, + shared: &mut RenderResources, scale: f32, shape: &Shape, stroke: &Stroke, @@ -830,7 +808,7 @@ fn draw_stroke_kind_aware(canvas: &Canvas, shape: &Shape, stroke: &Stroke, paint /// CPU image over it with `SrcIn` so only the stroke area shows the image. fn draw_image_stroke( canvas: &Canvas, - shared: &mut RenderState, + shared: &mut RenderResources, scale: f32, shape: &Shape, stroke: &Stroke, diff --git a/render-wasm/src/shapes/text_paths.rs b/render-wasm/src/shapes/text_paths.rs index 38cf30226f..c0d48c2e9e 100644 --- a/render-wasm/src/shapes/text_paths.rs +++ b/render-wasm/src/shapes/text_paths.rs @@ -1,4 +1,4 @@ -use crate::get_render_state; +use crate::get_resources; use crate::shapes::text::TextContent; use skia_safe::{ self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob, @@ -174,7 +174,7 @@ impl TextPaths { ) -> Option<(skia::Path, skia::Rect)> { let utf16_text = span_text.encode_utf16().collect::>(); let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) }; - let emoji_font = get_render_state().fonts().get_emoji_font(font.size()); + let emoji_font = get_resources().fonts.get_emoji_font(font.size()); let use_font = emoji_font.as_ref().unwrap_or(font); if let Some(mut text_blob) = TextBlob::from_text(text, use_font) { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 2ae6ba4067..9f9ff134ff 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -14,7 +14,7 @@ use crate::error::{Error, Result}; use crate::render::FrameType; use crate::shapes::{grid_layout::grid_cell_data, Shape}; use crate::uuid::Uuid; -use crate::{get_render_state, tiles}; +use crate::{get_render_state, get_resources, tiles}; /// This struct holds the state of the Rust application between JS calls. /// @@ -97,7 +97,7 @@ impl State { } pub fn render_shape_pdf(&mut self, id: &Uuid, scale: f32) -> Result> { - crate::render::pdf::render_to_pdf(get_render_state(), id, &self.shapes, scale) + crate::render::pdf::render_to_pdf(get_resources(), id, &self.shapes, scale) } pub fn start_render_loop(&mut self, timestamp: i32) -> Result { @@ -266,7 +266,7 @@ impl State { } pub fn font_collection(&self) -> &FontCollection { - get_render_state().fonts().font_collection() + get_resources().fonts.font_collection() } pub fn get_grid_coords(&self, pos_x: f32, pos_y: f32) -> Option<(i32, i32)> { diff --git a/render-wasm/src/utils.rs b/render-wasm/src/utils.rs index 206b91dbb0..2301846c10 100644 --- a/render-wasm/src/utils.rs +++ b/render-wasm/src/utils.rs @@ -1,4 +1,4 @@ -use crate::get_render_state; +use crate::get_resources; use crate::skia::textlayout::FontCollection; use crate::skia::Image; use crate::uuid::Uuid; @@ -25,12 +25,12 @@ pub fn uuid_from_u32(id: [u32; 4]) -> Uuid { } pub fn get_image(image_id: &Uuid) -> Option<&Image> { - get_render_state().images.get(image_id) + get_resources().images.get(image_id) } // FIXME: move to a different place ? pub fn get_fallback_fonts() -> &'static HashSet { - get_render_state().fonts().get_fallback() + get_resources().fonts.get_fallback() } pub fn get_font_collection() -> &'static FontCollection { diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index 65a4697181..778b80bf92 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -1,5 +1,5 @@ use crate::error::{Error, Result}; -use crate::get_render_state; +use crate::get_resources; use crate::mem; use crate::shapes::Fill; use crate::state::State; @@ -104,7 +104,10 @@ pub extern "C" fn store_image() -> Result<()> { let image_bytes = &bytes[IMAGE_HEADER_SIZE..]; with_state!(state, { - if let Err(msg) = get_render_state().add_image(ids.image_id, is_thumbnail, image_bytes) { + if let Err(msg) = get_resources() + .images + .add(ids.image_id, is_thumbnail, image_bytes) + { eprintln!("{}", msg); } touch_shapes_with_image(state, ids.image_id); @@ -174,7 +177,7 @@ pub extern "C" fn store_image_from_texture() -> Result<()> { ); with_state!(state, { - if let Err(msg) = get_render_state().add_image_from_gl_texture( + if let Err(msg) = get_resources().images.add_image_from_gl_texture( ids.image_id, is_thumbnail, texture_id, diff --git a/render-wasm/src/wasm/fonts.rs b/render-wasm/src/wasm/fonts.rs index 21c4e6a797..ac81e6ce96 100644 --- a/render-wasm/src/wasm/fonts.rs +++ b/render-wasm/src/wasm/fonts.rs @@ -1,6 +1,6 @@ use macros::{wasm_error, ToJs}; -use crate::get_render_state; +use crate::get_resources; use crate::mem; use crate::shapes::{FontFamily, FontStyle}; use crate::utils::uuid_from_u32_quartet; @@ -45,8 +45,8 @@ pub extern "C" fn store_font( let font_style = RawFontStyle::from(style); let family = FontFamily::new(id, weight, font_style.into()); - let _ = get_render_state() - .fonts_mut() + let _ = get_resources() + .fonts .add(family, &font_bytes, is_emoji, is_fallback); mem::free_bytes()?; @@ -66,7 +66,5 @@ pub extern "C" fn is_font_uploaded( let id = uuid_from_u32_quartet(a, b, c, d); let font_style = RawFontStyle::from(style); let family = FontFamily::new(id, weight, font_style.into()); - let res = get_render_state().fonts().has_family(&family, is_emoji); - - res + get_resources().fonts.has_family(&family, is_emoji) }