mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-22 22:14:07 +00:00
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:
parent
0f638314dc
commit
b1f2cf706e
5 changed files with 107 additions and 126 deletions
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue