Refactor the node graph UI wires to render using Kurbo (#2994)

* impl function to check bezpath insideness

* refactor network interface wires to use kurbo

* refactor

* refactor

* fix adding MoveTo instead of LineTo to the grid aligned wire bezpath

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Priyanshu 2025-08-06 14:47:00 +05:30 committed by GitHub
parent 0f638314dc
commit b1f2cf706e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 107 additions and 126 deletions

View file

@ -20,14 +20,13 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{self
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::proto::GraphErrors;
use graphene_std::math::math_ext::QuadExt;
use graphene_std::vector::misc::subpath_to_kurbo_bezpath;
use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
use graphene_std::*;
use kurbo::{Line, Point};
use kurbo::{DEFAULT_ACCURACY, Shape};
use renderer::Quad;
use std::cmp::Ordering;
@ -958,8 +957,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
to_connector_is_layer,
GraphWireStyle::Direct,
);
let mut path_string = String::new();
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
let path_string = vector_wire.to_svg();
let wire_path = WirePath {
path_string,
data_type: self.wire_in_progress_type,
@ -1196,7 +1194,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
.filter(|input| input.1.as_value().is_some())
.map(|input| input.0);
if let Some(selected_node_input_connect_index) = selected_node_input_connect_index {
let Some(bounding_box) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
let Some(node_bbox) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
log::error!("Could not get bounding box for node: {selected_node_id}");
return;
};
@ -1220,31 +1218,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
let bbox_rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY);
let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None);
let p1 = DVec2::new(bbox_rect.x0, bbox_rect.y0);
let p2 = DVec2::new(bbox_rect.x1, bbox_rect.y0);
let p3 = DVec2::new(bbox_rect.x1, bbox_rect.y1);
let p4 = DVec2::new(bbox_rect.x0, bbox_rect.y1);
let ps = [p1, p2, p3, p4];
let inside = wire.is_inside_subpath(&Subpath::from_anchors_linear(ps, true), None, None);
let wire = subpath_to_kurbo_bezpath(wire);
let intersect = wire.segments().any(|segment| {
let rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
let top_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0));
let bottom_line = Line::new(Point::new(rect.x0, rect.y1), Point::new(rect.x1, rect.y1));
let left_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x0, rect.y1));
let right_line = Line::new(Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1));
!segment.intersect_line(top_line).is_empty()
|| !segment.intersect_line(bottom_line).is_empty()
|| !segment.intersect_line(left_line).is_empty()
|| !segment.intersect_line(right_line).is_empty()
});
let intersect = wire
.segments()
.any(|segment| node_bbox.segments().filter_map(|segment| segment.as_line()).any(|line| !segment.intersect_line(line).is_empty()));
(intersect || inside).then_some((input, is_stack))
})

View file

