1use blinksy::{
64 color::{ColorCorrection, FromColor, LinearSrgb, Srgb},
65 driver::Driver,
66 layout::{Layout1d, Layout2d, Layout3d, LayoutForDim},
67 markers::{Dim1d, Dim2d, Dim3d},
68};
69use core::{fmt, marker::PhantomData};
70use egui_miniquad as egui_mq;
71use glam::{vec3, Mat4, Vec3, Vec4, Vec4Swizzles};
72use miniquad::*;
73use std::sync::mpsc::{channel, Receiver, SendError, Sender};
74
75#[derive(Debug, Clone)]
79pub struct DesktopConfig {
80 pub window_title: String,
82
83 pub window_width: i32,
85
86 pub window_height: i32,
88
89 pub led_radius: f32,
91
92 pub high_dpi: bool,
94
95 pub orthographic_view: bool,
97
98 pub background_color: (f32, f32, f32, f32),
100}
101
102impl Default for DesktopConfig {
103 fn default() -> Self {
104 Self {
105 window_title: "Blinksy".to_string(),
106 window_width: 540,
107 window_height: 540,
108 led_radius: 0.05,
109 high_dpi: true,
110 orthographic_view: true,
111 background_color: (0.1, 0.1, 0.1, 1.0),
112 }
113 }
114}
115
116pub struct Desktop<Dim, Layout> {
126 driver: DesktopDriver<Dim, Layout>,
127 stage: DesktopStageOptions,
128}
129
130impl Desktop<Dim1d, ()> {
131 pub fn new_1d<Layout>() -> Desktop<Dim1d, Layout>
143 where
144 Layout: Layout1d,
145 {
146 Self::new_1d_with_config::<Layout>(DesktopConfig::default())
147 }
148
149 pub fn new_1d_with_config<Layout>(config: DesktopConfig) -> Desktop<Dim1d, Layout>
163 where
164 Layout: Layout1d,
165 {
166 let mut positions = Vec::with_capacity(Layout::PIXEL_COUNT);
167 for x in Layout::points() {
168 positions.push(vec3(x, 0.0, 0.0));
169 }
170
171 let (sender, receiver) = channel();
172 let is_window_closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
173 let is_window_closed_2 = is_window_closed.clone();
174
175 let driver = DesktopDriver {
176 dim: PhantomData,
177 layout: PhantomData,
178 brightness: 1.0,
179 correction: ColorCorrection::default(),
180 sender,
181 is_window_closed,
182 };
183 let stage = DesktopStageOptions {
184 positions,
185 receiver,
186 config,
187 is_window_closed: is_window_closed_2,
188 };
189
190 Desktop { driver, stage }
191 }
192}
193
194impl Desktop<Dim2d, ()> {
195 pub fn new_2d<Layout>() -> Desktop<Dim2d, Layout>
208 where
209 Layout: Layout2d,
210 {
211 Self::new_2d_with_config::<Layout>(DesktopConfig::default())
212 }
213
214 pub fn new_2d_with_config<Layout>(config: DesktopConfig) -> Desktop<Dim2d, Layout>
228 where
229 Layout: Layout2d,
230 {
231 let mut positions = Vec::with_capacity(Layout::PIXEL_COUNT);
232 for point in Layout::points() {
233 positions.push(vec3(point.x, point.y, 0.0));
234 }
235
236 let (sender, receiver) = channel();
237 let is_window_closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
238 let is_window_closed_2 = is_window_closed.clone();
239
240 let driver = DesktopDriver {
241 dim: PhantomData,
242 layout: PhantomData,
243 brightness: 1.0,
244 correction: ColorCorrection::default(),
245 sender,
246 is_window_closed,
247 };
248 let stage = DesktopStageOptions {
249 positions,
250 receiver,
251 config,
252 is_window_closed: is_window_closed_2,
253 };
254
255 Desktop { driver, stage }
256 }
257}
258
259impl Desktop<Dim3d, ()> {
260 pub fn new_3d<Layout>() -> Desktop<Dim3d, Layout>
273 where
274 Layout: Layout3d,
275 {
276 Self::new_3d_with_config::<Layout>(DesktopConfig::default())
277 }
278
279 pub fn new_3d_with_config<Layout>(config: DesktopConfig) -> Desktop<Dim3d, Layout>
293 where
294 Layout: Layout3d,
295 {
296 let mut positions = Vec::with_capacity(Layout::PIXEL_COUNT);
297 for point in Layout::points() {
298 positions.push(vec3(point.x, point.y, point.z));
299 }
300
301 let (sender, receiver) = channel();
302 let is_window_closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
303 let is_window_closed_2 = is_window_closed.clone();
304
305 let driver = DesktopDriver {
306 dim: PhantomData,
307 layout: PhantomData,
308 brightness: 1.0,
309 correction: ColorCorrection::default(),
310 sender,
311 is_window_closed,
312 };
313 let stage = DesktopStageOptions {
314 positions,
315 receiver,
316 config,
317 is_window_closed: is_window_closed_2,
318 };
319
320 Desktop { driver, stage }
321 }
322}
323
324impl<Dim, Layout> Desktop<Dim, Layout>
325where
326 Dim: 'static + Send,
327 Layout: 'static + Send,
328{
329 pub fn start<F>(self, f: F)
330 where
331 F: 'static + FnOnce(DesktopDriver<Dim, Layout>) + Send,
332 {
333 let Self { driver, stage } = self;
334
335 std::thread::spawn(move || f(driver));
336
337 DesktopStage::start(move || DesktopStage::new(stage));
338 }
339}
340
341pub struct DesktopDriver<Dim, Layout> {
350 dim: PhantomData<Dim>,
351 layout: PhantomData<Layout>,
352 brightness: f32,
353 correction: ColorCorrection,
354 sender: Sender<LedMessage>,
355 is_window_closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
356}
357
358impl<Dim, Layout> DesktopDriver<Dim, Layout> {
359 fn send(&self, message: LedMessage) -> Result<(), DesktopError> {
360 if self
361 .is_window_closed
362 .load(std::sync::atomic::Ordering::Relaxed)
363 {
364 return Err(DesktopError::WindowClosed);
365 }
366 self.sender.send(message)?;
367 Ok(())
368 }
369}
370
371#[derive(Debug)]
373pub enum DesktopError {
374 ChannelSend,
376
377 WindowClosed,
379}
380
381impl fmt::Display for DesktopError {
382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 match self {
384 DesktopError::ChannelSend => write!(f, "render thread channel disconnected"),
385 DesktopError::WindowClosed => write!(f, "window closed"),
386 }
387 }
388}
389
390impl core::error::Error for DesktopError {}
391
392impl From<SendError<LedMessage>> for DesktopError {
393 fn from(_: SendError<LedMessage>) -> Self {
394 DesktopError::ChannelSend
395 }
396}
397
398enum LedMessage {
400 UpdateColors(Vec<LinearSrgb>),
402
403 UpdateBrightness(f32),
405
406 UpdateColorCorrection(ColorCorrection),
408
409 Quit,
411}
412
413impl<Dim, Layout> Driver for DesktopDriver<Dim, Layout>
414where
415 Layout: LayoutForDim<Dim>,
416{
417 type Error = DesktopError;
418 type Color = LinearSrgb;
419 type Word = LinearSrgb;
420
421 fn encode<const PIXEL_COUNT: usize, const FRAME_BUFFER_SIZE: usize, Pixels, Color>(
422 &mut self,
423 pixels: Pixels,
424 _brightness: f32,
425 _correction: ColorCorrection,
426 ) -> heapless::Vec<Self::Word, FRAME_BUFFER_SIZE>
427 where
428 Pixels: IntoIterator<Item = Color>,
429 Self::Color: FromColor<Color>,
430 {
431 pixels
432 .into_iter()
433 .map(|color| LinearSrgb::from_color(color))
434 .collect()
435 }
436
437 fn write<const FRAME_BUFFER_SIZE: usize>(
438 &mut self,
439 frame: heapless::Vec<Self::Word, FRAME_BUFFER_SIZE>,
440 brightness: f32,
441 correction: ColorCorrection,
442 ) -> Result<(), Self::Error> {
443 if self.brightness != brightness {
444 self.brightness = brightness;
445 self.send(LedMessage::UpdateBrightness(brightness))?;
446 }
447
448 if self.correction != correction {
449 self.correction = correction;
450 self.send(LedMessage::UpdateColorCorrection(correction))?;
451 }
452
453 let colors: Vec<LinearSrgb> = frame.into_iter().collect();
454
455 self.send(LedMessage::UpdateColors(colors))?;
456 Ok(())
457 }
458}
459
460impl<Dim, Layout> Drop for DesktopDriver<Dim, Layout> {
461 fn drop(&mut self) {
462 let _ = self.send(LedMessage::Quit);
463 }
464}
465
466struct Camera {
470 distance: f32,
472
473 target: Vec3,
475
476 yaw: f32,
478
479 pitch: f32,
481
482 aspect_ratio: f32,
484
485 use_orthographic: bool,
487
488 fov: f32,
490}
491
492impl Camera {
493 const DEFAULT_DISTANCE: f32 = 2.0;
494 const DEFAULT_TARGET: Vec3 = Vec3::ZERO;
495 const DEFAULT_YAW: f32 = core::f32::consts::PI * 0.5;
496 const DEFAULT_PITCH: f32 = 0.0;
497 const MIN_DISTANCE: f32 = 0.5;
498 const MAX_DISTANCE: f32 = 10.0;
499 const MAX_PITCH: f32 = core::f32::consts::PI / 2.0 - 0.1;
500 const MIN_PITCH: f32 = -core::f32::consts::PI / 2.0 + 0.1;
501
502 fn new(aspect_ratio: f32, use_orthographic: bool) -> Self {
504 let default_fov = 2.0 * ((1.0 / Self::DEFAULT_DISTANCE).atan());
505 Self {
506 distance: Self::DEFAULT_DISTANCE,
507 target: Self::DEFAULT_TARGET,
508 yaw: Self::DEFAULT_YAW,
509 pitch: Self::DEFAULT_PITCH,
510 aspect_ratio,
511 use_orthographic,
512 fov: default_fov,
513 }
514 }
515
516 fn reset(&mut self) {
518 self.distance = Self::DEFAULT_DISTANCE;
519 self.target = Self::DEFAULT_TARGET;
520 self.yaw = Self::DEFAULT_YAW;
521 self.pitch = Self::DEFAULT_PITCH;
522 }
523
524 fn set_aspect_ratio(&mut self, aspect_ratio: f32) {
526 self.aspect_ratio = aspect_ratio;
527 }
528
529 fn toggle_projection_mode(&mut self) {
531 self.use_orthographic = !self.use_orthographic;
532 }
533
534 fn rotate(&mut self, delta_x: f32, delta_y: f32) {
536 self.yaw -= delta_x * 0.01;
537 self.pitch += delta_y * 0.01;
538 self.pitch = self.pitch.clamp(Self::MIN_PITCH, Self::MAX_PITCH);
539 }
540
541 fn zoom(&mut self, delta: f32) {
543 self.distance -= delta * 0.2;
544 self.distance = self.distance.clamp(Self::MIN_DISTANCE, Self::MAX_DISTANCE);
545 }
546
547 fn position(&self) -> Vec3 {
549 let x = self.distance * self.pitch.cos() * self.yaw.cos();
550 let y = self.distance * self.pitch.sin();
551 let z = self.distance * self.pitch.cos() * self.yaw.sin();
552 self.target + vec3(x, y, z)
553 }
554
555 fn view_matrix(&self) -> Mat4 {
557 let eye = self.position();
558 let up = if self.pitch.abs() > std::f32::consts::PI * 0.49 {
559 Vec3::new(self.yaw.sin(), 0.0, -self.yaw.cos())
560 } else {
561 Vec3::Y
562 };
563 Mat4::look_at_rh(eye, self.target, up)
564 }
565
566 fn projection_matrix(&self) -> Mat4 {
568 if self.use_orthographic {
569 let vertical_size = 1.0 * (self.distance / 2.0);
570 Mat4::orthographic_rh_gl(
571 -vertical_size * self.aspect_ratio,
572 vertical_size * self.aspect_ratio,
573 -vertical_size,
574 vertical_size,
575 -100.0,
576 100.0,
577 )
578 } else {
579 Mat4::perspective_rh_gl(self.fov, self.aspect_ratio, 0.1, 100.0)
580 }
581 }
582
583 fn view_projection_matrix(&self) -> Mat4 {
585 self.projection_matrix() * self.view_matrix()
586 }
587}
588
589struct LedPicker {
591 positions: Vec<Vec3>,
592 selected_led: Option<usize>,
593 radius: f32,
594}
595
596impl LedPicker {
597 fn new(positions: Vec<Vec3>, radius: f32) -> Self {
598 Self {
599 positions,
600 selected_led: None,
601 radius,
602 }
603 }
604
605 fn screen_pos_to_ray(&self, screen_x: f32, screen_y: f32, camera: &Camera) -> (Vec3, Vec3) {
607 let (width, height) = window::screen_size();
608
609 let x = 2.0 * screen_x / width - 1.0;
611 let y = 1.0 - 2.0 * screen_y / height;
612
613 let proj_inv = camera.projection_matrix().inverse();
615 let view_inv = camera.view_matrix().inverse();
616
617 let near_point = proj_inv * Vec4::new(x, y, -1.0, 1.0);
619 let far_point = proj_inv * Vec4::new(x, y, 1.0, 1.0);
620
621 let near_point = near_point / near_point.w;
622 let far_point = far_point / far_point.w;
623
624 let near_point_world = view_inv * near_point;
625 let far_point_world = view_inv * far_point;
626
627 let origin = near_point_world.xyz();
628 let direction = (far_point_world.xyz() - near_point_world.xyz()).normalize();
629
630 (origin, direction)
631 }
632
633 fn pick_led(&self, screen_x: f32, screen_y: f32, camera: &Camera) -> Option<usize> {
635 let (ray_origin, ray_direction) = self.screen_pos_to_ray(screen_x, screen_y, camera);
636
637 let mut closest_led = None;
639 let mut closest_distance = f32::MAX;
640
641 for (i, &position) in self.positions.iter().enumerate() {
642 let oc = ray_origin - position;
644 let a = ray_direction.dot(ray_direction);
645 let b = 2.0 * oc.dot(ray_direction);
646 let c = oc.dot(oc) - self.radius * self.radius;
647 let discriminant = b * b - 4.0 * a * c;
648
649 if discriminant > 0.0 {
650 let t = (-b - discriminant.sqrt()) / (2.0 * a);
651 if t > 0.0 && t < closest_distance {
652 closest_distance = t;
653 closest_led = Some(i);
654 }
655 }
656 }
657
658 closest_led
659 }
660
661 fn try_select_at(&mut self, screen_x: f32, screen_y: f32, camera: &Camera) {
663 self.selected_led = self.pick_led(screen_x, screen_y, camera);
664 }
665
666 fn clear_selection(&mut self) {
668 self.selected_led = None;
669 }
670}
671
672struct UiManager {
674 egui_mq: egui_mq::EguiMq,
675 want_mouse_capture: bool,
676}
677
678impl UiManager {
679 fn new(ctx: &mut dyn RenderingBackend) -> Self {
680 Self {
681 egui_mq: egui_mq::EguiMq::new(ctx),
682 want_mouse_capture: false,
683 }
684 }
685
686 fn mouse_motion_event(&mut self, x: f32, y: f32) {
688 self.egui_mq.mouse_motion_event(x, y);
689 }
690
691 fn mouse_wheel_event(&mut self, x: f32, y: f32) {
693 self.egui_mq.mouse_wheel_event(x, y);
694 }
695
696 fn mouse_button_down_event(&mut self, button: MouseButton, x: f32, y: f32) {
698 self.egui_mq.mouse_button_down_event(button, x, y);
699 }
700
701 fn mouse_button_up_event(&mut self, button: MouseButton, x: f32, y: f32) {
703 self.egui_mq.mouse_button_up_event(button, x, y);
704 }
705
706 fn key_down_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
708 self.egui_mq.key_down_event(keycode, keymods);
709 }
710
711 fn key_up_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
713 self.egui_mq.key_up_event(keycode, keymods);
714 }
715
716 fn char_event(&mut self, character: char) {
718 self.egui_mq.char_event(character);
719 }
720
721 #[allow(clippy::too_many_arguments)]
723 fn render_led_info(
724 &mut self,
725 ctx: &mut dyn RenderingBackend,
726 led_picker: &mut LedPicker,
727 positions: &[Vec3],
728 colors: &[LinearSrgb],
729 brightness: f32,
730 correction: ColorCorrection,
731 ) {
732 self.egui_mq.run(ctx, |_mq_ctx, egui_ctx| {
733 self.want_mouse_capture = egui_ctx.wants_pointer_input();
734
735 if let Some(led_idx) = led_picker.selected_led {
737 let pos = positions[led_idx];
738 let color = colors[led_idx];
739
740 let (red, green, blue) = (color.red, color.green, color.blue);
741
742 let (bright_red, bright_green, bright_blue) =
744 (red * brightness, green * brightness, blue * brightness);
745
746 let (correct_red, correct_green, correct_blue) = (
748 bright_red * correction.red,
749 bright_green * correction.green,
750 bright_blue * correction.blue,
751 );
752
753 let Srgb {
755 red: srgb_red,
756 green: srgb_green,
757 blue: srgb_blue,
758 } = LinearSrgb::new(correct_red, correct_green, correct_blue).to_srgb();
759
760 egui::Window::new("LED Information")
761 .collapsible(false)
762 .resizable(false)
763 .show(egui_ctx, |ui| {
764 ui.label(format!("LED Index: {}", led_idx));
765 ui.label(format!(
766 "Position: ({:.3}, {:.3}, {:.3})",
767 pos.x, pos.y, pos.z
768 ));
769
770 ui.label(format!(
772 "Linear RGB: R={:.3}, G={:.3}, B={:.3}",
773 red, green, blue,
774 ));
775
776 ui.label(format!("Global Brightness: {:.3}", brightness));
778
779 ui.label(format!(
781 "Brightness-adjusted RGB: R={:.3}, G={:.3}, B={:.3}",
782 bright_red, bright_green, bright_blue
783 ));
784
785 ui.label(format!(
787 "Global Color Correction: R={:.3}, G={:.3}, B={:.3}",
788 correction.red, correction.green, correction.blue
789 ));
790
791 ui.label(format!(
793 "Correction-adjusted RGB: R={:.3}, G={:.3}, B={:.3}",
794 correct_red, correct_green, correct_blue
795 ));
796
797 ui.label(format!(
799 "Final sRGB: R={:.3}, G={:.3}, B={:.3}",
800 srgb_red, srgb_green, srgb_blue
801 ));
802
803 let (_, color_rect) =
805 ui.allocate_space(egui::vec2(ui.available_width(), 30.0));
806 let color_preview = egui::Color32::from_rgb(
807 (srgb_red * 255.0) as u8,
808 (srgb_green * 255.0) as u8,
809 (srgb_blue * 255.0) as u8,
810 );
811 ui.painter().rect_filled(color_rect, 4.0, color_preview);
812 ui.add_space(10.0); if ui.button("Deselect").clicked() {
816 led_picker.selected_led = None;
817 }
818 });
819 }
820 });
821 }
822
823 fn draw(&mut self, ctx: &mut dyn RenderingBackend) {
825 self.egui_mq.draw(ctx);
826 }
827}
828
829struct Renderer {
831 pipeline: Pipeline,
832 bindings: Bindings,
833}
834
835impl Renderer {
836 fn new(ctx: &mut dyn RenderingBackend, led_radius: f32) -> Self {
837 let vertex_buffer = Self::create_vertex_buffer(ctx, led_radius);
838 let index_buffer = Self::create_index_buffer(ctx);
839
840 let bindings = Bindings {
841 vertex_buffers: vec![vertex_buffer],
842 index_buffer,
843 images: vec![],
844 };
845
846 let shader = ctx
847 .new_shader(
848 ShaderSource::Glsl {
849 vertex: shader::VERTEX,
850 fragment: shader::FRAGMENT,
851 },
852 shader::meta(),
853 )
854 .unwrap();
855
856 let pipeline = ctx.new_pipeline(
857 &[
858 BufferLayout::default(),
859 BufferLayout {
860 step_func: VertexStep::PerInstance,
861 ..Default::default()
862 },
863 BufferLayout {
864 step_func: VertexStep::PerInstance,
865 ..Default::default()
866 },
867 ],
868 &[
869 VertexAttribute::with_buffer("in_pos", VertexFormat::Float3, 0),
870 VertexAttribute::with_buffer("in_color", VertexFormat::Float4, 0),
871 VertexAttribute::with_buffer("in_inst_pos", VertexFormat::Float3, 1),
872 VertexAttribute::with_buffer("in_inst_color", VertexFormat::Float4, 2),
873 ],
874 shader,
875 PipelineParams {
876 depth_test: Comparison::LessOrEqual,
877 depth_write: true,
878 ..Default::default()
879 },
880 );
881
882 Self { pipeline, bindings }
883 }
884
885 fn create_vertex_buffer(ctx: &mut dyn RenderingBackend, r: f32) -> BufferId {
886 #[rustfmt::skip]
887 let vertices: &[f32] = &[
888 0.0, -r, 0.0, 1.0, 0.0, 0.0, 1.0,
889 r, 0.0, r, 0.0, 1.0, 0.0, 1.0,
890 r, 0.0, -r, 0.0, 0.0, 1.0, 1.0,
891 -r, 0.0, -r, 1.0, 1.0, 0.0, 1.0,
892 -r, 0.0, r, 0.0, 1.0, 1.0, 1.0,
893 0.0, r, 0.0, 1.0, 0.0, 1.0, 1.0,
894 ];
895
896 ctx.new_buffer(
897 BufferType::VertexBuffer,
898 BufferUsage::Immutable,
899 BufferSource::slice(vertices),
900 )
901 }
902
903 fn create_index_buffer(ctx: &mut dyn RenderingBackend) -> BufferId {
904 #[rustfmt::skip]
905 let indices: &[u16] = &[
906 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1,
907 5, 1, 2, 5, 2, 3, 5, 3, 4, 5, 4, 1
908 ];
909
910 ctx.new_buffer(
911 BufferType::IndexBuffer,
912 BufferUsage::Immutable,
913 BufferSource::slice(indices),
914 )
915 }
916
917 fn update_positions_buffer(
918 &mut self,
919 ctx: &mut dyn RenderingBackend,
920 positions: &[Vec3],
921 ) -> BufferId {
922 let positions_buffer = ctx.new_buffer(
923 BufferType::VertexBuffer,
924 BufferUsage::Stream,
925 BufferSource::slice(positions),
926 );
927 self.bindings.vertex_buffers.push(positions_buffer);
928 positions_buffer
929 }
930
931 fn update_colors_buffer(
932 &mut self,
933 ctx: &mut dyn RenderingBackend,
934 colors: &[Vec4],
935 ) -> BufferId {
936 let colors_buffer = ctx.new_buffer(
937 BufferType::VertexBuffer,
938 BufferUsage::Stream,
939 BufferSource::slice(colors),
940 );
941 self.bindings.vertex_buffers.push(colors_buffer);
942 colors_buffer
943 }
944
945 fn render(
946 &self,
947 ctx: &mut dyn RenderingBackend,
948 positions: &[Vec3],
949 view_proj: Mat4,
950 background_color: (f32, f32, f32, f32),
951 ) {
952 let (r, g, b, a) = background_color;
953
954 ctx.begin_default_pass(PassAction::clear_color(r, g, b, a));
956
957 ctx.apply_pipeline(&self.pipeline);
959 ctx.apply_bindings(&self.bindings);
960 ctx.apply_uniforms(UniformsSource::table(&shader::Uniforms { mvp: view_proj }));
961
962 ctx.draw(0, 24, positions.len() as i32);
963 ctx.end_render_pass();
964 }
965}
966
967struct DesktopStageOptions {
969 pub positions: Vec<Vec3>,
970 pub receiver: Receiver<LedMessage>,
971 pub config: DesktopConfig,
972 pub is_window_closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
973}
974
975struct DesktopStage {
977 ctx: Box<dyn RenderingBackend>,
978 positions: Vec<Vec3>,
979 colors: Vec<LinearSrgb>,
980 colors_buffer: Vec<Vec4>,
981 brightness: f32,
982 correction: ColorCorrection,
983 receiver: Receiver<LedMessage>,
984 camera: Camera,
985 config: DesktopConfig,
986 is_window_closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
987 mouse_down: bool,
988 last_mouse_x: f32,
989 last_mouse_y: f32,
990 ui_manager: UiManager,
991 led_picker: LedPicker,
992 renderer: Renderer,
993}
994
995impl DesktopStage {
996 pub fn start<F, H>(f: F)
998 where
999 F: 'static + FnOnce() -> H,
1000 H: EventHandler + 'static,
1001 {
1002 let conf = conf::Conf {
1003 window_title: "Blinksy".to_string(),
1004 window_width: 800,
1005 window_height: 600,
1006 high_dpi: true,
1007 ..Default::default()
1008 };
1009 miniquad::start(conf, move || Box::new(f()));
1010 }
1011
1012 fn new(options: DesktopStageOptions) -> Self {
1014 let DesktopStageOptions {
1015 positions,
1016 receiver,
1017 config,
1018 is_window_closed,
1019 } = options;
1020
1021 let mut ctx: Box<dyn RenderingBackend> = window::new_rendering_backend();
1022
1023 let ui_manager = UiManager::new(&mut *ctx);
1025
1026 let led_picker = LedPicker::new(positions.clone(), config.led_radius);
1028
1029 let renderer = Renderer::new(&mut *ctx, config.led_radius);
1031
1032 let (width, height) = window::screen_size();
1034 let camera = Camera::new(width / height, config.orthographic_view);
1035
1036 let colors_buffer = (0..positions.len())
1038 .map(|_| Vec4::new(0.0, 0.0, 0.0, 1.0))
1039 .collect();
1040
1041 let mut stage = Self {
1043 ctx,
1044 positions: positions.clone(),
1045 colors: Vec::new(),
1046 colors_buffer,
1047 brightness: 1.0,
1048 correction: ColorCorrection::default(),
1049 receiver,
1050 camera,
1051 config,
1052 is_window_closed,
1053 mouse_down: false,
1054 last_mouse_x: 0.0,
1055 last_mouse_y: 0.0,
1056 ui_manager,
1057 led_picker,
1058 renderer,
1059 };
1060
1061 stage
1063 .renderer
1064 .update_positions_buffer(&mut *stage.ctx, &positions);
1065 stage
1066 .renderer
1067 .update_colors_buffer(&mut *stage.ctx, &stage.colors_buffer);
1068
1069 stage
1070 }
1071
1072 fn process_messages(&mut self) {
1074 while let Ok(message) = self.receiver.try_recv() {
1075 match message {
1076 LedMessage::UpdateColors(colors) => {
1077 self.colors = colors;
1078 }
1079 LedMessage::UpdateBrightness(brightness) => {
1080 self.brightness = brightness;
1081 }
1082 LedMessage::UpdateColorCorrection(correction) => {
1083 self.correction = correction;
1084 }
1085 LedMessage::Quit => {
1086 window::quit();
1087 }
1088 }
1089 }
1090 }
1091
1092 fn handle_camera_input(&mut self, keycode: KeyCode) {
1094 match keycode {
1095 KeyCode::R => {
1096 self.camera.reset();
1097 }
1098 KeyCode::O => {
1099 self.camera.toggle_projection_mode();
1100 }
1101 KeyCode::Escape => {
1102 self.led_picker.clear_selection();
1104 }
1105 _ => {}
1106 }
1107 }
1108}
1109
1110impl EventHandler for DesktopStage {
1111 fn update(&mut self) {
1112 self.process_messages();
1113 }
1114
1115 fn draw(&mut self) {
1116 let colors_buffer: Vec<Vec4> = self
1117 .colors
1118 .iter()
1119 .map(|color| {
1120 let (red, green, blue) = (color.red, color.green, color.blue);
1121
1122 let (red, green, blue) = (
1124 red * self.brightness,
1125 green * self.brightness,
1126 blue * self.brightness,
1127 );
1128
1129 let (red, green, blue) = (
1131 red * self.correction.red,
1132 green * self.correction.green,
1133 blue * self.correction.blue,
1134 );
1135
1136 let Srgb { red, green, blue } = LinearSrgb::new(red, green, blue).to_srgb();
1138
1139 Vec4::new(red, green, blue, 1.)
1140 })
1141 .collect();
1142
1143 self.colors_buffer = colors_buffer;
1145 self.ctx.buffer_update(
1146 self.renderer.bindings.vertex_buffers[2],
1147 BufferSource::slice(&self.colors_buffer),
1148 );
1149
1150 let view_proj = self.camera.view_projection_matrix();
1152 self.renderer.render(
1153 &mut *self.ctx,
1154 &self.positions,
1155 view_proj,
1156 self.config.background_color,
1157 );
1158
1159 self.ui_manager.render_led_info(
1161 &mut *self.ctx,
1162 &mut self.led_picker,
1163 &self.positions,
1164 &self.colors,
1165 self.brightness,
1166 self.correction,
1167 );
1168
1169 self.ui_manager.draw(&mut *self.ctx);
1171
1172 self.ctx.commit_frame();
1173 }
1174
1175 fn resize_event(&mut self, width: f32, height: f32) {
1176 self.camera.set_aspect_ratio(width / height);
1177 }
1178
1179 fn mouse_motion_event(&mut self, x: f32, y: f32) {
1180 self.ui_manager.mouse_motion_event(x, y);
1181
1182 if self.mouse_down && !self.ui_manager.want_mouse_capture {
1183 let dx = x - self.last_mouse_x;
1184 let dy = y - self.last_mouse_y;
1185 self.camera.rotate(dx, dy);
1186 }
1187 self.last_mouse_x = x;
1188 self.last_mouse_y = y;
1189 }
1190
1191 fn mouse_wheel_event(&mut self, x: f32, y: f32) {
1192 self.ui_manager.mouse_wheel_event(x, y);
1193
1194 if !self.ui_manager.want_mouse_capture {
1195 self.camera.zoom(y);
1196 }
1197 }
1198
1199 fn mouse_button_down_event(&mut self, button: MouseButton, x: f32, y: f32) {
1200 self.ui_manager.mouse_button_down_event(button, x, y);
1201
1202 if button == MouseButton::Left && !self.ui_manager.want_mouse_capture {
1203 if !self.mouse_down {
1205 self.led_picker.try_select_at(x, y, &self.camera);
1207 }
1208
1209 self.mouse_down = true;
1210 self.last_mouse_x = x;
1211 self.last_mouse_y = y;
1212 }
1213 }
1214
1215 fn mouse_button_up_event(&mut self, button: MouseButton, x: f32, y: f32) {
1216 self.ui_manager.mouse_button_up_event(button, x, y);
1217
1218 if button == MouseButton::Left {
1219 self.mouse_down = false;
1220 }
1221 }
1222
1223 fn key_down_event(&mut self, keycode: KeyCode, keymods: KeyMods, _repeat: bool) {
1224 self.ui_manager.key_down_event(keycode, keymods);
1225
1226 if !self.ui_manager.want_mouse_capture {
1227 self.handle_camera_input(keycode);
1228 }
1229 }
1230
1231 fn key_up_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
1232 self.ui_manager.key_up_event(keycode, keymods);
1233 }
1234
1235 fn char_event(&mut self, character: char, _keymods: KeyMods, _repeat: bool) {
1236 self.ui_manager.char_event(character);
1237 }
1238
1239 fn quit_requested_event(&mut self) {
1240 self.is_window_closed
1241 .store(true, std::sync::atomic::Ordering::Relaxed);
1242 }
1243}
1244
1245mod shader {
1247 use miniquad::*;
1248
1249 pub const VERTEX: &str = r#"#version 100
1251 attribute vec3 in_pos;
1252 attribute vec4 in_color;
1253 attribute vec3 in_inst_pos;
1254 attribute vec4 in_inst_color;
1255
1256 varying lowp vec4 color;
1257
1258 uniform mat4 mvp;
1259
1260 void main() {
1261 vec4 pos = vec4(in_pos + in_inst_pos, 1.0);
1262 gl_Position = mvp * pos;
1263 color = in_inst_color;
1264 }
1265 "#;
1266
1267 pub const FRAGMENT: &str = r#"#version 100
1269 varying lowp vec4 color;
1270
1271 void main() {
1272 gl_FragColor = color;
1273 }
1274 "#;
1275
1276 pub fn meta() -> ShaderMeta {
1278 ShaderMeta {
1279 images: vec![],
1280 uniforms: UniformBlockLayout {
1281 uniforms: vec![UniformDesc::new("mvp", UniformType::Mat4)],
1282 },
1283 }
1284 }
1285
1286 #[repr(C)]
1288 pub struct Uniforms {
1289 pub mvp: glam::Mat4,
1290 }
1291}