diff --git a/assets/test.vvg b/assets/test.vvg index e80dfaf..c92f84a 100644 --- a/assets/test.vvg +++ b/assets/test.vvg @@ -1,6 +1,9 @@ -0 0 0|1 0 0 -0 -0 1|0.68343294 0.073312916 0.7192233 --0 1 0|0.60703295 0.3218385 0.47832417 -0 1 1|0.68343294 0.073312916 0.7192233 -1 -0 -0|0.19322336 0.48136216 0.49846292 -1 -0 1|0.19322336 0.48136216 0.49846292 +version 1 +0 0 0|1 0 0 +0 0 1|1 0 0.77999973 +0 0 2|0.0800004 0 1 +0 0 3|0 0.7600002 1 +0 0 4|0 1 0.3399999 +0 0 5|0.5 1 0 +0 0 6|1 0.8399999 0 +0 0 7|1 0.4200003 0 \ No newline at end of file diff --git a/src/vvedit/editor_ui.rs b/src/vvedit/editor_ui.rs index cbef35c..e1814fb 100644 --- a/src/vvedit/editor_ui.rs +++ b/src/vvedit/editor_ui.rs @@ -1,16 +1,14 @@ use bevy::{ app::{Startup, Update}, - asset::{AssetId, AssetServer, Assets}, + asset::{AssetServer, Assets, Handle}, ecs::{ entity::Entity, query::With, system::{Commands, Query, Res, ResMut, Resource}, }, math::Vec3, - reflect::Reflect, render::{camera::Camera, color::Color}, transform::components::GlobalTransform, - utils::HashMap, window::Window, }; use bevy_egui::{ @@ -18,7 +16,7 @@ use bevy_egui::{ self, color_picker, ecolor::{hsv_from_rgb, rgb_from_hsv}, epaint::Hsva, - Id, + Id, Rect, }, EguiContexts, EguiPlugin, }; @@ -40,34 +38,97 @@ use super::{ orbit_camera::orbit_camera::{pan_orbit_camera, spawn_camera}, }; +pub struct AssetEditData { + path: String, + id: Handle, + has_changed_since_last_save: bool, +} + #[derive(Default, Resource)] pub struct EditWindowUIState { brush_color: Hsva, window_identifier: Option, - meshes: HashMap>, + meshes: Vec, + window: Option, } +// refactor into hashmaps of the AssetId impl EditWindowUIState { - pub fn name_for(&self, id: AssetId) -> Option { - for (each_str, each_id) in &self.meshes { - if each_id == &id { - return Some(each_str.clone()); + pub fn name_from_handle(&self, id: Handle) -> Option { + for each in &self.meshes { + if &each.id == &id { + return Some(each.path.clone()); } } None } - pub fn id_for(&self, name: &String) -> Option> { - if self.meshes.contains_key(name) { - return Some(self.meshes.get(name).unwrap().clone()); + pub fn has_changed_from_name(&self, name: &String) -> Option { + for each in &self.meshes { + if &each.path == name { + return Some(each.has_changed_since_last_save); + } } None } + pub fn has_changed_from_handle(&self, id: Handle) -> Option { + for each in &self.meshes { + if &each.id == &id { + return Some(each.has_changed_since_last_save); + } + } + None + } + pub fn handle_from_name(&self, name: &String) -> Option> { + for each in &self.meshes { + if &each.path == name { + return Some(each.id.clone()); + } + } + None + } + pub fn set_grid_data( + &mut self, + path: &String, + id: Handle, + has_changed: bool, + ) -> bool { + let mut match_count: usize = 0; + for each in &self.meshes { + if &each.path == path { + match_count += 1; + } else if &each.id == &id { + match_count += 1; + } + } + if match_count == 1 { + for each in &mut self.meshes { + if &each.path == path { + each.id = id.clone(); + each.has_changed_since_last_save = has_changed; + } else if &each.id == &id { + each.path = path.clone(); + each.has_changed_since_last_save = has_changed; + } + } + return true; + } else if match_count == 0 { + self.meshes.push(AssetEditData { + path: path.clone(), + id, + has_changed_since_last_save: has_changed, + }); + return true; + } else { + return false; + } + } } pub fn register_edit_ui(app: &mut bevy::prelude::App) -> &mut bevy::prelude::App { app.insert_resource(EditWindowUIState { brush_color: egui_color_from(Color::RED), window_identifier: None, - meshes: HashMap::new(), + meshes: Vec::new(), + window: None, }); app.add_plugins(EguiPlugin); app.add_systems(Startup, startup_system_edit_ui); @@ -88,7 +149,7 @@ pub fn edit_click_events( oct_tree: Query<(&GlobalTransform, &OctAssetMarker)>, windows: Query<&Window>, mut oct_assets: ResMut>, - shared_ui_state: ResMut, + mut shared_ui_state: ResMut, ) { let paint = input_data.get_binary_input(&PAINT_VOXEL.into()); let remove = input_data.get_binary_input(&REMOVE_VOXEL.into()); @@ -119,12 +180,20 @@ pub fn edit_click_events( col.voxel_index_location, color_from(shared_ui_state.brush_color), ); + let path = shared_ui_state.name_from_handle(oct.oct_handle.clone()); + if let Some(path) = path { + shared_ui_state.set_grid_data(&path, oct.oct_handle.clone(), true); + } } } else if let Some(_) = remove.filter(|input| input.pressed()) { if collision.is_some() { let col = collision.unwrap(); edit_octtree.remove_voxel(col.voxel_index_location); + let path = shared_ui_state.name_from_handle(oct.oct_handle.clone()); + if let Some(path) = path { + shared_ui_state.set_grid_data(&path, oct.oct_handle.clone(), true); + } } } else if let Some(_) = insert.filter(|input| input.pressed()) { if collision.is_some() { @@ -136,13 +205,17 @@ pub fn edit_click_events( hitvox.round(), color_from(shared_ui_state.brush_color), ); + let path = shared_ui_state.name_from_handle(oct.oct_handle.clone()); + if let Some(path) = path { + shared_ui_state.set_grid_data(&path, oct.oct_handle.clone(), true); + } } } } } } } - +// update to use events to respond to pub fn update_asset_mesh_list( oct_assets: Res>, mut shared_ui_state: ResMut, @@ -150,17 +223,22 @@ pub fn update_asset_mesh_list( ) { for each in oct_assets.ids() { let asset = oct_assets.get(each); - if asset.is_some() { + let handle = server.get_id_handle(each); + if asset.is_some() && handle.is_some() { + let handle = handle.unwrap(); let path = server.get_path(each); - if path.is_some() && shared_ui_state.name_for(each).is_none() { - let path_str = path.unwrap().to_string(); - if path_str.to_lowercase().ends_with(".vvg") { - let reversed: String = path_str.chars().rev().collect(); - let reversed_removed = reversed.replacen("gvv.", "", 1); - let forward: String = reversed_removed.chars().rev().collect(); - shared_ui_state.meshes.insert(forward, each.clone()); + if let Some(path) = path { + if shared_ui_state.name_from_handle(handle.clone()).is_none() { + let path_str = path.to_string(); + if path_str.to_lowercase().ends_with(".vvg") { + let reversed: String = path_str.chars().rev().collect(); + let reversed_removed = reversed.replacen("gvv.", "", 1); + let forward: String = reversed_removed.chars().rev().collect(); + shared_ui_state.set_grid_data(&forward, handle.clone(), false); + } } - } else if shared_ui_state.name_for(each).is_none() { + } + if shared_ui_state.name_from_handle(handle.clone()).is_none() { let base_str: String = "unnamed_mesh_".into(); let mut found_name = false; let mut count: usize = 1; @@ -168,19 +246,20 @@ pub fn update_asset_mesh_list( while !found_name { attempt = base_str.clone(); attempt.push_str(count.to_string().as_str()); - if shared_ui_state.id_for(&attempt).is_none() { + if shared_ui_state.handle_from_name(&attempt).is_none() { found_name = true; } else { count += 1; } } - shared_ui_state.meshes.insert(attempt, each.clone()); + let mut has_changed = shared_ui_state.has_changed_from_handle(handle.clone()); + if has_changed.is_none() { + has_changed = Some(false); + } + shared_ui_state.set_grid_data(&attempt, handle.clone(), has_changed.unwrap()); } } } - for (each_name, each_id) in &shared_ui_state.meshes { - println!("{}", each_name); - } } pub fn edit_window_ui( @@ -199,17 +278,14 @@ pub fn edit_window_ui( shared_ui_state.window_identifier = Some(id); } else { response = response.id(shared_ui_state.window_identifier.unwrap()); - /*for window_entity in &windows { + for window_entity in &windows { let ctx = contexts.ctx_for_window_mut(window_entity); let pos = ctx.memory(|memory| { // formatting comment memory.area_rect(shared_ui_state.window_identifier.unwrap()) }); - if let Some(pos) = pos { - let (x, y, w, h) = (pos.min.x, pos.min.y, pos.max.x, pos.max.y); - println!("window data: {x}, {y} to {w}, {h}"); - } - }*/ + shared_ui_state.window = pos; + } } response.show(contexts.ctx_mut(), |ui| { @@ -231,13 +307,32 @@ pub fn edit_window_ui( let id = pair.oct_handle.clone(); let tree = oct_assets.get_mut(id); if tree.is_some() { - // let tree = tree.unwrap(); tree.model .set_voxel_at_location(Vec3::ZERO, color_from(shared_ui_state.brush_color)); } } } + if ui.button("Save Any Changes").clicked() { + //save all files. + for each in &shared_ui_state.meshes { + if each.has_changed_since_last_save { + let handle = each.id.clone(); + let path = each.path.clone() + ".vvg"; + let asset = oct_assets.get(handle); + if asset.is_some() { + let result = + crate::vvlib::oct_asset::serialization::meshes::write_latest_version( + &path, + asset.unwrap(), + ); + if result { + println!("{} saved to disk.", &path); + } + } + } + } + } }); } diff --git a/src/vvlib/oct_asset.rs b/src/vvlib/oct_asset.rs index 91b908f..8e9ce32 100644 --- a/src/vvlib/oct_asset.rs +++ b/src/vvlib/oct_asset.rs @@ -24,7 +24,7 @@ use bevy::{ }; use thiserror::Error; -use super::octtree::{self, OctTree, Path}; +use super::octtree::{self, Path}; #[derive(Asset, TypePath)] pub struct OctTreeAsset { @@ -47,6 +47,229 @@ pub enum OctLoadError { FileNotFound(String), } +pub mod serialization { + + pub mod meshes { + use std::path::Path; + + use bevy::{ + asset::io::{AssetSource, AssetWriter}, + tasks::block_on, + }; + + use super::super::{OctLoadError, OctTreeAsset}; + + pub fn load_detect_version(bytes: Vec) -> Result { + let data_str = std::str::from_utf8(bytes.as_slice()); + if data_str.is_err() { + return Err(OctLoadError::FileDataInvalid("".to_string())); + } + let data: String = data_str.unwrap().into(); + let lines: std::str::Lines<'_> = data.lines(); + let mut iter: std::str::Lines<'_> = lines.into_iter(); + let first: Option<&str> = iter.next(); + let version: MeshFileVersions = determine_version(first); + match version { + MeshFileVersions::Version1 => { + return versions::version_1::load(iter); + } + MeshFileVersions::Error => { + return Err(OctLoadError::FileDataInvalid("invalid version data".into())) + } + } + } + + pub fn write_latest_version(path: &String, asset: &OctTreeAsset) -> bool { + write_any_version(path, asset, Default::default()) + } + + pub fn write_any_version( + path: &String, + asset: &OctTreeAsset, + version: MeshFileVersions, + ) -> bool { + if let Some(writer) = get_writer() { + let mut bytes: Vec = Vec::new(); + if let Some(mut header) = version_header(version) { + bytes.append(&mut header); + } else { + return false; + } + match version { + MeshFileVersions::Error => return false, + MeshFileVersions::Version1 => { + if let Some(mut contents) = versions::version_1::save(&asset) { + bytes.append(&mut contents); + } else { + return false; + } + } + } + + let res = + block_on(async { writer.write_bytes(Path::new(path), bytes.as_slice()).await }); + return res.is_ok(); + } + false + } + + pub fn get_writer() -> Option> { + AssetSource::get_default_writer("assets".into())(true) + } + + #[derive(Copy, Clone, Default)] + pub enum MeshFileVersions { + #[default] + Version1, + Error, + } + + mod versions { + + pub mod version_1 { + use bevy::{math::Vec3, render::color::Color}; + + use crate::vvlib::octtree::OctTree; + + use super::super::super::super::{OctLoadError, OctTreeAsset}; + pub fn load(data: std::str::Lines<'_>) -> Result { + let mut oct: Option> = None; + for line in data { + // + let line: String = line.into(); + let mut sides = line.split("|"); + if sides.clone().count() != 2 { + return Err(OctLoadError::FileDataInvalid(line)); + } + let left = sides.next(); + let right = sides.next(); + if !(left.is_some() && right.is_some()) { + return Err(OctLoadError::FileDataInvalid(line)); + } + let left: String = left.unwrap().into(); + let right: String = right.unwrap().into(); + let mut left_ax = left.split_whitespace(); + let mut right_ax = right.split_whitespace(); + if left_ax.clone().count() != 3 || right_ax.clone().count() != 3 { + return Err(OctLoadError::FileDataInvalid(line)); + } + + let loc_x = left_ax.next(); + if loc_x.is_none() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let loc_x = loc_x.unwrap().parse::(); + if loc_x.is_err() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let loc_x = loc_x.unwrap(); + + let loc_y = left_ax.next(); + if loc_y.is_none() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let loc_y = loc_y.unwrap().parse::(); + if loc_y.is_err() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let loc_y = loc_y.unwrap(); + + let loc_z = left_ax.next(); + if loc_z.is_none() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let loc_z = loc_z.unwrap().parse::(); + if loc_z.is_err() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let loc_z = loc_z.unwrap(); + let loc = Vec3::new(loc_x, loc_y, loc_z); + + let col_r = right_ax.next(); + if col_r.is_none() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let col_r = col_r.unwrap(); + let col_r = col_r.parse::(); + if col_r.is_err() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let col_r = col_r.unwrap(); + + let col_g = right_ax.next(); + if col_g.is_none() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let col_g = col_g.unwrap(); + let col_g = col_g.parse::(); + if col_g.is_err() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let col_g = col_g.unwrap(); + + let col_b = right_ax.next(); + if col_b.is_none() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let col_b = col_b.unwrap(); + let col_b = col_b.parse::(); + if col_b.is_err() { + return Err(OctLoadError::FileDataInvalid(line)); + } + let col_b = col_b.unwrap(); + + let col = Color::rgb(col_r, col_g, col_b); + if oct.is_none() { + oct = Some(OctTree::new(loc, col)); + } else { + let mut other = oct.unwrap(); + other.set_voxel_at_location(loc, col); + oct = Some(other); + } + } + if oct.is_none() { + return Err(OctLoadError::FileDataInvalid("".to_string())); + } + Ok(OctTreeAsset { + model: oct.unwrap(), + handle: None, + task: None, + }) + } + pub fn save(asset: &OctTreeAsset) -> Option> { + let mut data: String = "".into(); + let voxels = asset.model.collect_voxels(); + if voxels.len() == 0 { + return None; + } + for (_, each_pos, each_val) in voxels { + let (x, y, z) = (each_pos.x, each_pos.y, each_pos.z); + let (r, g, b) = (each_val.r(), each_val.g(), each_val.b()); + data = format!("{data}\n{x} {y} {z}|{r} {g} {b}"); + } + Some(data.as_bytes().to_vec()) + } + } + } + const VERSION1: &str = "version 1"; + pub fn determine_version(line: Option<&str>) -> MeshFileVersions { + if line.is_some() { + let line: String = line.unwrap().into(); + if line.to_lowercase() == String::from(VERSION1) { + return MeshFileVersions::Version1; + } + } + MeshFileVersions::Error + } + pub fn version_header(version: MeshFileVersions) -> Option> { + match version { + MeshFileVersions::Error => None, + MeshFileVersions::Version1 => Some((VERSION1.to_string()).as_bytes().to_vec()), + } + } + } +} + impl AssetLoader for OctLoader { type Asset = OctTreeAsset; type Settings = (); @@ -59,119 +282,14 @@ impl AssetLoader for OctLoader { _load_context: &'a mut bevy::asset::LoadContext, ) -> bevy::utils::BoxedFuture<'a, Result> { Box::pin(async move { - let mut bytes = Vec::::new(); + let mut bytes: Vec = Vec::::new(); let result = reader.read_to_end(&mut bytes).await; if result.is_err() { - return Err(OctLoadError::FileNotFound("".to_string())); + return Err(OctLoadError::FileNotFound( + "could not read_to_end the bytes".to_string(), + )); } - let data_str = std::str::from_utf8(bytes.as_slice()); - if data_str.is_err() { - return Err(OctLoadError::FileDataInvalid("".to_string())); - } - let data: String = data_str.unwrap().into(); - let lines = data.lines(); - let mut oct: Option> = None; - for line in lines { - // - let line: String = line.into(); - let mut sides = line.split("|"); - if sides.clone().count() != 2 { - return Err(OctLoadError::FileDataInvalid(line)); - } - let left = sides.next(); - let right = sides.next(); - if !(left.is_some() && right.is_some()) { - return Err(OctLoadError::FileDataInvalid(line)); - } - let left: String = left.unwrap().into(); - let right: String = right.unwrap().into(); - let mut left_ax = left.split_whitespace(); - let mut right_ax = right.split_whitespace(); - if left_ax.clone().count() != 3 || right_ax.clone().count() != 3 { - return Err(OctLoadError::FileDataInvalid(line)); - } - - let loc_x = left_ax.next(); - if loc_x.is_none() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let loc_x = loc_x.unwrap().parse::(); - if loc_x.is_err() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let loc_x = loc_x.unwrap(); - - let loc_y = left_ax.next(); - if loc_y.is_none() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let loc_y = loc_y.unwrap().parse::(); - if loc_y.is_err() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let loc_y = loc_y.unwrap(); - - let loc_z = left_ax.next(); - if loc_z.is_none() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let loc_z = loc_z.unwrap().parse::(); - if loc_z.is_err() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let loc_z = loc_z.unwrap(); - let loc = Vec3::new(loc_x, loc_y, loc_z); - - let col_r = right_ax.next(); - if col_r.is_none() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let col_r = col_r.unwrap(); - let col_r = col_r.parse::(); - if col_r.is_err() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let col_r = col_r.unwrap(); - - let col_g = right_ax.next(); - if col_g.is_none() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let col_g = col_g.unwrap(); - let col_g = col_g.parse::(); - if col_g.is_err() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let col_g = col_g.unwrap(); - - let col_b = right_ax.next(); - if col_b.is_none() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let col_b = col_b.unwrap(); - let col_b = col_b.parse::(); - if col_b.is_err() { - return Err(OctLoadError::FileDataInvalid(line)); - } - let col_b = col_b.unwrap(); - - let col = Color::rgb(col_r, col_g, col_b); - if oct.is_none() { - oct = Some(OctTree::new(loc, col)); - } else { - let mut other = oct.unwrap(); - other.set_voxel_at_location(loc, col); - oct = Some(other); - } - } - if oct.is_none() { - return Err(OctLoadError::FileDataInvalid("".to_string())); - } - Ok(OctTreeAsset { - model: oct.unwrap(), - handle: None, - task: None, - }) + return serialization::meshes::load_detect_version(bytes); }) } }