vixevoxerust/src/vvedit/editor_ui.rs

605 lines
22 KiB
Rust

use bevy::{
app::{Startup, Update},
asset::{AssetServer, Assets, Handle},
ecs::{
entity::Entity,
query::With,
system::{Commands, Query, Res, ResMut, Resource},
},
math::Vec3,
prelude::default,
render::{camera::Camera, color::Color},
transform::components::GlobalTransform,
window::{PrimaryWindow, Window},
};
use bevy_egui::{
egui::{
self, color_picker,
ecolor::{hsv_from_rgb, rgb_from_hsv},
epaint::Hsva,
Id, Pos2, Rect, Response, ScrollArea,
},
EguiContexts, EguiPlugin, EguiSettings,
};
use crate::{
vvedit::editor_bevy_input_shim::keybind_codes::REMOVE_VOXEL,
vvlib::{
inputs::InputRegister, intersections::octtree_ray_overlap, obb::OBB,
oct_asset::OctAssetMarker,
},
};
use crate::{
vvedit::editor_bevy_input_shim::keybind_codes::{INSERT_VOXEL, PAINT_VOXEL},
vvlib::oct_asset::{MeshingOctTreePairs, OctTreeAsset},
};
use super::{
editor_bevy_input_shim::setup_inputs,
orbit_camera::orbit_camera::{pan_orbit_camera, spawn_camera},
ui_extensions::{StringTree, StringTreeElement},
};
pub struct AssetEditData {
path: String,
id: Handle<OctTreeAsset>,
has_changed_since_last_save: bool,
}
#[derive(Default, Resource)]
pub struct EditWindowUIState {
brush_color: Hsva,
meshes: Vec<AssetEditData>,
structure: StringTree,
egui: EditWindowEguiState,
}
#[derive(Clone)]
pub struct EditWindowEguiState {
popup_identifier: Option<Id>,
window_identifier: Option<Id>,
windows: Vec<Rect>,
}
impl Default for EditWindowEguiState {
fn default() -> Self {
Self {
popup_identifier: None,
window_identifier: None,
windows: Vec::new(),
}
}
}
// refactor into hashmaps of the AssetId
impl EditWindowUIState {
pub fn name_from_handle(&self, id: Handle<OctTreeAsset>) -> Option<String> {
for each in &self.meshes {
if &each.id == &id {
return Some(each.path.clone());
}
}
None
}
pub fn has_changed_from_name(&self, name: &String) -> Option<bool> {
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<OctTreeAsset>) -> Option<bool> {
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<Handle<OctTreeAsset>> {
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<OctTreeAsset>,
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),
meshes: Vec::new(),
structure: StringTree::new(),
..default()
});
app.add_plugins(EguiPlugin);
app.add_systems(Startup, startup_system_edit_ui);
app.add_systems(Update, edit_window_ui);
app.add_systems(Update, pan_orbit_camera);
app.add_systems(Update, update_asset_mesh_list);
setup_inputs(app);
app.add_systems(Startup, update_ui_scale_factor);
app
}
pub fn startup_system_edit_ui(commands: Commands, mut shared_ui_state: ResMut<EditWindowUIState>) {
spawn_camera(commands);
shared_ui_state
.structure
.add_root_level(&String::from("example"));
shared_ui_state
.structure
.add_root_level(&String::from("example2"));
shared_ui_state
.structure
.add(&String::from("c1"), Some(&String::from("example")));
shared_ui_state
.structure
.add(&String::from("c2"), Some(&String::from("c1")));
}
fn update_ui_scale_factor(
mut egui_settings: ResMut<EguiSettings>,
windows: Query<&Window, With<PrimaryWindow>>,
) {
if let Ok(_window) = windows.get_single() {
egui_settings.scale_factor = 1.25;
}
}
pub fn edit_click_events(
input_data: ResMut<InputRegister>,
camera_query: Query<(&Camera, &GlobalTransform)>,
oct_tree: Query<(&GlobalTransform, &OctAssetMarker)>,
windows: Query<&Window>,
mut oct_assets: ResMut<Assets<OctTreeAsset>>,
mut shared_ui_state: ResMut<EditWindowUIState>,
egui_settings: Res<EguiSettings>,
) {
let paint = input_data.get_binary_input(&PAINT_VOXEL.into());
let remove = input_data.get_binary_input(&REMOVE_VOXEL.into());
let insert = input_data.get_binary_input(&INSERT_VOXEL.into());
for window in &windows {
for (trans, oct) in &oct_tree {
for (camera, cam_trans) in &camera_query {
let Some(cursor_pos) = window.cursor_position() else {
continue;
};
let Some(ray) = camera.viewport_to_world(cam_trans, cursor_pos) else {
continue;
};
let obb = OBB::from_transform(trans.compute_transform());
let octtree = oct_assets.get_mut(oct.oct_handle.clone());
if octtree.is_none() {
continue;
}
let edit_octtree = &mut octtree.unwrap().model;
let collision = {
let eoct = edit_octtree.read().unwrap();
octtree_ray_overlap(&eoct, &obb, ray.origin, ray.direction.normalize())
};
dbg!(&shared_ui_state.egui.windows);
let scale = egui_settings.scale_factor;
let mod_cursor_pos = Pos2::new(cursor_pos.x / scale, cursor_pos.y / scale);
dbg!(&mod_cursor_pos);
for rect in &shared_ui_state.egui.windows {
if rect.contains(mod_cursor_pos) {
return;
}
}
if let Some(_) = paint.filter(|input| input.pressed()) {
if collision.is_some() {
let col = collision.unwrap();
{
let mut eoct = edit_octtree.write().unwrap();
eoct.set_voxel_at_location(
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();
{
let mut eoct = edit_octtree.write().unwrap();
eoct.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() {
let col = collision.unwrap();
let hitloc = col.hit_data.hitpoint.local.clone();
let hitnorm = col.hit_data.normal.local.clone();
let hitvox = hitloc + (hitnorm * 0.25);
{
let mut eoct = edit_octtree.write().unwrap();
eoct.set_voxel_at_location(
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<Assets<OctTreeAsset>>,
mut shared_ui_state: ResMut<EditWindowUIState>,
server: Res<AssetServer>,
) {
for each in oct_assets.ids() {
let asset = oct_assets.get(each);
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 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);
}
}
}
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;
let mut attempt = base_str.clone();
while !found_name {
attempt = base_str.clone();
attempt.push_str(count.to_string().as_str());
if shared_ui_state.handle_from_name(&attempt).is_none() {
found_name = true;
} else {
count += 1;
}
}
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());
}
}
}
}
pub fn edit_window_ui(
mut shared_ui_state: ResMut<EditWindowUIState>,
mut contexts: EguiContexts,
mut oct_assets: ResMut<Assets<OctTreeAsset>>,
mut pairs: ResMut<MeshingOctTreePairs>,
windows: Query<Entity, With<Window>>,
window: Query<&Window>,
) {
shared_ui_state.egui.windows.clear();
let title = String::from("VVEdit");
let mut response = egui::Window::new(&title);
if shared_ui_state.egui.window_identifier.is_none() {
let id = egui::Id::new(title);
response = response.id(id);
shared_ui_state.egui.window_identifier = Some(id);
} else {
response = response.id(shared_ui_state.egui.window_identifier.unwrap());
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.egui.window_identifier.unwrap())
});
if let Some(pos) = pos {
shared_ui_state.egui.windows.push(pos);
}
}
}
if shared_ui_state.egui.popup_identifier.is_none() {
let id = egui::Id::new("popup");
response = response.id(id);
shared_ui_state.egui.popup_identifier = Some(id);
} else {
for window_entity in &windows {
let ctx = contexts.ctx_for_window_mut(window_entity);
let pos = ctx.memory(|memory| {
if !memory.is_popup_open(shared_ui_state.egui.popup_identifier.unwrap()) {
return None;
}
memory.area_rect(shared_ui_state.egui.popup_identifier.unwrap())
});
if let Some(pos) = pos {
shared_ui_state.egui.windows.push(pos);
}
}
}
for win in &window {
let size = &win.resolution;
response = response.fixed_pos(Pos2::new(0., 0.));
response = response.fixed_size(egui::Vec2::new(
size.width() as f32 / 3.,
size.height() as f32 / 1.,
));
}
response.show(contexts.ctx_mut(), |ui: &mut egui::Ui| {
ScrollArea::vertical().show(ui, |ui| {
let mut label = String::from("Pre-Alpha");
use std::fmt::Write;
writeln!(label, " Controls:").ok();
writeln!(label, "click adds a voxel:").ok();
writeln!(label, "R+click removes a voxel:").ok();
writeln!(label, "E+click paints (edits) a voxel:").ok();
color_picker::color_picker_hsva_2d(
ui,
&mut shared_ui_state.brush_color,
color_picker::Alpha::Opaque,
);
ui.label(label);
let mut color_to_set: Option<Color> = None;
for each in &shared_ui_state.meshes {
ui.vertical(|ui| {
//color palette clickables.
let handle = each.id.clone();
let asset = oct_assets.get(handle);
if asset.is_some() {
let list = {
let eoct = asset.unwrap().model.read().unwrap();
eoct.collect_voxels()
};
let mut count: Vec<(Color, usize)> = Vec::new();
fn eq(c1: &Color, c2: &mut Color) -> bool {
c1.r() == c2.r() && c1.g() == c2.g() && c1.b() == c2.b()
}
for (_, _, color) in &list {
let mut is_committed = false;
for (each_col, each_count) in &mut count {
if eq(color, each_col) {
*each_count += 1;
is_committed = true;
break;
}
}
if !is_committed {
count.push((color.clone(), 1));
}
}
count.sort_by(|a, b| b.1.cmp(&a.1));
if !count.is_empty() {
ui.label(each.path.clone());
ui.horizontal_wrapped(|ui| {
for (color, count) in count {
let button = egui::Button::new(count.to_string())
.fill(egui_color_from(color));
if ui.add(button).clicked() {
color_to_set = Some(color);
}
}
});
}
}
});
}
if let Some(color) = color_to_set {
shared_ui_state.brush_color = egui_color_from(color);
}
if ui.button("Add Origin Voxel").clicked() {
//
for pair in &mut pairs.handles {
let id = pair.oct_handle.clone();
let tree = oct_assets.get_mut(id);
if tree.is_some() {
let tree = tree.unwrap();
{
let mut eoct = tree.model.write().unwrap();
eoct.set_voxel_at_location(
Vec3::ZERO,
color_from(shared_ui_state.brush_color),
);
}
}
}
}
let response = ui.button("Save Any Changes");
if response.clicked() {
//save all files.
for each in &mut 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 {
each.has_changed_since_last_save = false;
println!("{} saved to disk.", &path);
}
}
}
}
}
show_editable_stringtree(
ui,
shared_ui_state.egui.clone(),
&mut shared_ui_state.structure,
&response,
);
});
});
}
pub fn show_editable_stringtree(
ui: &mut egui::Ui,
state: EditWindowEguiState,
tree: &mut StringTree,
response: &Response,
) {
egui::popup_below_widget(ui, state.popup_identifier.unwrap(), response, |ui| {
ScrollArea::vertical().show(ui, |ui| {
ui.vertical(|ui| {
for _ in 0..100 {
ui.label("filler");
}
});
});
});
ui.collapsing("Model Structure", |ui| {
ui.vertical(|ui| {
for each in &mut tree.root {
match each {
super::ui_extensions::StringTreeElement::None => {}
super::ui_extensions::StringTreeElement::Element(name, children) => {
ui.horizontal(|ui| {
ui.label(">".to_owned());
if ui.button(name.clone()).clicked() {
//
ui.memory_mut(|mem| {
mem.toggle_popup(state.popup_identifier.unwrap())
});
}
});
for each in children.as_mut() {
show_child(ui, each, 1);
}
}
}
}
});
});
}
pub fn show_child(ui: &mut egui::Ui, subject: &mut StringTreeElement, depth: usize) {
//
match subject {
StringTreeElement::None => {}
StringTreeElement::Element(name, children) => {
//
let mut prec = String::from("");
for i in 0..depth {
if i == 0 {
prec = prec + "+";
} else {
prec = prec + "+";
}
}
prec = prec + ">";
ui.horizontal(|ui| {
ui.label(prec);
ui.button(name.clone()).clicked();
});
for each in children.as_mut() {
show_child(ui, each, depth + 1);
}
}
}
}
fn color_from(egui_color: Hsva) -> Color {
let rgb = rgb_from_hsv((egui_color.h, egui_color.s, egui_color.v));
return Color::rgb_from_array(rgb);
}
fn egui_color_from(color: Color) -> Hsva {
let x = hsv_from_rgb([color.r(), color.g(), color.b()]);
return Hsva::new(x.0, x.1, x.2, 1.);
}
#[test]
pub fn round_trip_color() {
use rand::Rng;
let mut rng = rand::thread_rng();
let min = 0.;
let max = 1.;
for _ in 0..10000 {
let r = rng.gen_range(min..max);
let g = rng.gen_range(min..max);
let b = rng.gen_range(min..max);
let mut color = Color::rgb(r, g, b);
let mut egui_color = egui_color_from(color);
color = color_from(egui_color);
egui_color = egui_color_from(color);
color = color_from(egui_color);
egui_color = egui_color_from(color);
color = color_from(egui_color);
egui_color = egui_color_from(color);
color = color_from(egui_color);
egui_color = egui_color_from(color);
color = color_from(egui_color);
egui_color = egui_color_from(color);
color = color_from(egui_color);
egui_color = egui_color_from(color);
color = color_from(egui_color);
egui_color = egui_color_from(color);
color = color_from(egui_color);
assert_eq!((color.r() * 255.).floor(), (r * 255.).floor());
assert_eq!((color.g() * 255.).floor(), (g * 255.).floor());
assert_eq!((color.b() * 255.).floor(), (b * 255.).floor());
}
}