mirror of
https://github.com/Astatin3/rushroom.git
synced 2026-06-09 00:28:01 -06:00
Add code
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
debug/
|
||||
target/
|
||||
dist/
|
||||
|
||||
Cargo.lock
|
||||
|
||||
**/*.rs.bk
|
||||
|
||||
*.pdb
|
||||
+18
@@ -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"
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"root": {
|
||||
"Group": {
|
||||
"direction": "Vertical",
|
||||
"children": [
|
||||
{
|
||||
"Pane": "console"
|
||||
},
|
||||
{
|
||||
"Pane": "properties"
|
||||
}
|
||||
],
|
||||
"sizes": [
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
},
|
||||
"windowed_panes": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Properties": {
|
||||
"mode": "Hidden",
|
||||
"position": null,
|
||||
"size": null,
|
||||
"order": 1
|
||||
},
|
||||
"Console": {
|
||||
"mode": "Tiled",
|
||||
"position": null,
|
||||
"size": null,
|
||||
"order": 0
|
||||
}
|
||||
}
|
||||
+223
@@ -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<Mutex<PointRenderer>>,
|
||||
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<Self> {
|
||||
// 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);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
+78
@@ -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::<web_sys::HtmlCanvasElement>()
|
||||
.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(
|
||||
"<p> The app has crashed. See the developer console for details. </p>",
|
||||
);
|
||||
panic!("Failed to start eframe: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
+438
@@ -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<LayoutNode>,
|
||||
sizes: Vec<f32>, // 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<LayoutNode>,
|
||||
windowed_panes: HashMap<String, WindowedPaneConfig>,
|
||||
}
|
||||
|
||||
#[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<usize>,
|
||||
drag_started: bool,
|
||||
drop_target: Option<DropTarget>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DropTarget {
|
||||
target_pane: String,
|
||||
group_path: Vec<usize>,
|
||||
position: DropPosition,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
enum DropPosition {
|
||||
Above,
|
||||
Below,
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
}
|
||||
|
||||
pub struct PaneManager {
|
||||
panes: HashMap<String, Box<dyn Pane>>,
|
||||
layout: LayoutConfig,
|
||||
drag_state: Option<DragState>,
|
||||
}
|
||||
|
||||
impl PaneManager {
|
||||
pub fn new() -> Self {
|
||||
let mut panes: HashMap<String, Box<dyn Pane>> = 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<LayoutNode>,
|
||||
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<LayoutNode>,
|
||||
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<usize>,
|
||||
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<dyn Pane>,
|
||||
rect: egui::Rect,
|
||||
path: Vec<usize>,
|
||||
) {
|
||||
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<egui::Pos2> = 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) = <std::option::Option<LayoutNode> 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 {
|
||||
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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<glow::Context>,
|
||||
program: glow::Program,
|
||||
vao: glow::VertexArray,
|
||||
vbo: glow::Buffer,
|
||||
points: Vec<i32>,
|
||||
capacity: usize,
|
||||
pub camera: Camera,
|
||||
}
|
||||
|
||||
impl PointRenderer {
|
||||
pub fn new(gl: Option<Arc<glow::Context>>, 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::<i32>();
|
||||
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<B: BufRead>(lines: &mut std::io::Lines<B>) -> Result<PlyHeader, String> {
|
||||
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<B: BufRead>(
|
||||
&mut self,
|
||||
lines: std::io::Lines<B>,
|
||||
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::<f32>().map_err(|_| "Invalid X coordinate")?;
|
||||
let y = parts[1].parse::<f32>().map_err(|_| "Invalid Y coordinate")?;
|
||||
let z = parts[2].parse::<f32>().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::<u8>().unwrap_or(255);
|
||||
let g = parts[4].parse::<u8>().unwrap_or(255);
|
||||
let b = parts[5].parse::<u8>().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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user