Skip to content

Commit 166eb00

Browse files
BKSalmanKeavon
andauthored
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 <[email protected]>
1 parent c5800aa commit 166eb00

File tree

6 files changed

+62
-30
lines changed

6 files changed

+62
-30
lines changed

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
12221222
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
12231223
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
12241224
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false),
1225+
NodeInput::value(TaggedValue::Bool(false), false),
12251226
],
12261227
..Default::default()
12271228
},
@@ -1281,14 +1282,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
12811282
),
12821283
InputMetadata::with_name_description_override(
12831284
"Tilt",
1284-
"Faux italic",
1285+
"Faux italic.",
12851286
WidgetOverride::Number(NumberInputSettings {
12861287
min: Some(-85.),
12871288
max: Some(85.),
12881289
unit: Some("°".to_string()),
12891290
..Default::default()
12901291
}),
12911292
),
1293+
("Per-Glyph Instances", "Splits each text glyph into its own instance, i.e. row in the table of vector data.").into(),
12921294
],
12931295
output_names: vec!["Vector".to_string()],
12941296
..Default::default()

editor/src/messages/portfolio/document_migration.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
635635
}
636636

637637
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
638-
if reference == "Text" && inputs_count != 9 {
638+
if reference == "Text" && inputs_count != 10 {
639639
let mut template = resolve_document_node_type(reference)?.default_node_template();
640640
document.network_interface.replace_implementation(node_id, network_path, &mut template);
641641
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],
689689
},
690690
network_path,
691691
);
692+
document.network_interface.set_input(
693+
&InputConnector::node(*node_id, 9),
694+
if inputs_count >= 10 {
695+
old_inputs[9].clone()
696+
} else {
697+
NodeInput::value(TaggedValue::Bool(false), false)
698+
},
699+
network_path,
700+
);
692701
}
693702

694703
// 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

node-graph/gcore/src/logic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f
3434

3535
#[node_macro::node(category("Text"))]
3636
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> u32 {
37-
string.len() as u32
37+
string.chars().count() as u32
3838
}
3939

4040
#[node_macro::node(category("Math: Logic"))]

node-graph/gcore/src/text/to_path.rs

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::vector::PointId;
1+
use crate::instances::Instance;
2+
use crate::vector::{PointId, VectorData, VectorDataTable};
23
use bezier_rs::{ManipulatorGroup, Subpath};
34
use core::cell::RefCell;
45
use glam::{DAffine2, DVec2};
@@ -20,24 +21,20 @@ thread_local! {
2021

2122
struct PathBuilder {
2223
current_subpath: Subpath<PointId>,
23-
glyph_subpaths: Vec<Subpath<PointId>>,
24-
other_subpaths: Vec<Subpath<PointId>>,
2524
origin: DVec2,
25+
glyph_subpaths: Vec<Subpath<PointId>>,
26+
vector_table: VectorDataTable,
2627
scale: f64,
2728
id: PointId,
2829
}
2930

3031
impl PathBuilder {
3132
fn point(&self, x: f32, y: f32) -> DVec2 {
32-
// Y-axis inversion converts from font coordinate system (Y-up) to graphics coordinate system (Y-down)
3333
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
3434
}
3535

36-
fn set_origin(&mut self, x: f64, y: f64) {
37-
self.origin = DVec2::new(x, y);
38-
}
39-
40-
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], style_skew: Option<DAffine2>, skew: DAffine2) {
36+
#[allow(clippy::too_many_arguments)]
37+
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) {
4138
let location_ref = LocationRef::new(normalized_coords);
4239
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
4340
glyph.draw(settings, self).unwrap();
@@ -52,8 +49,19 @@ impl PathBuilder {
5249
glyph_subpath.apply_transform(skew);
5350
}
5451

55-
if !self.glyph_subpaths.is_empty() {
56-
self.other_subpaths.extend(core::mem::take(&mut self.glyph_subpaths));
52+
if per_glyph_instances {
53+
if !self.glyph_subpaths.is_empty() {
54+
self.vector_table.push(Instance {
55+
instance: VectorData::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
56+
transform: DAffine2::from_translation(glyph_offset),
57+
..Default::default()
58+
})
59+
}
60+
} else if !self.glyph_subpaths.is_empty() {
61+
for subpath in self.glyph_subpaths.iter() {
62+
// Unwrapping here is ok, since the check above guarantees there is at least one `VectorData`
63+
self.vector_table.get_mut(0).unwrap().instance.append_subpath(subpath, false);
64+
}
5765
}
5866
}
5967
}
@@ -112,7 +120,7 @@ impl Default for TypesettingConfig {
112120
}
113121
}
114122

