Skip to content

Commit 0b31c1a

Browse files
seam0s-devKeavon
andauthored
Add overlays for free-floating anchors on hovered/selected vector layers (#2630)
* Add selection overlay for free-floating anchors * Add hover overlay for free-floating anchors * Refactor outline_free_floating anchor * Add single-anchor click targets on VectorData * Modify ClickTarget to adapt for Subpath and PointGroup * Fix Rust formatting * Remove debug statements * Add point groups support in VectorDataTable::add_upstream_click_targets * Improve overlay for free floating anchors * Remove datatype for nodes_to_shift * Fix formatting in select_tool.rs * Lints * Code review * Remove references to point_group * Refactor ManipulatorGroup for FreePoint in ClickTargetGroup * Rename ClickTargetGroup to ClickTargetType * Refactor outline_free_floating_anchors into outline * Adapt TransformCage to disable dragging and rotating on a single anchor layer * Fix hover on single points * Fix comments * Lints * Code review pass --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent 269c572 commit 0b31c1a

File tree

11 files changed

+286
-91
lines changed

11 files changed

+286
-91
lines changed

editor/src/consts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pub const MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR: f64 = 40.;
8989
///
9090
/// The motion of the user's cursor by an `x` pixel offset results in `x * scale_factor` pixels of offset on the other side.
9191
pub const MAXIMUM_ALT_SCALE_FACTOR: f64 = 25.;
92+
/// The width or height that the transform cage needs before it is considered to have no width or height.
93+
pub const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4;
9294

9395
// SKEW TRIANGLES
9496
pub const SKEW_TRIANGLE_SIZE: f64 = 7.;

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
3232
use graphene_core::raster::BlendMode;
3333
use graphene_core::raster::image::RasterDataTable;
3434
use graphene_core::vector::style::ViewMode;
35-
use graphene_std::renderer::{ClickTarget, Quad};
35+
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
3636
use graphene_std::vector::{PointId, path_bool_lib};
3737
use std::time::Duration;
3838

@@ -1636,10 +1636,17 @@ impl DocumentMessageHandler {
16361636
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
16371637

16381638
layer_click_targets.is_some_and(|targets| {
1639-
targets.iter().all(|target| {
1640-
let mut subpath = target.subpath().clone();
1641-
subpath.apply_transform(layer_transform);
1642-
subpath.is_inside_subpath(&viewport_polygon, None, None)
1639+
targets.iter().all(|target| match target.target_type() {
1640+
ClickTargetType::Subpath(subpath) => {
1641+
let mut subpath = subpath.clone();
1642+
subpath.apply_transform(layer_transform);
1643+
subpath.is_inside_subpath(&viewport_polygon, None, None)
1644+
}
1645+
ClickTargetType::FreePoint(point) => {
1646+
let mut point = point.clone();
1647+
point.apply_transform(layer_transform);
1648+
viewport_polygon.contains_point(point.position)
1649+
}
16431650
})
16441651
})
16451652
}
@@ -2879,7 +2886,14 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
28792886
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
28802887
};
28812888
click_targets
2882-
.flat_map(|target| target.subpath().iter())
2889+
.filter_map(|target| {
2890+
if let ClickTargetType::Subpath(subpath) = target.target_type() {
2891+
Some(subpath.iter())
2892+
} else {
2893+
None
2894+
}
2895+
})
2896+
.flatten()
28832897
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x))))
28842898
.collect()
28852899
}

editor/src/messages/portfolio/document/overlays/utility_types.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use core::f64::consts::{FRAC_PI_2, TAU};
1010
use glam::{DAffine2, DVec2};
1111
use graphene_core::Color;
1212
use graphene_core::renderer::Quad;
13+
use graphene_std::renderer::ClickTargetType;
1314
use graphene_std::vector::{PointId, SegmentId, VectorData};
1415
use std::collections::HashMap;
1516
use wasm_bindgen::{JsCast, JsValue};
@@ -647,13 +648,24 @@ impl OverlayContext {
647648
self.end_dpi_aware_transform();
648649
}
649650

