Compare commits

...

10 commits

8 changed files with 575 additions and 104 deletions

View file

@ -2,43 +2,29 @@
Mostly a working level editor, why should you use it? because it exists and allows to make levels without tilemaps... Mostly a working level editor, why should you use it? because it exists and allows to make levels without tilemaps...
## Usage
Left mouse click for mostly everything, when creating a shape you it will build the shape as you go(giving you a visual indicator).
If you want to cancel a shape mid-build you can right click to undo the shape
When an item is selected in the items tree window(will be written in gold), clicking `X` will delete it
## TODO ## TODO
- [x] Items tree view showing all shapes with their child nodes and purposes(maybe location and such as well?) - [x] Pick default color for shapes
- [x] Change shape name in tree view - [x] Undo/Redo history(ctrl + Z/ctrl + shift + Z)
- [x] Delete shapes in tree view - I should probably make it ask for confirmation... - [ ] Select item by clicking/double clicking on the relevant shape/image
- [x] Highlight opened shapes in the tree(maybe allow only 1 opened shape? - I dont think egui allows me to do that)
- [x] Drag camera around
- [x] Zome in/out
- [x] Import images:
- [x] Insert images
- [x] Show images in tree view
- [x] Show/Hide images
- [x] Move/Drag images around
- [x] Control images Z value(so we could reorder them)
- [x] Delete images
- [x] Name images
- [x] Scale images
- [x] Show hide shapes
- [x] Control shape Z value
- [x] Snap to grid
- [x] Change grid size
- [x] Show/Hide grid(also make a visible grid in the first place)
- [x] Make grid fill the screen at all times
- [x] Comment/Editor note for shapes
- [x] Export
- [x] Import
## Quality of life todo ## Quality of life todo
- [ ] Double click on shape in tree view will center the relevant shape - [ ] Double click on shape in tree view will center the relevant shape
- [ ] Grab shapes instead of center points - [ ] Grab shapes instead of center points
- [ ] Reorder items tree - [ ] Reorder items tree
- [ ] Select item by clicking/double clicking on the relevant shape/image
- [ ] Allow for more types of export/import - [ ] Allow for more types of export/import
- [ ] Allow to set a relative export path for images(also allow to select when importing/save it in the saved file) - [ ] Allow to set a relative export path for images(also allow to select when importing/save it in the saved file)
- [ ] Group shapes/images under an empty/another object with an actual tree view
## Maybe, just maybe todo ## Maybe, just maybe todo
- [ ] Group shapes/images under an empty/another object with an actual tree view
- [ ] Duplicate shapes/images - [ ] Duplicate shapes/images

View file

