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 }} 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. diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 66c8009e02..ac47f4a057 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -42,40 +42,66 @@ 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))); + + // 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 { + circle.colinear_manipulators.push([ + HandleId::end(ids[i]), + HandleId::primary(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, - }, - ))) + 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. @@ -90,14 +116,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( + + 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, - ))) + )); + + + 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.