From fac5bda2f5e238de9dbd73032c59c136569a6d90 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Fri, 17 Oct 2025 15:04:00 +0200 Subject: [PATCH] Wip layers --- src/bin/render_tracks.rs | 2 +- src/main.rs | 177 ++++++++++++++++++++++----------------- src/trap/filters.rs | 63 ++++++++++++++ src/trap/laser.rs | 16 ++-- src/trap/tracks.rs | 30 +++++++ src/trap/zmqplugin.rs | 16 ++-- 6 files changed, 214 insertions(+), 90 deletions(-) diff --git a/src/bin/render_tracks.rs b/src/bin/render_tracks.rs index 0be3215..ae3564d 100644 --- a/src/bin/render_tracks.rs +++ b/src/bin/render_tracks.rs @@ -133,7 +133,7 @@ fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){ // false => trap::shapes::ARE_YOU_SURE, // }; - let points = model.current_lines.clone(); + let points = model.current_layers.clone(); let mut new_points = Vec::new(); for point in points.into_iter() { diff --git a/src/main.rs b/src/main.rs index 5c8e1ab..2aba108 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ //! A clone of the `laser_frame_stream.rs` example that allows for configuring laser settings via a //! UI. -use bevy::scene::ron::Map; // use bevy_nannou::prelude::DARK_GRAY; // use nannou::lyon::geom::euclid::Transform2D; use nannou::{geom::Rect, math::map_range as nannou_map_range}; @@ -15,7 +14,7 @@ use nannou_laser::{self as laser}; use serde_json::Result; use laserspace::trap::filters::{MappedPoint, PointFilters}; use laserspace::trap::laser::{shape_rect, LaserPoints, LaserSpace, StreamSource, STREAM_SOURCES, TMP_DESK_CLUBMAX, Corner}; -use laserspace::trap::tracks::CoordinateSpace; +use laserspace::trap::tracks::{CoordinateSpace, RenderableLayers}; use laserspace::trap::utils::{closest_edge, split_on_blank}; use laserspace::trap::{laser::{python_cv_h_into_mat3, LaserModel, TMP_PYTHON_LASER_H, DacConfig}, tracks::{RenderableLines}}; use zmq::Socket; @@ -72,7 +71,7 @@ struct GuiModel { egui: Egui, // socket for receiving points zmq: Socket, - current_lines: RenderableLines, // a copy for the drawing renderer + current_layers: RenderableLayers, // a copy for the drawing renderer last_update: Instant, // dimming_factor: f32, lost_alpha: f32, @@ -155,23 +154,23 @@ fn zmq_receive(model: &mut GuiModel) { ]; let _nr = zmq::poll(&mut items, 0).unwrap(); - let lines: RenderableLines; + let layers: RenderableLayers; 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); + 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 + layers = match res { + Ok(layers) => layers, // if Ok(255), set x to 255 Err(_e) => { println!("No valid json?"); println!("{}", _e); // empty if invalid - RenderableLines::new() + RenderableLayers::new() }, // if Err("some message"), panic with error message "some message" }; } else if model.last_update < Instant::now() - Duration::from_millis(100){ @@ -180,14 +179,14 @@ fn zmq_receive(model: &mut GuiModel) { if model.lost_alpha > 0.{ - println!("No input, clear lines!!"); + println!("No input, clear layers!!"); model.lost_alpha *= 0.80; if model.lost_alpha < 0.1{ model.lost_alpha = 0.; } - lines = model.current_lines.with_alpha(model.lost_alpha); + layers = model.current_layers.with_alpha(model.lost_alpha); } else { - lines = RenderableLines::new() + layers = RenderableLayers::new() } } else { // No new lines, break @@ -198,17 +197,17 @@ fn zmq_receive(model: &mut GuiModel) { for (_dac, stream) in (&model.laser_streams).into_iter() { // let lines = get_laser_lines(version); - let lines_for_laser: RenderableLines = lines.clone(); + let layers_for_laser: RenderableLayers = layers.clone(); let sending = stream.send(move |laser_model: &mut LaserModel| { - let laser_lines: RenderableLines = lines_for_laser; - laser_model.current_lines = laser_lines; + let laser_layers: RenderableLayers = layers_for_laser; + laser_model.current_layers = laser_layers; }); if let Err(e) = sending { println!("Error sending to laser! {e:?}"); } } - model.current_lines = lines; + model.current_layers = layers; model.last_update = Instant::now(); } @@ -393,7 +392,7 @@ fn model(app: &App) -> GuiModel { let egui_ctx = egui.ctx(); egui_ctx.set_style(style()); - let current_lines = RenderableLines::new(); //Vec::new(); + let current_layers = RenderableLayers::new(); //Vec::new(); GuiModel { laser_api, @@ -403,7 +402,7 @@ fn model(app: &App) -> GuiModel { dac_rx, egui, zmq, - current_lines: current_lines, + current_layers: current_layers, last_update: Instant::now(), lost_alpha: 1., connected: true, @@ -420,34 +419,35 @@ fn model(app: &App) -> GuiModel { } } +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) + }; + // let new_laser_points = new_points.points; + new_laser_points.extend(new_points.points); + } + } + split_on_blank(new_laser_points) +} + 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, &model.config); - - // let pointno = points.points.len(); - - let new_points = match points.space { - CoordinateSpace::RawLaser => 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); - // } - - // split by blanked points - let lines = split_on_blank(new_laser_points); + let lines = layers_to_points_for_config(&model.current_layers, &model.config); for line in lines { frame.add_lines(line); } - return; } @@ -542,7 +542,7 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) { ref mut laser_settings, ref mut per_laser_config, ref mut selected_stream, - ref mut current_lines, + ref mut current_layers, .. } = *model; @@ -566,9 +566,17 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) { }); ui.separator(); + + ui.columns(3, |cols| { + cols[0].label(format!("Layers {}", current_layers.0.len())); + cols[1].label(format!("Lines {}", current_layers.line_count())); + cols[2].label(format!("Points {}", current_layers.point_count())); + + }); - ui.add(egui::Label::new(format!("Lines {}", current_lines.lines.len()))); - ui.add(egui::Label::new(format!("Points {}", current_lines.point_count()))); + // ui.add(egui::Label::new(format!("Layers {}", current_layers.0.len()))); + // ui.add(egui::Label::new(format!("Lines {}", current_layers.line_count()))); + // ui.add(egui::Label::new(format!("Points {}", current_layers.point_count()))); ui.heading("General settings"); @@ -731,6 +739,40 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) { } }); + + ui.horizontal_wrapped(|ui| { + for layer_nr in 1..=8 { + let mask = 1 << layer_nr; + let mut enabled_bool = mask & selected_config.layers_enabled != 0; + + if ui + .add(egui::Checkbox::new(&mut enabled_bool,format!("{:?}", layer_nr))) + .changed() + { + selected_config.layers_enabled &= mask; + let layers_enabled = selected_config.layers_enabled; + if let Some(stream) = selected_laser_stream { + stream.send(move |laser_model: &mut LaserModel| { + laser_model.config.layers_enabled = layers_enabled; + }).unwrap(); + } + } + } + }); + + // 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(egui::Slider::new(&mut selected_config.filters.dim.intensity, 0.0..=1.).text("Dimming")) @@ -743,7 +785,7 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) { }).unwrap(); } } - + if ui .add(egui::Slider::new(&mut selected_config.filters.dim.intensity, 0.0..=1.).text("Dimming")) .changed() @@ -867,12 +909,9 @@ fn view_line_canvas(app: &App, model: &GuiModel, frame: Frame) { // 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{ + let bgcolor = match model.connected{ true => DARKGRAY, false => LIGHTCORAL, - }, }; draw.background().color(bgcolor); @@ -901,20 +940,23 @@ fn view_line_canvas(app: &App, model: &GuiModel, frame: Frame) { // 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) - }); + for (_, layer) in model.current_layers.0.iter() { - draw.polyline() + for line in &layer.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); @@ -1014,7 +1056,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 = 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("Selected stream not found in configs"); // 2. clipping mask + its anchor points @@ -1039,28 +1081,13 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { // 3. current shape of the laser - - - let current_points: LaserPoints = (&model.current_lines).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 lines = layers_to_points_for_config(&model.current_layers, &config); - let new_laser_points = new_points.points; // 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 - let lines = split_on_blank(new_laser_points); for line in lines { // similar to map code: diff --git a/src/trap/filters.rs b/src/trap/filters.rs index 57552bc..2835efe 100644 --- a/src/trap/filters.rs +++ b/src/trap/filters.rs @@ -47,6 +47,11 @@ pub struct ScaleFilter { pub factor: f32 } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct InterpolateFilter { + pub max_distance: f32 +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct PincushionFilter { pub k_x: f32, @@ -69,6 +74,7 @@ pub struct PincushionFilter { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct PointFilters{ + pub interpolate: InterpolateFilter, pub dim: DimFilter, pub homography: HomographyFilter, pub scale: ScaleFilter, @@ -91,9 +97,14 @@ pub struct PointFilters{ // } // } +pub fn lerp(a: f32, b: f32, t: f32) -> f32{ + return (1. - t) * a + t * b +} + impl PointFilters { pub fn apply(&self, points: &LaserPoints) -> LaserPoints{ let mut p = self.dim.apply(points); + // TODO: not implemented yet p = self.interpolate.apply(&p); p = self.homography.apply(&p); p = self.scale.apply(&p); p = self.crop.apply(&p); @@ -141,6 +152,7 @@ impl Default for PointFilters { // let crop_filter = CropFilter{}; Self { homography: HomographyFilter::default(), + interpolate: InterpolateFilter{max_distance: 0.2}, dim: DimFilter{intensity: 0.5}, scale: ScaleFilter { factor: 1. }, pincushion: PincushionFilter{k_x: 0.,k_x2: 0., k_y: 0., k_y2: 0.}, @@ -571,6 +583,57 @@ impl Filter for DimFilter { } } +impl Filter for InterpolateFilter { + fn apply(&self, points: &LaserPoints) -> LaserPoints { + // TODO: fix the code below + + // let mut new_positions: Vec<[f32;2]> = Vec::new(); + + // // min distance between points before geometric filters + // let interpolated_edges = p.points.iter().zip(p.points.iter().skip(1)).map(|(p1, p2)|{ + // let distance = f32::sqrt((p2.position[0] - p1.position[0]).powi(2) + (p2.position[1] - p1.position[1]).powi(2)); + // let steps: i32 = (distance / required_distance).ceil() as i32; + // let mut interpolated_edge = Vec::new(); + // for i in 0..steps { + // let t = (steps as f32) /(i as f32); + // interpolated_edge.push( + // laser::Point{ + // position: [ + // lerp(p1.position[0], p2.position[0], t), + // lerp(p1.position[1], p2.position[1], t) + // ], + // color: p1.color, + // weight: p1.weight, + // } + + // ); + // } + + // interpolated_edge + // }); + + // let mut interpolated_points: Vec = interpolated_edges.flatten().collect(); + // if let Some(last) = p.points.last() { + // interpolated_points.push(last.clone()); // add last one so we don't lose it + // } + + // LaserPoints{ + // points: interpolated_points, + // space: points.space, + // } + LaserPoints::default() + + } + + fn reverse(&self, points: &LaserPoints) -> LaserPoints{ + // cannot easily be undone + LaserPoints{ + points: points.points.clone(), + space: points.space, + } + } +} + fn scale(points: &LaserPoints, factor: f32) -> LaserPoints{ let new_points = points.points.iter().map(|point| { let mut position = point.position.clone(); diff --git a/src/trap/laser.rs b/src/trap/laser.rs index 88a324a..1ed7c48 100644 --- a/src/trap/laser.rs +++ b/src/trap/laser.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use nannou_laser as laser; use std::time::Instant; use serde::{Deserialize, Serialize}; -use crate::trap::{filters::{PointFilters}, tracks::CoordinateSpace}; +use crate::trap::{filters::PointFilters, tracks::{CoordinateSpace, RenderableLayers}}; use super::tracks::{RenderableLines}; @@ -74,7 +74,7 @@ pub fn apply_homography_matrix(h: Mat3, p: &[f32; 2]) -> [f32; 2]{ #[derive(Resource, Clone)] pub struct LaserModel{ pub t: Instant, // register start time, so that animations can be moving - pub current_lines: RenderableLines, + pub current_layers: RenderableLayers, // pub dimming: f32, pub config: DacConfig, // per dac configuration } @@ -83,7 +83,7 @@ impl LaserModel{ pub fn new() -> Self{ Self{ t: Instant::now(), - current_lines: RenderableLines::new(), + current_layers: RenderableLayers::new(), // dimming: 0.3, config: DacConfig::default(), } @@ -118,16 +118,18 @@ pub struct DacConfig{ // pub homography: Mat3, pub source: StreamSource, pub filters: PointFilters, + pub layers_enabled: u8, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub enum StreamSource { Disabled, - CurrentLines, + CurrentLayers, Rectangle, GridY, // lines WorldGrid, // grid in world space Homography, // points for homography alignment + // Layers ( layers: ) // Circle, // segments // Spiral, } @@ -135,7 +137,7 @@ pub enum StreamSource { // usefull to create pull downs with an iterator pub const STREAM_SOURCES: [StreamSource; 6] = [ StreamSource::Disabled, - StreamSource::CurrentLines, + StreamSource::CurrentLayers, StreamSource::Rectangle, StreamSource::GridY, StreamSource::WorldGrid, @@ -152,7 +154,7 @@ 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::CurrentLines, filters: PointFilters::default().with_homography(LASER_H) } + DacConfig { name: "Unknown".into(), source: StreamSource::CurrentLayers, filters: PointFilters::default().with_homography(LASER_H), layers_enabled: 0b1111_1111 } } } @@ -214,7 +216,7 @@ impl StreamSource{ pub fn get_shape(&self, current_points: LaserPoints, config: &DacConfig) -> LaserPoints { match self { - Self::CurrentLines => current_points, + Self::CurrentLayers => current_points, Self::Rectangle => { shape_rect(LaserSpace::READY, 11) }, diff --git a/src/trap/tracks.rs b/src/trap/tracks.rs index 04a55ea..363252d 100644 --- a/src/trap/tracks.rs +++ b/src/trap/tracks.rs @@ -86,6 +86,36 @@ pub struct RenderableLines{ pub space: CoordinateSpace, // enum in python } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(transparent)] +pub struct RenderableLayers(pub std::collections::HashMap); + +impl RenderableLayers{ + pub fn new() -> Self{ + Self (std::collections::HashMap::new()) + } + + pub fn with_alpha(&self, alpha: f32) -> Self { + if alpha == 1. { + return self.clone(); + } + + Self ( self.0.iter().map(|(layer, lines)| { (*layer, lines.with_alpha(alpha))}).collect() ) + } + + + pub fn line_count(&self) -> usize { + let s = self.0.values().map(|l| l.lines.len()).sum(); + s + } + + + pub fn point_count(&self) -> usize { + let s = self.0.values().map(|l| l.point_count()).sum(); + s + } +} + impl RenderableLines{ pub fn new() -> Self{ RenderableLines { lines: Vec::new(), space: CoordinateSpace::World } diff --git a/src/trap/zmqplugin.rs b/src/trap/zmqplugin.rs index 669741a..99a68a0 100644 --- a/src/trap/zmqplugin.rs +++ b/src/trap/zmqplugin.rs @@ -2,6 +2,8 @@ use zmq::Socket; use serde_json::Result; use bevy::{ecs::system::SystemState, prelude::*, render::Render}; use std::num::NonZero; +use crate::trap::tracks::RenderableLayers; + use super::{laser::{LaserApi, LaserTimer}, tracks::{Frame, RenderableLines, Track, TrackBundle}}; @@ -60,10 +62,10 @@ fn receive_zmq_lines( dbg!(&json); // let msg: Frame = serde_json::from_str(&json[4..]).expect("No valid json?"); - let res: Result = serde_json::from_str(&json); + let res: Result = serde_json::from_str(&json); - let lines: RenderableLines = match res { - Ok(lines) => lines, // if Ok(255), set x to 255 + let layers: RenderableLayers = match res { + Ok(layers) => layers, // if Ok(255), set x to 255 Err(_e) => { println!("No valid json? {json}"); println!("{}", _e); @@ -73,15 +75,15 @@ fn receive_zmq_lines( }, // if Err("some message"), panic with error message "some message" }; - println!("receive {}", lines.lines.len()); + println!("receive {} lines", layers.line_count()); for (laser_api, mut laser_timer) in lasers.iter_mut() { laser_timer.timer.tick(time.delta()); // let lines = get_laser_lines(version); - let lines: RenderableLines = lines.clone(); + let lines: RenderableLayers = layers.clone(); laser_api.laser_stream.send(|laser| { - let laser_lines: RenderableLines = lines; - laser.current_lines = laser_lines; + let current_layers: RenderableLayers = lines; + laser.current_layers = current_layers; }).unwrap(); } }