From f76c818472bdfc1437a34c3769340a2eed434b7c Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Wed, 13 May 2026 03:08:36 +0530 Subject: [PATCH 01/12] feat: Render List as raw paths in SVG and Vell mode --- Cargo.lock | 2 + .../data_panel/data_panel_message_handler.rs | 2 + editor/src/node_graph_executor/runtime.rs | 4 +- node-graph/libraries/core-types/src/lib.rs | 2 +- node-graph/libraries/core-types/src/list.rs | 6 + .../core-types/src/render_complexity.rs | 6 + .../libraries/graphic-types/src/graphic.rs | 29 ++ node-graph/libraries/rendering/Cargo.toml | 2 + .../libraries/rendering/src/renderer.rs | 343 +++++++++++++++++- node-graph/nodes/gstd/src/render_node.rs | 1 + node-graph/nodes/path-bool/src/lib.rs | 1 + node-graph/nodes/text/src/font_cache.rs | 5 + 12 files changed, 397 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e465c40ff7..9154c6ee2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4539,7 +4539,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 36cde235a8..c321e2385f 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 @@ -336,6 +336,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 @@ -350,6 +351,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 660fa6d2a4..ba60f62b33 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; @@ -386,6 +386,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 b251203ab4..9ed12d07f7 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -26,7 +26,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..9d55eea034 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::None, } } @@ -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::None, } } } @@ -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 6a6ca7c82b..246cb18982 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -13,7 +13,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}; @@ -24,8 +24,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; @@ -33,6 +41,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 { @@ -221,16 +250,26 @@ pub struct RenderParams { pub artboard_background: Option, /// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode. pub viewport_zoom: f64, + // All loaded font bytes extracted from the `FontCache`, keyed by CSS family name. + pub available_fonts: Arc<[(String, Arc<[u8]>)]>, } impl RenderParams { pub fn for_clipper(&self) -> Self { - Self { for_mask: true, ..*self } + Self { + for_mask: true, + available_fonts: self.available_fonts.clone(), + ..*self + } } pub fn for_alignment(&self, transform: DAffine2) -> Self { let alignment_parent_transform = Some(transform); - Self { alignment_parent_transform, ..*self } + Self { + alignment_parent_transform, + available_fonts: self.available_fonts.clone(), + ..*self + } } pub fn to_canvas(&self) -> bool { @@ -431,6 +470,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), } } @@ -442,6 +482,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), } } @@ -490,6 +531,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)); @@ -505,6 +554,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), } } @@ -516,6 +566,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), } } @@ -527,6 +578,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), } } @@ -538,6 +590,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, } } @@ -549,6 +602,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::Text(_) => (), } } } @@ -2040,6 +2094,287 @@ 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 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()); + + 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 { + let color = peniko::Color::new([0_f32, 0., 0., opacity]); + scene.fill(peniko::Fill::NonZero, affine, color, None, &bez_path); + } + } + } + } + } + }); + } + } + 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 font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + // TODO: temporary stepping stone until the Data Trees (Issue #3779) refactor is complete + let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(font_size * 6., font_size)); + 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 121131a062..9afded17d9 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..79854eaa0e 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -133,6 +133,11 @@ impl FontCache { pub fn insert(&mut self, font: Font, data: Vec) { self.font_file_data.insert(font.clone(), data); } + + /// Iterate over all loaded fonts + pub fn iter_fonts(&self) -> impl Iterator)> { + self.font_file_data.iter().map(|(font, bytes)| (font.font_family.as_str(), std::sync::Arc::from(bytes.as_slice()))) + } } // TODO: Eventually remove this migration document upgrade code From de917eaf41619dbab616910785cace9688847653 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Wed, 13 May 2026 03:56:52 +0530 Subject: [PATCH 02/12] chore: code review --- .../libraries/graphic-types/src/graphic.rs | 4 +-- .../libraries/rendering/src/renderer.rs | 27 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 9d55eea034..cd14dc682b 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -374,7 +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::None, + Graphic::Text(_) => RenderBoundingBox::Infinite, } } @@ -386,7 +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::None, + Graphic::Text(_) => RenderBoundingBox::Infinite, } } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 246cb18982..126184240e 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -250,24 +250,16 @@ pub struct RenderParams { pub artboard_background: Option, /// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode. pub viewport_zoom: f64, - // All loaded font bytes extracted from the `FontCache`, keyed by CSS family name. - pub available_fonts: Arc<[(String, Arc<[u8]>)]>, } impl RenderParams { pub fn for_clipper(&self) -> Self { - Self { - for_mask: true, - available_fonts: self.available_fonts.clone(), - ..*self - } + Self { for_mask: true, ..*self } } pub fn for_alignment(&self, transform: DAffine2) -> Self { - let alignment_parent_transform = Some(transform); Self { - alignment_parent_transform, - available_fonts: self.available_fonts.clone(), + alignment_parent_transform: Some(transform), ..*self } } @@ -2295,12 +2287,20 @@ impl Render for List { 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; @@ -2344,14 +2344,17 @@ impl Render for List { let (outline_stroke, outline_color) = get_outline_styles(render_params); scene.stroke(&outline_stroke, affine, outline_color, None, &bez_path); } else { - let color = peniko::Color::new([0_f32, 0., 0., opacity]); - scene.fill(peniko::Fill::NonZero, affine, color, None, &bez_path); + 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) { From b1d90a0f25ba524430ba62d6083c7b4ed44d2bfc Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Wed, 13 May 2026 04:23:01 +0530 Subject: [PATCH 03/12] chore: code review --- editor/src/node_graph_executor/runtime.rs | 2 +- node-graph/nodes/text/src/font_cache.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index ba60f62b33..bc0fd3ad16 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -386,7 +386,7 @@ 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))); + set_render_fonts(self.editor_api.font_cache.iter_fonts().map(|(family, bytes)| (family.to_string(), Arc::from(bytes)))); match self.executor.input_type() { Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()), diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 79854eaa0e..5c3cc92728 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -135,8 +135,8 @@ impl FontCache { } /// Iterate over all loaded fonts - pub fn iter_fonts(&self) -> impl Iterator)> { - self.font_file_data.iter().map(|(font, bytes)| (font.font_family.as_str(), std::sync::Arc::from(bytes.as_slice()))) + pub fn iter_fonts(&self) -> impl Iterator { + self.font_file_data.iter().map(|(font, bytes)| (font.font_family.as_str(), bytes.as_slice())) } } From c8e7856be2d77112ea557cbfa2201648d0fca46d Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Fri, 15 May 2026 03:22:45 +0530 Subject: [PATCH 04/12] chore: change the hardcoded layout bounds to parley's --- .../libraries/rendering/src/renderer.rs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 126184240e..30ecceb271 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2367,10 +2367,27 @@ impl Render for List { 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); - // TODO: temporary stepping stone until the Data Trees (Issue #3779) refactor is complete - let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(font_size * 6., font_size)); + + // 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); From fc6345c7aaec26a66304b5aa5c2794af9c0f5e97 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Fri, 15 May 2026 03:49:19 +0530 Subject: [PATCH 05/12] chore: optimize font loading --- editor/src/node_graph_executor/runtime.rs | 2 +- node-graph/nodes/text/src/font_cache.rs | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index bc0fd3ad16..ba60f62b33 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -386,7 +386,7 @@ 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(), Arc::from(bytes)))); + 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()), diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 5c3cc92728..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,12 +134,17 @@ 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 - pub fn iter_fonts(&self) -> impl Iterator { - self.font_file_data.iter().map(|(font, bytes)| (font.font_family.as_str(), bytes.as_slice())) + /// 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) + }) } } From bba3195a11cd37aeacd452aa5ab48e9f11f67080 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Sat, 16 May 2026 13:12:55 +0530 Subject: [PATCH 06/12] chore: code review --- editor/src/node_graph_executor/runtime.rs | 3 +- node-graph/libraries/core-types/src/list.rs | 2 +- .../libraries/graphic-types/src/graphic.rs | 7 ++- .../libraries/rendering/src/renderer.rs | 44 ++++++++++++------- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index ba60f62b33..f18f40f5b2 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -200,6 +200,7 @@ impl NodeRuntime { for request in requests { match request { GraphRuntimeRequest::FontCacheUpdate(font_cache) => { + set_render_fonts(font_cache.iter_fonts().map(|(family, bytes)| (family.to_string(), bytes))); self.editor_api = PlatformEditorApi { font_cache, application_io: self.editor_api.application_io.clone(), @@ -386,8 +387,6 @@ 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/list.rs b/node-graph/libraries/core-types/src/list.rs index d8722f3a55..4a3346a7ee 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -77,7 +77,7 @@ 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"`). +/// Text item's font family (`String`, implicit default `"Lato"`). pub const ATTR_FONT_FAMILY: &str = "font_family"; /// Text item's font size in document-space units (`f64`, implicit default `16.`). diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index cd14dc682b..93869ba580 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -276,7 +276,12 @@ impl IntoGraphicList for List { impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - List::new_from_element(Graphic::Text(self)) + let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); + let mut graphic_list = List::new_from_element(Graphic::Text(self)); + if !layer_path.is_empty() { + graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); + } + graphic_list } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 30ecceb271..e93da4785b 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -36,6 +36,7 @@ use std::borrow::Cow; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Write; +use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; @@ -43,7 +44,7 @@ use vello::*; // Thread local storage for font bytes thread_local! { - static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); + static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); } // Thread-local parley font shaping context @@ -53,12 +54,20 @@ thread_local! { // Tracks which font bytes have already been registered into FONT_CTX thread_local! { - static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); + 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(); + let slice: Arc<[(String, u64, Arc<[u8]>)]> = fonts + .into_iter() + .map(|(name, bytes)| { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + bytes.hash(&mut hasher); + (name, hasher.finish(), bytes) + }) + .collect::>() + .into(); RENDER_FONTS.with(|f| *f.borrow_mut() = slice); } @@ -2157,9 +2166,8 @@ 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) { + for (_, hash, bytes) in rf.borrow().iter() { + if reg.insert(*hash) { struct ArcBytes(std::sync::Arc<[u8]>); impl AsRef<[u8]> for ArcBytes { fn as_ref(&self) -> &[u8] { @@ -2294,13 +2302,6 @@ impl Render for List { 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; @@ -2313,6 +2314,15 @@ impl Render for List { let mut layout = builder.build(text); layout.break_all_lines(None); + 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 padding = font_size; + let bounds = kurbo::Rect::new(-padding, -padding, layout.full_width() as f64 + padding, layout.height() as f64 + padding); + let transformed_bounds = affine.transform_rect_bbox(bounds); + scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &transformed_bounds); + } + for line in layout.lines() { for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; @@ -2350,11 +2360,11 @@ impl Render for List { } } } - }); - if needs_layer { - scene.pop_layer(); - } + if needs_layer { + scene.pop_layer(); + } + }); } } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { From 9a415e6d8e03bc4691dd230dc3654cbe9a125845 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 03:30:30 +0530 Subject: [PATCH 07/12] feat: Split text node to text_layer and text_to_vector node --- .../graph_modification_utils.rs | 54 ++++ editor/src/node_graph_executor/runtime.rs | 2 +- node-graph/libraries/core-types/src/lib.rs | 3 +- node-graph/libraries/core-types/src/list.rs | 24 ++ .../libraries/rendering/src/renderer.rs | 259 +++++++++++++++--- node-graph/nodes/gstd/src/text.rs | 155 +++++++++++ node-graph/nodes/text/src/font_cache.rs | 4 +- 7 files changed, 463 insertions(+), 38 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index e658f7846a..9ed4afdc6d 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -427,6 +427,10 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) } +pub fn get_text_layer_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_layer::IDENTIFIER)) +} + pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER)) } @@ -484,6 +488,56 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter Some((text, font, typesetting, per_glyph_items)) } +/// Gets properties from the Text Layer node +pub fn get_text_layer(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig)> { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_layer::IDENTIFIER))?; + + let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text_layer::TextInput::INDEX].as_value() else { + return None; + }; + let Some(TaggedValue::Font(font)) = &inputs[graphene_std::text::text_layer::FontInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text_layer::SizeInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(line_height_ratio)) = inputs[graphene_std::text::text_layer::LineHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(character_spacing)) = inputs[graphene_std::text::text_layer::CharacterSpacingInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(has_max_width)) = inputs[graphene_std::text::text_layer::HasMaxWidthInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(max_width)) = inputs[graphene_std::text::text_layer::MaxWidthInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(has_max_height)) = inputs[graphene_std::text::text_layer::HasMaxHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(max_height)) = inputs[graphene_std::text::text_layer::MaxHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(tilt)) = inputs[graphene_std::text::text_layer::TiltInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text_layer::AlignInput::INDEX].as_value() else { + return None; + }; + + let typesetting = TypesettingConfig { + font_size, + line_height_ratio, + max_width: has_max_width.then_some(max_width), + max_height: has_max_height.then_some(max_height), + character_spacing, + tilt, + align, + }; + Some((text, font, typesetting)) +} + pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let weight_node_input_index = graphene_std::vector::stroke::WeightInput::INDEX; if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), weight_node_input_index)? { diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index f18f40f5b2..1588b52899 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -200,7 +200,7 @@ impl NodeRuntime { for request in requests { match request { GraphRuntimeRequest::FontCacheUpdate(font_cache) => { - set_render_fonts(font_cache.iter_fonts().map(|(family, bytes)| (family.to_string(), bytes))); + set_render_fonts(font_cache.iter_fonts().map(|(family, style, bytes)| (family.to_string(), style.to_string(), bytes))); self.editor_api = PlatformEditorApi { font_cache, application_io: self.editor_api.application_io.clone(), diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 9ed12d07f7..b698170692 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -26,7 +26,8 @@ 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_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, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_FONT_STYLE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, + ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, 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 4a3346a7ee..7d71104b83 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -80,9 +80,33 @@ pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; /// Text item's font family (`String`, implicit default `"Lato"`). pub const ATTR_FONT_FAMILY: &str = "font_family"; +/// Text item's font style (`String`, implicit default `"Regular"`). +pub const ATTR_FONT_STYLE: &str = "font_style"; + /// Text item's font size in document-space units (`f64`, implicit default `16.`). pub const ATTR_FONT_SIZE: &str = "font_size"; +/// Text item's full `Font` struct (family + style). Only set by `text_layer`; used by `text_to_vector` to reconstruct exact glyph paths. +pub const ATTR_TEXT_FONT: &str = "text_font"; + +/// Text item's line height ratio relative to the font size (`f64`, implicit default `1.2`). Only stored when it deviates from the default. +pub const ATTR_TEXT_LINE_HEIGHT: &str = "text_line_height"; + +/// Text item's extra inter-character spacing in document-space units (`f64`, implicit default `0.0`). Only stored when non-zero. +pub const ATTR_TEXT_CHARACTER_SPACING: &str = "text_character_spacing"; + +/// Text item's optional max line-wrap width (`Option`). Absent = no limit; present = wrap at that width. +pub const ATTR_TEXT_MAX_WIDTH: &str = "text_max_width"; + +/// Text item's optional max height cutoff (`Option`). Absent = no limit; lines whose baseline exceeds this value are not drawn. +pub const ATTR_TEXT_MAX_HEIGHT: &str = "text_max_height"; + +/// Text item's faux-italic tilt angle in degrees (`f64`, implicit default `0.0`). Only stored when non-zero. +pub const ATTR_TEXT_TILT: &str = "text_tilt"; + +/// Text item's horizontal alignment encoded as a `u8` discriminant of `TextAlign` (0 = AlignLeft, 1 = AlignCenter, 2 = AlignRight, 3–6 = justify variants). Only stored when non-zero. +pub const ATTR_TEXT_ALIGN: &str = "text_align"; + // ======================== // TRAIT: AnyAttributeValue // ======================== diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index e93da4785b..dc073bef7a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -13,7 +13,8 @@ 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_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_FONT_STYLE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, + ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, }; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -26,7 +27,7 @@ use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, S use graphic_types::{Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape}; use num_traits::Zero; -use parley::{FontContext, FontFamily, FontStack, LayoutContext, PositionedLayoutItem, StyleProperty}; +use parley::{AlignmentOptions, FontContext, FontFamily, FontStack, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; use skrifa::GlyphId; use skrifa::MetadataProvider; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; @@ -44,7 +45,7 @@ use vello::*; // Thread local storage for font bytes thread_local! { - static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); + static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); } // Thread-local parley font shaping context @@ -57,14 +58,19 @@ thread_local! { static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); } +// Caches the first FontInfo (weight/style/width) for each (family, style) pair after registration +thread_local! { + static FONT_INFO_CACHE: RefCell> = RefCell::new(HashMap::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, u64, Arc<[u8]>)]> = fonts +pub fn set_render_fonts(fonts: impl IntoIterator)>) { + let slice: Arc<[(String, String, u64, Arc<[u8]>)]> = fonts .into_iter() - .map(|(name, bytes)| { + .map(|(family, style, bytes)| { let mut hasher = std::collections::hash_map::DefaultHasher::new(); bytes.hash(&mut hasher); - (name, hasher.finish(), bytes) + (family, style, hasher.finish(), bytes) }) .collect::>() .into(); @@ -2100,12 +2106,13 @@ struct SvgGlyphPen { d: String, ox: f64, oy: f64, + tilt_tan: f64, } impl SvgGlyphPen { #[inline] - fn px(&self, x: f32) -> f64 { - self.ox + x as f64 + fn px(&self, x: f32, y: f32) -> f64 { + self.ox + x as f64 + (y as f64 * self.tilt_tan) } #[inline] @@ -2116,16 +2123,16 @@ impl SvgGlyphPen { impl OutlinePen for SvgGlyphPen { fn move_to(&mut self, x: f32, y: f32) { - write!(self.d, "M {} {} ", self.px(x), self.py(y)).ok(); + write!(self.d, "M {} {} ", self.px(x, y), self.py(y)).ok(); } fn line_to(&mut self, x: f32, y: f32) { - write!(self.d, "L {} {} ", self.px(x), self.py(y)).ok(); + write!(self.d, "L {} {} ", self.px(x, y), 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(); + write!(self.d, "Q {} {} {} {} ", self.px(x1, y1), self.py(y1), self.px(x, y), 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(); + write!(self.d, "C {} {} {} {} {} {} ", self.px(x1, y1), self.py(y1), self.px(x2, y2), self.py(y2), self.px(x, y), self.py(y)).ok(); } fn close(&mut self) { self.d.push_str("Z "); @@ -2137,24 +2144,33 @@ struct VelloPen<'a> { path: &'a mut BezPath, ox: f64, oy: f64, + tilt_tan: f64, +} + +impl VelloPen<'_> { + #[inline] + fn px(&self, x: f32, y: f32) -> f64 { + self.ox + x as f64 + (y as f64 * self.tilt_tan) + } + + #[inline] + fn py(&self, y: f32) -> f64 { + self.oy - y as 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)); + self.path.move_to((self.px(x, y), self.py(y))); } fn line_to(&mut self, x: f32, y: f32) { - self.path.line_to((self.ox + x as f64, self.oy - y as f64)); + self.path.line_to((self.px(x, y), self.py(y))); } 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)); + self.path.quad_to((self.px(cx, cy), self.py(cy)), (self.px(x, y), self.py(y))); } 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), - ); + self.path.curve_to((self.px(cx1, cy1), self.py(cy1)), (self.px(cx2, cy2), self.py(cy2)), (self.px(x, y), self.py(y))); } fn close(&mut self) { self.path.close_path(); @@ -2166,7 +2182,7 @@ fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { REGISTERED_FONTS.with(|reg| { let mut reg = reg.borrow_mut(); RENDER_FONTS.with(|rf| { - for (_, hash, bytes) in rf.borrow().iter() { + for (family, style, hash, bytes) in rf.borrow().iter() { if reg.insert(*hash) { struct ArcBytes(std::sync::Arc<[u8]>); impl AsRef<[u8]> for ArcBytes { @@ -2175,7 +2191,15 @@ fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { } } 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); + let families = font_ctx.collection.register_fonts(parley::fontique::Blob::new(font_data), None); + + if let Some((_, fonts_info)) = families.first() { + if let Some(font_info) = fonts_info.first() { + FONT_INFO_CACHE.with(|cache| { + cache.borrow_mut().insert((family.clone(), style.clone()), font_info.clone()); + }); + } + } } } }); @@ -2183,7 +2207,7 @@ fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { } const DEFAULT_FONT_FAMILY: &str = "Lato"; -const DEFAULT_FONT_SIZE: f64 = 16.; +const DEFAULT_FONT_SIZE: f64 = 24.; impl Render for List { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { @@ -2198,9 +2222,26 @@ impl Render for List { 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_style: String = self.attribute_cloned_or(ATTR_FONT_STYLE, index, "Regular".to_string()); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); + let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); + let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); + let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); + let tilt: f64 = self.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.); + let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; + let (parley_align, last_line_correction) = match align_u8 { + 1 => (parley::Alignment::Center, None), + 2 => (parley::Alignment::Right, None), + 3 => (parley::Alignment::Justify, Some(parley::Alignment::Left)), + 4 => (parley::Alignment::Justify, Some(parley::Alignment::Center)), + 5 => (parley::Alignment::Justify, Some(parley::Alignment::Right)), + 6 => (parley::Alignment::Justify, Some(parley::Alignment::Justify)), + _ => (parley::Alignment::Left, None), + }; + let mut glyph_paths: Vec = Vec::new(); FONT_CTX.with(|ctx| { @@ -2212,14 +2253,63 @@ impl Render for List { 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()))))); + builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); + builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); + + FONT_INFO_CACHE.with(|cache| { + let cache = cache.borrow(); + if let Some(font_info) = cache.get(&(font_family.clone(), font_style.clone())) { + builder.push_default(StyleProperty::FontWeight(font_info.weight())); + builder.push_default(StyleProperty::FontStyle(font_info.style())); + builder.push_default(StyleProperty::FontWidth(font_info.width())); + } + }); + let mut layout = builder.build(text); - layout.break_all_lines(None); + let max_width_f32 = max_width.map(|w| w as f32); + let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); + layout.break_all_lines(max_width_f32); + layout.align(max_width_f32, parley_align, AlignmentOptions::default()); + + let tilt_tan = tilt.to_radians().tan(); for line in layout.lines() { + let range = line.text_range(); + let is_last_para_line = range.end == text.len() || text.get(range.clone()).is_some_and(|s| s.ends_with('\n')); + + let (x_offset, space_extra) = if let (true, Some(correction)) = (is_last_para_line, last_line_correction) { + let metrics = line.metrics(); + let content_advance = metrics.advance - metrics.trailing_whitespace; + let free_space = alignment_width - content_advance; + // Correction is needed because Parley doesn't remove trailing whitespaces + match correction { + parley::Alignment::Center => (free_space * 0.5, 0.), + parley::Alignment::Right => (free_space, 0.), + parley::Alignment::Justify => { + let line_text = text.get(range.clone()).unwrap_or(""); + let trailing_len = line_text.len() - line_text.trim_end().len(); + let visible_end_index = range.end - trailing_len; + + let space_count: usize = line + .runs() + .map(|run| run.clusters().filter(|c| c.is_space_or_nbsp() && c.text_range().start < visible_end_index).count()) + .sum(); + let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; + (0., extra) + } + _ => (0., 0.), + } + } else { + (0., 0.) + }; + for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + if max_height.is_some_and(|mh| glyph_run.baseline() > mh as f32) { + continue; + } - let mut run_x = glyph_run.offset(); + let mut run_x = glyph_run.offset() + x_offset; let run_y = glyph_run.baseline(); let run = glyph_run.run(); let font = run.font(); @@ -2230,6 +2320,12 @@ impl Render for List { let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; let outlines = font_ref.outline_glyphs(); + let mut pen = SvgGlyphPen { + d: String::new(), + ox: 0., + oy: 0., + tilt_tan, + }; for glyph in glyph_run.glyphs() { let ox = (run_x + glyph.x) as f64; let oy = (run_y - glyph.y) as f64; @@ -2238,9 +2334,14 @@ impl Render for List { 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 }; + + pen.d.clear(); + pen.ox = ox; + pen.oy = oy; if outline.draw(settings, &mut pen).is_ok() && !pen.d.is_empty() { - glyph_paths.push(pen.d); + glyph_paths.push(pen.d.clone()); + } else if space_extra != 0. && glyph.advance > 0. { + run_x += space_extra; } } } @@ -2294,12 +2395,29 @@ impl Render for List { 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_style: String = self.attribute_cloned_or(ATTR_FONT_STYLE, index, "Regular".to_string()); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); + let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); + let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); + let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); + let tilt: f64 = self.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.); + let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); 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 (parley_align, last_line_correction) = match align_u8 { + 1 => (parley::Alignment::Center, None), + 2 => (parley::Alignment::Right, None), + 3 => (parley::Alignment::Justify, Some(parley::Alignment::Left)), + 4 => (parley::Alignment::Justify, Some(parley::Alignment::Center)), + 5 => (parley::Alignment::Justify, Some(parley::Alignment::Right)), + 6 => (parley::Alignment::Justify, Some(parley::Alignment::Justify)), + _ => (parley::Alignment::Left, None), + }; + let affine = Affine::new((transform * item_transform).to_cols_array()); FONT_CTX.with(|ctx| { @@ -2311,8 +2429,23 @@ impl Render for List { 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()))))); + builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); + builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); + + FONT_INFO_CACHE.with(|cache| { + let cache = cache.borrow(); + if let Some(font_info) = cache.get(&(font_family.clone(), font_style.clone())) { + builder.push_default(StyleProperty::FontWeight(font_info.weight())); + builder.push_default(StyleProperty::FontStyle(font_info.style())); + builder.push_default(StyleProperty::FontWidth(font_info.width())); + } + }); + let mut layout = builder.build(text); - layout.break_all_lines(None); + let max_width_f32 = max_width.map(|w| w as f32); + let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); + layout.break_all_lines(max_width_f32); + layout.align(max_width_f32, parley_align, AlignmentOptions::default()); let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); if needs_layer { @@ -2323,11 +2456,45 @@ impl Render for List { scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &transformed_bounds); } + let tilt_tan = tilt.to_radians().tan(); + for line in layout.lines() { + let range = line.text_range(); + let is_last_para_line = range.end == text.len() || text.get(range.clone()).is_some_and(|s| s.ends_with('\n')); + + let (x_offset, space_extra) = if let (true, Some(correction)) = (is_last_para_line, last_line_correction) { + let metrics = line.metrics(); + let content_advance = metrics.advance - metrics.trailing_whitespace; + let free_space = alignment_width - content_advance; + + match correction { + parley::Alignment::Center => (free_space * 0.5, 0.), + parley::Alignment::Right => (free_space, 0.), + parley::Alignment::Justify => { + let line_text = text.get(range.clone()).unwrap_or(""); + let trailing_len = line_text.len() - line_text.trim_end().len(); + let visible_end_index = range.end - trailing_len; + + let space_count: usize = line + .runs() + .map(|run| run.clusters().filter(|c| c.is_space_or_nbsp() && c.text_range().start < visible_end_index).count()) + .sum(); + let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; + (0., extra) + } + _ => (0., 0.), + } + } else { + (0., 0.) + }; + for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + if max_height.is_some_and(|mh| glyph_run.baseline() > mh as f32) { + continue; + } - let mut run_x = glyph_run.offset(); + let mut run_x = glyph_run.offset() + x_offset; let run_y = glyph_run.baseline(); let run = glyph_run.run(); let font = run.font(); @@ -2338,6 +2505,7 @@ impl Render for List { let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; let outlines = font_ref.outline_glyphs(); + let mut bez_path = BezPath::new(); for glyph in glyph_run.glyphs() { let ox = (run_x + glyph.x) as f64; let oy = (run_y - glyph.y) as f64; @@ -2347,8 +2515,13 @@ impl Render for List { 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 }; + bez_path.truncate(0); + let mut pen = VelloPen { + path: &mut bez_path, + ox, + oy, + tilt_tan, + }; 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); @@ -2356,6 +2529,8 @@ impl Render for List { } else { scene.fill(peniko::Fill::NonZero, affine, peniko::Color::BLACK, None, &bez_path); } + } else if space_extra != 0. && glyph.advance > 0. { + run_x += space_extra; } } } @@ -2380,6 +2555,17 @@ impl Render for List { 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 line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); + let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); + let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); + let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); + let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); + let parley_align = match align_u8 { + 1 => parley::Alignment::Center, + 2 => parley::Alignment::Right, + 3..=6 => parley::Alignment::Justify, + _ => parley::Alignment::Left, + }; let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); // Falls back to a single-em square if fonts are not yet registered. @@ -2391,9 +2577,14 @@ impl Render for List { 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()))))); + builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); + builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); - layout.break_all_lines(None); - Some((layout.width() as f64, layout.height() as f64)) + layout.break_all_lines(max_width.map(|w| w as f32)); + layout.align(max_width.map(|w| w as f32), parley_align, AlignmentOptions::default()); + let w = max_width.unwrap_or_else(|| layout.width() as f64); + let h = max_height.unwrap_or_else(|| layout.height() as f64); + Some((w, h)) }) .unwrap_or((font_size, font_size)); diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 89c557e7ca..96355b0ab1 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -1,5 +1,9 @@ use core_types::Ctx; use core_types::list::List; +use core_types::{ + ATTR_EDITOR_LAYER_PATH, ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_FONT_STYLE, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, + ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, +}; use graph_craft::application_io::PlatformEditorApi; use graphic_types::Vector; pub use text_nodes::*; @@ -75,3 +79,154 @@ fn text<'i: 'n>( to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyphs) } + +/// Produces a styled `List` carrying all typographic attributes. +#[node_macro::node(category("Text"))] +fn text_layer( + _: impl Ctx, + _primary: (), + /// The text content to display. + #[widget(ParsedWidgetOverride::Custom = "text_area")] + #[default("Lorem ipsum")] + text: String, + /// The typeface used to render the text. + #[widget(ParsedWidgetOverride::Custom = "text_font")] + font: Font, + /// Font size in document-space pixels. + #[unit(" px")] + #[default(24.)] + #[hard_min(1.)] + size: f64, + /// Line height ratio relative to the font size. 1.2 is the typical default for body copy. + #[unit("x")] + #[hard_min(0.)] + #[step(0.1)] + #[default(1.2)] + line_height: f64, + /// Additional spacing in document-space pixels added between every character pair. + #[unit(" px")] + #[step(0.1)] + character_spacing: f64, + /// Enables the maximum width constraint so lines can wrap. + #[widget(ParsedWidgetOverride::Hidden)] + has_max_width: bool, + /// Maximum line-wrap width in document-space pixels. + #[unit(" px")] + #[hard_min(1.)] + #[widget(ParsedWidgetOverride::Custom = "optional_f64")] + max_width: f64, + /// Enables the maximum height constraint so excess lines are clipped. + #[widget(ParsedWidgetOverride::Hidden)] + has_max_height: bool, + /// Maximum block height in document-space pixels; lines whose baseline exceeds this are not drawn. + #[unit(" px")] + #[hard_min(1.)] + #[widget(ParsedWidgetOverride::Custom = "optional_f64")] + max_height: f64, + /// Faux-italic slant angle in degrees. + #[unit("°")] + #[hard_min(-85.)] + #[hard_max(85.)] + tilt: f64, + /// Horizontal alignment of each line within the text block. + #[widget(ParsedWidgetOverride::Custom = "text_align")] + align: TextAlign, +) -> List { + const DEFAULT_FONT_SIZE: f64 = 24.; + const DEFAULT_LINE_HEIGHT: f64 = 1.2; + + let mut list = List::new_from_element(text); + + // Insert only when value deviates from its default as each stored attribute has runtime cost. + + if font != Font::default() { + list.set_attribute(ATTR_TEXT_FONT, 0, font.clone()); + list.set_attribute(ATTR_FONT_FAMILY, 0, font.font_family.clone()); + list.set_attribute(ATTR_FONT_STYLE, 0, font.font_style.clone()); + } + if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON { + list.set_attribute(ATTR_FONT_SIZE, 0, size); + } + if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON { + list.set_attribute(ATTR_TEXT_LINE_HEIGHT, 0, line_height); + } + if character_spacing != 0. { + list.set_attribute(ATTR_TEXT_CHARACTER_SPACING, 0, character_spacing); + } + if has_max_width { + list.set_attribute(ATTR_TEXT_MAX_WIDTH, 0, Some(max_width)); + } + if has_max_height { + list.set_attribute(ATTR_TEXT_MAX_HEIGHT, 0, Some(max_height)); + } + if tilt != 0. { + list.set_attribute(ATTR_TEXT_TILT, 0, tilt); + } + if align != TextAlign::default() { + list.set_attribute(ATTR_TEXT_ALIGN, 0, align as u8); + } + + list +} + +/// Converts a styled `List` into vector geometry. +/// Each string item is independently shaped by Parley and vectorised via skrifa. +#[node_macro::node(category("Text"))] +fn text_to_vector<'i: 'n>( + _: impl Ctx, + /// A styled list of text strings produced by the **Text Layer** node (or any other `List` source). + #[implementations(List)] + strings: List, + /// The Graphite editor's source for global font resources. + #[scope("editor-api")] + #[widget(ParsedWidgetOverride::Hidden)] + editor_resources: &'i PlatformEditorApi, + /// When enabled, each glyph is emitted as its own vector item instead of a single compound path per string. + separate_glyphs: bool, +) -> List { + let mut result = List::new(); + + for index in 0..strings.len() { + let Some(text) = strings.element(index) else { continue }; + if text.is_empty() { + continue; + } + + let font: Font = strings.attribute_cloned_or(ATTR_TEXT_FONT, index, Font::default()); + + let typesetting = TypesettingConfig { + font_size: strings.attribute_cloned_or(ATTR_FONT_SIZE, index, 24.), + line_height_ratio: strings.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2), + character_spacing: strings.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.), + max_width: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_WIDTH, index, None), + max_height: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_HEIGHT, index, None), + tilt: strings.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.), + align: match strings.attribute_cloned_or::(ATTR_TEXT_ALIGN, index, 0) { + 1 => TextAlign::AlignCenter, + 2 => TextAlign::AlignRight, + 3 => TextAlign::JustifyLeft, + 4 => TextAlign::JustifyCenter, + 5 => TextAlign::JustifyRight, + 6 => TextAlign::JustifyAll, + _ => TextAlign::AlignLeft, + }, + }; + + let vectors = to_path(text, &font, &editor_resources.font_cache, typesetting, separate_glyphs); + let transform = strings.attribute_cloned_or_default::(ATTR_TRANSFORM, index); + let layer_path = strings.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, index); + + for mut item in vectors.into_iter() { + if transform != glam::DAffine2::IDENTITY { + let local = item.attribute_cloned_or_default::(ATTR_TRANSFORM); + item.set_attribute(ATTR_TRANSFORM, transform * local); + } + if !layer_path.is_empty() { + item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); + } + result.push(item); + } + } + + result +} diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 4232cfd817..cc5353c792 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -140,10 +140,10 @@ impl FontCache { } /// Iterate over all loaded fonts, yielding a zero-copy `Arc<[u8]>` reference to each font's bytes. - pub fn iter_fonts(&self) -> impl Iterator)> { + 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) + (font.font_family.as_str(), font.font_style.as_str(), arc) }) } } From 49475f17df230f25ffe6315b6caf5a3ec6108cd2 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 03:50:54 +0530 Subject: [PATCH 08/12] fix: CI fail because of difference in nature of Mac and github action --- node-graph/libraries/rendering/src/renderer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index dc073bef7a..6fbe02d0c5 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -27,7 +27,7 @@ use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, S use graphic_types::{Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape}; use num_traits::Zero; -use parley::{AlignmentOptions, FontContext, FontFamily, FontStack, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; +use parley::{AlignmentOptions, FontContext, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; use skrifa::GlyphId; use skrifa::MetadataProvider; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; @@ -2252,7 +2252,7 @@ impl Render for List { 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()))))); + builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2428,7 +2428,7 @@ impl Render for List { 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()))))); + builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2576,7 +2576,7 @@ impl Render for List { 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()))))); + builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); From 83ccc3cf86be376ca903e57b293c19e3340bf3bc Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 04:02:36 +0530 Subject: [PATCH 09/12] chore: fix --- node-graph/libraries/rendering/src/renderer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6fbe02d0c5..f55257ec42 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2252,7 +2252,7 @@ impl Render for List { 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(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2428,7 +2428,7 @@ impl Render for List { 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(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2576,7 +2576,7 @@ impl Render for List { 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(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); From a999efd0f86ba6854a56e337020483ea1c6ddf7c Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 04:30:28 +0530 Subject: [PATCH 10/12] chore: replace FontStack as it got removed in parley 0.9 --- Cargo.lock | 2 +- node-graph/libraries/rendering/src/renderer.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9154c6ee2e..f54f9f44d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4541,7 +4541,7 @@ dependencies = [ "num-traits", "parley", "serde", - "skrifa 0.40.0", + "skrifa", "usvg", "vector-types", "vello", diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index f55257ec42..f504af72e4 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2252,7 +2252,7 @@ impl Render for List { 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(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2269,7 +2269,7 @@ impl Render for List { let max_width_f32 = max_width.map(|w| w as f32); let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); layout.break_all_lines(max_width_f32); - layout.align(max_width_f32, parley_align, AlignmentOptions::default()); + layout.align(parley_align, AlignmentOptions::default()); let tilt_tan = tilt.to_radians().tan(); @@ -2428,7 +2428,7 @@ impl Render for List { 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(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2445,7 +2445,7 @@ impl Render for List { let max_width_f32 = max_width.map(|w| w as f32); let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); layout.break_all_lines(max_width_f32); - layout.align(max_width_f32, parley_align, AlignmentOptions::default()); + layout.align(parley_align, AlignmentOptions::default()); let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); if needs_layer { @@ -2576,12 +2576,12 @@ impl Render for List { 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(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); layout.break_all_lines(max_width.map(|w| w as f32)); - layout.align(max_width.map(|w| w as f32), parley_align, AlignmentOptions::default()); + layout.align(parley_align, AlignmentOptions::default()); let w = max_width.unwrap_or_else(|| layout.width() as f64); let h = max_height.unwrap_or_else(|| layout.height() as f64); Some((w, h)) From 9e108bf25dbe0fe85537de7a638c880acd38ec36 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 04:31:23 +0530 Subject: [PATCH 11/12] chore: fmt --- node-graph/libraries/rendering/src/renderer.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index f504af72e4..9c6324c72c 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2252,7 +2252,9 @@ impl Render for List { 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::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( + font_family.as_str(), + ))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2428,7 +2430,9 @@ impl Render for List { 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::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( + font_family.as_str(), + ))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2576,7 +2580,9 @@ impl Render for List { 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::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( + font_family.as_str(), + ))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); From 4c94c89fbca2f75e39662c30a7501313b657d658 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 04:55:22 +0530 Subject: [PATCH 12/12] refactor: store TextAlign directly in node-graph attributes instead of casting to u8 --- node-graph/nodes/gstd/src/text.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 96355b0ab1..ccf68fe722 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -163,7 +163,7 @@ fn text_layer( list.set_attribute(ATTR_TEXT_TILT, 0, tilt); } if align != TextAlign::default() { - list.set_attribute(ATTR_TEXT_ALIGN, 0, align as u8); + list.set_attribute(ATTR_TEXT_ALIGN, 0, align); } list @@ -201,15 +201,7 @@ fn text_to_vector<'i: 'n>( max_width: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_WIDTH, index, None), max_height: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_HEIGHT, index, None), tilt: strings.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.), - align: match strings.attribute_cloned_or::(ATTR_TEXT_ALIGN, index, 0) { - 1 => TextAlign::AlignCenter, - 2 => TextAlign::AlignRight, - 3 => TextAlign::JustifyLeft, - 4 => TextAlign::JustifyCenter, - 5 => TextAlign::JustifyRight, - 6 => TextAlign::JustifyAll, - _ => TextAlign::AlignLeft, - }, + align: strings.attribute_cloned_or(ATTR_TEXT_ALIGN, index, TextAlign::default()), }; let vectors = to_path(text, &font, &editor_resources.font_cache, typesetting, separate_glyphs);