Skip to content

Commit b1f2cf7

Browse files
indierustyKeavon
andauthored
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 <[email protected]>
1 parent 0f63831 commit b1f2cf7

File tree

5 files changed

+107
-126
lines changed

5 files changed

+107
-126
lines changed

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{self
2020
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
2121
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
2222
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
23-
use bezier_rs::Subpath;
2423
use glam::{DAffine2, DVec2, IVec2};
2524
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
2625
use graph_craft::proto::GraphErrors;
2726
use graphene_std::math::math_ext::QuadExt;
28-
use graphene_std::vector::misc::subpath_to_kurbo_bezpath;
27+
use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
2928
use graphene_std::*;
30-
use kurbo::{Line, Point};
29+
use kurbo::{DEFAULT_ACCURACY, Shape};
3130
use renderer::Quad;
3231
use std::cmp::Ordering;
3332

@@ -958,8 +957,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
958957
to_connector_is_layer,
959958
GraphWireStyle::Direct,
960959
);
961-
let mut path_string = String::new();
962-
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
960+
let path_string = vector_wire.to_svg();
963961
let wire_path = WirePath {
964962
path_string,
965963
data_type: self.wire_in_progress_type,
@@ -1196,7 +1194,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
11961194
.filter(|input| input.1.as_value().is_some())
11971195
.map(|input| input.0);
11981196
if let Some(selected_node_input_connect_index) = selected_node_input_connect_index {
1199-
let Some(bounding_box) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
1197+
let Some(node_bbox) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
12001198
log::error!("Could not get bounding box for node: {selected_node_id}");
12011199
return;
12021200
};
@@ -1220,31 +1218,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
12201218
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
12211219
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
12221220

1223-
let bbox_rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
1221+
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);
1222+
let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None);
12241223

1225-
let p1 = DVec2::new(bbox_rect.x0, bbox_rect.y0);
1226-
let p2 = DVec2::new(bbox_rect.x1, bbox_rect.y0);
1227-
let p3 = DVec2::new(bbox_rect.x1, bbox_rect.y1);
1228-
let p4 = DVec2::new(bbox_rect.x0, bbox_rect.y1);
1229-
let ps = [p1, p2, p3, p4];
1230-
1231-
let inside = wire.is_inside_subpath(&Subpath::from_anchors_linear(ps, true), None, None);
1232-
1233-
let wire = subpath_to_kurbo_bezpath(wire);
1234-
1235-
let intersect = wire.segments().any(|segment| {
1236-
let rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
1237-
1238-
let top_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0));
1239-
let bottom_line = Line::new(Point::new(rect.x0, rect.y1), Point::new(rect.x1, rect.y1));
1240-
let left_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x0, rect.y1));
1241-
let right_line = Line::new(Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1));
1242-
1243-
!segment.intersect_line(top_line).is_empty()
1244-
|| !segment.intersect_line(bottom_line).is_empty()
1245-
|| !segment.intersect_line(left_line).is_empty()
1246-
|| !segment.intersect_line(right_line).is_empty()
1247-
});
1224+
let intersect = wire
1225+
.segments()
1226+
.any(|segment| node_bbox.segments().filter_map(|segment| segment.as_line()).any(|line| !segment.intersect_line(line).is_empty()));
12481227

12491228
(intersect || inside).then_some((input, is_stack))
12501229
})

editor/src/messages/portfolio/document/utility_types/network_interface.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
2121
use graphene_std::vector::{PointId, Vector, VectorModificationType};
2222
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
2323
use interpreted_executor::node_registry::NODE_REGISTRY;
24+
use kurbo::BezPath;
2425
use serde_json::{Value, json};
2526
use std::collections::{HashMap, HashSet, VecDeque};
2627
use std::hash::{DefaultHasher, Hash, Hasher};
@@ -2713,8 +2714,7 @@ impl NodeNetworkInterface {
27132714
let thick = vertical_end && vertical_start;
27142715
let vector_wire = build_vector_wire(output_position, input_position, vertical_start, vertical_end, graph_wire_style);
27152716

2716-
let mut path_string = String::new();
2717-
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
2717+
let path_string = vector_wire.to_svg();
27182718
let data_type = FrontendGraphDataType::from_type(&self.input_type(&input, network_path).0);
27192719
let wire_path_update = Some(WirePath {
27202720
path_string,
@@ -2731,14 +2731,14 @@ impl NodeNetworkInterface {
27312731
}
27322732

27332733
/// Returns the vector subpath and a boolean of whether the wire should be thick.
2734-
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(Subpath<PointId>, bool)> {
2734+
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, bool)> {
27352735
let Some(input_position) = self.get_input_center(input, network_path) else {
27362736
log::error!("Could not get dom rect for wire end: {:?}", input);
27372737
return None;
27382738
};
27392739
// An upstream output could not be found, so the wire does not exist, but it should still be loaded as as empty vector
27402740
let Some(upstream_output) = self.upstream_output_connector(input, network_path) else {
2741-
return Some((Subpath::from_anchors(std::iter::empty(), false), false));
2741+
return Some((BezPath::new(), false));
27422742
};
27432743
let Some(output_position) = self.get_output_center(&upstream_output, network_path) else {
27442744
log::error!("Could not get dom rect for wire start: {:?}", upstream_output);
@@ -2752,8 +2752,7 @@ impl NodeNetworkInterface {
27522752

27532753
pub fn wire_path_from_input(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, dashed: bool, network_path: &[NodeId]) -> Option<WirePath> {
27542754
let (vector_wire, thick) = self.vector_wire_from_input(input, graph_wire_style, network_path)?;
2755-
let mut path_string = String::new();
2756-
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
2755+
let path_string = vector_wire.to_svg();
27572756
let data_type = FrontendGraphDataType::from_type(&self.input_type(input, network_path).0);
27582757
Some(WirePath {
27592758
path_string,

editor/src/messages/portfolio/document/utility_types/wires.rs

Lines changed: 25 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
2-
use bezier_rs::{ManipulatorGroup, Subpath};
32
use glam::{DVec2, IVec2};
4-
use graphene_std::uuid::NodeId;
5-
use graphene_std::vector::PointId;
3+
use graphene_std::{uuid::NodeId, vector::misc::dvec2_to_point};
4+
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Shape};
65

76
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
87
pub struct WirePath {
@@ -53,7 +52,7 @@ impl GraphWireStyle {
5352
}
5453
}
5554

56-
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> Subpath<PointId> {
55+
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> BezPath {
5756
let grid_spacing = 24.;
5857
match graph_wire_style {
5958
GraphWireStyle::Direct => {
@@ -85,44 +84,21 @@ pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical
8584
let delta01 = DVec2::new((locations[1].x - locations[0].x) * smoothing, (locations[1].y - locations[0].y) * smoothing);
8685
let delta23 = DVec2::new((locations[3].x - locations[2].x) * smoothing, (locations[3].y - locations[2].y) * smoothing);
8786

88-
Subpath::new(
89-
vec![
90-
ManipulatorGroup {
91-
anchor: locations[0],
92-
in_handle: None,
93-
out_handle: None,
94-
id: PointId::generate(),
95-
},
96-
ManipulatorGroup {
97-
anchor: locations[1],
98-
in_handle: None,
99-
out_handle: Some(locations[1] + delta01),
100-
id: PointId::generate(),
101-
},
102-
ManipulatorGroup {
103-
anchor: locations[2],
104-
in_handle: Some(locations[2] - delta23),
105-
out_handle: None,
106-
id: PointId::generate(),
107-
},
108-
ManipulatorGroup {
109-
anchor: locations[3],
110-
in_handle: None,
111-
out_handle: None,
112-
id: PointId::generate(),
113-
},
114-
],
115-
false,
116-
)
87+
let mut wire = BezPath::new();
88+
wire.move_to(dvec2_to_point(locations[0]));
89+
wire.line_to(dvec2_to_point(locations[1]));
90+
wire.curve_to(dvec2_to_point(locations[1] + delta01), dvec2_to_point(locations[2] - delta23), dvec2_to_point(locations[2]));
91+
wire.line_to(dvec2_to_point(locations[3]));
92+
wire
11793
}
11894
GraphWireStyle::GridAligned => {
119-
let locations = straight_wire_paths(output_position, input_position, vertical_out, vertical_in);
120-
straight_wire_subpath(locations)
95+
let locations = straight_wire_path(output_position, input_position, vertical_out, vertical_in);
96+
straight_wire_to_bezpath(locations)
12197
}
12298
}
12399
}
124100

125-
fn straight_wire_paths(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
101+
fn straight_wire_path(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
126102
let grid_spacing = 24;
127103
let line_width = 2;
128104

@@ -446,40 +422,24 @@ fn straight_wire_paths(output_position: DVec2, input_position: DVec2, vertical_o
446422
vec![IVec2::new(x1, y1), IVec2::new(x20, y1), IVec2::new(x20, y3), IVec2::new(x4, y3)]
447423
}
448424

449-
fn straight_wire_subpath(locations: Vec<IVec2>) -> Subpath<PointId> {
425+
fn straight_wire_to_bezpath(locations: Vec<IVec2>) -> BezPath {
450426
if locations.is_empty() {
451-
return Subpath::new(Vec::new(), false);
427+
return BezPath::new();
452428
}
453429

430+
let to_point = |location: IVec2| Point::new(location.x as f64, location.y as f64);
431+
454432
if locations.len() == 2 {
455-
return Subpath::new(
456-
vec![
457-
ManipulatorGroup {
458-
anchor: locations[0].into(),
459-
in_handle: None,
460-
out_handle: None,
461-
id: PointId::generate(),
462-
},
463-
ManipulatorGroup {
464-
anchor: locations[1].into(),
465-
in_handle: None,
466-
out_handle: None,
467-
id: PointId::generate(),
468-
},
469-
],
470-
false,
471-
);
433+
let p1 = to_point(locations[0]);
434+
let p2 = to_point(locations[1]);
435+
Line::new(p1, p2).to_path(DEFAULT_ACCURACY);
472436
}
473437

474438
let corner_radius = 10;
475439

476440
// Create path with rounded corners
477-
let mut path = vec![ManipulatorGroup {
478-
anchor: locations[0].into(),
479-
in_handle: None,
480-
out_handle: None,
481-
id: PointId::generate(),
482-
}];
441+
let mut path = BezPath::new();
442+
path.move_to(to_point(locations[0]));
483443

484444
for i in 1..(locations.len() - 1) {
485445
let prev = locations[i - 1];
@@ -563,27 +523,9 @@ fn straight_wire_subpath(locations: Vec<IVec2>) -> Subpath<PointId> {
563523
},
564524
);
565525

566-
path.extend(vec![
567-
ManipulatorGroup {
568-
anchor: corner_start.into(),
569-
in_handle: None,
570-
out_handle: Some(corner_start_mid.into()),
571-
id: PointId::generate(),
572-
},
573-
ManipulatorGroup {
574-
anchor: corner_end.into(),
575-
in_handle: Some(corner_end_mid.into()),
576-
out_handle: None,
577-
id: PointId::generate(),
578-
},
579-
])
526+
path.line_to(to_point(corner_start));
527+
path.curve_to(to_point(corner_start_mid), to_point(corner_end_mid), to_point(corner_end));
580528
}
581-
582-
path.push(ManipulatorGroup {
583-
anchor: (*locations.last().unwrap()).into(),
584-
in_handle: None,
585-
out_handle: None,
586-
id: PointId::generate(),
587-
});
588-
Subpath::new(path, false)
529+
path.line_to(to_point(*locations.last().unwrap()));
530+
path
589531
}

node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,3 +466,70 @@ pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) ->
466466

467467
compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
468468
}
469+
470+
/// Returns `true` if the `bezpath1` is completely inside the `bezpath2`.
471+
pub fn bezpath_is_inside_bezpath(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option<f64>, minimum_separation: Option<f64>) -> bool {
472+
// Eliminate any possibility of one being inside the other, if either of them is empty
473+
if bezpath1.is_empty() || bezpath2.is_empty() {
474+
return false;
475+
}
476+
477+
let inner_bbox = bezpath1.bounding_box();
478+
let outer_bbox = bezpath2.bounding_box();
479+
480+
// Eliminate bezpath1 if its bounding box is not completely inside the bezpath2's bounding box.
481+
// Reasoning:
482+
// If the inner bezpath bounding box is larger than the outer bezpath bounding box in any direction
483+
// then the inner bezpath is intersecting with or outside the outer bezpath.
484+
if !outer_bbox.contains_rect(inner_bbox) {
485+
return false;
486+
}
487+
488+
// Eliminate bezpath1 if any of its anchor points are outside the bezpath2.
489+
if !bezpath1.elements().iter().filter_map(|el| el.end_point()).all(|point| bezpath2.contains(point)) {
490+
return false;
491+
}
492+
493+
// Eliminate this subpath if it intersects with the other subpath.
494+
if !bezpath_intersections(bezpath1, bezpath2, accuracy, minimum_separation).is_empty() {
495+
return false;
496+
}
497+
498+
// At this point:
499+
// (1) This subpath's bounding box is inside the other subpath's bounding box,
500+
// (2) Its anchors are inside the other subpath, and
501+
// (3) It is not intersecting with the other subpath.
502+
// Hence, this subpath is completely inside the given other subpath.
503+
true
504+
}
505+
506+
#[cfg(test)]
507+
mod tests {
508+
// TODO: add more intersection tests
509+
510+
use super::bezpath_is_inside_bezpath;
511+
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Rect, Shape};
512+
513+
#[test]
514+
fn is_inside_subpath() {
515+
let boundary_polygon = Rect::new(100., 100., 500., 500.).to_path(DEFAULT_ACCURACY);
516+
517+
let mut curve_intersection = BezPath::new();
518+
curve_intersection.move_to(Point::new(189., 289.));
519+
curve_intersection.quad_to(Point::new(9., 286.), Point::new(45., 410.));
520+
assert!(!bezpath_is_inside_bezpath(&curve_intersection, &boundary_polygon, None, None));
521+
522+
let mut curve_outside = BezPath::new();
523+
curve_outside.move_to(Point::new(115., 37.));
524+
curve_outside.quad_to(Point::new(51.4, 91.8), Point::new(76.5, 242.));
525+
assert!(!bezpath_is_inside_bezpath(&curve_outside, &boundary_polygon, None, None));
526+
527+
let mut curve_inside = BezPath::new();
528+
curve_inside.move_to(Point::new(210.1, 133.5));
529+
curve_inside.curve_to(Point::new(150.2, 436.9), Point::new(436., 285.), Point::new(247.6, 240.7));
530+
assert!(bezpath_is_inside_bezpath(&curve_inside, &boundary_polygon, None, None));
531+
532+
let line_inside = Line::new(Point::new(101., 101.5), Point::new(150.2, 499.)).to_path(DEFAULT_ACCURACY);
533+
assert!(bezpath_is_inside_bezpath(&line_inside, &boundary_polygon, None, None));
534+
}
535+
}

node-graph/gcore/src/vector/misc.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::PointId;
22
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
33
use crate::vector::{SegmentId, Vector};
4-
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
4+
use bezier_rs::{BezierHandles, ManipulatorGroup};
55
use dyn_any::DynAny;
66
use glam::DVec2;
77
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
136136
}
137137
}
138138

139-
pub fn subpath_to_kurbo_bezpath(subpath: Subpath<PointId>) -> BezPath {
140-
let manipulator_groups = subpath.manipulator_groups();
141-
let closed = subpath.closed();
142-
bezpath_from_manipulator_groups(manipulator_groups, closed)
143-
}
144-
145139
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>], closed: bool) -> BezPath {
146140
let mut bezpath = kurbo::BezPath::new();
147141
let mut out_handle;

0 commit comments

Comments
 (0)