Все заметки

История операций

Скриншот редактора
Если случайно удалить одно из состояний в редакторе анимаций, то восстановить его уже не получится

Во время доработки редактора случалось, что я двигал не тот объект или удалял не то что нужно. А сделать с этим ничего нельзя. Нет возможности отменить действие и вернуть как было. Только если ты откроешь JSON проекта, найдешь измененное место в куче других изменений и пользуясь инструментами git-а откатишь этот участок обратно. Это очень неудобно и ломает саму суть редактора, ведь я писал его чтобы больше никогда не открывать JSON. Таким образом, возникает необходимость реализовать хранение истории операций и добавить возможность отмены, т.е. нужен Ctrl+Z.

Об истории операций в проекте я подумал заранее и написал прослойку между интерфейсом редактора и конфигурацией проекта. Я назвал ее Commander. Любое изменение, будь то смена имени объекта в инспекторе или удаление уровня в проводнике – это команда, которую нужно “диспатчить” в хранилище приложения через интерфейс Commander-а. Похоже на Redux. Команды реализуют базовый CRUD. Остается только выбрать нужный тип операции и передать аргументы: путь изменения в JSON-е и данные, в случае если надо что-то добавить или изменить.

Поскольку Commander уже готов и все изменения проходят через него, остается организовать хранение этих изменений и добавить возможность отката. Команды о которых я упоминал ранее – это функции, которые отправляют изменения в хранилище редактора и ничего не возвращают. Если нужно отменить какое-то действие и вернуть как было, вместо “ничего” можно возвращать еще одну функцию – функцию отмены. Если к примеру командой являлось удаление, то в теле команды можно заранее сохранить в переменную то что мы собираемся удалить, а в функции отмены вернуть это значение обратно благодаря замыканию. Аналогичный подход подойдет и для всех остальных типов команд.

Схема работы commander-а
Commander отвечает за исполнение всех операций и хранит историю для отмены или повторения отмененного изменения

Историю операций внутри Commander-а по итогу формируют не сами действия, а функции отмены совершенных действий, а хранятся они в обычном массиве. Commander помимо сообщений с командами, научился принимать дополнительные сообщения типа undo/redo. Повтор отмененной операции (он же redo) выполняется похожим образом, а для хранения используется еще один стек. При помещении в стек информации о проделанной операции помимо функции отмены, я также складываю оригинальную функцию с привязкой к аргументам на момент вызова. В момент получения undo сообщения, Commander отменяет операцию и перекладывает операцию из одного стека в другой. С redo сообщением происходит тоже самое только в обратную сторону. Единственное отличие в том, что redo стек полностью очищается в случае любой новой операции, так как при попытке накатить отмененные изменения на уже измененные данные, может возникнуть конфликт.

Следующая проблема – некоторые операции в редакторе производятся в отдельных окнах, к примеру – редактирование анимаций. Было бы странно по нажатию на Ctrl+Z производить отмену действий, совершенных в уже закрытом окне. Поэтому потребовалось добавить React компонент-обертку, который задает контекст для всех операций, которые будут выполняться внутри и разметить интерфейс редактора при помощи этого компонента. Так, при отправке команды, определяется контекст и Commander помещает операцию в соответствующий стек.

Во время тестирования обнаружилась еще одна довольно неприятная проблема – иногда одно пользовательское изменение автоматически вызывает несколько дополнительных. К примеру, в формах инспектора, есть поля с зависимостями – они отображаются только если какое-то другое поле имеет определенное значение. Если коллайдер имеет круглую форму, то ему можно указать радиус, а если прямоугольную – длину и ширину. При изменении типа коллайдера, значения скрываемых полей удаляются и это производится при помощи дополнительных команд. Таким образом, возникают команды с сайд-эффектами. Для отмены всех операций разом, я добавил блок с опциями в функцию отправки команды в числе которых можно указать флаг isEffect. Команды-эффекты вкладываются в последнюю операцию в стеке, формируя список. При отмене последнего изменения, отменяются все привязанные эффекты.