Skip to main content

traaains/editor/train/placing/
mod.rs

1use bevy::{
2    app::{App, Plugin, Update},
3    camera::visibility::Visibility,
4    ecs::{
5        component::Component,
6        entity::Entity,
7        query::{With, Without},
8        resource::Resource,
9        schedule::IntoScheduleConfigs,
10        system::{Commands, If, Query, Res, ResMut, Single},
11    },
12    input::{
13        ButtonInput, common_conditions::input_just_pressed, keyboard::KeyCode, mouse::MouseButton,
14    },
15    log::tracing,
16    math::{Quat, Vec2},
17    state::{
18        app::AppExtStates,
19        condition::in_state,
20        state::{NextState, OnEnter, OnExit, State},
21    },
22    transform::components::Transform,
23    ui::widget::Text,
24};
25use shapes::{Circle, Intersect, lerp::Lerp};
26
27use crate::{
28    editor::{
29        StateText,
30        train::{
31            TrainEditorState,
32            placing::states::{
33                color_virtual_carriages_green, color_virtual_carriages_red, on_enter_placing_train,
34                on_exit_placing_train,
35                placed_state::{PlacedTrainState, place_train},
36            },
37        },
38    },
39    sim::{
40        track::{
41            node::{ConnectionLocation, TrackNode},
42            pos::TrackPos,
43            segment::TrackSegment,
44        },
45        train::{COUPLER_LENGTH, CarriageBogies, DOUBLE_BOGIE_IDX, SINGLE_BOGIE_IDX},
46    },
47    utils::Dir,
48};
49
50use self::{
51    states::hovering::{HoveringTrack, IsHoveringTrack, on_enter_hovering, on_exit_hovering},
52    templates::CarriageTemplate,
53};
54
55mod observers;
56mod states;
57pub mod templates;
58
59#[derive(Resource)]
60struct CurrentPlacingDir(Dir);
61
62/// Stores the template of the currently placing train. Used to create the virtual pieces and later the actually placed ones.
63#[derive(Clone, Resource)]
64pub struct CurrentPlacingTrain {
65    pub carriages: Vec<CarriageTemplate>,
66}
67
68// Default train when activating this editor
69impl Default for CurrentPlacingTrain {
70    fn default() -> Self {
71        // random default train
72        Self {
73            carriages: vec![CarriageTemplate::default(); 2],
74        }
75    }
76}
77
78/// Marker component grouping all of the virtual pieces of the trains together. The virtual pieces store only the information
79/// needed to provide visual feedback to the user, all other information is stored in [CurrentPlacingTrain].
80#[derive(Component, Default)]
81struct Virtual;
82
83#[derive(Component)]
84#[require(Virtual)]
85struct VirtualTrain {
86    carriages: Vec<Entity>,
87}
88
89#[derive(Component)]
90#[require(Virtual)]
91struct VirtualCarriage {
92    length: f32,
93    bogies: CarriageBogies,
94}
95
96#[derive(Component)]
97#[require(Virtual)]
98struct VirtualBogie;
99
100/// Handles switching the direction of the train.
101fn input_handler(input: Res<ButtonInput<KeyCode>>, mut placing_dir: ResMut<CurrentPlacingDir>) {
102    if input.just_pressed(KeyCode::KeyR) {
103        match placing_dir.0 {
104            Dir::Positive => placing_dir.0 = Dir::Negative,
105            Dir::Negative => placing_dir.0 = Dir::Positive,
106        }
107    }
108}
109
110fn get_intersection(ints: impl Iterator<Item = (f32, Vec2)>, dir: Dir) -> Option<(f32, Vec2)> {
111    match dir {
112        Dir::Positive => ints.min_by(|a, b| a.0.partial_cmp(&b.0).unwrap()),
113        Dir::Negative => ints.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap()),
114    }
115}
116
117fn get_filtered_intersection(
118    ints: impl Iterator<Item = (f32, Vec2)>,
119    dir: Dir,
120    curr_idx: f32,
121) -> Option<(f32, Vec2)> {
122    get_intersection(
123        ints.filter(|(int_idx, _)| match dir {
124            Dir::Positive => *int_idx > curr_idx,
125            Dir::Negative => *int_idx < curr_idx,
126        }),
127        dir,
128    )
129}
130
131/// Function which tries to move n meters in global space along active connections.
132fn move_n_meters_along_active(
133    n_meters: f32,
134    curr_idx: f32,
135    curr_segment_entity: Entity,
136    curr_dir: Dir,
137    q_segments: &Query<(Entity, &TrackSegment, &Transform)>,
138    q_nodes: &Query<(Entity, &TrackNode)>,
139) -> Option<(Entity, f32, Dir)> {
140    let curr_segment = q_segments.get(curr_segment_entity).unwrap();
141    let curr_curve_point = curr_segment.1.shape.lerp(curr_idx);
142    let offset_circle = Circle::new(curr_curve_point, n_meters);
143
144    tracing::debug!(
145        "moving from segment {} and idx {}",
146        curr_segment.0,
147        curr_idx
148    );
149    // active segment refers to the segment we are currently trying to intersect to find a viable position
150    let mut active_segment = Some(curr_segment);
151    let mut active_dir = Some(curr_dir);
152
153    while let Some(r#as) = active_segment
154        && let Some(ad) = active_dir
155    {
156        // get vector by which we want to translate everything
157        let curr_offset = r#as.2.translation.truncate() - curr_segment.2.translation.truncate();
158        // curr segment is treated as world origin for the moment, which means that we need to translate the active shape
159        let mut active_shape = r#as.1.shape.clone();
160        active_shape.translate_by(curr_offset);
161
162        // the segment isn't always a straight line and thus all of the possible points where the bogie mount can be is a circle
163        let intersections = active_shape.intersections(&offset_circle);
164        tracing::debug!(
165            "intersections (with segment {} {:?}, circle {:?}): {:?}",
166            r#as.0,
167            active_shape,
168            offset_circle,
169            intersections
170        );
171
172        let intersection = intersections.and_then(|ints| {
173            if r#as.0 == curr_segment.0 {
174                return get_filtered_intersection(ints.into_iter(), curr_dir, curr_idx);
175            }
176
177            get_intersection(ints.into_iter(), curr_dir)
178        });
179
180        match intersection {
181            // we have found an intersection and thus can happily place the bogie
182            Some((int_idx, _)) => return Some((r#as.0, int_idx, ad)),
183            // no intersection was found, try to get next active segment and try again
184            None => {
185                tracing::debug!("no intersection found, trying again");
186                let ac = q_nodes
187                    .get(match curr_dir {
188                        Dir::Positive => r#as.1.to_node,
189                        Dir::Negative => r#as.1.from_node,
190                    })
191                    .unwrap()
192                    .1
193                    .connections
194                    .get_active_for(r#as.0);
195
196                active_segment = ac.map(|ac| q_segments.get(ac.segment).unwrap());
197                active_dir = ac.map(|ac| match ac.location {
198                    ConnectionLocation::Start => Dir::Positive,
199                    ConnectionLocation::End => Dir::Negative,
200                });
201            }
202        }
203    }
204
205    tracing::debug!("no active connection found, cannot continue");
206    None
207}
208
209/// Tries to find and calculate `n` (length of `spacings`) points along active track starting at segment and idx, moving along dir.
210fn get_n_points_along_active_track(
211    spacings: &[f32],
212    segment: Entity,
213    idx: f32,
214    dir: Dir,
215    q_segments: &Query<(Entity, &TrackSegment, &Transform)>,
216    q_nodes: &Query<(Entity, &TrackNode)>,
217) -> Option<Vec<(Entity, f32, Dir, Vec2)>> {
218    let mut curr_segment = q_segments.get(segment).unwrap();
219    let mut curr_dir = dir;
220    let mut curr_idx = idx;
221
222    let mut points = Vec::with_capacity(spacings.len());
223    points.push((segment, idx, dir, curr_segment.1.shape.lerp(idx)));
224
225    for spacing in spacings {
226        match move_n_meters_along_active(
227            *spacing,
228            curr_idx,
229            curr_segment.0,
230            curr_dir,
231            q_segments,
232            q_nodes,
233        ) {
234            Some((final_segment_ent, final_idx, final_dir)) => {
235                curr_segment = q_segments.get(final_segment_ent).unwrap();
236                curr_idx = final_idx;
237                curr_dir = final_dir;
238
239                points.push((
240                    final_segment_ent,
241                    curr_idx,
242                    curr_dir,
243                    curr_segment.1.shape.lerp(curr_idx),
244                ));
245            }
246            // this whole thing failed, need to find all points, exit
247            None => return None,
248        }
249    }
250
251    Some(points)
252}
253
254/// System which updates virtual train's bogies positions according to head train position
255fn update_placing_train(
256    mut cmd: Commands,
257    q_train: Single<(&VirtualTrain, &TrackPos), Without<VirtualBogie>>,
258    q_carriages: Query<&VirtualCarriage>,
259    mut q_bogies: Query<(Entity, &mut TrackPos), With<VirtualBogie>>,
260    q_segments: Query<(Entity, &TrackSegment, &Transform)>,
261    q_nodes: Query<(Entity, &TrackNode)>,
262    mut next_state: ResMut<NextState<PlacedTrainState>>,
263) {
264    // curr_segment and friends refer to the segment where the last bogie was placed and from which we are trying to move
265    let curr_segment = q_segments.get(q_train.1.segment).unwrap();
266    let curr_dir = match q_train.1.dir.as_ref().unwrap() {
267        Dir::Positive => Dir::Negative,
268        Dir::Negative => Dir::Positive,
269    };
270    let curr_idx = q_train.1.idx;
271
272    let mut spacings = Vec::new();
273    for (i, carriage) in q_train.0.carriages.iter().enumerate() {
274        let carriage = q_carriages.get(*carriage).unwrap();
275
276        match carriage.bogies {
277            CarriageBogies::Single(_) => {
278                spacings.push(carriage.length * SINGLE_BOGIE_IDX);
279                spacings.push(carriage.length * SINGLE_BOGIE_IDX);
280            }
281            CarriageBogies::Double { front: _, back: _ } => {
282                spacings.push(carriage.length * DOUBLE_BOGIE_IDX.0);
283                spacings.push(carriage.length * (DOUBLE_BOGIE_IDX.1 - DOUBLE_BOGIE_IDX.0));
284                spacings.push(carriage.length * (1.0 - DOUBLE_BOGIE_IDX.1));
285            }
286        }
287
288        if i != q_train.0.carriages.len() - 1 {
289            spacings.push(COUPLER_LENGTH);
290            spacings.push(COUPLER_LENGTH);
291        }
292    }
293
294    match get_n_points_along_active_track(
295        &spacings,
296        curr_segment.0,
297        curr_idx,
298        curr_dir,
299        &q_segments,
300        &q_nodes,
301    ) {
302        Some(points) => {
303            let mut points = points.into_iter();
304            // starting at front of train, so move one
305            points.next();
306
307            for carriage_ent in q_train.0.carriages.iter() {
308                let carriage = q_carriages.get(*carriage_ent).unwrap();
309
310                match carriage.bogies {
311                    CarriageBogies::Single(entity) => {
312                        let (_, mut bogie_pos) = q_bogies.get_mut(entity).unwrap();
313
314                        let target = points.next().unwrap();
315                        bogie_pos.segment = target.0;
316                        bogie_pos.idx = target.1;
317                        bogie_pos.dir = Some(target.2.reverse());
318
319                        // move to back of train, then two couplers to prepare next carriage
320                        points.next();
321                        points.next();
322                        points.next();
323                    }
324                    CarriageBogies::Double { front, back } => {
325                        let bogies = q_bogies.get_many_mut([front, back]).unwrap();
326
327                        for mut bogie in bogies {
328                            let target = points.next().unwrap();
329                            bogie.1.segment = target.0;
330                            bogie.1.idx = target.1;
331                            // when traversing the connections, we go towards the back of the train,
332                            // but the bogies should be facing towards the front
333                            bogie.1.dir = Some(target.2.reverse());
334                        }
335
336                        // move to back of train, then two couplers to prepare next carriage
337                        points.next();
338                        points.next();
339                        points.next();
340                    }
341                }
342
343                cmd.entity(*carriage_ent).insert(Visibility::Visible);
344            }
345
346            next_state.set(PlacedTrainState::Valid);
347        }
348        None => {
349            for carriage in q_train.0.carriages.iter() {
350                cmd.entity(*carriage).insert(Visibility::Hidden);
351            }
352
353            next_state.set(PlacedTrainState::Invalid);
354        }
355    }
356}
357
358fn move_virtual_carriages_to_bogies(
359    q_carriages: Query<(&VirtualCarriage, &mut Transform), Without<VirtualBogie>>,
360    q_bogies: Query<&Transform, With<VirtualBogie>>,
361) {
362    for (carriage, mut transform) in q_carriages {
363        match &carriage.bogies {
364            CarriageBogies::Single(bogie) => {
365                *transform = *q_bogies.get(*bogie).unwrap();
366            }
367            CarriageBogies::Double { front, back } => {
368                let front_trans = *q_bogies.get(*front).unwrap();
369                let back_trans = *q_bogies.get(*back).unwrap();
370
371                transform.translation = (front_trans.translation + back_trans.translation) / 2.0;
372                let dir = front_trans.translation.truncate() - back_trans.translation.truncate();
373                transform.rotation = Quat::from_rotation_z(f32::atan2(dir.y, dir.x));
374            }
375        }
376    }
377}
378
379fn update_state_text(
380    mut text: Single<&mut Text, With<StateText>>,
381    current_dir: If<Res<CurrentPlacingDir>>,
382    valid: Option<Res<State<PlacedTrainState>>>,
383) {
384    text.0 = format!(
385        "Placing train\n[Esc] to exit\nCurrent direction: {}\nCurrent state: {}",
386        match current_dir.0.0 {
387            Dir::Positive => "Positive",
388            Dir::Negative => "Negative",
389        },
390        valid
391            .map(|v| v.get().to_string())
392            .unwrap_or(String::from("-"))
393    );
394}
395
396fn register_state_handlers(app: &mut App) {
397    app.add_sub_state::<HoveringTrack>()
398        .add_computed_state::<IsHoveringTrack>()
399        .add_sub_state::<PlacedTrainState>()
400        .add_systems(
401            OnEnter(TrainEditorState::PlacingTrain),
402            on_enter_placing_train,
403        )
404        .add_systems(
405            OnExit(TrainEditorState::PlacingTrain),
406            on_exit_placing_train,
407        )
408        .add_systems(OnEnter(IsHoveringTrack), on_enter_hovering)
409        .add_systems(OnExit(IsHoveringTrack), on_exit_hovering)
410        .add_systems(
411            OnEnter(PlacedTrainState::Valid),
412            color_virtual_carriages_green,
413        )
414        .add_systems(
415            OnEnter(PlacedTrainState::Invalid),
416            color_virtual_carriages_red,
417        );
418}
419
420pub struct PlacingTrainPlugin;
421
422impl Plugin for PlacingTrainPlugin {
423    fn build(&self, app: &mut App) {
424        register_state_handlers(app);
425
426        app.add_systems(
427            Update,
428            (
429                input_handler,
430                update_state_text,
431                update_placing_train,
432                move_virtual_carriages_to_bogies,
433            )
434                .run_if(in_state(TrainEditorState::PlacingTrain)),
435        )
436        .add_systems(
437            Update,
438            place_train
439                .after(update_placing_train)
440                .run_if(in_state(PlacedTrainState::Valid))
441                .run_if(input_just_pressed(MouseButton::Left)),
442        );
443    }
444}