Output to a window

This commit is contained in:
Ruben van de Ven 2025-11-11 13:23:16 +01:00
parent 9602b46839
commit 38baeacd01
2 changed files with 297 additions and 56 deletions

View file

@ -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<DacId, StreamConfig>;
type StreamMap = HashMap<DacId, laser::FrameStream<LaserModel>>;
type DisplayMap = HashMap<OutputId, WindowId>;
@ -61,7 +63,8 @@ struct GuiModel {
config_file_path: PathBuf,
laser_api: Arc<laser::Api>,
// 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<OutputId, DacConfig>;
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<WindowId> {
// 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<MonitorHandle> {
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<Vec<laser::Point>> {
let mut new_laser_points: Vec<laser::Point> = 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
// }
}

View file

@ -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
}
}
}