650-
/// Used by the Select tool to outline a path selected or hovered.
651-
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: Option<&str>) {
652-
self.push_path(subpaths, transform);
651+
/// Used by the Select tool to outline a path or a free point when selected or hovered.
652+
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
653+
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
653654

654-
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
655-
self.render_context.set_stroke_style_str(color);
656-
self.render_context.stroke();
655+
target_types.for_each(|target_type| match target_type.borrow() {
656+
ClickTargetType::FreePoint(point) => {
657+
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
658+
}
659+
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
660+
});
661+
662+
if !subpaths.is_empty() {
663+
self.push_path(subpaths.iter(), transform);
664+
665+
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
666+
self.render_context.set_stroke_style_str(color);
667+
self.render_context.stroke();
668+
}
657669
}
658670

659671
/// Fills the area inside the path. Assumes `color` is in gamma space.

editor/src/messages/portfolio/document/utility_types/document_metadata.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use crate::messages::portfolio::document::graph_operation::transform_utils;
33
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
44
use glam::{DAffine2, DVec2};
55
use graph_craft::document::NodeId;
6-
use graphene_core::renderer::ClickTarget;
7-
use graphene_core::renderer::Quad;
6+
use graphene_core::renderer::{ClickTarget, ClickTargetType, Quad};
87
use graphene_core::transform::Footprint;
98
use graphene_std::vector::{PointId, VectorData};
109
use std::collections::{HashMap, HashSet};
@@ -134,7 +133,10 @@ impl DocumentMetadata {
134133
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
135134
self.click_targets(layer)?
136135
.iter()
137-
.filter_map(|click_target| click_target.subpath().bounding_box_with_transform(transform))
136+
.filter_map(|click_target| match click_target.target_type() {
137+
ClickTargetType::Subpath(subpath) => subpath.bounding_box_with_transform(transform),
138+
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
139+
})
138140
.reduce(Quad::combine_bounds)
139141
}
140142

@@ -177,7 +179,16 @@ impl DocumentMetadata {
177179
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
178180
static EMPTY: Vec<ClickTarget> = Vec::new();
179181
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
180-
click_targets.iter().map(ClickTarget::subpath)
182+
click_targets.iter().filter_map(|target| match target.target_type() {
183+
ClickTargetType::Subpath(subpath) => Some(subpath),
184+
_ => None,
185+
})
186+
}
187+
188+
pub fn layer_with_free_points_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &ClickTargetType> {
189+
static EMPTY: Vec<ClickTarget> = Vec::new();
190+
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
191+
click_targets.iter().map(|target| target.target_type())
181192
}
182193

