Compare commits
26 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9c178f8db1 | ||
![]() |
df7f0f26df | ||
![]() |
0b9d4d12f7 | ||
![]() |
fbfea9d575 | ||
![]() |
32590c201e | ||
![]() |
1b8882b1c7 | ||
![]() |
dbb31c5bcb | ||
![]() |
c1f0f021a9 | ||
![]() |
744a3b3dfa | ||
![]() |
ad9c8d6d37 | ||
![]() |
fa3dc1d104 | ||
![]() |
e3fe78ec5c | ||
![]() |
9ef4c6745d | ||
![]() |
65acde713f | ||
![]() |
59788d5363 | ||
![]() |
d2fa6e007c | ||
![]() |
8cc79b24a7 | ||
![]() |
3a87be73f9 | ||
![]() |
f78fc95c50 | ||
![]() |
82b69bd6b9 | ||
![]() |
e554dc30d0 | ||
![]() |
f63b70d218 | ||
![]() |
308697fe6a | ||
![]() |
13c8628590 | ||
![]() |
319062b0b4 | ||
![]() |
a364a9daad |
17 changed files with 2702 additions and 1353 deletions
847
Cargo.lock
generated
847
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
@ -1,11 +1,14 @@
|
|||
[package]
|
||||
name = "trap_rust"
|
||||
name = "laserspace"
|
||||
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", 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"] }
|
||||
iyes_perf_ui = "0.4.0"
|
||||
|
||||
nannou_laser = { git = "https://github.com/rubenvandeven/nannou", branch = "helios_laser_DAC" }
|
||||
|
@ -19,6 +22,12 @@ 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]
|
||||
|
||||
|
||||
|
@ -28,5 +37,5 @@ serde_repr = "0.1.20"
|
|||
opt-level = 3
|
||||
|
||||
[[bin]]
|
||||
name="renderer"
|
||||
name="laserspace"
|
||||
path="src/main.rs"
|
||||
|
|
36
README.md
Normal file
36
README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
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
|
||||
```
|
BIN
assets/screenshots/laserspace-demo.mp4
Normal file
BIN
assets/screenshots/laserspace-demo.mp4
Normal file
Binary file not shown.
BIN
assets/screenshots/laserspace_screenshot_01.png
Normal file
BIN
assets/screenshots/laserspace_screenshot_01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
assets/screenshots/laserspace_screenshot_02.png
Normal file
BIN
assets/screenshots/laserspace_screenshot_02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
assets/screenshots/laserspace_screenshot_03.png
Normal file
BIN
assets/screenshots/laserspace_screenshot_03.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
217
examples/bevy_gui.rs
Normal file
217
examples/bevy_gui.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
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 trap_rust::trap::laser::{self, apply_homography_matrix};
|
||||
use laserspace::trap::laser::{self, apply_homography_matrix};
|
||||
|
||||
/*
|
||||
Compare output with the following python
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use serde_json::Result;
|
||||
use trap_rust::trap::tracks::RenderableLines;
|
||||
use laserspace::trap::tracks::RenderableLines;
|
||||
|
||||
/*
|
||||
Compare output with the following python
|
||||
|
|
|
@ -1,852 +0,0 @@
|
|||
//! 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 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 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 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 => trap_rust::trap::shapes::YOUR_FUTURE,
|
||||
false => trap_rust::trap::shapes::ARE_YOU_SURE,
|
||||
true => laserspace::trap::shapes::YOUR_FUTURE,
|
||||
false => laserspace::trap::shapes::ARE_YOU_SURE,
|
||||
};
|
||||
let points = positions.iter().cloned().map(text2points).collect();
|
||||
return points
|
||||
|
|
1486
src/main.rs
1486
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, 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};
|
||||
|
@ -9,6 +9,7 @@ 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)]
|
||||
|
@ -21,6 +22,12 @@ 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
|
||||
|
@ -58,6 +65,7 @@ pub struct PointFilters{
|
|||
pub scale: ScaleFilter,
|
||||
pub pincushion: PincushionFilter,
|
||||
pub crop: CropFilter,
|
||||
pub clip: ClipFilter,
|
||||
}
|
||||
|
||||
// list of enums deprecated in favour of struct
|
||||
|
@ -79,8 +87,37 @@ 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -99,6 +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: true, mask: Corner::in_laser_space() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,12 +154,11 @@ impl Filter for HomographyFilter {
|
|||
_ => panic!("Invalid coordinate space"),
|
||||
|
||||
};
|
||||
// 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.];
|
||||
// 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.];
|
||||
laser::Point {
|
||||
position: normalised_pos,
|
||||
position: new_position,
|
||||
.. point.clone()
|
||||
}
|
||||
|
||||
|
@ -131,6 +168,32 @@ 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{
|
||||
|
@ -266,40 +329,92 @@ 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 {
|
||||
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
|
||||
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;
|
||||
}
|
||||
Point::new(position, point.color)
|
||||
}).collect();
|
||||
LaserPoints {
|
||||
points: new_points,
|
||||
space: points.space
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for ScaleFilter {
|
||||
fn apply(&self, points: &LaserPoints) -> LaserPoints {
|
||||
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
|
||||
}
|
||||
scale(points, self.factor)
|
||||
}
|
||||
|
||||
fn reverse(&self, points: &LaserPoints) -> LaserPoints{
|
||||
scale(points, 1./self.factor)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,8 +425,6 @@ 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;
|
||||
|
@ -340,4 +453,33 @@ 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,11 +7,39 @@ 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],
|
||||
|
@ -94,6 +122,7 @@ pub struct DacConfig{
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub enum StreamSource {
|
||||
Disabled,
|
||||
CurrentLines,
|
||||
Rectangle,
|
||||
Grid, // lines
|
||||
|
@ -103,12 +132,12 @@ pub enum StreamSource {
|
|||
}
|
||||
|
||||
// usefull to create pull downs with an iterator
|
||||
pub const STREAM_SOURCES: [StreamSource; 4] = [
|
||||
pub const STREAM_SOURCES: [StreamSource; 5] = [
|
||||
StreamSource::Disabled,
|
||||
StreamSource::CurrentLines,
|
||||
StreamSource::Rectangle,
|
||||
StreamSource::Grid,
|
||||
StreamSource::WorldGrid,
|
||||
// StreamSource::Circle,
|
||||
// StreamSource::Spiral
|
||||
];
|
||||
|
||||
|
@ -132,49 +161,78 @@ 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 => 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::Rectangle => {
|
||||
shape_rect(LaserSpace::READY, 11)
|
||||
},
|
||||
Self::Grid => {
|
||||
let lines: usize = 5;
|
||||
let half = (0xFFF / 2) as f32;
|
||||
let mut points = Vec::new();
|
||||
for i in (0..=0xFFF).step_by(0xFFF / 5) {
|
||||
// vertical lines
|
||||
for i in 0..=lines {
|
||||
let offset = if i % 2 == 0 { 0 } else {0xFFF } as f32;
|
||||
let x = i * 0xFFF / lines;
|
||||
points.push(laser::Point{
|
||||
position:[i as f32, 0.],
|
||||
position:[(x as f32 - half) / half, (offset as f32 - half)/half],
|
||||
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:[i as f32, j as f32],
|
||||
position:[(x as f32 - half)/half, (y as f32 - half)/half],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
});
|
||||
|
@ -182,15 +240,19 @@ impl StreamSource{
|
|||
points.push(points[points.len()-1].blanked());
|
||||
}
|
||||
|
||||
for i in (0..=0xFFF).step_by(0xFFF / 5) {
|
||||
for i in 0..=lines {
|
||||
let offset = if i % 2 == 0 { 0 } else {0xFFF } as f32;
|
||||
let y = i * 0xFFF / lines;
|
||||
points.push(laser::Point{
|
||||
position:[0., i as f32],
|
||||
position:[(offset as f32 - half)/half, (y as f32 - half)/half],
|
||||
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:[j as f32, i as f32],
|
||||
position:[(x as f32 - half)/half, (y as f32 - half)/half],
|
||||
color: [1.,1.,1.],
|
||||
weight: 0,
|
||||
});
|
||||
|
@ -241,7 +303,31 @@ 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,4 +10,5 @@ pub mod tracks;
|
|||
pub mod shapes;
|
||||
|
||||
pub mod laser;
|
||||
pub mod filters;
|
||||
pub mod filters;
|
||||
pub mod utils;
|
199
src/trap/utils.rs
Normal file
199
src/trap/utils.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
// 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