commit 6818a6949fa5a9d6bddd3c95dd3afdb732196580 Author: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Fri Nov 22 21:08:08 2024 -0700 Add code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3b39c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +debug/ +target/ +dist/ + +Cargo.lock + +**/*.rs.bk + +*.pdb \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..70e28e3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rushroom" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck = "1.20.0" +eframe = { version = "0.29.1", features = [ + "accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies. + "default_fonts", # Embed the default egui fonts. + "glow", # Use the glow rendering backend. Alternative: "wgpu". + "persistence",]} + +egui = { version = "0.29.1", features = ["callstack", "default", "log"] } +glam = "0.29.2" +rand = "0.8.5" +serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.133" diff --git a/layout.json b/layout.json new file mode 100644 index 0000000..ce4ebb0 --- /dev/null +++ b/layout.json @@ -0,0 +1,20 @@ +{ + "root": { + "Group": { + "direction": "Vertical", + "children": [ + { + "Pane": "console" + }, + { + "Pane": "properties" + } + ], + "sizes": [ + 0.5, + 0.5 + ] + } + }, + "windowed_panes": {} +} \ No newline at end of file diff --git a/pane_layout.json b/pane_layout.json new file mode 100644 index 0000000..b6379d6 --- /dev/null +++ b/pane_layout.json @@ -0,0 +1,14 @@ +{ + "Properties": { + "mode": "Hidden", + "position": null, + "size": null, + "order": 1 + }, + "Console": { + "mode": "Tiled", + "position": null, + "size": null, + "order": 0 + } +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..c484d54 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,223 @@ +// use egui::{FontFamily, FontId, RichText, Visuals}; +use eframe::egui_glow; +use std::{sync::Arc, time::Instant}; +use egui::{accesskit::TextAlign, mutex::Mutex, Align2, Color32, FontId, Pos2, Stroke}; +use egui_glow::glow; + +use crate::{panes::Pane, point_cloud_renderer::PointRenderer, PaneManager}; + +/// We derive Deserialize/Serialize so we can persist app state on shutdown. +// #[derive(serde::Deserialize, serde::Serialize)] +// #[serde(default)] // if we add new fields, give them default values when deserializing old state +pub struct App { + // #[serde(skip)] + renderer: Arc>, + points: Vec<(i32, i32, i32, Color32)>, + file_dialog_open: bool, + cur_path: String, +} + + +impl App { + /// Called once before the first frame. + pub fn new(cc: &eframe::CreationContext<'_>) -> Option { + // This is also where you can customize the look and feel of egui using + // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. + + // Load previous app state (if any). + // Note that you must enable the `persistence` feature for this to work. + // if let Some(storage) = cc.storage { + // return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); + // } + + Some(Self { + renderer: Arc::new(Mutex::new(PointRenderer::new(cc.gl.clone(), 1_000_000))), + points: Vec::new(), + file_dialog_open: false, + cur_path: "./".to_string(), + }) + } +} + +impl eframe::App for App { + /// Called each time the UI needs repainting, which may be many times per second. + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. + // For inspiration and more examples, go to https://emilk.github.io/egui + + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + // The top panel is often a good place for a menu bar: + + egui::menu::bar(ui, |ui| { + // NOTE: no File->Quit on web pages! + let is_web = cfg!(target_arch = "wasm32"); + if !is_web { + ui.menu_button("File", |ui| { + if ui.button("Quit").clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + }); + ui.add_space(16.0); + } + + egui::widgets::global_theme_preference_switch(ui); + + if ui.button("Load PLY").clicked() { + self.file_dialog_open = true; + } + }); + }); + + if self.file_dialog_open { + egui::Window::new("Load PLY File") + .show(ctx, |ui| { + ui.label("Enter PLY file path:"); + ui.text_edit_singleline(&mut self.cur_path); // Add proper path handling + + ui.horizontal(|ui| { + if ui.button("Load").clicked() { + let renderer = &mut self.renderer.lock(); + // Add proper path handling and error reporting + let ply = renderer.load_ply(self.cur_path.clone()); + if let Err(e) = ply { + eprintln!("Failed to load PLY: {}", e); + }else{ + // self.renderer.lock().camera.reset(); + self.points = ply.unwrap(); + } + + self.file_dialog_open = false; + } + if ui.button("Cancel").clicked() { + self.file_dialog_open = false; + } + }); + }); + } + + + egui::CentralPanel::default().show(ctx, |ui| { + // egui::scroll_area::ScrollArea::vertical().show(ui, |ui| { + egui::Frame::canvas(ui.style()).show(ui, |ui| { + self.custom_painting(ui.max_rect(), ui); + }); + // }) + }); + + } + + // fn on_exit(&mut self, gl: Option<&glow::Context>) { + // if let Some(gl) = gl { + // self.rotating_triangle.lock().destroy(gl); + // } + // } + + // Called by the frame work to save state before shutdown. + // fn save(&mut self, storage: &mut dyn eframe::Storage) { + // eframe::set_value(storage, eframe::APP_KEY, self); + // } +} + +impl App { + fn custom_painting(&mut self, max_rect: egui::Rect, ui: &mut egui::Ui) { + + let start_time = Instant::now(); + + let (rect, response) = + ui.allocate_exact_size(egui::Vec2 { x: max_rect.width(), y: max_rect.height() }, egui::Sense::drag()); + + let input_state = ui.input(|input_state| {input_state.clone()}); + + // ui.painter();. + + // println!("{}",response.drag_motion().x); + + // let response = Box::new(response); + + // let ui = ui.to_owned(); + + // self.anglex += response.drag_motion().x * 0.01; + // self.angley += response.drag_motion().y * 0.01; + + // Clone locals so we can move them into the paint callback: + if self.points.is_empty() { + let radius = 1000i32; + for i in 0..100000 { + // let theta = (i as f32 * 0.1).sin() * std::f32::consts::PI; + // let phi = (i as f32 * 0.1).cos() * std::f32::consts::PI; + + let x = (radius as f32 * (i as f32).cos()) as i32; + let y = (radius as f32 * (i as f32).sin()) as i32; + let z = (i as f32 * 0.05) as i32; + + // let x = (i as f32 * 0.1) as u32; + // let y = (i as f32 * 0.1) as u32 ; + // let z = (i as f32 * 0.1) as u32; + + // Color based on position + let color = Color32::from_rgba_premultiplied( + ((x as f32 / radius as f32) * 255.0) as u8, + ((y as f32 / radius as f32) * 255.0) as u8, + ((z as f32 / radius as f32) * 255.0) as u8, + 255, + ); + + self.points.push((x, y, z, color)); + } + } + + let renderer = self.renderer.clone(); + renderer.lock().clear(); + + // let painter = ui.painter(); + + for &(x, y, z, color) in &self.points { + renderer.lock().add_point(x, y, z, color); + } + + let o = renderer.lock().camera.orientation.clone(); + + let cb = egui_glow::CallbackFn::new(move |_info, _painter| { + renderer.lock().render(rect, input_state.clone()); + }); + + let callback = egui::PaintCallback { + rect, + callback: Arc::new(cb), + }; + + ui.painter().add(callback); + + let pos1 = o.inverse()*glam::Vec3::X; + let pos2 = o.inverse()*glam::Vec3::Y; + let pos3 = o.inverse()*glam::Vec3::Z; + + let line_length:f32 = 20.; + + ui.painter().line_segment([rect.center(), rect.center() + egui::Vec2{ x: line_length*pos1.x, y: -line_length*pos1.y,}], Stroke { + width: 1.5, + color: Color32::RED, + }); + + + ui.painter().line_segment([rect.center(), rect.center() + egui::Vec2{ x: line_length*pos2.x, y: -line_length*pos2.y,}], Stroke { + width: 1.5, + color: Color32::BLUE, + }); + + + ui.painter().line_segment([rect.center(), rect.center() + egui::Vec2{ x: line_length*pos3.x, y: -line_length*pos3.y,}], Stroke { + width: 1.5, + color: Color32::GREEN, + }); + + let end_time = Instant::now(); + + println!("{}", end_time.duration_since(start_time).as_millis()); + + ui.painter().text(Pos2 {x:0.,y:0.}, Align2::LEFT_TOP, format!("{}",end_time.duration_since(start_time).as_millis()), FontId::monospace(12.), Color32::WHITE); + + + + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4d3c4aa --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(clippy::all, rust_2018_idioms)] +mod app; +mod point_cloud_renderer; +mod panes; + +pub use app::App; +pub use panes::PaneManager; +// pub use point_cloud_renderer::PointCloudApp; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2f67b4c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,78 @@ +#![warn(clippy::all, rust_2018_idioms)] +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use rushroom::App; +use rushroom::PaneManager; + +// When compiling natively: +#[cfg(not(target_arch = "wasm32"))] +fn main() -> eframe::Result { + // env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([400.0, 300.0]) + .with_min_inner_size([300.0, 220.0]), + depth_buffer: 24, + // .with_icon( + // // NOTE: Adding an icon is optional + // eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..]) + // .expect("Failed to load icon"), + // ), + ..Default::default() + }; + eframe::run_native( + "eframe template", + native_options, + // Box::new(|_cc| Box::new(PaneManager::default())), + // Box::new(|cc| Ok(Box::new(PaneManager::new()))), + Box::new(|cc| Ok(Box::new(App::new(cc).unwrap()))), + ) +} + +// When compiling to web using trunk: +#[cfg(target_arch = "wasm32")] +fn main() { + use eframe::wasm_bindgen::JsCast as _; + + // Redirect `log` message to `console.log` and friends: + eframe::WebLogger::init(log::LevelFilter::Debug).ok(); + + let web_options = eframe::WebOptions::default(); + + wasm_bindgen_futures::spawn_local(async { + let document = web_sys::window() + .expect("No window") + .document() + .expect("No document"); + + let canvas = document + .get_element_by_id("the_canvas_id") + .expect("Failed to find the_canvas_id") + .dyn_into::() + .expect("the_canvas_id was not a HtmlCanvasElement"); + + let start_result = eframe::WebRunner::new() + .start( + canvas, + web_options, + Box::new(|cc| Ok(Box::new(App::new(cc)))), + ) + .await; + + // Remove the loading text and spinner: + if let Some(loading_text) = document.get_element_by_id("loading_text") { + match start_result { + Ok(_) => { + loading_text.remove(); + } + Err(e) => { + loading_text.set_inner_html( + "

The app has crashed. See the developer console for details.

", + ); + panic!("Failed to start eframe: {e:?}"); + } + } + } + }); +} diff --git a/src/panes.rs b/src/panes.rs new file mode 100644 index 0000000..75ccaaa --- /dev/null +++ b/src/panes.rs @@ -0,0 +1,438 @@ +use eframe::egui; +use egui::Stroke; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use egui::Color32; + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +enum PaneMode { + Hidden, + Tiled, + Windowed, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +enum SplitDirection { + Horizontal, + Vertical, +} + +#[derive(Serialize, Deserialize, Clone)] +struct GroupNode { + direction: SplitDirection, + children: Vec, + sizes: Vec, // Stores the relative sizes of children +} + +#[derive(Serialize, Deserialize, Clone)] +enum LayoutNode { + Pane(String), // Stores pane identifier + Group(GroupNode), +} + +#[derive(Serialize, Deserialize, Clone)] +struct LayoutConfig { + root: Option, + windowed_panes: HashMap, +} + +#[derive(Serialize, Deserialize, Clone)] +struct WindowedPaneConfig { + position: [f32; 2], + size: [f32; 2], +} + +pub trait Pane { + fn name(&self) -> &str; + fn show(&mut self, ui: &mut egui::Ui); + fn default_size(&self) -> egui::Vec2; +} + +// Example panes implementation +struct ConsolePane { + content: String, +} + +impl Pane for ConsolePane { + fn name(&self) -> &str { "Console" } + fn show(&mut self, ui: &mut egui::Ui) { + ui.painter().rect(ui.max_rect(), 0., Color32::RED, Stroke::NONE); + ui.text_edit_multiline(&mut self.content); + } + fn default_size(&self) -> egui::Vec2 { egui::vec2(300.0, 200.0) } +} + +struct PropertiesPane { + value: f32, +} + +impl Pane for PropertiesPane { + fn name(&self) -> &str { "Properties" } + fn show(&mut self, ui: &mut egui::Ui) { + ui.painter().rect(ui.max_rect(), 0., Color32::BLUE, Stroke::NONE); + ui.add(egui::Slider::new(&mut self.value, 0.0..=100.0)); + } + fn default_size(&self) -> egui::Vec2 { egui::vec2(200.0, 300.0) } +} + +struct DragState { + dragged_pane: String, + original_group_path: Vec, + drag_started: bool, + drop_target: Option, +} + +#[derive(Clone)] +struct DropTarget { + target_pane: String, + group_path: Vec, + position: DropPosition, +} + +#[derive(Clone, PartialEq)] +enum DropPosition { + Above, + Below, + Left, + Right, + Center, +} + +pub struct PaneManager { + panes: HashMap>, + layout: LayoutConfig, + drag_state: Option, +} + +impl PaneManager { + pub fn new() -> Self { + let mut panes: HashMap> = HashMap::new(); + panes.insert("properties".to_string(), Box::new(PropertiesPane { value: 50.0 })); + panes.insert("console".to_string(), Box::new(ConsolePane { content: String::new() })); + + // Create initial layout + let initial_group = GroupNode { + direction: SplitDirection::Vertical, + children: vec![ + LayoutNode::Pane("console".to_string()), + LayoutNode::Pane("properties".to_string()), + ], + sizes: vec![0.5, 0.5], + }; + + let layout = LayoutConfig { + root: Some(LayoutNode::Group(initial_group)), + windowed_panes: HashMap::new(), + }; + + Self { + panes, + layout, + drag_state: None, + } + } + + fn save_layout(&self) { + if let Ok(json) = serde_json::to_string_pretty(&self.layout) { + let _ = fs::write("layout.json", json); + } + } + + fn load_layout(&mut self) { + if let Ok(contents) = fs::read_to_string("layout.json") { + if let Ok(layout) = serde_json::from_str(&contents) { + self.layout = layout; + } + } + } + + fn handle_drop(&mut self) { + if let Some(drag_state) = &self.drag_state { + if let Some(drop_target) = &drag_state.drop_target { + let mut new_layout = self.layout.clone(); + + // Remove dragged pane from original location + self.remove_pane_from_path(&mut new_layout.root, &drag_state.original_group_path, &drag_state.dragged_pane); + + // Insert at new location based on drop position + self.insert_pane_at_target( + &mut new_layout.root, + &drop_target.group_path, + &drop_target.target_pane, + &drag_state.dragged_pane, + &drop_target.position, + ); + + self.layout = new_layout; + } + } + self.drag_state = None; + } + + fn remove_pane_from_path( + &self, + node: &mut Option, + path: &[usize], + pane_id: &str, + ) -> bool { + if path.is_empty() { + return false; + } + + if let Some(LayoutNode::Group(group)) = node { + if path.len() == 1 { + if let Some(idx) = group.children.iter().position(|child| { + matches!(child, LayoutNode::Pane(id) if id == pane_id) + }) { + group.children.remove(idx); + group.sizes.remove(idx); + return true; + } + } else if path[0] < group.children.len() { + return self.remove_pane_from_path( + &mut Some(group.children[path[0]].clone()), + &path[1..], + pane_id, + ); + } + } + false + } + + fn insert_pane_at_target( + &self, + node: &mut Option, + path: &[usize], + target_pane: &str, + dragged_pane: &str, + position: &DropPosition, + ) { + if let Some(LayoutNode::Group(group)) = node { + if path.len() == 1 { + let target_idx = group.children.iter().position(|child| { + matches!(child, LayoutNode::Pane(id) if id == target_pane) + }).unwrap(); + // if target_idx.is_none(){ return; } + // target_idx = target_idx.unwrap(); + // target_idx = target_idx.unwrap(); + + match (position, &group.direction) { + (DropPosition::Above | DropPosition::Below, SplitDirection::Vertical) + | (DropPosition::Left | DropPosition::Right, SplitDirection::Horizontal) => { + // Insert in same group + let insert_idx = if matches!(position, DropPosition::Below | DropPosition::Right) { + target_idx + 1 + } else { + target_idx + }; + group.children.insert(insert_idx, LayoutNode::Pane(dragged_pane.to_string())); + group.sizes.insert(insert_idx, 1.0 / (group.sizes.len() + 1) as f32); + // Normalize sizes + let total: f32 = group.sizes.iter().sum(); + for size in &mut group.sizes { + *size /= total; + } + } + _ => { + // Create new group + let new_direction = if matches!(position, DropPosition::Above | DropPosition::Below) { + SplitDirection::Vertical + } else { + SplitDirection::Horizontal + }; + + let mut new_group = GroupNode { + direction: new_direction, + children: vec![], + sizes: vec![], + }; + + if matches!(position, DropPosition::Above | DropPosition::Left) { + new_group.children.push(LayoutNode::Pane(dragged_pane.to_string())); + new_group.children.push(LayoutNode::Pane(target_pane.to_string())); + } else { + new_group.children.push(LayoutNode::Pane(target_pane.to_string())); + new_group.children.push(LayoutNode::Pane(dragged_pane.to_string())); + } + new_group.sizes = vec![0.5, 0.5]; + + group.children[target_idx] = LayoutNode::Group(new_group); + } + } + } else if path[0] < group.children.len() { + self.insert_pane_at_target( + &mut Some(group.children[path[0]].clone()), + &path[1..], + target_pane, + dragged_pane, + position, + ); + } + } + } + + fn show_group( + &mut self, + ui: &mut egui::Ui, + group: &GroupNode, + path: &mut Vec, + rect: egui::Rect, + ) { + let mut current_offset = if group.direction == SplitDirection::Horizontal { + rect.left() + } else { + rect.top() + }; + + for (idx, (child, &size)) in group.children.iter().zip(group.sizes.iter()).enumerate() { + path.push(idx); + + let child_size = if group.direction == SplitDirection::Horizontal { + size * rect.width() + } else { + size * rect.height() + }; + + let child_rect = if group.direction == SplitDirection::Horizontal { + egui::Rect::from_min_size( + egui::pos2(current_offset, rect.top()), + egui::vec2(child_size, rect.height()), + ) + } else { + egui::Rect::from_min_size( + egui::pos2(rect.left(), current_offset), + egui::vec2(rect.width(), child_size), + ) + }; + + match child { + LayoutNode::Pane(id) => { + // let pane = self.panes.get_mut(id).unwrap(); + // if !pane.is_none(){ + self.show_pane(ui, id, child_rect, path.clone()); + // } + } + LayoutNode::Group(child_group) => { + self.show_group(ui, child_group, path, child_rect); + } + } + + current_offset += child_size; + path.pop(); + } + } + + fn show_pane( + &mut self, + ui: &mut egui::Ui, + id: &String, + // pane: &mut Box, + rect: egui::Rect, + path: Vec, + ) { + let pane = self.panes.get_mut(id).unwrap(); + let response = ui.allocate_rect(rect, egui::Sense::drag()); + + if response.dragged() { + if self.drag_state.is_none() { + self.drag_state = Some(DragState { + dragged_pane: pane.name().to_string(), + original_group_path: path.clone(), + drag_started: true, + drop_target: None, + }); + } + } + + // Handle drop targeting + if let Some(drag_state) = &mut self.drag_state { + if drag_state.dragged_pane != pane.name() { + let hover_pos: Option = ui.input(|i| {i.pointer.hover_pos().clone()}); + if let Some(pos) = hover_pos { + if rect.contains(pos) { + let relative_pos = (pos - rect.min) / rect.size(); + let position = if relative_pos.y < 0.25 { + DropPosition::Above + } else if relative_pos.y > 0.75 { + DropPosition::Below + } else if relative_pos.x < 0.25 { + DropPosition::Left + } else if relative_pos.x > 0.75 { + DropPosition::Right + } else { + DropPosition::Center + }; + + drag_state.drop_target = Some(DropTarget { + target_pane: pane.name().to_string(), + group_path: path, + position, + }); + } + } + } + } + + // Show the actual pane content + let frame = egui::Frame::none() + .fill(ui.style().visuals.window_fill) + .stroke(ui.style().visuals.window_stroke) + .inner_margin(egui::Margin::same(4.0)); + + frame.show(ui, |ui| { + ui.set_min_size(rect.size()); + pane.show(ui); + }); + } + + // fn is_valid_group +} + +impl eframe::App for PaneManager { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::TopBottomPanel::top("toolbar").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.menu_button("File", |ui| { + if ui.button("Save Layout").clicked() { + self.save_layout(); + ui.close_menu(); + } + if ui.button("Load Layout").clicked() { + self.load_layout(); + ui.close_menu(); + } + }); + }); + }); + + let any_down = ctx.input(|i| {i.pointer.any_down().clone()}); + if !any_down {// && drag_state.drag_started { + self.handle_drop(); + } + + egui::CentralPanel::default().show(ctx, |ui| { + + let mut root_group = if let LayoutNode::Group(c) = as Clone>::clone(&self.layout.root).unwrap() {c} else { unreachable!() }; + + // if let Some(LayoutNode::Group(root_group)) = &self.layout.root { + + + + // // let root = LayoutNode::Group(&mut self.layout.root.unwrap()); + + // // if LayoutNode::Group(root_group).unwrap() + + + + let mut path = Vec::new(); + self.show_group(ui, &root_group, &mut path, ui.available_rect_before_wrap()); + + }); + + // Handle drag end + // if let Some(drag_state) = &self.drag_state { + + // } + } +} diff --git a/src/point_cloud_renderer.rs b/src/point_cloud_renderer.rs new file mode 100644 index 0000000..cb76d6f --- /dev/null +++ b/src/point_cloud_renderer.rs @@ -0,0 +1,448 @@ +use egui::{vec2, Color32, InputState, Rect, Response}; +use eframe::egui_glow; +use egui_glow::glow; +use std::sync::Arc; +use glam::{Vec3, Mat4, Quat, Vec2}; +use std::fs::File; +use std::path::Path; +use std::io::{BufReader, BufRead}; + +// Shader sources updated for 3D rendering with fixed-point positions +const VERTEX_SHADER: &str = r#" + #version 330 core + layout (location = 0) in ivec3 position; // Using unsigned ints for position + layout (location = 1) in ivec4 color; // Using unsigned ints for color + + uniform mat4 u_view_projection; + uniform float u_position_scale; // Scale factor to convert from uint to world space + uniform float u_point_size_scale; // Added point size scaling + + out vec4 v_color; + + void main() { + // Convert uint positions to world space + vec3 worldPos = vec3(position) * u_position_scale; + gl_Position = u_view_projection * vec4(worldPos, 1.0); + gl_PointSize = max(u_point_size_scale * 10.0 * (1.0 - gl_Position.z / gl_Position.w), 1.0); + v_color = vec4(color) / 255.0; // Convert uint colors to float + } +"#; + +const FRAGMENT_SHADER: &str = r#" + #version 330 core + in vec4 v_color; + out vec4 FragColor; + + void main() { + // Create circular points + vec2 coord = gl_PointCoord * 2.0 - 1.0; + float r = dot(coord, coord); + if (r > 1.0) discard; + // if (coord.x > 1.0) discard; + // if (coord.y > 1.0) discard; + + // Apply simple lighting based on depth + // float depth = gl_FragCoord.z; + FragColor = v_color; + } +"#; + +// Camera controller for 3D navigation +pub struct Camera { + position: Vec3, + pub orientation: Quat, + distance: f32, + pub point_size_scale: f32, +} + + +impl Camera { + pub fn new() -> Self { + Self { + position: Vec3::new(0.0, 0.0, 5.0), + orientation: Quat::IDENTITY, + distance: 5.0, + point_size_scale: 0.1, + } + } + + pub fn reset(&mut self) { + self.position = Vec3::new(0.0, 0.0, 5.0); + self.orientation = Quat::IDENTITY; + self.distance = 5.0; + // self.point_size_scale = 0.1; + self.update_view(); + } + + pub fn update(&mut self, i: InputState) { + // let response = { + // Mouse controls + let mut changed = false; + + // Right mouse button for rotation + if i.pointer.secondary_down() { + let delta = i.pointer.delta(); + + let rotation_speed = 0.01; + let pitch = delta.y * rotation_speed; + let yaw = delta.x * rotation_speed; + + let pitch_rotation = Quat::from_axis_angle(Vec3::X, -pitch); + let yaw_rotation = Quat::from_axis_angle(Vec3::Y, -yaw); + let roll_rotation = Quat::from_axis_angle(Vec3::Z, 0.); + + self.orientation = self.orientation * pitch_rotation * yaw_rotation * roll_rotation; + self.orientation = self.orientation.normalize(); + + changed = true; + } + + // Scroll for zoom\ + let zoom_delta = i.smooth_scroll_delta.x + i.smooth_scroll_delta.y; + if zoom_delta != 0. { + if i.modifiers.shift { + // self.point_size_scale = (self.point_size_scale * (1. - zoom_delta * 0.001)); + let scale_delta = zoom_delta * 0.01; + self.point_size_scale = (self.point_size_scale + scale_delta).clamp(0.1, 1000.0); + // println!("{}", self.point_size_scale); + } else { + self.distance *= (1.0 - zoom_delta * 0.001).max(0.1); + } + changed = true; + } + + // Middle mouse button for camera-plane panning + if i.pointer.middle_down() { + let delta = i.pointer.delta(); + let pan_speed = self.distance * 0.001; + + + // Get camera-relative right and up vectors + let right = self.get_right(); + let up = self.get_up(); + + // Move camera in the camera plane + let pan = right * (-delta.x * pan_speed) + up * (delta.y * pan_speed); + self.position += pan; + + changed = true; + } + + + + + if changed { + self.update_view(); + } + } + + fn get_right(&self) -> Vec3 { + self.orientation * Vec3::X + } + + fn get_up(&self) -> Vec3 { + self.orientation * Vec3::Y + } + + fn get_forward(&self) -> Vec3 { + self.orientation * -Vec3::Z + } + + fn update_view(&mut self) { + // Ensure orientation stays normalized + self.orientation = self.orientation.normalize(); + } + + pub fn get_view_matrix(&self) -> Mat4 { + // Calculate view position by moving back from target along view direction + let forward = self.get_forward(); + let view_pos = self.position - forward * self.distance; + + Mat4::look_at_rh( + view_pos, + self.position, + self.get_up() + ) + } + + pub fn set_point_size_scale(&mut self, scale: f32) { + self.point_size_scale = scale.clamp(0.1, 10.0); + } + +} + + +// PLY parsing structures +#[derive(Debug)] +struct PlyHeader { + vertex_count: usize, + has_colors: bool, + is_binary: bool, +} + +// #[derive(Debug)] +// pub struct PlyPoint { +// position: (i32, i32, i32), +// color: Color32, +// } + +pub struct PointRenderer { + gl: Arc, + program: glow::Program, + vao: glow::VertexArray, + vbo: glow::Buffer, + points: Vec, + capacity: usize, + pub camera: Camera, +} + +impl PointRenderer { + pub fn new(gl: Option>, initial_capacity: usize) -> Self { + use glow::HasContext; + + let gl = gl.unwrap(); + + let program = unsafe { + let program = gl.create_program().expect("Cannot create program"); + + let vertex_shader = gl.create_shader(glow::VERTEX_SHADER) + .expect("Cannot create vertex shader"); + gl.shader_source(vertex_shader, VERTEX_SHADER); + gl.compile_shader(vertex_shader); + + let fragment_shader = gl.create_shader(glow::FRAGMENT_SHADER) + .expect("Cannot create fragment shader"); + gl.shader_source(fragment_shader, FRAGMENT_SHADER); + gl.compile_shader(fragment_shader); + + gl.attach_shader(program, vertex_shader); + gl.attach_shader(program, fragment_shader); + gl.link_program(program); + + gl.delete_shader(vertex_shader); + gl.delete_shader(fragment_shader); + + program + }; + + let vao = unsafe { + let vao = gl.create_vertex_array().expect("Cannot create vertex array"); + gl.bind_vertex_array(Some(vao)); + vao + }; + + let vbo = unsafe { + let vbo = gl.create_buffer().expect("Cannot create vertex buffer"); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); + + // Position (3) + Color (4) = 7 u32s per vertex + let buffer_size = initial_capacity * 7 * std::mem::size_of::(); + gl.buffer_data_size(glow::ARRAY_BUFFER, buffer_size as i32, glow::DYNAMIC_DRAW); + + // Position attribute (uvec3) + gl.vertex_attrib_pointer_i32(0, 3, glow::INT, 28, 0); + gl.enable_vertex_attrib_array(0); + + // Color attribute (uvec4) + gl.vertex_attrib_pointer_i32(1, 4, glow::INT, 28, 12); + gl.enable_vertex_attrib_array(1); + + vbo + }; + + PointRenderer { + gl, + program, + vao, + vbo, + points: Vec::with_capacity(initial_capacity * 7), + capacity: initial_capacity, + camera: Camera::new(), + } + } + + pub fn add_point(&mut self, x: i32, y: i32, z: i32, color: Color32) { + let [r, g, b, a] = color.to_array(); + self.points.extend_from_slice(&[x, y, z, r as i32, g as i32, b as i32, a as i32]); + } + + pub fn clear(&mut self) { + self.points.clear(); + } + + pub fn render(&mut self, rect: Rect, input_state: InputState) { + use glow::HasContext; + + // Update camera + self.camera.update(input_state); + + unsafe { + self.gl.use_program(Some(self.program)); + + // Set up view-projection matrix + let aspect = rect.width() / rect.height(); + let projection = Mat4::perspective_rh(45.0f32.to_radians(), aspect, 0.1, 1000.0); + let view = self.camera.get_view_matrix(); + let view_projection = projection * view; + + let location = self.gl.get_uniform_location(self.program, "u_view_projection") + .expect("Cannot get uniform location"); + self.gl.uniform_matrix_4_f32_slice(Some(&location), false, &view_projection.to_cols_array()); + + // Set position scale factor (converts uint positions to world space) + let scale_location = self.gl.get_uniform_location(self.program, "u_position_scale") + .expect("Cannot get scale uniform location"); + self.gl.uniform_1_f32(Some(&scale_location), 0.001); // Adjust this value to scale your point cloud + + let point_size_location = self.gl.get_uniform_location(self.program, "u_point_size_scale") + .expect("Cannot get point size scale location"); + self.gl.uniform_1_f32(Some(&point_size_location), self.camera.point_size_scale); + + self.gl.bind_vertex_array(Some(self.vao)); + self.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo)); + + self.gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + 0, + bytemuck::cast_slice(&self.points), + ); + + self.gl.enable(glow::PROGRAM_POINT_SIZE); + self.gl.enable(glow::DEPTH_TEST); + + + self.gl.clear_depth_f32(1.0); + self.gl.depth_func(glow::LESS); + self.gl.depth_mask(true); + + // self.gl.clear_color(0.3, 0.3, 0.3, 1.0); + self.gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT); + + self.gl.draw_arrays(glow::POINTS, 0, (self.points.len() / 7) as i32); + + self.gl.disable(glow::DEPTH_TEST); + self.gl.disable(glow::PROGRAM_POINT_SIZE); + } + } + + + + + + + + + + + + // Add method to load points from PLY file + pub fn load_ply(&mut self, path: String) -> Result<(Vec<(i32, i32, i32, Color32)>), String> { + let file = File::open(path).map_err(|e| format!("Failed to open file: {}", e))?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + // Parse header + let header = Self::parse_ply_header(&mut lines)?; + + // Clear existing points + self.clear(); + + // Reserve capacity + self.points.reserve(header.vertex_count * 7); + + // Parse vertices based on format + if header.is_binary { + return Err("Binary PLY files not yet supported".to_string()); + } else { + self.parse_ascii_ply_data(lines, header) + } + + } + + fn parse_ply_header(lines: &mut std::io::Lines) -> Result { + let mut vertex_count = 0; + let mut has_colors = false; + let mut is_binary = false; + let mut in_header = true; + + while in_header { + let line = lines.next() + .ok_or("Unexpected end of file").unwrap().unwrap() + .trim().to_string(); + + match line.as_str() { + "ply" => continue, + "format ascii 1.0" => is_binary = false, + "format binary_little_endian 1.0" => is_binary = true, + "end_header" => break, + _ => { + if line.starts_with("element vertex ") { + vertex_count = line.split_whitespace() + .last() + .ok_or("Invalid vertex count")? + .parse() + .map_err(|_| "Invalid vertex count")?; + } else if line.starts_with("property") && line.contains("red") { + has_colors = true; + } + } + } + } + + Ok(PlyHeader { + vertex_count, + has_colors, + is_binary, + }) + } + + fn parse_ascii_ply_data( + &mut self, + lines: std::io::Lines, + header: PlyHeader, + ) -> Result<(Vec<(i32, i32, i32, Color32)>), String> { + let mut vec: Vec<(i32, i32, i32, Color32)> = Vec::new(); + + for line in lines.take(header.vertex_count) { + let line = line.map_err(|e| format!("Failed to read line: {}", e))?; + let parts: Vec<&str> = line.split_whitespace().collect(); + + if parts.len() < 3 { + return Err("Invalid vertex data".to_string()); + } + + // Parse position + let x = parts[0].parse::().map_err(|_| "Invalid X coordinate")?; + let y = parts[1].parse::().map_err(|_| "Invalid Y coordinate")?; + let z = parts[2].parse::().map_err(|_| "Invalid Z coordinate")?; + + // Convert to fixed point (scale by 1000 for better precision) + let x = (x * 1000.0) as i32; + let y = (y * 1000.0) as i32; + let z = (z * 1000.0) as i32; + + // Parse colors if present + let color = if header.has_colors && parts.len() >= 6 { + let r = parts[3].parse::().unwrap_or(255); + let g = parts[4].parse::().unwrap_or(255); + let b = parts[5].parse::().unwrap_or(255); + Color32::from_rgb(r, g, b) + } else { + Color32::WHITE + }; + + vec.push((x,y,z,color)); + + // self.add_point(x, y, z, color); + } + + Ok((vec)) + } + +} + +impl Drop for PointRenderer { + fn drop(&mut self) { + // Clean up GPU resources + } +} \ No newline at end of file