183194
pub fn is_clip(&self, node: NodeId) -> bool {

editor/src/messages/portfolio/document/utility_types/network_interface.rs

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use glam::{DAffine2, DVec2, IVec2};
1212
use graph_craft::document::value::TaggedValue;
1313
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
1414
use graph_craft::{Type, concrete};
15-
use graphene_std::renderer::{ClickTarget, Quad};
15+
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
1616
use graphene_std::transform::Footprint;
1717
use graphene_std::vector::{PointId, VectorData, VectorModificationType};
1818
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
@@ -2120,7 +2120,7 @@ impl NodeNetworkInterface {
21202120
let bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_right;
21212121
let export_top_right: DVec2 = DVec2::new(viewport_top_right.x.max(bounding_box_top_right.x), viewport_top_right.y.min(bounding_box_top_right.y));
21222122
let add_export_center = export_top_right + DVec2::new(0., network.exports.len() as f64 * 24.);
2123-
let add_export = ClickTarget::new(
2123+
let add_export = ClickTarget::new_with_subpath(
21242124
Subpath::new_rounded_rect(add_export_center - DVec2::new(12., 12.), add_export_center + DVec2::new(12., 12.), [3.; 4]),
21252125
0.,
21262126
);
@@ -2146,7 +2146,7 @@ impl NodeNetworkInterface {
21462146
let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left;
21472147
let import_top_left = DVec2::new(viewport_top_left.x.min(bounding_box_top_left.x), viewport_top_left.y.min(bounding_box_top_left.y));
21482148
let add_import_center = import_top_left + DVec2::new(0., self.number_of_displayed_imports(network_path) as f64 * 24.);
2149-
let add_import = ClickTarget::new(
2149+
let add_import = ClickTarget::new_with_subpath(
21502150
Subpath::new_rounded_rect(add_import_center - DVec2::new(12., 12.), add_import_center + DVec2::new(12., 12.), [3.; 4]),
21512151
0.,
21522152
);
@@ -2165,8 +2165,8 @@ impl NodeNetworkInterface {
21652165
let reorder_import_center = (import_bounding_box[0] + import_bounding_box[1]) / 2. + DVec2::new(-12., 0.);
21662166
let remove_import_center = reorder_import_center + DVec2::new(-12., 0.);
21672167

2168-
let reorder_import = ClickTarget::new(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.);
2169-
let remove_import = ClickTarget::new(Subpath::new_rect(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.);
2168+
let reorder_import = ClickTarget::new_with_subpath(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.);
2169+
let remove_import = ClickTarget::new_with_subpath(Subpath::new_rect(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.);
21702170

21712171
reorder_imports_exports.insert_custom_output_port(*import_index, reorder_import);
21722172
remove_imports_exports.insert_custom_output_port(*import_index, remove_import);
@@ -2180,8 +2180,8 @@ impl NodeNetworkInterface {
21802180
let reorder_export_center = (export_bounding_box[0] + export_bounding_box[1]) / 2. + DVec2::new(12., 0.);
21812181
let remove_export_center = reorder_export_center + DVec2::new(12., 0.);
21822182

2183-
let reorder_export = ClickTarget::new(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.);
2184-
let remove_export = ClickTarget::new(Subpath::new_rect(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.);
2183+
let reorder_export = ClickTarget::new_with_subpath(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.);
2184+
let remove_export = ClickTarget::new_with_subpath(Subpath::new_rect(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.);
21852185

21862186
reorder_imports_exports.insert_custom_input_port(*export_index, reorder_export);
21872187
remove_imports_exports.insert_custom_input_port(*export_index, remove_export);
@@ -2572,7 +2572,7 @@ impl NodeNetworkInterface {
25722572

25732573
let radius = 3.;
25742574
let subpath = bezier_rs::Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
2575-
let node_click_target = ClickTarget::new(subpath, 0.);
2575+
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
25762576

25772577
DocumentNodeClickTargets {
25782578
node_click_target,
@@ -2597,12 +2597,12 @@ impl NodeNetworkInterface {
25972597
// Update visibility button click target
25982598
let visibility_offset = node_top_left + DVec2::new(width as f64, 24.);
25992599
let subpath = Subpath::new_rounded_rect(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]);
2600-
let visibility_click_target = ClickTarget::new(subpath, 0.);
2600+
let visibility_click_target = ClickTarget::new_with_subpath(subpath, 0.);
26012601

26022602
// Update grip button click target, which is positioned to the left of the left most icon
26032603
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2., 24.);
26042604
let subpath = Subpath::new_rounded_rect(DVec2::new(-8., -12.) + grip_offset_right_edge, DVec2::new(0., 12.) + grip_offset_right_edge, [0.; 4]);
2605-
let grip_click_target = ClickTarget::new(subpath, 0.);
2605+
let grip_click_target = ClickTarget::new_with_subpath(subpath, 0.);
26062606

26072607
// Create layer click target, which is contains the layer and the chain background
26082608
let chain_width_grid_spaces = self.chain_width(node_id, network_path);
@@ -2611,7 +2611,7 @@ impl NodeNetworkInterface {
26112611
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.);
26122612
let radius = 10.;
26132613
let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]);
2614-
let node_click_target = ClickTarget::new(subpath, 0.);
2614+
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
26152615

26162616
DocumentNodeClickTargets {
26172617
node_click_target,
@@ -2804,20 +2804,29 @@ impl NodeNetworkInterface {
28042804
if let (Some(import_export_click_targets), Some(node_click_targets)) = (self.import_export_ports(network_path).cloned(), self.node_click_targets(&node_id, network_path)) {
28052805
let mut node_path = String::new();
28062806

2807-
let _ = node_click_targets.node_click_target.subpath().subpath_to_svg(&mut node_path, DAffine2::IDENTITY);
2807+
if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() {
2808+
let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY);
2809+
}
28082810
all_node_click_targets.push((node_id, node_path));
28092811
for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) {
2810-
let mut port_path = String::new();
2811-
let _ = port.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
2812-
port_click_targets.push(port_path);
2812+
if let ClickTargetType::Subpath(subpath) = port.target_type() {
2813+
let mut port_path = String::new();
2814+
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
2815+
port_click_targets.push(port_path);
2816+
}
28132817
}
28142818
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
2815-
let mut port_path = String::new();
2816-
let _ = layer_metadata.visibility_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
2817-
icon_click_targets.push(port_path);
2818-
let mut port_path = String::new();
2819-
let _ = layer_metadata.grip_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
2820-
icon_click_targets.push(port_path);
2819+
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
2820+
let mut port_path = String::new();
2821+
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
2822+
icon_click_targets.push(port_path);
2823+
}
2824+
2825+
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
2826+
let mut port_path = String::new();
2827+
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
2828+
icon_click_targets.push(port_path);
2829+
}
28212830
}
28222831
}
28232832
});
@@ -2872,9 +2881,11 @@ impl NodeNetworkInterface {
28722881
.chain(modify_import_export_click_targets.remove_imports_exports.click_targets())
28732882
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
28742883
{
2875-
let mut remove_string = String::new();
2876-
let _ = click_target.subpath().subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
2877-
modify_import_export.push(remove_string);
2884+
if let ClickTargetType::Subpath(subpath) = click_target.target_type() {
2885+
let mut remove_string = String::new();
2886+
let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
2887+
modify_import_export.push(remove_string);
2888+
}
28782889
}
28792890
}
28802891
FrontendClickTargets {
@@ -3174,8 +3185,8 @@ impl NodeNetworkInterface {
31743185
self.document_metadata
31753186
.click_targets
31763187
.get(&layer)
3177-
.map(|click| click.iter().map(ClickTarget::subpath))
3178-
.map(|subpaths| VectorData::from_subpaths(subpaths, true))
3188+
.map(|click| click.iter().map(ClickTarget::target_type))
3189+
.map(|target_types| VectorData::from_target_types(target_types, true))
31793190
}
31803191

31813192
/// Loads the structure of layer nodes from a node graph.
@@ -5893,7 +5904,7 @@ impl Ports {
58935904

58945905
fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) {
58955906
let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
5896-
self.insert_custom_input_port(input_index, ClickTarget::new(subpath, 0.));
5907+
self.insert_custom_input_port(input_index, ClickTarget::new_with_subpath(subpath, 0.));
58975908
}
58985909

58995910
fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) {
@@ -5902,7 +5913,7 @@ impl Ports {
59025913

59035914
fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) {
59045915
let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
5905-
self.insert_custom_output_port(output_index, ClickTarget::new(subpath, 0.));
5916+
self.insert_custom_output_port(output_index, ClickTarget::new_with_subpath(subpath, 0.));
59065917
}
59075918

59085919
fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) {

editor/src/messages/tool/common_functionality/shape_editor.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ impl SelectedLayerState {
108108
}
109109

110110
pub fn selected_points_count(&self) -> usize {
111-
self.selected_points.len()
111+
let count = self.selected_points.iter().fold(0, |acc, point| {
112+
let is_ignored = (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors);
113+
acc + if is_ignored { 0 } else { 1 }
114+
});
115+
count
112116
}
113117
}
114118

editor/src/messages/tool/common_functionality/snapping.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,11 @@ impl SnapManager {
449449
if let Some(ind) = &self.indicator {
450450
for layer in &ind.outline_layers {
451451
let &Some(layer) = layer else { continue };
452-
overlay_context.outline(snap_data.document.metadata().layer_outline(layer), snap_data.document.metadata().transform_to_viewport(layer), None);
452+
overlay_context.outline(
453+
snap_data.document.metadata().layer_with_free_points_outline(layer),
454+
snap_data.document.metadata().transform_to_viewport(layer),
455+
None,
456+
);
453457
}
454458
if let Some(quad) = ind.target_bounds {
455459
overlay_context.quad(to_viewport * quad, None, None);

0 commit comments

Comments
 (0)