From 8cf7eb7c20c43b2e123259fe52d9da9752750f03 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sat, 3 May 2025 18:23:59 +0300 Subject: [PATCH 01/22] Add selection overlay for free-floating anchors --- .../document/overlays/utility_types.rs | 4 +++ .../tool/tool_messages/select_tool.rs | 25 ++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index c7d2bd70da..dbe7910e62 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -654,6 +654,10 @@ impl OverlayContext { self.render_context.stroke(); } + pub fn outline_free_floating_anchor(&mut self, position: DVec2) { + self.circle(position, 4.0, Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE)); + } + /// Fills the area inside the path. Assumes `color` is in gamma space. /// Used by the Pen tool to show the path being closed. pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 9affd74bc6..f4c3e3b9f2 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -527,12 +527,21 @@ impl Fsm for SelectToolFsmState { .selected_visible_and_unlocked_layers(&document.network_interface) .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) { - overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); + let layer_to_viewport = document.metadata().transform_to_viewport(layer); + overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); if is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text") { - let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, font_cache); + let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, font_cache); overlay_context.dashed_quad(transformed_quad, None, Some(7.), Some(5.), None); } + + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + for &position in vector_data.point_domain.positions() { + // Check if the point in the layer is not part of a segment + if vector_data.segment_domain.ids().is_empty() { + overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); + } + } } } @@ -573,7 +582,17 @@ impl Fsm for SelectToolFsmState { let not_selected_click = click.filter(|&hovered_layer| !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata())); if let Some(layer) = not_selected_click { if overlay_context.visibility_settings.hover_outline() { - overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); + let layer_to_viewport = document.metadata().transform_to_viewport(layer); + overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); + + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + for &position in vector_data.point_domain.positions() { + // Check if the point in the layer is not part of a segment + if vector_data.segment_domain.ids().is_empty() { + overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); + } + } + } } // Measure with Alt held down From 15582868a15f4de11201d518f315c3a2529587c8 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sat, 3 May 2025 23:04:38 +0300 Subject: [PATCH 02/22] Add hover overlay for free-floating anchors --- .../messages/tool/tool_messages/select_tool.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index f4c3e3b9f2..1ffc8ad07e 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -536,10 +536,12 @@ impl Fsm for SelectToolFsmState { } let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; - for &position in vector_data.point_domain.positions() { + for &point_id in vector_data.point_domain.ids() { // Check if the point in the layer is not part of a segment - if vector_data.segment_domain.ids().is_empty() { - overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); + if vector_data.connected_count(point_id) == 0 { + if let Some(position) = vector_data.point_domain.position_from_id(point_id) { + overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); + } } } } @@ -586,10 +588,12 @@ impl Fsm for SelectToolFsmState { overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - for &position in vector_data.point_domain.positions() { + for &point_id in vector_data.point_domain.ids() { // Check if the point in the layer is not part of a segment - if vector_data.segment_domain.ids().is_empty() { - overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); + if vector_data.connected_count(point_id) == 0 { + if let Some(position) = vector_data.point_domain.position_from_id(point_id) { + overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); + } } } } From 050aa9280f670f86af5c0edab8cb41f0b19c7083 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 4 May 2025 11:49:19 +0300 Subject: [PATCH 03/22] Refactor outline_free_floating anchor --- .../document/overlays/utility_types.rs | 13 +++++++++++-- .../tool/tool_messages/select_tool.rs | 19 +++---------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index dbe7910e62..4c57ddeceb 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -654,8 +654,17 @@ impl OverlayContext { self.render_context.stroke(); } - pub fn outline_free_floating_anchor(&mut self, position: DVec2) { - self.circle(position, 4.0, Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE)); + pub fn outline_free_floating_anchor(&mut self, vector_data: VectorData, transform: DAffine2) { + const SINGLE_ANCHOR_SELECTION_RADIUS: f64 = 4.; + + for &point_id in vector_data.point_domain.ids() { + // Check if the point in the layer is not part of a segment + if vector_data.connected_count(point_id) == 0 { + if let Some(position) = vector_data.point_domain.position_from_id(point_id) { + self.circle(transform.transform_point2(position), SINGLE_ANCHOR_SELECTION_RADIUS, Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE)); + } + } + } } /// Fills the area inside the path. Assumes `color` is in gamma space. diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 1ffc8ad07e..1b114e24fb 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -535,14 +535,8 @@ impl Fsm for SelectToolFsmState { overlay_context.dashed_quad(transformed_quad, None, Some(7.), Some(5.), None); } - let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; - for &point_id in vector_data.point_domain.ids() { - // Check if the point in the layer is not part of a segment - if vector_data.connected_count(point_id) == 0 { - if let Some(position) = vector_data.point_domain.position_from_id(point_id) { - overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); - } - } + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + overlay_context.outline_free_floating_anchor(vector_data, layer_to_viewport); } } } @@ -588,14 +582,7 @@ impl Fsm for SelectToolFsmState { overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - for &point_id in vector_data.point_domain.ids() { - // Check if the point in the layer is not part of a segment - if vector_data.connected_count(point_id) == 0 { - if let Some(position) = vector_data.point_domain.position_from_id(point_id) { - overlay_context.outline_free_floating_anchor(layer_to_viewport.transform_point2(position)); - } - } - } + overlay_context.outline_free_floating_anchor(vector_data, layer_to_viewport); } } From b0312a446b0cf8715018766297df71ff65c9e4e7 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 4 May 2025 19:59:36 +0300 Subject: [PATCH 04/22] Add single-anchor click targets on VectorData --- .../portfolio/document/overlays/utility_types.rs | 2 +- .../src/messages/tool/tool_messages/select_tool.rs | 11 ++++++++--- node-graph/gcore/src/graphic_element/renderer.rs | 14 +++++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 4c57ddeceb..1696804054 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -654,7 +654,7 @@ impl OverlayContext { self.render_context.stroke(); } - pub fn outline_free_floating_anchor(&mut self, vector_data: VectorData, transform: DAffine2) { + pub fn outline_free_floating_anchors(&mut self, vector_data: VectorData, transform: DAffine2) { const SINGLE_ANCHOR_SELECTION_RADIUS: f64 = 4.; for &point_id in vector_data.point_domain.ids() { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 1b114e24fb..81f047624e 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -536,7 +536,7 @@ impl Fsm for SelectToolFsmState { } if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - overlay_context.outline_free_floating_anchor(vector_data, layer_to_viewport); + overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); } } } @@ -582,7 +582,7 @@ impl Fsm for SelectToolFsmState { overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - overlay_context.outline_free_floating_anchor(vector_data, layer_to_viewport); + overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); } } @@ -794,7 +794,12 @@ impl Fsm for SelectToolFsmState { if overlay_context.visibility_settings.selection_outline() { // Draws a temporary outline on the layers that will be selected by the current box/lasso area for layer in layers_to_outline { - overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); + let layer_to_viewport = document.metadata().transform_to_viewport(layer); + overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); + + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); + } } } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 6b1f4f436f..d2852ec642 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -9,7 +9,7 @@ use crate::vector::style::{Fill, Stroke, ViewMode}; use crate::vector::{PointId, VectorDataTable}; use crate::{Artboard, ArtboardGroupTable, Color, GraphicElement, GraphicGroupTable, RasterFrame}; use base64::Engine; -use bezier_rs::Subpath; +use bezier_rs::{ManipulatorGroup, Subpath}; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; use num_traits::Zero; @@ -652,10 +652,22 @@ impl GraphicElementRendered for VectorDataTable { subpath }; + // For free-floating anchors, we need to add a click target for each + let single_anchors = instance.point_domain.ids().iter().filter(|&&point_id| instance.connected_count(point_id) == 0); + let single_anchors_targets = single_anchors + .map(|&point_id| { + let anchor = instance.point_domain.position_from_id(point_id).unwrap_or_default(); + ManipulatorGroup::new_anchor_with_id(anchor, point_id) + }) + .map(|group| Subpath::new(vec![group], false)) + .map(|subpath| ClickTarget::new(subpath, 0.)) + .collect::>(); + let click_targets = instance .stroke_bezier_paths() .map(fill) .map(|subpath| ClickTarget::new(subpath, stroke_width)) + .chain(single_anchors_targets.into_iter()) .collect::>(); metadata.click_targets.insert(element_id, click_targets); From fdc82c82caed6cb957a4bffa2cb7891c8c53b1ee Mon Sep 17 00:00:00 2001 From: seam0s Date: Fri, 9 May 2025 00:36:30 +0300 Subject: [PATCH 05/22] Modify ClickTarget to adapt for Subpath and PointGroup --- .../document/document_message_handler.rs | 46 +++++-- .../utility_types/document_metadata.rs | 16 ++- .../utility_types/network_interface.rs | 69 ++++++---- .../tool/common_functionality/shape_editor.rs | 9 +- .../tool/tool_messages/select_tool.rs | 6 +- .../gcore/src/graphic_element/renderer.rs | 127 +++++++++++++----- node-graph/gcore/src/vector/vector_data.rs | 35 ++++- 7 files changed, 229 insertions(+), 79 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index d324c4931d..428e815738 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -32,7 +32,7 @@ use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graphene_core::raster::BlendMode; use graphene_core::raster::image::ImageFrameTable; use graphene_core::vector::style::ViewMode; -use graphene_std::renderer::{ClickTarget, Quad}; +use graphene_std::renderer::{ClickTarget, ClickTargetGroup, Quad}; use graphene_std::vector::{PointId, path_bool_lib}; use std::time::Duration; @@ -1603,10 +1603,17 @@ impl DocumentMessageHandler { let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer); layer_click_targets.is_some_and(|targets| { - targets.iter().all(|target| { - let mut subpath = target.subpath().clone(); - subpath.apply_transform(layer_transform); - subpath.is_inside_subpath(&viewport_polygon, None, None) + targets.iter().all(|target| match target.target_group() { + ClickTargetGroup::Subpath(subpath) => { + let mut subpath = subpath.clone(); + subpath.apply_transform(layer_transform); + subpath.is_inside_subpath(&viewport_polygon, None, None) + } + ClickTargetGroup::PointGroup(point) => { + let mut point = point.clone(); + point.apply_transform(layer_transform); + viewport_polygon.contains_point(point.anchor) + } }) }) } @@ -1635,10 +1642,17 @@ impl DocumentMessageHandler { /// Find layers under the location in viewport space that was clicked, listed by their depth in the layer tree hierarchy. pub fn click_list<'a>(&'a self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator + use<'a> { + // debug!("click_xray: {:?}", self.click_xray(ipp).collect::>()); self.click_xray(ipp) - .filter(move |&layer| !self.network_interface.is_artboard(&layer.to_node(), &[])) + .filter(move |&layer| { + if !self.network_interface.is_artboard(&layer.to_node(), &[]) { + // debug!("Hover: we have been flagged 'not artboard'..."); + } + !self.network_interface.is_artboard(&layer.to_node(), &[]) + }) .skip_while(|&layer| layer == LayerNodeIdentifier::ROOT_PARENT) .scan(true, |last_had_children, layer| { + // debug!("Hover: we are being scanned after filtered out of click_xray..."); if *last_had_children { *last_had_children = layer.has_children(self.network_interface.document_metadata()); Some(layer) @@ -2733,7 +2747,18 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end), }; click_targets - .flat_map(|target| target.subpath().iter()) + .filter(|target| match target.target_group() { + ClickTargetGroup::Subpath(_) => true, + _ => false, + }) + .flat_map(|target| { + let subpath = if let ClickTargetGroup::Subpath(subpath) = target.target_group() { + subpath + } else { + panic!("Expected a subpath target group"); + }; + subpath.iter() + }) .map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x)))) .collect() } @@ -2801,7 +2826,12 @@ impl<'a> ClickXRayIter<'a> { match target { // Single points are much cheaper than paths so have their own special case XRayTarget::Point(point) => { - let intersects = click_targets.is_some_and(|targets| targets.iter().any(|target| target.intersect_point(*point, transform))); + let intersects = click_targets.is_some_and(|targets| { + targets.iter().any(|target| { + // debug!("Hover: we are about to test point intersection with no stroke..."); + target.intersect_point(*point, transform) + }) + }); XRayResult { clicked: intersects, use_children: !clip || intersects, diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 52509903c0..55f481b2cc 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -3,8 +3,7 @@ use crate::messages::portfolio::document::graph_operation::transform_utils; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; -use graphene_core::renderer::ClickTarget; -use graphene_core::renderer::Quad; +use graphene_core::renderer::{ClickTarget, Quad, ClickTargetGroup}; use graphene_core::transform::Footprint; use graphene_std::vector::{PointId, VectorData}; use std::collections::{HashMap, HashSet}; @@ -134,7 +133,7 @@ impl DocumentMetadata { pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> { self.click_targets(layer)? .iter() - .filter_map(|click_target| click_target.subpath().bounding_box_with_transform(transform)) + .filter_map(|click_target| click_target.bounding_box_with_transform(transform)) .reduce(Quad::combine_bounds) } @@ -177,7 +176,16 @@ impl DocumentMetadata { pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { static EMPTY: Vec = Vec::new(); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); - click_targets.iter().map(ClickTarget::subpath) + click_targets + .iter() + .filter(|target| match target.target_group() { + ClickTargetGroup::Subpath(_) => true, + _ => false, + }) + .map(|target| match target.target_group() { + ClickTargetGroup::Subpath(subpath) => subpath, + _ => unreachable!(), + }) } pub fn is_clip(&self, node: NodeId) -> bool { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index db083e5409..434d68984a 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -12,7 +12,7 @@ use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::{Type, concrete}; -use graphene_std::renderer::{ClickTarget, Quad}; +use graphene_std::renderer::{ClickTarget, ClickTargetGroup, Quad}; use graphene_std::transform::Footprint; use graphene_std::vector::{PointId, VectorData, VectorModificationType}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes; @@ -2120,7 +2120,7 @@ impl NodeNetworkInterface { 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; 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)); let add_export_center = export_top_right + DVec2::new(0., network.exports.len() as f64 * 24.); - let add_export = ClickTarget::new( + let add_export = ClickTarget::new_with_subpath( Subpath::new_rounded_rect(add_export_center - DVec2::new(12., 12.), add_export_center + DVec2::new(12., 12.), [3.; 4]), 0., ); @@ -2146,7 +2146,7 @@ impl NodeNetworkInterface { 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; 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)); let add_import_center = import_top_left + DVec2::new(0., self.number_of_displayed_imports(network_path) as f64 * 24.); - let add_import = ClickTarget::new( + let add_import = ClickTarget::new_with_subpath( Subpath::new_rounded_rect(add_import_center - DVec2::new(12., 12.), add_import_center + DVec2::new(12., 12.), [3.; 4]), 0., ); @@ -2165,8 +2165,8 @@ impl NodeNetworkInterface { let reorder_import_center = (import_bounding_box[0] + import_bounding_box[1]) / 2. + DVec2::new(-12., 0.); let remove_import_center = reorder_import_center + DVec2::new(-12., 0.); - let reorder_import = ClickTarget::new(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.); - let remove_import = ClickTarget::new(Subpath::new_rect(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.); + 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.); + 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.); reorder_imports_exports.insert_custom_output_port(*import_index, reorder_import); remove_imports_exports.insert_custom_output_port(*import_index, remove_import); @@ -2180,8 +2180,8 @@ impl NodeNetworkInterface { let reorder_export_center = (export_bounding_box[0] + export_bounding_box[1]) / 2. + DVec2::new(12., 0.); let remove_export_center = reorder_export_center + DVec2::new(12., 0.); - let reorder_export = ClickTarget::new(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.); - let remove_export = ClickTarget::new(Subpath::new_rect(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.); + 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.); + 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.); reorder_imports_exports.insert_custom_input_port(*export_index, reorder_export); remove_imports_exports.insert_custom_input_port(*export_index, remove_export); @@ -2572,7 +2572,7 @@ impl NodeNetworkInterface { let radius = 3.; let subpath = bezier_rs::Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]); - let node_click_target = ClickTarget::new(subpath, 0.); + let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); DocumentNodeClickTargets { node_click_target, @@ -2597,12 +2597,12 @@ impl NodeNetworkInterface { // Update visibility button click target let visibility_offset = node_top_left + DVec2::new(width as f64, 24.); let subpath = Subpath::new_rounded_rect(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]); - let visibility_click_target = ClickTarget::new(subpath, 0.); + let visibility_click_target = ClickTarget::new_with_subpath(subpath, 0.); // Update grip button click target, which is positioned to the left of the left most icon let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2., 24.); let subpath = Subpath::new_rounded_rect(DVec2::new(-8., -12.) + grip_offset_right_edge, DVec2::new(0., 12.) + grip_offset_right_edge, [0.; 4]); - let grip_click_target = ClickTarget::new(subpath, 0.); + let grip_click_target = ClickTarget::new_with_subpath(subpath, 0.); // Create layer click target, which is contains the layer and the chain background let chain_width_grid_spaces = self.chain_width(node_id, network_path); @@ -2611,7 +2611,7 @@ impl NodeNetworkInterface { let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.); let radius = 10.; let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]); - let node_click_target = ClickTarget::new(subpath, 0.); + let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); DocumentNodeClickTargets { node_click_target, @@ -2804,20 +2804,29 @@ impl NodeNetworkInterface { 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)) { let mut node_path = String::new(); - let _ = node_click_targets.node_click_target.subpath().subpath_to_svg(&mut node_path, DAffine2::IDENTITY); + if let ClickTargetGroup::Subpath(subpath) = node_click_targets.node_click_target.target_group() { + let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY); + } all_node_click_targets.push((node_id, node_path)); for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) { - let mut port_path = String::new(); - let _ = port.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY); - port_click_targets.push(port_path); + if let ClickTargetGroup::Subpath(subpath) = port.target_group() { + let mut port_path = String::new(); + let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); + port_click_targets.push(port_path); + } } if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata { - let mut port_path = String::new(); - let _ = layer_metadata.visibility_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY); - icon_click_targets.push(port_path); - let mut port_path = String::new(); - let _ = layer_metadata.grip_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY); - icon_click_targets.push(port_path); + if let ClickTargetGroup::Subpath(subpath) = layer_metadata.visibility_click_target.target_group() { + let mut port_path = String::new(); + let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); + icon_click_targets.push(port_path); + } + + if let ClickTargetGroup::Subpath(subpath) = layer_metadata.grip_click_target.target_group() { + let mut port_path = String::new(); + let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); + icon_click_targets.push(port_path); + } } } }); @@ -2872,9 +2881,11 @@ impl NodeNetworkInterface { .chain(modify_import_export_click_targets.remove_imports_exports.click_targets()) .chain(modify_import_export_click_targets.reorder_imports_exports.click_targets()) { - let mut remove_string = String::new(); - let _ = click_target.subpath().subpath_to_svg(&mut remove_string, DAffine2::IDENTITY); - modify_import_export.push(remove_string); + if let ClickTargetGroup::Subpath(subpath) = click_target.target_group() { + let mut remove_string = String::new(); + let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY); + modify_import_export.push(remove_string); + } } } FrontendClickTargets { @@ -3174,8 +3185,8 @@ impl NodeNetworkInterface { self.document_metadata .click_targets .get(&layer) - .map(|click| click.iter().map(ClickTarget::subpath)) - .map(|subpaths| VectorData::from_subpaths(subpaths, true)) + .map(|click| click.iter().map(ClickTarget::target_group)) + .map(|target_groups| VectorData::from_target_groups(target_groups, true)) } /// Loads the structure of layer nodes from a node graph. @@ -5174,7 +5185,7 @@ impl NodeNetworkInterface { } shifted_nodes.insert(*node_id); - let nodes_to_shift = self.check_collision_with_stack_dependents(node_id, shift_sign, network_path); + let nodes_to_shift: Vec<(NodeId, LayerOwner)> = self.check_collision_with_stack_dependents(node_id, shift_sign, network_path); for node_to_shift in nodes_to_shift { self.shift_node_or_parent(&node_to_shift.0, shift_sign, shifted_nodes, network_path); @@ -5884,7 +5895,7 @@ impl Ports { fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) { let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.)); - self.insert_custom_input_port(input_index, ClickTarget::new(subpath, 0.)); + self.insert_custom_input_port(input_index, ClickTarget::new_with_subpath(subpath, 0.)); } fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) { @@ -5893,7 +5904,7 @@ impl Ports { fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) { let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.)); - self.insert_custom_output_port(output_index, ClickTarget::new(subpath, 0.)); + self.insert_custom_output_port(output_index, ClickTarget::new_with_subpath(subpath, 0.)); } fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) { diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 7fa761acdc..ddd633ce6a 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -83,7 +83,14 @@ impl SelectedLayerState { self.selected_points.clear(); } pub fn selected_points_count(&self) -> usize { - self.selected_points.len() + let count = self.selected_points.iter().fold(0, |acc, point| { + if (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors) { + acc + } else { + acc + 1 + } + }); + count } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 81f047624e..916e35510c 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -575,12 +575,16 @@ impl Fsm for SelectToolFsmState { if !matches!(self, Self::Drawing { .. }) && !input.keyboard.get(Key::MouseMiddle as usize) { // Get the layer the user is hovering over let click = document.click(input); - let not_selected_click = click.filter(|&hovered_layer| !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata())); + let not_selected_click = click.filter(|&hovered_layer| { + // debug!("Hover: we are being checked for selection status..."); + !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata()) + }); if let Some(layer) = not_selected_click { if overlay_context.visibility_settings.hover_outline() { let layer_to_viewport = document.metadata().transform_to_viewport(layer); overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); + // debug!("Hover: We are in!"); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index d2852ec642..de45df539e 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -20,22 +20,47 @@ use std::fmt::Write; #[cfg(feature = "vello")] use vello::*; +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum ClickTargetGroup { + Subpath(bezier_rs::Subpath), + PointGroup(ManipulatorGroup), +} + /// Represents a clickable target for the layer #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ClickTarget { - subpath: bezier_rs::Subpath, + target_group: ClickTargetGroup, stroke_width: f64, bounding_box: Option<[DVec2; 2]>, } impl ClickTarget { - pub fn new(subpath: bezier_rs::Subpath, stroke_width: f64) -> Self { + pub fn new_with_subpath(subpath: bezier_rs::Subpath, stroke_width: f64) -> Self { let bounding_box = subpath.loose_bounding_box(); - Self { subpath, stroke_width, bounding_box } + Self { + target_group: ClickTargetGroup::Subpath(subpath), + stroke_width, + bounding_box, + } + } + + pub fn new_with_point_group(point: ManipulatorGroup) -> Self { + let stroke_width = 20.; + let bounding_box = Some([point.anchor - DVec2::splat(stroke_width / 2.), point.anchor + DVec2::splat(stroke_width / 2.)]); + + Self { + target_group: ClickTargetGroup::PointGroup(point), + stroke_width, + bounding_box, + } } - pub fn subpath(&self) -> &bezier_rs::Subpath { - &self.subpath + pub fn target_group(&self) -> &ClickTargetGroup { + &self.target_group + } + + pub fn target_group_mut(&mut self) -> &mut ClickTargetGroup { + &mut self.target_group } pub fn bounding_box(&self) -> Option<[DVec2; 2]> { @@ -47,12 +72,26 @@ impl ClickTarget { } pub fn apply_transform(&mut self, affine_transform: DAffine2) { - self.subpath.apply_transform(affine_transform); + match self.target_group { + ClickTargetGroup::Subpath(ref mut subpath) => { + subpath.apply_transform(affine_transform); + } + ClickTargetGroup::PointGroup(ref mut point_group) => { + point_group.apply_transform(affine_transform); + } + } self.update_bbox(); } fn update_bbox(&mut self) { - self.bounding_box = self.subpath.bounding_box(); + match self.target_group { + ClickTargetGroup::Subpath(ref subpath) => { + self.bounding_box = subpath.loose_bounding_box(); + } + ClickTargetGroup::PointGroup(ref point_group) => { + self.bounding_box = Some([point_group.anchor - DVec2::splat(self.stroke_width / 2.), point_group.anchor + DVec2::splat(self.stroke_width / 2.)]); + } + } } /// Does the click target intersect the path @@ -66,19 +105,27 @@ impl ClickTarget { let inverse = layer_transform.inverse(); let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point))); - // Check if outlines intersect - let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty()); - if self.subpath.iter().any(outline_intersects) { - return true; - } - // Check if selection is entirely within the shape - if self.subpath.closed() && bezier_iter().next().is_some_and(|bezier| self.subpath.contains_point(bezier.start)) { - return true; - } + match self.target_group() { + ClickTargetGroup::Subpath(subpath) => { + // Check if outlines intersect + let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty()); + if subpath.iter().any(outline_intersects) { + return true; + } + // Check if selection is entirely within the shape + if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) { + return true; + } - // Check if shape is entirely within selection - let any_point_from_subpath = self.subpath.manipulator_groups().first().map(|group| group.anchor); - any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0) + // Check if shape is entirely within selection + let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor); + return any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0); + } + ClickTargetGroup::PointGroup(point_group) => { + let point = point_group.anchor; + bezier_iter().map(|bezier| bezier.winding(point)).sum::() != 0 + } + } } /// Does the click target intersect the point (accounting for stroke size) @@ -107,7 +154,10 @@ impl ClickTarget { .is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y) { // Check if the point is within the shape - self.subpath.closed() && self.subpath.contains_point(point) + match self.target_group() { + ClickTargetGroup::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), + ClickTargetGroup::PointGroup(point_group) => point_group.anchor == point, + } } else { false } @@ -657,16 +707,16 @@ impl GraphicElementRendered for VectorDataTable { let single_anchors_targets = single_anchors .map(|&point_id| { let anchor = instance.point_domain.position_from_id(point_id).unwrap_or_default(); - ManipulatorGroup::new_anchor_with_id(anchor, point_id) + let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); + + ClickTarget::new_with_point_group(group) }) - .map(|group| Subpath::new(vec![group], false)) - .map(|subpath| ClickTarget::new(subpath, 0.)) .collect::>(); let click_targets = instance .stroke_bezier_paths() .map(fill) - .map(|subpath| ClickTarget::new(subpath, stroke_width)) + .map(|subpath| ClickTarget::new_with_subpath(subpath, stroke_width)) .chain(single_anchors_targets.into_iter()) .collect::>(); @@ -682,6 +732,20 @@ impl GraphicElementRendered for VectorDataTable { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for instance in self.instance_ref_iter() { + // // For free-floating anchors, we need to add a click target for each + // let single_anchors = instance.instance.point_domain.ids().iter().filter(|&&point_id| instance.instance.connected_count(point_id) == 0); + // let single_anchors_targets = single_anchors + // .map(|&point_id| { + // let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); + // let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); + // let subpath = Subpath::new(vec![group], false); + + // let click_target = ClickTarget::new_free_floating_anchor(subpath); + // // click_target.apply_transform(*instance.transform); + // click_target + // }) + // .collect::>(); + let stroke_width = instance.instance.style.stroke().as_ref().map_or(0., Stroke::weight); let filled = instance.instance.style.fill() != &Fill::None; let fill = |mut subpath: bezier_rs::Subpath<_>| { @@ -691,10 +755,11 @@ impl GraphicElementRendered for VectorDataTable { subpath }; click_targets.extend(instance.instance.stroke_bezier_paths().map(fill).map(|subpath| { - let mut click_target = ClickTarget::new(subpath, stroke_width); + let mut click_target = ClickTarget::new_with_subpath(subpath, stroke_width); click_target.apply_transform(*instance.transform); click_target })); + // click_targets.extend(single_anchors_targets); } } @@ -796,7 +861,7 @@ impl GraphicElementRendered for Artboard { fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option) { if let Some(element_id) = element_id { let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2()); - metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]); + metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]); metadata.upstream_footprints.insert(element_id, footprint); metadata.local_transforms.insert(element_id, DAffine2::from_translation(self.location.as_dvec2())); if self.clip { @@ -809,7 +874,7 @@ impl GraphicElementRendered for Artboard { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { let subpath_rectangle = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2()); - click_targets.push(ClickTarget::new(subpath_rectangle, 0.)); + click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); } fn contains_artboard(&self) -> bool { @@ -922,14 +987,14 @@ impl GraphicElementRendered for ImageFrameTable { let Some(element_id) = element_id else { return }; let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); - metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]); + metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]); metadata.upstream_footprints.insert(element_id, footprint); metadata.local_transforms.insert(element_id, instance_transform); } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); - click_targets.push(ClickTarget::new(subpath, 0.)); + click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); } } @@ -998,14 +1063,14 @@ impl GraphicElementRendered for RasterFrame { let Some(element_id) = element_id else { return }; let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); - metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]); + metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]); metadata.upstream_footprints.insert(element_id, footprint); metadata.local_transforms.insert(element_id, self.transform()); } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); - click_targets.push(ClickTarget::new(subpath, 0.)); + click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); } } diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index aba89db824..d1079c7c41 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -5,6 +5,7 @@ mod modification; use super::misc::point_to_dvec2; use super::style::{PathStyle, Stroke}; use crate::instances::Instances; +use crate::renderer::ClickTargetGroup; use crate::{AlphaBlending, Color, GraphicGroupTable}; pub use attributes::*; use bezier_rs::{BezierHandles, ManipulatorGroup}; @@ -114,11 +115,6 @@ impl VectorData { } } - /// Construct some new vector data from a single subpath with an identity transform and black fill. - pub fn from_subpath(subpath: impl Borrow>) -> Self { - Self::from_subpaths([subpath], false) - } - /// Push a subpath to the vector data pub fn append_subpath(&mut self, subpath: impl Borrow>, preserve_id: bool) { let subpath: &bezier_rs::Subpath = subpath.borrow(); @@ -134,6 +130,7 @@ impl VectorData { let mut segment_id = self.segment_domain.next_id(); let mut last_point = None; let mut first_point = None; + // Constructs a bezier segment from the two manipulators on the subpath. for pair in subpath.manipulator_groups().windows(2) { let start = last_point.unwrap_or_else(|| { let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) { @@ -177,6 +174,17 @@ impl VectorData { } } + pub fn append_point_group(&mut self, point_group: &ManipulatorGroup, preserve_id: bool) { + let mut point_id = self.point_domain.next_id(); + // Use the current point id if it is not already in the domain else generate a new one + let id = if preserve_id && !self.point_domain.ids().contains(&point_group.id) { + point_group.id + } else { + point_id.next_id() + }; + self.point_domain.push(id, point_group.anchor); + } + /// Appends a Kurbo BezPath to the vector data. pub fn append_bezpath(&mut self, bezpath: kurbo::BezPath) { let mut first_point_index = None; @@ -241,6 +249,11 @@ impl VectorData { } } + /// Construct some new vector data from a single subpath with an identity transform and black fill. + pub fn from_subpath(subpath: impl Borrow>) -> Self { + Self::from_subpaths([subpath], false) + } + /// Construct some new vector data from subpaths with an identity transform and black fill. pub fn from_subpaths(subpaths: impl IntoIterator>>, preserve_id: bool) -> Self { let mut vector_data = Self::empty(); @@ -252,6 +265,18 @@ impl VectorData { vector_data } + pub fn from_target_groups(target_groups: impl IntoIterator>, preserve_id: bool) -> Self { + let mut vector_data = Self::empty(); + for target_group in target_groups.into_iter() { + let target_group = target_group.borrow(); + match target_group { + ClickTargetGroup::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id), + ClickTargetGroup::PointGroup(point_group) => vector_data.append_point_group(point_group, preserve_id), + } + } + vector_data + } + /// Compute the bounding boxes of the subpaths without any transform pub fn bounding_box(&self) -> Option<[DVec2; 2]> { self.bounding_box_with_transform(DAffine2::IDENTITY) From db5727164f20876e04240db39f598843ec342509 Mon Sep 17 00:00:00 2001 From: seam0s Date: Fri, 9 May 2025 00:55:48 +0300 Subject: [PATCH 06/22] Fix Rust formatting --- .../portfolio/document/utility_types/document_metadata.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 55f481b2cc..2864d77d9e 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -3,7 +3,7 @@ use crate::messages::portfolio::document::graph_operation::transform_utils; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; -use graphene_core::renderer::{ClickTarget, Quad, ClickTargetGroup}; +use graphene_core::renderer::{ClickTarget, ClickTargetGroup, Quad}; use graphene_core::transform::Footprint; use graphene_std::vector::{PointId, VectorData}; use std::collections::{HashMap, HashSet}; From 99e5a2b867f875d426d3ca7558b6327b51773a08 Mon Sep 17 00:00:00 2001 From: seam0s Date: Fri, 9 May 2025 09:47:52 +0300 Subject: [PATCH 07/22] Remove debug statements --- .../document/document_message_handler.rs | 16 ++-------------- .../document/utility_types/document_metadata.rs | 5 ++++- .../messages/tool/tool_messages/select_tool.rs | 6 +----- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 428e815738..fe89b38a1b 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1642,17 +1642,10 @@ impl DocumentMessageHandler { /// Find layers under the location in viewport space that was clicked, listed by their depth in the layer tree hierarchy. pub fn click_list<'a>(&'a self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator + use<'a> { - // debug!("click_xray: {:?}", self.click_xray(ipp).collect::>()); self.click_xray(ipp) - .filter(move |&layer| { - if !self.network_interface.is_artboard(&layer.to_node(), &[]) { - // debug!("Hover: we have been flagged 'not artboard'..."); - } - !self.network_interface.is_artboard(&layer.to_node(), &[]) - }) + .filter(move |&layer| !self.network_interface.is_artboard(&layer.to_node(), &[])) .skip_while(|&layer| layer == LayerNodeIdentifier::ROOT_PARENT) .scan(true, |last_had_children, layer| { - // debug!("Hover: we are being scanned after filtered out of click_xray..."); if *last_had_children { *last_had_children = layer.has_children(self.network_interface.document_metadata()); Some(layer) @@ -2826,12 +2819,7 @@ impl<'a> ClickXRayIter<'a> { match target { // Single points are much cheaper than paths so have their own special case XRayTarget::Point(point) => { - let intersects = click_targets.is_some_and(|targets| { - targets.iter().any(|target| { - // debug!("Hover: we are about to test point intersection with no stroke..."); - target.intersect_point(*point, transform) - }) - }); + let intersects = click_targets.is_some_and(|targets| targets.iter().any(|target| target.intersect_point(*point, transform))); XRayResult { clicked: intersects, use_children: !clip || intersects, diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 2864d77d9e..597777abd2 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -133,7 +133,10 @@ impl DocumentMetadata { pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> { self.click_targets(layer)? .iter() - .filter_map(|click_target| click_target.bounding_box_with_transform(transform)) + .filter_map(|click_target| match click_target.target_group() { + ClickTargetGroup::Subpath(subpath) => subpath.bounding_box_with_transform(transform), + ClickTargetGroup::PointGroup(_) => click_target.bounding_box_with_transform(transform), + }) .reduce(Quad::combine_bounds) } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 916e35510c..81f047624e 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -575,16 +575,12 @@ impl Fsm for SelectToolFsmState { if !matches!(self, Self::Drawing { .. }) && !input.keyboard.get(Key::MouseMiddle as usize) { // Get the layer the user is hovering over let click = document.click(input); - let not_selected_click = click.filter(|&hovered_layer| { - // debug!("Hover: we are being checked for selection status..."); - !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata()) - }); + let not_selected_click = click.filter(|&hovered_layer| !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata())); if let Some(layer) = not_selected_click { if overlay_context.visibility_settings.hover_outline() { let layer_to_viewport = document.metadata().transform_to_viewport(layer); overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport); - // debug!("Hover: We are in!"); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); } From 45624d3a47031614d9d5238dd68959353ef1368d Mon Sep 17 00:00:00 2001 From: seam0s Date: Fri, 9 May 2025 09:48:46 +0300 Subject: [PATCH 08/22] Add point groups support in VectorDataTable::add_upstream_click_targets --- .../gcore/src/graphic_element/renderer.rs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index de45df539e..54403e9df2 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -45,7 +45,7 @@ impl ClickTarget { } pub fn new_with_point_group(point: ManipulatorGroup) -> Self { - let stroke_width = 20.; + let stroke_width = 10.; let bounding_box = Some([point.anchor - DVec2::splat(stroke_width / 2.), point.anchor + DVec2::splat(stroke_width / 2.)]); Self { @@ -59,10 +59,6 @@ impl ClickTarget { &self.target_group } - pub fn target_group_mut(&mut self) -> &mut ClickTargetGroup { - &mut self.target_group - } - pub fn bounding_box(&self) -> Option<[DVec2; 2]> { self.bounding_box } @@ -86,7 +82,7 @@ impl ClickTarget { fn update_bbox(&mut self) { match self.target_group { ClickTargetGroup::Subpath(ref subpath) => { - self.bounding_box = subpath.loose_bounding_box(); + self.bounding_box = subpath.bounding_box(); } ClickTargetGroup::PointGroup(ref point_group) => { self.bounding_box = Some([point_group.anchor - DVec2::splat(self.stroke_width / 2.), point_group.anchor + DVec2::splat(self.stroke_width / 2.)]); @@ -732,19 +728,17 @@ impl GraphicElementRendered for VectorDataTable { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for instance in self.instance_ref_iter() { - // // For free-floating anchors, we need to add a click target for each - // let single_anchors = instance.instance.point_domain.ids().iter().filter(|&&point_id| instance.instance.connected_count(point_id) == 0); - // let single_anchors_targets = single_anchors - // .map(|&point_id| { - // let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); - // let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); - // let subpath = Subpath::new(vec![group], false); - - // let click_target = ClickTarget::new_free_floating_anchor(subpath); - // // click_target.apply_transform(*instance.transform); - // click_target - // }) - // .collect::>(); + // For free-floating anchors, we need to add a click target for each + let single_anchors = instance.instance.point_domain.ids().iter().filter(|&&point_id| instance.instance.connected_count(point_id) == 0); + let single_anchors_targets = single_anchors + .map(|&point_id| { + let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); + let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); + let mut click_target = ClickTarget::new_with_point_group(group); + click_target.apply_transform(*instance.transform); + click_target + }) + .collect::>(); let stroke_width = instance.instance.style.stroke().as_ref().map_or(0., Stroke::weight); let filled = instance.instance.style.fill() != &Fill::None; @@ -759,7 +753,7 @@ impl GraphicElementRendered for VectorDataTable { click_target.apply_transform(*instance.transform); click_target })); - // click_targets.extend(single_anchors_targets); + click_targets.extend(single_anchors_targets); } } From 9e51cfe6499a8290cb54644e7318b72a33ae6b52 Mon Sep 17 00:00:00 2001 From: seam0s Date: Fri, 9 May 2025 10:56:32 +0300 Subject: [PATCH 09/22] Improve overlay for free floating anchors --- .../portfolio/document/overlays/utility_types.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 1696804054..398672a7fa 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -655,13 +655,18 @@ impl OverlayContext { } pub fn outline_free_floating_anchors(&mut self, vector_data: VectorData, transform: DAffine2) { - const SINGLE_ANCHOR_SELECTION_RADIUS: f64 = 4.; + const SINGLE_ANCHOR_SELECTION_RADIUS: f64 = 3.5; for &point_id in vector_data.point_domain.ids() { // Check if the point in the layer is not part of a segment if vector_data.connected_count(point_id) == 0 { if let Some(position) = vector_data.point_domain.position_from_id(point_id) { - self.circle(transform.transform_point2(position), SINGLE_ANCHOR_SELECTION_RADIUS, Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE)); + self.circle( + transform.transform_point2(position), + SINGLE_ANCHOR_SELECTION_RADIUS, + Some(COLOR_OVERLAY_WHITE), + Some(COLOR_OVERLAY_BLUE), + ); } } } From 1fffda16f1ac24745da0eea0a77f9bcb3ad6a438 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sat, 10 May 2025 20:57:38 +0300 Subject: [PATCH 10/22] Remove datatype for nodes_to_shift --- .../portfolio/document/utility_types/network_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 434d68984a..db7b0c6a57 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -5185,7 +5185,7 @@ impl NodeNetworkInterface { } shifted_nodes.insert(*node_id); - let nodes_to_shift: Vec<(NodeId, LayerOwner)> = self.check_collision_with_stack_dependents(node_id, shift_sign, network_path); + let nodes_to_shift = self.check_collision_with_stack_dependents(node_id, shift_sign, network_path); for node_to_shift in nodes_to_shift { self.shift_node_or_parent(&node_to_shift.0, shift_sign, shifted_nodes, network_path); From abe5e36558204006e141195f81c40846798773f0 Mon Sep 17 00:00:00 2001 From: seam0s <153828136+seam0s-dev@users.noreply.github.com> Date: Sun, 11 May 2025 19:22:14 +0300 Subject: [PATCH 11/22] Fix formatting in select_tool.rs --- editor/src/messages/tool/tool_messages/select_tool.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index efc6dd812b..8110658fe0 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -586,9 +586,9 @@ impl Fsm for SelectToolFsmState { } } else { overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport, color); - if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); - } + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); + } } }; let layer = match tool_data.nested_selection_behavior { From 0e35a131f3e3c69ffc94b3611ab83956b40bedda Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 11 May 2025 19:29:40 +0300 Subject: [PATCH 12/22] Lints --- editor/src/messages/tool/tool_messages/select_tool.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 8110658fe0..751aa0819a 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -586,9 +586,9 @@ impl Fsm for SelectToolFsmState { } } else { overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport, color); - if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); - } + } } }; let layer = match tool_data.nested_selection_behavior { @@ -613,6 +613,7 @@ impl Fsm for SelectToolFsmState { let fill_color = Some(fill_color.as_str()); hover_overlay_draw(new_selected, fill_color); } + } } // Measure with Alt held down From 02edf63c9d4c78fec11b9c53724741947c086a32 Mon Sep 17 00:00:00 2001 From: seam0s Date: Thu, 15 May 2025 11:58:47 +0300 Subject: [PATCH 13/22] Code review --- .../document/document_message_handler.rs | 18 +++--- .../document/overlays/utility_types.rs | 9 +-- .../utility_types/document_metadata.rs | 16 ++---- .../gcore/src/graphic_element/renderer.rs | 57 ++++++++++--------- node-graph/gcore/src/vector/vector_data.rs | 10 ++-- 5 files changed, 48 insertions(+), 62 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index fe89b38a1b..0cff823ff4 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1609,7 +1609,7 @@ impl DocumentMessageHandler { subpath.apply_transform(layer_transform); subpath.is_inside_subpath(&viewport_polygon, None, None) } - ClickTargetGroup::PointGroup(point) => { + ClickTargetGroup::ManipulatorGroup(point) => { let mut point = point.clone(); point.apply_transform(layer_transform); viewport_polygon.contains_point(point.anchor) @@ -2740,18 +2740,14 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end), }; click_targets - .filter(|target| match target.target_group() { - ClickTargetGroup::Subpath(_) => true, - _ => false, - }) - .flat_map(|target| { - let subpath = if let ClickTargetGroup::Subpath(subpath) = target.target_group() { - subpath + .filter_map(|target| { + if let ClickTargetGroup::Subpath(subpath) = target.target_group() { + Some(subpath.iter()) } else { - panic!("Expected a subpath target group"); - }; - subpath.iter() + None + } }) + .flatten() .map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x)))) .collect() } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 190560e0a5..36f85a5c8f 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -657,18 +657,11 @@ impl OverlayContext { } pub fn outline_free_floating_anchors(&mut self, vector_data: VectorData, transform: DAffine2) { - const SINGLE_ANCHOR_SELECTION_RADIUS: f64 = 3.5; - for &point_id in vector_data.point_domain.ids() { // Check if the point in the layer is not part of a segment if vector_data.connected_count(point_id) == 0 { if let Some(position) = vector_data.point_domain.position_from_id(point_id) { - self.circle( - transform.transform_point2(position), - SINGLE_ANCHOR_SELECTION_RADIUS, - Some(COLOR_OVERLAY_WHITE), - Some(COLOR_OVERLAY_BLUE), - ); + self.manipulator_anchor(transform.transform_point2(position), false, None); } } } diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 597777abd2..e31ea6f770 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -135,7 +135,7 @@ impl DocumentMetadata { .iter() .filter_map(|click_target| match click_target.target_group() { ClickTargetGroup::Subpath(subpath) => subpath.bounding_box_with_transform(transform), - ClickTargetGroup::PointGroup(_) => click_target.bounding_box_with_transform(transform), + ClickTargetGroup::ManipulatorGroup(_) => click_target.bounding_box_with_transform(transform), }) .reduce(Quad::combine_bounds) } @@ -179,16 +179,10 @@ impl DocumentMetadata { pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { static EMPTY: Vec = Vec::new(); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); - click_targets - .iter() - .filter(|target| match target.target_group() { - ClickTargetGroup::Subpath(_) => true, - _ => false, - }) - .map(|target| match target.target_group() { - ClickTargetGroup::Subpath(subpath) => subpath, - _ => unreachable!(), - }) + click_targets.iter().filter_map(|target| match target.target_group() { + ClickTargetGroup::Subpath(subpath) => Some(subpath), + _ => None, + }) } pub fn is_clip(&self, node: NodeId) -> bool { diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 54403e9df2..69d2d7925c 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -23,7 +23,7 @@ use vello::*; #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ClickTargetGroup { Subpath(bezier_rs::Subpath), - PointGroup(ManipulatorGroup), + ManipulatorGroup(ManipulatorGroup), } /// Represents a clickable target for the layer @@ -44,12 +44,12 @@ impl ClickTarget { } } - pub fn new_with_point_group(point: ManipulatorGroup) -> Self { + pub fn new_with_manipulator_group(point: ManipulatorGroup) -> Self { let stroke_width = 10.; let bounding_box = Some([point.anchor - DVec2::splat(stroke_width / 2.), point.anchor + DVec2::splat(stroke_width / 2.)]); Self { - target_group: ClickTargetGroup::PointGroup(point), + target_group: ClickTargetGroup::ManipulatorGroup(point), stroke_width, bounding_box, } @@ -72,7 +72,7 @@ impl ClickTarget { ClickTargetGroup::Subpath(ref mut subpath) => { subpath.apply_transform(affine_transform); } - ClickTargetGroup::PointGroup(ref mut point_group) => { + ClickTargetGroup::ManipulatorGroup(ref mut point_group) => { point_group.apply_transform(affine_transform); } } @@ -84,8 +84,8 @@ impl ClickTarget { ClickTargetGroup::Subpath(ref subpath) => { self.bounding_box = subpath.bounding_box(); } - ClickTargetGroup::PointGroup(ref point_group) => { - self.bounding_box = Some([point_group.anchor - DVec2::splat(self.stroke_width / 2.), point_group.anchor + DVec2::splat(self.stroke_width / 2.)]); + ClickTargetGroup::ManipulatorGroup(ref point) => { + self.bounding_box = Some([point.anchor - DVec2::splat(self.stroke_width / 2.), point.anchor + DVec2::splat(self.stroke_width / 2.)]); } } } @@ -117,8 +117,8 @@ impl ClickTarget { let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor); return any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0); } - ClickTargetGroup::PointGroup(point_group) => { - let point = point_group.anchor; + ClickTargetGroup::ManipulatorGroup(point) => { + let point = point.anchor; bezier_iter().map(|bezier| bezier.winding(point)).sum::() != 0 } } @@ -152,7 +152,7 @@ impl ClickTarget { // Check if the point is within the shape match self.target_group() { ClickTargetGroup::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), - ClickTargetGroup::PointGroup(point_group) => point_group.anchor == point, + ClickTargetGroup::ManipulatorGroup(point_group) => point_group.anchor == point, } } else { false @@ -699,15 +699,16 @@ impl GraphicElementRendered for VectorDataTable { }; // For free-floating anchors, we need to add a click target for each - let single_anchors = instance.point_domain.ids().iter().filter(|&&point_id| instance.connected_count(point_id) == 0); - let single_anchors_targets = single_anchors - .map(|&point_id| { + let single_anchors_targets = instance.point_domain.ids().iter().filter_map(|&point_id| { + if instance.connected_count(point_id) == 0 { let anchor = instance.point_domain.position_from_id(point_id).unwrap_or_default(); let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); - ClickTarget::new_with_point_group(group) - }) - .collect::>(); + Some(ClickTarget::new_with_manipulator_group(group)) + } else { + None + } + }); let click_targets = instance .stroke_bezier_paths() @@ -728,18 +729,6 @@ impl GraphicElementRendered for VectorDataTable { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for instance in self.instance_ref_iter() { - // For free-floating anchors, we need to add a click target for each - let single_anchors = instance.instance.point_domain.ids().iter().filter(|&&point_id| instance.instance.connected_count(point_id) == 0); - let single_anchors_targets = single_anchors - .map(|&point_id| { - let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); - let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); - let mut click_target = ClickTarget::new_with_point_group(group); - click_target.apply_transform(*instance.transform); - click_target - }) - .collect::>(); - let stroke_width = instance.instance.style.stroke().as_ref().map_or(0., Stroke::weight); let filled = instance.instance.style.fill() != &Fill::None; let fill = |mut subpath: bezier_rs::Subpath<_>| { @@ -753,6 +742,20 @@ impl GraphicElementRendered for VectorDataTable { click_target.apply_transform(*instance.transform); click_target })); + + // For free-floating anchors, we need to add a click target for each + let single_anchors_targets = instance.instance.point_domain.ids().iter().filter_map(|&point_id| { + if instance.instance.connected_count(point_id) == 0 { + let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); + let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); + + let mut click_target = ClickTarget::new_with_manipulator_group(group); + click_target.apply_transform(*instance.transform); + Some(click_target) + } else { + None + } + }); click_targets.extend(single_anchors_targets); } } diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index d1079c7c41..1e774facbe 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -174,15 +174,15 @@ impl VectorData { } } - pub fn append_point_group(&mut self, point_group: &ManipulatorGroup, preserve_id: bool) { + pub fn append_manipulator_group(&mut self, point: &ManipulatorGroup, preserve_id: bool) { let mut point_id = self.point_domain.next_id(); // Use the current point id if it is not already in the domain else generate a new one - let id = if preserve_id && !self.point_domain.ids().contains(&point_group.id) { - point_group.id + let id = if preserve_id && !self.point_domain.ids().contains(&point.id) { + point.id } else { point_id.next_id() }; - self.point_domain.push(id, point_group.anchor); + self.point_domain.push(id, point.anchor); } /// Appends a Kurbo BezPath to the vector data. @@ -271,7 +271,7 @@ impl VectorData { let target_group = target_group.borrow(); match target_group { ClickTargetGroup::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id), - ClickTargetGroup::PointGroup(point_group) => vector_data.append_point_group(point_group, preserve_id), + ClickTargetGroup::ManipulatorGroup(point) => vector_data.append_manipulator_group(point, preserve_id), } } vector_data From a56a8b1333145872041177332ba7159084480754 Mon Sep 17 00:00:00 2001 From: seam0s Date: Thu, 15 May 2025 12:04:09 +0300 Subject: [PATCH 14/22] Remove references to point_group --- node-graph/gcore/src/graphic_element/renderer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 69d2d7925c..89efc615c1 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -72,8 +72,8 @@ impl ClickTarget { ClickTargetGroup::Subpath(ref mut subpath) => { subpath.apply_transform(affine_transform); } - ClickTargetGroup::ManipulatorGroup(ref mut point_group) => { - point_group.apply_transform(affine_transform); + ClickTargetGroup::ManipulatorGroup(ref mut point) => { + point.apply_transform(affine_transform); } } self.update_bbox(); From 9a4ae9014afe114889b24b22a4a9e6c267dace48 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sat, 17 May 2025 12:33:39 +0300 Subject: [PATCH 15/22] Refactor ManipulatorGroup for FreePoint in ClickTargetGroup --- .../document/document_message_handler.rs | 4 +- .../utility_types/document_metadata.rs | 2 +- .../gcore/src/graphic_element/renderer.rs | 47 ++++++++++++------- node-graph/gcore/src/vector/vector_data.rs | 8 ++-- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 0cff823ff4..791ef96fc4 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1609,10 +1609,10 @@ impl DocumentMessageHandler { subpath.apply_transform(layer_transform); subpath.is_inside_subpath(&viewport_polygon, None, None) } - ClickTargetGroup::ManipulatorGroup(point) => { + ClickTargetGroup::FreePoint(point) => { let mut point = point.clone(); point.apply_transform(layer_transform); - viewport_polygon.contains_point(point.anchor) + viewport_polygon.contains_point(point.position) } }) }) diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index e31ea6f770..3a5172b1a1 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -135,7 +135,7 @@ impl DocumentMetadata { .iter() .filter_map(|click_target| match click_target.target_group() { ClickTargetGroup::Subpath(subpath) => subpath.bounding_box_with_transform(transform), - ClickTargetGroup::ManipulatorGroup(_) => click_target.bounding_box_with_transform(transform), + ClickTargetGroup::FreePoint(_) => click_target.bounding_box_with_transform(transform), }) .reduce(Quad::combine_bounds) } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 89efc615c1..98d56d53b3 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -9,7 +9,7 @@ use crate::vector::style::{Fill, Stroke, ViewMode}; use crate::vector::{PointId, VectorDataTable}; use crate::{Artboard, ArtboardGroupTable, Color, GraphicElement, GraphicGroupTable, RasterFrame}; use base64::Engine; -use bezier_rs::{ManipulatorGroup, Subpath}; +use bezier_rs::Subpath; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; use num_traits::Zero; @@ -20,10 +20,26 @@ use std::fmt::Write; #[cfg(feature = "vello")] use vello::*; +#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FreePoint { + pub id: PointId, + pub position: DVec2, +} + +impl FreePoint { + pub fn new(id: PointId, position: DVec2) -> Self { + Self { id, position } + } + + pub fn apply_transform(&mut self, transform: DAffine2) { + self.position = transform.transform_point2(self.position); + } +} + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ClickTargetGroup { Subpath(bezier_rs::Subpath), - ManipulatorGroup(ManipulatorGroup), + FreePoint(FreePoint), } /// Represents a clickable target for the layer @@ -44,12 +60,12 @@ impl ClickTarget { } } - pub fn new_with_manipulator_group(point: ManipulatorGroup) -> Self { + pub fn new_with_free_point(point: FreePoint) -> Self { let stroke_width = 10.; - let bounding_box = Some([point.anchor - DVec2::splat(stroke_width / 2.), point.anchor + DVec2::splat(stroke_width / 2.)]); + let bounding_box = Some([point.position - DVec2::splat(stroke_width / 2.), point.position + DVec2::splat(stroke_width / 2.)]); Self { - target_group: ClickTargetGroup::ManipulatorGroup(point), + target_group: ClickTargetGroup::FreePoint(point), stroke_width, bounding_box, } @@ -72,7 +88,7 @@ impl ClickTarget { ClickTargetGroup::Subpath(ref mut subpath) => { subpath.apply_transform(affine_transform); } - ClickTargetGroup::ManipulatorGroup(ref mut point) => { + ClickTargetGroup::FreePoint(ref mut point) => { point.apply_transform(affine_transform); } } @@ -84,8 +100,8 @@ impl ClickTarget { ClickTargetGroup::Subpath(ref subpath) => { self.bounding_box = subpath.bounding_box(); } - ClickTargetGroup::ManipulatorGroup(ref point) => { - self.bounding_box = Some([point.anchor - DVec2::splat(self.stroke_width / 2.), point.anchor + DVec2::splat(self.stroke_width / 2.)]); + ClickTargetGroup::FreePoint(ref point) => { + self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]); } } } @@ -117,10 +133,7 @@ impl ClickTarget { let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor); return any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0); } - ClickTargetGroup::ManipulatorGroup(point) => { - let point = point.anchor; - bezier_iter().map(|bezier| bezier.winding(point)).sum::() != 0 - } + ClickTargetGroup::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::() != 0, } } @@ -152,7 +165,7 @@ impl ClickTarget { // Check if the point is within the shape match self.target_group() { ClickTargetGroup::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), - ClickTargetGroup::ManipulatorGroup(point_group) => point_group.anchor == point, + ClickTargetGroup::FreePoint(free_point) => free_point.position == point, } } else { false @@ -702,9 +715,9 @@ impl GraphicElementRendered for VectorDataTable { let single_anchors_targets = instance.point_domain.ids().iter().filter_map(|&point_id| { if instance.connected_count(point_id) == 0 { let anchor = instance.point_domain.position_from_id(point_id).unwrap_or_default(); - let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); + let point = FreePoint::new(point_id, anchor); - Some(ClickTarget::new_with_manipulator_group(group)) + Some(ClickTarget::new_with_free_point(point)) } else { None } @@ -747,9 +760,9 @@ impl GraphicElementRendered for VectorDataTable { let single_anchors_targets = instance.instance.point_domain.ids().iter().filter_map(|&point_id| { if instance.instance.connected_count(point_id) == 0 { let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); - let group = ManipulatorGroup::new_anchor_with_id(anchor, point_id); + let point = FreePoint::new(point_id, anchor); - let mut click_target = ClickTarget::new_with_manipulator_group(group); + let mut click_target = ClickTarget::new_with_free_point(point); click_target.apply_transform(*instance.transform); Some(click_target) } else { diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 1e774facbe..12877c025d 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -5,7 +5,7 @@ mod modification; use super::misc::point_to_dvec2; use super::style::{PathStyle, Stroke}; use crate::instances::Instances; -use crate::renderer::ClickTargetGroup; +use crate::renderer::{ClickTargetGroup, FreePoint}; use crate::{AlphaBlending, Color, GraphicGroupTable}; pub use attributes::*; use bezier_rs::{BezierHandles, ManipulatorGroup}; @@ -174,7 +174,7 @@ impl VectorData { } } - pub fn append_manipulator_group(&mut self, point: &ManipulatorGroup, preserve_id: bool) { + pub fn append_free_point(&mut self, point: &FreePoint, preserve_id: bool) { let mut point_id = self.point_domain.next_id(); // Use the current point id if it is not already in the domain else generate a new one let id = if preserve_id && !self.point_domain.ids().contains(&point.id) { @@ -182,7 +182,7 @@ impl VectorData { } else { point_id.next_id() }; - self.point_domain.push(id, point.anchor); + self.point_domain.push(id, point.position); } /// Appends a Kurbo BezPath to the vector data. @@ -271,7 +271,7 @@ impl VectorData { let target_group = target_group.borrow(); match target_group { ClickTargetGroup::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id), - ClickTargetGroup::ManipulatorGroup(point) => vector_data.append_manipulator_group(point, preserve_id), + ClickTargetGroup::FreePoint(point) => vector_data.append_free_point(point, preserve_id), } } vector_data From 6c0036b1923cecdbaa2a563aa21584a706993cee Mon Sep 17 00:00:00 2001 From: seam0s Date: Sat, 17 May 2025 13:22:20 +0300 Subject: [PATCH 16/22] Rename ClickTargetGroup to ClickTargetType --- .../document/document_message_handler.rs | 10 +++--- .../utility_types/document_metadata.rs | 12 +++---- .../utility_types/network_interface.rs | 16 ++++----- .../gcore/src/graphic_element/renderer.rs | 36 +++++++++---------- node-graph/gcore/src/vector/vector_data.rs | 14 ++++---- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 791ef96fc4..2aef5a2f1d 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -32,7 +32,7 @@ use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graphene_core::raster::BlendMode; use graphene_core::raster::image::ImageFrameTable; use graphene_core::vector::style::ViewMode; -use graphene_std::renderer::{ClickTarget, ClickTargetGroup, Quad}; +use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; use graphene_std::vector::{PointId, path_bool_lib}; use std::time::Duration; @@ -1603,13 +1603,13 @@ impl DocumentMessageHandler { let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer); layer_click_targets.is_some_and(|targets| { - targets.iter().all(|target| match target.target_group() { - ClickTargetGroup::Subpath(subpath) => { + targets.iter().all(|target| match target.target_type() { + ClickTargetType::Subpath(subpath) => { let mut subpath = subpath.clone(); subpath.apply_transform(layer_transform); subpath.is_inside_subpath(&viewport_polygon, None, None) } - ClickTargetGroup::FreePoint(point) => { + ClickTargetType::FreePoint(point) => { let mut point = point.clone(); point.apply_transform(layer_transform); viewport_polygon.contains_point(point.position) @@ -2741,7 +2741,7 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator Option<[DVec2; 2]> { self.click_targets(layer)? .iter() - .filter_map(|click_target| match click_target.target_group() { - ClickTargetGroup::Subpath(subpath) => subpath.bounding_box_with_transform(transform), - ClickTargetGroup::FreePoint(_) => click_target.bounding_box_with_transform(transform), + .filter_map(|click_target| match click_target.target_type() { + ClickTargetType::Subpath(subpath) => subpath.bounding_box_with_transform(transform), + ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform), }) .reduce(Quad::combine_bounds) } @@ -179,8 +179,8 @@ impl DocumentMetadata { pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { static EMPTY: Vec = Vec::new(); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); - click_targets.iter().filter_map(|target| match target.target_group() { - ClickTargetGroup::Subpath(subpath) => Some(subpath), + click_targets.iter().filter_map(|target| match target.target_type() { + ClickTargetType::Subpath(subpath) => Some(subpath), _ => None, }) } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index db7b0c6a57..8b33997f4e 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -12,7 +12,7 @@ use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::{Type, concrete}; -use graphene_std::renderer::{ClickTarget, ClickTargetGroup, Quad}; +use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; use graphene_std::transform::Footprint; use graphene_std::vector::{PointId, VectorData, VectorModificationType}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes; @@ -2804,25 +2804,25 @@ impl NodeNetworkInterface { 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)) { let mut node_path = String::new(); - if let ClickTargetGroup::Subpath(subpath) = node_click_targets.node_click_target.target_group() { + if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() { let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY); } all_node_click_targets.push((node_id, node_path)); for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) { - if let ClickTargetGroup::Subpath(subpath) = port.target_group() { + if let ClickTargetType::Subpath(subpath) = port.target_type() { let mut port_path = String::new(); let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); port_click_targets.push(port_path); } } if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata { - if let ClickTargetGroup::Subpath(subpath) = layer_metadata.visibility_click_target.target_group() { + if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() { let mut port_path = String::new(); let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); icon_click_targets.push(port_path); } - if let ClickTargetGroup::Subpath(subpath) = layer_metadata.grip_click_target.target_group() { + if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() { let mut port_path = String::new(); let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); icon_click_targets.push(port_path); @@ -2881,7 +2881,7 @@ impl NodeNetworkInterface { .chain(modify_import_export_click_targets.remove_imports_exports.click_targets()) .chain(modify_import_export_click_targets.reorder_imports_exports.click_targets()) { - if let ClickTargetGroup::Subpath(subpath) = click_target.target_group() { + if let ClickTargetType::Subpath(subpath) = click_target.target_type() { let mut remove_string = String::new(); let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY); modify_import_export.push(remove_string); @@ -3185,8 +3185,8 @@ impl NodeNetworkInterface { self.document_metadata .click_targets .get(&layer) - .map(|click| click.iter().map(ClickTarget::target_group)) - .map(|target_groups| VectorData::from_target_groups(target_groups, true)) + .map(|click| click.iter().map(ClickTarget::target_type)) + .map(|target_types| VectorData::from_target_types(target_types, true)) } /// Loads the structure of layer nodes from a node graph. diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 98d56d53b3..f24d9af6c9 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -37,7 +37,7 @@ impl FreePoint { } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum ClickTargetGroup { +pub enum ClickTargetType { Subpath(bezier_rs::Subpath), FreePoint(FreePoint), } @@ -45,7 +45,7 @@ pub enum ClickTargetGroup { /// Represents a clickable target for the layer #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ClickTarget { - target_group: ClickTargetGroup, + target_type: ClickTargetType, stroke_width: f64, bounding_box: Option<[DVec2; 2]>, } @@ -54,7 +54,7 @@ impl ClickTarget { pub fn new_with_subpath(subpath: bezier_rs::Subpath, stroke_width: f64) -> Self { let bounding_box = subpath.loose_bounding_box(); Self { - target_group: ClickTargetGroup::Subpath(subpath), + target_type: ClickTargetType::Subpath(subpath), stroke_width, bounding_box, } @@ -65,14 +65,14 @@ impl ClickTarget { let bounding_box = Some([point.position - DVec2::splat(stroke_width / 2.), point.position + DVec2::splat(stroke_width / 2.)]); Self { - target_group: ClickTargetGroup::FreePoint(point), + target_type: ClickTargetType::FreePoint(point), stroke_width, bounding_box, } } - pub fn target_group(&self) -> &ClickTargetGroup { - &self.target_group + pub fn target_type(&self) -> &ClickTargetType { + &self.target_type } pub fn bounding_box(&self) -> Option<[DVec2; 2]> { @@ -84,11 +84,11 @@ impl ClickTarget { } pub fn apply_transform(&mut self, affine_transform: DAffine2) { - match self.target_group { - ClickTargetGroup::Subpath(ref mut subpath) => { + match self.target_type { + ClickTargetType::Subpath(ref mut subpath) => { subpath.apply_transform(affine_transform); } - ClickTargetGroup::FreePoint(ref mut point) => { + ClickTargetType::FreePoint(ref mut point) => { point.apply_transform(affine_transform); } } @@ -96,11 +96,11 @@ impl ClickTarget { } fn update_bbox(&mut self) { - match self.target_group { - ClickTargetGroup::Subpath(ref subpath) => { + match self.target_type { + ClickTargetType::Subpath(ref subpath) => { self.bounding_box = subpath.bounding_box(); } - ClickTargetGroup::FreePoint(ref point) => { + ClickTargetType::FreePoint(ref point) => { self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]); } } @@ -117,8 +117,8 @@ impl ClickTarget { let inverse = layer_transform.inverse(); let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point))); - match self.target_group() { - ClickTargetGroup::Subpath(subpath) => { + match self.target_type() { + ClickTargetType::Subpath(subpath) => { // Check if outlines intersect let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty()); if subpath.iter().any(outline_intersects) { @@ -133,7 +133,7 @@ impl ClickTarget { let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor); return any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0); } - ClickTargetGroup::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::() != 0, + ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::() != 0, } } @@ -163,9 +163,9 @@ impl ClickTarget { .is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y) { // Check if the point is within the shape - match self.target_group() { - ClickTargetGroup::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), - ClickTargetGroup::FreePoint(free_point) => free_point.position == point, + match self.target_type() { + ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), + ClickTargetType::FreePoint(free_point) => free_point.position == point, } } else { false diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 12877c025d..5bb989053f 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -5,7 +5,7 @@ mod modification; use super::misc::point_to_dvec2; use super::style::{PathStyle, Stroke}; use crate::instances::Instances; -use crate::renderer::{ClickTargetGroup, FreePoint}; +use crate::renderer::{ClickTargetType, FreePoint}; use crate::{AlphaBlending, Color, GraphicGroupTable}; pub use attributes::*; use bezier_rs::{BezierHandles, ManipulatorGroup}; @@ -265,13 +265,13 @@ impl VectorData { vector_data } - pub fn from_target_groups(target_groups: impl IntoIterator>, preserve_id: bool) -> Self { + pub fn from_target_types(target_types: impl IntoIterator>, preserve_id: bool) -> Self { let mut vector_data = Self::empty(); - for target_group in target_groups.into_iter() { - let target_group = target_group.borrow(); - match target_group { - ClickTargetGroup::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id), - ClickTargetGroup::FreePoint(point) => vector_data.append_free_point(point, preserve_id), + for target_type in target_types.into_iter() { + let target_type = target_type.borrow(); + match target_type { + ClickTargetType::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id), + ClickTargetType::FreePoint(point) => vector_data.append_free_point(point, preserve_id), } } vector_data From bd735c6e4960547c796b7f9c66b099a36ca3d229 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 18 May 2025 23:34:27 +0300 Subject: [PATCH 17/22] Refactor outline_free_floating_anchors into outline --- .../document/overlays/utility_types.rs | 31 ++++++++++--------- .../utility_types/document_metadata.rs | 6 ++++ .../tool/common_functionality/snapping.rs | 6 +++- .../tool/tool_messages/select_tool.rs | 17 ++-------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 36f85a5c8f..fb0a076ed5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -10,6 +10,7 @@ use core::f64::consts::{FRAC_PI_2, TAU}; use glam::{DAffine2, DVec2}; use graphene_core::Color; use graphene_core::renderer::Quad; +use graphene_std::renderer::ClickTargetType; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; @@ -647,23 +648,23 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - /// Used by the Select tool to outline a path selected or hovered. - pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: Option<&str>) { - self.push_path(subpaths, transform); + /// Used by the Select tool to outline a path or a free point when selected or hovered. + pub fn outline(&mut self, target_types: impl Iterator>, transform: DAffine2, color: Option<&str>) { + let mut subpaths: Vec> = vec![]; - let color = color.unwrap_or(COLOR_OVERLAY_BLUE); - self.render_context.set_stroke_style_str(color); - self.render_context.stroke(); - } - - pub fn outline_free_floating_anchors(&mut self, vector_data: VectorData, transform: DAffine2) { - for &point_id in vector_data.point_domain.ids() { - // Check if the point in the layer is not part of a segment - if vector_data.connected_count(point_id) == 0 { - if let Some(position) = vector_data.point_domain.position_from_id(point_id) { - self.manipulator_anchor(transform.transform_point2(position), false, None); - } + target_types.for_each(|target_type| match target_type.borrow() { + ClickTargetType::FreePoint(point) => { + self.manipulator_anchor(transform.transform_point2(point.position), false, None); } + ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()), + }); + + if !subpaths.is_empty() { + self.push_path(subpaths.iter(), transform); + + let color = color.unwrap_or(COLOR_OVERLAY_BLUE); + self.render_context.set_stroke_style_str(color); + self.render_context.stroke(); } } diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index bcc98bb482..f312ba59ae 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -185,6 +185,12 @@ impl DocumentMetadata { }) } + pub fn layer_with_free_points_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator { + static EMPTY: Vec = Vec::new(); + let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); + click_targets.iter().map(|target| target.target_type()) + } + pub fn is_clip(&self, node: NodeId) -> bool { self.clip_targets.contains(&node) } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 266976766d..d42f04d363 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -449,7 +449,11 @@ impl SnapManager { if let Some(ind) = &self.indicator { for layer in &ind.outline_layers { let &Some(layer) = layer else { continue }; - overlay_context.outline(snap_data.document.metadata().layer_outline(layer), snap_data.document.metadata().transform_to_viewport(layer), None); + overlay_context.outline( + snap_data.document.metadata().layer_with_free_points_outline(layer), + snap_data.document.metadata().transform_to_viewport(layer), + None, + ); } if let Some(quad) = ind.target_bounds { overlay_context.quad(to_viewport * quad, None, None); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 751aa0819a..467e3214b5 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -528,16 +528,12 @@ impl Fsm for SelectToolFsmState { .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) { let layer_to_viewport = document.metadata().transform_to_viewport(layer); - overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport, None); + overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None); if is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text") { let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, font_cache); overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None); } - - if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); - } } } @@ -585,10 +581,7 @@ impl Fsm for SelectToolFsmState { overlay_context.quad(Quad::from_box(bounds), color, None); } } else { - overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport, color); - if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); - } + overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, color); } }; let layer = match tool_data.nested_selection_behavior { @@ -827,11 +820,7 @@ impl Fsm for SelectToolFsmState { // Draws a temporary outline on the layers that will be selected by the current box/lasso area for layer in layers_to_outline { let layer_to_viewport = document.metadata().transform_to_viewport(layer); - overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport, None); - - if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport); - } + overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None); } } From 438e06d4d0c8957f97a66d75ab0dec69a2ff2523 Mon Sep 17 00:00:00 2001 From: seam0s Date: Tue, 20 May 2025 21:00:33 +0300 Subject: [PATCH 18/22] Adapt TransformCage to disable dragging and rotating on a single anchor layer --- .../transformation_cage.rs | 25 +++++++++++++++---- .../gcore/src/graphic_element/renderer.rs | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index f4c59c8e1a..1554c8f89f 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -52,6 +52,7 @@ enum TransformCageSizeCategory { Narrow, /// - ![Diagram](https://files.keavon.com/-/OpenPaleturquoiseArthropods/capture.png) Flat, + Point, } impl SelectedEdges { @@ -635,6 +636,10 @@ impl BoundingBoxManager { fn overlay_display_category(&self) -> TransformCageSizeCategory { let quad = self.transform * Quad::from_box(self.bounds); + if self.is_bounds_point() { + return TransformCageSizeCategory::Point; + } + // Check if the area is essentially zero because either the width or height is smaller than an epsilon if self.is_bounds_flat() { return TransformCageSizeCategory::Flat; @@ -664,6 +669,11 @@ impl BoundingBoxManager { (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any() } + /// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on actual points (axis-aligned, i.e. drawn at a single pixel) or when an element is scaled to zero in both X and Y. A point transform cage cannot be rotated by a transformation, and its local space remains a point. + fn is_bounds_point(&self) -> bool { + (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).all() + } + /// Determine if the given point in viewport space falls within the bounds of `self`. fn is_contained_in_bounds(&self, point: DVec2) -> bool { let document_point = self.transform.inverse().transform_point2(point); @@ -699,7 +709,7 @@ impl BoundingBoxManager { let [edge_min_x, edge_min_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR); let [midpoint_threshold_x, midpoint_threshold_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS); - if min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y && cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y { + if (min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y) && (cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y) { let mut top = (cursor.y - min.y).abs() < threshold_y; let mut bottom = (max.y - cursor.y).abs() < threshold_y; let mut left = (cursor.x - min.x).abs() < threshold_x; @@ -731,6 +741,7 @@ impl BoundingBoxManager { } // Prioritize single axis transformations on very small bounds + // debug!("check_selected_edges: corner_min_x: {}, corner_min_y: {}", corner_min_x, corner_min_y); if height < corner_min_y && (left || right) { top = false; bottom = false; @@ -741,11 +752,11 @@ impl BoundingBoxManager { } // On bounds with no width/height, disallow transformation in the relevant axis - if width < f64::EPSILON * 1000. { + if width < 1e-4 { left = false; right = false; } - if height < f64::EPSILON * 1000. { + if height < 1e-4 { top = false; bottom = false; } @@ -767,9 +778,12 @@ impl BoundingBoxManager { let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_ROTATE_THRESHOLD); let cursor = self.transform.inverse().transform_point2(cursor); - let flat = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any(); + let flat = self.is_bounds_flat(); + let point = self.is_bounds_point(); let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y; - if flat { + if point { + return false; + } else if flat { [self.bounds[0], self.bounds[1]].iter().any(within_square_bounds) } else { self.evaluate_transform_handle_positions().iter().any(within_square_bounds) @@ -793,6 +807,7 @@ impl BoundingBoxManager { }; } + // debug!("get_cursor: edges: {:?}, rotate: {}, dragging_bounds: {}, skew_edge: {:?}", edges, rotate, dragging_bounds, skew_edge); match edges { Some((top, bottom, left, right)) => match (top, bottom, left, right) { (true, _, false, false) | (_, true, false, false) => MouseCursorIcon::NSResize, diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index f24d9af6c9..b41eabcd95 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -61,7 +61,7 @@ impl ClickTarget { } pub fn new_with_free_point(point: FreePoint) -> Self { - let stroke_width = 10.; + let stroke_width = 2e-4; let bounding_box = Some([point.position - DVec2::splat(stroke_width / 2.), point.position + DVec2::splat(stroke_width / 2.)]); Self { From b587b764b712203f484f75f2ce266b740e5c8d4b Mon Sep 17 00:00:00 2001 From: seam0s Date: Tue, 20 May 2025 23:36:46 +0300 Subject: [PATCH 19/22] Fix hover on single points --- editor/src/consts.rs | 2 ++ .../portfolio/document/document_message_handler.rs | 5 ++--- .../common_functionality/transformation_cage.rs | 14 +++++++------- node-graph/gcore/src/graphic_element/renderer.rs | 8 ++++++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 9b58d589bf..9d097908fd 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -89,6 +89,8 @@ pub const MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR: f64 = 40.; /// /// The motion of the user's cursor by an `x` pixel offset results in `x * scale_factor` pixels of offset on the other side. pub const MAXIMUM_ALT_SCALE_FACTOR: f64 = 25.; +/// The width or height that the transform cage needs before it is considered to have no width or height. +pub const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4; // SKEW TRIANGLES pub const SKEW_TRIANGLE_SIZE: f64 = 7.; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 8577c100ec..1e072215c3 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2761,10 +2761,9 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator bool { - (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any() + (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).any() } /// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on actual points (axis-aligned, i.e. drawn at a single pixel) or when an element is scaled to zero in both X and Y. A point transform cage cannot be rotated by a transformation, and its local space remains a point. fn is_bounds_point(&self) -> bool { - (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).all() + (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).all() } /// Determine if the given point in viewport space falls within the bounds of `self`. @@ -752,11 +752,11 @@ impl BoundingBoxManager { } // On bounds with no width/height, disallow transformation in the relevant axis - if width < 1e-4 { + if width < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT { left = false; right = false; } - if height < 1e-4 { + if height < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT { top = false; bottom = false; } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index b41eabcd95..fde5c32d93 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -61,8 +61,12 @@ impl ClickTarget { } pub fn new_with_free_point(point: FreePoint) -> Self { - let stroke_width = 2e-4; - let bounding_box = Some([point.position - DVec2::splat(stroke_width / 2.), point.position + DVec2::splat(stroke_width / 2.)]); + const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.; + let stroke_width = 10.; + let bounding_box = Some([ + point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT), + point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT), + ]); Self { target_type: ClickTargetType::FreePoint(point), From 8202c4823570872462e0f78f6321382cd083275d Mon Sep 17 00:00:00 2001 From: seam0s Date: Fri, 23 May 2025 11:37:48 +0300 Subject: [PATCH 20/22] Fix comments --- .../tool/common_functionality/transformation_cage.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index f8b8758c26..e20d32bbe6 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -636,11 +636,12 @@ impl BoundingBoxManager { fn overlay_display_category(&self) -> TransformCageSizeCategory { let quad = self.transform * Quad::from_box(self.bounds); + // Check if the bounds are essentially the same because the width and height are smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT if self.is_bounds_point() { return TransformCageSizeCategory::Point; } - // Check if the area is essentially zero because either the width or height is smaller than an epsilon + // Check if the area is essentially zero because either the width or height is smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT if self.is_bounds_flat() { return TransformCageSizeCategory::Flat; } @@ -669,7 +670,7 @@ impl BoundingBoxManager { (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).any() } - /// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on actual points (axis-aligned, i.e. drawn at a single pixel) or when an element is scaled to zero in both X and Y. A point transform cage cannot be rotated by a transformation, and its local space remains a point. + /// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on points when an element is scaled to zero in both X and Y. A point transform cage cannot be rotated by a transformation, and its local space remains a point. fn is_bounds_point(&self) -> bool { (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).all() } From 1e3e218fce608211c27a99d6a70cc481891493b0 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 8 Jun 2025 12:07:13 +0300 Subject: [PATCH 21/22] Lints --- node-graph/gcore/src/vector/vector_data.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 785b90a3ae..a9d34dbbcd 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -116,11 +116,6 @@ impl core::hash::Hash for VectorData { } impl VectorData { - /// Construct some new vector data from a single subpath with an identity transform and black fill. - pub fn from_subpath(subpath: impl Borrow>) -> Self { - Self::from_subpaths([subpath], false) - } - /// Push a subpath to the vector data pub fn append_subpath(&mut self, subpath: impl Borrow>, preserve_id: bool) { let subpath: &bezier_rs::Subpath = subpath.borrow(); @@ -213,7 +208,7 @@ impl VectorData { } pub fn from_target_types(target_types: impl IntoIterator>, preserve_id: bool) -> Self { - let mut vector_data = Self::empty(); + let mut vector_data = Self::default(); for target_type in target_types.into_iter() { let target_type = target_type.borrow(); match target_type { From b9976ffccc8cc6e6c638d9d9a4475d42c3f34e0a Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 8 Jun 2025 16:12:10 -0700 Subject: [PATCH 22/22] Code review pass --- .../document/document_message_handler.rs | 5 +++-- .../tool/common_functionality/shape_editor.rs | 7 ++----- .../transformation_cage.rs | 7 +++---- .../gcore/src/graphic_element/renderer.rs | 20 +++++++++---------- node-graph/gcore/src/vector/vector_data.rs | 11 ++++++---- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 08baaf41b1..d8a3791515 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2888,9 +2888,10 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator usize { let count = self.selected_points.iter().fold(0, |acc, point| { - if (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors) { - acc - } else { - acc + 1 - } + let is_ignored = (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors); + acc + if is_ignored { 0 } else { 1 } }); count } diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index e20d32bbe6..4318ce028f 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -52,6 +52,7 @@ enum TransformCageSizeCategory { Narrow, /// - ![Diagram](https://files.keavon.com/-/OpenPaleturquoiseArthropods/capture.png) Flat, + /// A single point in space with no width or height. Point, } @@ -670,7 +671,7 @@ impl BoundingBoxManager { (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).any() } - /// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on points when an element is scaled to zero in both X and Y. A point transform cage cannot be rotated by a transformation, and its local space remains a point. + /// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on points when an element is scaled to zero in both X and Y, or if an element is just a single anchor point. A point transform cage cannot be rotated by a transformation, and its local space remains a point. fn is_bounds_point(&self) -> bool { (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).all() } @@ -742,7 +743,6 @@ impl BoundingBoxManager { } // Prioritize single axis transformations on very small bounds - // debug!("check_selected_edges: corner_min_x: {}, corner_min_y: {}", corner_min_x, corner_min_y); if height < corner_min_y && (left || right) { top = false; bottom = false; @@ -783,7 +783,7 @@ impl BoundingBoxManager { let point = self.is_bounds_point(); let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y; if point { - return false; + false } else if flat { [self.bounds[0], self.bounds[1]].iter().any(within_square_bounds) } else { @@ -808,7 +808,6 @@ impl BoundingBoxManager { }; } - // debug!("get_cursor: edges: {:?}, rotate: {}, dragging_bounds: {}, skew_edge: {:?}", edges, rotate, dragging_bounds, skew_edge); match edges { Some((top, bottom, left, right)) => match (top, bottom, left, right) { (true, _, false, false) | (_, true, false, false) => MouseCursorIcon::NSResize, diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 6d8ca72dda..403fc37665 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -135,7 +135,7 @@ impl ClickTarget { // Check if shape is entirely within selection let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor); - return any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0); + any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0) } ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::() != 0, } @@ -763,16 +763,16 @@ impl GraphicElementRendered for VectorDataTable { // For free-floating anchors, we need to add a click target for each let single_anchors_targets = instance.instance.point_domain.ids().iter().filter_map(|&point_id| { - if instance.instance.connected_count(point_id) == 0 { - let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); - let point = FreePoint::new(point_id, anchor); - - let mut click_target = ClickTarget::new_with_free_point(point); - click_target.apply_transform(*instance.transform); - Some(click_target) - } else { - None + if instance.instance.connected_count(point_id) > 0 { + return None; } + + let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default(); + let point = FreePoint::new(point_id, anchor); + + let mut click_target = ClickTarget::new_with_free_point(point); + click_target.apply_transform(*instance.transform); + Some(click_target) }); click_targets.extend(single_anchors_targets); } diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 99e1395209..a987260d07 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -131,7 +131,8 @@ impl VectorData { let mut segment_id = self.segment_domain.next_id(); let mut last_point = None; let mut first_point = None; - // Constructs a bezier segment from the two manipulators on the subpath. + + // Construct a bezier segment from the two manipulators on the subpath. for pair in subpath.manipulator_groups().windows(2) { let start = last_point.unwrap_or_else(|| { let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) { @@ -177,7 +178,8 @@ impl VectorData { pub fn append_free_point(&mut self, point: &FreePoint, preserve_id: bool) { let mut point_id = self.point_domain.next_id(); - // Use the current point id if it is not already in the domain else generate a new one + + // Use the current point ID if it's not already in the domain, otherwise generate a new one let id = if preserve_id && !self.point_domain.ids().contains(&point.id) { point.id } else { @@ -209,13 +211,14 @@ impl VectorData { pub fn from_target_types(target_types: impl IntoIterator>, preserve_id: bool) -> Self { let mut vector_data = Self::default(); + for target_type in target_types.into_iter() { - let target_type = target_type.borrow(); - match target_type { + match target_type.borrow() { ClickTargetType::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id), ClickTargetType::FreePoint(point) => vector_data.append_free_point(point, preserve_id), } } + vector_data }