@ -1,12 +1,28 @@
// TODO:
// * Debug Panel
// * Describe cell I am hovering over (Note, Octave, etc)
// * Count # of cells on the board
// * Fix tiling repeating
// * Negative coordinates should not mirror
// * Fix randomly audio not working
// * space -> play/pause sim
// * m -> un/mute audio
#![ allow(clippy::complexity) ]
// Only because of FromTemplat macros unfortunately...
#![ allow(dead_code) ]
use bevy ::{
feathers ::{ containers ::* , theme ::ThemeProps } ,
feathers ::{
constants ::{ fonts , size } ,
containers ::* ,
font_styles ::InheritableFont ,
theme ::{ InheritableThemeTextColor , ThemeProps } ,
tokens ,
} ,
text ::FontWeight ,
ui ::{ Checked , InteractionDisabled } ,
ui_widgets ::{
ActivateOnPress , SliderPrecision , SliderStep , ValueChange , checkbox_self_update ,
ActivateOnPress , Checkbox, SliderPrecision, SliderStep , ValueChange , checkbox_self_update ,
slider_self_update ,
} ,
} ;
@ -26,6 +42,7 @@ fn main() {
. init_resource ::< CellAssets > ( )
. init_resource ::< Panning > ( )
. init_resource ::< PitchMap > ( )
. init_resource ::< LastBoardState > ( )
. insert_resource ( ClearColor ( WHITE . into ( ) ) )
. insert_resource ( UiTheme ( create_light_theme ( ) ) )
. init_state ::< SimulationPlayback > ( )
@ -67,6 +84,8 @@ fn main() {
reassign_cell_pitch . run_if ( resource_changed ::< PitchMap > ) ,
update_audio ,
update_material ,
update_debug_info_cell_count ,
update_debug_info_coordinates ,
) ,
)
. add_systems ( Update , cell_lifecycle . run_if ( on_message ::< Lifecycle > ) )
@ -77,6 +96,14 @@ fn main() {
const SCALE : f32 = 100.0 ;
#[ derive(Component, PartialEq, Clone, Default) ]
enum DebugInfo {
CellCount ,
Coordinates ,
#[ default ]
Empty ,
}
#[ derive(States, Debug, Default, Hash, PartialEq, Eq, Clone, Component, FromTemplate) ]
enum AudioPlayback {
#[ default ]
@ -100,6 +127,8 @@ enum UiButton {
AddLowOctave ,
SubLowOctave ,
Note ,
Reset ,
Wipe ,
#[ default ]
None ,
}
@ -120,6 +149,8 @@ impl UiButton {
Self ::AddLowOctave = > "todo.png" ,
Self ::SubLowOctave = > "todo.png" ,
Self ::Note = > "todo.png" ,
Self ::Reset = > "return.png" ,
Self ::Wipe = > "trashcan.png" ,
Self ::None = > todo! ( ) ,
}
}
@ -175,24 +206,120 @@ fn update_pitch_map_resource_ui(
} ) ;
}
fn text_setup ( ) -> impl Scene {
bsn ! {
// Feathers propagates text styles only through entities that themselves have
// ThemedText, so a body holding loose text must carry these itself.
InheritableThemeTextColor ( tokens ::TEXT_MAIN )
InheritableFont {
font : fonts ::REGULAR ,
font_size : size ::MEDIUM_FONT ,
weight : FontWeight ::NORMAL ,
}
}
}
fn the_ui ( ) -> impl Scene {
bsn ! {
Node {
width : Val ::Percent ( 100.0 ) ,
top : Val ::Px ( 0.0 ) ,
left : Val ::Px ( 0.0 ) ,
display : Display ::Flex ,
flex_direction : FlexDirection ::Row ,
justify_content : JustifyContent ::SpaceBetween ,
}
Transform
Children [
primary_ui ( ) ,
debug_ui ( ) ,
]
}
}
fn debug_ui ( ) -> impl Scene {
bsn ! {
Node {
justify_self : JustifySelf ::End ,
display : Display ::Flex ,
flex_direction : FlexDirection ::Column ,
}
// TODO: Toggle visibility when debugging disabled
pane ( )
Transform
Children [
pane_header ( )
Children [
Text ( "Debug Info" ) ThemedText ,
] ,
pane_body ( )
Children [
subpane ( )
Node {
display : Display ::Flex ,
flex_direction : FlexDirection ::Row ,
}
Children [
subpane_header ( ) Children [
Text ( "Cell Count" ) ThemedText
]
subpane_body ( )
Node {
display : Display ::Flex ,
flex_direction : FlexDirection ::Row ,
align_items : AlignItems ::Stretch ,
justify_content : JustifyContent ::Start ,
}
text_setup ( )
Children [
Text ( "###" ) ThemedText template_value ( DebugInfo ::CellCount )
]
] ,
subpane ( )
Node {
display : Display ::Flex ,
flex_direction : FlexDirection ::Row ,
}
Children [
subpane_header ( ) Children [
Text ( "Coordinates" ) ThemedText
]
subpane_body ( )
Node {
display : Display ::Flex ,
flex_direction : FlexDirection ::Row ,
align_items : AlignItems ::Stretch ,
justify_content : JustifyContent ::Start ,
}
text_setup ( )
Children [
Text ( "##,##" ) ThemedText template_value ( DebugInfo ::Coordinates )
]
]
]
]
}
}
fn primary_ui ( ) -> impl Scene {
fn toggle_note (
this : On < Activate > ,
notes : Query < & Note , With < UiButton > > ,
change: On < ValueChange < bool > > ,
notes : Query < & Note , With < Checkbox > > ,
mut pitch_map : ResMut < PitchMap > ,
) {
let this_note = notes . get ( this . entity ) . unwrap ( ) ;
pitch_map . toggle ( this_note ) ;
if change . is_final {
// TODO: if last selected note and de-selected, undo that move please
let this_note = notes . get ( change . source ) . unwrap ( ) ;
pitch_map . set_note ( this_note , change . value ) ;
info ! ( "(note {:?}) Updated pitch map: {:#?}" , this_note , pitch_map ) ;
}
}
bsn ! {
Node {
top : Val ::Px ( 0.0 ) ,
left : Val ::Px ( 0.0 ) ,
display : Display ::Flex ,
flex_direction : FlexDirection ::Column ,
justify_content : JustifyContent ::Start ,
}
Transform
pane ( )
@ -235,7 +362,13 @@ fn the_ui() -> impl Scene {
} ) ,
ui_button ( UiButton ::Play )
SimulationPlayback ::Run
on ( | _ : On < Activate > , mut next : ResMut < NextState < SimulationPlayback > > | {
on ( |
_ : On < Activate > ,
mut next : ResMut < NextState < SimulationPlayback > > ,
mut last_board_state : ResMut < LastBoardState > ,
coordinates : Query < & Coordinates , With < Cell > >
| {
last_board_state . coordinates = coordinates . iter ( ) . cloned ( ) . collect ( ) ;
// Set simulation state
next . set ( SimulationPlayback ::Run ) ;
} ) ,
@ -243,6 +376,28 @@ fn the_ui() -> impl Scene {
on ( | _ : On < Activate > , mut next : ResMut < NextState < SimulationPlayback > > | {
next . set ( SimulationPlayback ::Step ) ;
} ) ,
ui_button ( UiButton ::Reset )
on ( |
_ : On < Activate > ,
last_board_state : Res < LastBoardState > ,
mut lifecycle : MessageWriter < Lifecycle > ,
cells : Query < Entity , With < Cell > > ,
mut commands : Commands ,
| {
// Todo: reconcile board instead of despawn/spawning the world
cells . iter ( ) . for_each ( | e | {
commands . entity ( e ) . despawn ( ) ;
} ) ;
last_board_state . coordinates . iter ( ) . for_each ( | c | {
lifecycle . write ( Lifecycle ::Alive ( c . clone ( ) ) ) ;
} ) ;
} ) ,
ui_button ( UiButton ::Wipe )
on ( | _ : On < Activate > , cells : Query < Entity , With < Cell > > , mut commands : Commands | {
cells . iter ( ) . for_each ( | e | {
commands . entity ( e ) . despawn ( ) ;
} ) ;
} ) ,
ui_button ( UiButton ::ZoomOut )
on ( | _ : On < Activate > , mut projection : Query < & mut Projection > | {
projection . iter_mut ( ) . for_each ( | mut p | match * p {
@ -287,40 +442,40 @@ fn the_ui() -> impl Scene {
}
Children [
ui_checkbox ( "A" )
Note::A
template_value( Note::A )
on ( toggle_note ) ,
ui_checkbox ( "A#" )
Note::ASharp
template_value( Note::ASharp )
on ( toggle_note ) ,
ui_checkbox ( "B" )
Note::B
template_value( Note::B )
on ( toggle_note ) ,
ui_checkbox ( "C" )
Note::C
template_value( Note::C )
on ( toggle_note ) ,
ui_checkbox ( "C#" )
Note::CSharp
template_value( Note::CSharp )
on ( toggle_note ) ,
ui_checkbox ( "D" )
Note::D
template_value( Note::D )
on ( toggle_note ) ,
ui_checkbox ( "D#" )
Note::DSharp
template_value( Note::DSharp )
on ( toggle_note ) ,
ui_checkbox ( "E" )
Note::E
template_value( Note::E )
on ( toggle_note ) ,
ui_checkbox ( "F" )
Note::F
template_value( Note::F )
on ( toggle_note ) ,
ui_checkbox ( "F#" )
Note::FSharp
template_value( Note::FSharp )
on ( toggle_note ) ,
ui_checkbox ( "G" )
Note::G
template_value( Note::G )
on ( toggle_note ) ,
ui_checkbox ( "G#" )
Note::GSharp
template_value( Note::GSharp )
on ( toggle_note ) ,
]
] ,
@ -342,6 +497,7 @@ fn the_ui() -> impl Scene {
on ( | value_change : On < ValueChange < f32 > > , mut pitch_map : ResMut < PitchMap > | {
if value_change . is_final {
pitch_map . octaves_up = value_change . value as usize ;
info ! ( "(octave up) Updated pitch map: {:#?}" , pitch_map ) ;
}
} )
on ( slider_self_update ) ,
@ -351,6 +507,7 @@ fn the_ui() -> impl Scene {
on ( | value_change : On < ValueChange < f32 > > , mut pitch_map : ResMut < PitchMap > | {
if value_change . is_final {
pitch_map . octaves_down = value_change . value as usize ;
info ! ( "(octave down) Updated pitch map: {:#?}" , pitch_map ) ;
}
} )
on ( slider_self_update ) ,
@ -363,6 +520,11 @@ fn the_ui() -> impl Scene {
#[ derive(Default, Debug, Resource) ]
struct NextCoordinates ( Coordinates ) ;
#[ derive(Default, Debug, Resource) ]
struct LastBoardState {
coordinates : Vec < Coordinates > ,
}
#[ derive(Debug, Default, Clone, PartialEq, Hash, Eq, Component, FromTemplate) ]
#[ require(Visibility, Transform) ]
struct Coordinates {
@ -610,15 +772,23 @@ fn new_cell(
let playback_settings = PlaybackSettings ::ONCE . with_volume ( volume ) ;
info ! ( "using coordinates {:?}" , c ) ;
let cell_pitch = pitch_map . coordinates_cell_pitch ( & c ) ;
#[ derive(Clone, Default, Component) ]
enum Foo {
#[ default ]
A ,
}
bsn ! {
Cell
Coordinates { x , y }
template_value ( cell_pitch )
template_value ( playback_settings )
template_value ( Transform ::from_translation ( c . as_translation ( ) + Vec3 ::new ( 0.0 , 0.0 , 1.0 ) ) )
template_value ( Mesh2dTemplate ( cell_assets . mesh . clone ( ) . into ( ) ) )
template_value ( AudioPlayerTemplate ( cell_assets . pitches . get ( & pitch_map . coordinates_cell_pitch ( & c ) ) . unwrap ( ) . clone ( ) . into ( ) ) )
// AudioPlayer added by update_audio
// MeshMaterial2d added by update_material
template_value ( MeshMaterial2dTemplate ( cell_assets . materials . get ( & pitch_map . coordinates_cell_pitch ( & c ) ) . unwrap ( ) . clone ( ) . into ( ) ) )
}
}
@ -673,8 +843,7 @@ struct CellAssets {
mesh : Handle < Mesh > ,
}
// TODO: Impl Ord to sort Notes
#[ derive(Hash, PartialEq, Eq, Debug, Clone, Component, Default, FromTemplate) ]
#[ derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, Hash, Ord, PartialOrd) ]
enum Note {
#[ default ]
A ,
@ -728,7 +897,7 @@ impl Note {
}
}
#[ derive( Hash, PartialEq, Eq, Debug, Component )]
#[ derive( Component, Clone, Debug, Default, Reflect, PartialEq, Eq, Hash )]
struct CellPitch {
note : Note ,
octave : isize ,
@ -783,12 +952,12 @@ impl CellPitch {
Note ::FSharp ,
Note ::G ,
Note ::GSharp ,
] . into_iter ( ) . flat_map ( | note | {
( - 5 .. 6 ) . map ( move | octave | {
CellPitch {
note : note . clone ( ) ,
octave ,
}
]
. into_iter ( )
. flat_map ( | note | {
( - 5 .. 6 ) . map ( move | octave | CellPitch {
note : note . clone ( ) ,
octave ,
} )
} )
}
@ -962,12 +1131,14 @@ impl Default for PitchMap {
}
impl PitchMap {
fn toggl e( & mut self , n : & Note ) {
if let Some ( idx ) = self . notes . iter ( ) . position ( | v | v = = n ) {
fn set_not e( & mut self , n : & Note , value : bool ) {
if ! value & & let Some ( idx ) = self . notes . iter ( ) . position ( | v | v = = n ) {
self . notes . swap_remove ( idx ) ;
} else {
} else if value {
self . notes . push ( n . clone ( ) ) ;
}
// always ensure the notes list is sorted
self . notes . sort ( ) ;
}
/// Given the currently selected notes and octave range selected in the PitchMap convert the given coordinates to a CellPitch
@ -979,37 +1150,35 @@ impl PitchMap {
} ;
let octave : isize = {
let num_octaves : usize = self . octaves_up + self . octaves_down + 1 ;
let y_coord : usize = c . y . strict_abs ( ) as usize ; // TODO: shift first or something
info ! ( "num_octaves: {:?}" , num_octaves ) ;
info ! ( "y_coord: {:?}" , y_coord ) ;
let y_coord : usize = c . y . strict_abs ( ) as usize ; // TODO: shift first or something
debug ! ( "num_octaves: {:?}" , num_octaves ) ;
debug ! ( "y_coord: {:?}" , y_coord ) ;
( y_coord % num_octaves ) as isize - 5
} ;
let cp = CellPitch { note , octave } ;
info ! ( "Cell Pitch: {:?}" , cp ) ;
debug ! ( "Cell Pitch: {:?}" , cp ) ;
cp
}
}
/// When the pitch map changes, update all cells to use the correct pitch (note + octave)
fn reassign_cell_pitch (
mut query : Query < ( & mut CellPitch , & Coordinates ) , With < Cell > > ,
pitch_map : Res < PitchMap > ,
) {
fn reassign_cell_pitch ( mut query : Query < ( & mut CellPitch , & Coordinates ) > , pitch_map : Res < PitchMap > ) {
info ! ( "query size: {:?}" , query . iter ( ) . len ( ) ) ;
query . iter_mut ( ) . for_each ( | ( mut cell_pitch , c ) | {
* cell_pitch = pitch_map . coordinates_cell_pitch ( c ) ;
info ! ( "Updating cell pitch for {:?}" , c ) ;
} ) ;
}
// When CellPitch is added/updated on a cell, update the material
fn update_material (
mut query : Query <
( & CellPitch , & mut MeshMaterial2d < ColorMaterial > ) ,
Or < ( Added < CellPitch > , Changed < CellPitch > ) > ,
> ,
mut query : Query < ( & CellPitch , Entity ) , Or < ( Added < CellPitch > , Changed < CellPitch > ) > > ,
cell_assets : ResMut < CellAssets > ,
mut commands : Commands ,
) {
query . iter_mut ( ) . for_each ( | ( cell_pitch , mut material ) | {
material . 0 = cell_assets . materials . get ( cell_pitch ) . unwrap ( ) . clone ( ) ;
query . iter_mut ( ) . for_each ( | ( cell_pitch , e ) | {
let h = cell_assets . materials . get ( cell_pitch ) . unwrap ( ) . clone ( ) ;
commands . entity ( e ) . insert ( MeshMaterial2d ( h ) ) ;
} )
}
@ -1024,3 +1193,22 @@ fn update_audio(
commands . entity ( e ) . insert ( AudioPlayer ( h ) ) ;
} ) ;
}
fn update_debug_info_cell_count ( query : Query < & Cell > , mut text : Query < ( & DebugInfo , & mut Text ) > ) {
text . iter_mut ( )
. filter ( | ( di , _ ) | * * di = = DebugInfo ::CellCount )
. for_each ( | ( _ , mut t ) | {
* t = Text ::new ( format! ( "{}" , query . count ( ) ) ) ;
} ) ;
}
fn update_debug_info_coordinates (
coordinates : Res < NextCoordinates > ,
mut text : Query < ( & DebugInfo , & mut Text ) > ,
) {
text . iter_mut ( )
. filter ( | ( di , _ ) | * * di = = DebugInfo ::Coordinates )
. for_each ( | ( _ , mut t ) | {
* t = Text ::new ( format! ( "{},{}" , coordinates . 0. x , coordinates . 0. y ) ) ;
} ) ;
}