cubic_splines/
cubic_splines.rs

1//! This example exhibits different available modes of constructing cubic Bezier curves.
2
3use bevy::{
4    app::{App, Startup, Update},
5    color::*,
6    ecs::system::Commands,
7    gizmos::gizmos::Gizmos,
8    input::{mouse::MouseButtonInput, ButtonState},
9    math::{cubic_splines::*, vec2},
10    prelude::*,
11};
12
13fn main() {
14    App::new()
15        .add_plugins(DefaultPlugins)
16        .add_systems(Startup, setup)
17        .add_systems(
18            Update,
19            (
20                handle_keypress,
21                handle_mouse_move,
22                handle_mouse_press,
23                draw_edit_move,
24                update_curve,
25                update_spline_mode_text,
26                update_cycling_mode_text,
27                draw_curve,
28                draw_control_points,
29            )
30                .chain(),
31        )
32        .run();
33}
34
35fn setup(mut commands: Commands) {
36    // Initialize the modes with their defaults:
37    let spline_mode = SplineMode::default();
38    commands.insert_resource(spline_mode);
39    let cycling_mode = CyclingMode::default();
40    commands.insert_resource(cycling_mode);
41
42    // Starting data for [`ControlPoints`]:
43    let default_points = vec![
44        vec2(-500., -200.),
45        vec2(-250., 250.),
46        vec2(250., 250.),
47        vec2(500., -200.),
48    ];
49
50    let default_tangents = vec![
51        vec2(0., 200.),
52        vec2(200., 0.),
53        vec2(0., -200.),
54        vec2(-200., 0.),
55    ];
56
57    let default_control_data = ControlPoints {
58        points_and_tangents: default_points.into_iter().zip(default_tangents).collect(),
59    };
60
61    let curve = form_curve(&default_control_data, spline_mode, cycling_mode);
62    commands.insert_resource(curve);
63    commands.insert_resource(default_control_data);
64
65    // Mouse tracking information:
66    commands.insert_resource(MousePosition::default());
67    commands.insert_resource(MouseEditMove::default());
68
69    commands.spawn(Camera2d);
70
71    // The instructions and modes are rendered on the left-hand side in a column.
72    let instructions_text = "Click and drag to add control points and their tangents\n\
73        R: Remove the last control point\n\
74        S: Cycle the spline construction being used\n\
75        C: Toggle cyclic curve construction";
76    let spline_mode_text = format!("Spline: {spline_mode}");
77    let cycling_mode_text = format!("{cycling_mode}");
78    let style = TextFont::default();
79
80    commands
81        .spawn(Node {
82            position_type: PositionType::Absolute,
83            top: px(12),
84            left: px(12),
85            flex_direction: FlexDirection::Column,
86            row_gap: px(20),
87            ..default()
88        })
89        .with_children(|parent| {
90            parent.spawn((Text::new(instructions_text), style.clone()));
91            parent.spawn((SplineModeText, Text(spline_mode_text), style.clone()));
92            parent.spawn((CyclingModeText, Text(cycling_mode_text), style.clone()));
93        });
94}
95
96// -----------------------------------
97// Curve-related Resources and Systems
98// -----------------------------------
99
100/// The current spline mode, which determines the spline method used in conjunction with the
101/// control points.
102#[derive(Clone, Copy, Resource, Default)]
103enum SplineMode {
104    #[default]
105    Hermite,
106    Cardinal,
107    B,
108}
109
110impl std::fmt::Display for SplineMode {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            SplineMode::Hermite => f.write_str("Hermite"),
114            SplineMode::Cardinal => f.write_str("Cardinal"),
115            SplineMode::B => f.write_str("B"),
116        }
117    }
118}
119
120/// The current cycling mode, which determines whether the control points should be interpolated
121/// cyclically (to make a loop).
122#[derive(Clone, Copy, Resource, Default)]
123enum CyclingMode {
124    #[default]
125    NotCyclic,
126    Cyclic,
127}
128
129impl std::fmt::Display for CyclingMode {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            CyclingMode::NotCyclic => f.write_str("Not Cyclic"),
133            CyclingMode::Cyclic => f.write_str("Cyclic"),
134        }
135    }
136}
137
138/// The curve presently being displayed. This is optional because there may not be enough control
139/// points to actually generate a curve.
140#[derive(Clone, Default, Resource)]
141struct Curve(Option<CubicCurve<Vec2>>);
142
143/// The control points used to generate a curve. The tangent components are only used in the case of
144/// Hermite interpolation.
145#[derive(Clone, Resource)]
146struct ControlPoints {
147    points_and_tangents: Vec<(Vec2, Vec2)>,
148}
149
150/// This system is responsible for updating the [`Curve`] when the [control points] or active modes
151/// change.
152///
153/// [control points]: ControlPoints
154fn update_curve(
155    control_points: Res<ControlPoints>,
156    spline_mode: Res<SplineMode>,
157    cycling_mode: Res<CyclingMode>,
158    mut curve: ResMut<Curve>,
159) {
160    if !control_points.is_changed() && !spline_mode.is_changed() && !cycling_mode.is_changed() {
161        return;
162    }
163
164    *curve = form_curve(&control_points, *spline_mode, *cycling_mode);
165}
166
167/// This system uses gizmos to draw the current [`Curve`] by breaking it up into a large number
168/// of line segments.
169fn draw_curve(curve: Res<Curve>, mut gizmos: Gizmos) {
170    let Some(ref curve) = curve.0 else {
171        return;
172    };
173    // Scale resolution with curve length so it doesn't degrade as the length increases.
174    let resolution = 100 * curve.segments().len();
175    gizmos.linestrip(
176        curve.iter_positions(resolution).map(|pt| pt.extend(0.0)),
177        Color::srgb(1.0, 1.0, 1.0),
178    );
179}
180
181/// This system uses gizmos to draw the current [control points] as circles, displaying their
182/// tangent vectors as arrows in the case of a Hermite spline.
183///
184/// [control points]: ControlPoints
185fn draw_control_points(
186    control_points: Res<ControlPoints>,
187    spline_mode: Res<SplineMode>,
188    mut gizmos: Gizmos,
189) {
190    for &(point, tangent) in &control_points.points_and_tangents {
191        gizmos.circle_2d(point, 10.0, Color::srgb(0.0, 1.0, 0.0));
192
193        if matches!(*spline_mode, SplineMode::Hermite) {
194            gizmos.arrow_2d(point, point + tangent, Color::srgb(1.0, 0.0, 0.0));
195        }
196    }
197}
198
199/// Helper function for generating a [`Curve`] from [control points] and selected modes.
200///
201/// [control points]: ControlPoints
202fn form_curve(
203    control_points: &ControlPoints,
204    spline_mode: SplineMode,
205    cycling_mode: CyclingMode,
206) -> Curve {
207    let (points, tangents): (Vec<_>, Vec<_>) =
208        control_points.points_and_tangents.iter().copied().unzip();
209
210    match spline_mode {
211        SplineMode::Hermite => {
212            let spline = CubicHermite::new(points, tangents);
213            Curve(match cycling_mode {
214                CyclingMode::NotCyclic => spline.to_curve().ok(),
215                CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
216            })
217        }
218        SplineMode::Cardinal => {
219            let spline = CubicCardinalSpline::new_catmull_rom(points);
220            Curve(match cycling_mode {
221                CyclingMode::NotCyclic => spline.to_curve().ok(),
222                CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
223            })
224        }
225        SplineMode::B => {
226            let spline = CubicBSpline::new(points);
227            Curve(match cycling_mode {
228                CyclingMode::NotCyclic => spline.to_curve().ok(),
229                CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
230            })
231        }
232    }
233}
234
235// --------------------
236// Text-related Components and Systems
237// --------------------
238
239/// Marker component for the text node that displays the current [`SplineMode`].
240#[derive(Component)]
241struct SplineModeText;
242
243/// Marker component for the text node that displays the current [`CyclingMode`].
244#[derive(Component)]
245struct CyclingModeText;
246
247fn update_spline_mode_text(
248    spline_mode: Res<SplineMode>,
249    mut spline_mode_text: Query<&mut Text, With<SplineModeText>>,
250) {
251    if !spline_mode.is_changed() {
252        return;
253    }
254
255    let new_text = format!("Spline: {}", *spline_mode);
256
257    for mut spline_mode_text in spline_mode_text.iter_mut() {
258        (**spline_mode_text).clone_from(&new_text);
259    }
260}
261
262fn update_cycling_mode_text(
263    cycling_mode: Res<CyclingMode>,
264    mut cycling_mode_text: Query<&mut Text, With<CyclingModeText>>,
265) {
266    if !cycling_mode.is_changed() {
267        return;
268    }
269
270    let new_text = format!("{}", *cycling_mode);
271
272    for mut cycling_mode_text in cycling_mode_text.iter_mut() {
273        (**cycling_mode_text).clone_from(&new_text);
274    }
275}
276
277// -----------------------------------
278// Input-related Resources and Systems
279// -----------------------------------
280
281/// A small state machine which tracks a click-and-drag motion used to create new control points.
282///
283/// When the user is not doing a click-and-drag motion, the `start` field is `None`. When the user
284/// presses the left mouse button, the location of that press is temporarily stored in the field.
285#[derive(Clone, Default, Resource)]
286struct MouseEditMove {
287    start: Option<Vec2>,
288}
289
290/// The current mouse position, if known.
291#[derive(Clone, Default, Resource)]
292struct MousePosition(Option<Vec2>);
293
294/// Update the current cursor position and track it in the [`MousePosition`] resource.
295fn handle_mouse_move(
296    mut cursor_moved_reader: MessageReader<CursorMoved>,
297    mut mouse_position: ResMut<MousePosition>,
298) {
299    if let Some(cursor_moved) = cursor_moved_reader.read().last() {
300        mouse_position.0 = Some(cursor_moved.position);
301    }
302}
303
304/// This system handles updating the [`MouseEditMove`] resource, orchestrating the logical part
305/// of the click-and-drag motion which actually creates new control points.
306fn handle_mouse_press(
307    mut mouse_button_input_reader: MessageReader<MouseButtonInput>,
308    mouse_position: Res<MousePosition>,
309    mut edit_move: ResMut<MouseEditMove>,
310    mut control_points: ResMut<ControlPoints>,
311    camera: Single<(&Camera, &GlobalTransform)>,
312) {
313    let Some(mouse_pos) = mouse_position.0 else {
314        return;
315    };
316
317    // Handle click and drag behavior
318    for mouse_button_input in mouse_button_input_reader.read() {
319        if mouse_button_input.button != MouseButton::Left {
320            continue;
321        }
322
323        match mouse_button_input.state {
324            ButtonState::Pressed => {
325                if edit_move.start.is_some() {
326                    // If the edit move already has a start, press event should do nothing.
327                    continue;
328                }
329                // This press represents the start of the edit move.
330                edit_move.start = Some(mouse_pos);
331            }
332
333            ButtonState::Released => {
334                // Release is only meaningful if we started an edit move.
335                let Some(start) = edit_move.start else {
336                    continue;
337                };
338
339                let (camera, camera_transform) = *camera;
340
341                // Convert the starting point and end point (current mouse pos) into world coords:
342                let Ok(point) = camera.viewport_to_world_2d(camera_transform, start) else {
343                    continue;
344                };
345                let Ok(end_point) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else {
346                    continue;
347                };
348                let tangent = end_point - point;
349
350                // The start of the click-and-drag motion represents the point to add,
351                // while the difference with the current position represents the tangent.
352                control_points.points_and_tangents.push((point, tangent));
353
354                // Reset the edit move since we've consumed it.
355                edit_move.start = None;
356            }
357        }
358    }
359}
360
361/// This system handles drawing the "preview" control point based on the state of [`MouseEditMove`].
362fn draw_edit_move(
363    edit_move: Res<MouseEditMove>,
364    mouse_position: Res<MousePosition>,
365    mut gizmos: Gizmos,
366    camera: Single<(&Camera, &GlobalTransform)>,
367) {
368    let Some(start) = edit_move.start else {
369        return;
370    };
371    let Some(mouse_pos) = mouse_position.0 else {
372        return;
373    };
374
375    let (camera, camera_transform) = *camera;
376
377    // Resources store data in viewport coordinates, so we need to convert to world coordinates
378    // to display them:
379    let Ok(start) = camera.viewport_to_world_2d(camera_transform, start) else {
380        return;
381    };
382    let Ok(end) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else {
383        return;
384    };
385
386    gizmos.circle_2d(start, 10.0, Color::srgb(0.0, 1.0, 0.7));
387    gizmos.circle_2d(start, 7.0, Color::srgb(0.0, 1.0, 0.7));
388    gizmos.arrow_2d(start, end, Color::srgb(1.0, 0.0, 0.7));
389}
390
391/// This system handles all keyboard commands.
392fn handle_keypress(
393    keyboard: Res<ButtonInput<KeyCode>>,
394    mut spline_mode: ResMut<SplineMode>,
395    mut cycling_mode: ResMut<CyclingMode>,
396    mut control_points: ResMut<ControlPoints>,
397) {
398    // S => change spline mode
399    if keyboard.just_pressed(KeyCode::KeyS) {
400        *spline_mode = match *spline_mode {
401            SplineMode::Hermite => SplineMode::Cardinal,
402            SplineMode::Cardinal => SplineMode::B,
403            SplineMode::B => SplineMode::Hermite,
404        }
405    }
406
407    // C => change cycling mode
408    if keyboard.just_pressed(KeyCode::KeyC) {
409        *cycling_mode = match *cycling_mode {
410            CyclingMode::NotCyclic => CyclingMode::Cyclic,
411            CyclingMode::Cyclic => CyclingMode::NotCyclic,
412        }
413    }
414
415    // R => remove last control point
416    if keyboard.just_pressed(KeyCode::KeyR) {
417        control_points.points_and_tangents.pop();
418    }
419}