Skip to main content

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