Implement Undo and Redo (#354)

* Implement undo and redo

* Create more save points and hook up menu entry

* Fix operation ordering

* Remove debug statement

* Fix folder changed order

* Don't store overlays in the history chain

* Keep selection
This commit is contained in:
TrueDoctor 2021-08-26 12:20:00 +02:00 committed by Keavon Chambers
parent 47fbb8d0fa
commit 0ccb181e2c
6 changed files with 133 additions and 38 deletions

View File

@ -19,6 +19,8 @@ use std::collections::VecDeque;
use super::movement_handler::{MovementMessage, MovementMessageHandler};
type DocumentSave = (InternalDocument, HashMap<Vec<LayerId>, LayerData>);
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum FlipAxis {
X,
@ -42,7 +44,8 @@ pub enum AlignAggregate {
#[derive(Clone, Debug)]
pub struct DocumentMessageHandler {
pub document: InternalDocument,
pub document_backup: Option<InternalDocument>,
pub document_history: Vec<DocumentSave>,
pub document_redo_history: Vec<DocumentSave>,
pub name: String,
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
movement_handler: MovementMessageHandler,
@ -52,7 +55,8 @@ impl Default for DocumentMessageHandler {
fn default() -> Self {
Self {
document: InternalDocument::default(),
document_backup: None,
document_history: Vec::new(),
document_redo_history: Vec::new(),
name: String::from("Untitled Document"),
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
@ -90,6 +94,10 @@ pub enum DocumentMessage {
SaveDocument,
RenderDocument,
Undo,
Redo,
DocumentHistoryBackward,
DocumentHistoryForward,
ClearOverlays,
NudgeSelectedLayers(f64, f64),
AlignSelectedLayers(AlignAxis, AlignAggregate),
MoveSelectedLayersTo {
@ -195,7 +203,8 @@ impl DocumentMessageHandler {
pub fn with_name(name: String) -> Self {
Self {
document: InternalDocument::default(),
document_backup: None,
document_history: Vec::new(),
document_redo_history: Vec::new(),
name,
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
@ -219,18 +228,42 @@ impl DocumentMessageHandler {
}
pub fn backup(&mut self) {
self.document_backup = Some(self.document.clone())
self.document_redo_history.clear();
let new_layer_data = self
.layer_data
.iter()
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
.collect();
self.document_history.push((self.document.clone_without_overlays(), new_layer_data))
}
pub fn rollback(&mut self) -> Result<(), EditorError> {
self.backup();
self.reset()
self.undo()
}
pub fn reset(&mut self) -> Result<(), EditorError> {
match self.document_backup.take() {
Some(backup) => {
self.document = backup;
pub fn undo(&mut self) -> Result<(), EditorError> {
match self.document_history.pop() {
Some((document, layer_data)) => {
let document = std::mem::replace(&mut self.document, document);
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
self.document_redo_history.push((document, layer_data));
Ok(())
}
None => Err(EditorError::NoTransactionInProgress),
}
}
pub fn redo(&mut self) -> Result<(), EditorError> {
match self.document_redo_history.pop() {
Some((document, layer_data)) => {
let document = std::mem::replace(&mut self.document, document);
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
let new_layer_data = layer_data
.iter()
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
.collect();
self.document_history.push((document.clone_without_overlays(), new_layer_data));
Ok(())
}
None => Err(EditorError::NoTransactionInProgress),
@ -284,10 +317,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
}
AbortTransaction => {
self.reset().unwrap_or_else(|e| log::warn!("{}", e));
self.undo().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
}
CommitTransaction => self.document_backup = None,
CommitTransaction => (),
ExportDocument => {
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
let size = bbox[1] - bbox[0];
@ -325,11 +358,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
)
}
SetBlendModeForSelectedLayers(blend_mode) => {
self.backup();
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
}
}
SetOpacityForSelectedLayers(opacity) => {
self.backup();
let opacity = opacity.clamp(0., 1.);
for path in self.selected_layers().cloned() {
@ -345,12 +380,20 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
SelectionChanged => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()),
DeleteSelectedLayers => {
self.backup();
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
responses.push_front(DocumentOperation::DeleteLayer { path }.into())
}
}
ClearOverlays => {
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
for path in self.layer_data.keys().filter(|path| self.document.layer(path).unwrap().overlay).cloned() {
responses.push_front(DocumentOperation::DeleteLayer { path }.into())
}
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
}
DuplicateSelectedLayers => {
self.backup();
for path in self.selected_layers_sorted() {
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
}
@ -365,7 +408,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
// TODO: Correctly update layer panel in clear_selection instead of here
responses.extend(self.handle_folder_changed(Vec::new()));
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
}
SelectAllLayers => {
let all_layer_paths = self
@ -374,16 +417,26 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
.filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay)
.cloned()
.collect::<Vec<_>>();
responses.push_back(SetSelectedLayers(all_layer_paths).into());
responses.push_front(SetSelectedLayers(all_layer_paths).into());
}
DeselectAllLayers => {
responses.push_back(SetSelectedLayers(vec![]).into());
responses.push_front(SetSelectedLayers(vec![]).into());
}
DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)),
DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)),
Undo => {
// this is a temporary fix and will be addressed by #123
if let Some(id) = self.document.root.as_folder().unwrap().list_layers().last() {
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
}
responses.push_back(SelectMessage::Abort.into());
responses.push_back(DocumentHistoryBackward.into());
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
}
Redo => {
responses.push_back(SelectMessage::Abort.into());
responses.push_back(DocumentHistoryForward.into());
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
}
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)),
DispatchOperation(op) => match self.document.handle_operation(&op) {
@ -446,6 +499,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
NudgeSelectedLayers(x, y) => {
self.backup();
for path in self.selected_layers().cloned() {
let operation = DocumentOperation::TransformLayerInViewport {
path,
@ -461,6 +515,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
}
ReorderSelectedLayers(relative_position) => {
self.backup();
let all_layer_paths = self.all_layers_sorted();
let selected_layers = self.selected_layers_sorted();
if let Some(pivot) = match relative_position.signum() {
@ -491,6 +546,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
}
FlipSelectedLayers(axis) => {
self.backup();
let scale = match axis {
FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.),
@ -512,6 +568,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
}
AlignSelectedLayers(axis, aggregate) => {
self.backup();
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip();
let axis = match axis {
@ -550,6 +607,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
fn actions(&self) -> ActionList {
let mut common = actions!(DocumentMessageDiscriminant;
Undo,
Redo,
SelectAllLayers,
DeselectAllLayers,
RenderDocument,

View File

@ -185,6 +185,7 @@ impl Default for Mapping {
// Editor Actions
entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]},
// Document Actions
entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]},

View File

@ -89,9 +89,9 @@ impl SelectToolData {
}
}
fn add_boundnig_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
fn add_boundnig_box(responses: &mut Vec<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
responses.push_back(
responses.push(
Operation::AddBoundingBox {
path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
@ -124,22 +124,25 @@ impl Fsm for SelectToolFsmState {
if let ToolMessage::Select(event) = event {
match (self, event) {
(_, UpdateSelectionBoundingBox) => {
let mut buffer = Vec::new();
let response = match (document.selected_layers_bounding_box(), data.bounding_box_id.take()) {
(None, Some(path)) => Operation::DeleteLayer { path }.into(),
(Some([pos1, pos2]), path) => {
let path = path.unwrap_or_else(|| add_boundnig_box(responses));
let path = path.unwrap_or_else(|| add_boundnig_box(&mut buffer));
data.bounding_box_id = Some(path.clone());
let transform = transform_from_box(pos1, pos2);
Operation::SetLayerTransformInViewport { path, transform }.into()
}
(_, _) => Message::NoOp,
};
responses.push_back(response);
responses.push_front(response);
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
self
}
(Ready, DragStart { add_to_selection }) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
let mut buffer = Vec::new();
let mut selected: Vec<_> = document.selected_layers().cloned().collect();
let quad = data.selection_quad();
let intersection = document.document.intersects_quad_root(quad);
@ -147,25 +150,29 @@ impl Fsm for SelectToolFsmState {
if selected.is_empty() {
if let Some(layer) = intersection.last() {
selected.push(layer.clone());
responses.push_back(DocumentMessage::SetSelectedLayers(selected.clone()).into());
buffer.push(DocumentMessage::SetSelectedLayers(selected.clone()).into());
}
}
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
// Otherwise enter the box select mode
if selected.iter().any(|path| intersection.contains(path)) {
let state = if selected.iter().any(|path| intersection.contains(path)) {
buffer.push(DocumentMessage::StartTransaction.into());
data.layers_dragging = selected;
Dragging
} else {
if !input.keyboard.get(add_to_selection as usize) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
buffer.push(DocumentMessage::DeselectAllLayers.into());
}
data.drag_box_id = Some(add_boundnig_box(responses));
data.drag_box_id = Some(add_boundnig_box(&mut buffer));
DrawingBox
}
};
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
state
}
(Dragging, MouseMove) => {
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
for path in data.layers_dragging.iter() {
responses.push_back(
responses.push_front(
Operation::TransformLayerInViewport {
path: path.clone(),
transform: DAffine2::from_translation(input.mouse.position - data.drag_current).to_cols_array(),
@ -173,7 +180,6 @@ impl Fsm for SelectToolFsmState {
.into(),
);
}
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
data.drag_current = input.mouse.position;
Dragging
}
@ -183,7 +189,7 @@ impl Fsm for SelectToolFsmState {
let start = data.drag_start + half_pixel_offset;
let size = data.drag_current - start + half_pixel_offset;
responses.push_back(
responses.push_front(
Operation::SetLayerTransformInViewport {
path: data.drag_box_id.clone().unwrap(),
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
@ -192,21 +198,27 @@ impl Fsm for SelectToolFsmState {
);
DrawingBox
}
(Dragging, DragStop) => Ready,
(Dragging, DragStop) => {
let response = match input.mouse.position.distance(data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::Undo,
false => DocumentMessage::CommitTransaction,
};
responses.push_front(response.into());
Ready
}
(DrawingBox, DragStop) => {
let quad = data.selection_quad();
responses.push_back(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into());
responses.push_back(
responses.push_front(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into());
responses.push_front(
Operation::DeleteLayer {
path: data.drag_box_id.take().unwrap(),
}
.into(),
);
data.drag_box_id = None;
Ready
}
(_, Abort) => {
let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_back(Operation::DeleteLayer { path }.into()));
let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_front(Operation::DeleteLayer { path }.into()));
delete(&mut data.drag_box_id);
delete(&mut data.bounding_box_id);
Ready

View File

@ -108,7 +108,7 @@ const menuEntries: MenuListEntries = [
children: [
[
{ label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => (await wasm).undo() },
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"] },
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => (await wasm).redo() },
],
[
{ label: "Cut", shortcut: ["Ctrl", "X"] },

View File

@ -212,6 +212,12 @@ pub fn undo() -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Undo)).map_err(convert_error)
}
/// Redo history one step
#[wasm_bindgen]
pub fn redo() -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Redo)).map_err(convert_error)
}
/// Select all layers
#[wasm_bindgen]
pub fn select_all_layers() -> Result<(), JsValue> {

View File

@ -272,6 +272,24 @@ impl Document {
self.set_transform_relative_to_scope(layer, None, transform)
}
fn remove_overlays(&mut self, path: &mut Vec<LayerId>) {
if self.layer(path).unwrap().overlay {
self.delete(path).unwrap()
}
let ids = self.folder(path).map(|folder| folder.layer_ids.clone()).unwrap_or_default();
for id in ids {
path.push(id);
self.remove_overlays(path);
path.pop();
}
}
pub fn clone_without_overlays(&self) -> Self {
let mut document = self.clone();
document.remove_overlays(&mut vec![]);
document
}
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
/// reaction from the frontend, responses may be returned.
pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {