//! From https://github.com/seem-less/nannou/blob/helios_laser_DAC/examples/laser/laser_frame_stream_gui.rs //! A clone of the `laser_frame_stream.rs` example that allows for configuring laser settings via a //! UI. // use bevy_nannou::prelude::DARK_GRAY; // use nannou::lyon::geom::euclid::Transform2D; use nannou::{geom::Rect, math::map_range as nannou_map_range}; use nannou::prelude::*; use bevy::prelude::Mat3; // for glam::f32::Mat3, which is distinct from nannou::prelude::Mat3 // use nannou_egui::egui::emath::inverse_lerp; use nannou_egui::{self, egui, Egui}; use nannou_laser::DacId; use nannou_laser::{self as laser}; use serde_json::Result; use trap_rust::trap::filters::PointFilters; use trap_rust::trap::laser::{shape_rect, LaserPoints, LaserSpace, StreamSource, STREAM_SOURCES, TMP_DESK_CLUBMAX, Corner}; use trap_rust::trap::tracks::CoordinateSpace; use trap_rust::trap::utils::closest_edge; use trap_rust::trap::{laser::{python_cv_h_into_mat3, LaserModel, TMP_PYTHON_LASER_H, DacConfig}, tracks::{RenderableLines}}; use zmq::Socket; use std::sync::{mpsc, Arc}; use std::time::{Instant, Duration}; use std::collections::HashMap; use std::cmp::{Ordering, min}; use serde::{Serialize,Deserialize}; use nannou::winit::dpi::PhysicalPosition; use homography::find_homography; use cv_core::FeatureMatch; // use opencv::prelude::Mat; // use opencv::imgproc; use std::error::Error; use std::fs::File; use std::io::BufReader; use std::path::Path; // use egui_dropdown::DropDownBox; const CONFIG_FILE_PATH: &str = "./config.json"; fn main() { nannou::app(model).update(update).run(); } pub struct StreamConfig{ pub stream: laser::FrameStream, pub config: DacConfig, } type StreamConfigMap = HashMap; type StreamMap = HashMap>; struct GuiModel { // A handle to the laser API used for spawning streams and detecting DACs. laser_api: Arc, // All of the live stream handles. laser_streams: StreamMap, // 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. laser_settings: LaserSettings, per_laser_config: DacConfigMap, // For receiving newly detected DACs. dac_rx: mpsc::Receiver, // The UI for control over laser parameters and settings. egui: Egui, // socket for receiving points zmq: Socket, current_lines: RenderableLines, // a copy for the drawing renderer last_update: Instant, // dimming_factor: f32, lost_alpha: f32, connected: bool, selected_stream: Option, canvas_scale: f32, canvas_translate: Vec2, canvas_dragging_corner: Option, preview_dragging_point: Option, // canvas_transform: Translation2D, // dragging: bool, } struct LaserSettings { point_hz: u32, latency_points: u32, frame_hz: u32, enable_optimisations: bool, enable_draw_reorder: bool, distance_per_point: f32, blank_delay_points: u32, radians_per_point: f32, } impl Default for LaserSettings { fn default() -> Self { use laser::stream; use laser::stream::frame::InterpolationConfig; LaserSettings { point_hz: 30000, //stream::DEFAULT_POINT_HZ, latency_points: stream::points_per_frame( stream::DEFAULT_POINT_HZ, stream::DEFAULT_FRAME_HZ, ) * 4, frame_hz: 35, //stream::DEFAULT_FRAME_HZ, enable_optimisations: true, enable_draw_reorder: true, distance_per_point: InterpolationConfig::DEFAULT_DISTANCE_PER_POINT, blank_delay_points: InterpolationConfig::DEFAULT_BLANK_DELAY_POINTS, radians_per_point: InterpolationConfig::DEFAULT_RADIANS_PER_POINT, } } } fn setup_zmq() -> Socket{ // let url = "tcp://100.109.175.82:99174"; let url = "tcp://127.0.0.1:99174"; let context = zmq::Context::new(); let subscriber = context.socket(zmq::SUB).unwrap(); subscriber.set_conflate(true).unwrap(); // only keep latest entry assert!(subscriber.connect(url).is_ok()); // let filter = "10001"; let filter = ""; //"msgs"; assert!(subscriber.set_subscribe(filter.as_bytes()).is_ok()); subscriber } // fn zmq_receive(subscriber: &Socket, laser_streams: &Vec>) { /// Receive items if available on the queue and update Model with the new data fn zmq_receive(model: &mut GuiModel) { let subscriber = &model.zmq; let mut items = [ subscriber.as_poll_item(zmq::POLLIN) ]; let _nr = zmq::poll(&mut items, 0).unwrap(); let lines: RenderableLines; if items[0].is_readable() { let json = subscriber.recv_string(0).unwrap().unwrap(); // dbg!(&json[4..]); // let msg: Frame = serde_json::from_str(&json[4..]).expect("No valid json?"); let res: Result = serde_json::from_str(&json); model.lost_alpha = 1.; model.connected = true; lines = match res { Ok(lines) => lines, // if Ok(255), set x to 255 Err(_e) => { println!("No valid json?"); println!("{}", _e); // empty if invalid RenderableLines::new() }, // if Err("some message"), panic with error message "some message" }; } else if model.last_update < Instant::now() - Duration::from_millis(100){ // set lines empty, if no new input for > 100ms (10fps) model.connected = false; if model.lost_alpha > 0.{ println!("No input, clear lines!!"); model.lost_alpha *= 0.80; if model.lost_alpha < 0.1{ model.lost_alpha = 0.; } lines = model.current_lines.with_alpha(model.lost_alpha); } else { lines = RenderableLines::new() } } else { // No new lines, break return } // println!("receive {}", lines.lines.len()); for (_dac, stream) in (&model.laser_streams).into_iter() { // let lines = get_laser_lines(version); let lines_for_laser: RenderableLines = lines.clone(); let sending = stream.send(move |laser_model: &mut LaserModel| { let laser_lines: RenderableLines = lines_for_laser; laser_model.current_lines = laser_lines; }); if let Err(e) = sending { println!("Error sending to laser! {e:?}"); } } model.current_lines = lines; model.last_update = Instant::now(); } type DacConfigMap = HashMap; #[derive(Debug, Serialize, Deserialize)] #[serde(remote = "DacId")] pub enum DacIdSerializable { EtherDream { mac_address: [u8; 6] }, Helios { id: u32 }, } #[derive(Debug, Serialize, Deserialize)] pub struct SavedDacConfig{ #[serde(with = "DacIdSerializable")] dac_id: DacId, config: DacConfig, } #[derive(Debug, Serialize, Deserialize)] pub struct SavedConfig { dacs: Vec } impl Into for SavedConfig { fn into(self) -> DacConfigMap { let mut configs = HashMap::new(); for dac in self.dacs { configs.insert(dac.dac_id, dac.config); } configs } } impl From<&mut DacConfigMap> for SavedConfig { fn from(per_laser_config: &mut DacConfigMap) -> SavedConfig { let mut dacs = Vec::new(); for (dac_id, config) in per_laser_config.into_iter() { dacs.push(SavedDacConfig{ dac_id: dac_id.clone(), config: config.clone() }); } SavedConfig { dacs } } } fn read_config_from_file>(path: P) -> std::result::Result> { // Open the file in read-only mode with buffer. let file = File::open(path)?; let reader = BufReader::new(file); // Read the JSON contents of the file as an instance of `DacConfig`. let u = serde_json::from_reader(reader)?; Ok(u) } fn save_config_file>(path: P, config: SavedConfig) -> std::result::Result<(), Box> { // Open the file in read-only mode with buffer. let mut file = File::create(path)?; serde_json::to_writer(&mut file, &config)?; Ok(()) } // Some hardcoded config. Not spending time on reading/writing config atm. fn get_dac_configs() -> DacConfigMap{ match read_config_from_file(CONFIG_FILE_PATH) { Err(err) => { eprintln!("Could not load config {}", err); return HashMap::new() }, Ok(saved_config) => return saved_config.into(), } } fn model(app: &App) -> GuiModel { // Create a window to receive keyboard events. let w_id_lasersettings = app .new_window() .size(312, 530) // .key_pressed(key_pressed) .raw_event(raw_window_event) .view(view_laser_settings) .build() .unwrap(); let w_id_linecanvas = app .new_window() .size(1024, 768) // .key_pressed(key_pressed) .mouse_wheel(map_wheel_zoom) .mouse_pressed(map_mouse_pressed) .mouse_released(map_mouse_released) .mouse_moved(map_mouse_moved) .view(view_line_canvas) .build() .unwrap(); let w_id_laserpreview = app .new_window() .size(1024, 1024) // .key_pressed(key_pressed) // .mouse_wheel(canvas_zoom) .mouse_pressed(laser_preview_mouse_pressed) .mouse_released(laser_mouse_released) .mouse_moved(laser_mouse_moved) .view(view_laser_preview) .build() .unwrap(); // 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(); let zmq = setup_zmq(); // TODO Implement `Clone` for `Api` so that we don't have to `Arc` it. let laser_api = Arc::new(laser::Api::new()); // A channel for receiving newly detected DACs. let (dac_tx, dac_rx) = mpsc::channel(); // Spawn a thread for detecting the DACs. let laser_api2 = laser_api.clone(); std::thread::spawn(move || { let mut detected = std::collections::HashSet::new(); // detect Helios DACs first since they can't be detected while simultaneously sending data to them for res in laser_api2.detect_dacs(laser::DacVariant::DacVariantHelios) { if let laser::DetectDacs::Helios { previous_dac } = res { if !detected.insert(laser::DetectedDac::from(previous_dac).id()) { break; } } } for detected_helios in &detected { if let laser::dac_manager::Id::Helios { id } = *detected_helios { let dac: laser::helios_dac::NativeHeliosDacParams = id.into(); println!("{:#?}", dac); if dac_tx.send(dac.into()).is_err() { break; } } } // for Etherdream DAC for res in laser_api2 .detect_dacs(laser::DacVariant::DacVariantEtherdream) .expect("failed to start detecting Etherdream DACs") { let dac = res.expect("error occurred during DAC detection"); if detected.insert(dac.id()) { // DacId::EtherDream { mac_address: () } println!("{:#?}", dac); if dac_tx.send(dac).is_err() { break; } } } }); // 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(); let egui = Egui::from_window(&window); let egui_ctx = egui.ctx(); egui_ctx.set_style(style()); let current_lines = RenderableLines::new(); //Vec::new(); GuiModel { laser_api, laser_settings, laser_model, laser_streams, dac_rx, egui, zmq, current_lines: current_lines, last_update: Instant::now(), lost_alpha: 1., connected: true, per_laser_config: get_dac_configs(), selected_stream: None, canvas_scale: 25., canvas_translate: Vec2::new(-300.,100.), canvas_dragging_corner: None, preview_dragging_point: None, // canvas_transform: Transform2D // dimming_factor: 1., } } fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){ let current_points: LaserPoints = (&model.current_lines).into(); let space = &model.current_lines.space; // check which source should be used, and get points accordingly. // potentially ignoring the points coming from the stream let points = model.config.source.get_shape(current_points); let pointno = points.points.len(); let new_points = model.config.filters.apply(&points); let new_laser_points = new_points.points; // if new_laser_points.len() < pointno { // println!("Cropped Points {} (was: {})", new_laser_points.len(), pointno); // } frame.add_lines(new_laser_points); return; } fn raw_window_event(_app: &App, model: &mut GuiModel, event: &nannou::winit::event::WindowEvent) { model.egui.handle_raw_event(event); } fn update(_app: &App, model: &mut GuiModel, update: Update) { // First, check for new laser DACs. for dac in model.dac_rx.try_recv() { println!("Detected DAC {:?}!", dac.id()); if !model.per_laser_config.contains_key(&dac.id()) { println!("Found unknown DAC, register with defaults"); model.per_laser_config.insert(dac.id(), DacConfig::default()); } let config = &model.per_laser_config[&dac.id()]; let stream = model .laser_api .new_frame_stream(model.laser_model.with_config(config), laser_frame_producer) .detected_dac(dac.clone()) .build() .expect("failed to establish stream with newly detected DAC"); // dbg!(stream.enable_draw_reorder()); model.laser_streams.insert(dac.id(), stream); // match save_config_file("./config.json", model.per_laser_config.clone().into()) { // Err(err) => eprintln!("failed saving config: {}", err), // Ok(_) => println!("Saved config"), // }; } // Check if any streams have dropped out (e.g network issues, DAC turned off) and attempt to // start them again. let mut dropped = vec![]; for (dac_id, stream) in model.laser_streams.iter() { if stream.is_closed() { dropped.push(dac_id.clone()); } } for dac_id in dropped.into_iter().rev() { // let stream = ; let s = model.laser_streams.remove(&dac_id); // if model.selected_stream == Some(dac_id){ // model.selected_stream = None; // } if let Some(stream) = s{ let dac = stream .dac() .expect("`dac` returned `None` even though one was specified during stream creation"); let res = stream .close() .expect("stream was unexpectedly already closed from another stream handle") .expect("failed to join stream thread"); if let Err(err) = res { eprintln!("Stream closed due to an error: {}", err); } // TODO: keeps looping on disconnect. println!("attempting to restart stream with DAC {:?}", dac.id()); let dac_id = dac.id(); let config = &model.per_laser_config[&dac.id()]; match model .laser_api .new_frame_stream(model.laser_model.with_config(config), laser_frame_producer) .detected_dac(dac) .build() { Err(err) => eprintln!("failed to restart stream: {}", err), Ok(stream) => { println!("Reinsert stream. {:?}", dac_id); model.laser_streams.insert(dac_id, stream); }, } } } // check if new messages have arrived. Update the model with new data. zmq_receive(model); // Update the GUI. let GuiModel { ref mut egui, ref mut laser_streams, ref mut laser_model, ref mut laser_settings, ref mut per_laser_config, ref mut selected_stream, ref mut current_lines, .. } = *model; egui.set_elapsed_time(update.since_start); let ctx = egui.begin_frame(); // The timeline area. egui::containers::CentralPanel::default().show(&ctx, |ui| { fn grid_min_col_width(ui: &egui::Ui, n_options: usize) -> f32 { let gap_space = ui.spacing().item_spacing.x * (n_options as f32 - 1.0); let grid_w = ui.available_width(); (grid_w - gap_space) / n_options as f32 } ui.horizontal(|ui| { ui.heading("Laser Points"); if ui.button("💾").clicked() { save_config_file(CONFIG_FILE_PATH, per_laser_config.into()); } }); ui.separator(); ui.add(egui::Label::new(format!("Lines {}", current_lines.lines.len()))); ui.add(egui::Label::new(format!("Points {}", current_lines.point_count()))); ui.heading("General settings"); if ui .add(egui::Slider::new(&mut laser_settings.point_hz, 1_000..=50_000).text("DAC PPS")) .changed() { let hz = laser_settings.point_hz; for (_dac_id, stream) in laser_streams.iter() { stream.set_point_hz(hz).ok(); } } if ui .add(egui::Slider::new(&mut laser_settings.latency_points, 10..=1_500).text("Latency")) .changed() { let latency = laser_settings.latency_points; for (_dac_id, stream) in laser_streams.iter() { stream.set_latency_points(latency).ok(); } } if ui .add(egui::Slider::new(&mut laser_settings.frame_hz, 1..=120).text("Target FPS")) .changed() { let hz = laser_settings.frame_hz; for (_dac_id, stream) in laser_streams.iter() { stream.set_frame_hz(hz).ok(); } } ui.separator(); ui.heading("Laser Path Interpolation"); if ui .checkbox(&mut laser_settings.enable_optimisations, "Optimize Path") .changed() { for (_dac_id, stream) in laser_streams.iter() { stream .enable_optimisations(laser_settings.enable_optimisations) .ok(); } } if ui .add_enabled(laser_settings.enable_optimisations, egui::Checkbox::new(&mut laser_settings.enable_draw_reorder,"Reorder paths") ) // .checkbox(&mut laser_settings.enable_draw_reorder, "Reorder paths") .changed() { for (_dac_id, stream) in laser_streams.iter() { stream .enable_draw_reorder(laser_settings.enable_draw_reorder) .ok(); } } if ui .add_enabled(laser_settings.enable_optimisations, egui::Slider::new(&mut laser_settings.distance_per_point, 0.01..=1.0) .text("Distance Per Point"), ) .changed() { let distance = laser_settings.distance_per_point; for (_dac_id, stream) in laser_streams.iter() { stream.set_distance_per_point(distance).ok(); } } if ui .add_enabled(laser_settings.enable_optimisations, egui::Slider::new(&mut laser_settings.blank_delay_points, 0..=32) .text("Blank Delay (Points)"), ) .changed() { let delay = laser_settings.blank_delay_points; for (_dac_id, stream) in laser_streams.iter() { stream.set_blank_delay_points(delay).ok(); } } let mut degrees = rad_to_deg(laser_settings.radians_per_point); if ui .add_enabled(laser_settings.enable_optimisations, egui::Slider::new(&mut degrees, 1.0..=180.0).text("Degrees Per Point") ) .changed() { let radians = deg_to_rad(degrees); laser_settings.radians_per_point = radians; for (_dac_id, stream) in laser_streams.iter() { stream.set_radians_per_point(radians).ok(); } } ui.separator(); ui.heading("Laser specific settings"); if per_laser_config.is_empty() { ui.label("No dacs available"); } else { ui.horizontal_wrapped(|ui| { ui.selectable_value( selected_stream, None, "⊗" ); for (dac_id, _config) in per_laser_config.iter() { let is_available = laser_streams.contains_key(&dac_id); 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(&dac_id) { config.name.clone() } else { "DAC".into() }; let indicator = if is_available{" 🔌"}else{""}; // egui::widgets::SelectableLabel cannot have border unless hovered/highlighted // TODO: alternatively underscore with ui.painter().rect/hline(ui.selectable_value().rect.max/min, ....) ui.selectable_value( selected_stream, Some(dac_id.clone()), format!("{name}{indicator}") ); } // reset ui.style_mut().visuals.override_text_color = None; }); } if let Some(selected_stream_value) = selected_stream { ui.separator(); ui.add(egui::Label::new(format!("{:?}", selected_stream_value))); let selected_config: &mut DacConfig = per_laser_config.get_mut(&selected_stream_value).unwrap(); ui.add(egui::TextEdit::singleline(&mut selected_config.name)); let selected_laser_stream = laser_streams.get(&selected_stream_value); // let stream_config: &mut StreamConfig = laser_streams.get_mut(&selected_stream_value).expect("Selected stream not found in configs"); let source = &mut selected_config.source; egui::ComboBox::from_label("Source") .selected_text(format!("{source:?}")) .show_ui(ui, |ui| { for source_option in STREAM_SOURCES { if ui.selectable_value(source, source_option.clone(), format!("{:?}", &source_option)).clicked() { if let Some(stream) = selected_laser_stream { // let source = source_option; stream.send(move |laser_model: &mut LaserModel| { laser_model.config.source = source_option; }).unwrap(); } }; } }); if ui .add(egui::Slider::new(&mut selected_config.filters.dim.intensity, 0.0..=1.).text("Dimming")) .changed() { let factor = selected_config.filters.dim.intensity; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.dim.intensity = factor; }).unwrap(); } } if ui .add(egui::Slider::new(&mut selected_config.filters.dim.intensity, 0.0..=1.).text("Dimming")) .changed() { let factor = selected_config.filters.dim.intensity; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.dim.intensity = factor; }).unwrap(); } } if ui .add(egui::Slider::new(&mut selected_config.filters.scale.factor, 0.001..=2.).text("Scale")) .changed() { let factor = selected_config.filters.scale.factor; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.scale.factor = factor; }).unwrap(); } } // Pincushion / Pillow / Barrel distortion. Generally, only needed for the x-axis if ui .add(egui::Slider::new(&mut selected_config.filters.pincushion.k_x, -0.5..=0.5).text("Pincushion x")) .changed() { let factor = selected_config.filters.pincushion.k_x; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.pincushion.k_x = factor; }).unwrap(); } } if ui .add(egui::Slider::new(&mut selected_config.filters.pincushion.k_x2, -0.2..=0.2).text("Higher order pincushion x")) .changed() { let factor = selected_config.filters.pincushion.k_x2; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.pincushion.k_x2 = factor; }).unwrap(); } } if ui .add(egui::Slider::new(&mut selected_config.filters.pincushion.k_y, -0.5..=0.5).text("Pincushion y")) .changed() { let factor = selected_config.filters.pincushion.k_y; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.pincushion.k_y = factor; }).unwrap(); } } if ui .add(egui::Slider::new(&mut selected_config.filters.pincushion.k_y2, -0.2..=0.2).text("Higher order pincushion y")) .changed() { let factor = selected_config.filters.pincushion.k_y2; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.pincushion.k_y2 = factor; }).unwrap(); } } if ui .checkbox(&mut selected_config.filters.crop.enabled ,"Crop") .changed() { let enabled = selected_config.filters.crop.enabled; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.crop.enabled = enabled; }).unwrap(); } } } else { ui.label("Select a DAC"); } }); } fn view_laser_settings(_app: &App, model: &GuiModel, frame: Frame) { model.egui.draw_to_frame(&frame).unwrap(); } fn view_line_canvas(app: &App, model: &GuiModel, frame: Frame) { // get canvas to draw on let draw = app.draw(); // LaserPoints supports a mode that sends points directly into the laser // if this mode is enabled by the sender, set background to blue // Red if nothing is sending / not for too long. let bgcolor = match model.current_lines.space { CoordinateSpace::Laser => MEDIUMSLATEBLUE, _ => match model.connected{ true => DARKGRAY, false => LIGHTCORAL, }, }; draw.background().color(bgcolor); let win = app.window_rect(); let scale = model.canvas_scale; let translate_x = model.canvas_translate.x; let translate_y = model.canvas_translate.y; // background grid draw_grid(&draw, &win, scale, 1.); let thickness = 2.0; // draw origin indicator draw.ellipse() .x_y(0. + translate_x, 0. + translate_y) .radius(3.) .color(BLUE); draw.line() .weight(thickness) .caps_round() .color(RED) .points([0. * scale + translate_x, 0. * -scale + translate_y].into(), [1. * scale + translate_x, 0. * -scale + translate_y].into()); // draw current laser lines for line in &model.current_lines.lines{ let vertices = line.points.iter().map(|p| { let color = srgba(p.color.red, p.color.green, p.color.blue, p.color.alpha); let pos = [p.position[0] * scale + translate_x, p.position[1] * -scale + translate_y]; (pos, color) }); draw.polyline() .weight(thickness) .join_round() .points_colored(vertices); } // show each configured laser in the canvas. Highlight the selected. for (dac_id, config) in model.per_laser_config.iter() { let rect = shape_rect(LaserSpace::READY, 11); let points = config.filters.reverse(&rect); let vertices = points.points.iter().map(|p| { let color = if model.selected_stream == Some(dac_id.clone()) { // Srgba::hex("e52d9f"); // ORCHID. to_srgba() srgba(229./255.,45./255.,159./255.,0.8) } else { // ORCHID; srgba(1.,1.,1., 0.2) }; let pos = [p.position[0] * scale + translate_x, p.position[1] * -scale + translate_y]; (pos, color) }); draw.polyline() .weight(thickness) .join_round() .points_colored(vertices); // draggable corners for the selected area if model.selected_stream == Some(dac_id.clone()){ let rect = shape_rect(LaserSpace::READY, 1); // find corners let points = config.filters.reverse(&rect); for p in points.points { let pos = [p.position[0] * scale + translate_x, p.position[1] * -scale + translate_y]; draw.ellipse() .x_y(pos[0], pos[1]) .radius(3.) .color(srgba(229./255.,45./255.,159./255.,0.8)); } } } // put everything on the frame draw.to_frame(app, &frame).unwrap(); } const LASER_PREVIEW_WIDTH: f32 = 1024.; const LASER_PREVIEW_HEIGHT: f32 = 1024.; // preview the selected laser, to draw clip mask fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { // get canvas to draw on let draw = app.draw(); draw.background().color(BLACK); let win = app.window_rect(); let w = LASER_PREVIEW_WIDTH; let h = LASER_PREVIEW_HEIGHT; let hh = h / 2.; let hw = w / 2.; let thickness = 2.0; let win_rect = app.main_window().rect().pad(20.0); match &model.selected_stream { None => { draw.text("Select a stream to preview") .color(WHITE) .font_size(24) .wh(win_rect.wh()); }, Some(dac_id) => { // let stream = model.laser_streams.get(&dac_id); //.expect("Selected stream not found in configs"); // 1. get config for laser let config = model.per_laser_config.get(&dac_id).expect("Selected stream not found in configs"); // 2 draw identifier of laser draw.text(&format!("{} {:?}", config.name, dac_id)) .h(win_rect.h()) .font_size(10) .align_text_bottom() .left_justify() .color(WHITE) .w(win_rect.w()); // 3. clipping mask + its anchor points let clip_points: Vec<[f32;2]> = config.filters.clip.mask.iter().map(|p| { [p[0] * hw, p[1] * hh] }).collect(); for point in clip_points.iter() { // TODO: does not work? draw.ellipse() .x_y(point[0], point[1]) .radius(5.) .stroke(WHITE); } draw.polygon() .color(srgba(1.,1.,1.,3.)) .stroke(PINK) .stroke_weight(thickness) .join_round() .points(clip_points); // 4. current shape of the laser let current_points: LaserPoints = (&model.current_lines).into(); let space = &model.current_lines.space; // 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); let pointno = points.points.len(); let new_points = config.filters.apply(&points); // similar to map code: let vertices = new_points.points.iter().map(|p| { let color = srgba(p.color[0], p.color[1], p.color[0], 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(); } fn laser_preview_mouse_pressed(app: &App, model: &mut GuiModel, button: MouseButton) { let dac_id = match &model.selected_stream { None => return, 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 laser_x = app.mouse.x / half_w; let laser_y = app.mouse.y / half_h; // if x > 1. || x < -1. || y > 1. || y < -1. { // println!("Click outside of canvas: {} {}", x, y); // return // } // 1. Always min. 4 points. We need closest two. So sort first let mask_points = &config.filters.clip.mask; let point_distances: Vec<(usize, f32, [f32;2])> = mask_points .iter() .enumerate() .map(|(i, &p)| { (i, (laser_x - p[0]).powi(2) + (laser_y - p[1]).powi(2), p) }) .collect(); // dbg!(&half_w, &half_h, &laser_x, &laser_y, &point_distances); let (idx, dist_sq, closest_point) = point_distances.iter().min_by(|a,b| a.1.partial_cmp(&b.1).unwrap()).expect("No clipping points existing?"); // = point_distances; // let (idx2, _, _) = point_distances.get(1).expect("No clipping points existing?"); // match_distance is in pixelspace. Convert to laser-space if dist_sq.sqrt() <= (MATCH_DISTANCE / half_w) { if button == MouseButton::Left { model.preview_dragging_point = Some(*idx); } if button == MouseButton::Right { // require minimum of three corners for clipping // (makes calculations/assumptions easier) // so don't remove if too little if config.filters.clip.mask.len() > 3 { config.filters.clip.mask.remove(*idx); } } // 2a. if closest is within MATCH_DISTANCE. Select point for dragging } else { if button == MouseButton::Left { // 2b. if not, find closest edge let insert_at = match closest_edge(&config.filters.clip.mask, [laser_x, laser_y]) { Some(idx) => (idx + 1) % config.filters.clip.mask.len(), None => return, }; // insert point // let instert_at = *min(idx, idx2); let new_point = [laser_x, laser_y]; config.filters.clip.mask.insert(insert_at % config.filters.clip.mask.len(), new_point); model.preview_dragging_point = Some(insert_at); } } } fn laser_mouse_moved(app: &App, model: &mut GuiModel, pos: Point2) { let dac_id = match &model.selected_stream { None => return, Some(d) => d, }; let point_idx = match &model.preview_dragging_point { None => return, Some(d) => d, }; let half_w = LASER_PREVIEW_WIDTH / 2.; let half_h = LASER_PREVIEW_HEIGHT / 2.; let laser_x = app.mouse.x / half_w; let laser_y = app.mouse.y / half_h; // 0. Get config let config = model.per_laser_config.get_mut(&dac_id).expect("Dac config unavailable"); // 1. Move selected point to laser_x, laser_y // config.filters.clip. let point = config.filters.clip.mask.get_mut(*point_idx).unwrap(); *point = [laser_x, laser_y]; } fn laser_mouse_released(_app: &App, model: &mut GuiModel, _button: MouseButton) { // deselect point model.preview_dragging_point = None; } fn draw_grid(draw: &Draw, win: &Rect, step: f32, weight: f32) { let step_by = || (0..).map(|i| i as f32 * step); let r_iter = step_by().take_while(|&f| f < win.right()); let l_iter = step_by().map(|f| -f).take_while(|&f| f > win.left()); let x_iter = r_iter.chain(l_iter); for x in x_iter { draw.line() .weight(weight) .points(pt2(x, win.bottom()), pt2(x, win.top())) .color(GRAY); } let t_iter = step_by().take_while(|&f| f < win.top()); let b_iter = step_by().map(|f| -f).take_while(|&f| f > win.bottom()); let y_iter = t_iter.chain(b_iter); for y in y_iter { draw.line() .weight(weight) .points(pt2(win.left(), y), pt2(win.right(), y)) .color(GRAY); } } fn style() -> egui::Style { let mut style = egui::Style::default(); style.spacing = egui::style::Spacing { item_spacing: egui::Vec2::splat(8.0), // window_margin: egui::Vec2::new(6.0, 6.0), button_padding: egui::Vec2::new(4.0, 2.0), interact_size: egui::Vec2::new(56.0, 24.0), indent: 10.0, icon_width: 20.0, icon_spacing: 1.0, ..style.spacing }; style.visuals.widgets.inactive.fg_stroke.color = egui::Color32::WHITE; style.visuals.extreme_bg_color = egui::Color32::from_gray(12); style.visuals.faint_bg_color = egui::Color32::from_gray(24); style.visuals.widgets.noninteractive.bg_fill = egui::Color32::from_gray(36); style.visuals.widgets.noninteractive.bg_stroke.color = egui::Color32::BLACK; style.visuals.widgets.noninteractive.fg_stroke.color = egui::Color32::WHITE; style } fn laser_corners_to_world(filters: &PointFilters) -> Vec<[f32;2]> { let corners_raw: Vec<[f32; 2]> = Corner::in_laser_space(); let corners_laser: LaserPoints = corners_raw.into(); let world_points = filters.reverse(&corners_laser); let world_corners: Vec<[f32;2]> = world_points.points.iter().map(|p| { p.position }).collect(); world_corners } fn map_mouse_moved(_app: &App, model: &mut GuiModel, pos: Point2) { let corner = match &model.canvas_dragging_corner { None => return, Some(c) => c, }; let dac_id = match &model.selected_stream { None => return, Some(d) => d, }; let config = model.per_laser_config.get_mut(&dac_id).expect("Dac config unavailable"); // 1. For the dragging point, reverse the canvas space to world space: let world_point = [ (pos[0] - model.canvas_translate.x) / model.canvas_scale, (pos[1] - model.canvas_translate.y) / -model.canvas_scale, ]; // 2. find existing corners in world space, replacing the // dragging corner let mut world_corners = laser_corners_to_world(&config.filters); world_corners[corner.index()] = world_point; // 3. find the corners of the laser, correct them for all // geometric filters, except the homography itself. let laser_corners: LaserPoints = Corner::in_laser_space().into(); let distorted_laser_corners: Vec<[f32;2]> = config.filters.reverse_without_homography(&laser_corners).into(); // 4. find new homography on pairs of points, and convert to compatible matrix type let matches: Vec>> = world_corners.iter().zip(distorted_laser_corners).map(|(world, laser)| { FeatureMatch( nalgebra::Point2::new(world[0] as f64, world[1] as f64), nalgebra::Point2::new(laser[0] as f64, laser[1] as f64), ) }).collect::>(); let m = find_homography(matches).unwrap(); let mat: Mat3 = Mat3::from_cols_array(&[m[0] as f32, m[1] as f32, m[2] as f32, m[3] as f32, m[4] as f32, m[5] as f32, m[6] as f32, m[7] as f32, m[8] as f32]); // 5. update config in Gui and laser stream threat config.filters.homography.homography_matrix = mat.clone(); let selected_laser_stream = model.laser_streams.get(&dac_id); if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { laser_model.config.filters.homography.homography_matrix = mat; }).unwrap(); } } const MATCH_DISTANCE: f32 = 30.; // screen pixels fn map_mouse_pressed(app: &App, model: &mut GuiModel, button: MouseButton) { if button != MouseButton::Left { // ignore return; } if let Some(dac_id) = &model.selected_stream { let config = model.per_laser_config.get(&dac_id).expect("Dac config unavailable"); // find close corner to drag let corners = laser_corners_to_world(&config.filters); let canvas_corners: Vec<[f32;2]> = corners.iter().map(|p| { [ p[0] * model.canvas_scale + model.canvas_translate.x, p[1] * -model.canvas_scale + model.canvas_translate.y ] }).collect(); dbg!("{:?}", &canvas_corners); dbg!("{:?}, {:?}", &app.mouse.x, &app.mouse.y); let mut selected: Option = None; for (i, c) in canvas_corners.iter().enumerate(){ if (app.mouse.x - c[0]).abs() < MATCH_DISTANCE && (app.mouse.y - c[1]).abs() < MATCH_DISTANCE { dbg!("close to corner! {:?}", &c); match i { 0 => selected = Some(Corner::TopLeft), 1 => selected = Some(Corner::TopRight), 2 => selected = Some(Corner::BottomRight), 3 => selected = Some(Corner::BottomLeft), _ => selected = None, } break; } } dbg!("selected! {:?}", &selected); model.canvas_dragging_corner = selected; } 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; if x > 1. || x < -1. || y > 1. || y < -1. { println!("Click outside of canvas: {} {}", x, y); return } } fn map_mouse_released(_app: &App, model: &mut GuiModel, _button: MouseButton) { model.canvas_dragging_corner = None; } fn map_wheel_zoom(_app: &App, model: &mut GuiModel, dt: MouseScrollDelta, _phase: TouchPhase) { let (x,y) = match dt { MouseScrollDelta::PixelDelta(PhysicalPosition::{ x, y }) => (x as f32, y as f32), MouseScrollDelta::LineDelta(x, y) => (x,y), }; model.canvas_scale += y; }