Compare commits
No commits in common. "main" and "filters" have entirely different histories.
17 changed files with 1353 additions and 2702 deletions
849
Cargo.lock
generated
849
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
@ -1,14 +1,11 @@
|
|||
[package]
|
||||
name = "laserspace"
|
||||
name = "trap_rust"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Ruben van de Ven <git@rubenvandeven.com>"]
|
||||
default-run = "laserspace"
|
||||
|
||||
[dependencies]
|
||||
bevy = "0.15.3"
|
||||
# bevy_nannou = { git = "https://github.com/nannou-org/nannou", branch = "bevy-refactor", version = "0.1.0", features = ["wayland"] }
|
||||
bevy_nannou = { git = "https://github.com/nannou-org/nannou", rev = "03135771b41944347a64ef385f299d89dbea45c1", version = "0.1.0", features = ["wayland"] }
|
||||
bevy_nannou = { git = "https://github.com/nannou-org/nannou", branch = "bevy-refactor", version = "0.1.0", features = ["wayland"] }
|
||||
iyes_perf_ui = "0.4.0"
|
||||
|
||||
nannou_laser = { git = "https://github.com/rubenvandeven/nannou", branch = "helios_laser_DAC" }
|
||||
|
@ -22,12 +19,6 @@ nannou_egui = { version = "0.19.0", features = ["wayland"] }
|
|||
serde_repr = "0.1.20"
|
||||
|
||||
|
||||
# homography and its dependents:
|
||||
homography = { git = "https://github.com/azazdeaz/homography" }
|
||||
nalgebra = "0.30.0"
|
||||
cv-core = "0.15.0"
|
||||
geo = "0.30.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
|
||||
|
@ -37,5 +28,5 @@ geo = "0.30.0"
|
|||
opt-level = 3
|
||||
|
||||
[[bin]]
|
||||
name="laserspace"
|
||||
name="renderer"
|
||||
path="src/main.rs"
|
||||
|
|
36
README.md
36
README.md
|
@ -1,36 +0,0 @@
|
|||
A tool to send lines to a series of laser projectors (showlasers). Uses the [nannou](https://github.com/nannou-org/nannou) creative coding framework for laser control, and optimisation of the lines before sending them to the DAC.
|
||||
|
||||
It's still a bit of a hacked-together tool. But it works for my case.
|
||||
|
||||
## Features
|
||||
|
||||
* Receive lines over ZMQ.
|
||||
* Safety feature: stop the output if no lines are received
|
||||
* Clipping mask, to mark laser-free zones
|
||||
* Homography by simply dragging the corners of the projection area/corner-pin.
|
||||
* Change intensity of projected lines.
|
||||
* Geometric (pincushion/barrel) correction for x and y axes independently
|
||||
* Particularly x-distortion tends to be present in laser systems due to the independent x/y galvanometer setup.
|
||||
* Configuration can be saved to a JSON file.
|
||||
* Many of the settings can be configured per DAC.
|
||||
* Some pre-defined shapes for debugging purposes.
|
||||
|
||||
## Basic idea
|
||||
|
||||
This tool was initially an adaption of the [laser_frame_stream_gui.rs example code](https://github.com/nannou-org/nannou/blob/master/examples/laser/laser_frame_stream.rs) to enable projection mapping of a large space, which required multiple lasers. Received lines are assumed to be in world-space coordinates. The space is mapped to distinct laser DACs by means of homography/corner-pinning and various geometric correction parameters.
|
||||
|
||||
By using ZMQ as input, the mapping of the lines is decoupled from the generation code. In my own setup ([trap](https://git.rubenvandeven.com/security_vision/trap)) the lines are generated by means of a sequence of Python scripts.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
<video controls="" src="/security_vision/laserspace/raw/branch/main/assets/screenshots/laserspace-demo.mp4">
|
||||
<strong>Your browser does not support the HTML5 "video" tag.</strong>
|
||||
</video>
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cargo run ZMQ_QUEUE_ADDRESS
|
||||
```
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
Before Width: | Height: | Size: 54 KiB |
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
|
@ -1,217 +0,0 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
use bevy_nannou::prelude::*;
|
||||
use bevy_nannou::NannouPlugin;
|
||||
|
||||
use nannou_laser::point::Rgb;
|
||||
|
||||
use trap::laser::apply_homography_matrix;
|
||||
use trap::laser::python_cv_h_into_mat3;
|
||||
use trap::laser::LaserApi;
|
||||
use trap::laser::LaserModel;
|
||||
use trap::laser::LaserTimer;
|
||||
use trap::laser::TMP_PYTHON_LASER_H;
|
||||
use trap::shapes::PositionAndIntensity;
|
||||
use trap::tracks::LaserPoints;
|
||||
use trap::tracks::RenderableLines;
|
||||
use trap::tracks::SpawnedTime;
|
||||
use trap::tracks::Track;
|
||||
use trap::zmqplugin::ZmqPlugin;
|
||||
// use iyes_perf_ui::PerfUiPlugin;
|
||||
|
||||
use nannou_laser as laser;
|
||||
use trap::zmqplugin::ZmqReceiveTarget;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
pub mod trap;
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
// app.add_plugins((DefaultPlugins, NannouPlugin))
|
||||
app.add_plugins((DefaultPlugins, NannouPlugin))
|
||||
// .add_plugins(bevy::diagnostic::FrameTimeDiagnosticsPlugin)
|
||||
// .add_plugins(bevy::diagnostic::EntityCountDiagnosticsPlugin)
|
||||
// .add_plugins(bevy::diagnostic::SystemInformationDiagnosticsPlugin)
|
||||
// .add_plugins(PerfUiPlugin)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Startup, setup_laser)
|
||||
// .add_systems(Update, update)
|
||||
.add_systems(Update, exit_system)
|
||||
|
||||
|
||||
.add_plugins(ZmqPlugin {
|
||||
// url: "tcp://localhost:5558".to_string(),
|
||||
url: "tcp://100.109.175.82:99174".to_string(),
|
||||
// url: "tcp://127.0.0.1:99173".to_string(),
|
||||
filter: "".to_string(),
|
||||
target: ZmqReceiveTarget::LINES
|
||||
})
|
||||
|
||||
|
||||
.run();
|
||||
}
|
||||
|
||||
|
||||
// Because of world.insert_non_send_resource, this is an exclusive system
|
||||
// see: https://bevy-cheatbook.github.io/programming/exclusive.html
|
||||
fn setup_laser(mut commands: Commands) {
|
||||
|
||||
// laser works on non-exclusive system (like the normal setup()), _but_ world
|
||||
// is needed in the laser callback
|
||||
|
||||
// Initialise the state that we want to live on the laser thread and spawn the stream.
|
||||
let laser_model = LaserModel::new();
|
||||
let _laser_api = laser::Api::new();
|
||||
// let detected_dacs = _laser_api.detect_dacs(DacVariant::DacVariantHelios);
|
||||
|
||||
// while let Ok(res) = detected_dacs {
|
||||
// if let laser::DetectDacs::Helios { previous_dac } = res {
|
||||
// info!("DACS: {:?}", previous_dac);
|
||||
// }
|
||||
// }
|
||||
|
||||
let laser_stream = _laser_api
|
||||
.new_frame_stream(laser_model, laser_frame_producer)
|
||||
// .detected_dac(dac)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
let api = LaserApi {
|
||||
_laser_api,
|
||||
laser_stream,
|
||||
// current_points: Vec::new(),
|
||||
};
|
||||
commands.spawn((api, LaserTimer {
|
||||
// create the non-repeating fuse timer
|
||||
timer: Timer::new(Duration::from_millis(1000), TimerMode::Repeating),
|
||||
}));
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
// Spawn a camera for our main window
|
||||
// TODO: look into https://docs.rs/visioncortex/latest/visioncortex/struct.PerspectiveTransform.html
|
||||
|
||||
commands.spawn(render::NannouCamera);
|
||||
|
||||
}
|
||||
|
||||
|
||||
fn text2points_with_color(position_and_intensity: PositionAndIntensity, color: Rgb) -> laser::Point{
|
||||
let used_color = color.map(|v| v * position_and_intensity[2]);
|
||||
let p = [position_and_intensity[0], -position_and_intensity[1]];
|
||||
laser::Point::new(p, color)
|
||||
}
|
||||
|
||||
fn text2points(position_and_intensity: PositionAndIntensity) -> laser::Point{
|
||||
let color = match position_and_intensity[2] {
|
||||
1.0 => [1.0; 3],
|
||||
0.0 => [0.0; 3],
|
||||
_ => [1.0; 3] // TODO add provided color
|
||||
};
|
||||
let p = [position_and_intensity[0], -position_and_intensity[1]];
|
||||
laser::Point::new(p, color)
|
||||
}
|
||||
|
||||
// impl Into<Mat3> for [[f32;3]; 3] {
|
||||
// fn from(m) -> Self{
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
const LASER_H: Mat3 = python_cv_h_into_mat3(TMP_PYTHON_LASER_H);
|
||||
// const LASER_H: Mat3 = python_cv_h_into_mat3(TMP_PYTHON_LASER_H_FOR_NANNOU);
|
||||
|
||||
fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){
|
||||
|
||||
// let dt = model.t.elapsed().as_millis();
|
||||
// let use_second = (dt % 1000) > 500;
|
||||
|
||||
// let positions = match use_second {
|
||||
// true => trap::shapes::YOUR_FUTURE,
|
||||
// false => trap::shapes::ARE_YOU_SURE,
|
||||
// };
|
||||
|
||||
let points = model.current_points.clone();
|
||||
|
||||
let mut new_points = Vec::new();
|
||||
for point in points.into_iter() {
|
||||
let p = point.position;
|
||||
let new_position = apply_homography_matrix(LASER_H, &p);
|
||||
// let s = 1.; // when using TMP_PYTHON_LASER_H_FOR_NANNOU -- doesn't work?
|
||||
let s = 0xFFF as f32; // when using TMP_PYTHON_LASER_H
|
||||
|
||||
let new_point = laser::Point {
|
||||
position: [new_position[0]/s, new_position[1]/s],
|
||||
.. point
|
||||
};
|
||||
new_points.push(new_point);
|
||||
}
|
||||
|
||||
info!("Points {}", new_points.len());
|
||||
// println!("{:?}", new_points);
|
||||
frame.add_lines(new_points);
|
||||
}
|
||||
|
||||
|
||||
fn get_laser_lines(use_second: bool) -> Vec<nannou_laser::Point>{
|
||||
let positions = match use_second {
|
||||
true => trap::shapes::YOUR_FUTURE,
|
||||
false => trap::shapes::ARE_YOU_SURE,
|
||||
};
|
||||
let points = positions.iter().cloned().map(text2points).collect();
|
||||
return points
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
fn exit_system(keys: Res<ButtonInput<KeyCode>>, mut exit: EventWriter<AppExit>) {
|
||||
if keys.just_pressed(KeyCode::KeyQ) {
|
||||
info!("Sending exit command");
|
||||
exit.send(AppExit::Success);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
// mut commands: Commands,
|
||||
// keys: Res<ButtonInput<KeyCode>>,
|
||||
draws: Query<&Draw>,
|
||||
mut lasers: Query<(&mut LaserApi, &mut LaserTimer)>,
|
||||
tracks: Query<(&Track, &SpawnedTime)>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let mut lines = RenderableLines::new();
|
||||
for (track, created_at) in tracks.iter() {
|
||||
// println!("{} {}, history: {}", track.track_id.to_string(), created_at.instant.elapsed().as_millis(), track.history.len());
|
||||
let rl = RenderableLines::from(track);
|
||||
lines.lines.extend(rl.lines);
|
||||
}
|
||||
|
||||
for (laser_api, mut laser_timer) in lasers.iter_mut() {
|
||||
|
||||
laser_timer.timer.tick(time.delta());
|
||||
|
||||
let version = laser_timer.timer.elapsed().as_millis() > 500;
|
||||
debug!("{} {}", version, laser_timer.timer.elapsed().as_millis());
|
||||
// let lines = get_laser_lines(version);
|
||||
let points: LaserPoints = (&lines).into();
|
||||
laser_api.laser_stream.send(|laser| {
|
||||
let laserpoints: LaserPoints = points;
|
||||
// TODO: homography
|
||||
laser.current_points = laserpoints;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
for draw in draws.iter() {
|
||||
draw.background().color(DIM_GRAY);
|
||||
|
||||
draw.ellipse().color(LIGHT_GRAY).w_h(100.0, 100.0);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use bevy::math::Vec3;
|
||||
use laserspace::trap::laser::{self, apply_homography_matrix};
|
||||
use trap_rust::trap::laser::{self, apply_homography_matrix};
|
||||
|
||||
/*
|
||||
Compare output with the following python
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use serde_json::Result;
|
||||
use laserspace::trap::tracks::RenderableLines;
|
||||
use trap_rust::trap::tracks::RenderableLines;
|
||||
|
||||
/*
|
||||
Compare output with the following python
|
||||
|
|
852
src/bin/render_lines_gui.rs
Normal file
852
src/bin/render_lines_gui.rs
Normal file
|
@ -0,0 +1,852 @@
|
|||
//! 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 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::{LaserPoints, StreamSource, STREAM_SOURCES, TMP_DESK_CLUBMAX};
|
||||
use trap_rust::trap::tracks::CoordinateSpace;
|
||||
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 egui_dropdown::DropDownBox;
|
||||
|
||||
|
||||
fn main() {
|
||||
nannou::app(model).update(update).run();
|
||||
}
|
||||
|
||||
pub struct StreamConfig{
|
||||
pub stream: laser::FrameStream<LaserModel>,
|
||||
pub config: DacConfig,
|
||||
}
|
||||
type StreamConfigMap = HashMap<DacId, StreamConfig>;
|
||||
|
||||
struct GuiModel {
|
||||
// A handle to the laser API used for spawning streams and detecting DACs.
|
||||
laser_api: Arc<laser::Api>,
|
||||
// All of the live stream handles.
|
||||
laser_streams: StreamConfigMap,
|
||||
// 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<laser::DetectedDac>,
|
||||
// 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<DacId>,
|
||||
// canvas_transform: Translation2D<f32, ScreenSpace, ScreenSpace>,
|
||||
// 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<laser::FrameStream<LaserModel>>) {
|
||||
|
||||
/// 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<RenderableLines> = 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_config) in (&model.laser_streams).into_iter() {
|
||||
// let lines = get_laser_lines(version);
|
||||
let lines_for_laser: RenderableLines = lines.clone();
|
||||
let sending = stream_config.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<DacId, DacConfig>;
|
||||
|
||||
|
||||
// Some hardcoded config. Not spending time on reading/writing config atm.
|
||||
fn get_dac_configs() -> DacConfigMap{
|
||||
|
||||
let mut dac_configs: DacConfigMap = HashMap::new();
|
||||
dac_configs.insert(
|
||||
DacId::Helios { id: 926298163 },
|
||||
DacConfig{
|
||||
name: "Helios#1".into(),
|
||||
.. DacConfig::default()
|
||||
}
|
||||
);
|
||||
dac_configs.insert(
|
||||
DacId::EtherDream {
|
||||
mac_address: [
|
||||
122,
|
||||
39,
|
||||
223,
|
||||
73,
|
||||
5,
|
||||
227,
|
||||
],
|
||||
},
|
||||
DacConfig{
|
||||
name: "ED - 192.168.8.101".into(),
|
||||
.. DacConfig::default()
|
||||
}
|
||||
);
|
||||
dac_configs.insert(
|
||||
DacId::EtherDream {
|
||||
mac_address: [
|
||||
98,
|
||||
120,
|
||||
178,
|
||||
228,
|
||||
198,
|
||||
175,
|
||||
],
|
||||
},
|
||||
DacConfig{
|
||||
name: "ED - 192.168.9.101".into(),
|
||||
.. DacConfig::default()
|
||||
// filters: PointFilters::default(),
|
||||
}
|
||||
);
|
||||
dac_configs.insert(
|
||||
DacId::EtherDream {
|
||||
mac_address: [
|
||||
18,
|
||||
52,
|
||||
86,
|
||||
120,
|
||||
154,
|
||||
188,
|
||||
],
|
||||
},
|
||||
DacConfig{
|
||||
name: "Emulator".into(),
|
||||
.. DacConfig::default()
|
||||
}
|
||||
);
|
||||
dac_configs
|
||||
}
|
||||
|
||||
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(canvas_zoom)
|
||||
.view(view_line_canvas)
|
||||
.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);
|
||||
// egui.ctx().set_fonts(fonts());
|
||||
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_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 points = LaserPoints { points: vec!(
|
||||
// laser::Point{
|
||||
// position:[ 9.4, 7.2],
|
||||
// color: [1.,1.,0.],
|
||||
// weight: 0,
|
||||
// },
|
||||
// laser::Point{
|
||||
// position:[ 12.4, 7.2],
|
||||
// color: [1.,1.,0.],
|
||||
// weight: 0,
|
||||
// },
|
||||
// laser::Point{
|
||||
// position:[ 12.4, 4.2],
|
||||
// color: [1.,1.,0.],
|
||||
// weight: 0,
|
||||
// },
|
||||
// laser::Point{
|
||||
// position:[ 9.4, 4.2],
|
||||
// color: [1.,1.,0.],
|
||||
// weight: 0,
|
||||
// },
|
||||
// ), space: CoordinateSpace::World };
|
||||
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);
|
||||
}
|
||||
|
||||
// on reconnect gives Unknown
|
||||
// dbg!(&model.config);
|
||||
// dbg!(&points.points[0]);
|
||||
// dbg!(&new_laser_points[0]);
|
||||
|
||||
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());
|
||||
let config = match model.per_laser_config.contains_key(&dac.id()) {
|
||||
true => &model.per_laser_config[&dac.id()],
|
||||
false => {
|
||||
println!("Found unknown DAC, try to register it in get_dac_configs()");
|
||||
dbg!(&dac.id());
|
||||
&DacConfig::default()
|
||||
},
|
||||
};
|
||||
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(), StreamConfig{ stream, config: config.clone() });
|
||||
}
|
||||
|
||||
// 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_config) in model.laser_streams.iter() {
|
||||
if stream_config.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_config) = s{
|
||||
let dac = stream_config.stream
|
||||
.dac()
|
||||
.expect("`dac` returned `None` even though one was specified during stream creation");
|
||||
let res = stream_config.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 = match model.per_laser_config.contains_key(&dac.id()) {
|
||||
true => &model.per_laser_config[&dac.id()],
|
||||
false => {
|
||||
println!("Found unknown DAC, try to register it in get_dac_configs()");
|
||||
dbg!(&dac.id());
|
||||
&DacConfig::default()
|
||||
},
|
||||
};
|
||||
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, StreamConfig{stream, config: stream_config.config});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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.heading("Laser Points");
|
||||
|
||||
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.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.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.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_config) in laser_streams.iter() {
|
||||
stream_config.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_config) in laser_streams.iter() {
|
||||
stream_config.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.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.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.stream.set_radians_per_point(radians).ok();
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Laser specific settings");
|
||||
|
||||
if laser_streams.is_empty() {
|
||||
ui.label("No dacs available");
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
for (dac_id, _stream) in laser_streams.iter() {
|
||||
ui.selectable_value(
|
||||
selected_stream,
|
||||
Some(dac_id.clone()),
|
||||
if let Some(config) = per_laser_config.get(&dac_id) { config.name.clone() } else { "DAC".into() }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(selected_stream_value) = selected_stream {
|
||||
|
||||
ui.separator();
|
||||
ui.add(egui::Label::new(format!("{:?}", 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 stream_config.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() {
|
||||
// let source = source_option;
|
||||
stream_config.stream.send(move |laser_model: &mut LaserModel| {
|
||||
laser_model.config.source = source_option;
|
||||
}).unwrap();
|
||||
};
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut stream_config.config.filters.dim.intensity, 0.0..=1.).text("Dimming"))
|
||||
.changed()
|
||||
{
|
||||
let factor = stream_config.config.filters.dim.intensity;
|
||||
stream_config.stream.send(move |laser_model: &mut LaserModel| {
|
||||
laser_model.config.filters.dim.intensity = factor;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut stream_config.config.filters.dim.intensity, 0.0..=1.).text("Dimming"))
|
||||
.changed()
|
||||
{
|
||||
let factor = stream_config.config.filters.dim.intensity;
|
||||
stream_config.stream.send(move |laser_model: &mut LaserModel| {
|
||||
laser_model.config.filters.dim.intensity = factor;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut stream_config.config.filters.scale.factor, 0.0..=2.).text("Scale"))
|
||||
.changed()
|
||||
{
|
||||
let factor = stream_config.config.filters.scale.factor;
|
||||
stream_config.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 stream_config.config.filters.pincushion.k_x, -0.5..=0.5).text("Pincushion x"))
|
||||
.changed()
|
||||
{
|
||||
let factor = stream_config.config.filters.pincushion.k_x;
|
||||
stream_config.stream.send(move |laser_model: &mut LaserModel| {
|
||||
laser_model.config.filters.pincushion.k_x = factor;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_x2, -0.2..=0.2).text("Higher order pincushion x"))
|
||||
.changed()
|
||||
{
|
||||
let factor = stream_config.config.filters.pincushion.k_x2;
|
||||
stream_config.stream.send(move |laser_model: &mut LaserModel| {
|
||||
laser_model.config.filters.pincushion.k_x2 = factor;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_y, -0.5..=0.5).text("Pincushion y"))
|
||||
.changed()
|
||||
{
|
||||
let factor = stream_config.config.filters.pincushion.k_y;
|
||||
stream_config.stream.send(move |laser_model: &mut LaserModel| {
|
||||
laser_model.config.filters.pincushion.k_y = factor;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_y2, -0.2..=0.2).text("Higher order pincushion y"))
|
||||
.changed()
|
||||
{
|
||||
let factor = stream_config.config.filters.pincushion.k_y2;
|
||||
stream_config.stream.send(move |laser_model: &mut LaserModel| {
|
||||
laser_model.config.filters.pincushion.k_y2 = factor;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
|
||||
if ui
|
||||
.checkbox(&mut stream_config.config.filters.crop.enabled ,"Crop")
|
||||
.changed()
|
||||
{
|
||||
let enabled = stream_config.config.filters.crop.enabled;
|
||||
stream_config.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();
|
||||
|
||||
// set background to blue
|
||||
|
||||
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 = 25.;
|
||||
let translate_x = -300.;
|
||||
let translate_y = 100.;
|
||||
|
||||
|
||||
|
||||
draw_grid(&draw, &win, scale, 1.);
|
||||
// let t = app.time;
|
||||
|
||||
// let n_points = 10;
|
||||
let thickness = 2.0;
|
||||
// let hz = ((app.mouse.x + win.right()) / win.w()).powi(4) * 1000.0;
|
||||
|
||||
// TODO refactor to using euclid::point2D for scale
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
// put everything on the frame
|
||||
draw.to_frame(app, &frame).unwrap();
|
||||
}
|
||||
|
||||
|
||||
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 mouse_moved(_app: &App, _model: &mut GuiModel, _pos: Point2) {
|
||||
}
|
||||
|
||||
fn mouse_pressed(_app: &App, _model: &mut GuiModel, _button: MouseButton) {
|
||||
// _model.dragging
|
||||
}
|
||||
|
||||
fn mouse_released(_app: &App, _model: &mut GuiModel, _button: MouseButton) {}
|
||||
|
||||
fn mouse_wheel(_app: &App, _model: &mut GuiModel, _dt: MouseScrollDelta, _phase: TouchPhase) {
|
||||
// canvas zoom
|
||||
}
|
|
@ -3,23 +3,23 @@ use bevy::prelude::*;
|
|||
use bevy_nannou::prelude::*;
|
||||
use bevy_nannou::NannouPlugin;
|
||||
use nannou_laser::point::Rgb;
|
||||
use laserspace::trap::laser::apply_homography_matrix;
|
||||
use laserspace::trap::laser::python_cv_h_into_mat3;
|
||||
use laserspace::trap::laser::LaserApi;
|
||||
use laserspace::trap::laser::LaserModel;
|
||||
use laserspace::trap::laser::LaserTimer;
|
||||
use laserspace::trap::laser::TMP_PYTHON_LASER_H;
|
||||
use laserspace::trap::laser::TMP_PYTHON_LASER_H_FOR_NANNOU;
|
||||
use laserspace::trap::shapes::PositionAndIntensity;
|
||||
use laserspace::trap::tracks::LaserPoints;
|
||||
use laserspace::trap::tracks::RenderableLines;
|
||||
use laserspace::trap::tracks::SpawnedTime;
|
||||
use laserspace::trap::tracks::Track;
|
||||
use laserspace::trap::zmqplugin::ZmqPlugin;
|
||||
use laserspace::trap::zmqplugin::ZmqReceiveTarget;
|
||||
use trap_rust::trap::laser::apply_homography_matrix;
|
||||
use trap_rust::trap::laser::python_cv_h_into_mat3;
|
||||
use trap_rust::trap::laser::LaserApi;
|
||||
use trap_rust::trap::laser::LaserModel;
|
||||
use trap_rust::trap::laser::LaserTimer;
|
||||
use trap_rust::trap::laser::TMP_PYTHON_LASER_H;
|
||||
use trap_rust::trap::laser::TMP_PYTHON_LASER_H_FOR_NANNOU;
|
||||
use trap_rust::trap::shapes::PositionAndIntensity;
|
||||
use trap_rust::trap::tracks::LaserPoints;
|
||||
use trap_rust::trap::tracks::RenderableLines;
|
||||
use trap_rust::trap::tracks::SpawnedTime;
|
||||
use trap_rust::trap::tracks::Track;
|
||||
use trap_rust::trap::zmqplugin::ZmqPlugin;
|
||||
// use iyes_perf_ui::PerfUiPlugin;
|
||||
|
||||
use nannou_laser as laser;
|
||||
use trap_rust::trap::zmqplugin::ZmqReceiveTarget;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -157,8 +157,8 @@ fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){
|
|||
|
||||
fn get_laser_lines(use_second: bool) -> Vec<nannou_laser::Point>{
|
||||
let positions = match use_second {
|
||||
true => laserspace::trap::shapes::YOUR_FUTURE,
|
||||
false => laserspace::trap::shapes::ARE_YOU_SURE,
|
||||
true => trap_rust::trap::shapes::YOUR_FUTURE,
|
||||
false => trap_rust::trap::shapes::ARE_YOU_SURE,
|
||||
};
|
||||
let points = positions.iter().cloned().map(text2points).collect();
|
||||
return points
|
||||
|
|
1484
src/main.rs
1484
src/main.rs
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
use bevy::prelude::*; // for glam::f32::Mat3
|
||||
|
||||
use crate::trap::{laser::{apply_homography_matrix, Corner, LaserPoints}, tracks::CoordinateSpace, utils::clip_lines};
|
||||
use crate::trap::{laser::{apply_homography_matrix, LaserPoints}, tracks::CoordinateSpace};
|
||||
|
||||
use nannou_laser::{self as laser, Point};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -9,7 +9,6 @@ pub trait Filter {
|
|||
// fn set_config(&self)
|
||||
// fn set_config(&self)
|
||||
fn apply(&self, points: &LaserPoints) -> LaserPoints;
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
|
@ -22,12 +21,6 @@ pub struct CropFilter {
|
|||
pub enabled: bool
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ClipFilter {
|
||||
pub enabled: bool,
|
||||
pub mask: Vec<[f32; 2]>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DimFilter {
|
||||
pub intensity: f32
|
||||
|
@ -65,7 +58,6 @@ pub struct PointFilters{
|
|||
pub scale: ScaleFilter,
|
||||
pub pincushion: PincushionFilter,
|
||||
pub crop: CropFilter,
|
||||
pub clip: ClipFilter,
|
||||
}
|
||||
|
||||
// list of enums deprecated in favour of struct
|
||||
|
@ -87,37 +79,8 @@ impl PointFilters {
|
|||
let mut p = self.dim.apply(points);
|
||||
p = self.homography.apply(&p);
|
||||
p = self.scale.apply(&p);
|
||||
p = self.crop.apply(&p);
|
||||
p = self.pincushion.apply(&p);
|
||||
p = self.clip.apply(&p);
|
||||
p
|
||||
}
|
||||
|
||||
// laser space to world space
|
||||
pub fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
let mut p = self.dim.reverse(points);
|
||||
// dbg!("in {:?}", &p.points[0]);
|
||||
p = self.pincushion.reverse(&p);
|
||||
// dbg!("undistort {:?}", &p.points[0]);
|
||||
p = self.scale.reverse(&p);
|
||||
// dbg!("unscale {:?}", &p.points[0]);
|
||||
p = self.homography.reverse(&p);
|
||||
// dbg!("unperspective {:?}", &p.points[0]);
|
||||
// p = self.crop.reverse(&p);
|
||||
p
|
||||
}
|
||||
|
||||
// same as reverse() but ignores homography. Required when gathering points for calculating homography.
|
||||
pub fn reverse_without_homography(&self, points: &LaserPoints) -> LaserPoints{
|
||||
let mut p = self.dim.reverse(points);
|
||||
// dbg!("in {:?}", &p.points[0]);
|
||||
p = self.pincushion.reverse(&p);
|
||||
// dbg!("undistort {:?}", &p.points[0]);
|
||||
p = self.scale.reverse(&p);
|
||||
// dbg!("unscale {:?}", &p.points[0]);
|
||||
// p = self.homography.reverse(&p);
|
||||
// dbg!("unperspective {:?}", &p.points[0]);
|
||||
// p = self.crop.reverse(&p);
|
||||
p = self.crop.apply(&p);
|
||||
p
|
||||
}
|
||||
|
||||
|
@ -136,7 +99,6 @@ 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: true, mask: Corner::in_laser_space() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,11 +116,12 @@ impl Filter for HomographyFilter {
|
|||
_ => panic!("Invalid coordinate space"),
|
||||
|
||||
};
|
||||
// also converts from world space to laser space (origin in center)
|
||||
// let s = 0xFFF as f32 / 2.;
|
||||
// let normalised_pos: [f32;2] = [new_position[0]/s - 1., new_position[1]/s - 1.];
|
||||
// let new_position = apply_homography_matrix(LASER_H, &p);
|
||||
// let s = 1.; // when using TMP_PYTHON_LASER_H_FOR_NANNOU -- doesn't work?
|
||||
let s = 0xFFF as f32 / 2.; // when using TMP_PYTHON_LASER_H
|
||||
let normalised_pos: [f32;2] = [new_position[0]/s - 1., new_position[1]/s - 1.];
|
||||
laser::Point {
|
||||
position: new_position,
|
||||
position: normalised_pos,
|
||||
.. point.clone()
|
||||
}
|
||||
|
||||
|
@ -168,32 +131,6 @@ impl Filter for HomographyFilter {
|
|||
space: CoordinateSpace::Laser
|
||||
}
|
||||
}
|
||||
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
let space = points.space;
|
||||
let inv_matrix = self.homography_matrix.inverse();
|
||||
|
||||
let projected_positions: Vec<laser::Point> = points.points.iter().map(|point| {
|
||||
let p = point.position;
|
||||
// let s = 0xFFF as f32 / 2.;
|
||||
// let de_normalised_position: [f32;2] = [(p[0] + 1.)*s, (p[1]+1.)*s];
|
||||
let new_position = match space {
|
||||
CoordinateSpace::World => p,
|
||||
CoordinateSpace::Laser => apply_homography_matrix(inv_matrix, &p),
|
||||
_ => panic!("Invalid coordinate space"),
|
||||
|
||||
};
|
||||
// also converts from world space to laser space (origin in center)
|
||||
laser::Point {
|
||||
position: new_position,
|
||||
.. point.clone()
|
||||
}
|
||||
}).collect();
|
||||
LaserPoints{
|
||||
points: projected_positions,
|
||||
space: CoordinateSpace::World
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HomographyFilter{
|
||||
|
@ -329,92 +266,40 @@ impl Filter for CropFilter {
|
|||
space
|
||||
}
|
||||
}
|
||||
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
// we cannot really conjure up points, can we
|
||||
return LaserPoints{
|
||||
points: points.points.clone(),
|
||||
space: points.space,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Filter for ClipFilter {
|
||||
|
||||
fn apply(&self, points: &LaserPoints) -> LaserPoints {
|
||||
if !self.enabled {
|
||||
// don't modify if disabled
|
||||
return LaserPoints{
|
||||
points: points.points.clone(),
|
||||
space: points.space,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
clip_lines(&self.mask, points)
|
||||
|
||||
}
|
||||
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
// we cannot really conjure up points, can we
|
||||
return LaserPoints{
|
||||
points: points.points.clone(),
|
||||
space: points.space,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn change_brightness(points: &LaserPoints, intensity: f32) -> LaserPoints{
|
||||
let new_points = points.points.iter().map(|point| {
|
||||
let mut color = point.color.clone();
|
||||
if intensity != 1.0 {
|
||||
color[0] *= intensity;
|
||||
color[1] *= intensity;
|
||||
color[2] *= intensity;
|
||||
}
|
||||
Point::new(point.position, color)
|
||||
}).collect();
|
||||
LaserPoints {
|
||||
points: new_points,
|
||||
space: points.space
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for DimFilter {
|
||||
fn apply(&self, points: &LaserPoints) -> LaserPoints {
|
||||
change_brightness(points, self.intensity)
|
||||
}
|
||||
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
change_brightness(points, 1./self.intensity)
|
||||
}
|
||||
}
|
||||
|
||||
fn scale(points: &LaserPoints, factor: f32) -> LaserPoints{
|
||||
let new_points = points.points.iter().map(|point| {
|
||||
let mut position = point.position.clone();
|
||||
if factor != 1.0 {
|
||||
position[0] *= factor;
|
||||
position[1] *= factor;
|
||||
let new_points = points.points.iter().map(|point| {
|
||||
let mut color = point.color.clone();
|
||||
if self.intensity != 1.0 {
|
||||
color[0] *= self.intensity;
|
||||
color[1] *= self.intensity;
|
||||
color[2] *= self.intensity;
|
||||
}
|
||||
Point::new(point.position, color)
|
||||
}).collect();
|
||||
LaserPoints {
|
||||
points: new_points,
|
||||
space: points.space
|
||||
}
|
||||
Point::new(position, point.color)
|
||||
}).collect();
|
||||
LaserPoints {
|
||||
points: new_points,
|
||||
space: points.space
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for ScaleFilter {
|
||||
fn apply(&self, points: &LaserPoints) -> LaserPoints {
|
||||
scale(points, self.factor)
|
||||
}
|
||||
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
scale(points, 1./self.factor)
|
||||
let new_points = points.points.iter().map(|point| {
|
||||
let mut position = point.position.clone();
|
||||
if self.factor != 1.0 {
|
||||
position[0] *= self.factor;
|
||||
position[1] *= self.factor;
|
||||
}
|
||||
Point::new(position, point.color)
|
||||
}).collect();
|
||||
LaserPoints {
|
||||
points: new_points,
|
||||
space: points.space
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -425,6 +310,8 @@ impl Filter for PincushionFilter {
|
|||
// becomes trivial
|
||||
fn apply(&self, points: &LaserPoints) -> LaserPoints{
|
||||
let space = points.space;
|
||||
// dbg!(&space);
|
||||
// assert!(!matches!(space, CoordinateSpace::Laser));
|
||||
|
||||
let projected_positions: Vec<laser::Point> = points.points.iter().map(|point| {
|
||||
let p = point.position;
|
||||
|
@ -453,33 +340,4 @@ impl Filter for PincushionFilter {
|
|||
space
|
||||
}
|
||||
}
|
||||
|
||||
// should be a reversal of what apply() does. TODO: check if the below actually
|
||||
// checks out. It might need to be slightly different, in particular, radius calculation
|
||||
// as an approximate, it might do
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
let space = points.space;
|
||||
|
||||
let projected_positions: Vec<laser::Point> = points.points.iter().map(|point| {
|
||||
let p = point.position;
|
||||
|
||||
let radius = (p[0].powi(2) + p[1].powi(2)).sqrt();
|
||||
|
||||
let new_position = [
|
||||
p[0] / (1. + self.k_x * radius.powi(2)+ self.k_x2 * radius.powi(4)),
|
||||
p[1] / (1. + self.k_y * radius.powi(2)+ self.k_y2 * radius.powi(4))
|
||||
];
|
||||
|
||||
laser::Point {
|
||||
position: new_position,
|
||||
.. point.clone()
|
||||
}
|
||||
|
||||
}).collect();
|
||||
|
||||
LaserPoints{
|
||||
points: projected_positions,
|
||||
space
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,39 +7,11 @@ use crate::trap::{filters::{PointFilters}, tracks::CoordinateSpace};
|
|||
use super::tracks::{RenderableLines};
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LaserPoints{
|
||||
pub points: Vec<laser::Point>,
|
||||
pub space: CoordinateSpace
|
||||
}
|
||||
|
||||
impl From<Vec<[f32;2]>> for LaserPoints{
|
||||
|
||||
// assumes input is in CoordinateSpace::Laser
|
||||
fn from(input: Vec<[f32;2]>) -> LaserPoints {
|
||||
// let points = Vec::new();
|
||||
let points = input.iter().map(|p| {
|
||||
laser::Point {
|
||||
position: p.clone(),
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
}
|
||||
}).collect();
|
||||
LaserPoints{
|
||||
points,
|
||||
space: CoordinateSpace::Laser
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Into<Vec<[f32;2]>> for LaserPoints{
|
||||
fn into(self) -> Vec<[f32;2]> {
|
||||
// let points = Vec::new();
|
||||
self.points.iter().map(|p| {
|
||||
p.position
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// homography for laserworld in studio
|
||||
pub const TMP_PYTHON_LASER_H: [[f32;3];3] = [[ 2.47442963e+02, -7.01714050e+01, -9.71749119e+01],
|
||||
|
@ -122,7 +94,6 @@ pub struct DacConfig{
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub enum StreamSource {
|
||||
Disabled,
|
||||
CurrentLines,
|
||||
Rectangle,
|
||||
Grid, // lines
|
||||
|
@ -132,12 +103,12 @@ pub enum StreamSource {
|
|||
}
|
||||
|
||||
// usefull to create pull downs with an iterator
|
||||
pub const STREAM_SOURCES: [StreamSource; 5] = [
|
||||
StreamSource::Disabled,
|
||||
pub const STREAM_SOURCES: [StreamSource; 4] = [
|
||||
StreamSource::CurrentLines,
|
||||
StreamSource::Rectangle,
|
||||
StreamSource::Grid,
|
||||
StreamSource::WorldGrid,
|
||||
// StreamSource::Circle,
|
||||
// StreamSource::Spiral
|
||||
];
|
||||
|
||||
|
@ -161,78 +132,49 @@ impl Default for LaserPoints {
|
|||
}
|
||||
}
|
||||
|
||||
// because code is a mess, old homography assumed conversion to 0xFFF (10bit range) as
|
||||
// accepted by Python helios code. Nannuo uses the relative -1..1 coordinate system
|
||||
// coordinates are converted behind the scenes.
|
||||
// to derpecate this, the homography filter needs to be adapted
|
||||
pub enum LaserSpace {
|
||||
OLD,
|
||||
READY,
|
||||
}
|
||||
|
||||
pub fn shape_rect(space: LaserSpace, steps: usize) -> LaserPoints {
|
||||
let offset: f32 = match space { LaserSpace::OLD => 0., _ => 1. };
|
||||
let factor: f32 = match space { LaserSpace::OLD => 1., _ => 2./0xFFF as f32 };
|
||||
let mut points = Vec::new();
|
||||
// let steps: usize = 10;
|
||||
for i in (0..=steps).rev() {
|
||||
points.push(laser::Point{
|
||||
position:[0xFFF as f32 * factor - offset, (0xFFF * i / steps) as f32 * factor - offset],
|
||||
color: [0.2,0.2,0.2],
|
||||
weight: 0,
|
||||
});
|
||||
}
|
||||
for i in (0..steps).rev() {
|
||||
points.push(laser::Point{
|
||||
position:[(0xFFF * i / steps) as f32 * factor - offset, 0.0 * factor - offset],
|
||||
color: [0.2,0.2,0.2],
|
||||
weight: 0,
|
||||
});
|
||||
}
|
||||
for i in 0..steps {
|
||||
points.push(laser::Point{
|
||||
position:[0.0 * factor - offset, (0xFFF * i / steps) as f32 * factor - offset],
|
||||
color: [0.2,0.2,0.2],
|
||||
weight: 0,
|
||||
});
|
||||
}
|
||||
for i in 0..=steps {
|
||||
points.push(laser::Point{
|
||||
position:[(0xFFF * i / steps) as f32 * factor - offset , 0xFFF as f32 * factor - offset],
|
||||
color: [0.2,0.2,0.2],
|
||||
weight: 0,
|
||||
});
|
||||
}
|
||||
// dbg!("{:?}", &points);
|
||||
LaserPoints { points, space: CoordinateSpace::Laser }
|
||||
}
|
||||
|
||||
// the different shapes that override the provided lines if needed
|
||||
impl StreamSource{
|
||||
pub fn get_shape(&self, current_lines: LaserPoints) -> LaserPoints {
|
||||
match self {
|
||||
Self::CurrentLines => current_lines,
|
||||
Self::Rectangle => {
|
||||
shape_rect(LaserSpace::READY, 11)
|
||||
},
|
||||
Self::Rectangle => LaserPoints { points: vec!(
|
||||
laser::Point{
|
||||
position:[0xFFF as f32, 0xFFF as f32],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
},
|
||||
laser::Point{
|
||||
position:[0xFFF as f32, 0.0],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
},
|
||||
laser::Point{
|
||||
position:[0.0, 0.0],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
},
|
||||
laser::Point{
|
||||
position:[0.0, 0xFFF as f32],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
},
|
||||
laser::Point{
|
||||
position:[0xFFF as f32, 0xFFF as f32],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
},
|
||||
), space: CoordinateSpace::Laser },
|
||||
Self::Grid => {
|
||||
let lines: usize = 5;
|
||||
let half = (0xFFF / 2) as f32;
|
||||
let mut points = Vec::new();
|
||||
// vertical lines
|
||||
for i in 0..=lines {
|
||||
let offset = if i % 2 == 0 { 0 } else {0xFFF } as f32;
|
||||
let x = i * 0xFFF / lines;
|
||||
for i in (0..=0xFFF).step_by(0xFFF / 5) {
|
||||
points.push(laser::Point{
|
||||
position:[(x as f32 - half) / half, (offset as f32 - half)/half],
|
||||
position:[i as f32, 0.],
|
||||
color: [0., 0., 0.],
|
||||
weight: 0,
|
||||
});
|
||||
for j in (0..=0xFFF).step_by(0xFFF / 10) {
|
||||
// go back and forth, so galvo has it easier
|
||||
let y = if i % 2 == 0 { j } else {0xFFF - j};
|
||||
points.push(laser::Point{
|
||||
position:[(x as f32 - half)/half, (y as f32 - half)/half],
|
||||
position:[i as f32, j as f32],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
});
|
||||
|
@ -240,19 +182,15 @@ impl StreamSource{
|
|||
points.push(points[points.len()-1].blanked());
|
||||
}
|
||||
|
||||
for i in 0..=lines {
|
||||
let offset = if i % 2 == 0 { 0 } else {0xFFF } as f32;
|
||||
let y = i * 0xFFF / lines;
|
||||
for i in (0..=0xFFF).step_by(0xFFF / 5) {
|
||||
points.push(laser::Point{
|
||||
position:[(offset as f32 - half)/half, (y as f32 - half)/half],
|
||||
position:[0., i as f32],
|
||||
color: [0., 0., 0.],
|
||||
weight: 0,
|
||||
});
|
||||
for j in (0..=0xFFF).step_by(0xFFF / 10) {
|
||||
// go back and forth, so galvo has it easier
|
||||
let x = if i % 2 == 0 { j } else {0xFFF - j};
|
||||
points.push(laser::Point{
|
||||
position:[(x as f32 - half)/half, (y as f32 - half)/half],
|
||||
position:[j as f32, i as f32],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
});
|
||||
|
@ -303,31 +241,7 @@ impl StreamSource{
|
|||
|
||||
LaserPoints { points, space: CoordinateSpace::World }
|
||||
},
|
||||
StreamSource::Disabled => LaserPoints::default(), // empty set
|
||||
_ => LaserPoints::default(), // empty set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Corner{
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
|
||||
impl Corner {
|
||||
pub fn in_laser_space() -> Vec<[f32; 2]>{
|
||||
vec!([-1.,1.], [1.,1.], [1., -1.], [-1.,-1.])
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
match self {
|
||||
Self::TopLeft => 0,
|
||||
Self::TopRight => 1,
|
||||
Self::BottomRight => 2,
|
||||
Self::BottomLeft => 3,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,5 +10,4 @@ pub mod tracks;
|
|||
pub mod shapes;
|
||||
|
||||
pub mod laser;
|
||||
pub mod filters;
|
||||
pub mod utils;
|
||||
pub mod filters;
|
|
@ -1,199 +0,0 @@
|
|||
// GPT generated code is isolated to this file
|
||||
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
|
||||
///////////////////////
|
||||
|
||||
// find distance of edge do point (used by GUI)
|
||||
pub fn point_to_segment_distance_squared(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
|
||||
let ab = [b[0] - a[0], b[1] - a[1]];
|
||||
let ap = [p[0] - a[0], p[1] - a[1]];
|
||||
let ab_len_sq = ab[0] * ab[0] + ab[1] * ab[1];
|
||||
|
||||
if ab_len_sq == 0.0 {
|
||||
// Edge is a single point
|
||||
return ap[0] * ap[0] + ap[1] * ap[1];
|
||||
}
|
||||
|
||||
// Project point onto segment, clamped to [0,1]
|
||||
let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / ab_len_sq).clamp(0.0, 1.0);
|
||||
let closest = [a[0] + t * ab[0], a[1] + t * ab[1]];
|
||||
|
||||
let dx = p[0] - closest[0];
|
||||
let dy = p[1] - closest[1];
|
||||
dx * dx + dy * dy
|
||||
}
|
||||
|
||||
|
||||
// find distance of edge do point (used by GUI)
|
||||
pub fn closest_edge(mask: &[[f32; 2]], target: [f32; 2]) -> Option<usize> {
|
||||
if mask.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut min_dist_sq = f32::MAX;
|
||||
let mut closest_edge_index = 0;
|
||||
|
||||
for i in 0..mask.len() {
|
||||
let a = mask[i];
|
||||
let b = mask[(i + 1) % mask.len()];
|
||||
let dist_sq = point_to_segment_distance_squared(target, a, b);
|
||||
|
||||
if dist_sq < min_dist_sq {
|
||||
min_dist_sq = dist_sq;
|
||||
closest_edge_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
Some(closest_edge_index)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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<f32>, p2: Coord<f32>, t: f32) -> Coord<f32> {
|
||||
Coord {
|
||||
x: p1.x + (p2.x - p1.x) * t,
|
||||
y: p1.y + (p2.y - p1.y) * t,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn clip_colored_path(points: &Vec<laser::Point>, bounds: &Polygon<f32>) -> Vec<laser::Point> {
|
||||
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<f32>, polygon: &Polygon<f32>) -> Option<Coord<f32>> {
|
||||
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>) -> 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<laser::Point>) -> Vec<Vec<laser::Point>> {
|
||||
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
|
||||
}
|
Loading…
Reference in a new issue