diff --git a/Cargo.lock b/Cargo.lock index a79c8fb..d82a340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,6 +2158,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "earcutr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01" +dependencies = [ + "itertools 0.11.0", + "num-traits", +] + [[package]] name = "ecolor" version = "0.23.0" @@ -2597,6 +2607,46 @@ dependencies = [ "typenum", ] +[[package]] +name = "geo" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1" +dependencies = [ + "earcutr", + "float_next_after 1.0.0", + "geo-types", + "geographiclib-rs", + "i_overlay", + "log", + "num-traits", + "robust", + "rstar", + "spade", +] + +[[package]] +name = "geo-types" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224" +dependencies = [ + "approx 0.5.1", + "num-traits", + "rayon", + "rstar", + "serde", +] + +[[package]] +name = "geographiclib-rs" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f611040a2bb37eaa29a78a128d1e92a378a03e0b6e66ae27398d42b1ba9a7841" +dependencies = [ + "libm", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -2901,6 +2951,15 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2924,6 +2983,8 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -2942,6 +3003,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -3000,6 +3071,50 @@ dependencies = [ "sample-consensus", ] +[[package]] +name = "i_float" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343" +dependencies = [ + "serde", +] + +[[package]] +name = "i_key_sort" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd" + +[[package]] +name = "i_overlay" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", + "rayon", +] + +[[package]] +name = "i_shape" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce" +dependencies = [ + "i_float", + "serde", +] + +[[package]] +name = "i_tree" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -3149,6 +3264,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -5118,6 +5242,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + [[package]] name = "rodio" version = "0.19.0" @@ -5147,6 +5277,17 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + [[package]] name = "rusb" version = "0.7.0" @@ -5544,6 +5685,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spade" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14e31a007e9f85c32784b04f89e6e194bb252a4d41b4a8ccd9e77245d901c8c" +dependencies = [ + "hashbrown 0.15.3", + "num-traits", + "robust", + "smallvec", +] + [[package]] name = "spirv" version = "0.2.0+1.5.4" @@ -5563,6 +5716,12 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "stackfuture" version = "0.3.0" @@ -5984,6 +6143,7 @@ dependencies = [ "bevy", "bevy_nannou", "cv-core", + "geo", "homography", "iyes_perf_ui", "nalgebra 0.30.1", diff --git a/Cargo.toml b/Cargo.toml index 7a8b8ac..a69c8e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ serde_repr = "0.1.20" homography = { git = "https://github.com/azazdeaz/homography" } nalgebra = "0.30.0" cv-core = "0.15.0" +geo = "0.30.0" [dev-dependencies] diff --git a/src/bin/render_lines_gui.rs b/src/bin/render_lines_gui.rs index a10d725..5c03a4e 100644 --- a/src/bin/render_lines_gui.rs +++ b/src/bin/render_lines_gui.rs @@ -15,7 +15,7 @@ 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::utils::{closest_edge, split_on_blank}; 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}; @@ -420,7 +420,12 @@ fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){ // println!("Cropped Points {} (was: {})", new_laser_points.len(), pointno); // } - frame.add_lines(new_laser_points); + // split by blanked points + let lines = split_on_blank(new_laser_points); + for line in lines { + frame.add_lines(line); + } + return; } @@ -740,6 +745,21 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) { }).unwrap(); } } + + + + + if ui + .checkbox(&mut selected_config.filters.crop.enabled ,"Crop before corrections (recommended)") + .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(); + } + } // Pincushion / Pillow / Barrel distortion. Generally, only needed for the x-axis @@ -794,13 +814,13 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) { if ui - .checkbox(&mut selected_config.filters.crop.enabled ,"Crop") + .checkbox(&mut selected_config.filters.clip.enabled ,"Apply clip mask") .changed() { - let enabled = selected_config.filters.crop.enabled; + let enabled = selected_config.filters.clip.enabled; if let Some(stream) = selected_laser_stream { stream.send(move |laser_model: &mut LaserModel| { - laser_model.config.filters.crop.enabled = enabled; + laser_model.config.filters.clip.enabled = enabled; }).unwrap(); } } @@ -895,9 +915,9 @@ fn view_line_canvas(app: &App, model: &GuiModel, frame: Frame) { }); draw.polyline() - .weight(thickness) - .join_round() - .points_colored(vertices); + .weight(thickness) + .join_round() + .points_colored(vertices); // draggable corners for the selected area if model.selected_stream == Some(dac_id.clone()){ @@ -929,7 +949,7 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { let draw = app.draw(); - draw.background().color(BLACK); + draw.background().color(srgba(0.3,0.3,0.3,1.)); let win = app.window_rect(); @@ -938,7 +958,7 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { let hh = h / 2.; let hw = w / 2.; - let thickness = 2.0; + let thickness = 3.0; let win_rect = app.main_window().rect().pad(20.0); @@ -979,7 +999,7 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { } draw.polygon() - .color(srgba(1.,1.,1.,3.)) + .color(srgba(0.3, 0.3, 0.3, 1.)) .stroke(PINK) .stroke_weight(thickness) .join_round() @@ -999,20 +1019,32 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) { let pointno = points.points.len(); let new_points = config.filters.apply(&points); + let new_laser_points = new_points.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.); + // 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: - let pos = [p.position[0] * hw, p.position[1] * hh]; - (pos, color) - }); - - draw.polyline() - .weight(thickness) - .join_round() - .points_colored(vertices); + let vertices = line.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); + } + } } @@ -1124,8 +1156,21 @@ fn laser_mouse_moved(app: &App, model: &mut GuiModel, pos: Point2) { // config.filters.clip. let point = config.filters.clip.mask.get_mut(*point_idx).unwrap(); + // set new position *point = [laser_x, laser_y]; + // 3. update config in laser stream threat + let mask = config.filters.clip.mask.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.clip.mask = mask; + }).unwrap(); + } + + + } fn laser_mouse_released(_app: &App, model: &mut GuiModel, _button: MouseButton) { diff --git a/src/trap/filters.rs b/src/trap/filters.rs index 3c4ad1b..cf54f60 100644 --- a/src/trap/filters.rs +++ b/src/trap/filters.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; // for glam::f32::Mat3 -use crate::trap::{laser::{apply_homography_matrix, Corner, LaserPoints}, tracks::CoordinateSpace}; +use crate::trap::{laser::{apply_homography_matrix, Corner, LaserPoints}, tracks::CoordinateSpace, utils::clip_lines}; use nannou_laser::{self as laser, Point}; use serde::{Deserialize, Serialize}; @@ -87,8 +87,9 @@ impl PointFilters { let mut p = self.dim.apply(points); p = self.homography.apply(&p); p = self.scale.apply(&p); - p = self.pincushion.apply(&p); p = self.crop.apply(&p); + p = self.pincushion.apply(&p); + p = self.clip.apply(&p); p } @@ -135,7 +136,7 @@ impl Default for PointFilters { scale: ScaleFilter { factor: 1. }, pincushion: PincushionFilter{k_x: 0.,k_x2: 0., k_y: 0., k_y2: 0.}, crop: CropFilter{ enabled: true }, - clip: ClipFilter{ enabled: false, mask: Corner::in_laser_space() }, + clip: ClipFilter{ enabled: true, mask: Corner::in_laser_space() }, } } } @@ -351,11 +352,9 @@ impl Filter for ClipFilter { }; } - // TODO - return LaserPoints{ - points: points.points.clone(), - space: points.space, - }; + + clip_lines(&self.mask, points) + } fn reverse(&self, points: &LaserPoints) -> LaserPoints{ diff --git a/src/trap/utils.rs b/src/trap/utils.rs index 703857b..d53775b 100644 --- a/src/trap/utils.rs +++ b/src/trap/utils.rs @@ -1,5 +1,9 @@ // GPT generated code is isolated to this file -use geo::{Coordinate, Line, Point, Polygon, prelude::*}; +use geo::{Coord, Line, Point, Polygon, prelude::*}; +use geo::line_intersection::{line_intersection, LineIntersection}; +use nannou_laser::{self as laser}; + +use crate::trap::laser::LaserPoints; /////////////////////// // Clip filter related @@ -50,3 +54,146 @@ pub fn closest_edge(mask: &[[f32; 2]], target: [f32; 2]) -> Option { } + + + +fn interpolate_color(c1: [f32; 3], c2: [f32; 3], t: f32) -> [f32; 3] { + [ + c1[0] + (c2[0] - c1[0]) * t as f32, + c1[1] + (c2[1] - c1[1]) * t as f32, + c1[2] + (c2[2] - c1[2]) * t as f32, + ] +} + +fn interpolate_point(p1: Coord, p2: Coord, t: f32) -> Coord { + Coord { + x: p1.x + (p2.x - p1.x) * t, + y: p1.y + (p2.y - p1.y) * t, + } +} + + +fn clip_colored_path(points: &Vec, bounds: &Polygon) -> Vec { + let mut result = Vec::new(); + + for pair in points.windows(2) { + let p1 = &pair[0]; + let p2 = &pair[1]; + + let inside1 = bounds.contains(&Point::from(p1.position)); + let inside2 = bounds.contains(&Point::from(p2.position)); + + let line = Line::new(p1.position, p2.position); + + match (inside1, inside2) { + (true, true) => { + // Both inside: keep both + if result.last().map(|r: &laser::Point| r.position != p1.position).unwrap_or(true) { + result.push(p1.clone()); + } + result.push(p2.clone()); + } + + (true, false) | (false, true) => { + // Crossing the boundary + if let Some(intersection) = line_intersect_polygon(line, bounds) { + let t = line_fraction(p1.position, p2.position, intersection); + let boundary_color = interpolate_color(p1.color, p2.color, t); + + let boundary_point = laser::Point { + position: intersection.into(), + color: boundary_color, + weight: p1.weight, + }; + + let blanked_boundary_point = boundary_point.blanked(); + + if inside1 { + if result.last().map(|r| r.position != p1.position).unwrap_or(true) { + result.push(p1.clone()); + } + result.push(boundary_point); + result.push(blanked_boundary_point); + } else { + result.push(blanked_boundary_point); + result.push(boundary_point); + result.push(p2.clone()); + } + } + } + + (false, false) => { + // Both outside: discard + } + } + } + + result +} + +fn line_intersect_polygon(line: Line, polygon: &Polygon) -> Option> { + for edge in polygon.exterior().lines() { + + // if let Some(point) = line.intersection(&edge) { + if let Some(point) = line_intersection(line, edge) { + return match point { + LineIntersection::SinglePoint {intersection, is_proper} => Some(intersection.into()), + LineIntersection::Collinear { intersection } => None, + }; + + } + } + None +} + +fn line_fraction(start: [f32;2], end: [f32;2], pt: Coord) -> f32 { + let total = ((end[0] - start[0]).powi(2) + (end[1] - start[1]).powi(2)).sqrt(); + let partial = ((pt.x - start[0]).powi(2) + (pt.y - start[1]).powi(2)).sqrt(); + if total == 0.0 { 0.0 } else { partial / total } +} + +pub fn clip_lines(mask: &Vec<[f32; 2]>, laser_points: &LaserPoints) -> LaserPoints { + + let path = &laser_points.points; + + let bounds = Polygon::new( + mask.clone() + .into(), + vec![], + ); + + let clipped = clip_colored_path(path, &bounds); + + LaserPoints{ + points: clipped, + space: laser_points.space, + } + + // for p in clipped { + // println!("Point: {:?}, Color: {:?}", p.position, p.color); + // } +} + +// split a Vec of laser::Points on blank'ed points, so that the optimiser can do its thing. +pub fn split_on_blank(points: Vec) -> Vec> { + let mut lines = Vec::new(); + let mut current_line = Vec::new(); + + for point in points { + if point.is_blank() { + if !current_line.is_empty() { + lines.push(current_line); + current_line = Vec::new(); + } + } else { + current_line.push(point); + } + } + + // Push the last line if not empty + if !current_line.is_empty() { + lines.push(current_line); + } + + lines +} \ No newline at end of file