From da6ebac92a757532b7b167b4a0918bc1b14aa062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 6 Jul 2024 22:35:40 -0400 Subject: [PATCH 001/109] Start implementing the animation editor --- crates/components/src/collapsing_view.rs | 43 ++- crates/core/src/lib.rs | 7 + crates/data/src/rmxp/animation.rs | 48 +++- crates/modals/src/sound_picker.rs | 6 +- crates/ui/src/windows/animations.rs | 318 +++++++++++++++++++++++ crates/ui/src/windows/mod.rs | 2 + src/app/top_bar.rs | 10 +- 7 files changed, 421 insertions(+), 13 deletions(-) create mode 100644 crates/ui/src/windows/animations.rs diff --git a/crates/components/src/collapsing_view.rs b/crates/components/src/collapsing_view.rs index 49222687..e9f57316 100644 --- a/crates/components/src/collapsing_view.rs +++ b/crates/components/src/collapsing_view.rs @@ -31,6 +31,7 @@ pub struct CollapsingView { depersisted_entries: usize, expanded_entry: luminol_data::OptionVec>, disable_animations: bool, + is_animating: bool, } impl CollapsingView { @@ -42,12 +43,37 @@ impl CollapsingView { /// them immediately this frame. pub fn clear_animations(&mut self) { self.disable_animations = true; + self.is_animating = false; } + /// Determines if a collapsing header is currently transitioning from open to closed or + /// vice-versa. If this is false, it's guaranteed at most one collapsing header body is + /// currently visible, so there will be at most one call to `show_body`. + pub fn is_animating(&self) -> bool { + self.is_animating + } + + /// Shows the widget. + /// + /// ui: `egui::Ui` where the widget should be shown. + /// + /// state_id: arbitrary integer that can be used to maintain more than one state for this + /// widget. This can be useful if you're showing this inside of a `DatabaseView` so that each + /// database item can get its own state for this widget. If you don't need more than one state, + /// set this to 0. + /// + /// vec: vector containing things that will be passed as argument to `show_header` and + /// `show_body`. + /// + /// show_header: this will be called exactly once for each item in `vec` to draw the headers of + /// the collapsing headers. + /// + /// show_body: this will be called at most once for each item in `vec` to draw the bodies of + /// the collapsing headers. pub fn show( &mut self, ui: &mut egui::Ui, - id: usize, + state_id: usize, vec: &mut Vec, mut show_header: impl FnMut(&mut egui::Ui, usize, &T), mut show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, @@ -55,16 +81,18 @@ impl CollapsingView { where T: Default, { + self.is_animating = false; + let mut inner_response = ui.with_cross_justify(|ui| { let mut modified = false; let mut deleted_entry = None; let mut new_entry = false; ui.group(|ui| { - if self.expanded_entry.get(id).is_none() { - self.expanded_entry.insert(id, None); + if self.expanded_entry.get(state_id).is_none() { + self.expanded_entry.insert(state_id, None); } - let expanded_entry = self.expanded_entry.get_mut(id).unwrap(); + let expanded_entry = self.expanded_entry.get_mut(state_id).unwrap(); for (i, entry) in vec.iter_mut().enumerate() { let ui_id = ui.make_persistent_id(i); @@ -95,6 +123,11 @@ impl CollapsingView { ui.ctx().animate_bool_with_time(ui_id, expanded, 0.); } + let openness = header.openness(ui.ctx()); + if openness > 0. && openness < 1. { + self.is_animating = true; + } + let layout = *ui.layout(); let (expand_button_response, _, _) = header .show_header(ui, |ui| { @@ -133,7 +166,7 @@ impl CollapsingView { self.disable_animations = false; if let Some(i) = deleted_entry { - if let Some(expanded_entry) = self.expanded_entry.get_mut(id) { + if let Some(expanded_entry) = self.expanded_entry.get_mut(state_id) { if *expanded_entry == Some(i) { self.disable_animations = true; *expanded_entry = None; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 0ee87f6f..0150a56b 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -443,3 +443,10 @@ pub fn slice_is_sorted(s: &[T]) -> bool { a <= b }) } + +pub fn slice_is_sorted_by std::cmp::Ordering>(s: &[T], mut f: F) -> bool { + s.windows(2).all(|w| { + let [a, b] = w else { unreachable!() }; // could maybe do unreachable_unchecked + f(a, b) != std::cmp::Ordering::Greater + }) +} diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index 98f16790..0328117b 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -42,10 +42,10 @@ pub struct Animation { pub struct Timing { pub frame: i32, pub se: AudioFile, - pub flash_scope: i32, + pub flash_scope: Scope, pub flash_color: Color, pub flash_duration: i32, - pub condition: i32, + pub condition: Condition, } #[derive(Default, Debug, serde::Deserialize, serde::Serialize)] @@ -77,3 +77,47 @@ pub enum Position { Bottom = 2, Screen = 3, } + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] +#[derive( + num_enum::TryFromPrimitive, + num_enum::IntoPrimitive, + strum::Display, + strum::EnumIter +)] +#[derive(serde::Deserialize, serde::Serialize)] +#[derive(alox_48::Deserialize, alox_48::Serialize)] +#[repr(u8)] +#[serde(into = "u8")] +#[serde(try_from = "u8")] +#[marshal(into = "u8")] +#[marshal(try_from = "u8")] +pub enum Scope { + #[default] + None = 0, + Target = 1, + Screen = 2, + #[strum(to_string = "Hide Target")] + HideTarget = 3, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] +#[derive( + num_enum::TryFromPrimitive, + num_enum::IntoPrimitive, + strum::Display, + strum::EnumIter +)] +#[derive(serde::Deserialize, serde::Serialize)] +#[derive(alox_48::Deserialize, alox_48::Serialize)] +#[repr(u8)] +#[serde(into = "u8")] +#[serde(try_from = "u8")] +#[marshal(into = "u8")] +#[marshal(try_from = "u8")] +pub enum Condition { + #[default] + None = 0, + Hit = 1, + Miss = 2, +} diff --git a/crates/modals/src/sound_picker.rs b/crates/modals/src/sound_picker.rs index 64e7946f..dbc99f17 100644 --- a/crates/modals/src/sound_picker.rs +++ b/crates/modals/src/sound_picker.rs @@ -79,11 +79,15 @@ impl luminol_core::Modal for Modal { fn reset(&mut self, _: &mut luminol_core::UpdateState<'_>, _data: Self::Data<'_>) { // we don't need to do much here - self.state = State::Closed; + self.close_window(); } } impl Modal { + pub fn close_window(&mut self) { + self.state = State::Closed; + } + pub fn show_window( &mut self, update_state: &mut luminol_core::UpdateState<'_>, diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs new file mode 100644 index 00000000..92f5233f --- /dev/null +++ b/crates/ui/src/windows/animations.rs @@ -0,0 +1,318 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use luminol_components::UiExt; +use luminol_core::Modal; + +use luminol_modals::sound_picker::Modal as SoundPicker; + +/// Database - Animations management window. +pub struct Window { + selected_animation_name: Option, + previous_animation: Option, + + collapsing_view: luminol_components::CollapsingView, + timing_se_picker: SoundPicker, + view: luminol_components::DatabaseView, +} + +impl Window { + pub fn new() -> Self { + Self { + selected_animation_name: None, + previous_animation: None, + collapsing_view: luminol_components::CollapsingView::new(), + timing_se_picker: SoundPicker::new( + luminol_audio::Source::SE, + "animations_timing_se_picker", + ), + view: luminol_components::DatabaseView::new(), + } + } + + fn show_timing_header(ui: &mut egui::Ui, timing: &luminol_data::rpg::animation::Timing) { + let mut vec = Vec::with_capacity(3); + + if let Some(path) = &timing.se.name { + vec.push(format!("play {:?}", path.file_name().unwrap_or_default())); + }; + + match timing.condition { + luminol_data::rpg::animation::Condition::None => {} + luminol_data::rpg::animation::Condition::Hit => vec.push("on hit".into()), + luminol_data::rpg::animation::Condition::Miss => vec.push("on miss".into()), + } + + match timing.flash_scope { + luminol_data::rpg::animation::Scope::None => {} + luminol_data::rpg::animation::Scope::Target => { + vec.push(format!( + "flash target #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", + timing.flash_color.red.round() as u8, + timing.flash_color.green.round() as u8, + timing.flash_color.blue.round() as u8, + timing.flash_color.alpha.round() as u8, + timing.flash_duration, + )); + } + luminol_data::rpg::animation::Scope::Screen => { + vec.push(format!( + "flash screen #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", + timing.flash_color.red.round() as u8, + timing.flash_color.green.round() as u8, + timing.flash_color.blue.round() as u8, + timing.flash_color.alpha.round() as u8, + timing.flash_duration, + )); + } + luminol_data::rpg::animation::Scope::HideTarget => { + vec.push(format!("hide target for {} frames", timing.flash_duration)); + } + } + + ui.label(format!( + "Frame {:0>3}: {}", + timing.frame + 1, + vec.join(", ") + )); + } + + fn show_timing_body( + ui: &mut egui::Ui, + update_state: &mut luminol_core::UpdateState<'_>, + animation_id: usize, + animation_frame_max: i32, + timing_se_picker: &mut SoundPicker, + timing: (usize, &mut luminol_data::rpg::animation::Timing), + ) -> egui::Response { + let (timing_index, timing) = timing; + let mut modified = false; + + let mut response = egui::Frame::none() + .show(ui, |ui| { + ui.columns(2, |columns| { + columns[0].columns(2, |columns| { + modified |= columns[1] + .add(luminol_components::Field::new( + "Condition", + luminol_components::EnumComboBox::new( + (animation_id, timing_index, "condition"), + &mut timing.condition, + ), + )) + .changed(); + + let mut frame = timing.frame + 1; + modified |= columns[0] + .add(luminol_components::Field::new( + "Frame", + egui::DragValue::new(&mut frame) + .clamp_range(1..=animation_frame_max), + )) + .changed(); + timing.frame = frame - 1; + }); + + modified |= columns[1] + .add(luminol_components::Field::new( + "SE", + timing_se_picker.button(&mut timing.se, update_state), + )) + .changed(); + }); + + if timing.flash_scope == luminol_data::rpg::animation::Scope::None { + modified |= ui + .add(luminol_components::Field::new( + "Flash", + luminol_components::EnumComboBox::new( + (animation_id, timing_index, "flash_scope"), + &mut timing.flash_scope, + ), + )) + .changed(); + } else { + ui.columns(2, |columns| { + modified |= columns[0] + .add(luminol_components::Field::new( + "Flash", + luminol_components::EnumComboBox::new( + (animation_id, timing_index, "flash_scope"), + &mut timing.flash_scope, + ), + )) + .changed(); + + modified |= columns[1] + .add(luminol_components::Field::new( + "Flash Duration", + egui::DragValue::new(&mut timing.flash_duration) + .clamp_range(1..=animation_frame_max), + )) + .changed(); + }); + } + + if matches!( + timing.flash_scope, + luminol_data::rpg::animation::Scope::Target + | luminol_data::rpg::animation::Scope::Screen + ) { + modified |= ui + .add(luminol_components::Field::new( + "Flash Color", + |ui: &mut egui::Ui| { + let mut color = [ + (timing.flash_color.red / 256.) as f32, + (timing.flash_color.green / 256.) as f32, + (timing.flash_color.blue / 256.) as f32, + (timing.flash_color.alpha / 256.) as f32, + ]; + ui.spacing_mut().interact_size.x = ui.available_width(); // make the color picker button as wide as possible + let response = ui.color_edit_button_rgba_unmultiplied(&mut color); + if response.changed() { + timing.flash_color.red = color[0] as f64 * 256.; + timing.flash_color.green = color[1] as f64 * 256.; + timing.flash_color.blue = color[2] as f64 * 256.; + timing.flash_color.alpha = color[3] as f64 * 256.; + } + response + }, + )) + .changed(); + } + }) + .response; + + if modified { + response.mark_changed(); + } + response + } +} + +impl luminol_core::Window for Window { + fn id(&self) -> egui::Id { + egui::Id::new("animation_editor") + } + + fn requires_filesystem(&self) -> bool { + true + } + + fn show( + &mut self, + ctx: &egui::Context, + open: &mut bool, + update_state: &mut luminol_core::UpdateState<'_>, + ) { + let data = std::mem::take(update_state.data); // take data to avoid borrow checker issues + let mut animations = data.animations(); + + let mut modified = false; + + self.selected_animation_name = None; + + let name = if let Some(name) = &self.selected_animation_name { + format!("Editing animation {:?}", name) + } else { + "Animation Editor".into() + }; + + let response = egui::Window::new(name) + .id(self.id()) + .default_width(500.) + .open(open) + .show(ctx, |ui| { + self.view.show( + ui, + update_state, + "Animations", + &mut animations.data, + |animation| format!("{:0>4}: {}", animation.id + 1, animation.name), + |ui, animations, id, update_state| { + let animation = &mut animations[id]; + self.selected_animation_name = Some(animation.name.clone()); + + ui.with_padded_stripe(false, |ui| { + modified |= ui + .add(luminol_components::Field::new( + "Name", + egui::TextEdit::singleline(&mut animation.name) + .desired_width(f32::INFINITY), + )) + .changed(); + }); + + ui.with_padded_stripe(true, |ui| { + modified |= ui + .add(luminol_components::Field::new( + "SE and Flash", + |ui: &mut egui::Ui| { + if self.previous_animation != Some(animation.id) { + self.collapsing_view.clear_animations(); + self.timing_se_picker.close_window(); + } else if self.collapsing_view.is_animating() { + self.timing_se_picker.close_window(); + } + self.collapsing_view.show( + ui, + animation.id, + &mut animation.timings, + |ui, _i, timing| Self::show_timing_header(ui, timing), + |ui, i, timing| { + Self::show_timing_body( + ui, + update_state, + animation.id, + animation.frame_max, + &mut self.timing_se_picker, + (i, timing), + ) + }, + ) + }, + )) + .changed(); + }); + + self.previous_animation = Some(animation.id); + }, + ) + }); + + if response.is_some_and(|ir| ir.inner.is_some_and(|ir| ir.inner.modified)) { + modified = true; + } + + if modified { + update_state.modified.set(true); + animations.modified = true; + } + + drop(animations); + + *update_state.data = data; // restore data + } +} diff --git a/crates/ui/src/windows/mod.rs b/crates/ui/src/windows/mod.rs index 4856887a..877eeb7f 100644 --- a/crates/ui/src/windows/mod.rs +++ b/crates/ui/src/windows/mod.rs @@ -26,6 +26,8 @@ pub mod about; /// The actor editor. pub mod actors; +/// The animation editor. +pub mod animations; pub mod appearance; /// The archive manager for creating and extracting RGSSAD archives. pub mod archive_manager; diff --git a/src/app/top_bar.rs b/src/app/top_bar.rs index 911bf437..db52da3c 100644 --- a/src/app/top_bar.rs +++ b/src/app/top_bar.rs @@ -176,11 +176,11 @@ impl TopBar { } }); - ui.add_enabled_ui(false, |ui| { - if ui.button("Animations [TODO]").clicked() { - todo!(); - } - }); + if ui.button("Animations").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::animations::Window::new()); + } if ui.button("Common Events").clicked() { update_state From 8b3e28da45ade3c40f7dac62f137a8e4b5695cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 6 Jul 2024 23:22:50 -0400 Subject: [PATCH 002/109] Sort animation timings by frame --- crates/components/src/collapsing_view.rs | 114 ++++++++++++++++++++++- crates/ui/src/windows/animations.rs | 6 +- 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/crates/components/src/collapsing_view.rs b/crates/components/src/collapsing_view.rs index e9f57316..6ea16e5d 100644 --- a/crates/components/src/collapsing_view.rs +++ b/crates/components/src/collapsing_view.rs @@ -32,6 +32,7 @@ pub struct CollapsingView { expanded_entry: luminol_data::OptionVec>, disable_animations: bool, is_animating: bool, + need_sort: bool, } impl CollapsingView { @@ -46,6 +47,11 @@ impl CollapsingView { self.is_animating = false; } + /// Force the next invocation of `.show_with_sort` to sort `vec`. + pub fn request_sort(&mut self) { + self.need_sort = true; + } + /// Determines if a collapsing header is currently transitioning from open to closed or /// vice-versa. If this is false, it's guaranteed at most one collapsing header body is /// currently visible, so there will be at most one call to `show_body`. @@ -53,7 +59,7 @@ impl CollapsingView { self.is_animating } - /// Shows the widget. + /// Shows the widget with no sorting applied to `vec`. /// /// ui: `egui::Ui` where the widget should be shown. /// @@ -71,12 +77,108 @@ impl CollapsingView { /// show_body: this will be called at most once for each item in `vec` to draw the bodies of /// the collapsing headers. pub fn show( + &mut self, + ui: &mut egui::Ui, + state_id: usize, + vec: &mut Vec, + show_header: impl FnMut(&mut egui::Ui, usize, &T), + show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, + ) -> egui::Response + where + T: Default, + { + self.show_impl( + ui, + state_id, + vec, + show_header, + show_body, + |_vec, _expanded_entry| false, + ) + } + + /// Shows the widget, also using a comparator function to sort `vec` when a new item is added + /// or when `.request_sort` is called. + /// + /// ui: `egui::Ui` where the widget should be shown. + /// + /// state_id: arbitrary integer that can be used to maintain more than one state for this + /// widget. This can be useful if you're showing this inside of a `DatabaseView` so that each + /// database item can get its own state for this widget. If you don't need more than one state, + /// set this to 0. + /// + /// vec: vector containing things that will be passed as argument to `show_header` and + /// `show_body`. + /// + /// cmp: comparator that will be used to sort the `vec` when a new item is added or when + /// `.request_sort` is called. + /// + /// show_header: this will be called exactly once for each item in `vec` to draw the headers of + /// the collapsing headers. + /// + /// show_body: this will be called at most once for each item in `vec` to draw the bodies of + /// the collapsing headers. + pub fn show_with_sort( + &mut self, + ui: &mut egui::Ui, + state_id: usize, + vec: &mut Vec, + show_header: impl FnMut(&mut egui::Ui, usize, &T), + show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, + mut cmp: impl FnMut(&T, &T) -> std::cmp::Ordering, + ) -> egui::Response + where + T: Default, + { + self.show_impl( + ui, + state_id, + vec, + show_header, + show_body, + |vec, expanded_entry| { + // Sort `vec` using the provided comparator function (if applicable) and + // update `expanded_entry` to account for the sort + if !luminol_core::slice_is_sorted_by(vec, &mut cmp) { + if expanded_entry.is_some() { + let (before, after) = vec.split_at(expanded_entry.unwrap()); + if let Some((cmp_item, after)) = after.split_first() { + *expanded_entry = Some( + before + .iter() + .filter(|item| { + cmp(item, cmp_item) != std::cmp::Ordering::Greater + }) + .count() + + after + .iter() + .filter(|item| { + cmp(item, cmp_item) == std::cmp::Ordering::Less + }) + .count(), + ); + } else { + *expanded_entry = None; + } + } + + vec.sort_by(&mut cmp); + true + } else { + false + } + }, + ) + } + + fn show_impl( &mut self, ui: &mut egui::Ui, state_id: usize, vec: &mut Vec, mut show_header: impl FnMut(&mut egui::Ui, usize, &T), mut show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, + mut sort_impl: impl FnMut(&mut Vec, &mut Option) -> bool, ) -> egui::Response where T: Default, @@ -94,6 +196,14 @@ impl CollapsingView { } let expanded_entry = self.expanded_entry.get_mut(state_id).unwrap(); + if self.need_sort { + self.need_sort = false; + if sort_impl(vec, expanded_entry) { + modified = true; + self.disable_animations = true; + } + } + for (i, entry) in vec.iter_mut().enumerate() { let ui_id = ui.make_persistent_id(i); @@ -160,6 +270,8 @@ impl CollapsingView { *expanded_entry = Some(vec.len()); vec.push(Default::default()); new_entry = true; + + sort_impl(vec, expanded_entry); } }); diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 92f5233f..626ed51e 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -270,13 +270,16 @@ impl luminol_core::Window for Window { .add(luminol_components::Field::new( "SE and Flash", |ui: &mut egui::Ui| { + if *update_state.modified_during_prev_frame { + self.collapsing_view.request_sort(); + } if self.previous_animation != Some(animation.id) { self.collapsing_view.clear_animations(); self.timing_se_picker.close_window(); } else if self.collapsing_view.is_animating() { self.timing_se_picker.close_window(); } - self.collapsing_view.show( + self.collapsing_view.show_with_sort( ui, animation.id, &mut animation.timings, @@ -291,6 +294,7 @@ impl luminol_core::Window for Window { (i, timing), ) }, + |a, b| a.frame.cmp(&b.frame), ) }, )) From 3150fac7fd9878255cd758ba0eb327b4e68a00ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 7 Jul 2024 14:05:06 -0400 Subject: [PATCH 003/109] Fix animation editor color conversions --- crates/ui/src/windows/animations.rs | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 626ed51e..45e4db26 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -69,20 +69,20 @@ impl Window { luminol_data::rpg::animation::Scope::Target => { vec.push(format!( "flash target #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", - timing.flash_color.red.round() as u8, - timing.flash_color.green.round() as u8, - timing.flash_color.blue.round() as u8, - timing.flash_color.alpha.round() as u8, + timing.flash_color.red.clamp(0., 255.).trunc() as u8, + timing.flash_color.green.clamp(0., 255.).trunc() as u8, + timing.flash_color.blue.clamp(0., 255.).trunc() as u8, + timing.flash_color.alpha.clamp(0., 255.).trunc() as u8, timing.flash_duration, )); } luminol_data::rpg::animation::Scope::Screen => { vec.push(format!( "flash screen #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", - timing.flash_color.red.round() as u8, - timing.flash_color.green.round() as u8, - timing.flash_color.blue.round() as u8, - timing.flash_color.alpha.round() as u8, + timing.flash_color.red.clamp(0., 255.).trunc() as u8, + timing.flash_color.green.clamp(0., 255.).trunc() as u8, + timing.flash_color.blue.clamp(0., 255.).trunc() as u8, + timing.flash_color.alpha.clamp(0., 255.).trunc() as u8, timing.flash_duration, )); } @@ -184,18 +184,18 @@ impl Window { "Flash Color", |ui: &mut egui::Ui| { let mut color = [ - (timing.flash_color.red / 256.) as f32, - (timing.flash_color.green / 256.) as f32, - (timing.flash_color.blue / 256.) as f32, - (timing.flash_color.alpha / 256.) as f32, + timing.flash_color.red.clamp(0., 255.).trunc() as u8, + timing.flash_color.green.clamp(0., 255.).trunc() as u8, + timing.flash_color.blue.clamp(0., 255.).trunc() as u8, + timing.flash_color.alpha.clamp(0., 255.).trunc() as u8, ]; ui.spacing_mut().interact_size.x = ui.available_width(); // make the color picker button as wide as possible - let response = ui.color_edit_button_rgba_unmultiplied(&mut color); + let response = ui.color_edit_button_srgba_unmultiplied(&mut color); if response.changed() { - timing.flash_color.red = color[0] as f64 * 256.; - timing.flash_color.green = color[1] as f64 * 256.; - timing.flash_color.blue = color[2] as f64 * 256.; - timing.flash_color.alpha = color[3] as f64 * 256.; + timing.flash_color.red = color[0] as f64; + timing.flash_color.green = color[1] as f64; + timing.flash_color.blue = color[2] as f64; + timing.flash_color.alpha = color[3] as f64; } response }, From fa1289d52f68a05f1c313c4f7d5d36aacfdd5b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 7 Jul 2024 14:58:17 -0400 Subject: [PATCH 004/109] Fix some problems with the animation timing edit UI * Timing duration now defaults to 1 instead of 0 * The "frame" drag value for timings now waits until you finish editing the drag value before applying changes --- crates/data/src/rmxp/animation.rs | 15 ++++++++++++++- crates/ui/src/windows/animations.rs | 26 ++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index 0328117b..caa69f68 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -36,7 +36,7 @@ pub struct Animation { pub timings: Vec, } -#[derive(Default, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(alox_48::Deserialize, alox_48::Serialize)] #[marshal(class = "RPG::Animation::Timing")] pub struct Timing { @@ -48,6 +48,19 @@ pub struct Timing { pub condition: Condition, } +impl Default for Timing { + fn default() -> Self { + Self { + frame: 0, + se: AudioFile::default(), + flash_scope: Scope::default(), + flash_color: Color::default(), + flash_duration: 1, + condition: Condition::default(), + } + } +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize)] #[derive(alox_48::Deserialize, alox_48::Serialize)] #[marshal(class = "RPG::Animation::Frame")] diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 45e4db26..8e4df435 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -22,6 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +use egui::Widget; use luminol_components::UiExt; use luminol_core::Modal; @@ -31,6 +32,7 @@ use luminol_modals::sound_picker::Modal as SoundPicker; pub struct Window { selected_animation_name: Option, previous_animation: Option, + previous_timing_frame: Option, collapsing_view: luminol_components::CollapsingView, timing_se_picker: SoundPicker, @@ -42,6 +44,7 @@ impl Window { Self { selected_animation_name: None, previous_animation: None, + previous_timing_frame: None, collapsing_view: luminol_components::CollapsingView::new(), timing_se_picker: SoundPicker::new( luminol_audio::Source::SE, @@ -104,6 +107,7 @@ impl Window { animation_id: usize, animation_frame_max: i32, timing_se_picker: &mut SoundPicker, + previous_timing_frame: &mut Option, timing: (usize, &mut luminol_data::rpg::animation::Timing), ) -> egui::Response { let (timing_index, timing) = timing; @@ -123,15 +127,28 @@ impl Window { )) .changed(); - let mut frame = timing.frame + 1; modified |= columns[0] .add(luminol_components::Field::new( "Frame", - egui::DragValue::new(&mut frame) - .clamp_range(1..=animation_frame_max), + |ui: &mut egui::Ui| { + let mut frame = + previous_timing_frame.unwrap_or(timing.frame + 1); + let mut response = egui::DragValue::new(&mut frame) + .clamp_range(1..=animation_frame_max) + .update_while_editing(false) + .ui(ui); + response.changed = false; + if response.dragged() { + *previous_timing_frame = Some(frame); + } else { + timing.frame = frame - 1; + *previous_timing_frame = None; + response.changed = true; + } + response + }, )) .changed(); - timing.frame = frame - 1; }); modified |= columns[1] @@ -291,6 +308,7 @@ impl luminol_core::Window for Window { animation.id, animation.frame_max, &mut self.timing_se_picker, + &mut self.previous_timing_frame, (i, timing), ) }, From 96a9c5583b1ad986c80e50683645f5ec1952fc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 7 Jul 2024 15:02:04 -0400 Subject: [PATCH 005/109] Swap SE and condition in the animation timing headers --- crates/ui/src/windows/animations.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 8e4df435..b9fd6bd1 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -57,16 +57,16 @@ impl Window { fn show_timing_header(ui: &mut egui::Ui, timing: &luminol_data::rpg::animation::Timing) { let mut vec = Vec::with_capacity(3); - if let Some(path) = &timing.se.name { - vec.push(format!("play {:?}", path.file_name().unwrap_or_default())); - }; - match timing.condition { luminol_data::rpg::animation::Condition::None => {} luminol_data::rpg::animation::Condition::Hit => vec.push("on hit".into()), luminol_data::rpg::animation::Condition::Miss => vec.push("on miss".into()), } + if let Some(path) = &timing.se.name { + vec.push(format!("play {:?}", path.file_name().unwrap_or_default())); + }; + match timing.flash_scope { luminol_data::rpg::animation::Scope::None => {} luminol_data::rpg::animation::Scope::Target => { From 0c85c80541577ee1f72e77b1b35521d8db6f1a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 9 Jul 2024 01:12:20 -0400 Subject: [PATCH 006/109] Start implementing the animation frame view and animation cells shader --- crates/components/src/animation_frame_view.rs | 172 +++++++++++++++ crates/components/src/lib.rs | 3 + crates/data/src/option_vec.rs | 21 ++ crates/graphics/src/frame.rs | 142 +++++++++++++ crates/graphics/src/lib.rs | 4 +- crates/graphics/src/loaders/atlas.rs | 42 +++- crates/graphics/src/primitives/cells/atlas.rs | 197 ++++++++++++++++++ .../graphics/src/primitives/cells/display.rs | 92 ++++++++ .../graphics/src/primitives/cells/instance.rs | 104 +++++++++ crates/graphics/src/primitives/cells/mod.rs | 147 +++++++++++++ .../graphics/src/primitives/cells/shader.rs | 120 +++++++++++ crates/graphics/src/primitives/mod.rs | 5 + .../src/primitives/shaders/cells.wgsl | 98 +++++++++ crates/graphics/src/primitives/tiles/atlas.rs | 2 +- crates/ui/src/windows/animations.rs | 68 +++++- 15 files changed, 1213 insertions(+), 4 deletions(-) create mode 100644 crates/components/src/animation_frame_view.rs create mode 100644 crates/graphics/src/frame.rs create mode 100644 crates/graphics/src/primitives/cells/atlas.rs create mode 100644 crates/graphics/src/primitives/cells/display.rs create mode 100644 crates/graphics/src/primitives/cells/instance.rs create mode 100644 crates/graphics/src/primitives/cells/mod.rs create mode 100644 crates/graphics/src/primitives/cells/shader.rs create mode 100644 crates/graphics/src/primitives/shaders/cells.wgsl diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs new file mode 100644 index 00000000..22752010 --- /dev/null +++ b/crates/components/src/animation_frame_view.rs @@ -0,0 +1,172 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; +use luminol_graphics::Renderable; + +pub struct AnimationFrameView { + pub frame: luminol_graphics::Frame, + + pub pan: egui::Vec2, + + pub scale: f32, + pub previous_scale: f32, + + pub data_id: egui::Id, +} + +impl AnimationFrameView { + pub fn new( + update_state: &luminol_core::UpdateState<'_>, + animation: &luminol_data::rpg::Animation, + frame_index: usize, + ) -> color_eyre::Result { + let data_id = egui::Id::new("luminol_animation_frame_view").with( + update_state + .project_config + .as_ref() + .expect("project not loaded") + .project + .persistence_id, + ); + let (pan, scale) = update_state + .ctx + .data_mut(|d| *d.get_persisted_mut_or_insert_with(data_id, || (egui::Vec2::ZERO, 50.))); + + let frame = luminol_graphics::Frame::new( + &update_state.graphics, + update_state.graphics.atlas_loader.load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + )?, + animation, + frame_index, + ); + + Ok(Self { + frame, + pan, + scale, + previous_scale: scale, + data_id, + }) + } + + pub fn ui( + &mut self, + ui: &mut egui::Ui, + update_state: &luminol_core::UpdateState<'_>, + ) -> egui::Response { + let canvas_rect = ui.max_rect(); + let canvas_center = canvas_rect.center(); + ui.set_clip_rect(canvas_rect); + + let mut response = ui.allocate_rect(canvas_rect, egui::Sense::click_and_drag()); + + let min_clip = (ui.ctx().screen_rect().min - canvas_rect.min).max(Default::default()); + let max_clip = (canvas_rect.max - ui.ctx().screen_rect().max).max(Default::default()); + let clip_offset = (max_clip - min_clip) / 2.; + let canvas_rect = ui.ctx().screen_rect().intersect(canvas_rect); + + // If the user changed the scale using the scale slider, pan the map so that the scale uses + // the center of the visible part of the map as the scale center + if self.scale != self.previous_scale { + self.pan = self.pan * self.scale / self.previous_scale; + } + + // Handle zoom + if let Some(pos) = response.hover_pos() { + // We need to store the old scale before applying any transformations + let old_scale = self.scale; + let delta = ui.input(|i| i.smooth_scroll_delta.y); + + // Apply scroll and cap max zoom to 15% + self.scale *= (delta / 9.0f32.exp2()).exp2(); + self.scale = self.scale.max(15.).min(300.); + + // Get the normalized cursor position relative to pan + let pos_norm = (pos - self.pan - canvas_center) / old_scale; + // Offset the pan to the cursor remains in the same place + // Still not sure how the math works out, if it ain't broke don't fix it + self.pan = pos - canvas_center - pos_norm * self.scale; + } + + self.previous_scale = self.scale; + + let ctrl_drag = + ui.input(|i| i.modifiers.command) && response.dragged_by(egui::PointerButton::Primary); + + let panning_map_view = response.dragged_by(egui::PointerButton::Middle) || ctrl_drag; + + if panning_map_view { + self.pan += response.drag_delta(); + ui.ctx().request_repaint(); + } + + // Handle cursor icon + if panning_map_view { + response = response.on_hover_cursor(egui::CursorIcon::Grabbing); + } else { + response = response.on_hover_cursor(egui::CursorIcon::Grab); + } + + // Determine some values which are relatively constant + // If we don't use pixels_per_point then the map is the wrong size. + // *don't ask me how i know this*. + // its a *long* story + let scale = self.scale / (ui.ctx().pixels_per_point() * 100.); + + let canvas_pos = canvas_center + self.pan; + + let width2 = FRAME_WIDTH as f32 / 2.; + let height2 = FRAME_HEIGHT as f32 / 2.; + let frame_size2 = egui::Vec2::new(width2, height2); + let frame_rect = egui::Rect { + min: canvas_pos - frame_size2, + max: canvas_pos + frame_size2, + }; + + // no idea why this math works (could probably be simplified) + let proj_center_x = width2 - (self.pan.x + clip_offset.x) / scale; + let proj_center_y = height2 - (self.pan.y + clip_offset.y) / scale; + let proj_width2 = canvas_rect.width() / scale / 2.; + let proj_height2 = canvas_rect.height() / scale / 2.; + self.frame.viewport.set( + &update_state.graphics.render_state, + glam::vec2(canvas_rect.width(), canvas_rect.height()), + glam::vec2(proj_width2 - proj_center_x, proj_height2 - proj_center_y) * scale, + glam::Vec2::splat(scale), + ); + + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs_f32(16. / 60.)); + + let painter = luminol_graphics::Painter::new(self.frame.prepare(&update_state.graphics)); + ui.painter() + .add(luminol_egui_wgpu::Callback::new_paint_callback( + canvas_rect, + painter, + )); + + ui.ctx().data_mut(|d| { + d.insert_persisted(self.data_id, (self.pan, self.scale)); + }); + + response + } +} diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index 01730abc..3199e9f9 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -49,6 +49,9 @@ pub use database_view::DatabaseView; mod collapsing_view; pub use collapsing_view::CollapsingView; +mod animation_frame_view; +pub use animation_frame_view::AnimationFrameView; + mod id_vec; pub use id_vec::{IdVecPlusMinusSelection, IdVecSelection, RankSelection}; diff --git a/crates/data/src/option_vec.rs b/crates/data/src/option_vec.rs index 13a71949..46f45a69 100644 --- a/crates/data/src/option_vec.rs +++ b/crates/data/src/option_vec.rs @@ -74,6 +74,10 @@ impl OptionVec { self.vec.reserve(additional); } + pub fn clear(&mut self) { + self.vec.clear(); + } + pub fn iter(&self) -> Iter<'_, T> { self.into_iter() } @@ -154,6 +158,23 @@ impl FromIterator<(usize, T)> for OptionVec { } } +impl Extend<(usize, T)> for OptionVec { + fn extend>(&mut self, iterable: I) { + for (i, v) in iterable.into_iter() { + if i >= self.vec.len() { + let additional = i - self.vec.len() + 1; + self.vec.reserve(additional); + self.vec + .extend(std::iter::repeat_with(|| None).take(additional)); + } + if self.vec[i].is_none() { + self.num_values += 1; + } + self.vec[i] = Some(v); + } + } +} + impl Index for OptionVec { type Output = T; fn index(&self, index: usize) -> &Self::Output { diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs new file mode 100644 index 00000000..872e4455 --- /dev/null +++ b/crates/graphics/src/frame.rs @@ -0,0 +1,142 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use crate::primitives::cells::Atlas; +use crate::{Drawable, GraphicsState, Renderable, Sprite, Viewport}; +use luminol_data::OptionVec; + +pub const FRAME_WIDTH: usize = 640; +pub const FRAME_HEIGHT: usize = 320; + +pub struct Frame { + pub atlas: Atlas, + pub sprites: OptionVec, + pub viewport: Viewport, +} + +impl Frame { + pub fn new( + graphics_state: &GraphicsState, + atlas: Atlas, + animation: &luminol_data::rpg::Animation, + frame_index: usize, + ) -> Self { + let viewport = Viewport::new( + graphics_state, + glam::vec2(FRAME_WIDTH as f32 * 32., FRAME_HEIGHT as f32 * 32.), + ); + + let frame = &animation.frames[frame_index]; + let sprites = frame.cell_data.as_slice() + [0..frame.cell_data.xsize().min(frame.cell_max as usize)] + .iter() + .copied() + .enumerate() + .map(|(i, cell_id)| { + ( + i, + Sprite::basic_hue_quad( + graphics_state, + 0, + atlas.calc_quad(cell_id), + &atlas.atlas_texture, + &viewport, + ), + ) + }) + .collect(); + + Self { + atlas, + sprites, + viewport, + } + } + + /// Updates the sprite for one cell based on the given animation frame. + pub fn update_cell( + &mut self, + graphics_state: &GraphicsState, + frame: &luminol_data::rpg::animation::Frame, + cell_index: usize, + ) { + let cell_id = frame.cell_data[(cell_index, 0)]; + self.sprites.insert( + cell_index, + Sprite::basic_hue_quad( + graphics_state, + 0, + self.atlas.calc_quad(cell_id), + &self.atlas.atlas_texture, + &self.viewport, + ), + ) + } + + /// Updates the sprite for every cell based on the given animation frame. + pub fn update_all_cells( + &mut self, + graphics_state: &GraphicsState, + frame: &luminol_data::rpg::animation::Frame, + ) { + self.sprites.clear(); + self.sprites.extend( + frame.cell_data.as_slice()[0..frame.cell_data.xsize().min(frame.cell_max as usize)] + .iter() + .copied() + .enumerate() + .map(|(i, cell_id)| { + ( + i, + Sprite::basic_hue_quad( + graphics_state, + 0, + self.atlas.calc_quad(cell_id), + &self.atlas.atlas_texture, + &self.viewport, + ), + ) + }), + ); + } +} + +pub struct Prepared { + sprites: Vec<::Prepared>, +} + +impl Renderable for Frame { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &std::sync::Arc) -> Self::Prepared { + Self::Prepared { + sprites: self + .sprites + .iter_mut() + .map(|(_, sprite)| sprite.prepare(graphics_state)) + .collect(), + } + } +} + +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + for sprite in &self.sprites { + sprite.draw(render_pass); + } + } +} diff --git a/crates/graphics/src/lib.rs b/crates/graphics/src/lib.rs index 0d873176..6eda9dc2 100644 --- a/crates/graphics/src/lib.rs +++ b/crates/graphics/src/lib.rs @@ -25,18 +25,20 @@ pub use loaders::texture::Texture; // Building blocks that make up more complex parts (i.e. the map view, or events) pub mod primitives; pub use primitives::{ - collision::Collision, grid::Grid, sprite::Sprite, tiles::Atlas, tiles::Tiles, + cells::Cells, collision::Collision, grid::Grid, sprite::Sprite, tiles::Atlas, tiles::Tiles, }; pub mod data; pub use data::*; pub mod event; +pub mod frame; pub mod map; pub mod plane; pub mod tilepicker; pub use event::Event; +pub use frame::Frame; pub use map::Map; pub use plane::Plane; pub use tilepicker::Tilepicker; diff --git a/crates/graphics/src/loaders/atlas.rs b/crates/graphics/src/loaders/atlas.rs index d517f813..b41192c8 100644 --- a/crates/graphics/src/loaders/atlas.rs +++ b/crates/graphics/src/loaders/atlas.rs @@ -14,11 +14,13 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . +use crate::primitives::cells::Atlas as AnimationAtlas; use crate::{Atlas, GraphicsState}; #[derive(Default)] pub struct Loader { atlases: dashmap::DashMap, + animation_atlases: dashmap::DashMap, } impl Loader { @@ -35,6 +37,19 @@ impl Loader { .clone()) } + pub fn load_animation_atlas( + &self, + graphics_state: &GraphicsState, + filesystem: &impl luminol_filesystem::FileSystem, + animation: &luminol_data::rpg::Animation, + ) -> color_eyre::Result { + Ok(self + .animation_atlases + .entry(animation.id) + .or_insert_with(|| AnimationAtlas::new(graphics_state, filesystem, animation)) + .clone()) + } + pub fn reload_atlas( &self, graphics_state: &GraphicsState, @@ -48,15 +63,40 @@ impl Loader { .clone()) } + pub fn reload_animation_atlas( + &self, + graphics_state: &GraphicsState, + filesystem: &impl luminol_filesystem::FileSystem, + animation: &luminol_data::rpg::Animation, + ) -> color_eyre::Result { + Ok(self + .animation_atlases + .entry(animation.id) + .insert(AnimationAtlas::new(graphics_state, filesystem, animation)) + .clone()) + } + pub fn get_atlas(&self, id: usize) -> Option { self.atlases.get(&id).map(|atlas| atlas.clone()) } + pub fn get_animation_atlas(&self, id: usize) -> Option { + self.animation_atlases.get(&id).map(|atlas| atlas.clone()) + } + pub fn get_expect(&self, id: usize) -> Atlas { self.atlases.get(&id).expect("Atlas not loaded!").clone() } + pub fn get_animation_expect(&self, id: usize) -> AnimationAtlas { + self.animation_atlases + .get(&id) + .expect("Atlas not loaded!") + .clone() + } + pub fn clear(&self) { - self.atlases.clear() + self.atlases.clear(); + self.animation_atlases.clear(); } } diff --git a/crates/graphics/src/primitives/cells/atlas.rs b/crates/graphics/src/primitives/cells/atlas.rs new file mode 100644 index 00000000..71c78d86 --- /dev/null +++ b/crates/graphics/src/primitives/cells/atlas.rs @@ -0,0 +1,197 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use color_eyre::eyre::WrapErr; +use image::EncodableLayout; +use itertools::Itertools; +use wgpu::util::DeviceExt; + +use crate::{GraphicsState, Quad, Texture}; + +pub const MAX_SIZE: u32 = 8192; // Max texture size in one dimension +pub const CELL_SIZE: u32 = 192; // Animation cells are 192x192 +pub const ANIMATION_COLUMNS: u32 = 5; // Animation sheets are 5 cells wide +pub const ANIMATION_WIDTH: u32 = CELL_SIZE * ANIMATION_COLUMNS; +pub const MAX_ROWS: u32 = MAX_SIZE / CELL_SIZE; // Max rows of cells that can fit before wrapping +pub const MAX_HEIGHT: u32 = MAX_ROWS * CELL_SIZE; +pub const MAX_CELLS: u32 = MAX_ROWS * ANIMATION_COLUMNS; + +use image::GenericImageView; +use std::sync::Arc; + +#[derive(Clone)] +pub struct Atlas { + pub atlas_texture: Arc, + pub animation_height: u32, +} + +impl Atlas { + pub fn new( + graphics_state: &GraphicsState, + filesystem: &impl luminol_filesystem::FileSystem, + animation: &luminol_data::rpg::Animation, + ) -> Atlas { + let animation_img = animation + .animation_name + .as_ref() + .and_then(|animation_name| { + let result = filesystem + .read(camino::Utf8Path::new("Graphics/Animations").join(animation_name)) + .and_then(|file| image::load_from_memory(&file).map_err(|e| e.into())) + .wrap_err_with(|| format!("Error loading atlas animation {animation_name:?}")); + // we don't actually need to unwrap this to a placeholder image because we fill in the atlas texture with the placeholder image. + match result { + Ok(img) => Some(img.into_rgba8()), + Err(e) => { + graphics_state.send_texture_error(e); + None + } + } + }); + + let animation_height = animation_img + .as_ref() + .map(|i| i.height() / CELL_SIZE * CELL_SIZE) + .unwrap_or(CELL_SIZE); + + let wrap_columns = animation_height.div_ceil(MAX_SIZE); + let width = wrap_columns * ANIMATION_WIDTH; + let height = animation_height.min(MAX_SIZE); + + let placeholder_img = graphics_state.texture_loader.placeholder_image(); + let atlas_texture = graphics_state.render_state.device.create_texture_with_data( + &graphics_state.render_state.queue, + &wgpu::TextureDescriptor { + label: Some("cells_atlas"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + dimension: wgpu::TextureDimension::D2, + mip_level_count: 1, + sample_count: 1, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + wgpu::util::TextureDataOrder::LayerMajor, + // we can avoid this collect_vec() by mapping a buffer and then copying that to a texture. it'd also allow us to copy everything easier too. do we want to do this? + &itertools::iproduct!(0..height, 0..width, 0..4) + .map(|(y, x, c)| { + // Tile the placeholder image to fill the atlas + placeholder_img.as_bytes()[(c + + (x % placeholder_img.width()) * 4 + + (y % placeholder_img.height()) * 4 * placeholder_img.width()) + as usize] + }) + .collect_vec(), + ); + + if let Some(animation_img) = animation_img { + for i in 0..wrap_columns { + let region_height = if i == wrap_columns - 1 { + animation_height - MAX_HEIGHT * i + } else { + MAX_HEIGHT + }; + write_texture_region( + &graphics_state.render_state, + &atlas_texture, + animation_img.view(0, MAX_HEIGHT * i, ANIMATION_WIDTH, region_height), + (ANIMATION_WIDTH * i, 0), + ); + } + } + + let atlas_texture = graphics_state + .texture_loader + .register_texture(format!("animation_atlases/{}", animation.id), atlas_texture); + + Atlas { + atlas_texture, + animation_height, + } + } + + pub fn calc_quad(&self, cell: i16) -> Quad { + let cell_u32 = if cell < 0 { 0 } else { cell as u32 }; + + let atlas_cell_position = egui::pos2( + ((cell_u32 % ANIMATION_COLUMNS + (cell_u32 / MAX_CELLS) * ANIMATION_COLUMNS) + * CELL_SIZE) as f32, + (cell_u32 / ANIMATION_COLUMNS % MAX_CELLS * CELL_SIZE) as f32, + ); + + Quad::new( + egui::Rect::from_min_size( + egui::pos2(0., 0.), + egui::vec2(CELL_SIZE as f32, CELL_SIZE as f32), + ), + // Reduced by 0.01 px on all sides to decrease texture bleeding + egui::Rect::from_min_size( + atlas_cell_position + egui::vec2(0.01, 0.01), + egui::vec2(CELL_SIZE as f32 - 0.02, CELL_SIZE as f32 - 0.02), + ), + ) + } +} + +fn write_texture_region

( + render_state: &luminol_egui_wgpu::RenderState, + texture: &wgpu::Texture, + image: image::SubImage<&image::ImageBuffer>>, + (dest_x, dest_y): (u32, u32), +) where + P: image::Pixel, + P::Subpixel: bytemuck::Pod, +{ + let (x, y) = image.offsets(); + let (width, height) = image.dimensions(); + let bytes = bytemuck::cast_slice(image.inner().as_raw()); + + let inner_width = image.inner().width(); + // let inner_width = subimage.width(); + let stride = inner_width * std::mem::size_of::

() as u32; + let offset = (y * inner_width + x) * std::mem::size_of::

() as u32; + + render_state.queue.write_texture( + wgpu::ImageCopyTexture { + texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: dest_x, + y: dest_y, + z: 0, + }, + aspect: wgpu::TextureAspect::All, + }, + bytes, + wgpu::ImageDataLayout { + offset: offset as wgpu::BufferAddress, + bytes_per_row: Some(stride), + rows_per_image: None, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); +} diff --git a/crates/graphics/src/primitives/cells/display.rs b/crates/graphics/src/primitives/cells/display.rs new file mode 100644 index 00000000..4faac58a --- /dev/null +++ b/crates/graphics/src/primitives/cells/display.rs @@ -0,0 +1,92 @@ +// Copyright (C) 2024 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use wgpu::util::DeviceExt; + +use crate::{BindGroupLayoutBuilder, GraphicsState}; + +#[derive(Debug)] +pub struct Display { + data: Data, + uniform: wgpu::Buffer, +} + +#[repr(C, align(16))] +#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Data { + cells_width: u32, + hue: f32, + _padding: u64, +} + +impl Display { + pub fn new(graphics_state: &GraphicsState, cells_width: u32) -> Self { + let data = Data { + cells_width, + hue: 0., + _padding: Default::default(), + }; + + let uniform = graphics_state.render_state.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("cells display buffer"), + contents: bytemuck::bytes_of(&data), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }, + ); + + Display { data, uniform } + } + + pub fn as_buffer(&self) -> &wgpu::Buffer { + &self.uniform + } + + pub fn set_hue(&mut self, render_state: &luminol_egui_wgpu::RenderState, hue: f32) { + if self.data.hue != hue { + self.data.hue = hue; + self.regen_buffer(render_state); + } + } + + fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { + render_state + .queue + .write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data)); + } +} + +pub fn add_to_bind_group_layout( + layout_builder: &mut BindGroupLayoutBuilder, +) -> &mut BindGroupLayoutBuilder { + layout_builder.append( + wgpu::ShaderStages::VERTEX_FRAGMENT, + wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + None, + ) +} diff --git a/crates/graphics/src/primitives/cells/instance.rs b/crates/graphics/src/primitives/cells/instance.rs new file mode 100644 index 00000000..763dd088 --- /dev/null +++ b/crates/graphics/src/primitives/cells/instance.rs @@ -0,0 +1,104 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use itertools::Itertools; +use wgpu::util::DeviceExt; + +#[derive(Debug)] +pub struct Instances { + instance_buffer: wgpu::Buffer, + + cells_width: usize, + cells_height: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +struct Instance { + cell_id: u32, +} + +impl Instances { + pub fn new( + render_state: &luminol_egui_wgpu::RenderState, + cells_data: &luminol_data::Table2, + ) -> Self { + let instances = Self::calculate_instances(cells_data); + let instance_buffer = + render_state + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("cells instance buffer"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + Self { + instance_buffer, + + cells_width: cells_data.xsize(), + cells_height: cells_data.ysize(), + } + } + + pub fn set_cell( + &self, + render_state: &luminol_egui_wgpu::RenderState, + cell_id: i16, + position: (usize, usize), + ) { + let offset = position.0 + (position.1 * self.cells_width); + let offset = offset * std::mem::size_of::(); + render_state.queue.write_buffer( + &self.instance_buffer, + offset as wgpu::BufferAddress, + bytemuck::bytes_of(&Instance { + cell_id: cell_id as u32, + }), + ) + } + + fn calculate_instances(cells_data: &luminol_data::Table2) -> Vec { + cells_data + .iter() + .copied() + .map(|cell_id| Instance { + cell_id: cell_id as u32, + }) + .collect_vec() + } + + pub fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + let count = (self.cells_width * self.cells_height) as u32; + + let start = 0 as wgpu::BufferAddress; + let end = (count as usize * std::mem::size_of::()) as wgpu::BufferAddress; + + render_pass.set_vertex_buffer(0, self.instance_buffer.slice(start..end)); + + render_pass.draw(0..6, 0..count); + } + + pub const fn desc() -> wgpu::VertexBufferLayout<'static> { + const ARRAY: &[wgpu::VertexAttribute] = &wgpu::vertex_attr_array![0 => Uint32]; + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: ARRAY, + } + } +} diff --git a/crates/graphics/src/primitives/cells/mod.rs b/crates/graphics/src/primitives/cells/mod.rs new file mode 100644 index 00000000..90e0f8ae --- /dev/null +++ b/crates/graphics/src/primitives/cells/mod.rs @@ -0,0 +1,147 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use std::sync::Arc; + +use crate::{ + BindGroupBuilder, BindGroupLayoutBuilder, Drawable, GraphicsState, Renderable, Transform, + Viewport, +}; + +pub use atlas::*; + +use display::Display; +use instance::Instances; + +mod atlas; +pub(crate) mod display; +mod instance; +pub(crate) mod shader; + +pub struct Cells { + pub display: Display, + pub transform: Transform, + + instances: Arc, + bind_group: Arc, +} + +impl Cells { + pub fn new( + graphics_state: &GraphicsState, + cells: &luminol_data::Table2, + // in order of use in bind group + atlas: &Atlas, + viewport: &Viewport, + transform: Transform, + ) -> Self { + let instances = Instances::new(&graphics_state.render_state, cells); + let display = Display::new(graphics_state, cells.xsize() as u32); + + let mut bind_group_builder = BindGroupBuilder::new(); + bind_group_builder + .append_texture_view(&atlas.atlas_texture.view) + .append_sampler(&graphics_state.nearest_sampler) + .append_buffer(viewport.as_buffer()) + .append_buffer(transform.as_buffer()) + .append_buffer(display.as_buffer()); + + let bind_group = bind_group_builder.build( + &graphics_state.render_state.device, + Some("cells bind group"), + &graphics_state.bind_group_layouts.cells, + ); + + Self { + display, + transform, + + instances: Arc::new(instances), + bind_group: Arc::new(bind_group), + } + } + + pub fn set_cell( + &self, + render_state: &luminol_egui_wgpu::RenderState, + cell_id: i16, + position: (usize, usize), + ) { + self.instances.set_cell(render_state, cell_id, position) + } +} + +pub struct Prepared { + bind_group: Arc, + instances: Arc, + graphics_state: Arc, +} + +impl Renderable for Cells { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &Arc) -> Self::Prepared { + let bind_group = Arc::clone(&self.bind_group); + let graphics_state = Arc::clone(graphics_state); + let instances = Arc::clone(&self.instances); + + Prepared { + bind_group, + instances, + graphics_state, + } + } +} + +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + render_pass.push_debug_group("cells renderer"); + render_pass.set_pipeline(&self.graphics_state.pipelines.cells); + + render_pass.set_bind_group(0, &self.bind_group, &[]); + + self.instances.draw(render_pass); + render_pass.pop_debug_group(); + } +} + +pub fn create_bind_group_layout( + render_state: &luminol_egui_wgpu::RenderState, +) -> wgpu::BindGroupLayout { + let mut builder = BindGroupLayoutBuilder::new(); + builder + .append( + wgpu::ShaderStages::VERTEX_FRAGMENT, + wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + None, + ) + .append( + wgpu::ShaderStages::FRAGMENT, + wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + None, + ); + + Viewport::add_to_bind_group_layout(&mut builder); + Transform::add_to_bind_group_layout(&mut builder); + display::add_to_bind_group_layout(&mut builder); + + builder.build(&render_state.device, Some("cells bind group layout")) +} diff --git a/crates/graphics/src/primitives/cells/shader.rs b/crates/graphics/src/primitives/cells/shader.rs new file mode 100644 index 00000000..cccb262d --- /dev/null +++ b/crates/graphics/src/primitives/cells/shader.rs @@ -0,0 +1,120 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use super::instance::Instances; +use crate::primitives::BindGroupLayouts; + +pub fn create_render_pipeline( + composer: &mut naga_oil::compose::Composer, + render_state: &luminol_egui_wgpu::RenderState, + bind_group_layouts: &BindGroupLayouts, +) -> Result { + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/translation.wgsl"), + file_path: "translation.wgsl", + ..Default::default() + })?; + + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/gamma.wgsl"), + file_path: "gamma.wgsl", + ..Default::default() + })?; + + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/hue.wgsl"), + file_path: "hue.wgsl", + ..Default::default() + })?; + + let module = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { + source: include_str!("../shaders/cells.wgsl"), + file_path: "cells.wgsl", + shader_type: naga_oil::compose::ShaderType::Wgsl, + shader_defs: std::collections::HashMap::from([ + ( + "MAX_SIZE".to_string(), + naga_oil::compose::ShaderDefValue::UInt(super::atlas::MAX_SIZE), + ), + ( + "CELL_SIZE".to_string(), + naga_oil::compose::ShaderDefValue::UInt(super::atlas::CELL_SIZE), + ), + ( + "ANIMATION_COLUMNS".to_string(), + naga_oil::compose::ShaderDefValue::UInt(super::atlas::ANIMATION_COLUMNS), + ), + ( + "ANIMATION_WIDTH".to_string(), + naga_oil::compose::ShaderDefValue::UInt(super::atlas::ANIMATION_WIDTH), + ), + ( + "MAX_ROWS".to_string(), + naga_oil::compose::ShaderDefValue::UInt(super::atlas::MAX_ROWS), + ), + ( + "MAX_HEIGHT".to_string(), + naga_oil::compose::ShaderDefValue::UInt(super::atlas::MAX_HEIGHT), + ), + ( + "MAX_CELLS".to_string(), + naga_oil::compose::ShaderDefValue::UInt(super::atlas::MAX_CELLS), + ), + ]), + additional_imports: &[], + })?; + + let shader_module = render_state + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Cells Shader Module"), + source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)), + }); + + let pipeline_layout = + render_state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Cells Render Pipeline Layout"), + bind_group_layouts: &[&bind_group_layouts.cells], + push_constant_ranges: &[], + }); + + Ok(render_state + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Cells Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: "vs_main", + buffers: &[Instances::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + ..render_state.target_format.into() + })], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + })) +} diff --git a/crates/graphics/src/primitives/mod.rs b/crates/graphics/src/primitives/mod.rs index dbcd28db..fe618cfc 100644 --- a/crates/graphics/src/primitives/mod.rs +++ b/crates/graphics/src/primitives/mod.rs @@ -22,6 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +pub mod cells; pub mod collision; pub mod grid; pub mod sprite; @@ -30,6 +31,7 @@ pub mod tiles; pub struct BindGroupLayouts { sprite: wgpu::BindGroupLayout, tiles: wgpu::BindGroupLayout, + cells: wgpu::BindGroupLayout, collision: wgpu::BindGroupLayout, grid: wgpu::BindGroupLayout, } @@ -37,6 +39,7 @@ pub struct BindGroupLayouts { pub struct Pipelines { sprites: std::collections::HashMap, tiles: wgpu::RenderPipeline, + cells: wgpu::RenderPipeline, collision: wgpu::RenderPipeline, grid: wgpu::RenderPipeline, } @@ -46,6 +49,7 @@ impl BindGroupLayouts { Self { sprite: sprite::create_bind_group_layout(render_state), tiles: tiles::create_bind_group_layout(render_state), + cells: cells::create_bind_group_layout(render_state), collision: collision::create_bind_group_layout(render_state), grid: grid::create_bind_group_layout(render_state), } @@ -82,6 +86,7 @@ impl Pipelines { render_state, bind_group_layouts, sprites: sprite::shader::create_sprite_shaders, tiles: tiles::shader::create_render_pipeline, + cells: cells::shader::create_render_pipeline, collision: collision::shader::create_render_pipeline, grid: grid::shader::create_render_pipeline } diff --git a/crates/graphics/src/primitives/shaders/cells.wgsl b/crates/graphics/src/primitives/shaders/cells.wgsl new file mode 100644 index 00000000..f11e6485 --- /dev/null +++ b/crates/graphics/src/primitives/shaders/cells.wgsl @@ -0,0 +1,98 @@ +#import luminol::gamma as Gamma +#import luminol::translation as Trans // 🏳️‍⚧️ +#import luminol::hue as Hue // 🏳️‍⚧️ + +struct InstanceInput { + @location(0) cell_id: u32, + @builtin(instance_index) index: u32 +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, +} + +@group(0) @binding(0) +var atlas: texture_2d; +@group(0) @binding(1) +var atlas_sampler: sampler; + +struct Display { + cells_width: u32, + hue: f32, + _padding: vec2, +} + +@group(0) @binding(2) +var viewport: Trans::Viewport; +@group(0) @binding(3) +var transform: Trans::Transform; +@group(0) @binding(4) +var display: Display; + +const VERTEX_POSITIONS = array( + vec2f(0.0, 0.0), + vec2f(192.0, 0.0), + vec2f(0.0, 192.0), + + vec2f(192.0, 0.0), + vec2f(0.0, 192.0), + vec2f(192.0, 192.0), +); +const TEX_COORDS = array( + // slightly smaller than 192x192 to reduce bleeding from adjacent pixels in the atlas + vec2f(0.01, 0.01), + vec2f(191.99, 0.01), + vec2f(0.01, 191.99), + + vec2f(191.99, 0.01), + vec2f(0.01, 191.99), + vec2f(191.99, 191.99), +); + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32, instance: InstanceInput) -> VertexOutput { + var out: VertexOutput; + + let tile_position = vec2( + f32(instance.index % display.cells_width), + f32(instance.index / display.cells_width) + ); + + var vertex_positions = VERTEX_POSITIONS; + let vertex_position = vertex_positions[vertex_index] + (tile_position * 192.0); + let normalized_pos = Trans::translate_vertex(vertex_position, viewport, transform); + + out.clip_position = vec4(normalized_pos, 0.0, 1.0); // we don't set the z because we have no z buffer + + let atlas_tile_position = vec2( + f32((instance.cell_id % #ANIMATION_COLUMNS + (instance.cell_id / #MAX_CELLS) * #ANIMATION_COLUMNS) * #CELL_SIZE), + f32(instance.cell_id / #ANIMATION_COLUMNS % #MAX_CELLS * #CELL_SIZE) + ); + + let tex_size = vec2(textureDimensions(atlas)); + var vertex_tex_coords = TEX_COORDS; + let vertex_tex_coord = vertex_tex_coords[vertex_index] / tex_size; + + out.tex_coords = vertex_tex_coord + (atlas_tile_position / tex_size); + + return out; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + var color = textureSample(atlas, atlas_sampler, input.tex_coords); + + if color.a <= 0.001 { + discard; + } + + if display.hue > 0.0 { + var hsv = Hue::rgb_to_hsv(color.rgb); + + hsv.x += display.hue; + color = vec4(Hue::hsv_to_rgb(hsv), color.a); + } + + return Gamma::from_linear_rgba(color); +} diff --git a/crates/graphics/src/primitives/tiles/atlas.rs b/crates/graphics/src/primitives/tiles/atlas.rs index ea4dcf45..7060a145 100644 --- a/crates/graphics/src/primitives/tiles/atlas.rs +++ b/crates/graphics/src/primitives/tiles/atlas.rs @@ -120,7 +120,7 @@ impl Atlas { let mut encoder = graphics_state.render_state.device.create_command_encoder( &wgpu::CommandEncoderDescriptor { - label: Some("atlas creation"), + label: Some("tilemap atlas creation"), }, ); diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index b9fd6bd1..b2c656c5 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -34,6 +34,9 @@ pub struct Window { previous_animation: Option, previous_timing_frame: Option, + frame: i32, + + frame_view: Option, collapsing_view: luminol_components::CollapsingView, timing_se_picker: SoundPicker, view: luminol_components::DatabaseView, @@ -45,6 +48,8 @@ impl Window { selected_animation_name: None, previous_animation: None, previous_timing_frame: None, + frame: 0, + frame_view: None, collapsing_view: luminol_components::CollapsingView::new(), timing_se_picker: SoundPicker::new( luminol_audio::Source::SE, @@ -227,6 +232,38 @@ impl Window { } response } + + fn show_frame_edit( + ui: &mut egui::Ui, + update_state: &mut luminol_core::UpdateState<'_>, + maybe_frame_view: &mut Option, + animation: &mut luminol_data::rpg::Animation, + frame: &mut i32, + ) -> bool { + let mut modified = false; + + let frame_view = if let Some(frame_view) = maybe_frame_view { + frame_view + } else { + *maybe_frame_view = Some( + luminol_components::AnimationFrameView::new( + update_state, + animation, + *frame as usize, + ) + .unwrap(), // TODO get rid of this unwrap + ); + maybe_frame_view.as_mut().unwrap() + }; + + ui.group(|ui| { + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + frame_view.ui(ui, update_state); + }); + }); + + modified + } } impl luminol_core::Window for Window { @@ -269,7 +306,7 @@ impl luminol_core::Window for Window { &mut animations.data, |animation| format!("{:0>4}: {}", animation.id + 1, animation.name), |ui, animations, id, update_state| { - let animation = &mut animations[id]; + let mut animation = &mut animations[id]; self.selected_animation_name = Some(animation.name.clone()); ui.with_padded_stripe(false, |ui| { @@ -283,6 +320,35 @@ impl luminol_core::Window for Window { }); ui.with_padded_stripe(true, |ui| { + if let Some(frame_view) = &mut self.frame_view { + if *update_state.modified_during_prev_frame + || self.previous_animation != Some(animation.id) + { + frame_view.frame.atlas = update_state + .graphics + .atlas_loader + .load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ) + .unwrap(); // TODO get rid of this unwrap + frame_view.frame.update_all_cells( + &update_state.graphics, + &animation.frames[self.frame as usize], + ); + } + } + modified |= Self::show_frame_edit( + ui, + update_state, + &mut self.frame_view, + &mut animation, + &mut self.frame, + ); + }); + + ui.with_padded_stripe(false, |ui| { modified |= ui .add(luminol_components::Field::new( "SE and Flash", From 229e263d1cdf74f35230574f4583f02322830b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 9 Jul 2024 11:20:46 -0400 Subject: [PATCH 007/109] Change the coordinate system for the animation frame view viewport (0, 0) is now the center of the animation frame instead of the top-left corner of it. Also, I added an offset to each of the cell sprites to shift them 96 texels to the left and 96 texels up in order to center them. --- crates/components/src/animation_frame_view.rs | 18 ++--------- crates/graphics/src/frame.rs | 31 +++++++++++++------ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 22752010..ff7d2505 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -15,7 +15,6 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; use luminol_graphics::Renderable; pub struct AnimationFrameView { @@ -131,19 +130,9 @@ impl AnimationFrameView { // its a *long* story let scale = self.scale / (ui.ctx().pixels_per_point() * 100.); - let canvas_pos = canvas_center + self.pan; - - let width2 = FRAME_WIDTH as f32 / 2.; - let height2 = FRAME_HEIGHT as f32 / 2.; - let frame_size2 = egui::Vec2::new(width2, height2); - let frame_rect = egui::Rect { - min: canvas_pos - frame_size2, - max: canvas_pos + frame_size2, - }; - // no idea why this math works (could probably be simplified) - let proj_center_x = width2 - (self.pan.x + clip_offset.x) / scale; - let proj_center_y = height2 - (self.pan.y + clip_offset.y) / scale; + let proj_center_x = -(self.pan.x + clip_offset.x) / scale; + let proj_center_y = -(self.pan.y + clip_offset.y) / scale; let proj_width2 = canvas_rect.width() / scale / 2.; let proj_height2 = canvas_rect.height() / scale / 2.; self.frame.viewport.set( @@ -153,9 +142,6 @@ impl AnimationFrameView { glam::Vec2::splat(scale), ); - ui.ctx() - .request_repaint_after(std::time::Duration::from_secs_f32(16. / 60.)); - let painter = luminol_graphics::Painter::new(self.frame.prepare(&update_state.graphics)); ui.painter() .add(luminol_egui_wgpu::Callback::new_paint_callback( diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 872e4455..26fbc862 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -15,13 +15,15 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use crate::primitives::cells::Atlas; -use crate::{Drawable, GraphicsState, Renderable, Sprite, Viewport}; -use luminol_data::OptionVec; +use crate::primitives::cells::{Atlas, CELL_SIZE}; +use crate::{Drawable, GraphicsState, Renderable, Sprite, Transform, Viewport}; +use luminol_data::{BlendMode, OptionVec}; pub const FRAME_WIDTH: usize = 640; pub const FRAME_HEIGHT: usize = 320; +const CELL_OFFSET: glam::Vec2 = glam::Vec2::splat(-(CELL_SIZE as f32) / 2.); + pub struct Frame { pub atlas: Atlas, pub sprites: OptionVec, @@ -37,7 +39,7 @@ impl Frame { ) -> Self { let viewport = Viewport::new( graphics_state, - glam::vec2(FRAME_WIDTH as f32 * 32., FRAME_HEIGHT as f32 * 32.), + glam::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32), ); let frame = &animation.frames[frame_index]; @@ -49,12 +51,15 @@ impl Frame { .map(|(i, cell_id)| { ( i, - Sprite::basic_hue_quad( + Sprite::new( graphics_state, - 0, atlas.calc_quad(cell_id), + 0, + 255, + BlendMode::Normal, &atlas.atlas_texture, &viewport, + Transform::new_position(graphics_state, CELL_OFFSET), ), ) }) @@ -77,12 +82,15 @@ impl Frame { let cell_id = frame.cell_data[(cell_index, 0)]; self.sprites.insert( cell_index, - Sprite::basic_hue_quad( + Sprite::new( graphics_state, - 0, self.atlas.calc_quad(cell_id), + 0, + 255, + BlendMode::Normal, &self.atlas.atlas_texture, &self.viewport, + Transform::new_position(graphics_state, CELL_OFFSET), ), ) } @@ -102,12 +110,15 @@ impl Frame { .map(|(i, cell_id)| { ( i, - Sprite::basic_hue_quad( + Sprite::new( graphics_state, - 0, self.atlas.calc_quad(cell_id), + 0, + 255, + BlendMode::Normal, &self.atlas.atlas_texture, &self.viewport, + Transform::new_position(graphics_state, CELL_OFFSET), ), ) }), From 1dd700dc98b3cd281ab9ce9444a840febfc77472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 9 Jul 2024 11:36:14 -0400 Subject: [PATCH 008/109] Fix `calc_quad` formula for animations --- crates/graphics/src/primitives/cells/atlas.rs | 2 +- crates/graphics/src/primitives/shaders/cells.wgsl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/graphics/src/primitives/cells/atlas.rs b/crates/graphics/src/primitives/cells/atlas.rs index 71c78d86..5cdac28c 100644 --- a/crates/graphics/src/primitives/cells/atlas.rs +++ b/crates/graphics/src/primitives/cells/atlas.rs @@ -136,7 +136,7 @@ impl Atlas { let atlas_cell_position = egui::pos2( ((cell_u32 % ANIMATION_COLUMNS + (cell_u32 / MAX_CELLS) * ANIMATION_COLUMNS) * CELL_SIZE) as f32, - (cell_u32 / ANIMATION_COLUMNS % MAX_CELLS * CELL_SIZE) as f32, + (cell_u32 / ANIMATION_COLUMNS % MAX_ROWS * CELL_SIZE) as f32, ); Quad::new( diff --git a/crates/graphics/src/primitives/shaders/cells.wgsl b/crates/graphics/src/primitives/shaders/cells.wgsl index f11e6485..40f27bb4 100644 --- a/crates/graphics/src/primitives/shaders/cells.wgsl +++ b/crates/graphics/src/primitives/shaders/cells.wgsl @@ -67,7 +67,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, instance: InstanceInput) -> let atlas_tile_position = vec2( f32((instance.cell_id % #ANIMATION_COLUMNS + (instance.cell_id / #MAX_CELLS) * #ANIMATION_COLUMNS) * #CELL_SIZE), - f32(instance.cell_id / #ANIMATION_COLUMNS % #MAX_CELLS * #CELL_SIZE) + f32(instance.cell_id / #ANIMATION_COLUMNS % #MAX_ROWS * #CELL_SIZE) ); let tex_size = vec2(textureDimensions(atlas)); From f5aceea30d4097f8f8638eefb5d2436ba4c09736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 9 Jul 2024 19:46:36 -0400 Subject: [PATCH 009/109] Implement animation cell translation, scale, rotation, opacity --- crates/graphics/src/frame.rs | 125 +++++++++--------- .../src/primitives/shaders/sprite.wgsl | 13 +- .../graphics/src/primitives/sprite/graphic.rs | 18 ++- crates/graphics/src/primitives/sprite/mod.rs | 28 +++- 4 files changed, 116 insertions(+), 68 deletions(-) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 26fbc862..b5af1200 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -42,34 +42,13 @@ impl Frame { glam::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32), ); - let frame = &animation.frames[frame_index]; - let sprites = frame.cell_data.as_slice() - [0..frame.cell_data.xsize().min(frame.cell_max as usize)] - .iter() - .copied() - .enumerate() - .map(|(i, cell_id)| { - ( - i, - Sprite::new( - graphics_state, - atlas.calc_quad(cell_id), - 0, - 255, - BlendMode::Normal, - &atlas.atlas_texture, - &viewport, - Transform::new_position(graphics_state, CELL_OFFSET), - ), - ) - }) - .collect(); - - Self { + let mut frame = Self { atlas, - sprites, + sprites: Default::default(), viewport, - } + }; + frame.update_all_cells(graphics_state, &animation.frames[frame_index]); + frame } /// Updates the sprite for one cell based on the given animation frame. @@ -79,20 +58,11 @@ impl Frame { frame: &luminol_data::rpg::animation::Frame, cell_index: usize, ) { - let cell_id = frame.cell_data[(cell_index, 0)]; - self.sprites.insert( - cell_index, - Sprite::new( - graphics_state, - self.atlas.calc_quad(cell_id), - 0, - 255, - BlendMode::Normal, - &self.atlas.atlas_texture, - &self.viewport, - Transform::new_position(graphics_state, CELL_OFFSET), - ), - ) + if let Some(sprite) = self.sprite_from_cell_data(graphics_state, frame, cell_index) { + self.sprites.insert(cell_index, sprite); + } else { + let _ = self.sprites.try_remove(cell_index); + } } /// Updates the sprite for every cell based on the given animation frame. @@ -101,28 +71,59 @@ impl Frame { graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, ) { - self.sprites.clear(); - self.sprites.extend( - frame.cell_data.as_slice()[0..frame.cell_data.xsize().min(frame.cell_max as usize)] - .iter() - .copied() - .enumerate() - .map(|(i, cell_id)| { - ( - i, - Sprite::new( - graphics_state, - self.atlas.calc_quad(cell_id), - 0, - 255, - BlendMode::Normal, - &self.atlas.atlas_texture, - &self.viewport, - Transform::new_position(graphics_state, CELL_OFFSET), - ), - ) - }), - ); + let mut sprites = std::mem::take(&mut self.sprites); + sprites.clear(); + sprites.extend((0..frame.cell_data.xsize()).filter_map(|i| { + self.sprite_from_cell_data(graphics_state, frame, i) + .map(|s| (i, s)) + })); + self.sprites = sprites; + } + + pub fn sprite_from_cell_data( + &self, + graphics_state: &GraphicsState, + frame: &luminol_data::rpg::animation::Frame, + cell_index: usize, + ) -> Option { + (cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0).then(|| { + let id = frame.cell_data[(cell_index, 0)]; + let offset_x = frame.cell_data[(cell_index, 1)] as f32; + let offset_y = frame.cell_data[(cell_index, 2)] as f32; + let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; + let rotation = -(frame.cell_data[(cell_index, 4)] as f32).to_radians(); + let flip = glam::vec2( + if frame.cell_data[(cell_index, 5)] == 1 { + -1. + } else { + 1. + }, + 1., + ); + let opacity = frame.cell_data[(cell_index, 6)] as i32; + let blend_mode = match frame.cell_data[(cell_index, 7)] { + 1 => BlendMode::Add, + 2 => BlendMode::Subtract, + _ => BlendMode::Normal, + }; + + Sprite::new_with_rotation( + graphics_state, + self.atlas.calc_quad(id), + 0, + opacity, + blend_mode, + &self.atlas.atlas_texture, + &self.viewport, + Transform::new( + graphics_state, + glam::Mat2::from_angle(rotation) + * (glam::vec2(offset_x, offset_y) + CELL_OFFSET * scale * flip), + scale * flip, + ), + rotation, + ) + }) } } diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl index b6d39624..45fd9d95 100644 --- a/crates/graphics/src/primitives/shaders/sprite.wgsl +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -17,7 +17,7 @@ struct Graphic { hue: f32, opacity: f32, opacity_multiplier: f32, - _padding: u32, + rotation: f32, // clockwise in radians } @group(0) @binding(0) @@ -39,7 +39,16 @@ fn vs_main( var out: VertexOutput; out.tex_coords = model.tex_coords; - out.clip_position = vec4(Trans::translate_vertex(model.position, viewport, transform), 0.0, 1.0); + var position_after_rotation: vec2; + if graphic.rotation == 0 { + position_after_rotation = model.position; + } else { + let c = cos(graphic.rotation); + let s = sin(graphic.rotation); + position_after_rotation = mat2x2(c, s, -s, c) * model.position; + } + + out.clip_position = vec4(Trans::translate_vertex(position_after_rotation, viewport, transform), 0.0, 1.0); return out; } diff --git a/crates/graphics/src/primitives/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs index aca6d955..33411bf7 100644 --- a/crates/graphics/src/primitives/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -31,18 +31,19 @@ struct Data { hue: f32, opacity: f32, opacity_multiplier: f32, - _padding: u32, + /// clockwise in radians + rotation: f32, } impl Graphic { - pub fn new(graphics_state: &GraphicsState, hue: i32, opacity: i32) -> Self { + pub fn new(graphics_state: &GraphicsState, hue: i32, opacity: i32, rotation: f32) -> Self { let hue = (hue % 360) as f32 / 360.0; let opacity = opacity as f32 / 255.; let data = Data { hue, opacity, opacity_multiplier: 1., - _padding: 0, + rotation, }; let uniform = graphics_state.render_state.device.create_buffer_init( @@ -97,6 +98,17 @@ impl Graphic { } } + pub fn rotation(&self) -> f32 { + self.data.rotation + } + + pub fn set_rotation(&mut self, render_state: &luminol_egui_wgpu::RenderState, rotation: f32) { + if self.data.rotation != rotation { + self.data.rotation = rotation; + self.regen_buffer(render_state); + } + } + pub fn as_buffer(&self) -> &wgpu::Buffer { &self.uniform } diff --git a/crates/graphics/src/primitives/sprite/mod.rs b/crates/graphics/src/primitives/sprite/mod.rs index 6300b4b0..79fb06df 100644 --- a/crates/graphics/src/primitives/sprite/mod.rs +++ b/crates/graphics/src/primitives/sprite/mod.rs @@ -47,10 +47,36 @@ impl Sprite { texture: &Texture, viewport: &Viewport, transform: Transform, + ) -> Self { + Self::new_with_rotation( + graphics_state, + quad, + hue, + opacity, + blend_mode, + texture, + viewport, + transform, + 0., + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_with_rotation( + graphics_state: &GraphicsState, + quad: Quad, + hue: i32, + opacity: i32, + blend_mode: luminol_data::BlendMode, + // arranged in order of use in bind group + texture: &Texture, + viewport: &Viewport, + transform: Transform, + rotation: f32, ) -> Self { let vertices = vertices::Vertices::from_quads(&graphics_state.render_state, &[quad], texture.size()); - let graphic = graphic::Graphic::new(graphics_state, hue, opacity); + let graphic = graphic::Graphic::new(graphics_state, hue, opacity, rotation); let mut bind_group_builder = BindGroupBuilder::new(); bind_group_builder From c0e64f856abda3d75890501ff167885a540947d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 9 Jul 2024 21:20:49 -0400 Subject: [PATCH 010/109] Fix animation cell translation formula --- crates/graphics/src/frame.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index b5af1200..705019dd 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -117,8 +117,8 @@ impl Frame { &self.viewport, Transform::new( graphics_state, - glam::Mat2::from_angle(rotation) - * (glam::vec2(offset_x, offset_y) + CELL_OFFSET * scale * flip), + glam::vec2(offset_x, offset_y) + + glam::Mat2::from_angle(rotation) * (scale * flip * CELL_OFFSET), scale * flip, ), rotation, From 6f59bc4fdb152e2d21d7e53c90162659a23fc8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 10 Jul 2024 13:13:52 -0400 Subject: [PATCH 011/109] Fix some animation frame view bugs * Fixed a problem where sprites rendered in the animation frame view can appear outside of the animation editor window when the scroll area is scrolled * Fixed a problem where scrolling to zoom the animation frame view also scrolls the scroll area * The animation frame view's height is now manually adjustable --- crates/components/src/animation_frame_view.rs | 3 ++- crates/ui/src/windows/animations.rs | 25 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index ff7d2505..15683ae6 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -70,10 +70,11 @@ impl AnimationFrameView { &mut self, ui: &mut egui::Ui, update_state: &luminol_core::UpdateState<'_>, + clip_rect: egui::Rect, ) -> egui::Response { let canvas_rect = ui.max_rect(); let canvas_center = canvas_rect.center(); - ui.set_clip_rect(canvas_rect); + ui.set_clip_rect(canvas_rect.intersect(clip_rect)); let mut response = ui.allocate_rect(canvas_rect, egui::Sense::click_and_drag()); diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index b2c656c5..d26d9011 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -236,6 +236,7 @@ impl Window { fn show_frame_edit( ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>, + clip_rect: egui::Rect, maybe_frame_view: &mut Option, animation: &mut luminol_data::rpg::Animation, frame: &mut i32, @@ -257,9 +258,24 @@ impl Window { }; ui.group(|ui| { - egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { - frame_view.ui(ui, update_state); - }); + egui::Resize::default() + .resizable([false, true]) + .min_width(ui.available_width()) + .max_width(ui.available_width()) + .show(ui, |ui| { + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let response = frame_view.ui(ui, update_state, clip_rect); + + // If the pointer is hovering over the frame view, prevent parent widgets + // from receiving scroll events so that scaling the frame view with the + // scroll wheel doesn't also scroll the scroll area that the frame view is + // in + if response.hovered() { + ui.ctx() + .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); + } + }); + }); }); modified @@ -309,6 +325,8 @@ impl luminol_core::Window for Window { let mut animation = &mut animations[id]; self.selected_animation_name = Some(animation.name.clone()); + let clip_rect = ui.clip_rect(); + ui.with_padded_stripe(false, |ui| { modified |= ui .add(luminol_components::Field::new( @@ -342,6 +360,7 @@ impl luminol_core::Window for Window { modified |= Self::show_frame_edit( ui, update_state, + clip_rect, &mut self.frame_view, &mut animation, &mut self.frame, From d5ff91c0f74ccbb7511a728241af98d5da1c7731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 10 Jul 2024 16:08:50 -0400 Subject: [PATCH 012/109] Draw borders on the edges of animation cells --- crates/components/src/animation_frame_view.rs | 9 ++++ crates/components/src/map_view.rs | 6 +-- crates/graphics/src/frame.rs | 41 +++++++++++-------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 15683ae6..4eb53ada 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -150,6 +150,15 @@ impl AnimationFrameView { painter, )); + // Draw a white rectangle on the border of every cell + for (_, (_, cell_rect)) in self.frame.sprites.iter() { + ui.painter().rect_stroke( + (*cell_rect * scale).translate(canvas_center.to_vec2() + self.pan), + 5., + egui::Stroke::new(1., egui::Color32::WHITE), + ); + } + ui.ctx().data_mut(|d| { d.insert_persisted(self.data_id, (self.pan, self.scale)); }); diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index 4f424f95..e72980e0 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -295,10 +295,10 @@ impl MapView { let width2 = map.width as f32 / 2.; let height2 = map.height as f32 / 2.; - let pos = egui::Vec2::new(width2 * tile_size, height2 * tile_size); + let map_size2 = egui::Vec2::new(width2 * tile_size, height2 * tile_size); let map_rect = egui::Rect { - min: canvas_pos - pos, - max: canvas_pos + pos, + min: canvas_pos - map_size2, + max: canvas_pos + map_size2, }; self.map.tiles.selected_layer = match self.selected_layer { diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 705019dd..9bec9eda 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -26,7 +26,7 @@ const CELL_OFFSET: glam::Vec2 = glam::Vec2::splat(-(CELL_SIZE as f32) / 2.); pub struct Frame { pub atlas: Atlas, - pub sprites: OptionVec, + pub sprites: OptionVec<(Sprite, egui::Rect)>, pub viewport: Viewport, } @@ -85,7 +85,7 @@ impl Frame { graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, cell_index: usize, - ) -> Option { + ) -> Option<(Sprite, egui::Rect)> { (cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0).then(|| { let id = frame.cell_data[(cell_index, 0)]; let offset_x = frame.cell_data[(cell_index, 1)] as f32; @@ -106,22 +106,29 @@ impl Frame { 2 => BlendMode::Subtract, _ => BlendMode::Normal, }; - - Sprite::new_with_rotation( - graphics_state, - self.atlas.calc_quad(id), - 0, - opacity, - blend_mode, - &self.atlas.atlas_texture, - &self.viewport, - Transform::new( + let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); + ( + Sprite::new_with_rotation( graphics_state, - glam::vec2(offset_x, offset_y) - + glam::Mat2::from_angle(rotation) * (scale * flip * CELL_OFFSET), - scale * flip, + self.atlas.calc_quad(id), + 0, + opacity, + blend_mode, + &self.atlas.atlas_texture, + &self.viewport, + Transform::new( + graphics_state, + glam::vec2(offset_x, offset_y) + + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) + * (scale * flip * CELL_OFFSET), + scale * flip, + ), + rotation, + ), + egui::Rect::from_center_size( + egui::pos2(offset_x, offset_y), + egui::Vec2::splat(CELL_SIZE as f32 * (cos.abs() + sin.abs()) * scale), ), - rotation, ) }) } @@ -139,7 +146,7 @@ impl Renderable for Frame { sprites: self .sprites .iter_mut() - .map(|(_, sprite)| sprite.prepare(graphics_state)) + .map(|(_, (sprite, _))| sprite.prepare(graphics_state)) .collect(), } } From bd6863ab301809e8ebc1ff207979f7dcbf63274c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 10 Jul 2024 18:23:52 -0400 Subject: [PATCH 013/109] Draw the border of the animation frame --- crates/components/src/animation_frame_view.rs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 4eb53ada..9013f60b 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -17,6 +17,8 @@ use luminol_graphics::Renderable; +use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; + pub struct AnimationFrameView { pub frame: luminol_graphics::Frame, @@ -150,10 +152,36 @@ impl AnimationFrameView { painter, )); + let offset = canvas_center.to_vec2() + self.pan; + + // Draw the grid lines and the border of the animation frame + ui.painter().line_segment( + [ + egui::pos2(-(FRAME_WIDTH as f32 / 2.), 0.) * scale + offset, + egui::pos2(FRAME_WIDTH as f32 / 2., 0.) * scale + offset, + ], + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + ui.painter().line_segment( + [ + egui::pos2(0., -(FRAME_HEIGHT as f32 / 2.)) * scale + offset, + egui::pos2(0., FRAME_HEIGHT as f32 / 2.) * scale + offset, + ], + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + ui.painter().rect_stroke( + egui::Rect::from_center_size( + offset.to_pos2(), + egui::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32) * scale, + ), + 5., + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + // Draw a white rectangle on the border of every cell for (_, (_, cell_rect)) in self.frame.sprites.iter() { ui.painter().rect_stroke( - (*cell_rect * scale).translate(canvas_center.to_vec2() + self.pan), + (*cell_rect * scale).translate(offset), 5., egui::Stroke::new(1., egui::Color32::WHITE), ); From 4ea2f77c0f3f1e41771e2a63b21316e44a5c210d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 10 Jul 2024 21:35:48 -0400 Subject: [PATCH 014/109] Fix panic when animation texture width < 960 pixels --- crates/graphics/src/primitives/cells/atlas.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/graphics/src/primitives/cells/atlas.rs b/crates/graphics/src/primitives/cells/atlas.rs index 5cdac28c..9663604a 100644 --- a/crates/graphics/src/primitives/cells/atlas.rs +++ b/crates/graphics/src/primitives/cells/atlas.rs @@ -106,6 +106,7 @@ impl Atlas { if let Some(animation_img) = animation_img { for i in 0..wrap_columns { + let region_width = ANIMATION_WIDTH.min(animation_img.width()); let region_height = if i == wrap_columns - 1 { animation_height - MAX_HEIGHT * i } else { @@ -114,7 +115,7 @@ impl Atlas { write_texture_region( &graphics_state.render_state, &atlas_texture, - animation_img.view(0, MAX_HEIGHT * i, ANIMATION_WIDTH, region_height), + animation_img.view(0, MAX_HEIGHT * i, region_width, region_height), (ANIMATION_WIDTH * i, 0), ); } From f60c1448e1167c68cbb2fd54243e30376447ce02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 13 Jul 2024 17:51:23 -0400 Subject: [PATCH 015/109] Fix animation cell with rotation and flip displaying incorrectly If a cell needs to be rotated and flipped, the flip is supposed to be applied first and then the rotation is to be applied afterwards. The sprite shader currently applies the rotation first and then flip, so I fixed this by reversing the direction of rotation passed to the shader if flip is needed. It can be shown that in two dimensions, a flip that goes through the origin followed by a rotation about the origin is the same thing as doing the rotation first and then the flip if you reverse the direction of rotation. --- crates/graphics/src/frame.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 453b3a8c..26d5c38f 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -92,21 +92,18 @@ impl Frame { let offset_y = frame.cell_data[(cell_index, 2)] as f32; let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; let rotation = -(frame.cell_data[(cell_index, 4)] as f32).to_radians(); - let flip = glam::vec2( - if frame.cell_data[(cell_index, 5)] == 1 { - -1. - } else { - 1. - }, - 1., - ); + let flip = frame.cell_data[(cell_index, 5)] == 1; let opacity = frame.cell_data[(cell_index, 6)] as i32; let blend_mode = match frame.cell_data[(cell_index, 7)] { 1 => BlendMode::Add, 2 => BlendMode::Subtract, _ => BlendMode::Normal, }; + + let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); + let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); + ( Sprite::new_with_rotation( graphics_state, @@ -120,10 +117,10 @@ impl Frame { graphics_state, glam::vec2(offset_x, offset_y) + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) - * (scale * flip * CELL_OFFSET), - scale * flip, + * (scale * flip_vec * CELL_OFFSET), + scale * flip_vec, ), - rotation, + if flip { -rotation } else { rotation }, ), egui::Rect::from_center_size( egui::pos2(offset_x, offset_y), From d68542d07e501723b8a75d58a5239745346c786f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 14 Jul 2024 19:00:05 -0400 Subject: [PATCH 016/109] Implement dragging animation cells to move them --- crates/components/src/animation_frame_view.rs | 94 ++++++++++++++++++- crates/data/src/option_vec.rs | 32 +++++++ crates/graphics/src/data/transform.rs | 25 ++++- crates/graphics/src/frame.rs | 49 +++++++++- .../graphics/src/primitives/sprite/graphic.rs | 24 ++++- crates/ui/src/windows/animations.rs | 55 ++++++----- 6 files changed, 249 insertions(+), 30 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 7247b5da..d483a172 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -22,6 +22,11 @@ use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; pub struct AnimationFrameView { pub frame: luminol_graphics::Frame, + pub selected_cell_index: Option, + pub hovered_cell_index: Option, + pub hovered_cell_drag_pos: Option<(i16, i16)>, + pub hovered_cell_drag_offset: Option, + pub pan: egui::Vec2, pub scale: f32, @@ -61,6 +66,10 @@ impl AnimationFrameView { Ok(Self { frame, + selected_cell_index: None, + hovered_cell_index: None, + hovered_cell_drag_pos: None, + hovered_cell_drag_offset: None, pan, scale, previous_scale: scale, @@ -178,12 +187,91 @@ impl AnimationFrameView { egui::Stroke::new(1., egui::Color32::DARK_GRAY), ); + // Find the cell that the cursor is hovering over; if multiple cells are hovered we + // prioritize the one with the greatest index + let cell_rect_iter = self + .frame + .sprites + .iter() + .map(|(i, (_, cell_rect))| (i, (*cell_rect * scale).translate(offset))); + if ui.input(|i| i.pointer.primary_clicked()) { + self.selected_cell_index = None; + } + if self.hovered_cell_drag_offset.is_none() { + self.hovered_cell_index = ui + .input(|i| !i.modifiers.shift) + .then(|| { + cell_rect_iter.clone().rev().find_map(|(i, cell_rect)| { + (response.hovered() && ui.rect_contains_pointer(cell_rect)).then(|| { + if ui.input(|i| i.pointer.primary_clicked()) { + // If the hovered cell was clicked, make it the selected cell + self.selected_cell_index = Some(i); + } + i + }) + }) + }) + .flatten(); + } + + if !response.is_pointer_button_down_on() + || ui.input(|i| { + !i.pointer.button_down(egui::PointerButton::Primary) || i.modifiers.shift + }) + { + self.hovered_cell_drag_offset = None; + } else if let (Some(i), None, true) = ( + self.hovered_cell_index, + self.hovered_cell_drag_offset, + response.drag_started_by(egui::PointerButton::Primary), + ) { + let (_, cell_rect) = self.frame.sprites[i]; + self.hovered_cell_drag_offset = + Some(cell_rect.center() - (response.hover_pos().unwrap() - offset) / scale); + } + + if let Some(drag_offset) = self.hovered_cell_drag_offset { + let pos = (response.hover_pos().unwrap() - offset) / scale + drag_offset; + self.hovered_cell_drag_pos = Some(( + pos.x.round_ties_even() as i16, + pos.y.round_ties_even() as i16, + )); + } else { + self.hovered_cell_drag_pos = None; + } + // Draw a white rectangle on the border of every cell - for (_, (_, cell_rect)) in self.frame.sprites.iter() { + for (_, cell_rect) in cell_rect_iter { + ui.painter().rect_stroke( + cell_rect, + 5., + egui::Stroke::new( + 1., + if ui.input(|i| i.modifiers.shift) { + egui::Color32::DARK_GRAY + } else { + egui::Color32::WHITE + }, + ), + ); + } + + // Draw a yellow rectangle on the border of the hovered cell + if let Some(i) = self.hovered_cell_index { + let (_, cell_rect) = self.frame.sprites[i]; + let cell_rect = (cell_rect * scale).translate(offset); + ui.painter() + .rect_stroke(cell_rect, 5., egui::Stroke::new(3., egui::Color32::YELLOW)); + } + + // Draw a magenta rectangle on the border of the selected cell + if let Some(i) = self.selected_cell_index { + let (_, cell_rect) = self.frame.sprites[i]; + let cell_rect = (cell_rect * scale).translate(offset); ui.painter().rect_stroke( - (*cell_rect * scale).translate(offset), + cell_rect, 5., - egui::Stroke::new(1., egui::Color32::WHITE), + egui::Stroke::new(3., egui::Color32::from_rgb(255, 0, 255)), ); } diff --git a/crates/data/src/option_vec.rs b/crates/data/src/option_vec.rs index 08eb5055..6e16ef12 100644 --- a/crates/data/src/option_vec.rs +++ b/crates/data/src/option_vec.rs @@ -27,10 +27,12 @@ pub struct OptionVec { num_values: usize, } +#[derive(Debug)] pub struct Iter<'a, T> { vec_iter: std::iter::Enumerate>>, } +#[derive(Debug)] pub struct IterMut<'a, T> { vec_iter: std::iter::Enumerate>>, } @@ -220,6 +222,17 @@ impl<'a, T> Iterator for Iter<'a, T> { } } +impl DoubleEndedIterator for Iter<'_, T> { + fn next_back(&mut self) -> Option { + while let Some((index, element)) = self.vec_iter.next_back() { + if let Some(element) = element { + return Some((index, element)); + } + } + None + } +} + impl<'a, T> Iterator for IterMut<'a, T> { type Item = (usize, &'a mut T); fn next(&mut self) -> Option { @@ -232,6 +245,25 @@ impl<'a, T> Iterator for IterMut<'a, T> { } } +impl DoubleEndedIterator for IterMut<'_, T> { + fn next_back(&mut self) -> Option { + while let Some((index, element)) = self.vec_iter.next_back() { + if let Some(element) = element { + return Some((index, element)); + } + } + None + } +} + +impl Clone for Iter<'_, T> { + fn clone(&self) -> Self { + Self { + vec_iter: self.vec_iter.clone(), + } + } +} + impl<'de, T> serde::de::Visitor<'de> for Visitor where T: serde::Deserialize<'de>, diff --git a/crates/graphics/src/data/transform.rs b/crates/graphics/src/data/transform.rs index 4805a357..990a698d 100644 --- a/crates/graphics/src/data/transform.rs +++ b/crates/graphics/src/data/transform.rs @@ -68,13 +68,30 @@ impl Transform { render_state: &luminol_egui_wgpu::RenderState, position: glam::Vec2, ) { - self.data.position = position; - self.regen_buffer(render_state); + if position != self.data.position { + self.data.position = position; + self.regen_buffer(render_state); + } } pub fn set_scale(&mut self, render_state: &luminol_egui_wgpu::RenderState, scale: glam::Vec2) { - self.data.scale = scale; - self.regen_buffer(render_state); + if scale != self.data.scale { + self.data.scale = scale; + self.regen_buffer(render_state); + } + } + + pub fn set( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + position: glam::Vec2, + scale: glam::Vec2, + ) { + let data = Data { position, scale }; + if data != self.data { + self.data = data; + self.regen_buffer(render_state); + } } fn regen_buffer(&mut self, render_state: &luminol_egui_wgpu::RenderState) { diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 26d5c38f..3a87be30 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -18,6 +18,7 @@ use crate::primitives::cells::{Atlas, CELL_SIZE}; use crate::{Drawable, GraphicsState, Renderable, Sprite, Transform, Viewport}; use luminol_data::{BlendMode, OptionVec}; +use luminol_egui_wgpu::RenderState; pub const FRAME_WIDTH: usize = 640; pub const FRAME_HEIGHT: usize = 320; @@ -101,7 +102,6 @@ impl Frame { }; let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); - let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); ( @@ -129,6 +129,53 @@ impl Frame { ) }) } + + pub fn update_cell_sprite( + &mut self, + render_state: &RenderState, + frame: &luminol_data::rpg::animation::Frame, + cell_index: usize, + ) { + if let Some((sprite, cell_rect)) = self.sprites.get_mut(cell_index) { + let offset_x = frame.cell_data[(cell_index, 1)] as f32; + let offset_y = frame.cell_data[(cell_index, 2)] as f32; + let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; + let rotation = -(frame.cell_data[(cell_index, 4)] as f32).to_radians(); + let flip = frame.cell_data[(cell_index, 5)] == 1; + let opacity = frame.cell_data[(cell_index, 6)] as i32; + let blend_mode = match frame.cell_data[(cell_index, 7)] { + 1 => BlendMode::Add, + 2 => BlendMode::Subtract, + _ => BlendMode::Normal, + }; + + let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); + let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); + + sprite.transform.set( + render_state, + glam::vec2(offset_x, offset_y) + + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) + * (scale * flip_vec * CELL_OFFSET), + scale * flip_vec, + ); + + sprite.graphic.set( + render_state, + 0, + opacity, + 1., + if flip { -rotation } else { rotation }, + ); + + sprite.blend_mode = blend_mode; + + *cell_rect = egui::Rect::from_center_size( + egui::pos2(offset_x, offset_y), + egui::Vec2::splat(CELL_SIZE as f32 * (cos.abs() + sin.abs()) * scale), + ); + } + } } pub struct Prepared { diff --git a/crates/graphics/src/primitives/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs index 21fab10d..5e684f65 100644 --- a/crates/graphics/src/primitives/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -26,7 +26,7 @@ pub struct Graphic { } #[repr(C)] -#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] struct Data { hue: f32, opacity: f32, @@ -109,6 +109,28 @@ impl Graphic { } } + pub fn set( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + hue: i32, + opacity: i32, + opacity_multiplier: f32, + rotation: f32, + ) { + let hue = (hue % 360) as f32 / 360.0; + let opacity = opacity as f32 / 255.0; + let data = Data { + hue, + opacity, + opacity_multiplier, + rotation, + }; + if data != self.data { + self.data = data; + self.regen_buffer(render_state); + } + } + pub fn as_buffer(&self) -> &wgpu::Buffer { &self.uniform } diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 7e50ad8d..0012622f 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -239,7 +239,7 @@ impl Window { clip_rect: egui::Rect, maybe_frame_view: &mut Option, animation: &mut luminol_data::rpg::Animation, - frame: &mut i32, + frame_index: &mut i32, ) -> bool { let mut modified = false; @@ -250,33 +250,46 @@ impl Window { luminol_components::AnimationFrameView::new( update_state, animation, - *frame as usize, + *frame_index as usize, ) .unwrap(), // TODO get rid of this unwrap ); maybe_frame_view.as_mut().unwrap() }; - ui.group(|ui| { - egui::Resize::default() - .resizable([false, true]) - .min_width(ui.available_width()) - .max_width(ui.available_width()) - .show(ui, |ui| { - egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { - let response = frame_view.ui(ui, update_state, clip_rect); - - // If the pointer is hovering over the frame view, prevent parent widgets - // from receiving scroll events so that scaling the frame view with the - // scroll wheel doesn't also scroll the scroll area that the frame view is - // in - if response.hovered() { - ui.ctx() - .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); - } - }); + let frame = &mut animation.frames[*frame_index as usize]; + + if let (Some(i), Some(drag_pos)) = ( + frame_view.hovered_cell_index, + frame_view.hovered_cell_drag_pos, + ) { + if (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) != drag_pos { + (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) = drag_pos; + frame_view + .frame + .update_cell_sprite(&update_state.graphics.render_state, frame, i); + modified = true; + } + } + + egui::Resize::default() + .resizable([false, true]) + .min_width(ui.available_width()) + .max_width(ui.available_width()) + .show(ui, |ui| { + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let response = frame_view.ui(ui, update_state, clip_rect); + + // If the pointer is hovering over the frame view, prevent parent widgets + // from receiving scroll events so that scaling the frame view with the + // scroll wheel doesn't also scroll the scroll area that the frame view is + // in + if response.hovered() { + ui.ctx() + .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); + } }); - }); + }); modified } From ce82fcb5ff8c99be6c47bbcb7be6468f1103202c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 14 Jul 2024 23:30:07 -0400 Subject: [PATCH 017/109] Clamp animation cell positions to frame size --- crates/components/src/animation_frame_view.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index d483a172..c62cb6c4 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -233,8 +233,12 @@ impl AnimationFrameView { if let Some(drag_offset) = self.hovered_cell_drag_offset { let pos = (response.hover_pos().unwrap() - offset) / scale + drag_offset; self.hovered_cell_drag_pos = Some(( - pos.x.round_ties_even() as i16, - pos.y.round_ties_even() as i16, + pos.x + .clamp(-(FRAME_WIDTH as f32 / 2.), FRAME_WIDTH as f32 / 2.) + .round_ties_even() as i16, + pos.y + .clamp(-(FRAME_HEIGHT as f32 / 2.), FRAME_HEIGHT as f32 / 2.) + .round_ties_even() as i16, )); } else { self.hovered_cell_drag_pos = None; From ff167b11f099b787b0d175991b680e7a31a50ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 15 Jul 2024 17:39:23 -0400 Subject: [PATCH 018/109] Implement animation cell property editing --- crates/components/src/animation_frame_view.rs | 4 +- crates/ui/src/windows/animations.rs | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index c62cb6c4..f5e43b79 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -194,7 +194,7 @@ impl AnimationFrameView { .sprites .iter() .map(|(i, (_, cell_rect))| (i, (*cell_rect * scale).translate(offset))); - if ui.input(|i| i.pointer.primary_clicked()) { + if response.clicked() { self.selected_cell_index = None; } if self.hovered_cell_drag_offset.is_none() { @@ -203,7 +203,7 @@ impl AnimationFrameView { .then(|| { cell_rect_iter.clone().rev().find_map(|(i, cell_rect)| { (response.hovered() && ui.rect_contains_pointer(cell_rect)).then(|| { - if ui.input(|i| i.pointer.primary_clicked()) { + if response.clicked() { // If the hovered cell was clicked, make it the selected cell self.selected_cell_index = Some(i); } diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 0012622f..6120c2a1 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -26,6 +26,9 @@ use egui::Widget; use luminol_components::UiExt; use luminol_core::Modal; +use luminol_data::BlendMode; +use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; +use luminol_graphics::primitives::cells::{ANIMATION_COLUMNS, CELL_SIZE}; use luminol_modals::sound_picker::Modal as SoundPicker; /// Database - Animations management window. @@ -291,6 +294,114 @@ impl Window { }); }); + if let Some(i) = frame_view.selected_cell_index { + let mut properties_modified = false; + + ui.label(format!("Cell {}", i + 1)); + + ui.columns(4, |columns| { + let mut pattern = frame.cell_data[(i, 0)] + 1; + let changed = columns[0] + .add(luminol_components::Field::new( + "Pattern", + egui::DragValue::new(&mut pattern).clamp_range( + 1..=(frame_view.frame.atlas.animation_height / CELL_SIZE + * ANIMATION_COLUMNS) as i16, + ), + )) + .changed(); + if changed { + frame.cell_data[(i, 0)] = pattern - 1; + properties_modified = true; + } + + properties_modified |= columns[1] + .add(luminol_components::Field::new( + "X", + egui::DragValue::new(&mut frame.cell_data[(i, 1)]) + .clamp_range(-(FRAME_WIDTH as i16 / 2)..=FRAME_WIDTH as i16 / 2), + )) + .changed(); + + properties_modified |= columns[2] + .add(luminol_components::Field::new( + "Y", + egui::DragValue::new(&mut frame.cell_data[(i, 2)]) + .clamp_range(-(FRAME_HEIGHT as i16 / 2)..=FRAME_HEIGHT as i16 / 2), + )) + .changed(); + + properties_modified |= columns[3] + .add(luminol_components::Field::new( + "Scale", + egui::DragValue::new(&mut frame.cell_data[(i, 3)]) + .clamp_range(1..=i16::MAX) + .suffix("%"), + )) + .changed(); + }); + + ui.columns(4, |columns| { + properties_modified |= columns[0] + .add(luminol_components::Field::new( + "Rotation", + egui::DragValue::new(&mut frame.cell_data[(i, 4)]) + .clamp_range(0..=360) + .suffix("°"), + )) + .changed(); + + let mut flip = frame.cell_data[(i, 5)] == 1; + let changed = columns[1] + .add(luminol_components::Field::new( + "Flip", + egui::Checkbox::without_text(&mut flip), + )) + .changed(); + if changed { + frame.cell_data[(i, 5)] = if flip { 1 } else { 0 }; + properties_modified = true; + } + + properties_modified |= columns[2] + .add(luminol_components::Field::new( + "Opacity", + egui::DragValue::new(&mut frame.cell_data[(i, 6)]).clamp_range(0..=255), + )) + .changed(); + + let mut blend_mode = match frame.cell_data[(i, 7)] { + 1 => BlendMode::Add, + 2 => BlendMode::Subtract, + _ => BlendMode::Normal, + }; + let changed = columns[3] + .add(luminol_components::Field::new( + "Blending", + luminol_components::EnumComboBox::new( + (animation.id, *frame_index, i, 7usize), + &mut blend_mode, + ), + )) + .changed(); + if changed { + frame.cell_data[(i, 7)] = match blend_mode { + BlendMode::Normal => 0, + BlendMode::Add => 1, + BlendMode::Subtract => 2, + }; + properties_modified = true; + } + }); + + if properties_modified { + frame_view + .frame + .update_cell_sprite(&update_state.graphics.render_state, frame, i); + modified = true; + } + } + modified } } From f1588347c1cb408ec75dd81293e75788e6f16d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 15 Jul 2024 18:20:42 -0400 Subject: [PATCH 019/109] Don't call `update_all_cells` when data cache is modified --- crates/ui/src/windows/animations.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 6120c2a1..f385bd46 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -463,9 +463,7 @@ impl luminol_core::Window for Window { ui.with_padded_stripe(true, |ui| { if let Some(frame_view) = &mut self.frame_view { - if *update_state.modified_during_prev_frame - || self.previous_animation != Some(animation.id) - { + if self.previous_animation != Some(animation.id) { frame_view.frame.atlas = update_state .graphics .atlas_loader From 4341f3cf5325015cd48fae6492141cdd661b4a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 15 Jul 2024 22:28:37 -0400 Subject: [PATCH 020/109] Fix cell sprites being desynced from their border rectangles --- crates/ui/src/windows/animations.rs | 45 +++++++++++++++++------------ 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index f385bd46..a001c43c 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -246,6 +246,19 @@ impl Window { ) -> bool { let mut modified = false; + let canvas_rect = egui::Resize::default() + .resizable([false, true]) + .min_width(ui.available_width()) + .max_width(ui.available_width()) + .show(ui, |ui| { + egui::Frame::dark_canvas(ui.style()) + .show(ui, |ui| { + let (_, rect) = ui.allocate_space(ui.available_size()); + rect + }) + .inner + }); + let frame_view = if let Some(frame_view) = maybe_frame_view { frame_view } else { @@ -275,25 +288,6 @@ impl Window { } } - egui::Resize::default() - .resizable([false, true]) - .min_width(ui.available_width()) - .max_width(ui.available_width()) - .show(ui, |ui| { - egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { - let response = frame_view.ui(ui, update_state, clip_rect); - - // If the pointer is hovering over the frame view, prevent parent widgets - // from receiving scroll events so that scaling the frame view with the - // scroll wheel doesn't also scroll the scroll area that the frame view is - // in - if response.hovered() { - ui.ctx() - .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); - } - }); - }); - if let Some(i) = frame_view.selected_cell_index { let mut properties_modified = false; @@ -402,6 +396,19 @@ impl Window { } } + ui.allocate_ui_at_rect(canvas_rect, |ui| { + let response = frame_view.ui(ui, update_state, clip_rect); + + // If the pointer is hovering over the frame view, prevent parent widgets + // from receiving scroll events so that scaling the frame view with the + // scroll wheel doesn't also scroll the scroll area that the frame view is + // in + if response.hovered() { + ui.ctx() + .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); + } + }); + modified } } From c4de40f39c8afed9e2a49df68e9f97d0d9456e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 16 Jul 2024 10:06:39 -0400 Subject: [PATCH 021/109] Keep animation cell sprite in sync with the selected pattern Changing the "Pattern" field of an animation cell will now actually update the sprite. --- crates/graphics/src/frame.rs | 7 +++++++ crates/graphics/src/primitives/sprite/mod.rs | 14 ++++++++++++++ crates/graphics/src/primitives/sprite/vertices.rs | 12 ++++++++++++ 3 files changed, 33 insertions(+) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 3a87be30..ed48fb30 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -137,6 +137,7 @@ impl Frame { cell_index: usize, ) { if let Some((sprite, cell_rect)) = self.sprites.get_mut(cell_index) { + let id = frame.cell_data[(cell_index, 0)]; let offset_x = frame.cell_data[(cell_index, 1)] as f32; let offset_y = frame.cell_data[(cell_index, 2)] as f32; let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; @@ -168,6 +169,12 @@ impl Frame { if flip { -rotation } else { rotation }, ); + sprite.set_quad( + render_state, + self.atlas.calc_quad(id), + self.atlas.atlas_texture.size(), + ); + sprite.blend_mode = blend_mode; *cell_rect = egui::Rect::from_center_size( diff --git a/crates/graphics/src/primitives/sprite/mod.rs b/crates/graphics/src/primitives/sprite/mod.rs index a670a804..9a018421 100644 --- a/crates/graphics/src/primitives/sprite/mod.rs +++ b/crates/graphics/src/primitives/sprite/mod.rs @@ -29,6 +29,7 @@ pub struct Sprite { pub graphic: graphic::Graphic, pub transform: Transform, pub blend_mode: luminol_data::BlendMode, + pub quad: Quad, // stored in an Arc so we can use it in rendering vertices: Arc, @@ -96,6 +97,7 @@ impl Sprite { graphic, blend_mode, transform, + quad, vertices: Arc::new(vertices), bind_group: Arc::new(bind_group), @@ -137,6 +139,18 @@ impl Sprite { pub fn basic(graphics_state: &GraphicsState, texture: &Texture, viewport: &Viewport) -> Self { Self::basic_hue(graphics_state, 0, texture, viewport) } + + pub fn set_quad( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + quad: Quad, + extents: wgpu::Extent3d, + ) { + if quad != self.quad { + self.quad = quad; + self.vertices.set(render_state, &[quad], extents); + } + } } pub struct Prepared { diff --git a/crates/graphics/src/primitives/sprite/vertices.rs b/crates/graphics/src/primitives/sprite/vertices.rs index 6a709491..55169e07 100644 --- a/crates/graphics/src/primitives/sprite/vertices.rs +++ b/crates/graphics/src/primitives/sprite/vertices.rs @@ -32,6 +32,18 @@ impl Vertices { Self { vertex_buffer } } + pub fn set( + &self, + render_state: &luminol_egui_wgpu::RenderState, + quads: &[Quad], + extents: wgpu::Extent3d, + ) { + let vertices = Quad::into_vertices(quads, extents); + render_state + .queue + .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices)); + } + pub fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); render_pass.draw(0..6, 0..1) From f84161cdb7bceeb9120db5402e4edbdbe0774c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 16 Jul 2024 10:20:05 -0400 Subject: [PATCH 022/109] Simplify transform formula for map view and frame view --- crates/components/src/animation_frame_view.rs | 10 ++++------ crates/components/src/map_view.rs | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index f5e43b79..ca7dccb9 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -142,15 +142,13 @@ impl AnimationFrameView { // its a *long* story let scale = self.scale / (ui.ctx().pixels_per_point() * 100.); - // no idea why this math works (could probably be simplified) - let proj_center_x = -(self.pan.x + clip_offset.x) / scale; - let proj_center_y = -(self.pan.y + clip_offset.y) / scale; - let proj_width2 = canvas_rect.width() / scale / 2.; - let proj_height2 = canvas_rect.height() / scale / 2.; self.frame.viewport.set( &update_state.graphics.render_state, glam::vec2(canvas_rect.width(), canvas_rect.height()), - glam::vec2(proj_width2 - proj_center_x, proj_height2 - proj_center_y) * scale, + glam::vec2( + canvas_rect.width() / 2. + self.pan.x + clip_offset.x, + canvas_rect.height() / 2. + self.pan.y + clip_offset.y, + ), glam::Vec2::splat(scale), ); diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index f00dfbbd..56f78381 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -311,15 +311,13 @@ impl MapView { SelectedLayer::Tiles(_) => None, }; - // no idea why this math works (could probably be simplified) - let proj_center_x = width2 * 32. - (self.pan.x + clip_offset.x) / scale; - let proj_center_y = height2 * 32. - (self.pan.y + clip_offset.y) / scale; - let proj_width2 = canvas_rect.width() / scale / 2.; - let proj_height2 = canvas_rect.height() / scale / 2.; self.map.viewport.set( &update_state.graphics.render_state, glam::vec2(canvas_rect.width(), canvas_rect.height()), - glam::vec2(proj_width2 - proj_center_x, proj_height2 - proj_center_y) * scale, + glam::vec2( + canvas_rect.width() / 2. + self.pan.x + clip_offset.x - width2 * 32. * scale, + canvas_rect.height() / 2. + self.pan.y + clip_offset.y - height2 * 32. * scale, + ), glam::Vec2::splat(scale), ); From c43e0d34e27b7c15483571e633b6bfe68192471e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 16 Jul 2024 15:35:22 -0400 Subject: [PATCH 023/109] Add animation editor scale slider and frame selection --- crates/graphics/src/frame.rs | 138 +++++++++++++++++----------- crates/ui/src/windows/animations.rs | 84 ++++++++++++----- src/app/top_bar.rs | 2 +- 3 files changed, 143 insertions(+), 81 deletions(-) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index ed48fb30..b800c971 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -18,7 +18,6 @@ use crate::primitives::cells::{Atlas, CELL_SIZE}; use crate::{Drawable, GraphicsState, Renderable, Sprite, Transform, Viewport}; use luminol_data::{BlendMode, OptionVec}; -use luminol_egui_wgpu::RenderState; pub const FRAME_WIDTH: usize = 640; pub const FRAME_HEIGHT: usize = 320; @@ -48,36 +47,42 @@ impl Frame { sprites: Default::default(), viewport, }; - frame.update_all_cells(graphics_state, &animation.frames[frame_index]); + frame.rebuild_all_cells( + graphics_state, + &animation.frames[frame_index], + animation.animation_hue, + ); frame } - /// Updates the sprite for one cell based on the given animation frame. - pub fn update_cell( + pub fn rebuild_cell( &mut self, graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, + hue: i32, cell_index: usize, ) { - if let Some(sprite) = self.sprite_from_cell_data(graphics_state, frame, cell_index) { + if let Some(sprite) = self.sprite_from_cell_data(graphics_state, frame, hue, cell_index) { self.sprites.insert(cell_index, sprite); } else { let _ = self.sprites.try_remove(cell_index); } } - /// Updates the sprite for every cell based on the given animation frame. - pub fn update_all_cells( + pub fn rebuild_all_cells( &mut self, graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, + hue: i32, ) { let mut sprites = std::mem::take(&mut self.sprites); sprites.clear(); - sprites.extend((0..frame.cell_data.xsize()).filter_map(|i| { - self.sprite_from_cell_data(graphics_state, frame, i) - .map(|s| (i, s)) - })); + sprites.extend( + (0..frame.cell_data.xsize().max(self.sprites.len())).filter_map(|i| { + self.sprite_from_cell_data(graphics_state, frame, hue, i) + .map(|s| (i, s)) + }), + ); self.sprites = sprites; } @@ -85,6 +90,7 @@ impl Frame { &self, graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, + hue: i32, cell_index: usize, ) -> Option<(Sprite, egui::Rect)> { (cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0).then(|| { @@ -108,7 +114,7 @@ impl Frame { Sprite::new_with_rotation( graphics_state, self.atlas.calc_quad(id), - 0, + hue, opacity, blend_mode, &self.atlas.atlas_texture, @@ -132,55 +138,75 @@ impl Frame { pub fn update_cell_sprite( &mut self, - render_state: &RenderState, + graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, + hue: i32, cell_index: usize, ) { - if let Some((sprite, cell_rect)) = self.sprites.get_mut(cell_index) { - let id = frame.cell_data[(cell_index, 0)]; - let offset_x = frame.cell_data[(cell_index, 1)] as f32; - let offset_y = frame.cell_data[(cell_index, 2)] as f32; - let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; - let rotation = -(frame.cell_data[(cell_index, 4)] as f32).to_radians(); - let flip = frame.cell_data[(cell_index, 5)] == 1; - let opacity = frame.cell_data[(cell_index, 6)] as i32; - let blend_mode = match frame.cell_data[(cell_index, 7)] { - 1 => BlendMode::Add, - 2 => BlendMode::Subtract, - _ => BlendMode::Normal, - }; + if cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0 { + if let Some((sprite, cell_rect)) = self.sprites.get_mut(cell_index) { + let id = frame.cell_data[(cell_index, 0)]; + let offset_x = frame.cell_data[(cell_index, 1)] as f32; + let offset_y = frame.cell_data[(cell_index, 2)] as f32; + let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; + let rotation = -(frame.cell_data[(cell_index, 4)] as f32).to_radians(); + let flip = frame.cell_data[(cell_index, 5)] == 1; + let opacity = frame.cell_data[(cell_index, 6)] as i32; + let blend_mode = match frame.cell_data[(cell_index, 7)] { + 1 => BlendMode::Add, + 2 => BlendMode::Subtract, + _ => BlendMode::Normal, + }; + + let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); + let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); + + sprite.transform.set( + &graphics_state.render_state, + glam::vec2(offset_x, offset_y) + + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) + * (scale * flip_vec * CELL_OFFSET), + scale * flip_vec, + ); + + sprite.graphic.set( + &graphics_state.render_state, + hue, + opacity, + 1., + if flip { -rotation } else { rotation }, + ); - let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); - let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); + sprite.set_quad( + &graphics_state.render_state, + self.atlas.calc_quad(id), + self.atlas.atlas_texture.size(), + ); + + sprite.blend_mode = blend_mode; + + *cell_rect = egui::Rect::from_center_size( + egui::pos2(offset_x, offset_y), + egui::Vec2::splat(CELL_SIZE as f32 * (cos.abs() + sin.abs()) * scale), + ); + } else if let Some((sprite, cell_rect)) = + self.sprite_from_cell_data(graphics_state, frame, hue, cell_index) + { + self.sprites.insert(cell_index, (sprite, cell_rect)); + } + } else { + let _ = self.sprites.try_remove(cell_index); + } + } - sprite.transform.set( - render_state, - glam::vec2(offset_x, offset_y) - + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) - * (scale * flip_vec * CELL_OFFSET), - scale * flip_vec, - ); - - sprite.graphic.set( - render_state, - 0, - opacity, - 1., - if flip { -rotation } else { rotation }, - ); - - sprite.set_quad( - render_state, - self.atlas.calc_quad(id), - self.atlas.atlas_texture.size(), - ); - - sprite.blend_mode = blend_mode; - - *cell_rect = egui::Rect::from_center_size( - egui::pos2(offset_x, offset_y), - egui::Vec2::splat(CELL_SIZE as f32 * (cos.abs() + sin.abs()) * scale), - ); + pub fn update_all_cell_sprites( + &mut self, + graphics_state: &GraphicsState, + frame: &luminol_data::rpg::animation::Frame, + hue: i32, + ) { + for cell_index in 0..frame.cell_data.xsize().max(self.sprites.len()) { + self.update_cell_sprite(graphics_state, frame, hue, cell_index); } } } diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index a001c43c..769505e9 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -45,8 +45,8 @@ pub struct Window { view: luminol_components::DatabaseView, } -impl Window { - pub fn new() -> Self { +impl Default for Window { + fn default() -> Self { Self { selected_animation_name: None, previous_animation: None, @@ -61,7 +61,9 @@ impl Window { view: luminol_components::DatabaseView::new(), } } +} +impl Window { fn show_timing_header(ui: &mut egui::Ui, timing: &luminol_data::rpg::animation::Timing) { let mut vec = Vec::with_capacity(3); @@ -246,19 +248,6 @@ impl Window { ) -> bool { let mut modified = false; - let canvas_rect = egui::Resize::default() - .resizable([false, true]) - .min_width(ui.available_width()) - .max_width(ui.available_width()) - .show(ui, |ui| { - egui::Frame::dark_canvas(ui.style()) - .show(ui, |ui| { - let (_, rect) = ui.allocate_space(ui.available_size()); - rect - }) - .inner - }); - let frame_view = if let Some(frame_view) = maybe_frame_view { frame_view } else { @@ -273,6 +262,46 @@ impl Window { maybe_frame_view.as_mut().unwrap() }; + ui.columns(2, |columns| { + columns[0].add(luminol_components::Field::new( + "Editor Scale", + egui::Slider::new(&mut frame_view.scale, 15.0..=300.0) + .suffix("%") + .logarithmic(true) + .fixed_decimals(0), + )); + + *frame_index += 1; + let changed = columns[1] + .add(luminol_components::Field::new( + "Frame", + egui::DragValue::new(frame_index) + .clamp_range(1..=animation.frames.len() as i32), + )) + .changed(); + *frame_index -= 1; + if changed { + frame_view.frame.update_all_cell_sprites( + &update_state.graphics, + &mut animation.frames[*frame_index as usize], + animation.animation_hue, + ); + } + }); + + let canvas_rect = egui::Resize::default() + .resizable([false, true]) + .min_width(ui.available_width()) + .max_width(ui.available_width()) + .show(ui, |ui| { + egui::Frame::dark_canvas(ui.style()) + .show(ui, |ui| { + let (_, rect) = ui.allocate_space(ui.available_size()); + rect + }) + .inner + }); + let frame = &mut animation.frames[*frame_index as usize]; if let (Some(i), Some(drag_pos)) = ( @@ -281,9 +310,12 @@ impl Window { ) { if (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) != drag_pos { (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) = drag_pos; - frame_view - .frame - .update_cell_sprite(&update_state.graphics.render_state, frame, i); + frame_view.frame.update_cell_sprite( + &update_state.graphics, + frame, + animation.animation_hue, + i, + ); modified = true; } } @@ -389,9 +421,12 @@ impl Window { }); if properties_modified { - frame_view - .frame - .update_cell_sprite(&update_state.graphics.render_state, frame, i); + frame_view.frame.update_cell_sprite( + &update_state.graphics, + frame, + animation.animation_hue, + i, + ); modified = true; } } @@ -453,7 +488,7 @@ impl luminol_core::Window for Window { &mut animations.data, |animation| format!("{:0>4}: {}", animation.id + 1, animation.name), |ui, animations, id, update_state| { - let mut animation = &mut animations[id]; + let animation = &mut animations[id]; self.selected_animation_name = Some(animation.name.clone()); let clip_rect = ui.clip_rect(); @@ -480,9 +515,10 @@ impl luminol_core::Window for Window { animation, ) .unwrap(); // TODO get rid of this unwrap - frame_view.frame.update_all_cells( + frame_view.frame.rebuild_all_cells( &update_state.graphics, &animation.frames[self.frame as usize], + animation.animation_hue, ); } } @@ -491,7 +527,7 @@ impl luminol_core::Window for Window { update_state, clip_rect, &mut self.frame_view, - &mut animation, + animation, &mut self.frame, ); }); diff --git a/src/app/top_bar.rs b/src/app/top_bar.rs index 1ab4388b..7e398c9a 100644 --- a/src/app/top_bar.rs +++ b/src/app/top_bar.rs @@ -170,7 +170,7 @@ impl TopBar { if ui.button("Animations").clicked() { update_state .edit_windows - .add_window(luminol_ui::windows::animations::Window::new()); + .add_window(luminol_ui::windows::animations::Window::default()); } if ui.button("Common Events").clicked() { From b41ec0d810f1279cd1064526418e43cc15cebb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 16 Jul 2024 22:23:35 -0400 Subject: [PATCH 024/109] Handle invalid animation frame/cell indices gracefully --- crates/ui/src/windows/animations.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 769505e9..c63689fb 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -271,6 +271,7 @@ impl Window { .fixed_decimals(0), )); + *frame_index = (*frame_index).clamp(0, animation.frames.len().saturating_sub(1) as i32); *frame_index += 1; let changed = columns[1] .add(luminol_components::Field::new( @@ -304,6 +305,21 @@ impl Window { let frame = &mut animation.frames[*frame_index as usize]; + if frame_view + .selected_cell_index + .is_some_and(|i| i >= frame.cell_data.xsize()) + { + frame_view.selected_cell_index = None; + } + if frame_view + .hovered_cell_index + .is_some_and(|i| i >= frame.cell_data.xsize()) + { + frame_view.hovered_cell_index = None; + frame_view.hovered_cell_drag_pos = None; + frame_view.hovered_cell_drag_offset = None; + } + if let (Some(i), Some(drag_pos)) = ( frame_view.hovered_cell_index, frame_view.hovered_cell_drag_pos, @@ -515,6 +531,9 @@ impl luminol_core::Window for Window { animation, ) .unwrap(); // TODO get rid of this unwrap + self.frame = self + .frame + .clamp(0, animation.frames.len().saturating_sub(1) as i32); frame_view.frame.rebuild_all_cells( &update_state.graphics, &animation.frames[self.frame as usize], From c372d834dd57b9e234110913bf2c413fb10636e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 17 Jul 2024 15:46:39 -0400 Subject: [PATCH 025/109] Add animation editor frame copying tools --- crates/data/src/rmxp/animation.rs | 2 +- .../modals/src/animations/copy_frames_tool.rs | 147 ++++++++++++++++++ crates/modals/src/animations/mod.rs | 25 +++ crates/modals/src/lib.rs | 2 + crates/ui/src/windows/animations.rs | 61 +++++++- 5 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 crates/modals/src/animations/copy_frames_tool.rs create mode 100644 crates/modals/src/animations/mod.rs diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index 3759ee4d..a3687230 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -61,7 +61,7 @@ impl Default for Timing { } } -#[derive(Default, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] #[derive(alox_48::Deserialize, alox_48::Serialize)] #[marshal(class = "RPG::Animation::Frame")] pub struct Frame { diff --git a/crates/modals/src/animations/copy_frames_tool.rs b/crates/modals/src/animations/copy_frames_tool.rs new file mode 100644 index 00000000..16877b03 --- /dev/null +++ b/crates/modals/src/animations/copy_frames_tool.rs @@ -0,0 +1,147 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub struct Modal { + state: State, + id_source: egui::Id, + pub frames_len: usize, + pub src_frame: usize, + pub dst_frame: usize, + pub frame_count: usize, +} + +enum State { + Closed, + Open, +} + +impl Modal { + pub fn new(id_source: impl Into) -> Self { + Self { + state: State::Closed, + id_source: id_source.into(), + frames_len: 1, + src_frame: 1, + dst_frame: 1, + frame_count: 1, + } + } +} + +impl luminol_core::Modal for Modal { + type Data<'m> = (); + + fn button<'m>( + &'m mut self, + _data: Self::Data<'m>, + _update_state: &'m mut luminol_core::UpdateState<'_>, + ) -> impl egui::Widget + 'm { + |ui: &mut egui::Ui| { + let response = ui.button("Copy frames"); + if response.clicked() { + self.state = State::Open; + } + response + } + } + + fn reset(&mut self, _: &mut luminol_core::UpdateState<'_>, _data: Self::Data<'_>) { + self.close_window(); + } +} + +impl Modal { + pub fn close_window(&mut self) { + self.state = State::Closed; + } + + pub fn show_window( + &mut self, + ctx: &egui::Context, + current_frame: usize, + frames_len: usize, + ) -> bool { + let mut win_open = true; + let mut keep_open = true; + let mut needs_save = false; + + if !matches!(self.state, State::Open) { + self.frames_len = frames_len; + self.src_frame = current_frame; + self.dst_frame = current_frame; + self.frame_count = 1; + return false; + } + + egui::Window::new("Copy Frames") + .open(&mut win_open) + .id(self.id_source.with("copy_frames_tool")) + .show(ctx, |ui| { + ui.columns(3, |columns| { + self.src_frame += 1; + columns[0].add(luminol_components::Field::new( + "Source Frame", + egui::DragValue::new(&mut self.src_frame).clamp_range(1..=self.frames_len), + )); + self.src_frame -= 1; + + self.dst_frame += 1; + columns[1].add(luminol_components::Field::new( + "Destination Frame", + egui::DragValue::new(&mut self.dst_frame).clamp_range(1..=self.frames_len), + )); + self.dst_frame -= 1; + + columns[2].add(luminol_components::Field::new( + "Frame Count", + egui::DragValue::new(&mut self.frame_count) + .clamp_range(1..=self.frames_len - self.src_frame.max(self.dst_frame)), + )); + }); + + ui.label(if self.frame_count == 1 { + format!( + "Copy frame {} to frame {}", + self.src_frame + 1, + self.dst_frame + 1, + ) + } else { + format!( + "Copy frames {}–{} to frames {}–{}", + self.src_frame + 1, + self.src_frame + self.frame_count, + self.dst_frame + 1, + self.dst_frame + self.frame_count, + ) + }); + + luminol_components::close_options_ui(ui, &mut keep_open, &mut needs_save); + }); + + if !(win_open && keep_open) { + self.state = State::Closed; + } + needs_save + } +} diff --git a/crates/modals/src/animations/mod.rs b/crates/modals/src/animations/mod.rs new file mode 100644 index 00000000..744a46f7 --- /dev/null +++ b/crates/modals/src/animations/mod.rs @@ -0,0 +1,25 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub mod copy_frames_tool; diff --git a/crates/modals/src/lib.rs b/crates/modals/src/lib.rs index 3cf7bb1b..61cc0a31 100644 --- a/crates/modals/src/lib.rs +++ b/crates/modals/src/lib.rs @@ -27,3 +27,5 @@ pub mod sound_picker; pub mod graphic_picker; pub mod database_modal; + +pub mod animations; diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index c63689fb..990bfecf 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -42,9 +42,20 @@ pub struct Window { frame_view: Option, collapsing_view: luminol_components::CollapsingView, timing_se_picker: SoundPicker, + modals: Modals, view: luminol_components::DatabaseView, } +struct Modals { + copy_frames: luminol_modals::animations::copy_frames_tool::Modal, +} + +impl Modals { + fn close_all(&mut self) { + self.copy_frames.close_window(); + } +} + impl Default for Window { fn default() -> Self { Self { @@ -58,6 +69,11 @@ impl Default for Window { luminol_audio::Source::SE, "animations_timing_se_picker", ), + modals: Modals { + copy_frames: luminol_modals::animations::copy_frames_tool::Modal::new( + "animations_copy_frames_tool", + ), + }, view: luminol_components::DatabaseView::new(), } } @@ -242,6 +258,7 @@ impl Window { ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>, clip_rect: egui::Rect, + modals: &mut Modals, maybe_frame_view: &mut Option, animation: &mut luminol_data::rpg::Animation, frame_index: &mut i32, @@ -262,7 +279,7 @@ impl Window { maybe_frame_view.as_mut().unwrap() }; - ui.columns(2, |columns| { + ui.columns(3, |columns| { columns[0].add(luminol_components::Field::new( "Editor Scale", egui::Slider::new(&mut frame_view.scale, 15.0..=300.0) @@ -284,12 +301,50 @@ impl Window { if changed { frame_view.frame.update_all_cell_sprites( &update_state.graphics, - &mut animation.frames[*frame_index as usize], + &animation.frames[*frame_index as usize], animation.animation_hue, ); } + + columns[2].menu_button("Tools ⏷", |ui| { + ui.add_enabled_ui(*frame_index != 0, |ui| { + if ui.button("Copy previous frame").clicked() && *frame_index != 0 { + animation.frames[*frame_index as usize] = + animation.frames[*frame_index as usize - 1].clone(); + frame_view.frame.update_all_cell_sprites( + &update_state.graphics, + &animation.frames[*frame_index as usize], + animation.animation_hue, + ); + modified = true; + } + }); + + ui.add(modals.copy_frames.button((), update_state)); + }); }); + if modals + .copy_frames + .show_window(ui.ctx(), *frame_index as usize, animation.frames.len()) + { + let mut iter = 0..modals.copy_frames.frame_count; + while let Some(i) = if modals.copy_frames.dst_frame <= modals.copy_frames.src_frame { + iter.next() + } else { + iter.next_back() + } { + animation.frames[modals.copy_frames.dst_frame + i] = + animation.frames[modals.copy_frames.src_frame + i].clone(); + } + frame_view.frame.update_all_cell_sprites( + &update_state.graphics, + &animation.frames[*frame_index as usize], + animation.animation_hue, + ); + modified = true; + } + let canvas_rect = egui::Resize::default() .resizable([false, true]) .min_width(ui.available_width()) @@ -522,6 +577,7 @@ impl luminol_core::Window for Window { ui.with_padded_stripe(true, |ui| { if let Some(frame_view) = &mut self.frame_view { if self.previous_animation != Some(animation.id) { + self.modals.close_all(); frame_view.frame.atlas = update_state .graphics .atlas_loader @@ -545,6 +601,7 @@ impl luminol_core::Window for Window { ui, update_state, clip_rect, + &mut self.modals, &mut self.frame_view, animation, &mut self.frame, From 82a88d82bdd588ce1a414b853a842e588f4027fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 18 Jul 2024 01:33:40 -0400 Subject: [PATCH 026/109] Add animation editor frame clearing and tweening tools --- .../src/animations/clear_frames_tool.rs | 141 ++++++++++++ .../modals/src/animations/copy_frames_tool.rs | 4 +- crates/modals/src/animations/mod.rs | 2 + crates/modals/src/animations/tween_tool.rs | 202 ++++++++++++++++++ crates/ui/src/windows/animations.rs | 106 +++++++++ 5 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 crates/modals/src/animations/clear_frames_tool.rs create mode 100644 crates/modals/src/animations/tween_tool.rs diff --git a/crates/modals/src/animations/clear_frames_tool.rs b/crates/modals/src/animations/clear_frames_tool.rs new file mode 100644 index 00000000..d206f40a --- /dev/null +++ b/crates/modals/src/animations/clear_frames_tool.rs @@ -0,0 +1,141 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub struct Modal { + state: State, + id_source: egui::Id, + pub frames_len: usize, + pub start_frame: usize, + pub end_frame: usize, +} + +enum State { + Closed, + Open, +} + +impl Modal { + pub fn new(id_source: impl Into) -> Self { + Self { + state: State::Closed, + id_source: id_source.into(), + frames_len: 1, + start_frame: 0, + end_frame: 0, + } + } +} + +impl luminol_core::Modal for Modal { + type Data<'m> = (); + + fn button<'m>( + &'m mut self, + _data: Self::Data<'m>, + _update_state: &'m mut luminol_core::UpdateState<'_>, + ) -> impl egui::Widget + 'm { + |ui: &mut egui::Ui| { + let response = ui.button("Clear frames"); + if response.clicked() { + self.state = State::Open; + } + response + } + } + + fn reset(&mut self, _: &mut luminol_core::UpdateState<'_>, _data: Self::Data<'_>) { + self.close_window(); + } +} + +impl Modal { + pub fn close_window(&mut self) { + self.state = State::Closed; + } + + pub fn show_window( + &mut self, + ctx: &egui::Context, + current_frame: usize, + frames_len: usize, + ) -> bool { + let mut win_open = true; + let mut keep_open = true; + let mut needs_save = false; + + if !matches!(self.state, State::Open) { + self.frames_len = frames_len; + self.start_frame = current_frame; + self.end_frame = current_frame; + return false; + } + + egui::Window::new("Clear Frames") + .open(&mut win_open) + .id(self.id_source.with("clear_frames_tool")) + .show(ctx, |ui| { + ui.columns(2, |columns| { + self.start_frame += 1; + columns[0].add(luminol_components::Field::new( + "Starting Frame", + egui::DragValue::new(&mut self.start_frame) + .clamp_range(1..=self.frames_len), + )); + self.start_frame -= 1; + + if self.start_frame > self.end_frame { + self.end_frame = self.start_frame; + } + + self.end_frame += 1; + columns[1].add(luminol_components::Field::new( + "Ending Frame", + egui::DragValue::new(&mut self.end_frame).clamp_range(1..=self.frames_len), + )); + self.end_frame -= 1; + + if self.end_frame < self.start_frame { + self.start_frame = self.end_frame; + } + }); + + ui.label(if self.start_frame == self.end_frame { + format!("Delete all cells in frame {}", self.start_frame + 1,) + } else { + format!( + "Delete all cells in frames {}–{}", + self.start_frame + 1, + self.end_frame + 1, + ) + }); + + luminol_components::close_options_ui(ui, &mut keep_open, &mut needs_save); + }); + + if !(win_open && keep_open) { + self.state = State::Closed; + } + needs_save + } +} diff --git a/crates/modals/src/animations/copy_frames_tool.rs b/crates/modals/src/animations/copy_frames_tool.rs index 16877b03..ba75a3aa 100644 --- a/crates/modals/src/animations/copy_frames_tool.rs +++ b/crates/modals/src/animations/copy_frames_tool.rs @@ -42,8 +42,8 @@ impl Modal { state: State::Closed, id_source: id_source.into(), frames_len: 1, - src_frame: 1, - dst_frame: 1, + src_frame: 0, + dst_frame: 0, frame_count: 1, } } diff --git a/crates/modals/src/animations/mod.rs b/crates/modals/src/animations/mod.rs index 744a46f7..7b7ac171 100644 --- a/crates/modals/src/animations/mod.rs +++ b/crates/modals/src/animations/mod.rs @@ -22,4 +22,6 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +pub mod clear_frames_tool; pub mod copy_frames_tool; +pub mod tween_tool; diff --git a/crates/modals/src/animations/tween_tool.rs b/crates/modals/src/animations/tween_tool.rs new file mode 100644 index 00000000..97b5b783 --- /dev/null +++ b/crates/modals/src/animations/tween_tool.rs @@ -0,0 +1,202 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub struct Modal { + state: State, + id_source: egui::Id, + pub frames_len: usize, + pub start_frame: usize, + pub end_frame: usize, + pub start_cell: usize, + pub end_cell: usize, + pub tween_pattern: bool, + pub tween_position: bool, + pub tween_shading: bool, +} + +enum State { + Closed, + Open, +} + +impl Modal { + pub fn new(id_source: impl Into) -> Self { + Self { + state: State::Closed, + id_source: id_source.into(), + frames_len: 1, + start_frame: 0, + end_frame: 0, + start_cell: 0, + end_cell: 15, + tween_pattern: true, + tween_position: true, + tween_shading: true, + } + } +} + +impl luminol_core::Modal for Modal { + type Data<'m> = (); + + fn button<'m>( + &'m mut self, + _data: Self::Data<'m>, + _update_state: &'m mut luminol_core::UpdateState<'_>, + ) -> impl egui::Widget + 'm { + |ui: &mut egui::Ui| { + let response = ui.button("Tween"); + if response.clicked() { + self.state = State::Open; + } + response + } + } + + fn reset(&mut self, _: &mut luminol_core::UpdateState<'_>, _data: Self::Data<'_>) { + self.close_window(); + } +} + +impl Modal { + pub fn close_window(&mut self) { + self.state = State::Closed; + } + + pub fn show_window( + &mut self, + ctx: &egui::Context, + current_frame: usize, + frames_len: usize, + ) -> bool { + let mut win_open = true; + let mut keep_open = true; + let mut needs_save = false; + + if !matches!(self.state, State::Open) { + self.frames_len = frames_len; + self.start_frame = frames_len + .saturating_sub(2) + .min(current_frame.saturating_sub(1)); + self.end_frame = 2.max(current_frame + 1); + return false; + } + + egui::Window::new("Tween") + .open(&mut win_open) + .id(self.id_source.with("tween_tool")) + .show(ctx, |ui| { + ui.columns(2, |columns| { + self.start_frame += 1; + columns[0].add(luminol_components::Field::new( + "Starting Frame", + egui::DragValue::new(&mut self.start_frame) + .clamp_range(1..=self.frames_len.saturating_sub(2)), + )); + self.start_frame -= 1; + + if self.start_frame + 2 > self.end_frame { + self.end_frame = self.start_frame + 2; + } + + self.end_frame += 1; + columns[1].add(luminol_components::Field::new( + "Ending Frame", + egui::DragValue::new(&mut self.end_frame).clamp_range(3..=self.frames_len), + )); + self.end_frame -= 1; + + if self.end_frame - 2 < self.start_frame { + self.start_frame = self.end_frame - 2; + } + }); + + ui.columns(2, |columns| { + self.start_cell += 1; + columns[0].add(luminol_components::Field::new( + "Starting Cell", + egui::DragValue::new(&mut self.start_cell) + .clamp_range(1..=i16::MAX as usize + 1), + )); + self.start_cell -= 1; + + if self.start_cell > self.end_cell { + self.end_cell = self.start_cell; + } + + self.end_cell += 1; + columns[1].add(luminol_components::Field::new( + "Ending Cell", + egui::DragValue::new(&mut self.end_cell) + .clamp_range(1..=i16::MAX as usize + 1), + )); + self.end_cell -= 1; + + if self.end_cell < self.start_cell { + self.start_cell = self.end_cell; + } + }); + + ui.checkbox(&mut self.tween_pattern, "Pattern"); + ui.checkbox(&mut self.tween_position, "Position, scale and rotation"); + ui.checkbox(&mut self.tween_shading, "Opacity and blending"); + + let mut vec = Vec::with_capacity(3); + if self.tween_pattern { + vec.push("pattern"); + } + if self.tween_position { + vec.push("position, scale, rotation"); + } + if self.tween_shading { + vec.push("opacity, blending"); + } + ui.label(if self.start_cell == self.end_cell { + format!( + "Linearly interpolate cell {} for cell {} from frame {} to frame {}", + vec.join(", "), + self.start_cell + 1, + self.start_frame + 1, + self.end_frame + 1, + ) + } else { + format!( + "Linearly interpolate cell {} for cells {}–{} from frame {} to frame {}", + vec.join(", "), + self.start_cell + 1, + self.end_cell + 1, + self.start_frame + 1, + self.end_frame + 1, + ) + }); + + luminol_components::close_options_ui(ui, &mut keep_open, &mut needs_save); + }); + + if !(win_open && keep_open) { + self.state = State::Closed; + } + needs_save + } +} diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 990bfecf..6c3c5f81 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -48,11 +48,15 @@ pub struct Window { struct Modals { copy_frames: luminol_modals::animations::copy_frames_tool::Modal, + clear_frames: luminol_modals::animations::clear_frames_tool::Modal, + tween: luminol_modals::animations::tween_tool::Modal, } impl Modals { fn close_all(&mut self) { self.copy_frames.close_window(); + self.clear_frames.close_window(); + self.tween.close_window(); } } @@ -73,6 +77,10 @@ impl Default for Window { copy_frames: luminol_modals::animations::copy_frames_tool::Modal::new( "animations_copy_frames_tool", ), + clear_frames: luminol_modals::animations::clear_frames_tool::Modal::new( + "animations_clear_frames_tool", + ), + tween: luminol_modals::animations::tween_tool::Modal::new("animations_tween_tool"), }, view: luminol_components::DatabaseView::new(), } @@ -321,6 +329,14 @@ impl Window { }); ui.add(modals.copy_frames.button((), update_state)); + + ui.separator(); + + ui.add(modals.clear_frames.button((), update_state)); + + ui.separator(); + + ui.add(modals.tween.button((), update_state)); }); }); @@ -345,6 +361,96 @@ impl Window { modified = true; } + if modals + .clear_frames + .show_window(ui.ctx(), *frame_index as usize, animation.frames.len()) + { + for i in modals.clear_frames.start_frame..=modals.clear_frames.end_frame { + animation.frames[i] = Default::default(); + } + frame_view.frame.update_all_cell_sprites( + &update_state.graphics, + &animation.frames[*frame_index as usize], + animation.animation_hue, + ); + modified = true; + } + + if modals + .tween + .show_window(ui.ctx(), *frame_index as usize, animation.frames.len()) + { + for i in modals.tween.start_cell..=modals.tween.end_cell { + let data = &animation.frames[modals.tween.start_frame].cell_data; + if i >= data.xsize() || data[(i, 0)] < 0 { + continue; + } + let data = &animation.frames[modals.tween.end_frame].cell_data; + if i >= data.xsize() || data[(i, 0)] < 0 { + continue; + } + + for j in modals.tween.start_frame..=modals.tween.end_frame { + let lerp = |frames: &Vec, property| { + ( + egui::lerp( + frames[modals.tween.start_frame].cell_data[(i, property)] as f64 + ..=frames[modals.tween.end_frame].cell_data[(i, property)] + as f64, + (j - modals.tween.start_frame) as f64 + / (modals.tween.end_frame - modals.tween.start_frame) as f64, + ), + frames[modals.tween.start_frame].cell_data[(i, property)] + <= frames[modals.tween.end_frame].cell_data[(i, property)], + ) + }; + + if animation.frames[j].cell_data.xsize() < i + 1 { + animation.frames[j].cell_data.resize(i + 1, 8); + animation.frames[j].cell_max = (i + 1) as i32; + } + + if modals.tween.tween_pattern { + let (val, orientation) = lerp(&animation.frames, 0); + animation.frames[j].cell_data[(i, 0)] = + if orientation { val.floor() } else { val.ceil() } as i16; + } else if animation.frames[j].cell_data[(i, 0)] < 0 { + animation.frames[j].cell_data[(i, 0)] = 0; + } + + if modals.tween.tween_position { + let (val, orientation) = lerp(&animation.frames, 1); + animation.frames[j].cell_data[(i, 1)] = + if orientation { val.floor() } else { val.ceil() } as i16; + + let (val, orientation) = lerp(&animation.frames, 2); + animation.frames[j].cell_data[(i, 2)] = + if orientation { val.floor() } else { val.ceil() } as i16; + + let (val, _) = lerp(&animation.frames, 3); + animation.frames[j].cell_data[(i, 3)] = val.floor() as i16; + + let (val, _) = lerp(&animation.frames, 4); + animation.frames[j].cell_data[(i, 4)] = val.floor() as i16; + } + + if modals.tween.tween_shading { + let (val, _) = lerp(&animation.frames, 6); + animation.frames[j].cell_data[(i, 6)] = val.floor() as i16; + + let (val, _) = lerp(&animation.frames, 7); + animation.frames[j].cell_data[(i, 7)] = val.floor() as i16; + } + } + } + frame_view.frame.update_all_cell_sprites( + &update_state.graphics, + &animation.frames[*frame_index as usize], + animation.animation_hue, + ); + modified = true; + } + let canvas_rect = egui::Resize::default() .resizable([false, true]) .min_width(ui.available_width()) From 7fae687e9d4e86681a8128e47ea770476d06955f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 18 Jul 2024 10:43:34 -0400 Subject: [PATCH 027/109] Disable tween tool if there are fewer than 3 frames --- crates/ui/src/windows/animations.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 6c3c5f81..3dc8004f 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -336,7 +336,13 @@ impl Window { ui.separator(); - ui.add(modals.tween.button((), update_state)); + ui.add_enabled_ui(animation.frames.len() >= 3, |ui| { + if animation.frames.len() >= 3 { + ui.add(modals.tween.button((), update_state)); + } else { + modals.tween.close_window(); + } + }) }); }); From e5289aa6b78f2565c2071793386520c543b3bf4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 18 Jul 2024 17:13:53 -0400 Subject: [PATCH 028/109] Add animation editor batch edit tool --- crates/components/src/lib.rs | 45 ++ .../modals/src/animations/batch_edit_tool.rs | 541 ++++++++++++++++++ crates/modals/src/animations/mod.rs | 1 + crates/ui/src/windows/animations.rs | 125 +++- 4 files changed, 707 insertions(+), 5 deletions(-) create mode 100644 crates/modals/src/animations/batch_edit_tool.rs diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index 1c143d32..b738d6ca 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -165,6 +165,51 @@ where } } +pub struct FieldWithCheckbox<'a, T> { + name: String, + checked: &'a mut bool, + widget: T, +} +impl<'a, T> FieldWithCheckbox<'a, T> +where + T: egui::Widget, +{ + pub fn new(name: impl Into, checked: &'a mut bool, widget: T) -> Self { + Self { + name: name.into(), + checked, + widget, + } + } +} + +impl egui::Widget for FieldWithCheckbox<'_, T> +where + T: egui::Widget, +{ + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + let mut changed = false; + let mut response = ui + .vertical(|ui| { + let spacing = ui.spacing().item_spacing.y; + ui.add_space(spacing); + ui.horizontal(|ui| { + ui.add(egui::Label::new(format!("{}:", self.name)).truncate(true)); + ui.add(egui::Checkbox::without_text(self.checked)); + }); + if ui.add_enabled(*self.checked, self.widget).changed() { + changed = true; + }; + ui.add_space(spacing); + }) + .response; + if changed { + response.mark_changed(); + } + response + } +} + pub struct EnumComboBox<'a, H, T> { id_source: H, reference: &'a mut T, diff --git a/crates/modals/src/animations/batch_edit_tool.rs b/crates/modals/src/animations/batch_edit_tool.rs new file mode 100644 index 00000000..1d621261 --- /dev/null +++ b/crates/modals/src/animations/batch_edit_tool.rs @@ -0,0 +1,541 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use luminol_components::UiExt; +use luminol_core::prelude::frame::{FRAME_HEIGHT, FRAME_WIDTH}; +use luminol_data::BlendMode; + +pub struct Modal { + state: State, + id_source: egui::Id, + pub mode: Mode, + pub frames_len: usize, + pub start_frame: usize, + pub end_frame: usize, + pub num_patterns: u32, + + pub set_pattern_enabled: bool, + pub set_x_enabled: bool, + pub set_y_enabled: bool, + pub set_scale_enabled: bool, + pub set_rotation_enabled: bool, + pub set_flip_enabled: bool, + pub set_opacity_enabled: bool, + pub set_blending_enabled: bool, + + pub set_pattern: i16, + pub set_x: i16, + pub set_y: i16, + pub set_scale: i16, + pub set_rotation: i16, + pub set_flip: i16, + pub set_opacity: i16, + pub set_blending: i16, + + pub add_pattern: i16, + pub add_x: i16, + pub add_y: i16, + pub add_scale: i16, + pub add_rotation: i16, + pub add_flip: bool, + pub add_opacity: i16, + pub add_blending: i16, + + pub mul_pattern: f64, + pub mul_x: f64, + pub mul_y: f64, + pub mul_scale: f64, + pub mul_rotation: f64, + pub mul_opacity: f64, +} + +enum State { + Closed, + Open, +} + +#[derive(PartialEq, Eq)] +pub enum Mode { + Set, + Add, + Mul, +} + +impl Modal { + pub fn new(id_source: impl Into) -> Self { + Self { + state: State::Closed, + id_source: id_source.into(), + mode: Mode::Set, + frames_len: 1, + start_frame: 0, + end_frame: 0, + num_patterns: 5, + + set_pattern_enabled: false, + set_x_enabled: false, + set_y_enabled: false, + set_scale_enabled: false, + set_rotation_enabled: false, + set_flip_enabled: false, + set_opacity_enabled: false, + set_blending_enabled: false, + + set_pattern: 0, + set_x: 0, + set_y: 0, + set_scale: 100, + set_rotation: 0, + set_flip: 0, + set_opacity: 255, + set_blending: 1, + + add_pattern: 0, + add_x: 0, + add_y: 0, + add_scale: 0, + add_rotation: 0, + add_flip: false, + add_opacity: 0, + add_blending: 0, + + mul_pattern: 1., + mul_x: 1., + mul_y: 1., + mul_scale: 1., + mul_rotation: 1., + mul_opacity: 1., + } + } +} + +impl luminol_core::Modal for Modal { + type Data<'m> = (); + + fn button<'m>( + &'m mut self, + _data: Self::Data<'m>, + _update_state: &'m mut luminol_core::UpdateState<'_>, + ) -> impl egui::Widget + 'm { + |ui: &mut egui::Ui| { + let response = ui.button("Batch Edit"); + if response.clicked() { + self.state = State::Open; + } + response + } + } + + fn reset(&mut self, _: &mut luminol_core::UpdateState<'_>, _data: Self::Data<'_>) { + self.close_window(); + } +} + +impl Modal { + pub fn close_window(&mut self) { + self.state = State::Closed; + } + + pub fn show_window( + &mut self, + ctx: &egui::Context, + current_frame: usize, + frames_len: usize, + num_patterns: u32, + ) -> bool { + let mut win_open = true; + let mut keep_open = true; + let mut needs_save = false; + + if !matches!(self.state, State::Open) { + self.frames_len = frames_len; + self.start_frame = current_frame; + self.end_frame = current_frame; + self.num_patterns = num_patterns; + return false; + } + + egui::Window::new("Batch Edit") + .open(&mut win_open) + .id(self.id_source.with("batch_edit_tool")) + .show(ctx, |ui| { + ui.with_padded_stripe(false, |ui| { + ui.columns(3, |columns| { + columns[0].selectable_value(&mut self.mode, Mode::Set, "Set value"); + columns[1].selectable_value(&mut self.mode, Mode::Add, "Add"); + columns[2].selectable_value(&mut self.mode, Mode::Mul, "Multiply"); + }); + }); + + ui.with_padded_stripe(true, |ui| { + ui.columns(2, |columns| { + self.start_frame += 1; + columns[0].add(luminol_components::Field::new( + "Starting Frame", + egui::DragValue::new(&mut self.start_frame) + .clamp_range(1..=self.frames_len), + )); + self.start_frame -= 1; + + if self.start_frame > self.end_frame { + self.end_frame = self.start_frame; + } + + self.end_frame += 1; + columns[1].add(luminol_components::Field::new( + "Ending Frame", + egui::DragValue::new(&mut self.end_frame) + .clamp_range(1..=self.frames_len), + )); + self.end_frame -= 1; + + if self.end_frame < self.start_frame { + self.start_frame = self.end_frame; + } + }); + }); + + match self.mode { + Mode::Set => { + ui.with_padded_stripe(false, |ui| { + ui.columns(4, |columns| { + self.set_pattern += 1; + columns[0].add(luminol_components::FieldWithCheckbox::new( + "Pattern", + &mut self.set_pattern_enabled, + egui::DragValue::new(&mut self.set_pattern) + .clamp_range(1..=num_patterns as i16), + )); + self.set_pattern -= 1; + + columns[1].add(luminol_components::FieldWithCheckbox::new( + "X", + &mut self.set_x_enabled, + egui::DragValue::new(&mut self.set_x).clamp_range( + -(FRAME_WIDTH as i16 / 2)..=FRAME_WIDTH as i16 / 2, + ), + )); + + columns[2].add(luminol_components::FieldWithCheckbox::new( + "Y", + &mut self.set_y_enabled, + egui::DragValue::new(&mut self.set_y).clamp_range( + -(FRAME_HEIGHT as i16 / 2)..=FRAME_HEIGHT as i16 / 2, + ), + )); + + columns[3].add(luminol_components::FieldWithCheckbox::new( + "Scale", + &mut self.set_scale_enabled, + egui::DragValue::new(&mut self.set_scale) + .clamp_range(1..=i16::MAX) + .suffix("%"), + )); + }); + }); + + ui.with_padded_stripe(true, |ui| { + ui.columns(4, |columns| { + columns[0].add(luminol_components::FieldWithCheckbox::new( + "Rotation", + &mut self.set_rotation_enabled, + egui::DragValue::new(&mut self.set_rotation) + .clamp_range(0..=360) + .suffix("°"), + )); + + let mut flip = self.set_flip == 1; + columns[1].add(luminol_components::FieldWithCheckbox::new( + "Flip", + &mut self.set_flip_enabled, + egui::Checkbox::without_text(&mut flip), + )); + self.set_flip = if flip { 1 } else { 0 }; + + columns[2].add(luminol_components::FieldWithCheckbox::new( + "Opacity", + &mut self.set_opacity_enabled, + egui::DragValue::new(&mut self.set_opacity) + .clamp_range(0..=255), + )); + + let mut blend_mode = match self.set_blending { + 1 => BlendMode::Add, + 2 => BlendMode::Subtract, + _ => BlendMode::Normal, + }; + columns[3].add(luminol_components::FieldWithCheckbox::new( + "Blending", + &mut self.set_blending_enabled, + luminol_components::EnumComboBox::new( + self.id_source.with("set_blending"), + &mut blend_mode, + ), + )); + self.set_blending = match blend_mode { + BlendMode::Normal => 0, + BlendMode::Add => 1, + BlendMode::Subtract => 2, + }; + }); + }); + + ui.with_padded_stripe(false, |ui| { + if ui.button("Reset values").clicked() { + self.set_pattern_enabled = false; + self.set_x_enabled = false; + self.set_y_enabled = false; + self.set_scale_enabled = false; + self.set_rotation_enabled = false; + self.set_flip_enabled = false; + self.set_opacity_enabled = false; + self.set_blending_enabled = false; + + self.set_pattern = 0; + self.set_x = 0; + self.set_y = 0; + self.set_scale = 100; + self.set_rotation = 0; + self.set_flip = 0; + self.set_opacity = 255; + self.set_blending = 1; + } + }); + } + + Mode::Add => { + ui.with_padded_stripe(false, |ui| { + ui.columns(4, |columns| { + let limit = num_patterns.saturating_sub(1) as i16; + columns[0].add(luminol_components::Field::new( + "Pattern", + egui::DragValue::new(&mut self.add_pattern) + .clamp_range(-limit..=limit), + )); + + columns[1].add(luminol_components::Field::new( + "X", + egui::DragValue::new(&mut self.add_x) + .clamp_range(-(FRAME_WIDTH as i16)..=FRAME_WIDTH as i16), + )); + + columns[2].add(luminol_components::Field::new( + "Y", + egui::DragValue::new(&mut self.add_y) + .clamp_range(-(FRAME_HEIGHT as i16)..=FRAME_HEIGHT as i16), + )); + + columns[3].add(luminol_components::Field::new( + "Scale", + egui::DragValue::new(&mut self.add_scale).suffix("%"), + )); + }); + }); + + ui.with_padded_stripe(true, |ui| { + ui.columns(4, |columns| { + columns[0].add(luminol_components::Field::new( + "Rotation", + egui::DragValue::new(&mut self.add_rotation) + .clamp_range(-360..=360) + .suffix("°"), + )); + + columns[1].add(luminol_components::Field::new( + "Flip", + egui::Checkbox::without_text(&mut self.add_flip), + )); + + columns[2].add(luminol_components::Field::new( + "Opacity", + egui::DragValue::new(&mut self.add_opacity) + .clamp_range(-255..=255), + )); + + columns[3].add(luminol_components::Field::new( + "Blending", + egui::DragValue::new(&mut self.add_blending) + .clamp_range(-2..=2), + )); + }); + }); + + ui.with_padded_stripe(false, |ui| { + if ui.button("Reset values").clicked() { + self.add_pattern = 0; + self.add_x = 0; + self.add_y = 0; + self.add_scale = 0; + self.add_rotation = 0; + self.add_flip = false; + self.add_opacity = 0; + self.add_blending = 0; + } + }); + } + + Mode::Mul => { + ui.with_padded_stripe(false, |ui| { + ui.columns(4, |columns| { + self.mul_pattern *= 100.; + columns[0].add(luminol_components::Field::new( + "Pattern", + egui::DragValue::new(&mut self.mul_pattern) + .clamp_range( + (num_patterns as f64).recip() * 100.0 + ..=num_patterns as f64 * 100.0, + ) + .suffix("%"), + )); + self.mul_pattern /= 100.; + + self.mul_x *= 100.; + columns[1].add(luminol_components::Field::new( + "X", + egui::DragValue::new(&mut self.mul_x) + .clamp_range( + -(FRAME_WIDTH as f64 / 2.) * 100.0 + ..=FRAME_WIDTH as f64 / 2. * 100.0, + ) + .suffix("%"), + )); + self.mul_x /= 100.; + + self.mul_y *= 100.; + columns[2].add(luminol_components::Field::new( + "Y", + egui::DragValue::new(&mut self.mul_y) + .clamp_range( + -(FRAME_HEIGHT as f64 / 2.) * 100.0 + ..=FRAME_HEIGHT as f64 / 2. * 100.0, + ) + .suffix("%"), + )); + self.mul_y /= 100.; + + self.mul_scale *= 100.; + columns[3].add(luminol_components::Field::new( + "Scale", + egui::DragValue::new(&mut self.mul_scale) + .clamp_range(0.0..=i16::MAX as f64 * 100.0) + .suffix("%"), + )); + self.mul_scale /= 100.; + }); + }); + + ui.with_padded_stripe(true, |ui| { + ui.columns(2, |columns| { + self.mul_rotation *= 100.; + columns[0].add(luminol_components::Field::new( + "Rotation", + egui::DragValue::new(&mut self.mul_rotation) + .clamp_range(-360. * 100.0..=360.0 * 100.0) + .suffix("%"), + )); + self.mul_rotation /= 100.; + + self.mul_opacity *= 100.; + columns[1].add(luminol_components::Field::new( + "Opacity", + egui::DragValue::new(&mut self.mul_opacity) + .clamp_range(0.0..=255. * 100.0) + .suffix("%"), + )); + self.mul_opacity /= 100.; + }); + }); + + ui.with_padded_stripe(false, |ui| { + if ui.button("Reset values").clicked() { + self.mul_pattern = 1.; + self.mul_x = 1.; + self.mul_y = 1.; + self.mul_scale = 1.; + self.mul_rotation = 1.; + self.mul_opacity = 1.; + } + }); + } + } + + ui.with_padded_stripe(true, |ui| { + ui.with_cross_justify(|ui| { + let spacing = ui.spacing().item_spacing.y; + ui.add_space(spacing); + + ui.label(match self.mode { + Mode::Set if self.start_frame == self.end_frame => { + format!( + "Set the above values for all cells in frame {}", + self.start_frame + 1, + ) + } + Mode::Set => { + format!( + "Set the above values for all cells in frames {}–{}", + self.start_frame + 1, + self.end_frame + 1, + ) + } + Mode::Add if self.start_frame == self.end_frame => { + format!( + "Add the above values for all cells in frame {}", + self.start_frame + 1, + ) + } + Mode::Add => { + format!( + "Add the above values for all cells in frames {}–{}", + self.start_frame + 1, + self.end_frame + 1, + ) + } + Mode::Mul if self.start_frame == self.end_frame => { + format!( + "Multiply by the above values for all cells in frame {}", + self.start_frame + 1, + ) + } + Mode::Mul => { + format!( + "Multiply by the above values for all cells in frames {}–{}", + self.start_frame + 1, + self.end_frame + 1, + ) + } + }); + + luminol_components::close_options_ui(ui, &mut keep_open, &mut needs_save); + ui.add_space(spacing); + }); + }); + }); + + if !(win_open && keep_open) { + self.state = State::Closed; + } + needs_save + } +} diff --git a/crates/modals/src/animations/mod.rs b/crates/modals/src/animations/mod.rs index 7b7ac171..2a7bd9ee 100644 --- a/crates/modals/src/animations/mod.rs +++ b/crates/modals/src/animations/mod.rs @@ -22,6 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +pub mod batch_edit_tool; pub mod clear_frames_tool; pub mod copy_frames_tool; pub mod tween_tool; diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 3dc8004f..6881e618 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -50,6 +50,7 @@ struct Modals { copy_frames: luminol_modals::animations::copy_frames_tool::Modal, clear_frames: luminol_modals::animations::clear_frames_tool::Modal, tween: luminol_modals::animations::tween_tool::Modal, + batch_edit: luminol_modals::animations::batch_edit_tool::Modal, } impl Modals { @@ -57,6 +58,7 @@ impl Modals { self.copy_frames.close_window(); self.clear_frames.close_window(); self.tween.close_window(); + self.batch_edit.close_window(); } } @@ -81,6 +83,9 @@ impl Default for Window { "animations_clear_frames_tool", ), tween: luminol_modals::animations::tween_tool::Modal::new("animations_tween_tool"), + batch_edit: luminol_modals::animations::batch_edit_tool::Modal::new( + "animations_batch_edit_tool", + ), }, view: luminol_components::DatabaseView::new(), } @@ -330,19 +335,17 @@ impl Window { ui.add(modals.copy_frames.button((), update_state)); - ui.separator(); - ui.add(modals.clear_frames.button((), update_state)); - ui.separator(); - ui.add_enabled_ui(animation.frames.len() >= 3, |ui| { if animation.frames.len() >= 3 { ui.add(modals.tween.button((), update_state)); } else { modals.tween.close_window(); } - }) + }); + + ui.add(modals.batch_edit.button((), update_state)); }); }); @@ -457,6 +460,118 @@ impl Window { modified = true; } + let num_patterns = frame_view.frame.atlas.animation_height / CELL_SIZE * ANIMATION_COLUMNS; + if modals.batch_edit.show_window( + ui.ctx(), + *frame_index as usize, + animation.frames.len(), + num_patterns, + ) { + for i in modals.batch_edit.start_frame..=modals.batch_edit.end_frame { + let data = &mut animation.frames[i].cell_data; + for j in 0..data.xsize() { + if data[(j, 0)] < 0 { + continue; + } + match modals.batch_edit.mode { + luminol_modals::animations::batch_edit_tool::Mode::Set => { + if modals.batch_edit.set_pattern_enabled { + data[(j, 0)] = modals.batch_edit.set_pattern; + } + if modals.batch_edit.set_x_enabled { + data[(j, 1)] = modals.batch_edit.set_x; + } + if modals.batch_edit.set_y_enabled { + data[(j, 2)] = modals.batch_edit.set_y; + } + if modals.batch_edit.set_scale_enabled { + data[(j, 3)] = modals.batch_edit.set_scale; + } + if modals.batch_edit.set_rotation_enabled { + data[(j, 4)] = modals.batch_edit.set_rotation; + } + if modals.batch_edit.set_flip_enabled { + data[(j, 5)] = modals.batch_edit.set_flip; + } + if modals.batch_edit.set_opacity_enabled { + data[(j, 6)] = modals.batch_edit.set_opacity; + } + if modals.batch_edit.set_blending_enabled { + data[(j, 7)] = modals.batch_edit.set_blending; + } + } + luminol_modals::animations::batch_edit_tool::Mode::Add => { + data[(j, 0)] = data[(j, 0)] + .saturating_add(modals.batch_edit.add_pattern) + .clamp(0, num_patterns.saturating_sub(1) as i16); + data[(j, 1)] = data[(j, 1)] + .saturating_add(modals.batch_edit.add_x) + .clamp(-(FRAME_WIDTH as i16 / 2), FRAME_WIDTH as i16 / 2); + data[(j, 2)] = data[(j, 2)] + .saturating_add(modals.batch_edit.add_y) + .clamp(-(FRAME_HEIGHT as i16 / 2), FRAME_HEIGHT as i16 / 2); + data[(j, 3)] = data[(j, 3)] + .saturating_add(modals.batch_edit.add_scale) + .max(1); + data[(j, 4)] += modals.batch_edit.add_rotation; + if !(0..=360).contains(&data[(j, 4)]) { + data[(j, 4)] = data[(j, 4)].rem_euclid(360); + } + if modals.batch_edit.add_flip { + if data[(j, 5)] == 1 { + data[(j, 5)] = 0; + } else { + data[(j, 5)] = 1; + } + } + data[(j, 6)] = data[(j, 6)] + .saturating_add(modals.batch_edit.add_opacity) + .clamp(0, 255); + data[(j, 7)] += modals.batch_edit.add_blending; + if !(0..3).contains(&data[(j, 7)]) { + data[(j, 7)] = data[(j, 7)].rem_euclid(3); + } + } + luminol_modals::animations::batch_edit_tool::Mode::Mul => { + data[(j, 0)] = + ((data[(j, 0)] + 1) as f64 * modals.batch_edit.mul_pattern) + .clamp(1., num_patterns as f64) + .round_ties_even() as i16 + - 1; + data[(j, 1)] = (data[(j, 1)] as f64 * modals.batch_edit.mul_x) + .clamp(-(FRAME_WIDTH as f64 / 2.), FRAME_WIDTH as f64 / 2.) + .round_ties_even() + as i16; + data[(j, 2)] = (data[(j, 2)] as f64 * modals.batch_edit.mul_y) + .clamp(-(FRAME_HEIGHT as f64 / 2.), FRAME_HEIGHT as f64 / 2.) + .round_ties_even() + as i16; + data[(j, 3)] = (data[(j, 3)] as f64 * modals.batch_edit.mul_scale) + .clamp(1., i16::MAX as f64) + .round_ties_even() + as i16; + data[(j, 4)] = (data[(j, 4)] as f64 * modals.batch_edit.mul_rotation) + .round_ties_even() + as i16; + if !(0..=360).contains(&data[(j, 4)]) { + data[(j, 4)] = data[(j, 4)].rem_euclid(360); + } + data[(j, 6)] = (data[(j, 6)] as f64 * modals.batch_edit.mul_opacity) + .min(255.) + .round_ties_even() + as i16; + } + } + } + } + frame_view.frame.update_all_cell_sprites( + &update_state.graphics, + &animation.frames[*frame_index as usize], + animation.animation_hue, + ); + modified = true; + } + let canvas_rect = egui::Resize::default() .resizable([false, true]) .min_width(ui.available_width()) From 0e6c4656008e2e3b227307364f52697e9871797d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 18 Jul 2024 20:43:38 -0400 Subject: [PATCH 029/109] Improve layouting of animation editor "Tools" button --- crates/ui/src/windows/animations.rs | 72 ++++++++++++++++++----------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 6881e618..7e9257bc 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -292,8 +292,8 @@ impl Window { maybe_frame_view.as_mut().unwrap() }; - ui.columns(3, |columns| { - columns[0].add(luminol_components::Field::new( + ui.horizontal(|ui| { + ui.add(luminol_components::Field::new( "Editor Scale", egui::Slider::new(&mut frame_view.scale, 15.0..=300.0) .suffix("%") @@ -303,7 +303,7 @@ impl Window { *frame_index = (*frame_index).clamp(0, animation.frames.len().saturating_sub(1) as i32); *frame_index += 1; - let changed = columns[1] + let changed = ui .add(luminol_components::Field::new( "Frame", egui::DragValue::new(frame_index) @@ -319,34 +319,52 @@ impl Window { ); } - columns[2].menu_button("Tools ⏷", |ui| { - ui.add_enabled_ui(*frame_index != 0, |ui| { - if ui.button("Copy previous frame").clicked() && *frame_index != 0 { - animation.frames[*frame_index as usize] = - animation.frames[*frame_index as usize - 1].clone(); - frame_view.frame.update_all_cell_sprites( - &update_state.graphics, - &animation.frames[*frame_index as usize], - animation.animation_hue, - ); - modified = true; - } - }); + ui.with_layout( + egui::Layout { + main_dir: egui::Direction::RightToLeft, + cross_align: egui::Align::Max, + ..*ui.layout() + }, + |ui| { + egui::Frame::none() + .outer_margin(egui::Margin { + bottom: 2. * ui.spacing().item_spacing.y, + ..egui::Margin::ZERO + }) + .show(ui, |ui| { + ui.menu_button("Tools ⏷", |ui| { + ui.add_enabled_ui(*frame_index != 0, |ui| { + if ui.button("Copy previous frame").clicked() + && *frame_index != 0 + { + animation.frames[*frame_index as usize] = + animation.frames[*frame_index as usize - 1].clone(); + frame_view.frame.update_all_cell_sprites( + &update_state.graphics, + &animation.frames[*frame_index as usize], + animation.animation_hue, + ); + modified = true; + } + }); - ui.add(modals.copy_frames.button((), update_state)); + ui.add(modals.copy_frames.button((), update_state)); - ui.add(modals.clear_frames.button((), update_state)); + ui.add(modals.clear_frames.button((), update_state)); - ui.add_enabled_ui(animation.frames.len() >= 3, |ui| { - if animation.frames.len() >= 3 { - ui.add(modals.tween.button((), update_state)); - } else { - modals.tween.close_window(); - } - }); + ui.add_enabled_ui(animation.frames.len() >= 3, |ui| { + if animation.frames.len() >= 3 { + ui.add(modals.tween.button((), update_state)); + } else { + modals.tween.close_window(); + } + }); - ui.add(modals.batch_edit.button((), update_state)); - }); + ui.add(modals.batch_edit.button((), update_state)); + }); + }); + }, + ); }); if modals From 27e35ab13ef5f53505ea504b66fd4ff71d24be42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 18 Jul 2024 21:22:15 -0400 Subject: [PATCH 030/109] Implement error handling for animation atlas texture loading --- crates/ui/src/windows/animations.rs | 67 +++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 7e9257bc..db30c4c4 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -275,19 +275,32 @@ impl Window { maybe_frame_view: &mut Option, animation: &mut luminol_data::rpg::Animation, frame_index: &mut i32, - ) -> bool { + ) -> (bool, bool) { let mut modified = false; let frame_view = if let Some(frame_view) = maybe_frame_view { frame_view } else { *maybe_frame_view = Some( - luminol_components::AnimationFrameView::new( + match luminol_components::AnimationFrameView::new( update_state, animation, *frame_index as usize, - ) - .unwrap(), // TODO get rid of this unwrap + ) { + Ok(atlas) => atlas, + Err(e) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + animation.animation_name, + animation.id + 1, + animation.name, + )), + ); + return (modified, true); + } + }, ); maybe_frame_view.as_mut().unwrap() }; @@ -760,7 +773,7 @@ impl Window { } }); - modified + (modified, false) } } @@ -819,19 +832,32 @@ impl luminol_core::Window for Window { .changed(); }); - ui.with_padded_stripe(true, |ui| { + let abort = ui.with_padded_stripe(true, |ui| { if let Some(frame_view) = &mut self.frame_view { if self.previous_animation != Some(animation.id) { self.modals.close_all(); - frame_view.frame.atlas = update_state + frame_view.frame.atlas = match update_state .graphics .atlas_loader .load_animation_atlas( &update_state.graphics, update_state.filesystem, animation, - ) - .unwrap(); // TODO get rid of this unwrap + ) { + Ok(atlas) => atlas, + Err(e) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + animation.animation_name, + animation.id + 1, + animation.name, + )), + ); + return true; + } + }; self.frame = self .frame .clamp(0, animation.frames.len().saturating_sub(1) as i32); @@ -842,7 +868,8 @@ impl luminol_core::Window for Window { ); } } - modified |= Self::show_frame_edit( + + let (inner_modified, abort) = Self::show_frame_edit( ui, update_state, clip_rect, @@ -851,7 +878,15 @@ impl luminol_core::Window for Window { animation, &mut self.frame, ); - }); + + modified |= inner_modified; + + abort + }).inner; + + if abort { + return true; + } ui.with_padded_stripe(false, |ui| { modified |= ui @@ -891,11 +926,15 @@ impl luminol_core::Window for Window { }); self.previous_animation = Some(animation.id); + false }, ) }); - if response.is_some_and(|ir| ir.inner.is_some_and(|ir| ir.inner.modified)) { + if response + .as_ref() + .is_some_and(|ir| ir.inner.as_ref().is_some_and(|ir| ir.inner.modified)) + { modified = true; } @@ -907,5 +946,9 @@ impl luminol_core::Window for Window { drop(animations); *update_state.data = data; // restore data + + if response.is_some_and(|ir| ir.inner.is_some_and(|ir| ir.inner.inner == Some(true))) { + *open = false; + } } } From 29ba5324daaed2762dfc79aa58a9f43113f30494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 19 Jul 2024 17:33:49 -0400 Subject: [PATCH 031/109] Implement onion skinning for animation cells --- crates/components/src/animation_frame_view.rs | 62 +++-- crates/graphics/src/frame.rs | 222 +++++++++++++----- .../graphics/src/primitives/sprite/graphic.rs | 12 +- crates/graphics/src/primitives/sprite/mod.rs | 10 +- crates/ui/src/windows/animations.rs | 212 +++++++++-------- 5 files changed, 334 insertions(+), 184 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index ca7dccb9..d23c7402 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -187,11 +187,6 @@ impl AnimationFrameView { // Find the cell that the cursor is hovering over; if multiple cells are hovered we // prioritize the one with the greatest index - let cell_rect_iter = self - .frame - .sprites - .iter() - .map(|(i, (_, cell_rect))| (i, (*cell_rect * scale).translate(offset))); if response.clicked() { self.selected_cell_index = None; } @@ -199,15 +194,20 @@ impl AnimationFrameView { self.hovered_cell_index = ui .input(|i| !i.modifiers.shift) .then(|| { - cell_rect_iter.clone().rev().find_map(|(i, cell_rect)| { - (response.hovered() && ui.rect_contains_pointer(cell_rect)).then(|| { - if response.clicked() { - // If the hovered cell was clicked, make it the selected cell - self.selected_cell_index = Some(i); - } - i + self.frame + .cells + .iter() + .map(|(i, cell)| (i, (cell.rect * scale).translate(offset))) + .rev() + .find_map(|(i, cell_rect)| { + (response.hovered() && ui.rect_contains_pointer(cell_rect)).then(|| { + if response.clicked() { + // If the hovered cell was clicked, make it the selected cell + self.selected_cell_index = Some(i); + } + i + }) }) - }) }) .flatten(); } @@ -223,9 +223,10 @@ impl AnimationFrameView { self.hovered_cell_drag_offset, response.drag_started_by(egui::PointerButton::Primary), ) { - let (_, cell_rect) = self.frame.sprites[i]; - self.hovered_cell_drag_offset = - Some(cell_rect.center() - (response.hover_pos().unwrap() - offset) / scale); + self.hovered_cell_drag_offset = Some( + self.frame.cells[i].rect.center() + - (response.hover_pos().unwrap() - offset) / scale, + ); } if let Some(drag_offset) = self.hovered_cell_drag_offset { @@ -242,8 +243,29 @@ impl AnimationFrameView { self.hovered_cell_drag_pos = None; } + // Draw a gray rectangle on the border of every onion-skinned cell + if self.frame.enable_onion_skin { + for cell_rect in self + .frame + .onion_skin_cells + .iter() + .map(|(_, cell)| (cell.rect * scale).translate(offset)) + { + ui.painter().rect_stroke( + cell_rect, + 5., + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + } + } + // Draw a white rectangle on the border of every cell - for (_, cell_rect) in cell_rect_iter { + for cell_rect in self + .frame + .cells + .iter() + .map(|(_, cell)| (cell.rect * scale).translate(offset)) + { ui.painter().rect_stroke( cell_rect, 5., @@ -260,16 +282,14 @@ impl AnimationFrameView { // Draw a yellow rectangle on the border of the hovered cell if let Some(i) = self.hovered_cell_index { - let (_, cell_rect) = self.frame.sprites[i]; - let cell_rect = (cell_rect * scale).translate(offset); + let cell_rect = (self.frame.cells[i].rect * scale).translate(offset); ui.painter() .rect_stroke(cell_rect, 5., egui::Stroke::new(3., egui::Color32::YELLOW)); } // Draw a magenta rectangle on the border of the selected cell if let Some(i) = self.selected_cell_index { - let (_, cell_rect) = self.frame.sprites[i]; - let cell_rect = (cell_rect * scale).translate(offset); + let cell_rect = (self.frame.cells[i].rect * scale).translate(offset); ui.painter().rect_stroke( cell_rect, 5., diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index b800c971..91b4fa32 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -26,8 +26,16 @@ const CELL_OFFSET: glam::Vec2 = glam::Vec2::splat(-(CELL_SIZE as f32) / 2.); pub struct Frame { pub atlas: Atlas, - pub sprites: OptionVec<(Sprite, egui::Rect)>, + pub cells: OptionVec, + pub onion_skin_cells: OptionVec, pub viewport: Viewport, + + pub enable_onion_skin: bool, +} + +pub struct Cell { + pub sprite: Sprite, + pub rect: egui::Rect, } impl Frame { @@ -44,55 +52,137 @@ impl Frame { let mut frame = Self { atlas, - sprites: Default::default(), + cells: Default::default(), + onion_skin_cells: Default::default(), viewport, + enable_onion_skin: false, }; - frame.rebuild_all_cells( - graphics_state, - &animation.frames[frame_index], - animation.animation_hue, - ); + frame.rebuild_all_cells(graphics_state, animation, frame_index); frame } - pub fn rebuild_cell( + pub fn rebuild_all_cells( &mut self, graphics_state: &GraphicsState, - frame: &luminol_data::rpg::animation::Frame, - hue: i32, - cell_index: usize, + animation: &luminol_data::rpg::Animation, + frame_index: usize, ) { - if let Some(sprite) = self.sprite_from_cell_data(graphics_state, frame, hue, cell_index) { - self.sprites.insert(cell_index, sprite); - } else { - let _ = self.sprites.try_remove(cell_index); - } + let mut cells = std::mem::take(&mut self.cells); + cells.clear(); + cells.extend( + (0..cells + .len() + .max(animation.frames[frame_index].cell_data.xsize())) + .filter_map(|i| { + self.cell_from_cell_data( + graphics_state, + &animation.frames[frame_index], + animation.animation_hue, + i, + 1., + ) + .map(|cell| (i, cell)) + }), + ); + self.cells = cells; + + let mut cells = std::mem::take(&mut self.onion_skin_cells); + cells.clear(); + cells.extend( + (0..cells.len().max( + animation.frames[frame_index.saturating_sub(1)] + .cell_data + .xsize(), + )) + .filter_map(|i| { + self.cell_from_cell_data( + graphics_state, + &animation.frames[frame_index.saturating_sub(1)], + animation.animation_hue, + i, + 0.5, + ) + .map(|cell| (i, cell)) + }), + ); + self.cells = cells; } - pub fn rebuild_all_cells( + pub fn update_cell( &mut self, graphics_state: &GraphicsState, - frame: &luminol_data::rpg::animation::Frame, - hue: i32, + animation: &luminol_data::rpg::Animation, + frame_index: usize, + cell_index: usize, ) { - let mut sprites = std::mem::take(&mut self.sprites); - sprites.clear(); - sprites.extend( - (0..frame.cell_data.xsize().max(self.sprites.len())).filter_map(|i| { - self.sprite_from_cell_data(graphics_state, frame, hue, i) - .map(|s| (i, s)) - }), + let cells = std::mem::take(&mut self.cells); + self.cells = self.update_cell_inner( + cells, + graphics_state, + &animation.frames[frame_index], + animation.animation_hue, + cell_index, + 1., + ); + + let cells = std::mem::take(&mut self.onion_skin_cells); + self.onion_skin_cells = self.update_cell_inner( + cells, + graphics_state, + &animation.frames[frame_index.saturating_sub(1)], + animation.animation_hue, + cell_index, + 0.5, ); - self.sprites = sprites; } - pub fn sprite_from_cell_data( + pub fn update_all_cells( + &mut self, + graphics_state: &GraphicsState, + animation: &luminol_data::rpg::Animation, + frame_index: usize, + ) { + for cell_index in 0..self + .cells + .len() + .max(animation.frames[frame_index].cell_data.xsize()) + { + let cells = std::mem::take(&mut self.cells); + self.cells = self.update_cell_inner( + cells, + graphics_state, + &animation.frames[frame_index], + animation.animation_hue, + cell_index, + 1., + ); + } + + for cell_index in 0..self.onion_skin_cells.len().max( + animation.frames[frame_index.saturating_sub(1)] + .cell_data + .xsize(), + ) { + let cells = std::mem::take(&mut self.onion_skin_cells); + self.onion_skin_cells = self.update_cell_inner( + cells, + graphics_state, + &animation.frames[frame_index.saturating_sub(1)], + animation.animation_hue, + cell_index, + 0.5, + ); + } + } + + fn cell_from_cell_data( &self, graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, hue: i32, cell_index: usize, - ) -> Option<(Sprite, egui::Rect)> { + opacity_multiplier: f32, + ) -> Option { (cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0).then(|| { let id = frame.cell_data[(cell_index, 0)]; let offset_x = frame.cell_data[(cell_index, 1)] as f32; @@ -110,12 +200,13 @@ impl Frame { let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); - ( - Sprite::new_with_rotation( + Cell { + sprite: Sprite::new_with_rotation( graphics_state, self.atlas.calc_quad(id), hue, opacity, + opacity_multiplier, blend_mode, &self.atlas.atlas_texture, &self.viewport, @@ -128,23 +219,26 @@ impl Frame { ), if flip { -rotation } else { rotation }, ), - egui::Rect::from_center_size( + + rect: egui::Rect::from_center_size( egui::pos2(offset_x, offset_y), egui::Vec2::splat(CELL_SIZE as f32 * (cos.abs() + sin.abs()) * scale), ), - ) + } }) } - pub fn update_cell_sprite( - &mut self, + fn update_cell_inner( + &self, + mut cells: OptionVec, graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, hue: i32, cell_index: usize, - ) { + opacity_multiplier: f32, + ) -> OptionVec { if cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0 { - if let Some((sprite, cell_rect)) = self.sprites.get_mut(cell_index) { + if let Some(cell) = cells.get_mut(cell_index) { let id = frame.cell_data[(cell_index, 0)]; let offset_x = frame.cell_data[(cell_index, 1)] as f32; let offset_y = frame.cell_data[(cell_index, 2)] as f32; @@ -161,7 +255,7 @@ impl Frame { let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); - sprite.transform.set( + cell.sprite.transform.set( &graphics_state.render_state, glam::vec2(offset_x, offset_y) + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) @@ -169,50 +263,42 @@ impl Frame { scale * flip_vec, ); - sprite.graphic.set( + cell.sprite.graphic.set( &graphics_state.render_state, hue, opacity, - 1., + opacity_multiplier, if flip { -rotation } else { rotation }, ); - sprite.set_quad( + cell.sprite.set_quad( &graphics_state.render_state, self.atlas.calc_quad(id), self.atlas.atlas_texture.size(), ); - sprite.blend_mode = blend_mode; + cell.sprite.blend_mode = blend_mode; - *cell_rect = egui::Rect::from_center_size( + cell.rect = egui::Rect::from_center_size( egui::pos2(offset_x, offset_y), egui::Vec2::splat(CELL_SIZE as f32 * (cos.abs() + sin.abs()) * scale), ); - } else if let Some((sprite, cell_rect)) = - self.sprite_from_cell_data(graphics_state, frame, hue, cell_index) + } else if let Some(cell) = + self.cell_from_cell_data(graphics_state, frame, hue, cell_index, opacity_multiplier) { - self.sprites.insert(cell_index, (sprite, cell_rect)); + cells.insert(cell_index, cell); } } else { - let _ = self.sprites.try_remove(cell_index); + let _ = cells.try_remove(cell_index); } - } - pub fn update_all_cell_sprites( - &mut self, - graphics_state: &GraphicsState, - frame: &luminol_data::rpg::animation::Frame, - hue: i32, - ) { - for cell_index in 0..frame.cell_data.xsize().max(self.sprites.len()) { - self.update_cell_sprite(graphics_state, frame, hue, cell_index); - } + cells } } pub struct Prepared { - sprites: Vec<::Prepared>, + cells: Vec<::Prepared>, + onion_skin_cells: Vec<::Prepared>, } impl Renderable for Frame { @@ -220,18 +306,30 @@ impl Renderable for Frame { fn prepare(&mut self, graphics_state: &std::sync::Arc) -> Self::Prepared { Self::Prepared { - sprites: self - .sprites + cells: self + .cells .iter_mut() - .map(|(_, (sprite, _))| sprite.prepare(graphics_state)) + .map(|(_, cell)| cell.sprite.prepare(graphics_state)) .collect(), + + onion_skin_cells: if self.enable_onion_skin { + self.onion_skin_cells + .iter_mut() + .map(|(_, cell)| cell.sprite.prepare(graphics_state)) + .collect() + } else { + Default::default() + }, } } } impl Drawable for Prepared { fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { - for sprite in &self.sprites { + for sprite in &self.onion_skin_cells { + sprite.draw(render_pass); + } + for sprite in &self.cells { sprite.draw(render_pass); } } diff --git a/crates/graphics/src/primitives/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs index 5e684f65..14eac783 100644 --- a/crates/graphics/src/primitives/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -37,12 +37,22 @@ struct Data { impl Graphic { pub fn new(graphics_state: &GraphicsState, hue: i32, opacity: i32, rotation: f32) -> Self { + Self::new_with_opacity_multiplier(graphics_state, hue, opacity, 1., rotation) + } + + pub fn new_with_opacity_multiplier( + graphics_state: &GraphicsState, + hue: i32, + opacity: i32, + opacity_multiplier: f32, + rotation: f32, + ) -> Self { let hue = (hue % 360) as f32 / 360.0; let opacity = opacity as f32 / 255.; let data = Data { hue, opacity, - opacity_multiplier: 1., + opacity_multiplier, rotation, }; diff --git a/crates/graphics/src/primitives/sprite/mod.rs b/crates/graphics/src/primitives/sprite/mod.rs index 9a018421..3236f2dd 100644 --- a/crates/graphics/src/primitives/sprite/mod.rs +++ b/crates/graphics/src/primitives/sprite/mod.rs @@ -54,6 +54,7 @@ impl Sprite { quad, hue, opacity, + 1., blend_mode, texture, viewport, @@ -68,6 +69,7 @@ impl Sprite { quad: Quad, hue: i32, opacity: i32, + opacity_multiplier: f32, blend_mode: luminol_data::BlendMode, // arranged in order of use in bind group texture: &Texture, @@ -77,7 +79,13 @@ impl Sprite { ) -> Self { let vertices = vertices::Vertices::from_quads(&graphics_state.render_state, &[quad], texture.size()); - let graphic = graphic::Graphic::new(graphics_state, hue, opacity, rotation); + let graphic = graphic::Graphic::new_with_opacity_multiplier( + graphics_state, + hue, + opacity, + opacity_multiplier, + rotation, + ); let mut bind_group_builder = BindGroupBuilder::new(); bind_group_builder diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index db30c4c4..d6498064 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -36,8 +36,7 @@ pub struct Window { selected_animation_name: Option, previous_animation: Option, previous_timing_frame: Option, - - frame: i32, + frame_edit_state: FrameEditState, frame_view: Option, collapsing_view: luminol_components::CollapsingView, @@ -46,6 +45,11 @@ pub struct Window { view: luminol_components::DatabaseView, } +struct FrameEditState { + frame_index: usize, + enable_onion_skin: bool, +} + struct Modals { copy_frames: luminol_modals::animations::copy_frames_tool::Modal, clear_frames: luminol_modals::animations::clear_frames_tool::Modal, @@ -68,7 +72,10 @@ impl Default for Window { selected_animation_name: None, previous_animation: None, previous_timing_frame: None, - frame: 0, + frame_edit_state: FrameEditState { + frame_index: 0, + enable_onion_skin: false, + }, frame_view: None, collapsing_view: luminol_components::CollapsingView::new(), timing_se_picker: SoundPicker::new( @@ -274,7 +281,7 @@ impl Window { modals: &mut Modals, maybe_frame_view: &mut Option, animation: &mut luminol_data::rpg::Animation, - frame_index: &mut i32, + state: &mut FrameEditState, ) -> (bool, bool) { let mut modified = false; @@ -285,7 +292,7 @@ impl Window { match luminol_components::AnimationFrameView::new( update_state, animation, - *frame_index as usize, + state.frame_index, ) { Ok(atlas) => atlas, Err(e) => { @@ -314,24 +321,31 @@ impl Window { .fixed_decimals(0), )); - *frame_index = (*frame_index).clamp(0, animation.frames.len().saturating_sub(1) as i32); - *frame_index += 1; + state.frame_index = state + .frame_index + .min(animation.frames.len().saturating_sub(1)); + state.frame_index += 1; let changed = ui .add(luminol_components::Field::new( "Frame", - egui::DragValue::new(frame_index) - .clamp_range(1..=animation.frames.len() as i32), + egui::DragValue::new(&mut state.frame_index) + .clamp_range(1..=animation.frames.len()), )) .changed(); - *frame_index -= 1; + state.frame_index -= 1; if changed { - frame_view.frame.update_all_cell_sprites( + frame_view.frame.update_all_cells( &update_state.graphics, - &animation.frames[*frame_index as usize], - animation.animation_hue, + animation, + state.frame_index, ); } + ui.add(luminol_components::Field::new( + "Onion Skinning", + egui::Checkbox::without_text(&mut state.enable_onion_skin), + )); + ui.with_layout( egui::Layout { main_dir: egui::Direction::RightToLeft, @@ -346,16 +360,16 @@ impl Window { }) .show(ui, |ui| { ui.menu_button("Tools ⏷", |ui| { - ui.add_enabled_ui(*frame_index != 0, |ui| { + ui.add_enabled_ui(state.frame_index != 0, |ui| { if ui.button("Copy previous frame").clicked() - && *frame_index != 0 + && state.frame_index != 0 { - animation.frames[*frame_index as usize] = - animation.frames[*frame_index as usize - 1].clone(); - frame_view.frame.update_all_cell_sprites( + animation.frames[state.frame_index] = + animation.frames[state.frame_index - 1].clone(); + frame_view.frame.update_all_cells( &update_state.graphics, - &animation.frames[*frame_index as usize], - animation.animation_hue, + animation, + state.frame_index, ); modified = true; } @@ -382,7 +396,7 @@ impl Window { if modals .copy_frames - .show_window(ui.ctx(), *frame_index as usize, animation.frames.len()) + .show_window(ui.ctx(), state.frame_index, animation.frames.len()) { let mut iter = 0..modals.copy_frames.frame_count; while let Some(i) = if modals.copy_frames.dst_frame <= modals.copy_frames.src_frame { @@ -393,32 +407,28 @@ impl Window { animation.frames[modals.copy_frames.dst_frame + i] = animation.frames[modals.copy_frames.src_frame + i].clone(); } - frame_view.frame.update_all_cell_sprites( - &update_state.graphics, - &animation.frames[*frame_index as usize], - animation.animation_hue, - ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); modified = true; } if modals .clear_frames - .show_window(ui.ctx(), *frame_index as usize, animation.frames.len()) + .show_window(ui.ctx(), state.frame_index, animation.frames.len()) { for i in modals.clear_frames.start_frame..=modals.clear_frames.end_frame { animation.frames[i] = Default::default(); } - frame_view.frame.update_all_cell_sprites( - &update_state.graphics, - &animation.frames[*frame_index as usize], - animation.animation_hue, - ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); modified = true; } if modals .tween - .show_window(ui.ctx(), *frame_index as usize, animation.frames.len()) + .show_window(ui.ctx(), state.frame_index, animation.frames.len()) { for i in modals.tween.start_cell..=modals.tween.end_cell { let data = &animation.frames[modals.tween.start_frame].cell_data; @@ -483,18 +493,16 @@ impl Window { } } } - frame_view.frame.update_all_cell_sprites( - &update_state.graphics, - &animation.frames[*frame_index as usize], - animation.animation_hue, - ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); modified = true; } let num_patterns = frame_view.frame.atlas.animation_height / CELL_SIZE * ANIMATION_COLUMNS; if modals.batch_edit.show_window( ui.ctx(), - *frame_index as usize, + state.frame_index, animation.frames.len(), num_patterns, ) { @@ -595,11 +603,9 @@ impl Window { } } } - frame_view.frame.update_all_cell_sprites( - &update_state.graphics, - &animation.frames[*frame_index as usize], - animation.animation_hue, - ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); modified = true; } @@ -616,7 +622,7 @@ impl Window { .inner }); - let frame = &mut animation.frames[*frame_index as usize]; + let frame = &mut animation.frames[state.frame_index]; if frame_view .selected_cell_index @@ -639,16 +645,18 @@ impl Window { ) { if (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) != drag_pos { (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) = drag_pos; - frame_view.frame.update_cell_sprite( + frame_view.frame.update_cell( &update_state.graphics, - frame, - animation.animation_hue, + animation, + state.frame_index, i, ); modified = true; } } + let frame = &mut animation.frames[state.frame_index]; + if let Some(i) = frame_view.selected_cell_index { let mut properties_modified = false; @@ -734,7 +742,7 @@ impl Window { .add(luminol_components::Field::new( "Blending", luminol_components::EnumComboBox::new( - (animation.id, *frame_index, i, 7usize), + (animation.id, state.frame_index, i, 7usize), &mut blend_mode, ), )) @@ -750,10 +758,10 @@ impl Window { }); if properties_modified { - frame_view.frame.update_cell_sprite( + frame_view.frame.update_cell( &update_state.graphics, - frame, - animation.animation_hue, + animation, + state.frame_index, i, ); modified = true; @@ -761,6 +769,7 @@ impl Window { } ui.allocate_ui_at_rect(canvas_rect, |ui| { + frame_view.frame.enable_onion_skin = state.enable_onion_skin && state.frame_index != 0; let response = frame_view.ui(ui, update_state, clip_rect); // If the pointer is hovering over the frame view, prevent parent widgets @@ -832,57 +841,62 @@ impl luminol_core::Window for Window { .changed(); }); - let abort = ui.with_padded_stripe(true, |ui| { - if let Some(frame_view) = &mut self.frame_view { - if self.previous_animation != Some(animation.id) { - self.modals.close_all(); - frame_view.frame.atlas = match update_state - .graphics - .atlas_loader - .load_animation_atlas( + let abort = ui + .with_padded_stripe(true, |ui| { + if let Some(frame_view) = &mut self.frame_view { + if self.previous_animation != Some(animation.id) { + self.modals.close_all(); + frame_view.frame.atlas = match update_state + .graphics + .atlas_loader + .load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ) { + Ok(atlas) => atlas, + Err(e) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err( + format!( + "While loading texture {:?} for animation {:0>4} {:?}", + animation.animation_name, + animation.id + 1, + animation.name, + ), + ), + ); + return true; + } + }; + self.frame_edit_state.frame_index = self + .frame_edit_state + .frame_index + .min(animation.frames.len().saturating_sub(1)); + frame_view.frame.rebuild_all_cells( &update_state.graphics, - update_state.filesystem, animation, - ) { - Ok(atlas) => atlas, - Err(e) => { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "While loading texture {:?} for animation {:0>4} {:?}", - animation.animation_name, - animation.id + 1, - animation.name, - )), - ); - return true; - } - }; - self.frame = self - .frame - .clamp(0, animation.frames.len().saturating_sub(1) as i32); - frame_view.frame.rebuild_all_cells( - &update_state.graphics, - &animation.frames[self.frame as usize], - animation.animation_hue, - ); + self.frame_edit_state.frame_index, + ); + } } - } - - let (inner_modified, abort) = Self::show_frame_edit( - ui, - update_state, - clip_rect, - &mut self.modals, - &mut self.frame_view, - animation, - &mut self.frame, - ); - - modified |= inner_modified; - abort - }).inner; + let (inner_modified, abort) = Self::show_frame_edit( + ui, + update_state, + clip_rect, + &mut self.modals, + &mut self.frame_view, + animation, + &mut self.frame_edit_state, + ); + + modified |= inner_modified; + + abort + }) + .inner; if abort { return true; From e4235fea62e19bcc1bbc9635f4309b472b69ac68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 19 Jul 2024 18:01:28 -0400 Subject: [PATCH 032/109] Fix typo in previous commit --- crates/graphics/src/frame.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 91b4fa32..c66d896c 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -105,7 +105,7 @@ impl Frame { .map(|cell| (i, cell)) }), ); - self.cells = cells; + self.onion_skin_cells = cells; } pub fn update_cell( From 1b21647871ee1028ecba6af10b1620b0e0e723ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 19 Jul 2024 18:02:03 -0400 Subject: [PATCH 033/109] Change "Onion Skinning" to "Onion Skin" for brevity --- crates/ui/src/windows/animations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index d6498064..ce6ab713 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -342,7 +342,7 @@ impl Window { } ui.add(luminol_components::Field::new( - "Onion Skinning", + "Onion Skin", egui::Checkbox::without_text(&mut state.enable_onion_skin), )); From 14a31ce6580d28ae5c4539416f117bbbc25a8d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 19 Jul 2024 18:11:08 -0400 Subject: [PATCH 034/109] Remove unnecessary arguments from `luminol_graphics::Frame::new` --- crates/components/src/animation_frame_view.rs | 3 --- crates/graphics/src/frame.rs | 13 +++---------- crates/ui/src/windows/animations.rs | 15 +++++++-------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index d23c7402..6bafe8d0 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -39,7 +39,6 @@ impl AnimationFrameView { pub fn new( update_state: &luminol_core::UpdateState<'_>, animation: &luminol_data::rpg::Animation, - frame_index: usize, ) -> color_eyre::Result { let data_id = egui::Id::new("luminol_animation_frame_view").with( update_state @@ -60,8 +59,6 @@ impl AnimationFrameView { update_state.filesystem, animation, )?, - animation, - frame_index, ); Ok(Self { diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index c66d896c..7bc3ae18 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -39,26 +39,19 @@ pub struct Cell { } impl Frame { - pub fn new( - graphics_state: &GraphicsState, - atlas: Atlas, - animation: &luminol_data::rpg::Animation, - frame_index: usize, - ) -> Self { + pub fn new(graphics_state: &GraphicsState, atlas: Atlas) -> Self { let viewport = Viewport::new( graphics_state, glam::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32), ); - let mut frame = Self { + Self { atlas, cells: Default::default(), onion_skin_cells: Default::default(), viewport, enable_onion_skin: false, - }; - frame.rebuild_all_cells(graphics_state, animation, frame_index); - frame + } } pub fn rebuild_all_cells( diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index ce6ab713..d2009770 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -288,12 +288,8 @@ impl Window { let frame_view = if let Some(frame_view) = maybe_frame_view { frame_view } else { - *maybe_frame_view = Some( - match luminol_components::AnimationFrameView::new( - update_state, - animation, - state.frame_index, - ) { + let mut frame_view = + match luminol_components::AnimationFrameView::new(update_state, animation) { Ok(atlas) => atlas, Err(e) => { luminol_core::error!( @@ -307,8 +303,11 @@ impl Window { ); return (modified, true); } - }, - ); + }; + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + *maybe_frame_view = Some(frame_view); maybe_frame_view.as_mut().unwrap() }; From 456c403ace4b66b18a3dfb86630360cfc2f971e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 20 Jul 2024 18:37:18 -0400 Subject: [PATCH 035/109] Implement cell picker for the animation editor --- crates/components/src/animation_frame_view.rs | 17 +-- crates/components/src/cellpicker.rs | 115 +++++++++++++++ crates/components/src/lib.rs | 2 + crates/data/src/rgss_structs.rs | 6 + crates/graphics/src/data/viewport.rs | 19 ++- crates/graphics/src/primitives/cells/mod.rs | 4 +- crates/ui/src/windows/animations.rs | 133 +++++++++++------- 7 files changed, 223 insertions(+), 73 deletions(-) create mode 100644 crates/components/src/cellpicker.rs diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 6bafe8d0..4564afd7 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -38,8 +38,8 @@ pub struct AnimationFrameView { impl AnimationFrameView { pub fn new( update_state: &luminol_core::UpdateState<'_>, - animation: &luminol_data::rpg::Animation, - ) -> color_eyre::Result { + atlas: luminol_graphics::primitives::cells::Atlas, + ) -> Self { let data_id = egui::Id::new("luminol_animation_frame_view").with( update_state .project_config @@ -52,16 +52,9 @@ impl AnimationFrameView { .ctx .data_mut(|d| *d.get_persisted_mut_or_insert_with(data_id, || (egui::Vec2::ZERO, 50.))); - let frame = luminol_graphics::Frame::new( - &update_state.graphics, - update_state.graphics.atlas_loader.load_animation_atlas( - &update_state.graphics, - update_state.filesystem, - animation, - )?, - ); + let frame = luminol_graphics::Frame::new(&update_state.graphics, atlas); - Ok(Self { + Self { frame, selected_cell_index: None, hovered_cell_index: None, @@ -71,7 +64,7 @@ impl AnimationFrameView { scale, previous_scale: scale, data_id, - }) + } } pub fn ui( diff --git a/crates/components/src/cellpicker.rs b/crates/components/src/cellpicker.rs new file mode 100644 index 00000000..21898ddf --- /dev/null +++ b/crates/components/src/cellpicker.rs @@ -0,0 +1,115 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use luminol_graphics::Renderable; + +use luminol_graphics::primitives::cells::{Atlas, ANIMATION_COLUMNS, CELL_SIZE}; +use luminol_graphics::{Cells, Transform, Viewport}; + +pub struct Cellpicker { + pub selected_cell: u32, + pub viewport: Viewport, + pub view: Cells, +} + +impl Cellpicker { + pub fn new(graphics_state: &luminol_graphics::GraphicsState, atlas: Atlas) -> Self { + let num_patterns = atlas.animation_height / CELL_SIZE * ANIMATION_COLUMNS; + + let cells = luminol_data::Table2::new_data( + num_patterns as usize, + 1, + (0..num_patterns as i16).collect(), + ); + + let viewport = Viewport::new( + graphics_state, + glam::vec2((num_patterns * CELL_SIZE) as f32, CELL_SIZE as f32) / 2., + ); + + let view = Cells::new( + graphics_state, + &cells, + atlas, + &viewport, + Transform::unit(graphics_state), + ); + + Self { + selected_cell: 0, + viewport, + view, + } + } + + pub fn ui( + &mut self, + update_state: &luminol_core::UpdateState<'_>, + ui: &mut egui::Ui, + scroll_rect: egui::Rect, + ) -> egui::Response { + let num_patterns = self.view.atlas.animation_height / CELL_SIZE * ANIMATION_COLUMNS; + + let (canvas_rect, response) = ui.allocate_exact_size( + egui::vec2((num_patterns * CELL_SIZE) as f32, CELL_SIZE as f32) / 2., + egui::Sense::click_and_drag(), + ); + + let absolute_scroll_rect = ui + .ctx() + .screen_rect() + .intersect(scroll_rect.translate(canvas_rect.min.to_vec2())); + let scroll_rect = absolute_scroll_rect.translate(-canvas_rect.min.to_vec2()); + + self.view.transform.set_position( + &update_state.graphics.render_state, + glam::vec2(-scroll_rect.left() * 2., 0.), + ); + self.viewport.set( + &update_state.graphics.render_state, + glam::vec2(scroll_rect.width(), scroll_rect.height()), + glam::Vec2::ZERO, + glam::Vec2::splat(0.5), + ); + + let painter = luminol_graphics::Painter::new(self.view.prepare(&update_state.graphics)); + ui.painter() + .add(luminol_egui_wgpu::Callback::new_paint_callback( + absolute_scroll_rect, + painter, + )); + + let rect = (egui::Rect::from_min_size( + egui::pos2((self.selected_cell * CELL_SIZE) as f32, 0.), + egui::Vec2::splat(CELL_SIZE as f32), + ) / 2.) + .translate(canvas_rect.min.to_vec2()); + ui.painter() + .rect_stroke(rect, 5.0, egui::Stroke::new(1.0, egui::Color32::WHITE)); + + if response.clicked() { + if let Some(pos) = response.interact_pointer_pos() { + self.selected_cell = + ((pos - canvas_rect.min) / CELL_SIZE as f32 * 2.).x.floor() as u32; + } + } + + self.selected_cell = self.selected_cell.min(num_patterns.saturating_sub(1)); + + response + } +} diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index b738d6ca..1c2adcc4 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -51,6 +51,8 @@ pub use collapsing_view::CollapsingView; mod animation_frame_view; pub use animation_frame_view::AnimationFrameView; +mod cellpicker; +pub use cellpicker::Cellpicker; mod id_vec; pub use id_vec::{IdVecPlusMinusSelection, IdVecSelection, RankSelection}; diff --git a/crates/data/src/rgss_structs.rs b/crates/data/src/rgss_structs.rs index c07b97c6..74b88cb9 100644 --- a/crates/data/src/rgss_structs.rs +++ b/crates/data/src/rgss_structs.rs @@ -257,6 +257,12 @@ impl Table2 { } } + #[must_use] + pub fn new_data(xsize: usize, ysize: usize, data: Vec) -> Self { + assert_eq!(xsize * ysize, data.len()); + Self { xsize, ysize, data } + } + /// Width of the table. #[must_use] pub fn xsize(&self) -> usize { diff --git a/crates/graphics/src/data/viewport.rs b/crates/graphics/src/data/viewport.rs index aa39f305..9a455f58 100644 --- a/crates/graphics/src/data/viewport.rs +++ b/crates/graphics/src/data/viewport.rs @@ -55,8 +55,10 @@ impl Viewport { } pub fn set_size(&mut self, render_state: &luminol_egui_wgpu::RenderState, size: glam::Vec2) { - self.data.viewport_size = size; - self.regen_buffer(render_state); + if self.data.viewport_size != size { + self.data.viewport_size = size; + self.regen_buffer(render_state); + } } pub fn set( @@ -66,10 +68,15 @@ impl Viewport { translation: glam::Vec2, scale: glam::Vec2, ) { - self.data.viewport_size = size; - self.data.viewport_translation = translation; - self.data.viewport_scale = scale; - self.regen_buffer(render_state); + if self.data.viewport_size != size + || self.data.viewport_translation != translation + || self.data.viewport_scale != scale + { + self.data.viewport_size = size; + self.data.viewport_translation = translation; + self.data.viewport_scale = scale; + self.regen_buffer(render_state); + } } pub fn as_buffer(&self) -> &wgpu::Buffer { diff --git a/crates/graphics/src/primitives/cells/mod.rs b/crates/graphics/src/primitives/cells/mod.rs index c909ba2e..643cbcda 100644 --- a/crates/graphics/src/primitives/cells/mod.rs +++ b/crates/graphics/src/primitives/cells/mod.rs @@ -34,6 +34,7 @@ pub(crate) mod shader; pub struct Cells { pub display: Display, + pub atlas: Atlas, pub transform: Transform, instances: Arc, @@ -45,7 +46,7 @@ impl Cells { graphics_state: &GraphicsState, cells: &luminol_data::Table2, // in order of use in bind group - atlas: &Atlas, + atlas: Atlas, viewport: &Viewport, transform: Transform, ) -> Self { @@ -68,6 +69,7 @@ impl Cells { Self { display, + atlas, transform, instances: Arc::new(instances), diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index d2009770..455a8708 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -38,7 +38,6 @@ pub struct Window { previous_timing_frame: Option, frame_edit_state: FrameEditState, - frame_view: Option, collapsing_view: luminol_components::CollapsingView, timing_se_picker: SoundPicker, modals: Modals, @@ -48,6 +47,8 @@ pub struct Window { struct FrameEditState { frame_index: usize, enable_onion_skin: bool, + frame_view: Option, + cellpicker: Option, } struct Modals { @@ -75,8 +76,9 @@ impl Default for Window { frame_edit_state: FrameEditState { frame_index: 0, enable_onion_skin: false, + frame_view: None, + cellpicker: None, }, - frame_view: None, collapsing_view: luminol_components::CollapsingView::new(), timing_se_picker: SoundPicker::new( luminol_audio::Source::SE, @@ -279,36 +281,48 @@ impl Window { update_state: &mut luminol_core::UpdateState<'_>, clip_rect: egui::Rect, modals: &mut Modals, - maybe_frame_view: &mut Option, animation: &mut luminol_data::rpg::Animation, state: &mut FrameEditState, ) -> (bool, bool) { let mut modified = false; - let frame_view = if let Some(frame_view) = maybe_frame_view { + let frame_view = if let Some(frame_view) = &mut state.frame_view { frame_view } else { - let mut frame_view = - match luminol_components::AnimationFrameView::new(update_state, animation) { - Ok(atlas) => atlas, - Err(e) => { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "While loading texture {:?} for animation {:0>4} {:?}", - animation.animation_name, - animation.id + 1, - animation.name, - )), - ); - return (modified, true); - } - }; + let atlas = match update_state.graphics.atlas_loader.load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ) { + Ok(atlas) => atlas, + Err(e) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + animation.animation_name, + animation.id + 1, + animation.name, + )), + ); + return (modified, true); + } + }; + let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); frame_view .frame .update_all_cells(&update_state.graphics, animation, state.frame_index); - *maybe_frame_view = Some(frame_view); - maybe_frame_view.as_mut().unwrap() + state.frame_view = Some(frame_view); + state.frame_view.as_mut().unwrap() + }; + + let cellpicker = if let Some(cellpicker) = &mut state.cellpicker { + cellpicker + } else { + let atlas = frame_view.frame.atlas.clone(); + let cellpicker = luminol_components::Cellpicker::new(&update_state.graphics, atlas); + state.cellpicker = Some(cellpicker); + state.cellpicker.as_mut().unwrap() }; ui.horizontal(|ui| { @@ -767,6 +781,10 @@ impl Window { } } + egui::ScrollArea::horizontal().show_viewport(ui, |ui, scroll_rect| { + cellpicker.ui(update_state, ui, scroll_rect); + }); + ui.allocate_ui_at_rect(canvas_rect, |ui| { frame_view.frame.enable_onion_skin = state.enable_onion_skin && state.frame_index != 0; let response = frame_view.ui(ui, update_state, clip_rect); @@ -842,43 +860,51 @@ impl luminol_core::Window for Window { let abort = ui .with_padded_stripe(true, |ui| { - if let Some(frame_view) = &mut self.frame_view { - if self.previous_animation != Some(animation.id) { - self.modals.close_all(); - frame_view.frame.atlas = match update_state - .graphics - .atlas_loader - .load_animation_atlas( - &update_state.graphics, - update_state.filesystem, - animation, - ) { - Ok(atlas) => atlas, - Err(e) => { - luminol_core::error!( - update_state.toasts, - e.wrap_err( - format!( - "While loading texture {:?} for animation {:0>4} {:?}", - animation.animation_name, - animation.id + 1, - animation.name, - ), - ), - ); - return true; - } - }; - self.frame_edit_state.frame_index = self - .frame_edit_state - .frame_index - .min(animation.frames.len().saturating_sub(1)); + if self.previous_animation != Some(animation.id) { + self.modals.close_all(); + self.frame_edit_state.frame_index = self + .frame_edit_state + .frame_index + .min(animation.frames.len().saturating_sub(1)); + + let atlas = match update_state + .graphics + .atlas_loader + .load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ) { + Ok(atlas) => atlas, + Err(e) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + animation.animation_name, + animation.id + 1, + animation.name, + ),), + ); + return true; + } + }; + + if let Some(frame_view) = &mut self.frame_edit_state.frame_view + { + frame_view.frame.atlas = atlas.clone(); frame_view.frame.rebuild_all_cells( &update_state.graphics, animation, self.frame_edit_state.frame_index, ); } + + self.frame_edit_state.cellpicker = + Some(luminol_components::Cellpicker::new( + &update_state.graphics, + atlas, + )); } let (inner_modified, abort) = Self::show_frame_edit( @@ -886,7 +912,6 @@ impl luminol_core::Window for Window { update_state, clip_rect, &mut self.modals, - &mut self.frame_view, animation, &mut self.frame_edit_state, ); From 33455590ded990d44e68bd99a39d3bb4a7b65643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 20 Jul 2024 19:06:18 -0400 Subject: [PATCH 036/109] Encapsulation --- crates/components/src/animation_frame_view.rs | 14 ++++++------ crates/components/src/cellpicker.rs | 21 +++++++++--------- crates/components/src/map_view.rs | 2 +- crates/components/src/tilepicker.rs | 4 ++-- crates/graphics/src/event.rs | 4 ++-- crates/graphics/src/frame.rs | 20 ++++++++++++----- crates/graphics/src/primitives/cells/atlas.rs | 22 +++++++++++++++++-- crates/graphics/src/primitives/cells/mod.rs | 2 +- crates/graphics/src/primitives/tiles/atlas.rs | 20 +++++++++++++---- crates/graphics/src/primitives/tiles/mod.rs | 2 +- crates/graphics/src/tilepicker.rs | 8 +++---- crates/modals/src/graphic_picker/event.rs | 2 +- crates/ui/src/windows/animations.rs | 17 +++++++------- 13 files changed, 89 insertions(+), 49 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 4564afd7..24ac328a 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -30,7 +30,7 @@ pub struct AnimationFrameView { pub pan: egui::Vec2, pub scale: f32, - pub previous_scale: f32, + previous_scale: f32, pub data_id: egui::Id, } @@ -185,7 +185,7 @@ impl AnimationFrameView { .input(|i| !i.modifiers.shift) .then(|| { self.frame - .cells + .cells() .iter() .map(|(i, cell)| (i, (cell.rect * scale).translate(offset))) .rev() @@ -214,7 +214,7 @@ impl AnimationFrameView { response.drag_started_by(egui::PointerButton::Primary), ) { self.hovered_cell_drag_offset = Some( - self.frame.cells[i].rect.center() + self.frame.cells()[i].rect.center() - (response.hover_pos().unwrap() - offset) / scale, ); } @@ -237,7 +237,7 @@ impl AnimationFrameView { if self.frame.enable_onion_skin { for cell_rect in self .frame - .onion_skin_cells + .onion_skin_cells() .iter() .map(|(_, cell)| (cell.rect * scale).translate(offset)) { @@ -252,7 +252,7 @@ impl AnimationFrameView { // Draw a white rectangle on the border of every cell for cell_rect in self .frame - .cells + .cells() .iter() .map(|(_, cell)| (cell.rect * scale).translate(offset)) { @@ -272,14 +272,14 @@ impl AnimationFrameView { // Draw a yellow rectangle on the border of the hovered cell if let Some(i) = self.hovered_cell_index { - let cell_rect = (self.frame.cells[i].rect * scale).translate(offset); + let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); ui.painter() .rect_stroke(cell_rect, 5., egui::Stroke::new(3., egui::Color32::YELLOW)); } // Draw a magenta rectangle on the border of the selected cell if let Some(i) = self.selected_cell_index { - let cell_rect = (self.frame.cells[i].rect * scale).translate(offset); + let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); ui.painter().rect_stroke( cell_rect, 5., diff --git a/crates/components/src/cellpicker.rs b/crates/components/src/cellpicker.rs index 21898ddf..26bfe442 100644 --- a/crates/components/src/cellpicker.rs +++ b/crates/components/src/cellpicker.rs @@ -17,7 +17,7 @@ use luminol_graphics::Renderable; -use luminol_graphics::primitives::cells::{Atlas, ANIMATION_COLUMNS, CELL_SIZE}; +use luminol_graphics::primitives::cells::{Atlas, CELL_SIZE}; use luminol_graphics::{Cells, Transform, Viewport}; pub struct Cellpicker { @@ -28,17 +28,15 @@ pub struct Cellpicker { impl Cellpicker { pub fn new(graphics_state: &luminol_graphics::GraphicsState, atlas: Atlas) -> Self { - let num_patterns = atlas.animation_height / CELL_SIZE * ANIMATION_COLUMNS; - let cells = luminol_data::Table2::new_data( - num_patterns as usize, + atlas.num_patterns() as usize, 1, - (0..num_patterns as i16).collect(), + (0..atlas.num_patterns() as i16).collect(), ); let viewport = Viewport::new( graphics_state, - glam::vec2((num_patterns * CELL_SIZE) as f32, CELL_SIZE as f32) / 2., + glam::vec2((atlas.num_patterns() * CELL_SIZE) as f32, CELL_SIZE as f32) / 2., ); let view = Cells::new( @@ -62,10 +60,11 @@ impl Cellpicker { ui: &mut egui::Ui, scroll_rect: egui::Rect, ) -> egui::Response { - let num_patterns = self.view.atlas.animation_height / CELL_SIZE * ANIMATION_COLUMNS; - let (canvas_rect, response) = ui.allocate_exact_size( - egui::vec2((num_patterns * CELL_SIZE) as f32, CELL_SIZE as f32) / 2., + egui::vec2( + (self.view.atlas.num_patterns() * CELL_SIZE) as f32, + CELL_SIZE as f32, + ) / 2., egui::Sense::click_and_drag(), ); @@ -108,7 +107,9 @@ impl Cellpicker { } } - self.selected_cell = self.selected_cell.min(num_patterns.saturating_sub(1)); + self.selected_cell = self + .selected_cell + .min(self.view.atlas.num_patterns().saturating_sub(1)); response } diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index 56f78381..91b44e8a 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -55,7 +55,7 @@ pub struct MapView { pub display_tile_ids: bool, pub scale: f32, - pub previous_scale: f32, + previous_scale: f32, /// Used to store the bounding boxes of event graphics in order to render them on top of the /// fog and collision layers diff --git a/crates/components/src/tilepicker.rs b/crates/components/src/tilepicker.rs index d0ef8369..4071568e 100644 --- a/crates/components/src/tilepicker.rs +++ b/crates/components/src/tilepicker.rs @@ -156,7 +156,7 @@ impl Tilepicker { self.brush_random = update_state.toolbar.brush_random != ui.input(|i| i.modifiers.alt); let (canvas_rect, response) = ui.allocate_exact_size( - egui::vec2(256., self.view.atlas.tileset_height as f32 + 32.), + egui::vec2(256., self.view.atlas.tileset_height() as f32 + 32.), egui::Sense::click_and_drag(), ); @@ -214,7 +214,7 @@ impl Tilepicker { pos }; let rect = egui::Rect::from_two_pos(drag_origin, pos); - let bottom = self.view.atlas.tileset_height as i16 / 32; + let bottom = self.view.atlas.tileset_height() as i16 / 32; self.selected_tiles_left = (rect.left() as i16).clamp(0, 7); self.selected_tiles_right = (rect.right() as i16).clamp(0, 7); self.selected_tiles_top = (rect.top() as i16).clamp(0, bottom); diff --git a/crates/graphics/src/event.rs b/crates/graphics/src/event.rs index d3b496e1..7b2ee08c 100644 --- a/crates/graphics/src/event.rs +++ b/crates/graphics/src/event.rs @@ -52,7 +52,7 @@ impl Event { } } } else if page.graphic.tile_id.is_some() { - atlas.atlas_texture.clone() + atlas.texture().clone() } else { return Ok(None); }; @@ -134,7 +134,7 @@ impl Event { } } } else if graphic.tile_id.is_some() { - atlas.atlas_texture.clone() + atlas.texture().clone() } else { return Ok(None); }; diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 7bc3ae18..a00a57d9 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -26,9 +26,9 @@ const CELL_OFFSET: glam::Vec2 = glam::Vec2::splat(-(CELL_SIZE as f32) / 2.); pub struct Frame { pub atlas: Atlas, - pub cells: OptionVec, - pub onion_skin_cells: OptionVec, pub viewport: Viewport, + cells: OptionVec, + onion_skin_cells: OptionVec, pub enable_onion_skin: bool, } @@ -47,13 +47,23 @@ impl Frame { Self { atlas, + viewport, cells: Default::default(), onion_skin_cells: Default::default(), - viewport, enable_onion_skin: false, } } + #[inline] + pub fn cells(&self) -> &OptionVec { + &self.cells + } + + #[inline] + pub fn onion_skin_cells(&self) -> &OptionVec { + &self.onion_skin_cells + } + pub fn rebuild_all_cells( &mut self, graphics_state: &GraphicsState, @@ -201,7 +211,7 @@ impl Frame { opacity, opacity_multiplier, blend_mode, - &self.atlas.atlas_texture, + self.atlas.texture(), &self.viewport, Transform::new( graphics_state, @@ -267,7 +277,7 @@ impl Frame { cell.sprite.set_quad( &graphics_state.render_state, self.atlas.calc_quad(id), - self.atlas.atlas_texture.size(), + self.atlas.texture().size(), ); cell.sprite.blend_mode = blend_mode; diff --git a/crates/graphics/src/primitives/cells/atlas.rs b/crates/graphics/src/primitives/cells/atlas.rs index 5b9de048..06c4e642 100644 --- a/crates/graphics/src/primitives/cells/atlas.rs +++ b/crates/graphics/src/primitives/cells/atlas.rs @@ -35,8 +35,8 @@ use std::sync::Arc; #[derive(Clone)] pub struct Atlas { - pub atlas_texture: Arc, - pub animation_height: u32, + atlas_texture: Arc, + animation_height: u32, } impl Atlas { @@ -152,6 +152,24 @@ impl Atlas { ), ) } + + /// Returns this atlas's texture + #[inline] + pub fn texture(&self) -> &Arc { + &self.atlas_texture + } + + /// Returns the height of the original animation texture in pixels + #[inline] + pub fn animation_height(&self) -> u32 { + self.animation_height + } + + /// Calculates the total number of cells in the atlas based on the size of the texture + #[inline] + pub fn num_patterns(&self) -> u32 { + self.animation_height / CELL_SIZE * ANIMATION_COLUMNS + } } fn write_texture_region

( diff --git a/crates/graphics/src/primitives/cells/mod.rs b/crates/graphics/src/primitives/cells/mod.rs index 643cbcda..d41c4e57 100644 --- a/crates/graphics/src/primitives/cells/mod.rs +++ b/crates/graphics/src/primitives/cells/mod.rs @@ -55,7 +55,7 @@ impl Cells { let mut bind_group_builder = BindGroupBuilder::new(); bind_group_builder - .append_texture_view(&atlas.atlas_texture.view) + .append_texture_view(&atlas.texture().view) .append_sampler(&graphics_state.nearest_sampler) .append_buffer(viewport.as_buffer()) .append_buffer(transform.as_buffer()) diff --git a/crates/graphics/src/primitives/tiles/atlas.rs b/crates/graphics/src/primitives/tiles/atlas.rs index 637d6a70..589cf424 100644 --- a/crates/graphics/src/primitives/tiles/atlas.rs +++ b/crates/graphics/src/primitives/tiles/atlas.rs @@ -48,10 +48,10 @@ use std::sync::Arc; #[derive(Clone)] pub struct Atlas { - pub atlas_texture: Arc, - pub autotile_width: u32, - pub tileset_height: u32, - pub autotile_frames: [u32; AUTOTILE_AMOUNT as usize], + atlas_texture: Arc, + tileset_height: u32, + pub(super) autotile_width: u32, + pub(super) autotile_frames: [u32; AUTOTILE_AMOUNT as usize], } impl Atlas { @@ -343,6 +343,18 @@ impl Atlas { ), ) } + + /// Returns this atlas's texture + #[inline] + pub fn texture(&self) -> &Arc { + &self.atlas_texture + } + + /// Returns the height of the original tileset texture in pixels + #[inline] + pub fn tileset_height(&self) -> u32 { + self.tileset_height + } } fn write_texture_region

( diff --git a/crates/graphics/src/primitives/tiles/mod.rs b/crates/graphics/src/primitives/tiles/mod.rs index 0c5a7b0b..eb5f91c9 100644 --- a/crates/graphics/src/primitives/tiles/mod.rs +++ b/crates/graphics/src/primitives/tiles/mod.rs @@ -67,7 +67,7 @@ impl Tiles { let mut bind_group_builder = BindGroupBuilder::new(); bind_group_builder - .append_texture_view(&atlas.atlas_texture.view) + .append_texture_view(&atlas.texture().view) .append_sampler(&graphics_state.nearest_sampler) .append_buffer(viewport.as_buffer()) .append_buffer(transform.as_buffer()) diff --git a/crates/graphics/src/tilepicker.rs b/crates/graphics/src/tilepicker.rs index 4dde70b3..91010912 100644 --- a/crates/graphics/src/tilepicker.rs +++ b/crates/graphics/src/tilepicker.rs @@ -55,23 +55,23 @@ impl Tilepicker { .load_atlas(graphics_state, filesystem, tileset)?; let tilepicker_data = if exclude_autotiles { - (384..(atlas.tileset_height as i16 / 32 * 8 + 384)).collect_vec() + (384..(atlas.tileset_height() as i16 / 32 * 8 + 384)).collect_vec() } else { (47..(384 + 47)) .step_by(48) - .chain(384..(atlas.tileset_height as i16 / 32 * 8 + 384)) + .chain(384..(atlas.tileset_height() as i16 / 32 * 8 + 384)) .collect_vec() }; let tilepicker_data = luminol_data::Table3::new_data( 8, - !exclude_autotiles as usize + (atlas.tileset_height / 32) as usize, + !exclude_autotiles as usize + (atlas.tileset_height() / 32) as usize, 1, tilepicker_data, ); let viewport = Viewport::new( graphics_state, - glam::vec2(256., atlas.tileset_height as f32 + 32.), + glam::vec2(256., atlas.tileset_height() as f32 + 32.), ); let tiles = Tiles::new( diff --git a/crates/modals/src/graphic_picker/event.rs b/crates/modals/src/graphic_picker/event.rs index 1b9970bb..b8cd9ca5 100644 --- a/crates/modals/src/graphic_picker/event.rs +++ b/crates/modals/src/graphic_picker/event.rs @@ -441,7 +441,7 @@ impl Modal { } Selected::Tile { tile_id, tilepicker } => { let (canvas_rect, response) = ui.allocate_exact_size( - egui::vec2(256., tilepicker.atlas.tileset_height as f32), + egui::vec2(256., tilepicker.atlas.tileset_height() as f32), egui::Sense::click(), ); diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 455a8708..61120d71 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -28,7 +28,6 @@ use luminol_core::Modal; use luminol_data::BlendMode; use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; -use luminol_graphics::primitives::cells::{ANIMATION_COLUMNS, CELL_SIZE}; use luminol_modals::sound_picker::Modal as SoundPicker; /// Database - Animations management window. @@ -512,12 +511,11 @@ impl Window { modified = true; } - let num_patterns = frame_view.frame.atlas.animation_height / CELL_SIZE * ANIMATION_COLUMNS; if modals.batch_edit.show_window( ui.ctx(), state.frame_index, animation.frames.len(), - num_patterns, + frame_view.frame.atlas.num_patterns(), ) { for i in modals.batch_edit.start_frame..=modals.batch_edit.end_frame { let data = &mut animation.frames[i].cell_data; @@ -555,7 +553,10 @@ impl Window { luminol_modals::animations::batch_edit_tool::Mode::Add => { data[(j, 0)] = data[(j, 0)] .saturating_add(modals.batch_edit.add_pattern) - .clamp(0, num_patterns.saturating_sub(1) as i16); + .clamp( + 0, + frame_view.frame.atlas.num_patterns().saturating_sub(1) as i16, + ); data[(j, 1)] = data[(j, 1)] .saturating_add(modals.batch_edit.add_x) .clamp(-(FRAME_WIDTH as i16 / 2), FRAME_WIDTH as i16 / 2); @@ -587,7 +588,7 @@ impl Window { luminol_modals::animations::batch_edit_tool::Mode::Mul => { data[(j, 0)] = ((data[(j, 0)] + 1) as f64 * modals.batch_edit.mul_pattern) - .clamp(1., num_patterns as f64) + .clamp(1., frame_view.frame.atlas.num_patterns() as f64) .round_ties_even() as i16 - 1; data[(j, 1)] = (data[(j, 1)] as f64 * modals.batch_edit.mul_x) @@ -680,10 +681,8 @@ impl Window { let changed = columns[0] .add(luminol_components::Field::new( "Pattern", - egui::DragValue::new(&mut pattern).clamp_range( - 1..=(frame_view.frame.atlas.animation_height / CELL_SIZE - * ANIMATION_COLUMNS) as i16, - ), + egui::DragValue::new(&mut pattern) + .clamp_range(1..=frame_view.frame.atlas.num_patterns() as i16), )) .changed(); if changed { From 15101f42fee2414f14a8641905f4788d7da8a418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 21 Jul 2024 13:05:51 -0400 Subject: [PATCH 037/109] Implement double-clicking to create animation cells --- crates/components/src/animation_frame_view.rs | 26 +++- crates/data/src/rmxp/animation.rs | 2 +- crates/ui/src/windows/animations.rs | 126 ++++++++++++++---- 3 files changed, 120 insertions(+), 34 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 24ac328a..03980261 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -72,7 +72,7 @@ impl AnimationFrameView { ui: &mut egui::Ui, update_state: &luminol_core::UpdateState<'_>, clip_rect: egui::Rect, - ) -> egui::Response { + ) -> egui::InnerResponse> { let canvas_rect = ui.max_rect(); let canvas_center = canvas_rect.center(); ui.set_clip_rect(canvas_rect.intersect(clip_rect)); @@ -202,6 +202,8 @@ impl AnimationFrameView { .flatten(); } + let hover_pos_in_frame_coords = response.hover_pos().map(|pos| (pos - offset) / scale); + if !response.is_pointer_button_down_on() || ui.input(|i| { !i.pointer.button_down(egui::PointerButton::Primary) || i.modifiers.shift @@ -213,14 +215,12 @@ impl AnimationFrameView { self.hovered_cell_drag_offset, response.drag_started_by(egui::PointerButton::Primary), ) { - self.hovered_cell_drag_offset = Some( - self.frame.cells()[i].rect.center() - - (response.hover_pos().unwrap() - offset) / scale, - ); + self.hovered_cell_drag_offset = + Some(self.frame.cells()[i].rect.center() - hover_pos_in_frame_coords.unwrap()); } if let Some(drag_offset) = self.hovered_cell_drag_offset { - let pos = (response.hover_pos().unwrap() - offset) / scale + drag_offset; + let pos = hover_pos_in_frame_coords.unwrap() + drag_offset; self.hovered_cell_drag_pos = Some(( pos.x .clamp(-(FRAME_WIDTH as f32 / 2.), FRAME_WIDTH as f32 / 2.) @@ -291,6 +291,18 @@ impl AnimationFrameView { d.insert_persisted(self.data_id, (self.pan, self.scale)); }); - response + egui::InnerResponse::new( + hover_pos_in_frame_coords.map(|pos| { + ( + pos.x + .clamp(-(FRAME_WIDTH as f32 / 2.), FRAME_WIDTH as f32 / 2.) + .round_ties_even() as i16, + pos.y + .clamp(-(FRAME_HEIGHT as f32 / 2.), FRAME_HEIGHT as f32 / 2.) + .round_ties_even() as i16, + ) + }), + response, + ) } } diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index a3687230..247efa23 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -65,7 +65,7 @@ impl Default for Timing { #[derive(alox_48::Deserialize, alox_48::Serialize)] #[marshal(class = "RPG::Animation::Frame")] pub struct Frame { - pub cell_max: i32, + pub cell_max: usize, pub cell_data: Table2, } diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 61120d71..c6c6af55 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -101,6 +101,22 @@ impl Default for Window { } impl Window { + fn log_atlas_error( + update_state: &mut luminol_core::UpdateState<'_>, + animation: &luminol_data::rpg::Animation, + e: color_eyre::Report, + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + animation.animation_name, + animation.id + 1, + animation.name, + ),), + ); + } + fn show_timing_header(ui: &mut egui::Ui, timing: &luminol_data::rpg::animation::Timing) { let mut vec = Vec::with_capacity(3); @@ -148,6 +164,36 @@ impl Window { )); } + fn resize_frame(frame: &mut luminol_data::rpg::animation::Frame, new_cell_max: usize) { + let old_capacity = frame.cell_data.xsize(); + let new_capacity = new_cell_max.next_power_of_two(); + + // Instead of resizing `frame.cell_data` every time we call this function, we increase the + // size of `frame.cell_data` only it's too small and we decrease the size of + // `frame.cell_data` only if it's at <= 50% capacity for better efficiency + let capacity_too_low = old_capacity < new_capacity; + let capacity_too_high = old_capacity >= new_capacity * 2; + + if capacity_too_low || capacity_too_high { + frame.cell_data.resize(new_capacity, 8); + } + + if capacity_too_low { + for i in old_capacity..new_capacity { + frame.cell_data[(i, 0)] = -1; + frame.cell_data[(i, 1)] = 0; + frame.cell_data[(i, 2)] = 0; + frame.cell_data[(i, 3)] = 100; + frame.cell_data[(i, 4)] = 0; + frame.cell_data[(i, 5)] = 0; + frame.cell_data[(i, 6)] = 255; + frame.cell_data[(i, 7)] = 1; + } + } + + frame.cell_max = new_cell_max; + } + fn show_timing_body( ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>, @@ -295,15 +341,7 @@ impl Window { ) { Ok(atlas) => atlas, Err(e) => { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "While loading texture {:?} for animation {:0>4} {:?}", - animation.animation_name, - animation.id + 1, - animation.name, - )), - ); + Self::log_atlas_error(update_state, animation, e); return (modified, true); } }; @@ -468,8 +506,9 @@ impl Window { }; if animation.frames[j].cell_data.xsize() < i + 1 { - animation.frames[j].cell_data.resize(i + 1, 8); - animation.frames[j].cell_max = (i + 1) as i32; + Self::resize_frame(&mut animation.frames[j], i + 1); + } else if animation.frames[j].cell_max < i + 1 { + animation.frames[j].cell_max = i + 1; } if modals.tween.tween_pattern { @@ -786,7 +825,10 @@ impl Window { ui.allocate_ui_at_rect(canvas_rect, |ui| { frame_view.frame.enable_onion_skin = state.enable_onion_skin && state.frame_index != 0; - let response = frame_view.ui(ui, update_state, clip_rect); + let egui::InnerResponse { + inner: hover_pos, + response, + } = frame_view.ui(ui, update_state, clip_rect); // If the pointer is hovering over the frame view, prevent parent widgets // from receiving scroll events so that scaling the frame view with the @@ -796,6 +838,38 @@ impl Window { ui.ctx() .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); } + + // Create new cell on double click + if response.double_clicked() { + if let Some((x, y)) = hover_pos { + let frame = &mut animation.frames[state.frame_index]; + + let next_cell_index = (frame.cell_max..frame.cell_data.xsize()) + .find(|i| frame.cell_data[(*i, 0)] < 0) + .unwrap_or(frame.cell_data.xsize()); + + Self::resize_frame(frame, next_cell_index + 1); + + frame.cell_data[(next_cell_index, 0)] = cellpicker.selected_cell as i16; + frame.cell_data[(next_cell_index, 1)] = x; + frame.cell_data[(next_cell_index, 2)] = y; + frame.cell_data[(next_cell_index, 3)] = 100; + frame.cell_data[(next_cell_index, 4)] = 0; + frame.cell_data[(next_cell_index, 5)] = 0; + frame.cell_data[(next_cell_index, 6)] = 255; + frame.cell_data[(next_cell_index, 7)] = 1; + + frame_view.frame.update_cell( + &update_state.graphics, + animation, + state.frame_index, + next_cell_index, + ); + frame_view.selected_cell_index = Some(next_cell_index); + + modified = true; + } + } }); (modified, false) @@ -876,15 +950,7 @@ impl luminol_core::Window for Window { ) { Ok(atlas) => atlas, Err(e) => { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "While loading texture {:?} for animation {:0>4} {:?}", - animation.animation_name, - animation.id + 1, - animation.name, - ),), - ); + Self::log_atlas_error(update_state, animation, e); return true; } }; @@ -899,11 +965,19 @@ impl luminol_core::Window for Window { ); } - self.frame_edit_state.cellpicker = - Some(luminol_components::Cellpicker::new( - &update_state.graphics, - atlas, - )); + let selected_cell = self + .frame_edit_state + .cellpicker + .as_ref() + .map(|cellpicker| cellpicker.selected_cell) + .unwrap_or_default() + .min(atlas.num_patterns().saturating_sub(1)); + let mut cellpicker = luminol_components::Cellpicker::new( + &update_state.graphics, + atlas, + ); + cellpicker.selected_cell = selected_cell; + self.frame_edit_state.cellpicker = Some(cellpicker); } let (inner_modified, abort) = Self::show_frame_edit( From 130e9df9a4823006c545fda87110ebe995944289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 21 Jul 2024 15:28:49 -0400 Subject: [PATCH 038/109] Implement pressing delete/backspace to delete animation cells --- crates/components/src/animation_frame_view.rs | 6 + crates/ui/src/windows/animations.rs | 274 ++++++++++-------- 2 files changed, 164 insertions(+), 116 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 03980261..b22988a7 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -79,6 +79,12 @@ impl AnimationFrameView { let mut response = ui.allocate_rect(canvas_rect, egui::Sense::click_and_drag()); + // Take focus when this view is interacted with so the map editor doesn't receive + // keypresses if it's also open at the same time + if response.clicked() || response.double_clicked() || response.dragged() { + response.request_focus(); + } + let min_clip = (ui.ctx().screen_rect().min - canvas_rect.min).max(Default::default()); let max_clip = (canvas_rect.max - ui.ctx().screen_rect().max).max(Default::default()); let clip_offset = (max_clip - min_clip) / 2.; diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index c6c6af55..254fa8e3 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -166,19 +166,20 @@ impl Window { fn resize_frame(frame: &mut luminol_data::rpg::animation::Frame, new_cell_max: usize) { let old_capacity = frame.cell_data.xsize(); - let new_capacity = new_cell_max.next_power_of_two(); + let new_capacity = if new_cell_max == 0 { + 0 + } else { + new_cell_max.next_power_of_two() + }; // Instead of resizing `frame.cell_data` every time we call this function, we increase the // size of `frame.cell_data` only it's too small and we decrease the size of - // `frame.cell_data` only if it's at <= 50% capacity for better efficiency + // `frame.cell_data` only if it's at <= 25% capacity for better efficiency let capacity_too_low = old_capacity < new_capacity; - let capacity_too_high = old_capacity >= new_capacity * 2; - - if capacity_too_low || capacity_too_high { - frame.cell_data.resize(new_capacity, 8); - } + let capacity_too_high = old_capacity >= new_capacity * 4; if capacity_too_low { + frame.cell_data.resize(new_capacity, 8); for i in old_capacity..new_capacity { frame.cell_data[(i, 0)] = -1; frame.cell_data[(i, 1)] = 0; @@ -189,6 +190,8 @@ impl Window { frame.cell_data[(i, 6)] = 255; frame.cell_data[(i, 7)] = 1; } + } else if capacity_too_high { + frame.cell_data.resize(new_capacity * 2, 8); } frame.cell_max = new_cell_max; @@ -679,19 +682,20 @@ impl Window { if frame_view .selected_cell_index - .is_some_and(|i| i >= frame.cell_data.xsize()) + .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) { frame_view.selected_cell_index = None; } if frame_view .hovered_cell_index - .is_some_and(|i| i >= frame.cell_data.xsize()) + .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) { frame_view.hovered_cell_index = None; frame_view.hovered_cell_drag_pos = None; frame_view.hovered_cell_drag_offset = None; } + // Handle dragging of cells to move them if let (Some(i), Some(drag_pos)) = ( frame_view.hovered_cell_index, frame_view.hovered_cell_drag_pos, @@ -708,116 +712,117 @@ impl Window { } } - let frame = &mut animation.frames[state.frame_index]; + egui::Frame::none().show(ui, |ui| { + let frame = &mut animation.frames[state.frame_index]; + if let Some(i) = frame_view.selected_cell_index { + let mut properties_modified = false; - if let Some(i) = frame_view.selected_cell_index { - let mut properties_modified = false; - - ui.label(format!("Cell {}", i + 1)); - - ui.columns(4, |columns| { - let mut pattern = frame.cell_data[(i, 0)] + 1; - let changed = columns[0] - .add(luminol_components::Field::new( - "Pattern", - egui::DragValue::new(&mut pattern) - .clamp_range(1..=frame_view.frame.atlas.num_patterns() as i16), - )) - .changed(); - if changed { - frame.cell_data[(i, 0)] = pattern - 1; - properties_modified = true; - } + ui.label(format!("Cell {}", i + 1)); - properties_modified |= columns[1] - .add(luminol_components::Field::new( - "X", - egui::DragValue::new(&mut frame.cell_data[(i, 1)]) - .clamp_range(-(FRAME_WIDTH as i16 / 2)..=FRAME_WIDTH as i16 / 2), - )) - .changed(); - - properties_modified |= columns[2] - .add(luminol_components::Field::new( - "Y", - egui::DragValue::new(&mut frame.cell_data[(i, 2)]) - .clamp_range(-(FRAME_HEIGHT as i16 / 2)..=FRAME_HEIGHT as i16 / 2), - )) - .changed(); - - properties_modified |= columns[3] - .add(luminol_components::Field::new( - "Scale", - egui::DragValue::new(&mut frame.cell_data[(i, 3)]) - .clamp_range(1..=i16::MAX) - .suffix("%"), - )) - .changed(); - }); + ui.columns(4, |columns| { + let mut pattern = frame.cell_data[(i, 0)] + 1; + let changed = columns[0] + .add(luminol_components::Field::new( + "Pattern", + egui::DragValue::new(&mut pattern) + .clamp_range(1..=frame_view.frame.atlas.num_patterns() as i16), + )) + .changed(); + if changed { + frame.cell_data[(i, 0)] = pattern - 1; + properties_modified = true; + } - ui.columns(4, |columns| { - properties_modified |= columns[0] - .add(luminol_components::Field::new( - "Rotation", - egui::DragValue::new(&mut frame.cell_data[(i, 4)]) - .clamp_range(0..=360) - .suffix("°"), - )) - .changed(); - - let mut flip = frame.cell_data[(i, 5)] == 1; - let changed = columns[1] - .add(luminol_components::Field::new( - "Flip", - egui::Checkbox::without_text(&mut flip), - )) - .changed(); - if changed { - frame.cell_data[(i, 5)] = if flip { 1 } else { 0 }; - properties_modified = true; - } + properties_modified |= columns[1] + .add(luminol_components::Field::new( + "X", + egui::DragValue::new(&mut frame.cell_data[(i, 1)]) + .clamp_range(-(FRAME_WIDTH as i16 / 2)..=FRAME_WIDTH as i16 / 2), + )) + .changed(); + + properties_modified |= columns[2] + .add(luminol_components::Field::new( + "Y", + egui::DragValue::new(&mut frame.cell_data[(i, 2)]) + .clamp_range(-(FRAME_HEIGHT as i16 / 2)..=FRAME_HEIGHT as i16 / 2), + )) + .changed(); + + properties_modified |= columns[3] + .add(luminol_components::Field::new( + "Scale", + egui::DragValue::new(&mut frame.cell_data[(i, 3)]) + .clamp_range(1..=i16::MAX) + .suffix("%"), + )) + .changed(); + }); + + ui.columns(4, |columns| { + properties_modified |= columns[0] + .add(luminol_components::Field::new( + "Rotation", + egui::DragValue::new(&mut frame.cell_data[(i, 4)]) + .clamp_range(0..=360) + .suffix("°"), + )) + .changed(); - properties_modified |= columns[2] - .add(luminol_components::Field::new( - "Opacity", - egui::DragValue::new(&mut frame.cell_data[(i, 6)]).clamp_range(0..=255), - )) - .changed(); - - let mut blend_mode = match frame.cell_data[(i, 7)] { - 1 => BlendMode::Add, - 2 => BlendMode::Subtract, - _ => BlendMode::Normal, - }; - let changed = columns[3] - .add(luminol_components::Field::new( - "Blending", - luminol_components::EnumComboBox::new( - (animation.id, state.frame_index, i, 7usize), - &mut blend_mode, - ), - )) - .changed(); - if changed { - frame.cell_data[(i, 7)] = match blend_mode { - BlendMode::Normal => 0, - BlendMode::Add => 1, - BlendMode::Subtract => 2, + let mut flip = frame.cell_data[(i, 5)] == 1; + let changed = columns[1] + .add(luminol_components::Field::new( + "Flip", + egui::Checkbox::without_text(&mut flip), + )) + .changed(); + if changed { + frame.cell_data[(i, 5)] = if flip { 1 } else { 0 }; + properties_modified = true; + } + + properties_modified |= columns[2] + .add(luminol_components::Field::new( + "Opacity", + egui::DragValue::new(&mut frame.cell_data[(i, 6)]).clamp_range(0..=255), + )) + .changed(); + + let mut blend_mode = match frame.cell_data[(i, 7)] { + 1 => BlendMode::Add, + 2 => BlendMode::Subtract, + _ => BlendMode::Normal, }; - properties_modified = true; - } - }); + let changed = columns[3] + .add(luminol_components::Field::new( + "Blending", + luminol_components::EnumComboBox::new( + (animation.id, state.frame_index, i, 7usize), + &mut blend_mode, + ), + )) + .changed(); + if changed { + frame.cell_data[(i, 7)] = match blend_mode { + BlendMode::Normal => 0, + BlendMode::Add => 1, + BlendMode::Subtract => 2, + }; + properties_modified = true; + } + }); - if properties_modified { - frame_view.frame.update_cell( - &update_state.graphics, - animation, - state.frame_index, - i, - ); - modified = true; + if properties_modified { + frame_view.frame.update_cell( + &update_state.graphics, + animation, + state.frame_index, + i, + ); + modified = true; + } } - } + }); egui::ScrollArea::horizontal().show_viewport(ui, |ui, scroll_rect| { cellpicker.ui(update_state, ui, scroll_rect); @@ -839,11 +844,11 @@ impl Window { .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); } - // Create new cell on double click - if response.double_clicked() { - if let Some((x, y)) = hover_pos { - let frame = &mut animation.frames[state.frame_index]; + let frame = &mut animation.frames[state.frame_index]; + // Create new cell on double click + if let Some((x, y)) = hover_pos { + if response.double_clicked() { let next_cell_index = (frame.cell_max..frame.cell_data.xsize()) .find(|i| frame.cell_data[(*i, 0)] < 0) .unwrap_or(frame.cell_data.xsize()); @@ -870,6 +875,43 @@ impl Window { modified = true; } } + + let frame = &mut animation.frames[state.frame_index]; + + // Handle pressing delete or backspace to delete cells + if let Some(i) = frame_view.selected_cell_index { + if i < frame.cell_data.xsize() + && frame.cell_data[(i, 0)] >= 0 + && response.has_focus() + && ui.input(|i| { + i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace) + }) + { + frame.cell_data[(i, 0)] = -1; + + if i + 1 >= frame.cell_max { + Self::resize_frame( + frame, + (0..frame + .cell_data + .xsize() + .min(frame.cell_max.saturating_sub(1))) + .rev() + .find_map(|i| (frame.cell_data[(i, 0)] >= 0).then_some(i + 1)) + .unwrap_or(0), + ); + } + + frame_view.frame.update_cell( + &update_state.graphics, + animation, + state.frame_index, + i, + ); + frame_view.selected_cell_index = None; + modified = true; + } + } }); (modified, false) From 9fd85e5c5ecc2807873d45432f694c19dcfc8fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 23 Jul 2024 21:58:36 -0400 Subject: [PATCH 039/109] Update `time` to 0.3.36 This fixes compiler errors when using very new Rust versions starting from around May 2024. --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b856984e..b3155420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5648,9 +5648,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -5669,9 +5669,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", From 03581f9be53f77d3c12559e265fe22f8fba76b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 23 Jul 2024 22:00:02 -0400 Subject: [PATCH 040/109] Remove unnecessary `parking_lot_core` dependency --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0429a678..a9d53583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,7 +121,6 @@ parking_lot = { version = "0.12.3", features = [ "nightly", # This is required for parking_lot to work properly in WebAssembly builds with atomics support "deadlock_detection", ] } -parking_lot_core = "0.9.10" once_cell = "1.18.0" crossbeam = "0.8.2" dashmap = "5.5.3" From ec6395a49b022fe82d38f15b80b3895d17ee4ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 24 Jul 2024 00:55:23 -0400 Subject: [PATCH 041/109] Implement displaying battler in the animation editor --- crates/graphics/src/frame.rs | 126 ++++++++++++++++++++++++++-- crates/ui/src/windows/animations.rs | 107 ++++++++++++++++++++++- 2 files changed, 226 insertions(+), 7 deletions(-) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index a00a57d9..fe38b007 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -16,17 +16,22 @@ // along with Luminol. If not, see . use crate::primitives::cells::{Atlas, CELL_SIZE}; -use crate::{Drawable, GraphicsState, Renderable, Sprite, Transform, Viewport}; +use crate::{Drawable, GraphicsState, Quad, Renderable, Sprite, Texture, Transform, Viewport}; use luminol_data::{BlendMode, OptionVec}; pub const FRAME_WIDTH: usize = 640; pub const FRAME_HEIGHT: usize = 320; +pub const BATTLER_BOTTOM_SPACING: usize = 16; const CELL_OFFSET: glam::Vec2 = glam::Vec2::splat(-(CELL_SIZE as f32) / 2.); pub struct Frame { pub atlas: Atlas, + pub battler_texture: Option>, pub viewport: Viewport, + + battler_sprite: Option, + cells: OptionVec, onion_skin_cells: OptionVec, @@ -47,13 +52,20 @@ impl Frame { Self { atlas, + battler_texture: None, viewport, + battler_sprite: None, cells: Default::default(), onion_skin_cells: Default::default(), enable_onion_skin: false, } } + #[inline] + pub fn battler_sprite(&self) -> Option<&Sprite> { + self.battler_sprite.as_ref() + } + #[inline] pub fn cells(&self) -> &OptionVec { &self.cells @@ -64,6 +76,101 @@ impl Frame { &self.onion_skin_cells } + pub fn rebuild_battler( + &mut self, + graphics_state: &GraphicsState, + system: &luminol_data::rpg::System, + animation: &luminol_data::rpg::Animation, + ) { + self.battler_sprite = + self.create_battler_sprite(graphics_state, animation.position, system.battler_hue); + } + + pub fn update_battler( + &mut self, + graphics_state: &GraphicsState, + system: &luminol_data::rpg::System, + animation: &luminol_data::rpg::Animation, + ) { + if let Some(texture) = &self.battler_texture { + if let Some(sprite) = &mut self.battler_sprite { + sprite.transform.set_position( + &graphics_state.render_state, + glam::vec2( + -(texture.texture.width() as f32 / 2.), + match animation.position { + luminol_data::rpg::animation::Position::Top => { + FRAME_HEIGHT as f32 / 4. - texture.texture.height() as f32 / 2. + } + luminol_data::rpg::animation::Position::Middle => { + -(texture.texture.height() as f32 / 2.) + } + luminol_data::rpg::animation::Position::Bottom => { + -(FRAME_HEIGHT as f32 / 4.) - texture.texture.height() as f32 / 2. + } + luminol_data::rpg::animation::Position::Screen => { + FRAME_HEIGHT as f32 / 2. + - texture.texture.height() as f32 + - BATTLER_BOTTOM_SPACING as f32 + } + }, + ), + ); + } else { + self.battler_sprite = self.create_battler_sprite( + graphics_state, + animation.position, + system.battler_hue, + ); + } + } else { + self.battler_sprite = None; + } + } + + fn create_battler_sprite( + &self, + graphics_state: &GraphicsState, + position: luminol_data::rpg::animation::Position, + hue: i32, + ) -> Option { + self.battler_texture.as_ref().map(|texture| { + let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, texture.size_vec2()); + let quad = Quad::new(rect, rect); + Sprite::new( + graphics_state, + quad, + hue, + 255, + BlendMode::Normal, + texture, + &self.viewport, + Transform::new_position( + graphics_state, + glam::vec2( + -(texture.texture.width() as f32 / 2.), + match position { + luminol_data::rpg::animation::Position::Top => { + FRAME_HEIGHT as f32 / 4. - texture.texture.height() as f32 / 2. + } + luminol_data::rpg::animation::Position::Middle => { + -(texture.texture.height() as f32 / 2.) + } + luminol_data::rpg::animation::Position::Bottom => { + -(FRAME_HEIGHT as f32 / 4.) - texture.texture.height() as f32 / 2. + } + luminol_data::rpg::animation::Position::Screen => { + FRAME_HEIGHT as f32 / 2. + - texture.texture.height() as f32 + - BATTLER_BOTTOM_SPACING as f32 + } + }, + ), + ), + ) + }) + } + pub fn rebuild_all_cells( &mut self, graphics_state: &GraphicsState, @@ -77,7 +184,7 @@ impl Frame { .len() .max(animation.frames[frame_index].cell_data.xsize())) .filter_map(|i| { - self.cell_from_cell_data( + self.create_cell( graphics_state, &animation.frames[frame_index], animation.animation_hue, @@ -98,7 +205,7 @@ impl Frame { .xsize(), )) .filter_map(|i| { - self.cell_from_cell_data( + self.create_cell( graphics_state, &animation.frames[frame_index.saturating_sub(1)], animation.animation_hue, @@ -178,7 +285,7 @@ impl Frame { } } - fn cell_from_cell_data( + fn create_cell( &self, graphics_state: &GraphicsState, frame: &luminol_data::rpg::animation::Frame, @@ -287,7 +394,7 @@ impl Frame { egui::Vec2::splat(CELL_SIZE as f32 * (cos.abs() + sin.abs()) * scale), ); } else if let Some(cell) = - self.cell_from_cell_data(graphics_state, frame, hue, cell_index, opacity_multiplier) + self.create_cell(graphics_state, frame, hue, cell_index, opacity_multiplier) { cells.insert(cell_index, cell); } @@ -300,6 +407,7 @@ impl Frame { } pub struct Prepared { + battler_sprite: Option<::Prepared>, cells: Vec<::Prepared>, onion_skin_cells: Vec<::Prepared>, } @@ -309,6 +417,11 @@ impl Renderable for Frame { fn prepare(&mut self, graphics_state: &std::sync::Arc) -> Self::Prepared { Self::Prepared { + battler_sprite: self + .battler_sprite + .as_mut() + .map(|sprite| sprite.prepare(graphics_state)), + cells: self .cells .iter_mut() @@ -329,6 +442,9 @@ impl Renderable for Frame { impl Drawable for Prepared { fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + if let Some(sprite) = &self.battler_sprite { + sprite.draw(render_pass); + } for sprite in &self.onion_skin_cells { sprite.draw(render_pass); } diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 259d2a0a..2c9fd0e1 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -34,6 +34,7 @@ use luminol_modals::sound_picker::Modal as SoundPicker; pub struct Window { selected_animation_name: Option, previous_animation: Option, + previous_battler_name: Option, previous_timing_frame: Option, frame_edit_state: FrameEditState, @@ -71,6 +72,7 @@ impl Default for Window { Self { selected_animation_name: None, previous_animation: None, + previous_battler_name: None, previous_timing_frame: None, frame_edit_state: FrameEditState { frame_index: 0, @@ -101,6 +103,23 @@ impl Default for Window { } impl Window { + fn log_battler_error( + update_state: &mut luminol_core::UpdateState<'_>, + system: &luminol_data::rpg::System, + animation: &luminol_data::rpg::Animation, + e: color_eyre::Report, + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + system.battler_name, + animation.id + 1, + animation.name, + ),), + ); + } + fn log_atlas_error( update_state: &mut luminol_core::UpdateState<'_>, animation: &luminol_data::rpg::Animation, @@ -329,6 +348,7 @@ impl Window { update_state: &mut luminol_core::UpdateState<'_>, clip_rect: egui::Rect, modals: &mut Modals, + system: &luminol_data::rpg::System, animation: &mut luminol_data::rpg::Animation, state: &mut FrameEditState, ) -> (bool, bool) { @@ -337,6 +357,20 @@ impl Window { let frame_view = if let Some(frame_view) = &mut state.frame_view { frame_view } else { + let battler_texture = if let Some(battler_name) = &system.battler_name { + match update_state.graphics.texture_loader.load_now( + update_state.filesystem, + format!("Graphics/Battlers/{battler_name}"), + ) { + Ok(texture) => Some(texture), + Err(e) => { + Self::log_battler_error(update_state, system, animation, e); + return (modified, true); + } + } + } else { + None + }; let atlas = match update_state.graphics.atlas_loader.load_animation_atlas( &update_state.graphics, update_state.filesystem, @@ -349,6 +383,10 @@ impl Window { } }; let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); + frame_view.frame.battler_texture = battler_texture; + frame_view + .frame + .update_battler(&update_state.graphics, system, animation); frame_view .frame .update_all_cells(&update_state.graphics, animation, state.frame_index); @@ -934,6 +972,7 @@ impl luminol_core::Window for Window { ) { let data = std::mem::take(update_state.data); // take data to avoid borrow checker issues let mut animations = data.animations(); + let system = data.system(); let mut modified = false; @@ -972,8 +1011,65 @@ impl luminol_core::Window for Window { .changed(); }); + ui.with_padded_stripe(true, |ui| { + let changed = ui + .add(luminol_components::Field::new( + "Battler Position", + luminol_components::EnumComboBox::new( + (animation.id, "position"), + &mut animation.position, + ), + )) + .changed(); + if changed { + if let Some(frame_view) = &mut self.frame_edit_state.frame_view { + frame_view.frame.update_battler( + &update_state.graphics, + &system, + animation, + ); + } + modified = true; + } + }); + let abort = ui - .with_padded_stripe(true, |ui| { + .with_padded_stripe(false, |ui| { + if self.previous_battler_name != system.battler_name { + let battler_texture = + if let Some(battler_name) = &system.battler_name { + match update_state.graphics.texture_loader.load_now( + update_state.filesystem, + format!("Graphics/Battlers/{battler_name}"), + ) { + Ok(texture) => Some(texture), + Err(e) => { + Self::log_battler_error( + update_state, + &system, + animation, + e, + ); + return true; + } + } + } else { + None + }; + + if let Some(frame_view) = &mut self.frame_edit_state.frame_view + { + frame_view.frame.battler_texture = battler_texture; + frame_view.frame.rebuild_battler( + &update_state.graphics, + &system, + animation, + ); + } + + self.previous_battler_name.clone_from(&system.battler_name); + } + if self.previous_animation != Some(animation.id) { self.modals.close_all(); self.frame_edit_state.frame_index = self @@ -999,6 +1095,11 @@ impl luminol_core::Window for Window { if let Some(frame_view) = &mut self.frame_edit_state.frame_view { frame_view.frame.atlas = atlas.clone(); + frame_view.frame.update_battler( + &update_state.graphics, + &system, + animation, + ); frame_view.frame.rebuild_all_cells( &update_state.graphics, animation, @@ -1026,6 +1127,7 @@ impl luminol_core::Window for Window { update_state, clip_rect, &mut self.modals, + &system, animation, &mut self.frame_edit_state, ); @@ -1040,7 +1142,7 @@ impl luminol_core::Window for Window { return true; } - ui.with_padded_stripe(false, |ui| { + ui.with_padded_stripe(true, |ui| { modified |= ui .add(luminol_components::Field::new( "SE and Flash", @@ -1096,6 +1198,7 @@ impl luminol_core::Window for Window { } drop(animations); + drop(system); *update_state.data = data; // restore data From 8e0fe6c0ecb4febbcfa60d3f89d2e5937ceae52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 24 Jul 2024 14:39:27 -0400 Subject: [PATCH 042/109] Update battler hue when updating animation editor battler sprite --- crates/graphics/src/frame.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index fe38b007..ab4d25d5 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -116,6 +116,9 @@ impl Frame { }, ), ); + sprite + .graphic + .set_hue(&graphics_state.render_state, system.battler_hue); } else { self.battler_sprite = self.create_battler_sprite( graphics_state, From 1e8fba9202af29cf95c838796824303cfd75b5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 28 Jul 2024 14:00:37 -0400 Subject: [PATCH 043/109] Refactor sprite shader to remove `opacity_multipler` --- .../src/primitives/shaders/sprite.wgsl | 3 +- .../graphics/src/primitives/sprite/graphic.rs | 38 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl index 45fd9d95..6fe93afc 100644 --- a/crates/graphics/src/primitives/shaders/sprite.wgsl +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -16,7 +16,6 @@ struct VertexOutput { struct Graphic { hue: f32, opacity: f32, - opacity_multiplier: f32, rotation: f32, // clockwise in radians } @@ -57,7 +56,7 @@ fn vs_main( fn fs_main(in: VertexOutput) -> @location(0) vec4 { var tex_sample = textureSample(t_diffuse, s_diffuse, in.tex_coords); - tex_sample.a *= graphic.opacity * graphic.opacity_multiplier; + tex_sample.a *= graphic.opacity; if tex_sample.a <= 0.001 { discard; } diff --git a/crates/graphics/src/primitives/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs index 14eac783..2803ebe4 100644 --- a/crates/graphics/src/primitives/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -23,6 +23,8 @@ use crate::{BindGroupLayoutBuilder, GraphicsState}; pub struct Graphic { data: Data, uniform: wgpu::Buffer, + opacity: i32, + opacity_multiplier: f32, } #[repr(C)] @@ -30,7 +32,6 @@ pub struct Graphic { struct Data { hue: f32, opacity: f32, - opacity_multiplier: f32, /// clockwise in radians rotation: f32, } @@ -48,11 +49,10 @@ impl Graphic { rotation: f32, ) -> Self { let hue = (hue % 360) as f32 / 360.0; - let opacity = opacity as f32 / 255.; + let computed_opacity = opacity as f32 / 255.0 * opacity_multiplier; let data = Data { hue, - opacity, - opacity_multiplier, + opacity: computed_opacity, rotation, }; @@ -64,7 +64,12 @@ impl Graphic { }, ); - Self { data, uniform } + Self { + data, + uniform, + opacity, + opacity_multiplier, + } } pub fn hue(&self) -> i32 { @@ -85,16 +90,17 @@ impl Graphic { } pub fn set_opacity(&mut self, render_state: &luminol_egui_wgpu::RenderState, opacity: i32) { - let opacity = opacity as f32 / 255.0; + let computed_opacity = opacity as f32 / 255.0 * self.opacity_multiplier; - if self.data.opacity != opacity { - self.data.opacity = opacity; + if computed_opacity != self.data.opacity { + self.opacity = opacity; + self.data.opacity = computed_opacity; self.regen_buffer(render_state); } } pub fn opacity_multiplier(&self) -> f32 { - self.data.opacity_multiplier + self.opacity_multiplier } pub fn set_opacity_multiplier( @@ -102,8 +108,11 @@ impl Graphic { render_state: &luminol_egui_wgpu::RenderState, opacity_multiplier: f32, ) { - if self.data.opacity_multiplier != opacity_multiplier { - self.data.opacity_multiplier = opacity_multiplier; + let computed_opacity = self.opacity as f32 / 255.0 * opacity_multiplier; + + if computed_opacity != self.data.opacity { + self.opacity_multiplier = opacity_multiplier; + self.data.opacity = computed_opacity; self.regen_buffer(render_state); } } @@ -128,14 +137,15 @@ impl Graphic { rotation: f32, ) { let hue = (hue % 360) as f32 / 360.0; - let opacity = opacity as f32 / 255.0; + let computed_opacity = opacity as f32 / 255.0 * opacity_multiplier; let data = Data { hue, - opacity, - opacity_multiplier, + opacity: computed_opacity, rotation, }; if data != self.data { + self.opacity = opacity; + self.opacity_multiplier = opacity_multiplier; self.data = data; self.regen_buffer(render_state); } From 2537fee83e58741344ab700e617c4ef1a45d47a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 28 Jul 2024 14:20:36 -0400 Subject: [PATCH 044/109] Make sure uniform buffers are 16-byte aligned --- crates/graphics/src/primitives/shaders/sprite.wgsl | 1 + crates/graphics/src/primitives/sprite/graphic.rs | 5 ++++- crates/graphics/src/primitives/tiles/display.rs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl index 6fe93afc..40314e66 100644 --- a/crates/graphics/src/primitives/shaders/sprite.wgsl +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -17,6 +17,7 @@ struct Graphic { hue: f32, opacity: f32, rotation: f32, // clockwise in radians + _padding: u32, } @group(0) @binding(0) diff --git a/crates/graphics/src/primitives/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs index 2803ebe4..68c3be5f 100644 --- a/crates/graphics/src/primitives/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -27,13 +27,14 @@ pub struct Graphic { opacity_multiplier: f32, } -#[repr(C)] +#[repr(C, align(16))] #[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] struct Data { hue: f32, opacity: f32, /// clockwise in radians rotation: f32, + _padding: u32, } impl Graphic { @@ -54,6 +55,7 @@ impl Graphic { hue, opacity: computed_opacity, rotation, + _padding: 0, }; let uniform = graphics_state.render_state.device.create_buffer_init( @@ -142,6 +144,7 @@ impl Graphic { hue, opacity: computed_opacity, rotation, + _padding: 0, }; if data != self.data { self.opacity = opacity; diff --git a/crates/graphics/src/primitives/tiles/display.rs b/crates/graphics/src/primitives/tiles/display.rs index 0b8b7742..45a1053f 100644 --- a/crates/graphics/src/primitives/tiles/display.rs +++ b/crates/graphics/src/primitives/tiles/display.rs @@ -38,7 +38,7 @@ struct LayerData { min_alignment_size: u32, } -#[repr(C)] +#[repr(C, align(16))] #[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] pub struct Data { opacity: f32, From 7d7d5f559d57fb149279c6b53b4bc20993a099b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 29 Jul 2024 00:52:42 -0400 Subject: [PATCH 045/109] Start implementing animation flash --- crates/components/src/animation_frame_view.rs | 15 + crates/components/src/collapsing_view.rs | 14 +- crates/core/src/lib.rs | 7 + crates/data/src/option_vec.rs | 34 +- crates/data/src/rmxp/animation.rs | 4 +- crates/graphics/src/frame.rs | 82 ++- .../src/primitives/shaders/sprite.wgsl | 36 +- .../graphics/src/primitives/sprite/graphic.rs | 83 ++- crates/graphics/src/primitives/sprite/mod.rs | 15 +- crates/ui/src/windows/animations.rs | 541 +++++++++++++++--- 10 files changed, 696 insertions(+), 135 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 3f4efb1b..2ef33ea6 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -72,6 +72,7 @@ impl AnimationFrameView { ui: &mut egui::Ui, update_state: &luminol_core::UpdateState<'_>, clip_rect: egui::Rect, + screen_color: luminol_data::Color, ) -> egui::InnerResponse> { let canvas_rect = ui.max_rect(); let canvas_center = canvas_rect.center(); @@ -155,6 +156,20 @@ impl AnimationFrameView { painter, )); + let screen_alpha = screen_color.alpha.clamp(0., 255.).trunc() as u8; + if screen_alpha > 0 { + ui.painter().rect_filled( + egui::Rect::EVERYTHING, + egui::Rounding::ZERO, + egui::Color32::from_rgba_unmultiplied( + screen_color.red.clamp(0., 255.).trunc() as u8, + screen_color.green.clamp(0., 255.).trunc() as u8, + screen_color.blue.clamp(0., 255.).trunc() as u8, + screen_color.alpha.clamp(0., 255.).trunc() as u8, + ), + ); + } + let offset = canvas_center.to_vec2() + self.pan; // Draw the grid lines and the border of the animation frame diff --git a/crates/components/src/collapsing_view.rs b/crates/components/src/collapsing_view.rs index f70cd274..692f5287 100644 --- a/crates/components/src/collapsing_view.rs +++ b/crates/components/src/collapsing_view.rs @@ -82,7 +82,7 @@ impl CollapsingView { state_id: usize, vec: &mut Vec, show_header: impl FnMut(&mut egui::Ui, usize, &T), - show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, + mut show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, ) -> egui::Response where T: Default, @@ -92,7 +92,7 @@ impl CollapsingView { state_id, vec, show_header, - show_body, + |ui, index, _before, item| show_body(ui, index, item), |_vec, _expanded_entry| false, ) } @@ -124,7 +124,7 @@ impl CollapsingView { state_id: usize, vec: &mut Vec, show_header: impl FnMut(&mut egui::Ui, usize, &T), - show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, + show_body: impl FnMut(&mut egui::Ui, usize, &[T], &mut T) -> egui::Response, mut cmp: impl FnMut(&T, &T) -> std::cmp::Ordering, ) -> egui::Response where @@ -177,7 +177,7 @@ impl CollapsingView { state_id: usize, vec: &mut Vec, mut show_header: impl FnMut(&mut egui::Ui, usize, &T), - mut show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, + mut show_body: impl FnMut(&mut egui::Ui, usize, &[T], &mut T) -> egui::Response, mut sort_impl: impl FnMut(&mut Vec, &mut Option) -> bool, ) -> egui::Response where @@ -204,7 +204,9 @@ impl CollapsingView { } } - for (i, entry) in vec.iter_mut().enumerate() { + for i in 0..vec.len() { + let (before, entry_and_after) = vec.split_at_mut(i); + let entry = &mut entry_and_after[0]; let ui_id = ui.make_persistent_id(i); // Forget whether the collapsing header was open from the last time @@ -247,7 +249,7 @@ impl CollapsingView { }) .body(|ui| { ui.with_layout(layout, |ui| { - modified |= show_body(ui, i, entry).changed(); + modified |= show_body(ui, i, before, entry).changed(); if ui.button("Delete").clicked() { modified = true; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 93f69485..a678e148 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -451,3 +451,10 @@ pub fn slice_is_sorted_by std::cmp::Ordering>(s: &[T], mu f(a, b) != std::cmp::Ordering::Greater }) } + +pub fn slice_is_sorted_by_key K>(s: &[T], mut f: F) -> bool { + s.windows(2).all(|w| { + let [a, b] = w else { unreachable!() }; // could maybe do unreachable_unchecked + f(a) <= f(b) + }) +} diff --git a/crates/data/src/option_vec.rs b/crates/data/src/option_vec.rs index 6e16ef12..96b317ab 100644 --- a/crates/data/src/option_vec.rs +++ b/crates/data/src/option_vec.rs @@ -15,7 +15,10 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use std::ops::{Index, IndexMut}; +use std::{ + iter::FusedIterator, + ops::{Index, IndexMut}, +}; use alox_48::SerializeHash; use serde::ser::SerializeMap; @@ -29,11 +32,13 @@ pub struct OptionVec { #[derive(Debug)] pub struct Iter<'a, T> { + size: usize, vec_iter: std::iter::Enumerate>>, } #[derive(Debug)] pub struct IterMut<'a, T> { + size: usize, vec_iter: std::iter::Enumerate>>, } @@ -60,6 +65,10 @@ impl OptionVec { self.vec.is_empty() } + pub fn contains(&self, index: usize) -> bool { + self.get(index).is_some() + } + pub fn get(&self, index: usize) -> Option<&T> { self.vec.get(index).and_then(|x| x.as_ref()) } @@ -195,6 +204,7 @@ impl<'a, T> IntoIterator for &'a OptionVec { type IntoIter = Iter<'a, T>; fn into_iter(self) -> Self::IntoIter { Self::IntoIter { + size: self.size(), vec_iter: self.vec.iter().enumerate(), } } @@ -205,6 +215,7 @@ impl<'a, T> IntoIterator for &'a mut OptionVec { type IntoIter = IterMut<'a, T>; fn into_iter(self) -> Self::IntoIter { Self::IntoIter { + size: self.size(), vec_iter: self.vec.iter_mut().enumerate(), } } @@ -215,6 +226,7 @@ impl<'a, T> Iterator for Iter<'a, T> { fn next(&mut self) -> Option { for (index, element) in &mut self.vec_iter { if let Some(element) = element { + self.size -= 1; return Some((index, element)); } } @@ -226,6 +238,7 @@ impl DoubleEndedIterator for Iter<'_, T> { fn next_back(&mut self) -> Option { while let Some((index, element)) = self.vec_iter.next_back() { if let Some(element) = element { + self.size -= 1; return Some((index, element)); } } @@ -233,11 +246,20 @@ impl DoubleEndedIterator for Iter<'_, T> { } } +impl ExactSizeIterator for Iter<'_, T> { + fn len(&self) -> usize { + self.size + } +} + +impl FusedIterator for Iter<'_, T> {} + impl<'a, T> Iterator for IterMut<'a, T> { type Item = (usize, &'a mut T); fn next(&mut self) -> Option { for (index, element) in &mut self.vec_iter { if let Some(element) = element { + self.size -= 1; return Some((index, element)); } } @@ -249,6 +271,7 @@ impl DoubleEndedIterator for IterMut<'_, T> { fn next_back(&mut self) -> Option { while let Some((index, element)) = self.vec_iter.next_back() { if let Some(element) = element { + self.size -= 1; return Some((index, element)); } } @@ -256,9 +279,18 @@ impl DoubleEndedIterator for IterMut<'_, T> { } } +impl ExactSizeIterator for IterMut<'_, T> { + fn len(&self) -> usize { + self.size + } +} + +impl FusedIterator for IterMut<'_, T> {} + impl Clone for Iter<'_, T> { fn clone(&self) -> Self { Self { + size: self.size, vec_iter: self.vec_iter.clone(), } } diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index 247efa23..401b5e1e 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -40,11 +40,11 @@ pub struct Animation { #[derive(alox_48::Deserialize, alox_48::Serialize)] #[marshal(class = "RPG::Animation::Timing")] pub struct Timing { - pub frame: i32, + pub frame: usize, pub se: AudioFile, pub flash_scope: Scope, pub flash_color: Color, - pub flash_duration: i32, + pub flash_duration: usize, pub condition: Condition, } diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index ab4d25d5..9e54dbe1 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -81,9 +81,16 @@ impl Frame { graphics_state: &GraphicsState, system: &luminol_data::rpg::System, animation: &luminol_data::rpg::Animation, + flash: luminol_data::Color, + hidden: bool, ) { - self.battler_sprite = - self.create_battler_sprite(graphics_state, animation.position, system.battler_hue); + self.battler_sprite = self.create_battler_sprite( + graphics_state, + animation.position, + system.battler_hue, + flash, + hidden, + ); } pub fn update_battler( @@ -91,6 +98,8 @@ impl Frame { graphics_state: &GraphicsState, system: &luminol_data::rpg::System, animation: &luminol_data::rpg::Animation, + flash: Option, + hidden: Option, ) { if let Some(texture) = &self.battler_texture { if let Some(sprite) = &mut self.battler_sprite { @@ -116,14 +125,43 @@ impl Frame { }, ), ); - sprite - .graphic - .set_hue(&graphics_state.render_state, system.battler_hue); + sprite.graphic.set( + &graphics_state.render_state, + if let Some(hidden) = hidden { + if hidden { + 0 + } else { + 255 + } + } else { + sprite.graphic.opacity() + }, + 1., + 0, + system.battler_hue, + if let Some(flash) = flash { + ( + flash.red.clamp(0., 255.).trunc() as u8, + flash.green.clamp(0., 255.).trunc() as u8, + flash.blue.clamp(0., 255.).trunc() as u8, + flash.alpha as f32, + ) + } else { + sprite.graphic.flash() + }, + ); } else { self.battler_sprite = self.create_battler_sprite( graphics_state, animation.position, system.battler_hue, + flash.unwrap_or(luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + }), + hidden.unwrap_or_default(), ); } } else { @@ -136,15 +174,18 @@ impl Frame { graphics_state: &GraphicsState, position: luminol_data::rpg::animation::Position, hue: i32, + flash: luminol_data::Color, + hidden: bool, ) -> Option { self.battler_texture.as_ref().map(|texture| { let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, texture.size_vec2()); let quad = Quad::new(rect, rect); - Sprite::new( + Sprite::new_full( graphics_state, quad, hue, - 255, + if hidden { 0 } else { 255 }, + 1., BlendMode::Normal, texture, &self.viewport, @@ -170,6 +211,13 @@ impl Frame { }, ), ), + 0, + ( + flash.red.clamp(0., 255.).trunc() as u8, + flash.green.clamp(0., 255.).trunc() as u8, + flash.blue.clamp(0., 255.).trunc() as u8, + flash.alpha as f32, + ), ) }) } @@ -301,7 +349,7 @@ impl Frame { let offset_x = frame.cell_data[(cell_index, 1)] as f32; let offset_y = frame.cell_data[(cell_index, 2)] as f32; let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; - let rotation = -(frame.cell_data[(cell_index, 4)] as f32).to_radians(); + let rotation = frame.cell_data[(cell_index, 4)]; let flip = frame.cell_data[(cell_index, 5)] == 1; let opacity = frame.cell_data[(cell_index, 6)] as i32; let blend_mode = match frame.cell_data[(cell_index, 7)] { @@ -311,10 +359,11 @@ impl Frame { }; let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); - let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); + let glam::Vec2 { x: cos, y: sin } = + glam::Vec2::from_angle((rotation as f32).to_radians()); Cell { - sprite: Sprite::new_with_rotation( + sprite: Sprite::new_full( graphics_state, self.atlas.calc_quad(id), hue, @@ -326,11 +375,12 @@ impl Frame { Transform::new( graphics_state, glam::vec2(offset_x, offset_y) - + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) + + glam::Mat2::from_cols_array(&[cos, -sin, sin, cos]) * (scale * flip_vec * CELL_OFFSET), scale * flip_vec, ), if flip { -rotation } else { rotation }, + (255, 255, 255, 0.), ), rect: egui::Rect::from_center_size( @@ -356,7 +406,7 @@ impl Frame { let offset_x = frame.cell_data[(cell_index, 1)] as f32; let offset_y = frame.cell_data[(cell_index, 2)] as f32; let scale = frame.cell_data[(cell_index, 3)] as f32 / 100.; - let rotation = -(frame.cell_data[(cell_index, 4)] as f32).to_radians(); + let rotation = frame.cell_data[(cell_index, 4)]; let flip = frame.cell_data[(cell_index, 5)] == 1; let opacity = frame.cell_data[(cell_index, 6)] as i32; let blend_mode = match frame.cell_data[(cell_index, 7)] { @@ -366,22 +416,24 @@ impl Frame { }; let flip_vec = glam::vec2(if flip { -1. } else { 1. }, 1.); - let glam::Vec2 { x: cos, y: sin } = glam::Vec2::from_angle(rotation); + let glam::Vec2 { x: cos, y: sin } = + glam::Vec2::from_angle((rotation as f32).to_radians()); cell.sprite.transform.set( &graphics_state.render_state, glam::vec2(offset_x, offset_y) - + glam::Mat2::from_cols_array(&[cos, sin, -sin, cos]) + + glam::Mat2::from_cols_array(&[cos, -sin, sin, cos]) * (scale * flip_vec * CELL_OFFSET), scale * flip_vec, ); cell.sprite.graphic.set( &graphics_state.render_state, - hue, opacity, opacity_multiplier, if flip { -rotation } else { rotation }, + hue, + (255, 255, 255, 0.), ); cell.sprite.set_quad( diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl index 40314e66..ba536ca1 100644 --- a/crates/graphics/src/primitives/shaders/sprite.wgsl +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -14,10 +14,10 @@ struct VertexOutput { } struct Graphic { - hue: f32, opacity: f32, - rotation: f32, // clockwise in radians - _padding: u32, + packed_hue_and_rotation: i32, + flash_alpha: f32, + packed_flash_color: u32, } @group(0) @binding(0) @@ -33,22 +33,22 @@ var transform: Trans::Transform; var graphic: Graphic; @vertex -fn vs_main( - model: VertexInput, -) -> VertexOutput { +fn vs_main(model: VertexInput) -> VertexOutput { var out: VertexOutput; out.tex_coords = model.tex_coords; + let rotation = extractBits(graphic.packed_hue_and_rotation, 16u, 16u); var position_after_rotation: vec2; - if graphic.rotation == 0 { + if rotation == 0 { position_after_rotation = model.position; } else { - let c = cos(graphic.rotation); - let s = sin(graphic.rotation); - position_after_rotation = mat2x2(c, s, -s, c) * model.position; + let r = radians(f32(rotation)); + let c = cos(r); + let s = sin(r); + position_after_rotation = mat2x2(c, -s, s, c) * model.position; } - out.clip_position = vec4(Trans::translate_vertex(position_after_rotation, viewport, transform), 0.0, 1.0); + out.clip_position = vec4(Trans::translate_vertex(position_after_rotation, viewport, transform), 0., 1.); return out; } @@ -62,10 +62,20 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { discard; } - if graphic.hue > 0.0 { + if graphic.flash_alpha > 0.001 { + let flash_color = vec3(vec3( + extractBits(graphic.packed_flash_color, 0u, 8u), + extractBits(graphic.packed_flash_color, 8u, 8u), + extractBits(graphic.packed_flash_color, 16u, 8u), + )) / 255.; + tex_sample = vec4(mix(tex_sample.rgb, flash_color, graphic.flash_alpha / 255.), tex_sample.a); + } + + let hue = extractBits(graphic.packed_hue_and_rotation, 0u, 16u); + if hue > 0 { var hsv = Hue::rgb_to_hsv(tex_sample.rgb); - hsv.x += graphic.hue; + hsv.x += f32(hue) / 360.; tex_sample = vec4(Hue::hsv_to_rgb(hsv), tex_sample.a); } diff --git a/crates/graphics/src/primitives/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs index 68c3be5f..d6dd1b84 100644 --- a/crates/graphics/src/primitives/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -30,31 +30,36 @@ pub struct Graphic { #[repr(C, align(16))] #[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] struct Data { - hue: f32, opacity: f32, - /// clockwise in radians - rotation: f32, - _padding: u32, + hue: i16, + /// counterclockwise in degrees + rotation: i16, + flash_alpha: f32, + flash_red: u8, + flash_green: u8, + flash_blue: u8, + _padding: u8, } impl Graphic { - pub fn new(graphics_state: &GraphicsState, hue: i32, opacity: i32, rotation: f32) -> Self { - Self::new_with_opacity_multiplier(graphics_state, hue, opacity, 1., rotation) - } - - pub fn new_with_opacity_multiplier( + pub fn new( graphics_state: &GraphicsState, - hue: i32, opacity: i32, opacity_multiplier: f32, - rotation: f32, + hue: i32, + rotation: i16, + flash: (u8, u8, u8, f32), ) -> Self { - let hue = (hue % 360) as f32 / 360.0; let computed_opacity = opacity as f32 / 255.0 * opacity_multiplier; + let (flash_red, flash_green, flash_blue, flash_alpha) = flash; let data = Data { - hue, opacity: computed_opacity, + hue: (hue % 360) as i16, rotation, + flash_alpha, + flash_red, + flash_green, + flash_blue, _padding: 0, }; @@ -75,11 +80,11 @@ impl Graphic { } pub fn hue(&self) -> i32 { - (self.data.hue * 360.) as i32 + self.data.hue as i32 } pub fn set_hue(&mut self, render_state: &luminol_egui_wgpu::RenderState, hue: i32) { - let hue = (hue % 360) as f32 / 360.0; + let hue = (hue % 360) as i16; if self.data.hue != hue { self.data.hue = hue; @@ -119,31 +124,67 @@ impl Graphic { } } - pub fn rotation(&self) -> f32 { + pub fn rotation(&self) -> i16 { self.data.rotation } - pub fn set_rotation(&mut self, render_state: &luminol_egui_wgpu::RenderState, rotation: f32) { + pub fn set_rotation(&mut self, render_state: &luminol_egui_wgpu::RenderState, rotation: i16) { if self.data.rotation != rotation { self.data.rotation = rotation; self.regen_buffer(render_state); } } + pub fn flash(&self) -> (u8, u8, u8, f32) { + ( + self.data.flash_red, + self.data.flash_green, + self.data.flash_blue, + self.data.flash_alpha, + ) + } + + pub fn set_flash( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + flash: (u8, u8, u8, f32), + ) { + if ( + self.data.flash_red, + self.data.flash_green, + self.data.flash_blue, + self.data.flash_alpha, + ) != flash + { + ( + self.data.flash_red, + self.data.flash_green, + self.data.flash_blue, + self.data.flash_alpha, + ) = flash; + self.regen_buffer(render_state); + } + } + pub fn set( &mut self, render_state: &luminol_egui_wgpu::RenderState, - hue: i32, opacity: i32, opacity_multiplier: f32, - rotation: f32, + rotation: i16, + hue: i32, + flash: (u8, u8, u8, f32), ) { - let hue = (hue % 360) as f32 / 360.0; let computed_opacity = opacity as f32 / 255.0 * opacity_multiplier; + let (flash_red, flash_green, flash_blue, flash_alpha) = flash; let data = Data { - hue, opacity: computed_opacity, + hue: (hue % 360) as i16, rotation, + flash_alpha, + flash_red, + flash_green, + flash_blue, _padding: 0, }; if data != self.data { diff --git a/crates/graphics/src/primitives/sprite/mod.rs b/crates/graphics/src/primitives/sprite/mod.rs index 3236f2dd..8c5f237e 100644 --- a/crates/graphics/src/primitives/sprite/mod.rs +++ b/crates/graphics/src/primitives/sprite/mod.rs @@ -49,7 +49,7 @@ impl Sprite { viewport: &Viewport, transform: Transform, ) -> Self { - Self::new_with_rotation( + Self::new_full( graphics_state, quad, hue, @@ -59,12 +59,13 @@ impl Sprite { texture, viewport, transform, - 0., + 0, + (255, 255, 255, 0.), ) } #[allow(clippy::too_many_arguments)] - pub fn new_with_rotation( + pub fn new_full( graphics_state: &GraphicsState, quad: Quad, hue: i32, @@ -75,16 +76,18 @@ impl Sprite { texture: &Texture, viewport: &Viewport, transform: Transform, - rotation: f32, + rotation: i16, + flash: (u8, u8, u8, f32), ) -> Self { let vertices = vertices::Vertices::from_quads(&graphics_state.render_state, &[quad], texture.size()); - let graphic = graphic::Graphic::new_with_opacity_multiplier( + let graphic = graphic::Graphic::new( graphics_state, - hue, opacity, opacity_multiplier, + hue, rotation, + flash, ); let mut bind_group_builder = BindGroupBuilder::new(); diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 539f2425..25ddbe70 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -26,7 +26,7 @@ use egui::Widget; use luminol_components::UiExt; use luminol_core::Modal; -use luminol_data::BlendMode; +use luminol_data::{rpg::animation::Scope, BlendMode}; use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; use luminol_modals::sound_picker::Modal as SoundPicker; @@ -35,11 +35,10 @@ pub struct Window { selected_animation_name: Option, previous_animation: Option, previous_battler_name: Option, - previous_timing_frame: Option, frame_edit_state: FrameEditState, + timing_edit_state: TimingEditState, collapsing_view: luminol_components::CollapsingView, - timing_se_picker: SoundPicker, modals: Modals, view: luminol_components::DatabaseView, } @@ -49,6 +48,19 @@ struct FrameEditState { enable_onion_skin: bool, frame_view: Option, cellpicker: Option, + flash_maps: luminol_data::OptionVec, +} + +struct TimingEditState { + previous_frame: Option, + se_picker: SoundPicker, +} + +#[derive(Debug, Default)] +struct FlashMaps { + hide: FlashMap, + target: FlashMap, + screen: FlashMap, } struct Modals { @@ -67,24 +79,152 @@ impl Modals { } } +#[derive(Debug, Clone, Copy)] +struct ColorFlash { + color: luminol_data::Color, + duration: usize, +} + +#[derive(Debug, Clone, Copy)] +struct HideFlash { + duration: usize, +} + +#[derive(Debug)] +struct FlashMap(std::collections::BTreeMap>); + +impl Default for FlashMap { + fn default() -> Self { + Self(std::collections::BTreeMap::new()) + } +} + +impl FromIterator<(usize, T)> for FlashMap +where + T: Copy, +{ + fn from_iter>(iterable: I) -> Self { + let mut map = Self(Default::default()); + for (frame, flash) in iterable.into_iter() { + map.insert(frame, flash); + } + map + } +} + +impl FlashMap +where + T: Copy, +{ + /// Adds a new flash into the map. + fn insert(&mut self, frame: usize, flash: T) { + self.0 + .entry(frame) + .and_modify(|e| e.push_back(flash)) + .or_insert_with(|| [flash].into()); + } + + /// Removes a flash from the map. + fn remove(&mut self, frame: usize, rank: usize) -> T { + let deque = self + .0 + .get_mut(&frame) + .expect("no flashes found for the given frame"); + let flash = deque.remove(rank).expect("rank out of bounds"); + if deque.is_empty() { + self.0.remove(&frame).unwrap(); + } + flash + } + + /// Modifies the frame number for a flash. + fn set_frame(&mut self, frame: usize, rank: usize, new_frame: usize) { + if frame == new_frame { + return; + } + let flash = self.remove(frame, rank); + self.0 + .entry(new_frame) + .and_modify(|e| { + if new_frame > frame { + e.push_front(flash) + } else { + e.push_back(flash) + } + }) + .or_insert_with(|| [flash].into()); + } + + fn get_mut(&mut self, frame: usize, rank: usize) -> Option<&mut T> { + self.0.get_mut(&frame).and_then(|deque| deque.get_mut(rank)) + } +} + +impl FlashMap { + /// Determines what color the flash should be for a given frame number. + fn compute(&self, frame: usize) -> luminol_data::Color { + let Some((&start_frame, deque)) = self.0.range(..=frame).next_back() else { + return luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + }; + }; + let flash = deque.back().unwrap(); + + let diff = frame - start_frame; + if diff < flash.duration { + let progression = diff as f64 / flash.duration as f64; + luminol_data::Color { + alpha: flash.color.alpha * (1. - progression), + ..flash.color + } + } else { + luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + } + } + } +} + +impl FlashMap { + /// Determines if the hide flash is active for a given frame number. + fn compute(&self, frame: usize) -> bool { + let Some((&start_frame, deque)) = self.0.range(..=frame).next_back() else { + return false; + }; + let flash = deque.back().unwrap(); + + let diff = frame - start_frame; + diff < flash.duration + } +} + impl Default for Window { fn default() -> Self { Self { selected_animation_name: None, previous_animation: None, previous_battler_name: None, - previous_timing_frame: None, frame_edit_state: FrameEditState { frame_index: 0, enable_onion_skin: false, frame_view: None, cellpicker: None, + flash_maps: Default::default(), + }, + timing_edit_state: TimingEditState { + previous_frame: None, + se_picker: SoundPicker::new( + luminol_audio::Source::SE, + "animations_timing_se_picker", + ), }, collapsing_view: luminol_components::CollapsingView::new(), - timing_se_picker: SoundPicker::new( - luminol_audio::Source::SE, - "animations_timing_se_picker", - ), modals: Modals { copy_frames: luminol_modals::animations::copy_frames_tool::Modal::new( "animations_copy_frames_tool", @@ -116,7 +256,7 @@ impl Window { system.battler_name, animation.id + 1, animation.name, - ),), + )), ); } @@ -132,7 +272,7 @@ impl Window { animation.animation_name, animation.id + 1, animation.name, - ),), + )), ); } @@ -150,8 +290,8 @@ impl Window { }; match timing.flash_scope { - luminol_data::rpg::animation::Scope::None => {} - luminol_data::rpg::animation::Scope::Target => { + Scope::None => {} + Scope::Target => { vec.push(format!( "flash target #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", timing.flash_color.red.clamp(0., 255.).trunc() as u8, @@ -161,7 +301,7 @@ impl Window { timing.flash_duration, )); } - luminol_data::rpg::animation::Scope::Screen => { + Scope::Screen => { vec.push(format!( "flash screen #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", timing.flash_color.red.clamp(0., 255.).trunc() as u8, @@ -171,7 +311,7 @@ impl Window { timing.flash_duration, )); } - luminol_data::rpg::animation::Scope::HideTarget => { + Scope::HideTarget => { vec.push(format!("hide target for {} frames", timing.flash_duration)); } } @@ -219,15 +359,27 @@ impl Window { fn show_timing_body( ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>, - animation_id: usize, - animation_frame_max: i32, - timing_se_picker: &mut SoundPicker, - previous_timing_frame: &mut Option, - timing: (usize, &mut luminol_data::rpg::animation::Timing), + animation: &luminol_data::rpg::Animation, + flash_maps: &mut FlashMaps, + state: &mut TimingEditState, + timing: ( + usize, + &[luminol_data::rpg::animation::Timing], + &mut luminol_data::rpg::animation::Timing, + ), ) -> egui::Response { - let (timing_index, timing) = timing; + let (timing_index, previous_timings, timing) = timing; let mut modified = false; + let rank = |frame, scope| { + previous_timings + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.flash_scope == scope) + .count() + }; + let mut response = egui::Frame::none() .show(ui, |ui| { ui.columns(2, |columns| { @@ -236,82 +388,189 @@ impl Window { .add(luminol_components::Field::new( "Condition", luminol_components::EnumComboBox::new( - (animation_id, timing_index, "condition"), + (animation.id, timing_index, "condition"), &mut timing.condition, ), )) .changed(); - modified |= columns[0] + let old_frame = timing.frame; + let changed = columns[0] .add(luminol_components::Field::new( "Frame", |ui: &mut egui::Ui| { let mut frame = - previous_timing_frame.unwrap_or(timing.frame + 1); + state.previous_frame.unwrap_or(timing.frame + 1); let mut response = egui::DragValue::new(&mut frame) - .range(1..=animation_frame_max) + .range(1..=animation.frame_max) .update_while_editing(false) .ui(ui); response.changed = false; if response.dragged() { - *previous_timing_frame = Some(frame); + state.previous_frame = Some(frame); } else { timing.frame = frame - 1; - *previous_timing_frame = None; + state.previous_frame = None; response.changed = true; } response }, )) .changed(); + if changed { + match timing.flash_scope { + Scope::Target => { + flash_maps.target.set_frame( + old_frame, + rank(old_frame, Scope::Target), + timing.frame, + ); + } + Scope::Screen => { + flash_maps.screen.set_frame( + old_frame, + rank(old_frame, Scope::Screen), + timing.frame, + ); + } + Scope::HideTarget => { + flash_maps.hide.set_frame( + old_frame, + rank(old_frame, Scope::HideTarget), + timing.frame, + ); + } + Scope::None => {} + } + modified = true; + } }); modified |= columns[1] .add(luminol_components::Field::new( "SE", - timing_se_picker.button(&mut timing.se, update_state), + state.se_picker.button(&mut timing.se, update_state), )) .changed(); }); - if timing.flash_scope == luminol_data::rpg::animation::Scope::None { - modified |= ui - .add(luminol_components::Field::new( + let old_scope = timing.flash_scope; + let (scope_changed, duration_changed) = if timing.flash_scope == Scope::None { + ( + ui.add(luminol_components::Field::new( "Flash", luminol_components::EnumComboBox::new( - (animation_id, timing_index, "flash_scope"), + (animation.id, timing_index, "flash_scope"), &mut timing.flash_scope, ), )) - .changed(); + .changed(), + false, + ) } else { ui.columns(2, |columns| { - modified |= columns[0] - .add(luminol_components::Field::new( - "Flash", - luminol_components::EnumComboBox::new( - (animation_id, timing_index, "flash_scope"), - &mut timing.flash_scope, - ), - )) - .changed(); + ( + columns[0] + .add(luminol_components::Field::new( + "Flash", + luminol_components::EnumComboBox::new( + (animation.id, timing_index, "flash_scope"), + &mut timing.flash_scope, + ), + )) + .changed(), + columns[1] + .add(luminol_components::Field::new( + "Flash Duration", + egui::DragValue::new(&mut timing.flash_duration) + .range(1..=animation.frame_max), + )) + .changed(), + ) + }) + }; + + if scope_changed { + match old_scope { + Scope::Target => { + flash_maps + .target + .remove(timing.frame, rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .screen + .remove(timing.frame, rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps + .hide + .remove(timing.frame, rank(timing.frame, Scope::HideTarget)); + } + Scope::None => {} + } + match timing.flash_scope { + Scope::Target => { + flash_maps.target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + modified = true; + } - modified |= columns[1] - .add(luminol_components::Field::new( - "Flash Duration", - egui::DragValue::new(&mut timing.flash_duration) - .range(1..=animation_frame_max), - )) - .changed(); - }); + if duration_changed { + match timing.flash_scope { + Scope::Target => { + flash_maps + .target + .get_mut(timing.frame, rank(timing.frame, Scope::Target)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::Screen => { + flash_maps + .screen + .get_mut(timing.frame, rank(timing.frame, Scope::Screen)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::HideTarget => { + flash_maps + .target + .get_mut(timing.frame, rank(timing.frame, Scope::HideTarget)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::None => unreachable!(), + } + modified = true; } - if matches!( - timing.flash_scope, - luminol_data::rpg::animation::Scope::Target - | luminol_data::rpg::animation::Scope::Screen - ) { - modified |= ui + if matches!(timing.flash_scope, Scope::Target | Scope::Screen) { + let changed = ui .add(luminol_components::Field::new( "Flash Color", |ui: &mut egui::Ui| { @@ -333,6 +592,26 @@ impl Window { }, )) .changed(); + if changed { + match timing.flash_scope { + Scope::Target => { + flash_maps + .target + .get_mut(timing.frame, rank(timing.frame, Scope::Target)) + .unwrap() + .color = timing.flash_color; + } + Scope::Screen => { + flash_maps + .screen + .get_mut(timing.frame, rank(timing.frame, Scope::Screen)) + .unwrap() + .color = timing.flash_color; + } + Scope::None | Scope::HideTarget => unreachable!(), + } + modified = true; + } } }) .response; @@ -354,6 +633,8 @@ impl Window { ) -> (bool, bool) { let mut modified = false; + let flash_maps = state.flash_maps.get_mut(animation.id).unwrap(); + let frame_view = if let Some(frame_view) = &mut state.frame_view { frame_view } else { @@ -384,9 +665,13 @@ impl Window { }; let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); frame_view.frame.battler_texture = battler_texture; - frame_view - .frame - .update_battler(&update_state.graphics, system, animation); + frame_view.frame.update_battler( + &update_state.graphics, + system, + animation, + Some(flash_maps.target.compute(state.frame_index)), + Some(flash_maps.hide.compute(state.frame_index)), + ); frame_view .frame .update_all_cells(&update_state.graphics, animation, state.frame_index); @@ -423,7 +708,16 @@ impl Window { )) .changed(); state.frame_index -= 1; + let battler_color = flash_maps.target.compute(state.frame_index); + let battler_hidden = flash_maps.hide.compute(state.frame_index); if changed { + frame_view.frame.update_battler( + &update_state.graphics, + system, + animation, + Some(battler_color), + Some(battler_hidden), + ); frame_view.frame.update_all_cells( &update_state.graphics, animation, @@ -872,7 +1166,12 @@ impl Window { let egui::InnerResponse { inner: hover_pos, response, - } = frame_view.ui(ui, update_state, clip_rect); + } = frame_view.ui( + ui, + update_state, + clip_rect, + flash_maps.screen.compute(state.frame_index), + ); // If the pointer is hovering over the frame view, prevent parent widgets // from receiving scroll events so that scaling the frame view with the @@ -1003,6 +1302,60 @@ impl luminol_core::Window for Window { let clip_rect = ui.clip_rect(); + if !self.frame_edit_state.flash_maps.contains(id) { + if !luminol_core::slice_is_sorted_by_key(&animation.timings, |timing| { + timing.frame + }) { + animation.timings.sort_by_key(|timing| timing.frame); + } + self.frame_edit_state.flash_maps.insert( + id, + FlashMaps { + hide: animation + .timings + .iter() + .filter(|timing| timing.flash_scope == Scope::HideTarget) + .map(|timing| { + ( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ) + }) + .collect(), + target: animation + .timings + .iter() + .filter(|timing| timing.flash_scope == Scope::Target) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + screen: animation + .timings + .iter() + .filter(|timing| timing.flash_scope == Scope::Screen) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + }, + ); + } + ui.with_padded_stripe(false, |ui| { modified |= ui .add(luminol_components::Field::new( @@ -1029,6 +1382,8 @@ impl luminol_core::Window for Window { &update_state.graphics, &system, animation, + None, + None, ); } modified = true; @@ -1066,6 +1421,13 @@ impl luminol_core::Window for Window { &update_state.graphics, &system, animation, + luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + }, + true, ); } @@ -1096,11 +1458,23 @@ impl luminol_core::Window for Window { if let Some(frame_view) = &mut self.frame_edit_state.frame_view { + let flash_maps = + self.frame_edit_state.flash_maps.get(id).unwrap(); frame_view.frame.atlas = atlas.clone(); frame_view.frame.update_battler( &update_state.graphics, &system, animation, + Some( + flash_maps + .target + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps + .hide + .compute(self.frame_edit_state.frame_index), + ), ); frame_view.frame.rebuild_all_cells( &update_state.graphics, @@ -1145,7 +1519,9 @@ impl luminol_core::Window for Window { } ui.with_padded_stripe(true, |ui| { - modified |= ui + let flash_maps = self.frame_edit_state.flash_maps.get_mut(id).unwrap(); + + let changed = ui .add(luminol_components::Field::new( "SE and Flash", |ui: &mut egui::Ui| { @@ -1154,31 +1530,54 @@ impl luminol_core::Window for Window { } if self.previous_animation != Some(animation.id) { self.collapsing_view.clear_animations(); - self.timing_se_picker.close_window(); + self.timing_edit_state.se_picker.close_window(); } else if self.collapsing_view.is_animating() { - self.timing_se_picker.close_window(); + self.timing_edit_state.se_picker.close_window(); } - self.collapsing_view.show_with_sort( + + let mut timings = std::mem::take(&mut animation.timings); + let response = self.collapsing_view.show_with_sort( ui, animation.id, - &mut animation.timings, + &mut timings, |ui, _i, timing| Self::show_timing_header(ui, timing), - |ui, i, timing| { + |ui, i, previous_timings, timing| { Self::show_timing_body( ui, update_state, - animation.id, - animation.frame_max, - &mut self.timing_se_picker, - &mut self.previous_timing_frame, - (i, timing), + animation, + flash_maps, + &mut self.timing_edit_state, + (i, previous_timings, timing), ) }, |a, b| a.frame.cmp(&b.frame), - ) + ); + animation.timings = timings; + response }, )) .changed(); + if changed { + if let Some(frame_view) = &mut self.frame_edit_state.frame_view { + frame_view.frame.update_battler( + &update_state.graphics, + &system, + animation, + Some( + flash_maps + .target + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps + .hide + .compute(self.frame_edit_state.frame_index), + ), + ); + } + modified = true; + } }); self.previous_animation = Some(animation.id); From 678a5721de0d4af09bdd4300bc5a758f39bd5e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 29 Jul 2024 12:03:35 -0400 Subject: [PATCH 046/109] Remove `extractBits` usage from sprite shader Naga doesn't seem to be able to translate `extractBits` properly to GLSL. --- crates/graphics/src/primitives/shaders/sprite.wgsl | 12 ++++++------ crates/graphics/src/primitives/sprite/graphic.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl index ba536ca1..de282979 100644 --- a/crates/graphics/src/primitives/shaders/sprite.wgsl +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -15,7 +15,7 @@ struct VertexOutput { struct Graphic { opacity: f32, - packed_hue_and_rotation: i32, + packed_rotation_and_hue: i32, flash_alpha: f32, packed_flash_color: u32, } @@ -37,7 +37,7 @@ fn vs_main(model: VertexInput) -> VertexOutput { var out: VertexOutput; out.tex_coords = model.tex_coords; - let rotation = extractBits(graphic.packed_hue_and_rotation, 16u, 16u); + let rotation = (graphic.packed_rotation_and_hue << 16) >> 16; var position_after_rotation: vec2; if rotation == 0 { position_after_rotation = model.position; @@ -64,14 +64,14 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if graphic.flash_alpha > 0.001 { let flash_color = vec3(vec3( - extractBits(graphic.packed_flash_color, 0u, 8u), - extractBits(graphic.packed_flash_color, 8u, 8u), - extractBits(graphic.packed_flash_color, 16u, 8u), + graphic.packed_flash_color & 0xff, + (graphic.packed_flash_color >> 8) & 0xff, + (graphic.packed_flash_color >> 16) & 0xff, )) / 255.; tex_sample = vec4(mix(tex_sample.rgb, flash_color, graphic.flash_alpha / 255.), tex_sample.a); } - let hue = extractBits(graphic.packed_hue_and_rotation, 0u, 16u); + let hue = graphic.packed_rotation_and_hue >> 16; if hue > 0 { var hsv = Hue::rgb_to_hsv(tex_sample.rgb); diff --git a/crates/graphics/src/primitives/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs index d6dd1b84..b4228233 100644 --- a/crates/graphics/src/primitives/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -31,9 +31,9 @@ pub struct Graphic { #[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] struct Data { opacity: f32, - hue: i16, /// counterclockwise in degrees rotation: i16, + hue: i16, flash_alpha: f32, flash_red: u8, flash_green: u8, From 1c4180429380de92c3f5b6ad67b9147ec12ab365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 29 Jul 2024 12:37:06 -0400 Subject: [PATCH 047/109] Apply target flash after gamma conversion instead of before --- .../src/primitives/shaders/sprite.wgsl | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl index de282979..79b60621 100644 --- a/crates/graphics/src/primitives/shaders/sprite.wgsl +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -62,6 +62,16 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { discard; } + let hue = graphic.packed_rotation_and_hue >> 16; + if hue > 0 { + var hsv = Hue::rgb_to_hsv(tex_sample.rgb); + + hsv.x += f32(hue) / 360.; + tex_sample = vec4(Hue::hsv_to_rgb(hsv), tex_sample.a); + } + + tex_sample = Gamma::from_linear_rgba(tex_sample); + if graphic.flash_alpha > 0.001 { let flash_color = vec3(vec3( graphic.packed_flash_color & 0xff, @@ -71,13 +81,5 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { tex_sample = vec4(mix(tex_sample.rgb, flash_color, graphic.flash_alpha / 255.), tex_sample.a); } - let hue = graphic.packed_rotation_and_hue >> 16; - if hue > 0 { - var hsv = Hue::rgb_to_hsv(tex_sample.rgb); - - hsv.x += f32(hue) / 360.; - tex_sample = vec4(Hue::hsv_to_rgb(hsv), tex_sample.a); - } - - return Gamma::from_linear_rgba(tex_sample); + return tex_sample; } From 17bd35fb987bf11f710b4504990a14c33ba17826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 29 Jul 2024 14:15:17 -0400 Subject: [PATCH 048/109] Fix alpha of animation editor screen flash being wrong --- crates/components/src/animation_frame_view.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 2ef33ea6..ec4cbd4e 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -156,7 +156,9 @@ impl AnimationFrameView { painter, )); - let screen_alpha = screen_color.alpha.clamp(0., 255.).trunc() as u8; + let screen_alpha = (egui::ecolor::linear_from_gamma(screen_color.alpha as f32 / 255.) + * 255.) + .trunc() as u8; if screen_alpha > 0 { ui.painter().rect_filled( egui::Rect::EVERYTHING, @@ -165,7 +167,7 @@ impl AnimationFrameView { screen_color.red.clamp(0., 255.).trunc() as u8, screen_color.green.clamp(0., 255.).trunc() as u8, screen_color.blue.clamp(0., 255.).trunc() as u8, - screen_color.alpha.clamp(0., 255.).trunc() as u8, + screen_alpha, ), ); } From a38ac52eb5187a8f23f13f62f1d12763d2fe37f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 29 Jul 2024 14:19:44 -0400 Subject: [PATCH 049/109] Round animation editor colors instead of truncating --- crates/components/src/animation_frame_view.rs | 8 +++---- crates/graphics/src/frame.rs | 12 +++++----- crates/ui/src/windows/animations.rs | 24 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index ec4cbd4e..9eea03cf 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -158,15 +158,15 @@ impl AnimationFrameView { let screen_alpha = (egui::ecolor::linear_from_gamma(screen_color.alpha as f32 / 255.) * 255.) - .trunc() as u8; + .round() as u8; if screen_alpha > 0 { ui.painter().rect_filled( egui::Rect::EVERYTHING, egui::Rounding::ZERO, egui::Color32::from_rgba_unmultiplied( - screen_color.red.clamp(0., 255.).trunc() as u8, - screen_color.green.clamp(0., 255.).trunc() as u8, - screen_color.blue.clamp(0., 255.).trunc() as u8, + screen_color.red.clamp(0., 255.).round() as u8, + screen_color.green.clamp(0., 255.).round() as u8, + screen_color.blue.clamp(0., 255.).round() as u8, screen_alpha, ), ); diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 9e54dbe1..8ed7373d 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -141,9 +141,9 @@ impl Frame { system.battler_hue, if let Some(flash) = flash { ( - flash.red.clamp(0., 255.).trunc() as u8, - flash.green.clamp(0., 255.).trunc() as u8, - flash.blue.clamp(0., 255.).trunc() as u8, + flash.red.clamp(0., 255.).round() as u8, + flash.green.clamp(0., 255.).round() as u8, + flash.blue.clamp(0., 255.).round() as u8, flash.alpha as f32, ) } else { @@ -213,9 +213,9 @@ impl Frame { ), 0, ( - flash.red.clamp(0., 255.).trunc() as u8, - flash.green.clamp(0., 255.).trunc() as u8, - flash.blue.clamp(0., 255.).trunc() as u8, + flash.red.clamp(0., 255.).round() as u8, + flash.green.clamp(0., 255.).round() as u8, + flash.blue.clamp(0., 255.).round() as u8, flash.alpha as f32, ), ) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 25ddbe70..f5b941ad 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -294,20 +294,20 @@ impl Window { Scope::Target => { vec.push(format!( "flash target #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", - timing.flash_color.red.clamp(0., 255.).trunc() as u8, - timing.flash_color.green.clamp(0., 255.).trunc() as u8, - timing.flash_color.blue.clamp(0., 255.).trunc() as u8, - timing.flash_color.alpha.clamp(0., 255.).trunc() as u8, + timing.flash_color.red.clamp(0., 255.).round() as u8, + timing.flash_color.green.clamp(0., 255.).round() as u8, + timing.flash_color.blue.clamp(0., 255.).round() as u8, + timing.flash_color.alpha.clamp(0., 255.).round() as u8, timing.flash_duration, )); } Scope::Screen => { vec.push(format!( "flash screen #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", - timing.flash_color.red.clamp(0., 255.).trunc() as u8, - timing.flash_color.green.clamp(0., 255.).trunc() as u8, - timing.flash_color.blue.clamp(0., 255.).trunc() as u8, - timing.flash_color.alpha.clamp(0., 255.).trunc() as u8, + timing.flash_color.red.clamp(0., 255.).round() as u8, + timing.flash_color.green.clamp(0., 255.).round() as u8, + timing.flash_color.blue.clamp(0., 255.).round() as u8, + timing.flash_color.alpha.clamp(0., 255.).round() as u8, timing.flash_duration, )); } @@ -575,10 +575,10 @@ impl Window { "Flash Color", |ui: &mut egui::Ui| { let mut color = [ - timing.flash_color.red.clamp(0., 255.).trunc() as u8, - timing.flash_color.green.clamp(0., 255.).trunc() as u8, - timing.flash_color.blue.clamp(0., 255.).trunc() as u8, - timing.flash_color.alpha.clamp(0., 255.).trunc() as u8, + timing.flash_color.red.clamp(0., 255.).round() as u8, + timing.flash_color.green.clamp(0., 255.).round() as u8, + timing.flash_color.blue.clamp(0., 255.).round() as u8, + timing.flash_color.alpha.clamp(0., 255.).round() as u8, ]; ui.spacing_mut().interact_size.x = ui.available_width(); // make the color picker button as wide as possible let response = ui.color_edit_button_srgba_unmultiplied(&mut color); From 7e541e17692310a440e5783f45cbbaba9fe9eeb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 29 Jul 2024 15:00:54 -0400 Subject: [PATCH 050/109] Apply gamma conversion before hue shift in sprite shader --- crates/graphics/src/primitives/shaders/sprite.wgsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl index 79b60621..55650214 100644 --- a/crates/graphics/src/primitives/shaders/sprite.wgsl +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -62,16 +62,16 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { discard; } + tex_sample = Gamma::from_linear_rgba(tex_sample); + let hue = graphic.packed_rotation_and_hue >> 16; - if hue > 0 { + if hue != 0 { var hsv = Hue::rgb_to_hsv(tex_sample.rgb); hsv.x += f32(hue) / 360.; tex_sample = vec4(Hue::hsv_to_rgb(hsv), tex_sample.a); } - tex_sample = Gamma::from_linear_rgba(tex_sample); - if graphic.flash_alpha > 0.001 { let flash_color = vec3(vec3( graphic.packed_flash_color & 0xff, From 934755740c2780f16f72bf0d715f5600b4d39362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 30 Jul 2024 12:11:10 -0400 Subject: [PATCH 051/109] Fix typo in animation editor `duration_changed` flash map handler --- crates/ui/src/windows/animations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index f5b941ad..12e6908d 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -559,7 +559,7 @@ impl Window { } Scope::HideTarget => { flash_maps - .target + .hide .get_mut(timing.frame, rank(timing.frame, Scope::HideTarget)) .unwrap() .duration = timing.flash_duration; From f63c73c9a873d49db10ce1a9a261d9c784827b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 30 Jul 2024 12:11:41 -0400 Subject: [PATCH 052/109] Implement flash map handling of creating and deleting flashes --- crates/components/src/collapsing_view.rs | 46 ++++++-- crates/ui/src/windows/animations.rs | 142 ++++++++++++++++++++--- crates/ui/src/windows/classes.rs | 38 +++--- crates/ui/src/windows/enemies.rs | 38 +++--- 4 files changed, 202 insertions(+), 62 deletions(-) diff --git a/crates/components/src/collapsing_view.rs b/crates/components/src/collapsing_view.rs index 692f5287..2de7ad61 100644 --- a/crates/components/src/collapsing_view.rs +++ b/crates/components/src/collapsing_view.rs @@ -35,6 +35,21 @@ pub struct CollapsingView { need_sort: bool, } +#[derive(Clone, Copy)] +pub struct CollapsingViewInner { + pub created_entry: Option, + pub deleted_entry: Option<(usize, T)>, +} + +impl Default for CollapsingViewInner { + fn default() -> Self { + Self { + created_entry: None, + deleted_entry: None, + } + } +} + impl CollapsingView { pub fn new() -> Self { Default::default() @@ -83,7 +98,7 @@ impl CollapsingView { vec: &mut Vec, show_header: impl FnMut(&mut egui::Ui, usize, &T), mut show_body: impl FnMut(&mut egui::Ui, usize, &mut T) -> egui::Response, - ) -> egui::Response + ) -> egui::InnerResponse> where T: Default, { @@ -126,7 +141,7 @@ impl CollapsingView { show_header: impl FnMut(&mut egui::Ui, usize, &T), show_body: impl FnMut(&mut egui::Ui, usize, &[T], &mut T) -> egui::Response, mut cmp: impl FnMut(&T, &T) -> std::cmp::Ordering, - ) -> egui::Response + ) -> egui::InnerResponse> where T: Default, { @@ -179,15 +194,18 @@ impl CollapsingView { mut show_header: impl FnMut(&mut egui::Ui, usize, &T), mut show_body: impl FnMut(&mut egui::Ui, usize, &[T], &mut T) -> egui::Response, mut sort_impl: impl FnMut(&mut Vec, &mut Option) -> bool, - ) -> egui::Response + ) -> egui::InnerResponse> where T: Default, { self.is_animating = false; + let mut created_entry_index = None; + let mut deleted_entry_index = None; + let mut deleted_entry = None; + let mut inner_response = ui.with_cross_justify(|ui| { let mut modified = false; - let mut deleted_entry = None; let mut new_entry = false; ui.group(|ui| { @@ -253,7 +271,7 @@ impl CollapsingView { if ui.button("Delete").clicked() { modified = true; - deleted_entry = Some(i); + deleted_entry_index = Some(i); } ui.add_space(ui.spacing().item_spacing.y); @@ -274,12 +292,14 @@ impl CollapsingView { new_entry = true; sort_impl(vec, expanded_entry); + + created_entry_index = *expanded_entry; } }); self.disable_animations = false; - if let Some(i) = deleted_entry { + if let Some(i) = deleted_entry_index { if let Some(expanded_entry) = self.expanded_entry.get_mut(state_id) { if *expanded_entry == Some(i) { self.disable_animations = true; @@ -290,7 +310,7 @@ impl CollapsingView { } } - vec.remove(i); + deleted_entry = Some(vec.remove(i)); } self.depersisted_entries = vec.len(); @@ -304,6 +324,16 @@ impl CollapsingView { if inner_response.inner { inner_response.response.mark_changed(); } - inner_response.response + egui::InnerResponse { + inner: CollapsingViewInner { + created_entry: created_entry_index, + deleted_entry: if let (Some(i), Some(e)) = (deleted_entry_index, deleted_entry) { + Some((i, e)) + } else { + None + }, + }, + response: inner_response.response, + } } } diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 12e6908d..707a9a53 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -1518,9 +1518,10 @@ impl luminol_core::Window for Window { return true; } - ui.with_padded_stripe(true, |ui| { - let flash_maps = self.frame_edit_state.flash_maps.get_mut(id).unwrap(); + let mut collapsing_view_inner = Default::default(); + let flash_maps = self.frame_edit_state.flash_maps.get_mut(id).unwrap(); + ui.with_padded_stripe(true, |ui| { let changed = ui .add(luminol_components::Field::new( "SE and Flash", @@ -1536,23 +1537,27 @@ impl luminol_core::Window for Window { } let mut timings = std::mem::take(&mut animation.timings); - let response = self.collapsing_view.show_with_sort( - ui, - animation.id, - &mut timings, - |ui, _i, timing| Self::show_timing_header(ui, timing), - |ui, i, previous_timings, timing| { - Self::show_timing_body( - ui, - update_state, - animation, - flash_maps, - &mut self.timing_edit_state, - (i, previous_timings, timing), - ) - }, - |a, b| a.frame.cmp(&b.frame), - ); + let egui::InnerResponse { inner, response } = + self.collapsing_view.show_with_sort( + ui, + animation.id, + &mut timings, + |ui, _i, timing| { + Self::show_timing_header(ui, timing) + }, + |ui, i, previous_timings, timing| { + Self::show_timing_body( + ui, + update_state, + animation, + flash_maps, + &mut self.timing_edit_state, + (i, previous_timings, timing), + ) + }, + |a, b| a.frame.cmp(&b.frame), + ); + collapsing_view_inner = inner; animation.timings = timings; response }, @@ -1580,6 +1585,105 @@ impl luminol_core::Window for Window { } }); + if let Some(i) = collapsing_view_inner.created_entry { + let timing = &animation.timings[i]; + match timing.flash_scope { + Scope::Target => { + flash_maps.target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + self.frame_edit_state + .frame_view + .as_mut() + .unwrap() + .frame + .update_battler( + &update_state.graphics, + &system, + animation, + Some( + flash_maps + .target + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps.hide.compute(self.frame_edit_state.frame_index), + ), + ); + } + + if let Some((i, timing)) = collapsing_view_inner.deleted_entry { + let rank = |frame, scope| { + animation.timings[..i] + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.flash_scope == scope) + .count() + }; + match timing.flash_scope { + Scope::Target => { + flash_maps + .target + .remove(timing.frame, rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .screen + .remove(timing.frame, rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps.hide.remove( + timing.frame, + rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + self.frame_edit_state + .frame_view + .as_mut() + .unwrap() + .frame + .update_battler( + &update_state.graphics, + &system, + animation, + Some( + flash_maps + .target + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps.hide.compute(self.frame_edit_state.frame_index), + ), + ); + } + self.previous_animation = Some(animation.id); false }, diff --git a/crates/ui/src/windows/classes.rs b/crates/ui/src/windows/classes.rs index 316962fd..bb5f0c1c 100644 --- a/crates/ui/src/windows/classes.rs +++ b/crates/ui/src/windows/classes.rs @@ -176,23 +176,27 @@ impl luminol_core::Window for Window { if self.previous_class != Some(class.id) { self.collapsing_view.clear_animations(); } - self.collapsing_view.show( - ui, - class.id, - &mut class.learnings, - |ui, _i, learning| { - Self::show_learning_header(ui, &skills, learning) - }, - |ui, i, learning| { - Self::show_learning_body( - ui, - update_state, - &skills, - class.id, - (i, learning), - ) - }, - ) + self.collapsing_view + .show( + ui, + class.id, + &mut class.learnings, + |ui, _i, learning| { + Self::show_learning_header( + ui, &skills, learning, + ) + }, + |ui, i, learning| { + Self::show_learning_body( + ui, + update_state, + &skills, + class.id, + (i, learning), + ) + }, + ) + .response }, )) .changed(); diff --git a/crates/ui/src/windows/enemies.rs b/crates/ui/src/windows/enemies.rs index 606589c5..7e9626fe 100644 --- a/crates/ui/src/windows/enemies.rs +++ b/crates/ui/src/windows/enemies.rs @@ -585,24 +585,26 @@ impl luminol_core::Window for Window { if self.previous_enemy != Some(enemy.id) { self.collapsing_view.clear_animations(); } - self.collapsing_view.show( - ui, - enemy.id, - &mut enemy.actions, - |ui, _i, action| { - Self::show_action_header(ui, &skills, action) - }, - |ui, i, action| { - Self::show_action_body( - ui, - update_state, - &system, - &skills, - enemy.id, - (i, action), - ) - }, - ) + self.collapsing_view + .show( + ui, + enemy.id, + &mut enemy.actions, + |ui, _i, action| { + Self::show_action_header(ui, &skills, action) + }, + |ui, i, action| { + Self::show_action_body( + ui, + update_state, + &system, + &skills, + enemy.id, + (i, action), + ) + }, + ) + .response }, )) .changed(); From d7fad94e46b45695438d468f06472c8a67cc4183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 30 Jul 2024 15:25:47 -0400 Subject: [PATCH 053/109] Add "no-store" cache setting to sw.js requests --- assets/js/sw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/sw.js b/assets/js/sw.js index 1cea85eb..fa3d4c75 100644 --- a/assets/js/sw.js +++ b/assets/js/sw.js @@ -84,7 +84,7 @@ if (typeof window === 'undefined') { event.respondWith( self.caches .match(url) - .then((cached) => cached || fetch(request)) // Respond with cached response if one exists for this request + .then((cached) => cached || fetch(request, { cache: "no-store" })) // Respond with cached response if one exists for this request .then((response) => { if (response.status === 0) { return new Response(); From 6868610d0d2a42243b1031947de4def9de9cd3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 31 Jul 2024 21:44:53 -0400 Subject: [PATCH 054/109] Update Cargo.lock to correspond to the updated Cargo.toml --- Cargo.lock | 354 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 208 insertions(+), 146 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3155420..9472aaeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,7 +63,7 @@ dependencies = [ "futures-lite 1.13.0", "once_cell", "serde", - "zbus", + "zbus 3.15.1", ] [[package]] @@ -341,6 +341,24 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "async-fs 2.1.1", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus 4.1.2", +] + [[package]] name = "async-broadcast" version = "0.5.1" @@ -351,6 +369,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener 5.1.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -486,6 +516,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io 2.3.1", + "blocking", + "futures-lite 2.2.0", +] + [[package]] name = "async-once-cell" version = "0.5.3" @@ -509,6 +550,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "async-process" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a" +dependencies = [ + "async-channel 2.2.0", + "async-io 2.3.1", + "async-lock 3.3.0", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.1.0", + "futures-lite 2.2.0", + "rustix 0.38.31", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "async-recursion" version = "1.0.5" @@ -591,18 +652,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "atk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -629,9 +678,9 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus", - "zbus_names", - "zvariant", + "zbus 3.15.1", + "zbus_names 2.6.0", + "zvariant 3.15.1", ] [[package]] @@ -643,7 +692,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite 1.13.0", - "zbus", + "zbus 3.15.1", ] [[package]] @@ -654,7 +703,7 @@ checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" dependencies = [ "atspi-common", "serde", - "zbus", + "zbus 3.15.1", ] [[package]] @@ -912,16 +961,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "calloop" version = "0.12.4" @@ -1634,6 +1673,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" version = "0.6.0" @@ -2092,36 +2137,6 @@ dependencies = [ "thread_local", ] -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - [[package]] name = "generator" version = "0.7.6" @@ -2185,19 +2200,6 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - [[package]] name = "git-version" version = "0.3.9" @@ -2238,16 +2240,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "glob" version = "0.3.1" @@ -2343,17 +2335,6 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - [[package]] name = "gpu-alloc" version = "0.6.0" @@ -2406,24 +2387,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "gtk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - [[package]] name = "h2" version = "0.3.26" @@ -3731,6 +3694,19 @@ dependencies = [ "memoffset 0.7.1", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.0", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -4201,18 +4177,6 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "parking" version = "2.2.0" @@ -4839,21 +4803,21 @@ dependencies = [ [[package]] name = "rfd" -version = "0.12.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" dependencies = [ + "ashpd", "block", "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", "js-sys", "log", "objc", "objc-foundation", "objc_id", - "raw-window-handle 0.5.2", + "pollster", + "raw-window-handle 0.6.0", + "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6048,8 +6012,15 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "usvg" version = "0.37.0" @@ -7078,16 +7049,16 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zbus" -version = "3.15.2" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" +checksum = "5acecd3f8422f198b1a2f954bcc812fe89f3fa4281646f3da1da7925db80085d" dependencies = [ - "async-broadcast", + "async-broadcast 0.5.1", "async-executor", "async-fs 1.6.0", "async-io 1.13.0", "async-lock 2.8.0", - "async-process", + "async-process 1.8.1", "async-recursion", "async-task", "async-trait", @@ -7100,7 +7071,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.26.4", "once_cell", "ordered-stream", "rand", @@ -7112,16 +7083,55 @@ dependencies = [ "uds_windows", "winapi", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 3.15.1", + "zbus_names 2.6.0", + "zvariant 3.15.1", +] + +[[package]] +name = "zbus" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" +dependencies = [ + "async-broadcast 0.7.1", + "async-executor", + "async-fs 2.1.1", + "async-io 2.3.1", + "async-lock 3.3.0", + "async-process 2.2.3", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "derivative", + "enumflags2", + "event-listener 5.1.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.28.0", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.1.2", + "zbus_names 3.0.0", + "zvariant 4.0.2", ] [[package]] name = "zbus_macros" -version = "3.15.2" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" +checksum = "2207eb71efebda17221a579ca78b45c4c5f116f074eb745c3a172e688ccf89f5" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -7131,6 +7141,20 @@ dependencies = [ "zvariant_utils", ] +[[package]] +name = "zbus_macros" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + [[package]] name = "zbus_names" version = "2.6.0" @@ -7139,7 +7163,18 @@ checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 3.15.1", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.0.2", ] [[package]] @@ -7228,23 +7263,37 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.15.2" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" +checksum = "c5b4fcf3660d30fc33ae5cd97e2017b23a96e85afd7a1dd014534cd0bf34ba67" dependencies = [ "byteorder", "enumflags2", "libc", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 3.15.1", +] + +[[package]] +name = "zvariant" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive 4.0.2", ] [[package]] name = "zvariant_derive" -version = "3.15.2" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" +checksum = "0277758a8a0afc0e573e80ed5bfd9d9c2b48bd3108ffe09384f9f738c83f4a55" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -7253,11 +7302,24 @@ dependencies = [ "zvariant_utils", ] +[[package]] +name = "zvariant_derive" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + [[package]] name = "zvariant_utils" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" dependencies = [ "proc-macro2", "quote", From 1b0e88b9adc508fe68c5418d16cd67915fcff608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 2 Aug 2024 23:07:18 -0400 Subject: [PATCH 055/109] Create separate flash maps for each flash condition --- crates/ui/src/windows/animations.rs | 840 +++++++++++++++++++++++++--- 1 file changed, 750 insertions(+), 90 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 707a9a53..1f362c80 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -26,7 +26,7 @@ use egui::Widget; use luminol_components::UiExt; use luminol_core::Modal; -use luminol_data::{rpg::animation::Scope, BlendMode}; +use luminol_data::{rpg::animation::Condition, rpg::animation::Scope, BlendMode}; use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; use luminol_modals::sound_picker::Modal as SoundPicker; @@ -45,6 +45,7 @@ pub struct Window { struct FrameEditState { frame_index: usize, + condition: Condition, enable_onion_skin: bool, frame_view: Option, cellpicker: Option, @@ -58,9 +59,44 @@ struct TimingEditState { #[derive(Debug, Default)] struct FlashMaps { - hide: FlashMap, - target: FlashMap, - screen: FlashMap, + none_hide: FlashMap, + hit_hide: FlashMap, + miss_hide: FlashMap, + none_target: FlashMap, + hit_target: FlashMap, + miss_target: FlashMap, + none_screen: FlashMap, + hit_screen: FlashMap, + miss_screen: FlashMap, +} + +impl FlashMaps { + /// Determines what color the target flash should be for a given frame number and condition. + fn compute_target(&self, frame: usize, condition: Condition) -> luminol_data::Color { + match condition { + Condition::None => self.none_target.compute(frame), + Condition::Hit => self.hit_target.compute(frame), + Condition::Miss => self.miss_target.compute(frame), + } + } + + /// Determines what color the screen flash should be for a given frame number and condition. + fn compute_screen(&self, frame: usize, condition: Condition) -> luminol_data::Color { + match condition { + Condition::None => self.none_screen.compute(frame), + Condition::Hit => self.hit_screen.compute(frame), + Condition::Miss => self.miss_screen.compute(frame), + } + } + + /// Determines if the hide target flash is active for a given frame number and condition. + fn compute_hide(&self, frame: usize, condition: Condition) -> bool { + match condition { + Condition::None => self.none_hide.compute(frame), + Condition::Hit => self.hit_hide.compute(frame), + Condition::Miss => self.miss_hide.compute(frame), + } + } } struct Modals { @@ -212,6 +248,7 @@ impl Default for Window { previous_battler_name: None, frame_edit_state: FrameEditState { frame_index: 0, + condition: Condition::None, enable_onion_skin: false, frame_view: None, cellpicker: None, @@ -371,7 +408,7 @@ impl Window { let (timing_index, previous_timings, timing) = timing; let mut modified = false; - let rank = |frame, scope| { + let none_rank = |frame, scope| { previous_timings .iter() .rev() @@ -379,12 +416,29 @@ impl Window { .filter(|t| t.flash_scope == scope) .count() }; + let hit_rank = |frame, scope| { + previous_timings + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.condition != Condition::Miss && t.flash_scope == scope) + .count() + }; + let miss_rank = |frame, scope| { + previous_timings + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.condition != Condition::Hit && t.flash_scope == scope) + .count() + }; let mut response = egui::Frame::none() .show(ui, |ui| { ui.columns(2, |columns| { columns[0].columns(2, |columns| { - modified |= columns[1] + let old_condition = timing.condition; + let changed = columns[1] .add(luminol_components::Field::new( "Condition", luminol_components::EnumComboBox::new( @@ -393,6 +447,117 @@ impl Window { ), )) .changed(); + if changed { + if old_condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.remove( + timing.frame, + hit_rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.hit_screen.remove( + timing.frame, + hit_rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.remove( + timing.frame, + hit_rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } + if old_condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.remove( + timing.frame, + miss_rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.miss_screen.remove( + timing.frame, + miss_rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.remove( + timing.frame, + miss_rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.hit_screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.miss_screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + modified = true; + } let old_frame = timing.frame; let changed = columns[0] @@ -420,28 +585,80 @@ impl Window { if changed { match timing.flash_scope { Scope::Target => { - flash_maps.target.set_frame( + flash_maps.none_target.set_frame( old_frame, - rank(old_frame, Scope::Target), + none_rank(old_frame, Scope::Target), timing.frame, ); } Scope::Screen => { - flash_maps.screen.set_frame( + flash_maps.none_screen.set_frame( old_frame, - rank(old_frame, Scope::Screen), + none_rank(old_frame, Scope::Screen), timing.frame, ); } Scope::HideTarget => { - flash_maps.hide.set_frame( + flash_maps.none_hide.set_frame( old_frame, - rank(old_frame, Scope::HideTarget), + none_rank(old_frame, Scope::HideTarget), timing.frame, ); } Scope::None => {} } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.set_frame( + old_frame, + hit_rank(old_frame, Scope::Target), + timing.frame, + ); + } + Scope::Screen => { + flash_maps.hit_screen.set_frame( + old_frame, + hit_rank(old_frame, Scope::Screen), + timing.frame, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.set_frame( + old_frame, + hit_rank(old_frame, Scope::HideTarget), + timing.frame, + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.set_frame( + old_frame, + miss_rank(old_frame, Scope::Target), + timing.frame, + ); + } + Scope::Screen => { + flash_maps.miss_screen.set_frame( + old_frame, + miss_rank(old_frame, Scope::Screen), + timing.frame, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.set_frame( + old_frame, + miss_rank(old_frame, Scope::HideTarget), + timing.frame, + ); + } + Scope::None => {} + } + } modified = true; } }); @@ -494,24 +711,24 @@ impl Window { match old_scope { Scope::Target => { flash_maps - .target - .remove(timing.frame, rank(timing.frame, Scope::Target)); + .none_target + .remove(timing.frame, none_rank(timing.frame, Scope::Target)); } Scope::Screen => { flash_maps - .screen - .remove(timing.frame, rank(timing.frame, Scope::Screen)); + .none_screen + .remove(timing.frame, none_rank(timing.frame, Scope::Screen)); } Scope::HideTarget => { flash_maps - .hide - .remove(timing.frame, rank(timing.frame, Scope::HideTarget)); + .none_hide + .remove(timing.frame, none_rank(timing.frame, Scope::HideTarget)); } Scope::None => {} } match timing.flash_scope { Scope::Target => { - flash_maps.target.insert( + flash_maps.none_target.insert( timing.frame, ColorFlash { color: timing.flash_color, @@ -520,7 +737,7 @@ impl Window { ); } Scope::Screen => { - flash_maps.screen.insert( + flash_maps.none_screen.insert( timing.frame, ColorFlash { color: timing.flash_color, @@ -529,7 +746,7 @@ impl Window { ); } Scope::HideTarget => { - flash_maps.hide.insert( + flash_maps.none_hide.insert( timing.frame, HideFlash { duration: timing.flash_duration, @@ -538,6 +755,106 @@ impl Window { } Scope::None => {} } + if timing.condition != Condition::Miss { + match old_scope { + Scope::Target => { + flash_maps + .hit_target + .remove(timing.frame, hit_rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .hit_screen + .remove(timing.frame, hit_rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps.hit_hide.remove( + timing.frame, + hit_rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.hit_screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + match old_scope { + Scope::Target => { + flash_maps + .miss_target + .remove(timing.frame, miss_rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .miss_screen + .remove(timing.frame, miss_rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps.miss_hide.remove( + timing.frame, + miss_rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.miss_screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } modified = true; } @@ -545,27 +862,85 @@ impl Window { match timing.flash_scope { Scope::Target => { flash_maps - .target - .get_mut(timing.frame, rank(timing.frame, Scope::Target)) + .none_target + .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) .unwrap() .duration = timing.flash_duration; } Scope::Screen => { flash_maps - .screen - .get_mut(timing.frame, rank(timing.frame, Scope::Screen)) + .none_screen + .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) .unwrap() .duration = timing.flash_duration; } Scope::HideTarget => { flash_maps - .hide - .get_mut(timing.frame, rank(timing.frame, Scope::HideTarget)) + .none_hide + .get_mut(timing.frame, none_rank(timing.frame, Scope::HideTarget)) .unwrap() .duration = timing.flash_duration; } Scope::None => unreachable!(), } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps + .hit_target + .get_mut(timing.frame, hit_rank(timing.frame, Scope::Target)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::Screen => { + flash_maps + .hit_screen + .get_mut(timing.frame, hit_rank(timing.frame, Scope::Screen)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::HideTarget => { + flash_maps + .hit_hide + .get_mut( + timing.frame, + hit_rank(timing.frame, Scope::HideTarget), + ) + .unwrap() + .duration = timing.flash_duration; + } + Scope::None => unreachable!(), + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps + .miss_target + .get_mut(timing.frame, miss_rank(timing.frame, Scope::Target)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::Screen => { + flash_maps + .miss_screen + .get_mut(timing.frame, miss_rank(timing.frame, Scope::Screen)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::HideTarget => { + flash_maps + .miss_hide + .get_mut( + timing.frame, + miss_rank(timing.frame, Scope::HideTarget), + ) + .unwrap() + .duration = timing.flash_duration; + } + Scope::None => unreachable!(), + } + } modified = true; } @@ -596,20 +971,70 @@ impl Window { match timing.flash_scope { Scope::Target => { flash_maps - .target - .get_mut(timing.frame, rank(timing.frame, Scope::Target)) + .none_target + .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) .unwrap() .color = timing.flash_color; } Scope::Screen => { flash_maps - .screen - .get_mut(timing.frame, rank(timing.frame, Scope::Screen)) + .none_screen + .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) .unwrap() .color = timing.flash_color; } Scope::None | Scope::HideTarget => unreachable!(), } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps + .hit_target + .get_mut( + timing.frame, + hit_rank(timing.frame, Scope::Target), + ) + .unwrap() + .color = timing.flash_color; + } + Scope::Screen => { + flash_maps + .hit_screen + .get_mut( + timing.frame, + hit_rank(timing.frame, Scope::Screen), + ) + .unwrap() + .color = timing.flash_color; + } + Scope::None | Scope::HideTarget => unreachable!(), + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps + .miss_target + .get_mut( + timing.frame, + miss_rank(timing.frame, Scope::Target), + ) + .unwrap() + .color = timing.flash_color; + } + Scope::Screen => { + flash_maps + .miss_screen + .get_mut( + timing.frame, + miss_rank(timing.frame, Scope::Screen), + ) + .unwrap() + .color = timing.flash_color; + } + Scope::None | Scope::HideTarget => unreachable!(), + } + } modified = true; } } @@ -669,8 +1094,8 @@ impl Window { &update_state.graphics, system, animation, - Some(flash_maps.target.compute(state.frame_index)), - Some(flash_maps.hide.compute(state.frame_index)), + Some(flash_maps.compute_target(state.frame_index, state.condition)), + Some(flash_maps.compute_hide(state.frame_index, state.condition)), ); frame_view .frame @@ -689,6 +1114,8 @@ impl Window { }; ui.horizontal(|ui| { + let mut recompute_flash = false; + ui.add(luminol_components::Field::new( "Editor Scale", egui::Slider::new(&mut frame_view.scale, 15.0..=300.0) @@ -701,22 +1128,35 @@ impl Window { .frame_index .min(animation.frames.len().saturating_sub(1)); state.frame_index += 1; - let changed = ui + recompute_flash |= ui .add(luminol_components::Field::new( "Frame", egui::DragValue::new(&mut state.frame_index).range(1..=animation.frames.len()), )) .changed(); state.frame_index -= 1; - let battler_color = flash_maps.target.compute(state.frame_index); - let battler_hidden = flash_maps.hide.compute(state.frame_index); - if changed { + + recompute_flash |= ui + .add(luminol_components::Field::new( + "Condition", + luminol_components::EnumComboBox::new("condition", &mut state.condition) + .max_width(18.) + .wrap_mode(egui::TextWrapMode::Extend), + )) + .changed(); + + ui.add(luminol_components::Field::new( + "Onion Skin", + egui::Checkbox::without_text(&mut state.enable_onion_skin), + )); + + if recompute_flash { frame_view.frame.update_battler( &update_state.graphics, system, animation, - Some(battler_color), - Some(battler_hidden), + Some(flash_maps.compute_target(state.frame_index, state.condition)), + Some(flash_maps.compute_hide(state.frame_index, state.condition)), ); frame_view.frame.update_all_cells( &update_state.graphics, @@ -725,11 +1165,6 @@ impl Window { ); } - ui.add(luminol_components::Field::new( - "Onion Skin", - egui::Checkbox::without_text(&mut state.enable_onion_skin), - )); - ui.with_layout( egui::Layout { main_dir: egui::Direction::RightToLeft, @@ -1170,7 +1605,7 @@ impl Window { ui, update_state, clip_rect, - flash_maps.screen.compute(state.frame_index), + flash_maps.compute_screen(state.frame_index, state.condition), ); // If the pointer is hovering over the frame view, prevent parent widgets @@ -1311,7 +1746,7 @@ impl luminol_core::Window for Window { self.frame_edit_state.flash_maps.insert( id, FlashMaps { - hide: animation + none_hide: animation .timings .iter() .filter(|timing| timing.flash_scope == Scope::HideTarget) @@ -1324,7 +1759,39 @@ impl luminol_core::Window for Window { ) }) .collect(), - target: animation + hit_hide: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Miss + && timing.flash_scope == Scope::HideTarget + }) + .map(|timing| { + ( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ) + }) + .collect(), + miss_hide: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Hit + && timing.flash_scope == Scope::HideTarget + }) + .map(|timing| { + ( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ) + }) + .collect(), + none_target: animation .timings .iter() .filter(|timing| timing.flash_scope == Scope::Target) @@ -1338,7 +1805,41 @@ impl luminol_core::Window for Window { ) }) .collect(), - screen: animation + hit_target: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Miss + && timing.flash_scope == Scope::Target + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + miss_target: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Hit + && timing.flash_scope == Scope::Target + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + none_screen: animation .timings .iter() .filter(|timing| timing.flash_scope == Scope::Screen) @@ -1352,6 +1853,40 @@ impl luminol_core::Window for Window { ) }) .collect(), + hit_screen: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Miss + && timing.flash_scope == Scope::Screen + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + miss_screen: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Hit + && timing.flash_scope == Scope::Screen + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), }, ); } @@ -1465,16 +2000,14 @@ impl luminol_core::Window for Window { &update_state.graphics, &system, animation, - Some( - flash_maps - .target - .compute(self.frame_edit_state.frame_index), - ), - Some( - flash_maps - .hide - .compute(self.frame_edit_state.frame_index), - ), + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), ); frame_view.frame.rebuild_all_cells( &update_state.graphics, @@ -1569,16 +2102,14 @@ impl luminol_core::Window for Window { &update_state.graphics, &system, animation, - Some( - flash_maps - .target - .compute(self.frame_edit_state.frame_index), - ), - Some( - flash_maps - .hide - .compute(self.frame_edit_state.frame_index), - ), + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), ); } modified = true; @@ -1589,7 +2120,7 @@ impl luminol_core::Window for Window { let timing = &animation.timings[i]; match timing.flash_scope { Scope::Target => { - flash_maps.target.insert( + flash_maps.none_target.insert( timing.frame, ColorFlash { color: timing.flash_color, @@ -1598,7 +2129,7 @@ impl luminol_core::Window for Window { ); } Scope::Screen => { - flash_maps.screen.insert( + flash_maps.none_screen.insert( timing.frame, ColorFlash { color: timing.flash_color, @@ -1607,7 +2138,7 @@ impl luminol_core::Window for Window { ); } Scope::HideTarget => { - flash_maps.hide.insert( + flash_maps.none_hide.insert( timing.frame, HideFlash { duration: timing.flash_duration, @@ -1616,6 +2147,68 @@ impl luminol_core::Window for Window { } Scope::None => {} } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.hit_screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.miss_screen.insert( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.insert( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } self.frame_edit_state .frame_view .as_mut() @@ -1625,14 +2218,14 @@ impl luminol_core::Window for Window { &update_state.graphics, &system, animation, - Some( - flash_maps - .target - .compute(self.frame_edit_state.frame_index), - ), - Some( - flash_maps.hide.compute(self.frame_edit_state.frame_index), - ), + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), ); } @@ -1648,22 +2241,89 @@ impl luminol_core::Window for Window { match timing.flash_scope { Scope::Target => { flash_maps - .target + .none_target .remove(timing.frame, rank(timing.frame, Scope::Target)); } Scope::Screen => { flash_maps - .screen + .none_screen .remove(timing.frame, rank(timing.frame, Scope::Screen)); } Scope::HideTarget => { - flash_maps.hide.remove( + flash_maps.none_hide.remove( timing.frame, rank(timing.frame, Scope::HideTarget), ); } Scope::None => {} } + if timing.condition != Condition::Miss { + let rank = |frame, scope| { + animation.timings[..i] + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| { + t.condition != Condition::Miss && t.flash_scope == scope + }) + .count() + }; + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.remove( + timing.frame, + rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.hit_screen.remove( + timing.frame, + rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.remove( + timing.frame, + rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + let rank = |frame, scope| { + animation.timings[..i] + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| { + t.condition != Condition::Hit && t.flash_scope == scope + }) + .count() + }; + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.remove( + timing.frame, + rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.miss_screen.remove( + timing.frame, + rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.remove( + timing.frame, + rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } + self.frame_edit_state .frame_view .as_mut() @@ -1673,14 +2333,14 @@ impl luminol_core::Window for Window { &update_state.graphics, &system, animation, - Some( - flash_maps - .target - .compute(self.frame_edit_state.frame_index), - ), - Some( - flash_maps.hide.compute(self.frame_edit_state.frame_index), - ), + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), ); } From dc7d31fbc8891853f3a41d63a10205fcdaa424ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 2 Aug 2024 23:32:35 -0400 Subject: [PATCH 056/109] Make sure new flash map flashes are inserted at the correct rank --- crates/ui/src/windows/animations.rs | 65 +++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 1f362c80..f6cfb7db 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -142,7 +142,7 @@ where fn from_iter>(iterable: I) -> Self { let mut map = Self(Default::default()); for (frame, flash) in iterable.into_iter() { - map.insert(frame, flash); + map.append(frame, flash); } map } @@ -152,14 +152,22 @@ impl FlashMap where T: Copy, { - /// Adds a new flash into the map. - fn insert(&mut self, frame: usize, flash: T) { + /// Adds a new flash into the map at the maximum rank. + fn append(&mut self, frame: usize, flash: T) { self.0 .entry(frame) .and_modify(|e| e.push_back(flash)) .or_insert_with(|| [flash].into()); } + /// Adds a new flash into the map at the given rank. + fn insert(&mut self, frame: usize, rank: usize, flash: T) { + self.0 + .entry(frame) + .and_modify(|e| e.insert(rank, flash)) + .or_insert_with(|| [flash].into()); + } + /// Removes a flash from the map. fn remove(&mut self, frame: usize, rank: usize) -> T { let deque = self @@ -448,7 +456,9 @@ impl Window { )) .changed(); if changed { - if old_condition != Condition::Miss { + if old_condition != Condition::Miss + && timing.condition == Condition::Miss + { match timing.flash_scope { Scope::Target => { flash_maps.hit_target.remove( @@ -470,8 +480,9 @@ impl Window { } Scope::None => {} } - } - if old_condition != Condition::Hit { + } else if old_condition != Condition::Hit + && timing.condition == Condition::Hit + { match timing.flash_scope { Scope::Target => { flash_maps.miss_target.remove( @@ -494,11 +505,14 @@ impl Window { Scope::None => {} } } - if timing.condition != Condition::Miss { + if old_condition == Condition::Miss + && timing.condition != Condition::Miss + { match timing.flash_scope { Scope::Target => { flash_maps.hit_target.insert( timing.frame, + hit_rank(timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -508,6 +522,7 @@ impl Window { Scope::Screen => { flash_maps.hit_screen.insert( timing.frame, + hit_rank(timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -517,6 +532,7 @@ impl Window { Scope::HideTarget => { flash_maps.hit_hide.insert( timing.frame, + hit_rank(timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -524,12 +540,14 @@ impl Window { } Scope::None => {} } - } - if timing.condition != Condition::Hit { + } else if old_condition == Condition::Hit + && timing.condition != Condition::Hit + { match timing.flash_scope { Scope::Target => { flash_maps.miss_target.insert( timing.frame, + miss_rank(timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -539,6 +557,7 @@ impl Window { Scope::Screen => { flash_maps.miss_screen.insert( timing.frame, + miss_rank(timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -548,6 +567,7 @@ impl Window { Scope::HideTarget => { flash_maps.miss_hide.insert( timing.frame, + miss_rank(timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -730,6 +750,7 @@ impl Window { Scope::Target => { flash_maps.none_target.insert( timing.frame, + none_rank(timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -739,6 +760,7 @@ impl Window { Scope::Screen => { flash_maps.none_screen.insert( timing.frame, + none_rank(timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -748,6 +770,7 @@ impl Window { Scope::HideTarget => { flash_maps.none_hide.insert( timing.frame, + none_rank(timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -779,6 +802,7 @@ impl Window { Scope::Target => { flash_maps.hit_target.insert( timing.frame, + hit_rank(timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -788,6 +812,7 @@ impl Window { Scope::Screen => { flash_maps.hit_screen.insert( timing.frame, + hit_rank(timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -797,6 +822,7 @@ impl Window { Scope::HideTarget => { flash_maps.hit_hide.insert( timing.frame, + hit_rank(timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -829,6 +855,7 @@ impl Window { Scope::Target => { flash_maps.miss_target.insert( timing.frame, + miss_rank(timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -838,6 +865,7 @@ impl Window { Scope::Screen => { flash_maps.miss_screen.insert( timing.frame, + miss_rank(timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -847,6 +875,7 @@ impl Window { Scope::HideTarget => { flash_maps.miss_hide.insert( timing.frame, + miss_rank(timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -2120,7 +2149,7 @@ impl luminol_core::Window for Window { let timing = &animation.timings[i]; match timing.flash_scope { Scope::Target => { - flash_maps.none_target.insert( + flash_maps.none_target.append( timing.frame, ColorFlash { color: timing.flash_color, @@ -2129,7 +2158,7 @@ impl luminol_core::Window for Window { ); } Scope::Screen => { - flash_maps.none_screen.insert( + flash_maps.none_screen.append( timing.frame, ColorFlash { color: timing.flash_color, @@ -2138,7 +2167,7 @@ impl luminol_core::Window for Window { ); } Scope::HideTarget => { - flash_maps.none_hide.insert( + flash_maps.none_hide.append( timing.frame, HideFlash { duration: timing.flash_duration, @@ -2150,7 +2179,7 @@ impl luminol_core::Window for Window { if timing.condition != Condition::Miss { match timing.flash_scope { Scope::Target => { - flash_maps.hit_target.insert( + flash_maps.hit_target.append( timing.frame, ColorFlash { color: timing.flash_color, @@ -2159,7 +2188,7 @@ impl luminol_core::Window for Window { ); } Scope::Screen => { - flash_maps.hit_screen.insert( + flash_maps.hit_screen.append( timing.frame, ColorFlash { color: timing.flash_color, @@ -2168,7 +2197,7 @@ impl luminol_core::Window for Window { ); } Scope::HideTarget => { - flash_maps.hit_hide.insert( + flash_maps.hit_hide.append( timing.frame, HideFlash { duration: timing.flash_duration, @@ -2181,7 +2210,7 @@ impl luminol_core::Window for Window { if timing.condition != Condition::Hit { match timing.flash_scope { Scope::Target => { - flash_maps.miss_target.insert( + flash_maps.miss_target.append( timing.frame, ColorFlash { color: timing.flash_color, @@ -2190,7 +2219,7 @@ impl luminol_core::Window for Window { ); } Scope::Screen => { - flash_maps.miss_screen.insert( + flash_maps.miss_screen.append( timing.frame, ColorFlash { color: timing.flash_color, @@ -2199,7 +2228,7 @@ impl luminol_core::Window for Window { ); } Scope::HideTarget => { - flash_maps.miss_hide.insert( + flash_maps.miss_hide.append( timing.frame, HideFlash { duration: timing.flash_duration, From c084c20b9fbc7a3514c1e4d82dd07f697bbbbc16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 3 Aug 2024 00:22:03 -0400 Subject: [PATCH 057/109] Start implementing animation editor animation playing --- crates/data/src/rmxp/animation.rs | 2 +- crates/ui/src/windows/animations.rs | 88 +++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index 401b5e1e..86f7f24c 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -31,7 +31,7 @@ pub struct Animation { pub animation_name: Path, pub animation_hue: i32, pub position: Position, - pub frame_max: i32, + pub frame_max: usize, pub frames: Vec, pub timings: Vec, } diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index f6cfb7db..192ac4b2 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -50,6 +50,13 @@ struct FrameEditState { frame_view: Option, cellpicker: Option, flash_maps: luminol_data::OptionVec, + animation_state: Option, +} + +#[derive(Debug, Clone, Copy)] +struct AnimationState { + saved_frame_index: usize, + start_time: f64, } struct TimingEditState { @@ -261,6 +268,7 @@ impl Default for Window { frame_view: None, cellpicker: None, flash_maps: Default::default(), + animation_state: None, }, timing_edit_state: TimingEditState { previous_frame: None, @@ -587,7 +595,7 @@ impl Window { let mut frame = state.previous_frame.unwrap_or(timing.frame + 1); let mut response = egui::DragValue::new(&mut frame) - .range(1..=animation.frame_max) + .range(1..=animation.frames.len()) .update_while_editing(false) .ui(ui); response.changed = false; @@ -720,7 +728,7 @@ impl Window { .add(luminol_components::Field::new( "Flash Duration", egui::DragValue::new(&mut timing.flash_duration) - .range(1..=animation.frame_max), + .range(1..=animation.frames.len()), )) .changed(), ) @@ -1086,6 +1094,7 @@ impl Window { state: &mut FrameEditState, ) -> (bool, bool) { let mut modified = false; + let mut recompute_flash = false; let flash_maps = state.flash_maps.get_mut(animation.id).unwrap(); @@ -1143,8 +1152,6 @@ impl Window { }; ui.horizontal(|ui| { - let mut recompute_flash = false; - ui.add(luminol_components::Field::new( "Editor Scale", egui::Slider::new(&mut frame_view.scale, 15.0..=300.0) @@ -1158,10 +1165,14 @@ impl Window { .min(animation.frames.len().saturating_sub(1)); state.frame_index += 1; recompute_flash |= ui - .add(luminol_components::Field::new( - "Frame", - egui::DragValue::new(&mut state.frame_index).range(1..=animation.frames.len()), - )) + .add_enabled( + state.animation_state.is_none(), + luminol_components::Field::new( + "Frame", + egui::DragValue::new(&mut state.frame_index) + .range(1..=animation.frames.len()), + ), + ) .changed(); state.frame_index -= 1; @@ -1179,21 +1190,6 @@ impl Window { egui::Checkbox::without_text(&mut state.enable_onion_skin), )); - if recompute_flash { - frame_view.frame.update_battler( - &update_state.graphics, - system, - animation, - Some(flash_maps.compute_target(state.frame_index, state.condition)), - Some(flash_maps.compute_hide(state.frame_index, state.condition)), - ); - frame_view.frame.update_all_cells( - &update_state.graphics, - animation, - state.frame_index, - ); - } - ui.with_layout( egui::Layout { main_dir: egui::Direction::RightToLeft, @@ -1239,11 +1235,40 @@ impl Window { ui.add(modals.batch_edit.button((), update_state)); }); + + if ui.button("Play").clicked() { + if let Some(animation_state) = state.animation_state.take() { + state.frame_index = animation_state.saved_frame_index; + } else { + state.animation_state = Some(AnimationState { + saved_frame_index: state.frame_index, + start_time: ui.input(|i| i.time), + }); + state.frame_index = 0; + } + } }); }, ); }); + if let Some(animation_state) = &mut state.animation_state { + let previous_frame_index = state.frame_index; + state.frame_index = + ((ui.input(|i| i.time) - animation_state.start_time) * 15.) as usize; + + if state.frame_index != previous_frame_index { + recompute_flash = true; + } + + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs_f64(1. / 15.)); + } + if state.frame_index >= animation.frames.len() { + let animation_state = state.animation_state.take().unwrap(); + state.frame_index = animation_state.saved_frame_index; + } + if modals .copy_frames .show_window(ui.ctx(), state.frame_index, animation.frames.len()) @@ -1716,6 +1741,19 @@ impl Window { } }); + if recompute_flash { + frame_view.frame.update_battler( + &update_state.graphics, + system, + animation, + Some(flash_maps.compute_target(state.frame_index, state.condition)), + Some(flash_maps.compute_hide(state.frame_index, state.condition)), + ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + } + (modified, false) } } @@ -1763,6 +1801,10 @@ impl luminol_core::Window for Window { |ui, animations, id, update_state| { let animation = &mut animations[id]; self.selected_animation_name = Some(animation.name.clone()); + if animation.frames.is_empty() { + animation.frames.push(Default::default()); + animation.frame_max = 1; + } let clip_rect = ui.clip_rect(); From 92b355bda3014dc46ac068a69794e4315e3ae59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 3 Aug 2024 16:15:27 -0400 Subject: [PATCH 058/109] Hide borders and onion skin when playing animation --- crates/components/src/animation_frame_view.rs | 132 ++++++++++-------- crates/ui/src/windows/animations.rs | 45 +++--- 2 files changed, 97 insertions(+), 80 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index 9eea03cf..ebaace43 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -73,6 +73,7 @@ impl AnimationFrameView { update_state: &luminol_core::UpdateState<'_>, clip_rect: egui::Rect, screen_color: luminol_data::Color, + draw_rects: bool, ) -> egui::InnerResponse> { let canvas_rect = ui.max_rect(); let canvas_center = canvas_rect.center(); @@ -175,28 +176,30 @@ impl AnimationFrameView { let offset = canvas_center.to_vec2() + self.pan; // Draw the grid lines and the border of the animation frame - ui.painter().line_segment( - [ - egui::pos2(-(FRAME_WIDTH as f32 / 2.), 0.) * scale + offset, - egui::pos2(FRAME_WIDTH as f32 / 2., 0.) * scale + offset, - ], - egui::Stroke::new(1., egui::Color32::DARK_GRAY), - ); - ui.painter().line_segment( - [ - egui::pos2(0., -(FRAME_HEIGHT as f32 / 2.)) * scale + offset, - egui::pos2(0., FRAME_HEIGHT as f32 / 2.) * scale + offset, - ], - egui::Stroke::new(1., egui::Color32::DARK_GRAY), - ); - ui.painter().rect_stroke( - egui::Rect::from_center_size( - offset.to_pos2(), - egui::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32) * scale, - ), - 5., - egui::Stroke::new(1., egui::Color32::DARK_GRAY), - ); + if draw_rects { + ui.painter().line_segment( + [ + egui::pos2(-(FRAME_WIDTH as f32 / 2.), 0.) * scale + offset, + egui::pos2(FRAME_WIDTH as f32 / 2., 0.) * scale + offset, + ], + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + ui.painter().line_segment( + [ + egui::pos2(0., -(FRAME_HEIGHT as f32 / 2.)) * scale + offset, + egui::pos2(0., FRAME_HEIGHT as f32 / 2.) * scale + offset, + ], + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + ui.painter().rect_stroke( + egui::Rect::from_center_size( + offset.to_pos2(), + egui::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32) * scale, + ), + 5., + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + } // Find the cell that the cursor is hovering over; if multiple cells are hovered we // prioritize the one with the greatest index @@ -256,58 +259,63 @@ impl AnimationFrameView { self.hovered_cell_drag_pos = None; } - // Draw a gray rectangle on the border of every onion-skinned cell - if self.frame.enable_onion_skin { + if draw_rects { + // Draw a gray rectangle on the border of every onion-skinned cell + if draw_rects && self.frame.enable_onion_skin { + for cell_rect in self + .frame + .onion_skin_cells() + .iter() + .map(|(_, cell)| (cell.rect * scale).translate(offset)) + { + ui.painter().rect_stroke( + cell_rect, + 5., + egui::Stroke::new(1., egui::Color32::DARK_GRAY), + ); + } + } + + // Draw a white rectangle on the border of every cell for cell_rect in self .frame - .onion_skin_cells() + .cells() .iter() .map(|(_, cell)| (cell.rect * scale).translate(offset)) { ui.painter().rect_stroke( cell_rect, 5., - egui::Stroke::new(1., egui::Color32::DARK_GRAY), + egui::Stroke::new( + 1., + if ui.input(|i| i.modifiers.shift) { + egui::Color32::DARK_GRAY + } else { + egui::Color32::WHITE + }, + ), ); } - } - // Draw a white rectangle on the border of every cell - for cell_rect in self - .frame - .cells() - .iter() - .map(|(_, cell)| (cell.rect * scale).translate(offset)) - { - ui.painter().rect_stroke( - cell_rect, - 5., - egui::Stroke::new( - 1., - if ui.input(|i| i.modifiers.shift) { - egui::Color32::DARK_GRAY - } else { - egui::Color32::WHITE - }, - ), - ); - } - - // Draw a yellow rectangle on the border of the hovered cell - if let Some(i) = self.hovered_cell_index { - let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); - ui.painter() - .rect_stroke(cell_rect, 5., egui::Stroke::new(3., egui::Color32::YELLOW)); - } + // Draw a yellow rectangle on the border of the hovered cell + if let Some(i) = self.hovered_cell_index { + let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); + ui.painter().rect_stroke( + cell_rect, + 5., + egui::Stroke::new(3., egui::Color32::YELLOW), + ); + } - // Draw a magenta rectangle on the border of the selected cell - if let Some(i) = self.selected_cell_index { - let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); - ui.painter().rect_stroke( - cell_rect, - 5., - egui::Stroke::new(3., egui::Color32::from_rgb(255, 0, 255)), - ); + // Draw a magenta rectangle on the border of the selected cell + if let Some(i) = self.selected_cell_index { + let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); + ui.painter().rect_stroke( + cell_rect, + 5., + egui::Stroke::new(3., egui::Color32::from_rgb(255, 0, 255)), + ); + } } ui.ctx().data_mut(|d| { diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 192ac4b2..b40d0b5b 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -263,7 +263,7 @@ impl Default for Window { previous_battler_name: None, frame_edit_state: FrameEditState { frame_index: 0, - condition: Condition::None, + condition: Condition::Hit, enable_onion_skin: false, frame_view: None, cellpicker: None, @@ -1254,15 +1254,21 @@ impl Window { if let Some(animation_state) = &mut state.animation_state { let previous_frame_index = state.frame_index; - state.frame_index = - ((ui.input(|i| i.time) - animation_state.start_time) * 15.) as usize; + let time = ui.input(|i| i.time); + state.frame_index = ((time - animation_state.start_time) * 15.) as usize; if state.frame_index != previous_frame_index { recompute_flash = true; } + let frame_delay = 1. / 15.; // 15 FPS + let time_rem = time.rem_euclid(frame_delay); ui.ctx() - .request_repaint_after(std::time::Duration::from_secs_f64(1. / 15.)); + .request_repaint_after(std::time::Duration::from_secs_f64(if time_rem == 0. { + frame_delay + } else { + time_rem + })); } if state.frame_index >= animation.frames.len() { let animation_state = state.animation_state.take().unwrap(); @@ -1646,12 +1652,27 @@ impl Window { } }); + if recompute_flash { + frame_view.frame.update_battler( + &update_state.graphics, + system, + animation, + Some(flash_maps.compute_target(state.frame_index, state.condition)), + Some(flash_maps.compute_hide(state.frame_index, state.condition)), + ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + } + egui::ScrollArea::horizontal().show_viewport(ui, |ui, scroll_rect| { cellpicker.ui(update_state, ui, scroll_rect); }); ui.allocate_ui_at_rect(canvas_rect, |ui| { - frame_view.frame.enable_onion_skin = state.enable_onion_skin && state.frame_index != 0; + frame_view.frame.enable_onion_skin = state.enable_onion_skin + && state.frame_index != 0 + && state.animation_state.is_none(); let egui::InnerResponse { inner: hover_pos, response, @@ -1660,6 +1681,7 @@ impl Window { update_state, clip_rect, flash_maps.compute_screen(state.frame_index, state.condition), + state.animation_state.is_none(), ); // If the pointer is hovering over the frame view, prevent parent widgets @@ -1741,19 +1763,6 @@ impl Window { } }); - if recompute_flash { - frame_view.frame.update_battler( - &update_state.graphics, - system, - animation, - Some(flash_maps.compute_target(state.frame_index, state.condition)), - Some(flash_maps.compute_hide(state.frame_index, state.condition)), - ); - frame_view - .frame - .update_all_cells(&update_state.graphics, animation, state.frame_index); - } - (modified, false) } } From 441447eb9e0f4939f55ef26f50625a1d3d4938bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 3 Aug 2024 19:45:11 -0400 Subject: [PATCH 059/109] Fix animation editor repaint delay formula --- crates/ui/src/windows/animations.rs | 47 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index b40d0b5b..78b42a38 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -1151,6 +1151,30 @@ impl Window { state.cellpicker.as_mut().unwrap() }; + // Handle playing of animations + if let Some(animation_state) = &mut state.animation_state { + // Determine what frame in the animation we're at by using the egui time and the + // framerate + let previous_frame_index = state.frame_index; + let time_diff = ui.input(|i| i.time) - animation_state.start_time; + state.frame_index = (time_diff * 15.) as usize; + + if state.frame_index != previous_frame_index { + recompute_flash = true; + } + + // Request a repaint every few frames + let frame_delay = 1. / 15.; // 15 FPS + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs_f64( + frame_delay - time_diff.rem_euclid(frame_delay), + )); + } + if state.frame_index >= animation.frames.len() { + let animation_state = state.animation_state.take().unwrap(); + state.frame_index = animation_state.saved_frame_index; + } + ui.horizontal(|ui| { ui.add(luminol_components::Field::new( "Editor Scale", @@ -1252,29 +1276,6 @@ impl Window { ); }); - if let Some(animation_state) = &mut state.animation_state { - let previous_frame_index = state.frame_index; - let time = ui.input(|i| i.time); - state.frame_index = ((time - animation_state.start_time) * 15.) as usize; - - if state.frame_index != previous_frame_index { - recompute_flash = true; - } - - let frame_delay = 1. / 15.; // 15 FPS - let time_rem = time.rem_euclid(frame_delay); - ui.ctx() - .request_repaint_after(std::time::Duration::from_secs_f64(if time_rem == 0. { - frame_delay - } else { - time_rem - })); - } - if state.frame_index >= animation.frames.len() { - let animation_state = state.animation_state.take().unwrap(); - state.frame_index = animation_state.saved_frame_index; - } - if modals .copy_frames .show_window(ui.ctx(), state.frame_index, animation.frames.len()) From 3a6ade7b8addac95c2c92df9243b3ecf27d0cb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 3 Aug 2024 22:28:45 -0400 Subject: [PATCH 060/109] Implement playing sound effects when playing animations --- crates/audio/src/lib.rs | 23 ++++++++++------- crates/components/src/sound_tab.rs | 2 +- crates/term/src/widget/mod.rs | 2 +- crates/ui/src/windows/animations.rs | 38 +++++++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index 10bcd601..22329139 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -82,7 +82,7 @@ impl Audio { filesystem: &T, volume: u8, pitch: u8, - source: Source, + source: Option, ) -> Result<()> where T: luminol_filesystem::FileSystem, @@ -104,7 +104,7 @@ impl Audio { is_midi: bool, volume: u8, pitch: u8, - source: Source, + source: Option, ) -> Result<()> { let mut inner = self.inner.lock(); // Create a sink @@ -112,7 +112,7 @@ impl Audio { // Select decoder type based on sound source match source { - Source::SE | Source::ME => { + None | Some(Source::SE | Source::ME) => { // Non looping if is_midi { sink.append(midi::MidiSource::new(file, false)?); @@ -135,12 +135,17 @@ impl Audio { sink.set_volume(f32::from(volume) / 100.); // Play sound. sink.play(); - // Add sink to hash, stop the current one if it's there. - if let Some(s) = inner.sinks.insert(source, sink) { - s.stop(); - #[cfg(not(target_arch = "wasm32"))] - s.sleep_until_end(); // wait for the sink to stop, there is a ~5ms delay where it will not - }; + + if let Some(source) = source { + // Add sink to hash, stop the current one if it's there. + if let Some(s) = inner.sinks.insert(source, sink) { + s.stop(); + #[cfg(not(target_arch = "wasm32"))] + s.sleep_until_end(); // wait for the sink to stop, there is a ~5ms delay where it will not + }; + } else { + sink.detach(); + } Ok(()) } diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index 2347ed39..68585327 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -65,7 +65,7 @@ impl SoundTab { if let Err(e) = update_state .audio - .play(path, update_state.filesystem, volume, pitch, source) + .play(path, update_state.filesystem, volume, pitch, Some(source)) { luminol_core::error!( update_state.toasts, diff --git a/crates/term/src/widget/mod.rs b/crates/term/src/widget/mod.rs index 08f59a0c..46c1a7a2 100644 --- a/crates/term/src/widget/mod.rs +++ b/crates/term/src/widget/mod.rs @@ -270,7 +270,7 @@ where let cursor = std::io::Cursor::new(bell); update_state .audio - .play_from_file(cursor, false, 25, 100, luminol_audio::Source::SE) + .play_from_file(cursor, false, 25, 100, Some(luminol_audio::Source::SE)) .unwrap(); } _ => {} diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 78b42a38..95e60404 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -57,6 +57,7 @@ struct FrameEditState { struct AnimationState { saved_frame_index: usize, start_time: f64, + timing_index: usize, } struct TimingEditState { @@ -1157,14 +1158,46 @@ impl Window { // framerate let previous_frame_index = state.frame_index; let time_diff = ui.input(|i| i.time) - animation_state.start_time; - state.frame_index = (time_diff * 15.) as usize; + state.frame_index = (time_diff * 20.) as usize; if state.frame_index != previous_frame_index { recompute_flash = true; } + // Play sound effects + for (i, timing) in animation.timings[animation_state.timing_index..] + .iter() + .enumerate() + { + if timing.frame > state.frame_index { + animation_state.timing_index += i; + break; + } + if let Some(se_name) = &timing.se.name { + if let Err(e) = update_state.audio.play( + format!("Audio/SE/{se_name}"), + update_state.filesystem, + timing.se.volume, + timing.se.pitch, + None, + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!("Error playing animation sound effect {se_name}")) + ); + } + } + } + if animation + .timings + .last() + .is_some_and(|timing| state.frame_index >= timing.frame) + { + animation_state.timing_index = animation.timings.len(); + } + // Request a repaint every few frames - let frame_delay = 1. / 15.; // 15 FPS + let frame_delay = 1. / 20.; // 20 FPS ui.ctx() .request_repaint_after(std::time::Duration::from_secs_f64( frame_delay - time_diff.rem_euclid(frame_delay), @@ -1267,6 +1300,7 @@ impl Window { state.animation_state = Some(AnimationState { saved_frame_index: state.frame_index, start_time: ui.input(|i| i.time), + timing_index: 0, }); state.frame_index = 0; } From 444d927c81b50b6d21f82eba9bcbcf571f7696a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 11:05:27 -0400 Subject: [PATCH 061/109] Fix web build --- crates/audio/src/wrapper.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/audio/src/wrapper.rs b/crates/audio/src/wrapper.rs index a4e10e36..8e826fce 100644 --- a/crates/audio/src/wrapper.rs +++ b/crates/audio/src/wrapper.rs @@ -45,7 +45,7 @@ enum AudioWrapperCommandInner { is_midi: bool, volume: u8, pitch: u8, - source: Source, + source: Option, oneshot_tx: oneshot::Sender>, }, SetPitch { @@ -82,7 +82,7 @@ impl AudioWrapper { filesystem: &impl luminol_filesystem::FileSystem, volume: u8, pitch: u8, - source: Source, + source: Option, ) -> Result<()> { // We have to load the file on the current thread, // otherwise if we read the file in the main thread of a web browser From 51c67cc3cf74507489104552fa9b87cb5f79e41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 11:20:13 -0400 Subject: [PATCH 062/109] Refactor audio I moved the native audio implementation into crates/audio/src/native.rs and then changed the audio crate so that it exports the native audio implementation in native builds or the audio wrapper in web builds. --- Cargo.lock | 2 +- crates/audio/Cargo.toml | 5 +- crates/audio/src/lib.rs | 157 ++-------------------------- crates/audio/src/native.rs | 155 ++++++++++++++++++++++++++++ crates/audio/src/wrapper.rs | 158 ++++++++++++++++------------- crates/components/src/sound_tab.rs | 8 +- crates/core/src/lib.rs | 3 - src/app/mod.rs | 5 +- src/main.rs | 4 +- 9 files changed, 263 insertions(+), 234 deletions(-) create mode 100644 crates/audio/src/native.rs diff --git a/Cargo.lock b/Cargo.lock index 9472aaeb..0206e1d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3067,12 +3067,12 @@ dependencies = [ "once_cell", "oneshot", "parking_lot", - "poll-promise", "rodio", "rustysynth", "slab", "strum", "thiserror", + "wasm-bindgen-futures", "web-sys", ] diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 0859aa4d..84a47978 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -35,11 +35,12 @@ rodio = "0.19.0" [target.'cfg(target_arch = "wasm32")'.dependencies] rodio = { version = "0.19.0", features = ["wasm-bindgen"] } -web-sys = { version = "0.3", features = ["Window"] } + +web-sys.workspace = true +wasm-bindgen-futures.workspace = true flume.workspace = true oneshot.workspace = true once_cell.workspace = true -poll-promise.workspace = true slab.workspace = true diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index 22329139..a89fc6e2 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -22,31 +22,26 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -mod midi; - mod error; +mod midi; pub use error::{Error, Result}; +mod native; #[cfg(target_arch = "wasm32")] mod wrapper; -#[cfg(target_arch = "wasm32")] -pub use wrapper::*; - -use strum::Display; -use strum::EnumIter; -/// A struct for playing Audio. -pub struct Audio { - inner: parking_lot::Mutex, -} +#[cfg(not(target_arch = "wasm32"))] +pub use native::Audio; +#[cfg(target_arch = "wasm32")] +pub use wrapper::Audio; -struct Inner { - output_stream_handle: rodio::OutputStreamHandle, - sinks: std::collections::HashMap, -} +#[cfg(target_arch = "wasm32")] +trait ReadSeek: std::io::Read + std::io::Seek {} +#[cfg(target_arch = "wasm32")] +impl ReadSeek for T where T: std::io::Read + std::io::Seek {} /// Different sound sources. -#[derive(EnumIter, Display, PartialEq, Eq, Clone, Copy, Hash)] +#[derive(strum::EnumIter, strum::Display, PartialEq, Eq, Clone, Copy, Hash)] #[allow(clippy::upper_case_acronyms)] #[allow(missing_docs)] pub enum Source { @@ -56,136 +51,6 @@ pub enum Source { SE, } -impl Default for Audio { - fn default() -> Self { - #[cfg(target_arch = "wasm32")] - if web_sys::window().is_none() { - panic!("in web builds, `Audio` can only be created on the main thread"); - } - - let (output_stream, output_stream_handle) = rodio::OutputStream::try_default().unwrap(); - std::mem::forget(output_stream); // Prevent the stream from being dropped - Self { - inner: parking_lot::Mutex::new(Inner { - output_stream_handle, - sinks: std::collections::HashMap::default(), - }), - } - } -} - -impl Audio { - /// Play a sound on a source. - pub fn play( - &self, - path: impl AsRef, - filesystem: &T, - volume: u8, - pitch: u8, - source: Option, - ) -> Result<()> - where - T: luminol_filesystem::FileSystem, - T::File: 'static, - { - let path = path.as_ref(); - let file = filesystem.open_file(path, luminol_filesystem::OpenFlags::Read)?; - - let is_midi = path - .extension() - .is_some_and(|e| matches!(e, "mid" | "midi")); - - self.play_from_file(file, is_midi, volume, pitch, source) - } - - pub fn play_from_file( - &self, - file: impl std::io::Read + std::io::Seek + Send + Sync + 'static, - is_midi: bool, - volume: u8, - pitch: u8, - source: Option, - ) -> Result<()> { - let mut inner = self.inner.lock(); - // Create a sink - let sink = rodio::Sink::try_new(&inner.output_stream_handle)?; - - // Select decoder type based on sound source - match source { - None | Some(Source::SE | Source::ME) => { - // Non looping - if is_midi { - sink.append(midi::MidiSource::new(file, false)?); - } else { - sink.append(rodio::Decoder::new(file)?); - } - } - _ => { - // Looping - if is_midi { - sink.append(midi::MidiSource::new(file, true)?); - } else { - sink.append(rodio::Decoder::new_looped(file)?); - } - } - } - - // Set pitch and volume - sink.set_speed(f32::from(pitch) / 100.); - sink.set_volume(f32::from(volume) / 100.); - // Play sound. - sink.play(); - - if let Some(source) = source { - // Add sink to hash, stop the current one if it's there. - if let Some(s) = inner.sinks.insert(source, sink) { - s.stop(); - #[cfg(not(target_arch = "wasm32"))] - s.sleep_until_end(); // wait for the sink to stop, there is a ~5ms delay where it will not - }; - } else { - sink.detach(); - } - - Ok(()) - } - - /// Set the pitch of a source. - pub fn set_pitch(&self, pitch: u8, source: &Source) { - let mut inner = self.inner.lock(); - if let Some(s) = inner.sinks.get_mut(source) { - s.set_speed(f32::from(pitch) / 100.); - } - } - - /// Set the volume of a source. - pub fn set_volume(&self, volume: u8, source: &Source) { - let mut inner = self.inner.lock(); - if let Some(s) = inner.sinks.get_mut(source) { - s.set_volume(f32::from(volume) / 100.); - } - } - - pub fn clear_sinks(&self) { - let mut inner = self.inner.lock(); - for (_, sink) in inner.sinks.iter_mut() { - sink.stop(); - #[cfg(not(target_arch = "wasm32"))] - // Sleeping ensures that the inner file is dropped. There is a delay of ~5ms where it is not dropped and this could lead to a panic - sink.sleep_until_end(); - } - inner.sinks.clear(); - } - - /// Stop a source. - pub fn stop(&self, source: &Source) { - let mut inner = self.inner.lock(); - if let Some(s) = inner.sinks.get_mut(source) { - s.stop(); - } - } -} - impl Source { pub fn as_path(&self) -> &camino::Utf8Path { camino::Utf8Path::new(match self { diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs new file mode 100644 index 00000000..bcdff226 --- /dev/null +++ b/crates/audio/src/native.rs @@ -0,0 +1,155 @@ +use crate::{midi, Result, Source}; + +/// A struct for playing Audio. +pub struct Audio { + inner: parking_lot::Mutex, +} + +struct Inner { + output_stream_handle: rodio::OutputStreamHandle, + sinks: std::collections::HashMap, +} + +impl Default for Audio { + fn default() -> Self { + #[cfg(target_arch = "wasm32")] + if web_sys::window().is_none() { + panic!("in web builds, `Audio` can only be created on the main thread"); + } + + let (output_stream, output_stream_handle) = rodio::OutputStream::try_default().unwrap(); + std::mem::forget(output_stream); // Prevent the stream from being dropped + Self { + inner: parking_lot::Mutex::new(Inner { + output_stream_handle, + sinks: std::collections::HashMap::default(), + }), + } + } +} + +impl Audio { + #[cfg(not(target_arch = "wasm32"))] + /// Play a sound on a source. + pub fn play( + &self, + path: impl AsRef, + filesystem: &T, + volume: u8, + pitch: u8, + source: Option, + ) -> Result<()> + where + T: luminol_filesystem::FileSystem, + T::File: 'static, + { + let path = path.as_ref(); + let file = filesystem.open_file(path, luminol_filesystem::OpenFlags::Read)?; + + let is_midi = path + .extension() + .is_some_and(|e| matches!(e, "mid" | "midi")); + + self.play_from_file(file, is_midi, volume, pitch, source) + } + + #[cfg(target_arch = "wasm32")] + pub(crate) fn play_from_file_dyn( + &self, + file: Box, + is_midi: bool, + volume: u8, + pitch: u8, + source: Option, + ) -> Result<()> { + self.play_from_file(file, is_midi, volume, pitch, source) + } + + /// Play a sound from a file on a source. + pub fn play_from_file( + &self, + file: impl std::io::Read + std::io::Seek + Send + Sync + 'static, + is_midi: bool, + volume: u8, + pitch: u8, + source: Option, + ) -> Result<()> { + let mut inner = self.inner.lock(); + // Create a sink + let sink = rodio::Sink::try_new(&inner.output_stream_handle)?; + + // Select decoder type based on sound source + match source { + None | Some(Source::SE | Source::ME) => { + // Non looping + if is_midi { + sink.append(midi::MidiSource::new(file, false)?); + } else { + sink.append(rodio::Decoder::new(file)?); + } + } + _ => { + // Looping + if is_midi { + sink.append(midi::MidiSource::new(file, true)?); + } else { + sink.append(rodio::Decoder::new_looped(file)?); + } + } + } + + // Set pitch and volume + sink.set_speed(f32::from(pitch) / 100.); + sink.set_volume(f32::from(volume) / 100.); + // Play sound. + sink.play(); + + if let Some(source) = source { + // Add sink to hash, stop the current one if it's there. + if let Some(s) = inner.sinks.insert(source, sink) { + s.stop(); + #[cfg(not(target_arch = "wasm32"))] + s.sleep_until_end(); // wait for the sink to stop, there is a ~5ms delay where it will not + }; + } else { + sink.detach(); + } + + Ok(()) + } + + /// Set the pitch of a source. + pub fn set_pitch(&self, pitch: u8, source: Source) { + let mut inner = self.inner.lock(); + if let Some(s) = inner.sinks.get_mut(&source) { + s.set_speed(f32::from(pitch) / 100.); + } + } + + /// Set the volume of a source. + pub fn set_volume(&self, volume: u8, source: Source) { + let mut inner = self.inner.lock(); + if let Some(s) = inner.sinks.get_mut(&source) { + s.set_volume(f32::from(volume) / 100.); + } + } + + pub fn clear_sinks(&self) { + let mut inner = self.inner.lock(); + for (_, sink) in inner.sinks.iter_mut() { + sink.stop(); + #[cfg(not(target_arch = "wasm32"))] + // Sleeping ensures that the inner file is dropped. There is a delay of ~5ms where it is not dropped and this could lead to a panic + sink.sleep_until_end(); + } + inner.sinks.clear(); + } + + /// Stop a source. + pub fn stop(&self, source: Source) { + let mut inner = self.inner.lock(); + if let Some(s) = inner.sinks.get_mut(&source) { + s.stop(); + } + } +} diff --git a/crates/audio/src/wrapper.rs b/crates/audio/src/wrapper.rs index 8e826fce..314cd321 100644 --- a/crates/audio/src/wrapper.rs +++ b/crates/audio/src/wrapper.rs @@ -22,24 +22,14 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use poll_promise::Promise; +use crate::{native::Audio as NativeAudio, ReadSeek, Result, Source}; -use super::{Audio, Result, Source}; - -thread_local! { - static SLAB: once_cell::sync::Lazy>>> = - once_cell::sync::Lazy::new(|| std::cell::RefCell::new(slab::Slab::new())); -} - -#[derive(Debug)] -pub struct AudioWrapper { - key: usize, - tx: flume::Sender, +/// A struct for playing Audio. +pub struct Audio { + tx: flume::Sender, } -pub struct AudioWrapperCommand(AudioWrapperCommandInner); - -enum AudioWrapperCommandInner { +enum Command { Play { cursor: std::io::Cursor>, is_midi: bool, @@ -48,6 +38,14 @@ enum AudioWrapperCommandInner { source: Option, oneshot_tx: oneshot::Sender>, }, + PlayFromFile { + file: Box, + is_midi: bool, + volume: u8, + pitch: u8, + source: Option, + oneshot_tx: oneshot::Sender>, + }, SetPitch { pitch: u8, source: Source, @@ -65,17 +63,15 @@ enum AudioWrapperCommandInner { source: Source, oneshot_tx: oneshot::Sender<()>, }, - Drop { - key: usize, - oneshot_tx: oneshot::Sender, - }, + Drop, } -impl AudioWrapper { +impl Audio { pub fn new() -> Self { Default::default() } + /// Play a sound on a source. pub fn play( &self, path: impl AsRef, @@ -97,75 +93,92 @@ impl AudioWrapper { let (oneshot_tx, oneshot_rx) = oneshot::channel(); self.tx - .send(AudioWrapperCommand(AudioWrapperCommandInner::Play { + .send(Command::Play { cursor, is_midi, volume, pitch, source, oneshot_tx, - })) + }) .unwrap(); oneshot_rx.recv().unwrap() } - pub fn set_pitch(&self, pitch: u8, source: &Source) { + /// Play a sound from a file on a source. + pub fn play_from_file( + &self, + file: impl std::io::Read + std::io::Seek + Send + Sync + 'static, + is_midi: bool, + volume: u8, + pitch: u8, + source: Option, + ) -> Result<()> { let (oneshot_tx, oneshot_rx) = oneshot::channel(); self.tx - .send(AudioWrapperCommand(AudioWrapperCommandInner::SetPitch { + .send(Command::PlayFromFile { + file: Box::new(file), + is_midi, + volume, pitch, - source: *source, + source, oneshot_tx, - })) + }) .unwrap(); oneshot_rx.recv().unwrap() } - pub fn set_volume(&self, volume: u8, source: &Source) { + /// Set the pitch of a source. + pub fn set_pitch(&self, pitch: u8, source: Source) { let (oneshot_tx, oneshot_rx) = oneshot::channel(); self.tx - .send(AudioWrapperCommand(AudioWrapperCommandInner::SetVolume { - volume, - source: *source, + .send(Command::SetPitch { + pitch, + source, oneshot_tx, - })) + }) .unwrap(); oneshot_rx.recv().unwrap() } - pub fn clear_sinks(&self) { + /// Set the volume of a source. + pub fn set_volume(&self, volume: u8, source: Source) { let (oneshot_tx, oneshot_rx) = oneshot::channel(); self.tx - .send(AudioWrapperCommand(AudioWrapperCommandInner::ClearSinks { + .send(Command::SetVolume { + volume, + source, oneshot_tx, - })) + }) .unwrap(); oneshot_rx.recv().unwrap() } - pub fn stop(&self, source: &Source) { + pub fn clear_sinks(&self) { let (oneshot_tx, oneshot_rx) = oneshot::channel(); - self.tx - .send(AudioWrapperCommand(AudioWrapperCommandInner::Stop { - source: *source, - oneshot_tx, - })) - .unwrap(); + self.tx.send(Command::ClearSinks { oneshot_tx }).unwrap(); + oneshot_rx.recv().unwrap() + } + + /// Stop a source. + pub fn stop(&self, source: Source) { + let (oneshot_tx, oneshot_rx) = oneshot::channel(); + self.tx.send(Command::Stop { source, oneshot_tx }).unwrap(); oneshot_rx.recv().unwrap() } } -impl Default for AudioWrapper { +impl Default for Audio { fn default() -> Self { #[cfg(target_arch = "wasm32")] if web_sys::window().is_none() { - panic!("in web builds, `AudioWrapper` can only be created on the main thread"); + panic!("in web builds, `Audio` can only be created on the main thread"); } - let (tx, rx) = flume::unbounded::(); + let (tx, rx) = flume::unbounded::(); let mut maybe_audio = None; - let promise = poll_promise::Promise::spawn_local(async move { + wasm_bindgen_futures::spawn_local(async move { loop { let Ok(command) = rx.recv_async().await else { return; @@ -174,12 +187,12 @@ impl Default for AudioWrapper { let audio = if let Some(audio) = &maybe_audio { audio } else { - maybe_audio = Some(Audio::default()); + maybe_audio = Some(NativeAudio::default()); maybe_audio.as_ref().unwrap() }; - match command.0 { - AudioWrapperCommandInner::Play { + match command { + Command::Play { cursor, is_midi, volume, @@ -192,59 +205,60 @@ impl Default for AudioWrapper { .unwrap(); } - AudioWrapperCommandInner::SetPitch { + Command::PlayFromFile { + file, + is_midi, + volume, + pitch, + source, + oneshot_tx, + } => { + oneshot_tx + .send(audio.play_from_file_dyn(file, is_midi, volume, pitch, source)) + .unwrap(); + } + + Command::SetPitch { pitch, source, oneshot_tx, } => { - audio.set_pitch(pitch, &source); + audio.set_pitch(pitch, source); oneshot_tx.send(()).unwrap(); } - AudioWrapperCommandInner::SetVolume { + Command::SetVolume { volume, source, oneshot_tx, } => { - audio.set_volume(volume, &source); + audio.set_volume(volume, source); oneshot_tx.send(()).unwrap(); } - AudioWrapperCommandInner::ClearSinks { oneshot_tx } => { + Command::ClearSinks { oneshot_tx } => { audio.clear_sinks(); oneshot_tx.send(()).unwrap(); } - AudioWrapperCommandInner::Stop { source, oneshot_tx } => { - audio.stop(&source); + Command::Stop { source, oneshot_tx } => { + audio.stop(source); oneshot_tx.send(()).unwrap(); } - AudioWrapperCommandInner::Drop { key, oneshot_tx } => { - let promise = SLAB.with(|slab| slab.borrow_mut().try_remove(key)); - oneshot_tx.send(promise.is_some()).unwrap(); - return; + Command::Drop => { + break; } } } }); - Self { - key: SLAB.with(|slab| slab.borrow_mut().insert(promise)), - tx, - } + Self { tx } } } -impl Drop for AudioWrapper { +impl Drop for Audio { fn drop(&mut self) { - let (oneshot_tx, oneshot_rx) = oneshot::channel(); - self.tx - .send(AudioWrapperCommand(AudioWrapperCommandInner::Drop { - key: self.key, - oneshot_tx, - })) - .unwrap(); - oneshot_rx.recv().unwrap(); + self.tx.send(Command::Drop).unwrap(); } } diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index 68585327..c8032499 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -73,7 +73,7 @@ impl SoundTab { ); } } else { - update_state.audio.stop(&self.source); + update_state.audio.stop(self.source); } } @@ -90,7 +90,7 @@ impl SoundTab { if ui.button("Stop").clicked() { // Stop sound. - update_state.audio.stop(&self.source); + update_state.audio.stop(self.source); } }); @@ -109,7 +109,7 @@ impl SoundTab { if ui.add(slider).changed() { update_state .audio - .set_volume(self.audio_file.volume, &self.source); + .set_volume(self.audio_file.volume, self.source); }; let slider = egui::Slider::new(&mut self.audio_file.pitch, 50..=150) @@ -121,7 +121,7 @@ impl SoundTab { if ui.add(slider).changed() { update_state .audio - .set_pitch(self.audio_file.pitch, &self.source); + .set_pitch(self.audio_file.pitch, self.source); }; }); }); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index a678e148..44292ccc 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -65,10 +65,7 @@ pub fn set_git_revision(revision: &'static str) { pub struct UpdateState<'res> { pub ctx: &'res egui::Context, - #[cfg(not(target_arch = "wasm32"))] pub audio: &'res mut luminol_audio::Audio, - #[cfg(target_arch = "wasm32")] - pub audio: &'res mut luminol_audio::AudioWrapper, pub graphics: Arc, pub filesystem: &'res mut luminol_filesystem::project::FileSystem, // FIXME: this is probably wrong diff --git a/src/app/mod.rs b/src/app/mod.rs index 0b59ca32..5f1047b8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -39,10 +39,7 @@ pub struct App { log: log_window::LogWindow, lumi: Lumi, - #[cfg(not(target_arch = "wasm32"))] audio: luminol_audio::Audio, - #[cfg(target_arch = "wasm32")] - audio: luminol_audio::AudioWrapper, graphics: Arc, filesystem: luminol_filesystem::project::FileSystem, @@ -90,7 +87,7 @@ impl App { modified: luminol_core::ModifiedState, #[cfg(not(target_arch = "wasm32"))] log_byte_rx: std::sync::mpsc::Receiver, #[cfg(not(target_arch = "wasm32"))] try_load_path: Option, - #[cfg(target_arch = "wasm32")] audio: luminol_audio::AudioWrapper, + #[cfg(target_arch = "wasm32")] audio: luminol_audio::Audio, #[cfg(feature = "steamworks")] steamworks: Steamworks, ) -> Self { luminol_core::set_git_revision(crate::git_revision()); diff --git a/src/main.rs b/src/main.rs index 2f656a76..d5d4dfc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -269,7 +269,7 @@ const CANVAS_ID: &str = "luminol-canvas"; #[cfg(target_arch = "wasm32")] struct WorkerData { report: Option, - audio: luminol_audio::AudioWrapper, + audio: luminol_audio::Audio, modified: luminol_core::ModifiedState, prefers_color_scheme_dark: Option, fs_worker_channels: luminol_filesystem::web::WorkerChannels, @@ -398,7 +398,7 @@ pub fn luminol_main_start() { *WORKER_DATA.lock() = Some(WorkerData { report, - audio: luminol_audio::AudioWrapper::default(), + audio: luminol_audio::Audio::default(), modified: modified.clone(), prefers_color_scheme_dark, fs_worker_channels, From a5e94d98af9f63fc22d2878a6d05c40721cb5d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 11:30:51 -0400 Subject: [PATCH 063/109] Respect flash condition when playing animation sound effects --- crates/ui/src/windows/animations.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 95e60404..069f5828 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -1173,6 +1173,12 @@ impl Window { animation_state.timing_index += i; break; } + if state.condition != timing.condition + && state.condition != Condition::None + && timing.condition != Condition::None + { + continue; + } if let Some(se_name) = &timing.se.name { if let Err(e) = update_state.audio.play( format!("Audio/SE/{se_name}"), From 02ca2cf9b23b5a26692001390f8b491936fe0f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 12:53:34 -0400 Subject: [PATCH 064/109] Replace `Audio::play_from_file` with `Audio::play_from_slice` This allows for more efficient implementation of the audio wrapper in web builds. --- crates/audio/src/lib.rs | 5 ---- crates/audio/src/native.rs | 11 ++++--- crates/audio/src/wrapper.rs | 55 +++++++---------------------------- crates/term/src/widget/mod.rs | 3 +- 4 files changed, 17 insertions(+), 57 deletions(-) diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index a89fc6e2..e3610a66 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -35,11 +35,6 @@ pub use native::Audio; #[cfg(target_arch = "wasm32")] pub use wrapper::Audio; -#[cfg(target_arch = "wasm32")] -trait ReadSeek: std::io::Read + std::io::Seek {} -#[cfg(target_arch = "wasm32")] -impl ReadSeek for T where T: std::io::Read + std::io::Seek {} - /// Different sound sources. #[derive(strum::EnumIter, strum::Display, PartialEq, Eq, Clone, Copy, Hash)] #[allow(clippy::upper_case_acronyms)] diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs index bcdff226..b4c84807 100644 --- a/crates/audio/src/native.rs +++ b/crates/audio/src/native.rs @@ -53,20 +53,19 @@ impl Audio { self.play_from_file(file, is_midi, volume, pitch, source) } - #[cfg(target_arch = "wasm32")] - pub(crate) fn play_from_file_dyn( + /// Play a sound on a source from audio file data. + pub fn play_from_slice( &self, - file: Box, + slice: impl AsRef<[u8]> + Send + Sync + 'static, is_midi: bool, volume: u8, pitch: u8, source: Option, ) -> Result<()> { - self.play_from_file(file, is_midi, volume, pitch, source) + self.play_from_file(std::io::Cursor::new(slice), is_midi, volume, pitch, source) } - /// Play a sound from a file on a source. - pub fn play_from_file( + fn play_from_file( &self, file: impl std::io::Read + std::io::Seek + Send + Sync + 'static, is_midi: bool, diff --git a/crates/audio/src/wrapper.rs b/crates/audio/src/wrapper.rs index 314cd321..103c8d26 100644 --- a/crates/audio/src/wrapper.rs +++ b/crates/audio/src/wrapper.rs @@ -22,7 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use crate::{native::Audio as NativeAudio, ReadSeek, Result, Source}; +use crate::{native::Audio as NativeAudio, Result, Source}; /// A struct for playing Audio. pub struct Audio { @@ -31,15 +31,7 @@ pub struct Audio { enum Command { Play { - cursor: std::io::Cursor>, - is_midi: bool, - volume: u8, - pitch: u8, - source: Option, - oneshot_tx: oneshot::Sender>, - }, - PlayFromFile { - file: Box, + slice: std::sync::Arc<[u8]>, is_midi: bool, volume: u8, pitch: u8, @@ -84,31 +76,19 @@ impl Audio { // otherwise if we read the file in the main thread of a web browser // we will block the main thread let path = path.as_ref(); - let file = filesystem.read(path)?; - let cursor = std::io::Cursor::new(file); + let slice: std::sync::Arc<[u8]> = filesystem.read(path)?.into(); let is_midi = path .extension() .is_some_and(|e| matches!(e, "mid" | "midi")); - let (oneshot_tx, oneshot_rx) = oneshot::channel(); - self.tx - .send(Command::Play { - cursor, - is_midi, - volume, - pitch, - source, - oneshot_tx, - }) - .unwrap(); - oneshot_rx.recv().unwrap() + self.play_from_slice(slice, is_midi, volume, pitch, source) } - /// Play a sound from a file on a source. - pub fn play_from_file( + /// Play a sound on a source from audio file data. + pub fn play_from_slice( &self, - file: impl std::io::Read + std::io::Seek + Send + Sync + 'static, + slice: impl AsRef<[u8]> + Send + Sync + 'static, is_midi: bool, volume: u8, pitch: u8, @@ -116,8 +96,8 @@ impl Audio { ) -> Result<()> { let (oneshot_tx, oneshot_rx) = oneshot::channel(); self.tx - .send(Command::PlayFromFile { - file: Box::new(file), + .send(Command::Play { + slice: slice.as_ref().into(), is_midi, volume, pitch, @@ -193,20 +173,7 @@ impl Default for Audio { match command { Command::Play { - cursor, - is_midi, - volume, - pitch, - source, - oneshot_tx, - } => { - oneshot_tx - .send(audio.play_from_file(cursor, is_midi, volume, pitch, source)) - .unwrap(); - } - - Command::PlayFromFile { - file, + slice, is_midi, volume, pitch, @@ -214,7 +181,7 @@ impl Default for Audio { oneshot_tx, } => { oneshot_tx - .send(audio.play_from_file_dyn(file, is_midi, volume, pitch, source)) + .send(audio.play_from_slice(slice, is_midi, volume, pitch, source)) .unwrap(); } diff --git a/crates/term/src/widget/mod.rs b/crates/term/src/widget/mod.rs index 46c1a7a2..8ba93c96 100644 --- a/crates/term/src/widget/mod.rs +++ b/crates/term/src/widget/mod.rs @@ -267,10 +267,9 @@ where Event::ResetTitle => "Luminol Terminal".clone_into(&mut self.title), Event::Bell => { let bell = luminol_macros::include_asset!("assets/sounds/bell.wav"); - let cursor = std::io::Cursor::new(bell); update_state .audio - .play_from_file(cursor, false, 25, 100, Some(luminol_audio::Source::SE)) + .play_from_slice(bell, false, 25, 100, Some(luminol_audio::Source::SE)) .unwrap(); } _ => {} From 81a9f73c4883faeb0d7058bdf8876bdd4117f978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 13:05:45 -0400 Subject: [PATCH 065/109] Preload audio files when playing animations --- crates/ui/src/windows/animations.rs | 86 ++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index 069f5828..b14e17f3 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -25,8 +25,12 @@ use egui::Widget; use luminol_components::UiExt; use luminol_core::Modal; +use luminol_filesystem::FileSystem; -use luminol_data::{rpg::animation::Condition, rpg::animation::Scope, BlendMode}; +use luminol_data::{ + rpg::animation::{Condition, Scope, Timing}, + BlendMode, +}; use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; use luminol_modals::sound_picker::Modal as SoundPicker; @@ -53,11 +57,12 @@ struct FrameEditState { animation_state: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug)] struct AnimationState { saved_frame_index: usize, start_time: f64, timing_index: usize, + audio_data: std::collections::HashMap>>, } struct TimingEditState { @@ -330,7 +335,35 @@ impl Window { ); } - fn show_timing_header(ui: &mut egui::Ui, timing: &luminol_data::rpg::animation::Timing) { + fn log_se_load_error( + update_state: &mut luminol_core::UpdateState<'_>, + timing: &Timing, + e: color_eyre::Report, + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "Error loading animation sound effect {:?}", + timing.se.name + )) + ); + } + + fn log_se_play_error( + update_state: &mut luminol_core::UpdateState<'_>, + timing: &Timing, + e: color_eyre::Report, + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "Error playing animation sound effect {:?}", + timing.se.name + )) + ); + } + + fn show_timing_header(ui: &mut egui::Ui, timing: &Timing) { let mut vec = Vec::with_capacity(3); match timing.condition { @@ -416,11 +449,7 @@ impl Window { animation: &luminol_data::rpg::Animation, flash_maps: &mut FlashMaps, state: &mut TimingEditState, - timing: ( - usize, - &[luminol_data::rpg::animation::Timing], - &mut luminol_data::rpg::animation::Timing, - ), + timing: (usize, &[Timing], &mut Timing), ) -> egui::Response { let (timing_index, previous_timings, timing) = timing; let mut modified = false; @@ -1180,17 +1209,18 @@ impl Window { continue; } if let Some(se_name) = &timing.se.name { - if let Err(e) = update_state.audio.play( - format!("Audio/SE/{se_name}"), - update_state.filesystem, + let Some(Some(audio_data)) = animation_state.audio_data.get(se_name.as_str()) + else { + continue; + }; + if let Err(e) = update_state.audio.play_from_slice( + audio_data.clone(), + false, timing.se.volume, timing.se.pitch, None, ) { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!("Error playing animation sound effect {se_name}")) - ); + Self::log_se_play_error(update_state, timing, e); } } } @@ -1303,10 +1333,36 @@ impl Window { if let Some(animation_state) = state.animation_state.take() { state.frame_index = animation_state.saved_frame_index; } else { + // Preload the audio files used by the animation for + // performance reasons + let mut audio_data = std::collections::HashMap::new(); + for timing in &animation.timings { + let Some(se_name) = &timing.se.name else { + continue; + }; + if audio_data.contains_key(se_name.as_str()) { + continue; + } + match update_state + .filesystem + .read(format!("Audio/SE/{se_name}")) + { + Ok(data) => { + audio_data + .insert(se_name.to_string(), Some(data.into())); + } + Err(e) => { + Self::log_se_load_error(update_state, timing, e); + audio_data.insert(se_name.to_string(), None); + } + } + } + state.animation_state = Some(AnimationState { saved_frame_index: state.frame_index, start_time: ui.input(|i| i.time), timing_index: 0, + audio_data, }); state.frame_index = 0; } From 6d6656394009d018619d56215ca72952e7a4bb73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 14:10:09 -0400 Subject: [PATCH 066/109] Refactor animation SE preloading code into a function --- crates/ui/src/windows/animations.rs | 94 ++++++++++++++--------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs index b14e17f3..aec0a789 100644 --- a/crates/ui/src/windows/animations.rs +++ b/crates/ui/src/windows/animations.rs @@ -335,32 +335,36 @@ impl Window { ); } - fn log_se_load_error( + fn load_se( update_state: &mut luminol_core::UpdateState<'_>, + animation_state: &mut AnimationState, + condition: Condition, timing: &Timing, - e: color_eyre::Report, - ) { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "Error loading animation sound effect {:?}", - timing.se.name - )) - ); - } - - fn log_se_play_error( - update_state: &mut luminol_core::UpdateState<'_>, - timing: &Timing, - e: color_eyre::Report, ) { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "Error playing animation sound effect {:?}", - timing.se.name - )) - ); + let Some(se_name) = &timing.se.name else { + return; + }; + if (condition != timing.condition + && condition != Condition::None + && timing.condition != Condition::None) + || animation_state.audio_data.contains_key(se_name.as_str()) + { + return; + } + match update_state.filesystem.read(format!("Audio/SE/{se_name}")) { + Ok(data) => { + animation_state + .audio_data + .insert(se_name.to_string(), Some(data.into())); + } + Err(e) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!("Error loading animation sound effect {se_name}")) + ); + animation_state.audio_data.insert(se_name.to_string(), None); + } + } } fn show_timing_header(ui: &mut egui::Ui, timing: &Timing) { @@ -1209,6 +1213,7 @@ impl Window { continue; } if let Some(se_name) = &timing.se.name { + Self::load_se(update_state, animation_state, state.condition, timing); let Some(Some(audio_data)) = animation_state.audio_data.get(se_name.as_str()) else { continue; @@ -1220,7 +1225,10 @@ impl Window { timing.se.pitch, None, ) { - Self::log_se_play_error(update_state, timing, e); + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!("Error playing animation sound effect {se_name}")) + ); } } } @@ -1333,38 +1341,24 @@ impl Window { if let Some(animation_state) = state.animation_state.take() { state.frame_index = animation_state.saved_frame_index; } else { - // Preload the audio files used by the animation for - // performance reasons - let mut audio_data = std::collections::HashMap::new(); - for timing in &animation.timings { - let Some(se_name) = &timing.se.name else { - continue; - }; - if audio_data.contains_key(se_name.as_str()) { - continue; - } - match update_state - .filesystem - .read(format!("Audio/SE/{se_name}")) - { - Ok(data) => { - audio_data - .insert(se_name.to_string(), Some(data.into())); - } - Err(e) => { - Self::log_se_load_error(update_state, timing, e); - audio_data.insert(se_name.to_string(), None); - } - } - } - state.animation_state = Some(AnimationState { saved_frame_index: state.frame_index, start_time: ui.input(|i| i.time), timing_index: 0, - audio_data, + audio_data: Default::default(), }); state.frame_index = 0; + + // Preload the audio files used by the animation for + // performance reasons + for timing in &animation.timings { + Self::load_se( + update_state, + state.animation_state.as_mut().unwrap(), + state.condition, + timing, + ); + } } } }); From d4fad093b5d7dede47abd8590add7194c9796e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 14:11:55 -0400 Subject: [PATCH 067/109] Add missing `new` constructor to native audio implementation --- crates/audio/src/native.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs index b4c84807..36041005 100644 --- a/crates/audio/src/native.rs +++ b/crates/audio/src/native.rs @@ -29,6 +29,10 @@ impl Default for Audio { } impl Audio { + pub fn new() -> Self { + Default::default() + } + #[cfg(not(target_arch = "wasm32"))] /// Play a sound on a source. pub fn play( From 044d126ca5feefdad9aff8f217910d518b709e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 14:47:38 -0400 Subject: [PATCH 068/109] Fix compilation warning in web builds --- crates/audio/src/native.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs index 36041005..ceb519f6 100644 --- a/crates/audio/src/native.rs +++ b/crates/audio/src/native.rs @@ -29,6 +29,7 @@ impl Default for Audio { } impl Audio { + #[cfg(not(target_arch = "wasm32"))] pub fn new() -> Self { Default::default() } From 194218802f79ff1ec3f5319b58fc4b8185cc267c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 14:48:17 -0400 Subject: [PATCH 069/109] Split animations.rs into smaller files --- crates/ui/src/windows/animations.rs | 2545 ----------------- .../ui/src/windows/animations/frame_edit.rs | 755 +++++ crates/ui/src/windows/animations/mod.rs | 120 + crates/ui/src/windows/animations/timing.rs | 725 +++++ crates/ui/src/windows/animations/util.rs | 306 ++ crates/ui/src/windows/animations/window.rs | 718 +++++ 6 files changed, 2624 insertions(+), 2545 deletions(-) delete mode 100644 crates/ui/src/windows/animations.rs create mode 100644 crates/ui/src/windows/animations/frame_edit.rs create mode 100644 crates/ui/src/windows/animations/mod.rs create mode 100644 crates/ui/src/windows/animations/timing.rs create mode 100644 crates/ui/src/windows/animations/util.rs create mode 100644 crates/ui/src/windows/animations/window.rs diff --git a/crates/ui/src/windows/animations.rs b/crates/ui/src/windows/animations.rs deleted file mode 100644 index aec0a789..00000000 --- a/crates/ui/src/windows/animations.rs +++ /dev/null @@ -1,2545 +0,0 @@ -// Copyright (C) 2024 Melody Madeline Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this Program, or any covered work, by linking or combining -// it with Steamworks API by Valve Corporation, containing parts covered by -// terms of the Steamworks API by Valve Corporation, the licensors of this -// Program grant you additional permission to convey the resulting work. - -use egui::Widget; -use luminol_components::UiExt; -use luminol_core::Modal; -use luminol_filesystem::FileSystem; - -use luminol_data::{ - rpg::animation::{Condition, Scope, Timing}, - BlendMode, -}; -use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; -use luminol_modals::sound_picker::Modal as SoundPicker; - -/// Database - Animations management window. -pub struct Window { - selected_animation_name: Option, - previous_animation: Option, - previous_battler_name: Option, - frame_edit_state: FrameEditState, - timing_edit_state: TimingEditState, - - collapsing_view: luminol_components::CollapsingView, - modals: Modals, - view: luminol_components::DatabaseView, -} - -struct FrameEditState { - frame_index: usize, - condition: Condition, - enable_onion_skin: bool, - frame_view: Option, - cellpicker: Option, - flash_maps: luminol_data::OptionVec, - animation_state: Option, -} - -#[derive(Debug)] -struct AnimationState { - saved_frame_index: usize, - start_time: f64, - timing_index: usize, - audio_data: std::collections::HashMap>>, -} - -struct TimingEditState { - previous_frame: Option, - se_picker: SoundPicker, -} - -#[derive(Debug, Default)] -struct FlashMaps { - none_hide: FlashMap, - hit_hide: FlashMap, - miss_hide: FlashMap, - none_target: FlashMap, - hit_target: FlashMap, - miss_target: FlashMap, - none_screen: FlashMap, - hit_screen: FlashMap, - miss_screen: FlashMap, -} - -impl FlashMaps { - /// Determines what color the target flash should be for a given frame number and condition. - fn compute_target(&self, frame: usize, condition: Condition) -> luminol_data::Color { - match condition { - Condition::None => self.none_target.compute(frame), - Condition::Hit => self.hit_target.compute(frame), - Condition::Miss => self.miss_target.compute(frame), - } - } - - /// Determines what color the screen flash should be for a given frame number and condition. - fn compute_screen(&self, frame: usize, condition: Condition) -> luminol_data::Color { - match condition { - Condition::None => self.none_screen.compute(frame), - Condition::Hit => self.hit_screen.compute(frame), - Condition::Miss => self.miss_screen.compute(frame), - } - } - - /// Determines if the hide target flash is active for a given frame number and condition. - fn compute_hide(&self, frame: usize, condition: Condition) -> bool { - match condition { - Condition::None => self.none_hide.compute(frame), - Condition::Hit => self.hit_hide.compute(frame), - Condition::Miss => self.miss_hide.compute(frame), - } - } -} - -struct Modals { - copy_frames: luminol_modals::animations::copy_frames_tool::Modal, - clear_frames: luminol_modals::animations::clear_frames_tool::Modal, - tween: luminol_modals::animations::tween_tool::Modal, - batch_edit: luminol_modals::animations::batch_edit_tool::Modal, -} - -impl Modals { - fn close_all(&mut self) { - self.copy_frames.close_window(); - self.clear_frames.close_window(); - self.tween.close_window(); - self.batch_edit.close_window(); - } -} - -#[derive(Debug, Clone, Copy)] -struct ColorFlash { - color: luminol_data::Color, - duration: usize, -} - -#[derive(Debug, Clone, Copy)] -struct HideFlash { - duration: usize, -} - -#[derive(Debug)] -struct FlashMap(std::collections::BTreeMap>); - -impl Default for FlashMap { - fn default() -> Self { - Self(std::collections::BTreeMap::new()) - } -} - -impl FromIterator<(usize, T)> for FlashMap -where - T: Copy, -{ - fn from_iter>(iterable: I) -> Self { - let mut map = Self(Default::default()); - for (frame, flash) in iterable.into_iter() { - map.append(frame, flash); - } - map - } -} - -impl FlashMap -where - T: Copy, -{ - /// Adds a new flash into the map at the maximum rank. - fn append(&mut self, frame: usize, flash: T) { - self.0 - .entry(frame) - .and_modify(|e| e.push_back(flash)) - .or_insert_with(|| [flash].into()); - } - - /// Adds a new flash into the map at the given rank. - fn insert(&mut self, frame: usize, rank: usize, flash: T) { - self.0 - .entry(frame) - .and_modify(|e| e.insert(rank, flash)) - .or_insert_with(|| [flash].into()); - } - - /// Removes a flash from the map. - fn remove(&mut self, frame: usize, rank: usize) -> T { - let deque = self - .0 - .get_mut(&frame) - .expect("no flashes found for the given frame"); - let flash = deque.remove(rank).expect("rank out of bounds"); - if deque.is_empty() { - self.0.remove(&frame).unwrap(); - } - flash - } - - /// Modifies the frame number for a flash. - fn set_frame(&mut self, frame: usize, rank: usize, new_frame: usize) { - if frame == new_frame { - return; - } - let flash = self.remove(frame, rank); - self.0 - .entry(new_frame) - .and_modify(|e| { - if new_frame > frame { - e.push_front(flash) - } else { - e.push_back(flash) - } - }) - .or_insert_with(|| [flash].into()); - } - - fn get_mut(&mut self, frame: usize, rank: usize) -> Option<&mut T> { - self.0.get_mut(&frame).and_then(|deque| deque.get_mut(rank)) - } -} - -impl FlashMap { - /// Determines what color the flash should be for a given frame number. - fn compute(&self, frame: usize) -> luminol_data::Color { - let Some((&start_frame, deque)) = self.0.range(..=frame).next_back() else { - return luminol_data::Color { - red: 255., - green: 255., - blue: 255., - alpha: 0., - }; - }; - let flash = deque.back().unwrap(); - - let diff = frame - start_frame; - if diff < flash.duration { - let progression = diff as f64 / flash.duration as f64; - luminol_data::Color { - alpha: flash.color.alpha * (1. - progression), - ..flash.color - } - } else { - luminol_data::Color { - red: 255., - green: 255., - blue: 255., - alpha: 0., - } - } - } -} - -impl FlashMap { - /// Determines if the hide flash is active for a given frame number. - fn compute(&self, frame: usize) -> bool { - let Some((&start_frame, deque)) = self.0.range(..=frame).next_back() else { - return false; - }; - let flash = deque.back().unwrap(); - - let diff = frame - start_frame; - diff < flash.duration - } -} - -impl Default for Window { - fn default() -> Self { - Self { - selected_animation_name: None, - previous_animation: None, - previous_battler_name: None, - frame_edit_state: FrameEditState { - frame_index: 0, - condition: Condition::Hit, - enable_onion_skin: false, - frame_view: None, - cellpicker: None, - flash_maps: Default::default(), - animation_state: None, - }, - timing_edit_state: TimingEditState { - previous_frame: None, - se_picker: SoundPicker::new( - luminol_audio::Source::SE, - "animations_timing_se_picker", - ), - }, - collapsing_view: luminol_components::CollapsingView::new(), - modals: Modals { - copy_frames: luminol_modals::animations::copy_frames_tool::Modal::new( - "animations_copy_frames_tool", - ), - clear_frames: luminol_modals::animations::clear_frames_tool::Modal::new( - "animations_clear_frames_tool", - ), - tween: luminol_modals::animations::tween_tool::Modal::new("animations_tween_tool"), - batch_edit: luminol_modals::animations::batch_edit_tool::Modal::new( - "animations_batch_edit_tool", - ), - }, - view: luminol_components::DatabaseView::new(), - } - } -} - -impl Window { - fn log_battler_error( - update_state: &mut luminol_core::UpdateState<'_>, - system: &luminol_data::rpg::System, - animation: &luminol_data::rpg::Animation, - e: color_eyre::Report, - ) { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "While loading texture {:?} for animation {:0>4} {:?}", - system.battler_name, - animation.id + 1, - animation.name, - )), - ); - } - - fn log_atlas_error( - update_state: &mut luminol_core::UpdateState<'_>, - animation: &luminol_data::rpg::Animation, - e: color_eyre::Report, - ) { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "While loading texture {:?} for animation {:0>4} {:?}", - animation.animation_name, - animation.id + 1, - animation.name, - )), - ); - } - - fn load_se( - update_state: &mut luminol_core::UpdateState<'_>, - animation_state: &mut AnimationState, - condition: Condition, - timing: &Timing, - ) { - let Some(se_name) = &timing.se.name else { - return; - }; - if (condition != timing.condition - && condition != Condition::None - && timing.condition != Condition::None) - || animation_state.audio_data.contains_key(se_name.as_str()) - { - return; - } - match update_state.filesystem.read(format!("Audio/SE/{se_name}")) { - Ok(data) => { - animation_state - .audio_data - .insert(se_name.to_string(), Some(data.into())); - } - Err(e) => { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!("Error loading animation sound effect {se_name}")) - ); - animation_state.audio_data.insert(se_name.to_string(), None); - } - } - } - - fn show_timing_header(ui: &mut egui::Ui, timing: &Timing) { - let mut vec = Vec::with_capacity(3); - - match timing.condition { - luminol_data::rpg::animation::Condition::None => {} - luminol_data::rpg::animation::Condition::Hit => vec.push("on hit".into()), - luminol_data::rpg::animation::Condition::Miss => vec.push("on miss".into()), - } - - if let Some(path) = &timing.se.name { - vec.push(format!("play {:?}", path.file_name().unwrap_or_default())); - }; - - match timing.flash_scope { - Scope::None => {} - Scope::Target => { - vec.push(format!( - "flash target #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", - timing.flash_color.red.clamp(0., 255.).round() as u8, - timing.flash_color.green.clamp(0., 255.).round() as u8, - timing.flash_color.blue.clamp(0., 255.).round() as u8, - timing.flash_color.alpha.clamp(0., 255.).round() as u8, - timing.flash_duration, - )); - } - Scope::Screen => { - vec.push(format!( - "flash screen #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", - timing.flash_color.red.clamp(0., 255.).round() as u8, - timing.flash_color.green.clamp(0., 255.).round() as u8, - timing.flash_color.blue.clamp(0., 255.).round() as u8, - timing.flash_color.alpha.clamp(0., 255.).round() as u8, - timing.flash_duration, - )); - } - Scope::HideTarget => { - vec.push(format!("hide target for {} frames", timing.flash_duration)); - } - } - - ui.label(format!( - "Frame {:0>3}: {}", - timing.frame + 1, - vec.join(", ") - )); - } - - fn resize_frame(frame: &mut luminol_data::rpg::animation::Frame, new_cell_max: usize) { - let old_capacity = frame.cell_data.xsize(); - let new_capacity = if new_cell_max == 0 { - 0 - } else { - new_cell_max.next_power_of_two() - }; - - // Instead of resizing `frame.cell_data` every time we call this function, we increase the - // size of `frame.cell_data` only it's too small and we decrease the size of - // `frame.cell_data` only if it's at <= 25% capacity for better efficiency - let capacity_too_low = old_capacity < new_capacity; - let capacity_too_high = old_capacity >= new_capacity * 4; - - if capacity_too_low { - frame.cell_data.resize(new_capacity, 8); - for i in old_capacity..new_capacity { - frame.cell_data[(i, 0)] = -1; - frame.cell_data[(i, 1)] = 0; - frame.cell_data[(i, 2)] = 0; - frame.cell_data[(i, 3)] = 100; - frame.cell_data[(i, 4)] = 0; - frame.cell_data[(i, 5)] = 0; - frame.cell_data[(i, 6)] = 255; - frame.cell_data[(i, 7)] = 1; - } - } else if capacity_too_high { - frame.cell_data.resize(new_capacity * 2, 8); - } - - frame.cell_max = new_cell_max; - } - - fn show_timing_body( - ui: &mut egui::Ui, - update_state: &mut luminol_core::UpdateState<'_>, - animation: &luminol_data::rpg::Animation, - flash_maps: &mut FlashMaps, - state: &mut TimingEditState, - timing: (usize, &[Timing], &mut Timing), - ) -> egui::Response { - let (timing_index, previous_timings, timing) = timing; - let mut modified = false; - - let none_rank = |frame, scope| { - previous_timings - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| t.flash_scope == scope) - .count() - }; - let hit_rank = |frame, scope| { - previous_timings - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| t.condition != Condition::Miss && t.flash_scope == scope) - .count() - }; - let miss_rank = |frame, scope| { - previous_timings - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| t.condition != Condition::Hit && t.flash_scope == scope) - .count() - }; - - let mut response = egui::Frame::none() - .show(ui, |ui| { - ui.columns(2, |columns| { - columns[0].columns(2, |columns| { - let old_condition = timing.condition; - let changed = columns[1] - .add(luminol_components::Field::new( - "Condition", - luminol_components::EnumComboBox::new( - (animation.id, timing_index, "condition"), - &mut timing.condition, - ), - )) - .changed(); - if changed { - if old_condition != Condition::Miss - && timing.condition == Condition::Miss - { - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.remove( - timing.frame, - hit_rank(timing.frame, Scope::Target), - ); - } - Scope::Screen => { - flash_maps.hit_screen.remove( - timing.frame, - hit_rank(timing.frame, Scope::Screen), - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.remove( - timing.frame, - hit_rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - } else if old_condition != Condition::Hit - && timing.condition == Condition::Hit - { - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.remove( - timing.frame, - miss_rank(timing.frame, Scope::Target), - ); - } - Scope::Screen => { - flash_maps.miss_screen.remove( - timing.frame, - miss_rank(timing.frame, Scope::Screen), - ); - } - Scope::HideTarget => { - flash_maps.miss_hide.remove( - timing.frame, - miss_rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - } - if old_condition == Condition::Miss - && timing.condition != Condition::Miss - { - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.insert( - timing.frame, - hit_rank(timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.hit_screen.insert( - timing.frame, - hit_rank(timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.insert( - timing.frame, - hit_rank(timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } else if old_condition == Condition::Hit - && timing.condition != Condition::Hit - { - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.insert( - timing.frame, - miss_rank(timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.miss_screen.insert( - timing.frame, - miss_rank(timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.miss_hide.insert( - timing.frame, - miss_rank(timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } - modified = true; - } - - let old_frame = timing.frame; - let changed = columns[0] - .add(luminol_components::Field::new( - "Frame", - |ui: &mut egui::Ui| { - let mut frame = - state.previous_frame.unwrap_or(timing.frame + 1); - let mut response = egui::DragValue::new(&mut frame) - .range(1..=animation.frames.len()) - .update_while_editing(false) - .ui(ui); - response.changed = false; - if response.dragged() { - state.previous_frame = Some(frame); - } else { - timing.frame = frame - 1; - state.previous_frame = None; - response.changed = true; - } - response - }, - )) - .changed(); - if changed { - match timing.flash_scope { - Scope::Target => { - flash_maps.none_target.set_frame( - old_frame, - none_rank(old_frame, Scope::Target), - timing.frame, - ); - } - Scope::Screen => { - flash_maps.none_screen.set_frame( - old_frame, - none_rank(old_frame, Scope::Screen), - timing.frame, - ); - } - Scope::HideTarget => { - flash_maps.none_hide.set_frame( - old_frame, - none_rank(old_frame, Scope::HideTarget), - timing.frame, - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.set_frame( - old_frame, - hit_rank(old_frame, Scope::Target), - timing.frame, - ); - } - Scope::Screen => { - flash_maps.hit_screen.set_frame( - old_frame, - hit_rank(old_frame, Scope::Screen), - timing.frame, - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.set_frame( - old_frame, - hit_rank(old_frame, Scope::HideTarget), - timing.frame, - ); - } - Scope::None => {} - } - } - if timing.condition != Condition::Hit { - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.set_frame( - old_frame, - miss_rank(old_frame, Scope::Target), - timing.frame, - ); - } - Scope::Screen => { - flash_maps.miss_screen.set_frame( - old_frame, - miss_rank(old_frame, Scope::Screen), - timing.frame, - ); - } - Scope::HideTarget => { - flash_maps.miss_hide.set_frame( - old_frame, - miss_rank(old_frame, Scope::HideTarget), - timing.frame, - ); - } - Scope::None => {} - } - } - modified = true; - } - }); - - modified |= columns[1] - .add(luminol_components::Field::new( - "SE", - state.se_picker.button(&mut timing.se, update_state), - )) - .changed(); - }); - - let old_scope = timing.flash_scope; - let (scope_changed, duration_changed) = if timing.flash_scope == Scope::None { - ( - ui.add(luminol_components::Field::new( - "Flash", - luminol_components::EnumComboBox::new( - (animation.id, timing_index, "flash_scope"), - &mut timing.flash_scope, - ), - )) - .changed(), - false, - ) - } else { - ui.columns(2, |columns| { - ( - columns[0] - .add(luminol_components::Field::new( - "Flash", - luminol_components::EnumComboBox::new( - (animation.id, timing_index, "flash_scope"), - &mut timing.flash_scope, - ), - )) - .changed(), - columns[1] - .add(luminol_components::Field::new( - "Flash Duration", - egui::DragValue::new(&mut timing.flash_duration) - .range(1..=animation.frames.len()), - )) - .changed(), - ) - }) - }; - - if scope_changed { - match old_scope { - Scope::Target => { - flash_maps - .none_target - .remove(timing.frame, none_rank(timing.frame, Scope::Target)); - } - Scope::Screen => { - flash_maps - .none_screen - .remove(timing.frame, none_rank(timing.frame, Scope::Screen)); - } - Scope::HideTarget => { - flash_maps - .none_hide - .remove(timing.frame, none_rank(timing.frame, Scope::HideTarget)); - } - Scope::None => {} - } - match timing.flash_scope { - Scope::Target => { - flash_maps.none_target.insert( - timing.frame, - none_rank(timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.none_screen.insert( - timing.frame, - none_rank(timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.none_hide.insert( - timing.frame, - none_rank(timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { - match old_scope { - Scope::Target => { - flash_maps - .hit_target - .remove(timing.frame, hit_rank(timing.frame, Scope::Target)); - } - Scope::Screen => { - flash_maps - .hit_screen - .remove(timing.frame, hit_rank(timing.frame, Scope::Screen)); - } - Scope::HideTarget => { - flash_maps.hit_hide.remove( - timing.frame, - hit_rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.insert( - timing.frame, - hit_rank(timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.hit_screen.insert( - timing.frame, - hit_rank(timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.insert( - timing.frame, - hit_rank(timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } - if timing.condition != Condition::Hit { - match old_scope { - Scope::Target => { - flash_maps - .miss_target - .remove(timing.frame, miss_rank(timing.frame, Scope::Target)); - } - Scope::Screen => { - flash_maps - .miss_screen - .remove(timing.frame, miss_rank(timing.frame, Scope::Screen)); - } - Scope::HideTarget => { - flash_maps.miss_hide.remove( - timing.frame, - miss_rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.insert( - timing.frame, - miss_rank(timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.miss_screen.insert( - timing.frame, - miss_rank(timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.miss_hide.insert( - timing.frame, - miss_rank(timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } - modified = true; - } - - if duration_changed { - match timing.flash_scope { - Scope::Target => { - flash_maps - .none_target - .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::Screen => { - flash_maps - .none_screen - .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::HideTarget => { - flash_maps - .none_hide - .get_mut(timing.frame, none_rank(timing.frame, Scope::HideTarget)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::None => unreachable!(), - } - if timing.condition != Condition::Miss { - match timing.flash_scope { - Scope::Target => { - flash_maps - .hit_target - .get_mut(timing.frame, hit_rank(timing.frame, Scope::Target)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::Screen => { - flash_maps - .hit_screen - .get_mut(timing.frame, hit_rank(timing.frame, Scope::Screen)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::HideTarget => { - flash_maps - .hit_hide - .get_mut( - timing.frame, - hit_rank(timing.frame, Scope::HideTarget), - ) - .unwrap() - .duration = timing.flash_duration; - } - Scope::None => unreachable!(), - } - } - if timing.condition != Condition::Hit { - match timing.flash_scope { - Scope::Target => { - flash_maps - .miss_target - .get_mut(timing.frame, miss_rank(timing.frame, Scope::Target)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::Screen => { - flash_maps - .miss_screen - .get_mut(timing.frame, miss_rank(timing.frame, Scope::Screen)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::HideTarget => { - flash_maps - .miss_hide - .get_mut( - timing.frame, - miss_rank(timing.frame, Scope::HideTarget), - ) - .unwrap() - .duration = timing.flash_duration; - } - Scope::None => unreachable!(), - } - } - modified = true; - } - - if matches!(timing.flash_scope, Scope::Target | Scope::Screen) { - let changed = ui - .add(luminol_components::Field::new( - "Flash Color", - |ui: &mut egui::Ui| { - let mut color = [ - timing.flash_color.red.clamp(0., 255.).round() as u8, - timing.flash_color.green.clamp(0., 255.).round() as u8, - timing.flash_color.blue.clamp(0., 255.).round() as u8, - timing.flash_color.alpha.clamp(0., 255.).round() as u8, - ]; - ui.spacing_mut().interact_size.x = ui.available_width(); // make the color picker button as wide as possible - let response = ui.color_edit_button_srgba_unmultiplied(&mut color); - if response.changed() { - timing.flash_color.red = color[0] as f64; - timing.flash_color.green = color[1] as f64; - timing.flash_color.blue = color[2] as f64; - timing.flash_color.alpha = color[3] as f64; - } - response - }, - )) - .changed(); - if changed { - match timing.flash_scope { - Scope::Target => { - flash_maps - .none_target - .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) - .unwrap() - .color = timing.flash_color; - } - Scope::Screen => { - flash_maps - .none_screen - .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) - .unwrap() - .color = timing.flash_color; - } - Scope::None | Scope::HideTarget => unreachable!(), - } - if timing.condition != Condition::Miss { - match timing.flash_scope { - Scope::Target => { - flash_maps - .hit_target - .get_mut( - timing.frame, - hit_rank(timing.frame, Scope::Target), - ) - .unwrap() - .color = timing.flash_color; - } - Scope::Screen => { - flash_maps - .hit_screen - .get_mut( - timing.frame, - hit_rank(timing.frame, Scope::Screen), - ) - .unwrap() - .color = timing.flash_color; - } - Scope::None | Scope::HideTarget => unreachable!(), - } - } - if timing.condition != Condition::Hit { - match timing.flash_scope { - Scope::Target => { - flash_maps - .miss_target - .get_mut( - timing.frame, - miss_rank(timing.frame, Scope::Target), - ) - .unwrap() - .color = timing.flash_color; - } - Scope::Screen => { - flash_maps - .miss_screen - .get_mut( - timing.frame, - miss_rank(timing.frame, Scope::Screen), - ) - .unwrap() - .color = timing.flash_color; - } - Scope::None | Scope::HideTarget => unreachable!(), - } - } - modified = true; - } - } - }) - .response; - - if modified { - response.mark_changed(); - } - response - } - - fn show_frame_edit( - ui: &mut egui::Ui, - update_state: &mut luminol_core::UpdateState<'_>, - clip_rect: egui::Rect, - modals: &mut Modals, - system: &luminol_data::rpg::System, - animation: &mut luminol_data::rpg::Animation, - state: &mut FrameEditState, - ) -> (bool, bool) { - let mut modified = false; - let mut recompute_flash = false; - - let flash_maps = state.flash_maps.get_mut(animation.id).unwrap(); - - let frame_view = if let Some(frame_view) = &mut state.frame_view { - frame_view - } else { - let battler_texture = if let Some(battler_name) = &system.battler_name { - match update_state.graphics.texture_loader.load_now( - update_state.filesystem, - format!("Graphics/Battlers/{battler_name}"), - ) { - Ok(texture) => Some(texture), - Err(e) => { - Self::log_battler_error(update_state, system, animation, e); - return (modified, true); - } - } - } else { - None - }; - let atlas = match update_state.graphics.atlas_loader.load_animation_atlas( - &update_state.graphics, - update_state.filesystem, - animation, - ) { - Ok(atlas) => atlas, - Err(e) => { - Self::log_atlas_error(update_state, animation, e); - return (modified, true); - } - }; - let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); - frame_view.frame.battler_texture = battler_texture; - frame_view.frame.update_battler( - &update_state.graphics, - system, - animation, - Some(flash_maps.compute_target(state.frame_index, state.condition)), - Some(flash_maps.compute_hide(state.frame_index, state.condition)), - ); - frame_view - .frame - .update_all_cells(&update_state.graphics, animation, state.frame_index); - state.frame_view = Some(frame_view); - state.frame_view.as_mut().unwrap() - }; - - let cellpicker = if let Some(cellpicker) = &mut state.cellpicker { - cellpicker - } else { - let atlas = frame_view.frame.atlas.clone(); - let cellpicker = luminol_components::Cellpicker::new(&update_state.graphics, atlas); - state.cellpicker = Some(cellpicker); - state.cellpicker.as_mut().unwrap() - }; - - // Handle playing of animations - if let Some(animation_state) = &mut state.animation_state { - // Determine what frame in the animation we're at by using the egui time and the - // framerate - let previous_frame_index = state.frame_index; - let time_diff = ui.input(|i| i.time) - animation_state.start_time; - state.frame_index = (time_diff * 20.) as usize; - - if state.frame_index != previous_frame_index { - recompute_flash = true; - } - - // Play sound effects - for (i, timing) in animation.timings[animation_state.timing_index..] - .iter() - .enumerate() - { - if timing.frame > state.frame_index { - animation_state.timing_index += i; - break; - } - if state.condition != timing.condition - && state.condition != Condition::None - && timing.condition != Condition::None - { - continue; - } - if let Some(se_name) = &timing.se.name { - Self::load_se(update_state, animation_state, state.condition, timing); - let Some(Some(audio_data)) = animation_state.audio_data.get(se_name.as_str()) - else { - continue; - }; - if let Err(e) = update_state.audio.play_from_slice( - audio_data.clone(), - false, - timing.se.volume, - timing.se.pitch, - None, - ) { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!("Error playing animation sound effect {se_name}")) - ); - } - } - } - if animation - .timings - .last() - .is_some_and(|timing| state.frame_index >= timing.frame) - { - animation_state.timing_index = animation.timings.len(); - } - - // Request a repaint every few frames - let frame_delay = 1. / 20.; // 20 FPS - ui.ctx() - .request_repaint_after(std::time::Duration::from_secs_f64( - frame_delay - time_diff.rem_euclid(frame_delay), - )); - } - if state.frame_index >= animation.frames.len() { - let animation_state = state.animation_state.take().unwrap(); - state.frame_index = animation_state.saved_frame_index; - } - - ui.horizontal(|ui| { - ui.add(luminol_components::Field::new( - "Editor Scale", - egui::Slider::new(&mut frame_view.scale, 15.0..=300.0) - .suffix("%") - .logarithmic(true) - .fixed_decimals(0), - )); - - state.frame_index = state - .frame_index - .min(animation.frames.len().saturating_sub(1)); - state.frame_index += 1; - recompute_flash |= ui - .add_enabled( - state.animation_state.is_none(), - luminol_components::Field::new( - "Frame", - egui::DragValue::new(&mut state.frame_index) - .range(1..=animation.frames.len()), - ), - ) - .changed(); - state.frame_index -= 1; - - recompute_flash |= ui - .add(luminol_components::Field::new( - "Condition", - luminol_components::EnumComboBox::new("condition", &mut state.condition) - .max_width(18.) - .wrap_mode(egui::TextWrapMode::Extend), - )) - .changed(); - - ui.add(luminol_components::Field::new( - "Onion Skin", - egui::Checkbox::without_text(&mut state.enable_onion_skin), - )); - - ui.with_layout( - egui::Layout { - main_dir: egui::Direction::RightToLeft, - cross_align: egui::Align::Max, - ..*ui.layout() - }, - |ui| { - egui::Frame::none() - .outer_margin(egui::Margin { - bottom: 2. * ui.spacing().item_spacing.y, - ..egui::Margin::ZERO - }) - .show(ui, |ui| { - ui.menu_button("Tools ⏷", |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - - ui.add_enabled_ui(state.frame_index != 0, |ui| { - if ui.button("Copy previous frame").clicked() - && state.frame_index != 0 - { - animation.frames[state.frame_index] = - animation.frames[state.frame_index - 1].clone(); - frame_view.frame.update_all_cells( - &update_state.graphics, - animation, - state.frame_index, - ); - modified = true; - } - }); - - ui.add(modals.copy_frames.button((), update_state)); - - ui.add(modals.clear_frames.button((), update_state)); - - ui.add_enabled_ui(animation.frames.len() >= 3, |ui| { - if animation.frames.len() >= 3 { - ui.add(modals.tween.button((), update_state)); - } else { - modals.tween.close_window(); - } - }); - - ui.add(modals.batch_edit.button((), update_state)); - }); - - if ui.button("Play").clicked() { - if let Some(animation_state) = state.animation_state.take() { - state.frame_index = animation_state.saved_frame_index; - } else { - state.animation_state = Some(AnimationState { - saved_frame_index: state.frame_index, - start_time: ui.input(|i| i.time), - timing_index: 0, - audio_data: Default::default(), - }); - state.frame_index = 0; - - // Preload the audio files used by the animation for - // performance reasons - for timing in &animation.timings { - Self::load_se( - update_state, - state.animation_state.as_mut().unwrap(), - state.condition, - timing, - ); - } - } - } - }); - }, - ); - }); - - if modals - .copy_frames - .show_window(ui.ctx(), state.frame_index, animation.frames.len()) - { - let mut iter = 0..modals.copy_frames.frame_count; - while let Some(i) = if modals.copy_frames.dst_frame <= modals.copy_frames.src_frame { - iter.next() - } else { - iter.next_back() - } { - animation.frames[modals.copy_frames.dst_frame + i] = - animation.frames[modals.copy_frames.src_frame + i].clone(); - } - frame_view - .frame - .update_all_cells(&update_state.graphics, animation, state.frame_index); - modified = true; - } - - if modals - .clear_frames - .show_window(ui.ctx(), state.frame_index, animation.frames.len()) - { - for i in modals.clear_frames.start_frame..=modals.clear_frames.end_frame { - animation.frames[i] = Default::default(); - } - frame_view - .frame - .update_all_cells(&update_state.graphics, animation, state.frame_index); - modified = true; - } - - if modals - .tween - .show_window(ui.ctx(), state.frame_index, animation.frames.len()) - { - for i in modals.tween.start_cell..=modals.tween.end_cell { - let data = &animation.frames[modals.tween.start_frame].cell_data; - if i >= data.xsize() || data[(i, 0)] < 0 { - continue; - } - let data = &animation.frames[modals.tween.end_frame].cell_data; - if i >= data.xsize() || data[(i, 0)] < 0 { - continue; - } - - for j in modals.tween.start_frame..=modals.tween.end_frame { - let lerp = |frames: &Vec, property| { - ( - egui::lerp( - frames[modals.tween.start_frame].cell_data[(i, property)] as f64 - ..=frames[modals.tween.end_frame].cell_data[(i, property)] - as f64, - (j - modals.tween.start_frame) as f64 - / (modals.tween.end_frame - modals.tween.start_frame) as f64, - ), - frames[modals.tween.start_frame].cell_data[(i, property)] - <= frames[modals.tween.end_frame].cell_data[(i, property)], - ) - }; - - if animation.frames[j].cell_data.xsize() < i + 1 { - Self::resize_frame(&mut animation.frames[j], i + 1); - } else if animation.frames[j].cell_max < i + 1 { - animation.frames[j].cell_max = i + 1; - } - - if modals.tween.tween_pattern { - let (val, orientation) = lerp(&animation.frames, 0); - animation.frames[j].cell_data[(i, 0)] = - if orientation { val.floor() } else { val.ceil() } as i16; - } else if animation.frames[j].cell_data[(i, 0)] < 0 { - animation.frames[j].cell_data[(i, 0)] = 0; - } - - if modals.tween.tween_position { - let (val, orientation) = lerp(&animation.frames, 1); - animation.frames[j].cell_data[(i, 1)] = - if orientation { val.floor() } else { val.ceil() } as i16; - - let (val, orientation) = lerp(&animation.frames, 2); - animation.frames[j].cell_data[(i, 2)] = - if orientation { val.floor() } else { val.ceil() } as i16; - - let (val, _) = lerp(&animation.frames, 3); - animation.frames[j].cell_data[(i, 3)] = val.floor() as i16; - - let (val, _) = lerp(&animation.frames, 4); - animation.frames[j].cell_data[(i, 4)] = val.floor() as i16; - } - - if modals.tween.tween_shading { - let (val, _) = lerp(&animation.frames, 6); - animation.frames[j].cell_data[(i, 6)] = val.floor() as i16; - - let (val, _) = lerp(&animation.frames, 7); - animation.frames[j].cell_data[(i, 7)] = val.floor() as i16; - } - } - } - frame_view - .frame - .update_all_cells(&update_state.graphics, animation, state.frame_index); - modified = true; - } - - if modals.batch_edit.show_window( - ui.ctx(), - state.frame_index, - animation.frames.len(), - frame_view.frame.atlas.num_patterns(), - ) { - for i in modals.batch_edit.start_frame..=modals.batch_edit.end_frame { - let data = &mut animation.frames[i].cell_data; - for j in 0..data.xsize() { - if data[(j, 0)] < 0 { - continue; - } - match modals.batch_edit.mode { - luminol_modals::animations::batch_edit_tool::Mode::Set => { - if modals.batch_edit.set_pattern_enabled { - data[(j, 0)] = modals.batch_edit.set_pattern; - } - if modals.batch_edit.set_x_enabled { - data[(j, 1)] = modals.batch_edit.set_x; - } - if modals.batch_edit.set_y_enabled { - data[(j, 2)] = modals.batch_edit.set_y; - } - if modals.batch_edit.set_scale_enabled { - data[(j, 3)] = modals.batch_edit.set_scale; - } - if modals.batch_edit.set_rotation_enabled { - data[(j, 4)] = modals.batch_edit.set_rotation; - } - if modals.batch_edit.set_flip_enabled { - data[(j, 5)] = modals.batch_edit.set_flip; - } - if modals.batch_edit.set_opacity_enabled { - data[(j, 6)] = modals.batch_edit.set_opacity; - } - if modals.batch_edit.set_blending_enabled { - data[(j, 7)] = modals.batch_edit.set_blending; - } - } - luminol_modals::animations::batch_edit_tool::Mode::Add => { - data[(j, 0)] = data[(j, 0)] - .saturating_add(modals.batch_edit.add_pattern) - .clamp( - 0, - frame_view.frame.atlas.num_patterns().saturating_sub(1) as i16, - ); - data[(j, 1)] = data[(j, 1)] - .saturating_add(modals.batch_edit.add_x) - .clamp(-(FRAME_WIDTH as i16 / 2), FRAME_WIDTH as i16 / 2); - data[(j, 2)] = data[(j, 2)] - .saturating_add(modals.batch_edit.add_y) - .clamp(-(FRAME_HEIGHT as i16 / 2), FRAME_HEIGHT as i16 / 2); - data[(j, 3)] = data[(j, 3)] - .saturating_add(modals.batch_edit.add_scale) - .max(1); - data[(j, 4)] += modals.batch_edit.add_rotation; - if !(0..=360).contains(&data[(j, 4)]) { - data[(j, 4)] = data[(j, 4)].rem_euclid(360); - } - if modals.batch_edit.add_flip { - if data[(j, 5)] == 1 { - data[(j, 5)] = 0; - } else { - data[(j, 5)] = 1; - } - } - data[(j, 6)] = data[(j, 6)] - .saturating_add(modals.batch_edit.add_opacity) - .clamp(0, 255); - data[(j, 7)] += modals.batch_edit.add_blending; - if !(0..3).contains(&data[(j, 7)]) { - data[(j, 7)] = data[(j, 7)].rem_euclid(3); - } - } - luminol_modals::animations::batch_edit_tool::Mode::Mul => { - data[(j, 0)] = - ((data[(j, 0)] + 1) as f64 * modals.batch_edit.mul_pattern) - .clamp(1., frame_view.frame.atlas.num_patterns() as f64) - .round_ties_even() as i16 - - 1; - data[(j, 1)] = (data[(j, 1)] as f64 * modals.batch_edit.mul_x) - .clamp(-(FRAME_WIDTH as f64 / 2.), FRAME_WIDTH as f64 / 2.) - .round_ties_even() - as i16; - data[(j, 2)] = (data[(j, 2)] as f64 * modals.batch_edit.mul_y) - .clamp(-(FRAME_HEIGHT as f64 / 2.), FRAME_HEIGHT as f64 / 2.) - .round_ties_even() - as i16; - data[(j, 3)] = (data[(j, 3)] as f64 * modals.batch_edit.mul_scale) - .clamp(1., i16::MAX as f64) - .round_ties_even() - as i16; - data[(j, 4)] = (data[(j, 4)] as f64 * modals.batch_edit.mul_rotation) - .round_ties_even() - as i16; - if !(0..=360).contains(&data[(j, 4)]) { - data[(j, 4)] = data[(j, 4)].rem_euclid(360); - } - data[(j, 6)] = (data[(j, 6)] as f64 * modals.batch_edit.mul_opacity) - .min(255.) - .round_ties_even() - as i16; - } - } - } - } - frame_view - .frame - .update_all_cells(&update_state.graphics, animation, state.frame_index); - modified = true; - } - - let canvas_rect = egui::Resize::default() - .resizable([false, true]) - .min_width(ui.available_width()) - .max_width(ui.available_width()) - .show(ui, |ui| { - egui::Frame::dark_canvas(ui.style()) - .show(ui, |ui| { - let (_, rect) = ui.allocate_space(ui.available_size()); - rect - }) - .inner - }); - - let frame = &mut animation.frames[state.frame_index]; - - if frame_view - .selected_cell_index - .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) - { - frame_view.selected_cell_index = None; - } - if frame_view - .hovered_cell_index - .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) - { - frame_view.hovered_cell_index = None; - frame_view.hovered_cell_drag_pos = None; - frame_view.hovered_cell_drag_offset = None; - } - - // Handle dragging of cells to move them - if let (Some(i), Some(drag_pos)) = ( - frame_view.hovered_cell_index, - frame_view.hovered_cell_drag_pos, - ) { - if (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) != drag_pos { - (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) = drag_pos; - frame_view.frame.update_cell( - &update_state.graphics, - animation, - state.frame_index, - i, - ); - modified = true; - } - } - - egui::Frame::none().show(ui, |ui| { - let frame = &mut animation.frames[state.frame_index]; - if let Some(i) = frame_view.selected_cell_index { - let mut properties_modified = false; - - ui.label(format!("Cell {}", i + 1)); - - ui.columns(4, |columns| { - let mut pattern = frame.cell_data[(i, 0)] + 1; - let changed = columns[0] - .add(luminol_components::Field::new( - "Pattern", - egui::DragValue::new(&mut pattern) - .range(1..=frame_view.frame.atlas.num_patterns() as i16), - )) - .changed(); - if changed { - frame.cell_data[(i, 0)] = pattern - 1; - properties_modified = true; - } - - properties_modified |= columns[1] - .add(luminol_components::Field::new( - "X", - egui::DragValue::new(&mut frame.cell_data[(i, 1)]) - .range(-(FRAME_WIDTH as i16 / 2)..=FRAME_WIDTH as i16 / 2), - )) - .changed(); - - properties_modified |= columns[2] - .add(luminol_components::Field::new( - "Y", - egui::DragValue::new(&mut frame.cell_data[(i, 2)]) - .range(-(FRAME_HEIGHT as i16 / 2)..=FRAME_HEIGHT as i16 / 2), - )) - .changed(); - - properties_modified |= columns[3] - .add(luminol_components::Field::new( - "Scale", - egui::DragValue::new(&mut frame.cell_data[(i, 3)]) - .range(1..=i16::MAX) - .suffix("%"), - )) - .changed(); - }); - - ui.columns(4, |columns| { - properties_modified |= columns[0] - .add(luminol_components::Field::new( - "Rotation", - egui::DragValue::new(&mut frame.cell_data[(i, 4)]) - .range(0..=360) - .suffix("°"), - )) - .changed(); - - let mut flip = frame.cell_data[(i, 5)] == 1; - let changed = columns[1] - .add(luminol_components::Field::new( - "Flip", - egui::Checkbox::without_text(&mut flip), - )) - .changed(); - if changed { - frame.cell_data[(i, 5)] = if flip { 1 } else { 0 }; - properties_modified = true; - } - - properties_modified |= columns[2] - .add(luminol_components::Field::new( - "Opacity", - egui::DragValue::new(&mut frame.cell_data[(i, 6)]).range(0..=255), - )) - .changed(); - - let mut blend_mode = match frame.cell_data[(i, 7)] { - 1 => BlendMode::Add, - 2 => BlendMode::Subtract, - _ => BlendMode::Normal, - }; - let changed = columns[3] - .add(luminol_components::Field::new( - "Blending", - luminol_components::EnumComboBox::new( - (animation.id, state.frame_index, i, 7usize), - &mut blend_mode, - ), - )) - .changed(); - if changed { - frame.cell_data[(i, 7)] = match blend_mode { - BlendMode::Normal => 0, - BlendMode::Add => 1, - BlendMode::Subtract => 2, - }; - properties_modified = true; - } - }); - - if properties_modified { - frame_view.frame.update_cell( - &update_state.graphics, - animation, - state.frame_index, - i, - ); - modified = true; - } - } - }); - - if recompute_flash { - frame_view.frame.update_battler( - &update_state.graphics, - system, - animation, - Some(flash_maps.compute_target(state.frame_index, state.condition)), - Some(flash_maps.compute_hide(state.frame_index, state.condition)), - ); - frame_view - .frame - .update_all_cells(&update_state.graphics, animation, state.frame_index); - } - - egui::ScrollArea::horizontal().show_viewport(ui, |ui, scroll_rect| { - cellpicker.ui(update_state, ui, scroll_rect); - }); - - ui.allocate_ui_at_rect(canvas_rect, |ui| { - frame_view.frame.enable_onion_skin = state.enable_onion_skin - && state.frame_index != 0 - && state.animation_state.is_none(); - let egui::InnerResponse { - inner: hover_pos, - response, - } = frame_view.ui( - ui, - update_state, - clip_rect, - flash_maps.compute_screen(state.frame_index, state.condition), - state.animation_state.is_none(), - ); - - // If the pointer is hovering over the frame view, prevent parent widgets - // from receiving scroll events so that scaling the frame view with the - // scroll wheel doesn't also scroll the scroll area that the frame view is - // in - if response.hovered() { - ui.ctx() - .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); - } - - let frame = &mut animation.frames[state.frame_index]; - - // Create new cell on double click - if let Some((x, y)) = hover_pos { - if response.double_clicked() { - let next_cell_index = (frame.cell_max..frame.cell_data.xsize()) - .find(|i| frame.cell_data[(*i, 0)] < 0) - .unwrap_or(frame.cell_data.xsize()); - - Self::resize_frame(frame, next_cell_index + 1); - - frame.cell_data[(next_cell_index, 0)] = cellpicker.selected_cell as i16; - frame.cell_data[(next_cell_index, 1)] = x; - frame.cell_data[(next_cell_index, 2)] = y; - frame.cell_data[(next_cell_index, 3)] = 100; - frame.cell_data[(next_cell_index, 4)] = 0; - frame.cell_data[(next_cell_index, 5)] = 0; - frame.cell_data[(next_cell_index, 6)] = 255; - frame.cell_data[(next_cell_index, 7)] = 1; - - frame_view.frame.update_cell( - &update_state.graphics, - animation, - state.frame_index, - next_cell_index, - ); - frame_view.selected_cell_index = Some(next_cell_index); - - modified = true; - } - } - - let frame = &mut animation.frames[state.frame_index]; - - // Handle pressing delete or backspace to delete cells - if let Some(i) = frame_view.selected_cell_index { - if i < frame.cell_data.xsize() - && frame.cell_data[(i, 0)] >= 0 - && response.has_focus() - && ui.input(|i| { - i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace) - }) - { - frame.cell_data[(i, 0)] = -1; - - if i + 1 >= frame.cell_max { - Self::resize_frame( - frame, - (0..frame - .cell_data - .xsize() - .min(frame.cell_max.saturating_sub(1))) - .rev() - .find_map(|i| (frame.cell_data[(i, 0)] >= 0).then_some(i + 1)) - .unwrap_or(0), - ); - } - - frame_view.frame.update_cell( - &update_state.graphics, - animation, - state.frame_index, - i, - ); - frame_view.selected_cell_index = None; - modified = true; - } - } - }); - - (modified, false) - } -} - -impl luminol_core::Window for Window { - fn id(&self) -> egui::Id { - egui::Id::new("animation_editor") - } - - fn requires_filesystem(&self) -> bool { - true - } - - fn show( - &mut self, - ctx: &egui::Context, - open: &mut bool, - update_state: &mut luminol_core::UpdateState<'_>, - ) { - let data = std::mem::take(update_state.data); // take data to avoid borrow checker issues - let mut animations = data.animations(); - let system = data.system(); - - let mut modified = false; - - self.selected_animation_name = None; - - let name = if let Some(name) = &self.selected_animation_name { - format!("Editing animation {:?}", name) - } else { - "Animation Editor".into() - }; - - let response = egui::Window::new(name) - .id(self.id()) - .default_width(500.) - .open(open) - .show(ctx, |ui| { - self.view.show( - ui, - update_state, - "Animations", - &mut animations.data, - |animation| format!("{:0>4}: {}", animation.id + 1, animation.name), - |ui, animations, id, update_state| { - let animation = &mut animations[id]; - self.selected_animation_name = Some(animation.name.clone()); - if animation.frames.is_empty() { - animation.frames.push(Default::default()); - animation.frame_max = 1; - } - - let clip_rect = ui.clip_rect(); - - if !self.frame_edit_state.flash_maps.contains(id) { - if !luminol_core::slice_is_sorted_by_key(&animation.timings, |timing| { - timing.frame - }) { - animation.timings.sort_by_key(|timing| timing.frame); - } - self.frame_edit_state.flash_maps.insert( - id, - FlashMaps { - none_hide: animation - .timings - .iter() - .filter(|timing| timing.flash_scope == Scope::HideTarget) - .map(|timing| { - ( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ) - }) - .collect(), - hit_hide: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Miss - && timing.flash_scope == Scope::HideTarget - }) - .map(|timing| { - ( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ) - }) - .collect(), - miss_hide: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Hit - && timing.flash_scope == Scope::HideTarget - }) - .map(|timing| { - ( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ) - }) - .collect(), - none_target: animation - .timings - .iter() - .filter(|timing| timing.flash_scope == Scope::Target) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - hit_target: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Miss - && timing.flash_scope == Scope::Target - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - miss_target: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Hit - && timing.flash_scope == Scope::Target - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - none_screen: animation - .timings - .iter() - .filter(|timing| timing.flash_scope == Scope::Screen) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - hit_screen: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Miss - && timing.flash_scope == Scope::Screen - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - miss_screen: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Hit - && timing.flash_scope == Scope::Screen - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - }, - ); - } - - ui.with_padded_stripe(false, |ui| { - modified |= ui - .add(luminol_components::Field::new( - "Name", - egui::TextEdit::singleline(&mut animation.name) - .desired_width(f32::INFINITY), - )) - .changed(); - }); - - ui.with_padded_stripe(true, |ui| { - let changed = ui - .add(luminol_components::Field::new( - "Battler Position", - luminol_components::EnumComboBox::new( - (animation.id, "position"), - &mut animation.position, - ), - )) - .changed(); - if changed { - if let Some(frame_view) = &mut self.frame_edit_state.frame_view { - frame_view.frame.update_battler( - &update_state.graphics, - &system, - animation, - None, - None, - ); - } - modified = true; - } - }); - - let abort = ui - .with_padded_stripe(false, |ui| { - if self.previous_battler_name != system.battler_name { - let battler_texture = - if let Some(battler_name) = &system.battler_name { - match update_state.graphics.texture_loader.load_now( - update_state.filesystem, - format!("Graphics/Battlers/{battler_name}"), - ) { - Ok(texture) => Some(texture), - Err(e) => { - Self::log_battler_error( - update_state, - &system, - animation, - e, - ); - return true; - } - } - } else { - None - }; - - if let Some(frame_view) = &mut self.frame_edit_state.frame_view - { - frame_view.frame.battler_texture = battler_texture; - frame_view.frame.rebuild_battler( - &update_state.graphics, - &system, - animation, - luminol_data::Color { - red: 255., - green: 255., - blue: 255., - alpha: 0., - }, - true, - ); - } - - self.previous_battler_name.clone_from(&system.battler_name); - } - - if self.previous_animation != Some(animation.id) { - self.modals.close_all(); - self.frame_edit_state.frame_index = self - .frame_edit_state - .frame_index - .min(animation.frames.len().saturating_sub(1)); - - let atlas = match update_state - .graphics - .atlas_loader - .load_animation_atlas( - &update_state.graphics, - update_state.filesystem, - animation, - ) { - Ok(atlas) => atlas, - Err(e) => { - Self::log_atlas_error(update_state, animation, e); - return true; - } - }; - - if let Some(frame_view) = &mut self.frame_edit_state.frame_view - { - let flash_maps = - self.frame_edit_state.flash_maps.get(id).unwrap(); - frame_view.frame.atlas = atlas.clone(); - frame_view.frame.update_battler( - &update_state.graphics, - &system, - animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - ); - frame_view.frame.rebuild_all_cells( - &update_state.graphics, - animation, - self.frame_edit_state.frame_index, - ); - } - - let selected_cell = self - .frame_edit_state - .cellpicker - .as_ref() - .map(|cellpicker| cellpicker.selected_cell) - .unwrap_or_default() - .min(atlas.num_patterns().saturating_sub(1)); - let mut cellpicker = luminol_components::Cellpicker::new( - &update_state.graphics, - atlas, - ); - cellpicker.selected_cell = selected_cell; - self.frame_edit_state.cellpicker = Some(cellpicker); - } - - let (inner_modified, abort) = Self::show_frame_edit( - ui, - update_state, - clip_rect, - &mut self.modals, - &system, - animation, - &mut self.frame_edit_state, - ); - - modified |= inner_modified; - - abort - }) - .inner; - - if abort { - return true; - } - - let mut collapsing_view_inner = Default::default(); - let flash_maps = self.frame_edit_state.flash_maps.get_mut(id).unwrap(); - - ui.with_padded_stripe(true, |ui| { - let changed = ui - .add(luminol_components::Field::new( - "SE and Flash", - |ui: &mut egui::Ui| { - if *update_state.modified_during_prev_frame { - self.collapsing_view.request_sort(); - } - if self.previous_animation != Some(animation.id) { - self.collapsing_view.clear_animations(); - self.timing_edit_state.se_picker.close_window(); - } else if self.collapsing_view.is_animating() { - self.timing_edit_state.se_picker.close_window(); - } - - let mut timings = std::mem::take(&mut animation.timings); - let egui::InnerResponse { inner, response } = - self.collapsing_view.show_with_sort( - ui, - animation.id, - &mut timings, - |ui, _i, timing| { - Self::show_timing_header(ui, timing) - }, - |ui, i, previous_timings, timing| { - Self::show_timing_body( - ui, - update_state, - animation, - flash_maps, - &mut self.timing_edit_state, - (i, previous_timings, timing), - ) - }, - |a, b| a.frame.cmp(&b.frame), - ); - collapsing_view_inner = inner; - animation.timings = timings; - response - }, - )) - .changed(); - if changed { - if let Some(frame_view) = &mut self.frame_edit_state.frame_view { - frame_view.frame.update_battler( - &update_state.graphics, - &system, - animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - ); - } - modified = true; - } - }); - - if let Some(i) = collapsing_view_inner.created_entry { - let timing = &animation.timings[i]; - match timing.flash_scope { - Scope::Target => { - flash_maps.none_target.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.none_screen.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.none_hide.append( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.hit_screen.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.append( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } - if timing.condition != Condition::Hit { - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.miss_screen.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.miss_hide.append( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } - self.frame_edit_state - .frame_view - .as_mut() - .unwrap() - .frame - .update_battler( - &update_state.graphics, - &system, - animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - ); - } - - if let Some((i, timing)) = collapsing_view_inner.deleted_entry { - let rank = |frame, scope| { - animation.timings[..i] - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| t.flash_scope == scope) - .count() - }; - match timing.flash_scope { - Scope::Target => { - flash_maps - .none_target - .remove(timing.frame, rank(timing.frame, Scope::Target)); - } - Scope::Screen => { - flash_maps - .none_screen - .remove(timing.frame, rank(timing.frame, Scope::Screen)); - } - Scope::HideTarget => { - flash_maps.none_hide.remove( - timing.frame, - rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { - let rank = |frame, scope| { - animation.timings[..i] - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| { - t.condition != Condition::Miss && t.flash_scope == scope - }) - .count() - }; - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.remove( - timing.frame, - rank(timing.frame, Scope::Target), - ); - } - Scope::Screen => { - flash_maps.hit_screen.remove( - timing.frame, - rank(timing.frame, Scope::Screen), - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.remove( - timing.frame, - rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - } - if timing.condition != Condition::Hit { - let rank = |frame, scope| { - animation.timings[..i] - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| { - t.condition != Condition::Hit && t.flash_scope == scope - }) - .count() - }; - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.remove( - timing.frame, - rank(timing.frame, Scope::Target), - ); - } - Scope::Screen => { - flash_maps.miss_screen.remove( - timing.frame, - rank(timing.frame, Scope::Screen), - ); - } - Scope::HideTarget => { - flash_maps.miss_hide.remove( - timing.frame, - rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - } - - self.frame_edit_state - .frame_view - .as_mut() - .unwrap() - .frame - .update_battler( - &update_state.graphics, - &system, - animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - ); - } - - self.previous_animation = Some(animation.id); - false - }, - ) - }); - - if response - .as_ref() - .is_some_and(|ir| ir.inner.as_ref().is_some_and(|ir| ir.inner.modified)) - { - modified = true; - } - - if modified { - update_state.modified.set(true); - animations.modified = true; - } - - drop(animations); - drop(system); - - *update_state.data = data; // restore data - - if response.is_some_and(|ir| ir.inner.is_some_and(|ir| ir.inner.inner == Some(true))) { - *open = false; - } - } -} diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs new file mode 100644 index 00000000..0a0266c8 --- /dev/null +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -0,0 +1,755 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use luminol_core::Modal; + +use luminol_data::{rpg::animation::Condition, BlendMode}; +use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; + +pub fn show_frame_edit( + ui: &mut egui::Ui, + update_state: &mut luminol_core::UpdateState<'_>, + clip_rect: egui::Rect, + modals: &mut super::Modals, + system: &luminol_data::rpg::System, + animation: &mut luminol_data::rpg::Animation, + state: &mut super::FrameEditState, +) -> (bool, bool) { + let mut modified = false; + let mut recompute_flash = false; + + let flash_maps = state.flash_maps.get_mut(animation.id).unwrap(); + + let frame_view = if let Some(frame_view) = &mut state.frame_view { + frame_view + } else { + let battler_texture = if let Some(battler_name) = &system.battler_name { + match update_state.graphics.texture_loader.load_now( + update_state.filesystem, + format!("Graphics/Battlers/{battler_name}"), + ) { + Ok(texture) => Some(texture), + Err(e) => { + super::util::log_battler_error(update_state, system, animation, e); + return (modified, true); + } + } + } else { + None + }; + let atlas = match update_state.graphics.atlas_loader.load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ) { + Ok(atlas) => atlas, + Err(e) => { + super::util::log_atlas_error(update_state, animation, e); + return (modified, true); + } + }; + let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); + frame_view.frame.battler_texture = battler_texture; + frame_view.frame.update_battler( + &update_state.graphics, + system, + animation, + Some(flash_maps.compute_target(state.frame_index, state.condition)), + Some(flash_maps.compute_hide(state.frame_index, state.condition)), + ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + state.frame_view = Some(frame_view); + state.frame_view.as_mut().unwrap() + }; + + let cellpicker = if let Some(cellpicker) = &mut state.cellpicker { + cellpicker + } else { + let atlas = frame_view.frame.atlas.clone(); + let cellpicker = luminol_components::Cellpicker::new(&update_state.graphics, atlas); + state.cellpicker = Some(cellpicker); + state.cellpicker.as_mut().unwrap() + }; + + // Handle playing of animations + if let Some(animation_state) = &mut state.animation_state { + // Determine what frame in the animation we're at by using the egui time and the + // framerate + let previous_frame_index = state.frame_index; + let time_diff = ui.input(|i| i.time) - animation_state.start_time; + state.frame_index = (time_diff * 20.) as usize; + + if state.frame_index != previous_frame_index { + recompute_flash = true; + } + + // Play sound effects + for (i, timing) in animation.timings[animation_state.timing_index..] + .iter() + .enumerate() + { + if timing.frame > state.frame_index { + animation_state.timing_index += i; + break; + } + if state.condition != timing.condition + && state.condition != Condition::None + && timing.condition != Condition::None + { + continue; + } + if let Some(se_name) = &timing.se.name { + super::util::load_se(update_state, animation_state, state.condition, timing); + let Some(Some(audio_data)) = animation_state.audio_data.get(se_name.as_str()) + else { + continue; + }; + if let Err(e) = update_state.audio.play_from_slice( + audio_data.clone(), + false, + timing.se.volume, + timing.se.pitch, + None, + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!("Error playing animation sound effect {se_name}")) + ); + } + } + } + if animation + .timings + .last() + .is_some_and(|timing| state.frame_index >= timing.frame) + { + animation_state.timing_index = animation.timings.len(); + } + + // Request a repaint every few frames + let frame_delay = 1. / 20.; // 20 FPS + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs_f64( + frame_delay - time_diff.rem_euclid(frame_delay), + )); + } + if state.frame_index >= animation.frames.len() { + let animation_state = state.animation_state.take().unwrap(); + state.frame_index = animation_state.saved_frame_index; + } + + ui.horizontal(|ui| { + ui.add(luminol_components::Field::new( + "Editor Scale", + egui::Slider::new(&mut frame_view.scale, 15.0..=300.0) + .suffix("%") + .logarithmic(true) + .fixed_decimals(0), + )); + + state.frame_index = state + .frame_index + .min(animation.frames.len().saturating_sub(1)); + state.frame_index += 1; + recompute_flash |= ui + .add_enabled( + state.animation_state.is_none(), + luminol_components::Field::new( + "Frame", + egui::DragValue::new(&mut state.frame_index).range(1..=animation.frames.len()), + ), + ) + .changed(); + state.frame_index -= 1; + + recompute_flash |= ui + .add(luminol_components::Field::new( + "Condition", + luminol_components::EnumComboBox::new("condition", &mut state.condition) + .max_width(18.) + .wrap_mode(egui::TextWrapMode::Extend), + )) + .changed(); + + ui.add(luminol_components::Field::new( + "Onion Skin", + egui::Checkbox::without_text(&mut state.enable_onion_skin), + )); + + ui.with_layout( + egui::Layout { + main_dir: egui::Direction::RightToLeft, + cross_align: egui::Align::Max, + ..*ui.layout() + }, + |ui| { + egui::Frame::none() + .outer_margin(egui::Margin { + bottom: 2. * ui.spacing().item_spacing.y, + ..egui::Margin::ZERO + }) + .show(ui, |ui| { + ui.menu_button("Tools ⏷", |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + ui.add_enabled_ui(state.frame_index != 0, |ui| { + if ui.button("Copy previous frame").clicked() + && state.frame_index != 0 + { + animation.frames[state.frame_index] = + animation.frames[state.frame_index - 1].clone(); + frame_view.frame.update_all_cells( + &update_state.graphics, + animation, + state.frame_index, + ); + modified = true; + } + }); + + ui.add(modals.copy_frames.button((), update_state)); + + ui.add(modals.clear_frames.button((), update_state)); + + ui.add_enabled_ui(animation.frames.len() >= 3, |ui| { + if animation.frames.len() >= 3 { + ui.add(modals.tween.button((), update_state)); + } else { + modals.tween.close_window(); + } + }); + + ui.add(modals.batch_edit.button((), update_state)); + }); + + if ui.button("Play").clicked() { + if let Some(animation_state) = state.animation_state.take() { + state.frame_index = animation_state.saved_frame_index; + } else { + state.animation_state = Some(super::AnimationState { + saved_frame_index: state.frame_index, + start_time: ui.input(|i| i.time), + timing_index: 0, + audio_data: Default::default(), + }); + state.frame_index = 0; + + // Preload the audio files used by the animation for + // performance reasons + for timing in &animation.timings { + super::util::load_se( + update_state, + state.animation_state.as_mut().unwrap(), + state.condition, + timing, + ); + } + } + } + }); + }, + ); + }); + + if modals + .copy_frames + .show_window(ui.ctx(), state.frame_index, animation.frames.len()) + { + let mut iter = 0..modals.copy_frames.frame_count; + while let Some(i) = if modals.copy_frames.dst_frame <= modals.copy_frames.src_frame { + iter.next() + } else { + iter.next_back() + } { + animation.frames[modals.copy_frames.dst_frame + i] = + animation.frames[modals.copy_frames.src_frame + i].clone(); + } + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + modified = true; + } + + if modals + .clear_frames + .show_window(ui.ctx(), state.frame_index, animation.frames.len()) + { + for i in modals.clear_frames.start_frame..=modals.clear_frames.end_frame { + animation.frames[i] = Default::default(); + } + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + modified = true; + } + + if modals + .tween + .show_window(ui.ctx(), state.frame_index, animation.frames.len()) + { + for i in modals.tween.start_cell..=modals.tween.end_cell { + let data = &animation.frames[modals.tween.start_frame].cell_data; + if i >= data.xsize() || data[(i, 0)] < 0 { + continue; + } + let data = &animation.frames[modals.tween.end_frame].cell_data; + if i >= data.xsize() || data[(i, 0)] < 0 { + continue; + } + + for j in modals.tween.start_frame..=modals.tween.end_frame { + let lerp = |frames: &Vec, property| { + ( + egui::lerp( + frames[modals.tween.start_frame].cell_data[(i, property)] as f64 + ..=frames[modals.tween.end_frame].cell_data[(i, property)] as f64, + (j - modals.tween.start_frame) as f64 + / (modals.tween.end_frame - modals.tween.start_frame) as f64, + ), + frames[modals.tween.start_frame].cell_data[(i, property)] + <= frames[modals.tween.end_frame].cell_data[(i, property)], + ) + }; + + if animation.frames[j].cell_data.xsize() < i + 1 { + super::util::resize_frame(&mut animation.frames[j], i + 1); + } else if animation.frames[j].cell_max < i + 1 { + animation.frames[j].cell_max = i + 1; + } + + if modals.tween.tween_pattern { + let (val, orientation) = lerp(&animation.frames, 0); + animation.frames[j].cell_data[(i, 0)] = + if orientation { val.floor() } else { val.ceil() } as i16; + } else if animation.frames[j].cell_data[(i, 0)] < 0 { + animation.frames[j].cell_data[(i, 0)] = 0; + } + + if modals.tween.tween_position { + let (val, orientation) = lerp(&animation.frames, 1); + animation.frames[j].cell_data[(i, 1)] = + if orientation { val.floor() } else { val.ceil() } as i16; + + let (val, orientation) = lerp(&animation.frames, 2); + animation.frames[j].cell_data[(i, 2)] = + if orientation { val.floor() } else { val.ceil() } as i16; + + let (val, _) = lerp(&animation.frames, 3); + animation.frames[j].cell_data[(i, 3)] = val.floor() as i16; + + let (val, _) = lerp(&animation.frames, 4); + animation.frames[j].cell_data[(i, 4)] = val.floor() as i16; + } + + if modals.tween.tween_shading { + let (val, _) = lerp(&animation.frames, 6); + animation.frames[j].cell_data[(i, 6)] = val.floor() as i16; + + let (val, _) = lerp(&animation.frames, 7); + animation.frames[j].cell_data[(i, 7)] = val.floor() as i16; + } + } + } + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + modified = true; + } + + if modals.batch_edit.show_window( + ui.ctx(), + state.frame_index, + animation.frames.len(), + frame_view.frame.atlas.num_patterns(), + ) { + for i in modals.batch_edit.start_frame..=modals.batch_edit.end_frame { + let data = &mut animation.frames[i].cell_data; + for j in 0..data.xsize() { + if data[(j, 0)] < 0 { + continue; + } + match modals.batch_edit.mode { + luminol_modals::animations::batch_edit_tool::Mode::Set => { + if modals.batch_edit.set_pattern_enabled { + data[(j, 0)] = modals.batch_edit.set_pattern; + } + if modals.batch_edit.set_x_enabled { + data[(j, 1)] = modals.batch_edit.set_x; + } + if modals.batch_edit.set_y_enabled { + data[(j, 2)] = modals.batch_edit.set_y; + } + if modals.batch_edit.set_scale_enabled { + data[(j, 3)] = modals.batch_edit.set_scale; + } + if modals.batch_edit.set_rotation_enabled { + data[(j, 4)] = modals.batch_edit.set_rotation; + } + if modals.batch_edit.set_flip_enabled { + data[(j, 5)] = modals.batch_edit.set_flip; + } + if modals.batch_edit.set_opacity_enabled { + data[(j, 6)] = modals.batch_edit.set_opacity; + } + if modals.batch_edit.set_blending_enabled { + data[(j, 7)] = modals.batch_edit.set_blending; + } + } + luminol_modals::animations::batch_edit_tool::Mode::Add => { + data[(j, 0)] = data[(j, 0)] + .saturating_add(modals.batch_edit.add_pattern) + .clamp( + 0, + frame_view.frame.atlas.num_patterns().saturating_sub(1) as i16, + ); + data[(j, 1)] = data[(j, 1)] + .saturating_add(modals.batch_edit.add_x) + .clamp(-(FRAME_WIDTH as i16 / 2), FRAME_WIDTH as i16 / 2); + data[(j, 2)] = data[(j, 2)] + .saturating_add(modals.batch_edit.add_y) + .clamp(-(FRAME_HEIGHT as i16 / 2), FRAME_HEIGHT as i16 / 2); + data[(j, 3)] = data[(j, 3)] + .saturating_add(modals.batch_edit.add_scale) + .max(1); + data[(j, 4)] += modals.batch_edit.add_rotation; + if !(0..=360).contains(&data[(j, 4)]) { + data[(j, 4)] = data[(j, 4)].rem_euclid(360); + } + if modals.batch_edit.add_flip { + if data[(j, 5)] == 1 { + data[(j, 5)] = 0; + } else { + data[(j, 5)] = 1; + } + } + data[(j, 6)] = data[(j, 6)] + .saturating_add(modals.batch_edit.add_opacity) + .clamp(0, 255); + data[(j, 7)] += modals.batch_edit.add_blending; + if !(0..3).contains(&data[(j, 7)]) { + data[(j, 7)] = data[(j, 7)].rem_euclid(3); + } + } + luminol_modals::animations::batch_edit_tool::Mode::Mul => { + data[(j, 0)] = ((data[(j, 0)] + 1) as f64 * modals.batch_edit.mul_pattern) + .clamp(1., frame_view.frame.atlas.num_patterns() as f64) + .round_ties_even() as i16 + - 1; + data[(j, 1)] = (data[(j, 1)] as f64 * modals.batch_edit.mul_x) + .clamp(-(FRAME_WIDTH as f64 / 2.), FRAME_WIDTH as f64 / 2.) + .round_ties_even() as i16; + data[(j, 2)] = (data[(j, 2)] as f64 * modals.batch_edit.mul_y) + .clamp(-(FRAME_HEIGHT as f64 / 2.), FRAME_HEIGHT as f64 / 2.) + .round_ties_even() as i16; + data[(j, 3)] = (data[(j, 3)] as f64 * modals.batch_edit.mul_scale) + .clamp(1., i16::MAX as f64) + .round_ties_even() as i16; + data[(j, 4)] = (data[(j, 4)] as f64 * modals.batch_edit.mul_rotation) + .round_ties_even() as i16; + if !(0..=360).contains(&data[(j, 4)]) { + data[(j, 4)] = data[(j, 4)].rem_euclid(360); + } + data[(j, 6)] = (data[(j, 6)] as f64 * modals.batch_edit.mul_opacity) + .min(255.) + .round_ties_even() as i16; + } + } + } + } + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + modified = true; + } + + let canvas_rect = egui::Resize::default() + .resizable([false, true]) + .min_width(ui.available_width()) + .max_width(ui.available_width()) + .show(ui, |ui| { + egui::Frame::dark_canvas(ui.style()) + .show(ui, |ui| { + let (_, rect) = ui.allocate_space(ui.available_size()); + rect + }) + .inner + }); + + let frame = &mut animation.frames[state.frame_index]; + + if frame_view + .selected_cell_index + .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) + { + frame_view.selected_cell_index = None; + } + if frame_view + .hovered_cell_index + .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) + { + frame_view.hovered_cell_index = None; + frame_view.hovered_cell_drag_pos = None; + frame_view.hovered_cell_drag_offset = None; + } + + // Handle dragging of cells to move them + if let (Some(i), Some(drag_pos)) = ( + frame_view.hovered_cell_index, + frame_view.hovered_cell_drag_pos, + ) { + if (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) != drag_pos { + (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) = drag_pos; + frame_view + .frame + .update_cell(&update_state.graphics, animation, state.frame_index, i); + modified = true; + } + } + + egui::Frame::none().show(ui, |ui| { + let frame = &mut animation.frames[state.frame_index]; + if let Some(i) = frame_view.selected_cell_index { + let mut properties_modified = false; + + ui.label(format!("Cell {}", i + 1)); + + ui.columns(4, |columns| { + let mut pattern = frame.cell_data[(i, 0)] + 1; + let changed = columns[0] + .add(luminol_components::Field::new( + "Pattern", + egui::DragValue::new(&mut pattern) + .range(1..=frame_view.frame.atlas.num_patterns() as i16), + )) + .changed(); + if changed { + frame.cell_data[(i, 0)] = pattern - 1; + properties_modified = true; + } + + properties_modified |= columns[1] + .add(luminol_components::Field::new( + "X", + egui::DragValue::new(&mut frame.cell_data[(i, 1)]) + .range(-(FRAME_WIDTH as i16 / 2)..=FRAME_WIDTH as i16 / 2), + )) + .changed(); + + properties_modified |= columns[2] + .add(luminol_components::Field::new( + "Y", + egui::DragValue::new(&mut frame.cell_data[(i, 2)]) + .range(-(FRAME_HEIGHT as i16 / 2)..=FRAME_HEIGHT as i16 / 2), + )) + .changed(); + + properties_modified |= columns[3] + .add(luminol_components::Field::new( + "Scale", + egui::DragValue::new(&mut frame.cell_data[(i, 3)]) + .range(1..=i16::MAX) + .suffix("%"), + )) + .changed(); + }); + + ui.columns(4, |columns| { + properties_modified |= columns[0] + .add(luminol_components::Field::new( + "Rotation", + egui::DragValue::new(&mut frame.cell_data[(i, 4)]) + .range(0..=360) + .suffix("°"), + )) + .changed(); + + let mut flip = frame.cell_data[(i, 5)] == 1; + let changed = columns[1] + .add(luminol_components::Field::new( + "Flip", + egui::Checkbox::without_text(&mut flip), + )) + .changed(); + if changed { + frame.cell_data[(i, 5)] = if flip { 1 } else { 0 }; + properties_modified = true; + } + + properties_modified |= columns[2] + .add(luminol_components::Field::new( + "Opacity", + egui::DragValue::new(&mut frame.cell_data[(i, 6)]).range(0..=255), + )) + .changed(); + + let mut blend_mode = match frame.cell_data[(i, 7)] { + 1 => BlendMode::Add, + 2 => BlendMode::Subtract, + _ => BlendMode::Normal, + }; + let changed = columns[3] + .add(luminol_components::Field::new( + "Blending", + luminol_components::EnumComboBox::new( + (animation.id, state.frame_index, i, 7usize), + &mut blend_mode, + ), + )) + .changed(); + if changed { + frame.cell_data[(i, 7)] = match blend_mode { + BlendMode::Normal => 0, + BlendMode::Add => 1, + BlendMode::Subtract => 2, + }; + properties_modified = true; + } + }); + + if properties_modified { + frame_view.frame.update_cell( + &update_state.graphics, + animation, + state.frame_index, + i, + ); + modified = true; + } + } + }); + + if recompute_flash { + frame_view.frame.update_battler( + &update_state.graphics, + system, + animation, + Some(flash_maps.compute_target(state.frame_index, state.condition)), + Some(flash_maps.compute_hide(state.frame_index, state.condition)), + ); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + } + + egui::ScrollArea::horizontal().show_viewport(ui, |ui, scroll_rect| { + cellpicker.ui(update_state, ui, scroll_rect); + }); + + ui.allocate_ui_at_rect(canvas_rect, |ui| { + frame_view.frame.enable_onion_skin = + state.enable_onion_skin && state.frame_index != 0 && state.animation_state.is_none(); + let egui::InnerResponse { + inner: hover_pos, + response, + } = frame_view.ui( + ui, + update_state, + clip_rect, + flash_maps.compute_screen(state.frame_index, state.condition), + state.animation_state.is_none(), + ); + + // If the pointer is hovering over the frame view, prevent parent widgets + // from receiving scroll events so that scaling the frame view with the + // scroll wheel doesn't also scroll the scroll area that the frame view is + // in + if response.hovered() { + ui.ctx() + .input_mut(|i| i.smooth_scroll_delta = egui::Vec2::ZERO); + } + + let frame = &mut animation.frames[state.frame_index]; + + // Create new cell on double click + if let Some((x, y)) = hover_pos { + if response.double_clicked() { + let next_cell_index = (frame.cell_max..frame.cell_data.xsize()) + .find(|i| frame.cell_data[(*i, 0)] < 0) + .unwrap_or(frame.cell_data.xsize()); + + super::util::resize_frame(frame, next_cell_index + 1); + + frame.cell_data[(next_cell_index, 0)] = cellpicker.selected_cell as i16; + frame.cell_data[(next_cell_index, 1)] = x; + frame.cell_data[(next_cell_index, 2)] = y; + frame.cell_data[(next_cell_index, 3)] = 100; + frame.cell_data[(next_cell_index, 4)] = 0; + frame.cell_data[(next_cell_index, 5)] = 0; + frame.cell_data[(next_cell_index, 6)] = 255; + frame.cell_data[(next_cell_index, 7)] = 1; + + frame_view.frame.update_cell( + &update_state.graphics, + animation, + state.frame_index, + next_cell_index, + ); + frame_view.selected_cell_index = Some(next_cell_index); + + modified = true; + } + } + + let frame = &mut animation.frames[state.frame_index]; + + // Handle pressing delete or backspace to delete cells + if let Some(i) = frame_view.selected_cell_index { + if i < frame.cell_data.xsize() + && frame.cell_data[(i, 0)] >= 0 + && response.has_focus() + && ui.input(|i| { + i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace) + }) + { + frame.cell_data[(i, 0)] = -1; + + if i + 1 >= frame.cell_max { + super::util::resize_frame( + frame, + (0..frame + .cell_data + .xsize() + .min(frame.cell_max.saturating_sub(1))) + .rev() + .find_map(|i| (frame.cell_data[(i, 0)] >= 0).then_some(i + 1)) + .unwrap_or(0), + ); + } + + frame_view.frame.update_cell( + &update_state.graphics, + animation, + state.frame_index, + i, + ); + frame_view.selected_cell_index = None; + modified = true; + } + } + }); + + (modified, false) +} diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs new file mode 100644 index 00000000..87ecd1c4 --- /dev/null +++ b/crates/ui/src/windows/animations/mod.rs @@ -0,0 +1,120 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +mod frame_edit; +mod timing; +mod util; +mod window; + +/// Database - Animations management window. +pub struct Window { + selected_animation_name: Option, + previous_animation: Option, + previous_battler_name: Option, + frame_edit_state: FrameEditState, + timing_edit_state: TimingEditState, + + collapsing_view: luminol_components::CollapsingView, + modals: Modals, + view: luminol_components::DatabaseView, +} + +struct FrameEditState { + frame_index: usize, + condition: luminol_data::rpg::animation::Condition, + enable_onion_skin: bool, + frame_view: Option, + cellpicker: Option, + flash_maps: luminol_data::OptionVec, + animation_state: Option, +} + +#[derive(Debug)] +struct AnimationState { + saved_frame_index: usize, + start_time: f64, + timing_index: usize, + audio_data: std::collections::HashMap>>, +} + +struct TimingEditState { + previous_frame: Option, + se_picker: luminol_modals::sound_picker::Modal, +} + +struct Modals { + copy_frames: luminol_modals::animations::copy_frames_tool::Modal, + clear_frames: luminol_modals::animations::clear_frames_tool::Modal, + tween: luminol_modals::animations::tween_tool::Modal, + batch_edit: luminol_modals::animations::batch_edit_tool::Modal, +} + +impl Modals { + fn close_all(&mut self) { + self.copy_frames.close_window(); + self.clear_frames.close_window(); + self.tween.close_window(); + self.batch_edit.close_window(); + } +} + +impl Default for Window { + fn default() -> Self { + Self { + selected_animation_name: None, + previous_animation: None, + previous_battler_name: None, + frame_edit_state: FrameEditState { + frame_index: 0, + condition: luminol_data::rpg::animation::Condition::Hit, + enable_onion_skin: false, + frame_view: None, + cellpicker: None, + flash_maps: Default::default(), + animation_state: None, + }, + timing_edit_state: TimingEditState { + previous_frame: None, + se_picker: luminol_modals::sound_picker::Modal::new( + luminol_audio::Source::SE, + "animations_timing_se_picker", + ), + }, + collapsing_view: luminol_components::CollapsingView::new(), + modals: Modals { + copy_frames: luminol_modals::animations::copy_frames_tool::Modal::new( + "animations_copy_frames_tool", + ), + clear_frames: luminol_modals::animations::clear_frames_tool::Modal::new( + "animations_clear_frames_tool", + ), + tween: luminol_modals::animations::tween_tool::Modal::new("animations_tween_tool"), + batch_edit: luminol_modals::animations::batch_edit_tool::Modal::new( + "animations_batch_edit_tool", + ), + }, + view: luminol_components::DatabaseView::new(), + } + } +} diff --git a/crates/ui/src/windows/animations/timing.rs b/crates/ui/src/windows/animations/timing.rs new file mode 100644 index 00000000..d7aeff37 --- /dev/null +++ b/crates/ui/src/windows/animations/timing.rs @@ -0,0 +1,725 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use egui::Widget; +use luminol_core::Modal; + +use super::{ + util::{ColorFlash, HideFlash}, + TimingEditState, +}; +use luminol_data::rpg::animation::{Condition, Scope, Timing}; + +pub fn show_timing_header(ui: &mut egui::Ui, timing: &Timing) { + let mut vec = Vec::with_capacity(3); + + match timing.condition { + luminol_data::rpg::animation::Condition::None => {} + luminol_data::rpg::animation::Condition::Hit => vec.push("on hit".into()), + luminol_data::rpg::animation::Condition::Miss => vec.push("on miss".into()), + } + + if let Some(path) = &timing.se.name { + vec.push(format!("play {:?}", path.file_name().unwrap_or_default())); + }; + + match timing.flash_scope { + Scope::None => {} + Scope::Target => { + vec.push(format!( + "flash target #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", + timing.flash_color.red.clamp(0., 255.).round() as u8, + timing.flash_color.green.clamp(0., 255.).round() as u8, + timing.flash_color.blue.clamp(0., 255.).round() as u8, + timing.flash_color.alpha.clamp(0., 255.).round() as u8, + timing.flash_duration, + )); + } + Scope::Screen => { + vec.push(format!( + "flash screen #{:0>2x}{:0>2x}{:0>2x}{:0>2x} for {} frames", + timing.flash_color.red.clamp(0., 255.).round() as u8, + timing.flash_color.green.clamp(0., 255.).round() as u8, + timing.flash_color.blue.clamp(0., 255.).round() as u8, + timing.flash_color.alpha.clamp(0., 255.).round() as u8, + timing.flash_duration, + )); + } + Scope::HideTarget => { + vec.push(format!("hide target for {} frames", timing.flash_duration)); + } + } + + ui.label(format!( + "Frame {:0>3}: {}", + timing.frame + 1, + vec.join(", ") + )); +} + +pub fn show_timing_body( + ui: &mut egui::Ui, + update_state: &mut luminol_core::UpdateState<'_>, + animation: &luminol_data::rpg::Animation, + flash_maps: &mut super::util::FlashMaps, + state: &mut TimingEditState, + timing: (usize, &[Timing], &mut Timing), +) -> egui::Response { + let (timing_index, previous_timings, timing) = timing; + let mut modified = false; + + let none_rank = |frame, scope| { + previous_timings + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.flash_scope == scope) + .count() + }; + let hit_rank = |frame, scope| { + previous_timings + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.condition != Condition::Miss && t.flash_scope == scope) + .count() + }; + let miss_rank = |frame, scope| { + previous_timings + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.condition != Condition::Hit && t.flash_scope == scope) + .count() + }; + + let mut response = egui::Frame::none() + .show(ui, |ui| { + ui.columns(2, |columns| { + columns[0].columns(2, |columns| { + let old_condition = timing.condition; + let changed = columns[1] + .add(luminol_components::Field::new( + "Condition", + luminol_components::EnumComboBox::new( + (animation.id, timing_index, "condition"), + &mut timing.condition, + ), + )) + .changed(); + if changed { + if old_condition != Condition::Miss && timing.condition == Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.remove( + timing.frame, + hit_rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.hit_screen.remove( + timing.frame, + hit_rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.remove( + timing.frame, + hit_rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } else if old_condition != Condition::Hit + && timing.condition == Condition::Hit + { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.remove( + timing.frame, + miss_rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.miss_screen.remove( + timing.frame, + miss_rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.remove( + timing.frame, + miss_rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } + if old_condition == Condition::Miss && timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.insert( + timing.frame, + hit_rank(timing.frame, Scope::Target), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.hit_screen.insert( + timing.frame, + hit_rank(timing.frame, Scope::Screen), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.insert( + timing.frame, + hit_rank(timing.frame, Scope::HideTarget), + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } else if old_condition == Condition::Hit + && timing.condition != Condition::Hit + { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.insert( + timing.frame, + miss_rank(timing.frame, Scope::Target), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.miss_screen.insert( + timing.frame, + miss_rank(timing.frame, Scope::Screen), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.insert( + timing.frame, + miss_rank(timing.frame, Scope::HideTarget), + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + modified = true; + } + + let old_frame = timing.frame; + let changed = columns[0] + .add(luminol_components::Field::new( + "Frame", + |ui: &mut egui::Ui| { + let mut frame = state.previous_frame.unwrap_or(timing.frame + 1); + let mut response = egui::DragValue::new(&mut frame) + .range(1..=animation.frames.len()) + .update_while_editing(false) + .ui(ui); + response.changed = false; + if response.dragged() { + state.previous_frame = Some(frame); + } else { + timing.frame = frame - 1; + state.previous_frame = None; + response.changed = true; + } + response + }, + )) + .changed(); + if changed { + match timing.flash_scope { + Scope::Target => { + flash_maps.none_target.set_frame( + old_frame, + none_rank(old_frame, Scope::Target), + timing.frame, + ); + } + Scope::Screen => { + flash_maps.none_screen.set_frame( + old_frame, + none_rank(old_frame, Scope::Screen), + timing.frame, + ); + } + Scope::HideTarget => { + flash_maps.none_hide.set_frame( + old_frame, + none_rank(old_frame, Scope::HideTarget), + timing.frame, + ); + } + Scope::None => {} + } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.set_frame( + old_frame, + hit_rank(old_frame, Scope::Target), + timing.frame, + ); + } + Scope::Screen => { + flash_maps.hit_screen.set_frame( + old_frame, + hit_rank(old_frame, Scope::Screen), + timing.frame, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.set_frame( + old_frame, + hit_rank(old_frame, Scope::HideTarget), + timing.frame, + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.set_frame( + old_frame, + miss_rank(old_frame, Scope::Target), + timing.frame, + ); + } + Scope::Screen => { + flash_maps.miss_screen.set_frame( + old_frame, + miss_rank(old_frame, Scope::Screen), + timing.frame, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.set_frame( + old_frame, + miss_rank(old_frame, Scope::HideTarget), + timing.frame, + ); + } + Scope::None => {} + } + } + modified = true; + } + }); + + modified |= columns[1] + .add(luminol_components::Field::new( + "SE", + state.se_picker.button(&mut timing.se, update_state), + )) + .changed(); + }); + + let old_scope = timing.flash_scope; + let (scope_changed, duration_changed) = if timing.flash_scope == Scope::None { + ( + ui.add(luminol_components::Field::new( + "Flash", + luminol_components::EnumComboBox::new( + (animation.id, timing_index, "flash_scope"), + &mut timing.flash_scope, + ), + )) + .changed(), + false, + ) + } else { + ui.columns(2, |columns| { + ( + columns[0] + .add(luminol_components::Field::new( + "Flash", + luminol_components::EnumComboBox::new( + (animation.id, timing_index, "flash_scope"), + &mut timing.flash_scope, + ), + )) + .changed(), + columns[1] + .add(luminol_components::Field::new( + "Flash Duration", + egui::DragValue::new(&mut timing.flash_duration) + .range(1..=animation.frames.len()), + )) + .changed(), + ) + }) + }; + + if scope_changed { + match old_scope { + Scope::Target => { + flash_maps + .none_target + .remove(timing.frame, none_rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .none_screen + .remove(timing.frame, none_rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps + .none_hide + .remove(timing.frame, none_rank(timing.frame, Scope::HideTarget)); + } + Scope::None => {} + } + match timing.flash_scope { + Scope::Target => { + flash_maps.none_target.insert( + timing.frame, + none_rank(timing.frame, Scope::Target), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.none_screen.insert( + timing.frame, + none_rank(timing.frame, Scope::Screen), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.none_hide.insert( + timing.frame, + none_rank(timing.frame, Scope::HideTarget), + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + if timing.condition != Condition::Miss { + match old_scope { + Scope::Target => { + flash_maps + .hit_target + .remove(timing.frame, hit_rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .hit_screen + .remove(timing.frame, hit_rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps + .hit_hide + .remove(timing.frame, hit_rank(timing.frame, Scope::HideTarget)); + } + Scope::None => {} + } + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.insert( + timing.frame, + hit_rank(timing.frame, Scope::Target), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.hit_screen.insert( + timing.frame, + hit_rank(timing.frame, Scope::Screen), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.insert( + timing.frame, + hit_rank(timing.frame, Scope::HideTarget), + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + match old_scope { + Scope::Target => { + flash_maps + .miss_target + .remove(timing.frame, miss_rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .miss_screen + .remove(timing.frame, miss_rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps + .miss_hide + .remove(timing.frame, miss_rank(timing.frame, Scope::HideTarget)); + } + Scope::None => {} + } + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.insert( + timing.frame, + miss_rank(timing.frame, Scope::Target), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.miss_screen.insert( + timing.frame, + miss_rank(timing.frame, Scope::Screen), + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.insert( + timing.frame, + miss_rank(timing.frame, Scope::HideTarget), + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + modified = true; + } + + if duration_changed { + match timing.flash_scope { + Scope::Target => { + flash_maps + .none_target + .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::Screen => { + flash_maps + .none_screen + .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::HideTarget => { + flash_maps + .none_hide + .get_mut(timing.frame, none_rank(timing.frame, Scope::HideTarget)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::None => unreachable!(), + } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps + .hit_target + .get_mut(timing.frame, hit_rank(timing.frame, Scope::Target)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::Screen => { + flash_maps + .hit_screen + .get_mut(timing.frame, hit_rank(timing.frame, Scope::Screen)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::HideTarget => { + flash_maps + .hit_hide + .get_mut(timing.frame, hit_rank(timing.frame, Scope::HideTarget)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::None => unreachable!(), + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps + .miss_target + .get_mut(timing.frame, miss_rank(timing.frame, Scope::Target)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::Screen => { + flash_maps + .miss_screen + .get_mut(timing.frame, miss_rank(timing.frame, Scope::Screen)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::HideTarget => { + flash_maps + .miss_hide + .get_mut(timing.frame, miss_rank(timing.frame, Scope::HideTarget)) + .unwrap() + .duration = timing.flash_duration; + } + Scope::None => unreachable!(), + } + } + modified = true; + } + + if matches!(timing.flash_scope, Scope::Target | Scope::Screen) { + let changed = ui + .add(luminol_components::Field::new( + "Flash Color", + |ui: &mut egui::Ui| { + let mut color = [ + timing.flash_color.red.clamp(0., 255.).round() as u8, + timing.flash_color.green.clamp(0., 255.).round() as u8, + timing.flash_color.blue.clamp(0., 255.).round() as u8, + timing.flash_color.alpha.clamp(0., 255.).round() as u8, + ]; + ui.spacing_mut().interact_size.x = ui.available_width(); // make the color picker button as wide as possible + let response = ui.color_edit_button_srgba_unmultiplied(&mut color); + if response.changed() { + timing.flash_color.red = color[0] as f64; + timing.flash_color.green = color[1] as f64; + timing.flash_color.blue = color[2] as f64; + timing.flash_color.alpha = color[3] as f64; + } + response + }, + )) + .changed(); + if changed { + match timing.flash_scope { + Scope::Target => { + flash_maps + .none_target + .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) + .unwrap() + .color = timing.flash_color; + } + Scope::Screen => { + flash_maps + .none_screen + .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) + .unwrap() + .color = timing.flash_color; + } + Scope::None | Scope::HideTarget => unreachable!(), + } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps + .hit_target + .get_mut(timing.frame, hit_rank(timing.frame, Scope::Target)) + .unwrap() + .color = timing.flash_color; + } + Scope::Screen => { + flash_maps + .hit_screen + .get_mut(timing.frame, hit_rank(timing.frame, Scope::Screen)) + .unwrap() + .color = timing.flash_color; + } + Scope::None | Scope::HideTarget => unreachable!(), + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps + .miss_target + .get_mut(timing.frame, miss_rank(timing.frame, Scope::Target)) + .unwrap() + .color = timing.flash_color; + } + Scope::Screen => { + flash_maps + .miss_screen + .get_mut(timing.frame, miss_rank(timing.frame, Scope::Screen)) + .unwrap() + .color = timing.flash_color; + } + Scope::None | Scope::HideTarget => unreachable!(), + } + } + modified = true; + } + } + }) + .response; + + if modified { + response.mark_changed(); + } + response +} diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs new file mode 100644 index 00000000..0042741e --- /dev/null +++ b/crates/ui/src/windows/animations/util.rs @@ -0,0 +1,306 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use luminol_filesystem::FileSystem; + +use luminol_data::rpg::animation::Condition; + +#[derive(Debug, Default)] +pub struct FlashMaps { + pub none_hide: FlashMap, + pub hit_hide: FlashMap, + pub miss_hide: FlashMap, + pub none_target: FlashMap, + pub hit_target: FlashMap, + pub miss_target: FlashMap, + pub none_screen: FlashMap, + pub hit_screen: FlashMap, + pub miss_screen: FlashMap, +} + +impl FlashMaps { + /// Determines what color the target flash should be for a given frame number and condition. + pub fn compute_target(&self, frame: usize, condition: Condition) -> luminol_data::Color { + match condition { + Condition::None => self.none_target.compute(frame), + Condition::Hit => self.hit_target.compute(frame), + Condition::Miss => self.miss_target.compute(frame), + } + } + + /// Determines what color the screen flash should be for a given frame number and condition. + pub fn compute_screen(&self, frame: usize, condition: Condition) -> luminol_data::Color { + match condition { + Condition::None => self.none_screen.compute(frame), + Condition::Hit => self.hit_screen.compute(frame), + Condition::Miss => self.miss_screen.compute(frame), + } + } + + /// Determines if the hide target flash is active for a given frame number and condition. + pub fn compute_hide(&self, frame: usize, condition: Condition) -> bool { + match condition { + Condition::None => self.none_hide.compute(frame), + Condition::Hit => self.hit_hide.compute(frame), + Condition::Miss => self.miss_hide.compute(frame), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ColorFlash { + pub color: luminol_data::Color, + pub duration: usize, +} + +#[derive(Debug, Clone, Copy)] +pub struct HideFlash { + pub duration: usize, +} + +#[derive(Debug)] +pub struct FlashMap { + map: std::collections::BTreeMap>, +} + +impl Default for FlashMap { + fn default() -> Self { + Self { + map: Default::default(), + } + } +} + +impl FromIterator<(usize, T)> for FlashMap +where + T: Copy, +{ + fn from_iter>(iterable: I) -> Self { + let mut map = Self::default(); + for (frame, flash) in iterable.into_iter() { + map.append(frame, flash); + } + map + } +} + +impl FlashMap +where + T: Copy, +{ + /// Adds a new flash into the map at the maximum rank. + pub fn append(&mut self, frame: usize, flash: T) { + self.map + .entry(frame) + .and_modify(|e| e.push_back(flash)) + .or_insert_with(|| [flash].into()); + } + + /// Adds a new flash into the map at the given rank. + pub fn insert(&mut self, frame: usize, rank: usize, flash: T) { + self.map + .entry(frame) + .and_modify(|e| e.insert(rank, flash)) + .or_insert_with(|| [flash].into()); + } + + /// Removes a flash from the map. + pub fn remove(&mut self, frame: usize, rank: usize) -> T { + let deque = self + .map + .get_mut(&frame) + .expect("no flashes found for the given frame"); + let flash = deque.remove(rank).expect("rank out of bounds"); + if deque.is_empty() { + self.map.remove(&frame).unwrap(); + } + flash + } + + /// Modifies the frame number for a flash. + pub fn set_frame(&mut self, frame: usize, rank: usize, new_frame: usize) { + if frame == new_frame { + return; + } + let flash = self.remove(frame, rank); + self.map + .entry(new_frame) + .and_modify(|e| { + if new_frame > frame { + e.push_front(flash) + } else { + e.push_back(flash) + } + }) + .or_insert_with(|| [flash].into()); + } + + pub fn get_mut(&mut self, frame: usize, rank: usize) -> Option<&mut T> { + self.map + .get_mut(&frame) + .and_then(|deque| deque.get_mut(rank)) + } +} + +impl FlashMap { + /// Determines what color the flash should be for a given frame number. + fn compute(&self, frame: usize) -> luminol_data::Color { + let Some((&start_frame, deque)) = self.map.range(..=frame).next_back() else { + return luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + }; + }; + let flash = deque.back().unwrap(); + + let diff = frame - start_frame; + if diff < flash.duration { + let progression = diff as f64 / flash.duration as f64; + luminol_data::Color { + alpha: flash.color.alpha * (1. - progression), + ..flash.color + } + } else { + luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + } + } + } +} + +impl FlashMap { + /// Determines if the hide flash is active for a given frame number. + fn compute(&self, frame: usize) -> bool { + let Some((&start_frame, deque)) = self.map.range(..=frame).next_back() else { + return false; + }; + let flash = deque.back().unwrap(); + + let diff = frame - start_frame; + diff < flash.duration + } +} + +pub fn log_battler_error( + update_state: &mut luminol_core::UpdateState<'_>, + system: &luminol_data::rpg::System, + animation: &luminol_data::rpg::Animation, + e: color_eyre::Report, +) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + system.battler_name, + animation.id + 1, + animation.name, + )), + ); +} + +pub fn log_atlas_error( + update_state: &mut luminol_core::UpdateState<'_>, + animation: &luminol_data::rpg::Animation, + e: color_eyre::Report, +) { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "While loading texture {:?} for animation {:0>4} {:?}", + animation.animation_name, + animation.id + 1, + animation.name, + )), + ); +} + +pub fn load_se( + update_state: &mut luminol_core::UpdateState<'_>, + animation_state: &mut super::AnimationState, + condition: Condition, + timing: &luminol_data::rpg::animation::Timing, +) { + let Some(se_name) = &timing.se.name else { + return; + }; + if (condition != timing.condition + && condition != Condition::None + && timing.condition != Condition::None) + || animation_state.audio_data.contains_key(se_name.as_str()) + { + return; + } + match update_state.filesystem.read(format!("Audio/SE/{se_name}")) { + Ok(data) => { + animation_state + .audio_data + .insert(se_name.to_string(), Some(data.into())); + } + Err(e) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!("Error loading animation sound effect {se_name}")) + ); + animation_state.audio_data.insert(se_name.to_string(), None); + } + } +} + +pub fn resize_frame(frame: &mut luminol_data::rpg::animation::Frame, new_cell_max: usize) { + let old_capacity = frame.cell_data.xsize(); + let new_capacity = if new_cell_max == 0 { + 0 + } else { + new_cell_max.next_power_of_two() + }; + + // Instead of resizing `frame.cell_data` every time we call this function, we increase the + // size of `frame.cell_data` only it's too small and we decrease the size of + // `frame.cell_data` only if it's at <= 25% capacity for better efficiency + let capacity_too_low = old_capacity < new_capacity; + let capacity_too_high = old_capacity >= new_capacity * 4; + + if capacity_too_low { + frame.cell_data.resize(new_capacity, 8); + for i in old_capacity..new_capacity { + frame.cell_data[(i, 0)] = -1; + frame.cell_data[(i, 1)] = 0; + frame.cell_data[(i, 2)] = 0; + frame.cell_data[(i, 3)] = 100; + frame.cell_data[(i, 4)] = 0; + frame.cell_data[(i, 5)] = 0; + frame.cell_data[(i, 6)] = 255; + frame.cell_data[(i, 7)] = 1; + } + } else if capacity_too_high { + frame.cell_data.resize(new_capacity * 2, 8); + } + + frame.cell_max = new_cell_max; +} diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs new file mode 100644 index 00000000..ef763ccc --- /dev/null +++ b/crates/ui/src/windows/animations/window.rs @@ -0,0 +1,718 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use luminol_components::UiExt; + +use super::util::{ColorFlash, HideFlash}; +use luminol_data::rpg::animation::{Condition, Scope}; + +impl luminol_core::Window for super::Window { + fn id(&self) -> egui::Id { + egui::Id::new("animation_editor") + } + + fn requires_filesystem(&self) -> bool { + true + } + + fn show( + &mut self, + ctx: &egui::Context, + open: &mut bool, + update_state: &mut luminol_core::UpdateState<'_>, + ) { + let data = std::mem::take(update_state.data); // take data to avoid borrow checker issues + let mut animations = data.animations(); + let system = data.system(); + + let mut modified = false; + + self.selected_animation_name = None; + + let name = if let Some(name) = &self.selected_animation_name { + format!("Editing animation {:?}", name) + } else { + "Animation Editor".into() + }; + + let response = egui::Window::new(name) + .id(self.id()) + .default_width(500.) + .open(open) + .show(ctx, |ui| { + self.view.show( + ui, + update_state, + "Animations", + &mut animations.data, + |animation| format!("{:0>4}: {}", animation.id + 1, animation.name), + |ui, animations, id, update_state| { + let animation = &mut animations[id]; + self.selected_animation_name = Some(animation.name.clone()); + if animation.frames.is_empty() { + animation.frames.push(Default::default()); + animation.frame_max = 1; + } + + let clip_rect = ui.clip_rect(); + + if !self.frame_edit_state.flash_maps.contains(id) { + if !luminol_core::slice_is_sorted_by_key(&animation.timings, |timing| { + timing.frame + }) { + animation.timings.sort_by_key(|timing| timing.frame); + } + self.frame_edit_state.flash_maps.insert( + id, + super::util::FlashMaps { + none_hide: animation + .timings + .iter() + .filter(|timing| timing.flash_scope == Scope::HideTarget) + .map(|timing| { + ( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ) + }) + .collect(), + hit_hide: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Miss + && timing.flash_scope == Scope::HideTarget + }) + .map(|timing| { + ( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ) + }) + .collect(), + miss_hide: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Hit + && timing.flash_scope == Scope::HideTarget + }) + .map(|timing| { + ( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ) + }) + .collect(), + none_target: animation + .timings + .iter() + .filter(|timing| timing.flash_scope == Scope::Target) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + hit_target: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Miss + && timing.flash_scope == Scope::Target + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + miss_target: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Hit + && timing.flash_scope == Scope::Target + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + none_screen: animation + .timings + .iter() + .filter(|timing| timing.flash_scope == Scope::Screen) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + hit_screen: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Miss + && timing.flash_scope == Scope::Screen + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + miss_screen: animation + .timings + .iter() + .filter(|timing| { + timing.condition != Condition::Hit + && timing.flash_scope == Scope::Screen + }) + .map(|timing| { + ( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ) + }) + .collect(), + }, + ); + } + + ui.with_padded_stripe(false, |ui| { + modified |= ui + .add(luminol_components::Field::new( + "Name", + egui::TextEdit::singleline(&mut animation.name) + .desired_width(f32::INFINITY), + )) + .changed(); + }); + + ui.with_padded_stripe(true, |ui| { + let changed = ui + .add(luminol_components::Field::new( + "Battler Position", + luminol_components::EnumComboBox::new( + (animation.id, "position"), + &mut animation.position, + ), + )) + .changed(); + if changed { + if let Some(frame_view) = &mut self.frame_edit_state.frame_view { + frame_view.frame.update_battler( + &update_state.graphics, + &system, + animation, + None, + None, + ); + } + modified = true; + } + }); + + let abort = ui + .with_padded_stripe(false, |ui| { + if self.previous_battler_name != system.battler_name { + let battler_texture = + if let Some(battler_name) = &system.battler_name { + match update_state.graphics.texture_loader.load_now( + update_state.filesystem, + format!("Graphics/Battlers/{battler_name}"), + ) { + Ok(texture) => Some(texture), + Err(e) => { + super::util::log_battler_error( + update_state, + &system, + animation, + e, + ); + return true; + } + } + } else { + None + }; + + if let Some(frame_view) = &mut self.frame_edit_state.frame_view + { + frame_view.frame.battler_texture = battler_texture; + frame_view.frame.rebuild_battler( + &update_state.graphics, + &system, + animation, + luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + }, + true, + ); + } + + self.previous_battler_name.clone_from(&system.battler_name); + } + + if self.previous_animation != Some(animation.id) { + self.modals.close_all(); + self.frame_edit_state.frame_index = self + .frame_edit_state + .frame_index + .min(animation.frames.len().saturating_sub(1)); + + let atlas = match update_state + .graphics + .atlas_loader + .load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ) { + Ok(atlas) => atlas, + Err(e) => { + super::util::log_atlas_error( + update_state, + animation, + e, + ); + return true; + } + }; + + if let Some(frame_view) = &mut self.frame_edit_state.frame_view + { + let flash_maps = + self.frame_edit_state.flash_maps.get(id).unwrap(); + frame_view.frame.atlas = atlas.clone(); + frame_view.frame.update_battler( + &update_state.graphics, + &system, + animation, + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + ); + frame_view.frame.rebuild_all_cells( + &update_state.graphics, + animation, + self.frame_edit_state.frame_index, + ); + } + + let selected_cell = self + .frame_edit_state + .cellpicker + .as_ref() + .map(|cellpicker| cellpicker.selected_cell) + .unwrap_or_default() + .min(atlas.num_patterns().saturating_sub(1)); + let mut cellpicker = luminol_components::Cellpicker::new( + &update_state.graphics, + atlas, + ); + cellpicker.selected_cell = selected_cell; + self.frame_edit_state.cellpicker = Some(cellpicker); + } + + let (inner_modified, abort) = super::frame_edit::show_frame_edit( + ui, + update_state, + clip_rect, + &mut self.modals, + &system, + animation, + &mut self.frame_edit_state, + ); + + modified |= inner_modified; + + abort + }) + .inner; + + if abort { + return true; + } + + let mut collapsing_view_inner = Default::default(); + let flash_maps = self.frame_edit_state.flash_maps.get_mut(id).unwrap(); + + ui.with_padded_stripe(true, |ui| { + let changed = ui + .add(luminol_components::Field::new( + "SE and Flash", + |ui: &mut egui::Ui| { + if *update_state.modified_during_prev_frame { + self.collapsing_view.request_sort(); + } + if self.previous_animation != Some(animation.id) { + self.collapsing_view.clear_animations(); + self.timing_edit_state.se_picker.close_window(); + } else if self.collapsing_view.is_animating() { + self.timing_edit_state.se_picker.close_window(); + } + + let mut timings = std::mem::take(&mut animation.timings); + let egui::InnerResponse { inner, response } = + self.collapsing_view.show_with_sort( + ui, + animation.id, + &mut timings, + |ui, _i, timing| { + super::timing::show_timing_header(ui, timing) + }, + |ui, i, previous_timings, timing| { + super::timing::show_timing_body( + ui, + update_state, + animation, + flash_maps, + &mut self.timing_edit_state, + (i, previous_timings, timing), + ) + }, + |a, b| a.frame.cmp(&b.frame), + ); + collapsing_view_inner = inner; + animation.timings = timings; + response + }, + )) + .changed(); + if changed { + if let Some(frame_view) = &mut self.frame_edit_state.frame_view { + frame_view.frame.update_battler( + &update_state.graphics, + &system, + animation, + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + ); + } + modified = true; + } + }); + + if let Some(i) = collapsing_view_inner.created_entry { + let timing = &animation.timings[i]; + match timing.flash_scope { + Scope::Target => { + flash_maps.none_target.append( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.none_screen.append( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.none_hide.append( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + if timing.condition != Condition::Miss { + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.append( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.hit_screen.append( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.append( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.append( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::Screen => { + flash_maps.miss_screen.append( + timing.frame, + ColorFlash { + color: timing.flash_color, + duration: timing.flash_duration, + }, + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.append( + timing.frame, + HideFlash { + duration: timing.flash_duration, + }, + ); + } + Scope::None => {} + } + } + self.frame_edit_state + .frame_view + .as_mut() + .unwrap() + .frame + .update_battler( + &update_state.graphics, + &system, + animation, + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + ); + } + + if let Some((i, timing)) = collapsing_view_inner.deleted_entry { + let rank = |frame, scope| { + animation.timings[..i] + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| t.flash_scope == scope) + .count() + }; + match timing.flash_scope { + Scope::Target => { + flash_maps + .none_target + .remove(timing.frame, rank(timing.frame, Scope::Target)); + } + Scope::Screen => { + flash_maps + .none_screen + .remove(timing.frame, rank(timing.frame, Scope::Screen)); + } + Scope::HideTarget => { + flash_maps.none_hide.remove( + timing.frame, + rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + if timing.condition != Condition::Miss { + let rank = |frame, scope| { + animation.timings[..i] + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| { + t.condition != Condition::Miss && t.flash_scope == scope + }) + .count() + }; + match timing.flash_scope { + Scope::Target => { + flash_maps.hit_target.remove( + timing.frame, + rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.hit_screen.remove( + timing.frame, + rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.hit_hide.remove( + timing.frame, + rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } + if timing.condition != Condition::Hit { + let rank = |frame, scope| { + animation.timings[..i] + .iter() + .rev() + .take_while(|t| t.frame == frame) + .filter(|t| { + t.condition != Condition::Hit && t.flash_scope == scope + }) + .count() + }; + match timing.flash_scope { + Scope::Target => { + flash_maps.miss_target.remove( + timing.frame, + rank(timing.frame, Scope::Target), + ); + } + Scope::Screen => { + flash_maps.miss_screen.remove( + timing.frame, + rank(timing.frame, Scope::Screen), + ); + } + Scope::HideTarget => { + flash_maps.miss_hide.remove( + timing.frame, + rank(timing.frame, Scope::HideTarget), + ); + } + Scope::None => {} + } + } + + self.frame_edit_state + .frame_view + .as_mut() + .unwrap() + .frame + .update_battler( + &update_state.graphics, + &system, + animation, + Some(flash_maps.compute_target( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + Some(flash_maps.compute_hide( + self.frame_edit_state.frame_index, + self.frame_edit_state.condition, + )), + ); + } + + self.previous_animation = Some(animation.id); + false + }, + ) + }); + + if response + .as_ref() + .is_some_and(|ir| ir.inner.as_ref().is_some_and(|ir| ir.inner.modified)) + { + modified = true; + } + + if modified { + update_state.modified.set(true); + animations.modified = true; + } + + drop(animations); + drop(system); + + *update_state.data = data; // restore data + + if response.is_some_and(|ir| ir.inner.is_some_and(|ir| ir.inner.inner == Some(true))) { + *open = false; + } + } +} From 70e3be0ac924624a67640e7a2f6816a586202eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 15:23:58 -0400 Subject: [PATCH 070/109] Refactor timing.rs to reduce code duplication --- .../ui/src/windows/animations/frame_edit.rs | 20 +- crates/ui/src/windows/animations/timing.rs | 374 ++++-------------- crates/ui/src/windows/animations/util.rs | 55 ++- crates/ui/src/windows/animations/window.rs | 72 ++-- 4 files changed, 160 insertions(+), 361 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 0a0266c8..3e9e0a3f 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -75,8 +75,12 @@ pub fn show_frame_edit( &update_state.graphics, system, animation, - Some(flash_maps.compute_target(state.frame_index, state.condition)), - Some(flash_maps.compute_hide(state.frame_index, state.condition)), + Some( + flash_maps + .target(state.condition) + .compute(state.frame_index), + ), + Some(flash_maps.hide(state.condition).compute(state.frame_index)), ); frame_view .frame @@ -646,8 +650,12 @@ pub fn show_frame_edit( &update_state.graphics, system, animation, - Some(flash_maps.compute_target(state.frame_index, state.condition)), - Some(flash_maps.compute_hide(state.frame_index, state.condition)), + Some( + flash_maps + .target(state.condition) + .compute(state.frame_index), + ), + Some(flash_maps.hide(state.condition).compute(state.frame_index)), ); frame_view .frame @@ -668,7 +676,9 @@ pub fn show_frame_edit( ui, update_state, clip_rect, - flash_maps.compute_screen(state.frame_index, state.condition), + flash_maps + .screen(state.condition) + .compute(state.frame_index), state.animation_state.is_none(), ); diff --git a/crates/ui/src/windows/animations/timing.rs b/crates/ui/src/windows/animations/timing.rs index d7aeff37..9bd011a1 100644 --- a/crates/ui/src/windows/animations/timing.rs +++ b/crates/ui/src/windows/animations/timing.rs @@ -31,6 +31,16 @@ use super::{ }; use luminol_data::rpg::animation::{Condition, Scope, Timing}; +fn apply_with_condition(condition: Condition, mut closure: impl FnMut(Condition)) { + closure(Condition::None); + if condition != Condition::Miss { + closure(Condition::Hit); + } + if condition != Condition::Hit { + closure(Condition::Miss); + } +} + pub fn show_timing_header(ui: &mut egui::Ui, timing: &Timing) { let mut vec = Vec::with_capacity(3); @@ -89,28 +99,16 @@ pub fn show_timing_body( let (timing_index, previous_timings, timing) = timing; let mut modified = false; - let none_rank = |frame, scope| { - previous_timings - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| t.flash_scope == scope) - .count() - }; - let hit_rank = |frame, scope| { + let rank = |condition, frame, scope| { previous_timings .iter() .rev() .take_while(|t| t.frame == frame) - .filter(|t| t.condition != Condition::Miss && t.flash_scope == scope) - .count() - }; - let miss_rank = |frame, scope| { - previous_timings - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| t.condition != Condition::Hit && t.flash_scope == scope) + .filter(|t| match condition { + Condition::None => true, + Condition::Hit => t.condition != Condition::Miss, + Condition::Miss => t.condition != Condition::Hit, + } && t.flash_scope == scope) .count() }; @@ -134,19 +132,19 @@ pub fn show_timing_body( Scope::Target => { flash_maps.hit_target.remove( timing.frame, - hit_rank(timing.frame, Scope::Target), + rank(Condition::Hit, timing.frame, Scope::Target), ); } Scope::Screen => { flash_maps.hit_screen.remove( timing.frame, - hit_rank(timing.frame, Scope::Screen), + rank(Condition::Hit, timing.frame, Scope::Screen), ); } Scope::HideTarget => { flash_maps.hit_hide.remove( timing.frame, - hit_rank(timing.frame, Scope::HideTarget), + rank(Condition::Hit, timing.frame, Scope::HideTarget), ); } Scope::None => {} @@ -158,19 +156,19 @@ pub fn show_timing_body( Scope::Target => { flash_maps.miss_target.remove( timing.frame, - miss_rank(timing.frame, Scope::Target), + rank(Condition::Miss, timing.frame, Scope::Target), ); } Scope::Screen => { flash_maps.miss_screen.remove( timing.frame, - miss_rank(timing.frame, Scope::Screen), + rank(Condition::Miss, timing.frame, Scope::Screen), ); } Scope::HideTarget => { flash_maps.miss_hide.remove( timing.frame, - miss_rank(timing.frame, Scope::HideTarget), + rank(Condition::Miss, timing.frame, Scope::HideTarget), ); } Scope::None => {} @@ -181,7 +179,7 @@ pub fn show_timing_body( Scope::Target => { flash_maps.hit_target.insert( timing.frame, - hit_rank(timing.frame, Scope::Target), + rank(Condition::Hit, timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -191,7 +189,7 @@ pub fn show_timing_body( Scope::Screen => { flash_maps.hit_screen.insert( timing.frame, - hit_rank(timing.frame, Scope::Screen), + rank(Condition::Hit, timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -201,7 +199,7 @@ pub fn show_timing_body( Scope::HideTarget => { flash_maps.hit_hide.insert( timing.frame, - hit_rank(timing.frame, Scope::HideTarget), + rank(Condition::Hit, timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -216,7 +214,7 @@ pub fn show_timing_body( Scope::Target => { flash_maps.miss_target.insert( timing.frame, - miss_rank(timing.frame, Scope::Target), + rank(Condition::Miss, timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -226,7 +224,7 @@ pub fn show_timing_body( Scope::Screen => { flash_maps.miss_screen.insert( timing.frame, - miss_rank(timing.frame, Scope::Screen), + rank(Condition::Miss, timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -236,7 +234,7 @@ pub fn show_timing_body( Scope::HideTarget => { flash_maps.miss_hide.insert( timing.frame, - miss_rank(timing.frame, Scope::HideTarget), + rank(Condition::Miss, timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -271,82 +269,32 @@ pub fn show_timing_body( )) .changed(); if changed { - match timing.flash_scope { - Scope::Target => { - flash_maps.none_target.set_frame( - old_frame, - none_rank(old_frame, Scope::Target), - timing.frame, - ); - } - Scope::Screen => { - flash_maps.none_screen.set_frame( - old_frame, - none_rank(old_frame, Scope::Screen), - timing.frame, - ); - } - Scope::HideTarget => { - flash_maps.none_hide.set_frame( - old_frame, - none_rank(old_frame, Scope::HideTarget), - timing.frame, - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.set_frame( - old_frame, - hit_rank(old_frame, Scope::Target), - timing.frame, - ); - } - Scope::Screen => { - flash_maps.hit_screen.set_frame( - old_frame, - hit_rank(old_frame, Scope::Screen), - timing.frame, - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.set_frame( - old_frame, - hit_rank(old_frame, Scope::HideTarget), - timing.frame, - ); - } - Scope::None => {} - } - } - if timing.condition != Condition::Hit { + apply_with_condition(timing.condition, |condition| { match timing.flash_scope { Scope::Target => { - flash_maps.miss_target.set_frame( + flash_maps.target_mut(condition).set_frame( old_frame, - miss_rank(old_frame, Scope::Target), + rank(condition, old_frame, Scope::Target), timing.frame, ); } Scope::Screen => { - flash_maps.miss_screen.set_frame( + flash_maps.screen_mut(condition).set_frame( old_frame, - miss_rank(old_frame, Scope::Screen), + rank(condition, old_frame, Scope::Screen), timing.frame, ); } Scope::HideTarget => { - flash_maps.miss_hide.set_frame( + flash_maps.hide_mut(condition).set_frame( old_frame, - miss_rank(old_frame, Scope::HideTarget), + rank(condition, old_frame, Scope::HideTarget), timing.frame, ); } Scope::None => {} } - } + }); modified = true; } }); @@ -396,132 +344,31 @@ pub fn show_timing_body( }; if scope_changed { - match old_scope { - Scope::Target => { - flash_maps - .none_target - .remove(timing.frame, none_rank(timing.frame, Scope::Target)); - } - Scope::Screen => { - flash_maps - .none_screen - .remove(timing.frame, none_rank(timing.frame, Scope::Screen)); - } - Scope::HideTarget => { - flash_maps - .none_hide - .remove(timing.frame, none_rank(timing.frame, Scope::HideTarget)); - } - Scope::None => {} - } - match timing.flash_scope { - Scope::Target => { - flash_maps.none_target.insert( - timing.frame, - none_rank(timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.none_screen.insert( - timing.frame, - none_rank(timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.none_hide.insert( - timing.frame, - none_rank(timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { + apply_with_condition(timing.condition, |condition| { match old_scope { Scope::Target => { flash_maps - .hit_target - .remove(timing.frame, hit_rank(timing.frame, Scope::Target)); + .target_mut(condition) + .remove(timing.frame, rank(condition, timing.frame, Scope::Target)); } Scope::Screen => { flash_maps - .hit_screen - .remove(timing.frame, hit_rank(timing.frame, Scope::Screen)); + .screen_mut(condition) + .remove(timing.frame, rank(condition, timing.frame, Scope::Screen)); } Scope::HideTarget => { - flash_maps - .hit_hide - .remove(timing.frame, hit_rank(timing.frame, Scope::HideTarget)); - } - Scope::None => {} - } - match timing.flash_scope { - Scope::Target => { - flash_maps.hit_target.insert( + flash_maps.hide_mut(condition).remove( timing.frame, - hit_rank(timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, + rank(condition, timing.frame, Scope::HideTarget), ); } - Scope::Screen => { - flash_maps.hit_screen.insert( - timing.frame, - hit_rank(timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.insert( - timing.frame, - hit_rank(timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } - if timing.condition != Condition::Hit { - match old_scope { - Scope::Target => { - flash_maps - .miss_target - .remove(timing.frame, miss_rank(timing.frame, Scope::Target)); - } - Scope::Screen => { - flash_maps - .miss_screen - .remove(timing.frame, miss_rank(timing.frame, Scope::Screen)); - } - Scope::HideTarget => { - flash_maps - .miss_hide - .remove(timing.frame, miss_rank(timing.frame, Scope::HideTarget)); - } Scope::None => {} } match timing.flash_scope { Scope::Target => { - flash_maps.miss_target.insert( + flash_maps.target_mut(condition).insert( timing.frame, - miss_rank(timing.frame, Scope::Target), + rank(condition, timing.frame, Scope::Target), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -529,9 +376,9 @@ pub fn show_timing_body( ); } Scope::Screen => { - flash_maps.miss_screen.insert( + flash_maps.screen_mut(condition).insert( timing.frame, - miss_rank(timing.frame, Scope::Screen), + rank(condition, timing.frame, Scope::Screen), ColorFlash { color: timing.flash_color, duration: timing.flash_duration, @@ -539,9 +386,9 @@ pub fn show_timing_body( ); } Scope::HideTarget => { - flash_maps.miss_hide.insert( + flash_maps.hide_mut(condition).insert( timing.frame, - miss_rank(timing.frame, Scope::HideTarget), + rank(condition, timing.frame, Scope::HideTarget), HideFlash { duration: timing.flash_duration, }, @@ -549,87 +396,38 @@ pub fn show_timing_body( } Scope::None => {} } - } + }); modified = true; } if duration_changed { - match timing.flash_scope { + apply_with_condition(timing.condition, |condition| match timing.flash_scope { Scope::Target => { flash_maps - .none_target - .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) + .target_mut(condition) + .get_mut(timing.frame, rank(condition, timing.frame, Scope::Target)) .unwrap() .duration = timing.flash_duration; } Scope::Screen => { flash_maps - .none_screen - .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) + .screen_mut(condition) + .get_mut(timing.frame, rank(condition, timing.frame, Scope::Screen)) .unwrap() .duration = timing.flash_duration; } Scope::HideTarget => { flash_maps - .none_hide - .get_mut(timing.frame, none_rank(timing.frame, Scope::HideTarget)) + .hide_mut(condition) + .get_mut( + timing.frame, + rank(condition, timing.frame, Scope::HideTarget), + ) .unwrap() .duration = timing.flash_duration; } Scope::None => unreachable!(), - } - if timing.condition != Condition::Miss { - match timing.flash_scope { - Scope::Target => { - flash_maps - .hit_target - .get_mut(timing.frame, hit_rank(timing.frame, Scope::Target)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::Screen => { - flash_maps - .hit_screen - .get_mut(timing.frame, hit_rank(timing.frame, Scope::Screen)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::HideTarget => { - flash_maps - .hit_hide - .get_mut(timing.frame, hit_rank(timing.frame, Scope::HideTarget)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::None => unreachable!(), - } - } - if timing.condition != Condition::Hit { - match timing.flash_scope { - Scope::Target => { - flash_maps - .miss_target - .get_mut(timing.frame, miss_rank(timing.frame, Scope::Target)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::Screen => { - flash_maps - .miss_screen - .get_mut(timing.frame, miss_rank(timing.frame, Scope::Screen)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::HideTarget => { - flash_maps - .miss_hide - .get_mut(timing.frame, miss_rank(timing.frame, Scope::HideTarget)) - .unwrap() - .duration = timing.flash_duration; - } - Scope::None => unreachable!(), - } - } + }); modified = true; } @@ -657,61 +455,23 @@ pub fn show_timing_body( )) .changed(); if changed { - match timing.flash_scope { + apply_with_condition(timing.condition, |condition| match timing.flash_scope { Scope::Target => { flash_maps - .none_target - .get_mut(timing.frame, none_rank(timing.frame, Scope::Target)) + .target_mut(condition) + .get_mut(timing.frame, rank(condition, timing.frame, Scope::Target)) .unwrap() .color = timing.flash_color; } Scope::Screen => { flash_maps - .none_screen - .get_mut(timing.frame, none_rank(timing.frame, Scope::Screen)) + .screen_mut(condition) + .get_mut(timing.frame, rank(condition, timing.frame, Scope::Screen)) .unwrap() .color = timing.flash_color; } Scope::None | Scope::HideTarget => unreachable!(), - } - if timing.condition != Condition::Miss { - match timing.flash_scope { - Scope::Target => { - flash_maps - .hit_target - .get_mut(timing.frame, hit_rank(timing.frame, Scope::Target)) - .unwrap() - .color = timing.flash_color; - } - Scope::Screen => { - flash_maps - .hit_screen - .get_mut(timing.frame, hit_rank(timing.frame, Scope::Screen)) - .unwrap() - .color = timing.flash_color; - } - Scope::None | Scope::HideTarget => unreachable!(), - } - } - if timing.condition != Condition::Hit { - match timing.flash_scope { - Scope::Target => { - flash_maps - .miss_target - .get_mut(timing.frame, miss_rank(timing.frame, Scope::Target)) - .unwrap() - .color = timing.flash_color; - } - Scope::Screen => { - flash_maps - .miss_screen - .get_mut(timing.frame, miss_rank(timing.frame, Scope::Screen)) - .unwrap() - .color = timing.flash_color; - } - Scope::None | Scope::HideTarget => unreachable!(), - } - } + }); modified = true; } } diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs index 0042741e..cb6fe6a8 100644 --- a/crates/ui/src/windows/animations/util.rs +++ b/crates/ui/src/windows/animations/util.rs @@ -40,30 +40,51 @@ pub struct FlashMaps { } impl FlashMaps { - /// Determines what color the target flash should be for a given frame number and condition. - pub fn compute_target(&self, frame: usize, condition: Condition) -> luminol_data::Color { + pub fn target(&self, condition: Condition) -> &FlashMap { match condition { - Condition::None => self.none_target.compute(frame), - Condition::Hit => self.hit_target.compute(frame), - Condition::Miss => self.miss_target.compute(frame), + Condition::None => &self.none_target, + Condition::Hit => &self.hit_target, + Condition::Miss => &self.miss_target, } } - /// Determines what color the screen flash should be for a given frame number and condition. - pub fn compute_screen(&self, frame: usize, condition: Condition) -> luminol_data::Color { + pub fn target_mut(&mut self, condition: Condition) -> &mut FlashMap { match condition { - Condition::None => self.none_screen.compute(frame), - Condition::Hit => self.hit_screen.compute(frame), - Condition::Miss => self.miss_screen.compute(frame), + Condition::None => &mut self.none_target, + Condition::Hit => &mut self.hit_target, + Condition::Miss => &mut self.miss_target, } } - /// Determines if the hide target flash is active for a given frame number and condition. - pub fn compute_hide(&self, frame: usize, condition: Condition) -> bool { + pub fn screen(&self, condition: Condition) -> &FlashMap { match condition { - Condition::None => self.none_hide.compute(frame), - Condition::Hit => self.hit_hide.compute(frame), - Condition::Miss => self.miss_hide.compute(frame), + Condition::None => &self.none_screen, + Condition::Hit => &self.hit_screen, + Condition::Miss => &self.miss_screen, + } + } + + pub fn screen_mut(&mut self, condition: Condition) -> &mut FlashMap { + match condition { + Condition::None => &mut self.none_screen, + Condition::Hit => &mut self.hit_screen, + Condition::Miss => &mut self.miss_screen, + } + } + + pub fn hide(&self, condition: Condition) -> &FlashMap { + match condition { + Condition::None => &self.none_hide, + Condition::Hit => &self.hit_hide, + Condition::Miss => &self.miss_hide, + } + } + + pub fn hide_mut(&mut self, condition: Condition) -> &mut FlashMap { + match condition { + Condition::None => &mut self.none_hide, + Condition::Hit => &mut self.hit_hide, + Condition::Miss => &mut self.miss_hide, } } } @@ -165,7 +186,7 @@ where impl FlashMap { /// Determines what color the flash should be for a given frame number. - fn compute(&self, frame: usize) -> luminol_data::Color { + pub fn compute(&self, frame: usize) -> luminol_data::Color { let Some((&start_frame, deque)) = self.map.range(..=frame).next_back() else { return luminol_data::Color { red: 255., @@ -196,7 +217,7 @@ impl FlashMap { impl FlashMap { /// Determines if the hide flash is active for a given frame number. - fn compute(&self, frame: usize) -> bool { + pub fn compute(&self, frame: usize) -> bool { let Some((&start_frame, deque)) = self.map.range(..=frame).next_back() else { return false; }; diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index ef763ccc..66f3573a 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -344,14 +344,16 @@ impl luminol_core::Window for super::Window { &update_state.graphics, &system, animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), + Some( + flash_maps + .target(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps + .hide(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), ); frame_view.frame.rebuild_all_cells( &update_state.graphics, @@ -446,14 +448,16 @@ impl luminol_core::Window for super::Window { &update_state.graphics, &system, animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), + Some( + flash_maps + .target(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps + .hide(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), ); } modified = true; @@ -562,14 +566,16 @@ impl luminol_core::Window for super::Window { &update_state.graphics, &system, animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), + Some( + flash_maps + .target(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps + .hide(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), ); } @@ -677,14 +683,16 @@ impl luminol_core::Window for super::Window { &update_state.graphics, &system, animation, - Some(flash_maps.compute_target( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), - Some(flash_maps.compute_hide( - self.frame_edit_state.frame_index, - self.frame_edit_state.condition, - )), + Some( + flash_maps + .target(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps + .hide(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), ); } From 008d1724f915d6a9486bf779705d52b4ccf3eaef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 15:56:21 -0400 Subject: [PATCH 071/109] Defer calculation of animation start time until audio is preloaded --- crates/ui/src/windows/animations/frame_edit.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 3e9e0a3f..9101e644 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -100,10 +100,16 @@ pub fn show_frame_edit( // Handle playing of animations if let Some(animation_state) = &mut state.animation_state { + let time = ui.input(|i| i.time); + + if animation_state.start_time.is_nan() { + animation_state.start_time = time; + } + // Determine what frame in the animation we're at by using the egui time and the // framerate let previous_frame_index = state.frame_index; - let time_diff = ui.input(|i| i.time) - animation_state.start_time; + let time_diff = time - animation_state.start_time; state.frame_index = (time_diff * 20.) as usize; if state.frame_index != previous_frame_index { @@ -255,7 +261,7 @@ pub fn show_frame_edit( } else { state.animation_state = Some(super::AnimationState { saved_frame_index: state.frame_index, - start_time: ui.input(|i| i.time), + start_time: f64::NAN, timing_index: 0, audio_data: Default::default(), }); From 38dea6431b3dd448f722a77551a816a25ce88df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sun, 4 Aug 2024 19:27:38 -0400 Subject: [PATCH 072/109] Refactor animation editor flash map code --- crates/data/src/rmxp/animation.rs | 2 +- crates/ui/src/windows/animations/timing.rs | 139 ++++----- crates/ui/src/windows/animations/util.rs | 124 +++++++- crates/ui/src/windows/animations/window.rs | 319 ++------------------- 4 files changed, 183 insertions(+), 401 deletions(-) diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index 86f7f24c..ac541895 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -36,7 +36,7 @@ pub struct Animation { pub timings: Vec, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] #[derive(alox_48::Deserialize, alox_48::Serialize)] #[marshal(class = "RPG::Animation::Timing")] pub struct Timing { diff --git a/crates/ui/src/windows/animations/timing.rs b/crates/ui/src/windows/animations/timing.rs index 9bd011a1..e13106c3 100644 --- a/crates/ui/src/windows/animations/timing.rs +++ b/crates/ui/src/windows/animations/timing.rs @@ -25,22 +25,9 @@ use egui::Widget; use luminol_core::Modal; -use super::{ - util::{ColorFlash, HideFlash}, - TimingEditState, -}; +use super::{util::update_flash_maps, TimingEditState}; use luminol_data::rpg::animation::{Condition, Scope, Timing}; -fn apply_with_condition(condition: Condition, mut closure: impl FnMut(Condition)) { - closure(Condition::None); - if condition != Condition::Miss { - closure(Condition::Hit); - } - if condition != Condition::Hit { - closure(Condition::Miss); - } -} - pub fn show_timing_header(ui: &mut egui::Ui, timing: &Timing) { let mut vec = Vec::with_capacity(3); @@ -104,11 +91,7 @@ pub fn show_timing_body( .iter() .rev() .take_while(|t| t.frame == frame) - .filter(|t| match condition { - Condition::None => true, - Condition::Hit => t.condition != Condition::Miss, - Condition::Miss => t.condition != Condition::Hit, - } && t.flash_scope == scope) + .filter(|t| t.flash_scope == scope && super::util::filter_timing(t, condition)) .count() }; @@ -130,19 +113,19 @@ pub fn show_timing_body( if old_condition != Condition::Miss && timing.condition == Condition::Miss { match timing.flash_scope { Scope::Target => { - flash_maps.hit_target.remove( + flash_maps.target_mut(Condition::Hit).remove( timing.frame, rank(Condition::Hit, timing.frame, Scope::Target), ); } Scope::Screen => { - flash_maps.hit_screen.remove( + flash_maps.screen_mut(Condition::Hit).remove( timing.frame, rank(Condition::Hit, timing.frame, Scope::Screen), ); } Scope::HideTarget => { - flash_maps.hit_hide.remove( + flash_maps.hide_mut(Condition::Hit).remove( timing.frame, rank(Condition::Hit, timing.frame, Scope::HideTarget), ); @@ -154,19 +137,19 @@ pub fn show_timing_body( { match timing.flash_scope { Scope::Target => { - flash_maps.miss_target.remove( + flash_maps.target_mut(Condition::Miss).remove( timing.frame, rank(Condition::Miss, timing.frame, Scope::Target), ); } Scope::Screen => { - flash_maps.miss_screen.remove( + flash_maps.screen_mut(Condition::Miss).remove( timing.frame, rank(Condition::Miss, timing.frame, Scope::Screen), ); } Scope::HideTarget => { - flash_maps.miss_hide.remove( + flash_maps.hide_mut(Condition::Miss).remove( timing.frame, rank(Condition::Miss, timing.frame, Scope::HideTarget), ); @@ -177,32 +160,24 @@ pub fn show_timing_body( if old_condition == Condition::Miss && timing.condition != Condition::Miss { match timing.flash_scope { Scope::Target => { - flash_maps.hit_target.insert( + flash_maps.target_mut(Condition::Hit).insert( timing.frame, rank(Condition::Hit, timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::Screen => { - flash_maps.hit_screen.insert( + flash_maps.screen_mut(Condition::Hit).insert( timing.frame, rank(Condition::Hit, timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::HideTarget => { - flash_maps.hit_hide.insert( + flash_maps.hide_mut(Condition::Hit).insert( timing.frame, rank(Condition::Hit, timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::None => {} @@ -212,32 +187,24 @@ pub fn show_timing_body( { match timing.flash_scope { Scope::Target => { - flash_maps.miss_target.insert( + flash_maps.target_mut(Condition::Miss).insert( timing.frame, rank(Condition::Miss, timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::Screen => { - flash_maps.miss_screen.insert( + flash_maps.screen_mut(Condition::Miss).insert( timing.frame, rank(Condition::Miss, timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::HideTarget => { - flash_maps.miss_hide.insert( + flash_maps.hide_mut(Condition::Miss).insert( timing.frame, rank(Condition::Miss, timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::None => {} @@ -269,31 +236,29 @@ pub fn show_timing_body( )) .changed(); if changed { - apply_with_condition(timing.condition, |condition| { - match timing.flash_scope { - Scope::Target => { - flash_maps.target_mut(condition).set_frame( - old_frame, - rank(condition, old_frame, Scope::Target), - timing.frame, - ); - } - Scope::Screen => { - flash_maps.screen_mut(condition).set_frame( - old_frame, - rank(condition, old_frame, Scope::Screen), - timing.frame, - ); - } - Scope::HideTarget => { - flash_maps.hide_mut(condition).set_frame( - old_frame, - rank(condition, old_frame, Scope::HideTarget), - timing.frame, - ); - } - Scope::None => {} + update_flash_maps(timing.condition, |condition| match timing.flash_scope { + Scope::Target => { + flash_maps.target_mut(condition).set_frame( + old_frame, + rank(condition, old_frame, Scope::Target), + timing.frame, + ); + } + Scope::Screen => { + flash_maps.screen_mut(condition).set_frame( + old_frame, + rank(condition, old_frame, Scope::Screen), + timing.frame, + ); + } + Scope::HideTarget => { + flash_maps.hide_mut(condition).set_frame( + old_frame, + rank(condition, old_frame, Scope::HideTarget), + timing.frame, + ); } + Scope::None => {} }); modified = true; } @@ -344,7 +309,7 @@ pub fn show_timing_body( }; if scope_changed { - apply_with_condition(timing.condition, |condition| { + update_flash_maps(timing.condition, |condition| { match old_scope { Scope::Target => { flash_maps @@ -369,29 +334,21 @@ pub fn show_timing_body( flash_maps.target_mut(condition).insert( timing.frame, rank(condition, timing.frame, Scope::Target), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::Screen => { flash_maps.screen_mut(condition).insert( timing.frame, rank(condition, timing.frame, Scope::Screen), - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::HideTarget => { flash_maps.hide_mut(condition).insert( timing.frame, rank(condition, timing.frame, Scope::HideTarget), - HideFlash { - duration: timing.flash_duration, - }, + timing.into(), ); } Scope::None => {} @@ -401,7 +358,7 @@ pub fn show_timing_body( } if duration_changed { - apply_with_condition(timing.condition, |condition| match timing.flash_scope { + update_flash_maps(timing.condition, |condition| match timing.flash_scope { Scope::Target => { flash_maps .target_mut(condition) @@ -455,7 +412,7 @@ pub fn show_timing_body( )) .changed(); if changed { - apply_with_condition(timing.condition, |condition| match timing.flash_scope { + update_flash_maps(timing.condition, |condition| match timing.flash_scope { Scope::Target => { flash_maps .target_mut(condition) diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs index cb6fe6a8..ed6b711b 100644 --- a/crates/ui/src/windows/animations/util.rs +++ b/crates/ui/src/windows/animations/util.rs @@ -24,22 +24,36 @@ use luminol_filesystem::FileSystem; -use luminol_data::rpg::animation::Condition; +use luminol_data::rpg::animation::{Condition, Scope, Timing}; #[derive(Debug, Default)] pub struct FlashMaps { - pub none_hide: FlashMap, - pub hit_hide: FlashMap, - pub miss_hide: FlashMap, - pub none_target: FlashMap, - pub hit_target: FlashMap, - pub miss_target: FlashMap, - pub none_screen: FlashMap, - pub hit_screen: FlashMap, - pub miss_screen: FlashMap, + none_hide: FlashMap, + hit_hide: FlashMap, + miss_hide: FlashMap, + none_target: FlashMap, + hit_target: FlashMap, + miss_target: FlashMap, + none_screen: FlashMap, + hit_screen: FlashMap, + miss_screen: FlashMap, } impl FlashMaps { + pub fn new(timings: &[Timing]) -> Self { + Self { + none_hide: >::new(timings, Condition::None, Scope::HideTarget), + hit_hide: >::new(timings, Condition::Hit, Scope::HideTarget), + miss_hide: >::new(timings, Condition::Miss, Scope::HideTarget), + none_target: >::new(timings, Condition::None, Scope::Target), + hit_target: >::new(timings, Condition::Hit, Scope::Target), + miss_target: >::new(timings, Condition::Miss, Scope::Target), + none_screen: >::new(timings, Condition::None, Scope::Screen), + hit_screen: >::new(timings, Condition::Hit, Scope::Screen), + miss_screen: >::new(timings, Condition::Miss, Scope::Screen), + } + } + pub fn target(&self, condition: Condition) -> &FlashMap { match condition { Condition::None => &self.none_target, @@ -100,6 +114,47 @@ pub struct HideFlash { pub duration: usize, } +impl<'a> From<&'a Timing> for ColorFlash { + fn from(timing: &'a Timing) -> Self { + Self { + color: timing.flash_color, + duration: timing.flash_duration, + } + } +} + +impl<'a> From<&'a mut Timing> for ColorFlash { + fn from(timing: &'a mut Timing) -> Self { + (&*timing).into() + } +} + +impl From for ColorFlash { + fn from(timing: Timing) -> Self { + (&timing).into() + } +} + +impl<'a> From<&'a Timing> for HideFlash { + fn from(timing: &'a Timing) -> Self { + Self { + duration: timing.flash_duration, + } + } +} + +impl<'a> From<&'a mut Timing> for HideFlash { + fn from(timing: &'a mut Timing) -> Self { + (&*timing).into() + } +} + +impl From for HideFlash { + fn from(timing: Timing) -> Self { + (&timing).into() + } +} + #[derive(Debug)] pub struct FlashMap { map: std::collections::BTreeMap>, @@ -185,6 +240,14 @@ where } impl FlashMap { + fn new(timings: &[Timing], condition: Condition, scope: Scope) -> Self { + timings + .iter() + .filter(|timing| timing.flash_scope == scope && filter_timing(timing, condition)) + .map(|timing| (timing.frame, timing.into())) + .collect() + } + /// Determines what color the flash should be for a given frame number. pub fn compute(&self, frame: usize) -> luminol_data::Color { let Some((&start_frame, deque)) = self.map.range(..=frame).next_back() else { @@ -216,6 +279,14 @@ impl FlashMap { } impl FlashMap { + fn new(timings: &[Timing], condition: Condition, scope: Scope) -> Self { + timings + .iter() + .filter(|timing| timing.flash_scope == scope && filter_timing(timing, condition)) + .map(|timing| (timing.frame, timing.into())) + .collect() + } + /// Determines if the hide flash is active for a given frame number. pub fn compute(&self, frame: usize) -> bool { let Some((&start_frame, deque)) = self.map.range(..=frame).next_back() else { @@ -261,22 +332,27 @@ pub fn log_atlas_error( ); } +/// If the given timing has a sound effect and the given timing should be shown based on the given +/// condition, caches the audio data for that sound effect into `animation_state.audio_data`. pub fn load_se( update_state: &mut luminol_core::UpdateState<'_>, animation_state: &mut super::AnimationState, condition: Condition, timing: &luminol_data::rpg::animation::Timing, ) { + // Do nothing if this timing has no sound effect let Some(se_name) = &timing.se.name else { return; }; - if (condition != timing.condition - && condition != Condition::None - && timing.condition != Condition::None) + + // Do nothing if the timing shouldn't be shown based on the condition currently selected in the + // UI or if the timing's sound effect has already been loaded + if !filter_timing(timing, condition) || animation_state.audio_data.contains_key(se_name.as_str()) { return; } + match update_state.filesystem.read(format!("Audio/SE/{se_name}")) { Ok(data) => { animation_state @@ -325,3 +401,25 @@ pub fn resize_frame(frame: &mut luminol_data::rpg::animation::Frame, new_cell_ma frame.cell_max = new_cell_max; } + +/// Determines whether or not a timing should be used based on the given condition. +pub fn filter_timing(timing: &Timing, condition: Condition) -> bool { + match condition { + Condition::None => true, + Condition::Hit => timing.condition != Condition::Miss, + Condition::Miss => timing.condition != Condition::Hit, + } +} + +/// Helper function for updating `FlashMaps` when a flash is updated. Given the condition of a +/// flash, this calls the given closure once for the condition of each flash map that must be +/// updated. +pub fn update_flash_maps(condition: Condition, mut closure: impl FnMut(Condition)) { + closure(Condition::None); + if condition != Condition::Miss { + closure(Condition::Hit); + } + if condition != Condition::Hit { + closure(Condition::Miss); + } +} diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index 66f3573a..baeb4634 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -24,8 +24,8 @@ use luminol_components::UiExt; -use super::util::{ColorFlash, HideFlash}; -use luminol_data::rpg::animation::{Condition, Scope}; +use super::util::update_flash_maps; +use luminol_data::rpg::animation::Scope; impl luminol_core::Window for super::Window { fn id(&self) -> egui::Id { @@ -83,152 +83,9 @@ impl luminol_core::Window for super::Window { }) { animation.timings.sort_by_key(|timing| timing.frame); } - self.frame_edit_state.flash_maps.insert( - id, - super::util::FlashMaps { - none_hide: animation - .timings - .iter() - .filter(|timing| timing.flash_scope == Scope::HideTarget) - .map(|timing| { - ( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ) - }) - .collect(), - hit_hide: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Miss - && timing.flash_scope == Scope::HideTarget - }) - .map(|timing| { - ( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ) - }) - .collect(), - miss_hide: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Hit - && timing.flash_scope == Scope::HideTarget - }) - .map(|timing| { - ( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ) - }) - .collect(), - none_target: animation - .timings - .iter() - .filter(|timing| timing.flash_scope == Scope::Target) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - hit_target: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Miss - && timing.flash_scope == Scope::Target - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - miss_target: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Hit - && timing.flash_scope == Scope::Target - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - none_screen: animation - .timings - .iter() - .filter(|timing| timing.flash_scope == Scope::Screen) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - hit_screen: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Miss - && timing.flash_scope == Scope::Screen - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - miss_screen: animation - .timings - .iter() - .filter(|timing| { - timing.condition != Condition::Hit - && timing.flash_scope == Scope::Screen - }) - .map(|timing| { - ( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ) - }) - .collect(), - }, - ); + self.frame_edit_state + .flash_maps + .insert(id, super::util::FlashMaps::new(&animation.timings)); } ui.with_padded_stripe(false, |ui| { @@ -466,97 +323,26 @@ impl luminol_core::Window for super::Window { if let Some(i) = collapsing_view_inner.created_entry { let timing = &animation.timings[i]; - match timing.flash_scope { - Scope::Target => { - flash_maps.none_target.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.none_screen.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.none_hide.append( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { + update_flash_maps(timing.condition, |condition| { match timing.flash_scope { Scope::Target => { - flash_maps.hit_target.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::Screen => { - flash_maps.hit_screen.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); - } - Scope::HideTarget => { - flash_maps.hit_hide.append( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ); - } - Scope::None => {} - } - } - if timing.condition != Condition::Hit { - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); + flash_maps + .target_mut(condition) + .append(timing.frame, timing.into()); } Scope::Screen => { - flash_maps.miss_screen.append( - timing.frame, - ColorFlash { - color: timing.flash_color, - duration: timing.flash_duration, - }, - ); + flash_maps + .screen_mut(condition) + .append(timing.frame, timing.into()); } Scope::HideTarget => { - flash_maps.miss_hide.append( - timing.frame, - HideFlash { - duration: timing.flash_duration, - }, - ); + flash_maps + .hide_mut(condition) + .append(timing.frame, timing.into()); } Scope::None => {} } - } + }); self.frame_edit_state .frame_view .as_mut() @@ -580,99 +366,40 @@ impl luminol_core::Window for super::Window { } if let Some((i, timing)) = collapsing_view_inner.deleted_entry { - let rank = |frame, scope| { - animation.timings[..i] - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| t.flash_scope == scope) - .count() - }; - match timing.flash_scope { - Scope::Target => { - flash_maps - .none_target - .remove(timing.frame, rank(timing.frame, Scope::Target)); - } - Scope::Screen => { - flash_maps - .none_screen - .remove(timing.frame, rank(timing.frame, Scope::Screen)); - } - Scope::HideTarget => { - flash_maps.none_hide.remove( - timing.frame, - rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - if timing.condition != Condition::Miss { + update_flash_maps(timing.condition, |condition| { let rank = |frame, scope| { animation.timings[..i] .iter() .rev() .take_while(|t| t.frame == frame) .filter(|t| { - t.condition != Condition::Miss && t.flash_scope == scope + t.flash_scope == scope + && super::util::filter_timing(t, condition) }) .count() }; match timing.flash_scope { Scope::Target => { - flash_maps.hit_target.remove( + flash_maps.target_mut(condition).remove( timing.frame, rank(timing.frame, Scope::Target), ); } Scope::Screen => { - flash_maps.hit_screen.remove( + flash_maps.screen_mut(condition).remove( timing.frame, rank(timing.frame, Scope::Screen), ); } Scope::HideTarget => { - flash_maps.hit_hide.remove( + flash_maps.hide_mut(condition).remove( timing.frame, rank(timing.frame, Scope::HideTarget), ); } Scope::None => {} } - } - if timing.condition != Condition::Hit { - let rank = |frame, scope| { - animation.timings[..i] - .iter() - .rev() - .take_while(|t| t.frame == frame) - .filter(|t| { - t.condition != Condition::Hit && t.flash_scope == scope - }) - .count() - }; - match timing.flash_scope { - Scope::Target => { - flash_maps.miss_target.remove( - timing.frame, - rank(timing.frame, Scope::Target), - ); - } - Scope::Screen => { - flash_maps.miss_screen.remove( - timing.frame, - rank(timing.frame, Scope::Screen), - ); - } - Scope::HideTarget => { - flash_maps.miss_hide.remove( - timing.frame, - rank(timing.frame, Scope::HideTarget), - ); - } - Scope::None => {} - } - } + }); self.frame_edit_state .frame_view From 13551ea7004d523a3665068a13e592f8472d6da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 5 Aug 2024 00:47:07 -0400 Subject: [PATCH 073/109] Add FPS setting to animation editor --- .../ui/src/windows/animations/frame_edit.rs | 23 +++++++++++++++++-- crates/ui/src/windows/animations/mod.rs | 2 ++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 9101e644..6e3150d5 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -110,7 +110,7 @@ pub fn show_frame_edit( // framerate let previous_frame_index = state.frame_index; let time_diff = time - animation_state.start_time; - state.frame_index = (time_diff * 20.) as usize; + state.frame_index = (time_diff * state.animation_fps) as usize; if state.frame_index != previous_frame_index { recompute_flash = true; @@ -160,7 +160,7 @@ pub fn show_frame_edit( } // Request a repaint every few frames - let frame_delay = 1. / 20.; // 20 FPS + let frame_delay = state.animation_fps.recip(); ui.ctx() .request_repaint_after(std::time::Duration::from_secs_f64( frame_delay - time_diff.rem_euclid(frame_delay), @@ -209,6 +209,25 @@ pub fn show_frame_edit( egui::Checkbox::without_text(&mut state.enable_onion_skin), )); + let old_fps = state.animation_fps; + let changed = ui + .add(luminol_components::Field::new( + "FPS", + egui::DragValue::new(&mut state.animation_fps).range(0.1..=f64::MAX), + )) + .changed(); + if changed { + // If the animation is playing, recalculate the start time so that the + // animation playback progress stays the same with the new FPS + if let Some(animation_state) = &mut state.animation_state { + if animation_state.start_time.is_finite() { + let time = ui.input(|i| i.time); + let diff = animation_state.start_time - time; + animation_state.start_time = time + diff * (old_fps / state.animation_fps); + } + } + } + ui.with_layout( egui::Layout { main_dir: egui::Direction::RightToLeft, diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs index 87ecd1c4..a4b03c44 100644 --- a/crates/ui/src/windows/animations/mod.rs +++ b/crates/ui/src/windows/animations/mod.rs @@ -41,6 +41,7 @@ pub struct Window { } struct FrameEditState { + animation_fps: f64, frame_index: usize, condition: luminol_data::rpg::animation::Condition, enable_onion_skin: bool, @@ -86,6 +87,7 @@ impl Default for Window { previous_animation: None, previous_battler_name: None, frame_edit_state: FrameEditState { + animation_fps: 20., frame_index: 0, condition: luminol_data::rpg::animation::Condition::Hit, enable_onion_skin: false, From 455a277b2fb769f166b2b887d3d88cb2790e341e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 5 Aug 2024 01:37:39 -0400 Subject: [PATCH 074/109] Add a fudge factor to animation repaint delay This fixes an issue where when playing an animation in the animation editor, sometimes the app repaints twice per animation frame instead of once per animation frame as intended. --- crates/ui/src/windows/animations/frame_edit.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 6e3150d5..cd30c86c 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -159,11 +159,14 @@ pub fn show_frame_edit( animation_state.timing_index = animation.timings.len(); } - // Request a repaint every few frames + // Request a repaint every few frames (a small fudge factor is added because otherwise, + // rounding errors may cause egui to request a repaint after too little time, and that + // would lead to the app being unnecessarily repainted multiple times per animation frame) let frame_delay = state.animation_fps.recip(); + let fudge_factor = ui.input(|i| i.predicted_dt) as f64; ui.ctx() .request_repaint_after(std::time::Duration::from_secs_f64( - frame_delay - time_diff.rem_euclid(frame_delay), + frame_delay - time_diff.rem_euclid(frame_delay) + fudge_factor, )); } if state.frame_index >= animation.frames.len() { From 9e41a28380752e462d64e78708482c37b330e934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 5 Aug 2024 12:37:20 -0400 Subject: [PATCH 075/109] Revert "Add a fudge factor to animation repaint delay" This reverts commit 455a277b2fb769f166b2b887d3d88cb2790e341e. That workaround was causing problems at higher framerates, so I decided it wasn't worth it. --- crates/ui/src/windows/animations/frame_edit.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index cd30c86c..6e3150d5 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -159,14 +159,11 @@ pub fn show_frame_edit( animation_state.timing_index = animation.timings.len(); } - // Request a repaint every few frames (a small fudge factor is added because otherwise, - // rounding errors may cause egui to request a repaint after too little time, and that - // would lead to the app being unnecessarily repainted multiple times per animation frame) + // Request a repaint every few frames let frame_delay = state.animation_fps.recip(); - let fudge_factor = ui.input(|i| i.predicted_dt) as f64; ui.ctx() .request_repaint_after(std::time::Duration::from_secs_f64( - frame_delay - time_diff.rem_euclid(frame_delay) + fudge_factor, + frame_delay - time_diff.rem_euclid(frame_delay), )); } if state.frame_index >= animation.frames.len() { From d5168cc9008ab992d89eea26aee4fbd7e0a9c07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 5 Aug 2024 16:58:59 -0400 Subject: [PATCH 076/109] Fix the scale that `luminol_audio::Audio` uses for volume --- crates/audio/src/native.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs index ceb519f6..322b5c46 100644 --- a/crates/audio/src/native.rs +++ b/crates/audio/src/native.rs @@ -104,7 +104,8 @@ impl Audio { // Set pitch and volume sink.set_speed(f32::from(pitch) / 100.); - sink.set_volume(f32::from(volume) / 100.); + let raw_volume = f32::from(volume) / 100.; + sink.set_volume(raw_volume * raw_volume); // Play sound. sink.play(); @@ -134,7 +135,8 @@ impl Audio { pub fn set_volume(&self, volume: u8, source: Source) { let mut inner = self.inner.lock(); if let Some(s) = inner.sinks.get_mut(&source) { - s.set_volume(f32::from(volume) / 100.); + let raw_volume = f32::from(volume) / 100.; + s.set_volume(raw_volume * raw_volume); } } From 1e90f9b11136407a5548fdc72bce57c47c571bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 6 Aug 2024 09:22:46 -0400 Subject: [PATCH 077/109] Fix the volume scale for real this time --- crates/audio/src/native.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs index 322b5c46..103a6168 100644 --- a/crates/audio/src/native.rs +++ b/crates/audio/src/native.rs @@ -103,9 +103,13 @@ impl Audio { } // Set pitch and volume - sink.set_speed(f32::from(pitch) / 100.); - let raw_volume = f32::from(volume) / 100.; - sink.set_volume(raw_volume * raw_volume); + sink.set_speed(pitch as f32 / 100.); + sink.set_volume(if volume == 0 { + 0. + } else { + // -0.35 dB per percent below 100% volume + 10f32.powf(-(0.35 / 20.) * (100 - volume.min(100)) as f32) + }); // Play sound. sink.play(); @@ -135,8 +139,12 @@ impl Audio { pub fn set_volume(&self, volume: u8, source: Source) { let mut inner = self.inner.lock(); if let Some(s) = inner.sinks.get_mut(&source) { - let raw_volume = f32::from(volume) / 100.; - s.set_volume(raw_volume * raw_volume); + s.set_volume(if volume == 0 { + 0. + } else { + // -0.35 dB per percent below 100% volume + 10f32.powf(-(0.35 / 20.) * (100 - volume.min(100)) as f32) + }); } } From ab20845aef2a14fb4d794104571a9e8c52aec232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 6 Aug 2024 22:38:33 -0400 Subject: [PATCH 078/109] Make the volume scale configurable in the project config --- Cargo.lock | 1 + crates/audio/Cargo.toml | 1 + crates/audio/src/lib.rs | 2 + crates/audio/src/native.rs | 47 ++++++++++++------- crates/audio/src/wrapper.rs | 20 ++++++-- crates/components/src/sound_tab.rs | 29 +++++++++--- crates/config/src/lib.rs | 12 +++++ crates/config/src/project.rs | 4 +- crates/term/src/widget/mod.rs | 9 +++- .../ui/src/windows/animations/frame_edit.rs | 6 +++ crates/ui/src/windows/config_window.rs | 16 ++++++- 11 files changed, 116 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0206e1d3..9f329bf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3063,6 +3063,7 @@ dependencies = [ "color-eyre", "flume", "fragile", + "luminol-config", "luminol-filesystem", "once_cell", "oneshot", diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 84a47978..1468fb9d 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -27,6 +27,7 @@ once_cell.workspace = true color-eyre.workspace = true thiserror.workspace = true +luminol-config.workspace = true luminol-filesystem.workspace = true fragile.workspace = true diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index e3610a66..aab9846e 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -26,6 +26,8 @@ mod error; mod midi; pub use error::{Error, Result}; +pub use luminol_config::VolumeScale; + mod native; #[cfg(target_arch = "wasm32")] mod wrapper; diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs index 103a6168..3ebe7c01 100644 --- a/crates/audio/src/native.rs +++ b/crates/audio/src/native.rs @@ -1,4 +1,4 @@ -use crate::{midi, Result, Source}; +use crate::{midi, Result, Source, VolumeScale}; /// A struct for playing Audio. pub struct Audio { @@ -28,6 +28,21 @@ impl Default for Audio { } } +fn apply_scale(volume: u8, scale: VolumeScale) -> f32 { + let volume = volume.min(100); + match scale { + VolumeScale::Linear => volume as f32 / 100., + VolumeScale::Db35 => { + if volume == 0 { + 0. + } else { + // -0.35 dB per percent below 100% volume + 10f32.powf(-(0.35 / 20.) * (100 - volume) as f32) + } + } + } +} + impl Audio { #[cfg(not(target_arch = "wasm32"))] pub fn new() -> Self { @@ -43,6 +58,7 @@ impl Audio { volume: u8, pitch: u8, source: Option, + scale: VolumeScale, ) -> Result<()> where T: luminol_filesystem::FileSystem, @@ -55,7 +71,7 @@ impl Audio { .extension() .is_some_and(|e| matches!(e, "mid" | "midi")); - self.play_from_file(file, is_midi, volume, pitch, source) + self.play_from_file(file, is_midi, volume, pitch, source, scale) } /// Play a sound on a source from audio file data. @@ -66,8 +82,16 @@ impl Audio { volume: u8, pitch: u8, source: Option, + scale: VolumeScale, ) -> Result<()> { - self.play_from_file(std::io::Cursor::new(slice), is_midi, volume, pitch, source) + self.play_from_file( + std::io::Cursor::new(slice), + is_midi, + volume, + pitch, + source, + scale, + ) } fn play_from_file( @@ -77,6 +101,7 @@ impl Audio { volume: u8, pitch: u8, source: Option, + scale: VolumeScale, ) -> Result<()> { let mut inner = self.inner.lock(); // Create a sink @@ -104,12 +129,7 @@ impl Audio { // Set pitch and volume sink.set_speed(pitch as f32 / 100.); - sink.set_volume(if volume == 0 { - 0. - } else { - // -0.35 dB per percent below 100% volume - 10f32.powf(-(0.35 / 20.) * (100 - volume.min(100)) as f32) - }); + sink.set_volume(apply_scale(volume, scale)); // Play sound. sink.play(); @@ -136,15 +156,10 @@ impl Audio { } /// Set the volume of a source. - pub fn set_volume(&self, volume: u8, source: Source) { + pub fn set_volume(&self, volume: u8, source: Source, scale: VolumeScale) { let mut inner = self.inner.lock(); if let Some(s) = inner.sinks.get_mut(&source) { - s.set_volume(if volume == 0 { - 0. - } else { - // -0.35 dB per percent below 100% volume - 10f32.powf(-(0.35 / 20.) * (100 - volume.min(100)) as f32) - }); + s.set_volume(apply_scale(volume, scale)); } } diff --git a/crates/audio/src/wrapper.rs b/crates/audio/src/wrapper.rs index 103c8d26..f9b13cda 100644 --- a/crates/audio/src/wrapper.rs +++ b/crates/audio/src/wrapper.rs @@ -22,7 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use crate::{native::Audio as NativeAudio, Result, Source}; +use crate::{native::Audio as NativeAudio, Result, Source, VolumeScale}; /// A struct for playing Audio. pub struct Audio { @@ -36,6 +36,7 @@ enum Command { volume: u8, pitch: u8, source: Option, + scale: VolumeScale, oneshot_tx: oneshot::Sender>, }, SetPitch { @@ -46,6 +47,7 @@ enum Command { SetVolume { volume: u8, source: Source, + scale: VolumeScale, oneshot_tx: oneshot::Sender<()>, }, ClearSinks { @@ -71,6 +73,7 @@ impl Audio { volume: u8, pitch: u8, source: Option, + scale: VolumeScale, ) -> Result<()> { // We have to load the file on the current thread, // otherwise if we read the file in the main thread of a web browser @@ -82,7 +85,7 @@ impl Audio { .extension() .is_some_and(|e| matches!(e, "mid" | "midi")); - self.play_from_slice(slice, is_midi, volume, pitch, source) + self.play_from_slice(slice, is_midi, volume, pitch, source, scale) } /// Play a sound on a source from audio file data. @@ -93,6 +96,7 @@ impl Audio { volume: u8, pitch: u8, source: Option, + scale: VolumeScale, ) -> Result<()> { let (oneshot_tx, oneshot_rx) = oneshot::channel(); self.tx @@ -102,6 +106,7 @@ impl Audio { volume, pitch, source, + scale, oneshot_tx, }) .unwrap(); @@ -122,12 +127,13 @@ impl Audio { } /// Set the volume of a source. - pub fn set_volume(&self, volume: u8, source: Source) { + pub fn set_volume(&self, volume: u8, source: Source, scale: VolumeScale) { let (oneshot_tx, oneshot_rx) = oneshot::channel(); self.tx .send(Command::SetVolume { volume, source, + scale, oneshot_tx, }) .unwrap(); @@ -178,10 +184,13 @@ impl Default for Audio { volume, pitch, source, + scale, oneshot_tx, } => { oneshot_tx - .send(audio.play_from_slice(slice, is_midi, volume, pitch, source)) + .send( + audio.play_from_slice(slice, is_midi, volume, pitch, source, scale), + ) .unwrap(); } @@ -197,9 +206,10 @@ impl Default for Audio { Command::SetVolume { volume, source, + scale, oneshot_tx, } => { - audio.set_volume(volume, source); + audio.set_volume(volume, source, scale); oneshot_tx.send(()).unwrap(); } diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index c8032499..e4bccd72 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -62,11 +62,19 @@ impl SoundTab { let volume = self.audio_file.volume; let source = self.source; - if let Err(e) = + if let Err(e) = update_state.audio.play( + path, + update_state.filesystem, + volume, + pitch, + Some(source), update_state - .audio - .play(path, update_state.filesystem, volume, pitch, Some(source)) - { + .project_config + .as_ref() + .expect("project not loaded") + .project + .volume_scale, + ) { luminol_core::error!( update_state.toasts, e.wrap_err("Error playing from audio file") @@ -107,9 +115,16 @@ impl SoundTab { // Add a slider. // If it's changed, update the volume. if ui.add(slider).changed() { - update_state - .audio - .set_volume(self.audio_file.volume, self.source); + update_state.audio.set_volume( + self.audio_file.volume, + self.source, + update_state + .project_config + .as_ref() + .expect("project not loaded") + .project + .volume_scale, + ); }; let slider = egui::Slider::new(&mut self.audio_file.pitch, 50..=150) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index e872cb17..848f9261 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -80,6 +80,18 @@ pub enum RMVer { Ace = 3, } +#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(strum::EnumIter, strum::Display)] +#[allow(missing_docs)] +pub enum VolumeScale { + #[default] + #[strum(to_string = "-35 dB")] + Db35, + #[strum(to_string = "Linear")] + Linear, +} + #[derive(Clone, Copy, Hash, PartialEq, Debug)] #[derive(serde::Serialize, serde::Deserialize)] pub struct CodeTheme { diff --git a/crates/config/src/project.rs b/crates/config/src/project.rs index e3f1c035..ffe6f9b5 100644 --- a/crates/config/src/project.rs +++ b/crates/config/src/project.rs @@ -23,7 +23,7 @@ // Program grant you additional permission to convey the resulting work. use serde::{Deserialize, Serialize}; -use super::{command_db, DataFormat, RGSSVer, RMVer}; +use super::{command_db, DataFormat, RGSSVer, RMVer, VolumeScale}; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] @@ -43,6 +43,7 @@ pub struct Project { pub data_format: DataFormat, pub rgss_ver: RGSSVer, pub editor_ver: RMVer, + pub volume_scale: VolumeScale, pub playtest_exe: String, pub prefer_rgssad: bool, pub persistence_id: u64, @@ -56,6 +57,7 @@ impl Default for Project { data_format: DataFormat::Marshal, rgss_ver: RGSSVer::RGSS1, editor_ver: RMVer::XP, + volume_scale: VolumeScale::Db35, playtest_exe: "game".to_string(), prefer_rgssad: false, persistence_id: 0, diff --git a/crates/term/src/widget/mod.rs b/crates/term/src/widget/mod.rs index 8ba93c96..74f52423 100644 --- a/crates/term/src/widget/mod.rs +++ b/crates/term/src/widget/mod.rs @@ -269,7 +269,14 @@ where let bell = luminol_macros::include_asset!("assets/sounds/bell.wav"); update_state .audio - .play_from_slice(bell, false, 25, 100, Some(luminol_audio::Source::SE)) + .play_from_slice( + bell, + false, + 25, + 100, + Some(luminol_audio::Source::SE), + luminol_audio::VolumeScale::Linear, + ) .unwrap(); } _ => {} diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 6e3150d5..9021612d 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -143,6 +143,12 @@ pub fn show_frame_edit( timing.se.volume, timing.se.pitch, None, + update_state + .project_config + .as_ref() + .expect("project not loaded") + .project + .volume_scale, ) { luminol_core::error!( update_state.toasts, diff --git a/crates/ui/src/windows/config_window.rs b/crates/ui/src/windows/config_window.rs index 405ae163..7ac9db3a 100644 --- a/crates/ui/src/windows/config_window.rs +++ b/crates/ui/src/windows/config_window.rs @@ -22,8 +22,8 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use async_std::io::{ReadExt, WriteExt}; use egui::Widget; +use futures_lite::{AsyncReadExt, AsyncWriteExt}; use luminol_core::data_formats::Handler as FormatHandler; use luminol_data::rpg; use luminol_filesystem::{FileSystem, OpenFlags}; @@ -385,6 +385,20 @@ impl luminol_core::Window for Window { .changed(); } }); + + egui::ComboBox::from_label("Volume Scale") + .selected_text(config.project.volume_scale.to_string()) + .show_ui(ui, |ui| { + for scale in luminol_config::VolumeScale::iter() { + modified |= ui + .selectable_value( + &mut config.project.volume_scale, + scale, + scale.to_string(), + ) + .changed(); + } + }); }); ui.label("Game.ini settings"); From eb1523ec46881ee4d5f473d1190557b5f663afd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 01:39:12 -0400 Subject: [PATCH 079/109] Make animation editor larger by default and use radio buttons for position --- .../ui/src/windows/animations/frame_edit.rs | 1 + crates/ui/src/windows/animations/window.rs | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 9021612d..0018cea2 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -524,6 +524,7 @@ pub fn show_frame_edit( .resizable([false, true]) .min_width(ui.available_width()) .max_width(ui.available_width()) + .default_height(240.) .show(ui, |ui| { egui::Frame::dark_canvas(ui.style()) .show(ui, |ui| { diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index baeb4634..4f3feb56 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -23,6 +23,7 @@ // Program grant you additional permission to convey the resulting work. use luminol_components::UiExt; +use strum::IntoEnumIterator; use super::util::update_flash_maps; use luminol_data::rpg::animation::Scope; @@ -58,7 +59,7 @@ impl luminol_core::Window for super::Window { let response = egui::Window::new(name) .id(self.id()) - .default_width(500.) + .default_width(720.) .open(open) .show(ctx, |ui| { self.view.show( @@ -102,10 +103,22 @@ impl luminol_core::Window for super::Window { let changed = ui .add(luminol_components::Field::new( "Battler Position", - luminol_components::EnumComboBox::new( - (animation.id, "position"), - &mut animation.position, - ), + |ui: &mut egui::Ui| { + let mut modified = false; + let mut response = egui::Frame::none().show(ui, |ui| { + ui.columns(luminol_data::rpg::animation::Position::iter().count(), |columns| { + for (i, position) in luminol_data::rpg::animation::Position::iter().enumerate() { + if columns[i].radio_value(&mut animation.position, position, position.to_string()).changed() { + modified = true; + } + } + }); + }).response; + if modified { + response.mark_changed(); + } + response + } )) .changed(); if changed { From eaf4c6cdbc58f2ddef39667f9c839ea011eaac26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 02:01:50 -0400 Subject: [PATCH 080/109] Fix closing the app window not working when timing edit is visible --- crates/ui/src/windows/animations/timing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui/src/windows/animations/timing.rs b/crates/ui/src/windows/animations/timing.rs index e13106c3..aaf9869d 100644 --- a/crates/ui/src/windows/animations/timing.rs +++ b/crates/ui/src/windows/animations/timing.rs @@ -226,7 +226,7 @@ pub fn show_timing_body( response.changed = false; if response.dragged() { state.previous_frame = Some(frame); - } else { + } else if state.previous_frame.is_some() { timing.frame = frame - 1; state.previous_frame = None; response.changed = true; From b4f92e356e7d3d70956f80e26806731a0c20ee53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 15:37:32 -0400 Subject: [PATCH 081/109] Fix formatting of crates/ui/src/windows/animations/window.rs --- crates/ui/src/windows/animations/window.rs | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index 4f3feb56..1b749f11 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -26,7 +26,7 @@ use luminol_components::UiExt; use strum::IntoEnumIterator; use super::util::update_flash_maps; -use luminol_data::rpg::animation::Scope; +use luminol_data::rpg::animation::{Position, Scope}; impl luminol_core::Window for super::Window { fn id(&self) -> egui::Id { @@ -105,20 +105,31 @@ impl luminol_core::Window for super::Window { "Battler Position", |ui: &mut egui::Ui| { let mut modified = false; - let mut response = egui::Frame::none().show(ui, |ui| { - ui.columns(luminol_data::rpg::animation::Position::iter().count(), |columns| { - for (i, position) in luminol_data::rpg::animation::Position::iter().enumerate() { - if columns[i].radio_value(&mut animation.position, position, position.to_string()).changed() { - modified = true; + let mut response = egui::Frame::none() + .show(ui, |ui| { + ui.columns(Position::iter().count(), |columns| { + for (i, position) in + Position::iter().enumerate() + { + if columns[i] + .radio_value( + &mut animation.position, + position, + position.to_string(), + ) + .changed() + { + modified = true; + } } - } - }); - }).response; + }); + }) + .response; if modified { response.mark_changed(); } response - } + }, )) .changed(); if changed { From 944e5a0184c500814c94f5f4183a3a0c5203775a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 15:51:53 -0400 Subject: [PATCH 082/109] Remove useless error handling for atlas texture loading --- crates/components/src/tilepicker.rs | 8 +++--- crates/graphics/src/loaders/atlas.rs | 28 ++++++++----------- crates/graphics/src/map.rs | 2 +- crates/graphics/src/tilepicker.rs | 8 +++--- crates/modals/src/graphic_picker/event.rs | 13 ++++----- crates/ui/src/tabs/map/mod.rs | 2 +- .../ui/src/windows/animations/frame_edit.rs | 10 ++----- crates/ui/src/windows/animations/util.rs | 16 ----------- crates/ui/src/windows/animations/window.rs | 18 ++---------- 9 files changed, 32 insertions(+), 73 deletions(-) diff --git a/crates/components/src/tilepicker.rs b/crates/components/src/tilepicker.rs index 4071568e..f04cc85a 100644 --- a/crates/components/src/tilepicker.rs +++ b/crates/components/src/tilepicker.rs @@ -66,7 +66,7 @@ impl Tilepicker { pub fn new( update_state: &luminol_core::UpdateState<'_>, map_id: usize, // FIXME - ) -> color_eyre::Result { + ) -> Tilepicker { let map = update_state.data.get_or_load_map( map_id, update_state.filesystem, @@ -80,7 +80,7 @@ impl Tilepicker { tileset, update_state.filesystem, false, - )?; + ); let mut brush_seed = [0u8; 16]; brush_seed[0..8].copy_from_slice( @@ -94,7 +94,7 @@ impl Tilepicker { ); brush_seed[8..16].copy_from_slice(&(map_id as u64).to_le_bytes()); - Ok(Self { + Self { view, selected_tiles_left: 0, @@ -105,7 +105,7 @@ impl Tilepicker { drag_origin: None, brush_seed, brush_random: false, - }) + } } pub fn get_tile_from_offset( diff --git a/crates/graphics/src/loaders/atlas.rs b/crates/graphics/src/loaders/atlas.rs index 2ae9b1fe..c2174c9a 100644 --- a/crates/graphics/src/loaders/atlas.rs +++ b/crates/graphics/src/loaders/atlas.rs @@ -29,12 +29,11 @@ impl Loader { graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, tileset: &luminol_data::rpg::Tileset, - ) -> color_eyre::Result { - Ok(self - .atlases + ) -> Atlas { + self.atlases .entry(tileset.id) .or_insert_with(|| Atlas::new(graphics_state, filesystem, tileset)) - .clone()) + .clone() } pub fn load_animation_atlas( @@ -42,12 +41,11 @@ impl Loader { graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, animation: &luminol_data::rpg::Animation, - ) -> color_eyre::Result { - Ok(self - .animation_atlases + ) -> AnimationAtlas { + self.animation_atlases .entry(animation.id) .or_insert_with(|| AnimationAtlas::new(graphics_state, filesystem, animation)) - .clone()) + .clone() } pub fn reload_atlas( @@ -55,12 +53,11 @@ impl Loader { graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, tileset: &luminol_data::rpg::Tileset, - ) -> color_eyre::Result { - Ok(self - .atlases + ) -> Atlas { + self.atlases .entry(tileset.id) .insert(Atlas::new(graphics_state, filesystem, tileset)) - .clone()) + .clone() } pub fn reload_animation_atlas( @@ -68,12 +65,11 @@ impl Loader { graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, animation: &luminol_data::rpg::Animation, - ) -> color_eyre::Result { - Ok(self - .animation_atlases + ) -> AnimationAtlas { + self.animation_atlases .entry(animation.id) .insert(AnimationAtlas::new(graphics_state, filesystem, animation)) - .clone()) + .clone() } pub fn get_atlas(&self, id: usize) -> Option { diff --git a/crates/graphics/src/map.rs b/crates/graphics/src/map.rs index 55cdb05e..7dba42d1 100644 --- a/crates/graphics/src/map.rs +++ b/crates/graphics/src/map.rs @@ -52,7 +52,7 @@ impl Map { ) -> color_eyre::Result { let atlas = graphics_state .atlas_loader - .load_atlas(graphics_state, filesystem, tileset)?; + .load_atlas(graphics_state, filesystem, tileset); let viewport = Viewport::new( graphics_state, diff --git a/crates/graphics/src/tilepicker.rs b/crates/graphics/src/tilepicker.rs index 91010912..55e28950 100644 --- a/crates/graphics/src/tilepicker.rs +++ b/crates/graphics/src/tilepicker.rs @@ -49,10 +49,10 @@ impl Tilepicker { tileset: &luminol_data::rpg::Tileset, filesystem: &impl luminol_filesystem::FileSystem, exclude_autotiles: bool, - ) -> color_eyre::Result { + ) -> Self { let atlas = graphics_state .atlas_loader - .load_atlas(graphics_state, filesystem, tileset)?; + .load_atlas(graphics_state, filesystem, tileset); let tilepicker_data = if exclude_autotiles { (384..(atlas.tileset_height() as i16 / 32 * 8 + 384)).collect_vec() @@ -113,7 +113,7 @@ impl Tilepicker { &passages, ); - Ok(Self { + Self { tiles, collision, grid, @@ -124,7 +124,7 @@ impl Tilepicker { coll_enabled: false, grid_enabled: true, ani_time: None, - }) + } } pub fn update_animation(&mut self, render_state: &luminol_egui_wgpu::RenderState, time: f64) { diff --git a/crates/modals/src/graphic_picker/event.rs b/crates/modals/src/graphic_picker/event.rs index b8cd9ca5..f6e62184 100644 --- a/crates/modals/src/graphic_picker/event.rs +++ b/crates/modals/src/graphic_picker/event.rs @@ -126,7 +126,7 @@ impl luminol_core::Modal for Modal { if response.clicked() && !is_open { let selected = if let Some(tile_id) = data.tile_id { - let tilepicker = Self::load_tilepicker(update_state, self.tileset_id).unwrap(); // TODO handle + let tilepicker = Self::load_tilepicker(update_state, self.tileset_id); Selected::Tile { tile_id, @@ -211,10 +211,7 @@ impl Modal { }); } - fn load_tilepicker( - update_state: &UpdateState<'_>, - tileset_id: usize, - ) -> color_eyre::Result { + fn load_tilepicker(update_state: &UpdateState<'_>, tileset_id: usize) -> Tilepicker { let tilesets = update_state.data.tilesets(); let tileset = &tilesets.data[tileset_id]; @@ -223,10 +220,10 @@ impl Modal { tileset, update_state.filesystem, true, - )?; + ); tilepicker.tiles.auto_opacity = false; - Ok(tilepicker) + tilepicker } fn load_preview_sprite( @@ -340,7 +337,7 @@ impl Modal { ui.with_stripe(true, |ui| { let res = ui.selectable_label(checked, "(Tileset)"); if res.clicked() && !checked { - let tilepicker = Self::load_tilepicker(update_state, self.tileset_id).unwrap(); // TODO handle + let tilepicker = Self::load_tilepicker(update_state, self.tileset_id); *selected = Selected::Tile { tile_id: 384, tilepicker }; } }); diff --git a/crates/ui/src/tabs/map/mod.rs b/crates/ui/src/tabs/map/mod.rs index b53ca7cf..4a8f885e 100644 --- a/crates/ui/src/tabs/map/mod.rs +++ b/crates/ui/src/tabs/map/mod.rs @@ -124,7 +124,7 @@ impl Tab { // *sigh* // borrow checker. let view = luminol_components::MapView::new(update_state, id)?; - let tilepicker = luminol_components::Tilepicker::new(update_state, id)?; + let tilepicker = luminol_components::Tilepicker::new(update_state, id); let map = update_state.data.get_or_load_map( id, diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 0018cea2..2ce692fd 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -58,17 +58,11 @@ pub fn show_frame_edit( } else { None }; - let atlas = match update_state.graphics.atlas_loader.load_animation_atlas( + let atlas = update_state.graphics.atlas_loader.load_animation_atlas( &update_state.graphics, update_state.filesystem, animation, - ) { - Ok(atlas) => atlas, - Err(e) => { - super::util::log_atlas_error(update_state, animation, e); - return (modified, true); - } - }; + ); let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); frame_view.frame.battler_texture = battler_texture; frame_view.frame.update_battler( diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs index ed6b711b..897b8e80 100644 --- a/crates/ui/src/windows/animations/util.rs +++ b/crates/ui/src/windows/animations/util.rs @@ -316,22 +316,6 @@ pub fn log_battler_error( ); } -pub fn log_atlas_error( - update_state: &mut luminol_core::UpdateState<'_>, - animation: &luminol_data::rpg::Animation, - e: color_eyre::Report, -) { - luminol_core::error!( - update_state.toasts, - e.wrap_err(format!( - "While loading texture {:?} for animation {:0>4} {:?}", - animation.animation_name, - animation.id + 1, - animation.name, - )), - ); -} - /// If the given timing has a sound effect and the given timing should be shown based on the given /// condition, caches the audio data for that sound effect into `animation_state.audio_data`. pub fn load_se( diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index 1b749f11..885d6ca2 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -197,24 +197,12 @@ impl luminol_core::Window for super::Window { .frame_index .min(animation.frames.len().saturating_sub(1)); - let atlas = match update_state - .graphics - .atlas_loader - .load_animation_atlas( + let atlas = + update_state.graphics.atlas_loader.load_animation_atlas( &update_state.graphics, update_state.filesystem, animation, - ) { - Ok(atlas) => atlas, - Err(e) => { - super::util::log_atlas_error( - update_state, - animation, - e, - ); - return true; - } - }; + ); if let Some(frame_view) = &mut self.frame_edit_state.frame_view { From 05a7b4f640c3961c16b6118d10430e283b4462e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 16:50:35 -0400 Subject: [PATCH 083/109] Handle battler texture errors more gracefully --- .../ui/src/windows/animations/frame_edit.rs | 29 ++- crates/ui/src/windows/animations/window.rs | 209 ++++++++---------- 2 files changed, 108 insertions(+), 130 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 2ce692fd..631c8d1e 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -35,7 +35,7 @@ pub fn show_frame_edit( system: &luminol_data::rpg::System, animation: &mut luminol_data::rpg::Animation, state: &mut super::FrameEditState, -) -> (bool, bool) { +) -> bool { let mut modified = false; let mut recompute_flash = false; @@ -44,27 +44,26 @@ pub fn show_frame_edit( let frame_view = if let Some(frame_view) = &mut state.frame_view { frame_view } else { - let battler_texture = if let Some(battler_name) = &system.battler_name { + let atlas = update_state.graphics.atlas_loader.load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ); + let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); + if let Some(battler_name) = &system.battler_name { match update_state.graphics.texture_loader.load_now( update_state.filesystem, format!("Graphics/Battlers/{battler_name}"), ) { - Ok(texture) => Some(texture), + Ok(texture) => { + frame_view.frame.battler_texture = Some(texture); + } Err(e) => { + frame_view.frame.battler_texture = None; super::util::log_battler_error(update_state, system, animation, e); - return (modified, true); } } - } else { - None - }; - let atlas = update_state.graphics.atlas_loader.load_animation_atlas( - &update_state.graphics, - update_state.filesystem, - animation, - ); - let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); - frame_view.frame.battler_texture = battler_texture; + } frame_view.frame.update_battler( &update_state.graphics, system, @@ -787,5 +786,5 @@ pub fn show_frame_edit( } }); - (modified, false) + modified } diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index 885d6ca2..3a288d47 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -146,125 +146,112 @@ impl luminol_core::Window for super::Window { } }); - let abort = ui - .with_padded_stripe(false, |ui| { - if self.previous_battler_name != system.battler_name { - let battler_texture = - if let Some(battler_name) = &system.battler_name { - match update_state.graphics.texture_loader.load_now( - update_state.filesystem, - format!("Graphics/Battlers/{battler_name}"), - ) { - Ok(texture) => Some(texture), - Err(e) => { - super::util::log_battler_error( - update_state, - &system, - animation, - e, - ); - return true; - } + ui.with_padded_stripe(false, |ui| { + if self.previous_battler_name != system.battler_name { + if let Some(frame_view) = &mut self.frame_edit_state.frame_view { + if let Some(battler_name) = &system.battler_name { + match update_state.graphics.texture_loader.load_now( + update_state.filesystem, + format!("Graphics/Battlers/{battler_name}"), + ) { + Ok(texture) => { + frame_view.frame.battler_texture = Some(texture); } - } else { - None - }; - - if let Some(frame_view) = &mut self.frame_edit_state.frame_view - { - frame_view.frame.battler_texture = battler_texture; - frame_view.frame.rebuild_battler( - &update_state.graphics, - &system, - animation, - luminol_data::Color { - red: 255., - green: 255., - blue: 255., - alpha: 0., - }, - true, - ); + Err(e) => { + frame_view.frame.battler_texture = None; + super::util::log_battler_error( + update_state, + &system, + animation, + e, + ); + } + } } - - self.previous_battler_name.clone_from(&system.battler_name); + frame_view.frame.rebuild_battler( + &update_state.graphics, + &system, + animation, + luminol_data::Color { + red: 255., + green: 255., + blue: 255., + alpha: 0., + }, + true, + ); } - if self.previous_animation != Some(animation.id) { - self.modals.close_all(); - self.frame_edit_state.frame_index = self - .frame_edit_state - .frame_index - .min(animation.frames.len().saturating_sub(1)); + self.previous_battler_name.clone_from(&system.battler_name); + } - let atlas = - update_state.graphics.atlas_loader.load_animation_atlas( - &update_state.graphics, - update_state.filesystem, - animation, - ); + if self.previous_animation != Some(animation.id) { + self.modals.close_all(); + self.frame_edit_state.frame_index = self + .frame_edit_state + .frame_index + .min(animation.frames.len().saturating_sub(1)); - if let Some(frame_view) = &mut self.frame_edit_state.frame_view - { - let flash_maps = - self.frame_edit_state.flash_maps.get(id).unwrap(); - frame_view.frame.atlas = atlas.clone(); - frame_view.frame.update_battler( - &update_state.graphics, - &system, - animation, - Some( - flash_maps - .target(self.frame_edit_state.condition) - .compute(self.frame_edit_state.frame_index), - ), - Some( - flash_maps - .hide(self.frame_edit_state.condition) - .compute(self.frame_edit_state.frame_index), - ), - ); - frame_view.frame.rebuild_all_cells( - &update_state.graphics, - animation, - self.frame_edit_state.frame_index, - ); - } + let atlas = + update_state.graphics.atlas_loader.load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation, + ); - let selected_cell = self - .frame_edit_state - .cellpicker - .as_ref() - .map(|cellpicker| cellpicker.selected_cell) - .unwrap_or_default() - .min(atlas.num_patterns().saturating_sub(1)); - let mut cellpicker = luminol_components::Cellpicker::new( + if let Some(frame_view) = &mut self.frame_edit_state.frame_view { + let flash_maps = + self.frame_edit_state.flash_maps.get(id).unwrap(); + frame_view.frame.atlas = atlas.clone(); + frame_view.frame.update_battler( &update_state.graphics, - atlas, + &system, + animation, + Some( + flash_maps + .target(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), + Some( + flash_maps + .hide(self.frame_edit_state.condition) + .compute(self.frame_edit_state.frame_index), + ), + ); + frame_view.frame.rebuild_all_cells( + &update_state.graphics, + animation, + self.frame_edit_state.frame_index, ); - cellpicker.selected_cell = selected_cell; - self.frame_edit_state.cellpicker = Some(cellpicker); } - let (inner_modified, abort) = super::frame_edit::show_frame_edit( - ui, - update_state, - clip_rect, - &mut self.modals, - &system, - animation, - &mut self.frame_edit_state, + let selected_cell = self + .frame_edit_state + .cellpicker + .as_ref() + .map(|cellpicker| cellpicker.selected_cell) + .unwrap_or_default() + .min(atlas.num_patterns().saturating_sub(1)); + let mut cellpicker = luminol_components::Cellpicker::new( + &update_state.graphics, + atlas, ); + cellpicker.selected_cell = selected_cell; + self.frame_edit_state.cellpicker = Some(cellpicker); + } - modified |= inner_modified; - - abort - }) - .inner; - - if abort { - return true; - } + let inner_modified = super::frame_edit::show_frame_edit( + ui, + update_state, + clip_rect, + &mut self.modals, + &system, + animation, + &mut self.frame_edit_state, + ); + + modified |= inner_modified; + }); let mut collapsing_view_inner = Default::default(); let flash_maps = self.frame_edit_state.flash_maps.get_mut(id).unwrap(); @@ -436,15 +423,11 @@ impl luminol_core::Window for super::Window { } self.previous_animation = Some(animation.id); - false }, ) }); - if response - .as_ref() - .is_some_and(|ir| ir.inner.as_ref().is_some_and(|ir| ir.inner.modified)) - { + if response.is_some_and(|ir| ir.inner.is_some_and(|ir| ir.inner.modified)) { modified = true; } @@ -457,9 +440,5 @@ impl luminol_core::Window for super::Window { drop(system); *update_state.data = data; // restore data - - if response.is_some_and(|ir| ir.inner.is_some_and(|ir| ir.inner.inner == Some(true))) { - *open = false; - } } } From 43ec36d1d61d45100231ecab8c771b7c0d1ff98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 17:02:23 -0400 Subject: [PATCH 084/109] Fix panic in sound tab when an audio folder doesn't exist --- crates/components/src/sound_tab.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index e4bccd72..5615f4f6 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -41,7 +41,9 @@ impl SoundTab { source: luminol_audio::Source, audio_file: luminol_data::rpg::AudioFile, ) -> Self { - let mut folder_children = filesystem.read_dir(format!("Audio/{source}")).unwrap(); + let mut folder_children = filesystem + .read_dir(format!("Audio/{source}")) + .unwrap_or_default(); folder_children.sort_unstable_by(|a, b| a.file_name().cmp(b.file_name())); Self { source, From 2aff3dcc73d7a8ca88ff75e3a00638aeb45d4a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 18:36:24 -0400 Subject: [PATCH 085/109] Fix problems with the graphic and sound pickers --- Cargo.lock | 1 + Cargo.toml | 2 ++ crates/components/Cargo.toml | 2 +- crates/components/src/sound_tab.rs | 38 +++++++++++++++++++---- crates/filesystem/src/project.rs | 7 +++++ crates/modals/Cargo.toml | 2 ++ crates/modals/src/graphic_picker/actor.rs | 24 +++++++++----- crates/modals/src/graphic_picker/basic.rs | 24 +++++++++----- crates/modals/src/graphic_picker/hue.rs | 24 +++++++++----- crates/modals/src/graphic_picker/mod.rs | 35 ++++++++++++--------- 10 files changed, 113 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f329bf7..3896373b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3304,6 +3304,7 @@ dependencies = [ "egui", "fuzzy-matcher", "glam", + "lexical-sort", "luminol-components", "luminol-core", "luminol-data", diff --git a/Cargo.toml b/Cargo.toml index a837ba77..2b4cf668 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,6 +153,8 @@ wasm-bindgen-futures = "0.4.42" web-sys = "0.3.67" js-sys = "0.3" +lexical-sort = "0.3.1" + luminol-audio = { version = "0.4.0", path = "crates/audio/" } luminol-components = { version = "0.4.0", path = "crates/components/" } luminol-config = { version = "0.4.0", path = "crates/config/" } diff --git a/crates/components/Cargo.toml b/crates/components/Cargo.toml index bb27c0fc..bf72d3ee 100644 --- a/crates/components/Cargo.toml +++ b/crates/components/Cargo.toml @@ -42,7 +42,7 @@ color-eyre.workspace = true qp-trie.workspace = true indextree = "4.6.0" -lexical-sort = "0.3.1" +lexical-sort.workspace = true fragile.workspace = true parking_lot.workspace = true diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index 5615f4f6..bf1451e0 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -44,7 +44,9 @@ impl SoundTab { let mut folder_children = filesystem .read_dir(format!("Audio/{source}")) .unwrap_or_default(); - folder_children.sort_unstable_by(|a, b| a.file_name().cmp(b.file_name())); + folder_children.sort_unstable_by(|a, b| { + lexical_sort::natural_lexical_cmp(a.file_name(), b.file_name()) + }); Self { source, audio_file, @@ -175,6 +177,18 @@ impl SoundTab { } ui.separator(); + let audio_file_name = self.audio_file.name.as_ref().and_then(|name| { + update_state + .filesystem + .desensitize( + camino::Utf8Path::new("Audio") + .join(self.source.as_path()) + .join(name), + ) + .ok() + .map(|path| path.file_name().unwrap().to_string()) + }); + egui::ScrollArea::both() .id_source((persistence_id, self.source)) .auto_shrink([false, false]) @@ -203,11 +217,23 @@ impl SoundTab { { let faint = (i + row_range.start) % 2 == 0; let res = ui.with_stripe(faint, |ui| { - ui.selectable_value( - &mut self.audio_file.name, - Some(entry.file_name().into()), - entry.file_name(), - ) + let entry_name = entry.file_name(); + let res = ui.add(egui::SelectableLabel::new( + audio_file_name.as_deref() == Some(entry_name), + entry_name, + )); + if res.clicked() { + self.audio_file.name = if let Some((file_stem, _)) = + entry_name.rsplit_once('.') + { + Some(file_stem.into()) + } else { + audio_file_name + .as_ref() + .map(|name| name.clone().into()) + }; + } + res }); // need to move this out because the borrow checker isn't smart enough // Did the user double click a sound? diff --git a/crates/filesystem/src/project.rs b/crates/filesystem/src/project.rs index 73d1f39a..face0387 100644 --- a/crates/filesystem/src/project.rs +++ b/crates/filesystem/src/project.rs @@ -263,6 +263,13 @@ impl FileSystem { } => Some(host_filesystem.clone()), } } + + pub fn desensitize(&self, path: impl AsRef) -> Result { + match self { + FileSystem::Unloaded | FileSystem::HostLoaded(_) => Err(Error::NotExist.into()), + FileSystem::Loaded { filesystem, .. } => filesystem.desensitize(path), + } + } } // Specific to windows diff --git a/crates/modals/Cargo.toml b/crates/modals/Cargo.toml index 4dff7cfd..de0d2d41 100644 --- a/crates/modals/Cargo.toml +++ b/crates/modals/Cargo.toml @@ -31,3 +31,5 @@ color-eyre.workspace = true fuzzy-matcher = "0.3.7" ouroboros = "0.18.4" + +lexical-sort.workspace = true diff --git a/crates/modals/src/graphic_picker/actor.rs b/crates/modals/src/graphic_picker/actor.rs index cd4440f8..503a8ee2 100644 --- a/crates/modals/src/graphic_picker/actor.rs +++ b/crates/modals/src/graphic_picker/actor.rs @@ -277,14 +277,22 @@ impl Modal { rows.start = rows.start.saturating_sub(1); rows.end = rows.end.saturating_sub(1); - Entry::ui(filtered_entries, ui, rows, selected, |path| { - Self::load_preview_sprite( - update_state, - &self.directory, - path, - ) - .unwrap() - }) + Entry::ui( + filtered_entries, + &self.directory, + update_state, + ui, + rows, + selected, + |path| { + Self::load_preview_sprite( + update_state, + &self.directory, + path, + ) + .unwrap() + }, + ) }, ); }); diff --git a/crates/modals/src/graphic_picker/basic.rs b/crates/modals/src/graphic_picker/basic.rs index 3ffeaa1a..e665410a 100644 --- a/crates/modals/src/graphic_picker/basic.rs +++ b/crates/modals/src/graphic_picker/basic.rs @@ -254,14 +254,22 @@ impl Modal { rows.start = rows.start.saturating_sub(1); rows.end = rows.end.saturating_sub(1); - Entry::ui(filtered_entries, ui, rows, selected, |path| { - Self::load_preview_sprite( - update_state, - &self.directory, - path, - ) - .unwrap() - }) + Entry::ui( + filtered_entries, + &self.directory, + update_state, + ui, + rows, + selected, + |path| { + Self::load_preview_sprite( + update_state, + &self.directory, + path, + ) + .unwrap() + }, + ) }, ); }); diff --git a/crates/modals/src/graphic_picker/hue.rs b/crates/modals/src/graphic_picker/hue.rs index a28eda0c..2af5a871 100644 --- a/crates/modals/src/graphic_picker/hue.rs +++ b/crates/modals/src/graphic_picker/hue.rs @@ -260,14 +260,22 @@ impl Modal { rows.start = rows.start.saturating_sub(1); rows.end = rows.end.saturating_sub(1); - Entry::ui(filtered_entries, ui, rows, selected, |path| { - Self::load_preview_sprite( - update_state, - &self.directory, - path, - ) - .unwrap() - }) + Entry::ui( + filtered_entries, + &self.directory, + update_state, + ui, + rows, + selected, + |path| { + Self::load_preview_sprite( + update_state, + &self.directory, + path, + ) + .unwrap() + }, + ) }, ); }); diff --git a/crates/modals/src/graphic_picker/mod.rs b/crates/modals/src/graphic_picker/mod.rs index c04f2041..431114af 100644 --- a/crates/modals/src/graphic_picker/mod.rs +++ b/crates/modals/src/graphic_picker/mod.rs @@ -104,19 +104,14 @@ impl Entry { .read_dir(directory) .unwrap() .into_iter() - .map(|m| { - let path = m - .path - .strip_prefix(directory) - .unwrap_or(&m.path) - .with_extension(""); - Entry { - path, - invalid: false, - } + .map(|m| Entry { + path: m.path.file_name().unwrap_or_default().into(), + invalid: false, }) .collect(); - entries.sort_unstable(); + entries.sort_unstable_by(|a, b| { + lexical_sort::natural_lexical_cmp(a.path.as_str(), b.path.as_str()) + }); entries } @@ -131,15 +126,25 @@ impl Entry { fn ui( entries: &mut [Self], + directory: &camino::Utf8Path, + update_state: &UpdateState<'_>, ui: &mut egui::Ui, rows: std::ops::Range, selected: &mut Selected, load_preview_sprite: impl Fn(&camino::Utf8Path) -> PreviewSprite, ) { + let selected_name = match &selected { + Selected::Entry { path, .. } => update_state + .filesystem + .desensitize(directory.join(path)) + .ok() + .map(|path| path.file_name().unwrap_or_default().to_string()), + Selected::None => None, + }; for i in entries[rows.clone()].iter_mut().enumerate() { let (i, Self { path, invalid }) = i; - let checked = matches!(selected, Selected::Entry { path: p, .. } if p == path); - let mut text = egui::RichText::new(path.as_str()); + let checked = selected_name.as_deref() == Some(path.as_str()); + let mut text = egui::RichText::new(path.file_name().unwrap_or_default()); if *invalid { text = text.color(egui::Color32::LIGHT_RED); } @@ -149,8 +154,8 @@ impl Entry { if res.clicked() { *selected = Selected::Entry { - path: path.clone(), - sprite: load_preview_sprite(path), + sprite: load_preview_sprite(path.file_name().unwrap_or_default().into()), + path: path.file_stem().unwrap_or_default().into(), }; } }); From a8ca441745e2397185e4490c03c3eef5b6ab72c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 19:10:53 -0400 Subject: [PATCH 086/109] Don't use a source when playing terminal bell sound --- crates/term/src/widget/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/term/src/widget/mod.rs b/crates/term/src/widget/mod.rs index 74f52423..f090f75b 100644 --- a/crates/term/src/widget/mod.rs +++ b/crates/term/src/widget/mod.rs @@ -274,7 +274,7 @@ where false, 25, 100, - Some(luminol_audio::Source::SE), + None, luminol_audio::VolumeScale::Linear, ) .unwrap(); From 4604c9d8343d445eb2aedc9e916393a94d6abd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 19:22:09 -0400 Subject: [PATCH 087/109] Disable parts of the animation editor when animation is playing --- crates/ui/src/windows/animations/frame_edit.rs | 13 ++++++++++--- crates/ui/src/windows/animations/window.rs | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 631c8d1e..dec3c761 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -545,9 +545,10 @@ pub fn show_frame_edit( } // Handle dragging of cells to move them - if let (Some(i), Some(drag_pos)) = ( + if let (Some(i), Some(drag_pos), true) = ( frame_view.hovered_cell_index, frame_view.hovered_cell_drag_pos, + state.animation_state.is_none(), ) { if (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) != drag_pos { (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) = drag_pos; @@ -560,7 +561,10 @@ pub fn show_frame_edit( egui::Frame::none().show(ui, |ui| { let frame = &mut animation.frames[state.frame_index]; - if let Some(i) = frame_view.selected_cell_index { + if let (Some(i), true) = ( + frame_view.selected_cell_index, + state.animation_state.is_none(), + ) { let mut properties_modified = false; ui.label(format!("Cell {}", i + 1)); @@ -751,7 +755,10 @@ pub fn show_frame_edit( let frame = &mut animation.frames[state.frame_index]; // Handle pressing delete or backspace to delete cells - if let Some(i) = frame_view.selected_cell_index { + if let (Some(i), true) = ( + frame_view.selected_cell_index, + state.animation_state.is_none(), + ) { if i < frame.cell_data.xsize() && frame.cell_data[(i, 0)] >= 0 && response.has_focus() diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index 3a288d47..d4f6c45a 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -192,6 +192,14 @@ impl luminol_core::Window for super::Window { .frame_index .min(animation.frames.len().saturating_sub(1)); + // Stop the currently playing animation + if self.frame_edit_state.animation_state.is_some() { + let animation_state = + self.frame_edit_state.animation_state.take().unwrap(); + self.frame_edit_state.frame_index = + animation_state.saved_frame_index; + } + let atlas = update_state.graphics.atlas_loader.load_animation_atlas( &update_state.graphics, From e0fe11ce80cb797446fa2d2db27926923481aed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 21:57:41 -0400 Subject: [PATCH 088/109] Improve handling of invalid frame numbers and cells If the selected frame or selected cell in the animation editor becomes invalid after the selected animation or frame number changes, it'll now save what the frame index or selected cell index was and restore them if they become valid again. This is similar to the mechanism that text editors use when you use the arrow keys to move between lines of a text document. If the line that you move the cursor onto has fewer columns than the column that the cursor was previously on, it saves the column and restores it if you move the cursor onto a line with enough columns later. --- .../ui/src/windows/animations/frame_edit.rs | 34 ++++++++++++++----- crates/ui/src/windows/animations/mod.rs | 4 +++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index dec3c761..c035e4cd 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -24,7 +24,7 @@ use luminol_core::Modal; -use luminol_data::{rpg::animation::Condition, BlendMode}; +use luminol_data::BlendMode; use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; pub fn show_frame_edit( @@ -118,10 +118,7 @@ pub fn show_frame_edit( animation_state.timing_index += i; break; } - if state.condition != timing.condition - && state.condition != Condition::None - && timing.condition != Condition::None - { + if !super::util::filter_timing(timing, state.condition) { continue; } if let Some(se_name) = &timing.se.name { @@ -179,11 +176,14 @@ pub fn show_frame_edit( .fixed_decimals(0), )); - state.frame_index = state - .frame_index - .min(animation.frames.len().saturating_sub(1)); + let max_frame_index = animation.frames.len().saturating_sub(1); + if let Some(saved_frame_index) = state.saved_frame_index { + state.frame_index = saved_frame_index.min(max_frame_index); + } else if state.frame_index > max_frame_index { + state.frame_index = max_frame_index; + } state.frame_index += 1; - recompute_flash |= ui + let changed = ui .add_enabled( state.animation_state.is_none(), luminol_components::Field::new( @@ -193,6 +193,10 @@ pub fn show_frame_edit( ) .changed(); state.frame_index -= 1; + if changed { + recompute_flash = true; + state.saved_frame_index = Some(state.frame_index); + } recompute_flash |= ui .add(luminol_components::Field::new( @@ -535,6 +539,15 @@ pub fn show_frame_edit( { frame_view.selected_cell_index = None; } + + if frame_view.selected_cell_index.is_none() + && state + .saved_selected_cell_index + .is_some_and(|i| i < frame.cell_data.xsize() && frame.cell_data[(i, 0)] >= 0) + { + frame_view.selected_cell_index = state.saved_selected_cell_index; + } + if frame_view .hovered_cell_index .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) @@ -710,6 +723,9 @@ pub fn show_frame_edit( .compute(state.frame_index), state.animation_state.is_none(), ); + if response.clicked() { + state.saved_selected_cell_index = frame_view.selected_cell_index; + } // If the pointer is hovering over the frame view, prevent parent widgets // from receiving scroll events so that scaling the frame view with the diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs index a4b03c44..65d83a25 100644 --- a/crates/ui/src/windows/animations/mod.rs +++ b/crates/ui/src/windows/animations/mod.rs @@ -49,6 +49,8 @@ struct FrameEditState { cellpicker: Option, flash_maps: luminol_data::OptionVec, animation_state: Option, + saved_frame_index: Option, + saved_selected_cell_index: Option, } #[derive(Debug)] @@ -95,6 +97,8 @@ impl Default for Window { cellpicker: None, flash_maps: Default::default(), animation_state: None, + saved_frame_index: None, + saved_selected_cell_index: None, }, timing_edit_state: TimingEditState { previous_frame: None, From 40e0036c20cbbabe168ae5b678c958759a3158b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 22:06:57 -0400 Subject: [PATCH 089/109] Add menu SE picker to skill editor --- crates/ui/src/windows/skills.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/windows/skills.rs b/crates/ui/src/windows/skills.rs index eeb40e92..280303d9 100644 --- a/crates/ui/src/windows/skills.rs +++ b/crates/ui/src/windows/skills.rs @@ -23,15 +23,31 @@ // Program grant you additional permission to convey the resulting work. use luminol_components::UiExt; +use luminol_core::Modal; + +use luminol_modals::sound_picker::Modal as SoundPicker; -#[derive(Default)] pub struct Window { selected_skill_name: Option, + + menu_se_picker: SoundPicker, + previous_skill: Option, view: luminol_components::DatabaseView, } +impl Default for Window { + fn default() -> Self { + Self { + selected_skill_name: None, + menu_se_picker: SoundPicker::new(luminol_audio::Source::SE, "skill_menu_se_picker"), + previous_skill: None, + view: luminol_components::DatabaseView::default(), + } + } +} + impl Window { pub fn new() -> Self { Default::default() @@ -172,9 +188,14 @@ impl luminol_core::Window for Window { modified |= columns[0] .add(luminol_components::Field::new( "Menu Use SE", - egui::Label::new("TODO"), + self.menu_se_picker + .button(&mut skill.menu_se, update_state), )) .changed(); + if self.previous_skill != Some(skill.id) { + // reset the modal if the skill has changed (this is practically a no-op) + self.menu_se_picker.reset(update_state, &mut skill.menu_se); + } modified |= columns[1] .add(luminol_components::Field::new( From b6c97a3238a8b4a641fb778da2251ae5f58c179e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 22:14:49 -0400 Subject: [PATCH 090/109] Add a magnifying glass to every search box for consistency --- crates/components/src/database_view.rs | 2 +- crates/components/src/id_vec.rs | 6 +++--- crates/components/src/lib.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/components/src/database_view.rs b/crates/components/src/database_view.rs index 011dacc6..d11091fe 100644 --- a/crates/components/src/database_view.rs +++ b/crates/components/src/database_view.rs @@ -153,7 +153,7 @@ impl DatabaseView { let search_box_response = ui.add( egui::TextEdit::singleline(&mut search_string) - .hint_text("Search"), + .hint_text("Search 🔎"), ); ui.add_space(ui.spacing().item_spacing.y); diff --git a/crates/components/src/id_vec.rs b/crates/components/src/id_vec.rs index 5a20aaa2..85034abd 100644 --- a/crates/components/src/id_vec.rs +++ b/crates/components/src/id_vec.rs @@ -188,7 +188,7 @@ where ui.set_width(ui.available_width()); let search_box_response = ui.add( - egui::TextEdit::singleline(&mut state.search_string).hint_text("Search"), + egui::TextEdit::singleline(&mut state.search_string).hint_text("Search 🔎"), ); ui.add_space(ui.spacing().item_spacing.y); @@ -345,7 +345,7 @@ where ui.set_width(ui.available_width()); let search_box_response = ui.add( - egui::TextEdit::singleline(&mut state.search_string).hint_text("Search"), + egui::TextEdit::singleline(&mut state.search_string).hint_text("Search 🔎"), ); ui.add_space(ui.spacing().item_spacing.y); @@ -529,7 +529,7 @@ where ui.set_width(ui.available_width()); let search_box_response = ui.add( - egui::TextEdit::singleline(&mut state.search_string).hint_text("Search"), + egui::TextEdit::singleline(&mut state.search_string).hint_text("Search 🔎"), ); ui.add_space(ui.spacing().item_spacing.y); diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index da3f79a4..9d042424 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -354,7 +354,7 @@ where let mut search_matched_ids = search_matched_ids_lock.lock(); let search_box_response = - ui.add(egui::TextEdit::singleline(&mut search_string).hint_text("Search")); + ui.add(egui::TextEdit::singleline(&mut search_string).hint_text("Search 🔎")); ui.add_space(ui.spacing().item_spacing.y); From aa44fb28a31e8d1996f3b0d1f5ff4fe42dcfc9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 23:01:56 -0400 Subject: [PATCH 091/109] Fix truncation of text in the UI It seems `ui.truncate_text` doesn't work anymore as of egui 0.28. I've replaced all remaining `ui.truncate_text` calls using egui 0.28's new `TextWrapMode`. --- crates/components/src/database_view.rs | 8 +++++--- crates/components/src/filesystem_view.rs | 13 ++++++------ crates/components/src/id_vec.rs | 19 ++++++++++-------- crates/components/src/lib.rs | 25 ++++++++++++------------ crates/components/src/ui_ext.rs | 16 --------------- 5 files changed, 35 insertions(+), 46 deletions(-) diff --git a/crates/components/src/database_view.rs b/crates/components/src/database_view.rs index d11091fe..16b564e2 100644 --- a/crates/components/src/database_view.rs +++ b/crates/components/src/database_view.rs @@ -81,14 +81,14 @@ impl DatabaseView { egui::Layout::bottom_up(ui.layout().horizontal_align()), |ui| { ui.horizontal(|ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); ui.add(egui::DragValue::new(self.maximum.as_mut().unwrap())); if ui .add_enabled( self.maximum != Some(vec.len()), - egui::Button::new(ui.truncate_text("Set Maximum")), + egui::Button::new("Set Maximum"), ) .clicked() { @@ -195,11 +195,13 @@ impl DatabaseView { let entry = &mut vec[id]; ui.with_stripe(is_faint, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + let response = ui .selectable_value( &mut self.selected_id, id, - ui.truncate_text(formatter(entry)), + formatter(entry), ) .interact(egui::Sense::click()); diff --git a/crates/components/src/filesystem_view.rs b/crates/components/src/filesystem_view.rs index bc500f4d..6d90f57b 100644 --- a/crates/components/src/filesystem_view.rs +++ b/crates/components/src/filesystem_view.rs @@ -274,10 +274,9 @@ where match self.arena[node_id].get_mut() { Entry::File { name, selected } => { ui.with_stripe(is_faint, |ui| { - if ui - .selectable_label(*selected, ui.truncate_text(name.to_string())) - .clicked() - { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + + if ui.selectable_label(*selected, name.to_string()).clicked() { should_toggle = true; }; }); @@ -314,10 +313,12 @@ where header_response = Some(header.show_header(ui, |ui| { ui.with_layout(layout, |ui| { ui.with_stripe(is_faint, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if ui .selectable_label( *selected, - ui.truncate_text(format!( + format!( "{} {}", if *selected { '▣' @@ -328,7 +329,7 @@ where '⊟' }, name - )), + ), ) .clicked() { diff --git a/crates/components/src/id_vec.rs b/crates/components/src/id_vec.rs index 85034abd..a9e3b67d 100644 --- a/crates/components/src/id_vec.rs +++ b/crates/components/src/id_vec.rs @@ -222,11 +222,10 @@ where self.reference.binary_search(&(id - first_id)).is_ok(); ui.with_stripe(is_faint, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if ui - .selectable_label( - is_id_selected, - ui.truncate_text((self.formatter)(id)), - ) + .selectable_label(is_id_selected, (self.formatter)(id)) .clicked() { clicked_id = Some(id - first_id); @@ -380,6 +379,8 @@ where self.minus.binary_search(&(id - first_id)).is_ok(); ui.with_stripe(is_faint, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + // Make the background of the selectable label red if it's // a minus if is_id_minus { @@ -391,13 +392,13 @@ where if ui .selectable_label( is_id_plus || is_id_minus, - ui.truncate_text(if is_id_plus { + if is_id_plus { format!("+ {label}") } else if is_id_minus { format!("‒ {label}") } else { label - }), + }, ) .clicked() { @@ -564,6 +565,8 @@ where .map(|id| (id, self.reference[id + 1])) { ui.with_stripe(is_faint, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + // Color the background of the selectable label depending on the // rank ui.visuals_mut().selection.bg_fill = match rank { @@ -582,7 +585,7 @@ where if ui .selectable_label( matches!(rank, 1 | 2 | 4 | 5 | 6), - ui.truncate_text(format!( + format!( "{} - {label}", match rank { 1 => 'A', @@ -593,7 +596,7 @@ where 6 => 'F', _ => '?', } - )), + ), ) .clicked() { diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index 9d042424..32ce05b1 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -79,6 +79,8 @@ impl<'e, T: ToString + PartialEq + strum::IntoEnumIterator> egui::Widget for Enu egui::ComboBox::from_id_source(self.id) .selected_text(self.current_value.to_string()) .show_ui(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + for variant in T::iter() { let text = variant.to_string(); ui.selectable_value(self.current_value, variant, text); @@ -104,6 +106,8 @@ impl<'e, T: ToString + PartialEq + strum::IntoEnumIterator> egui::Widget for Enu let mut response = ui .vertical(|ui| { ui.with_cross_justify(|ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + for variant in T::iter() { let text = variant.to_string(); if ui.radio_value(self.current_value, variant, text).changed() { @@ -268,7 +272,7 @@ where .selectable_label( std::mem::discriminant(self.reference) == std::mem::discriminant(&variant), - ui.truncate_text(variant.to_string()), + variant.to_string(), ) .clicked() { @@ -472,10 +476,7 @@ where if show_none && ui .with_stripe(false, |ui| { - ui.selectable_label( - this.reference.is_none(), - ui.truncate_text("(None)"), - ) + ui.selectable_label(this.reference.is_none(), "(None)") }) .inner .clicked() @@ -488,11 +489,10 @@ where for id in ids { ui.with_stripe(is_faint, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if ui - .selectable_label( - *this.reference == Some(id), - ui.truncate_text((this.formatter)(id)), - ) + .selectable_label(*this.reference == Some(id), (this.formatter)(id)) .clicked() { *this.reference = Some(id); @@ -529,11 +529,10 @@ where for id in ids { ui.with_stripe(is_faint, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if ui - .selectable_label( - *this.reference == id, - ui.truncate_text((this.formatter)(id)), - ) + .selectable_label(*this.reference == id, (this.formatter)(id)) .clicked() { *this.reference = id; diff --git a/crates/components/src/ui_ext.rs b/crates/components/src/ui_ext.rs index 1518929b..b53354ef 100644 --- a/crates/components/src/ui_ext.rs +++ b/crates/components/src/ui_ext.rs @@ -57,10 +57,6 @@ pub trait UiExt { faint: bool, f: impl FnOnce(&mut Self) -> R, ) -> InnerResponse; - - /// Modifies the given `egui::WidgetText` to truncate when the text is too long to fit in the - /// current layout, rather than wrapping the text or expanding the layout. - fn truncate_text(&self, text: impl Into) -> egui::WidgetText; } impl UiExt for egui::Ui { @@ -141,16 +137,4 @@ impl UiExt for egui::Ui { } .show(self, f) } - - fn truncate_text(&self, text: impl Into) -> egui::WidgetText { - let mut job = Into::::into(text).into_layout_job( - self.style(), - egui::TextStyle::Body.into(), - self.layout().vertical_align(), - ); - job.wrap.max_width = self.available_width(); - job.wrap.max_rows = 1; - job.wrap.break_anywhere = true; - job.into() - } } From 39c55f99b8382b8daba1c6509ef88ed5a4f809c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 7 Aug 2024 23:41:33 -0400 Subject: [PATCH 092/109] Determine if file is MIDI using magic number instead of extension There's no reason to be determining if an audio file is a MIDI file based on the file extension when rodio determines the type of an audio file based on its contents. This also frees us from having to manually determine if an audio file is MIDI when the audio data is passed as a slice. --- crates/audio/src/native.rs | 50 +++++++++++++------ crates/audio/src/wrapper.rs | 14 +----- crates/term/src/widget/mod.rs | 1 - .../ui/src/windows/animations/frame_edit.rs | 1 - 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/crates/audio/src/native.rs b/crates/audio/src/native.rs index 3ebe7c01..5d9bb5f0 100644 --- a/crates/audio/src/native.rs +++ b/crates/audio/src/native.rs @@ -1,3 +1,29 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use std::io::{Read, Seek}; + use crate::{midi, Result, Source, VolumeScale}; /// A struct for playing Audio. @@ -67,42 +93,34 @@ impl Audio { let path = path.as_ref(); let file = filesystem.open_file(path, luminol_filesystem::OpenFlags::Read)?; - let is_midi = path - .extension() - .is_some_and(|e| matches!(e, "mid" | "midi")); - - self.play_from_file(file, is_midi, volume, pitch, source, scale) + self.play_from_file(file, volume, pitch, source, scale) } /// Play a sound on a source from audio file data. pub fn play_from_slice( &self, slice: impl AsRef<[u8]> + Send + Sync + 'static, - is_midi: bool, volume: u8, pitch: u8, source: Option, scale: VolumeScale, ) -> Result<()> { - self.play_from_file( - std::io::Cursor::new(slice), - is_midi, - volume, - pitch, - source, - scale, - ) + self.play_from_file(std::io::Cursor::new(slice), volume, pitch, source, scale) } fn play_from_file( &self, - file: impl std::io::Read + std::io::Seek + Send + Sync + 'static, - is_midi: bool, + mut file: impl Read + Seek + Send + Sync + 'static, volume: u8, pitch: u8, source: Option, scale: VolumeScale, ) -> Result<()> { + let mut magic_header_buf = [0u8; 4]; + file.read_exact(&mut magic_header_buf)?; + file.seek(std::io::SeekFrom::Current(-4))?; + let is_midi = &magic_header_buf == b"MThd"; + let mut inner = self.inner.lock(); // Create a sink let sink = rodio::Sink::try_new(&inner.output_stream_handle)?; diff --git a/crates/audio/src/wrapper.rs b/crates/audio/src/wrapper.rs index f9b13cda..2b12d6d5 100644 --- a/crates/audio/src/wrapper.rs +++ b/crates/audio/src/wrapper.rs @@ -32,7 +32,6 @@ pub struct Audio { enum Command { Play { slice: std::sync::Arc<[u8]>, - is_midi: bool, volume: u8, pitch: u8, source: Option, @@ -81,18 +80,13 @@ impl Audio { let path = path.as_ref(); let slice: std::sync::Arc<[u8]> = filesystem.read(path)?.into(); - let is_midi = path - .extension() - .is_some_and(|e| matches!(e, "mid" | "midi")); - - self.play_from_slice(slice, is_midi, volume, pitch, source, scale) + self.play_from_slice(slice, volume, pitch, source, scale) } /// Play a sound on a source from audio file data. pub fn play_from_slice( &self, slice: impl AsRef<[u8]> + Send + Sync + 'static, - is_midi: bool, volume: u8, pitch: u8, source: Option, @@ -102,7 +96,6 @@ impl Audio { self.tx .send(Command::Play { slice: slice.as_ref().into(), - is_midi, volume, pitch, source, @@ -180,7 +173,6 @@ impl Default for Audio { match command { Command::Play { slice, - is_midi, volume, pitch, source, @@ -188,9 +180,7 @@ impl Default for Audio { oneshot_tx, } => { oneshot_tx - .send( - audio.play_from_slice(slice, is_midi, volume, pitch, source, scale), - ) + .send(audio.play_from_slice(slice, volume, pitch, source, scale)) .unwrap(); } diff --git a/crates/term/src/widget/mod.rs b/crates/term/src/widget/mod.rs index f090f75b..aff1bb32 100644 --- a/crates/term/src/widget/mod.rs +++ b/crates/term/src/widget/mod.rs @@ -271,7 +271,6 @@ where .audio .play_from_slice( bell, - false, 25, 100, None, diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index c035e4cd..19768595 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -129,7 +129,6 @@ pub fn show_frame_edit( }; if let Err(e) = update_state.audio.play_from_slice( audio_data.clone(), - false, timing.se.volume, timing.se.pitch, None, From 31e91754d740719f405259d286dc09b703ef6c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 00:21:45 -0400 Subject: [PATCH 093/109] Add some keyboard shortcuts to the animation editor --- .../ui/src/windows/animations/frame_edit.rs | 98 ++++++++++++++----- 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 19768595..ace6a483 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -27,6 +27,37 @@ use luminol_core::Modal; use luminol_data::BlendMode; use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; +fn start_animation_playback( + update_state: &mut luminol_core::UpdateState<'_>, + animation: &luminol_data::rpg::Animation, + animation_state: &mut Option, + frame_index: &mut usize, + condition: luminol_data::rpg::animation::Condition, +) { + if let Some(animation_state) = animation_state.take() { + *frame_index = animation_state.saved_frame_index; + } else { + *animation_state = Some(super::AnimationState { + saved_frame_index: *frame_index, + start_time: f64::NAN, + timing_index: 0, + audio_data: Default::default(), + }); + *frame_index = 0; + + // Preload the audio files used by the animation for + // performance reasons + for timing in &animation.timings { + super::util::load_se( + update_state, + animation_state.as_mut().unwrap(), + condition, + timing, + ); + } + } +} + pub fn show_frame_edit( ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>, @@ -277,28 +308,13 @@ pub fn show_frame_edit( }); if ui.button("Play").clicked() { - if let Some(animation_state) = state.animation_state.take() { - state.frame_index = animation_state.saved_frame_index; - } else { - state.animation_state = Some(super::AnimationState { - saved_frame_index: state.frame_index, - start_time: f64::NAN, - timing_index: 0, - audio_data: Default::default(), - }); - state.frame_index = 0; - - // Preload the audio files used by the animation for - // performance reasons - for timing in &animation.timings { - super::util::load_se( - update_state, - state.animation_state.as_mut().unwrap(), - state.condition, - timing, - ); - } - } + start_animation_playback( + update_state, + animation, + &mut state.animation_state, + &mut state.frame_index, + state.condition, + ); } }); }, @@ -806,6 +822,44 @@ pub fn show_frame_edit( modified = true; } } + + if response.has_focus() { + ui.memory_mut(|m| { + m.set_focus_lock_filter( + response.id, + egui::EventFilter { + tab: false, + horizontal_arrows: true, + vertical_arrows: false, + escape: false, + }, + ) + }); + + // Press left/right arrow keys to change frame + if ui.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { + state.frame_index = state.frame_index.saturating_sub(1); + state.saved_frame_index = None; + } + if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) { + state.frame_index = state + .frame_index + .saturating_add(1) + .min(animation.frames.len().saturating_sub(1)); + state.saved_frame_index = None; + } + + // Press space or enter to start/stop animation playback + if ui.input(|i| i.key_pressed(egui::Key::Space) || i.key_pressed(egui::Key::Enter)) { + start_animation_playback( + update_state, + animation, + &mut state.animation_state, + &mut state.frame_index, + state.condition, + ); + } + } }); modified From 43c1ddf0a76dbb695cc111ab0774335fe0228c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 12:05:17 -0400 Subject: [PATCH 094/109] Add animation graphic picker --- Cargo.lock | 1 + crates/components/src/cellpicker.rs | 45 ++- crates/graphics/src/loaders/atlas.rs | 42 ++- crates/graphics/src/primitives/cells/atlas.rs | 41 +-- crates/modals/Cargo.toml | 1 + crates/modals/src/graphic_picker/actor.rs | 2 +- crates/modals/src/graphic_picker/animation.rs | 268 ++++++++++++++++++ crates/modals/src/graphic_picker/basic.rs | 2 +- crates/modals/src/graphic_picker/event.rs | 2 +- crates/modals/src/graphic_picker/hue.rs | 2 +- crates/modals/src/graphic_picker/mod.rs | 1 + .../ui/src/windows/animations/frame_edit.rs | 44 ++- crates/ui/src/windows/animations/mod.rs | 2 + crates/ui/src/windows/animations/window.rs | 18 +- 14 files changed, 419 insertions(+), 52 deletions(-) create mode 100644 crates/modals/src/graphic_picker/animation.rs diff --git a/Cargo.lock b/Cargo.lock index 3896373b..a4887a78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3309,6 +3309,7 @@ dependencies = [ "luminol-core", "luminol-data", "luminol-egui-wgpu", + "luminol-graphics", "ouroboros", ] diff --git a/crates/components/src/cellpicker.rs b/crates/components/src/cellpicker.rs index 26bfe442..49d048ac 100644 --- a/crates/components/src/cellpicker.rs +++ b/crates/components/src/cellpicker.rs @@ -24,14 +24,23 @@ pub struct Cellpicker { pub selected_cell: u32, pub viewport: Viewport, pub view: Cells, + pub cols: u32, + pub scale: f32, } impl Cellpicker { - pub fn new(graphics_state: &luminol_graphics::GraphicsState, atlas: Atlas) -> Self { + pub fn new( + graphics_state: &luminol_graphics::GraphicsState, + atlas: Atlas, + cols: Option, + scale: f32, + ) -> Self { + let cols = cols.unwrap_or(atlas.num_patterns()); + let rows = (atlas.num_patterns()).div_ceil(cols); let cells = luminol_data::Table2::new_data( - atlas.num_patterns() as usize, - 1, - (0..atlas.num_patterns() as i16).collect(), + cols as usize, + rows as usize, + (0..(rows * cols) as i16).collect(), ); let viewport = Viewport::new( @@ -51,9 +60,16 @@ impl Cellpicker { selected_cell: 0, viewport, view, + cols, + scale, } } + #[inline] + pub fn rows(&self) -> u32 { + self.view.atlas.num_patterns().div_ceil(self.cols) + } + pub fn ui( &mut self, update_state: &luminol_core::UpdateState<'_>, @@ -62,9 +78,9 @@ impl Cellpicker { ) -> egui::Response { let (canvas_rect, response) = ui.allocate_exact_size( egui::vec2( - (self.view.atlas.num_patterns() * CELL_SIZE) as f32, - CELL_SIZE as f32, - ) / 2., + (self.cols * CELL_SIZE) as f32, + (self.rows() * CELL_SIZE) as f32, + ) * self.scale, egui::Sense::click_and_drag(), ); @@ -76,13 +92,13 @@ impl Cellpicker { self.view.transform.set_position( &update_state.graphics.render_state, - glam::vec2(-scroll_rect.left() * 2., 0.), + glam::vec2(-scroll_rect.left(), -scroll_rect.top()) / self.scale, ); self.viewport.set( &update_state.graphics.render_state, glam::vec2(scroll_rect.width(), scroll_rect.height()), glam::Vec2::ZERO, - glam::Vec2::splat(0.5), + glam::Vec2::splat(self.scale), ); let painter = luminol_graphics::Painter::new(self.view.prepare(&update_state.graphics)); @@ -93,17 +109,20 @@ impl Cellpicker { )); let rect = (egui::Rect::from_min_size( - egui::pos2((self.selected_cell * CELL_SIZE) as f32, 0.), + egui::pos2( + ((self.selected_cell % self.cols) * CELL_SIZE) as f32, + ((self.selected_cell / self.cols) * CELL_SIZE) as f32, + ), egui::Vec2::splat(CELL_SIZE as f32), - ) / 2.) + ) * self.scale) .translate(canvas_rect.min.to_vec2()); ui.painter() .rect_stroke(rect, 5.0, egui::Stroke::new(1.0, egui::Color32::WHITE)); if response.clicked() { if let Some(pos) = response.interact_pointer_pos() { - self.selected_cell = - ((pos - canvas_rect.min) / CELL_SIZE as f32 * 2.).x.floor() as u32; + let mapped_pos = (pos - canvas_rect.min) / (CELL_SIZE as f32 * self.scale); + self.selected_cell = mapped_pos.x as u32 + mapped_pos.y as u32 * self.cols; } } diff --git a/crates/graphics/src/loaders/atlas.rs b/crates/graphics/src/loaders/atlas.rs index c2174c9a..c1711a6e 100644 --- a/crates/graphics/src/loaders/atlas.rs +++ b/crates/graphics/src/loaders/atlas.rs @@ -20,7 +20,7 @@ use crate::{Atlas, GraphicsState}; #[derive(Default)] pub struct Loader { atlases: dashmap::DashMap, - animation_atlases: dashmap::DashMap, + animation_atlases: dashmap::DashMap, } impl Loader { @@ -40,11 +40,15 @@ impl Loader { &self, graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, - animation: &luminol_data::rpg::Animation, + animation_name: Option<&camino::Utf8Path>, ) -> AnimationAtlas { self.animation_atlases - .entry(animation.id) - .or_insert_with(|| AnimationAtlas::new(graphics_state, filesystem, animation)) + .entry( + animation_name + .unwrap_or(&camino::Utf8PathBuf::default()) + .into(), + ) + .or_insert_with(|| AnimationAtlas::new(graphics_state, filesystem, animation_name)) .clone() } @@ -64,11 +68,19 @@ impl Loader { &self, graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, - animation: &luminol_data::rpg::Animation, + animation_name: Option<&camino::Utf8Path>, ) -> AnimationAtlas { self.animation_atlases - .entry(animation.id) - .insert(AnimationAtlas::new(graphics_state, filesystem, animation)) + .entry( + animation_name + .unwrap_or(&camino::Utf8PathBuf::default()) + .into(), + ) + .insert(AnimationAtlas::new( + graphics_state, + filesystem, + animation_name, + )) .clone() } @@ -76,17 +88,25 @@ impl Loader { self.atlases.get(&id).map(|atlas| atlas.clone()) } - pub fn get_animation_atlas(&self, id: usize) -> Option { - self.animation_atlases.get(&id).map(|atlas| atlas.clone()) + pub fn get_animation_atlas( + &self, + animation_name: Option<&camino::Utf8Path>, + ) -> Option { + self.animation_atlases + .get(animation_name.unwrap_or(&camino::Utf8PathBuf::default())) + .map(|atlas| atlas.clone()) } pub fn get_expect(&self, id: usize) -> Atlas { self.atlases.get(&id).expect("Atlas not loaded!").clone() } - pub fn get_animation_expect(&self, id: usize) -> AnimationAtlas { + pub fn get_animation_expect( + &self, + animation_name: Option<&camino::Utf8Path>, + ) -> AnimationAtlas { self.animation_atlases - .get(&id) + .get(animation_name.unwrap_or(&camino::Utf8PathBuf::default())) .expect("Atlas not loaded!") .clone() } diff --git a/crates/graphics/src/primitives/cells/atlas.rs b/crates/graphics/src/primitives/cells/atlas.rs index 06c4e642..65804f34 100644 --- a/crates/graphics/src/primitives/cells/atlas.rs +++ b/crates/graphics/src/primitives/cells/atlas.rs @@ -43,25 +43,22 @@ impl Atlas { pub fn new( graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, - animation: &luminol_data::rpg::Animation, + animation_name: Option<&camino::Utf8Path>, ) -> Atlas { - let animation_img = animation - .animation_name - .as_ref() - .and_then(|animation_name| { - let result = filesystem - .read(camino::Utf8Path::new("Graphics/Animations").join(animation_name)) - .and_then(|file| image::load_from_memory(&file).map_err(|e| e.into())) - .wrap_err_with(|| format!("Error loading atlas animation {animation_name:?}")); - // we don't actually need to unwrap this to a placeholder image because we fill in the atlas texture with the placeholder image. - match result { - Ok(img) => Some(img.into_rgba8()), - Err(e) => { - graphics_state.send_texture_error(e); - None - } + let animation_img = animation_name.as_ref().and_then(|animation_name| { + let result = filesystem + .read(camino::Utf8Path::new("Graphics/Animations").join(animation_name)) + .and_then(|file| image::load_from_memory(&file).map_err(|e| e.into())) + .wrap_err_with(|| format!("Error loading atlas animation {animation_name:?}")); + // we don't actually need to unwrap this to a placeholder image because we fill in the atlas texture with the placeholder image. + match result { + Ok(img) => Some(img.into_rgba8()), + Err(e) => { + graphics_state.send_texture_error(e); + None } - }); + } + }); let animation_height = animation_img .as_ref() @@ -121,9 +118,13 @@ impl Atlas { } } - let atlas_texture = graphics_state - .texture_loader - .register_texture(format!("animation_atlases/{}", animation.id), atlas_texture); + let atlas_texture = graphics_state.texture_loader.register_texture( + format!( + "animation_atlases/{}", + animation_name.unwrap_or(&camino::Utf8PathBuf::default()) + ), + atlas_texture, + ); Atlas { atlas_texture, diff --git a/crates/modals/Cargo.toml b/crates/modals/Cargo.toml index de0d2d41..7efe7a82 100644 --- a/crates/modals/Cargo.toml +++ b/crates/modals/Cargo.toml @@ -23,6 +23,7 @@ camino.workspace = true luminol-core.workspace = true luminol-data.workspace = true +luminol-graphics.workspace = true luminol-components.workspace = true luminol-egui-wgpu.workspace = true glam.workspace = true diff --git a/crates/modals/src/graphic_picker/actor.rs b/crates/modals/src/graphic_picker/actor.rs index 503a8ee2..c2b82177 100644 --- a/crates/modals/src/graphic_picker/actor.rs +++ b/crates/modals/src/graphic_picker/actor.rs @@ -22,7 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use color_eyre::eyre::Context; +use color_eyre::eyre::WrapErr; use luminol_components::UiExt; use luminol_core::prelude::*; diff --git a/crates/modals/src/graphic_picker/animation.rs b/crates/modals/src/graphic_picker/animation.rs new file mode 100644 index 00000000..6aa6a588 --- /dev/null +++ b/crates/modals/src/graphic_picker/animation.rs @@ -0,0 +1,268 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use luminol_components::{Cellpicker, UiExt}; +use luminol_core::prelude::*; + +use super::Entry; + +pub struct Modal { + state: State, + id_source: egui::Id, + animation_name: Option, + animation_hue: i32, +} + +enum State { + Closed, + Open { + entries: Vec, + filtered_entries: Vec, + search_text: String, + cellpicker: luminol_components::Cellpicker, + }, +} + +impl Modal { + pub fn new(animation: &rpg::Animation, id_source: egui::Id) -> Self { + Self { + state: State::Closed, + id_source, + animation_name: animation.animation_name.clone(), + animation_hue: animation.animation_hue, + } + } +} + +impl luminol_core::Modal for Modal { + type Data<'m> = &'m mut luminol_data::rpg::Animation; + + fn button<'m>( + &'m mut self, + data: Self::Data<'m>, + update_state: &'m mut UpdateState<'_>, + ) -> impl egui::Widget + 'm { + move |ui: &mut egui::Ui| { + let is_open = matches!(self.state, State::Open { .. }); + + let button_text = if let Some(track) = &data.animation_name { + format!("Graphics/Animations/{}", track) + } else { + "(None)".to_string() + }; + let mut response = ui.button(button_text); + + if response.clicked() && !is_open { + let entries = Entry::load(update_state, "Graphics/Animations".into()); + + self.state = State::Open { + filtered_entries: entries.clone(), + entries, + cellpicker: Self::load_cellpicker( + update_state, + &self.animation_name, + self.animation_hue, + ), + search_text: String::new(), + }; + } + if self.show_window(update_state, ui.ctx(), data) { + response.mark_changed(); + } + + response + } + } + + fn reset(&mut self, _update_state: &mut UpdateState<'_>, data: Self::Data<'_>) { + self.animation_name.clone_from(&data.animation_name); + self.animation_hue = data.animation_hue; + self.state = State::Closed; + } +} + +impl Modal { + fn load_cellpicker( + update_state: &mut luminol_core::UpdateState<'_>, + animation_name: &Option, + animation_hue: i32, + ) -> Cellpicker { + let atlas = update_state.graphics.atlas_loader.load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation_name.as_deref(), + ); + let mut cellpicker = luminol_components::Cellpicker::new( + &update_state.graphics, + atlas, + Some(luminol_graphics::primitives::cells::ANIMATION_COLUMNS), + 1., + ); + cellpicker.view.display.set_hue( + &update_state.graphics.render_state, + animation_hue as f32 / 360., + ); + cellpicker + } + + fn show_window( + &mut self, + update_state: &mut luminol_core::UpdateState<'_>, + ctx: &egui::Context, + data: &mut rpg::Animation, + ) -> bool { + let mut win_open = true; + let mut keep_open = true; + let mut needs_save = false; + + let State::Open { + entries, + filtered_entries, + search_text, + cellpicker, + } = &mut self.state + else { + return false; + }; + + egui::Window::new("Animation Graphic Picker") + .resizable(true) + .open(&mut win_open) + .id(self.id_source.with("window")) + .show(ctx, |ui| { + egui::SidePanel::left(self.id_source.with("sidebar")).show_inside(ui, |ui| { + let out = egui::TextEdit::singleline(search_text) + .hint_text("Search 🔎") + .show(ui); + if out.response.changed() { + *filtered_entries = Entry::filter(entries, search_text); + } + + ui.separator(); + + // Get row height. + let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this + // FIXME scroll to selected on first open + ui.with_cross_justify(|ui| { + egui::ScrollArea::vertical() + .auto_shrink([false, true]) + .show_rows( + ui, + row_height, + filtered_entries.len() + 1, + |ui, mut rows| { + if rows.contains(&0) { + let res = ui.selectable_label( + self.animation_name.is_none(), + "(None)", + ); + if res.clicked() && self.animation_name.is_some() { + self.animation_name = None; + *cellpicker = + Self::load_cellpicker(update_state, &None, 0); + } + } + + // subtract 1 to account for (None) + rows.start = rows.start.saturating_sub(1); + rows.end = rows.end.saturating_sub(1); + + for (i, Entry { path, invalid }) in + filtered_entries[rows.clone()].iter_mut().enumerate() + { + let checked = self.animation_name.as_ref() == Some(path); + let mut text = egui::RichText::new(path.as_str()); + if *invalid { + text = text.color(egui::Color32::LIGHT_RED); + } + let faint = (i + rows.start) % 2 == 1; + ui.with_stripe(faint, |ui| { + let res = ui.add_enabled( + !*invalid, + egui::SelectableLabel::new(checked, text), + ); + + if res.clicked() { + if let Some(animation_name) = + &mut self.animation_name + { + animation_name.clone_from(path); + } else { + self.animation_name = Some(path.clone()); + } + *cellpicker = Self::load_cellpicker( + update_state, + &self.animation_name, + self.animation_hue, + ); + } + }); + } + }, + ); + }); + }); + + egui::TopBottomPanel::top(self.id_source.with("top")).show_inside(ui, |ui| { + ui.add_space(1.0); // pad out the top + ui.horizontal(|ui| { + ui.label("Hue"); + if ui + .add(egui::Slider::new(&mut self.animation_hue, 0..=360)) + .changed() + { + cellpicker.view.display.set_hue( + &update_state.graphics.render_state, + self.animation_hue as f32 / 360., + ); + } + }); + ui.add_space(1.0); // pad out the bottom + }); + egui::TopBottomPanel::bottom(self.id_source.with("bottom")).show_inside(ui, |ui| { + ui.add_space(ui.style().spacing.item_spacing.y); + luminol_components::close_options_ui(ui, &mut keep_open, &mut needs_save); + }); + + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::both() + .auto_shrink([false, false]) + .show_viewport(ui, |ui, scroll_rect| { + cellpicker.ui(update_state, ui, scroll_rect); + }); + }); + }); + + if needs_save { + data.animation_name.clone_from(&self.animation_name); + data.animation_hue = self.animation_hue; + } + + if !(win_open && keep_open) { + self.state = State::Closed; + } + + needs_save + } +} diff --git a/crates/modals/src/graphic_picker/basic.rs b/crates/modals/src/graphic_picker/basic.rs index e665410a..99f03a83 100644 --- a/crates/modals/src/graphic_picker/basic.rs +++ b/crates/modals/src/graphic_picker/basic.rs @@ -22,7 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use color_eyre::eyre::Context; +use color_eyre::eyre::WrapErr; use luminol_components::UiExt; use luminol_core::prelude::*; diff --git a/crates/modals/src/graphic_picker/event.rs b/crates/modals/src/graphic_picker/event.rs index f6e62184..aa557b71 100644 --- a/crates/modals/src/graphic_picker/event.rs +++ b/crates/modals/src/graphic_picker/event.rs @@ -22,7 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use color_eyre::eyre::Context; +use color_eyre::eyre::WrapErr; use egui::Widget; use luminol_components::UiExt; use luminol_core::prelude::*; diff --git a/crates/modals/src/graphic_picker/hue.rs b/crates/modals/src/graphic_picker/hue.rs index 2af5a871..8a738819 100644 --- a/crates/modals/src/graphic_picker/hue.rs +++ b/crates/modals/src/graphic_picker/hue.rs @@ -22,7 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use color_eyre::eyre::Context; +use color_eyre::eyre::WrapErr; use luminol_components::UiExt; use luminol_core::prelude::*; diff --git a/crates/modals/src/graphic_picker/mod.rs b/crates/modals/src/graphic_picker/mod.rs index 431114af..14901fec 100644 --- a/crates/modals/src/graphic_picker/mod.rs +++ b/crates/modals/src/graphic_picker/mod.rs @@ -26,6 +26,7 @@ use luminol_components::UiExt; use luminol_core::prelude::*; pub mod actor; +pub mod animation; pub mod basic; pub mod event; pub mod hue; diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index ace6a483..21764e86 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -78,7 +78,7 @@ pub fn show_frame_edit( let atlas = update_state.graphics.atlas_loader.load_animation_atlas( &update_state.graphics, update_state.filesystem, - animation, + animation.animation_name.as_deref(), ); let mut frame_view = luminol_components::AnimationFrameView::new(update_state, atlas); if let Some(battler_name) = &system.battler_name { @@ -117,11 +117,27 @@ pub fn show_frame_edit( cellpicker } else { let atlas = frame_view.frame.atlas.clone(); - let cellpicker = luminol_components::Cellpicker::new(&update_state.graphics, atlas); + let mut cellpicker = + luminol_components::Cellpicker::new(&update_state.graphics, atlas, None, 0.5); + cellpicker.view.display.set_hue( + &update_state.graphics.render_state, + animation.animation_hue as f32 / 360., + ); state.cellpicker = Some(cellpicker); state.cellpicker.as_mut().unwrap() }; + let animation_graphic_picker = if let Some(modal) = &mut state.animation_graphic_picker { + modal + } else { + state.animation_graphic_picker = + Some(luminol_modals::graphic_picker::animation::Modal::new( + animation, + "animation_graphic_picker".into(), + )); + state.animation_graphic_picker.as_mut().unwrap() + }; + // Handle playing of animations if let Some(animation_state) = &mut state.animation_state { let time = ui.input(|i| i.time); @@ -723,6 +739,10 @@ pub fn show_frame_edit( cellpicker.ui(update_state, ui, scroll_rect); }); + let animation_graphic_changed = ui + .add(animation_graphic_picker.button(animation, update_state)) + .changed(); + ui.allocate_ui_at_rect(canvas_rect, |ui| { frame_view.frame.enable_onion_skin = state.enable_onion_skin && state.frame_index != 0 && state.animation_state.is_none(); @@ -862,5 +882,25 @@ pub fn show_frame_edit( } }); + if animation_graphic_changed { + let atlas = update_state.graphics.atlas_loader.load_animation_atlas( + &update_state.graphics, + update_state.filesystem, + animation.animation_name.as_deref(), + ); + frame_view.frame.atlas = atlas.clone(); + frame_view + .frame + .rebuild_all_cells(&update_state.graphics, animation, state.frame_index); + let mut cellpicker = + luminol_components::Cellpicker::new(&update_state.graphics, atlas, None, 0.5); + cellpicker.view.display.set_hue( + &update_state.graphics.render_state, + animation.animation_hue as f32 / 360., + ); + state.cellpicker = Some(cellpicker); + modified = true; + } + modified } diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs index 65d83a25..1a789154 100644 --- a/crates/ui/src/windows/animations/mod.rs +++ b/crates/ui/src/windows/animations/mod.rs @@ -47,6 +47,7 @@ struct FrameEditState { enable_onion_skin: bool, frame_view: Option, cellpicker: Option, + animation_graphic_picker: Option, flash_maps: luminol_data::OptionVec, animation_state: Option, saved_frame_index: Option, @@ -95,6 +96,7 @@ impl Default for Window { enable_onion_skin: false, frame_view: None, cellpicker: None, + animation_graphic_picker: None, flash_maps: Default::default(), animation_state: None, saved_frame_index: None, diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index d4f6c45a..6d67de23 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -23,6 +23,7 @@ // Program grant you additional permission to convey the resulting work. use luminol_components::UiExt; +use luminol_core::Modal; use strum::IntoEnumIterator; use super::util::update_flash_maps; @@ -192,8 +193,15 @@ impl luminol_core::Window for super::Window { .frame_index .min(animation.frames.len().saturating_sub(1)); - // Stop the currently playing animation + if let Some(modal) = + &mut self.frame_edit_state.animation_graphic_picker + { + // reset the modal if the animation has changed (this is practically a no-op) + modal.reset(update_state, animation); + } + if self.frame_edit_state.animation_state.is_some() { + // Stop the currently playing animation let animation_state = self.frame_edit_state.animation_state.take().unwrap(); self.frame_edit_state.frame_index = @@ -204,7 +212,7 @@ impl luminol_core::Window for super::Window { update_state.graphics.atlas_loader.load_animation_atlas( &update_state.graphics, update_state.filesystem, - animation, + animation.animation_name.as_deref(), ); if let Some(frame_view) = &mut self.frame_edit_state.frame_view { @@ -243,6 +251,12 @@ impl luminol_core::Window for super::Window { let mut cellpicker = luminol_components::Cellpicker::new( &update_state.graphics, atlas, + None, + 0.5, + ); + cellpicker.view.display.set_hue( + &update_state.graphics.render_state, + animation.animation_hue as f32 / 360., ); cellpicker.selected_cell = selected_cell; self.frame_edit_state.cellpicker = Some(cellpicker); From 959e51d5d66d2f0cc757b64e29a52fde093c9b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 12:34:53 -0400 Subject: [PATCH 095/109] Use desensitization in event and animation graphic pickers This commit applies the changes from 2aff3dcc73d7a8ca88ff75e3a00638aeb45d4a92 to the event graphic picker and the animation graphic picker. I previously forgot to do that. --- crates/components/src/sound_tab.rs | 21 +-- crates/modals/src/graphic_picker/animation.rs | 26 ++- crates/modals/src/graphic_picker/event.rs | 171 +++++++++++++----- crates/modals/src/graphic_picker/mod.rs | 4 +- 4 files changed, 154 insertions(+), 68 deletions(-) diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index bf1451e0..399df8f0 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -186,7 +186,7 @@ impl SoundTab { .join(name), ) .ok() - .map(|path| path.file_name().unwrap().to_string()) + .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap())) }); egui::ScrollArea::both() @@ -217,21 +217,18 @@ impl SoundTab { { let faint = (i + row_range.start) % 2 == 0; let res = ui.with_stripe(faint, |ui| { - let entry_name = entry.file_name(); + let entry_name = camino::Utf8Path::new(entry.file_name()); let res = ui.add(egui::SelectableLabel::new( audio_file_name.as_deref() == Some(entry_name), - entry_name, + entry_name.as_str(), )); if res.clicked() { - self.audio_file.name = if let Some((file_stem, _)) = - entry_name.rsplit_once('.') - { - Some(file_stem.into()) - } else { - audio_file_name - .as_ref() - .map(|name| name.clone().into()) - }; + self.audio_file.name = Some( + entry_name + .file_stem() + .unwrap_or(entry_name.as_str()) + .into(), + ); } res }); diff --git a/crates/modals/src/graphic_picker/animation.rs b/crates/modals/src/graphic_picker/animation.rs index 6aa6a588..49a73d52 100644 --- a/crates/modals/src/graphic_picker/animation.rs +++ b/crates/modals/src/graphic_picker/animation.rs @@ -66,8 +66,8 @@ impl luminol_core::Modal for Modal { move |ui: &mut egui::Ui| { let is_open = matches!(self.state, State::Open { .. }); - let button_text = if let Some(track) = &data.animation_name { - format!("Graphics/Animations/{}", track) + let button_text = if let Some(name) = &data.animation_name { + format!("Graphics/Animations/{name}") } else { "(None)".to_string() }; @@ -146,6 +146,14 @@ impl Modal { return false; }; + let animation_name = self.animation_name.as_ref().and_then(|name| { + update_state + .filesystem + .desensitize(format!("Graphics/Animations/{name}")) + .ok() + .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap_or_default())) + }); + egui::Window::new("Animation Graphic Picker") .resizable(true) .open(&mut win_open) @@ -191,7 +199,7 @@ impl Modal { for (i, Entry { path, invalid }) in filtered_entries[rows.clone()].iter_mut().enumerate() { - let checked = self.animation_name.as_ref() == Some(path); + let checked = animation_name.as_ref() == Some(path); let mut text = egui::RichText::new(path.as_str()); if *invalid { text = text.color(egui::Color32::LIGHT_RED); @@ -204,13 +212,11 @@ impl Modal { ); if res.clicked() { - if let Some(animation_name) = - &mut self.animation_name - { - animation_name.clone_from(path); - } else { - self.animation_name = Some(path.clone()); - } + self.animation_name = Some( + path.file_stem() + .unwrap_or(path.as_str()) + .into(), + ); *cellpicker = Self::load_cellpicker( update_state, &self.animation_name, diff --git a/crates/modals/src/graphic_picker/event.rs b/crates/modals/src/graphic_picker/event.rs index aa557b71..6f3deb5f 100644 --- a/crates/modals/src/graphic_picker/event.rs +++ b/crates/modals/src/graphic_picker/event.rs @@ -69,7 +69,6 @@ enum Selected { }, } -// FIXME DEAR GOD THE FORMATTING impl Modal { pub fn new( update_state: &UpdateState<'_>, @@ -299,6 +298,15 @@ impl Modal { return false; }; + let event_name = match selected { + Selected::Graphic { path, .. } => update_state + .filesystem + .desensitize(format!("Graphics/Characters/{path}")) + .ok() + .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap_or_default())), + Selected::None | Selected::Tile { .. } => None, + }; + egui::Window::new("Event Graphic Picker") .resizable(true) .open(&mut win_open) @@ -316,7 +324,7 @@ impl Modal { // Get row height. let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this - // FIXME scroll to selected on first open + // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { egui::ScrollArea::vertical() .auto_shrink([false, true]) @@ -326,19 +334,28 @@ impl Modal { filtered_entries.len() + 2, |ui, mut rows| { if rows.contains(&0) { - let res = ui.selectable_label(matches!(selected, Selected::None), "(None)"); + let res = ui.selectable_label( + matches!(selected, Selected::None), + "(None)", + ); if res.clicked() && !matches!(selected, Selected::None) { *selected = Selected::None; } } if rows.contains(&1) { - let checked = matches!(selected, Selected::Tile {..}); + let checked = matches!(selected, Selected::Tile { .. }); ui.with_stripe(true, |ui| { - let res = ui.selectable_label(checked, "(Tileset)"); + let res = ui.selectable_label(checked, "(Tileset)"); if res.clicked() && !checked { - let tilepicker = Self::load_tilepicker(update_state, self.tileset_id); - *selected = Selected::Tile { tile_id: 384, tilepicker }; + let tilepicker = Self::load_tilepicker( + update_state, + self.tileset_id, + ); + *selected = Selected::Tile { + tile_id: 384, + tilepicker, + }; } }); } @@ -347,31 +364,52 @@ impl Modal { rows.start = rows.start.saturating_sub(2); rows.end = rows.end.saturating_sub(2); - for (i, Entry { path ,invalid}) in filtered_entries[rows.clone()].iter_mut().enumerate() { - let checked = - matches!(selected, Selected::Graphic { path: p, .. } if p == path); + for (i, Entry { path, invalid }) in + filtered_entries[rows.clone()].iter_mut().enumerate() + { + let checked = event_name.as_ref() == Some(path); let mut text = egui::RichText::new(path.as_str()); if *invalid { text = text.color(egui::Color32::LIGHT_RED); } let faint = (i + rows.start) % 2 == 1; ui.with_stripe(faint, |ui| { - let res = ui.add_enabled(!*invalid, egui::SelectableLabel::new(checked, text)); + let res = ui.add_enabled( + !*invalid, + egui::SelectableLabel::new(checked, text), + ); if res.clicked() { - let sprite = match Self::load_preview_sprite(update_state, path, *hue, *opacity) { + let name = camino::Utf8PathBuf::from( + path.file_stem().unwrap_or(path.as_str()), + ); + let sprite = match Self::load_preview_sprite( + update_state, + &name, + *hue, + *opacity, + ) { Ok(sprite) => sprite, Err(e) => { - luminol_core::error!(update_state.toasts, e); + luminol_core::error!( + update_state.toasts, + e + ); *invalid = true; // FIXME update non-filtered entry too return; } }; - *selected = Selected::Graphic { path: path.clone(), direction: 2, pattern: 0, sprite }; + *selected = Selected::Graphic { + path: name, + direction: 2, + pattern: 0, + sprite, + }; } }); } - }); + }, + ); }); }); @@ -381,24 +419,33 @@ impl Modal { ui.label("Opacity"); if ui.add(egui::Slider::new(opacity, 0..=255)).changed() { match selected { - Selected::Graphic { sprite,.. } => { - sprite.sprite.graphic.set_opacity(&update_state.graphics.render_state, *opacity) - }, - Selected::Tile { tilepicker,.. } => { - tilepicker.tiles.display.set_opacity(&update_state.graphics.render_state, *opacity as f32 / 255., 0) + Selected::Graphic { sprite, .. } => sprite + .sprite + .graphic + .set_opacity(&update_state.graphics.render_state, *opacity), + Selected::Tile { tilepicker, .. } => { + tilepicker.tiles.display.set_opacity( + &update_state.graphics.render_state, + *opacity as f32 / 255., + 0, + ) } Selected::None => {} } - } ui.label("Hue"); if ui.add(egui::Slider::new(hue, 0..=360)).changed() { match selected { - Selected::Graphic { sprite,.. } => { - sprite.sprite.graphic.set_hue(&update_state.graphics.render_state, *hue) - }, - Selected::Tile { tilepicker,.. } => { - tilepicker.tiles.display.set_hue(&update_state.graphics.render_state, *hue as f32 / 360., 0) + Selected::Graphic { sprite, .. } => sprite + .sprite + .graphic + .set_hue(&update_state.graphics.render_state, *hue), + Selected::Tile { tilepicker, .. } => { + tilepicker.tiles.display.set_hue( + &update_state.graphics.render_state, + *hue as f32 / 360., + 0, + ) } Selected::None => {} } @@ -406,7 +453,11 @@ impl Modal { }); ui.horizontal(|ui| { ui.label("Blend Mode"); - luminol_components::EnumComboBox::new(self.id_source.with("blend_mode"), blend_mode).ui(ui); + luminol_components::EnumComboBox::new( + self.id_source.with("blend_mode"), + blend_mode, + ) + .ui(ui); }); ui.add_space(1.0); // pad out the bottom }); @@ -416,27 +467,46 @@ impl Modal { }); egui::CentralPanel::default().show_inside(ui, |ui| { - egui::ScrollArea::both().auto_shrink([false,false]).show_viewport(ui, |ui, viewport| { - match selected { + egui::ScrollArea::both() + .auto_shrink([false, false]) + .show_viewport(ui, |ui, viewport| match selected { Selected::None => {} - Selected::Graphic { direction, pattern, sprite, .. } => { + Selected::Graphic { + direction, + pattern, + sprite, + .. + } => { let response = sprite.ui(ui, viewport, update_state); let ch = sprite.sprite_size.y / 4.; let cw = sprite.sprite_size.x / 4.; - let min = egui::pos2(*pattern as f32 * cw, (*direction as f32 - 2.) * ch / 2.); + let min = egui::pos2( + *pattern as f32 * cw, + (*direction as f32 - 2.) * ch / 2., + ); let size = egui::vec2(cw, ch); - let rect = egui::Rect::from_min_size(min, size).translate(response.rect.min.to_vec2()); - ui.painter().rect_stroke(rect, 5.0, egui::Stroke::new(1.0, egui::Color32::WHITE)); + let rect = egui::Rect::from_min_size(min, size) + .translate(response.rect.min.to_vec2()); + ui.painter().rect_stroke( + rect, + 5.0, + egui::Stroke::new(1.0, egui::Color32::WHITE), + ); if response.clicked() { - let pos = (response.interact_pointer_pos().unwrap() - response.rect.min) / egui::vec2(cw, ch); + let pos = (response.interact_pointer_pos().unwrap() + - response.rect.min) + / egui::vec2(cw, ch); *direction = pos.y as i32 * 2 + 2; *pattern = pos.x as i32; } } - Selected::Tile { tile_id, tilepicker } => { + Selected::Tile { + tile_id, + tilepicker, + } => { let (canvas_rect, response) = ui.allocate_exact_size( egui::vec2(256., tilepicker.atlas.tileset_height() as f32), egui::Sense::click(), @@ -446,7 +516,8 @@ impl Modal { .ctx() .screen_rect() .intersect(viewport.translate(canvas_rect.min.to_vec2())); - let scroll_rect = absolute_scroll_rect.translate(-canvas_rect.min.to_vec2()); + let scroll_rect = + absolute_scroll_rect.translate(-canvas_rect.min.to_vec2()); tilepicker.grid.display.set_pixels_per_point( &update_state.graphics.render_state, @@ -464,10 +535,13 @@ impl Modal { glam::Vec2::ONE, ); - tilepicker - .update_animation(&update_state.graphics.render_state, ui.input(|i| i.time)); + tilepicker.update_animation( + &update_state.graphics.render_state, + ui.input(|i| i.time), + ); - let painter = Painter::new(tilepicker.prepare(&update_state.graphics)); + let painter = + Painter::new(tilepicker.prepare(&update_state.graphics)); ui.painter() .add(luminol_egui_wgpu::Callback::new_paint_callback( absolute_scroll_rect, @@ -476,16 +550,25 @@ impl Modal { let tile_x = (*tile_id - 384) % 8; let tile_y = (*tile_id - 384) / 8; - let rect = egui::Rect::from_min_size(egui::Pos2::new(tile_x as f32, tile_y as f32) * 32., egui::Vec2::splat(32.)).translate(canvas_rect.min.to_vec2()); - ui.painter().rect_stroke(rect, 5.0, egui::Stroke::new(1.0, egui::Color32::WHITE)); + let rect = egui::Rect::from_min_size( + egui::Pos2::new(tile_x as f32, tile_y as f32) * 32., + egui::Vec2::splat(32.), + ) + .translate(canvas_rect.min.to_vec2()); + ui.painter().rect_stroke( + rect, + 5.0, + egui::Stroke::new(1.0, egui::Color32::WHITE), + ); if response.clicked() { - let pos = (response.interact_pointer_pos().unwrap() - response.rect.min) / 32.; + let pos = (response.interact_pointer_pos().unwrap() + - response.rect.min) + / 32.; *tile_id = pos.x as usize + pos.y as usize * 8 + 384; } } - } - }); + }); }); }); diff --git a/crates/modals/src/graphic_picker/mod.rs b/crates/modals/src/graphic_picker/mod.rs index 14901fec..7bb16032 100644 --- a/crates/modals/src/graphic_picker/mod.rs +++ b/crates/modals/src/graphic_picker/mod.rs @@ -139,12 +139,12 @@ impl Entry { .filesystem .desensitize(directory.join(path)) .ok() - .map(|path| path.file_name().unwrap_or_default().to_string()), + .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap_or_default())), Selected::None => None, }; for i in entries[rows.clone()].iter_mut().enumerate() { let (i, Self { path, invalid }) = i; - let checked = selected_name.as_deref() == Some(path.as_str()); + let checked = selected_name.as_deref() == Some(path); let mut text = egui::RichText::new(path.file_name().unwrap_or_default()); if *invalid { text = text.color(egui::Color32::LIGHT_RED); From cc077f2e9916d9c93b8376a5f9a1aad518a6e6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 13:19:29 -0400 Subject: [PATCH 096/109] Fix modal row height calculation --- crates/components/src/sound_tab.rs | 4 +++- crates/modals/src/graphic_picker/actor.rs | 9 ++++++--- crates/modals/src/graphic_picker/animation.rs | 7 +++++-- crates/modals/src/graphic_picker/basic.rs | 9 ++++++--- crates/modals/src/graphic_picker/event.rs | 7 +++++-- crates/modals/src/graphic_picker/hue.rs | 9 ++++++--- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index 399df8f0..b711e731 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -148,7 +148,9 @@ impl SoundTab { egui::CentralPanel::default().show_inside(ui, |ui| { // Get row height. - let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this + let row_height = ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + 2. * ui.spacing().button_padding.y, + ); let persistence_id = update_state .project_config diff --git a/crates/modals/src/graphic_picker/actor.rs b/crates/modals/src/graphic_picker/actor.rs index c2b82177..6d16b268 100644 --- a/crates/modals/src/graphic_picker/actor.rs +++ b/crates/modals/src/graphic_picker/actor.rs @@ -253,8 +253,11 @@ impl Modal { ui.separator(); // Get row height. - let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this - // FIXME scroll to selected on first open + let row_height = ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + ); + // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { egui::ScrollArea::vertical() .auto_shrink([false, true]) @@ -273,7 +276,7 @@ impl Modal { } } - // subtract 2 to account for (None) + // subtract 1 to account for (None) rows.start = rows.start.saturating_sub(1); rows.end = rows.end.saturating_sub(1); diff --git a/crates/modals/src/graphic_picker/animation.rs b/crates/modals/src/graphic_picker/animation.rs index 49a73d52..5780b1e4 100644 --- a/crates/modals/src/graphic_picker/animation.rs +++ b/crates/modals/src/graphic_picker/animation.rs @@ -170,8 +170,11 @@ impl Modal { ui.separator(); // Get row height. - let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this - // FIXME scroll to selected on first open + let row_height = ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + ); + // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { egui::ScrollArea::vertical() .auto_shrink([false, true]) diff --git a/crates/modals/src/graphic_picker/basic.rs b/crates/modals/src/graphic_picker/basic.rs index 99f03a83..0cca5751 100644 --- a/crates/modals/src/graphic_picker/basic.rs +++ b/crates/modals/src/graphic_picker/basic.rs @@ -230,8 +230,11 @@ impl Modal { ui.separator(); // Get row height. - let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this - // FIXME scroll to selected on first open + let row_height = ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + ); + // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { egui::ScrollArea::vertical() .auto_shrink([false, true]) @@ -250,7 +253,7 @@ impl Modal { } } - // subtract 2 to account for (None) + // subtract 1 to account for (None) rows.start = rows.start.saturating_sub(1); rows.end = rows.end.saturating_sub(1); diff --git a/crates/modals/src/graphic_picker/event.rs b/crates/modals/src/graphic_picker/event.rs index 6f3deb5f..3f4cc174 100644 --- a/crates/modals/src/graphic_picker/event.rs +++ b/crates/modals/src/graphic_picker/event.rs @@ -323,8 +323,11 @@ impl Modal { ui.separator(); // Get row height. - let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this - // FIXME scroll to selected on first open + let row_height = ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + ); + // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { egui::ScrollArea::vertical() .auto_shrink([false, true]) diff --git a/crates/modals/src/graphic_picker/hue.rs b/crates/modals/src/graphic_picker/hue.rs index 8a738819..16e03174 100644 --- a/crates/modals/src/graphic_picker/hue.rs +++ b/crates/modals/src/graphic_picker/hue.rs @@ -236,8 +236,11 @@ impl Modal { ui.separator(); // Get row height. - let row_height = ui.text_style_height(&egui::TextStyle::Body); // i do not trust this - // FIXME scroll to selected on first open + let row_height = ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + ); + // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { egui::ScrollArea::vertical() .auto_shrink([false, true]) @@ -256,7 +259,7 @@ impl Modal { } } - // subtract 2 to account for (None) + // subtract 1 to account for (None) rows.start = rows.start.saturating_sub(1); rows.end = rows.end.saturating_sub(1); From aceef70285fe9ecb8f8f0cffc84bd6517a083926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 16:32:47 -0400 Subject: [PATCH 097/109] Scroll sound and graphic pickers on first open --- crates/modals/src/graphic_picker/actor.rs | 49 +++++++++++++++--- crates/modals/src/graphic_picker/animation.rs | 46 ++++++++++++++--- crates/modals/src/graphic_picker/basic.rs | 49 +++++++++++++++--- crates/modals/src/graphic_picker/event.rs | 50 ++++++++++++++++--- crates/modals/src/graphic_picker/hue.rs | 49 +++++++++++++++--- crates/modals/src/graphic_picker/mod.rs | 26 +++++++++- 6 files changed, 236 insertions(+), 33 deletions(-) diff --git a/crates/modals/src/graphic_picker/actor.rs b/crates/modals/src/graphic_picker/actor.rs index 6d16b268..4395beab 100644 --- a/crates/modals/src/graphic_picker/actor.rs +++ b/crates/modals/src/graphic_picker/actor.rs @@ -36,6 +36,8 @@ pub struct Modal { directory: camino::Utf8PathBuf, // do we make this &'static Utf8Path? button_sprite: Option, + + scrolled_on_first_open: bool, } enum State { @@ -91,6 +93,7 @@ impl Modal { button_size, directory, button_sprite, + scrolled_on_first_open: false, } } } @@ -147,6 +150,7 @@ impl luminol_core::Modal for Modal { fn reset(&mut self, update_state: &mut luminol_core::UpdateState<'_>, data: Self::Data<'_>) { self.update_graphic(update_state, data); // we need to update the button sprite to prevent desyncs self.state = State::Closed; + self.scrolled_on_first_open = false; } } @@ -234,6 +238,7 @@ impl Modal { selected, } = &mut self.state else { + self.scrolled_on_first_open = false; return false; }; @@ -257,9 +262,8 @@ impl Modal { ui.text_style_height(&egui::TextStyle::Button) + 2. * ui.spacing().button_padding.y, ); - // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { - egui::ScrollArea::vertical() + let mut scroll_area_output = egui::ScrollArea::vertical() .auto_shrink([false, true]) .show_rows( ui, @@ -267,10 +271,8 @@ impl Modal { filtered_entries.len() + 1, |ui, mut rows| { if rows.contains(&0) { - let res = ui.selectable_label( - matches!(selected, Selected::None), - "(None)", - ); + let checked = matches!(selected, Selected::None); + let res = ui.selectable_label(checked, "(None)"); if res.clicked() && !matches!(selected, Selected::None) { *selected = Selected::None; } @@ -298,6 +300,40 @@ impl Modal { ) }, ); + + // Scroll the selected item into view + if !self.scrolled_on_first_open { + let row = if matches!(selected, Selected::None) { + Some(0) + } else { + Entry::find_matching_entry( + filtered_entries, + &self.directory, + update_state, + selected, + ) + .map(|i| i + 1) + }; + if let Some(row) = row { + let spacing = ui.spacing().item_spacing.y; + let max = row as f32 * (row_height + spacing) + spacing; + let min = row as f32 * (row_height + spacing) + row_height + - spacing + - scroll_area_output.inner_rect.height(); + if scroll_area_output.state.offset.y > max { + scroll_area_output.state.offset.y = max; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } else if scroll_area_output.state.offset.y < min { + scroll_area_output.state.offset.y = min; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } + } + self.scrolled_on_first_open = true; + } }); }); @@ -349,6 +385,7 @@ impl Modal { if !(win_open && keep_open) { self.state = State::Closed; + self.scrolled_on_first_open = false; } needs_save diff --git a/crates/modals/src/graphic_picker/animation.rs b/crates/modals/src/graphic_picker/animation.rs index 5780b1e4..463a6374 100644 --- a/crates/modals/src/graphic_picker/animation.rs +++ b/crates/modals/src/graphic_picker/animation.rs @@ -32,6 +32,7 @@ pub struct Modal { id_source: egui::Id, animation_name: Option, animation_hue: i32, + scrolled_on_first_open: bool, } enum State { @@ -51,6 +52,7 @@ impl Modal { id_source, animation_name: animation.animation_name.clone(), animation_hue: animation.animation_hue, + scrolled_on_first_open: false, } } } @@ -99,6 +101,7 @@ impl luminol_core::Modal for Modal { self.animation_name.clone_from(&data.animation_name); self.animation_hue = data.animation_hue; self.state = State::Closed; + self.scrolled_on_first_open = false; } } @@ -143,6 +146,7 @@ impl Modal { cellpicker, } = &mut self.state else { + self.scrolled_on_first_open = false; return false; }; @@ -174,9 +178,8 @@ impl Modal { ui.text_style_height(&egui::TextStyle::Button) + 2. * ui.spacing().button_padding.y, ); - // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { - egui::ScrollArea::vertical() + let mut scroll_area_output = egui::ScrollArea::vertical() .auto_shrink([false, true]) .show_rows( ui, @@ -184,10 +187,8 @@ impl Modal { filtered_entries.len() + 1, |ui, mut rows| { if rows.contains(&0) { - let res = ui.selectable_label( - self.animation_name.is_none(), - "(None)", - ); + let checked = self.animation_name.is_none(); + let res = ui.selectable_label(checked, "(None)"); if res.clicked() && self.animation_name.is_some() { self.animation_name = None; *cellpicker = @@ -207,7 +208,7 @@ impl Modal { if *invalid { text = text.color(egui::Color32::LIGHT_RED); } - let faint = (i + rows.start) % 2 == 1; + let faint = (i + rows.start) % 2 == 0; ui.with_stripe(faint, |ui| { let res = ui.add_enabled( !*invalid, @@ -230,6 +231,36 @@ impl Modal { } }, ); + + // Scroll the selected item into view + if !self.scrolled_on_first_open { + let row = if self.animation_name.is_none() { + Some(0) + } else { + filtered_entries.iter().enumerate().find_map(|(i, entry)| { + (animation_name.as_ref() == Some(&entry.path)).then_some(i + 1) + }) + }; + if let Some(row) = row { + let spacing = ui.spacing().item_spacing.y; + let max = row as f32 * (row_height + spacing) + spacing; + let min = row as f32 * (row_height + spacing) + row_height + - spacing + - scroll_area_output.inner_rect.height(); + if scroll_area_output.state.offset.y > max { + scroll_area_output.state.offset.y = max; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } else if scroll_area_output.state.offset.y < min { + scroll_area_output.state.offset.y = min; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } + } + self.scrolled_on_first_open = true; + } }); }); @@ -270,6 +301,7 @@ impl Modal { if !(win_open && keep_open) { self.state = State::Closed; + self.scrolled_on_first_open = false; } needs_save diff --git a/crates/modals/src/graphic_picker/basic.rs b/crates/modals/src/graphic_picker/basic.rs index 0cca5751..cde30aad 100644 --- a/crates/modals/src/graphic_picker/basic.rs +++ b/crates/modals/src/graphic_picker/basic.rs @@ -36,6 +36,8 @@ pub struct Modal { directory: camino::Utf8PathBuf, // do we make this &'static Utf8Path? button_sprite: Option, + + scrolled_on_first_open: bool, } enum State { @@ -79,6 +81,7 @@ impl Modal { button_size, directory, button_sprite, + scrolled_on_first_open: false, } } } @@ -134,6 +137,7 @@ impl luminol_core::Modal for Modal { fn reset(&mut self, update_state: &mut luminol_core::UpdateState<'_>, data: Self::Data<'_>) { self.update_graphic(update_state, data); // we need to update the button sprite to prevent desyncs self.state = State::Closed; + self.scrolled_on_first_open = false; } } @@ -211,6 +215,7 @@ impl Modal { selected, } = &mut self.state else { + self.scrolled_on_first_open = false; return false; }; @@ -234,9 +239,8 @@ impl Modal { ui.text_style_height(&egui::TextStyle::Button) + 2. * ui.spacing().button_padding.y, ); - // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { - egui::ScrollArea::vertical() + let mut scroll_area_output = egui::ScrollArea::vertical() .auto_shrink([false, true]) .show_rows( ui, @@ -244,10 +248,8 @@ impl Modal { filtered_entries.len() + 1, |ui, mut rows| { if rows.contains(&0) { - let res = ui.selectable_label( - matches!(selected, Selected::None), - "(None)", - ); + let checked = matches!(selected, Selected::None); + let res = ui.selectable_label(checked, "(None)"); if res.clicked() && !matches!(selected, Selected::None) { *selected = Selected::None; } @@ -275,6 +277,40 @@ impl Modal { ) }, ); + + // Scroll the selected item into view + if !self.scrolled_on_first_open { + let row = if matches!(selected, Selected::None) { + Some(0) + } else { + Entry::find_matching_entry( + filtered_entries, + &self.directory, + update_state, + selected, + ) + .map(|i| i + 1) + }; + if let Some(row) = row { + let spacing = ui.spacing().item_spacing.y; + let max = row as f32 * (row_height + spacing) + spacing; + let min = row as f32 * (row_height + spacing) + row_height + - spacing + - scroll_area_output.inner_rect.height(); + if scroll_area_output.state.offset.y > max { + scroll_area_output.state.offset.y = max; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } else if scroll_area_output.state.offset.y < min { + scroll_area_output.state.offset.y = min; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } + } + self.scrolled_on_first_open = true; + } }); }); @@ -306,6 +342,7 @@ impl Modal { if !(win_open && keep_open) { self.state = State::Closed; + self.scrolled_on_first_open = false; } needs_save diff --git a/crates/modals/src/graphic_picker/event.rs b/crates/modals/src/graphic_picker/event.rs index 3f4cc174..84d48514 100644 --- a/crates/modals/src/graphic_picker/event.rs +++ b/crates/modals/src/graphic_picker/event.rs @@ -36,6 +36,8 @@ pub struct Modal { tileset_id: usize, button_sprite: Option, + + scrolled_on_first_open: bool, } enum State { @@ -100,6 +102,8 @@ impl Modal { tileset_id, button_sprite, + + scrolled_on_first_open: false, } } } @@ -184,6 +188,7 @@ impl luminol_core::Modal for Modal { fn reset(&mut self, update_state: &mut UpdateState<'_>, data: Self::Data<'_>) { self.update_graphic(update_state, data); // we need to update the button sprite to prevent desyncs self.state = State::Closed; + self.scrolled_on_first_open = false; } } @@ -295,6 +300,7 @@ impl Modal { blend_mode, } = &mut self.state else { + self.scrolled_on_first_open = false; return false; }; @@ -327,9 +333,8 @@ impl Modal { ui.text_style_height(&egui::TextStyle::Button) + 2. * ui.spacing().button_padding.y, ); - // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { - egui::ScrollArea::vertical() + let mut scroll_area_output = egui::ScrollArea::vertical() .auto_shrink([false, true]) .show_rows( ui, @@ -337,18 +342,16 @@ impl Modal { filtered_entries.len() + 2, |ui, mut rows| { if rows.contains(&0) { - let res = ui.selectable_label( - matches!(selected, Selected::None), - "(None)", - ); + let checked = matches!(selected, Selected::None); + let res = ui.selectable_label(checked, "(None)"); if res.clicked() && !matches!(selected, Selected::None) { *selected = Selected::None; } } if rows.contains(&1) { - let checked = matches!(selected, Selected::Tile { .. }); ui.with_stripe(true, |ui| { + let checked = matches!(selected, Selected::Tile { .. }); let res = ui.selectable_label(checked, "(Tileset)"); if res.clicked() && !checked { let tilepicker = Self::load_tilepicker( @@ -413,6 +416,38 @@ impl Modal { } }, ); + + // Scroll the selected item into view + if !self.scrolled_on_first_open { + let row = match selected { + Selected::None => Some(0), + Selected::Tile { .. } => Some(1), + Selected::Graphic { .. } => { + filtered_entries.iter().enumerate().find_map(|(i, entry)| { + (event_name.as_ref() == Some(&entry.path)).then_some(i + 2) + }) + } + }; + if let Some(row) = row { + let spacing = ui.spacing().item_spacing.y; + let max = row as f32 * (row_height + spacing) + spacing; + let min = row as f32 * (row_height + spacing) + row_height + - spacing + - scroll_area_output.inner_rect.height(); + if scroll_area_output.state.offset.y > max { + scroll_area_output.state.offset.y = max; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } else if scroll_area_output.state.offset.y < min { + scroll_area_output.state.offset.y = min; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } + } + self.scrolled_on_first_open = true; + } }); }); @@ -605,6 +640,7 @@ impl Modal { if !(win_open && keep_open) { self.state = State::Closed; + self.scrolled_on_first_open = false; } needs_save diff --git a/crates/modals/src/graphic_picker/hue.rs b/crates/modals/src/graphic_picker/hue.rs index 16e03174..e4613971 100644 --- a/crates/modals/src/graphic_picker/hue.rs +++ b/crates/modals/src/graphic_picker/hue.rs @@ -36,6 +36,8 @@ pub struct Modal { directory: camino::Utf8PathBuf, // do we make this &'static Utf8Path? button_sprite: Option, + + scrolled_on_first_open: bool, } enum State { @@ -82,6 +84,7 @@ impl Modal { button_size, directory, button_sprite, + scrolled_on_first_open: false, } } } @@ -138,6 +141,7 @@ impl luminol_core::Modal for Modal { fn reset(&mut self, update_state: &mut luminol_core::UpdateState<'_>, data: Self::Data<'_>) { self.update_graphic(update_state, data); // we need to update the button sprite to prevent desyncs self.state = State::Closed; + self.scrolled_on_first_open = false; } } @@ -217,6 +221,7 @@ impl Modal { selected, } = &mut self.state else { + self.scrolled_on_first_open = false; return false; }; @@ -240,9 +245,8 @@ impl Modal { ui.text_style_height(&egui::TextStyle::Button) + 2. * ui.spacing().button_padding.y, ); - // FIXME scroll to selected on first open ui.with_cross_justify(|ui| { - egui::ScrollArea::vertical() + let mut scroll_area_output = egui::ScrollArea::vertical() .auto_shrink([false, true]) .show_rows( ui, @@ -250,10 +254,8 @@ impl Modal { filtered_entries.len() + 1, |ui, mut rows| { if rows.contains(&0) { - let res = ui.selectable_label( - matches!(selected, Selected::None), - "(None)", - ); + let checked = matches!(selected, Selected::None); + let res = ui.selectable_label(checked, "(None)"); if res.clicked() && !matches!(selected, Selected::None) { *selected = Selected::None; } @@ -281,6 +283,40 @@ impl Modal { ) }, ); + + // Scroll the selected item into view + if !self.scrolled_on_first_open { + let row = if matches!(selected, Selected::None) { + Some(0) + } else { + Entry::find_matching_entry( + filtered_entries, + &self.directory, + update_state, + selected, + ) + .map(|i| i + 1) + }; + if let Some(row) = row { + let spacing = ui.spacing().item_spacing.y; + let max = row as f32 * (row_height + spacing) + spacing; + let min = row as f32 * (row_height + spacing) + row_height + - spacing + - scroll_area_output.inner_rect.height(); + if scroll_area_output.state.offset.y > max { + scroll_area_output.state.offset.y = max; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } else if scroll_area_output.state.offset.y < min { + scroll_area_output.state.offset.y = min; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } + } + self.scrolled_on_first_open = true; + } }); }); @@ -332,6 +368,7 @@ impl Modal { if !(win_open && keep_open) { self.state = State::Closed; + self.scrolled_on_first_open = false; } needs_save diff --git a/crates/modals/src/graphic_picker/mod.rs b/crates/modals/src/graphic_picker/mod.rs index 7bb16032..af595789 100644 --- a/crates/modals/src/graphic_picker/mod.rs +++ b/crates/modals/src/graphic_picker/mod.rs @@ -149,7 +149,7 @@ impl Entry { if *invalid { text = text.color(egui::Color32::LIGHT_RED); } - let faint = (i + rows.start) % 2 == 1; + let faint = (i + rows.start) % 2 == 0; ui.with_stripe(faint, |ui| { let res = ui.add_enabled(!*invalid, egui::SelectableLabel::new(checked, text)); @@ -162,6 +162,30 @@ impl Entry { }); } } + + fn find_matching_entry( + entries: &[Self], + directory: &camino::Utf8Path, + update_state: &UpdateState<'_>, + selected: &Selected, + ) -> Option { + let selected_name = match &selected { + Selected::Entry { path, .. } => update_state + .filesystem + .desensitize(directory.join(path)) + .ok() + .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap_or_default())), + Selected::None => None, + }; + for i in entries.iter().enumerate() { + let (i, Self { path, .. }) = i; + let checked = selected_name.as_deref() == Some(path); + if checked { + return Some(i); + } + } + None + } } impl PreviewSprite { From b590ae929b334a5d11185ee9681f8af395f06473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 18:44:13 -0400 Subject: [PATCH 098/109] Add more tools to the animation editor --- .../src/animations/change_cell_number_tool.rs | 168 ++++++++++++++++++ .../src/animations/change_frame_count_tool.rs | 124 +++++++++++++ crates/modals/src/animations/mod.rs | 2 + .../ui/src/windows/animations/frame_edit.rs | 84 +++++++++ crates/ui/src/windows/animations/mod.rs | 14 ++ crates/ui/src/windows/animations/util.rs | 8 +- 6 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 crates/modals/src/animations/change_cell_number_tool.rs create mode 100644 crates/modals/src/animations/change_frame_count_tool.rs diff --git a/crates/modals/src/animations/change_cell_number_tool.rs b/crates/modals/src/animations/change_cell_number_tool.rs new file mode 100644 index 00000000..caec0239 --- /dev/null +++ b/crates/modals/src/animations/change_cell_number_tool.rs @@ -0,0 +1,168 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub struct Modal { + state: State, + id_source: egui::Id, + pub frames_len: usize, + pub start_frame: usize, + pub end_frame: usize, + pub first_cell: usize, + pub second_cell: usize, +} + +enum State { + Closed, + Open, +} + +impl Modal { + pub fn new(id_source: impl Into) -> Self { + Self { + state: State::Closed, + id_source: id_source.into(), + frames_len: 1, + start_frame: 0, + end_frame: 0, + first_cell: 0, + second_cell: 0, + } + } +} + +impl luminol_core::Modal for Modal { + type Data<'m> = (); + + fn button<'m>( + &'m mut self, + _data: Self::Data<'m>, + _update_state: &'m mut luminol_core::UpdateState<'_>, + ) -> impl egui::Widget + 'm { + |ui: &mut egui::Ui| { + let response = ui.button("Change cell number"); + if response.clicked() { + self.state = State::Open; + } + response + } + } + + fn reset(&mut self, _: &mut luminol_core::UpdateState<'_>, _data: Self::Data<'_>) { + self.close_window(); + } +} + +impl Modal { + pub fn close_window(&mut self) { + self.state = State::Closed; + } + + pub fn show_window( + &mut self, + ctx: &egui::Context, + current_frame: usize, + frames_len: usize, + ) -> bool { + let mut win_open = true; + let mut keep_open = true; + let mut needs_save = false; + + if !matches!(self.state, State::Open) { + self.frames_len = frames_len; + self.start_frame = current_frame; + self.end_frame = current_frame; + return false; + } + + egui::Window::new("Change Cell Number") + .open(&mut win_open) + .id(self.id_source.with("change_cell_number_tool")) + .show(ctx, |ui| { + ui.columns(2, |columns| { + self.start_frame += 1; + columns[0].add(luminol_components::Field::new( + "Starting Frame", + egui::DragValue::new(&mut self.start_frame).range(1..=self.frames_len), + )); + self.start_frame -= 1; + + if self.start_frame > self.end_frame { + self.end_frame = self.start_frame; + } + + self.end_frame += 1; + columns[1].add(luminol_components::Field::new( + "Ending Frame", + egui::DragValue::new(&mut self.end_frame).range(1..=self.frames_len), + )); + self.end_frame -= 1; + + if self.end_frame < self.start_frame { + self.start_frame = self.end_frame; + } + }); + + ui.columns(2, |columns| { + self.first_cell += 1; + columns[0].add(luminol_components::Field::new( + "Cell Index", + egui::DragValue::new(&mut self.first_cell).range(1..=i16::MAX as usize + 1), + )); + self.first_cell -= 1; + + self.second_cell += 1; + columns[1].add(luminol_components::Field::new( + "Cell Index", + egui::DragValue::new(&mut self.second_cell) + .range(1..=i16::MAX as usize + 1), + )); + self.second_cell -= 1; + }); + + ui.label(if self.start_frame == self.end_frame { + format!( + "Swap the numbers of cell {} and cell {} in frame {}", + self.first_cell + 1, + self.second_cell + 1, + self.start_frame + 1, + ) + } else { + format!( + "Swap the numbers of cell {} and cell {} in frames {}–{}", + self.first_cell + 1, + self.second_cell + 1, + self.start_frame + 1, + self.end_frame + 1, + ) + }); + + luminol_components::close_options_ui(ui, &mut keep_open, &mut needs_save); + }); + + if !(win_open && keep_open) { + self.state = State::Closed; + } + needs_save + } +} diff --git a/crates/modals/src/animations/change_frame_count_tool.rs b/crates/modals/src/animations/change_frame_count_tool.rs new file mode 100644 index 00000000..aff5f83a --- /dev/null +++ b/crates/modals/src/animations/change_frame_count_tool.rs @@ -0,0 +1,124 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub struct Modal { + state: State, + id_source: egui::Id, + pub frames_len: usize, + pub new_frames_len: usize, +} + +enum State { + Closed, + Open, +} + +impl Modal { + pub fn new(id_source: impl Into) -> Self { + Self { + state: State::Closed, + id_source: id_source.into(), + frames_len: 1, + new_frames_len: 1, + } + } +} + +impl luminol_core::Modal for Modal { + type Data<'m> = (); + + fn button<'m>( + &'m mut self, + _data: Self::Data<'m>, + _update_state: &'m mut luminol_core::UpdateState<'_>, + ) -> impl egui::Widget + 'm { + |ui: &mut egui::Ui| { + let response = ui.button("Change frame count"); + if response.clicked() { + self.state = State::Open; + } + response + } + } + + fn reset(&mut self, _: &mut luminol_core::UpdateState<'_>, _data: Self::Data<'_>) { + self.close_window(); + } +} + +impl Modal { + pub fn close_window(&mut self) { + self.state = State::Closed; + } + + pub fn show_window(&mut self, ctx: &egui::Context, frames_len: usize) -> bool { + let mut win_open = true; + let mut keep_open = true; + let mut needs_save = false; + + if !matches!(self.state, State::Open) { + self.frames_len = frames_len; + self.new_frames_len = frames_len; + return false; + } + + egui::Window::new("Change Frame Count") + .open(&mut win_open) + .id(self.id_source.with("change_frame_count_tool")) + .show(ctx, |ui| { + ui.add(luminol_components::Field::new( + "Frame Count", + egui::DragValue::new(&mut self.new_frames_len).range(1..=usize::MAX), + )); + + if self.frames_len <= 999 && self.new_frames_len > 999 { + egui::Frame::none().show(ui, |ui| { + ui.style_mut() + .visuals + .widgets + .noninteractive + .bg_stroke + .color = ui.style().visuals.warn_fg_color; + egui::Frame::group(ui.style()) + .fill(ui.visuals().gray_out(ui.visuals().gray_out( + ui.visuals().gray_out(ui.style().visuals.warn_fg_color), + ))) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + ui.label(egui::RichText::new("Setting the frame count above 999 may introduce performance issues and instability").color(ui.style().visuals.warn_fg_color)); + }); + }); + } + + ui.label(format!("Change the number of frames in this animation from {} to {}", self.frames_len, self.new_frames_len)); + + luminol_components::close_options_ui(ui, &mut keep_open, &mut needs_save); + }); + + if !(win_open && keep_open) { + self.state = State::Closed; + } + needs_save + } +} diff --git a/crates/modals/src/animations/mod.rs b/crates/modals/src/animations/mod.rs index 2a7bd9ee..cf41495d 100644 --- a/crates/modals/src/animations/mod.rs +++ b/crates/modals/src/animations/mod.rs @@ -23,6 +23,8 @@ // Program grant you additional permission to convey the resulting work. pub mod batch_edit_tool; +pub mod change_cell_number_tool; +pub mod change_frame_count_tool; pub mod clear_frames_tool; pub mod copy_frames_tool; pub mod tween_tool; diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 21764e86..0f58c067 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -321,6 +321,10 @@ pub fn show_frame_edit( }); ui.add(modals.batch_edit.button((), update_state)); + + ui.add(modals.change_frame_count.button((), update_state)); + + ui.add(modals.change_cell_number.button((), update_state)); }); if ui.button("Play").clicked() { @@ -548,6 +552,86 @@ pub fn show_frame_edit( modified = true; } + if modals + .change_frame_count + .show_window(ui.ctx(), animation.frames.len()) + { + modals.close_all_except_frame_count(); + animation + .frames + .resize_with(modals.change_frame_count.new_frames_len, Default::default); + animation.frame_max = modals.change_frame_count.new_frames_len; + state.frame_index = state + .frame_index + .min(animation.frames.len().saturating_sub(1)); + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + modified = true; + } + + if modals + .change_cell_number + .show_window(ui.ctx(), state.frame_index, animation.frames.len()) + && modals.change_cell_number.first_cell != modals.change_cell_number.second_cell + { + let max_cell = modals + .change_cell_number + .first_cell + .max(modals.change_cell_number.second_cell); + let min_cell = modals + .change_cell_number + .first_cell + .min(modals.change_cell_number.second_cell); + + for i in modals.change_cell_number.start_frame..=modals.change_cell_number.end_frame { + let frame = &mut animation.frames[i]; + + if max_cell + 1 >= frame.cell_max + && max_cell < frame.cell_data.xsize() + && frame.cell_data[(max_cell, 0)] >= 0 + && frame.cell_data[(min_cell, 0)] < 0 + { + for j in 0..frame.cell_data.ysize() { + frame.cell_data[(min_cell, j)] = frame.cell_data[(max_cell, j)]; + } + super::util::resize_frame( + frame, + (0..frame + .cell_data + .xsize() + .min(frame.cell_max.saturating_sub(1))) + .rev() + .find_map(|j| (frame.cell_data[(j, 0)] >= 0).then_some(j + 1)) + .unwrap_or(0) + .max(min_cell + 1), + ); + continue; + } + + if max_cell >= frame.cell_data.xsize() { + if min_cell >= frame.cell_data.xsize() || frame.cell_data[(min_cell, 0)] < 0 { + continue; + } + super::util::resize_frame(frame, max_cell + 1); + } + + for j in 0..frame.cell_data.ysize() { + let xsize = frame.cell_data.xsize(); + let slice = frame.cell_data.as_mut_slice(); + slice.swap( + modals.change_cell_number.first_cell + j * xsize, + modals.change_cell_number.second_cell + j * xsize, + ); + } + } + + frame_view + .frame + .update_all_cells(&update_state.graphics, animation, state.frame_index); + modified = true; + } + let canvas_rect = egui::Resize::default() .resizable([false, true]) .min_width(ui.available_width()) diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs index 1a789154..90f459b6 100644 --- a/crates/ui/src/windows/animations/mod.rs +++ b/crates/ui/src/windows/animations/mod.rs @@ -72,14 +72,22 @@ struct Modals { clear_frames: luminol_modals::animations::clear_frames_tool::Modal, tween: luminol_modals::animations::tween_tool::Modal, batch_edit: luminol_modals::animations::batch_edit_tool::Modal, + change_frame_count: luminol_modals::animations::change_frame_count_tool::Modal, + change_cell_number: luminol_modals::animations::change_cell_number_tool::Modal, } impl Modals { fn close_all(&mut self) { + self.close_all_except_frame_count(); + self.change_frame_count.close_window(); + } + + fn close_all_except_frame_count(&mut self) { self.copy_frames.close_window(); self.clear_frames.close_window(); self.tween.close_window(); self.batch_edit.close_window(); + self.change_cell_number.close_window(); } } @@ -121,6 +129,12 @@ impl Default for Window { batch_edit: luminol_modals::animations::batch_edit_tool::Modal::new( "animations_batch_edit_tool", ), + change_frame_count: luminol_modals::animations::change_frame_count_tool::Modal::new( + "change_frame_count_tool", + ), + change_cell_number: luminol_modals::animations::change_cell_number_tool::Modal::new( + "change_cell_number_tool", + ), }, view: luminol_components::DatabaseView::new(), } diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs index 897b8e80..519bfe91 100644 --- a/crates/ui/src/windows/animations/util.rs +++ b/crates/ui/src/windows/animations/util.rs @@ -368,7 +368,9 @@ pub fn resize_frame(frame: &mut luminol_data::rpg::animation::Frame, new_cell_ma let capacity_too_high = old_capacity >= new_capacity * 4; if capacity_too_low { - frame.cell_data.resize(new_capacity, 8); + frame + .cell_data + .resize(new_capacity, frame.cell_data.ysize().max(8)); for i in old_capacity..new_capacity { frame.cell_data[(i, 0)] = -1; frame.cell_data[(i, 1)] = 0; @@ -380,7 +382,9 @@ pub fn resize_frame(frame: &mut luminol_data::rpg::animation::Frame, new_cell_ma frame.cell_data[(i, 7)] = 1; } } else if capacity_too_high { - frame.cell_data.resize(new_capacity * 2, 8); + frame + .cell_data + .resize(new_capacity * 2, frame.cell_data.ysize().max(8)); } frame.cell_max = new_cell_max; From 50e0a797d1cb94b4c56da62e58c11b2fb1c48d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 19:25:24 -0400 Subject: [PATCH 099/109] Allocate a separate egui painter for the animation frame view This prevents the clip rect of the cellpicker in the animation editor from being messed up. --- crates/components/src/animation_frame_view.rs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index ebaace43..b16df1f4 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -77,7 +77,9 @@ impl AnimationFrameView { ) -> egui::InnerResponse> { let canvas_rect = ui.max_rect(); let canvas_center = canvas_rect.center(); - ui.set_clip_rect(canvas_rect.intersect(clip_rect)); + let egui_painter = ui + .painter() + .with_clip_rect(canvas_rect.intersect(clip_rect)); let mut response = ui.allocate_rect(canvas_rect, egui::Sense::click_and_drag()); @@ -151,17 +153,16 @@ impl AnimationFrameView { ); let painter = luminol_graphics::Painter::new(self.frame.prepare(&update_state.graphics)); - ui.painter() - .add(luminol_egui_wgpu::Callback::new_paint_callback( - canvas_rect, - painter, - )); + egui_painter.add(luminol_egui_wgpu::Callback::new_paint_callback( + canvas_rect, + painter, + )); let screen_alpha = (egui::ecolor::linear_from_gamma(screen_color.alpha as f32 / 255.) * 255.) .round() as u8; if screen_alpha > 0 { - ui.painter().rect_filled( + egui_painter.rect_filled( egui::Rect::EVERYTHING, egui::Rounding::ZERO, egui::Color32::from_rgba_unmultiplied( @@ -177,21 +178,21 @@ impl AnimationFrameView { // Draw the grid lines and the border of the animation frame if draw_rects { - ui.painter().line_segment( + egui_painter.line_segment( [ egui::pos2(-(FRAME_WIDTH as f32 / 2.), 0.) * scale + offset, egui::pos2(FRAME_WIDTH as f32 / 2., 0.) * scale + offset, ], egui::Stroke::new(1., egui::Color32::DARK_GRAY), ); - ui.painter().line_segment( + egui_painter.line_segment( [ egui::pos2(0., -(FRAME_HEIGHT as f32 / 2.)) * scale + offset, egui::pos2(0., FRAME_HEIGHT as f32 / 2.) * scale + offset, ], egui::Stroke::new(1., egui::Color32::DARK_GRAY), ); - ui.painter().rect_stroke( + egui_painter.rect_stroke( egui::Rect::from_center_size( offset.to_pos2(), egui::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32) * scale, @@ -268,7 +269,7 @@ impl AnimationFrameView { .iter() .map(|(_, cell)| (cell.rect * scale).translate(offset)) { - ui.painter().rect_stroke( + egui_painter.rect_stroke( cell_rect, 5., egui::Stroke::new(1., egui::Color32::DARK_GRAY), @@ -283,7 +284,7 @@ impl AnimationFrameView { .iter() .map(|(_, cell)| (cell.rect * scale).translate(offset)) { - ui.painter().rect_stroke( + egui_painter.rect_stroke( cell_rect, 5., egui::Stroke::new( @@ -300,7 +301,7 @@ impl AnimationFrameView { // Draw a yellow rectangle on the border of the hovered cell if let Some(i) = self.hovered_cell_index { let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); - ui.painter().rect_stroke( + egui_painter.rect_stroke( cell_rect, 5., egui::Stroke::new(3., egui::Color32::YELLOW), @@ -310,7 +311,7 @@ impl AnimationFrameView { // Draw a magenta rectangle on the border of the selected cell if let Some(i) = self.selected_cell_index { let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); - ui.painter().rect_stroke( + egui_painter.rect_stroke( cell_rect, 5., egui::Stroke::new(3., egui::Color32::from_rgb(255, 0, 255)), From 6bd318e6c2151e2c140297cff9de1eeb6a1b96d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 8 Aug 2024 21:23:10 -0400 Subject: [PATCH 100/109] Truncate text in the graphic/sound picker left panels --- crates/components/src/sound_tab.rs | 2 ++ crates/modals/src/graphic_picker/actor.rs | 2 ++ crates/modals/src/graphic_picker/animation.rs | 2 ++ crates/modals/src/graphic_picker/basic.rs | 2 ++ crates/modals/src/graphic_picker/event.rs | 2 ++ crates/modals/src/graphic_picker/hue.rs | 2 ++ 6 files changed, 12 insertions(+) diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index b711e731..01785943 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -201,6 +201,8 @@ impl SoundTab { self.filtered_children.len() + 1, // +1 for (None) |ui, mut row_range| { ui.with_cross_justify(|ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + // we really want to only show (None) if it's in range, we can collapse this but itd rely on short circuiting #[allow(clippy::collapsible_if)] if row_range.contains(&0) { diff --git a/crates/modals/src/graphic_picker/actor.rs b/crates/modals/src/graphic_picker/actor.rs index 4395beab..1a09628b 100644 --- a/crates/modals/src/graphic_picker/actor.rs +++ b/crates/modals/src/graphic_picker/actor.rs @@ -270,6 +270,8 @@ impl Modal { row_height, filtered_entries.len() + 1, |ui, mut rows| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if rows.contains(&0) { let checked = matches!(selected, Selected::None); let res = ui.selectable_label(checked, "(None)"); diff --git a/crates/modals/src/graphic_picker/animation.rs b/crates/modals/src/graphic_picker/animation.rs index 463a6374..97e5c530 100644 --- a/crates/modals/src/graphic_picker/animation.rs +++ b/crates/modals/src/graphic_picker/animation.rs @@ -186,6 +186,8 @@ impl Modal { row_height, filtered_entries.len() + 1, |ui, mut rows| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if rows.contains(&0) { let checked = self.animation_name.is_none(); let res = ui.selectable_label(checked, "(None)"); diff --git a/crates/modals/src/graphic_picker/basic.rs b/crates/modals/src/graphic_picker/basic.rs index cde30aad..4c3b9ca0 100644 --- a/crates/modals/src/graphic_picker/basic.rs +++ b/crates/modals/src/graphic_picker/basic.rs @@ -247,6 +247,8 @@ impl Modal { row_height, filtered_entries.len() + 1, |ui, mut rows| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if rows.contains(&0) { let checked = matches!(selected, Selected::None); let res = ui.selectable_label(checked, "(None)"); diff --git a/crates/modals/src/graphic_picker/event.rs b/crates/modals/src/graphic_picker/event.rs index 84d48514..6a058172 100644 --- a/crates/modals/src/graphic_picker/event.rs +++ b/crates/modals/src/graphic_picker/event.rs @@ -341,6 +341,8 @@ impl Modal { row_height, filtered_entries.len() + 2, |ui, mut rows| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if rows.contains(&0) { let checked = matches!(selected, Selected::None); let res = ui.selectable_label(checked, "(None)"); diff --git a/crates/modals/src/graphic_picker/hue.rs b/crates/modals/src/graphic_picker/hue.rs index e4613971..fc1c672f 100644 --- a/crates/modals/src/graphic_picker/hue.rs +++ b/crates/modals/src/graphic_picker/hue.rs @@ -253,6 +253,8 @@ impl Modal { row_height, filtered_entries.len() + 1, |ui, mut rows| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + if rows.contains(&0) { let checked = matches!(selected, Selected::None); let res = ui.selectable_label(checked, "(None)"); From 9d0770c27bfb04df726a198bd6c66e8bb9391862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 9 Aug 2024 01:12:18 -0400 Subject: [PATCH 101/109] Fix some problems with `saved_frame_index` logic --- .../ui/src/windows/animations/frame_edit.rs | 30 ++++++++++++------- crates/ui/src/windows/animations/window.rs | 2 ++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 0f58c067..e6f3812c 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -32,10 +32,12 @@ fn start_animation_playback( animation: &luminol_data::rpg::Animation, animation_state: &mut Option, frame_index: &mut usize, + saved_frame_index: &mut Option, condition: luminol_data::rpg::animation::Condition, ) { if let Some(animation_state) = animation_state.take() { *frame_index = animation_state.saved_frame_index; + *saved_frame_index = Some(animation_state.saved_frame_index); } else { *animation_state = Some(super::AnimationState { saved_frame_index: *frame_index, @@ -44,6 +46,7 @@ fn start_animation_playback( audio_data: Default::default(), }); *frame_index = 0; + *saved_frame_index = None; // Preload the audio files used by the animation for // performance reasons @@ -211,6 +214,7 @@ pub fn show_frame_edit( if state.frame_index >= animation.frames.len() { let animation_state = state.animation_state.take().unwrap(); state.frame_index = animation_state.saved_frame_index; + state.saved_frame_index = Some(animation_state.saved_frame_index); } ui.horizontal(|ui| { @@ -333,6 +337,7 @@ pub fn show_frame_edit( animation, &mut state.animation_state, &mut state.frame_index, + &mut state.saved_frame_index, state.condition, ); } @@ -940,17 +945,19 @@ pub fn show_frame_edit( ) }); - // Press left/right arrow keys to change frame - if ui.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { - state.frame_index = state.frame_index.saturating_sub(1); - state.saved_frame_index = None; - } - if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) { - state.frame_index = state - .frame_index - .saturating_add(1) - .min(animation.frames.len().saturating_sub(1)); - state.saved_frame_index = None; + if state.animation_state.is_none() { + // Press left/right arrow keys to change frame + if ui.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { + state.frame_index = state.frame_index.saturating_sub(1); + state.saved_frame_index = Some(state.frame_index); + } + if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) { + state.frame_index = state + .frame_index + .saturating_add(1) + .min(animation.frames.len().saturating_sub(1)); + state.saved_frame_index = Some(state.frame_index); + } } // Press space or enter to start/stop animation playback @@ -960,6 +967,7 @@ pub fn show_frame_edit( animation, &mut state.animation_state, &mut state.frame_index, + &mut state.saved_frame_index, state.condition, ); } diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index 6d67de23..2742ce86 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -206,6 +206,8 @@ impl luminol_core::Window for super::Window { self.frame_edit_state.animation_state.take().unwrap(); self.frame_edit_state.frame_index = animation_state.saved_frame_index; + self.frame_edit_state.saved_frame_index = + Some(animation_state.saved_frame_index); } let atlas = From e5a4c255ee485fa1a1070de9d9323a00a7a8b671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 9 Aug 2024 13:35:18 -0400 Subject: [PATCH 102/109] Implement auto scrolling for sound picker Somehow I forgot to implement this. --- crates/components/src/sound_tab.rs | 42 ++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index 01785943..7747083f 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -32,6 +32,8 @@ pub struct SoundTab { search_text: String, folder_children: Vec, filtered_children: Vec, + + scrolled_on_first_open: bool, } impl SoundTab { @@ -54,6 +56,8 @@ impl SoundTab { filtered_children: folder_children.clone(), search_text: String::new(), folder_children, + + scrolled_on_first_open: false, } } @@ -191,9 +195,9 @@ impl SoundTab { .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap())) }); - egui::ScrollArea::both() + let mut scroll_area_output = egui::ScrollArea::vertical() .id_source((persistence_id, self.source)) - .auto_shrink([false, false]) + .auto_shrink([false, true]) // Show only visible rows. .show_rows( ui, @@ -246,6 +250,40 @@ impl SoundTab { }); }, ); + + // Scroll the selected item into view + if !self.scrolled_on_first_open { + let row = if self.audio_file.name.is_none() { + Some(0) + } else { + self.filtered_children + .iter() + .enumerate() + .find_map(|(i, entry)| { + (audio_file_name.as_deref() == Some(entry.file_name().into())) + .then_some(i + 1) + }) + }; + if let Some(row) = row { + let spacing = ui.spacing().item_spacing.y; + let max = row as f32 * (row_height + spacing) + spacing; + let min = row as f32 * (row_height + spacing) + row_height + - spacing + - scroll_area_output.inner_rect.height(); + if scroll_area_output.state.offset.y > max { + scroll_area_output.state.offset.y = max; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } else if scroll_area_output.state.offset.y < min { + scroll_area_output.state.offset.y = min; + scroll_area_output + .state + .store(ui.ctx(), scroll_area_output.id); + } + } + self.scrolled_on_first_open = true; + } }); }); } From c679be06f06699da3c914832e5cd68df2dc8e7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Fri, 9 Aug 2024 16:55:05 -0400 Subject: [PATCH 103/109] Fix tween tool text when nothing is being interpolated --- crates/modals/src/animations/tween_tool.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/modals/src/animations/tween_tool.rs b/crates/modals/src/animations/tween_tool.rs index c2290ab6..1026a349 100644 --- a/crates/modals/src/animations/tween_tool.rs +++ b/crates/modals/src/animations/tween_tool.rs @@ -170,7 +170,9 @@ impl Modal { if self.tween_shading { vec.push("opacity, blending"); } - ui.label(if self.start_cell == self.end_cell { + ui.label(if vec.is_empty() { + "Do nothing".to_string() + } else if self.start_cell == self.end_cell { format!( "Linearly interpolate cell {} for cell {} from frame {} to frame {}", vec.join(", "), From db9e316755302c5df0482702140c28676bd4126c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 10 Aug 2024 12:33:40 -0400 Subject: [PATCH 104/109] Fix frame sometimes not updating when it should update --- crates/ui/src/windows/animations/frame_edit.rs | 16 +++++++++++----- crates/ui/src/windows/animations/mod.rs | 2 ++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index e6f3812c..6013e766 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -33,11 +33,13 @@ fn start_animation_playback( animation_state: &mut Option, frame_index: &mut usize, saved_frame_index: &mut Option, + frame_needs_update: &mut bool, condition: luminol_data::rpg::animation::Condition, ) { if let Some(animation_state) = animation_state.take() { *frame_index = animation_state.saved_frame_index; *saved_frame_index = Some(animation_state.saved_frame_index); + *frame_needs_update = true; } else { *animation_state = Some(super::AnimationState { saved_frame_index: *frame_index, @@ -71,7 +73,6 @@ pub fn show_frame_edit( state: &mut super::FrameEditState, ) -> bool { let mut modified = false; - let mut recompute_flash = false; let flash_maps = state.flash_maps.get_mut(animation.id).unwrap(); @@ -156,7 +157,7 @@ pub fn show_frame_edit( state.frame_index = (time_diff * state.animation_fps) as usize; if state.frame_index != previous_frame_index { - recompute_flash = true; + state.frame_needs_update = true; } // Play sound effects @@ -244,11 +245,11 @@ pub fn show_frame_edit( .changed(); state.frame_index -= 1; if changed { - recompute_flash = true; + state.frame_needs_update = true; state.saved_frame_index = Some(state.frame_index); } - recompute_flash |= ui + state.frame_needs_update |= ui .add(luminol_components::Field::new( "Condition", luminol_components::EnumComboBox::new("condition", &mut state.condition) @@ -338,6 +339,7 @@ pub fn show_frame_edit( &mut state.animation_state, &mut state.frame_index, &mut state.saved_frame_index, + &mut state.frame_needs_update, state.condition, ); } @@ -807,7 +809,7 @@ pub fn show_frame_edit( } }); - if recompute_flash { + if state.frame_needs_update { frame_view.frame.update_battler( &update_state.graphics, system, @@ -822,6 +824,7 @@ pub fn show_frame_edit( frame_view .frame .update_all_cells(&update_state.graphics, animation, state.frame_index); + state.frame_needs_update = false; } egui::ScrollArea::horizontal().show_viewport(ui, |ui, scroll_rect| { @@ -950,6 +953,7 @@ pub fn show_frame_edit( if ui.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { state.frame_index = state.frame_index.saturating_sub(1); state.saved_frame_index = Some(state.frame_index); + state.frame_needs_update = true; } if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) { state.frame_index = state @@ -957,6 +961,7 @@ pub fn show_frame_edit( .saturating_add(1) .min(animation.frames.len().saturating_sub(1)); state.saved_frame_index = Some(state.frame_index); + state.frame_needs_update = true; } } @@ -968,6 +973,7 @@ pub fn show_frame_edit( &mut state.animation_state, &mut state.frame_index, &mut state.saved_frame_index, + &mut state.frame_needs_update, state.condition, ); } diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs index 90f459b6..b651559f 100644 --- a/crates/ui/src/windows/animations/mod.rs +++ b/crates/ui/src/windows/animations/mod.rs @@ -52,6 +52,7 @@ struct FrameEditState { animation_state: Option, saved_frame_index: Option, saved_selected_cell_index: Option, + frame_needs_update: bool, } #[derive(Debug)] @@ -109,6 +110,7 @@ impl Default for Window { animation_state: None, saved_frame_index: None, saved_selected_cell_index: None, + frame_needs_update: false, }, timing_edit_state: TimingEditState { previous_frame: None, From c7bc2f1f5ad9de4466035b128ce52023fb0f2d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 10 Aug 2024 17:03:24 -0400 Subject: [PATCH 105/109] Implement undo/redo for animation editor frame edit --- .../ui/src/windows/animations/frame_edit.rs | 137 +++++++++++++++++- crates/ui/src/windows/animations/mod.rs | 127 ++++++++++++++++ crates/ui/src/windows/animations/util.rs | 51 +++++++ crates/ui/src/windows/animations/window.rs | 5 + 4 files changed, 315 insertions(+), 5 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index 6013e766..cb5ae5b8 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -24,6 +24,7 @@ use luminol_core::Modal; +use super::HistoryEntry; use luminol_data::BlendMode; use luminol_graphics::frame::{FRAME_HEIGHT, FRAME_WIDTH}; @@ -302,8 +303,20 @@ pub fn show_frame_edit( if ui.button("Copy previous frame").clicked() && state.frame_index != 0 { - animation.frames[state.frame_index] = - animation.frames[state.frame_index - 1].clone(); + let (prev_frame, curr_frame) = super::util::get_two_mut( + &mut animation.frames, + state.frame_index - 1, + state.frame_index, + ); + state.history.push( + animation.id, + state.frame_index, + super::util::history_entries_from_two_tables( + &curr_frame.cell_data, + &prev_frame.cell_data, + ), + ); + *curr_frame = prev_frame.clone(); frame_view.frame.update_all_cells( &update_state.graphics, animation, @@ -351,15 +364,28 @@ pub fn show_frame_edit( if modals .copy_frames .show_window(ui.ctx(), state.frame_index, animation.frames.len()) + && modals.copy_frames.dst_frame != modals.copy_frames.src_frame { let mut iter = 0..modals.copy_frames.frame_count; - while let Some(i) = if modals.copy_frames.dst_frame <= modals.copy_frames.src_frame { + while let Some(i) = if modals.copy_frames.dst_frame < modals.copy_frames.src_frame { iter.next() } else { iter.next_back() } { - animation.frames[modals.copy_frames.dst_frame + i] = - animation.frames[modals.copy_frames.src_frame + i].clone(); + let (dst_frame, src_frame) = super::util::get_two_mut( + &mut animation.frames, + modals.copy_frames.dst_frame + i, + modals.copy_frames.src_frame + i, + ); + state.history.push( + animation.id, + modals.copy_frames.dst_frame + i, + super::util::history_entries_from_two_tables( + &dst_frame.cell_data, + &src_frame.cell_data, + ), + ); + *dst_frame = src_frame.clone(); } frame_view .frame @@ -372,6 +398,14 @@ pub fn show_frame_edit( .show_window(ui.ctx(), state.frame_index, animation.frames.len()) { for i in modals.clear_frames.start_frame..=modals.clear_frames.end_frame { + state.history.push( + animation.id, + i, + super::util::history_entries_from_two_tables( + &animation.frames[i].cell_data, + &Default::default(), + ), + ); animation.frames[i] = Default::default(); } frame_view @@ -408,12 +442,20 @@ pub fn show_frame_edit( ) }; + let mut entries = Vec::with_capacity(2); + if animation.frames[j].cell_data.xsize() < i + 1 { + entries.push(HistoryEntry::new_resize_cells( + &animation.frames[j].cell_data, + )); super::util::resize_frame(&mut animation.frames[j], i + 1); } else if animation.frames[j].cell_max < i + 1 { animation.frames[j].cell_max = i + 1; } + entries.push(HistoryEntry::new_cell(&animation.frames[j].cell_data, i)); + state.history.push(animation.id, j, entries); + if modals.tween.tween_pattern { let (val, orientation) = lerp(&animation.frames, 0); animation.frames[j].cell_data[(i, 0)] = @@ -465,6 +507,9 @@ pub fn show_frame_edit( if data[(j, 0)] < 0 { continue; } + state + .history + .push(animation.id, i, vec![HistoryEntry::new_cell(data, j)]); match modals.batch_edit.mode { luminol_modals::animations::batch_edit_tool::Mode::Set => { if modals.batch_edit.set_pattern_enabled { @@ -564,6 +609,9 @@ pub fn show_frame_edit( .show_window(ui.ctx(), animation.frames.len()) { modals.close_all_except_frame_count(); + for i in modals.change_frame_count.new_frames_len..animation.frames.len() { + state.history.remove_frame(animation.id, i); + } animation .frames .resize_with(modals.change_frame_count.new_frames_len, Default::default); @@ -599,6 +647,14 @@ pub fn show_frame_edit( && frame.cell_data[(max_cell, 0)] >= 0 && frame.cell_data[(min_cell, 0)] < 0 { + state.history.push( + animation.id, + i, + vec![ + HistoryEntry::new_resize_cells(&frame.cell_data), + HistoryEntry::new_cell(&frame.cell_data, min_cell), + ], + ); for j in 0..frame.cell_data.ysize() { frame.cell_data[(min_cell, j)] = frame.cell_data[(max_cell, j)]; } @@ -616,14 +672,25 @@ pub fn show_frame_edit( continue; } + let mut entries = Vec::with_capacity(3); + if max_cell >= frame.cell_data.xsize() { if min_cell >= frame.cell_data.xsize() || frame.cell_data[(min_cell, 0)] < 0 { continue; } + entries.push(HistoryEntry::new_resize_cells(&frame.cell_data)); super::util::resize_frame(frame, max_cell + 1); } for j in 0..frame.cell_data.ysize() { + entries.push(HistoryEntry::new_cell( + &frame.cell_data, + modals.change_cell_number.first_cell, + )); + entries.push(HistoryEntry::new_cell( + &frame.cell_data, + modals.change_cell_number.second_cell, + )); let xsize = frame.cell_data.xsize(); let slice = frame.cell_data.as_mut_slice(); slice.swap( @@ -631,6 +698,8 @@ pub fn show_frame_edit( modals.change_cell_number.second_cell + j * xsize, ); } + + state.history.push(animation.id, i, entries); } frame_view @@ -686,12 +755,38 @@ pub fn show_frame_edit( state.animation_state.is_none(), ) { if (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) != drag_pos { + if !state + .drag_state + .as_ref() + .is_some_and(|drag_state| drag_state.cell_index == i) + { + state.drag_state = Some(super::DragState { + cell_index: i, + original_x: frame.cell_data[(i, 1)], + original_y: frame.cell_data[(i, 2)], + }); + } (frame.cell_data[(i, 1)], frame.cell_data[(i, 2)]) = drag_pos; frame_view .frame .update_cell(&update_state.graphics, animation, state.frame_index, i); modified = true; } + } else if let Some(drag_state) = state.drag_state.take() { + let x = frame.cell_data[(drag_state.cell_index, 1)]; + let y = frame.cell_data[(drag_state.cell_index, 2)]; + frame.cell_data[(drag_state.cell_index, 1)] = drag_state.original_x; + frame.cell_data[(drag_state.cell_index, 2)] = drag_state.original_y; + state.history.push( + animation.id, + state.frame_index, + vec![HistoryEntry::new_cell( + &frame.cell_data, + drag_state.cell_index, + )], + ); + frame.cell_data[(drag_state.cell_index, 1)] = x; + frame.cell_data[(drag_state.cell_index, 2)] = y; } egui::Frame::none().show(ui, |ui| { @@ -872,8 +967,12 @@ pub fn show_frame_edit( .find(|i| frame.cell_data[(*i, 0)] < 0) .unwrap_or(frame.cell_data.xsize()); + let mut entries = Vec::with_capacity(2); + + entries.push(HistoryEntry::new_resize_cells(&frame.cell_data)); super::util::resize_frame(frame, next_cell_index + 1); + entries.push(HistoryEntry::new_cell(&frame.cell_data, next_cell_index)); frame.cell_data[(next_cell_index, 0)] = cellpicker.selected_cell as i16; frame.cell_data[(next_cell_index, 1)] = x; frame.cell_data[(next_cell_index, 2)] = y; @@ -883,6 +982,8 @@ pub fn show_frame_edit( frame.cell_data[(next_cell_index, 6)] = 255; frame.cell_data[(next_cell_index, 7)] = 1; + state.history.push(animation.id, state.frame_index, entries); + frame_view.frame.update_cell( &update_state.graphics, animation, @@ -909,9 +1010,13 @@ pub fn show_frame_edit( i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace) }) { + let mut entries = Vec::with_capacity(2); + + entries.push(HistoryEntry::new_cell(&frame.cell_data, i)); frame.cell_data[(i, 0)] = -1; if i + 1 >= frame.cell_max { + entries.push(HistoryEntry::new_resize_cells(&frame.cell_data)); super::util::resize_frame( frame, (0..frame @@ -924,6 +1029,8 @@ pub fn show_frame_edit( ); } + state.history.push(animation.id, state.frame_index, entries); + frame_view.frame.update_cell( &update_state.graphics, animation, @@ -963,6 +1070,26 @@ pub fn show_frame_edit( state.saved_frame_index = Some(state.frame_index); state.frame_needs_update = true; } + + let frame = &mut animation.frames[state.frame_index]; + + // Ctrl+Z for undo + if ui.input(|i| { + i.modifiers.command && !i.modifiers.shift && i.key_pressed(egui::Key::Z) + }) { + state.history.undo(animation.id, state.frame_index, frame); + state.frame_needs_update = true; + } + + // Ctrl+Y or Ctrl+Shift+Z for redo + if ui.input(|i| { + i.modifiers.command + && (i.key_pressed(egui::Key::Y) + || (i.modifiers.shift && i.key_pressed(egui::Key::Z))) + }) { + state.history.redo(animation.id, state.frame_index, frame); + state.frame_needs_update = true; + } } // Press space or enter to start/stop animation playback diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs index b651559f..aa89221e 100644 --- a/crates/ui/src/windows/animations/mod.rs +++ b/crates/ui/src/windows/animations/mod.rs @@ -27,6 +27,8 @@ mod timing; mod util; mod window; +const HISTORY_SIZE: usize = 50; + /// Database - Animations management window. pub struct Window { selected_animation_name: Option, @@ -53,6 +55,15 @@ struct FrameEditState { saved_frame_index: Option, saved_selected_cell_index: Option, frame_needs_update: bool, + history: History, + drag_state: Option, +} + +#[derive(Debug)] +struct DragState { + cell_index: usize, + original_x: i16, + original_y: i16, } #[derive(Debug)] @@ -68,6 +79,120 @@ struct TimingEditState { se_picker: luminol_modals::sound_picker::Modal, } +#[derive(Debug, Default)] +struct History(luminol_data::OptionVec>); + +#[derive(Debug, Default)] +struct HistoryInner { + undo: std::collections::VecDeque>, + redo: Vec>, +} + +impl History { + fn inner(&mut self, animation_index: usize, frame_index: usize) -> &mut HistoryInner { + if !self.0.contains(animation_index) { + self.0.insert(animation_index, Default::default()); + } + let map = self.0.get_mut(animation_index).unwrap(); + if !map.contains(frame_index) { + map.insert(frame_index, Default::default()); + } + map.get_mut(frame_index).unwrap() + } + + fn remove_animation(&mut self, animation_index: usize) { + let _ = self.0.try_remove(animation_index); + } + + fn remove_frame(&mut self, animation_index: usize, frame_index: usize) { + if let Some(map) = self.0.get_mut(animation_index) { + let _ = map.try_remove(frame_index); + } + } + + fn push(&mut self, animation_index: usize, frame_index: usize, mut entries: Vec) { + entries.shrink_to_fit(); + let inner = self.inner(animation_index, frame_index); + inner.redo.clear(); + while inner.undo.len() >= HISTORY_SIZE { + inner.undo.pop_front(); + } + inner.undo.push_back(entries); + } + + fn undo( + &mut self, + animation_index: usize, + frame_index: usize, + frame: &mut luminol_data::rpg::animation::Frame, + ) { + let inner = self.inner(animation_index, frame_index); + let Some(mut vec) = inner.undo.pop_back() else { + return; + }; + vec.reverse(); + for entry in vec.iter_mut() { + entry.apply(frame); + } + inner.redo.push(vec); + } + + fn redo( + &mut self, + animation_index: usize, + frame_index: usize, + frame: &mut luminol_data::rpg::animation::Frame, + ) { + let inner = self.inner(animation_index, frame_index); + let Some(mut vec) = inner.redo.pop() else { + return; + }; + vec.reverse(); + for entry in vec.iter_mut() { + entry.apply(frame); + } + inner.undo.push_back(vec); + } +} + +#[derive(Debug)] +enum HistoryEntry { + Cell { index: usize, data: [i16; 8] }, + ResizeCells(usize), +} + +impl HistoryEntry { + fn new_cell(cell_data: &luminol_data::Table2, cell_index: usize) -> Self { + let mut data = [0i16; 8]; + for i in 0..8 { + data[i] = cell_data[(cell_index, i)]; + } + Self::Cell { + index: cell_index, + data, + } + } + + fn new_resize_cells(cell_data: &luminol_data::Table2) -> Self { + Self::ResizeCells(cell_data.xsize()) + } + + fn apply(&mut self, frame: &mut luminol_data::rpg::animation::Frame) { + match self { + HistoryEntry::Cell { index, data } => { + for (i, item) in data.iter_mut().enumerate() { + std::mem::swap(item, &mut frame.cell_data[(*index, i)]); + } + } + HistoryEntry::ResizeCells(size) => { + let old_size = frame.cell_data.xsize(); + util::resize_frame(frame, *size); + *size = old_size; + } + } + } +} + struct Modals { copy_frames: luminol_modals::animations::copy_frames_tool::Modal, clear_frames: luminol_modals::animations::clear_frames_tool::Modal, @@ -111,6 +236,8 @@ impl Default for Window { saved_frame_index: None, saved_selected_cell_index: None, frame_needs_update: false, + drag_state: None, + history: Default::default(), }, timing_edit_state: TimingEditState { previous_frame: None, diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs index 519bfe91..95b35600 100644 --- a/crates/ui/src/windows/animations/util.rs +++ b/crates/ui/src/windows/animations/util.rs @@ -411,3 +411,54 @@ pub fn update_flash_maps(condition: Condition, mut closure: impl FnMut(Condition closure(Condition::Miss); } } + +/// Gets mutable references at two different indices of a slice. Panics if the two indices are the +/// same. +pub fn get_two_mut(slice: &mut [T], index1: usize, index2: usize) -> (&mut T, &mut T) { + if index1 >= slice.len() { + panic!("index1 out of range"); + } + if index2 >= slice.len() { + panic!("index2 out of range"); + } + if index1 == index2 { + panic!("index1 and index2 are the same"); + } + let slice = &mut slice[if index1 < index2 { + index1..=index2 + } else { + index2..=index1 + }]; + let (min, slice) = slice.split_first_mut().unwrap(); + let (max, _) = slice.split_last_mut().unwrap(); + if index1 < index2 { + (min, max) + } else { + (max, min) + } +} + +/// Computes the list of history entries necessary to undo the transformation from `src_data` to +/// `dst_data`. +pub fn history_entries_from_two_tables( + dst_data: &luminol_data::Table2, + src_data: &luminol_data::Table2, +) -> Vec { + let cell_iter = (0..dst_data.xsize()) + .filter(|&i| (0..8).any(|j| i >= src_data.xsize() || src_data[(i, j)] != dst_data[(i, j)])) + .map(|i| super::HistoryEntry::new_cell(dst_data, i)); + let resize_iter = std::iter::once(super::HistoryEntry::new_resize_cells(dst_data)); + match src_data.xsize().cmp(&dst_data.xsize()) { + std::cmp::Ordering::Equal => cell_iter.collect(), + std::cmp::Ordering::Less => cell_iter.chain(resize_iter).collect(), + std::cmp::Ordering::Greater => resize_iter + .chain(cell_iter) + .chain( + (dst_data.xsize()..src_data.xsize()).map(|i| super::HistoryEntry::Cell { + index: i, + data: [-1, 0, 0, 0, 0, 0, 0, 0], + }), + ) + .collect(), + } +} diff --git a/crates/ui/src/windows/animations/window.rs b/crates/ui/src/windows/animations/window.rs index 2742ce86..74352070 100644 --- a/crates/ui/src/windows/animations/window.rs +++ b/crates/ui/src/windows/animations/window.rs @@ -46,6 +46,7 @@ impl luminol_core::Window for super::Window { ) { let data = std::mem::take(update_state.data); // take data to avoid borrow checker issues let mut animations = data.animations(); + let animations_len = animations.data.len(); let system = data.system(); let mut modified = false; @@ -70,6 +71,10 @@ impl luminol_core::Window for super::Window { &mut animations.data, |animation| format!("{:0>4}: {}", animation.id + 1, animation.name), |ui, animations, id, update_state| { + for i in animations.len()..animations_len { + self.frame_edit_state.history.remove_animation(i); + } + let animation = &mut animations[id]; self.selected_animation_name = Some(animation.name.clone()); if animation.frames.is_empty() { From 30998a75c2ef439f2f0db22c6c9f72601fd5f51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 10 Aug 2024 17:10:32 -0400 Subject: [PATCH 106/109] Fix mistake in the previous commit --- crates/ui/src/windows/animations/frame_edit.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index cb5ae5b8..dcf1bc4c 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -682,15 +682,16 @@ pub fn show_frame_edit( super::util::resize_frame(frame, max_cell + 1); } + entries.push(HistoryEntry::new_cell( + &frame.cell_data, + modals.change_cell_number.first_cell, + )); + entries.push(HistoryEntry::new_cell( + &frame.cell_data, + modals.change_cell_number.second_cell, + )); + for j in 0..frame.cell_data.ysize() { - entries.push(HistoryEntry::new_cell( - &frame.cell_data, - modals.change_cell_number.first_cell, - )); - entries.push(HistoryEntry::new_cell( - &frame.cell_data, - modals.change_cell_number.second_cell, - )); let xsize = frame.cell_data.xsize(); let slice = frame.cell_data.as_mut_slice(); slice.swap( From c4b53f4068627d6b78db9e457517eb261c6d93af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 10 Aug 2024 17:29:29 -0400 Subject: [PATCH 107/109] Fix doc comment for `history_entries_from_two_tables` --- crates/ui/src/windows/animations/util.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs index 95b35600..7b25d94d 100644 --- a/crates/ui/src/windows/animations/util.rs +++ b/crates/ui/src/windows/animations/util.rs @@ -438,23 +438,23 @@ pub fn get_two_mut(slice: &mut [T], index1: usize, index2: usize) -> (&mut T, } } -/// Computes the list of history entries necessary to undo the transformation from `src_data` to -/// `dst_data`. +/// Computes the list of history entries necessary to undo the transformation from `old_data` to +/// `new_data`. pub fn history_entries_from_two_tables( - dst_data: &luminol_data::Table2, - src_data: &luminol_data::Table2, + old_data: &luminol_data::Table2, + new_data: &luminol_data::Table2, ) -> Vec { - let cell_iter = (0..dst_data.xsize()) - .filter(|&i| (0..8).any(|j| i >= src_data.xsize() || src_data[(i, j)] != dst_data[(i, j)])) - .map(|i| super::HistoryEntry::new_cell(dst_data, i)); - let resize_iter = std::iter::once(super::HistoryEntry::new_resize_cells(dst_data)); - match src_data.xsize().cmp(&dst_data.xsize()) { + let cell_iter = (0..old_data.xsize()) + .filter(|&i| (0..8).any(|j| i >= new_data.xsize() || new_data[(i, j)] != old_data[(i, j)])) + .map(|i| super::HistoryEntry::new_cell(old_data, i)); + let resize_iter = std::iter::once(super::HistoryEntry::new_resize_cells(old_data)); + match new_data.xsize().cmp(&old_data.xsize()) { std::cmp::Ordering::Equal => cell_iter.collect(), std::cmp::Ordering::Less => cell_iter.chain(resize_iter).collect(), std::cmp::Ordering::Greater => resize_iter .chain(cell_iter) .chain( - (dst_data.xsize()..src_data.xsize()).map(|i| super::HistoryEntry::Cell { + (old_data.xsize()..new_data.xsize()).map(|i| super::HistoryEntry::Cell { index: i, data: [-1, 0, 0, 0, 0, 0, 0, 0], }), From 5dbf1ea2cc036714ff3e8e2ad9610eaf9f1dc550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Sat, 10 Aug 2024 20:39:32 -0400 Subject: [PATCH 108/109] Replace `frame.cell_data.xsize()` with `frame.len()` The `.xsize()` of `frame.cell_data` doesn't always correspond to the maximum cell number in a frame because of how the frame resizing function defined in crates/ui/src/windows/animations/util.rs works. So for all the code that was incorrectly assuming `.xsize()` was related to the maximum cell number, I replaced the `.cell_data.xsize()` with a new `.len()` method that more accurately determines the correct value. --- crates/data/src/rmxp/animation.rs | 14 ++++ crates/graphics/src/frame.rs | 51 ++++++-------- .../ui/src/windows/animations/frame_edit.rs | 68 ++++++++----------- crates/ui/src/windows/animations/mod.rs | 12 ++-- crates/ui/src/windows/animations/util.rs | 26 ++++--- 5 files changed, 81 insertions(+), 90 deletions(-) diff --git a/crates/data/src/rmxp/animation.rs b/crates/data/src/rmxp/animation.rs index ac541895..d58c01ee 100644 --- a/crates/data/src/rmxp/animation.rs +++ b/crates/data/src/rmxp/animation.rs @@ -69,6 +69,20 @@ pub struct Frame { pub cell_data: Table2, } +impl Frame { + /// Returns one more than the maximum cell number in this frame. + #[inline] + pub fn len(&self) -> usize { + self.cell_max.min(self.cell_data.xsize()) + } + + /// Returns true if there are no cells in this frame, otherwise false. + #[inline] + pub fn is_empty(&self) -> bool { + self.cell_max == 0 || self.cell_data.is_empty() + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] #[derive( num_enum::TryFromPrimitive, diff --git a/crates/graphics/src/frame.rs b/crates/graphics/src/frame.rs index 8ed7373d..47a4bae4 100644 --- a/crates/graphics/src/frame.rs +++ b/crates/graphics/src/frame.rs @@ -231,30 +231,25 @@ impl Frame { let mut cells = std::mem::take(&mut self.cells); cells.clear(); cells.extend( - (0..cells - .len() - .max(animation.frames[frame_index].cell_data.xsize())) - .filter_map(|i| { - self.create_cell( - graphics_state, - &animation.frames[frame_index], - animation.animation_hue, - i, - 1., - ) - .map(|cell| (i, cell)) - }), + (0..cells.len().max(animation.frames[frame_index].len())).filter_map(|i| { + self.create_cell( + graphics_state, + &animation.frames[frame_index], + animation.animation_hue, + i, + 1., + ) + .map(|cell| (i, cell)) + }), ); self.cells = cells; let mut cells = std::mem::take(&mut self.onion_skin_cells); cells.clear(); cells.extend( - (0..cells.len().max( - animation.frames[frame_index.saturating_sub(1)] - .cell_data - .xsize(), - )) + (0..cells + .len() + .max(animation.frames[frame_index.saturating_sub(1)].len())) .filter_map(|i| { self.create_cell( graphics_state, @@ -303,11 +298,7 @@ impl Frame { animation: &luminol_data::rpg::Animation, frame_index: usize, ) { - for cell_index in 0..self - .cells - .len() - .max(animation.frames[frame_index].cell_data.xsize()) - { + for cell_index in 0..self.cells.len().max(animation.frames[frame_index].len()) { let cells = std::mem::take(&mut self.cells); self.cells = self.update_cell_inner( cells, @@ -319,11 +310,11 @@ impl Frame { ); } - for cell_index in 0..self.onion_skin_cells.len().max( - animation.frames[frame_index.saturating_sub(1)] - .cell_data - .xsize(), - ) { + for cell_index in 0..self + .onion_skin_cells + .len() + .max(animation.frames[frame_index.saturating_sub(1)].len()) + { let cells = std::mem::take(&mut self.onion_skin_cells); self.onion_skin_cells = self.update_cell_inner( cells, @@ -344,7 +335,7 @@ impl Frame { cell_index: usize, opacity_multiplier: f32, ) -> Option { - (cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0).then(|| { + (cell_index < frame.len() && frame.cell_data[(cell_index, 0)] >= 0).then(|| { let id = frame.cell_data[(cell_index, 0)]; let offset_x = frame.cell_data[(cell_index, 1)] as f32; let offset_y = frame.cell_data[(cell_index, 2)] as f32; @@ -400,7 +391,7 @@ impl Frame { cell_index: usize, opacity_multiplier: f32, ) -> OptionVec { - if cell_index < frame.cell_data.xsize() && frame.cell_data[(cell_index, 0)] >= 0 { + if cell_index < frame.len() && frame.cell_data[(cell_index, 0)] >= 0 { if let Some(cell) = cells.get_mut(cell_index) { let id = frame.cell_data[(cell_index, 0)]; let offset_x = frame.cell_data[(cell_index, 1)] as f32; diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index dcf1bc4c..fe764ec4 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -312,8 +312,7 @@ pub fn show_frame_edit( animation.id, state.frame_index, super::util::history_entries_from_two_tables( - &curr_frame.cell_data, - &prev_frame.cell_data, + curr_frame, prev_frame, ), ); *curr_frame = prev_frame.clone(); @@ -380,10 +379,7 @@ pub fn show_frame_edit( state.history.push( animation.id, modals.copy_frames.dst_frame + i, - super::util::history_entries_from_two_tables( - &dst_frame.cell_data, - &src_frame.cell_data, - ), + super::util::history_entries_from_two_tables(dst_frame, src_frame), ); *dst_frame = src_frame.clone(); } @@ -402,7 +398,7 @@ pub fn show_frame_edit( animation.id, i, super::util::history_entries_from_two_tables( - &animation.frames[i].cell_data, + &animation.frames[i], &Default::default(), ), ); @@ -419,12 +415,14 @@ pub fn show_frame_edit( .show_window(ui.ctx(), state.frame_index, animation.frames.len()) { for i in modals.tween.start_cell..=modals.tween.end_cell { + let len = animation.frames[modals.tween.start_frame].len(); let data = &animation.frames[modals.tween.start_frame].cell_data; - if i >= data.xsize() || data[(i, 0)] < 0 { + if i >= len || data[(i, 0)] < 0 { continue; } + let len = animation.frames[modals.tween.end_frame].len(); let data = &animation.frames[modals.tween.end_frame].cell_data; - if i >= data.xsize() || data[(i, 0)] < 0 { + if i >= len || data[(i, 0)] < 0 { continue; } @@ -444,13 +442,9 @@ pub fn show_frame_edit( let mut entries = Vec::with_capacity(2); - if animation.frames[j].cell_data.xsize() < i + 1 { - entries.push(HistoryEntry::new_resize_cells( - &animation.frames[j].cell_data, - )); + if animation.frames[j].len() < i + 1 { + entries.push(HistoryEntry::ResizeCells(animation.frames[j].len())); super::util::resize_frame(&mut animation.frames[j], i + 1); - } else if animation.frames[j].cell_max < i + 1 { - animation.frames[j].cell_max = i + 1; } entries.push(HistoryEntry::new_cell(&animation.frames[j].cell_data, i)); @@ -502,8 +496,9 @@ pub fn show_frame_edit( frame_view.frame.atlas.num_patterns(), ) { for i in modals.batch_edit.start_frame..=modals.batch_edit.end_frame { + let len = animation.frames[i].len(); let data = &mut animation.frames[i].cell_data; - for j in 0..data.xsize() { + for j in 0..len { if data[(j, 0)] < 0 { continue; } @@ -642,8 +637,7 @@ pub fn show_frame_edit( for i in modals.change_cell_number.start_frame..=modals.change_cell_number.end_frame { let frame = &mut animation.frames[i]; - if max_cell + 1 >= frame.cell_max - && max_cell < frame.cell_data.xsize() + if max_cell + 1 == frame.len() && frame.cell_data[(max_cell, 0)] >= 0 && frame.cell_data[(min_cell, 0)] < 0 { @@ -651,8 +645,8 @@ pub fn show_frame_edit( animation.id, i, vec![ - HistoryEntry::new_resize_cells(&frame.cell_data), HistoryEntry::new_cell(&frame.cell_data, min_cell), + HistoryEntry::ResizeCells(frame.len()), ], ); for j in 0..frame.cell_data.ysize() { @@ -660,10 +654,7 @@ pub fn show_frame_edit( } super::util::resize_frame( frame, - (0..frame - .cell_data - .xsize() - .min(frame.cell_max.saturating_sub(1))) + (0..frame.len().saturating_sub(1)) .rev() .find_map(|j| (frame.cell_data[(j, 0)] >= 0).then_some(j + 1)) .unwrap_or(0) @@ -674,11 +665,11 @@ pub fn show_frame_edit( let mut entries = Vec::with_capacity(3); - if max_cell >= frame.cell_data.xsize() { - if min_cell >= frame.cell_data.xsize() || frame.cell_data[(min_cell, 0)] < 0 { + if max_cell >= frame.len() { + if min_cell >= frame.len() || frame.cell_data[(min_cell, 0)] < 0 { continue; } - entries.push(HistoryEntry::new_resize_cells(&frame.cell_data)); + entries.push(HistoryEntry::ResizeCells(frame.len())); super::util::resize_frame(frame, max_cell + 1); } @@ -691,8 +682,8 @@ pub fn show_frame_edit( modals.change_cell_number.second_cell, )); + let xsize = frame.cell_data.xsize(); for j in 0..frame.cell_data.ysize() { - let xsize = frame.cell_data.xsize(); let slice = frame.cell_data.as_mut_slice(); slice.swap( modals.change_cell_number.first_cell + j * xsize, @@ -727,7 +718,7 @@ pub fn show_frame_edit( if frame_view .selected_cell_index - .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) + .is_some_and(|i| i >= frame.len() || frame.cell_data[(i, 0)] < 0) { frame_view.selected_cell_index = None; } @@ -735,14 +726,14 @@ pub fn show_frame_edit( if frame_view.selected_cell_index.is_none() && state .saved_selected_cell_index - .is_some_and(|i| i < frame.cell_data.xsize() && frame.cell_data[(i, 0)] >= 0) + .is_some_and(|i| i < frame.len() && frame.cell_data[(i, 0)] >= 0) { frame_view.selected_cell_index = state.saved_selected_cell_index; } if frame_view .hovered_cell_index - .is_some_and(|i| i >= frame.cell_data.xsize() || frame.cell_data[(i, 0)] < 0) + .is_some_and(|i| i >= frame.len() || frame.cell_data[(i, 0)] < 0) { frame_view.hovered_cell_index = None; frame_view.hovered_cell_drag_pos = None; @@ -964,13 +955,11 @@ pub fn show_frame_edit( // Create new cell on double click if let Some((x, y)) = hover_pos { if response.double_clicked() { - let next_cell_index = (frame.cell_max..frame.cell_data.xsize()) - .find(|i| frame.cell_data[(*i, 0)] < 0) - .unwrap_or(frame.cell_data.xsize()); + let next_cell_index = frame.len(); let mut entries = Vec::with_capacity(2); - entries.push(HistoryEntry::new_resize_cells(&frame.cell_data)); + entries.push(HistoryEntry::ResizeCells(frame.len())); super::util::resize_frame(frame, next_cell_index + 1); entries.push(HistoryEntry::new_cell(&frame.cell_data, next_cell_index)); @@ -1004,7 +993,7 @@ pub fn show_frame_edit( frame_view.selected_cell_index, state.animation_state.is_none(), ) { - if i < frame.cell_data.xsize() + if i < frame.len() && frame.cell_data[(i, 0)] >= 0 && response.has_focus() && ui.input(|i| { @@ -1016,14 +1005,11 @@ pub fn show_frame_edit( entries.push(HistoryEntry::new_cell(&frame.cell_data, i)); frame.cell_data[(i, 0)] = -1; - if i + 1 >= frame.cell_max { - entries.push(HistoryEntry::new_resize_cells(&frame.cell_data)); + if i + 1 == frame.len() { + entries.push(HistoryEntry::ResizeCells(frame.len())); super::util::resize_frame( frame, - (0..frame - .cell_data - .xsize() - .min(frame.cell_max.saturating_sub(1))) + (0..frame.len().saturating_sub(1)) .rev() .find_map(|i| (frame.cell_data[(i, 0)] >= 0).then_some(i + 1)) .unwrap_or(0), diff --git a/crates/ui/src/windows/animations/mod.rs b/crates/ui/src/windows/animations/mod.rs index aa89221e..c59cf4ec 100644 --- a/crates/ui/src/windows/animations/mod.rs +++ b/crates/ui/src/windows/animations/mod.rs @@ -173,10 +173,6 @@ impl HistoryEntry { } } - fn new_resize_cells(cell_data: &luminol_data::Table2) -> Self { - Self::ResizeCells(cell_data.xsize()) - } - fn apply(&mut self, frame: &mut luminol_data::rpg::animation::Frame) { match self { HistoryEntry::Cell { index, data } => { @@ -184,10 +180,10 @@ impl HistoryEntry { std::mem::swap(item, &mut frame.cell_data[(*index, i)]); } } - HistoryEntry::ResizeCells(size) => { - let old_size = frame.cell_data.xsize(); - util::resize_frame(frame, *size); - *size = old_size; + HistoryEntry::ResizeCells(len) => { + let old_len = frame.len(); + util::resize_frame(frame, *len); + *len = old_len; } } } diff --git a/crates/ui/src/windows/animations/util.rs b/crates/ui/src/windows/animations/util.rs index 7b25d94d..8089a298 100644 --- a/crates/ui/src/windows/animations/util.rs +++ b/crates/ui/src/windows/animations/util.rs @@ -430,7 +430,7 @@ pub fn get_two_mut(slice: &mut [T], index1: usize, index2: usize) -> (&mut T, index2..=index1 }]; let (min, slice) = slice.split_first_mut().unwrap(); - let (max, _) = slice.split_last_mut().unwrap(); + let max = slice.last_mut().unwrap(); if index1 < index2 { (min, max) } else { @@ -438,23 +438,27 @@ pub fn get_two_mut(slice: &mut [T], index1: usize, index2: usize) -> (&mut T, } } -/// Computes the list of history entries necessary to undo the transformation from `old_data` to -/// `new_data`. +/// Computes the list of history entries necessary to undo the transformation from `old_frame` to +/// `new_frame`. pub fn history_entries_from_two_tables( - old_data: &luminol_data::Table2, - new_data: &luminol_data::Table2, + old_frame: &luminol_data::rpg::animation::Frame, + new_frame: &luminol_data::rpg::animation::Frame, ) -> Vec { - let cell_iter = (0..old_data.xsize()) - .filter(|&i| (0..8).any(|j| i >= new_data.xsize() || new_data[(i, j)] != old_data[(i, j)])) - .map(|i| super::HistoryEntry::new_cell(old_data, i)); - let resize_iter = std::iter::once(super::HistoryEntry::new_resize_cells(old_data)); - match new_data.xsize().cmp(&old_data.xsize()) { + let cell_iter = (0..old_frame.len()) + .filter(|&i| { + (0..8).any(|j| { + i >= new_frame.len() || new_frame.cell_data[(i, j)] != old_frame.cell_data[(i, j)] + }) + }) + .map(|i| super::HistoryEntry::new_cell(&old_frame.cell_data, i)); + let resize_iter = std::iter::once_with(|| super::HistoryEntry::ResizeCells(old_frame.len())); + match new_frame.len().cmp(&old_frame.len()) { std::cmp::Ordering::Equal => cell_iter.collect(), std::cmp::Ordering::Less => cell_iter.chain(resize_iter).collect(), std::cmp::Ordering::Greater => resize_iter .chain(cell_iter) .chain( - (old_data.xsize()..new_data.xsize()).map(|i| super::HistoryEntry::Cell { + (old_frame.len()..new_frame.len()).map(|i| super::HistoryEntry::Cell { index: i, data: [-1, 0, 0, 0, 0, 0, 0, 0], }), From 1140aa2cb39676a6421afdb0cfd75cf5d9741655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 13 Aug 2024 20:26:42 -0400 Subject: [PATCH 109/109] Fix animation editor cellpicker clipping (#148) * Revert "Allocate a separate egui painter for the animation frame view" This reverts commit 50e0a797d1cb94b4c56da62e58c11b2fb1c48d5c. * Fix frame view cellpicker `scroll_rect` height I'm not sure why egui is setting the height of `scroll_rect` incorrectly, but since we know what the height of the cellpicker should be we can easily fix this. --- crates/components/src/animation_frame_view.rs | 29 +++++++++---------- .../ui/src/windows/animations/frame_edit.rs | 4 ++- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/components/src/animation_frame_view.rs b/crates/components/src/animation_frame_view.rs index b16df1f4..ebaace43 100644 --- a/crates/components/src/animation_frame_view.rs +++ b/crates/components/src/animation_frame_view.rs @@ -77,9 +77,7 @@ impl AnimationFrameView { ) -> egui::InnerResponse> { let canvas_rect = ui.max_rect(); let canvas_center = canvas_rect.center(); - let egui_painter = ui - .painter() - .with_clip_rect(canvas_rect.intersect(clip_rect)); + ui.set_clip_rect(canvas_rect.intersect(clip_rect)); let mut response = ui.allocate_rect(canvas_rect, egui::Sense::click_and_drag()); @@ -153,16 +151,17 @@ impl AnimationFrameView { ); let painter = luminol_graphics::Painter::new(self.frame.prepare(&update_state.graphics)); - egui_painter.add(luminol_egui_wgpu::Callback::new_paint_callback( - canvas_rect, - painter, - )); + ui.painter() + .add(luminol_egui_wgpu::Callback::new_paint_callback( + canvas_rect, + painter, + )); let screen_alpha = (egui::ecolor::linear_from_gamma(screen_color.alpha as f32 / 255.) * 255.) .round() as u8; if screen_alpha > 0 { - egui_painter.rect_filled( + ui.painter().rect_filled( egui::Rect::EVERYTHING, egui::Rounding::ZERO, egui::Color32::from_rgba_unmultiplied( @@ -178,21 +177,21 @@ impl AnimationFrameView { // Draw the grid lines and the border of the animation frame if draw_rects { - egui_painter.line_segment( + ui.painter().line_segment( [ egui::pos2(-(FRAME_WIDTH as f32 / 2.), 0.) * scale + offset, egui::pos2(FRAME_WIDTH as f32 / 2., 0.) * scale + offset, ], egui::Stroke::new(1., egui::Color32::DARK_GRAY), ); - egui_painter.line_segment( + ui.painter().line_segment( [ egui::pos2(0., -(FRAME_HEIGHT as f32 / 2.)) * scale + offset, egui::pos2(0., FRAME_HEIGHT as f32 / 2.) * scale + offset, ], egui::Stroke::new(1., egui::Color32::DARK_GRAY), ); - egui_painter.rect_stroke( + ui.painter().rect_stroke( egui::Rect::from_center_size( offset.to_pos2(), egui::vec2(FRAME_WIDTH as f32, FRAME_HEIGHT as f32) * scale, @@ -269,7 +268,7 @@ impl AnimationFrameView { .iter() .map(|(_, cell)| (cell.rect * scale).translate(offset)) { - egui_painter.rect_stroke( + ui.painter().rect_stroke( cell_rect, 5., egui::Stroke::new(1., egui::Color32::DARK_GRAY), @@ -284,7 +283,7 @@ impl AnimationFrameView { .iter() .map(|(_, cell)| (cell.rect * scale).translate(offset)) { - egui_painter.rect_stroke( + ui.painter().rect_stroke( cell_rect, 5., egui::Stroke::new( @@ -301,7 +300,7 @@ impl AnimationFrameView { // Draw a yellow rectangle on the border of the hovered cell if let Some(i) = self.hovered_cell_index { let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); - egui_painter.rect_stroke( + ui.painter().rect_stroke( cell_rect, 5., egui::Stroke::new(3., egui::Color32::YELLOW), @@ -311,7 +310,7 @@ impl AnimationFrameView { // Draw a magenta rectangle on the border of the selected cell if let Some(i) = self.selected_cell_index { let cell_rect = (self.frame.cells()[i].rect * scale).translate(offset); - egui_painter.rect_stroke( + ui.painter().rect_stroke( cell_rect, 5., egui::Stroke::new(3., egui::Color32::from_rgb(255, 0, 255)), diff --git a/crates/ui/src/windows/animations/frame_edit.rs b/crates/ui/src/windows/animations/frame_edit.rs index fe764ec4..fa0bdd07 100644 --- a/crates/ui/src/windows/animations/frame_edit.rs +++ b/crates/ui/src/windows/animations/frame_edit.rs @@ -914,7 +914,9 @@ pub fn show_frame_edit( state.frame_needs_update = false; } - egui::ScrollArea::horizontal().show_viewport(ui, |ui, scroll_rect| { + egui::ScrollArea::horizontal().show_viewport(ui, |ui, mut scroll_rect| { + scroll_rect + .set_height(luminol_graphics::primitives::cells::CELL_SIZE as f32 * cellpicker.scale); cellpicker.ui(update_state, ui, scroll_rect); });