图形编辑器:历史记录设计
大家好,我是前端西瓜哥。今天讲一下图形编辑器如何实现历史记录,做到撤销重做。
其实就是版本号的更替。每个版本保存一个状态。
数据结构
要记录图形编辑器的历史记录,支持撤销重做功能,需要两个栈:撤销(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…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...
宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...
4. TypeScript 类型推断与类型组合
一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...
【LeetCode】算法详解#6 ---除自身以外数组的乘积
1.题目介绍 给定一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O…...
