docs/bevy/examples/math/cubic_splines.rs 13.4 K · 420 lines · raw · history

1 //! This example exhibits different available modes of constructing cubic Bezier curves.
2
3 use 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
14 fn 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
36 fn 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)]
104 enum SplineMode {
105 #[default]
106 Hermite,
107 Cardinal,
108 B,
109 }
110
111 impl 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)]
124 enum CyclingMode {
125 #[default]
126 NotCyclic,
127 Cyclic,
128 }
129
130 impl 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)]
142 struct 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)]
147 struct 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
155 fn 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.
170 fn 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
186 fn 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
203 fn 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)]
242 struct SplineModeText;
243
244 /// Marker component for the text node that displays the current [`CyclingMode`].
245 #[derive(Component)]
246 struct CyclingModeText;
247
248 fn 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
263 fn 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)]
287 struct MouseEditMove {
288 start: Option<Vec2>,
289 }
290
291 /// The current mouse position, if known.
292 #[derive(Clone, Default, Resource)]
293 struct MousePosition(Option<Vec2>);
294
295 /// Update the current cursor position and track it in the [`MousePosition`] resource.
296 fn 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.
307 fn 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`].
363 fn 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.
393 fn 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 }