1use 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 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 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 commands.insert_resource(MousePosition::default());
67 commands.insert_resource(MouseEditMove::default());
68
69 commands.spawn(Camera2d);
70
71 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#[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#[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#[derive(Clone, Default, Resource)]
141struct Curve(Option<CubicCurve<Vec2>>);
142
143#[derive(Clone, Resource)]
146struct ControlPoints {
147 points_and_tangents: Vec<(Vec2, Vec2)>,
148}
149
150fn 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
167fn draw_curve(curve: Res<Curve>, mut gizmos: Gizmos) {
170 let Some(ref curve) = curve.0 else {
171 return;
172 };
173 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
181fn 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
199fn 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#[derive(Component)]
241struct SplineModeText;
242
243#[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#[derive(Clone, Default, Resource)]
286struct MouseEditMove {
287 start: Option<Vec2>,
288}
289
290#[derive(Clone, Default, Resource)]
292struct MousePosition(Option<Vec2>);
293
294fn 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
304fn 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 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 continue;
328 }
329 edit_move.start = Some(mouse_pos);
331 }
332
333 ButtonState::Released => {
334 let Some(start) = edit_move.start else {
336 continue;
337 };
338
339 let (camera, camera_transform) = *camera;
340
341 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 control_points.points_and_tangents.push((point, tangent));
353
354 edit_move.start = None;
356 }
357 }
358 }
359}
360
361fn 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 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
391fn 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 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 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 if keyboard.just_pressed(KeyCode::KeyR) {
417 control_points.points_and_tangents.pop();
418 }
419}