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 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -654,6 +654,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
23 changes: 19 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));
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);
}

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,7 +578,12 @@ 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) {
overlay_context.outline_free_floating_anchors(vector_data, layer_to_viewport);
}
}

// Measure with Alt held down
Expand Down Expand Up @@ -786,7 +796,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);
}
}
}

Expand Down
Loading
Loading