From e405858bc7de848644d2f87a0920e0cd6732c904 Mon Sep 17 00:00:00 2001 From: Chetan Sahney Date: Sat, 14 Mar 2026 11:31:52 +0530 Subject: [PATCH 1/6] fix: populate colinear_manipulators for Circle, Arc, and Spiral (#3891) --- .../nodes/vector/src/generator_nodes.rs | 100 +++++++++++++----- 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 66c8009e02..27d5949bb6 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -42,42 +42,69 @@ impl CornerRadius for [f64; 4] { /// Generates a circle shape with a chosen radius. #[node_macro::node(category("Vector: Shape"))] fn circle( - _: impl Ctx, - _primary: (), - #[unit(" px")] - #[default(50.)] - radius: f64, + _: impl Ctx, + _primary: (), + #[unit(" px")] + #[default(50.)] + radius: f64, ) -> Table { - let radius = radius.abs(); - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius)))) + let radius = radius.abs(); + // 1. Create the vector + let mut circle = Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))); + + // 2. Register the 4 pairs of colinear handles + let len = circle.segment_domain.ids().len(); + for i in 0..len { + circle.colinear_manipulators.push([ + HandleId::end(circle.segment_domain.ids()[i]), + HandleId::primary(circle.segment_domain.ids()[(i + 1) % len]) + ]); + } + + Table::new_from_element(circle) } /// Generates an arc shape forming a portion of a circle which may be open, closed, or a pie slice. #[node_macro::node(category("Vector: Shape"))] fn arc( - _: impl Ctx, - _primary: (), - #[unit(" px")] - #[default(50.)] - radius: f64, - start_angle: Angle, - #[default(270.)] - #[range((0., 360.))] - sweep_angle: Angle, - arc_type: ArcType, + _: impl Ctx, + _primary: (), + #[unit(" px")] + #[default(50.)] + radius: f64, + start_angle: Angle, + #[default(270.)] + #[range((0., 360.))] + sweep_angle: Angle, + arc_type: ArcType, ) -> Table { - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_arc( - radius, - start_angle / 360. * std::f64::consts::TAU, - sweep_angle / 360. * std::f64::consts::TAU, - match arc_type { - ArcType::Open => subpath::ArcType::Open, - ArcType::Closed => subpath::ArcType::Closed, - ArcType::PieSlice => subpath::ArcType::PieSlice, - }, - ))) + // 1. Create the arc vector + let mut arc_vector = Vector::from_subpath(subpath::Subpath::new_arc( + radius, + start_angle / 360. * std::f64::consts::TAU, + sweep_angle / 360. * std::f64::consts::TAU, + match arc_type { + ArcType::Open => subpath::ArcType::Open, + ArcType::Closed => subpath::ArcType::Closed, + ArcType::PieSlice => subpath::ArcType::PieSlice, + }, + )); + + // 2. Link handles only if both adjacent segments are cubic beziers + let len = arc_vector.segment_domain.ids().len(); + for i in 0..len.saturating_sub(1) { + if arc_vector.segment_domain.handles()[i].is_cubic() && arc_vector.segment_domain.handles()[i + 1].is_cubic() { + arc_vector.colinear_manipulators.push([ + HandleId::end(arc_vector.segment_domain.ids()[i]), + HandleId::primary(arc_vector.segment_domain.ids()[i + 1]) + ]); + } + } + + Table::new_from_element(arc_vector) } +/// Generates a spiral shape that winds from an inner to an outer radius. /// Generates a spiral shape that winds from an inner to an outer radius. #[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))] fn spiral( @@ -90,14 +117,29 @@ fn spiral( #[default(25)] outer_radius: f64, #[default(90.)] angular_resolution: f64, ) -> Table { - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_spiral( + // 1. Create the spiral vector + let mut spiral_vector = Vector::from_subpath(subpath::Subpath::new_spiral( inner_radius, outer_radius, turns, start_angle.to_radians(), angular_resolution.to_radians(), spiral_type, - ))) + )); + + // 2. NEW: Link consecutive curved segments + let len = spiral_vector.segment_domain.ids().len(); + for i in 0..len.saturating_sub(1) { + // Ensure both segments meeting at the anchor point are cubic beziers + if spiral_vector.segment_domain.handles()[i].is_cubic() && spiral_vector.segment_domain.handles()[i + 1].is_cubic() { + spiral_vector.colinear_manipulators.push([ + HandleId::end(spiral_vector.segment_domain.ids()[i]), + HandleId::primary(spiral_vector.segment_domain.ids()[i + 1]) + ]); + } + } + + Table::new_from_element(spiral_vector) } /// Generates an ellipse shape (an oval or stretched circle) with the chosen radii. From 9c40108717482d001c32c5209e30fbb5bf10d321 Mon Sep 17 00:00:00 2001 From: Chetan Sahney <84283700+Chetansahney@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:28:19 +0530 Subject: [PATCH 2/6] Update node-graph/nodes/vector/src/generator_nodes.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- node-graph/nodes/vector/src/generator_nodes.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 27d5949bb6..ce5a90b430 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -53,11 +53,13 @@ fn circle( let mut circle = Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))); // 2. Register the 4 pairs of colinear handles - let len = circle.segment_domain.ids().len(); + // 2. Register the 4 pairs of colinear handles + let ids = circle.segment_domain.ids(); + let len = ids.len(); for i in 0..len { circle.colinear_manipulators.push([ - HandleId::end(circle.segment_domain.ids()[i]), - HandleId::primary(circle.segment_domain.ids()[(i + 1) % len]) + HandleId::end(ids[i]), + HandleId::primary(ids[(i + 1) % len]), ]); } From abe8f7f41f2bbd6ea89b344d0d20f7da72bfe8d6 Mon Sep 17 00:00:00 2001 From: Chetan Sahney Date: Sat, 14 Mar 2026 14:47:27 +0530 Subject: [PATCH 3/6] cleaned up code(Removed extra lines) --- node-graph/nodes/vector/src/generator_nodes.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 27d5949bb6..fd3d7da86b 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -104,7 +104,6 @@ fn arc( Table::new_from_element(arc_vector) } -/// Generates a spiral shape that winds from an inner to an outer radius. /// Generates a spiral shape that winds from an inner to an outer radius. #[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))] fn spiral( From 82b25fc92377f20df3a9e3b610c66029a949bbe5 Mon Sep 17 00:00:00 2001 From: Chetan Sahney Date: Sat, 14 Mar 2026 15:01:13 +0530 Subject: [PATCH 4/6] final cleaned up code (Removed extra lines and improved the comments) --- node-graph/nodes/vector/src/generator_nodes.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 1489119323..ac47f4a057 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -52,8 +52,7 @@ fn circle( // 1. Create the vector let mut circle = Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))); - // 2. Register the 4 pairs of colinear handles - // 2. Register the 4 pairs of colinear handles + // Created the collinear_manipulators so that all handles are linked, making it easier to edit the circle as a circle instead of a 4 point shape. let ids = circle.segment_domain.ids(); let len = ids.len(); for i in 0..len { @@ -80,7 +79,6 @@ fn arc( sweep_angle: Angle, arc_type: ArcType, ) -> Table { - // 1. Create the arc vector let mut arc_vector = Vector::from_subpath(subpath::Subpath::new_arc( radius, start_angle / 360. * std::f64::consts::TAU, @@ -118,7 +116,7 @@ fn spiral( #[default(25)] outer_radius: f64, #[default(90.)] angular_resolution: f64, ) -> Table { - // 1. Create the spiral vector + let mut spiral_vector = Vector::from_subpath(subpath::Subpath::new_spiral( inner_radius, outer_radius, @@ -128,7 +126,7 @@ fn spiral( spiral_type, )); - // 2. NEW: Link consecutive curved segments + let len = spiral_vector.segment_domain.ids().len(); for i in 0..len.saturating_sub(1) { // Ensure both segments meeting at the anchor point are cubic beziers From e5a4d8f556d7e9bade48d4e96d2e19c4c8ef185e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 18 Mar 2026 23:08:30 -0700 Subject: [PATCH 5/6] Restore the CI "View deployment" button in PRs after building --- .github/workflows/build.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58467c78ec..8f1863bb9d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,6 +126,33 @@ jobs: echo "All $MAX_ATTEMPTS Cloudflare Pages publish attempts failed." exit 1 + - name: 🚀 Create GitHub Deployment for "View deployment" button + if: inputs.checkout_repo == '' || inputs.checkout_repo == github.repository + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CF_URL: ${{ steps.cloudflare.outputs.url }} + run: | + if [ -z "$CF_URL" ]; then + echo "No Cloudflare URL available, skipping deployment." + exit 0 + fi + DEPLOY_ID=$(gh api \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + repos/${{ github.repository }}/deployments \ + -f ref="$(git rev-parse HEAD)" \ + -f environment="graphite-dev (Preview)" \ + -F auto_merge=false \ + -f required_contexts="[]" \ + --jq '.id') + gh api \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses \ + -f state=success \ + -f environment_url="$CF_URL" \ + -f log_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + - name: 💬 Comment with the build link env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From a6f4d2d0f7c0e3719f3dea733eecb01dcc0ea688 Mon Sep 17 00:00:00 2001 From: Chetan Sahney Date: Thu, 19 Mar 2026 12:23:20 +0530 Subject: [PATCH 6/6] fix(pen): prevent merging with closed paths by only considering endpoints --- .../common_functionality/utility_functions.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index e8d4293721..fc18a0131b 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -24,7 +24,21 @@ use kurbo::{CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, PathSeg, Point, QuadBe /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64, layers: impl Iterator) -> Option<(LayerNodeIdentifier, PointId, DVec2)> { - closest_point(document, goal, tolerance, layers, |_| false) + let mut best = None; + let mut best_distance_squared = tolerance * tolerance; + for layer in layers { + let viewspace = document.metadata().transform_to_viewport(layer); + let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; + for id in vector.anchor_endpoints() { + 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; + } + } + } + best } /// Determine the closest point to the goal point under max_distance.