diff --git a/src/main.rs b/src/main.rs index ed63cc5..3171661 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ //! A clone of the `laser_frame_stream.rs` example that allows for configuring laser settings via a //! UI. +use nannou::winit::monitor::MonitorHandle; // use bevy_nannou::prelude::DARK_GRAY; // use nannou::lyon::geom::euclid::Transform2D; use nannou::{geom::Rect, math::map_range as nannou_map_range}; @@ -53,6 +54,7 @@ pub struct StreamConfig{ } type StreamConfigMap = HashMap; type StreamMap = HashMap>; +type DisplayMap = HashMap; @@ -61,7 +63,8 @@ struct GuiModel { config_file_path: PathBuf, laser_api: Arc, // All of the live stream handles. - laser_streams: StreamMap, + laser_streams: StreamMap, // outputId -> laserstreams + display_outputs: DisplayMap, // outputId -> windows // A copy of the state that will live on the laser thread so we can present a GUI. laser_model: LaserModel, // A copy of the laser settings so that we can control them with the GUI. @@ -207,7 +210,7 @@ fn zmq_receive(model: &mut GuiModel) { model.lost_alpha = 0.; } layers = model.current_layers.with_alpha(model.lost_alpha); - } else { + } else { layers = RenderableLayers::new() } } else { @@ -241,7 +244,7 @@ type OutputConfigMap = HashMap; pub enum OutputId{ EtherDream { mac_address: [u8; 6] }, Helios { id: u32 }, - Display { monitor: u32, enabled: bool } + Display { id: u32 } } @@ -384,6 +387,20 @@ fn model(app: &App) -> GuiModel { .build() .unwrap(); + + + // We'll use a `Vec` to collect laser streams as they appear. + let laser_streams = HashMap::new(); //vec![]; + let mut display_outputs = HashMap::new(); //vec![]; + + let per_laser_config = get_dac_configs(&config_file_path); + + for (output_id, dac_config) in &per_laser_config { + create_window_for_config(app, &output_id, &dac_config, &mut display_outputs); + } + + + // Initialise the state that we want to live on the laser thread and spawn the stream. let laser_settings = LaserSettings::default(); let laser_model = LaserModel::new(); @@ -435,8 +452,6 @@ fn model(app: &App) -> GuiModel { } }); - // We'll use a `Vec` to collect laser streams as they appear. - let laser_streams = HashMap::new(); //vec![]; // A user-interface to tweak the settings. let window = app.window(w_id_lasersettings).unwrap(); @@ -447,7 +462,6 @@ fn model(app: &App) -> GuiModel { let current_layers = RenderableLayers::new(); //Vec::new(); - let per_laser_config = get_dac_configs(&config_file_path); GuiModel { config_file_path, @@ -455,6 +469,7 @@ fn model(app: &App) -> GuiModel { laser_settings, laser_model, laser_streams, + display_outputs, dac_rx, egui, zmq, @@ -475,27 +490,81 @@ fn model(app: &App) -> GuiModel { } } +fn create_window_for_config(app: &App, output_id: &OutputId, config: &DacConfig, display_outputs: &mut DisplayMap) -> Option { + + // alreday created? Make visible + if let Some(win_id) = display_outputs.get(&output_id) { + if let Some(window) = app.window(win_id.clone()) { + // let win = &*window; + window.set_visible(config.window_enabled); + return Some(win_id.clone()); + } + } + + + // make window, even if disabled (but set to invisible) + if let OutputId::Display { id } = output_id { + let handle = monitor_handle_for_name(app, &config.window_monitor); + + let fs = match (config.window_fullscreen, handle) { + (true, Some(handle)) => Some(Fullscreen::Borderless(Some(handle))), + _ => None, + }; + // let fs = None; + let w_id = app + .new_window() + .fullscreen_with(fs) + .view(view_display) + .visible(config.window_enabled) + .build() + .unwrap(); + display_outputs.insert(output_id.clone(), w_id.clone()); + return Some(w_id); + + } + + None +} + +fn monitor_handle_for_name(app: &App, name: &String) -> Option { + app.available_monitors() + .iter() + .find(|w| w.name().unwrap() == *name) + .map(|w| w.clone()) +} + fn layers_to_points_for_config(current_layers: &RenderableLayers, config: &DacConfig ) -> Vec> { let mut new_laser_points: Vec = Vec::new(); - for (layer_nr, layer) in current_layers.0.iter() { - let mask = 1 << layer_nr; - if (mask & config.layers_enabled) != 0 { - let current_points: LaserPoints = layer.into(); - - // check which source should be used, and get points accordingly. - // potentially ignoring the points coming from the stream - let points = config.source.get_shape(current_points, config); - - // let pointno = points.points.len(); - - let new_points = match points.space { - CoordinateSpace::RawLaser => points, - _ => config.filters.apply(&points) - }; + match config.source { + StreamSource::CurrentLayers => { + for (layer_nr, layer) in current_layers.0.iter() { + let mask = 1 << layer_nr; + if (mask & config.layers_enabled) != 0 { + let points: LaserPoints = layer.into(); + + // check which source should be used, and get points accordingly. + // potentially ignoring the points coming from the stream + // let points = config.source.get_shape(current_points, config); + + // let pointno = points.points.len(); + + let new_points = match points.space { + CoordinateSpace::RawLaser => points, + _ => config.filters.apply(&points) + }; + // let new_laser_points = new_points.points; + new_laser_points.extend(new_points.points); + } + } + }, + _ => { + let points = config.source.get_shape(LaserPoints::default(), config); + let new_points = config.filters.apply(&points); // let new_laser_points = new_points.points; new_laser_points.extend(new_points.points); } } + split_on_blank(new_laser_points) } @@ -621,6 +690,7 @@ fn update(app: &App, model: &mut GuiModel, update: Update) { ref mut egui, ref mut laser_streams, ref mut laser_model, + ref mut display_outputs, ref mut laser_settings, ref mut per_laser_config, ref mut selected_stream, @@ -770,18 +840,18 @@ fn update(app: &App, model: &mut GuiModel, update: Update) { ui.label("No dacs available"); } else { - for (output_id, _config) in per_laser_config.iter() { + for (output_id, config) in per_laser_config.iter() { let is_available = match &output_id.clone().try_into() { Ok(dac_id) => laser_streams.contains_key(dac_id), Err(_output_id) => { - if let OutputId::Display { enabled, .. } = output_id { - *enabled + if let OutputId::Display { .. } = output_id { + config.window_enabled } else { false } }, }; - + ui.style_mut().visuals.override_text_color = if is_available {Some(egui::Color32::GREEN)} else {None}; ui.style_mut().visuals.widgets.inactive.bg_stroke = if is_available {egui::Stroke::new(2.0, egui::Color32::GREEN)} else {egui::Stroke::NONE}; let name = if let Some(config) = per_laser_config.get(&output_id) { config.name.clone() } else { "DAC".into() }; @@ -799,8 +869,27 @@ fn update(app: &App, model: &mut GuiModel, update: Update) { } if ui.button("+").clicked() { - let output_id = OutputId::Display { monitor: 0, enabled: false }; - per_laser_config.insert(output_id, DacConfig::default()); + let name = app.available_monitors().first().unwrap().name().expect("A monitor should be connected"); + // find highest id: + + let max_id = per_laser_config + .keys() + .filter_map(|c| match c { + OutputId::Display{id} => Some(id), + _ => None, + }) + .max().cloned().unwrap_or(0 as u32); + + + let output_id = OutputId::Display { id: max_id+1 }; + let new_config = DacConfig{ + window_monitor: name, + window_enabled: false, + window_fullscreen: false, + ..DacConfig::default() + }; + create_window_for_config(app, &output_id, &new_config, display_outputs); + per_laser_config.insert(output_id, new_config); } }); @@ -820,31 +909,90 @@ fn update(app: &App, model: &mut GuiModel, update: Update) { // let stream_config: &mut StreamConfig = laser_streams.get_mut(&selected_stream_value).expect("Selected stream not found in configs"); - - if let OutputId::Display { monitor, enabled: fullscreen } = selected_output { + let sel_output = selected_output.clone(); + if let OutputId::Display { id } = selected_output { let monitors = app.available_monitors(); // monitors.get(0).unwrap().name() // fullscreen = FullScreen::Borderless(Some(monitor_handle)); // app.new_window().fullscreen_with(fullscreen) - let mut selected_monitor_name = String::from("haha"); + // let mut selected_monitor_name = String::from("haha"); + + let win_id = display_outputs.get(&sel_output); + egui::ComboBox::from_label("Monitor") - .selected_text(format!("MONITOR -- {selected_monitor_name:?}")) + .selected_text(format!("MONITOR -- {:?}", &selected_config.window_monitor)) .show_ui(ui, |ui| { for monitor_option in monitors { if let Some(monitor_name) = monitor_option.name() { - if ui.selectable_value(&mut selected_monitor_name, monitor_name.clone(), format!("{:?}", &monitor_name)).clicked() { - if let Some(stream) = selected_laser_stream { - // TODO: make this for monitor: switch it + if ui.selectable_value(&mut selected_config.window_monitor, monitor_name.clone(), format!("{:?}", &monitor_name)).clicked() { + let handle = monitor_handle_for_name(app, &selected_config.window_monitor); + + let fs = match (selected_config.window_fullscreen, handle) { + (true, Some(handle)) => Some(Fullscreen::Borderless(Some(handle))), + _ => None, + }; + + if let Some(window_id) = win_id { + if let Some(window) = app.window(*window_id) { + // let win = &*window; + // win.set_fullscreen_with(fs); + } } }; } } }); + + + + if ui + .add(egui::Checkbox::new(&mut selected_config.window_enabled,"Enable output")) + .changed() + { + if let Some(win_id) = display_outputs.get(&sel_output) { + if let Some(window) = app.window(win_id.clone()) { + // let win = &*window; + window.set_visible(selected_config.window_enabled); + } + } + // match enabled { + // false => { + // if let Some(window_id) = win_id { + // if let Some(window) = app.window(*window_id) { + // let win = &*window; + // win.set_visible(false); + // } + // } + // }, + // true => { + // create_window_for_config(app, &sel_output, display_outputs); + // } + // }; + } + if ui + .add(egui::Checkbox::new(&mut selected_config.window_fullscreen,"Fullscreen")) + .changed() + { + + let handle = monitor_handle_for_name(app, &selected_config.window_monitor); + + let fs = match (selected_config.window_fullscreen, handle) { + (true, Some(handle)) => Some(Fullscreen::Borderless(Some(handle))), + _ => None, + }; + + if let Some(window_id) = win_id { + if let Some(window) = app.window(*window_id) { + // let win = &*window; + window.set_fullscreen_with(fs); + } + } + } } let source = &mut selected_config.source; @@ -1015,6 +1163,7 @@ fn view_line_canvas(app: &App, model: &GuiModel, frame: Frame) { }; draw.background().color(bgcolor); + let win = app.window_rect(); let scale = model.canvas_scale; @@ -1123,6 +1272,86 @@ fn view_line_canvas(app: &App, model: &GuiModel, frame: Frame) { draw.to_frame(app, &frame).unwrap(); } +fn view_display(app: &App, model: &GuiModel, frame: Frame) { + // get canvas to draw on + let draw = app.draw(); + + let win_id = frame.window_id(); + let frame_rect = frame.rect(); + // let win = app.window(win_id).unwrap(); + + let output_id = model.display_outputs.keys() + .find(|key| model.display_outputs.get(key) == Some(&win_id)) + .cloned(); + + if output_id.is_none() { + return; + } + + let output_id = output_id.unwrap(); + let drawing_config = model.per_laser_config.get(&output_id); + // if config.is_none() { + // return; + // } + // let config = config.unwrap(); + + + // draw.background().color(srgba(0.3,0.3,0.3,1.)); + draw.background().color(srgba(0.,0.,0., 1.)); + + let (w, h) = (frame_rect.w() as f32, frame_rect.h() as f32); + + + // let w = win.w(); + // let h = win.h(); + let hh = h as f32 / 2.; + let hw = w as f32 / 2.; + + let thickness = 3.0; + + let win_rect = frame_rect.pad(20.0); + + match &drawing_config { + None => { + draw.text("No config available. This should not happen?") + .color(WHITE) + .font_size(24) + .wh(win_rect.wh()); + }, + Some(config) => { + + // 3. current shape of the laser + let lines = layers_to_points_for_config(&model.current_layers, &config); + + // draw as distinct lines (this is how it is send to post-processing) + // TODO: alternatively, if the optimisation becomes an actual filter + // this should be drawn as a single line, and we can have an option to + // visualise the intermediate lines to make the draw order apparent + for line in lines { + // similar to map code: + + let vertices = line.iter().map(|p| { + let color = srgba(p.color[0], p.color[1], p.color[2], 1.); + let pos = [p.position[0] * hw, p.position[1] * hh]; + + (pos, color) + + }); + + draw.polyline() + .weight(thickness) + .join_round() + .points_colored(vertices); + } + + } + } + + draw.to_frame(app, &frame).unwrap(); + +} + + const LASER_PREVIEW_WIDTH: f32 = 1024.; const LASER_PREVIEW_HEIGHT: f32 = 1024.; @@ -1135,16 +1364,19 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { // draw.background().color(srgba(0.3,0.3,0.3,1.)); draw.background().color(srgba(0.,0.,0., 1.)); - let win = app.window_rect(); + + let win_id = frame.window_id(); + let win = app.window(win_id).unwrap(); - let w = LASER_PREVIEW_WIDTH; - let h = LASER_PREVIEW_HEIGHT; - let hh = h / 2.; - let hw = w / 2.; + let (w, h) = win.inner_size_pixels(); +; + let hh = (h as f32) / 2.; + let hw = (w as f32) / 2.; let thickness = 3.0; - let win_rect = app.main_window().rect().pad(20.0); + + let win_rect = win.rect().pad(20.0); match &model.selected_stream { None => { @@ -1157,7 +1389,7 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { // let stream = model.laser_streams.get(&dac_id); //.expect("Selected stream not found in configs"); // 1. get config for laser - let config: &DacConfig = model.per_laser_config.get(&dac_id).expect("Selected stream not found in configs"); + let config: &DacConfig = model.per_laser_config.get(&dac_id).expect(&format!("Selected stream not found in configs {:?} {:?}", &dac_id, &model.per_laser_config)); // 2. clipping mask + its anchor points @@ -1249,11 +1481,11 @@ fn laser_preview_mouse_pressed(app: &App, model: &mut GuiModel, button: MouseBut Some(d) => d, }; - let config = model.per_laser_config.get_mut(&dac_id).expect("Dac config unavailable"); - - let half_w = LASER_PREVIEW_WIDTH / 2.; - let half_h = LASER_PREVIEW_HEIGHT / 2.; + + let (w, h) = app.main_window().inner_size_pixels(); + let half_w = w as f32 / 2.; + let half_h = h as f32 / 2.; let laser_x = app.mouse.x / half_w; let laser_y = app.mouse.y / half_h; @@ -1342,8 +1574,9 @@ fn laser_mouse_moved(app: &App, model: &mut GuiModel, pos: Point2) { Some(d) => d, }; - let half_w = LASER_PREVIEW_WIDTH / 2.; - let half_h = LASER_PREVIEW_HEIGHT / 2.; + let (w, h) = app.main_window().inner_size_pixels(); + let half_w = w as f32 / 2.; + let half_h = h as f32 / 2.; let laser_x = app.mouse.x / half_w; let laser_y = app.mouse.y / half_h; @@ -1645,16 +1878,16 @@ fn map_mouse_pressed(app: &App, model: &mut GuiModel, button: MouseButton) { } - let half_w = (1024 / 2) as f32; - let half_h = (1024 / 2) as f32; + // let half_w = (1024 / 2) as f32; + // let half_h = (1024 / 2) as f32; - let x = app.mouse.x / half_w; - let y = app.mouse.y / half_h; + // let x = app.mouse.x / half_w; + // let y = app.mouse.y / half_h; - if x > 1. || x < -1. || y > 1. || y < -1. { - println!("Click outside of canvas: {} {}", x, y); - return - } + // if x > 1. || x < -1. || y > 1. || y < -1. { + // println!("Click outside of canvas: {} {}", x, y); + // return + // } } diff --git a/src/trap/laser.rs b/src/trap/laser.rs index d536080..e3ad95e 100644 --- a/src/trap/laser.rs +++ b/src/trap/laser.rs @@ -120,6 +120,11 @@ pub struct DacConfig{ pub filters: PointFilters, #[serde(default = "default_layers_enabled")] pub layers_enabled: u8, + + // window specific settings + pub window_monitor: String, + pub window_enabled: bool, + pub window_fullscreen: bool, } fn default_layers_enabled() -> u8 { @@ -159,7 +164,10 @@ impl Default for DacConfig{ fn default() -> DacConfig{ //DacConfig { name: "Unknown".into(), homography: Mat3::IDENTITY } // DacConfig { name: "Unknown".into(), homography: LASER_H_CM } - DacConfig { name: "Unknown".into(), source: StreamSource::CurrentLayers, filters: PointFilters::default().with_homography(LASER_H), layers_enabled: 0b1111_1111 } + DacConfig { + name: "Unknown".into(), source: StreamSource::CurrentLayers, filters: PointFilters::default().with_homography(LASER_H), layers_enabled: 0b1111_1111 , + window_monitor: "".into(), window_enabled: false, window_fullscreen: false + } } }