Skip to main content

jellyflow_runtime/runtime/geometry/paths/
smoothstep.rs

1use jellyflow_core::core::CanvasPoint;
2
3use super::super::endpoints::{EdgeEndpointPosition, HandlePosition};
4use super::types::{EdgePath, EdgePathLabel, PathCommand};
5
6/// Smoothstep-like edge options.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct SmoothStepEdgeOptions {
9    pub offset: f32,
10    pub step_position: f32,
11}
12
13impl Default for SmoothStepEdgeOptions {
14    fn default() -> Self {
15        Self {
16            offset: 20.0,
17            step_position: 0.5,
18        }
19    }
20}
21
22pub fn smoothstep_edge_path(
23    source: EdgeEndpointPosition,
24    target: EdgeEndpointPosition,
25    options: SmoothStepEdgeOptions,
26) -> Option<EdgePath> {
27    if !source.point.is_finite() || !target.point.is_finite() {
28        return None;
29    }
30
31    let options = normalized_smoothstep_options(options);
32    let source_dir = handle_direction(source.position);
33    let target_dir = handle_direction(target.position);
34    let source_gap = translate_point(source.point, source_dir, options.offset)?;
35    let target_gap = translate_point(target.point, target_dir, options.offset)?;
36
37    let mut points = Vec::with_capacity(6);
38    push_distinct_point(&mut points, source.point);
39    push_distinct_point(&mut points, source_gap);
40
41    let horizontal = matches!(
42        source.position,
43        HandlePosition::Left | HandlePosition::Right
44    );
45    let label_point = if horizontal {
46        let center_x = source_gap.x + (target_gap.x - source_gap.x) * options.step_position;
47        push_distinct_point(
48            &mut points,
49            CanvasPoint {
50                x: center_x,
51                y: source_gap.y,
52            },
53        );
54        push_distinct_point(
55            &mut points,
56            CanvasPoint {
57                x: center_x,
58                y: target_gap.y,
59            },
60        );
61        CanvasPoint {
62            x: center_x,
63            y: 0.5 * (source_gap.y + target_gap.y),
64        }
65    } else {
66        let center_y = source_gap.y + (target_gap.y - source_gap.y) * options.step_position;
67        push_distinct_point(
68            &mut points,
69            CanvasPoint {
70                x: source_gap.x,
71                y: center_y,
72            },
73        );
74        push_distinct_point(
75            &mut points,
76            CanvasPoint {
77                x: target_gap.x,
78                y: center_y,
79            },
80        );
81        CanvasPoint {
82            x: 0.5 * (source_gap.x + target_gap.x),
83            y: center_y,
84        }
85    };
86
87    push_distinct_point(&mut points, target_gap);
88    push_distinct_point(&mut points, target.point);
89
90    let mut commands = Vec::with_capacity(points.len());
91    let first = *points.first()?;
92    commands.push(PathCommand::MoveTo(first));
93    for point in points.into_iter().skip(1) {
94        commands.push(PathCommand::LineTo(point));
95    }
96
97    Some(EdgePath {
98        commands,
99        label: EdgePathLabel {
100            point: label_point,
101            offset_x: (label_point.x - source.point.x).abs(),
102            offset_y: (label_point.y - source.point.y).abs(),
103        },
104    })
105}
106
107fn normalized_smoothstep_options(options: SmoothStepEdgeOptions) -> SmoothStepEdgeOptions {
108    SmoothStepEdgeOptions {
109        offset: if options.offset.is_finite() && options.offset >= 0.0 {
110            options.offset
111        } else {
112            SmoothStepEdgeOptions::default().offset
113        },
114        step_position: if options.step_position.is_finite() {
115            options.step_position.clamp(0.0, 1.0)
116        } else {
117            SmoothStepEdgeOptions::default().step_position
118        },
119    }
120}
121
122fn handle_direction(position: HandlePosition) -> (f32, f32) {
123    match position {
124        HandlePosition::Top => (0.0, -1.0),
125        HandlePosition::Right => (1.0, 0.0),
126        HandlePosition::Bottom => (0.0, 1.0),
127        HandlePosition::Left => (-1.0, 0.0),
128    }
129}
130
131fn translate_point(
132    point: CanvasPoint,
133    direction: (f32, f32),
134    distance: f32,
135) -> Option<CanvasPoint> {
136    let out = CanvasPoint {
137        x: point.x + direction.0 * distance,
138        y: point.y + direction.1 * distance,
139    };
140    out.is_finite().then_some(out)
141}
142
143fn push_distinct_point(points: &mut Vec<CanvasPoint>, point: CanvasPoint) {
144    if points.last().is_some_and(|last| *last == point) {
145        return;
146    }
147    points.push(point);
148}