From 166eb00c9c1a52005b55d625398c52df73dd324f Mon Sep 17 00:00:00 2001 From: Salman Abuhaimed <85521119+BKSalman@users.noreply.github.com> Date: Mon, 14 Jul 2025 04:06:41 +0300 Subject: [PATCH] Separate the Text node's generated glyphs into separate vector table rows (#2821) * Separate glyphs into Vector data rows * Fix `String Length` node - Properly count characters with `str.chars().count()` instead of bytes `str.len()` - Change `String Length` node's output to `u32` * Apply transform on instance instead of applying it when drawing the glyph * Add checkbox to enable/disable per-glyph instances * Tooltips --------- Co-authored-by: Keavon Chambers --- .../node_graph/document_node_definitions.rs | 4 +- .../messages/portfolio/document_migration.rs | 11 +++- node-graph/gcore/src/logic.rs | 2 +- node-graph/gcore/src/text/to_path.rs | 63 ++++++++++++------- node-graph/gcore/src/vector/vector_nodes.rs | 2 +- node-graph/gstd/src/text.rs | 10 +-- 6 files changed, 62 insertions(+), 30 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 3c851020..67f77045 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1222,6 +1222,7 @@ fn static_nodes() -> Vec { NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false), NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false), + NodeInput::value(TaggedValue::Bool(false), false), ], ..Default::default() }, @@ -1281,7 +1282,7 @@ fn static_nodes() -> Vec { ), InputMetadata::with_name_description_override( "Tilt", - "Faux italic", + "Faux italic.", WidgetOverride::Number(NumberInputSettings { min: Some(-85.), max: Some(85.), @@ -1289,6 +1290,7 @@ fn static_nodes() -> Vec { ..Default::default() }), ), + ("Per-Glyph Instances", "Splits each text glyph into its own instance, i.e. row in the table of vector data.").into(), ], output_names: vec!["Vector".to_string()], ..Default::default() diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 822801f0..7d9eac4b 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -635,7 +635,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], } // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 - if reference == "Text" && inputs_count != 9 { + if reference == "Text" && inputs_count != 10 { let mut template = resolve_document_node_type(reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; @@ -689,6 +689,15 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], }, network_path, ); + document.network_interface.set_input( + &InputConnector::node(*node_id, 9), + if inputs_count >= 10 { + old_inputs[9].clone() + } else { + NodeInput::value(TaggedValue::Bool(false), false) + }, + network_path, + ); } // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default diff --git a/node-graph/gcore/src/logic.rs b/node-graph/gcore/src/logic.rs index 3abd7c49..9be5e7c6 100644 --- a/node-graph/gcore/src/logic.rs +++ b/node-graph/gcore/src/logic.rs @@ -34,7 +34,7 @@ fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f #[node_macro::node(category("Text"))] fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> u32 { - string.len() as u32 + string.chars().count() as u32 } #[node_macro::node(category("Math: Logic"))] diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index bfe60da8..ec649ce3 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -1,4 +1,5 @@ -use crate::vector::PointId; +use crate::instances::Instance; +use crate::vector::{PointId, VectorData, VectorDataTable}; use bezier_rs::{ManipulatorGroup, Subpath}; use core::cell::RefCell; use glam::{DAffine2, DVec2}; @@ -20,24 +21,20 @@ thread_local! { struct PathBuilder { current_subpath: Subpath, - glyph_subpaths: Vec>, - other_subpaths: Vec>, origin: DVec2, + glyph_subpaths: Vec>, + vector_table: VectorDataTable, scale: f64, id: PointId, } impl PathBuilder { fn point(&self, x: f32, y: f32) -> DVec2 { - // Y-axis inversion converts from font coordinate system (Y-up) to graphics coordinate system (Y-down) DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale } - fn set_origin(&mut self, x: f64, y: f64) { - self.origin = DVec2::new(x, y); - } - - fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], style_skew: Option, skew: DAffine2) { + #[allow(clippy::too_many_arguments)] + fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option, skew: DAffine2, per_glyph_instances: bool) { let location_ref = LocationRef::new(normalized_coords); let settings = DrawSettings::unhinted(Size::new(size), location_ref); glyph.draw(settings, self).unwrap(); @@ -52,8 +49,19 @@ impl PathBuilder { glyph_subpath.apply_transform(skew); } - if !self.glyph_subpaths.is_empty() { - self.other_subpaths.extend(core::mem::take(&mut self.glyph_subpaths)); + if per_glyph_instances { + if !self.glyph_subpaths.is_empty() { + self.vector_table.push(Instance { + instance: VectorData::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false), + transform: DAffine2::from_translation(glyph_offset), + ..Default::default() + }) + } + } else if !self.glyph_subpaths.is_empty() { + for subpath in self.glyph_subpaths.iter() { + // Unwrapping here is ok, since the check above guarantees there is at least one `VectorData` + self.vector_table.get_mut(0).unwrap().instance.append_subpath(subpath, false); + } } } } @@ -112,7 +120,7 @@ impl Default for TypesettingConfig { } } -fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64) { +fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64, per_glyph_instances: bool) { let mut run_x = glyph_run.offset(); let run_y = glyph_run.baseline(); @@ -145,14 +153,15 @@ fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder let outlines = font_ref.outline_glyphs(); for glyph in glyph_run.glyphs() { - let glyph_x = run_x + glyph.x; - let glyph_y = run_y - glyph.y; + let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64); run_x += glyph.advance; let glyph_id = GlyphId::from(glyph.id); if let Some(glyph_outline) = outlines.get(glyph_id) { - path_builder.set_origin(glyph_x as f64, glyph_y as f64); - path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, skew); + if !per_glyph_instances { + path_builder.origin = glyph_offset; + } + path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances); } } } @@ -187,27 +196,37 @@ fn layout_text(str: &str, font_data: Option>, typesetting: TypesettingC Some(layout) } -pub fn to_path(str: &str, font_data: Option>, typesetting: TypesettingConfig) -> Vec> { - let Some(layout) = layout_text(str, font_data, typesetting) else { return Vec::new() }; +pub fn to_path(str: &str, font_data: Option>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> VectorDataTable { + let Some(layout) = layout_text(str, font_data, typesetting) else { + return VectorDataTable::new(VectorData::default()); + }; let mut path_builder = PathBuilder { current_subpath: Subpath::new(Vec::new(), false), glyph_subpaths: Vec::new(), - other_subpaths: Vec::new(), - origin: DVec2::ZERO, + vector_table: if per_glyph_instances { + VectorDataTable::default() + } else { + VectorDataTable::new(VectorData::default()) + }, scale: layout.scale() as f64, id: PointId::ZERO, + origin: DVec2::default(), }; for line in layout.lines() { for item in line.items() { if let PositionedLayoutItem::GlyphRun(glyph_run) = item { - render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt); + render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt, per_glyph_instances); } } } - path_builder.other_subpaths + if path_builder.vector_table.is_empty() { + path_builder.vector_table = VectorDataTable::new(VectorData::default()); + } + + path_builder.vector_table } pub fn bounding_box(str: &str, font_data: Option>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 6db9b992..82c9fab1 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -2041,7 +2041,7 @@ fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool { #[node_macro::node(category("General"), path(graphene_core::vector))] async fn count_elements(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable, RasterDataTable)] source: Instances) -> u64 { - source.instance_iter().count() as u64 + source.len() as u64 } #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] diff --git a/node-graph/gstd/src/text.rs b/node-graph/gstd/src/text.rs index 71eea96f..ec5df252 100644 --- a/node-graph/gstd/src/text.rs +++ b/node-graph/gstd/src/text.rs @@ -1,4 +1,4 @@ -use crate::vector::{VectorData, VectorDataTable}; +use crate::vector::VectorDataTable; use graph_craft::wasm_application_io::WasmEditorApi; use graphene_core::Ctx; pub use graphene_core::text::*; @@ -24,9 +24,13 @@ fn text<'i: 'n>( #[unit(" px")] #[default(None)] max_height: Option, + /// Faux italic. #[unit("°")] #[default(0.)] tilt: f64, + /// Splits each text glyph into its own instance, i.e. row in the table of vector data. + #[default(false)] + per_glyph_instances: bool, ) -> VectorDataTable { let typesetting = TypesettingConfig { font_size, @@ -39,7 +43,5 @@ fn text<'i: 'n>( let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f)); - let result = VectorData::from_subpaths(to_path(&text, font_data, typesetting), false); - - VectorDataTable::new(result) + to_path(&text, font_data, typesetting, per_glyph_instances) }