Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
37 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
9b2522c
Merge branch 'master' into 2558-free-floating-anchors-overlays
Keavon May 22, 2025
d151899
Merge branch 'master' into 2558-free-floating-anchors-overlays
Keavon May 22, 2025
8202c48
Fix comments
seam0s-dev May 23, 2025
e4f1395
Merge branch 'master' into 2558-free-floating-anchors-overlays
seam0s-dev May 24, 2025
87eb747
Merge branch 'master' into 2558-free-floating-anchors-overlays
seam0s-dev May 25, 2025
139ac6f
Merge branch 'master' into 2558-free-floating-anchors-overlays
seam0s-dev May 28, 2025
55e4d9d
Merge remote-tracking branch 'origin/master' into 2558-free-floating-…
seam0s-dev Jun 8, 2025
1e3e218
Lints
seam0s-dev Jun 8, 2025
4131e1e
Merge branch 'master' into 2558-free-floating-anchors-overlays
Keavon Jun 8, 2025
b9976ff
Code review pass
Keavon Jun 8, 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
30 changes: 24 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, ClickTargetGroup, Quad};
use graphene_std::vector::{PointId, path_bool_lib};
use std::time::Duration;

Expand Down Expand Up @@ -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)
}
})
})
}
Expand Down Expand Up @@ -2733,7 +2740,18 @@ 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(|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()
}
Expand Down
18 changes: 18 additions & 0 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,24 @@ impl OverlayContext {
self.render_context.stroke();
}

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),
);
}
}
}
}

/// 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<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
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, ClickTargetGroup, 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_group() {
ClickTargetGroup::Subpath(subpath) => subpath.bounding_box_with_transform(transform),
ClickTargetGroup::PointGroup(_) => 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(|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 {
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, ClickTargetGroup, 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 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);
}
}
}
});
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 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 {
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_group))
.map(|target_groups| VectorData::from_target_groups(target_groups, 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 @@ -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
}
}

Expand Down
22 changes: 18 additions & 4 deletions editor/src/messages/tool/tool_messages/select_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,12 +527,17 @@ 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), None);
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
overlay_context.outline(document.metadata().layer_outline(layer), layer_to_viewport, None);

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, 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);
}
}
}

Expand Down Expand Up @@ -573,13 +578,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() {
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
let mut hover_overlay_draw = |layer: LayerNodeIdentifier, color: Option<&str>| {
if layer.has_children(document.metadata()) {
if let Some(bounds) = document.metadata().bounding_box_viewport(layer) {
overlay_context.quad(Quad::from_box(bounds), color, None);
}
} else {
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), color);
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);
}
}
};
let layer = match tool_data.nested_selection_behavior {
Expand Down Expand Up @@ -817,7 +826,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), None);
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);
}
}
}

Expand Down
Loading
Loading