Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/math/cubic_splines.rs
6592 views
1
//! This example exhibits different available modes of constructing cubic Bezier curves.
2
3
use 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
13
fn 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
35
fn 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)]
103
enum SplineMode {
104
#[default]
105
Hermite,
106
Cardinal,
107
B,
108
}
109
110
impl 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)]
123
enum CyclingMode {
124
#[default]
125
NotCyclic,
126
Cyclic,
127
}
128
129
impl 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)]
141
struct 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)]
146
struct 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
154
fn 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.
169
fn 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
185
fn 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
202
fn 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)]
241
struct SplineModeText;
242
243
/// Marker component for the text node that displays the current [`CyclingMode`].
244
#[derive(Component)]
245
struct CyclingModeText;
246
247
fn 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
262
fn 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)]
286
struct MouseEditMove {
287
start: Option<Vec2>,
288
}
289
290
/// The current mouse position, if known.
291
#[derive(Clone, Default, Resource)]
292
struct MousePosition(Option<Vec2>);
293
294
/// Update the current cursor position and track it in the [`MousePosition`] resource.
295
fn handle_mouse_move(
296
mut cursor_events: EventReader<CursorMoved>,
297
mut mouse_position: ResMut<MousePosition>,
298
) {
299
if let Some(cursor_event) = cursor_events.read().last() {
300
mouse_position.0 = Some(cursor_event.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.
306
fn handle_mouse_press(
307
mut button_events: EventReader<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 button_event in button_events.read() {
319
if button_event.button != MouseButton::Left {
320
continue;
321
}
322
323
match button_event.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`].
362
fn 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.
392
fn 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
}
420
421