当前位置: 首页 > news >正文

图形编辑器:历史记录设计

大家好,我是前端西瓜哥。今天讲一下图形编辑器如何实现历史记录,做到撤销重做。

其实就是版本号的更替。每个版本保存一个状态。

数据结构

要记录图形编辑器的历史记录,支持撤销重做功能,需要两个栈:撤销(undo)栈和重做(redo)栈

每当用户进行一个操作(比如移动一个图形),就会产生一个新的版本,将这个操作产生的状态保持加入到 undo 栈顶,此外 redo 栈会清空。因为用户可能撤销了几次然后产生了新的操作,无法重做它们了。

当用户撤销,undo 栈出栈,并放到 redo 栈,然后使用 undo 栈顶的状态。当用户重做时,redo 栈出栈,再放到 undo 栈上,并应用 undo 栈顶的状态。

原理大概这样。

浏览器的回退前进的表现其实就是一个很常见的例子。

数据结构还有另一种方案:双向链表加两个指针,一个指针指向当前版本状态,另一个指针指向 redo 最后一次可执行到达的状态。

然后是如果要支持协同的场景,你的撤回不会回到之前的版本,而是将之前的版本的状态拿出来作为一个新的版本。

然后是协同中你不能撤回别人的操作,只能撤回自己的,并且要用协同算法处理和其他协同者的冲突逻辑。

要保存哪些状态

那么我们的状态要保存哪些状态呢?

  1. 图形树数据
  2. 图形树需要的引用
  3. 一些设置

图形树是必要的,我们需要用它渲染画布内容。此外还有游离在图形树之外的被用到的对象,比如图层、被多次引用的图形。你可以也把它们也放到图形树里面去。

最后是一些需要共享的设置,比如表格的行高、筛选条件等。

像是颜色主题、国际化语言设置则不需要历史记录,它是用户自己选择的个性化定制。

我们看具体的几种实现。

全量快照

每次操作的到的新状态,完全拷贝一份保存起来。

因为对象如果只是浅拷贝,其中的引用对象可能会被意外的修改,通常我们会选择 序列化成字符串 保存,即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。

但第二种方式用户体验会更好些,用户能实时看到一个图形的变化,判断是不是自己需要的效果,而不是看到一个 “通往未来的幻影”。

结尾

我是前端西瓜哥,关注我,学习前端不迷路。

相关文章:

图形编辑器:历史记录设计

大家好,我是前端西瓜哥。今天讲一下图形编辑器如何实现历史记录,做到撤销重做。 其实就是版本号的更替。每个版本保存一个状态。 数据结构 要记录图形编辑器的历史记录,支持撤销重做功能,需要两个栈:撤销&#xff0…...

ubuntu22.04下挂载第二块硬盘

文章目录 一、查看硬盘情况二、找到nvme1n1三、挂载四、修改分区文件 一、查看硬盘情况 首先要查看一下系统识别出来的设备。也就是说,我希望知道,ubuntu到底发现了几块硬盘。用命令:lsblk 显示结果如下: 有两块硬盘&#xff1a…...

举例说明.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…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

vue3 定时器-定义全局方法 vue+ts

1.创建ts文件 路径&#xff1a;src/utils/timer.ts 完整代码&#xff1a; import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

稳定币的深度剖析与展望

一、引言 在当今数字化浪潮席卷全球的时代&#xff0c;加密货币作为一种新兴的金融现象&#xff0c;正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而&#xff0c;加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下&#xff0c;稳定…...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习

禁止商业或二改转载&#xff0c;仅供自学使用&#xff0c;侵权必究&#xff0c;如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

Reasoning over Uncertain Text by Generative Large Language Models

https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

2025季度云服务器排行榜

在全球云服务器市场&#xff0c;各厂商的排名和地位并非一成不变&#xff0c;而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势&#xff0c;对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析&#xff1a; 一、全球“三巨头”…...

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

uniapp 小程序 学习(一)

利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 &#xff1a;开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置&#xff0c;将微信开发者工具放入到Hbuilder中&#xff0c; 打开后出现 如下 bug 解…...

二维FDTD算法仿真

二维FDTD算法仿真&#xff0c;并带完全匹配层&#xff0c;输入波形为高斯波、平面波 FDTD_二维/FDTD.zip , 6075 FDTD_二维/FDTD_31.m , 1029 FDTD_二维/FDTD_32.m , 2806 FDTD_二维/FDTD_33.m , 3782 FDTD_二维/FDTD_34.m , 4182 FDTD_二维/FDTD_35.m , 4793...