Compare commits
10 commits
34fc13721b
...
9652ae1a08
Author | SHA1 | Date | |
---|---|---|---|
9652ae1a08 | |||
718a2c3f06 | |||
0b36b71b86 | |||
c8c3b79ac2 | |||
32c438ce16 | |||
92e3b9f620 | |||
0ddb8bdba4 | |||
a7eb757a8f | |||
b1d675a3a2 | |||
23605ab38a |
8 changed files with 575 additions and 104 deletions
38
README.md
38
README.md
|
@ -2,43 +2,29 @@
|
|||
|
||||
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
|
||||
|
||||
- [x] Items tree view showing all shapes with their child nodes and purposes(maybe location and such as well?)
|
||||
- [x] Change shape name in tree view
|
||||
- [x] Delete shapes in tree view - I should probably make it ask for confirmation...
|
||||
- [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
|
||||
- [x] Pick default color for shapes
|
||||
- [x] Undo/Redo history(ctrl + Z/ctrl + shift + Z)
|
||||
- [ ] Select item by clicking/double clicking on the relevant shape/image
|
||||
|
||||
## Quality of life todo
|
||||
|
||||
- [ ] Double click on shape in tree view will center the relevant shape
|
||||
- [ ] Grab shapes instead of center points
|
||||
- [ ] Reorder items tree
|
||||
- [ ] Select item by clicking/double clicking on the relevant shape/image
|
||||
- [ ] 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)
|
||||
- [ ] Group shapes/images under an empty/another object with an actual tree view
|
||||
|
||||
## Maybe, just maybe todo
|
||||
|
||||
- [ ] Group shapes/images under an empty/another object with an actual tree view
|
||||
- [ ] Duplicate shapes/images
|
||||
|
|
|
@ -5,7 +5,9 @@ use bevy_prototype_lyon::prelude::*;
|
|||
|
||||
pub fn create_sys(
|
||||
mut coms: Commands,
|
||||
mut undo: ResMut<undo::UndoStack>,
|
||||
p_size: Res<PointSize>,
|
||||
default_color: Res<DefaultColor>,
|
||||
state: Res<UiState>,
|
||||
mouse: Res<Input<MouseButton>>,
|
||||
wnds: Res<Windows>,
|
||||
|
@ -59,6 +61,9 @@ pub fn create_sys(
|
|||
ms.add_child(*e);
|
||||
}
|
||||
ms.insert(s);
|
||||
|
||||
undo.push(UndoItem::CreateShape { entity: ms.id() });
|
||||
|
||||
*shape = None;
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +81,7 @@ pub fn create_sys(
|
|||
else if mouse.just_released(MouseButton::Left) {
|
||||
// We can now spawn a shape in the current mouse position...
|
||||
// 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...
|
||||
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),
|
||||
};
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ pub struct ImportItem {
|
|||
pub data: ExportData,
|
||||
}
|
||||
|
||||
pub fn toml_save_sys(
|
||||
pub fn json_save_sys(
|
||||
mut imp_exp: ResMut<ShouldImportExport>,
|
||||
shapes: Query<&ShapeData>,
|
||||
images: Query<(&ImageData, &Transform)>,
|
||||
|
@ -107,7 +107,6 @@ pub fn import_sys(
|
|||
mut imp_exp: ResMut<ShouldImportExport>,
|
||||
assets: Res<AssetServer>,
|
||||
p_size: Res<PointSize>,
|
||||
|
||||
) {
|
||||
imp_exp.import = false;
|
||||
let file = rfd::FileDialog::new()
|
||||
|
|
23
src/lib.rs
23
src/lib.rs
|
@ -1,3 +1,5 @@
|
|||
#![feature(let_chains)]
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::egui;
|
||||
|
||||
|
@ -7,10 +9,15 @@ pub mod modify;
|
|||
pub mod ui;
|
||||
pub mod infinite_grid;
|
||||
pub mod export;
|
||||
pub mod undo;
|
||||
pub use undo::UndoItem;
|
||||
pub use modify::modify_sys;
|
||||
pub use create::create_sys;
|
||||
pub use helpers::*;
|
||||
|
||||
#[derive(Debug, Clone, Deref, DerefMut)]
|
||||
pub struct DefaultColor(pub Color);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SnapGrid {
|
||||
pub width: f32,
|
||||
|
@ -41,14 +48,14 @@ impl Default for SnapGrid {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct PointSize(pub f32);
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
#[derive(Component, Debug, Default, Clone)]
|
||||
pub struct ImageData {
|
||||
pub name: String,
|
||||
pub note: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
#[derive(Component, Debug, Clone)]
|
||||
pub struct ShapeData {
|
||||
pub name: String,
|
||||
pub shape: CreateShape,
|
||||
|
@ -57,6 +64,18 @@ pub struct ShapeData {
|
|||
pub center: Option<Entity>,
|
||||
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)]
|
||||
pub struct MainCamera;
|
||||
|
|
29
src/main.rs
29
src/main.rs
|
@ -28,6 +28,8 @@ fn main() {
|
|||
.insert_resource(SnapGrid::default())
|
||||
.insert_resource(ui::SelectedItem::default())
|
||||
.insert_resource(ShouldImportExport::default())
|
||||
.insert_resource(DefaultColor(Color::rgba(0.0, 0.5, 0.5, 0.4)))
|
||||
.insert_resource(undo::UndoStack::default())
|
||||
;
|
||||
|
||||
app
|
||||
|
@ -65,12 +67,13 @@ fn main() {
|
|||
.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 }
|
||||
))
|
||||
.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 }
|
||||
}))
|
||||
.add_system(export::import_sys.with_run_criteria(|ie: Res<ShouldImportExport>| {
|
||||
if ie.import { ShouldRun::Yes} else { ShouldRun::No }
|
||||
}))
|
||||
.add_system(undo::undo_redo_sys)
|
||||
;
|
||||
|
||||
app.run();
|
||||
|
@ -92,11 +95,35 @@ fn configure_visuals(mut egui_ctx: ResMut<EguiContext>) {
|
|||
|
||||
fn delete_selected_item_on_del_sys(
|
||||
mut coms: Commands,
|
||||
mut undo: ResMut<undo::UndoStack>,
|
||||
mut selected: ResMut<SelectedItem>,
|
||||
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 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();
|
||||
selected.0 = None;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ pub enum Holding {
|
|||
pub fn modify_sys(
|
||||
// Which entity the user currently drags, and which specific part of it(ShapeData entity, path entity)
|
||||
mut holding: Local<Holding>,
|
||||
mut held_from: Local<Vec2>,
|
||||
mut undo: ResMut<undo::UndoStack>,
|
||||
mouse: Res<Input<MouseButton>>,
|
||||
wnds: Res<Windows>,
|
||||
scale: Query<&OrthographicProjection, With<MainCamera>>,
|
||||
|
@ -38,8 +40,8 @@ pub fn modify_sys(
|
|||
for (e, sd) in shapes.iter() {
|
||||
if let Ok(t) = gtransforms.get(sd.center.unwrap()) {
|
||||
let t = t.translation().xy();
|
||||
|
||||
if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) {
|
||||
*held_from = t;
|
||||
*holding = Holding::Shape(e, sd.center.unwrap());
|
||||
break;
|
||||
}
|
||||
|
@ -49,6 +51,7 @@ pub fn modify_sys(
|
|||
let t = t.translation().xy();
|
||||
|
||||
if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) {
|
||||
*held_from = t;
|
||||
*holding = Holding::Shape(e, *edge);
|
||||
break;
|
||||
}
|
||||
|
@ -65,6 +68,7 @@ pub fn modify_sys(
|
|||
// Disregard rotations for now plz
|
||||
let diff = (mouse_pos - t.translation().xy()).abs();
|
||||
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());
|
||||
break;
|
||||
}
|
||||
|
@ -73,6 +77,28 @@ pub fn modify_sys(
|
|||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
else if let Holding::Shape(se, pe) = *holding {
|
||||
|
|
229
src/ui.rs
229
src/ui.rs
|
@ -9,9 +9,11 @@ pub struct SelectedItem(pub Option<Entity>);
|
|||
|
||||
pub fn action_bar_sys(
|
||||
mut coms: Commands,
|
||||
mut undo: ResMut<undo::UndoStack>,
|
||||
assets: Res<AssetServer>,
|
||||
mut egui_ctx: ResMut<EguiContext>,
|
||||
mut state: ResMut<UiState>,
|
||||
mut default_color: ResMut<DefaultColor>,
|
||||
colors: Res<ButtonsColors>,
|
||||
) {
|
||||
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 path = file.to_str().unwrap().to_string();
|
||||
let image: Handle<Image> = assets.load(file);
|
||||
coms.spawn_bundle(SpriteBundle {
|
||||
let id = coms.spawn_bundle(SpriteBundle {
|
||||
texture: image,
|
||||
..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;
|
||||
// }
|
||||
});
|
||||
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(
|
||||
mut selected: ResMut<SelectedItem>, // Which item is currently selected
|
||||
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 shapes: Query<&mut ShapeData>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
|
@ -207,6 +221,7 @@ pub fn inspector_sys(
|
|||
mut draw_modes: Query<&mut DrawMode, With<Path>>,
|
||||
mut visible: Query<&mut Visibility, Or<(With<Path>, With<Sprite>)>>,
|
||||
mut images: Query<&mut ImageData, With<Sprite>>,
|
||||
image_handles: Query<&Handle<Image>, With<ImageData>>,
|
||||
) {
|
||||
if let Some(e) = **selected {
|
||||
let mut open = true;
|
||||
|
@ -219,40 +234,119 @@ pub fn inspector_sys(
|
|||
.open(&mut open)
|
||||
.resizable(false)
|
||||
.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| {
|
||||
hui.label("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();
|
||||
if let Ok(mut t) = transforms.get_mut(e) {
|
||||
ui.horizontal(|hui| {
|
||||
hui.label("Name:");
|
||||
hui.text_edit_singleline(&mut sd.name);
|
||||
hui.label("Translation:");
|
||||
let xdrag = hui.add(egui::DragValue::new(&mut t.translation.x));
|
||||
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.separator();
|
||||
if let Ok(mut t) = transforms.get_mut(sd.main_shape) {
|
||||
ui.horizontal(|hui| {
|
||||
hui.label("Rotation:");
|
||||
let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees();
|
||||
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());
|
||||
}
|
||||
hui.label("Z:");
|
||||
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("Translation:");
|
||||
hui.add(egui::DragValue::new(&mut t.translation.x));
|
||||
hui.add(egui::DragValue::new(&mut t.translation.y));
|
||||
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.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.separator();
|
||||
}
|
||||
ui.separator();
|
||||
if let Ok(ref mut sd) = shape {
|
||||
for (i, edge) in sd.edges.iter().enumerate() {
|
||||
ui.horizontal(|hui| {
|
||||
hui.label(format!("Edge {}", i));
|
||||
let gt = global_transforms.get(*edge).unwrap();
|
||||
let mut gt_x = gt.translation().x;
|
||||
let mut gt_y = gt.translation().y;
|
||||
let c1 = hui.add(egui::DragValue::new(&mut gt_x)).changed();
|
||||
let c2 = hui.add(egui::DragValue::new(&mut gt_y)).changed();
|
||||
let rx = hui.add(egui::DragValue::new(&mut gt_x));
|
||||
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 ang = rot.to_euler(EulerRot::XYZ).2;
|
||||
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();
|
||||
if let Ok(mut v) = visible.get_mut(e) {
|
||||
ui.checkbox(&mut v.is_visible, "Visible");
|
||||
}
|
||||
}
|
||||
if let Ok(mut v) = visible.get_mut(e) {
|
||||
ui.checkbox(&mut v.is_visible, "Visible");
|
||||
}
|
||||
if shape.is_ok() {
|
||||
if let Ok(mut dm) = draw_modes.get_mut(e) {
|
||||
ui.separator();
|
||||
if let DrawMode::Outlined { fill_mode: f, outline_mode: _ } = &mut *dm {
|
||||
|
@ -293,51 +389,43 @@ pub fn inspector_sys(
|
|||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Notes");
|
||||
ui.text_edit_multiline(&mut sd.note);
|
||||
|
||||
|
||||
ui.separator();
|
||||
if ui.button("Delete").clicked() {
|
||||
coms.entity(e).despawn_recursive();
|
||||
**selected = None;
|
||||
}
|
||||
}
|
||||
else if let Ok(mut name) = images.get_mut(e) {
|
||||
ui.horizontal(|hui| {
|
||||
hui.label("Name:");
|
||||
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));
|
||||
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");
|
||||
let ne = ui.text_edit_multiline(note);
|
||||
if ne.gained_focus() {
|
||||
changes.1 = note.clone();
|
||||
}
|
||||
if ne.lost_focus() {
|
||||
let mut new = String::new();
|
||||
std::mem::swap(&mut new, &mut changes.1);
|
||||
undo.push(UndoItem::NoteChange { entity: e, from: new });
|
||||
}
|
||||
|
||||
});
|
||||
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();
|
||||
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 });
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
if ui.button("Delete").clicked() {
|
||||
coms.entity(e).despawn_recursive();
|
||||
**selected = None;
|
||||
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();
|
||||
**selected = None;
|
||||
}
|
||||
});
|
||||
if !open {
|
||||
|
@ -345,3 +433,10 @@ pub fn inspector_sys(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
314
src/undo.rs
Normal 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..."),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue