图形编辑器:历史记录设计
大家好,我是前端西瓜哥。今天讲一下图形编辑器如何实现历史记录,做到撤销重做。
其实就是版本号的更替。每个版本保存一个状态。
数据结构
要记录图形编辑器的历史记录,支持撤销重做功能,需要两个栈:撤销(undo)栈和重做(redo)栈。
每当用户进行一个操作(比如移动一个图形),就会产生一个新的版本,将这个操作产生的状态保持加入到 undo 栈顶,此外 redo 栈会清空。因为用户可能撤销了几次然后产生了新的操作,无法重做它们了。
当用户撤销,undo 栈出栈,并放到 redo 栈,然后使用 undo 栈顶的状态。当用户重做时,redo 栈出栈,再放到 undo 栈上,并应用 undo 栈顶的状态。
原理大概这样。
浏览器的回退前进的表现其实就是一个很常见的例子。
数据结构还有另一种方案:双向链表加两个指针,一个指针指向当前版本状态,另一个指针指向 redo 最后一次可执行到达的状态。
然后是如果要支持协同的场景,你的撤回不会回到之前的版本,而是将之前的版本的状态拿出来作为一个新的版本。
然后是协同中你不能撤回别人的操作,只能撤回自己的,并且要用协同算法处理和其他协同者的冲突逻辑。
要保存哪些状态
那么我们的状态要保存哪些状态呢?
- 图形树数据
- 图形树需要的引用
- 一些设置
图形树是必要的,我们需要用它渲染画布内容。此外还有游离在图形树之外的被用到的对象,比如图层、被多次引用的图形。你可以也把它们也放到图形树里面去。
最后是一些需要共享的设置,比如表格的行高、筛选条件等。
像是颜色主题、国际化语言设置则不需要历史记录,它是用户自己选择的个性化定制。
我们看具体的几种实现。
全量快照
每次操作的到的新状态,完全拷贝一份保存起来。
因为对象如果只是浅拷贝,其中的引用对象可能会被意外的修改,通常我们会选择 序列化成字符串 保存,即JSON.stringify。撤销重做的时候再解析出来作为当前状态。
优点是实现简单。
缺点是当状态很大的时候,每次生成快照都会比较耗时,且操作很多产生很多版本时,需要大量的内存空间保证这些完整状态。
如果画布上有一万个独立的实体,就意味着每进行一次操作,就要将这个一万个实体深拷贝一份。100 次就是 100w,很恐怖。
仅推荐简单的图形编辑器使用,或者做 demo 用。
补丁(patch)
全量快照让编辑器的上限很低,不是最优解。
一种更好的解法,是 打补丁(patch)。
基于上一个版本 1,打一个补丁,变成下一个版本 2。同时我们记录一个反向的补丁,撤回的时候能通过它从版本 2 回到版本 1。
这个方案对应了设计模式的 命令模式,我们构建 Command 类,这个类有 execute、redo、undo 方法,这些方法会对传入的旧的状态对象打补丁,得到一个新的状态。
比如添加矩形命令,execute 和 redo 时我们会往图形树的末尾加一个矩形对象,undo 就是将这个矩形从图形树中移除。undo 栈和 redo 栈此时记录的就是一个个 command 对象了。

纯纯用朴实无华的命令模式去实现,还是有点坑的。因为要实现的命令太多了,比如添加图形、修改图形属性、删除图形、对几个图形做右对齐等,这些都要自己一个个实现 redo 和 undo。复杂一点就要抓瞎,建议找一些轮子。比如 immer、y.js。
使用补丁方案还有一个好处,就方便实现 “动作” 功能。(当然这不是一个优先级很高的功能)
比如我们想要给一个图形先顺时针旋转 45 度,然后向右移动 10 个单位,我们希望记录这两个操作,给其他图形也应用这些操作。
快照的方式就不好搞,或许我们可以对比新旧状态找不同推断出行为,但不好搞。因为属性的变化可能来自不同的操作,比如移动,可以通过移动工具相对位移产生,也可能直接属性面板改 x 值,也可能是通过对齐操作产生的。
patch 就很适合。
什么时候保存状态
我们需要确认一个操作完成的时刻,将它加入到历史记录中。
我们操作图形,会产生一些 中间状态。比如移动一个图形,拖拽的过程中不生产一个历史版本,直到拖拽结束才记录。
一种方式是:操作图形的替身,操作结束后才更新真正的状态。
一些编辑器,比如 Adobe Illustrator、AutoCAD,我们在操作图形的时候,会看到一个临时的替身,就是将被选中图形的轮廓线或拷贝做鼠标的跟随,释放后才真正修改图形属性。
还比如颜色的修改,在拾色器中挑选颜色时不会立即修改图形,在点击确认才真正修改图形。

另一种方式是:直接操作真正的状态,在操作结束的时候,记录这个时刻的状态。

第一种方式的好处是,状态没有中间状态,替身操作完,计算出新状态应用到真正的状态上就好了。
第二种方式就要额外在操作开始时,保存原始状态的快照,因为之后我们会产生中间状态,然后在操作结束后计算 patch。
但第二种方式用户体验会更好些,用户能实时看到一个图形的变化,判断是不是自己需要的效果,而不是看到一个 “通往未来的幻影”。
结尾
我是前端西瓜哥,关注我,学习前端不迷路。
相关文章:
图形编辑器:历史记录设计
大家好,我是前端西瓜哥。今天讲一下图形编辑器如何实现历史记录,做到撤销重做。 其实就是版本号的更替。每个版本保存一个状态。 数据结构 要记录图形编辑器的历史记录,支持撤销重做功能,需要两个栈:撤销࿰…...
ubuntu22.04下挂载第二块硬盘
文章目录 一、查看硬盘情况二、找到nvme1n1三、挂载四、修改分区文件 一、查看硬盘情况 首先要查看一下系统识别出来的设备。也就是说,我希望知道,ubuntu到底发现了几块硬盘。用命令:lsblk 显示结果如下: 有两块硬盘:…...
举例说明.net中in与out的作用与区别
-----作用 在 .NET 中,in 和 out 是用于泛型类型参数的修饰符,它们用于指定参数类型的协变性和逆变性。 - in 修饰符:表示这个泛型类型参数是协变的。也就是说,in 类型参数可以从较特殊的类型隐式转换为较通用的类型。例如&…...
Java常见的100道面试题(内附答案及代码示例)持续更新
什么是Java程序? Java程序是一组可执行的代码,由Java编译器编译生成,可以在Java虚拟机(JVM)上运行。 public class HelloWorld {public static void main(String[] args) {System.out.println("Hello, World!&qu…...
策略设计模式知多少
目录 目标 概述 实现 目标 熟悉策略设计模式,了解策略设计模式的使用场景、具体实现。 概述 一、行为设计模式 行为设计模式是设计模式的一种类型。该类型的设计模式关注的重点在于对象的行为(通信和交互),而非对象的创建方…...
第三十九章 配置镜像 - 配置 ISCAgent - 在 UNIX Linux 和 macOS 系统上为非根实例启动 ISCAgent
文章目录 第三十九章 配置镜像 - 配置 ISCAgent - 在 UNIX Linux 和 macOS 系统上为非根实例启动 ISCAgent在 UNIX/Linux 和 macOS 系统上为非根实例启动 ISCAgent在 Microsoft Windows 系统上启动 ISCAgent 自定义 ISCAgent 第三十九章 配置镜像 - 配置 ISCAgent - 在 UNIX Li…...
嵌入式安卓开发:使用Camera2获取相机
文章目录 Camera2介绍Camera2的主要API类介绍CameraManager通过CameraManage获取Cameracharacteristics通过CameraManage获取CameraDevice从CameraDevice获取CameraCaptureSession预览效果 参考 Camera2介绍 从Android 5.0开始,Google 引入了一套全新的相机框架 Ca…...
阿里云g8i服务器Intel Xeon(Sapphire Rapids) Platinum 8475B
阿里云服务器ECS通用型实例规格族g8i采用2.7 GHz主频的Intel Xeon(Sapphire Rapids) Platinum 8475B处理器,3.2 GHz睿频,g8i实例采用阿里云全新CIPU架构,可提供稳定的算力输出、更强劲的I/O引擎以及芯片级的安全加固。阿里云百科分享阿里云服…...
设计模式——组件协作模式之观察者模式
文章目录 前言一、“组件协作” 模式二、Observer 观察者模式1、动机2、模式定义3、伪代码示例①、第一种方案,最朴素的方式②、第二种方案,重构使得遵循DIP原则:③、进一步的小优化:④、修改使得支持多个观察者: 4、结…...
观察者设计模式知多少
目录 目标 概述 实现 推设计模式 拉设计模式 被动观察者设计模式 目标 熟悉观察者设计模式,了解观察者设计模式的使用场景、具体实现(包括:推设计模式、拉设计模式、被动观察者设计模式)。 概述 一、行为设计模式 行为设…...
Flink之TaskManager内存解析
一、CK失败 Flink任务的checkpoint操作失败大致分为两种情况,ck decline和ck expire: (1)ck decline 发生ck decline情况时,我们可以通过查看JobManager.log或TaskManager.log查明具体原因。其中有一种特殊情况为ck cancel&…...
为何越来越多人不喜欢“试用期六个月”的公司?网友:感觉不靠谱
众所周知,任何一份工作都有试用期,一般是三月左右。但如果你遇到试用期达到半年的公司,你会不会进入? 近日,就有人遇到了此类公司,并对是否要进入该公司犹豫不决。他在论坛上发帖求助:大家是怎…...
单例模式的四种创建方式
前言 单例模式是日常开发中最常见的一种设计模式,常用来做为池对象,或者计数器之类的需要保证全局唯一的场景。 单例模式的目的是保证在整个程序中只存在一个对象实例,使用单例一个前提条件就是构造器私有化,不允许通过new 对象…...
Nginx+Keepalived 中的脑裂现象
如何解决和预防 NginxKeepalived 中会出现的脑裂现象? Nginx是一种高性能的Web服务器和反向代理服务器,可以处理大量并发请求。Keepalived是一种开源软件,用于实现IP负载均衡和故障转移。在Nginx和Keepalived结合使用时,可以通过将多个Ngin…...
04 KVM虚拟化网络概述
文章目录 04 KVM虚拟化网络概述4.1 Linux Bridge4.2 Open vSwitch 04 KVM虚拟化网络概述 为了使虚拟机可以与外部进行网络通信,需要为虚拟机配置网络环境。KVM虚拟化支持Linux Bridge、Open vSwitch网桥等多种类型的网桥。如图1所示,数据传输路径为“虚…...
110页智慧农业解决方案(农业信息化解决方案)(ppt可编辑)
本资料来源公开网络,仅供个人学习,请勿商用,如有侵权请联系删除。 第一部分 智慧农业概述 智慧农业以农业资源为基础、市场为导向、效益为中心、产业化为抓手,面向农业管理部门、农技推广部门、农业企业、农业园区和基地、农业专…...
Java知识体系及聊天室程序
Java知识体系结构梳理如下: 基础语法:Java的基本语法,包括数据类型、运算符、控制语句、数组等。 面向对象编程:Java是一种面向对象的编程语言,需要掌握类、对象、继承、多态等概念。 异常处理:Java提供了…...
java的详细发展历程
Java是一种跨平台、面向对象的编程语言,具有简单性、可移植性、安全性等特点。Java的历史可以追溯到上世纪90年代初期,以下是Java的详细发展历程: 1991年,Sun Microsystems公司的James Gosling和他的团队开始开发一种名为Oak的编程…...
丢石子
I 一堆石子有n个,两人轮流取.先取者第1次可以取任意多个,但不能全部取完.以后每次取的石子数不能超过上次取子数的2倍。取完者胜.先取者负输出"Second win".先取者胜输出"First win". 思路: 任何正整数都可以表示为不连续斐波那契…...
skywalking手动上报一些指标信息
skywalking的相关概念我就不介绍了,有兴趣可以参看官网文档 以下提供以下简单示例手工上报一些对问题排查比较有用的一些信息。当然这些内容你也可以写成探针插件的形式,怎么开发探针插件也自行参考官方文档。此处仅在项目框架层面提供一些简单的示例&am…...
【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...
多模态大语言模型arxiv论文略读(108)
CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题:CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者:Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
C++:多态机制详解
目录 一. 多态的概念 1.静态多态(编译时多态) 二.动态多态的定义及实现 1.多态的构成条件 2.虚函数 3.虚函数的重写/覆盖 4.虚函数重写的一些其他问题 1).协变 2).析构函数的重写 5.override 和 final关键字 1&#…...
多模态图像修复系统:基于深度学习的图片修复实现
多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...
Linux基础开发工具——vim工具
文章目录 vim工具什么是vimvim的多模式和使用vim的基础模式vim的三种基础模式三种模式的初步了解 常用模式的详细讲解插入模式命令模式模式转化光标的移动文本的编辑 底行模式替换模式视图模式总结 使用vim的小技巧vim的配置(了解) vim工具 本文章仍然是继续讲解Linux系统下的…...