@ -5,7 +5,9 @@ use bevy_prototype_lyon::prelude::*;
pub fn create_sys( pub fn create_sys(
mut coms: Commands, mut coms: Commands,
mut undo: ResMut<undo::UndoStack>,
p_size: Res<PointSize>, p_size: Res<PointSize>,
default_color: Res<DefaultColor>,
state: Res<UiState>, state: Res<UiState>,
mouse: Res<Input<MouseButton>>, mouse: Res<Input<MouseButton>>,
wnds: Res<Windows>, wnds: Res<Windows>,
@ -59,6 +61,9 @@ pub fn create_sys(
ms.add_child(*e); ms.add_child(*e);
} }
ms.insert(s); ms.insert(s);
undo.push(UndoItem::CreateShape { entity: ms.id() });
*shape = None; *shape = None;
} }
} }
@ -76,7 +81,7 @@ pub fn create_sys(
else if mouse.just_released(MouseButton::Left) { else if mouse.just_released(MouseButton::Left) {
// We can now spawn a shape in the current mouse position... // We can now spawn a shape in the current mouse position...
// Spawn the first point // Spawn the first point
*shape = Some(create_new_shape(&mut coms, mouse_pos, state.create_shape, p_size.0)); *shape = Some(create_new_shape(&mut coms, mouse_pos, state.create_shape, p_size.0, **default_color));
} }
} }
} }
@ -275,10 +280,10 @@ fn update_main_shape_creation(
} }
} }
fn create_new_shape(coms: &mut Commands, pos: Vec2, create_shape: CreateShape, p_size: f32) -> ShapeData { fn create_new_shape(coms: &mut Commands, pos: Vec2, create_shape: CreateShape, p_size: f32, color: Color) -> ShapeData {
// Shape draw mode... // Shape draw mode...
let draw_mode = DrawMode::Outlined { let draw_mode = DrawMode::Outlined {
fill_mode: FillMode::color(Color::rgba(0.0, 0.5, 0.5, 0.4)), fill_mode: FillMode::color(color),
outline_mode: StrokeMode::new(Color::rgba(0.0, 0.5, 0.5, 0.6), 3.0), outline_mode: StrokeMode::new(Color::rgba(0.0, 0.5, 0.5, 0.6), 3.0),
}; };

View file

@ -42,7 +42,7 @@ pub struct ImportItem {
pub data: ExportData, pub data: ExportData,
} }
pub fn toml_save_sys( pub fn json_save_sys(
mut imp_exp: ResMut<ShouldImportExport>, mut imp_exp: ResMut<ShouldImportExport>,
shapes: Query<&ShapeData>, shapes: Query<&ShapeData>,
images: Query<(&ImageData, &Transform)>, images: Query<(&ImageData, &Transform)>,
@ -107,7 +107,6 @@ pub fn import_sys(
mut imp_exp: ResMut<ShouldImportExport>, mut imp_exp: ResMut<ShouldImportExport>,
assets: Res<AssetServer>, assets: Res<AssetServer>,
p_size: Res<PointSize>, p_size: Res<PointSize>,
) { ) {
imp_exp.import = false; imp_exp.import = false;
let file = rfd::FileDialog::new() let file = rfd::FileDialog::new()

View file

@ -1,3 +1,5 @@
#![feature(let_chains)]
use bevy::prelude::*; use bevy::prelude::*;
use bevy_egui::egui; use bevy_egui::egui;
@ -7,10 +9,15 @@ pub mod modify;
pub mod ui; pub mod ui;
pub mod infinite_grid; pub mod infinite_grid;
pub mod export; pub mod export;
pub mod undo;
pub use undo::UndoItem;
pub use modify::modify_sys; pub use modify::modify_sys;
pub use create::create_sys; pub use create::create_sys;
pub use helpers::*; pub use helpers::*;
#[derive(Debug, Clone, Deref, DerefMut)]
pub struct DefaultColor(pub Color);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SnapGrid { pub struct SnapGrid {
pub width: f32, pub width: f32,
@ -41,14 +48,14 @@ impl Default for SnapGrid {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PointSize(pub f32); pub struct PointSize(pub f32);
#[derive(Component, Debug)] #[derive(Component, Debug, Default, Clone)]
pub struct ImageData { pub struct ImageData {
pub name: String, pub name: String,
pub note: String, pub note: String,
pub path: String, pub path: String,
} }
#[derive(Component, Debug)] #[derive(Component, Debug, Clone)]
pub struct ShapeData { pub struct ShapeData {
pub name: String, pub name: String,
pub shape: CreateShape, pub shape: CreateShape,
@ -57,6 +64,18 @@ pub struct ShapeData {
pub center: Option<Entity>, pub center: Option<Entity>,
pub note: String, pub note: String,
} }
impl ShapeData {
pub fn shallow_copy(&self) -> Self {
Self {
name: Default::default(),
shape: self.shape,
main_shape: self.main_shape,
edges: Default::default(),
center: Default::default(),
note: Default::default()
}
}
}
#[derive(Component)] #[derive(Component)]
pub struct MainCamera; pub struct MainCamera;

View file

@ -28,6 +28,8 @@ fn main() {
.insert_resource(SnapGrid::default()) .insert_resource(SnapGrid::default())
.insert_resource(ui::SelectedItem::default()) .insert_resource(ui::SelectedItem::default())
.insert_resource(ShouldImportExport::default()) .insert_resource(ShouldImportExport::default())
.insert_resource(DefaultColor(Color::rgba(0.0, 0.5, 0.5, 0.4)))
.insert_resource(undo::UndoStack::default())
; ;
app app
@ -65,12 +67,13 @@ fn main() {
.add_system(delete_selected_item_on_del_sys.with_run_criteria(|mut ec: ResMut<EguiContext>| .add_system(delete_selected_item_on_del_sys.with_run_criteria(|mut ec: ResMut<EguiContext>|
if ec.ctx_mut().wants_keyboard_input() { ShouldRun::No } else { ShouldRun::Yes } if ec.ctx_mut().wants_keyboard_input() { ShouldRun::No } else { ShouldRun::Yes }
)) ))
.add_system(export::toml_save_sys.with_run_criteria(|ie: Res<ShouldImportExport>| { .add_system(export::json_save_sys.with_run_criteria(|ie: Res<ShouldImportExport>| {
if ie.export { ShouldRun::Yes } else { ShouldRun::No } if ie.export { ShouldRun::Yes } else { ShouldRun::No }
})) }))
.add_system(export::import_sys.with_run_criteria(|ie: Res<ShouldImportExport>| { .add_system(export::import_sys.with_run_criteria(|ie: Res<ShouldImportExport>| {
if ie.import { ShouldRun::Yes} else { ShouldRun::No } if ie.import { ShouldRun::Yes} else { ShouldRun::No }
})) }))
.add_system(undo::undo_redo_sys)
; ;
app.run(); app.run();
@ -92,11 +95,35 @@ fn configure_visuals(mut egui_ctx: ResMut<EguiContext>) {
fn delete_selected_item_on_del_sys( fn delete_selected_item_on_del_sys(
mut coms: Commands, mut coms: Commands,
mut undo: ResMut<undo::UndoStack>,
mut selected: ResMut<SelectedItem>, mut selected: ResMut<SelectedItem>,
keyboard: Res<Input<KeyCode>>, keyboard: Res<Input<KeyCode>>,
mut shapes: Query<&mut ShapeData>,
mut images: Query<&mut ImageData>,
transforms: Query<&Transform>,
image_handles: Query<&Handle<Image>>,
draw_modes: Query<&DrawMode>,
) { ) {
if keyboard.just_pressed(KeyCode::Delete) || keyboard.just_pressed(KeyCode::X) { if keyboard.just_pressed(KeyCode::Delete) || keyboard.just_pressed(KeyCode::X) {
if let Some(e) = selected.0 { if let Some(e) = selected.0 {
if let Ok(mut sd) = shapes.get_mut(e) {
let mut name = String::new();
let mut note = String::new();
std::mem::swap(&mut name, &mut sd.name);
std::mem::swap(&mut note, &mut sd.note);
let edges = sd.edges.iter().map(|e| transforms.get(*e).unwrap().translation.xy()).collect::<Vec<Vec2>>();
let transform = transforms.get(e).map(|t| *t).unwrap_or_default();
let dm = *draw_modes.get(e).unwrap();
undo.push(UndoItem::DeleteShape { transform, edges, name, note, shape: sd.shape, dm, old_entity: e });
}
else if let Ok(mut id) = images.get_mut(e) {
let mut nid = ImageData::default();
std::mem::swap(&mut nid, &mut *id);
let transform = transforms.get(e).map(|t| *t).unwrap_or_default();
let handle = image_handles.get(e).unwrap().clone();
undo.push(UndoItem::DeleteImage { id: nid, transform: transform, handle, old_entity: e });
}
coms.entity(e).despawn_recursive(); coms.entity(e).despawn_recursive();
selected.0 = None; selected.0 = None;
} }

View file

@ -16,6 +16,8 @@ pub enum Holding {
pub fn modify_sys( pub fn modify_sys(
// Which entity the user currently drags, and which specific part of it(ShapeData entity, path entity) // Which entity the user currently drags, and which specific part of it(ShapeData entity, path entity)
mut holding: Local<Holding>, mut holding: Local<Holding>,
mut held_from: Local<Vec2>,
mut undo: ResMut<undo::UndoStack>,
mouse: Res<Input<MouseButton>>, mouse: Res<Input<MouseButton>>,
wnds: Res<Windows>, wnds: Res<Windows>,
scale: Query<&OrthographicProjection, With<MainCamera>>, scale: Query<&OrthographicProjection, With<MainCamera>>,
@ -38,8 +40,8 @@ pub fn modify_sys(
for (e, sd) in shapes.iter() { for (e, sd) in shapes.iter() {
if let Ok(t) = gtransforms.get(sd.center.unwrap()) { if let Ok(t) = gtransforms.get(sd.center.unwrap()) {
let t = t.translation().xy(); let t = t.translation().xy();
if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) { if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) {
*held_from = t;
*holding = Holding::Shape(e, sd.center.unwrap()); *holding = Holding::Shape(e, sd.center.unwrap());
break; break;
} }
@ -49,6 +51,7 @@ pub fn modify_sys(
let t = t.translation().xy(); let t = t.translation().xy();
if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) { if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) {
*held_from = t;
*holding = Holding::Shape(e, *edge); *holding = Holding::Shape(e, *edge);
break; break;
} }
@ -65,6 +68,7 @@ pub fn modify_sys(
// Disregard rotations for now plz // Disregard rotations for now plz
let diff = (mouse_pos - t.translation().xy()).abs(); let diff = (mouse_pos - t.translation().xy()).abs();
if diff.x < size.x * scale.x && diff.y < size.y * scale.y { if diff.x < size.x * scale.x && diff.y < size.y * scale.y {
*held_from = t.translation().xy();
*holding = Holding::Image(e, mouse_pos - t.translation().xy()); *holding = Holding::Image(e, mouse_pos - t.translation().xy());
break; break;
} }
@ -73,6 +77,28 @@ pub fn modify_sys(
} }
} }
else if mouse.just_released(MouseButton::Left) && *holding != Holding::None { else if mouse.just_released(MouseButton::Left) && *holding != Holding::None {
match *holding {
Holding::Shape(shape_data, dot) => {
if let Ok(sd) = shapes.get_component::<ShapeData>(shape_data) {
if sd.center == Some(dot) {
undo.push(UndoItem::MoveItem { shape: shape_data, from: *held_from, to: snap.snap_to_grid(mouse_pos) });
}
else {
undo.push(UndoItem::MoveDot {
shape_data,
dot: sd.edges.iter().position(|e| *e == dot).unwrap_or(0),
from: *held_from,
to: snap.snap_to_grid(mouse_pos),
});
}
}
},
Holding::Image(e, off) => {
// ye i know it says MoVeShaPe but really it doesnt matter that much here!
undo.push(UndoItem::MoveItem { shape: e, from: *held_from, to: snap.snap_to_grid(mouse_pos - off) });
},
Holding::None => {},
}
*holding = Holding::None; // We just released our sad little dot/shape *holding = Holding::None; // We just released our sad little dot/shape
} }
else if let Holding::Shape(se, pe) = *holding { else if let Holding::Shape(se, pe) = *holding {

195
src/ui.rs
View file

@ -9,9 +9,11 @@ pub struct SelectedItem(pub Option<Entity>);
pub fn action_bar_sys( pub fn action_bar_sys(
mut coms: Commands, mut coms: Commands,
mut undo: ResMut<undo::UndoStack>,
assets: Res<AssetServer>, assets: Res<AssetServer>,
mut egui_ctx: ResMut<EguiContext>, mut egui_ctx: ResMut<EguiContext>,
mut state: ResMut<UiState>, mut state: ResMut<UiState>,
mut default_color: ResMut<DefaultColor>,
colors: Res<ButtonsColors>, colors: Res<ButtonsColors>,
) { ) {
egui::Window::new("buttons_float") egui::Window::new("buttons_float")
@ -42,11 +44,13 @@ pub fn action_bar_sys(
let name = String::from(file.file_name().unwrap().to_str().unwrap_or("Image")); let name = String::from(file.file_name().unwrap().to_str().unwrap_or("Image"));
let path = file.to_str().unwrap().to_string(); let path = file.to_str().unwrap().to_string();
let image: Handle<Image> = assets.load(file); let image: Handle<Image> = assets.load(file);
coms.spawn_bundle(SpriteBundle { let id = coms.spawn_bundle(SpriteBundle {
texture: image, texture: image,
..Default::default() ..Default::default()
}) })
.insert(ImageData { name, note: String::new(), path }); .insert(ImageData { name, note: String::new(), path }).id();
undo.push(UndoItem::CreateImage { entity: id });
} }
} }
@ -76,6 +80,14 @@ pub fn action_bar_sys(
// state.create_shape = CreateShape::Capsule; // state.create_shape = CreateShape::Capsule;
// } // }
}); });
ui.horizontal(|hui| {
hui.label("Color: ");
let c = **default_color;
let mut color = [c.r(), c.g(), c.b(), c.a()];
if hui.color_edit_button_rgba_unmultiplied(&mut color).changed() {
**default_color = Color::from(color);
}
});
} }
}); });
} }
@ -197,8 +209,10 @@ pub fn items_tree_sys(
} }
pub fn inspector_sys( pub fn inspector_sys(
mut selected: ResMut<SelectedItem>, // Which item is currently selected
mut coms: Commands, // For deletion! mut coms: Commands, // For deletion!
mut changes: Local<(f32, String)>,
mut undo: ResMut<undo::UndoStack>,
mut selected: ResMut<SelectedItem>, // Which item is currently selected
mut egui_ctx: ResMut<EguiContext>, mut egui_ctx: ResMut<EguiContext>,
mut shapes: Query<&mut ShapeData>, mut shapes: Query<&mut ShapeData>,
mut transforms: Query<&mut Transform>, mut transforms: Query<&mut Transform>,
@ -207,6 +221,7 @@ pub fn inspector_sys(
mut draw_modes: Query<&mut DrawMode, With<Path>>, mut draw_modes: Query<&mut DrawMode, With<Path>>,
mut visible: Query<&mut Visibility, Or<(With<Path>, With<Sprite>)>>, mut visible: Query<&mut Visibility, Or<(With<Path>, With<Sprite>)>>,
mut images: Query<&mut ImageData, With<Sprite>>, mut images: Query<&mut ImageData, With<Sprite>>,
image_handles: Query<&Handle<Image>, With<ImageData>>,
) { ) {
if let Some(e) = **selected { if let Some(e) = **selected {
let mut open = true; let mut open = true;
@ -219,40 +234,119 @@ pub fn inspector_sys(
.open(&mut open) .open(&mut open)
.resizable(false) .resizable(false)
.show(egui_ctx.ctx_mut(), |ui| { .show(egui_ctx.ctx_mut(), |ui| {
if let Ok(mut sd) = shapes.get_mut(e) { let mut shape = shapes.get_mut(e);
let mut image = images.get_mut(e);
if shape.is_err() && image.is_err() {
bevy::log::error!("Selected item is not a shape nor an image, so... weird");
**selected = None;
return;
}
let name = if let Ok(ref mut sd) = shape { &mut sd.name }
else if let Ok(ref mut id) = image { &mut id.name }
else { unreachable!() };
ui.horizontal(|hui| { ui.horizontal(|hui| {
hui.label("Name:"); hui.label("Name:");
hui.text_edit_singleline(&mut sd.name); let te = hui.text_edit_singleline(name);
if te.gained_focus() {
changes.1 = name.clone();
}
if te.lost_focus() {
let mut new = String::new();
std::mem::swap(&mut new, &mut changes.1);
undo.push(UndoItem::NameChange { entity: e, from: new });
}
}); });
ui.separator(); ui.separator();
if let Ok(mut t) = transforms.get_mut(sd.main_shape) { if let Ok(mut t) = transforms.get_mut(e) {
ui.horizontal(|hui| { ui.horizontal(|hui| {
hui.label("Translation:"); hui.label("Translation:");
hui.add(egui::DragValue::new(&mut t.translation.x)); let xdrag = hui.add(egui::DragValue::new(&mut t.translation.x));
hui.add(egui::DragValue::new(&mut t.translation.y)); if started_edit(&xdrag) {
changes.0 = t.translation.x;
}
else if stopped_edit(&xdrag) {
undo.push(UndoItem::MoveItem { shape: e, from: Vec2::new(changes.0, t.translation.y), to: t.translation.xy() });
}
let ydrag = hui.add(egui::DragValue::new(&mut t.translation.y));
if started_edit(&ydrag) {
changes.0 = t.translation.y;
}
else if stopped_edit(&ydrag) {
undo.push(UndoItem::MoveItem { shape: e, from: Vec2::new(t.translation.x , changes.0), to: t.translation.xy() });
}
}); });
ui.horizontal(|hui| { ui.horizontal(|hui| {
hui.label("Rotation:"); hui.label("Rotation:");
let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees(); let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees();
if hui.add(egui::DragValue::new(&mut rot).suffix("°")).changed() { let drag = hui.add(egui::DragValue::new(&mut rot).suffix("°"));
if started_edit(&drag) {
changes.0 = rot;
}
if stopped_edit(&drag) {
undo.push(UndoItem::Rotate { entity: e, from: changes.0, to: rot });
}
if drag.changed() {
t.rotation = Quat::from_rotation_z(rot.to_radians()); t.rotation = Quat::from_rotation_z(rot.to_radians());
} }
hui.label("Z:"); hui.label("Z:");
hui.add(egui::DragValue::new(&mut t.translation.z).clamp_range(0..=i32::MAX)); let z = hui.add(egui::DragValue::new(&mut t.translation.z).clamp_range(0..=i32::MAX));
if started_edit(&z) {
changes.0 = t.translation.z;
}
if stopped_edit(&z) {
undo.push(UndoItem::ChangeZ { entity: e, from: changes.0, to: t.translation.z });
}
});
if image.is_ok() {
ui.horizontal(|hui| {
hui.label("Scale:");
let xs = hui.add(egui::DragValue::new(&mut t.scale.x).speed(0.01));
if started_edit(&xs) {
changes.0 = t.scale.x;
}
if stopped_edit(&xs) {
undo.push(UndoItem::ReScale { entity: e, from: Vec2::new(changes.0, t.scale.y), to: t.scale.xy() });
}
let ys = hui.add(egui::DragValue::new(&mut t.scale.y).speed(0.01));
if started_edit(&ys) {
changes.0 = t.scale.y;
}
if stopped_edit(&ys) {
undo.push(UndoItem::ReScale { entity: e, from: Vec2::new(t.scale.x, changes.0), to: t.scale.xy() });
}
}); });
} }
}
ui.separator(); ui.separator();
if let Ok(ref mut sd) = shape {
for (i, edge) in sd.edges.iter().enumerate() { for (i, edge) in sd.edges.iter().enumerate() {
ui.horizontal(|hui| { ui.horizontal(|hui| {
hui.label(format!("Edge {}", i)); hui.label(format!("Edge {}", i));
let gt = global_transforms.get(*edge).unwrap(); let gt = global_transforms.get(*edge).unwrap();
let mut gt_x = gt.translation().x; let mut gt_x = gt.translation().x;
let mut gt_y = gt.translation().y; let mut gt_y = gt.translation().y;
let c1 = hui.add(egui::DragValue::new(&mut gt_x)).changed(); let rx = hui.add(egui::DragValue::new(&mut gt_x));
let c2 = hui.add(egui::DragValue::new(&mut gt_y)).changed(); let ry = hui.add(egui::DragValue::new(&mut gt_y));
if c1 || c2 { if started_edit(&rx) {
changes.0 = gt_x;
}
if started_edit(&ry) {
changes.0 = gt_y;
}
if stopped_edit(&rx) {
undo.push(UndoItem::MoveDot { shape_data: e, dot: i, from: Vec2::new(changes.0, gt_y), to: Vec2::new(gt_x, gt_y) });
}
if stopped_edit(&ry) {
undo.push(UndoItem::MoveDot { shape_data: e, dot: i, from: Vec2::new(gt_x, changes.0), to: Vec2::new(gt_x, gt_y) });
}
if rx.changed() || ry.changed() {
let rot = gt.to_scale_rotation_translation().1; let rot = gt.to_scale_rotation_translation().1;
let ang = rot.to_euler(EulerRot::XYZ).2; let ang = rot.to_euler(EulerRot::XYZ).2;
let delta = Mat2::from_angle(-ang) * (Vec2::new(gt_x, gt_y) - gt.translation().xy()); let delta = Mat2::from_angle(-ang) * (Vec2::new(gt_x, gt_y) - gt.translation().xy());
@ -277,9 +371,11 @@ pub fn inspector_sys(
}); });
} }
ui.separator(); ui.separator();
}
if let Ok(mut v) = visible.get_mut(e) { if let Ok(mut v) = visible.get_mut(e) {
ui.checkbox(&mut v.is_visible, "Visible"); ui.checkbox(&mut v.is_visible, "Visible");
} }
if shape.is_ok() {
if let Ok(mut dm) = draw_modes.get_mut(e) { if let Ok(mut dm) = draw_modes.get_mut(e) {
ui.separator(); ui.separator();
if let DrawMode::Outlined { fill_mode: f, outline_mode: _ } = &mut *dm { if let DrawMode::Outlined { fill_mode: f, outline_mode: _ } = &mut *dm {
@ -293,55 +389,54 @@ pub fn inspector_sys(
} }
} }
ui.separator(); ui.separator();
}
let note = if let Ok(ref mut sd) = shape { &mut sd.note }
else if let Ok(ref mut id) = image { &mut id.note }
else { unreachable!() };
ui.label("Notes"); ui.label("Notes");
ui.text_edit_multiline(&mut sd.note); let ne = ui.text_edit_multiline(note);
if ne.gained_focus() {
changes.1 = note.clone();
ui.separator();
if ui.button("Delete").clicked() {
coms.entity(e).despawn_recursive();
**selected = None;
} }
} if ne.lost_focus() {
else if let Ok(mut name) = images.get_mut(e) { let mut new = String::new();
ui.horizontal(|hui| { std::mem::swap(&mut new, &mut changes.1);
hui.label("Name:"); undo.push(UndoItem::NoteChange { entity: e, from: new });
hui.text_edit_singleline(&mut name.name);
});
ui.separator();
if let Ok(mut t) = transforms.get_mut(e) {
ui.horizontal(|hui| {
hui.label("Translation:");
hui.add(egui::DragValue::new(&mut t.translation.x));
hui.add(egui::DragValue::new(&mut t.translation.y));
});
ui.horizontal(|hui| {
hui.label("Rotation:");
let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees();
if hui.add(egui::DragValue::new(&mut rot).suffix("°")).changed() {
t.rotation = Quat::from_rotation_z(rot.to_radians());
}
hui.label("Z:");
hui.add(egui::DragValue::new(&mut t.translation.z).clamp_range(0..=i32::MAX));
});
ui.horizontal(|hui| {
hui.label("Scale:");
hui.add(egui::DragValue::new(&mut t.scale.x).speed(0.01));
hui.add(egui::DragValue::new(&mut t.scale.y).speed(0.01));
});
} }
ui.separator(); ui.separator();
if ui.button("Delete").clicked() { if ui.button("Delete").clicked() {
if let Ok(mut sd) = shape {
let mut name = String::new();
let mut note = String::new();
std::mem::swap(&mut name, &mut sd.name);
std::mem::swap(&mut note, &mut sd.note);
let edges = sd.edges.iter().map(|e| transforms.get(*e).unwrap().translation.xy()).collect::<Vec<Vec2>>();
let transform = transforms.get(e).map(|t| *t).unwrap_or_default();
let dm = *draw_modes.get(e).unwrap();
undo.push(UndoItem::DeleteShape { transform, edges, name, note, shape: sd.shape, dm, old_entity: e });
}
else if let Ok(mut id) = image {
let mut nid = ImageData::default();
std::mem::swap(&mut nid, &mut *id);
let transform = transforms.get(e).map(|t| *t).unwrap_or_default();
let handle = image_handles.get(e).unwrap().clone();
undo.push(UndoItem::DeleteImage { id: nid, transform: transform, handle, old_entity: e });
}
coms.entity(e).despawn_recursive(); coms.entity(e).despawn_recursive();
**selected = None; **selected = None;
} }
}
}); });
if !open { if !open {
**selected = None; **selected = None;
} }
} }
} }
fn started_edit(res: &egui::Response) -> bool {
res.drag_started() || res.gained_focus()
}
fn stopped_edit(res :&egui::Response) -> bool {
res.drag_released() || res.lost_focus()
}

314
src/undo.rs Normal file
View file

@ -0,0 +1,314 @@
use bevy::math::Vec3Swizzles;
use bevy::prelude::*;
use bevy_prototype_lyon::prelude::*;
use crate::{ShapeData, ImageData, CreateShape, PointSize};
#[derive(Debug, Clone)]
pub enum UndoItem {
Base,
CreateShape { entity: Entity },
DeleteShape { transform: Transform, edges: Vec<Vec2>, name: String, note: String, shape: CreateShape, dm: DrawMode, old_entity: Entity },
MoveItem { shape: Entity, from: Vec2, to: Vec2 },
MoveDot { shape_data: Entity, dot: usize, from: Vec2, to: Vec2 },
Rotate { entity: Entity, from: f32, to: f32 },
NameChange { entity: Entity, from: String },
NoteChange { entity: Entity, from: String },
ChangeZ { entity: Entity, from: f32, to: f32 },
ReScale { entity: Entity, from: Vec2, to: Vec2 },
CreateImage { entity: Entity },
DeleteImage { id: ImageData, transform: Transform, handle: Handle<Image>, old_entity: Entity },
}
pub struct UndoStack {
items: Vec<UndoItem>,
current_action: usize,
last_valid: usize,
}
impl Default for UndoStack {
fn default() -> Self {
Self { items: [UndoItem::Base].to_vec(), current_action: 0, last_valid: 0 }
}
}
impl UndoStack {
pub fn push(&mut self, item: UndoItem) {
bevy::log::info!("PUSHING: {:?}", item);
if self.current_action < self.items.len() - 1 {
// We need to do a "semi push"
self.current_action += 1;
self.items[self.current_action] = item;
}
else {
// We push a completly new item and might need more space!
self.items.push(item);
self.current_action = self.items.len() - 1;
}
self.last_valid = self.current_action;
}
/// Pop the last item in the stack
pub fn pop(&mut self) -> Option<&mut UndoItem> {
// If this happens we are clearly into invalid territory
assert!(self.current_action < self.items.len());
if self.current_action > 0 {
let item = &mut self.items[self.current_action];
self.current_action -= 1;
Some(item)
}
else {
None
}
}
// If exists, "unpops" the last popped item
pub fn unpop(&mut self) -> Option<&mut UndoItem> {
if self.current_action < self.last_valid {
self.current_action += 1;
let item = &mut self.items[self.current_action];
Some(item)
}
else {
None
}
}
}
pub fn undo_redo_sys(
mut coms: Commands,
mut stack: ResMut<UndoStack>,
keyboard: Res<Input<KeyCode>>,
p_size: Res<PointSize>,
mut shapes: Query<&mut ShapeData>,
mut transforms: Query<&mut Transform>,
global_transforms: Query<&GlobalTransform>,
mut images: Query<(&mut ImageData, &Handle<Image>)>,
mut paths: Query<&mut bevy_prototype_lyon::prelude::Path>,
draw_modes: Query<&DrawMode>,
) {
if keyboard.just_pressed(KeyCode::Z) && keyboard.pressed(KeyCode::LControl) {
let item = if keyboard.pressed(KeyCode::LShift) { stack.unpop() } else { stack.pop() };
if let Some(item) = item {
match item {
UndoItem::Base => bevy::log::error!("POP/UNPOP: Got UndoItem::Base as a result!"),
UndoItem::CreateShape { entity } => {
let transform = transforms.get(*entity).map(|t| *t).unwrap_or_default();
let mut sd = shapes.get_mut(*entity).unwrap();
let mut name = String::new();
let mut note = String::new();
std::mem::swap(&mut name, &mut sd.name);
std::mem::swap(&mut note, &mut sd.note);
let edges = sd.edges.iter().map(|e| transforms.get(*e).unwrap().translation.xy()).collect::<Vec<Vec2>>();
let dm = *draw_modes.get(*entity).unwrap();
let shape = sd.shape;
coms.entity(*entity).despawn_recursive();
*item = UndoItem::DeleteShape { transform, edges, name, note, shape, dm, old_entity: *entity };
},
UndoItem::DeleteShape {
transform,
edges,
name,
note,
shape,
dm,
old_entity,
} => {
let main_shape = spawn_main_shape(&mut coms, *dm, *transform, *shape, edges.clone());
let dots_dm = DrawMode::Fill(FillMode::color(Color::RED));
let dot_shape = shapes::Circle { radius: p_size.0, center: Vec2::ZERO };
let center = coms.spawn_bundle(GeometryBuilder::build_as(
&dot_shape,
dots_dm,
Transform::from_xyz(0.0, 0.0, 0.5)
)).id();
let edges = edges.iter().map(|v| {
coms.spawn_bundle(GeometryBuilder::build_as(
&dot_shape,
dots_dm,
Transform::from_translation(v.extend(0.5))
)).id()
}).collect::<Vec<Entity>>();
let mut nname = String::new();
let mut nnote = String::new();
std::mem::swap(&mut nname, name);
std::mem::swap(&mut nnote, note);
let mut main_shape = coms.entity(main_shape);
edges.iter().for_each(|e| { main_shape.add_child(*e); });
let sd = ShapeData { name: nname, shape: *shape, main_shape: main_shape.id(), edges, center: Some(center), note: nnote };
main_shape
.insert(sd)
.add_child(center);
let oe = *old_entity;
*item = UndoItem::CreateShape { entity: main_shape.id() };
let f = |ne: &mut Entity| { if *ne == oe { *ne = main_shape.id(); }};
for i in stack.items.iter_mut() {
match i {
UndoItem::CreateShape { entity } => f(entity),
UndoItem::MoveDot { shape_data, ..} => f(shape_data),
UndoItem::MoveItem { shape, .. } => f(shape),
UndoItem::Rotate { entity, .. } => f(entity),
UndoItem::NameChange { entity, .. } => f(entity),
UndoItem::NoteChange { entity, .. } => f(entity),
UndoItem::ChangeZ { entity, .. } => f(entity),
UndoItem::ReScale { entity, .. } => f(entity),
_ => {},
}
}
},
UndoItem::MoveItem { shape, from, to } => {
if let Ok(mut t) = transforms.get_mut(*shape) {
t.translation = from.extend(t.translation.z);
}
let t = *from;
*from = *to;
*to = t;
},
UndoItem::MoveDot { shape_data, dot, from, to } => {
let sd = shapes.get(*shape_data).unwrap();
let dot = sd.edges[*dot];
let gt = global_transforms.get(dot).unwrap();
let rot = gt.to_scale_rotation_translation().1;
let ang = rot.to_euler(EulerRot::XYZ).2;
let delta = Mat2::from_angle(-ang) * (*from - gt.translation().xy());
if let Ok(mut t) = transforms.get_mut(dot) {
t.translation += delta.extend(0.0);
}
// We need to recalculate the center, and update the points to be the new relevant point from the center
let center_offset = crate::modify::calc_shape_center_offset(&transforms, &*sd);
// Update each edge's offset, and then move the main shape's translation
for edge in sd.edges.iter() {
if let Ok(mut t) = transforms.get_mut(*edge) {
t.translation -= center_offset.extend(0.0);
}
}
if let Ok(mut t) = transforms.get_mut(sd.main_shape) {
t.translation += (Mat2::from_angle(ang) * center_offset).extend(0.0);
}
// Now we need to update the shape itself
crate::modify::update_main_shape(&mut paths, &transforms, &*sd);
let t = *from;
*from = *to;
*to = t;
},
UndoItem::Rotate { entity, from, to } => {
if let Ok(mut t) = transforms.get_mut(*entity) {
t.rotation = Quat::from_rotation_z(from.to_radians());
}
let t = *from;
*from = *to;
*to = t;
},
UndoItem::NameChange { entity, from } => {
if let Ok(mut sd) = shapes.get_mut(*entity) {
std::mem::swap(&mut sd.name, from);
}
else if let Ok((mut id, _)) = images.get_mut(*entity) {
std::mem::swap(&mut id.name, from);
}
// No need to update the item, so no need to replace
},
UndoItem::ChangeZ { entity, from, to } => {
if let Ok(mut t) = transforms.get_mut(*entity) {
t.translation.z = *from;
}
let t = *from;
*from = *to;
*to = t;
},
UndoItem::ReScale { entity, from, to } => {
if let Ok(mut t) = transforms.get_mut(*entity) {
t.scale = from.extend(1.0);
}
let t = *from;
*from = *to;
*to = t;
},
UndoItem::CreateImage { entity } => {
if let Ok((mut id, handle)) = images.get_mut(*entity) {
let transform = transforms.get(*entity).map(|t| *t).unwrap_or(Transform::default());
let mut nid = ImageData::default();
std::mem::swap(&mut *id, &mut nid);
coms.entity(*entity).despawn_recursive();
*item = UndoItem::DeleteImage { id: nid, transform, handle: handle.clone(), old_entity: *entity };
}
},
UndoItem::DeleteImage { transform, handle, id, old_entity } => {
let mut nid = ImageData::default();
std::mem::swap(id, &mut nid);
let e = coms.spawn_bundle(SpriteBundle {
texture: handle.clone(),
transform: *transform,
..Default::default()
})
.insert(nid).id();
let oe = *old_entity;
*item = UndoItem::CreateImage { entity: e };
// Update each item with the same entity id
let f = |ne: &mut Entity| { if *ne == oe { *ne = e; }};
for i in stack.items.iter_mut() {
match i {
UndoItem::CreateShape { entity } => f(entity),
UndoItem::MoveItem { shape, .. } => f(shape),
UndoItem::Rotate { entity, .. } => f(entity),
UndoItem::NameChange { entity, .. } => f(entity),
UndoItem::NoteChange { entity, .. } => f(entity),
UndoItem::ChangeZ { entity, .. } => f(entity),
UndoItem::ReScale { entity, .. } => f(entity),
_ => {},
}
}
},
UndoItem::NoteChange { entity, from } => {
if let Ok(mut sd) = shapes.get_mut(*entity) {
std::mem::swap(&mut sd.note, from);
}
else if let Ok((mut id, _)) = images.get_mut(*entity) {
std::mem::swap(&mut id.note, from);
}
},
}
}
else {
bevy::log::info!("Nothing to pop/unpop!");
}
}
}
fn spawn_main_shape(coms: &mut Commands, dm: DrawMode, trans: Transform, shape: CreateShape, edges: Vec<Vec2>) -> Entity {
match shape {
CreateShape::Triangle => {
coms.spawn_bundle(GeometryBuilder::build_as(
&shapes::Polygon { points: edges, closed: true },
dm,
trans
)).id()
},
CreateShape::Square => {
coms.spawn_bundle(GeometryBuilder::build_as(
&shapes::Rectangle { extents: (edges[0] - edges[1]).abs(), ..Default::default() },
dm,
trans
)).id()
},
CreateShape::Circle => {
coms.spawn_bundle(GeometryBuilder::build_as(
&shapes::Circle { radius: edges[0].length(), center: Vec2::ZERO },
dm,
trans
)).id()
},
CreateShape::Capsule => unimplemented!("I should prob get rid of this..."),
}
}