From 61cd4767ba5a2fcde2ebc520823ed453f66a6412 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Sat, 23 Nov 2024 20:32:08 -0700 Subject: [PATCH] Add Panes --- Cargo.toml | 1 + src/app.rs | 198 +------ src/main.rs | 82 +-- src/panes.rs | 1042 +++++++++++++++++++++-------------- src/point_cloud_renderer.rs | 211 ++++++- 5 files changed, 887 insertions(+), 647 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 70e28e3..4d2f896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ eframe = { version = "0.29.1", features = [ "persistence",]} egui = { version = "0.29.1", features = ["callstack", "default", "log"] } +erased-serde = "0.4.5" glam = "0.29.2" rand = "0.8.5" serde = { version = "1.0.215", features = ["derive"] } diff --git a/src/app.rs b/src/app.rs index c484d54..99fbaa1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,20 +1,16 @@ // 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 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}; +use crate::panes::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 +// #[serde(default)] pub struct App { - // #[serde(skip)] - renderer: Arc>, - points: Vec<(i32, i32, i32, Color32)>, - file_dialog_open: bool, - cur_path: String, + pane_manager: PaneManager, } @@ -31,79 +27,35 @@ impl App { // } 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(), + pane_manager: PaneManager::new(Some(cc)), }) } } +impl Default for App { + fn default() -> Self { + Self { + pane_manager: PaneManager::new(None), + } + } +} + 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| { + self.pane_manager.render(ui); // egui::scroll_area::ScrollArea::vertical().show(ui, |ui| { - egui::Frame::canvas(ui.style()).show(ui, |ui| { - self.custom_painting(ui.max_rect(), 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>) { @@ -116,108 +68,4 @@ impl eframe::App for App { // 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/main.rs b/src/main.rs index 2f67b4c..f814bcc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release use rushroom::App; -use rushroom::PaneManager; +// use rushroom::PaneManager; // When compiling natively: #[cfg(not(target_arch = "wasm32"))] @@ -30,49 +30,49 @@ fn main() -> eframe::Result { ) } -// When compiling to web using trunk: -#[cfg(target_arch = "wasm32")] -fn main() { - use eframe::wasm_bindgen::JsCast as _; +// // 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(); +// // Redirect `log` message to `console.log` and friends: +// eframe::WebLogger::init(log::LevelFilter::Debug).ok(); - let web_options = eframe::WebOptions::default(); +// let web_options = eframe::WebOptions::default(); - wasm_bindgen_futures::spawn_local(async { - let document = web_sys::window() - .expect("No window") - .document() - .expect("No document"); +// 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 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; +// 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:?}"); - } - } - } - }); -} +// // 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 index 75ccaaa..cae7c60 100644 --- a/src/panes.rs +++ b/src/panes.rs @@ -1,438 +1,662 @@ -use eframe::egui; -use egui::Stroke; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs; -use egui::Color32; +use egui::{Ui, Color32, Stroke}; +// use erased_serde::serialize_trait_object; -#[derive(Serialize, Deserialize, Clone, PartialEq)] -enum PaneMode { +#[derive(serde::Deserialize, serde::Serialize, PartialEq)] +pub 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, + Left, + Bottom, Center, } +pub trait Pane { + fn new(cc: &eframe::CreationContext<'_>) -> PaneState where Self: Sized; + fn name(&mut self) -> &str; + fn render(&mut self, ui: &mut Ui); + fn context_menu(&mut self, ui: &mut Ui); +} + +// #[derive(serde::Deserialize, serde::Serialize)] +pub struct PaneState { + // #[serde(skip)] + pub pane: Box, + pub id: String, + pub mode: PaneMode, + // pub window_location: Pos2, +} + +// pub struct NoPane {} +// impl Pane for NoPane { +// fn name(&mut self) -> &str {"ERROR"} +// fn render(&mut self, _ui: &mut Ui){} +// fn context_menu(&mut self, _ui: &mut Ui) {} +// } + + +// impl Default for dyn Pane { +// fn default() -> (impl Pane + 'static) { +// Box::new(NoPane {}) +// } +// } + +impl PaneState { + pub fn render(&mut self, ui: &mut Ui) { + self.pane.render(ui); + } +} + +// pub struct PaneGroup { +// pub direction: TileDirection, +// pub panes: Vec, +// } + +// #[derive(serde::Deserialize, serde::Serialize)] pub struct PaneManager { - panes: HashMap>, - layout: LayoutConfig, - drag_state: Option, + pub panes: Vec, +} + + + +pub struct BluePane {} +impl Pane for BluePane { + fn new(cc: &eframe::CreationContext<'_>) -> PaneState where Self: Sized { + + let mut s = Self {}; + PaneState { + id: s.name().to_string(), + mode: PaneMode::Left, + pane: Box::new(s), + } + } + fn name(&mut self) -> &str {"BLUE"} + fn render(&mut self, ui: &mut Ui){ + ui.painter().rect(ui.max_rect(), 0., Color32::BLUE, Stroke::NONE); + } + fn context_menu(&mut self, _ui: &mut Ui) {} +} + +pub struct GreenPane {} +impl Pane for GreenPane { + fn new(cc: &eframe::CreationContext<'_>) -> PaneState where Self: Sized { + let mut s = Self {}; + PaneState { + id: s.name().to_string(), + mode: PaneMode::Bottom, + pane: Box::new(s), + } + } + fn name(&mut self) -> &str {"Green"} + fn render(&mut self, ui: &mut Ui){ + ui.painter().rect(ui.max_rect(), 0., Color32::GREEN, Stroke::NONE); + } + fn context_menu(&mut self, _ui: &mut Ui) {} } 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; + pub fn new(cc: Option<&eframe::CreationContext<'_>>) -> Self { + if let Some(cc) = cc { + Self { + panes: vec![ + BluePane::new(cc), + GreenPane::new(cc), + // PaneState {pane: Box::new(BluePane{}), id: "iqnhjqnbekjq".to_string(), mode: PaneMode::Windowed}, + // PaneState {pane: Box::new(GreenPane{}), id: "kjwkjwqfd".to_string(), mode: PaneMode::Right}, + crate::point_cloud_renderer::PointRendererPane::new(cc), + ], } - } - } - - 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); - } + Self { + panes: vec![] } - - 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, + + pub fn render(&mut self, ui: &mut Ui){ + let len = self.panes.len(); + + egui::TopBottomPanel::top("top_panel").show(ui.ctx(), |ui| { + + egui::menu::bar(ui, |ui| { + // NOTE: no File->Quit on web pages! + + egui::widgets::global_theme_preference_switch(ui); + + ui.menu_button("File", |ui| { + if ui.button("Quit").clicked() { + ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); + } }); - } - } - // 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 - }; + ui.menu_button("View", |ui| { - drag_state.drop_target = Some(DropTarget { - target_pane: pane.name().to_string(), - group_path: path, - position, + for i in 0..len { + ui.menu_button(self.panes[i].id.clone(), |ui| { + if ui.button("Hidden").clicked() { + self.panes[i].mode = PaneMode::Hidden; + } + if ui.button("Windowed").clicked() { + self.panes[i].mode = PaneMode::Windowed; + } + if ui.button("Left").clicked() { + self.panes[i].mode = PaneMode::Left; + } + if ui.button("Right").clicked() { + self.panes[i].mode = PaneMode::Right; + } + if ui.button("Bottom").clicked() { + self.panes[i].mode = PaneMode::Bottom; + } + if ui.button("Center").clicked() { + for a in 0..len { + let pane2: &mut PaneState = &mut self.panes[a]; + if pane2.mode == PaneMode::Center { + pane2.mode = PaneMode::Windowed; + } + } + self.panes[i].mode = PaneMode::Center; + } }); } - } - } - } - - // 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(); - } }); + + + ui.separator(); + // self. }); }); - let any_down = ctx.input(|i| {i.pointer.any_down().clone()}); - if !any_down {// && drag_state.drag_started { - self.handle_drop(); + for i in 0..len { + let pane: &mut PaneState = &mut self.panes[i]; + + match pane.mode { + PaneMode::Hidden => {}, + PaneMode::Left => { + egui::panel::SidePanel::left(pane.id.clone()) + .resizable(true) + .show(ui.ctx(), |ui| { + pane.render(ui); + }); + }, + PaneMode::Right => { + egui::panel::SidePanel::right(pane.id.clone()) + .resizable(true) + .show(ui.ctx(), |ui| { + pane.render(ui); + }); + }, + PaneMode::Bottom => { + egui::panel::TopBottomPanel::bottom(pane.id.clone()) + .resizable(true) + .show(ui.ctx(), |ui| { + pane.render(ui); + }); + }, + PaneMode::Windowed => { + egui::Window::new(pane.id.clone()) + .resizable(true) + .max_width(ui.clip_rect().width()).max_height(ui.clip_rect().height()) + .show(ui.ctx(), |ui| { + pane.render(ui); + }); + }, + PaneMode::Center => { + egui::CentralPanel::default() + .show(ui.ctx(), |ui| { + pane.render(ui); + }); + } + } } - - 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 { - - // } } } + +// impl + + + + + + + + +// 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 index cb76d6f..f40c496 100644 --- a/src/point_cloud_renderer.rs +++ b/src/point_cloud_renderer.rs @@ -1,11 +1,19 @@ -use egui::{vec2, Color32, InputState, Rect, Response}; +use egui::{Color32, InputState, Rect}; use eframe::egui_glow; use egui_glow::glow; use std::sync::Arc; -use glam::{Vec3, Mat4, Quat, Vec2}; +use glam::{Vec3, Mat4, Quat}; use std::fs::File; -use std::path::Path; +// use std::path::Path; use std::io::{BufReader, BufRead}; +use crate::panes::{Pane, PaneMode, PaneState}; +use std::sync::Mutex; +use egui::FontId; +use egui::Align2; +// use egui::Pos2; +use std::time::Instant; +use egui::Stroke; +use egui::Ui; // Shader sources updated for 3D rendering with fixed-point positions const VERTEX_SHADER: &str = r#" @@ -66,13 +74,13 @@ impl Camera { } } - 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 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 = { @@ -112,7 +120,7 @@ impl Camera { } // Middle mouse button for camera-plane panning - if i.pointer.middle_down() { + if i.pointer.primary_down() { let delta = i.pointer.delta(); let pan_speed = self.distance * 0.001; @@ -165,9 +173,9 @@ impl Camera { ) } - pub fn set_point_size_scale(&mut self, scale: f32) { - self.point_size_scale = scale.clamp(0.1, 10.0); - } + // pub fn set_point_size_scale(&mut self, scale: f32) { + // self.point_size_scale = scale.clamp(0.1, 10.0); + // } } @@ -192,7 +200,7 @@ pub struct PointRenderer { vao: glow::VertexArray, vbo: glow::Buffer, points: Vec, - capacity: usize, + // capacity: usize, pub camera: Camera, } @@ -256,7 +264,7 @@ impl PointRenderer { vao, vbo, points: Vec::with_capacity(initial_capacity * 7), - capacity: initial_capacity, + // capacity: initial_capacity, camera: Camera::new(), } } @@ -270,11 +278,13 @@ impl PointRenderer { self.points.clear(); } - pub fn render(&mut self, rect: Rect, input_state: InputState) { + pub fn render(&mut self, rect: Rect, input_state: Option) { use glow::HasContext; // Update camera - self.camera.update(input_state); + if let Some(i) = input_state{ + self.camera.update(i); + } unsafe { self.gl.use_program(Some(self.program)); @@ -336,7 +346,7 @@ impl PointRenderer { // Add method to load points from PLY file - pub fn load_ply(&mut self, path: String) -> Result<(Vec<(i32, i32, i32, Color32)>), String> { + pub fn load_ply(&mut self, path: String) -> Result, 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(); @@ -363,7 +373,7 @@ impl PointRenderer { let mut vertex_count = 0; let mut has_colors = false; let mut is_binary = false; - let mut in_header = true; + let in_header = true; while in_header { let line = lines.next() @@ -400,7 +410,7 @@ impl PointRenderer { &mut self, lines: std::io::Lines, header: PlyHeader, - ) -> Result<(Vec<(i32, i32, i32, Color32)>), String> { + ) -> Result, String> { let mut vec: Vec<(i32, i32, i32, Color32)> = Vec::new(); for line in lines.take(header.vertex_count) { @@ -436,7 +446,7 @@ impl PointRenderer { // self.add_point(x, y, z, color); } - Ok((vec)) + Ok(vec) } } @@ -445,4 +455,161 @@ impl Drop for PointRenderer { fn drop(&mut self) { // Clean up GPU resources } +} + +pub struct PointRendererPane { + renderer: Arc>, + points: Vec<(i32, i32, i32, Color32)>, + file_dialog_open: bool, + cur_path: String, +} + +impl Pane for PointRendererPane { + fn new(cc: &eframe::CreationContext<'_>) -> PaneState where Self: Sized { + let mut s = 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(), + }; + PaneState { + id: s.name().to_string(), + mode: PaneMode::Center, + pane: Box::new(s), + } + } + fn name(&mut self) -> &str {"Point Cloud"} + fn render(&mut self, ui: &mut Ui){ + let max_rect = ui.max_rect(); + + let renderer = self.renderer.clone(); + renderer.lock().expect("Renderer Not Initialized").clear(); + + + if self.file_dialog_open { + egui::Window::new("Load PLY File") + .show(ui.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 renderer.lock().expect("Renderer Not Initialized"); + // 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; + } + }); + }); + } + + 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: Option = ui.input(|input_state| + if response.hovered() { //&& response.has_focus() { + Some(input_state.clone()) + }else{None} + ); + + 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 painter = ui.painter(); + + for &(x, y, z, color) in &self.points { + renderer.lock().expect("Renderer Not Initialized").add_point(x, y, z, color); + } + + let o = renderer.lock().expect("Renderer Not Initialized").camera.orientation.clone(); + + let cb = egui_glow::CallbackFn::new(move |_info, _painter| { + renderer.lock().expect("Renderer Not Initialized").render(max_rect, input_state.clone()); + }); + + let callback = egui::PaintCallback { + rect: max_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()); + + let text_size = 12.; + + ui.painter().text(max_rect.min, Align2::LEFT_TOP, + format!("{} ms",end_time.duration_since(start_time).as_millis()), + FontId::monospace(text_size), Color32::WHITE); + + ui.painter().text(max_rect.min + egui::Vec2 {x:0.,y:text_size}, Align2::LEFT_TOP, + format!("{} points", self.points.len()), + FontId::monospace(text_size), Color32::WHITE); + } + fn context_menu(&mut self, ui: &mut Ui) { + if ui.button("Load PLY").clicked() { + self.file_dialog_open = true; + } + } } \ No newline at end of file