Skip to main content

traaains/editor/signal/
mod.rs

1//! Implements placing a signal to a specific segment.
2
3use bevy::{
4    app::{Plugin, Update},
5    ecs::{
6        component::Component,
7        entity::Entity,
8        query::{With, Without},
9        resource::Resource,
10        schedule::IntoScheduleConfigs,
11        system::{Commands, Query, Res, ResMut, Single},
12    },
13    input::{
14        ButtonInput, common_conditions::input_just_pressed, keyboard::KeyCode, mouse::MouseButton,
15    },
16    math::Vec3,
17    mesh::Mesh2d,
18    sprite_render::MeshMaterial2d,
19    state::state::{NextState, OnEnter, OnExit},
20    transform::components::Transform,
21    ui::widget::Text,
22};
23use bezier::Bezier;
24use shapes::{Derivable, lerp::Lerp};
25
26use crate::{
27    AssetType, AssetsManager,
28    editor::{EditorState, StateText},
29    game::cursor::CursorPos,
30    sim::{
31        signal::{self, SignalPos, Signals},
32        track::{
33            pos::{IgnoreTrackPosUpdate, TrackPos},
34            segment::{TrackSegment, signals_manager::SignalsManager},
35        },
36    },
37    ui::UiState,
38    utils::Dir,
39};
40
41/// Tracks to which track we want to place the signal.
42#[derive(Resource)]
43pub struct TargetSegment(pub Entity);
44
45/// Simple indicator of the current location where the signal will be placed.
46#[derive(Component)]
47pub struct PositionIndicator;
48
49/// Updates [TrackPos] of [PositionIndicator] to the current projection of the cursor position to the target segment.
50pub fn update_pos_indicator(
51    mut pos_indicator: Single<(&mut TrackPos, &mut Transform), With<PositionIndicator>>,
52    q_segments: Query<(&TrackSegment, &Transform), Without<PositionIndicator>>,
53    target_segment: Res<TargetSegment>,
54    cursor_pos: Res<CursorPos>,
55) {
56    let (ts, ts_transform) = q_segments.get(target_segment.0).unwrap();
57    let segment_pos_vec2 = ts_transform.translation.truncate();
58    let proj = ts.shape.project(cursor_pos.0 - segment_pos_vec2);
59    pos_indicator.0.idx = proj;
60
61    let signal_trans = segment_pos_vec2 + ts.shape.lerp(proj);
62    let normal = ts.shape.derivative().lerp(proj).normalize().perp()
63        * signal::TRACK_OFFSET
64        * pos_indicator.0.dir.unwrap().sig_f32()
65        * -1.0;
66    pos_indicator.1.translation = (signal_trans + normal).extend(0.0);
67}
68
69/// Places the signal on left click.
70pub fn place_signal(
71    mut cmd: Commands,
72    pos_indicator: Single<&TrackPos, With<PositionIndicator>>,
73    mut q_segments: Query<&mut SignalsManager>,
74    target_segment: Res<TargetSegment>,
75    assets_manager: Res<AssetsManager>,
76    mut next_editor_state: ResMut<NextState<EditorState>>,
77) {
78    let mut ts_signals = q_segments.get_mut(target_segment.0).unwrap();
79
80    let signal_idx = SignalPos(pos_indicator.idx);
81    if ts_signals.inner.contains_key(&signal_idx) {
82        return; // currently multiple signals with same positions are not supported
83    }
84
85    let spawned_signal = cmd
86        .spawn((Signals::Stop, **pos_indicator, Transform::default()))
87        .with_child((
88            Transform::from_translation(Vec3::new(0.0, signal::TRACK_OFFSET, 0.0)),
89            Mesh2d(
90                assets_manager
91                    .meshes
92                    .get(&AssetType::SignalNormal)
93                    .unwrap()
94                    .clone(),
95            ),
96            MeshMaterial2d(
97                assets_manager
98                    .materials
99                    .get(&AssetType::SignalNormal)
100                    .unwrap()
101                    .clone(),
102            ),
103        ))
104        .id();
105    ts_signals.inner.insert(signal_idx, spawned_signal);
106
107    next_editor_state.set(EditorState::Viewing);
108}
109
110/// Handles switching inputs for changing the side of the track where the signal is going to be placed and canceling the operation.
111pub fn input_handler(
112    input: Res<ButtonInput<KeyCode>>,
113    mut pos_indicator: Single<&mut TrackPos, With<PositionIndicator>>,
114    mut next_editor_state: ResMut<NextState<EditorState>>,
115) {
116    if input.just_pressed(KeyCode::KeyR) {
117        pos_indicator.dir = Some(pos_indicator.dir.as_ref().unwrap().reverse());
118    }
119
120    if input.just_pressed(KeyCode::Escape) {
121        next_editor_state.set(EditorState::Viewing);
122    }
123}
124
125/// Handles going into [EditorState::Signal], spawning the position indicator and changing the state text.
126pub fn on_enter_signal(
127    mut cmd: Commands,
128    mut text: Single<&mut Text, With<StateText>>,
129    target_segment: Res<TargetSegment>,
130    assets_manager: Res<AssetsManager>,
131) {
132    cmd.spawn((
133        PositionIndicator,
134        TrackPos {
135            segment: target_segment.0,
136            idx: 0.0,
137            dir: Some(Dir::Positive),
138        },
139        IgnoreTrackPosUpdate,
140        Mesh2d(
141            assets_manager
142                .meshes
143                .get(&AssetType::Indicator)
144                .cloned()
145                .unwrap(),
146        ),
147        MeshMaterial2d(
148            assets_manager
149                .materials
150                .get(&AssetType::Indicator)
151                .cloned()
152                .unwrap(),
153        ),
154    ));
155
156    text.0 = String::from("Placing signal\n[R] Change track orientation\n[Esc] Exit");
157}
158
159/// Handles going out of [EditorState::Signal], removing the position indicator and going back to the segment's window.
160pub fn on_exit_signal(
161    mut cmd: Commands,
162    pos_indicator: Single<Entity, With<PositionIndicator>>,
163    target_segment: Res<TargetSegment>,
164    mut next_state: ResMut<NextState<UiState>>,
165) {
166    cmd.entity(*pos_indicator).despawn();
167    cmd.remove_resource::<TargetSegment>();
168
169    next_state.set(UiState::SegmentWindow {
170        target: target_segment.0,
171    });
172}
173
174/// Implements placing a signal to a specific segment.
175pub struct SignalEditorPlugin;
176
177impl Plugin for SignalEditorPlugin {
178    fn build(&self, app: &mut bevy::app::App) {
179        app.add_systems(OnEnter(EditorState::Signal), on_enter_signal)
180            .add_systems(OnExit(EditorState::Signal), on_exit_signal)
181            .add_systems(Update, input_handler)
182            .add_systems(Update, update_pos_indicator)
183            .add_systems(
184                Update,
185                place_signal.run_if(input_just_pressed(MouseButton::Left)),
186            );
187    }
188}