mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add custom click targets to make Text node output easier to select (#4095)
* Stop pushing duplicate layer entries when re-clicking an already-selected layer * Make Text node generate per-glyph bounding box click targets * Show source-geometry outlines and aggregate all rows for layer click targets * Strip 'editor:click_target' override on Path node so direct edits restore precise hit testing * Fix inverting a zero-determinate transform
This commit is contained in:
@@ -215,6 +215,9 @@ pub enum DocumentMessage {
|
||||
UpdateClickTargets {
|
||||
click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||
},
|
||||
UpdateOutlines {
|
||||
outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||
},
|
||||
UpdateClipTargets {
|
||||
clip_targets: HashSet<NodeId>,
|
||||
},
|
||||
|
||||
@@ -1270,6 +1270,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
.collect();
|
||||
self.network_interface.update_click_targets(layer_click_targets);
|
||||
}
|
||||
DocumentMessage::UpdateOutlines { outlines } => {
|
||||
let layer_outlines = outlines
|
||||
.into_iter()
|
||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||
.filter_map(|(node_id, outlines)| {
|
||||
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
||||
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
|
||||
(layer, outlines)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.network_interface.update_outlines(layer_outlines);
|
||||
}
|
||||
DocumentMessage::UpdateClipTargets { clip_targets } => {
|
||||
self.network_interface.update_clip_targets(clip_targets);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ pub struct DocumentMetadata {
|
||||
pub first_element_source_ids: HashMap<NodeId, Option<NodeId>>,
|
||||
pub structure: HashMap<LayerNodeIdentifier, NodeRelations>,
|
||||
pub click_targets: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>,
|
||||
/// Source-geometry outlines for hover/selection overlays, separate from `click_targets` so
|
||||
/// nodes with an `editor:click_target` override still outline the precise geometry.
|
||||
pub outlines: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>,
|
||||
pub clip_targets: HashSet<NodeId>,
|
||||
pub vector_modify: HashMap<NodeId, Vector>,
|
||||
/// Vector data keyed by layer ID, used as fallback when no Path node exists.
|
||||
@@ -154,9 +157,15 @@ impl DocumentMetadata {
|
||||
// ===============================
|
||||
|
||||
impl DocumentMetadata {
|
||||
/// Outline targets if present, otherwise click targets. Used for bounding boxes and outline
|
||||
/// drawing so layers with an `editor:click_target` override report precise geometry bounds.
|
||||
fn visual_targets(&self, layer: LayerNodeIdentifier) -> Option<&[Arc<ClickTarget>]> {
|
||||
self.outlines.get(&layer).or_else(|| self.click_targets.get(&layer)).map(|v| v.as_slice())
|
||||
}
|
||||
|
||||
/// Get the bounding box of the click target of the specified layer in the specified transform space
|
||||
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.click_targets(layer)?
|
||||
self.visual_targets(layer)?
|
||||
.iter()
|
||||
.filter_map(|click_target| click_target.bounding_box_with_transform(transform))
|
||||
.reduce(Quad::combine_bounds)
|
||||
@@ -164,7 +173,7 @@ impl DocumentMetadata {
|
||||
|
||||
/// Get the loose bounding box of the click target of the specified layer in the specified transform space
|
||||
pub fn loose_bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.click_targets(layer)?
|
||||
self.visual_targets(layer)?
|
||||
.iter()
|
||||
.filter_map(|click_target| match click_target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.loose_bounding_box_with_transform(transform),
|
||||
@@ -210,18 +219,14 @@ impl DocumentMetadata {
|
||||
}
|
||||
|
||||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> {
|
||||
static EMPTY: Vec<Arc<ClickTarget>> = Vec::new();
|
||||
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
|
||||
click_targets.iter().filter_map(|target| match target.target_type() {
|
||||
self.visual_targets(layer).unwrap_or(&[]).iter().filter_map(|target| match target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => Some(subpath),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn layer_with_free_points_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &ClickTargetType> {
|
||||
static EMPTY: Vec<Arc<ClickTarget>> = Vec::new();
|
||||
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
|
||||
click_targets.iter().map(|target| target.target_type())
|
||||
self.visual_targets(layer).unwrap_or(&[]).iter().map(|target| target.target_type())
|
||||
}
|
||||
|
||||
pub fn is_clip(&self, node: NodeId) -> bool {
|
||||
|
||||
@@ -3276,6 +3276,7 @@ impl NodeNetworkInterface {
|
||||
self.document_metadata.local_transforms.retain(|node, _| nodes.contains(node));
|
||||
self.document_metadata.vector_modify.retain(|node, _| nodes.contains(node));
|
||||
self.document_metadata.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||
self.document_metadata.outlines.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||
}
|
||||
|
||||
/// Update the cached transforms of the layers
|
||||
@@ -3294,6 +3295,11 @@ impl NodeNetworkInterface {
|
||||
self.document_metadata.click_targets = new_click_targets;
|
||||
}
|
||||
|
||||
/// Update the cached source-geometry outline targets of the layers
|
||||
pub fn update_outlines(&mut self, new_outlines: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>) {
|
||||
self.document_metadata.outlines = new_outlines;
|
||||
}
|
||||
|
||||
/// Update the cached clip targets of the layers
|
||||
pub fn update_clip_targets(&mut self, new_clip_targets: HashSet<NodeId>) {
|
||||
self.document_metadata.clip_targets = new_clip_targets;
|
||||
|
||||
@@ -1823,7 +1823,10 @@ fn drag_shallowest_manipulation(responses: &mut VecDeque<Message>, selected: Vec
|
||||
}
|
||||
|
||||
let new_selected = final_selection.unwrap_or_else(|| clicked_layer.ancestors(document.metadata()).filter(not_artboard(document)).last().unwrap_or(clicked_layer));
|
||||
tool_data.layers_dragging.extend(vec![new_selected]);
|
||||
// Duplicates cause `SelectedNodesSet` to carry the layer twice, breaking the Data panel's single-selection check in `node_to_inspect`
|
||||
if !tool_data.layers_dragging.contains(&new_selected) {
|
||||
tool_data.layers_dragging.push(new_selected);
|
||||
}
|
||||
tool_data.layers_dragging.retain(|&selected_layer| !selected_layer.is_child_of(metadata, &new_selected));
|
||||
if remove {
|
||||
tool_data.layers_dragging.retain(|&selected_layer| clicked_layer != selected_layer);
|
||||
@@ -1885,7 +1888,10 @@ fn drag_deepest_manipulation(responses: &mut VecDeque<Message>, selected: Vec<La
|
||||
);
|
||||
|
||||
if !remove {
|
||||
tool_data.layers_dragging.extend(vec![layer]);
|
||||
// Duplicates cause `SelectedNodesSet` to carry the layer twice, breaking the Data panel's single-selection check in `node_to_inspect`
|
||||
if !tool_data.layers_dragging.contains(&layer) {
|
||||
tool_data.layers_dragging.push(layer);
|
||||
}
|
||||
} else {
|
||||
tool_data.layers_dragging.retain(|&selected_layer| layer != selected_layer);
|
||||
}
|
||||
|
||||
@@ -309,6 +309,7 @@ impl NodeGraphExecutor {
|
||||
Err(e) => {
|
||||
// Clear the click targets while the graph is in an un-renderable state
|
||||
document.network_interface.update_click_targets(HashMap::new());
|
||||
document.network_interface.update_outlines(HashMap::new());
|
||||
document.network_interface.update_vector_modify(HashMap::new());
|
||||
return Err(format!("Node graph evaluation failed:\n{e}"));
|
||||
}
|
||||
@@ -355,6 +356,7 @@ impl NodeGraphExecutor {
|
||||
// Clear the click targets while the graph is in an un-renderable state
|
||||
|
||||
document.network_interface.update_click_targets(HashMap::new());
|
||||
document.network_interface.update_outlines(HashMap::new());
|
||||
document.network_interface.update_vector_modify(HashMap::new());
|
||||
|
||||
log::trace!("{e}");
|
||||
@@ -415,6 +417,7 @@ impl NodeGraphExecutor {
|
||||
local_transforms,
|
||||
first_element_source_id,
|
||||
click_targets,
|
||||
outlines,
|
||||
clip_targets,
|
||||
vector_data,
|
||||
backgrounds: _,
|
||||
@@ -427,6 +430,7 @@ impl NodeGraphExecutor {
|
||||
first_element_source_id,
|
||||
});
|
||||
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
||||
responses.add(DocumentMessage::UpdateOutlines { outlines });
|
||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||
responses.add(DocumentMessage::UpdateVectorData { vector_data });
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
|
||||
Reference in New Issue
Block a user