Extend open paths from endpoints with the Freehand tool and skip hidden layers in path edits (#4508)

* Let the Freehand tool continue a selected open path from its endpoint and show endpoint overlays in the Freehand and Spline tools

* Share the closest-point search with the open path endpoint lookup, tally endpoint connections in a single pass, and rename the endpoint overlay function

* Skip hidden layers and use the feeds-aware transform in path point searches, path overlays, and Freehand point placement

* Map the Spline tool's merge goal with the feeds-aware transform so it matches the endpoint candidates

* Highlight the hovered open path endpoint in the Freehand and Spline tools
This commit is contained in:
Keavon Chambers
2026-09-06 20:37:49 -07:00
committed by GitHub
parent 97430120f4
commit 5612b63633
9 changed files with 408 additions and 27 deletions

View File

@@ -1,9 +1,10 @@
use super::utility_types::{DrawHandles, OverlayContext};
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::consts::{HIDE_HANDLE_DISTANCE, SNAP_POINT_TOLERANCE};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
pub use crate::messages::portfolio::document::utility_types::text_metrics::text_width;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::common_functionality::utility_functions::closest_open_path_endpoint;
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use glam::{DAffine2, DVec2};
use graphene_std::vector::misc::{BezierHandles, ManipulatorPointId, point_to_dvec2, segment_to_handles};
@@ -132,7 +133,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
let display_handles = overlay_context.visibility_settings.handles();
let display_anchors = overlay_context.visibility_settings.anchors();
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
if display_path {
@@ -201,6 +202,43 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
}
}
/// Draws an anchor overlay at each endpoint of every open path on the selected visible layers, in the selected style for endpoints that are part of the path editing selection.
/// Given a pointer position, the endpoint a press there would continue from is drawn in the hover style instead.
pub fn open_path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &ShapeState, pointer: Option<DVec2>, overlay_context: &mut OverlayContext) {
if !overlay_context.visibility_settings.anchors() {
return;
}
let selected_nodes = document.network_interface.selected_nodes();
let is_selected = |layer: LayerNodeIdentifier, id: PointId| {
shape_editor
.selected_shape_state
.get(&layer)
.is_some_and(|state| state.is_point_selected(ManipulatorPointId::Anchor(id)))
};
let hovered = pointer.and_then(|pointer| closest_open_path_endpoint(document, pointer, SNAP_POINT_TOLERANCE, selected_nodes.selected_visible_layers(&document.network_interface)));
for layer in selected_nodes.selected_visible_layers(&document.network_interface) {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
for id in vector.anchor_endpoints() {
if hovered.is_some_and(|(hovered_layer, hovered_id, _)| hovered_layer == layer && hovered_id == id) {
continue;
}
let Some(position) = vector.point_domain.position_from_id(id) else { continue };
overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(layer, id), None);
}
}
// Drawn last so its halo sits above any other endpoint at the same spot
if let Some((layer, id, position)) = hovered {
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
overlay_context.hover_manipulator_anchor(transform.transform_point2(position), is_selected(layer, id));
}
}
pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] {
let hex = hex.trim().trim_start_matches('#');
if hex.len() != 6 && hex.len() != 8 {

View File

@@ -841,6 +841,13 @@ impl NodeNetworkInterface {
self.query(network_path, "is_visible", |view| view.is_visible(node_id)).unwrap_or_default()
}
/// Whether a layer in the document network is visible, which also requires every ancestor to be visible.
pub fn is_layer_visible(&self, layer: LayerNodeIdentifier) -> bool {
layer
.ancestors(self.document_metadata())
.all(|ancestor| ancestor == LayerNodeIdentifier::ROOT_PARENT || self.is_visible(&ancestor.to_node(), &[]))
}
pub fn is_layer(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
self.query(network_path, "is_layer", |view| view.is_layer(node_id)).unwrap_or_default()
}

View File

@@ -52,13 +52,7 @@ pub struct SelectedNodes(pub Vec<NodeId>);
impl SelectedNodes {
pub fn layer_visible(&self, layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool {
layer.ancestors(network_interface.document_metadata()).all(|layer| {
if layer != LayerNodeIdentifier::ROOT_PARENT {
network_interface.is_visible(&layer.to_node(), &[])
} else {
true
}
})
network_interface.is_layer_visible(layer)
}
pub fn selected_visible_layers<'a>(&'a self, network_interface: &'a NodeNetworkInterface) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {

View File

@@ -23,6 +23,12 @@ pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance:
closest_point(document, goal, tolerance, layers, |_| false)
}
/// Finds the endpoint of an open path closest to the goal (in viewport space) across the given layers, if one lies within the tolerance.
/// Only anchors with a single connected segment qualify, so closed paths are never matched. Returns the endpoint's position in the layer's local space.
pub fn closest_open_path_endpoint(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64, layers: impl Iterator<Item = LayerNodeIdentifier>) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
closest_candidate_point(document, goal, tolerance, layers, |vector| vector.anchor_endpoints().collect())
}
/// Determine the closest point to the goal point under max_distance.
/// Additionally exclude checking closeness to the point which given to exclude() returns true.
pub fn closest_point<T>(
@@ -35,19 +41,32 @@ pub fn closest_point<T>(
where
T: Fn(PointId) -> bool,
{
closest_candidate_point(document, goal, max_distance, layers, |vector| vector.anchor_points().filter(|&id| !exclude(id)).collect())
}
/// Determines the closest of each visible layer's candidate points to the goal (in viewport space) under max_distance. Returns the point's position in the layer's local space.
fn closest_candidate_point(
document: &DocumentMessageHandler,
goal: DVec2,
max_distance: f64,
layers: impl Iterator<Item = LayerNodeIdentifier>,
candidates: impl Fn(&Vector) -> Vec<PointId>,
) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
let mut best = None;
let mut best_distance_squared = max_distance * max_distance;
for layer in layers {
let viewspace = document.metadata().transform_to_viewport(layer);
if !document.network_interface.is_layer_visible(layer) {
continue;
}
let viewspace = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
for id in vector.anchor_points() {
if exclude(id) {
continue;
}
for id in candidates(&vector) {
let Some(point) = vector.point_domain.position_from_id(id) else { continue };
let distance_squared = viewspace.transform_point2(point).distance_squared(goal);
if distance_squared < best_distance_squared {
best = Some((layer, id, point));
best_distance_squared = distance_squared;

View File

@@ -1,5 +1,8 @@
use super::tool_prelude::*;
use crate::consts::SNAP_POINT_TOLERANCE;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_network_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::open_path_endpoint_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::color_selector::{
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, reset_colors_on_deactivation, swap_fill_and_stroke,
@@ -8,6 +11,7 @@ use crate::messages::tool::common_functionality::color_selector::{
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::translation_transform_set;
use crate::messages::tool::common_functionality::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
use crate::messages::tool::common_functionality::utility_functions::closest_open_path_endpoint;
use glam::DVec2;
use graph_craft::document::NodeId;
use graphene_std::Color;
@@ -39,6 +43,7 @@ impl Default for FreehandOptions {
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum FreehandToolMessage {
// Standard messages
Overlays { context: OverlayContext },
Abort,
SelectionChanged,
WorkingColorChanged,
@@ -196,6 +201,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
FreehandToolFsmState::Ready => actions!(FreehandToolMessageDiscriminant;
DragStart,
DragStop,
PointerMove,
),
FreehandToolFsmState::Drawing => actions!(FreehandToolMessageDiscriminant;
DragStop,
@@ -209,6 +215,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
impl ToolTransition for FreehandTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
tool_abort: Some(FreehandToolMessage::Abort.into()),
selection_changed: Some(FreehandToolMessage::SelectionChanged.into()),
graph_changed: Some(FreehandToolMessage::SelectionChanged.into()),
@@ -240,10 +247,25 @@ impl Fsm for FreehandToolFsmState {
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let ToolActionMessageContext { document, input, viewport, .. } = tool_action_data;
let ToolActionMessageContext {
document,
input,
viewport,
shape_editor,
..
} = tool_action_data;
let ToolMessage::Freehand(event) = event else { return self };
match (self, event) {
(_, FreehandToolMessage::Overlays { context: mut overlay_context }) => {
let pointer = (self == FreehandToolFsmState::Ready).then_some(input.mouse.position);
open_path_endpoint_overlays(document, shape_editor, pointer, &mut overlay_context);
self
}
(FreehandToolFsmState::Ready, FreehandToolMessage::PointerMove) => {
responses.add(OverlaysMessage::Draw);
self
}
(FreehandToolFsmState::Ready, FreehandToolMessage::DragStart { append_to_selected }) => {
responses.add(DocumentMessage::StartTransaction);
@@ -251,14 +273,25 @@ impl Fsm for FreehandToolFsmState {
tool_data.end_point = None;
tool_data.new_layer_viewport_start = None;
// Pressing on the endpoint of a selected open path continues that path in place, keeping the layer, its selection, and its style
let selected_nodes = document.network_interface.selected_nodes();
let selected_visible_layers = selected_nodes.selected_visible_layers(&document.network_interface);
if let Some((layer, endpoint, position)) = closest_open_path_endpoint(document, input.mouse.position, SNAP_POINT_TOLERANCE, selected_visible_layers) {
tool_data.layer = Some(layer);
tool_data.end_point = Some((position, endpoint));
return FreehandToolFsmState::Drawing;
}
if input.keyboard.key(append_to_selected) {
let selected_nodes = document.network_interface.selected_nodes();
let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface);
let existing_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none());
let mut appendable_layers = selected_nodes
.selected_visible_layers(&document.network_interface)
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
let existing_layer = appendable_layers.next().filter(|_| appendable_layers.next().is_none());
if let Some(layer) = existing_layer {
tool_data.layer = Some(layer);
let transform = document.metadata().transform_to_viewport(layer);
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
let position = transform.inverse().transform_point2(input.mouse.position);
extend_path_with_next_segment(tool_data, position, false, responses);
@@ -291,7 +324,7 @@ impl Fsm for FreehandToolFsmState {
}
(FreehandToolFsmState::Drawing, FreehandToolMessage::PointerMove) => {
if let Some(layer) = tool_data.layer {
let transform = document.metadata().transform_to_viewport(layer);
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
// For newly created layers, the deferred TransformSet may not yet be reflected
// in the metadata, so compute local position from the known viewport start.
@@ -401,7 +434,7 @@ mod test_freehand {
use crate::messages::tool::common_functionality::stroke_options::StrokeOptionsUpdate;
use crate::messages::tool::tool_messages::freehand_tool::FreehandOptionsUpdate;
use crate::test_utils::test_prelude::*;
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DMat2, DVec2};
use graphene_std::vector::Vector;
async fn get_vector_and_transform_list(editor: &mut EditorTestUtils) -> Vec<(Vector, DAffine2)> {
@@ -651,4 +684,274 @@ mod test_freehand {
stroke_width.unwrap()
);
}
/// Draws a Freehand stroke through the given viewport positions and returns the layer it created, which the tool leaves selected.
async fn draw_freehand_stroke(editor: &mut EditorTestUtils, points: &[DVec2]) -> LayerNodeIdentifier {
editor.select_tool(ToolType::Freehand).await;
editor.drag_path(points, ModifierKeys::empty()).await;
editor.get_selected_layer().await.expect("The Freehand stroke should create and select a layer")
}
fn layer_count(editor: &EditorTestUtils) -> usize {
editor.active_document().metadata().all_layers().count()
}
fn point_and_segment_counts(editor: &EditorTestUtils, layer: LayerNodeIdentifier) -> (usize, usize) {
let vector = editor.active_document().network_interface.compute_modified_vector(layer).expect("Layer should have vector geometry");
(vector.point_domain.ids().len(), vector.segment_domain.ids().len())
}
/// The viewport positions of the layer's anchors, split into the endpoints of its open paths and every other anchor.
fn endpoint_and_other_anchor_viewport_positions(editor: &EditorTestUtils, layer: LayerNodeIdentifier) -> (Vec<DVec2>, Vec<DVec2>) {
let document = editor.active_document();
let vector = document.network_interface.compute_modified_vector(layer).expect("Layer should have vector geometry");
let transform = document.metadata().transform_to_viewport(layer);
let endpoints: Vec<_> = vector.anchor_endpoints().collect();
let viewport_position = |id| vector.point_domain.position_from_id(id).map(|position| transform.transform_point2(position));
let endpoint_positions = endpoints.iter().filter_map(|&id| viewport_position(id)).collect();
let other_positions = vector.anchor_points().filter(|id| !endpoints.contains(id)).filter_map(viewport_position).collect();
(endpoint_positions, other_positions)
}
const INITIAL_STROKE: [DVec2; 3] = [DVec2::new(100., 100.), DVec2::new(200., 150.), DVec2::new(300., 100.)];
#[tokio::test]
async fn test_extend_open_path_from_endpoint() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = draw_freehand_stroke(&mut editor, &INITIAL_STROKE).await;
let (initial_point_count, initial_segment_count) = point_and_segment_counts(&editor, layer);
assert_eq!(initial_segment_count, initial_point_count - 1, "The initial stroke should be a single open path");
// Press just inside the snapping tolerance of an endpoint, then keep drawing
let (endpoints, _) = endpoint_and_other_anchor_viewport_positions(&editor, layer);
let endpoint = *endpoints.last().expect("The stroke should have endpoints");
let continuation = [endpoint + DVec2::new(2., -2.), DVec2::new(400., 150.), DVec2::new(500., 100.)];
editor.drag_path(&continuation, ModifierKeys::empty()).await;
assert_eq!(layer_count(&editor), 1, "Continuing from an endpoint should not create a new layer");
assert_eq!(editor.get_selected_layer().await, Some(layer), "The selection should be left as it was");
let (point_count, segment_count) = point_and_segment_counts(&editor, layer);
assert_eq!(point_count, initial_point_count + continuation.len() - 1, "Each pointer move after the press should add a point");
assert_eq!(segment_count, point_count - 1, "The continued stroke should form a single open path");
}
#[tokio::test]
async fn test_stroke_away_from_endpoint_creates_new_layer() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = draw_freehand_stroke(&mut editor, &INITIAL_STROKE).await;
let initial_counts = point_and_segment_counts(&editor, layer);
editor.drag_path(&[DVec2::new(100., 300.), DVec2::new(200., 350.), DVec2::new(300., 300.)], ModifierKeys::empty()).await;
assert_eq!(layer_count(&editor), 2, "A stroke starting away from any endpoint should create a new layer");
assert_eq!(point_and_segment_counts(&editor, layer), initial_counts, "The existing path should be untouched");
}
#[tokio::test]
async fn test_middle_anchor_does_not_extend() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
// Four pointer positions capture three points, so the path has an anchor that is not an endpoint
let layer = draw_freehand_stroke(&mut editor, &[DVec2::new(100., 100.), DVec2::new(200., 150.), DVec2::new(300., 100.), DVec2::new(400., 150.)]).await;
let initial_counts = point_and_segment_counts(&editor, layer);
let (_, other_anchors) = endpoint_and_other_anchor_viewport_positions(&editor, layer);
let middle_anchor = *other_anchors.first().expect("The stroke should have an anchor between its endpoints");
editor
.drag_path(&[middle_anchor, middle_anchor + DVec2::new(0., 100.), middle_anchor + DVec2::new(0., 200.)], ModifierKeys::empty())
.await;
assert_eq!(layer_count(&editor), 2, "Only the endpoints of a path should be continued from");
assert_eq!(point_and_segment_counts(&editor, layer), initial_counts, "The existing path should be untouched");
}
#[tokio::test]
async fn test_closed_path_never_extends() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.draw_rect(100., 100., 300., 300.).await;
let rectangle = editor.get_selected_layer().await.expect("The rectangle should be selected");
let (endpoints, corners) = endpoint_and_other_anchor_viewport_positions(&editor, rectangle);
assert!(endpoints.is_empty(), "A closed path has no endpoints");
let corner = *corners.first().expect("The rectangle should have corner anchors");
editor.select_tool(ToolType::Freehand).await;
editor.drag_path(&[corner, corner + DVec2::new(100., 50.), corner + DVec2::new(200., 0.)], ModifierKeys::empty()).await;
assert_eq!(layer_count(&editor), 2, "Pressing on a closed path's anchor should start a new layer");
}
#[tokio::test]
async fn test_extend_after_layer_transform() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = draw_freehand_stroke(&mut editor, &INITIAL_STROKE).await;
let (initial_point_count, _) = point_and_segment_counts(&editor, layer);
editor
.handle_message(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(40., -25.)),
transform_in: TransformIn::Local,
skip_rerender: false,
})
.await;
let transform = editor.active_document().metadata().transform_to_viewport(layer);
assert!(!transform.matrix2.abs_diff_eq(DMat2::IDENTITY, 1e-6), "The layer should be rotated and scaled");
// Press within tolerance of where an endpoint now sits in the viewport, then keep drawing
let (endpoints, _) = endpoint_and_other_anchor_viewport_positions(&editor, layer);
let endpoint = *endpoints.first().expect("The stroke should have endpoints");
let continuation = [endpoint + DVec2::new(-2., 2.), endpoint + DVec2::new(80., 60.), endpoint + DVec2::new(160., 20.)];
editor.drag_path(&continuation, ModifierKeys::empty()).await;
assert_eq!(layer_count(&editor), 1, "Continuing from a transformed layer's endpoint should not create a new layer");
let (point_count, segment_count) = point_and_segment_counts(&editor, layer);
assert_eq!(point_count, initial_point_count + continuation.len() - 1, "Each pointer move after the press should add a point");
assert_eq!(segment_count, point_count - 1, "The continued stroke should form a single open path");
// The new points are stored in the layer's local space, so they should sit under the pointer once transformed back to the viewport
let document = editor.active_document();
let vector = document.network_interface.compute_modified_vector(layer).expect("Layer should have vector geometry");
let transform = document.metadata().transform_to_viewport(layer);
for &pointer_position in &continuation[1..] {
let under_pointer = vector
.point_domain
.positions()
.iter()
.any(|&position| transform.transform_point2(position).distance(pointer_position) < 1.);
assert!(under_pointer, "Expected a point under the pointer at {pointer_position:?}");
}
}
#[tokio::test]
async fn test_endpoint_takes_priority_over_shift_append() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = draw_freehand_stroke(&mut editor, &INITIAL_STROKE).await;
let (initial_point_count, _) = point_and_segment_counts(&editor, layer);
let (endpoints, _) = endpoint_and_other_anchor_viewport_positions(&editor, layer);
let endpoint = *endpoints.last().expect("The stroke should have endpoints");
let continuation = [endpoint + DVec2::new(2., 2.), DVec2::new(400., 150.), DVec2::new(500., 100.)];
editor.drag_path(&continuation, ModifierKeys::SHIFT).await;
assert_eq!(layer_count(&editor), 1, "Shift should keep drawing on the selected layer");
// Appending a disconnected subpath would leave the layer with one fewer segment than a single continued path has
let (point_count, segment_count) = point_and_segment_counts(&editor, layer);
assert_eq!(point_count, initial_point_count + continuation.len() - 1, "Each pointer move after the press should add a point");
assert_eq!(segment_count, point_count - 1, "Shift on an endpoint should continue the path rather than append a new subpath");
}
#[tokio::test]
async fn test_press_and_release_on_endpoint_leaves_path_unchanged() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = draw_freehand_stroke(&mut editor, &INITIAL_STROKE).await;
let initial_counts = point_and_segment_counts(&editor, layer);
let (endpoints, _) = endpoint_and_other_anchor_viewport_positions(&editor, layer);
let endpoint = *endpoints.last().expect("The stroke should have endpoints");
editor.move_mouse(endpoint.x, endpoint.y, ModifierKeys::empty(), MouseKeys::empty()).await;
editor.left_mousedown(endpoint.x, endpoint.y, ModifierKeys::empty()).await;
editor.left_mouseup(endpoint.x, endpoint.y, ModifierKeys::empty()).await;
assert_eq!(layer_count(&editor), 1, "Releasing without moving should not create a layer");
assert_eq!(point_and_segment_counts(&editor, layer), initial_counts, "Releasing without moving should leave the path unchanged");
}
#[tokio::test]
async fn test_hidden_layer_endpoint_is_not_extended() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = draw_freehand_stroke(&mut editor, &INITIAL_STROKE).await;
let initial_counts = point_and_segment_counts(&editor, layer);
let (endpoints, _) = endpoint_and_other_anchor_viewport_positions(&editor, layer);
let endpoint = *endpoints.first().expect("The stroke should have endpoints");
// Where the hidden layer's endpoint would land without its transform
let metadata = editor.active_document().metadata();
let untransformed_endpoint = metadata
.document_to_viewport
.transform_point2(metadata.transform_to_viewport(layer).inverse().transform_point2(endpoint));
editor
.handle_message(NodeGraphMessage::ToggleVisibility {
node_id: layer.to_node(),
network_path: Vec::new(),
})
.await;
assert_eq!(editor.get_selected_layer().await, Some(layer), "Hiding the layer should leave it selected");
editor
.drag_path(&[endpoint + DVec2::new(2., -2.), DVec2::new(400., 150.), DVec2::new(500., 100.)], ModifierKeys::empty())
.await;
assert_eq!(layer_count(&editor), 2, "A stroke at a hidden layer's endpoint should create a new layer");
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
editor
.drag_path(
&[untransformed_endpoint, untransformed_endpoint + DVec2::new(100., 50.), untransformed_endpoint + DVec2::new(200., 0.)],
ModifierKeys::empty(),
)
.await;
assert_eq!(
layer_count(&editor),
3,
"A stroke where the hidden layer's untransformed endpoint would sit should also create a new layer"
);
editor
.handle_message(NodeGraphMessage::ToggleVisibility {
node_id: layer.to_node(),
network_path: Vec::new(),
})
.await;
assert_eq!(point_and_segment_counts(&editor, layer), initial_counts, "The hidden layer should be untouched");
}
#[tokio::test]
async fn test_locked_layer_endpoint_is_extended() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = draw_freehand_stroke(&mut editor, &INITIAL_STROKE).await;
let (initial_point_count, _) = point_and_segment_counts(&editor, layer);
let (endpoints, _) = endpoint_and_other_anchor_viewport_positions(&editor, layer);
let endpoint = *endpoints.last().expect("The stroke should have endpoints");
editor
.handle_message(NodeGraphMessage::ToggleLocked {
node_id: layer.to_node(),
network_path: Vec::new(),
})
.await;
assert_eq!(editor.get_selected_layer().await, Some(layer), "Locking the layer should leave it selected");
let continuation = [endpoint + DVec2::new(2., -2.), DVec2::new(400., 150.), DVec2::new(500., 100.)];
editor.drag_path(&continuation, ModifierKeys::empty()).await;
// Locking only blocks viewport picking by the Select tool, so a selected locked layer stays editable
assert_eq!(layer_count(&editor), 1, "A selected locked layer's endpoint should still be continued from");
let (point_count, segment_count) = point_and_segment_counts(&editor, layer);
assert_eq!(point_count, initial_point_count + continuation.len() - 1, "Each pointer move after the press should add a point");
assert_eq!(segment_count, point_count - 1, "The continued stroke should form a single open path");
}
}

View File

@@ -1587,7 +1587,7 @@ impl Fsm for PathToolFsmState {
match (self, event) {
(_, PathToolMessage::SelectionChanged) => {
// Set the newly targeted layers to visible
let target_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect();
let target_layers = document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface).collect();
shape_editor.set_selected_layers(target_layers);

View File

@@ -2,6 +2,7 @@ use super::tool_prelude::*;
use crate::consts::{DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE};
use crate::messages::input_mapper::utility_types::pointer::MouseKeys;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{resolve_network_node_type, resolve_proto_node_type};
use crate::messages::portfolio::document::overlays::utility_functions::open_path_endpoint_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -302,12 +303,20 @@ impl Fsm for SplineToolFsmState {
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let ToolActionMessageContext { document, input, viewport, .. } = tool_action_data;
let ToolActionMessageContext {
document,
input,
viewport,
shape_editor,
..
} = tool_action_data;
let ToolMessage::Spline(event) = event else { return self };
match (self, event) {
(_, SplineToolMessage::CanvasTransformed) => self,
(_, SplineToolMessage::Overlays { context: mut overlay_context }) => {
let pointer = (self == SplineToolFsmState::Ready).then_some(input.mouse.position);
open_path_endpoint_overlays(document, shape_editor, pointer, &mut overlay_context);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input, viewport), &mut overlay_context);
self
}
@@ -565,7 +574,10 @@ fn try_merging_lastest_endpoint(document: &DocumentMessageHandler, tool_data: &m
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
let exclude = |p: PointId| preview_point.is_some_and(|pp| pp == p) || *last_endpoint == p;
let position = document.metadata().transform_to_viewport(current_layer).transform_point2(*last_endpoint_position);
let position = document
.metadata()
.transform_to_viewport_if_feeds(current_layer, &document.network_interface)
.transform_point2(*last_endpoint_position);
let (layer, endpoint, _) = closest_point(document, position, PATH_JOIN_THRESHOLD, layers, exclude)?;
tool_data.merge_layers.insert(layer);

View File

@@ -584,7 +584,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
self.mouse_position = input.mouse.position;
}
TransformLayerMessage::SelectionChanged => {
let target_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect();
let target_layers = document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface).collect();
shape_editor.set_selected_layers(target_layers);
}
TransformLayerMessage::TypeBackspace => {