├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── i18n.toml ├── i18n ├── en │ └── kson_editor.ftl └── sv │ └── kson_editor.ftl ├── kson-music-playback ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── src ├── action_stack.rs ├── assets │ ├── mod.rs │ ├── shaders │ │ ├── button_frag.glsl │ │ ├── button_vert.glsl │ │ ├── color_mesh_frag.glsl │ │ ├── color_mesh_vert.glsl │ │ ├── holdbutton_frag.glsl │ │ ├── holdbutton_vert.glsl │ │ ├── laser_frag.glsl │ │ ├── laser_vert.glsl │ │ ├── track_frag.glsl │ │ └── track_vert.glsl │ └── textures │ │ ├── button.png │ │ ├── buttonhold.png │ │ ├── fxbutton.png │ │ ├── fxbuttonhold.png │ │ ├── laser.png │ │ └── track.png ├── camera_widget.rs ├── chart_camera.rs ├── chart_editor.rs ├── i18n.rs ├── main.rs ├── tools │ ├── bpm_ts.rs │ ├── buttons.rs │ ├── camera.rs │ ├── laser.rs │ └── mod.rs └── utils.rs └── track_test.png /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build_windows: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/cache@v2 17 | with: 18 | path: | 19 | ~/.cargo/registry 20 | ~/.cargo/git 21 | target 22 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 23 | - name: Build 24 | run: rustup update && cargo build --release 25 | - name: Upload artifact 26 | uses: actions/upload-artifact@master 27 | with: 28 | name: editor_windows 29 | path: target/release/kson-editor.exe 30 | 31 | build_linux: 32 | 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: install packages 38 | run: | 39 | sudo apt update 40 | sudo apt install -y libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libasound2-dev 41 | - uses: actions/cache@v2 42 | with: 43 | path: | 44 | ~/.cargo/registry 45 | ~/.cargo/git 46 | target 47 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 48 | - name: Build 49 | run: rustup update && cargo build --release 50 | - name: Upload artifact 51 | uses: actions/upload-artifact@master 52 | with: 53 | name: editor_linux_x86_64 54 | path: target/release/kson-editor 55 | 56 | build_macos: 57 | 58 | runs-on: macos-latest 59 | 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions/cache@v2 63 | with: 64 | path: | 65 | ~/.cargo/registry 66 | ~/.cargo/git 67 | target 68 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 69 | - name: Build 70 | run: rustup update && cargo build --release 71 | - name: Upload artifact 72 | uses: actions/upload-artifact@master 73 | with: 74 | name: editor_macos 75 | path: target/release/kson-editor 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/* 2 | .vscode/* 3 | imgui.ini 4 | profiling.json 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name="kson-editor" 3 | version="0.1.0" 4 | edition = "2018" 5 | authors = ["Emil Draws "] 6 | 7 | [dependencies] 8 | serde = { version = "1.0", features = ["derive"] } 9 | serde_json = "1.0" 10 | regex = "1.3.1" 11 | libmath = "0.2.1" 12 | puffin = "0.13" 13 | puffin_http = "0.10" 14 | rodio = { version = "0.17.1" } 15 | serde_cbor = "0.11.1" 16 | kson = { git = "https://github.com/Drewol/kson-rs.git"} 17 | kson-audio = { git = "https://github.com/Drewol/kson-rs.git" } 18 | directories-next = "2.0.0" 19 | nfd = { git = "https://github.com/SpaceManiac/nfd-rs.git", branch = "zenity" } 20 | anyhow = "1" 21 | log = "0.4.14" 22 | env_logger = "0.9" 23 | emath = "0.18" 24 | glam = "0.20" 25 | once_cell = "1.10.0" 26 | eframe = {version = "0.18", features = ["persistence"]} 27 | egui_glow = "0.18.1" 28 | image = {version ="0.24", default_features = false, features = ["png"]} 29 | bytemuck = "1.9.1" 30 | tracing = {version = "0.1.34", features = ["log-always"]} 31 | i18n-embed-fl = "0.6.4" 32 | i18n-embed = { version = "0.13.4", features = ["fluent-system"] } 33 | rust-embed = "6.4.1" 34 | kson-music-playback = {path = "./kson-music-playback"} 35 | [features] 36 | profiling = ["eframe/puffin"] 37 | 38 | [workspace] 39 | members = [".", "./kson-music-playback"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Emil Draws 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Moved to kson-rs monorepo: https://github.com/Drewol/kson-rs 2 | -------------------------------------------------------------------------------- /i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | [fluent] 3 | assets_dir = "i18n" -------------------------------------------------------------------------------- /i18n/en/kson_editor.ftl: -------------------------------------------------------------------------------- 1 | yes=Yes 2 | no=No 3 | cancel=Cancel 4 | ok=Ok 5 | unsaved_changes_alert=There are unsaved changes, save changes before closing? 6 | title=Title 7 | artist=Artist 8 | effector=Effector 9 | jacket=Jacket 10 | jacket_artist=Jacket Artist 11 | undo=Undo: {$action} 12 | redo=Redo: {$action} 13 | file=File 14 | new=New 15 | open=Open 16 | save=Save 17 | save_as=Save As 18 | export_ksh=Export Ksh 19 | exit=Exit 20 | edit=Edit 21 | remove_note=Remove {$lane} note 22 | add_fx=Add {$side} FX Note 23 | add_bt=Add BT-{$lane} Note 24 | difficulty=Difficulty 25 | level=Level 26 | name=Name 27 | add_bpm_change=Add BPM Change 28 | edit_bpm_change=Edit BPM Change 29 | change_bpm=Change BPM 30 | remove_bpm_change=Remove BPM Change 31 | remove_time_signature_change=Remove Time Signature Change 32 | add_time_signature_change=Add Time Signature Change 33 | edit_time_signature_change=Edit Time Signature Change 34 | change_time_signature=Change Time Signature 35 | unsaved_changes=Unsaved Changes 36 | preferences=Preferences 37 | metadata=Metadata 38 | update_metadata=Update Metadata 39 | music_info=Music Info 40 | update_music_info=Update Music Info 41 | track_width=Track width 42 | beats_per_col=Beats per column 43 | hotkeys=Hotkeys 44 | reset_to_default=Reset to default 45 | radius=Radius 46 | angle=Angle 47 | camera=Camera 48 | display_line=Visa linje 49 | edit_curve_for_camera=Edit curve for camera {$graph}. 50 | add_control_point=Add Control Point 51 | added_camera_control_point=Added camera control point 52 | add_laser=Add {$side} Laser 53 | adjust_laser_curve=Adjust {$side} Laser Curve 54 | remove_laser=Remove {$side} laser 55 | short_name=Short Name 56 | index=Index 57 | filename=Filename: 58 | audio_file=Audio File: 59 | destination_folder=Destination folder (audio folder will be used if empty): 60 | offset=Offset 61 | volume=Volume 62 | preview_offset=Preview Offset 63 | preview_duration=Preview Duration 64 | left=Left 65 | right=Right -------------------------------------------------------------------------------- /i18n/sv/kson_editor.ftl: -------------------------------------------------------------------------------- 1 | yes=Ja 2 | no=Nej 3 | cancel=Avbryt 4 | ok=Ok 5 | unsaved_changes_alert=Det finns osparade ändringar, spara innan programmet avslutas? 6 | title=Titel 7 | artist=Artist 8 | effector=Effektsättare 9 | jacket=Omslag 10 | jacket_artist=Omslagsartist 11 | undo=Ångra: {$action} 12 | redo=Gör om: {$action} 13 | file=Arkiv 14 | new=Ny 15 | open=Öppna 16 | save=Spara 17 | save_as=Spara som 18 | export_ksh=Exportera Ksh 19 | preferences=Inställningar 20 | exit=Avsluta 21 | edit=Redigera 22 | remove_note=Ta bort {$lane} not 23 | add_fx=Skapa {$side} FX Not 24 | add_bt=Skapa BT-{$lane} Not 25 | difficulty=Svårighetsgrad 26 | level=Level 27 | name=Namn 28 | add_bpm_change=Skapa BPM Ändring 29 | edit_bpm_change=Justera BPM Ändring 30 | change_bpm=Ändra BPM 31 | remove_bpm_change=Radera BPM Ändring 32 | remove_time_signature_change=Radera Taktartsangivelseändring 33 | add_time_signature_change=Skapa Taktartsangivelseändring 34 | edit_time_signature_change=Justera Taktartsangivelseändring 35 | change_time_signature=Ändra Taktartsangivelse 36 | music_info=Musikinfo 37 | unsaved_changes=Osparade ändringar 38 | metadata=Metadata 39 | update_metadata=Uppdatera Metadata 40 | update_music_info=Uppdatera musikinfo 41 | track_width=Spårbredd 42 | beats_per_col=Takter per kolumn 43 | hotkeys=Hotkeys 44 | reset_to_default=Återställ till orginalvärden 45 | radius=Radie 46 | angle=Vinkel 47 | camera=Kamera 48 | display_line=Display Line 49 | edit_curve_for_camera=Justera kurva för kamera {$graph}. 50 | add_control_point=Skapa kontrollpunkt 51 | added_camera_control_point=Skapade kamerakontrollpunkt 52 | add_laser=Skapa {$side} Laser 53 | adjust_laser_curve=Justera {$side} Laser Kurva 54 | remove_laser=Radera {$side} laser 55 | short_name=Förkortning 56 | index=Index 57 | filename=Filnamn: 58 | audio_file=Ljudfil: 59 | destination_folder=Projektmapp (ljudfilens mapp kommer användas om tom): 60 | offset=Förskjutning 61 | volume=Volym 62 | preview_offset=Förhandsgranskningsförskjutning 63 | preview_duration=Förhandsgranskningslängd 64 | left=Vänster 65 | right=Höger -------------------------------------------------------------------------------- /kson-music-playback/.gitignore: -------------------------------------------------------------------------------- 1 | target/* 2 | .vscode/* 3 | imgui.ini 4 | profiling.json 5 | Cargo.lock -------------------------------------------------------------------------------- /kson-music-playback/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kson-music-playback" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | rodio = "*" 10 | kson = { git = "https://github.com/Drewol/kson-rs.git"} 11 | kson-audio = { git = "https://github.com/Drewol/kson-rs.git" } 12 | anyhow = "*" -------------------------------------------------------------------------------- /kson-music-playback/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use kson::effects::*; 3 | use kson::parameter::EffectParameter; 4 | use kson::parameter::*; 5 | use kson::{Chart, GraphSectionPoint}; 6 | use kson_audio::Dsp; 7 | use kson_audio::*; 8 | pub use rodio::Source; 9 | use rodio::*; 10 | use std::fmt::Debug; 11 | use std::fs::File; 12 | use std::io::{BufReader, Read, Seek}; 13 | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 14 | use std::sync::{Arc, Mutex}; 15 | 16 | #[derive(Clone)] 17 | pub struct AudioFile { 18 | samples: Arc>>, 19 | sample_rate: u32, 20 | channels: u16, 21 | size: usize, 22 | pos: Arc, 23 | stopped: Arc, 24 | laser_dsp: Arc>>, 25 | fx_dsp: [Option>>; 2], 26 | fx_enable: [Arc; 2], 27 | } 28 | 29 | pub struct EventList { 30 | events: Vec<(u32, T)>, 31 | } 32 | 33 | impl EventList { 34 | pub fn update(&mut self, tick: &u32, f: &dyn Fn(&T)) { 35 | //while let in case of multiple events on a tick 36 | //or if multiple ticks passed since the last update. 37 | while let Some((event_tick, value)) = self.events.first() { 38 | if event_tick <= tick { 39 | f(value); 40 | self.events.remove(0); 41 | } else { 42 | return; 43 | } 44 | } 45 | } 46 | 47 | pub fn add(&mut self, tick: u32, value: T) { 48 | match self.events.binary_search_by(|t| t.0.cmp(&tick)) { 49 | Ok(index) => self.events.insert(index, (tick, value)), 50 | Err(index) => self.events.insert(index, (tick, value)), 51 | } 52 | } 53 | 54 | pub fn clear(&mut self) { 55 | self.events.clear(); 56 | } 57 | } 58 | 59 | impl Iterator for AudioFile { 60 | type Item = f32; 61 | 62 | fn next(&mut self) -> Option { 63 | { 64 | if self.stopped.load(Ordering::SeqCst) { 65 | return None; 66 | } 67 | } 68 | { 69 | let mut pos = self.pos.load(Ordering::SeqCst); 70 | let samples = self.samples.lock().unwrap(); 71 | 72 | if pos >= self.size { 73 | None 74 | } else { 75 | let mut v = *(*samples).get(pos).unwrap(); 76 | v *= 0.6; 77 | pos += 1; 78 | 79 | //apply DSPs 80 | for i in 0..2 { 81 | let d = &self.fx_dsp[i]; 82 | let en = &self.fx_enable[i]; 83 | if en.load(Ordering::SeqCst) { 84 | if let Some(d) = d { 85 | let mut d = d.lock().unwrap(); 86 | d.process(&mut v, pos % self.channels as usize); 87 | } 88 | } 89 | } 90 | 91 | //apply Laser DSP 92 | { 93 | let mut laser = self.laser_dsp.lock().unwrap(); 94 | //(*laser).process(&mut v, pos % self.channels as usize); 95 | } 96 | self.pos.store(pos, Ordering::SeqCst); 97 | Some(v) 98 | } 99 | } 100 | } 101 | 102 | #[inline] 103 | fn size_hint(&self) -> (usize, Option) { 104 | (0, Some(self.size - 1)) 105 | } 106 | } 107 | 108 | impl Source for AudioFile { 109 | fn current_frame_len(&self) -> Option { 110 | let pos = self.pos.load(Ordering::SeqCst); 111 | if pos == self.size { 112 | Some(0) 113 | } else { 114 | Some(32.min(self.size - pos)) 115 | } 116 | } 117 | 118 | #[inline] 119 | fn channels(&self) -> u16 { 120 | self.channels 121 | } 122 | 123 | #[inline] 124 | fn sample_rate(&self) -> u32 { 125 | self.sample_rate 126 | } 127 | 128 | #[inline] 129 | fn total_duration(&self) -> Option { 130 | Some(std::time::Duration::from_secs_f64( 131 | (self.size / self.channels as usize) as f64 / self.sample_rate as f64, 132 | )) 133 | } 134 | } 135 | 136 | impl AudioFile { 137 | fn get_ms(&self) -> f64 { 138 | (self.pos.load(Ordering::SeqCst) / self.channels as usize) as f64 139 | / (self.sample_rate as f64 / 1000.0) 140 | } 141 | 142 | fn set_ms(&mut self, ms: f64) { 143 | let mut pos = ((ms / 1000.0) * (self.sample_rate * self.channels as u32) as f64) as usize; 144 | pos -= pos % self.channels as usize; 145 | self.pos.store(pos, Ordering::SeqCst); 146 | } 147 | 148 | fn set_stopped(&mut self, val: bool) { 149 | self.stopped.store(val, Ordering::SeqCst); 150 | } 151 | } 152 | type LaserFn = Box f32>; 153 | 154 | pub struct AudioPlayback { 155 | file: Option, 156 | last_file: String, 157 | laser_funcs: [Vec<(u32, u32, LaserFn)>; 2], 158 | laser_values: (Option, Option), 159 | } 160 | 161 | impl AudioPlayback { 162 | pub fn new() -> Self { 163 | AudioPlayback { 164 | file: None, 165 | last_file: String::new(), 166 | laser_funcs: [Vec::new(), Vec::new()], 167 | laser_values: (None, None), 168 | } 169 | } 170 | 171 | fn make_laser_fn( 172 | base_y: u32, 173 | p1: &GraphSectionPoint, 174 | p2: &GraphSectionPoint, 175 | ) -> Box f32> { 176 | let start_tick = (base_y + p1.ry) as f32; 177 | let end_tick = (base_y + p2.ry) as f32; 178 | let start_value = match p1.vf { 179 | Some(v) => v, 180 | None => p1.v, 181 | } as f32; 182 | let end_value = p2.v as f32; 183 | let value_delta = end_value - start_value; 184 | let length = end_tick - start_tick; 185 | let a = p1.a.unwrap_or(0.5); 186 | let b = p1.b.unwrap_or(0.5); 187 | if (start_value - end_value).abs() < f32::EPSILON { 188 | Box::new(move |_: f32| start_value) 189 | } else if (a - b).abs() > f64::EPSILON { 190 | Box::new(move |y: f32| { 191 | let x = ((y - start_tick) / length).min(1.0).max(0.0) as f64; 192 | start_value + value_delta * kson::do_curve(x, a, b) as f32 193 | }) 194 | } else { 195 | Box::new(move |y: f32| start_value + value_delta * ((y - start_tick) / length)) 196 | } 197 | } 198 | 199 | pub fn build_effects(&mut self, chart: &Chart) { 200 | for i in 0..2 { 201 | self.laser_funcs[i].clear(); 202 | for section in &chart.note.laser[i] { 203 | for se in section.segments() { 204 | let s = section.tick() + se[0].ry; 205 | let e = section.tick() + se[1].ry; 206 | self.laser_funcs[i].push(( 207 | s, 208 | e, 209 | AudioPlayback::make_laser_fn(section.tick(), &se[0], &se[1]), 210 | )); 211 | } 212 | } 213 | } 214 | } 215 | 216 | pub fn get_ms(&self) -> f64 { 217 | if let Some(file) = &self.file { 218 | file.get_ms() 219 | } else { 220 | 0.0 221 | } 222 | } 223 | 224 | pub fn get_tick(&self, chart: &Chart) -> f64 { 225 | if self.is_playing() { 226 | let ms = self.get_ms(); 227 | let offset = match &chart.audio.bgm { 228 | Some(bgm) => bgm.offset, 229 | None => 0, 230 | }; 231 | let ms = ms - offset as f64; 232 | chart.ms_to_tick(ms) as f64 233 | } else { 234 | 0.0 235 | } 236 | } 237 | 238 | pub fn is_playing(&self) -> bool { 239 | match &self.file { 240 | Some(f) => !f.stopped.load(Ordering::SeqCst), 241 | None => false, 242 | } 243 | } 244 | 245 | fn get_laser_value_at(&self, side: usize, tick: f64) -> Option { 246 | let utick = tick as u32; 247 | for (s, e, f) in &self.laser_funcs[side] { 248 | if utick < *s { 249 | return None; 250 | } 251 | if utick > *e { 252 | continue; 253 | } 254 | if utick <= *e && utick >= *s { 255 | let v = f(tick as f32); 256 | if side == 1 { 257 | return Some(1.0 - v); 258 | } else { 259 | return Some(v); 260 | } 261 | } 262 | } 263 | 264 | None 265 | } 266 | 267 | pub fn get_source(&self) -> Option { 268 | self.file.clone() 269 | } 270 | 271 | pub fn update(&mut self, tick: f64) { 272 | if !self.is_playing() { 273 | return; 274 | } 275 | 276 | self.laser_values = ( 277 | self.get_laser_value_at(0, tick), 278 | self.get_laser_value_at(1, tick), 279 | ); 280 | 281 | let dsp_value = match self.laser_values { 282 | (Some(v1), Some(v2)) => Some(v1.max(v2)), 283 | (Some(v), None) => Some(v), 284 | (None, Some(v)) => Some(v), 285 | (None, None) => None, 286 | }; 287 | 288 | if self.is_playing() { 289 | if let Some(file) = &mut self.file { 290 | if let Some(dsp_value) = dsp_value { 291 | let mut laser = file.laser_dsp.lock().unwrap(); 292 | laser.set_param_transition(dsp_value, true); 293 | } else { 294 | let mut laser = file.laser_dsp.lock().unwrap(); 295 | laser.set_param_transition(0.0, false); 296 | } 297 | } 298 | } 299 | } 300 | 301 | pub fn get_laser_values(&self) -> (Option, Option) { 302 | self.laser_values 303 | } 304 | 305 | pub fn play(&mut self) -> bool { 306 | if self.is_playing() { 307 | true 308 | } else { 309 | if let Some(file) = &mut self.file { 310 | file.set_stopped(false); 311 | return true; 312 | } 313 | 314 | false 315 | } 316 | } 317 | pub fn open(&mut self, source: impl Source, filename: &str) -> Result<()> { 318 | let rate = source.sample_rate(); 319 | let channels = source.channels(); 320 | let dataref: Arc>> = Arc::new(Mutex::new(source.collect())); 321 | let data = dataref.lock().unwrap(); 322 | 323 | let laser_dsp = kson_audio::dsp_from_definition(AudioEffect::PeakingFilter( 324 | kson::effects::PeakingFilter { 325 | freq: EffectParameter { 326 | off: EffectParameterValue::Freq(EffectFreq::Hz(100)..=EffectFreq::Hz(100)), 327 | ..Default::default() 328 | }, 329 | freq_max: EffectParameter { 330 | off: EffectParameterValue::Freq(EffectFreq::Hz(16000)..=EffectFreq::Hz(16000)), 331 | ..Default::default() 332 | }, 333 | q: EffectParameter { 334 | off: EffectParameterValue::Float(1.0..=1.0), 335 | ..Default::default() 336 | }, 337 | delay: EffectParameter { 338 | off: EffectParameterValue::Float(1.0..=1.0), 339 | ..Default::default() 340 | }, 341 | mix: EffectParameter { 342 | off: EffectParameterValue::Float(0.0..=0.0), 343 | on: Some(EffectParameterValue::Float(1.0..=1.0)), 344 | ..Default::default() 345 | }, 346 | ..Default::default() 347 | }, 348 | )); 349 | 350 | self.file = Some(AudioFile { 351 | size: (*data).len(), 352 | samples: dataref.clone(), 353 | sample_rate: rate, 354 | channels, 355 | pos: Arc::new(AtomicUsize::new(0)), 356 | stopped: Arc::new(AtomicBool::new(false)), 357 | fx_enable: [ 358 | Arc::new(AtomicBool::new(false)), 359 | Arc::new(AtomicBool::new(false)), 360 | ], 361 | fx_dsp: [None, None], 362 | laser_dsp: Arc::new(Mutex::new(laser_dsp)), 363 | }); 364 | self.last_file = filename.to_string(); 365 | Ok(()) 366 | } 367 | 368 | pub fn open_path(&mut self, path: &str) -> Result<()> { 369 | let new_file = String::from(path); 370 | if self.file.is_some() && self.last_file.eq(&new_file) { 371 | //don't reopen already opened file 372 | return Ok(()); 373 | } 374 | 375 | self.close(); 376 | let file = File::open(path)?; 377 | let source = rodio::Decoder::new(BufReader::new(file))?; 378 | self.open(source.convert_samples(), path) 379 | } 380 | 381 | pub fn stop(&mut self) { 382 | if let Some(file) = &mut self.file { 383 | file.set_stopped(true); 384 | } 385 | } 386 | 387 | //release trhe currently loaded file 388 | pub fn close(&mut self) { 389 | self.stop(); 390 | if self.file.is_some() { 391 | self.file.as_mut().unwrap().set_stopped(true); 392 | self.file = None; 393 | } 394 | } 395 | 396 | pub fn release(&mut self) { 397 | self.close(); 398 | } 399 | 400 | pub fn set_poistion(&mut self, ms: f64) { 401 | if let Some(file) = &mut self.file { 402 | file.set_ms(ms); 403 | } 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/action_stack.rs: -------------------------------------------------------------------------------- 1 | use crate::i18n; 2 | use anyhow::Result; 3 | 4 | pub struct Action { 5 | id: u32, 6 | pub description: String, 7 | pub action: Box Result<()>>, 8 | } 9 | 10 | pub struct ActionStack { 11 | original: T, 12 | undo_stack: Vec>, 13 | redo_stack: Vec>, 14 | saved: Option, 15 | next_id: u32, 16 | } 17 | 18 | impl ActionStack 19 | where 20 | T: Clone, 21 | { 22 | pub fn new(original: T) -> Self { 23 | ActionStack { 24 | original, 25 | undo_stack: Vec::new(), 26 | redo_stack: Vec::new(), 27 | saved: None, 28 | next_id: 0, 29 | } 30 | } 31 | 32 | pub fn new_action(&mut self) -> &mut Action { 33 | self.undo_stack.push(Action { 34 | action: Box::new(|_| panic!("Unset Action")), 35 | description: String::new(), 36 | id: self.next_id, 37 | }); 38 | self.next_id += 1; 39 | self.redo_stack.clear(); 40 | self.undo_stack.last_mut().unwrap() 41 | } 42 | 43 | pub fn undo(&mut self) { 44 | if let Some(action) = self.undo_stack.pop() { 45 | self.redo_stack.push(action); 46 | } 47 | } 48 | 49 | pub fn redo(&mut self) { 50 | if let Some(action) = self.redo_stack.pop() { 51 | self.undo_stack.push(action); 52 | } 53 | } 54 | 55 | pub fn reset(&mut self, origin: T) { 56 | self.original = origin; 57 | self.redo_stack.clear(); 58 | self.undo_stack.clear(); 59 | self.saved = None; 60 | } 61 | 62 | pub fn apply(&mut self) { 63 | if let Ok(current) = self.get_current() { 64 | self.reset(current); 65 | } 66 | } 67 | 68 | pub fn next_action_desc(&self) -> Option { 69 | self.redo_stack.last().map(|next| next.description.clone()) 70 | } 71 | 72 | pub fn prev_action_desc(&self) -> Option { 73 | self.undo_stack.last().map(|next| next.description.clone()) 74 | } 75 | 76 | pub fn get_current(&mut self) -> Result { 77 | let mut current = self.original.clone(); 78 | for action in &self.undo_stack { 79 | action.action.as_ref()(&mut current)?; 80 | } 81 | Ok(current) 82 | } 83 | 84 | pub fn save(&mut self) { 85 | match self.undo_stack.last() { 86 | Some(a) => self.saved = Some(a.id), 87 | None => self.saved = None, 88 | } 89 | } 90 | 91 | pub fn saved(&self) -> bool { 92 | match (self.undo_stack.last(), self.saved) { 93 | (Some(a), Some(saved)) => a.id == saved, 94 | (Some(_), None) => false, 95 | _ => true, 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/assets/mod.rs: -------------------------------------------------------------------------------- 1 | use egui_glow::glow::{Context, HasContext, Program, Texture}; 2 | use image::DynamicImage; 3 | use once_cell::sync::OnceCell; 4 | 5 | pub mod textures { 6 | pub const LASER: &[u8] = include_bytes!("textures/laser.png"); 7 | pub const TRACK: &[u8] = include_bytes!("textures/track.png"); 8 | pub const BT_CHIP: &[u8] = include_bytes!("textures/button.png"); 9 | pub const BT_HOLD: &[u8] = include_bytes!("textures/buttonhold.png"); 10 | pub const FX_CHIP: &[u8] = include_bytes!("textures/fxbutton.png"); 11 | pub const FX_HOLD: &[u8] = include_bytes!("textures/fxbuttonhold.png"); 12 | } 13 | #[derive(Debug, Copy, Clone)] 14 | pub struct AssetInstance { 15 | pub(crate) laser_texture: Texture, 16 | pub(crate) track_texture: Texture, 17 | pub(crate) bt_chip_texture: Texture, 18 | pub(crate) bt_hold_texture: Texture, 19 | pub(crate) fx_chip_texture: Texture, 20 | pub(crate) fx_hold_texture: Texture, 21 | pub(crate) laser_shader: Program, 22 | pub(crate) track_shader: Program, 23 | pub(crate) color_mesh_shader: Program, 24 | pub(crate) chip_shader: Program, 25 | pub(crate) hold_shader: Program, 26 | } 27 | fn load_shader( 28 | gl: &Context, 29 | vertex: &str, 30 | fragment: &str, 31 | ) -> Result { 32 | use egui_glow::glow; 33 | unsafe { 34 | let vert = gl.create_shader(glow::VERTEX_SHADER)?; 35 | gl.shader_source(vert, vertex); 36 | gl.compile_shader(vert); 37 | 38 | let frag = gl.create_shader(glow::FRAGMENT_SHADER)?; 39 | gl.shader_source(frag, fragment); 40 | gl.compile_shader(frag); 41 | 42 | let program = gl.create_program()?; 43 | gl.attach_shader(program, vert); 44 | gl.attach_shader(program, frag); 45 | gl.link_program(program); 46 | 47 | if gl.get_program_link_status(program) { 48 | let attribs = gl.get_active_attributes(program); 49 | 50 | log::debug!("Listing attributes"); 51 | for i in 0..attribs { 52 | if let Some(attrib) = gl.get_active_attribute(program, i) { 53 | log::debug!("name: {}, size: {}", attrib.name, attrib.size); 54 | } 55 | } 56 | 57 | Ok(program) 58 | } else { 59 | Err(gl.get_program_info_log(program)) 60 | } 61 | } 62 | } 63 | 64 | fn load_texture(gl: &Context, texture: &[u8]) -> Result { 65 | use egui_glow::glow; 66 | unsafe { 67 | let tex = gl.create_texture()?; 68 | let img = image::load_from_memory_with_format(texture, image::ImageFormat::Png) 69 | .map_err(|e| format!("{}", e))?; 70 | 71 | gl.bind_texture(glow::TEXTURE_2D, Some(tex)); 72 | 73 | gl.tex_image_2d( 74 | glow::TEXTURE_2D, 75 | 0, 76 | glow::SRGB8_ALPHA8 as i32, 77 | img.width() as i32, 78 | img.height() as i32, 79 | 0, 80 | glow::RGBA, 81 | glow::UNSIGNED_BYTE, 82 | Some(&DynamicImage::ImageRgba8(img.into_rgba8()).into_bytes()), 83 | ); 84 | 85 | gl.generate_mipmap(glow::TEXTURE_2D); 86 | 87 | gl.tex_parameter_i32( 88 | glow::TEXTURE_2D, 89 | glow::TEXTURE_WRAP_S, 90 | glow::CLAMP_TO_EDGE as i32, 91 | ); 92 | gl.tex_parameter_i32( 93 | glow::TEXTURE_2D, 94 | glow::TEXTURE_WRAP_T, 95 | glow::CLAMP_TO_EDGE as i32, 96 | ); 97 | gl.tex_parameter_i32( 98 | glow::TEXTURE_2D, 99 | glow::TEXTURE_MAG_FILTER, 100 | glow::LINEAR as i32, 101 | ); 102 | gl.tex_parameter_i32( 103 | glow::TEXTURE_2D, 104 | glow::TEXTURE_MIN_FILTER, 105 | glow::LINEAR_MIPMAP_LINEAR as i32, 106 | ); 107 | 108 | Ok(tex) 109 | } 110 | } 111 | static INSTANCE: OnceCell = OnceCell::new(); 112 | 113 | pub fn instance(gl: &Context) -> AssetInstance { 114 | *INSTANCE 115 | .get_or_try_init(|| -> Result { 116 | log::debug!("Initializing asset instance"); 117 | Ok(AssetInstance { 118 | laser_texture: load_texture(gl, textures::LASER)?, 119 | track_texture: load_texture(gl, textures::TRACK)?, 120 | bt_chip_texture: load_texture(gl, textures::BT_CHIP)?, 121 | bt_hold_texture: load_texture(gl, textures::BT_HOLD)?, 122 | fx_chip_texture: load_texture(gl, textures::FX_CHIP)?, 123 | fx_hold_texture: load_texture(gl, textures::FX_HOLD)?, 124 | laser_shader: shaders::laser::load(gl)?, 125 | track_shader: shaders::track::load(gl)?, 126 | color_mesh_shader: shaders::color_mesh::load(gl)?, 127 | chip_shader: shaders::button::load_chip(gl)?, 128 | hold_shader: shaders::button::load_hold(gl)?, 129 | }) 130 | }) 131 | .unwrap() 132 | } 133 | 134 | pub mod shaders { 135 | 136 | pub mod laser { 137 | use egui_glow::glow::{Context, Program}; 138 | 139 | pub const FRAGMENT: &str = include_str!("shaders/laser_frag.glsl"); 140 | pub const VERTEX: &str = include_str!("shaders/laser_vert.glsl"); 141 | 142 | pub fn load(gl: &Context) -> Result { 143 | super::super::load_shader(gl, VERTEX, FRAGMENT) 144 | } 145 | } 146 | 147 | pub mod track { 148 | use egui_glow::glow::{Context, Program}; 149 | 150 | pub const FRAGMENT: &str = include_str!("shaders/track_frag.glsl"); 151 | pub const VERTEX: &str = include_str!("shaders/track_vert.glsl"); 152 | 153 | pub fn load(gl: &Context) -> Result { 154 | super::super::load_shader(gl, VERTEX, FRAGMENT) 155 | } 156 | } 157 | 158 | pub mod color_mesh { 159 | use egui_glow::glow::{Context, Program}; 160 | 161 | pub const FRAGMENT: &str = include_str!("shaders/color_mesh_frag.glsl"); 162 | pub const VERTEX: &str = include_str!("shaders/color_mesh_vert.glsl"); 163 | 164 | pub fn load(gl: &Context) -> Result { 165 | super::super::load_shader(gl, VERTEX, FRAGMENT) 166 | } 167 | } 168 | 169 | pub mod button { 170 | use egui_glow::glow::{Context, Program}; 171 | 172 | pub const CHIP_FRAGMENT: &str = include_str!("shaders/button_frag.glsl"); 173 | pub const CHIP_VERTEX: &str = include_str!("shaders/button_vert.glsl"); 174 | pub const HOLD_FRAGMENT: &str = include_str!("shaders/holdbutton_frag.glsl"); 175 | pub const HOLD_VERTEX: &str = include_str!("shaders/holdbutton_vert.glsl"); 176 | 177 | pub fn load_chip(gl: &Context) -> Result { 178 | super::super::load_shader(gl, CHIP_VERTEX, CHIP_FRAGMENT) 179 | } 180 | 181 | pub fn load_hold(gl: &Context) -> Result { 182 | super::super::load_shader(gl, HOLD_VERTEX, HOLD_FRAGMENT) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/assets/shaders/button_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | varying lowp vec2 uv; 4 | varying lowp vec4 color; 5 | 6 | uniform sampler2D mainTex; 7 | uniform int hasSample; 8 | 9 | void main() 10 | { 11 | vec4 mainColor = texture2D(mainTex, uv.xy); 12 | float addition = abs(0.5 - uv.x) * - 1.; 13 | addition += 0.2; 14 | addition = max(addition,0.); 15 | addition *= 2.8; 16 | mainColor.xyzw += addition * float(hasSample); 17 | gl_FragColor = mainColor; 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/shaders/button_vert.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | attribute vec2 position; 4 | attribute vec2 texcoord; 5 | attribute vec4 color0; 6 | 7 | varying lowp vec2 uv; 8 | varying lowp vec4 color; 9 | 10 | uniform mat4 Model; 11 | uniform mat4 Projection; 12 | 13 | void main() { 14 | gl_Position = Projection * Model * vec4(position.x, 0, position.y, 1); 15 | color = color0 / 255.0; 16 | uv = texcoord; 17 | } -------------------------------------------------------------------------------- /src/assets/shaders/color_mesh_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | 4 | varying lowp vec2 uv; 5 | varying lowp vec4 color; 6 | 7 | uniform float brightness; 8 | 9 | void main() { 10 | gl_FragColor = color; 11 | } -------------------------------------------------------------------------------- /src/assets/shaders/color_mesh_vert.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | attribute vec2 position; 4 | attribute vec2 texcoord; 5 | attribute vec4 color0; 6 | 7 | varying lowp vec2 uv; 8 | varying lowp vec4 color; 9 | 10 | uniform mat4 Model; 11 | uniform mat4 Projection; 12 | 13 | void main() { 14 | gl_Position = Projection * Model * vec4(position.x, 0, position.y, 1); 15 | color = color0 / 255.0; 16 | uv = texcoord; 17 | } -------------------------------------------------------------------------------- /src/assets/shaders/holdbutton_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | 4 | varying vec2 uv; 5 | 6 | uniform sampler2D mainTex; 7 | uniform float objectGlow; 8 | // 20Hz flickering. 0 = Miss, 1 = Inactive, 2 & 3 = Active alternating. 9 | uniform int hitState; 10 | 11 | 12 | void main() 13 | { 14 | vec4 mainColor = texture2D(mainTex, uv.xy); 15 | 16 | vec4 target = mainColor; 17 | target.xyz = target.xyz * (1.0 + objectGlow * 0.3); 18 | target.a = min(1.0, target.a + target.a * objectGlow * 0.9); 19 | gl_FragColor = target; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/assets/shaders/holdbutton_vert.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | attribute vec2 position; 4 | attribute vec2 texcoord; 5 | attribute vec4 color0; 6 | 7 | varying lowp vec2 uv; 8 | varying lowp vec4 color; 9 | 10 | uniform mat4 Model; 11 | uniform mat4 Projection; 12 | 13 | void main() { 14 | gl_Position = Projection * Model * vec4(position.x, 0, position.y, 1); 15 | color = color0 / 255.0; 16 | uv = texcoord; 17 | } -------------------------------------------------------------------------------- /src/assets/shaders/laser_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | varying vec2 fsTex; 4 | varying vec4 uv; 5 | 6 | uniform sampler2D mainTex; 7 | varying vec3 color; 8 | 9 | // 20Hz flickering. 0 = Miss, 1 = Inactive, 2 & 3 = Active alternating. 10 | uniform int state; 11 | 12 | 13 | void main() 14 | { 15 | float x = fsTex.x; 16 | float laserSize = 0.85; //0.0 to 1.0 17 | x -= 0.5; 18 | x /= laserSize; 19 | x += 0.5; 20 | vec4 mainColor = texture2D(mainTex, vec2(x,fsTex.y)) * step(0.0, x) * (1.0 - step(1.0, x)); 21 | 22 | float brightness = (3.5 / 4.0) + float(state - 1) / 4.0; 23 | 24 | 25 | gl_FragColor = (mainColor * vec4(color, 1)) * brightness; 26 | } 27 | -------------------------------------------------------------------------------- /src/assets/shaders/laser_vert.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | attribute vec2 position; 4 | attribute vec2 texcoord; 5 | attribute vec4 color0; 6 | 7 | uniform float offset; 8 | uniform mat4 Model; 9 | uniform mat4 Projection; 10 | uniform float scale; 11 | 12 | varying vec2 fsTex; 13 | varying vec3 color; 14 | 15 | void main() 16 | { 17 | fsTex = texcoord; 18 | gl_Position = Projection * Model * vec4(position.x, 0, position.y, 1); 19 | color = color0.xyz / vec3(255.0); 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/shaders/track_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | varying lowp vec2 uv; 4 | varying lowp vec4 color; 5 | 6 | uniform sampler2D mainTex; 7 | uniform vec4 lCol; 8 | uniform vec4 rCol; 9 | 10 | void main() 11 | { 12 | vec4 mainColor = texture2D(mainTex, uv.xy); 13 | vec4 col = mainColor; 14 | //Red channel to color right lane 15 | col.xyz = vec3(.9) * rCol.xyz * vec3(mainColor.x); 16 | 17 | //Blue channel to color left lane 18 | col.xyz += vec3(.9) * lCol.xyz * vec3(mainColor.z); 19 | 20 | //Color green channel white 21 | col.xyz += vec3(.6) * vec3(mainColor.y); 22 | 23 | col.xyz += vec3(.002); 24 | 25 | gl_FragColor = col; 26 | } -------------------------------------------------------------------------------- /src/assets/shaders/track_vert.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | attribute vec2 position; 4 | attribute vec2 texcoord; 5 | attribute vec4 color0; 6 | 7 | varying lowp vec2 uv; 8 | varying lowp vec4 color; 9 | 10 | uniform mat4 Model; 11 | uniform mat4 Projection; 12 | 13 | void main() { 14 | gl_Position = Projection * Model * vec4(position.x, 0, position.y, 1); 15 | color = color0 / 255.0; 16 | uv = texcoord; 17 | } -------------------------------------------------------------------------------- /src/assets/textures/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drewol/kson-editor/7818f12cc3402e1a76ed8d0a53e51e8dc5aef7b7/src/assets/textures/button.png -------------------------------------------------------------------------------- /src/assets/textures/buttonhold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drewol/kson-editor/7818f12cc3402e1a76ed8d0a53e51e8dc5aef7b7/src/assets/textures/buttonhold.png -------------------------------------------------------------------------------- /src/assets/textures/fxbutton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drewol/kson-editor/7818f12cc3402e1a76ed8d0a53e51e8dc5aef7b7/src/assets/textures/fxbutton.png -------------------------------------------------------------------------------- /src/assets/textures/fxbuttonhold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drewol/kson-editor/7818f12cc3402e1a76ed8d0a53e51e8dc5aef7b7/src/assets/textures/fxbuttonhold.png -------------------------------------------------------------------------------- /src/assets/textures/laser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drewol/kson-editor/7818f12cc3402e1a76ed8d0a53e51e8dc5aef7b7/src/assets/textures/laser.png -------------------------------------------------------------------------------- /src/assets/textures/track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drewol/kson-editor/7818f12cc3402e1a76ed8d0a53e51e8dc5aef7b7/src/assets/textures/track.png -------------------------------------------------------------------------------- /src/camera_widget.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use bytemuck::offset_of; 4 | use eframe::{ 5 | egui::{Sense, Widget}, 6 | epaint::{color::Hsva, Color32, PaintCallback, Stroke, Vertex}, 7 | glow::{Context, HasContext, NativeBuffer}, 8 | }; 9 | use egui_glow::check_for_gl_error; 10 | use emath::{pos2, vec2, Rect, Vec2}; 11 | use kson::Chart; 12 | use once_cell::sync::OnceCell; 13 | use puffin::{profile_function, profile_scope}; 14 | 15 | use crate::{assets, chart_camera::ChartCamera, rect_xy_wh}; 16 | 17 | pub enum Material { 18 | Track(Color32, Color32), 19 | ChipBT, 20 | ChipFX, 21 | LongBT, 22 | LongFX, 23 | Laser(u8), 24 | Solid(BlendMode), 25 | } 26 | 27 | pub struct Mesh { 28 | mesh: eframe::epaint::Mesh, 29 | material: Material, 30 | } 31 | 32 | pub struct CameraView { 33 | desired_size: Vec2, 34 | camera: ChartCamera, 35 | meshes: Vec, 36 | } 37 | 38 | impl CameraView { 39 | const TRACK_LENGH: f32 = 16.0; 40 | const TRACK_WIDTH: f32 = 1.0; 41 | 42 | pub fn new(desired_size: Vec2, camera: ChartCamera) -> Self { 43 | Self { 44 | desired_size, 45 | camera, 46 | meshes: Default::default(), 47 | } 48 | } 49 | 50 | pub fn add_track(&mut self, laser_colors: &[Color32; 2]) { 51 | let left = -(Self::TRACK_WIDTH / 2.0); 52 | let right = Self::TRACK_WIDTH / 2.0; 53 | 54 | let mut track_mesh = eframe::epaint::Mesh::with_texture(Default::default()); 55 | 56 | track_mesh.add_rect_with_uv( 57 | Rect { 58 | min: pos2(left, 0.0), 59 | max: pos2(right, Self::TRACK_LENGH), 60 | }, 61 | Rect { 62 | min: pos2(0.0, 0.5), 63 | max: pos2(1.0, 0.5), 64 | }, 65 | Color32::from_gray(255), 66 | ); 67 | 68 | self.meshes.push(Mesh { 69 | mesh: track_mesh, 70 | material: Material::Track(laser_colors[0], laser_colors[1]), 71 | }); 72 | } 73 | pub fn add_mesh(&mut self, mesh: Mesh) { 74 | self.meshes.push(mesh) 75 | } 76 | 77 | pub fn add_chart_objects(&mut self, chart: &Chart, tick: f32, laser_colors: &[Color32; 2]) { 78 | profile_function!(); 79 | let tick_height = -0.05; 80 | let bottom_margin = -tick * tick_height; 81 | 82 | let min_tick_render = tick as i32 - chart.beat.resolution as i32 * 8; 83 | 84 | let screen = crate::chart_editor::ScreenState { 85 | beat_res: chart.beat.resolution, 86 | beats_per_col: u32::MAX, //whole chart in one column 87 | bottom_margin, //one of these could maybe scroll the chart 88 | top_margin: 0.0, 89 | h: 0.0, 90 | w: 1.0, 91 | top: 0.0, 92 | left_margin: -Self::TRACK_WIDTH, 93 | tick_height, 94 | track_width: Self::TRACK_WIDTH, 95 | x_offset: 0.0, 96 | x_offset_target: 0.0, 97 | curve_per_tick: 1.0, 98 | }; 99 | let uv_rect = Rect { 100 | min: pos2(0.0, 0.0), 101 | max: pos2(1.0, 0.0), 102 | }; 103 | let lane_width = screen.lane_width(); 104 | //bt 105 | let mut bt_chip_mesh = eframe::epaint::Mesh::with_texture(Default::default()); 106 | let mut bt_hold_mesh = eframe::epaint::Mesh::with_texture(Default::default()); 107 | { 108 | profile_scope!("BT Components"); 109 | for i in 0..4 { 110 | for n in &chart.note.bt[i] { 111 | if ((n.y + n.l) as i32) < min_tick_render { 112 | continue; 113 | } 114 | 115 | if n.l == 0 { 116 | let (x, y) = screen.tick_to_pos(n.y); 117 | 118 | let x = x + i as f32 * lane_width + lane_width + screen.track_width / 2.0; 119 | let y = y as f32; 120 | let w = screen.track_width as f32 / 6.0; 121 | let h = Self::TRACK_LENGH / 100.0; 122 | 123 | bt_chip_mesh.add_rect_with_uv( 124 | rect_xy_wh([x, y, w, h]), 125 | uv_rect, 126 | Color32::WHITE, 127 | ); 128 | } else { 129 | for (x, y, h, _) in screen.interval_to_ranges(n) { 130 | let x = 131 | x + i as f32 * lane_width + lane_width + screen.track_width / 2.0; 132 | let w = screen.track_width as f32 / 6.0; 133 | 134 | bt_hold_mesh.add_rect_with_uv( 135 | rect_xy_wh([x, y, w, h]), 136 | uv_rect, 137 | Color32::WHITE, 138 | ); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | let mut fx_chip_mesh = eframe::epaint::Mesh::with_texture(Default::default()); 146 | let mut fx_hold_mesh = eframe::epaint::Mesh::with_texture(Default::default()); 147 | //fx 148 | { 149 | profile_scope!("FX Components"); 150 | for i in 0..2 { 151 | for n in &chart.note.fx[i] { 152 | if ((n.y + n.l) as i32) < min_tick_render { 153 | continue; 154 | } 155 | 156 | if n.l == 0 { 157 | let (x, y) = screen.tick_to_pos(n.y); 158 | 159 | let x = x 160 | + (i as f32 * lane_width * 2.0) 161 | + screen.track_width / 2.0 162 | + lane_width; 163 | let w = lane_width * 2.0; 164 | let h = Self::TRACK_LENGH / 100.0; 165 | 166 | fx_chip_mesh.add_rect_with_uv( 167 | rect_xy_wh([x, y, w, h]), 168 | uv_rect, 169 | Color32::WHITE, 170 | ); 171 | } else { 172 | for (x, y, h, _) in screen.interval_to_ranges(n) { 173 | let x = x 174 | + (i as f32 * lane_width * 2.0) 175 | + screen.track_width / 2.0 176 | + lane_width; 177 | let w = lane_width * 2.0; 178 | 179 | fx_hold_mesh.add_rect_with_uv( 180 | rect_xy_wh([x, y, w, h]), 181 | uv_rect, 182 | Color32::WHITE, 183 | ); 184 | } 185 | } 186 | } 187 | } 188 | } 189 | self.add_mesh(Mesh { 190 | mesh: fx_hold_mesh, 191 | material: Material::LongFX, 192 | }); 193 | self.add_mesh(Mesh { 194 | mesh: bt_hold_mesh, 195 | material: Material::LongBT, 196 | }); 197 | self.add_mesh(Mesh { 198 | mesh: fx_chip_mesh, 199 | material: Material::ChipFX, 200 | }); 201 | self.add_mesh(Mesh { 202 | mesh: bt_chip_mesh, 203 | material: Material::ChipBT, 204 | }); 205 | let mapped_color = laser_colors 206 | .iter() 207 | .map(Color32::to_srgba_unmultiplied) 208 | .map(Hsva::from_srgba_unmultiplied) 209 | .map(|hsva| Hsva::new(hsva.h, 1.0, 1.0, 1.0)) 210 | .map(Color32::from) 211 | .collect::>(); 212 | for (side, lane) in chart.note.laser.iter().enumerate() { 213 | let mut laser_meshes = Vec::new(); 214 | 215 | for section in lane { 216 | screen 217 | .draw_laser_section(section, &mut laser_meshes, mapped_color[side], true) 218 | .unwrap(); 219 | } 220 | self.meshes.append( 221 | &mut laser_meshes 222 | .into_iter() 223 | .map(|mesh| Mesh { 224 | mesh, 225 | material: Material::Laser(side as u8), 226 | }) 227 | .collect::>(), 228 | ) 229 | } 230 | } 231 | 232 | pub fn add_track_overlay(&mut self) { 233 | let left = -(Self::TRACK_WIDTH / 2.0); 234 | let right = Self::TRACK_WIDTH / 2.0; 235 | 236 | let mut mesh = eframe::epaint::Mesh::with_texture(Default::default()); 237 | mesh.add_colored_rect( 238 | Rect::from_x_y_ranges((2.0 * left)..=(right * 2.0), -Self::TRACK_LENGH..=0.0), 239 | Color32::from_gray(20), 240 | ); 241 | self.add_mesh(Mesh { 242 | mesh, 243 | material: Material::Solid(BlendMode::Min), 244 | }); 245 | 246 | let mut mesh = eframe::epaint::Mesh::with_texture(Default::default()); 247 | mesh.add_colored_rect( 248 | Rect::from_x_y_ranges(left..=right, -0.002..=0.002), 249 | Color32::RED, 250 | ); 251 | self.add_mesh(Mesh { 252 | mesh, 253 | material: Material::Solid(BlendMode::Normal), 254 | }); 255 | } 256 | } 257 | 258 | impl Widget for CameraView { 259 | fn ui(self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { 260 | let width = ui.available_size_before_wrap().x.max(self.desired_size.x); 261 | let height = width / (16.0 / 9.0); //16:9 aspect ratio, potentially allow toggle to 9:16 262 | let time = ui.ctx().input().time; 263 | let (response, painter) = ui.allocate_painter(vec2(width, height), Sense::click()); 264 | let view_rect = response.rect; 265 | let size = view_rect.size(); 266 | let projection = self.camera.matrix(size); 267 | painter.rect( 268 | ui.max_rect(), 269 | 0.0, 270 | Color32::from_rgb(0, 0, 0), 271 | Stroke::none(), 272 | ); 273 | 274 | for mesh in self.meshes { 275 | let proj = projection.to_cols_array(); 276 | let callback = PaintCallback { 277 | rect: view_rect, 278 | callback: std::sync::Arc::new(move |_info, render_ctx| unsafe { 279 | paint_mesh_callback(render_ctx, &mesh, &proj, time); 280 | }), 281 | }; 282 | painter.add(callback); 283 | } 284 | 285 | response 286 | } 287 | } 288 | thread_local! { 289 | pub static MODEL: [f32; 16] = (glam::Mat4::from_rotation_y(90_f32.to_radians()) 290 | * glam::Mat4::from_rotation_z(180_f32.to_radians())) 291 | .to_cols_array(); 292 | } 293 | 294 | unsafe fn paint_mesh_callback( 295 | render_ctx: &mut dyn std::any::Any, 296 | mesh: &Mesh, 297 | projection: &[f32], 298 | _time: f64, 299 | ) { 300 | if let Some(painter) = render_ctx.downcast_ref::() { 301 | use egui_glow::glow; 302 | let gl = painter.gl(); 303 | gl.bind_vertex_array(None); // Unbind egui_glow vertex array object 304 | 305 | let assets = assets::instance(gl); 306 | 307 | static CAMERA_ARRAY_BUFFER: OnceCell = OnceCell::new(); 308 | static CAMERA_ELEMENT_BUFFER: OnceCell = OnceCell::new(); 309 | 310 | let vertex_buffer = CAMERA_ARRAY_BUFFER.get_or_init(|| gl.create_buffer().unwrap()); 311 | gl.bind_buffer(glow::ARRAY_BUFFER, Some(*vertex_buffer)); 312 | 313 | let index_buffer = CAMERA_ELEMENT_BUFFER.get_or_init(|| gl.create_buffer().unwrap()); 314 | gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(*index_buffer)); 315 | let stride = std::mem::size_of::() as i32; 316 | 317 | let (program, texture) = match mesh.material { 318 | Material::Track(lcol, rcol) => { 319 | gl.use_program(Some(assets.track_shader)); 320 | 321 | gl.uniform_4_f32_slice( 322 | gl.get_uniform_location(assets.track_shader, "lCol") 323 | .as_ref(), 324 | &lcol.to_srgba_unmultiplied().map(|v| v as f32 / 255.0), 325 | ); 326 | 327 | gl.uniform_4_f32_slice( 328 | gl.get_uniform_location(assets.track_shader, "rCol") 329 | .as_ref(), 330 | &rcol.to_srgba_unmultiplied().map(|v| v as f32 / 255.0), 331 | ); 332 | 333 | (assets.track_shader, Some(assets.track_texture)) 334 | } 335 | Material::ChipBT => (assets.chip_shader, Some(assets.bt_chip_texture)), 336 | Material::ChipFX => (assets.chip_shader, Some(assets.fx_chip_texture)), 337 | Material::LongBT => (assets.hold_shader, Some(assets.bt_hold_texture)), 338 | Material::LongFX => (assets.hold_shader, Some(assets.fx_hold_texture)), 339 | Material::Laser(side) => { 340 | gl.use_program(Some(assets.laser_shader)); 341 | let color = if side == 0 { 342 | Hsva::new(0.5, 1.0, 1.0, 1.0) 343 | } else { 344 | Hsva::new(0.0, 1.0, 1.0, 1.0) 345 | }; 346 | 347 | gl.uniform_3_f32_slice( 348 | gl.get_uniform_location(assets.laser_shader, "color") 349 | .as_ref(), 350 | &color.to_srgb().map(|v| v as f32 / 255.0), 351 | ); 352 | (assets.laser_shader, Some(assets.laser_texture)) 353 | } 354 | Material::Solid(mode) => { 355 | set_blend_mode(gl, mode); 356 | (assets.color_mesh_shader, None) 357 | } 358 | }; 359 | 360 | match mesh.material { 361 | Material::LongFX | Material::Laser(_) => set_blend_mode(gl, BlendMode::Add), 362 | _ => {} 363 | } 364 | 365 | gl.use_program(Some(program)); 366 | gl.bind_texture(glow::TEXTURE_2D, texture); 367 | gl.active_texture(glow::TEXTURE0); 368 | 369 | if let Some(pos_loc) = gl.get_attrib_location(program, "position") { 370 | gl.vertex_attrib_pointer_f32( 371 | pos_loc, 372 | 2, 373 | glow::FLOAT, 374 | false, 375 | stride, 376 | offset_of!(Vertex, pos) as i32, 377 | ); 378 | gl.enable_vertex_attrib_array(pos_loc); 379 | }; 380 | if let Some(texcoord_loc) = gl.get_attrib_location(program, "texcoord") { 381 | gl.vertex_attrib_pointer_f32( 382 | texcoord_loc, 383 | 2, 384 | glow::FLOAT, 385 | false, 386 | stride, 387 | offset_of!(Vertex, uv) as i32, 388 | ); 389 | gl.enable_vertex_attrib_array(texcoord_loc); 390 | } 391 | if let Some(color_loc) = gl.get_attrib_location(program, "color0") { 392 | gl.vertex_attrib_pointer_f32( 393 | color_loc, 394 | 4, 395 | glow::UNSIGNED_BYTE, 396 | false, 397 | stride, 398 | offset_of!(Vertex, color) as i32, 399 | ); 400 | gl.enable_vertex_attrib_array(color_loc); 401 | } 402 | 403 | MODEL.with(|m| { 404 | gl.uniform_matrix_4_f32_slice( 405 | gl.get_uniform_location(program, "Model").as_ref(), 406 | false, 407 | m, 408 | ); 409 | }); 410 | 411 | gl.uniform_matrix_4_f32_slice( 412 | gl.get_uniform_location(program, "Projection").as_ref(), 413 | false, 414 | projection, 415 | ); 416 | 417 | gl.uniform_1_i32(gl.get_uniform_location(program, "mainTex").as_ref(), 0); 418 | gl.uniform_1_i32(gl.get_uniform_location(program, "hitState").as_ref(), 1); 419 | gl.uniform_1_i32(gl.get_uniform_location(program, "hasSample").as_ref(), 0); 420 | gl.uniform_1_f32(gl.get_uniform_location(program, "objectGlow").as_ref(), 0.0); 421 | 422 | gl.buffer_data_u8_slice( 423 | glow::ARRAY_BUFFER, 424 | bytemuck::cast_slice(&mesh.mesh.vertices), 425 | glow::STREAM_DRAW, 426 | ); 427 | 428 | gl.buffer_data_u8_slice( 429 | glow::ELEMENT_ARRAY_BUFFER, 430 | bytemuck::cast_slice(&mesh.mesh.indices), 431 | glow::STREAM_DRAW, 432 | ); 433 | check_for_gl_error!(gl); 434 | 435 | gl.draw_elements( 436 | glow::TRIANGLES, 437 | mesh.mesh.indices.len() as i32, 438 | glow::UNSIGNED_INT, 439 | 0, 440 | ); 441 | set_blend_mode(gl, BlendMode::Normal); 442 | check_for_gl_error!(gl); 443 | } else { 444 | eprintln!("Can't do custom painting because we are not using a glow context"); 445 | } 446 | } 447 | 448 | #[derive(Debug, Copy, Clone)] 449 | pub enum BlendMode { 450 | Normal, 451 | Add, 452 | Min, 453 | } 454 | 455 | unsafe fn set_blend_mode(gl: &Rc, mode: BlendMode) { 456 | use egui_glow::glow; 457 | gl.enable(glow::BLEND); 458 | match mode { 459 | BlendMode::Normal => gl.blend_func_separate( 460 | glow::SRC_ALPHA, 461 | glow::ONE_MINUS_SRC_ALPHA, 462 | glow::ONE, 463 | glow::ONE, 464 | ), 465 | BlendMode::Add => gl.blend_func(glow::ONE, glow::ONE), 466 | BlendMode::Min => { 467 | gl.blend_func(glow::ONE, glow::ONE); 468 | gl.blend_equation(glow::MIN); 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/chart_camera.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Vec2; 2 | use glam::{Mat4, Vec3}; 3 | 4 | #[derive(Debug, Clone, Copy)] 5 | pub struct ChartCamera { 6 | pub fov: f32, 7 | pub radius: f32, 8 | pub angle: f32, 9 | pub center: Vec3, 10 | pub track_length: f32, 11 | pub tilt: f32, 12 | } 13 | 14 | impl ChartCamera { 15 | const UP: Vec3 = Vec3::Y; 16 | const TRACK_DIRECTION: Vec3 = Vec3::X; 17 | const Z_NEAR: f32 = 0.01; 18 | } 19 | 20 | impl ChartCamera { 21 | pub fn matrix(&self, view_size: Vec2) -> Mat4 { 22 | let camera_rotation_axis: Vec3 = Vec3::ONE - Self::TRACK_DIRECTION - Self::UP; 23 | let aspect = view_size.x / view_size.y; 24 | let angle = self.angle.to_radians(); 25 | let base_angle = self.fov.to_radians() / 2.2; 26 | 27 | let track_end: Vec3 = Self::TRACK_DIRECTION * self.track_length; 28 | let final_camera_angle = -angle - (self.fov.to_radians() / 2.2); 29 | let final_camera_pos: Vec3 = -Self::UP * self.radius * angle.sin() 30 | + Self::TRACK_DIRECTION * self.radius * angle.cos(); 31 | let camera_unit: Vec3 = 32 | -Self::TRACK_DIRECTION * final_camera_angle.cos() - Self::UP * final_camera_angle.sin(); 33 | 34 | let neg_angle = -angle - base_angle; 35 | let tilt_unit: Vec3 = Self::TRACK_DIRECTION * neg_angle.cos() + Self::UP * neg_angle.sin(); 36 | 37 | let tilt = Mat4::from_translation(self.center) 38 | * Mat4::from_axis_angle(tilt_unit, self.tilt.to_radians()) 39 | * Mat4::from_translation(-self.center); 40 | 41 | let position = Mat4::from_translation(Self::TRACK_DIRECTION * self.radius); 42 | 43 | let end_dist = -camera_unit.dot(track_end + final_camera_pos); 44 | let begin_dist = -camera_unit.dot(-track_end + final_camera_pos); 45 | 46 | let z_far = end_dist.max(begin_dist); 47 | 48 | let rotation = Mat4::from_axis_angle(camera_rotation_axis, -angle); 49 | let base_rotation = 50 | Mat4::from_axis_angle(camera_rotation_axis, -self.fov.to_radians() / 2.2); 51 | 52 | let target: Vec3 = self.center + Self::TRACK_DIRECTION; 53 | Mat4::perspective_rh_gl(self.fov.to_radians(), aspect, Self::Z_NEAR, z_far) 54 | * (Mat4::look_at_rh(self.center, target, Self::UP) * tilt * (base_rotation * position)) 55 | * (rotation) 56 | } 57 | } 58 | 59 | fn create_perspective(field_of_view: f32, aspect_ratio: f32, z_near: f32, z_far: f32) -> Mat4 { 60 | let mut result = [0_f32; 16]; 61 | 62 | let height = z_near * f32::tan((field_of_view.to_radians()) * 0.5); 63 | let width = height * aspect_ratio; 64 | 65 | let f1 = z_near * 2.0; 66 | let f2 = width * 2.0; 67 | let f3 = height * 2.0; 68 | let f4 = z_far - z_near; 69 | result[0] = f1 / f2; 70 | result[5] = f1 / f3; 71 | result[10] = (-z_far - z_near) / f4; 72 | result[11] = -1.0; 73 | result[14] = (-f1 * z_far) / f4; 74 | result[15] = 0.0; 75 | 76 | Mat4::from_cols_array(&result) 77 | } 78 | -------------------------------------------------------------------------------- /src/chart_editor.rs: -------------------------------------------------------------------------------- 1 | use crate::tools::*; 2 | use crate::*; 3 | use anyhow::{bail, Result}; 4 | 5 | use eframe::egui::epaint::{Mesh, Vertex, WHITE_UV}; 6 | use eframe::egui::{ 7 | pos2, Align2, Color32, Context, PointerButton, Pos2, Rect, Response, Sense, Shape, Stroke, 8 | }; 9 | use eframe::egui::{Painter, Rgba}; 10 | 11 | use eframe::epaint::FontId; 12 | use egui::Ui; 13 | use kson::{GraphPoint, GraphSectionPoint, Interval, Ksh, Vox}; 14 | use kson_music_playback as playback; 15 | use log::debug; 16 | use playback::*; 17 | use puffin::profile_scope; 18 | use rodio::Source; 19 | use rodio::{OutputStream, OutputStreamHandle, Sink}; 20 | use std::collections::VecDeque; 21 | use std::ffi::OsStr; 22 | use std::fs::File; 23 | use std::io::prelude::*; 24 | use std::io::BufReader; 25 | use std::path::Path; 26 | use std::path::PathBuf; 27 | pub const EGUI_ID: &str = "chart_editor"; 28 | 29 | pub struct MainState { 30 | pub sink: Sink, 31 | pub chart: kson::Chart, 32 | pub save_path: Option, 33 | pub mouse_x: f32, 34 | pub mouse_y: f32, 35 | pub gui_event_queue: VecDeque, 36 | pub cursor_line: u32, 37 | pub cursor_object: Option>, 38 | pub current_tool: ChartTool, 39 | pub actions: action_stack::ActionStack, 40 | pub screen: ScreenState, 41 | pub audio_playback: playback::AudioPlayback, 42 | pub laser_colors: [Color32; 2], 43 | pub output_stream: OutputStream, 44 | pub output_stream_handle: OutputStreamHandle, 45 | } 46 | 47 | #[derive(Copy, Clone)] 48 | pub struct ScreenState { 49 | pub w: f32, 50 | pub h: f32, 51 | pub tick_height: f32, 52 | pub track_width: f32, 53 | pub top_margin: f32, 54 | pub top: f32, 55 | pub left_margin: f32, 56 | pub bottom_margin: f32, 57 | pub beats_per_col: u32, 58 | pub x_offset: f32, 59 | pub x_offset_target: f32, 60 | pub beat_res: u32, 61 | pub curve_per_tick: f32, 62 | } 63 | 64 | impl ScreenState { 65 | pub fn draw_laser_section( 66 | &self, 67 | section: &kson::LaserSection, 68 | mb: &mut Vec, 69 | color: Color32, 70 | with_uv: bool, 71 | ) -> Result<()> { 72 | //TODO: Draw sections as a single `Mesh` 73 | profile_scope!("Section"); 74 | let y_base = section.tick(); 75 | let slam_uv = Rect { 76 | min: pos2(0.0, 0.0), 77 | max: pos2(1.0, 1.0), 78 | }; 79 | let wide = section.wide() == 2; 80 | let slam_height = 6.0_f32 * self.note_height_mult(); 81 | let half_lane = self.lane_width() / 2.0; 82 | let half_track = self.track_width / 2.0; 83 | let track_lane_diff = self.track_width - self.lane_width(); 84 | 85 | let mut mesh = Mesh::with_texture(Default::default()); 86 | let make_vert: Box Vertex> = if with_uv { 87 | Box::new(|p: &[f32; 3]| Vertex { 88 | pos: [p[0], p[1]].into(), 89 | color, 90 | uv: pos2(p[2], 0.5), 91 | }) 92 | } else { 93 | Box::new(|p: &[f32; 3]| Vertex { 94 | pos: [p[0], p[1]].into(), 95 | color, 96 | uv: WHITE_UV, 97 | }) 98 | }; 99 | 100 | let add_slam_rect = |mesh: &mut Mesh, slam_rect: Rect| { 101 | let i_off = mesh.vertices.len() as u32; 102 | mesh.add_triangle(i_off, i_off + 1, i_off + 2); 103 | mesh.add_triangle(i_off + 2, i_off + 1, i_off + 3); 104 | mesh.reserve_vertices(4); 105 | mesh.vertices.push(Vertex { 106 | pos: slam_rect.left_top(), 107 | uv: [0.0, 0.5].into(), 108 | color, 109 | }); 110 | mesh.vertices.push(Vertex { 111 | pos: slam_rect.right_top(), 112 | uv: [0.0, 0.5].into(), 113 | color, 114 | }); 115 | mesh.vertices.push(Vertex { 116 | pos: slam_rect.left_bottom(), 117 | uv: [1.0, 0.5].into(), 118 | color, 119 | }); 120 | mesh.vertices.push(Vertex { 121 | pos: slam_rect.right_bottom(), 122 | uv: [1.0, 0.5].into(), 123 | color, 124 | }); 125 | }; 126 | 127 | for se in section.segments() { 128 | profile_scope!("Window"); 129 | let s = &se[0]; 130 | let e = &se[1]; 131 | let l = e.ry - s.ry; 132 | let interval = kson::Interval { 133 | y: s.ry + y_base, 134 | l, 135 | }; 136 | 137 | if interval.l == 0 { 138 | continue; 139 | } 140 | 141 | let mut start_value = s.v as f32; 142 | let mut syoff = 0.0_f32; 143 | 144 | if let Some(value) = s.vf { 145 | profile_scope!("Slam"); 146 | start_value = value as f32; 147 | syoff = slam_height; 148 | let mut sv: f32 = s.v as f32; 149 | let mut ev: f32 = value as f32; 150 | if wide { 151 | ev = ev * 2.0 - 0.5; 152 | sv = sv * 2.0 - 0.5; 153 | } 154 | 155 | //draw slam 156 | let (pos_x, pos_y) = self.tick_to_pos(interval.y); 157 | 158 | let sx = pos_x + sv * track_lane_diff + half_track + half_lane; 159 | let ex = pos_x + ev * track_lane_diff + half_track + half_lane; 160 | 161 | let (x, width): (f32, f32) = if sx > ex { 162 | (sx + half_lane, (ex - half_lane) - (sx + half_lane)) 163 | } else { 164 | (sx - half_lane, (ex + half_lane) - (sx - half_lane)) 165 | }; 166 | 167 | if with_uv { 168 | add_slam_rect(&mut mesh, rect_xy_wh([x, pos_y, width, -slam_height])); 169 | } else { 170 | mesh.add_colored_rect(rect_xy_wh([x, pos_y, width, -slam_height]), color); 171 | } 172 | } 173 | 174 | let mut value_width = (e.v as f32 - start_value) as f32; 175 | if wide { 176 | value_width *= 2.0; 177 | start_value = start_value * 2.0 - 0.5; 178 | } 179 | 180 | let curve_points = (s.a.unwrap_or(0.5), s.b.unwrap_or(0.5)); 181 | 182 | for (x, y, h, (sv, ev)) in self.interval_to_ranges(&interval) { 183 | if (curve_points.0 - curve_points.1).abs() < std::f64::EPSILON { 184 | profile_scope!("Range - Linear"); 185 | let sx = x 186 | + (start_value + (sv * value_width)) * track_lane_diff 187 | + half_track 188 | + half_lane; 189 | let ex = x 190 | + (start_value + (ev * value_width)) * track_lane_diff 191 | + half_track 192 | + half_lane; 193 | 194 | let sy = y; 195 | let ey = y + h; 196 | 197 | let xoff = half_lane; 198 | let mut points = vec![ 199 | [ex - xoff, ey, 0.0], 200 | [ex + xoff, ey, 1.0], 201 | [sx + xoff, sy - syoff, 1.0], 202 | [sx - xoff, sy - syoff, 0.0], 203 | ] 204 | .iter() 205 | .map(|p| make_vert(p)) 206 | .collect(); 207 | 208 | let i_off = mesh.vertices.len() as u32; 209 | mesh.vertices.append(&mut points); 210 | mesh.indices.append(&mut vec![ 211 | i_off, 212 | 1 + i_off, 213 | 2 + i_off, 214 | i_off, 215 | 2 + i_off, 216 | 3 + i_off, 217 | ]); 218 | } else { 219 | profile_scope!("Range - Curved"); 220 | let sy = y - syoff; 221 | syoff = 0.0; //only first section after slam needs this 222 | let ey = y + h; 223 | let curve_segments = 224 | ((ey - sy).abs() / (self.tick_height.abs() / self.curve_per_tick)) as i32; 225 | let curve_segment_h = (ey - sy) / curve_segments as f32; 226 | let curve_segment_progress_h = (ev - sv) / curve_segments as f32; 227 | // let interval_start_value = start_value + sv * value_width; 228 | // let interval_value_width = 229 | // (start_value + ev * value_width) - interval_start_value; 230 | 231 | for i in 0..curve_segments { 232 | let cssv = sv + curve_segment_progress_h * i as f32; 233 | let csev = sv + curve_segment_progress_h * (i + 1) as f32; 234 | let csv = do_curve(cssv as f64, curve_points.0, curve_points.1) as f32; 235 | let cev = do_curve(csev as f64, curve_points.0, curve_points.1) as f32; 236 | 237 | let sx = x 238 | + (start_value + (csv * value_width)) * track_lane_diff 239 | + half_track 240 | + half_lane; 241 | let ex = x 242 | + (start_value + (cev * value_width)) * track_lane_diff 243 | + half_track 244 | + half_lane; 245 | 246 | let csy = sy + curve_segment_h * i as f32; 247 | let cey = sy + curve_segment_h * i as f32 + curve_segment_h; 248 | 249 | let xoff = half_lane; 250 | let i_off = mesh.vertices.len() as u32; 251 | 252 | let mut points: Vec = vec![ 253 | [ex - xoff, cey, 0.0], 254 | [ex + xoff, cey, 1.0], 255 | [sx + xoff, csy, 1.0], 256 | [sx - xoff, csy, 0.0], 257 | ] 258 | .iter() 259 | .map(|p| make_vert(p)) 260 | .collect(); 261 | mesh.vertices.append(&mut points); 262 | mesh.indices.append(&mut vec![ 263 | i_off, 264 | 1 + i_off, 265 | 2 + i_off, 266 | i_off, 267 | 2 + i_off, 268 | 3 + i_off, 269 | ]); 270 | } 271 | } 272 | } 273 | } 274 | 275 | if let Some(l) = section.last() { 276 | if let Some(vf) = l.vf { 277 | profile_scope!("End Slam"); 278 | //draw slam 279 | let mut sv: f32 = l.v as f32; 280 | let mut ev: f32 = vf as f32; 281 | if wide { 282 | sv = sv * 2.0 - 0.5; 283 | ev = ev * 2.0 - 0.5; 284 | } 285 | 286 | let (x, y) = self.tick_to_pos(l.ry + y_base); 287 | let sx = x + sv * track_lane_diff + half_track + half_lane; 288 | let ex = x + ev as f32 * track_lane_diff + half_track + half_lane; 289 | 290 | let (x, w): (f32, f32) = if sx > ex { 291 | (sx + half_lane, (ex - half_lane) - (sx + half_lane)) 292 | } else { 293 | (sx - half_lane, (ex + half_lane) - (sx - half_lane)) 294 | }; 295 | let end_rect_x = if sx > ex { 0.0 } else { self.lane_width() }; 296 | 297 | if with_uv { 298 | add_slam_rect(&mut mesh, rect_xy_wh([x, y, w, -slam_height])); 299 | 300 | mesh.add_rect_with_uv( 301 | rect_xy_wh([ 302 | x + w - end_rect_x, 303 | y - slam_height * self.tick_height.signum(), 304 | self.lane_width(), 305 | -slam_height, 306 | ]), 307 | slam_uv, 308 | color, 309 | ); 310 | } else { 311 | mesh.add_colored_rect(rect_xy_wh([x, y, w, -slam_height]), color); 312 | 313 | mesh.add_colored_rect( 314 | rect_xy_wh([ 315 | x + w - end_rect_x, 316 | y - slam_height, 317 | self.lane_width(), 318 | -slam_height, 319 | ]), 320 | color, 321 | ); 322 | } 323 | } 324 | } 325 | 326 | if let Some(l) = section.first() { 327 | if l.vf.is_some() { 328 | let mut sv: f32 = l.v as f32; 329 | if wide { 330 | sv = sv * 2.0 - 0.5; 331 | } 332 | 333 | let (x, y) = self.tick_to_pos(l.ry + y_base); 334 | let x = x + sv * track_lane_diff + half_track; 335 | if with_uv { 336 | let slam_rect = rect_xy_wh([ 337 | x, 338 | y - slam_height, 339 | self.lane_width(), 340 | slam_height * self.tick_height.signum(), 341 | ]); 342 | mesh.add_rect_with_uv(slam_rect, slam_uv, color); 343 | } else { 344 | mesh.add_colored_rect( 345 | rect_xy_wh([x, y, self.lane_width(), slam_height]), 346 | color, 347 | ); 348 | } 349 | } 350 | } 351 | 352 | let segment = Mesh { 353 | indices: mesh.indices, 354 | vertices: mesh.vertices, 355 | ..Default::default() 356 | }; 357 | mb.push(segment); 358 | 359 | Ok(()) 360 | } 361 | 362 | pub fn lane_width(&self) -> f32 { 363 | self.track_width / 6.0 364 | } 365 | 366 | pub fn ticks_per_col(&self) -> u32 { 367 | self.beats_per_col.saturating_mul(self.beat_res) 368 | } 369 | 370 | pub fn track_spacing(&self) -> f32 { 371 | self.track_width * 2.0 372 | } 373 | 374 | pub fn note_height_mult(&self) -> f32 { 375 | self.track_width / 72.0 376 | } 377 | 378 | pub fn tick_to_pos(&self, in_y: u32) -> (f32, f32) { 379 | let h = self.chart_draw_height(); 380 | let x = (in_y / self.ticks_per_col()) as f32 * self.track_spacing() + self.left_margin 381 | - self.x_offset; 382 | let y = (in_y % self.ticks_per_col()) as f32 * self.tick_height; 383 | let y = h - y + self.top_margin; 384 | (x, y) 385 | } 386 | 387 | pub fn chart_draw_height(&self) -> f32 { 388 | self.h - (self.bottom_margin + self.top_margin) + self.top 389 | } 390 | 391 | pub fn pos_to_tick(&self, in_x: f32, in_y: f32) -> u32 { 392 | self.pos_to_tick_f(in_x, in_y).floor() as u32 393 | } 394 | 395 | pub fn pos_to_tick_f(&self, in_x: f32, in_y: f32) -> f64 { 396 | let h = self.chart_draw_height() as f64; 397 | let y: f64 = 1.0 - ((in_y - self.top_margin).max(0.0) / h as f32).min(1.0) as f64; 398 | let x = (in_x + self.x_offset - self.left_margin) as f64; 399 | let x = math::round::floor(x as f64 / self.track_spacing() as f64, 0); 400 | ((y + x) * self.beats_per_col as f64 * self.beat_res as f64).max(0.0) 401 | } 402 | 403 | pub fn pos_to_lane(&self, in_x: f32) -> f32 { 404 | let mut x = (in_x + self.x_offset + self.left_margin) % self.track_spacing(); 405 | x = ((x - self.track_width as f32 / 2.0).max(0.0) / self.track_width as f32).min(1.0); 406 | (x * 6.0).min(6.0) as f32 407 | } 408 | 409 | pub fn update(&mut self, delta_time: f32, beat_res: u32) -> bool { 410 | self.beat_res = beat_res; 411 | self.x_offset = self.x_offset + (self.x_offset_target - self.x_offset) * delta_time; 412 | if (self.x_offset_target - self.x_offset).abs() < 0.5 { 413 | self.x_offset = self.x_offset_target; 414 | false 415 | } else { 416 | true 417 | } 418 | } 419 | 420 | pub fn get_control_point_pos_section( 421 | &self, 422 | points: &[GraphSectionPoint], 423 | start_y: u32, 424 | bounds: (f32, f32), 425 | track_bounds: Option<(f32, f32)>, 426 | ) -> Option { 427 | self.get_control_point_pos( 428 | &points 429 | .iter() 430 | .map(|p| GraphPoint { 431 | y: p.ry + start_y, 432 | v: p.v, 433 | vf: p.vf, 434 | a: p.a, 435 | b: p.b, 436 | }) 437 | .collect::>(), 438 | bounds, 439 | track_bounds, 440 | ) 441 | } 442 | 443 | pub fn get_control_point_pos( 444 | &self, 445 | points: &[GraphPoint], 446 | bounds: (f32, f32), 447 | track_bounds: Option<(f32, f32)>, 448 | ) -> Option { 449 | if let (None, None) = (points.get(0), points.get(1)) { 450 | return None; 451 | } 452 | 453 | let track_bounds = track_bounds.unwrap_or((0.0, 1.0)); 454 | 455 | let start = points.get(0).unwrap(); 456 | 457 | let (a, b) = if let (Some(a), Some(b)) = (start.a, start.b) { 458 | (a, b) 459 | } else { 460 | (0.5, 0.5) 461 | }; 462 | 463 | let transform_value = |v: f64| (v - bounds.0 as f64) / (bounds.1 - bounds.0) as f64; 464 | 465 | let start_value = if let Some(vf) = start.vf { 466 | transform_value(vf) 467 | } else { 468 | transform_value(start.v) 469 | }; 470 | let end = points.get(1).unwrap(); 471 | let start_tick = start.y; 472 | let end_tick = end.y; 473 | match start_tick.cmp(&end_tick) { 474 | std::cmp::Ordering::Greater => panic!("Laser section start later than end."), 475 | std::cmp::Ordering::Equal => return None, 476 | _ => {} 477 | }; 478 | let intervals = self.interval_to_ranges(&Interval { 479 | y: start_tick, 480 | l: end_tick - start_tick, 481 | }); 482 | 483 | if let Some(&(interval_x, interval_y, interval_h, (interval_start, interval_end))) = 484 | intervals.iter().find(|&&v| { 485 | let s = (v.3).0 as f64; 486 | let e = (v.3).1 as f64; 487 | a >= s && a <= e 488 | }) 489 | { 490 | let value_width = transform_value(end.v) - start_value; 491 | let x = (start_value + b * value_width) as f32; 492 | let x = track_bounds.0 + x * (track_bounds.1 - track_bounds.0); 493 | let x = x * self.track_width + interval_x + self.track_width / 2.0; 494 | let y = interval_y 495 | + interval_h * (a as f32 - interval_start) / (interval_end - interval_start); 496 | Some(Pos2::new(x, y)) 497 | } else { 498 | panic!("Curve `a` was not in any interval"); 499 | } 500 | } 501 | /// Returns (x,y,h, (start,end)) 502 | pub fn interval_to_ranges( 503 | &self, 504 | in_interval: &kson::Interval, 505 | ) -> Vec<(f32, f32, f32, (f32, f32))> { 506 | let mut res: Vec<(f32, f32, f32, (f32, f32))> = Vec::new(); 507 | let mut ranges: Vec<(u32, u32)> = Vec::new(); 508 | let ticks_per_col = self.beats_per_col.saturating_mul(self.beat_res); 509 | let mut start = in_interval.y; 510 | let end = start + in_interval.l; 511 | while start / ticks_per_col < end / ticks_per_col { 512 | ranges.push((start, ticks_per_col * (1 + start / ticks_per_col))); 513 | start = ticks_per_col * (1 + start / ticks_per_col); 514 | } 515 | ranges.push((start, end)); 516 | 517 | for (s, e) in ranges { 518 | let in_l = in_interval.l; 519 | let prog_s = (s - in_interval.y) as f32 / in_l as f32; 520 | let prog_e = (e - in_interval.y) as f32 / in_l as f32; 521 | let start_pos = self.tick_to_pos(s); 522 | let end_pos = self.tick_to_pos(e); 523 | if (start_pos.0 - end_pos.0).abs() > f32::EPSILON { 524 | res.push(( 525 | start_pos.0, 526 | start_pos.1, 527 | self.top_margin - start_pos.1, 528 | (prog_s, prog_e), 529 | )); 530 | } else { 531 | res.push(( 532 | start_pos.0, 533 | start_pos.1, 534 | end_pos.1 - start_pos.1, 535 | (prog_s, prog_e), 536 | )) 537 | } 538 | } 539 | res 540 | } 541 | } 542 | 543 | impl MainState { 544 | pub fn new() -> Result { 545 | let (new_chart, save_path) = if let Some(Ok(Some((chart, path)))) = std::env::args() 546 | .nth(1) 547 | .map(|p| open_chart_file(PathBuf::from(p))) 548 | { 549 | (chart, Some(path)) 550 | } else { 551 | let mut c = kson::Chart::new(); 552 | c.beat.bpm.push((0, 120.0)); 553 | c.beat.time_sig.push((0, kson::TimeSignature(4, 4))); 554 | 555 | (c, None) 556 | }; 557 | 558 | let (output_stream, handle) = OutputStream::try_default()?; 559 | let sink = Sink::try_new(&handle)?; 560 | 561 | let s = MainState { 562 | chart: new_chart.clone(), 563 | screen: ScreenState { 564 | top: 0.0, 565 | w: 800.0, 566 | h: 600.0, 567 | tick_height: 1.0, 568 | track_width: 72.0, 569 | top_margin: 60.0, 570 | bottom_margin: 10.0, 571 | left_margin: 0.0, 572 | beats_per_col: 16, 573 | x_offset: 0.0, 574 | x_offset_target: 0.0, 575 | beat_res: 48, 576 | curve_per_tick: 1.5, 577 | }, 578 | gui_event_queue: VecDeque::new(), 579 | save_path, 580 | mouse_x: 0.0, 581 | mouse_y: 0.0, 582 | current_tool: ChartTool::None, 583 | 584 | cursor_object: None, 585 | audio_playback: playback::AudioPlayback::new(), 586 | cursor_line: 0, 587 | actions: action_stack::ActionStack::new(new_chart), 588 | laser_colors: [ 589 | Color32::from_rgba_unmultiplied(0, 115, 144, 127), 590 | Color32::from_rgba_unmultiplied(194, 6, 140, 127), 591 | ], 592 | sink, 593 | output_stream, 594 | output_stream_handle: handle, 595 | }; 596 | Ok(s) 597 | } 598 | 599 | pub fn get_cursor_ms_from_mouse(&self) -> f64 { 600 | let tick = self.screen.pos_to_tick(self.mouse_x, self.mouse_y); 601 | let tick = tick - (tick % (self.chart.beat.resolution / 2)); 602 | self.chart.tick_to_ms(tick) 603 | } 604 | 605 | pub fn get_cursor_tick_from_mouse(&self) -> u32 { 606 | self.screen.pos_to_tick(self.mouse_x, self.mouse_y) 607 | } 608 | 609 | pub fn get_cursor_tick_from_mouse_f(&self) -> f64 { 610 | self.screen.pos_to_tick_f(self.mouse_x, self.mouse_y) 611 | } 612 | 613 | pub fn get_cursor_lane_from_mouse(&self) -> f32 { 614 | self.screen.pos_to_lane(self.mouse_x) 615 | } 616 | 617 | pub fn get_current_cursor_tick(&self) -> f32 { 618 | if self.audio_playback.is_playing() { 619 | self.audio_playback.get_tick(&self.chart) as f32 620 | } else { 621 | self.cursor_line as f32 622 | } 623 | } 624 | 625 | pub fn draw_cursor_line(&self, painter: &Painter, tick: u32, color: Color32) { 626 | let (x, y) = self.screen.tick_to_pos(tick as u32); 627 | let x = x + self.screen.track_width / 2.0; 628 | let p1 = egui::pos2(x, y); 629 | let p2 = egui::pos2(x + self.screen.track_width, y); 630 | 631 | painter.line_segment([p1, p2], Stroke { color, width: 1.5 }); 632 | } 633 | 634 | pub fn draw_graph( 635 | &self, 636 | graph: &impl kson::Graph, 637 | painter: &Painter, 638 | bounds: (f32, f32), 639 | stroke: Stroke, 640 | ) { 641 | let transform_value = |v: f32| (v - bounds.0) / (bounds.1 - bounds.0); 642 | 643 | let ticks_per_col = self.screen.beats_per_col * self.chart.beat.resolution; 644 | let min_tick_render = self.screen.pos_to_tick(-100.0, self.screen.h); 645 | let max_tick_render = self.screen.pos_to_tick(self.screen.w + 50.0, 0.0); 646 | 647 | let min_tick_render = min_tick_render - min_tick_render % ticks_per_col; 648 | 649 | let max_tick_render = max_tick_render - max_tick_render % ticks_per_col; 650 | 651 | let resolution = 3; 652 | for col in (min_tick_render..max_tick_render) 653 | .collect::>() 654 | .chunks(ticks_per_col as usize) 655 | { 656 | for segment_ticks in col.windows(resolution).step_by(resolution - 1) { 657 | //could miss end of column with bad resolutions 658 | let s = segment_ticks[0]; 659 | let e = segment_ticks[resolution - 1]; 660 | let sv = transform_value(graph.value_at(s as f64) as f32); 661 | let ev = transform_value(graph.value_at(e as f64) as f32); 662 | 663 | let (sx, sy) = self.screen.tick_to_pos(s); 664 | let (ex, ey) = self.screen.tick_to_pos(e); 665 | 666 | let sx = sx + sv * self.screen.track_width + self.screen.track_width / 2.0; 667 | let ex = ex + ev * self.screen.track_width + self.screen.track_width / 2.0; 668 | 669 | painter.line_segment([pos2(sx, sy), pos2(ex, ey)], stroke); 670 | } 671 | } 672 | } 673 | //TODO: Shares most code with draw_graph, combine somehow? 674 | pub fn draw_graph_segmented( 675 | &self, 676 | graph: &impl kson::Graph>, 677 | painter: &Painter, 678 | bounds: (f32, f32), 679 | stroke: Stroke, 680 | ) { 681 | let transform_value = |v: f32| (v - bounds.0) / (bounds.1 - bounds.0); 682 | 683 | let ticks_per_col = self.screen.beats_per_col * self.chart.beat.resolution; 684 | let min_tick_render = self.screen.pos_to_tick(-100.0, self.screen.h); 685 | let max_tick_render = self.screen.pos_to_tick(self.screen.w + 50.0, 0.0); 686 | 687 | let min_tick_render = min_tick_render - min_tick_render % ticks_per_col; 688 | 689 | let max_tick_render = max_tick_render - max_tick_render % ticks_per_col; 690 | 691 | let resolution = 3; 692 | for col in (min_tick_render..max_tick_render) 693 | .collect::>() 694 | .chunks(ticks_per_col as usize) 695 | { 696 | for segment_ticks in col.windows(resolution).step_by(resolution - 1) { 697 | //could miss end of column with bad resolutions 698 | let s = segment_ticks[0]; 699 | let e = segment_ticks[resolution - 1]; 700 | 701 | let sv = graph.value_at(s as f64); 702 | let ev = graph.value_at(e as f64); 703 | 704 | let (sv, ev) = match (sv, ev) { 705 | (Some(sv), Some(ev)) => (sv, ev), 706 | _ => continue, 707 | }; 708 | 709 | let sv = transform_value(sv as f32); 710 | let ev = transform_value(ev as f32); 711 | 712 | let (sx, sy) = self.screen.tick_to_pos(s); 713 | let (ex, ey) = self.screen.tick_to_pos(e); 714 | 715 | let sx = sx + sv * self.screen.track_width + self.screen.track_width / 2.0; 716 | let ex = ex + ev * self.screen.track_width + self.screen.track_width / 2.0; 717 | 718 | painter.line_segment([pos2(sx, sy), pos2(ex, ey)], stroke); 719 | } 720 | } 721 | } 722 | 723 | pub fn save(&mut self) -> Result { 724 | match (&self.save_path, self.actions.get_current()) { 725 | (None, Ok(chart)) => { 726 | if let Some(new_path) = save_chart_as(&chart).unwrap_or_else(|e| { 727 | println!("Failed to save chart:"); 728 | println!("\t{}", e); 729 | None 730 | }) { 731 | self.save_path = Some(new_path); 732 | self.actions.save(); 733 | Ok(true) 734 | } else { 735 | Ok(false) 736 | } 737 | } 738 | (Some(path), Ok(chart)) => { 739 | let mut file = File::create(&path).unwrap(); 740 | profile_scope!("Write kson"); 741 | file.write_all(serde_json::to_string(&chart)?.as_bytes())?; 742 | self.actions.save(); 743 | Ok(true) 744 | } 745 | _ => bail!("Could not save chart."), 746 | } 747 | } 748 | 749 | pub fn update(&mut self, ctx: &Context) -> Result<()> { 750 | while let Some(e) = self.gui_event_queue.pop_front() { 751 | match e { 752 | GuiEvent::Open => { 753 | if let Some(new_chart) = open_chart().unwrap_or_else(|e| { 754 | println!("Failed to open chart:"); 755 | println!("\t{}", e); 756 | None 757 | }) { 758 | self.chart = new_chart.0.clone(); 759 | self.actions.reset(new_chart.0); 760 | self.save_path = Some(new_chart.1); 761 | } 762 | } 763 | GuiEvent::Save => { 764 | self.save()?; 765 | } 766 | GuiEvent::SaveAs => { 767 | if let Ok(chart) = self.actions.get_current() { 768 | if let Some(new_path) = save_chart_as(&chart).unwrap_or_else(|e| { 769 | println!("Failed to save chart:"); 770 | println!("\t{}", e); 771 | None 772 | }) { 773 | self.save_path = Some(new_path); 774 | self.actions.save(); 775 | } 776 | } 777 | } 778 | GuiEvent::ToolChanged(new_tool) => { 779 | if self.current_tool != new_tool { 780 | self.cursor_object = match new_tool { 781 | ChartTool::None => None, 782 | ChartTool::BT => Some(Box::new(ButtonInterval::new(false))), 783 | ChartTool::FX => Some(Box::new(ButtonInterval::new(true))), 784 | ChartTool::LLaser => Some(Box::new(LaserTool::new(false))), 785 | ChartTool::RLaser => Some(Box::new(LaserTool::new(true))), 786 | ChartTool::BPM => Some(Box::new(BpmTool::new())), 787 | ChartTool::TimeSig => Some(Box::new(TimeSigTool::new())), 788 | ChartTool::Camera => Some(Box::new(CameraTool::default())), 789 | }; 790 | self.current_tool = new_tool; 791 | ctx.request_repaint(); 792 | } 793 | } 794 | GuiEvent::Undo => self.actions.undo(), 795 | GuiEvent::Redo => self.actions.redo(), 796 | GuiEvent::NewChart(new_chart_opts) => { 797 | let mut new_chart = kson::Chart::new(); 798 | new_chart.beat.bpm.push((0, 120.0)); 799 | new_chart.beat.time_sig.push((0, kson::TimeSignature(4, 4))); 800 | 801 | let audio_pathbuf = std::path::PathBuf::from(new_chart_opts.audio); 802 | new_chart.audio.bgm = Some(kson::BgmInfo { 803 | filename: Some(String::from( 804 | audio_pathbuf.file_name().unwrap().to_str().unwrap(), 805 | )), 806 | offset: 0, 807 | vol: 1.0, 808 | preview: { 809 | kson::PreviewInfo { 810 | offset: 0, 811 | duration: 15000, 812 | preview_filename: None, 813 | } 814 | }, 815 | legacy: kson::LegacyBgmInfo { 816 | fp_filenames: vec![], 817 | }, 818 | }); 819 | self.save_path = if let Some(save_path) = new_chart_opts.destination { 820 | //copy audio file 821 | let mut audio_new_path = save_path.clone(); 822 | audio_new_path.push(audio_pathbuf.file_name().unwrap()); 823 | if !audio_new_path.exists() { 824 | std::fs::copy(audio_pathbuf, audio_new_path).unwrap(); 825 | } 826 | Some(save_path) 827 | } else { 828 | Some(audio_pathbuf.parent().unwrap().to_path_buf()) 829 | }; 830 | 831 | let mut kson_path = self.save_path.clone().unwrap(); 832 | kson_path.push(new_chart_opts.filename); 833 | kson_path.set_extension("kson"); 834 | self.save_path = Some(kson_path.clone()); 835 | if let Ok(mut file) = File::create(kson_path) { 836 | file.write_all(serde_json::to_string(&new_chart).unwrap().as_bytes()) 837 | .unwrap(); 838 | } 839 | self.actions.reset(new_chart.clone()); 840 | self.chart = new_chart; 841 | } 842 | GuiEvent::ExportKsh => { 843 | if let Ok(chart) = self.actions.get_current() { 844 | let dialog_result = nfd::open_save_dialog(Some("ksh"), None); 845 | 846 | if let Ok(nfd::Response::Okay(file_path)) = dialog_result { 847 | let mut path = PathBuf::from(file_path); 848 | path.set_extension("ksh"); 849 | let file = File::create(&path).unwrap(); 850 | profile_scope!("Write KSH"); 851 | chart.to_ksh(file)?; 852 | } 853 | } 854 | } 855 | GuiEvent::Play => { 856 | if self.audio_playback.is_playing() { 857 | self.audio_playback.stop() 858 | } else if let Some(path) = &self.save_path { 859 | let path = Path::new(path).parent().unwrap(); 860 | if let Some(bgm) = &self.chart.audio.bgm { 861 | if let Some(filename) = &bgm.filename { 862 | let filename = &filename.split(';').next().unwrap(); 863 | let path = path.join(Path::new(filename)); 864 | info!("Playing file: {}", path.display()); 865 | let path = path.to_str().unwrap(); 866 | match self.audio_playback.open_path(path) { 867 | Ok(_) => { 868 | let ms = self.chart.tick_to_ms(self.cursor_line) 869 | + bgm.offset as f64; 870 | let ms = ms.max(0.0); 871 | self.audio_playback.build_effects(&self.chart); 872 | self.audio_playback.set_poistion(ms); 873 | self.audio_playback.play(); 874 | if self.sink.len() > 0 { 875 | self.sink.clear(); 876 | self.sink.sleep_until_end(); 877 | } 878 | self.sink.append( 879 | self.audio_playback 880 | .get_source() 881 | .expect("Source not available"), 882 | ); 883 | 884 | self.audio_playback.play(); 885 | 886 | self.sink.play(); 887 | } 888 | Err(msg) => { 889 | println!("{}", msg); 890 | } 891 | } 892 | } 893 | } 894 | } 895 | } 896 | GuiEvent::Home => self.screen.x_offset_target = 0.0, 897 | GuiEvent::End => { 898 | let mut target: f32 = 0.0; 899 | 900 | //check pos of last bt 901 | for i in 0..4 { 902 | if let Some(note) = self.chart.note.bt[i].last() { 903 | target = target.max( 904 | self.screen.tick_to_pos(note.y + note.l).0 + self.screen.x_offset, 905 | ) 906 | } 907 | } 908 | 909 | //check pos of last fx 910 | for i in 0..2 { 911 | if let Some(note) = self.chart.note.fx[i].last() { 912 | target = target.max( 913 | self.screen.tick_to_pos(note.y + note.l).0 + self.screen.x_offset, 914 | ) 915 | } 916 | } 917 | 918 | //check pos of last lasers 919 | for i in 0..2 { 920 | if let Some(section) = self.chart.note.laser[i].last() { 921 | if let Some(segment) = section.last() { 922 | target = target.max( 923 | self.screen.tick_to_pos(segment.ry + section.tick()).0 924 | + self.screen.x_offset, 925 | ) 926 | } 927 | } 928 | } 929 | 930 | self.screen.x_offset_target = target - (target % self.screen.track_spacing()) 931 | } 932 | GuiEvent::Next => { 933 | self.screen.x_offset_target = (self.screen.x_offset_target 934 | - (self.screen.w - (self.screen.w % self.screen.track_spacing()))) 935 | .max(0.0) 936 | } 937 | GuiEvent::Previous => { 938 | self.screen.x_offset_target += 939 | self.screen.w - (self.screen.w % self.screen.track_spacing()) 940 | } 941 | _ => (), 942 | } 943 | } 944 | if let Ok(current_chart) = self.actions.get_current() { 945 | self.chart = current_chart; 946 | } 947 | 948 | let delta_time = (10.0 * ctx.input().unstable_dt).min(1.0); 949 | if self.screen.update(delta_time, self.chart.beat.resolution) 950 | || self.audio_playback.is_playing() 951 | { 952 | ctx.request_repaint(); 953 | } 954 | let tick = self.audio_playback.get_tick(&self.chart); 955 | self.audio_playback.update(tick); 956 | Ok(()) 957 | } 958 | 959 | pub fn draw(&mut self, ui: &Ui) -> Result { 960 | puffin::profile_function!(); 961 | 962 | ui.make_persistent_id(EGUI_ID); 963 | self.resize_event(ui.max_rect()); 964 | 965 | let painter = ui.painter_at(ui.max_rect()); 966 | //draw notes 967 | let mut track_line_builder = Vec::new(); 968 | let mut track_measure_builder = Vec::new(); 969 | let mut bt_builder = Vec::new(); 970 | let mut long_bt_builder = Vec::new(); 971 | let mut fx_builder = Vec::new(); 972 | let mut long_fx_builder = Vec::new(); 973 | let mut laser_builder = Vec::new(); 974 | let min_tick_render = self.screen.pos_to_tick(-100.0, self.screen.h); 975 | let max_tick_render = self.screen.pos_to_tick(self.screen.w + 50.0, 0.0); 976 | info!("Sink: {}, {}", self.sink.is_paused(), self.sink.len()); 977 | 978 | let chart_draw_height = self.screen.chart_draw_height(); 979 | let lane_width = self.screen.lane_width(); 980 | let track_spacing = self.screen.track_spacing(); 981 | { 982 | profile_scope!("Build components"); 983 | //draw track 984 | { 985 | let track_count = 2 + (self.screen.w / self.screen.track_spacing()) as u32; 986 | profile_scope!("Track Components"); 987 | let x = self.screen.track_width / 2.0 + lane_width + self.screen.left_margin 988 | - (self.screen.x_offset % (self.screen.track_width * 2.0)); 989 | for i in 0..track_count { 990 | let x = x + i as f32 * track_spacing; 991 | for j in 0..5 { 992 | let x = x + j as f32 * lane_width; 993 | track_line_builder.push(Shape::rect_filled( 994 | rect_xy_wh([x, self.screen.top_margin, 1.0, chart_draw_height]), 995 | 0.0, 996 | Color32::GRAY, 997 | )); 998 | } 999 | } 1000 | 1001 | //measure & beat lines 1002 | let x = self.screen.track_width / 2.0 + self.screen.lane_width(); 1003 | let w = self.screen.lane_width() * 4.0; 1004 | for (tick, is_measure) in self.chart.beat_line_iter() { 1005 | if tick < min_tick_render { 1006 | continue; 1007 | } else if tick > max_tick_render { 1008 | break; 1009 | } 1010 | 1011 | let (tx, y) = self.screen.tick_to_pos(tick); 1012 | let x = tx + x; 1013 | let color = if is_measure { 1014 | Rgba::from_rgb(1.0, 1.0, 0.0) 1015 | } else { 1016 | Rgba::from_gray(0.5) 1017 | }; 1018 | track_measure_builder.push(Shape::rect_filled( 1019 | rect_xy_wh([x, painter.round_to_pixel(y), w, -1.0]), 1020 | 0.0, 1021 | color, 1022 | )); 1023 | } 1024 | } 1025 | 1026 | //bt 1027 | { 1028 | profile_scope!("BT Components"); 1029 | for i in 0..4 { 1030 | for n in &self.chart.note.bt[i] { 1031 | if n.y + n.l < min_tick_render { 1032 | continue; 1033 | } 1034 | if n.y > max_tick_render { 1035 | break; 1036 | } 1037 | 1038 | if n.l == 0 { 1039 | let (x, y) = self.screen.tick_to_pos(n.y); 1040 | 1041 | let x = x 1042 | + i as f32 * self.screen.lane_width() 1043 | + 1.0 * i as f32 1044 | + self.screen.lane_width() 1045 | + self.screen.track_width / 2.0; 1046 | let y = y as f32; 1047 | let w = self.screen.track_width as f32 / 6.0 - 2.0; 1048 | let h = -2.0 * self.screen.note_height_mult(); 1049 | 1050 | bt_builder.push(Shape::rect_filled( 1051 | rect_xy_wh([x, y, w, h]), 1052 | 0.0, 1053 | Color32::WHITE, 1054 | )); 1055 | } else { 1056 | for (x, y, h, _) in self.screen.interval_to_ranges(n) { 1057 | let x = x 1058 | + i as f32 * self.screen.lane_width() 1059 | + 1.0 * i as f32 1060 | + self.screen.lane_width() 1061 | + self.screen.track_width / 2.0; 1062 | let w = self.screen.track_width as f32 / 6.0 - 2.0; 1063 | 1064 | long_bt_builder.push(Shape::rect_filled( 1065 | rect_xy_wh([x, y, w, h]), 1066 | 0.0, 1067 | Color32::WHITE, 1068 | )); 1069 | } 1070 | } 1071 | } 1072 | } 1073 | } 1074 | 1075 | //fx 1076 | { 1077 | profile_scope!("FX Components"); 1078 | for i in 0..2 { 1079 | for n in &self.chart.note.fx[i] { 1080 | if n.y + n.l < min_tick_render { 1081 | continue; 1082 | } 1083 | if n.y > max_tick_render { 1084 | break; 1085 | } 1086 | 1087 | if n.l == 0 { 1088 | let (x, y) = self.screen.tick_to_pos(n.y); 1089 | 1090 | let x = x 1091 | + (i as f32 * self.screen.lane_width() * 2.0) 1092 | + self.screen.track_width / 2.0 1093 | + 2.0 * i as f32 1094 | + self.screen.lane_width(); 1095 | let w = self.screen.lane_width() * 2.0 - 1.0; 1096 | let h = -2.0 * self.screen.note_height_mult(); 1097 | let color = Color32::from_rgb(255, 77, 0); 1098 | 1099 | fx_builder.push(Shape::rect_filled( 1100 | rect_xy_wh([x, y, w, h]), 1101 | 0.0, 1102 | color, 1103 | )); 1104 | } else { 1105 | for (x, y, h, _) in self.screen.interval_to_ranges(n) { 1106 | let x = x 1107 | + (i as f32 * self.screen.lane_width() * 2.0) 1108 | + self.screen.track_width / 2.0 1109 | + 2.0 * i as f32 1110 | + self.screen.lane_width(); 1111 | let w = self.screen.lane_width() * 2.0 - 1.0; 1112 | let color = Color32::from_rgba_unmultiplied(255, 77, 0, 180); 1113 | 1114 | long_fx_builder.push(Shape::rect_filled( 1115 | rect_xy_wh([x, y, w, h]), 1116 | 0.0, 1117 | color, 1118 | )); 1119 | } 1120 | } 1121 | } 1122 | } 1123 | } 1124 | 1125 | //laser 1126 | { 1127 | profile_scope!("Laser Components"); 1128 | for (lane, color) in self.chart.note.laser.iter().zip(self.laser_colors.iter()) { 1129 | for section in lane { 1130 | let y_base = section.tick(); 1131 | if section.last().unwrap().ry + y_base < min_tick_render { 1132 | continue; 1133 | } 1134 | if y_base > max_tick_render { 1135 | break; 1136 | } 1137 | 1138 | self.screen.draw_laser_section( 1139 | section, 1140 | &mut laser_builder, 1141 | *color, 1142 | false, 1143 | )?; 1144 | } 1145 | } 1146 | } 1147 | } 1148 | 1149 | //meshses 1150 | { 1151 | profile_scope!("Build Meshes"); 1152 | //draw built meshes 1153 | //track 1154 | { 1155 | profile_scope!("Track Mesh"); 1156 | painter.extend(track_line_builder); 1157 | painter.extend(track_measure_builder); 1158 | } 1159 | //long fx 1160 | { 1161 | profile_scope!("Long FX Mesh"); 1162 | painter.extend(long_fx_builder); 1163 | } 1164 | //long bt 1165 | { 1166 | profile_scope!("Long BT Mesh"); 1167 | painter.extend(long_bt_builder); 1168 | } 1169 | //fx 1170 | { 1171 | profile_scope!("FX Mesh"); 1172 | painter.extend(fx_builder); 1173 | } 1174 | //bt 1175 | { 1176 | profile_scope!("BT Mesh"); 1177 | painter.extend(bt_builder); 1178 | } 1179 | //laser 1180 | { 1181 | profile_scope!("Laser Mesh"); 1182 | painter.extend(laser_builder.into_iter().map(Shape::mesh).collect()); 1183 | } 1184 | } 1185 | 1186 | if let Some(cursor) = &self.cursor_object { 1187 | profile_scope!("Tool"); 1188 | cursor 1189 | .draw(self, &painter) 1190 | .unwrap_or_else(|e| println!("{}", e)); 1191 | } 1192 | 1193 | { 1194 | self.draw_cursor_line( 1195 | &painter, 1196 | self.get_current_cursor_tick() as u32, 1197 | Color32::from_rgb(255u8, 0u8, 0u8), 1198 | ); 1199 | } 1200 | 1201 | //BPM & Time Signatures 1202 | { 1203 | profile_scope!("BPM & Time Signatures"); 1204 | let mut changes: Vec<(u32, Vec<(String, Color32)>)> = Vec::new(); 1205 | { 1206 | profile_scope!("Build BPM & Time signature change list"); 1207 | for bpm_change in &self.chart.beat.bpm { 1208 | let color = Color32::from_rgba_unmultiplied(0, 128, 255, 255); 1209 | 1210 | let entry = ( 1211 | emath::format_with_decimals_in_range(bpm_change.1, 0..=3), 1212 | color, 1213 | ); 1214 | match changes.binary_search_by(|c| c.0.cmp(&bpm_change.0)) { 1215 | Ok(idx) => changes.get_mut(idx).unwrap().1.push(entry), 1216 | Err(new_idx) => { 1217 | let new_vec = vec![entry]; 1218 | changes.insert(new_idx, (bpm_change.0, new_vec)); 1219 | } 1220 | } 1221 | } 1222 | 1223 | for ts_change in &self.chart.beat.time_sig { 1224 | let tick = self.chart.measure_to_tick(ts_change.0); 1225 | 1226 | let color = Color32::from_rgba_premultiplied(255, 255, 0, 255); 1227 | let entry = (format!("{}/{}", ts_change.1 .0, ts_change.1 .1), color); 1228 | 1229 | match changes.binary_search_by(|c| c.0.cmp(&tick)) { 1230 | Ok(idx) => changes.get_mut(idx).unwrap().1.push(entry), 1231 | Err(new_idx) => { 1232 | let new_vec = vec![entry]; 1233 | changes.insert(new_idx, (tick, new_vec)); 1234 | } 1235 | } 1236 | } 1237 | } 1238 | 1239 | { 1240 | //TODO: Cache text, it renders very slow but it will have to do for now 1241 | profile_scope!("Build Text"); 1242 | for c in changes { 1243 | if c.0 < min_tick_render { 1244 | continue; 1245 | } else if c.0 > max_tick_render { 1246 | break; 1247 | } 1248 | let (x, y) = self.screen.tick_to_pos(c.0); 1249 | let x = x + self.screen.track_width * 1.5; 1250 | let line_height = 12.0; 1251 | 1252 | for (i, (text, color)) in c.1.iter().enumerate() { 1253 | painter.text( 1254 | pos2(x, y - i as f32 * line_height), 1255 | Align2::RIGHT_BOTTOM, 1256 | text, 1257 | FontId::monospace(12.0), 1258 | *color, 1259 | ); 1260 | } 1261 | } 1262 | } 1263 | } 1264 | 1265 | Ok(ui.interact(ui.max_rect(), ui.id(), Sense::click_and_drag())) 1266 | } 1267 | 1268 | pub fn drag_start(&mut self, button: PointerButton, x: f32, y: f32, modifiers: &Modifiers) { 1269 | if let PointerButton::Primary = button { 1270 | let res = self.chart.beat.resolution; 1271 | let lane = self.screen.pos_to_lane(x); 1272 | let tick = self.screen.pos_to_tick(x, y); 1273 | let tick = tick - (tick % (res / 2)); 1274 | let tick_f = self.screen.pos_to_tick_f(x, y); 1275 | if let Some(ref mut cursor) = self.cursor_object { 1276 | cursor.drag_start( 1277 | self.screen, 1278 | tick, 1279 | tick_f, 1280 | lane, 1281 | &self.chart, 1282 | &mut self.actions, 1283 | pos2(x, y), 1284 | modifiers, 1285 | ) 1286 | } 1287 | } 1288 | } 1289 | 1290 | pub fn drag_end(&mut self, button: PointerButton, x: f32, y: f32) { 1291 | if let PointerButton::Primary = button { 1292 | let lane = self.screen.pos_to_lane(x); 1293 | let tick = self.screen.pos_to_tick(x, y); 1294 | let tick_f = self.screen.pos_to_tick_f(x, y); 1295 | let tick = tick - (tick % (self.chart.beat.resolution / 2)); 1296 | if let Some(cursor) = &mut self.cursor_object { 1297 | cursor.drag_end( 1298 | self.screen, 1299 | tick, 1300 | tick_f, 1301 | lane, 1302 | &self.chart, 1303 | &mut self.actions, 1304 | pos2(x, y), 1305 | ); 1306 | } 1307 | } 1308 | } 1309 | 1310 | fn resize_event(&mut self, size: Rect) { 1311 | self.screen.w = size.width(); 1312 | self.screen.h = size.height(); 1313 | self.screen.top = size.top(); 1314 | self.screen.top_margin = size.top() + 20.0; 1315 | self.screen.left_margin = size.left(); 1316 | 1317 | self.screen.tick_height = self.screen.chart_draw_height() 1318 | / (self.chart.beat.resolution * self.screen.beats_per_col) as f32; 1319 | } 1320 | 1321 | fn get_clicked_data(&self, pos: Pos2) -> (f32, u32, f64) { 1322 | let lane = self.screen.pos_to_lane(pos.x); 1323 | let tick = self.screen.pos_to_tick(pos.x, pos.y); 1324 | let tick_f: f64 = self.screen.pos_to_tick_f(pos.x, pos.y); 1325 | let tick = tick - (tick % (self.chart.beat.resolution / 2)); 1326 | 1327 | (lane, tick, tick_f) 1328 | } 1329 | 1330 | pub fn primary_clicked(&mut self, pos: Pos2) { 1331 | self.mouse_x = pos.x; 1332 | self.mouse_y = pos.y; 1333 | let (lane, tick, tick_f) = self.get_clicked_data(pos); 1334 | self.cursor_line = tick; 1335 | 1336 | if let Some(cursor) = &mut self.cursor_object { 1337 | cursor.primary_click( 1338 | self.screen, 1339 | tick, 1340 | tick_f, 1341 | lane, 1342 | &self.chart, 1343 | &mut self.actions, 1344 | pos2(pos.x, pos.y), 1345 | ); 1346 | } 1347 | } 1348 | 1349 | pub fn middle_clicked(&mut self, pos: Pos2) { 1350 | self.mouse_x = pos.x; 1351 | self.mouse_y = pos.y; 1352 | let (lane, tick, tick_f) = self.get_clicked_data(pos); 1353 | 1354 | if let Some(cursor) = &mut self.cursor_object { 1355 | cursor.middle_click( 1356 | self.screen, 1357 | tick, 1358 | tick_f, 1359 | lane, 1360 | &self.chart, 1361 | &mut self.actions, 1362 | pos2(pos.x, pos.y), 1363 | ) 1364 | } 1365 | } 1366 | 1367 | pub fn mouse_motion_event(&mut self, pos: Pos2) { 1368 | self.mouse_x = pos.x; 1369 | self.mouse_y = pos.y; 1370 | let (lane, tick, tick_f) = self.get_clicked_data(pos); 1371 | 1372 | if let Some(cursor) = &mut self.cursor_object { 1373 | cursor.update(tick, tick_f, lane, pos2(pos.x, pos.y), &self.chart); 1374 | } 1375 | } 1376 | 1377 | pub fn mouse_wheel_event(&mut self, y: f32) { 1378 | self.screen.x_offset_target += y.signum() * self.screen.track_width * 2.0; 1379 | self.screen.x_offset_target = self.screen.x_offset_target.max(0.0); 1380 | } 1381 | } 1382 | 1383 | fn get_extension_from_filename(filename: &str) -> Option<&str> { 1384 | Path::new(filename).extension().and_then(OsStr::to_str) 1385 | } 1386 | 1387 | //https://github.com/m4saka/ksh2kson/issues/4#issuecomment-573343229 1388 | pub fn do_curve(x: f64, a: f64, b: f64) -> f64 { 1389 | let t = if x < std::f64::EPSILON || a < std::f64::EPSILON { 1390 | (a - (a * a + x - 2.0 * a * x).sqrt()) / (-1.0 + 2.0 * a) 1391 | } else { 1392 | x / (a + (a * a + (1.0 - 2.0 * a) * x).sqrt()) 1393 | }; 1394 | 2.0 * (1.0 - t) * t * b + t * t 1395 | } 1396 | 1397 | fn open_chart_file(path: PathBuf) -> Result> { 1398 | match path.extension().and_then(OsStr::to_str).unwrap_or_default() { 1399 | "ksh" => { 1400 | let mut data = String::from(""); 1401 | File::open(&path).unwrap().read_to_string(&mut data)?; 1402 | Ok(Some((kson::Chart::from_ksh(&data)?, path))) 1403 | } 1404 | "kson" => { 1405 | let file = File::open(&path)?; 1406 | let reader = BufReader::new(file); 1407 | profile_scope!("kson parse"); 1408 | Ok(Some((serde_json::from_reader(reader)?, path))) 1409 | } 1410 | "vox" => { 1411 | let mut data = String::from(""); 1412 | File::open(&path).unwrap().read_to_string(&mut data)?; 1413 | Ok(Some((kson::Chart::from_vox(&data)?, path))) 1414 | } 1415 | 1416 | _ => Ok(None), 1417 | } 1418 | } 1419 | 1420 | fn open_chart() -> Result> { 1421 | let dialog_result = nfd::dialog().filter("ksh,kson").open()?; 1422 | 1423 | match dialog_result { 1424 | nfd::Response::Okay(file_path) => { 1425 | let path = PathBuf::from(&file_path); 1426 | open_chart_file(path) 1427 | } 1428 | _ => Ok(None), 1429 | } 1430 | } 1431 | 1432 | fn save_chart_as(chart: &kson::Chart) -> Result> { 1433 | let dialog_result = nfd::open_save_dialog(Some("kson"), None)?; 1434 | 1435 | match dialog_result { 1436 | nfd::Response::Okay(file_path) => { 1437 | let mut path = PathBuf::from(&file_path); 1438 | path.set_extension("kson"); 1439 | let mut file = File::create(&path).unwrap(); 1440 | profile_scope!("Write kson"); 1441 | file.write_all(serde_json::to_string(&chart)?.as_bytes())?; 1442 | Ok(Some(path)) 1443 | } 1444 | _ => Ok(None), 1445 | } 1446 | } 1447 | -------------------------------------------------------------------------------- /src/i18n.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::{ 2 | fluent::{fluent_language_loader, FluentLanguageLoader}, 3 | DefaultLocalizer, LanguageLoader, Localizer, 4 | }; 5 | use once_cell::sync::Lazy; 6 | use rust_embed::RustEmbed; 7 | 8 | #[derive(RustEmbed)] 9 | #[folder = "i18n/"] 10 | struct Localizations; 11 | 12 | pub static LANGUAGE_LOADER: Lazy = Lazy::new(|| { 13 | let loader: FluentLanguageLoader = fluent_language_loader!(); 14 | 15 | // Load the fallback langauge by default so that users of the 16 | // library don't need to if they don't care about localization. 17 | loader 18 | .load_fallback_language(&Localizations) 19 | .expect("Error while loading fallback language"); 20 | 21 | loader 22 | }); 23 | 24 | macro_rules! fl { 25 | ($message_id:literal) => {{ 26 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) 27 | }}; 28 | 29 | ($message_id:literal, $($args:expr),*) => {{ 30 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) 31 | }}; 32 | } 33 | 34 | pub fn localizer() -> Box { 35 | Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) 36 | } 37 | 38 | pub(crate) use fl; 39 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | use std::collections::HashMap; 4 | use std::path::PathBuf; 5 | use std::str::FromStr; 6 | 7 | use anyhow::Result; 8 | use chart_editor::MainState; 9 | use eframe::egui::style::Selection; 10 | use eframe::egui::{ 11 | self, menu, warn_if_debug_build, Button, Color32, ComboBox, DragValue, Frame, Grid, Key, Label, 12 | Layout, Pos2, Rect, Response, RichText, Sense, Slider, Ui, Vec2, Visuals, 13 | }; 14 | use eframe::App; 15 | use i18n_embed::unic_langid::LanguageIdentifier; 16 | use kson::{BgmInfo, Chart, MetaInfo}; 17 | use puffin::profile_scope; 18 | use serde::{Deserialize, Serialize}; 19 | 20 | mod action_stack; 21 | mod assets; 22 | mod camera_widget; 23 | mod chart_camera; 24 | mod chart_editor; 25 | mod i18n; 26 | mod tools; 27 | mod utils; 28 | 29 | pub trait Widget { 30 | fn ui(self, ui: &mut Ui) -> Response; 31 | } 32 | 33 | use i18n::fl; 34 | use tracing::info; 35 | 36 | #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] 37 | pub struct NewChartOptions { 38 | audio: String, 39 | filename: String, 40 | destination: Option, 41 | } 42 | 43 | impl Widget for &mut kson::MetaInfo { 44 | fn ui(self, ui: &mut Ui) -> Response { 45 | let edit_row = |ui: &mut Ui, label: &str, data: &mut String| { 46 | ui.label(label); 47 | ui.text_edit_singleline(data); 48 | ui.end_row(); 49 | }; 50 | 51 | egui::Grid::new("metadata_editor") 52 | .show(ui, |ui| { 53 | edit_row(ui, &i18n::fl!("title"), &mut self.title); 54 | edit_row(ui, &i18n::fl!("artist"), &mut self.artist); 55 | edit_row(ui, &i18n::fl!("effector"), &mut self.chart_author); 56 | edit_row(ui, &i18n::fl!("jacket"), &mut self.jacket_filename); 57 | edit_row(ui, &i18n::fl!("jacket_artist"), &mut self.jacket_author); 58 | 59 | ui.label(i18n::fl!("difficulty")); 60 | ui.end_row(); 61 | 62 | ui.label(i18n::fl!("level")); 63 | ui.add(DragValue::new(&mut self.level).clamp_range(1..=20)); 64 | ui.end_row(); 65 | 66 | ui.label(i18n::fl!("index")); 67 | ui.add(DragValue::new(&mut self.difficulty)); 68 | }) 69 | .response 70 | } 71 | } 72 | 73 | impl Widget for &mut NewChartOptions { 74 | fn ui(self, ui: &mut Ui) -> Response { 75 | ui.horizontal(|ui| { 76 | ui.label(i18n::fl!("filename")); 77 | ui.text_edit_singleline(&mut self.filename); 78 | }); 79 | 80 | ui.separator(); 81 | ui.label(i18n::fl!("audio_file")); 82 | ui.label(&self.audio); 83 | if ui.button("...").clicked() { 84 | let picked_file = 85 | nfd::open_file_dialog(Some("mp3,flac,wav,ogg"), None).map(|res| match res { 86 | nfd::Response::Okay(s) => Some(s), 87 | _ => None, 88 | }); 89 | 90 | if let Ok(Some(picked_file)) = picked_file { 91 | self.audio = picked_file; 92 | } 93 | } 94 | 95 | ui.separator(); 96 | ui.label(i18n::fl!("destination_folder")); 97 | if ui.button("...").clicked() { 98 | let picked_folder = nfd::open_pick_folder(None).map(|res| match res { 99 | nfd::Response::Okay(s) => Some(PathBuf::from_str(&s)), 100 | _ => None, 101 | }); 102 | 103 | if let Ok(Some(Ok(picked_folder))) = picked_folder { 104 | self.destination = Some(picked_folder); 105 | } 106 | } 107 | ui.separator(); 108 | 109 | ui.add_enabled( 110 | !self.audio.is_empty() && !self.filename.is_empty(), 111 | Button::new(i18n::fl!("ok")), 112 | ) 113 | } 114 | } 115 | 116 | impl Widget for &mut kson::BgmInfo { 117 | fn ui(self, ui: &mut Ui) -> Response { 118 | if self.filename.is_none() { 119 | self.filename = Some(Default::default()); 120 | } 121 | 122 | Grid::new("bgm_info") 123 | .show(ui, |ui| { 124 | ui.label(i18n::fl!("audio_file")); 125 | ui.text_edit_singleline(self.filename.as_mut().unwrap()); 126 | ui.end_row(); 127 | 128 | ui.label(i18n::fl!("offset")); 129 | ui.add(DragValue::new(&mut self.offset).suffix("ms")); 130 | ui.end_row(); 131 | 132 | ui.label(i18n::fl!("volume")); 133 | ui.add(Slider::new(&mut self.vol, 0.0..=1.0).clamp_to_range(true)); 134 | ui.end_row(); 135 | 136 | ui.separator(); 137 | ui.end_row(); 138 | 139 | ui.label(i18n::fl!("preview_offset")); 140 | ui.add(DragValue::new(&mut self.preview.offset).suffix("ms")); 141 | ui.end_row(); 142 | 143 | ui.label(i18n::fl!("preview_duration")); 144 | ui.add(DragValue::new(&mut self.preview.duration).suffix("ms")); 145 | ui.end_row(); 146 | }) 147 | .response 148 | } 149 | } 150 | 151 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] 152 | pub enum GuiEvent { 153 | #[serde(skip_serializing)] 154 | NewChart(NewChartOptions), //(Audio, Filename, Destination) 155 | New, 156 | Open, 157 | Save, 158 | SaveAs, 159 | Metadata, 160 | MusicInfo, 161 | ToolChanged(ChartTool), 162 | Play, 163 | Undo, 164 | Redo, 165 | Home, 166 | End, 167 | Next, 168 | Previous, 169 | ExportKsh, 170 | Preferences, 171 | } 172 | 173 | impl std::fmt::Display for GuiEvent { 174 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 175 | if let GuiEvent::ToolChanged(tool) = self { 176 | write!(f, "{:?}", tool) 177 | } else { 178 | write!(f, "{:?}", self) 179 | } 180 | } 181 | } 182 | 183 | #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Eq, PartialOrd, Ord)] 184 | pub enum ChartTool { 185 | None, 186 | BT, 187 | FX, 188 | RLaser, 189 | LLaser, 190 | BPM, 191 | TimeSig, 192 | Camera, 193 | } 194 | 195 | #[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)] 196 | pub struct KeyCombo { 197 | key: egui::Key, 198 | modifiers: Modifiers, 199 | } 200 | 201 | #[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Copy, Clone)] 202 | pub struct Modifiers { 203 | pub alt: bool, 204 | pub ctrl: bool, 205 | pub shift: bool, 206 | pub mac_cmd: bool, 207 | pub command: bool, 208 | } 209 | 210 | struct AppState { 211 | editor: chart_editor::MainState, 212 | key_bindings: HashMap, 213 | show_preferences: bool, 214 | new_chart: Option, 215 | meta_edit: Option, 216 | bgm_edit: Option, 217 | exiting: bool, 218 | language: LanguageIdentifier, 219 | } 220 | 221 | #[derive(Debug, Serialize, Deserialize)] 222 | struct Config { 223 | key_bindings: HashMap, 224 | track_width: f32, 225 | beats_per_column: u32, 226 | language: LanguageIdentifier, 227 | } 228 | 229 | //TODO: ehhhhhhhhh 230 | impl From for Modifiers { 231 | fn from( 232 | egui::Modifiers { 233 | alt, 234 | ctrl, 235 | shift, 236 | mac_cmd, 237 | command, 238 | }: egui::Modifiers, 239 | ) -> Self { 240 | Self { 241 | alt, 242 | ctrl, 243 | shift, 244 | mac_cmd, 245 | command, 246 | } 247 | } 248 | } 249 | 250 | impl KeyCombo { 251 | fn new(key: egui::Key, modifiers: Modifiers) -> Self { 252 | Self { key, modifiers } 253 | } 254 | } 255 | 256 | impl std::fmt::Display for Modifiers { 257 | #[cfg(not(target_os = "macos"))] 258 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 259 | let mut keys = Vec::new(); 260 | if self.ctrl { 261 | keys.push("ctrl"); 262 | } 263 | if self.alt { 264 | keys.push("alt"); 265 | } 266 | if self.shift { 267 | keys.push("shift"); 268 | } 269 | 270 | write!(f, "{}", keys.join(" + ")) 271 | } 272 | #[cfg(target_os = "macos")] 273 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 274 | let mut keys = Vec::new(); 275 | if self.ctrl { 276 | keys.push("ctrl"); 277 | } 278 | if self.alt { 279 | keys.push("opt") 280 | } 281 | if self.shift { 282 | keys.push("shift") 283 | } 284 | if self.command { 285 | keys.push("cmd") 286 | } 287 | 288 | write!(f, "{}", keys.join(" + ")) 289 | } 290 | } 291 | 292 | impl std::fmt::Display for KeyCombo { 293 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 294 | if self.modifiers.any() { 295 | write!(f, "{} + {:?}", self.modifiers, self.key) 296 | } else { 297 | write!(f, "{:?}", self.key) 298 | } 299 | } 300 | } 301 | 302 | impl Modifiers { 303 | fn new() -> Self { 304 | Self { 305 | alt: false, 306 | command: false, 307 | ctrl: false, 308 | mac_cmd: false, 309 | shift: false, 310 | } 311 | } 312 | 313 | fn alt(mut self) -> Self { 314 | self.alt = true; 315 | self 316 | } 317 | fn command(mut self) -> Self { 318 | self.command = true; 319 | self 320 | } 321 | #[cfg(target_os = "macos")] 322 | fn ctrl(mut self) -> Self { 323 | self.ctrl = true; 324 | self 325 | } 326 | #[cfg(not(target_os = "macos"))] 327 | fn ctrl(mut self) -> Self { 328 | self.ctrl = true; 329 | self.command = true; 330 | self 331 | } 332 | fn mac_cmd(mut self) -> Self { 333 | self.mac_cmd = true; 334 | self 335 | } 336 | fn shift(mut self) -> Self { 337 | self.shift = true; 338 | self 339 | } 340 | 341 | fn any(self) -> bool { 342 | self.alt || self.command || self.ctrl || self.mac_cmd || self.shift 343 | } 344 | } 345 | 346 | impl Default for Config { 347 | fn default() -> Self { 348 | let mut default_bindings = HashMap::new(); 349 | let nomod = Modifiers::new(); 350 | 351 | default_bindings.insert( 352 | KeyCombo::new(Key::S, Modifiers::new().ctrl()), 353 | GuiEvent::Save, 354 | ); 355 | default_bindings.insert( 356 | KeyCombo::new(Key::N, Modifiers::new().ctrl()), 357 | GuiEvent::New, 358 | ); 359 | default_bindings.insert( 360 | KeyCombo::new(Key::P, Modifiers::new().ctrl()), 361 | GuiEvent::Preferences, 362 | ); 363 | default_bindings.insert( 364 | KeyCombo::new(Key::T, Modifiers::new().ctrl()), 365 | GuiEvent::Metadata, 366 | ); 367 | default_bindings.insert( 368 | KeyCombo::new(Key::M, Modifiers::new().ctrl()), 369 | GuiEvent::MusicInfo, 370 | ); 371 | default_bindings.insert( 372 | KeyCombo::new(Key::S, Modifiers::new().ctrl().shift()), 373 | GuiEvent::SaveAs, 374 | ); 375 | default_bindings.insert( 376 | KeyCombo::new(Key::O, Modifiers::new().ctrl()), 377 | GuiEvent::Open, 378 | ); 379 | default_bindings.insert( 380 | KeyCombo::new(Key::Z, Modifiers::new().ctrl()), 381 | GuiEvent::Undo, 382 | ); 383 | default_bindings.insert( 384 | KeyCombo::new(Key::Y, Modifiers::new().ctrl()), 385 | GuiEvent::Redo, 386 | ); 387 | 388 | //Tools 389 | { 390 | default_bindings.insert( 391 | KeyCombo::new(Key::Num0, nomod), 392 | GuiEvent::ToolChanged(ChartTool::None), 393 | ); 394 | default_bindings.insert( 395 | KeyCombo::new(Key::Num1, nomod), 396 | GuiEvent::ToolChanged(ChartTool::BT), 397 | ); 398 | default_bindings.insert( 399 | KeyCombo::new(Key::Num2, nomod), 400 | GuiEvent::ToolChanged(ChartTool::FX), 401 | ); 402 | default_bindings.insert( 403 | KeyCombo::new(Key::Num3, nomod), 404 | GuiEvent::ToolChanged(ChartTool::LLaser), 405 | ); 406 | default_bindings.insert( 407 | KeyCombo::new(Key::Num4, nomod), 408 | GuiEvent::ToolChanged(ChartTool::RLaser), 409 | ); 410 | default_bindings.insert( 411 | KeyCombo::new(Key::Num5, nomod), 412 | GuiEvent::ToolChanged(ChartTool::BPM), 413 | ); 414 | default_bindings.insert( 415 | KeyCombo::new(Key::Num6, nomod), 416 | GuiEvent::ToolChanged(ChartTool::TimeSig), 417 | ); 418 | default_bindings.insert( 419 | KeyCombo::new(Key::Num7, nomod), 420 | GuiEvent::ToolChanged(ChartTool::Camera), 421 | ); 422 | } 423 | 424 | default_bindings.insert(KeyCombo::new(Key::Space, nomod), GuiEvent::Play); 425 | default_bindings.insert(KeyCombo::new(Key::Home, nomod), GuiEvent::Home); 426 | default_bindings.insert(KeyCombo::new(Key::End, nomod), GuiEvent::End); 427 | default_bindings.insert(KeyCombo::new(Key::PageDown, nomod), GuiEvent::Next); 428 | default_bindings.insert(KeyCombo::new(Key::PageUp, nomod), GuiEvent::Previous); 429 | 430 | Self { 431 | key_bindings: default_bindings, 432 | track_width: 72.0, 433 | beats_per_column: 16, 434 | language: "en".parse().unwrap(), 435 | } 436 | } 437 | } 438 | 439 | pub fn rect_xy_wh(rect: [f32; 4]) -> Rect { 440 | let (mut x, mut y, mut w, mut h) = (rect[0], rect[1], rect[2], rect[3]); 441 | if w < 0.0 { 442 | x += w; 443 | w = w.abs(); 444 | } 445 | 446 | if h < 0.0 { 447 | y += h; 448 | h = h.abs(); 449 | } 450 | 451 | Rect::from_x_y_ranges(x..=x + w, y..=y + h) 452 | } 453 | 454 | const TOOLS: [(&str, ChartTool); 6] = [ 455 | ("BT", ChartTool::BT), 456 | ("FX", ChartTool::FX), 457 | ("LL", ChartTool::LLaser), 458 | ("RL", ChartTool::RLaser), 459 | ("BPM", ChartTool::BPM), 460 | ("TS", ChartTool::TimeSig), 461 | ]; 462 | 463 | impl AppState { 464 | fn preferences(&mut self, ui: &mut Ui) { 465 | warn_if_debug_build(ui); 466 | 467 | ui.add( 468 | Slider::new(&mut self.editor.screen.track_width, 50.0..=300.0) 469 | .clamp_to_range(true) 470 | .text(i18n::fl!("track_width")), 471 | ); 472 | 473 | ui.add( 474 | Slider::new(&mut self.editor.screen.beats_per_col, 4..=32) 475 | .clamp_to_range(true) 476 | .text(i18n::fl!("beats_per_col")), 477 | ); 478 | 479 | let selected = ComboBox::new("lang_select", "Language") 480 | .selected_text(&self.language.language.to_string()) 481 | .show_ui(ui, |ui| { 482 | [ 483 | ui.selectable_value( 484 | &mut self.language, 485 | "en".parse::().unwrap(), 486 | "en", 487 | ), 488 | ui.selectable_value( 489 | &mut self.language, 490 | "sv".parse::().unwrap(), 491 | "sv", 492 | ), 493 | ] 494 | }); 495 | 496 | if let Some(inner) = selected.inner { 497 | if inner.iter().any(|r| r.clicked()) { 498 | i18n::localizer().select(&[self.language.clone()]).unwrap(); 499 | } 500 | } 501 | 502 | let mut binding_vec: Vec<(&KeyCombo, &GuiEvent)> = self.key_bindings.iter().collect(); 503 | binding_vec.sort_by_key(|f| f.1); 504 | ui.separator(); 505 | ui.label(i18n::fl!("hotkeys")); 506 | Grid::new("hotkey_grid").striped(true).show(ui, |ui| { 507 | for (key, event) in binding_vec { 508 | ui.label(format!("{}", event)); 509 | ui.add(Label::new(format!("{}", key)).wrap(false)); 510 | ui.end_row(); 511 | } 512 | }); 513 | 514 | if ui.button(i18n::fl!("reset_to_default")).clicked() { 515 | self.key_bindings = Config::default().key_bindings; 516 | } 517 | } 518 | } 519 | 520 | const CONFIG_KEY: &str = "CONFIG_2"; 521 | 522 | fn menu_ui(ui: &mut Ui, title: impl ToString, min_width: f32, add_contents: impl FnOnce(&mut Ui)) { 523 | menu::menu_button(ui, title.to_string(), |ui| { 524 | ui.with_layout(Layout::top_down_justified(egui::Align::Min), |ui| { 525 | ui.allocate_exact_size(Vec2::new(min_width, 0.0), Sense::hover()); 526 | add_contents(ui); 527 | }); 528 | }); 529 | } 530 | 531 | impl App for AppState { 532 | fn on_exit_event(&mut self) -> bool { 533 | let at_save = self.editor.actions.saved(); 534 | if !at_save { 535 | self.exiting = true; 536 | } 537 | 538 | at_save 539 | } 540 | 541 | fn warm_up_enabled(&self) -> bool { 542 | false 543 | } 544 | 545 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 546 | let new_config = Config { 547 | key_bindings: self.key_bindings.clone(), 548 | beats_per_column: self.editor.screen.beats_per_col, 549 | track_width: self.editor.screen.track_width, 550 | language: self.language.clone(), 551 | }; 552 | 553 | eframe::set_value(storage, CONFIG_KEY, &new_config) 554 | } 555 | 556 | fn on_exit(&mut self, _ctx: &eframe::glow::Context) {} 557 | 558 | fn auto_save_interval(&self) -> std::time::Duration { 559 | std::time::Duration::from_secs(300) 560 | } 561 | 562 | fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { 563 | //input checking 564 | //TODO: Block events when exiting? 565 | let events = { ctx.input().events.clone() }; 566 | for e in events { 567 | match e { 568 | egui::Event::Copy => {} 569 | egui::Event::Cut => {} 570 | egui::Event::Key { 571 | key, 572 | pressed, 573 | modifiers, 574 | } => { 575 | if pressed && !ctx.wants_keyboard_input() { 576 | let key_combo = KeyCombo { 577 | key, 578 | modifiers: modifiers.into(), 579 | }; 580 | 581 | match self.key_bindings.get(&key_combo) { 582 | Some(GuiEvent::New) => { 583 | if self.new_chart.is_none() { 584 | self.new_chart = Some(Default::default()) 585 | } 586 | } 587 | Some(GuiEvent::Preferences) => self.show_preferences = true, 588 | Some(GuiEvent::Metadata) => { 589 | self.meta_edit = Some(self.editor.chart.meta.clone()) 590 | } 591 | Some(GuiEvent::MusicInfo) => { 592 | self.bgm_edit = 593 | Some(self.editor.chart.audio.bgm.clone().unwrap_or_default()) 594 | } 595 | 596 | Some(action) => self.editor.gui_event_queue.push_back(action.clone()), 597 | None => (), 598 | } 599 | } 600 | } 601 | egui::Event::PointerMoved(pos) => self.editor.mouse_motion_event(pos), 602 | 603 | _ => {} 604 | } 605 | } 606 | 607 | if let Err(e) = self.editor.update(ctx) { 608 | panic!("{}", e); 609 | } 610 | 611 | //draw 612 | //menu 613 | { 614 | egui::TopBottomPanel::top("menubar").show(ctx, |ui| { 615 | menu::bar(ui, |ui| { 616 | menu_ui(ui, i18n::fl!("file"), 100.0, |ui| { 617 | if ui.button(i18n::fl!("new")).clicked() { 618 | self.new_chart = Some(Default::default()); 619 | } 620 | if ui.button(i18n::fl!("open")).clicked() { 621 | self.editor.gui_event_queue.push_back(GuiEvent::Open); 622 | } 623 | if ui.button(i18n::fl!("save")).clicked() { 624 | self.editor.gui_event_queue.push_back(GuiEvent::Save) 625 | } 626 | if ui.button(i18n::fl!("save_as")).clicked() { 627 | self.editor.gui_event_queue.push_back(GuiEvent::SaveAs) 628 | } 629 | if ui.button(i18n::fl!("export_ksh")).clicked() { 630 | self.editor.gui_event_queue.push_back(GuiEvent::ExportKsh) 631 | } 632 | ui.separator(); 633 | if ui.button(i18n::fl!("preferences")).clicked() { 634 | self.show_preferences = true; 635 | } 636 | ui.separator(); 637 | if ui.button(i18n::fl!("exit")).clicked() { 638 | frame.quit(); 639 | } 640 | }); 641 | menu_ui(ui, i18n::fl!("edit"), 70.0, |ui| { 642 | let undo_desc = self.editor.actions.prev_action_desc(); 643 | let redo_desc = self.editor.actions.next_action_desc(); 644 | 645 | if ui 646 | .add_enabled( 647 | undo_desc.is_some(), 648 | Button::new(i18n::fl!( 649 | "undo", 650 | action = undo_desc.as_ref().unwrap_or(&String::new()).clone() 651 | )), 652 | ) 653 | .clicked() 654 | { 655 | self.editor.gui_event_queue.push_back(GuiEvent::Undo); 656 | } 657 | if ui 658 | .add_enabled( 659 | redo_desc.is_some(), 660 | Button::new(i18n::fl!( 661 | "redo", 662 | action = redo_desc.as_ref().unwrap_or(&String::new()).clone() 663 | )), 664 | ) 665 | .clicked() 666 | { 667 | self.editor.gui_event_queue.push_back(GuiEvent::Redo); 668 | } 669 | 670 | ui.separator(); 671 | if ui.button(i18n::fl!("metadata")).clicked() && self.meta_edit.is_none() { 672 | self.meta_edit = Some(self.editor.chart.meta.clone()); 673 | } 674 | if ui.button(i18n::fl!("music_info")).clicked() && self.meta_edit.is_none() 675 | { 676 | i18n::localizer().select(&vec!["sv".parse().unwrap()]); 677 | self.bgm_edit = 678 | Some(self.editor.chart.audio.bgm.clone().unwrap_or_default()); 679 | } 680 | }); 681 | 682 | if !self.editor.actions.saved() { 683 | ui.with_layout(Layout::right_to_left(), |ui| { 684 | ui.add(egui::Label::new(RichText::new("*").color(Color32::RED))) 685 | .on_hover_text(i18n::fl!("unsaved_changes")) 686 | }); 687 | } 688 | }); 689 | ui.separator(); 690 | menu::bar(ui, |ui| { 691 | for (name, tool) in &TOOLS { 692 | if ui 693 | .selectable_label(self.editor.current_tool == *tool, *name) 694 | .clicked() 695 | { 696 | if *tool == self.editor.current_tool { 697 | self.editor 698 | .gui_event_queue 699 | .push_back(GuiEvent::ToolChanged(ChartTool::None)) 700 | } else { 701 | self.editor 702 | .gui_event_queue 703 | .push_back(GuiEvent::ToolChanged(*tool)); 704 | } 705 | } 706 | } 707 | }) 708 | }); 709 | } 710 | 711 | //stuff 712 | { 713 | let mut open = self.show_preferences; 714 | egui::Window::new(i18n::fl!("preferences")) 715 | .open(&mut open) 716 | .show(ctx, |ui| { 717 | ui.with_layout(Layout::top_down_justified(egui::Align::Min), |ui| { 718 | self.preferences(ui); 719 | }); 720 | }); 721 | self.show_preferences = open; 722 | 723 | //New chart dialog 724 | if let Some(new_chart) = &mut self.new_chart { 725 | let mut open = true; 726 | let mut event = None; 727 | egui::Window::new(i18n::fl!("new")) 728 | .open(&mut open) 729 | .show(ctx, |ui| { 730 | if new_chart.ui(ui).clicked() { 731 | event = Some(GuiEvent::NewChart(new_chart.clone())); 732 | } 733 | }); 734 | 735 | if let Some(event) = event { 736 | self.editor.gui_event_queue.push_back(event); 737 | self.new_chart = None; 738 | } 739 | 740 | if !open { 741 | self.new_chart = None; 742 | } 743 | } 744 | 745 | //Metadata dialog 746 | if self.meta_edit.is_some() { 747 | let mut open = true; 748 | egui::Window::new(i18n::fl!("metadata")) 749 | .open(&mut open) 750 | .show(ctx, |ui| { 751 | self.meta_edit.as_mut().unwrap().ui(ui); 752 | ui.add_space(10.0); 753 | if ui.button(i18n::fl!("ok")).clicked() { 754 | let new_action = self.editor.actions.new_action(); 755 | let new_meta = self.meta_edit.take().unwrap(); 756 | new_action.action = Box::new(move |chart: &mut Chart| { 757 | chart.meta = new_meta.clone(); 758 | Ok(()) 759 | }); 760 | new_action.description = String::from(i18n::fl!("update_metadata")); 761 | } 762 | }); 763 | if !open { 764 | self.meta_edit = None; 765 | } 766 | } 767 | 768 | //Music data dialog 769 | if self.bgm_edit.is_some() { 770 | let mut open = true; 771 | egui::Window::new(i18n::fl!("music_info")) 772 | .open(&mut open) 773 | .show(ctx, |ui| { 774 | self.bgm_edit.as_mut().unwrap().ui(ui); 775 | ui.add_space(10.0); 776 | if ui.button(i18n::fl!("ok")).clicked() { 777 | let new_action = self.editor.actions.new_action(); 778 | let new_bgm = self.bgm_edit.take().unwrap(); 779 | new_action.description = i18n::fl!("update_music_info").into(); 780 | new_action.action = Box::new(move |chart: &mut Chart| { 781 | chart.audio.bgm = Some(new_bgm.clone()); 782 | Ok(()) 783 | }); 784 | } 785 | }); 786 | if !open { 787 | self.bgm_edit = None; 788 | } 789 | } 790 | } 791 | 792 | //main 793 | { 794 | let main_frame = Frame { 795 | outer_margin: 0.0.into(), 796 | inner_margin: 0.0.into(), 797 | fill: Color32::BLACK, 798 | ..Default::default() 799 | }; 800 | { 801 | // Move the tool out of the editor state so it can't modify itself in unexpected ways. Pleases borrow checker. 802 | let mut borrowed_tool = self.editor.cursor_object.take(); 803 | if let Some(tool) = borrowed_tool.as_mut() { 804 | profile_scope!("Tool UI"); 805 | tool.draw_ui(&mut self.editor, ctx); 806 | } 807 | self.editor.cursor_object = borrowed_tool; 808 | } 809 | 810 | let main_response = egui::CentralPanel::default() 811 | .frame(main_frame) 812 | .show(ctx, |ui| self.editor.draw(ui)) 813 | .inner; 814 | 815 | match main_response { 816 | Ok(response) => { 817 | let pos = ctx.input().pointer.hover_pos().unwrap_or(Pos2::ZERO); 818 | if response.hovered() && ctx.input().scroll_delta != Vec2::ZERO { 819 | self.editor.mouse_wheel_event(ctx.input().scroll_delta.y); 820 | } 821 | 822 | if response.clicked() { 823 | self.editor.primary_clicked(pos) 824 | } 825 | 826 | if response.middle_clicked() { 827 | self.editor.middle_clicked(pos) 828 | } 829 | 830 | if response.drag_started() 831 | && ctx 832 | .input() 833 | .pointer 834 | .button_down(egui::PointerButton::Primary) 835 | { 836 | self.editor.drag_start( 837 | egui::PointerButton::Primary, 838 | pos.x, 839 | pos.y, 840 | &Modifiers::from(ctx.input().modifiers), 841 | ) 842 | } 843 | 844 | if response.drag_released() { 845 | self.editor 846 | .drag_end(egui::PointerButton::Primary, pos.x, pos.y) 847 | } 848 | } 849 | Err(e) => panic!("{}", e), 850 | } 851 | } 852 | //exiting 853 | { 854 | if self.exiting { 855 | egui::Window::new(i18n::fl!("unsaved_changes_alert")) 856 | .collapsible(false) 857 | .resizable(false) 858 | .show(ctx, |ui| { 859 | ui.horizontal(|ui| { 860 | if ui.button(i18n::fl!("yes")).clicked() { 861 | self.exiting = false; 862 | if matches!(self.editor.save(), Ok(true)) { 863 | frame.quit(); 864 | } 865 | } 866 | if ui.button(i18n::fl!("no")).clicked() { 867 | self.exiting = false; 868 | self.editor.actions.save(); //marks as saved but doesn't actually save 869 | frame.quit(); 870 | } 871 | if ui.button(i18n::fl!("cancel")).clicked() { 872 | self.exiting = false; 873 | } 874 | }); 875 | }); 876 | } 877 | } 878 | } 879 | } 880 | 881 | fn main() -> Result<()> { 882 | env_logger::init(); 883 | #[cfg(feature = "profiling")] 884 | { 885 | start_puffin_server(); 886 | } 887 | 888 | let options = eframe::NativeOptions { 889 | drag_and_drop_support: false, 890 | multisampling: 4, 891 | vsync: true, 892 | ..Default::default() 893 | }; 894 | 895 | eframe::run_native( 896 | "KSON Editor", 897 | options, 898 | Box::new(|cc| { 899 | let config = if let Some(storage) = cc.storage { 900 | let c: Option = eframe::get_value(storage, CONFIG_KEY); 901 | c.unwrap_or_default() 902 | } else { 903 | Config::default() 904 | }; 905 | 906 | let mut app = AppState { 907 | editor: MainState::new().unwrap_or_else(|_| todo!()), 908 | key_bindings: HashMap::new(), 909 | show_preferences: false, 910 | new_chart: None, 911 | meta_edit: None, 912 | bgm_edit: None, 913 | exiting: false, 914 | language: config.language, 915 | }; 916 | 917 | app.key_bindings = config.key_bindings; 918 | app.editor.screen.track_width = config.track_width; 919 | app.editor.screen.beats_per_col = config.beats_per_column; 920 | cc.egui_ctx.set_visuals(Visuals::dark()); 921 | 922 | Box::new(app) 923 | }), 924 | ); 925 | } 926 | 927 | //https://github.com/emilk/egui/blob/master/examples/puffin_profiler/src/main.rs 928 | #[cfg(feature = "profiling")] 929 | fn start_puffin_server() { 930 | puffin::set_scopes_on(true); // tell puffin to collect data 931 | 932 | match puffin_http::Server::new("0.0.0.0:8585") { 933 | Ok(puffin_server) => { 934 | log::info!("Run: cargo install puffin_viewer && puffin_viewer --url 127.0.0.1:8585"); 935 | 936 | // We can store the server if we want, but in this case we just want 937 | // it to keep running. Dropping it closes the server, so let's not drop it! 938 | #[allow(clippy::mem_forget)] 939 | std::mem::forget(puffin_server); 940 | } 941 | Err(err) => { 942 | log::error!("Failed to start puffin server: {}", err); 943 | } 944 | }; 945 | } 946 | -------------------------------------------------------------------------------- /src/tools/bpm_ts.rs: -------------------------------------------------------------------------------- 1 | use crate::i18n; 2 | use crate::tools::CursorObject; 3 | use crate::{ 4 | action_stack::ActionStack, 5 | chart_editor::{MainState, ScreenState}, 6 | }; 7 | use anyhow::{bail, Result}; 8 | use eframe::egui::{self, Color32, Context, DragValue, Label, Painter, Pos2, Window}; 9 | use kson::Chart; 10 | enum CursorToolStates { 11 | None, 12 | Add(u32), 13 | Edit(usize), 14 | } 15 | 16 | pub struct BpmTool { 17 | bpm: f64, 18 | state: CursorToolStates, 19 | cursor_tick: u32, 20 | } 21 | 22 | impl BpmTool { 23 | pub fn new() -> Self { 24 | BpmTool { 25 | bpm: 120.0, 26 | state: CursorToolStates::None, 27 | cursor_tick: 0, 28 | } 29 | } 30 | } 31 | 32 | impl CursorObject for BpmTool { 33 | fn primary_click( 34 | &mut self, 35 | _screen: ScreenState, 36 | tick: u32, 37 | _tick_f: f64, 38 | _lane: f32, 39 | chart: &Chart, 40 | _actions: &mut ActionStack, 41 | _pos: Pos2, 42 | ) { 43 | if let CursorToolStates::None = self.state { 44 | //check for bpm changes on selected tick 45 | for (i, change) in chart.beat.bpm.iter().enumerate() { 46 | if change.0 == tick { 47 | self.state = CursorToolStates::Edit(i); 48 | self.bpm = change.1; 49 | return; 50 | } 51 | } 52 | 53 | self.state = CursorToolStates::Add(tick); 54 | } 55 | } 56 | 57 | fn update(&mut self, tick: u32, _tick_f: f64, _lane: f32, _pos: Pos2, _chart: &Chart) { 58 | if let CursorToolStates::None = self.state { 59 | self.cursor_tick = tick; 60 | } 61 | } 62 | 63 | fn draw(&self, state: &MainState, painter: &Painter) -> Result<()> { 64 | state.draw_cursor_line(painter, self.cursor_tick, Color32::from_rgb(0, 128, 255)); 65 | Ok(()) 66 | } 67 | 68 | fn draw_ui(&mut self, state: &mut MainState, ctx: &Context) { 69 | let complete_func: Option, f64)>> = match self.state { 70 | CursorToolStates::None => None, 71 | CursorToolStates::Add(tick) => { 72 | Some(Box::new(move |a: &mut ActionStack, bpm: f64| { 73 | let v = bpm; 74 | let y = tick; 75 | 76 | let new_action = a.new_action(); 77 | 78 | new_action.description = String::from(i18n::fl!("add_bpm_change")); 79 | new_action.action = Box::new(move |c| { 80 | c.beat.bpm.push((y, v)); 81 | c.beat.bpm.sort_by(|a, b| a.0.cmp(&b.0)); 82 | Ok(()) 83 | }); 84 | })) 85 | } 86 | CursorToolStates::Edit(index) => { 87 | Some(Box::new(move |a: &mut ActionStack, bpm: f64| { 88 | let v = bpm; 89 | 90 | let new_action = a.new_action(); 91 | new_action.description = String::from(i18n::fl!("edit_bpm_change")); 92 | new_action.action = Box::new(move |c| { 93 | if let Some(change) = c.beat.bpm.get_mut(index) { 94 | change.1 = v; 95 | Ok(()) 96 | } else { 97 | bail!("Tried to edit non existing BPM Change") 98 | } 99 | }); 100 | })) 101 | } 102 | }; 103 | 104 | if let Some(complete) = complete_func { 105 | let mut bpm = self.bpm as f32; 106 | Window::new(i18n::fl!("change_bpm")) 107 | .title_bar(true) 108 | .default_size([300.0, 600.0]) 109 | .default_pos([100.0, 100.0]) 110 | .show(ctx, |ui| { 111 | ui.horizontal_wrapped(|ui| { 112 | ui.add(Label::new("BPM:")); 113 | ui.add(DragValue::new(&mut bpm).speed(0.1)); 114 | self.bpm = bpm as f64; 115 | 116 | ui.end_row(); 117 | ui.end_row(); 118 | 119 | if ui.button(i18n::fl!("cancel")).clicked() { 120 | self.state = CursorToolStates::None; 121 | } 122 | if ui.button(i18n::fl!("ok")).clicked() { 123 | complete(&mut state.actions, bpm as f64); 124 | self.state = CursorToolStates::None; 125 | } 126 | }); 127 | }); 128 | } 129 | } 130 | 131 | fn middle_click( 132 | &mut self, 133 | _screen: ScreenState, 134 | tick: u32, 135 | _tick_f: f64, 136 | _lane: f32, 137 | chart: &Chart, 138 | actions: &mut ActionStack, 139 | _pos: Pos2, 140 | ) { 141 | if let Ok(index) = chart.beat.bpm.binary_search_by_key(&tick, |f| f.0) { 142 | let new_action = actions.new_action(); 143 | new_action.description = i18n::fl!("remove_bpm_change").into(); 144 | new_action.action = Box::new(move |chart: &mut Chart| { 145 | chart.beat.bpm.remove(index); 146 | Ok(()) 147 | }) 148 | } 149 | } 150 | } 151 | 152 | pub struct TimeSigTool { 153 | ts: kson::TimeSignature, 154 | state: CursorToolStates, 155 | cursor_tick: u32, 156 | } 157 | 158 | impl TimeSigTool { 159 | pub fn new() -> Self { 160 | TimeSigTool { 161 | ts: kson::TimeSignature(4, 4), 162 | state: CursorToolStates::None, 163 | cursor_tick: 0, 164 | } 165 | } 166 | } 167 | 168 | impl CursorObject for TimeSigTool { 169 | fn primary_click( 170 | &mut self, 171 | _screen: ScreenState, 172 | tick: u32, 173 | _tick_f: f64, 174 | _lane: f32, 175 | chart: &Chart, 176 | _actions: &mut ActionStack, 177 | _pos: Pos2, 178 | ) { 179 | let measure = chart.tick_to_measure(tick); 180 | if let CursorToolStates::None = self.state { 181 | //check for bpm changes on selected tick 182 | if let Ok(idx) = chart 183 | .beat 184 | .time_sig 185 | .binary_search_by(|tsc| tsc.0.cmp(&measure)) 186 | { 187 | self.state = CursorToolStates::Edit(idx); 188 | self.ts = chart.beat.time_sig.get(idx).unwrap().1; 189 | } else { 190 | self.state = CursorToolStates::Add(measure); 191 | self.ts = kson::TimeSignature(4, 4); 192 | } 193 | } 194 | } 195 | 196 | fn middle_click( 197 | &mut self, 198 | _screen: ScreenState, 199 | tick: u32, 200 | _tick_f: f64, 201 | _lane: f32, 202 | chart: &Chart, 203 | actions: &mut ActionStack, 204 | _pos: Pos2, 205 | ) { 206 | let measure = chart.tick_to_measure(tick); 207 | if let Ok(index) = chart.beat.time_sig.binary_search_by_key(&measure, |f| f.0) { 208 | let new_action = actions.new_action(); 209 | new_action.description = i18n::fl!("remove_time_signature_change").into(); 210 | new_action.action = Box::new(move |chart: &mut Chart| { 211 | chart.beat.time_sig.remove(index); 212 | Ok(()) 213 | }) 214 | } 215 | } 216 | 217 | fn update(&mut self, tick: u32, _tick_f: f64, _lane: f32, _pos: Pos2, _chart: &Chart) { 218 | if let CursorToolStates::None = self.state { 219 | self.cursor_tick = tick; 220 | } 221 | } 222 | 223 | fn draw(&self, state: &MainState, painter: &Painter) -> Result<()> { 224 | let tick = state 225 | .chart 226 | .measure_to_tick(state.chart.tick_to_measure(self.cursor_tick)); 227 | state.draw_cursor_line(painter, tick, Color32::from_rgb(255, 255, 0)); 228 | Ok(()) 229 | } 230 | 231 | fn draw_ui(&mut self, state: &mut MainState, ctx: &Context) { 232 | let complete_func: Option, [i32; 2])>> = match self.state 233 | { 234 | CursorToolStates::None => None, 235 | CursorToolStates::Add(measure) => Some(Box::new(move |a, ts| { 236 | let v = kson::TimeSignature(ts[0] as u32, ts[1] as u32); 237 | let idx = measure; 238 | 239 | let new_action = a.new_action(); 240 | new_action.description = String::from(i18n::fl!("add_time_signature_change")); 241 | new_action.action = Box::new(move |c| { 242 | c.beat.time_sig.push((idx, v)); 243 | c.beat.time_sig.sort_by(|a, b| a.0.cmp(&b.0)); 244 | Ok(()) 245 | }); 246 | })), 247 | CursorToolStates::Edit(index) => Some(Box::new(move |a, ts| { 248 | let new_action = a.new_action(); 249 | new_action.description = String::from(i18n::fl!("edit_time_signature_change")); 250 | new_action.action = Box::new(move |c| { 251 | if let Some(change) = c.beat.time_sig.get_mut(index) { 252 | change.1 .0 = ts[0] as u32; 253 | change.1 .1 = ts[1] as u32; 254 | Ok(()) 255 | } else { 256 | bail!("Tried to edit non existing Time Signature Change") 257 | } 258 | }); 259 | })), 260 | }; 261 | 262 | if let Some(complete) = complete_func { 263 | egui::Window::new(i18n::fl!("change_time_signature")) 264 | .title_bar(true) 265 | .default_size([300.0, 600.0]) 266 | .default_pos([100.0, 100.0]) 267 | .show(ctx, |ui| { 268 | ui.horizontal_wrapped(|ui| { 269 | let (mut ts_n, mut ts_d) = (self.ts.0, self.ts.1); 270 | 271 | ui.add(egui::widgets::DragValue::new(&mut ts_n).speed(0.2)); 272 | ui.add(egui::Label::new("/")); 273 | ui.add(egui::widgets::DragValue::new(&mut ts_d).speed(0.2)); 274 | ui.end_row(); 275 | ui.end_row(); 276 | 277 | self.ts.0 = ts_n; 278 | self.ts.1 = ts_d; 279 | 280 | if ui.button(i18n::fl!("ok")).clicked() { 281 | complete(&mut state.actions, [ts_n as i32, ts_d as i32]); 282 | self.state = CursorToolStates::None; 283 | } 284 | if ui.button(i18n::fl!("cancel")).clicked() { 285 | self.state = CursorToolStates::None; 286 | } 287 | }); 288 | }); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/tools/buttons.rs: -------------------------------------------------------------------------------- 1 | use crate::i18n; 2 | use crate::tools::CursorObject; 3 | use crate::utils::Overlaps; 4 | use crate::Modifiers; 5 | use crate::{ 6 | action_stack::ActionStack, 7 | chart_editor::{MainState, ScreenState}, 8 | rect_xy_wh, 9 | }; 10 | use anyhow::Result; 11 | use eframe::egui::{Painter, Pos2, Rgba, Shape}; 12 | use kson::{Chart, Interval}; 13 | 14 | //structs for cursor objects 15 | pub struct ButtonInterval { 16 | pressed: bool, 17 | fx: bool, 18 | interval: Interval, 19 | lane: usize, 20 | } 21 | 22 | impl ButtonInterval { 23 | pub fn new(fx: bool) -> Self { 24 | ButtonInterval { 25 | pressed: false, 26 | fx, 27 | interval: Interval { y: 0, l: 0 }, 28 | lane: 0, 29 | } 30 | } 31 | } 32 | 33 | impl CursorObject for ButtonInterval { 34 | fn drag_start( 35 | &mut self, 36 | _screen: ScreenState, 37 | tick: u32, 38 | _tick_f: f64, 39 | lane: f32, 40 | _chart: &Chart, 41 | _actions: &mut ActionStack, 42 | _pos: Pos2, 43 | _modifiers: &Modifiers, 44 | ) { 45 | self.pressed = true; 46 | if self.fx { 47 | self.lane = if lane < 3.0 { 0 } else { 1 }; 48 | } else { 49 | self.lane = (lane as usize).max(1).min(4) - 1; 50 | } 51 | self.interval.y = tick; 52 | } 53 | 54 | fn middle_click( 55 | &mut self, 56 | _screen: ScreenState, 57 | tick: u32, 58 | _tick_f: f64, 59 | lane: f32, 60 | chart: &Chart, 61 | actions: &mut ActionStack, 62 | _pos: Pos2, 63 | ) { 64 | if self.pressed { 65 | return; 66 | } 67 | 68 | let lane = if self.fx { 69 | if lane < 3.0 { 70 | 0 71 | } else { 72 | 1 73 | } 74 | } else { 75 | (lane as usize).max(1).min(4) - 1 76 | }; 77 | 78 | //hit test 79 | let lane_data = if self.fx { 80 | &chart.note.fx[lane] 81 | } else { 82 | &chart.note.bt[lane] 83 | }; 84 | 85 | let index = lane_data 86 | .iter() 87 | .enumerate() 88 | .find(|(_, n)| n.contains(tick)) 89 | .map(|(i, _)| i); 90 | 91 | if let Some(index) = index { 92 | // remove found index 93 | let new_action = actions.new_action(); 94 | let fx = self.fx; 95 | new_action.description = i18n::fl!("remove_note", lane = if fx { "FX" } else { "BT" }); 96 | new_action.action = Box::new(move |chart: &mut Chart| { 97 | if fx { 98 | chart.note.fx[lane].remove(index); 99 | } else { 100 | chart.note.bt[lane].remove(index); 101 | } 102 | 103 | Ok(()) 104 | }); 105 | } 106 | } 107 | 108 | fn drag_end( 109 | &mut self, 110 | _screen: ScreenState, 111 | tick: u32, 112 | _tick_f: f64, 113 | _lane: f32, 114 | _chart: &Chart, 115 | actions: &mut ActionStack, 116 | _pos: Pos2, 117 | ) { 118 | if !self.pressed { 119 | return; 120 | } 121 | 122 | if self.interval.y >= tick { 123 | self.interval.l = 0; 124 | } else { 125 | self.interval.l = tick - self.interval.y; 126 | } 127 | let v = std::mem::replace(&mut self.interval, Interval { y: 0, l: 0 }); 128 | if self.fx { 129 | let l = self.lane; 130 | 131 | let new_action = actions.new_action(); 132 | new_action.description = i18n::fl!( 133 | "add_fx", 134 | side = if self.lane == 0 { 135 | i18n::fl!("left") 136 | } else { 137 | i18n::fl!("right") 138 | } 139 | ); 140 | new_action.action = Box::new(move |edit_chart: &mut Chart| { 141 | edit_chart.note.fx[l].push(v); 142 | edit_chart.note.fx[l].sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap()); 143 | Ok(()) 144 | }); 145 | } else { 146 | let l = self.lane; 147 | 148 | let new_action = actions.new_action(); 149 | new_action.description = i18n::fl!( 150 | "add_bt", 151 | lane = std::char::from_u32('A' as u32 + self.lane as u32) 152 | .unwrap_or_default() 153 | .to_string() 154 | ); 155 | new_action.action = Box::new(move |edit_chart: &mut Chart| { 156 | edit_chart.note.bt[l].push(v); 157 | edit_chart.note.bt[l].sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap()); 158 | Ok(()) 159 | }); 160 | } 161 | self.pressed = false; 162 | self.lane = 0; 163 | } 164 | 165 | fn update(&mut self, tick: u32, _tick_f: f64, lane: f32, _pos: Pos2, _chart: &Chart) { 166 | if !self.pressed { 167 | self.interval.y = tick; 168 | if self.fx { 169 | self.lane = if lane < 3.0 { 0 } else { 1 }; 170 | } else { 171 | self.lane = (lane as usize).max(1).min(4) - 1; 172 | } 173 | } 174 | if self.interval.y >= tick { 175 | self.interval.l = 0; 176 | } else { 177 | self.interval.l = tick - self.interval.y; 178 | } 179 | } 180 | 181 | fn draw(&self, state: &MainState, painter: &Painter) -> Result<()> { 182 | let color = if self.fx { 183 | Rgba::from_rgba_premultiplied(1.0, 0.3, 0.0, 0.5) 184 | } else { 185 | Rgba::from_rgba_premultiplied(1.0, 1.0, 1.0, 0.5) 186 | }; 187 | if self.interval.l == 0 { 188 | let (x, y) = state.screen.tick_to_pos(self.interval.y); 189 | 190 | let x = if self.fx { 191 | x + self.lane as f32 * state.screen.lane_width() * 2.0 192 | + 2.0 * self.lane as f32 193 | + state.screen.lane_width() 194 | + state.screen.track_width / 2.0 195 | } else { 196 | x + self.lane as f32 * state.screen.lane_width() 197 | + 1.0 * self.lane as f32 198 | + state.screen.lane_width() 199 | + state.screen.track_width / 2.0 200 | }; 201 | let y = y as f32; 202 | 203 | let w = if self.fx { 204 | state.screen.track_width as f32 / 3.0 - 1.0 205 | } else { 206 | state.screen.track_width as f32 / 6.0 - 2.0 207 | }; 208 | let h = -2.0; 209 | 210 | painter.rect_filled(rect_xy_wh([x, y, w, h]), 0.0, color); 211 | Ok(()) 212 | } else { 213 | let mut long_bt_builder = Vec::::new(); 214 | for (x, y, h, _) in state.screen.interval_to_ranges(&self.interval) { 215 | let x = if self.fx { 216 | x + self.lane as f32 * state.screen.lane_width() * 2.0 217 | + 2.0 * self.lane as f32 218 | + state.screen.lane_width() 219 | + state.screen.track_width / 2.0 220 | } else { 221 | x + self.lane as f32 * state.screen.lane_width() 222 | + 1.0 * self.lane as f32 223 | + state.screen.lane_width() 224 | + state.screen.track_width / 2.0 225 | }; 226 | 227 | let w = if self.fx { 228 | state.screen.track_width as f32 / 3.0 - 1.0 229 | } else { 230 | state.screen.track_width as f32 / 6.0 - 2.0 231 | }; 232 | 233 | long_bt_builder.push(Shape::rect_filled(rect_xy_wh([x, y, w, h]), 0.0, color)); 234 | } 235 | 236 | painter.extend(long_bt_builder); 237 | Ok(()) 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/tools/camera.rs: -------------------------------------------------------------------------------- 1 | use eframe::{ 2 | egui::{vec2, Color32, ComboBox, Pos2, Slider, Stroke}, 3 | epaint::Rgba, 4 | }; 5 | 6 | use crate::i18n; 7 | use glam::vec3; 8 | use kson::{Chart, Graph, GraphPoint, GraphSectionPoint}; 9 | use std::{default::Default, f32::EPSILON, ops::Sub}; 10 | 11 | use crate::camera_widget::CameraView; 12 | use crate::chart_camera::ChartCamera; 13 | 14 | use super::CursorObject; 15 | 16 | #[derive(Debug, PartialEq, Clone, Copy)] 17 | enum CameraPaths { 18 | Zoom, 19 | RotationX, 20 | } 21 | 22 | impl Default for CameraPaths { 23 | fn default() -> Self { 24 | Self::Zoom 25 | } 26 | } 27 | 28 | impl ToString for CameraPaths { 29 | fn to_string(&self) -> String { 30 | match self { 31 | CameraPaths::Zoom => i18n::fl!("radius").to_string(), 32 | CameraPaths::RotationX => i18n::fl!("angle").to_string(), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Default)] 38 | pub struct CameraTool { 39 | radius: f32, 40 | angle: f32, 41 | angle_dirty: bool, 42 | radius_dirty: bool, 43 | display_line: CameraPaths, 44 | curving_index: Option<(usize, f64, f64)>, 45 | } 46 | 47 | impl CameraTool { 48 | fn current_graph<'a>(&mut self, chart: &'a kson::Chart) -> &'a Vec { 49 | match self.display_line { 50 | CameraPaths::Zoom => &chart.camera.cam.body.zoom, 51 | CameraPaths::RotationX => &chart.camera.cam.body.rotation_x, 52 | } 53 | } 54 | } 55 | 56 | impl CursorObject for CameraTool { 57 | fn update(&mut self, _tick: u32, tick_f: f64, lane: f32, _pos: Pos2, chart: &Chart) { 58 | if let Some((c_idx, _, _)) = self.curving_index { 59 | let transform_value = |v: f64| (v + 3.0) / 6.0; 60 | 61 | if let Some(section) = self.current_graph(chart).windows(2).nth(c_idx) { 62 | let a = tick_f - section[0].y as f64; 63 | let a = a / (section[1].y - section[0].y) as f64; 64 | 65 | //TODO: map b value to match mouse position better 66 | let point = §ion[0]; 67 | let end_point = §ion[1]; 68 | let start_value = transform_value(point.vf.unwrap_or(point.v)); 69 | let in_value = lane as f64 / 6.0; 70 | let value = (in_value - start_value) / (transform_value(end_point.v) - start_value); 71 | 72 | self.curving_index = Some((c_idx, a.max(0.0).min(1.0), value.max(0.0).min(1.0))); 73 | } 74 | } 75 | } 76 | 77 | fn draw( 78 | &self, 79 | state: &crate::chart_editor::MainState, 80 | painter: &eframe::egui::Painter, 81 | ) -> anyhow::Result<()> { 82 | let (graph, stroke) = match self.display_line { 83 | CameraPaths::Zoom => ( 84 | &state.chart.camera.cam.body.zoom, 85 | Stroke::new(1.0, Rgba::from_rgb(1.0, 1.0, 0.0)), 86 | ), 87 | CameraPaths::RotationX => ( 88 | &state.chart.camera.cam.body.rotation_x, 89 | Stroke::new(1.0, Rgba::from_rgb(0.0, 1.0, 1.0)), 90 | ), 91 | }; 92 | 93 | state.draw_graph(graph, painter, (-3.0, 3.0), stroke); 94 | 95 | for (i, start_end) in graph.windows(2).enumerate() { 96 | let (color, points) = if matches!(self.curving_index, Some((ci, _, _)) if ci == i) { 97 | let new_start = if let Some((_, a, b)) = self.curving_index { 98 | GraphPoint { 99 | y: start_end[0].y, 100 | v: start_end[0].v, 101 | vf: start_end[0].vf, 102 | a: Some(a), 103 | b: Some(b), 104 | } 105 | } else { 106 | start_end[0] 107 | }; 108 | 109 | ( 110 | Rgba::from_rgba_premultiplied(0.0, 1.0, 0.0, 1.0), 111 | [new_start, start_end[1]], 112 | ) 113 | } else { 114 | ( 115 | Rgba::from_rgba_premultiplied(0.0, 0.0, 1.0, 1.0), 116 | [start_end[0], start_end[1]], 117 | ) 118 | }; 119 | 120 | if let Some(pos) = state 121 | .screen 122 | .get_control_point_pos(&points, (-3.0, 3.0), None) 123 | { 124 | painter.circle(pos, 5.0, color, Stroke::none()); 125 | } 126 | } 127 | 128 | if let Some((c_idx, a, b)) = self.curving_index { 129 | if let Some(points) = graph.windows(2).nth(c_idx) { 130 | state.draw_graph_segmented( 131 | &points 132 | .iter() 133 | .map(|p| GraphSectionPoint { 134 | ry: p.y, 135 | v: p.v, 136 | vf: p.vf, 137 | a: Some(a), 138 | b: Some(b), 139 | }) 140 | .collect::>(), 141 | painter, 142 | (-3.0, 3.0), 143 | Stroke { 144 | width: 1.0, 145 | color: Color32::GREEN, 146 | }, 147 | ); 148 | } 149 | } 150 | 151 | Ok(()) 152 | } 153 | 154 | fn drag_start( 155 | &mut self, 156 | screen: crate::chart_editor::ScreenState, 157 | _tick: u32, 158 | _tick_f: f64, 159 | _lane: f32, 160 | chart: &kson::Chart, 161 | _actions: &mut crate::action_stack::ActionStack, 162 | pos: Pos2, 163 | _modifiers: &crate::Modifiers, 164 | ) { 165 | let graph = self.current_graph(chart); 166 | 167 | for (i, points) in graph.windows(2).enumerate() { 168 | if let Some(control_point) = screen.get_control_point_pos(points, (-3.0, 3.0), None) { 169 | if control_point.distance(pos) < 5.0 { 170 | self.curving_index = 171 | Some((i, points[0].a.unwrap_or(0.5), points[0].b.unwrap_or(0.5))); 172 | } 173 | } 174 | } 175 | } 176 | 177 | fn drag_end( 178 | &mut self, 179 | _screen: crate::chart_editor::ScreenState, 180 | _tick: u32, 181 | _tick_f: f64, 182 | _lane: f32, 183 | _chart: &kson::Chart, 184 | actions: &mut crate::action_stack::ActionStack, 185 | _pos: Pos2, 186 | ) { 187 | if let Some((ci, a, b)) = self.curving_index { 188 | let new_action = actions.new_action(); 189 | let active_line = self.display_line; 190 | new_action.action = Box::new(move |chart| { 191 | let graph = match active_line { 192 | CameraPaths::Zoom => &mut chart.camera.cam.body.zoom, 193 | CameraPaths::RotationX => &mut chart.camera.cam.body.rotation_x, 194 | }; 195 | 196 | if let Some(point) = graph.get_mut(ci) { 197 | point.a = Some(a); 198 | point.b = Some(b); 199 | } 200 | 201 | Ok(()) 202 | }); 203 | 204 | new_action.description = i18n::fl!( 205 | "edit_curve_for_camera", 206 | graph = match self.display_line { 207 | CameraPaths::Zoom => i18n::fl!("radius"), 208 | CameraPaths::RotationX => i18n::fl!("angle"), 209 | } 210 | ) 211 | } 212 | 213 | self.curving_index = None 214 | } 215 | 216 | fn draw_ui(&mut self, state: &mut crate::chart_editor::MainState, ctx: &eframe::egui::Context) { 217 | //Draw winodw, with a viewport that uses the ChartCamera to project a track in using current camera parameters. 218 | let cursor_tick = state.get_current_cursor_tick() as f64; 219 | 220 | let old_rad = if self.radius_dirty { 221 | self.radius 222 | } else { 223 | state.chart.camera.cam.body.zoom.value_at(cursor_tick) as f32 224 | }; 225 | 226 | let old_angle = if self.angle_dirty { 227 | self.angle 228 | } else { 229 | state.chart.camera.cam.body.rotation_x.value_at(cursor_tick) as f32 230 | }; 231 | 232 | self.angle = old_angle; 233 | self.radius = old_rad; 234 | 235 | let camera = ChartCamera { 236 | center: vec3(0.0, 0.0, 0.0), 237 | angle: -45.0 - 14.0 * self.angle, 238 | fov: 70.0, 239 | radius: (-self.radius + 3.1) / 2.0, 240 | tilt: 0.0, 241 | track_length: 16.0, 242 | }; 243 | 244 | eframe::egui::Window::new(i18n::fl!("camera")) 245 | .title_bar(true) 246 | .open(&mut true) 247 | .resizable(true) 248 | .show(ctx, |ui| { 249 | let mut camera_view = CameraView::new(vec2(300.0, 200.0), camera); 250 | camera_view.add_track(&state.laser_colors); 251 | camera_view.add_chart_objects( 252 | &state.chart, 253 | cursor_tick as f32, 254 | &state.laser_colors, 255 | ); 256 | camera_view.add_track_overlay(); 257 | ui.add(camera_view); 258 | ui.add(Slider::new(&mut self.radius, -3.0..=3.0).text(i18n::fl!("radius"))); 259 | ui.add(Slider::new(&mut self.angle, -3.0..=3.0).text(i18n::fl!("angle"))); 260 | 261 | if old_angle.sub(self.angle).abs() > EPSILON { 262 | self.angle_dirty = true; 263 | } 264 | 265 | if old_rad.sub(self.radius).abs() > EPSILON { 266 | self.radius_dirty = true; 267 | } 268 | 269 | ComboBox::from_label(i18n::fl!("display_line")) 270 | .selected_text(self.display_line.to_string()) 271 | .show_ui(ui, |ui| { 272 | ui.selectable_value( 273 | &mut self.display_line, 274 | CameraPaths::Zoom, 275 | CameraPaths::Zoom.to_string(), 276 | ); 277 | ui.selectable_value( 278 | &mut self.display_line, 279 | CameraPaths::RotationX, 280 | CameraPaths::RotationX.to_string(), 281 | ); 282 | }); 283 | 284 | if ui.button(i18n::fl!("add_control_point")).clicked() { 285 | let new_action = state.actions.new_action(); 286 | new_action.description = i18n::fl!("added_camera_control_point").to_string(); 287 | let Self { 288 | angle, 289 | radius, 290 | radius_dirty, 291 | angle_dirty, 292 | display_line: _, 293 | curving_index: _, 294 | } = *self; 295 | let y = state.cursor_line; 296 | new_action.action = Box::new(move |c| { 297 | if angle_dirty { 298 | c.camera.cam.body.rotation_x.push(kson::GraphPoint { 299 | y, 300 | v: angle as f64, 301 | vf: None, 302 | a: Some(0.5), 303 | b: Some(0.5), 304 | }) 305 | } 306 | if radius_dirty { 307 | c.camera.cam.body.zoom.push(kson::GraphPoint { 308 | y, 309 | v: radius as f64, 310 | vf: None, 311 | a: Some(0.5), 312 | b: Some(0.5), 313 | }); 314 | } 315 | 316 | //TODO: just insert sorted instead 317 | c.camera.cam.body.zoom.sort_by_key(|p| p.y); 318 | c.camera.cam.body.rotation_x.sort_by_key(|p| p.y); 319 | Ok(()) 320 | }); 321 | 322 | self.radius_dirty = false; 323 | self.angle_dirty = false; 324 | } 325 | }); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/tools/laser.rs: -------------------------------------------------------------------------------- 1 | use crate::i18n; 2 | use crate::tools::CursorObject; 3 | use crate::Modifiers; 4 | use crate::{ 5 | action_stack::ActionStack, 6 | chart_editor::{MainState, ScreenState}, 7 | utils::Overlaps, 8 | }; 9 | use anyhow::Result; 10 | use eframe::egui::{Painter, Pos2, Rgba, Stroke}; 11 | use eframe::epaint::Shape; 12 | use kson::{Chart, GraphSectionPoint, LaserSection}; 13 | 14 | pub struct LaserTool { 15 | right: bool, 16 | section: LaserSection, 17 | mode: LaserEditMode, 18 | } 19 | 20 | #[derive(Copy, Clone)] 21 | struct LaserEditState { 22 | section_index: usize, 23 | curving_index: Option, 24 | } 25 | 26 | enum LaserEditMode { 27 | None, 28 | New, 29 | Edit(LaserEditState), 30 | } 31 | 32 | impl LaserTool { 33 | pub fn new(right: bool) -> Self { 34 | LaserTool { 35 | right, 36 | mode: LaserEditMode::None, 37 | section: LaserSection(0, Vec::new(), 0), 38 | } 39 | } 40 | 41 | fn gsp(ry: u32, v: f64) -> GraphSectionPoint { 42 | GraphSectionPoint { 43 | ry, 44 | v, 45 | vf: None, 46 | a: Some(0.5), 47 | b: Some(0.5), 48 | } 49 | } 50 | 51 | fn lane_to_pos(lane: f32, wide: u8) -> f64 { 52 | let resolution: f64 = 10.0 * wide as f64; 53 | math::round::floor(resolution * lane as f64 / 6.0, 0) / resolution 54 | } 55 | 56 | fn get_second_to_last(&self) -> Option<&GraphSectionPoint> { 57 | let len = self.section.1.len(); 58 | let idx = len.checked_sub(2); 59 | idx.and_then(|i| self.section.1.get(i)) 60 | } 61 | 62 | /* 63 | fn get_second_to_last_mut(&mut self) -> Option<&mut GraphSectionPoint> { 64 | let len = self.section.v.len(); 65 | let idx = len.checked_sub(2); 66 | let idx = idx.unwrap(); 67 | self.section.v.get_mut(idx) 68 | } 69 | */ 70 | 71 | fn calc_ry(&self, tick: u32) -> u32 { 72 | let ry = if tick <= self.section.tick() { 73 | 0 74 | } else { 75 | tick - self.section.tick() 76 | }; 77 | 78 | if let Some(secont_last) = self.get_second_to_last() { 79 | (*secont_last).ry.max(ry) 80 | } else { 81 | ry 82 | } 83 | } 84 | 85 | fn hit_test(&self, chart: &Chart, tick: u32) -> Option { 86 | let side_index: usize = if self.right { 1 } else { 0 }; 87 | 88 | chart.note.laser[side_index] 89 | .iter() 90 | .enumerate() 91 | .find(|(_, s)| s.contains(tick)) 92 | .map(|(i, _)| i) 93 | } 94 | } 95 | 96 | impl CursorObject for LaserTool { 97 | fn drag_start( 98 | &mut self, 99 | screen: ScreenState, 100 | tick: u32, 101 | _tick_f: f64, 102 | lane: f32, 103 | chart: &Chart, 104 | actions: &mut ActionStack, 105 | pos: Pos2, 106 | modifiers: &Modifiers, 107 | ) { 108 | let wide = modifiers.shift; 109 | let v = LaserTool::lane_to_pos(lane, if wide { 2 } else { 1 }); 110 | let ry = self.calc_ry(tick); 111 | let mut finalize = false; 112 | 113 | match self.mode { 114 | LaserEditMode::None => { 115 | //hit test existing lasers 116 | //if a laser exists enter edit mode for that laser 117 | //if no lasers exist create new laser 118 | let side_index: usize = if self.right { 1 } else { 0 }; 119 | if let Some(section_index) = self.hit_test(chart, tick) { 120 | self.section = chart.note.laser[side_index][section_index].clone(); 121 | self.mode = LaserEditMode::Edit(LaserEditState { 122 | section_index, 123 | curving_index: None, 124 | }); 125 | } else { 126 | self.section.0 = tick; 127 | self.section.1.push(LaserTool::gsp(0, v)); 128 | self.section.1.push(LaserTool::gsp(0, v)); 129 | self.section.2 = if wide { 2 } else { 1 }; 130 | self.mode = LaserEditMode::New; 131 | } 132 | } 133 | LaserEditMode::New => { 134 | if let Some(last) = self.get_second_to_last() { 135 | finalize = match (*last).vf { 136 | Some(_) => ry == last.ry, 137 | None => ry == last.ry && (v - last.v).abs() < f64::EPSILON, 138 | }; 139 | } 140 | if finalize { 141 | self.mode = LaserEditMode::None; 142 | self.section.1.pop(); 143 | let v = std::mem::replace(&mut self.section, LaserSection(0, Vec::new(), 1)); 144 | let v = std::rc::Rc::new(v); //Can't capture by clone so use RC 145 | let i = if self.right { 1 } else { 0 }; 146 | let new_action = actions.new_action(); 147 | new_action.description = i18n::fl!( 148 | "add_laser", 149 | side = if self.right { 150 | i18n::fl!("right") 151 | } else { 152 | i18n::fl!("left") 153 | } 154 | ); 155 | new_action.action = Box::new(move |edit_chart| { 156 | edit_chart.note.laser[i].push(v.as_ref().clone()); 157 | edit_chart.note.laser[i].sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); 158 | Ok(()) 159 | }); 160 | 161 | return; 162 | } 163 | 164 | self.section.1.push(LaserTool::gsp( 165 | ry, 166 | LaserTool::lane_to_pos(lane, self.section.wide()), 167 | )); 168 | } 169 | LaserEditMode::Edit(edit_state) => { 170 | if self.hit_test(chart, tick) == Some(edit_state.section_index) { 171 | for (i, points) in self.section.segments().enumerate() { 172 | if let Some(control_point) = screen.get_control_point_pos_section( 173 | points, 174 | self.section.tick(), 175 | (0.0, 1.0), 176 | Some((0.5 / 6.0, 5.5 / 6.0)), 177 | ) { 178 | if control_point.distance(pos) < 5.0 { 179 | self.mode = LaserEditMode::Edit(LaserEditState { 180 | section_index: edit_state.section_index, 181 | curving_index: Some(i), 182 | }) 183 | } 184 | } 185 | } 186 | //TODO: Subdivide and stuff 187 | } else { 188 | self.mode = LaserEditMode::None; 189 | self.section = LaserSection(tick, Vec::new(), 1) 190 | } 191 | } 192 | } 193 | } 194 | fn drag_end( 195 | &mut self, 196 | _screen: ScreenState, 197 | _tick: u32, 198 | _tick_f: f64, 199 | _lane: f32, 200 | _chart: &Chart, 201 | actions: &mut ActionStack, 202 | _pos: Pos2, 203 | ) { 204 | if let LaserEditMode::Edit(edit_state) = self.mode { 205 | if let Some(curving_index) = edit_state.curving_index { 206 | let right = self.right; 207 | let laser_text = if right { 208 | i18n::fl!("right") 209 | } else { 210 | i18n::fl!("left") 211 | }; 212 | let section_index = edit_state.section_index; 213 | let laser_i = if right { 1 } else { 0 }; 214 | let updated_point = self.section.1[curving_index]; 215 | 216 | let new_action = actions.new_action(); 217 | new_action.description = i18n::fl!("adjust_laser_curve", side = laser_text); 218 | new_action.action = Box::new(move |c| { 219 | c.note.laser[laser_i][section_index].1[curving_index] = updated_point; 220 | Ok(()) 221 | }); 222 | } 223 | self.mode = LaserEditMode::Edit(LaserEditState { 224 | section_index: edit_state.section_index, 225 | curving_index: None, 226 | }) 227 | } 228 | } 229 | 230 | fn middle_click( 231 | &mut self, 232 | _screen: ScreenState, 233 | tick: u32, 234 | _tick_f: f64, 235 | _lane: f32, 236 | chart: &Chart, 237 | actions: &mut ActionStack, 238 | _pos: Pos2, 239 | ) { 240 | if let Some(index) = self.hit_test(chart, tick) { 241 | let laser_i = if self.right { 1 } else { 0 }; 242 | let new_action = actions.new_action(); 243 | new_action.description = i18n::fl!( 244 | "remove_laser", 245 | side = if self.right { 246 | i18n::fl!("right") 247 | } else { 248 | i18n::fl!("left") 249 | } 250 | ); 251 | new_action.action = Box::new(move |chart: &mut Chart| { 252 | chart.note.laser[laser_i].remove(index); 253 | Ok(()) 254 | }); 255 | } 256 | } 257 | 258 | fn update(&mut self, tick: u32, tick_f: f64, lane: f32, _pos: Pos2, _chart: &Chart) { 259 | match self.mode { 260 | LaserEditMode::New => { 261 | let ry = self.calc_ry(tick); 262 | let v = LaserTool::lane_to_pos(lane, self.section.wide()); 263 | let second_last: Option = self.get_second_to_last().copied(); 264 | if let Some(last) = self.section.1.last_mut() { 265 | (*last).ry = ry; 266 | (*last).v = v; 267 | 268 | if let Some(second_last) = second_last { 269 | if second_last.ry == ry { 270 | (*last).v = second_last.v; 271 | (*last).vf = Some(v); 272 | } else { 273 | (*last).vf = None; 274 | } 275 | } 276 | } 277 | } 278 | LaserEditMode::None => {} 279 | LaserEditMode::Edit(edit_state) => { 280 | for gp in &mut self.section.1 { 281 | if gp.a.is_none() { 282 | gp.a = Some(0.5); 283 | } 284 | if gp.b.is_none() { 285 | gp.b = Some(0.5); 286 | } 287 | } 288 | if let Some(curving_index) = edit_state.curving_index { 289 | let end_point = self.section.1[curving_index + 1]; 290 | let point = &mut self.section.1[curving_index]; 291 | let start_tick = (self.section.0 + point.ry) as f64; 292 | let end_tick = (self.section.0 + end_point.ry) as f64; 293 | point.a = Some( 294 | ((tick_f - start_tick) / (end_tick - start_tick)) 295 | .max(0.0) 296 | .min(1.0), 297 | ); 298 | 299 | let start_value = point.vf.unwrap_or(point.v); 300 | let in_value = lane as f64 / 5.0 - 0.5 / 6.0; 301 | let value = (in_value - start_value) / (end_point.v - start_value); 302 | 303 | self.section.1[curving_index].b = Some(value.min(1.0).max(0.0)); 304 | } 305 | } 306 | } 307 | } 308 | fn draw(&self, state: &MainState, painter: &Painter) -> Result<()> { 309 | if self.section.1.len() > 1 { 310 | //Draw laser mesh 311 | if let Some(color) = match self.mode { 312 | LaserEditMode::None => None, 313 | LaserEditMode::New => { 314 | let b = 0.8; 315 | if self.right { 316 | Some(Rgba::from_rgba_premultiplied( 317 | 0.76 * b, 318 | 0.024 * b, 319 | 0.55 * b, 320 | 1.0, 321 | )) 322 | } else { 323 | Some(Rgba::from_rgba_premultiplied(0.0, 0.45 * b, 0.565 * b, 1.0)) 324 | } 325 | } 326 | LaserEditMode::Edit(_) => Some(Rgba::from_rgba_premultiplied(0.0, 0.76, 0.0, 0.25)), 327 | } { 328 | let mut mb = Vec::new(); 329 | state 330 | .screen 331 | .draw_laser_section(&self.section, &mut mb, color.into(), false)?; 332 | painter.extend(mb.into_iter().map(Shape::mesh).collect()); 333 | } 334 | 335 | //Draw curve control points 336 | if let LaserEditMode::Edit(edit_state) = self.mode { 337 | for (i, start_end) in self.section.1.windows(2).enumerate() { 338 | let color = if edit_state.curving_index == Some(i) { 339 | Rgba::from_rgba_premultiplied(0.0, 1.0, 0.0, 1.0) 340 | } else { 341 | Rgba::from_rgba_premultiplied(0.0, 0.0, 1.0, 1.0) 342 | }; 343 | 344 | if let Some(pos) = state.screen.get_control_point_pos_section( 345 | start_end, 346 | self.section.tick(), 347 | (0.0, 1.0), 348 | Some((0.5 / 6.0, 5.5 / 6.0)), 349 | ) { 350 | painter.circle(pos, 5.0, color, Stroke::none()); 351 | } 352 | } 353 | } 354 | } 355 | Ok(()) 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | action_stack::ActionStack, 3 | chart_editor::{MainState, ScreenState}, 4 | Modifiers, 5 | }; 6 | use anyhow::Result; 7 | use eframe::egui::Pos2; 8 | use eframe::egui::{Context, Painter}; 9 | use kson::Chart; 10 | 11 | mod bpm_ts; 12 | mod buttons; 13 | mod camera; 14 | mod laser; 15 | pub use bpm_ts::*; 16 | pub use buttons::*; 17 | pub use camera::*; 18 | pub use laser::*; 19 | 20 | pub trait CursorObject { 21 | fn primary_click( 22 | &mut self, 23 | _screen: ScreenState, 24 | _tick: u32, 25 | _tick_f: f64, 26 | _lane: f32, 27 | _chart: &Chart, 28 | _actions: &mut ActionStack, 29 | _pos: Pos2, 30 | ) { 31 | } 32 | 33 | fn secondary_click( 34 | &mut self, 35 | _screen: ScreenState, 36 | _tick: u32, 37 | _tick_f: f64, 38 | _lane: f32, 39 | _chart: &Chart, 40 | _actions: &mut ActionStack, 41 | _pos: Pos2, 42 | ) { 43 | } 44 | 45 | //Used as delete for most tools 46 | fn middle_click( 47 | &mut self, 48 | _screen: ScreenState, 49 | _tick: u32, 50 | _tick_f: f64, 51 | _lane: f32, 52 | _chart: &Chart, 53 | _actions: &mut ActionStack, 54 | _pos: Pos2, 55 | ) { 56 | } 57 | 58 | fn drag_end( 59 | &mut self, 60 | _screen: ScreenState, 61 | _tick: u32, 62 | _tick_f: f64, 63 | _lane: f32, 64 | _chart: &Chart, 65 | _actions: &mut ActionStack, 66 | _pos: Pos2, 67 | ) { 68 | } 69 | 70 | fn drag_start( 71 | &mut self, 72 | _screen: ScreenState, 73 | _tick: u32, 74 | _tick_f: f64, 75 | _lane: f32, 76 | _chart: &Chart, 77 | _actions: &mut ActionStack, 78 | _pos: Pos2, 79 | _modifiers: &Modifiers, 80 | ) { 81 | } 82 | 83 | fn update(&mut self, tick: u32, tick_f: f64, lane: f32, pos: Pos2, chart: &Chart); 84 | fn draw(&self, state: &MainState, painter: &Painter) -> Result<()>; 85 | fn draw_ui(&mut self, _state: &mut MainState, _ctx: &Context) {} 86 | } 87 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub trait Overlaps { 2 | fn overlaps(&self, other: &Self) -> bool; 3 | fn contains(&self, y: u32) -> bool; 4 | } 5 | 6 | impl Overlaps for kson::Interval { 7 | fn overlaps(&self, other: &Self) -> bool { 8 | self.y <= other.y + other.l && other.y <= self.y + self.l 9 | } 10 | 11 | fn contains(&self, y: u32) -> bool { 12 | (self.y..=self.y + self.l).contains(&y) 13 | } 14 | } 15 | 16 | impl Overlaps for kson::LaserSection { 17 | fn overlaps(&self, other: &Self) -> bool { 18 | match (self.last(), other.last()) { 19 | (Some(self_last), Some(other_last)) => { 20 | self.tick() <= other.tick() + other_last.ry 21 | && other.tick() <= self.tick() + self_last.ry 22 | } 23 | _ => false, 24 | } 25 | } 26 | 27 | fn contains(&self, y: u32) -> bool { 28 | if let Some(last) = self.last() { 29 | (self.tick()..=self.tick() + last.ry).contains(&y) 30 | } else { 31 | false 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /track_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drewol/kson-editor/7818f12cc3402e1a76ed8d0a53e51e8dc5aef7b7/track_test.png --------------------------------------------------------------------------------