605 lines
22 KiB
Rust
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());
|
|
}
|
|
}
|