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 <keavon@keavon.com>
This commit is contained in:
Salman Abuhaimed
2025-07-13 18:06:41 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent c5800aa96e
commit 166eb00c9c
6 changed files with 62 additions and 30 deletions
@@ -1222,6 +1222,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false), NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false),
NodeInput::value(TaggedValue::Bool(false), false),
], ],
..Default::default() ..Default::default()
}, },
@@ -1281,7 +1282,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
), ),
InputMetadata::with_name_description_override( InputMetadata::with_name_description_override(
"Tilt", "Tilt",
"Faux italic", "Faux italic.",
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
min: Some(-85.), min: Some(-85.),
max: Some(85.), max: Some(85.),
@@ -1289,6 +1290,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..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()], output_names: vec!["Vector".to_string()],
..Default::default() ..Default::default()
@@ -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 // 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(); let mut template = resolve_document_node_type(reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut 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)?; 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, 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 // 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
+1 -1
View File
@@ -34,7 +34,7 @@ fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f
#[node_macro::node(category("Text"))] #[node_macro::node(category("Text"))]
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> u32 { 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"))] #[node_macro::node(category("Math: Logic"))]
+41 -22
View File
@@ -1,4 +1,5 @@
use crate::vector::PointId; use crate::instances::Instance;
use crate::vector::{PointId, VectorData, VectorDataTable};
use bezier_rs::{ManipulatorGroup, Subpath}; use bezier_rs::{ManipulatorGroup, Subpath};
use core::cell::RefCell; use core::cell::RefCell;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
@@ -20,24 +21,20 @@ thread_local! {
struct PathBuilder { struct PathBuilder {
current_subpath: Subpath<PointId>, current_subpath: Subpath<PointId>,
glyph_subpaths: Vec<Subpath<PointId>>,
other_subpaths: Vec<Subpath<PointId>>,
origin: DVec2, origin: DVec2,
glyph_subpaths: Vec<Subpath<PointId>>,
vector_table: VectorDataTable,
scale: f64, scale: f64,
id: PointId, id: PointId,
} }
impl PathBuilder { impl PathBuilder {
fn point(&self, x: f32, y: f32) -> DVec2 { 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 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) { #[allow(clippy::too_many_arguments)]
self.origin = DVec2::new(x, y); fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option<DAffine2>, skew: DAffine2, per_glyph_instances: bool) {
}
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], style_skew: Option<DAffine2>, skew: DAffine2) {
let location_ref = LocationRef::new(normalized_coords); let location_ref = LocationRef::new(normalized_coords);
let settings = DrawSettings::unhinted(Size::new(size), location_ref); let settings = DrawSettings::unhinted(Size::new(size), location_ref);
glyph.draw(settings, self).unwrap(); glyph.draw(settings, self).unwrap();
@@ -52,8 +49,19 @@ impl PathBuilder {
glyph_subpath.apply_transform(skew); glyph_subpath.apply_transform(skew);
} }
if !self.glyph_subpaths.is_empty() { if per_glyph_instances {
self.other_subpaths.extend(core::mem::take(&mut self.glyph_subpaths)); 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 mut run_x = glyph_run.offset();
let run_y = glyph_run.baseline(); 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(); let outlines = font_ref.outline_glyphs();
for glyph in glyph_run.glyphs() { for glyph in glyph_run.glyphs() {
let glyph_x = run_x + glyph.x; let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
let glyph_y = run_y - glyph.y;
run_x += glyph.advance; run_x += glyph.advance;
let glyph_id = GlyphId::from(glyph.id); let glyph_id = GlyphId::from(glyph.id);
if let Some(glyph_outline) = outlines.get(glyph_id) { if let Some(glyph_outline) = outlines.get(glyph_id) {
path_builder.set_origin(glyph_x as f64, glyph_y as f64); if !per_glyph_instances {
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, skew); 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<Blob<u8>>, typesetting: TypesettingC
Some(layout) Some(layout)
} }
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> { pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> VectorDataTable {
let Some(layout) = layout_text(str, font_data, typesetting) else { return Vec::new() }; let Some(layout) = layout_text(str, font_data, typesetting) else {
return VectorDataTable::new(VectorData::default());
};
let mut path_builder = PathBuilder { let mut path_builder = PathBuilder {
current_subpath: Subpath::new(Vec::new(), false), current_subpath: Subpath::new(Vec::new(), false),
glyph_subpaths: Vec::new(), glyph_subpaths: Vec::new(),
other_subpaths: Vec::new(), vector_table: if per_glyph_instances {
origin: DVec2::ZERO, VectorDataTable::default()
} else {
VectorDataTable::new(VectorData::default())
},
scale: layout.scale() as f64, scale: layout.scale() as f64,
id: PointId::ZERO, id: PointId::ZERO,
origin: DVec2::default(),
}; };
for line in layout.lines() { for line in layout.lines() {
for item in line.items() { for item in line.items() {
if let PositionedLayoutItem::GlyphRun(glyph_run) = item { 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<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
+1 -1
View File
@@ -2041,7 +2041,7 @@ fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool {
#[node_macro::node(category("General"), path(graphene_core::vector))] #[node_macro::node(category("General"), path(graphene_core::vector))]
async fn count_elements<I>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] source: Instances<I>) -> u64 { async fn count_elements<I>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] source: Instances<I>) -> u64 {
source.instance_iter().count() as u64 source.len() as u64
} }
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
+6 -4
View File
@@ -1,4 +1,4 @@
use crate::vector::{VectorData, VectorDataTable}; use crate::vector::VectorDataTable;
use graph_craft::wasm_application_io::WasmEditorApi; use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_core::Ctx; use graphene_core::Ctx;
pub use graphene_core::text::*; pub use graphene_core::text::*;
@@ -24,9 +24,13 @@ fn text<'i: 'n>(
#[unit(" px")] #[unit(" px")]
#[default(None)] #[default(None)]
max_height: Option<f64>, max_height: Option<f64>,
/// Faux italic.
#[unit("°")] #[unit("°")]
#[default(0.)] #[default(0.)]
tilt: f64, 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 { ) -> VectorDataTable {
let typesetting = TypesettingConfig { let typesetting = TypesettingConfig {
font_size, 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 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); to_path(&text, font_data, typesetting, per_glyph_instances)
VectorDataTable::new(result)
} }