115-
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64) {
123+
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64, per_glyph_instances: bool) {
116124
let mut run_x = glyph_run.offset();
117125
let run_y = glyph_run.baseline();
118126

@@ -145,14 +153,15 @@ fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder
145153
let outlines = font_ref.outline_glyphs();
146154

147155
for glyph in glyph_run.glyphs() {
148-
let glyph_x = run_x + glyph.x;
149-
let glyph_y = run_y - glyph.y;
156+
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
150157
run_x += glyph.advance;
151158

152159
let glyph_id = GlyphId::from(glyph.id);
153160
if let Some(glyph_outline) = outlines.get(glyph_id) {
154-
path_builder.set_origin(glyph_x as f64, glyph_y as f64);
155-
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, skew);
161+
if !per_glyph_instances {
162+
path_builder.origin = glyph_offset;
163+
}
164+
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances);
156165
}
157166
}
158167
}
@@ -187,27 +196,37 @@ fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingC
187196
Some(layout)
188197
}
189198

190-
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
191-
let Some(layout) = layout_text(str, font_data, typesetting) else { return Vec::new() };
199+
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> VectorDataTable {
200+
let Some(layout) = layout_text(str, font_data, typesetting) else {
201+
return VectorDataTable::new(VectorData::default());
202+
};
192203

193204
let mut path_builder = PathBuilder {
194205
current_subpath: Subpath::new(Vec::new(), false),
195206
glyph_subpaths: Vec::new(),
196-
other_subpaths: Vec::new(),
197-
origin: DVec2::ZERO,
207+
vector_table: if per_glyph_instances {
208+
VectorDataTable::default()
209+
} else {
210+
VectorDataTable::new(VectorData::default())
211+
},
198212
scale: layout.scale() as f64,
199213
id: PointId::ZERO,
214+
origin: DVec2::default(),
200215
};
201216

202217
for line in layout.lines() {
203218
for item in line.items() {
204219
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
205-
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt);
220+
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt, per_glyph_instances);
206221
}
207222
}
208223
}
209224

210-
path_builder.other_subpaths
225+
if path_builder.vector_table.is_empty() {
226+
path_builder.vector_table = VectorDataTable::new(VectorData::default());
227+
}
228+
229+
path_builder.vector_table
211230
}
212231

213232
pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {

node-graph/gcore/src/vector/vector_nodes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2041,7 +2041,7 @@ fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool {
20412041

20422042
#[node_macro::node(category("General"), path(graphene_core::vector))]
20432043
async fn count_elements<I>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] source: Instances<I>) -> u64 {
2044-
source.instance_iter().count() as u64
2044+
source.len() as u64
20452045
}
20462046

20472047
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]

node-graph/gstd/src/text.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::vector::{VectorData, VectorDataTable};
1+
use crate::vector::VectorDataTable;
22
use graph_craft::wasm_application_io::WasmEditorApi;
33
use graphene_core::Ctx;
44
pub use graphene_core::text::*;
@@ -24,9 +24,13 @@ fn text<'i: 'n>(
2424
#[unit(" px")]
2525
#[default(None)]
2626
max_height: Option<f64>,
27+
/// Faux italic.
2728
#[unit("°")]
2829
#[default(0.)]
2930
tilt: f64,
31+
/// Splits each text glyph into its own instance, i.e. row in the table of vector data.
32+
#[default(false)]
33+
per_glyph_instances: bool,
3034
) -> VectorDataTable {
3135
let typesetting = TypesettingConfig {
3236
font_size,
@@ -39,7 +43,5 @@ fn text<'i: 'n>(
3943

4044
let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f));
4145

42-
let result = VectorData::from_subpaths(to_path(&text, font_data, typesetting), false);
43-
44-
VectorDataTable::new(result)
46+
to_path(&text, font_data, typesetting, per_glyph_instances)
4547
}

0 commit comments

Comments
 (0)