diff --git a/Cargo.lock b/Cargo.lock index dcc71e997b..5b28b1aa4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4464,7 +4464,9 @@ dependencies = [ "kurbo", "log", "num-traits", + "parley", "serde", + "skrifa 0.40.0", "usvg", "vector-types", "vello", diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 3b298ad244..356a0f2ecf 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -335,6 +335,7 @@ impl TableItemLayout for Graphic { Self::RasterGPU(list) => list.identifier(), Self::Color(list) => list.identifier(), Self::Gradient(list) => list.identifier(), + Self::Text(list) => list.identifier(), } } // Don't put a breadcrumb for Graphic @@ -349,6 +350,7 @@ impl TableItemLayout for Graphic { Self::RasterGPU(list) => list.layout_with_breadcrumb(data), Self::Color(list) => list.layout_with_breadcrumb(data), Self::Gradient(list) => list.layout_with_breadcrumb(data), + Self::Text(list) => list.layout_with_breadcrumb(data), } } } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index abe6dca28d..912d5629e2 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -15,7 +15,7 @@ use graphene_std::ops::Convert; #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; -use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; +use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment, set_render_fonts}; use graphene_std::text::FontCache; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; @@ -395,6 +395,8 @@ impl NodeRuntime { async fn execute_network(&mut self, render_config: RenderConfig) -> Result { use graph_craft::graphene_compiler::Executor; + set_render_fonts(self.editor_api.font_cache.iter_fonts().map(|(family, bytes)| (family.to_string(), bytes))); + match self.executor.input_type() { Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()), Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()), diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index fcebee42b6..0ad5bc23f2 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -25,7 +25,7 @@ pub use graphene_hash; pub use graphene_hash::CacheHash; pub use list::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END, - ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, }; pub use memo::MemoHash; pub use no_std_types::AsU32; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 61f54597b4..d8722f3a55 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -77,6 +77,12 @@ pub const ATTR_SPREAD_METHOD: &str = "spread_method"; /// Gradient's `GradientType` (`Linear` or `Radial`). pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; +/// Text item's font family (`String`, implicit default `"sans-serif"`). +pub const ATTR_FONT_FAMILY: &str = "font_family"; + +/// Text item's font size in document-space units (`f64`, implicit default `16.`). +pub const ATTR_FONT_SIZE: &str = "font_size"; + // ======================== // TRAIT: AnyAttributeValue // ======================== diff --git a/node-graph/libraries/core-types/src/render_complexity.rs b/node-graph/libraries/core-types/src/render_complexity.rs index fc035c720a..15578d771c 100644 --- a/node-graph/libraries/core-types/src/render_complexity.rs +++ b/node-graph/libraries/core-types/src/render_complexity.rs @@ -19,3 +19,9 @@ impl RenderComplexity for Color { 1 } } + +impl RenderComplexity for String { + fn render_complexity(&self) -> usize { + 1 + } +} diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 0efade8880..cd14dc682b 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -22,6 +22,7 @@ pub enum Graphic { RasterGPU(List>), Color(List), Gradient(List), + Text(List), } impl Default for Graphic { @@ -103,6 +104,18 @@ impl From> for Graphic { } } +// String +impl From for Graphic { + fn from(text: String) -> Self { + Graphic::Text(List::new_from_element(text)) + } +} +impl From> for Graphic { + fn from(text: List) -> Self { + Graphic::Text(text) + } +} + /// Deeply flattens a `List`, collecting only elements matching a specific variant (extracted by `extract_variant`) /// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity. fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) -> Option>) -> List { @@ -199,6 +212,12 @@ impl TryFromGraphic for GradientStops { } } +impl TryFromGraphic for String { + fn try_from_graphic(graphic: Graphic) -> Option> { + if let Graphic::Text(t) = graphic { Some(t) } else { None } + } +} + // Local trait to convert types to List (avoids orphan rule issues) pub trait IntoGraphicList { fn into_graphic_list(self) -> List; @@ -255,6 +274,12 @@ impl IntoGraphicList for List { } } +impl IntoGraphicList for List { + fn into_graphic_list(self) -> List { + List::new_from_element(Graphic::Text(self)) + } +} + impl IntoGraphicList for DAffine2 { fn into_graphic_list(self) -> List { List::new_from_element(Graphic::default()) @@ -324,6 +349,7 @@ impl Graphic { Graphic::RasterGPU(list) => all_clipped(list), Graphic::Color(list) => all_clipped(list), Graphic::Gradient(list) => all_clipped(list), + Graphic::Text(list) => all_clipped(list), } } @@ -348,6 +374,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(list) => list.bounding_box(transform, include_stroke), Graphic::Color(list) => list.bounding_box(transform, include_stroke), Graphic::Gradient(list) => list.bounding_box(transform, include_stroke), + Graphic::Text(_) => RenderBoundingBox::Infinite, } } @@ -359,6 +386,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke), Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), + Graphic::Text(_) => RenderBoundingBox::Infinite, } } } @@ -388,6 +416,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(list) => list.render_complexity(), Self::Color(list) => list.render_complexity(), Self::Gradient(list) => list.render_complexity(), + Self::Text(list) => list.len(), } } } diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index 1fdfb0c839..c8e4375e3c 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -27,6 +27,8 @@ vector-types = { workspace = true } graphic-types = { workspace = true } vello = { workspace = true } vello_encoding = { workspace = true } +parley = { workspace = true } +skrifa = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index c7064992bb..8b3c1c1d3d 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -12,7 +12,7 @@ use core_types::transform::Footprint; use core_types::uuid::{NodeId, generate_uuid}; use core_types::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, - ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, }; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -23,8 +23,16 @@ use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign}; use graphic_types::{Artboard, Graphic, Vector}; -use kurbo::{Affine, Cap, Join, Shape}; +use kurbo::{Affine, BezPath, Cap, Join, Shape}; use num_traits::Zero; +use parley::{FontContext, FontFamily, FontStack, LayoutContext, PositionedLayoutItem, StyleProperty}; +use skrifa::GlyphId; +use skrifa::MetadataProvider; +use skrifa::instance::{LocationRef, NormalizedCoord, Size}; +use skrifa::outline::{DrawSettings, OutlinePen}; +use skrifa::raw::FontRef as SkrifaFontRef; +use std::borrow::Cow; +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::ops::Deref; @@ -32,6 +40,27 @@ use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; use vello::*; +// Thread local storage for font bytes +thread_local! { + static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); +} + +// Thread-local parley font shaping context +thread_local! { + static FONT_CTX: RefCell<(FontContext, LayoutContext<()>)> = RefCell::new((FontContext::default(), LayoutContext::default())); +} + +// Tracks which font bytes have already been registered into FONT_CTX +thread_local! { + static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); +} + +// Set the font bytes available to the renderer for the current execution. +pub fn set_render_fonts(fonts: impl IntoIterator)>) { + let slice: Arc<[(String, Arc<[u8]>)]> = fonts.into_iter().collect::>().into(); + RENDER_FONTS.with(|f| *f.borrow_mut() = slice); +} + #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum MaskType { @@ -228,8 +257,10 @@ impl RenderParams { } pub fn for_alignment(&self, transform: DAffine2) -> Self { - let alignment_parent_transform = Some(transform); - Self { alignment_parent_transform, ..*self } + Self { + alignment_parent_transform: Some(transform), + ..*self + } } pub fn to_canvas(&self) -> bool { @@ -429,6 +460,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(list) => list.render_svg(render, render_params), Graphic::Gradient(list) => list.render_svg(render, render_params), + Graphic::Text(list) => list.render_svg(render, render_params), } } @@ -440,6 +472,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Color(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(list) => list.render_to_vello(scene, transform, context, render_params), + Graphic::Text(list) => list.render_to_vello(scene, transform, context, render_params), } } @@ -488,6 +521,14 @@ impl Render for Graphic { Graphic::Gradient(list) => { metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + } + } + Graphic::Text(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first item if !list.is_empty() { metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); @@ -503,6 +544,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.collect_metadata(metadata, footprint, element_id), Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id), Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id), + Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id), } } @@ -514,6 +556,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.add_upstream_click_targets(click_targets), Graphic::Color(list) => list.add_upstream_click_targets(click_targets), Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets), + Graphic::Text(list) => list.add_upstream_click_targets(click_targets), } } @@ -525,6 +568,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.add_upstream_outline_targets(outlines), Graphic::Color(list) => list.add_upstream_outline_targets(outlines), Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines), + Graphic::Text(list) => list.add_upstream_outline_targets(outlines), } } @@ -536,6 +580,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.contains_artboard(), Graphic::Color(list) => list.contains_artboard(), Graphic::Gradient(list) => list.contains_artboard(), + Graphic::Text(_) => false, } } @@ -547,6 +592,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::Text(_) => (), } } } @@ -2038,6 +2084,315 @@ impl Render for List { } } +/// Helper struct to write path data to a string +struct SvgGlyphPen { + d: String, + ox: f64, + oy: f64, +} + +impl SvgGlyphPen { + #[inline] + fn px(&self, x: f32) -> f64 { + self.ox + x as f64 + } + + #[inline] + fn py(&self, y: f32) -> f64 { + self.oy - y as f64 + } +} + +impl OutlinePen for SvgGlyphPen { + fn move_to(&mut self, x: f32, y: f32) { + write!(self.d, "M {} {} ", self.px(x), self.py(y)).ok(); + } + fn line_to(&mut self, x: f32, y: f32) { + write!(self.d, "L {} {} ", self.px(x), self.py(y)).ok(); + } + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + write!(self.d, "Q {} {} {} {} ", self.px(x1), self.py(y1), self.px(x), self.py(y)).ok(); + } + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + write!(self.d, "C {} {} {} {} {} {} ", self.px(x1), self.py(y1), self.px(x2), self.py(y2), self.px(x), self.py(y)).ok(); + } + fn close(&mut self) { + self.d.push_str("Z "); + } +} + +/// Helper struct to build a `kurbo::BezPath` for Vello rendering. +struct VelloPen<'a> { + path: &'a mut BezPath, + ox: f64, + oy: f64, +} + +impl OutlinePen for VelloPen<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.path.move_to((self.ox + x as f64, self.oy - y as f64)); + } + fn line_to(&mut self, x: f32, y: f32) { + self.path.line_to((self.ox + x as f64, self.oy - y as f64)); + } + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + self.path.quad_to((self.ox + cx as f64, self.oy - cy as f64), (self.ox + x as f64, self.oy - y as f64)); + } + fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { + self.path.curve_to( + (self.ox + cx1 as f64, self.oy - cy1 as f64), + (self.ox + cx2 as f64, self.oy - cy2 as f64), + (self.ox + x as f64, self.oy - y as f64), + ); + } + fn close(&mut self) { + self.path.close_path(); + } +} + +/// Registers all fonts from `RENDER_FONTS` that aren't yet in `FONT_CTX`. +fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { + REGISTERED_FONTS.with(|reg| { + let mut reg = reg.borrow_mut(); + RENDER_FONTS.with(|rf| { + for (_, bytes) in rf.borrow().iter() { + let key = bytes.as_ptr() as usize; + if reg.insert(key) { + struct ArcBytes(std::sync::Arc<[u8]>); + impl AsRef<[u8]> for ArcBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } + } + let font_data: std::sync::Arc + Send + Sync> = std::sync::Arc::new(ArcBytes(bytes.clone())); + font_ctx.collection.register_fonts(parley::fontique::Blob::new(font_data), None); + } + } + }); + }); +} + +const DEFAULT_FONT_FAMILY: &str = "Lato"; +const DEFAULT_FONT_SIZE: f64 = 16.; + +impl Render for List { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + for index in 0..self.len() { + let Some(text) = self.element(index) else { continue }; + if text.is_empty() { + continue; + } + + let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); + let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); + let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; + + let mut glyph_paths: Vec = Vec::new(); + + FONT_CTX.with(|ctx| { + let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; + let (font_ctx, layout_ctx) = &mut *ctx; + + ensure_fonts_registered(font_ctx); + + let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); + builder.push_default(StyleProperty::FontSize(font_size as f32)); + builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + let mut layout = builder.build(text); + layout.break_all_lines(None); + + for line in layout.lines() { + for item in line.items() { + let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let run = glyph_run.run(); + let font = run.font(); + let font_size_pts = run.font_size(); + let normalized_coords: Vec = run.normalized_coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect(); + + let font_data = font.data.as_ref(); + let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; + let outlines = font_ref.outline_glyphs(); + + for glyph in glyph_run.glyphs() { + let ox = (run_x + glyph.x) as f64; + let oy = (run_y - glyph.y) as f64; + run_x += glyph.advance; + + let glyph_id = GlyphId::from(glyph.id); + let Some(outline) = outlines.get(glyph_id) else { continue }; + let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords)); + let mut pen = SvgGlyphPen { d: String::new(), ox, oy }; + if outline.draw(settings, &mut pen).is_ok() && !pen.d.is_empty() { + glyph_paths.push(pen.d); + } + } + } + } + }); + + if glyph_paths.is_empty() { + continue; + } + + // Wrap all glyph elements in a with the item's transform/opacity/blend-mode. + render.parent_tag( + "g", + |attributes| { + let matrix = format_transform_matrix(transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); + } + if blend_mode_attr != BlendMode::default() { + attributes.push("style", blend_mode_attr.render()); + } + }, + |render| { + for path_d in glyph_paths { + render.leaf_tag("path", |attributes| { + attributes.push("d", path_d); + if let RenderMode::Outline = render_params.render_mode { + attributes.push("fill", "none"); + attributes.push("stroke", "black"); + attributes.push("stroke-width", "1"); + } else { + attributes.push("fill", "black"); + attributes.push("fill-rule", "nonzero"); + } + }); + } + }, + ); + } + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + for index in 0..self.len() { + let Some(text) = self.element(index) else { continue }; + if text.is_empty() { + continue; + } + + let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); + let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); + let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; + + let affine = Affine::new((transform * item_transform).to_cols_array()); + + let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); + if needs_layer { + let blending = peniko::BlendMode::new(blend_mode_attr.to_peniko(), peniko::Compose::SrcOver); + let inf_rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(f64::INFINITY, f64::INFINITY)); + scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &inf_rect); + } + + FONT_CTX.with(|ctx| { + let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; + let (font_ctx, layout_ctx) = &mut *ctx; + + ensure_fonts_registered(font_ctx); + + let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); + builder.push_default(StyleProperty::FontSize(font_size as f32)); + builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + let mut layout = builder.build(text); + layout.break_all_lines(None); + + for line in layout.lines() { + for item in line.items() { + let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let run = glyph_run.run(); + let font = run.font(); + let font_size_pts = run.font_size(); + let normalized_coords: Vec = run.normalized_coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect(); + + let font_data = font.data.as_ref(); + let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; + let outlines = font_ref.outline_glyphs(); + + for glyph in glyph_run.glyphs() { + let ox = (run_x + glyph.x) as f64; + let oy = (run_y - glyph.y) as f64; + run_x += glyph.advance; + + let glyph_id = GlyphId::from(glyph.id); + let Some(outline) = outlines.get(glyph_id) else { continue }; + let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords)); + + let mut bez_path = BezPath::new(); + let mut pen = VelloPen { path: &mut bez_path, ox, oy }; + if outline.draw(settings, &mut pen).is_ok() && !bez_path.elements().is_empty() { + if let RenderMode::Outline = render_params.render_mode { + let (outline_stroke, outline_color) = get_outline_styles(render_params); + scene.stroke(&outline_stroke, affine, outline_color, None, &bez_path); + } else { + scene.fill(peniko::Fill::NonZero, affine, peniko::Color::BLACK, None, &bez_path); + } + } + } + } + } + }); + + if needs_layer { + scene.pop_layer(); + } + } + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { + let Some(element_id) = element_id else { return }; + metadata.upstream_footprints.insert(element_id, footprint); + if !self.is_empty() { + metadata.local_transforms.insert(element_id, self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + } + } + + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { + for index in 0..self.len() { + let Some(text) = self.element(index) else { continue }; + let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + + // Falls back to a single-em square if fonts are not yet registered. + let (width, height) = FONT_CTX + .with(|ctx| { + let Ok(mut ctx) = ctx.try_borrow_mut() else { return None }; + let (font_ctx, layout_ctx) = &mut *ctx; + ensure_fonts_registered(font_ctx); + let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); + builder.push_default(StyleProperty::FontSize(font_size as f32)); + builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + let mut layout = builder.build(text); + layout.break_all_lines(None); + Some((layout.width() as f64, layout.height() as f64)) + }) + .unwrap_or((font_size, font_size)); + + let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(width, height)); + let mut target = ClickTarget::new_with_subpath(subpath, 0.); + target.apply_transform(transform); + click_targets.push(target); + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str), diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index bdcd02ea22..fb516ff607 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -39,6 +39,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Context -> List>, Context -> List, Context -> List, + Context -> List, )] data: impl Node, Output = T>, ) -> RenderIntermediate { diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 14c0025d52..9fffc0a704 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -278,6 +278,7 @@ fn flatten_vector(graphic_list: &List) -> List { Item::from_parts(element, attributes) }) .collect::>(), + Graphic::Text(_) => Vec::new(), } }) .collect() diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 258452ebc4..4232cfd817 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -78,6 +78,9 @@ impl Default for Font { pub struct FontCache { /// Actual font file data used for rendering a font font_file_data: HashMap>, + /// Built once per `insert` call and never re-allocated. + #[cfg_attr(feature = "serde", serde(skip))] + arc_cache: HashMap>, } impl std::fmt::Debug for FontCache { @@ -131,8 +134,18 @@ impl FontCache { /// Insert a new font into the cache pub fn insert(&mut self, font: Font, data: Vec) { + let arc: Arc<[u8]> = Arc::from(data.as_slice()); + self.arc_cache.insert(font.clone(), arc); self.font_file_data.insert(font.clone(), data); } + + /// Iterate over all loaded fonts, yielding a zero-copy `Arc<[u8]>` reference to each font's bytes. + pub fn iter_fonts(&self) -> impl Iterator)> { + self.font_file_data.iter().map(|(font, bytes)| { + let arc = self.arc_cache.get(font).cloned().unwrap_or_else(|| Arc::from(bytes.as_slice())); + (font.font_family.as_str(), arc) + }) + } } // TODO: Eventually remove this migration document upgrade code