@ -21,6 +21,7 @@ use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, Vector, VectorModificationType};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
use interpreted_executor::node_registry::NODE_REGISTRY;
use kurbo::BezPath;
use serde_json::{Value, json};
use std::collections::{HashMap, HashSet, VecDeque};
use std::hash::{DefaultHasher, Hash, Hasher};
@ -2713,8 +2714,7 @@ impl NodeNetworkInterface {
let thick = vertical_end && vertical_start;
let vector_wire = build_vector_wire(output_position, input_position, vertical_start, vertical_end, graph_wire_style);
let mut path_string = String::new();
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
let path_string = vector_wire.to_svg();
let data_type = FrontendGraphDataType::from_type(&self.input_type(&input, network_path).0);
let wire_path_update = Some(WirePath {
path_string,
@ -2731,14 +2731,14 @@ impl NodeNetworkInterface {
}
/// Returns the vector subpath and a boolean of whether the wire should be thick.
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(Subpath<PointId>, bool)> {
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, bool)> {
let Some(input_position) = self.get_input_center(input, network_path) else {
log::error!("Could not get dom rect for wire end: {:?}", input);
return None;
};
// An upstream output could not be found, so the wire does not exist, but it should still be loaded as as empty vector
let Some(upstream_output) = self.upstream_output_connector(input, network_path) else {
return Some((Subpath::from_anchors(std::iter::empty(), false), false));
return Some((BezPath::new(), false));
};
let Some(output_position) = self.get_output_center(&upstream_output, network_path) else {
log::error!("Could not get dom rect for wire start: {:?}", upstream_output);
@ -2752,8 +2752,7 @@ impl NodeNetworkInterface {
pub fn wire_path_from_input(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, dashed: bool, network_path: &[NodeId]) -> Option<WirePath> {
let (vector_wire, thick) = self.vector_wire_from_input(input, graph_wire_style, network_path)?;
let mut path_string = String::new();
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
let path_string = vector_wire.to_svg();
let data_type = FrontendGraphDataType::from_type(&self.input_type(input, network_path).0);
Some(WirePath {
path_string,

View file

@ -1,8 +1,7 @@
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::{DVec2, IVec2};
use graphene_std::uuid::NodeId;
use graphene_std::vector::PointId;
use graphene_std::{uuid::NodeId, vector::misc::dvec2_to_point};
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Shape};
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WirePath {
@ -53,7 +52,7 @@ impl GraphWireStyle {
}
}
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> Subpath<PointId> {
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> BezPath {
let grid_spacing = 24.;
match graph_wire_style {
GraphWireStyle::Direct => {
@ -85,44 +84,21 @@ pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical
let delta01 = DVec2::new((locations[1].x - locations[0].x) * smoothing, (locations[1].y - locations[0].y) * smoothing);
let delta23 = DVec2::new((locations[3].x - locations[2].x) * smoothing, (locations[3].y - locations[2].y) * smoothing);
Subpath::new(
vec![
ManipulatorGroup {
anchor: locations[0],
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[1],
in_handle: None,
out_handle: Some(locations[1] + delta01),
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[2],
in_handle: Some(locations[2] - delta23),
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[3],
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
],
false,
)
let mut wire = BezPath::new();
wire.move_to(dvec2_to_point(locations[0]));
wire.line_to(dvec2_to_point(locations[1]));
wire.curve_to(dvec2_to_point(locations[1] + delta01), dvec2_to_point(locations[2] - delta23), dvec2_to_point(locations[2]));
wire.line_to(dvec2_to_point(locations[3]));
wire
}
GraphWireStyle::GridAligned => {
let locations = straight_wire_paths(output_position, input_position, vertical_out, vertical_in);
straight_wire_subpath(locations)
let locations = straight_wire_path(output_position, input_position, vertical_out, vertical_in);
straight_wire_to_bezpath(locations)
}
}
}
fn straight_wire_paths(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
fn straight_wire_path(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
let grid_spacing = 24;
let line_width = 2;
@ -446,40 +422,24 @@ fn straight_wire_paths(output_position: DVec2, input_position: DVec2, vertical_o
vec![IVec2::new(x1, y1), IVec2::new(x20, y1), IVec2::new(x20, y3), IVec2::new(x4, y3)]
}
fn straight_wire_subpath(locations: Vec<IVec2>) -> Subpath<PointId> {
fn straight_wire_to_bezpath(locations: Vec<IVec2>) -> BezPath {
if locations.is_empty() {
return Subpath::new(Vec::new(), false);
return BezPath::new();
}
let to_point = |location: IVec2| Point::new(location.x as f64, location.y as f64);
if locations.len() == 2 {
return Subpath::new(
vec![
ManipulatorGroup {
anchor: locations[0].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[1].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
],
false,
);
let p1 = to_point(locations[0]);
let p2 = to_point(locations[1]);
Line::new(p1, p2).to_path(DEFAULT_ACCURACY);
}
let corner_radius = 10;
// Create path with rounded corners
let mut path = vec![ManipulatorGroup {
anchor: locations[0].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
}];
let mut path = BezPath::new();
path.move_to(to_point(locations[0]));
for i in 1..(locations.len() - 1) {
let prev = locations[i - 1];
@ -563,27 +523,9 @@ fn straight_wire_subpath(locations: Vec<IVec2>) -> Subpath<PointId> {
},
);
path.extend(vec![
ManipulatorGroup {
anchor: corner_start.into(),
in_handle: None,
out_handle: Some(corner_start_mid.into()),
id: PointId::generate(),
},
ManipulatorGroup {
anchor: corner_end.into(),
in_handle: Some(corner_end_mid.into()),
out_handle: None,
id: PointId::generate(),
},
])
path.line_to(to_point(corner_start));
path.curve_to(to_point(corner_start_mid), to_point(corner_end_mid), to_point(corner_end));
}
path.push(ManipulatorGroup {
anchor: (*locations.last().unwrap()).into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
});
Subpath::new(path, false)
path.line_to(to_point(*locations.last().unwrap()));
path
}

View file

@ -466,3 +466,70 @@ pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) ->
compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
}
/// Returns `true` if the `bezpath1` is completely inside the `bezpath2`.
pub fn bezpath_is_inside_bezpath(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option<f64>, minimum_separation: Option<f64>) -> bool {
// Eliminate any possibility of one being inside the other, if either of them is empty
if bezpath1.is_empty() || bezpath2.is_empty() {
return false;
}
let inner_bbox = bezpath1.bounding_box();
let outer_bbox = bezpath2.bounding_box();
// Eliminate bezpath1 if its bounding box is not completely inside the bezpath2's bounding box.
// Reasoning:
// If the inner bezpath bounding box is larger than the outer bezpath bounding box in any direction
// then the inner bezpath is intersecting with or outside the outer bezpath.
if !outer_bbox.contains_rect(inner_bbox) {
return false;
}
// Eliminate bezpath1 if any of its anchor points are outside the bezpath2.
if !bezpath1.elements().iter().filter_map(|el| el.end_point()).all(|point| bezpath2.contains(point)) {
return false;
}
// Eliminate this subpath if it intersects with the other subpath.
if !bezpath_intersections(bezpath1, bezpath2, accuracy, minimum_separation).is_empty() {
return false;
}
// At this point:
// (1) This subpath's bounding box is inside the other subpath's bounding box,
// (2) Its anchors are inside the other subpath, and
// (3) It is not intersecting with the other subpath.
// Hence, this subpath is completely inside the given other subpath.
true
}
#[cfg(test)]
mod tests {
// TODO: add more intersection tests
use super::bezpath_is_inside_bezpath;
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Rect, Shape};
#[test]
fn is_inside_subpath() {
let boundary_polygon = Rect::new(100., 100., 500., 500.).to_path(DEFAULT_ACCURACY);
let mut curve_intersection = BezPath::new();
curve_intersection.move_to(Point::new(189., 289.));
curve_intersection.quad_to(Point::new(9., 286.), Point::new(45., 410.));
assert!(!bezpath_is_inside_bezpath(&curve_intersection, &boundary_polygon, None, None));
let mut curve_outside = BezPath::new();
curve_outside.move_to(Point::new(115., 37.));
curve_outside.quad_to(Point::new(51.4, 91.8), Point::new(76.5, 242.));
assert!(!bezpath_is_inside_bezpath(&curve_outside, &boundary_polygon, None, None));
let mut curve_inside = BezPath::new();
curve_inside.move_to(Point::new(210.1, 133.5));
curve_inside.curve_to(Point::new(150.2, 436.9), Point::new(436., 285.), Point::new(247.6, 240.7));
assert!(bezpath_is_inside_bezpath(&curve_inside, &boundary_polygon, None, None));
let line_inside = Line::new(Point::new(101., 101.5), Point::new(150.2, 499.)).to_path(DEFAULT_ACCURACY);
assert!(bezpath_is_inside_bezpath(&line_inside, &boundary_polygon, None, None));
}
}

View file

@ -1,7 +1,7 @@
use super::PointId;
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use crate::vector::{SegmentId, Vector};
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
use bezier_rs::{BezierHandles, ManipulatorGroup};
use dyn_any::DynAny;
use glam::DVec2;
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
@ -136,12 +136,6 @@ pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> P
}
}
pub fn subpath_to_kurbo_bezpath(subpath: Subpath<PointId>) -> BezPath {
let manipulator_groups = subpath.manipulator_groups();
let closed = subpath.closed();
bezpath_from_manipulator_groups(manipulator_groups, closed)
}
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>], closed: bool) -> BezPath {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;