Skip to content

Add overlays for free-floating anchors on hovered/selected vector layers #2630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8cf7eb7
Add selection overlay for free-floating anchors
seam0s-dev May 3, 2025
1558286
Add hover overlay for free-floating anchors
seam0s-dev May 3, 2025
050aa92
Refactor outline_free_floating anchor
seam0s-dev May 4, 2025
b0312a4
Add single-anchor click targets on VectorData
seam0s-dev May 4, 2025
e63b529
Merge branch 'GraphiteEditor:master' into 2558-free-floating-anchors-…
seam0s-dev May 6, 2025
fdc82c8
Modify ClickTarget to adapt for Subpath and PointGroup
seam0s-dev May 8, 2025
db57271
Fix Rust formatting
seam0s-dev May 8, 2025
99e5a2b
Remove debug statements
seam0s-dev May 9, 2025
45624d3
Add point groups support in VectorDataTable::add_upstream_click_targets
seam0s-dev May 9, 2025
96b5e17
Merge branch 'master' into 2558-free-floating-anchors-overlays
Keavon May 9, 2025
9e51cfe
Improve overlay for free floating anchors
seam0s-dev May 9, 2025
45dde55
Merge branch 'GraphiteEditor:master' into 2558-free-floating-anchors-…
seam0s-dev May 9, 2025
1fffda1
Remove datatype for nodes_to_shift
seam0s-dev May 10, 2025
f501cf4
Merge branch 'master' into 2558-free-floating-anchors-overlays
seam0s-dev May 11, 2025
abe5e36
Fix formatting in select_tool.rs
seam0s-dev May 11, 2025
0e35a13
Lints
seam0s-dev May 11, 2025
02edf63
Code review
seam0s-dev May 15, 2025
a56a8b1
Remove references to point_group
seam0s-dev May 15, 2025
9a4ae90
Refactor ManipulatorGroup for FreePoint in ClickTargetGroup
seam0s-dev May 17, 2025
6c0036b
Rename ClickTargetGroup to ClickTargetType
seam0s-dev May 17, 2025
bd735c6
Refactor outline_free_floating_anchors into outline
seam0s-dev May 18, 2025
71314c2
Merge branch 'master' into 2558-free-floating-anchors-overlays
seam0s-dev May 18, 2025
9135bb4
Merge branch 'master' into 2558-free-floating-anchors-overlays
Keavon May 18, 2025
54b3ba2
Merge branch 'master' into 2558-free-floating-anchors-overlays
seam0s-dev May 19, 2025
68f6800
Merge branch 'master' into 2558-free-floating-anchors-overlays
seam0s-dev May 20, 2025
438e06d
Adapt TransformCage to disable dragging and rotating on a single anch…
seam0s-dev May 20, 2025
b587b76
Fix hover on single points
seam0s-dev May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.;
Expand Down
25 changes: 19 additions & 6 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, ClickTargetType, Quad};
use graphene_std::vector::{PointId, path_bool_lib};
use std::time::Duration;

Expand Down Expand Up @@ -1622,10 +1622,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_type() {
ClickTargetType::Subpath(subpath) => {
let mut subpath = subpath.clone();
subpath.apply_transform(layer_transform);
subpath.is_inside_subpath(&viewport_polygon, None, None)
}
ClickTargetType::FreePoint(point) => {
let mut point = point.clone();
point.apply_transform(layer_transform);
viewport_polygon.contains_point(point.position)
}
})
})
}
Expand Down Expand Up @@ -2752,7 +2759,13 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
};
click_targets
.flat_map(|target| target.subpath().iter())
.filter_map(|target| {
if let ClickTargetType::Subpath(subpath) = target.target_type() {
return Some(subpath.iter());
}
None
})
.flatten()
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x))))
.collect()
}
Expand Down
24 changes: 18 additions & 6 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -647,13 +648,24 @@ 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<Item = impl Borrow<Subpath<PointId>>>, 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<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];

let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
self.render_context.set_stroke_style_str(color);
self.render_context.stroke();
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();
}
}

/// Fills the area inside the path. Assumes `color` is in gamma space.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, ClickTargetType, Quad};
use graphene_core::transform::Footprint;
use graphene_std::vector::{PointId, VectorData};
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -134,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.subpath().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)
}

Expand Down Expand Up @@ -177,7 +179,16 @@ impl DocumentMetadata {
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(ClickTarget::subpath)
click_targets.iter().filter_map(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => Some(subpath),
_ => None,
})
}

pub fn layer_with_free_points_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &ClickTargetType> {
static EMPTY: Vec<ClickTarget> = 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, ClickTargetType, Quad};
use graphene_std::transform::Footprint;
use graphene_std::vector::{PointId, VectorData, VectorModificationType};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
Expand Down Expand Up @@ -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.,
);
Expand All @@ -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.,
);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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 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()) {
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 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 {
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 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 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);
}
}
}
});
Expand Down Expand Up @@ -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 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);
}
}
}
FrontendClickTargets {
Expand Down Expand Up @@ -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_type))
.map(|target_types| VectorData::from_target_types(target_types, true))
}

/// Loads the structure of layer nodes from a node graph.
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,14 @@ impl SelectedLayerState {
}

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
}
}

Expand Down
6 changes: 5 additions & 1 deletion editor/src/messages/tool/common_functionality/snapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading