Skip to content

Ctrl-drag to pull zero-length handles with angle locking #2620

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

Conversation

0SlowPoke0
Copy link
Collaborator

@0SlowPoke0 0SlowPoke0 commented Apr 25, 2025

Partially closes #1870

Ctrl-drag behavior for anchors (new since video was recorded):

  • If Ctrl is pressed before dragging begins, and the selected anchor has exactly one zero-length handle (i.e., one handle exists and the other doesn’t), dragging will pull out the zero-length handle, making it colinear with the opposite non-zero-length handle. The new handle will be locked to that angle during the drag.
  • This only applies to anchors with exactly two handles, where one is zero-length.
  • In all other cases, the Ctrl key is ignored and the anchor is dragged normally.
  • Additionally, if a non-endpoint anchor has no handles (both are zero-length), and Ctrl + Alt are both pressed before dragging, the handle being pulled out is locked to the angle of the line perpendicular to the average of the two tangents extending from that anchor to its adjacent curves.
    Reference Image

also fixes

If a single anchor is double clicked, and it has only one handle (the other is zero-length or doesn't exist), currently this converts it to sharp by removing the handle. But we want that to make the new handle colinear and extend it to an equal length as the present handle. Only if both handles are present, should double-clicking remove both handles like it does currently in that situation. And if both handles are gone, it should also do what it does now by restoring them as colinear.

@Keavon
Copy link
Member

Keavon commented Apr 27, 2025

!build

Copy link

📦 Build Complete for a2f96b1
https://8779b2aa.graphite.pages.dev

@Keavon
Copy link
Member

Keavon commented Apr 27, 2025

This seems to happen occasionally, where it changes angle:

capture_97_.mp4

It seems unreliable to replicate but I've done it a few times. Each time, it was from newly drawing a Pen tool layer, using the Path tool to select and Delete a handle, then upon the first attempt (as shown in the video) it would sometimes switch to a different angle (maybe due to a conflict with snapping?). Can you try and investigate further? Thanks.

@Keavon Keavon marked this pull request as draft April 29, 2025 22:53
@0SlowPoke0 0SlowPoke0 marked this pull request as ready for review May 13, 2025 06:03
@Keavon
Copy link
Member

Keavon commented May 13, 2025

!build

Copy link

📦 Build Complete for 0aa6a11
https://1d7e2f81.graphite.pages.dev

@Keavon
Copy link
Member

Keavon commented May 13, 2025

If a single anchor is double clicked, and it has only one handle (the other is zero-length or doesn't exist), currently this converts it to sharp by removing the handle. But we want that to make the new handle colinear and extend it to an equal length as the present handle. Only if both handles are present, should double-clicking remove both handles like it does currently in that situation. And if both handles are gone, it should also do what it does now by restoring them as colinear.

This isn't working:

capture_10_.mp4

@Keavon
Copy link
Member

Keavon commented May 13, 2025

This is probably related to the previously reported issue, but I'm now noticing it also occurs even on regular two-handle colinear manipulator groups. Ctrl-dragging a handle rotates the handle pair when it should just lock the angle like it does in master.

capture_11_.mp4

Here's that layer:

graphite/layer: [{"nodes":[[0,{"document_node":{"inputs":[{"Value":{"tagged_value":{"GraphicGroup":{"instance":[],"transform":[],"alpha_blending":[],"source_node_id":[]}},"exposed":true}},{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"manual_composition":null,"implementation":{"Network":{"exports":[{"Node":{"node_id":3,"output_index":0,"lambda":false}}],"nodes":[[0,{"inputs":[{"Network":{"import_type":{"Generic":"T"},"import_index":1}}],"manual_composition":{"Generic":"T"},"implementation":{"ProtoNode":{"name":"graphene_core::graphic_element::ToElementNode"}},"visible":true,"skip_deduplication":false}],[3,{"inputs":[{"Node":{"node_id":1,"output_index":0,"lambda":false}},{"Node":{"node_id":2,"output_index":0,"lambda":false}},{"Reflection":"DocumentNodePath"}],"manual_composition":{"Generic":"T"},"implementation":{"ProtoNode":{"name":"graphene_core::graphic_element::LayerNode"}},"visible":true,"skip_deduplication":false}],[2,{"inputs":[{"Node":{"node_id":0,"output_index":0,"lambda":false}}],"manual_composition":{"Generic":"T"},"implementation":{"ProtoNode":{"name":"graphene_core::memo::MonitorNode"}},"visible":true,"skip_deduplication":true}],[1,{"inputs":[{"Network":{"import_type":{"Generic":"T"},"import_index":0}}],"manual_composition":{"Generic":"T"},"implementation":{"ProtoNode":{"name":"graphene_core::graphic_element::ToGroupNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]}},"visible":true,"skip_deduplication":false},"persistent_node_metadata":{"reference":"Merge","display_name":"","input_properties":[{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null}],"output_names":["Out"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Layer":{"position":{"Absolute":[-6,5]}}},"network_metadata":{"persistent_metadata":{"node_metadata":[[1,{"persistent_metadata":{"reference":null,"display_name":"To Group","input_properties":[{"input_data":{},"widget_override":null}],"output_names":[],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[-14,-3]}}},"network_metadata":null}}],[0,{"persistent_metadata":{"reference":null,"display_name":"To Element","input_properties":[{"input_data":{},"widget_override":null}],"output_names":[],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[-14,-1]}}},"network_metadata":null}}],[2,{"persistent_metadata":{"reference":null,"display_name":"Monitor","input_properties":[{"input_data":{},"widget_override":null}],"output_names":[],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[-7,-1]}}},"network_metadata":null}}],[3,{"persistent_metadata":{"reference":null,"display_name":"Layer","input_properties":[{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null}],"output_names":[],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[1,-3]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}}}],[3,{"document_node":{"inputs":[{"Value":{"tagged_value":{"VectorData":{"instance":[{"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":0.0,"dash_lengths":[],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0,"transform":[1.0,0.0,0.0,1.0,0.0,0.0],"non_scaling":false},"fill":"None"},"colinear_manipulators":[],"point_domain":{"id":[],"position":[]},"segment_domain":{"id":[],"start_point":[],"end_point":[],"handles":[],"stroke":[]},"region_domain":{"id":[],"segment_range":[],"fill":[]},"upstream_graphic_group":null}],"transform":[[1.0,0.0,0.0,1.0,0.0,0.0]],"alpha_blending":[{"opacity":1.0,"blend_mode":"Normal"}],"source_node_id":[null]}},"exposed":true}},{"Value":{"tagged_value":{"VectorModification":{"points":{"add":[12329518315244532894,13460809662442519794,3021445137031832196,151564135377924366,2270819100019773377,13012049054736123441],"remove":[],"delta":[[13460809662442519794,[602.0,585.0]],[151564135377924366,[995.0,599.0]],[12329518315244532894,[532.0,760.0]],[13012049054736123441,[988.0,725.0]],[2270819100019773377,[1146.0,734.0]],[3021445137031832196,[907.0,486.0]]]},"segments":{"add":[10691771554992190075,14444844282311828821,15546923750390961614,17623234208566839480,12997061673947707185],"remove":[],"start_point":[[14444844282311828821,13460809662442519794],[17623234208566839480,151564135377924366],[12997061673947707185,2270819100019773377],[15546923750390961614,3021445137031832196],[10691771554992190075,12329518315244532894]],"end_point":[[12997061673947707185,13012049054736123441],[15546923750390961614,151564135377924366],[10691771554992190075,13460809662442519794],[14444844282311828821,3021445137031832196],[17623234208566839480,2270819100019773377]],"handle_primary":[[12997061673947707185,[-128.0,84.0]],[14444844282311828821,[135.0,27.0]],[15546923750390961614,[0.0,0.0]],[17623234208566839480,[230.00000000000003,-94.99999999999994]],[10691771554992190075,[0.0,0.0]]],"handle_end":[[17623234208566839480,[128.0,-84.0]],[15546923750390961614,[-146.02260799354053,60.3136859103754]],[10691771554992190075,[-135.0,-27.0]],[12997061673947707185,[0.0,0.0]],[14444844282311828821,[0.0,0.0]]],"stroke":[[17623234208566839480,0],[15546923750390961614,0],[10691771554992190075,0],[12997061673947707185,0],[14444844282311828821,0]]},"regions":{"add":[],"remove":[],"segment_range":[],"fill":[]},"add_g1_continuous":[[{"ty":"End","segment":10691771554992190075},{"ty":"Primary","segment":14444844282311828821}],[{"ty":"End","segment":15546923750390961614},{"ty":"Primary","segment":17623234208566839480}],[{"ty":"End","segment":17623234208566839480},{"ty":"Primary","segment":12997061673947707185}]],"remove_g1_continuous":[[{"ty":"End","segment":14444844282311828821},{"ty":"Primary","segment":15546923750390961614}]]}},"exposed":false}}],"manual_composition":null,"implementation":{"Network":{"exports":[{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"nodes":[[0,{"inputs":[{"Network":{"import_type":{"Concrete":{"name":"graphene_core::instances::Instances<graphene_core::vector::vector_data::VectorData>","alias":null}},"import_index":0}}],"manual_composition":{"Generic":"T"},"implementation":{"ProtoNode":{"name":"graphene_core::memo::MonitorNode"}},"visible":true,"skip_deduplication":true}],[1,{"inputs":[{"Node":{"node_id":0,"output_index":0,"lambda":false}},{"Network":{"import_type":{"Concrete":{"name":"graphene_core::vector::vector_data::modification::VectorModification","alias":null}},"import_index":1}}],"manual_composition":{"Generic":"T"},"implementation":{"ProtoNode":{"name":"graphene_core::vector::vector_data::modification::PathModifyNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]}},"visible":true,"skip_deduplication":false},"persistent_node_metadata":{"reference":"Path","display_name":"","input_properties":[{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null}],"output_names":["Vector Data"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":"Chain"}},"network_metadata":{"persistent_metadata":{"node_metadata":[[0,{"persistent_metadata":{"reference":null,"display_name":"Monitor","input_properties":[{"input_data":{},"widget_override":null}],"output_names":[],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[0,0]}}},"network_metadata":null}}],[1,{"persistent_metadata":{"reference":null,"display_name":"Path Modify","input_properties":[{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null}],"output_names":[],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[7,0]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}}}],[1,{"document_node":{"inputs":[{"Node":{"node_id":2,"output_index":0,"lambda":false}},{"Value":{"tagged_value":{"OptionalColor":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}},"exposed":false}},{"Value":{"tagged_value":{"F64":2.0},"exposed":false}},{"Value":{"tagged_value":{"VecF64":[]},"exposed":false}},{"Value":{"tagged_value":{"F64":0.0},"exposed":false}},{"Value":{"tagged_value":{"LineCap":"Butt"},"exposed":false}},{"Value":{"tagged_value":{"LineJoin":"Miter"},"exposed":false}},{"Value":{"tagged_value":{"F64":4.0},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::StrokeNode"}},"visible":true,"skip_deduplication":false},"persistent_node_metadata":{"reference":"Stroke","display_name":"","input_properties":[{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null}],"output_names":["Future<Instances<VectorData>>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":"Chain"}},"network_metadata":null}}],[2,{"document_node":{"inputs":[{"Node":{"node_id":3,"output_index":0,"lambda":false}},{"Value":{"tagged_value":{"Fill":{"Solid":{"red":0.99999994,"green":0.99999994,"blue":0.99999994,"alpha":1.0}}},"exposed":false}},{"Value":{"tagged_value":{"OptionalColor":{"red":0.99999994,"green":0.99999994,"blue":0.99999994,"alpha":1.0}},"exposed":false}},{"Value":{"tagged_value":{"Gradient":{"stops":[[0.0,{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}],[1.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]],"gradient_type":"Linear","start":[0.0,0.5],"end":[1.0,0.5],"transform":[1.0,0.0,0.0,1.0,0.0,0.0]}},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::FillNode"}},"visible":true,"skip_deduplication":false},"persistent_node_metadata":{"reference":"Fill","display_name":"","input_properties":[{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null},{"input_data":{},"widget_override":null}],"output_names":["Future<Instances<VectorData>>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":"Chain"}},"network_metadata":null}}]],"selected":true,"visible":true,"locked":false,"collapsed":false}]

@Keavon Keavon marked this pull request as draft May 13, 2025 18:48
@0SlowPoke0 0SlowPoke0 marked this pull request as ready for review May 14, 2025 20:00
@Keavon
Copy link
Member

Keavon commented May 14, 2025

!build

Copy link

📦 Build Complete for eaaa719
https://1bc67b66.graphite.pages.dev

@Keavon
Copy link
Member

Keavon commented May 14, 2025

For Ctrl+Alt, this is off by 90° (it's the tangent not the normal):

capture_13_.mp4

To more clearly illustrate the original diagram in the PR description by adding color-coding, we want the blue not the green:

capture

@Keavon
Copy link
Member

Keavon commented May 14, 2025

!build

Copy link

📦 Build Complete for 8bf6cb0
https://ca169308.graphite.pages.dev

} else {
// Push both in and out handles into the correct position
for ((handle, sign), other_anchor) in handles.iter().zip([1., -1.]).zip(&anchor_positions) {
// To find the length of the new tangent we just take the distance to the anchor and divide by 3 (pretty arbitrary)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What changes with different values than 3? I don't see what part of this feature relies on an arbitrary constant.

@Keavon
Copy link
Member

Keavon commented May 17, 2025

!build

Copy link

📦 Build Complete for 6305d0a
https://f95b4d39.graphite.pages.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Tracking Issue: Pen and Path tool improvements
2 participants