1use 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 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 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 commands.insert_resource(MousePosition::default());
68 commands.insert_resource(MouseEditMove::default());
69
70 commands.spawn(Camera2d);
71
72 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#[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#[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#[derive(Clone, Default, Resource)]
142struct Curve(Option<CubicCurve<Vec2>>);
143
144#[derive(Clone, Resource)]
147struct ControlPoints {
148 points_and_tangents: Vec<(Vec2, Vec2)>,
149}
150
151fn 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
168fn draw_curve(curve: Res<Curve>, mut gizmos: Gizmos) {
171 let Some(ref curve) = curve.0 else {
172 return;
173 };
174 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
182fn 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
200fn 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#[derive(Component)]
242struct SplineModeText;
243
244#[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#[derive(Clone, Default, Resource)]
287struct MouseEditMove {
288 start: Option<Vec2>,
289}
290
291#[derive(Clone, Default, Resource)]
293struct MousePosition(Option<Vec2>);
294
295fn 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
305fn 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 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 continue;
329 }
330 edit_move.start = Some(mouse_pos);
332 }
333
334 ButtonState::Released => {
335 let Some(start) = edit_move.start else {
337 continue;
338 };
339
340 let (camera, camera_transform) = *camera;
341
342 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 control_points.points_and_tangents.push((point, tangent));
354
355 edit_move.start = None;
357 }
358 }
359 }
360}
361
362fn 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 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
392fn 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 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 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 if keyboard.just_pressed(KeyCode::KeyR) {
418 control_points.points_and_tangents.pop();
419 }
420}