Skip to content

Commit 34496cc

Browse files
committed
Make Text node generate per-glyph bounding box click targets
1 parent d5593cb commit 34496cc

4 files changed

Lines changed: 43 additions & 8 deletions

File tree

node-graph/libraries/core-types/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ use std::any::TypeId;
3434
use std::future::Future;
3535
use std::pin::Pin;
3636
pub use table::{
37-
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME,
38-
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
37+
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_END, ATTR_GRADIENT_TYPE,
38+
ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
3939
};
4040
#[cfg(feature = "wasm")]
4141
pub use tsify;

node-graph/libraries/core-types/src/table.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path";
3636
/// the original child layers after their content has been collapsed.
3737
pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers";
3838

39+
/// Optional `Vector` that overrides the row's own geometry for click-target generation.
40+
/// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable
41+
/// by clicking anywhere within their bounds, not just the filled letterform.
42+
pub const ATTR_EDITOR_CLICK_TARGET: &str = "editor:click_target";
43+
3944
/// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes).
4045
pub const ATTR_START: &str = "start";
4146

node-graph/libraries/rendering/src/renderer.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use core_types::table::{Table, TableRow};
1111
use core_types::transform::Footprint;
1212
use core_types::uuid::{NodeId, generate_uuid};
1313
use core_types::{
14-
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY,
15-
ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM,
14+
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_GRADIENT_TYPE, ATTR_LOCATION,
15+
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM,
1616
};
1717
use dyn_any::DynAny;
1818
use glam::{DAffine2, DVec2};
@@ -1381,9 +1381,12 @@ impl Render for Table<Vector> {
13811381

13821382
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
13831383
for index in 0..self.len() {
1384-
let Some(vector) = self.element(index) else { continue };
1384+
let Some(source) = self.element(index) else { continue };
13851385
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
13861386

1387+
// Use click-target override geometry if the row provides one (e.g. Text node's per-glyph bounding boxes)
1388+
let vector = self.attribute::<Vector>(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source);
1389+
13871390
let stroke_width = vector.style.stroke().as_ref().map_or(0., Stroke::effective_width);
13881391
let filled = vector.style.fill() != &Fill::None;
13891392
let fill = |mut subpath: Subpath<_>| {

node-graph/nodes/text/src/path_builder.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use core_types::ATTR_TRANSFORM;
21
use core_types::table::{Table, TableRow};
2+
use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_TRANSFORM};
33
use glam::{DAffine2, DVec2};
44
use parley::GlyphRun;
55
use skrifa::GlyphId;
@@ -15,6 +15,8 @@ pub struct PathBuilder {
1515
origin: DVec2,
1616
glyph_subpaths: Vec<Subpath<PointId>>,
1717
pub vector_table: Table<Vector>,
18+
/// Per-glyph bbox rectangles collected in single-row mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`.
19+
merged_click_target_subpaths: Vec<Subpath<PointId>>,
1820
scale: f64,
1921
id: PointId,
2022
}
@@ -25,6 +27,7 @@ impl PathBuilder {
2527
current_subpath: Subpath::new(Vec::new(), false),
2628
glyph_subpaths: Vec::new(),
2729
vector_table: if per_glyph_items { Table::new() } else { Table::new_from_element(Vector::default()) },
30+
merged_click_target_subpaths: Vec::new(),
2831
scale,
2932
id: PointId::ZERO,
3033
origin: DVec2::default(),
@@ -51,14 +54,24 @@ impl PathBuilder {
5154
glyph_subpath.apply_transform(skew);
5255
}
5356

57+
// Bounding-box rectangle for click-targeting the glyph's full bounds (not just the letterform)
58+
let glyph_bbox_rectangle = subpaths_bounding_box(&self.glyph_subpaths).map(|[min, max]| Subpath::new_rectangle(min, max));
59+
5460
if per_glyph_items {
55-
self.vector_table
56-
.push(TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)).with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset)));
61+
let row = TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)).with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset));
62+
let row = match glyph_bbox_rectangle {
63+
Some(rect) => row.with_attribute(ATTR_EDITOR_CLICK_TARGET, Vector::from_subpaths([rect], false)),
64+
None => row,
65+
};
66+
self.vector_table.push(row);
5767
} else {
5868
for subpath in self.glyph_subpaths.drain(..) {
5969
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Table<Vector>` item
6070
self.vector_table.element_mut(0).unwrap().append_subpath(subpath, false);
6171
}
72+
if let Some(rect) = glyph_bbox_rectangle {
73+
self.merged_click_target_subpaths.push(rect);
74+
}
6275
}
6376
}
6477

@@ -120,10 +133,24 @@ impl PathBuilder {
120133
if self.vector_table.is_empty() {
121134
self.vector_table = Table::new_from_element(Vector::default());
122135
}
136+
137+
// With "Separate Glyph Elements" inactive, combine the accumulated per-glyph AABBs as one override `Vector`
138+
if !self.merged_click_target_subpaths.is_empty() {
139+
self.vector_table
140+
.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_subpaths(self.merged_click_target_subpaths, false));
141+
}
142+
123143
self.vector_table
124144
}
125145
}
126146

147+
fn subpaths_bounding_box(subpaths: &[Subpath<PointId>]) -> Option<[DVec2; 2]> {
148+
subpaths
149+
.iter()
150+
.filter_map(|subpath| subpath.bounding_box())
151+
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)])
152+
}
153+
127154
impl OutlinePen for PathBuilder {
128155
fn move_to(&mut self, x: f32, y: f32) {
129156
if !self.current_subpath.is_empty() {

0 commit comments

Comments
 (0)