【问题标题】:Fast undo/redo for bitmap editor when memory is limited?内存有限时位图编辑器的快速撤消/重做?
【发布时间】:2023-10-22 01:41:01
【问题描述】:

我正在尝试为移动设备(即 Photoshop 的受限版本)编写位图编辑器。用户的文档由大约 4 个位图组成,每个位图大小约为 1000x500。

我想要一个尽可能简单的强大而高效的撤消/重做系统。我的目标是大约 0.2 秒来撤消或重做编辑。我正在寻找一些关于我当前预期方法的反馈或一些我可以使用的新想法。我认为我拥有的东西太复杂了,所以我对继续进行持谨慎态度,所以只要知道这是我能做的最好的事情就好了。

我已经尝试将命令模式和备忘录模式组合用于我的撤消/重做系统。到目前为止,我得出的一些结论是:

  1. 我没有足够的内存,我无法将内存写入磁盘的速度足够快,无法在许多情况下使用备忘录来支持对上一个命令的“未执行”操作,例如:如果用户非常快速地完成了几个单独的笔画,我将无法存储代表用户所绘制内容的位图,而无需让用户等待它们被保存。

  2. 如果我将文档恢复到其初始状态并重放除最后一个执行撤消的命令之外的所有命令,即使执行少量命令(例如重播 10 次绘画或 5 次涂抹需要大约 1 秒,这太慢了。

  3. 我可以通过在后台定期将整个文档保存到磁盘并在播放命令之前恢复到此检查点来绕过前一点。要撤消比上一个检查点更远的位置,我们会重新加载此之前的检查点并重播命令。

方法 2 和 3 可以正常工作,只是随着添加更多图层,保存整个文档会变得越来越慢,并且使用 4 个位图(约 5 - 10 秒等待)已经很慢了。因此,我需要修改 3,以便仅保存自上次以来所做的更改。

由于许多命令只对一层进行操作,因此只保存自上次检查点以来已修改的层是有意义的。例如,如果我有 3 个初始层,我已经指出了可能保存检查点的位置,那么我的命令堆栈可能看起来像这样。

(Checkpoint1: Save layer 1, 2 and 3.)
Paint on layer 1
Paint on layer 1
(Checkpoint2: Save layer 1. Reuse saved layers 2 and 3 from Checkpoint1.)
Paint on layer 2
Paint on layer 2
(Checkpoint3: Save layer 2. Reuse saved layers 1 and 3 from Checkpoint2.)
Paint on layer 3
Paint on layer 3
Flip layer 3 horizontally.
(Checkpoint4: Save layer 3. Reuse saved layers 1 and 2 from Checkpoint3.)
Resize layer 1, 2 and 3.
(Checkpoint5: Save layer 1, 2, 3.)

在编辑过程中,我会跟踪自上一个检查点以来修改了哪些图层。当我恢复检查点时,我只恢复已更改的图层,例如为了在修改第 2 层和第 3 层后恢复 Checkpoint4,我从磁盘重新加载第 2 层和第 3 层的备份。添加检查点时,我只保存到目前为止已修改的图层。我可以使所有这些大部分自动化,除非我的界面中需要强制用户等待检查点被保存的地方,因为我一次只能在内存中保留大约 1 个图层的临时副本。

你怎么看?它比我希望的要复杂得多,但我看不到任何其他方式。还有其他有用的模式可以让我的生活更轻松吗?

【问题讨论】:

    标签: algorithm design-patterns optimization undo


    【解决方案1】:

    以下内容对于使用图像的图层和撤消缓冲区可能很方便:

    • 将最新图像保留为图像
    • 以前的版本存储为与下一个版本的异或,然后(假设并非所有内容都以相同的方式更改或更改)使用简单的压缩算法(如运行长度编码)进行压缩

    这样有以下优点

    • 以前的版本可以轻松合并(异或在一起)。

    这可能不适用于:

    • 颜色调整(色调、亮度等)
    • 坐标变换(裁剪、变形等)

    【讨论】:

    • 谢谢。这将使保存检查点的速度稍快一些,空间效率更高,但会使恢复检查点的速度稍慢一些,因为我需要加载和组合多个检查点来恢复以前的状态。不过,我会很感激我的整体撤消/重做方案中的一些 cmets 以及如何使它变得更简单。
    【解决方案2】:

    一种方法是将某些“框架”保留为完整的框架,而将其他“框架”保留为从前一个框架创建框架所必需的命令。你在#2 中提到了这一点。在内存中保留一些帧可能会有所帮助。

    一个有助于平衡性能与可用于保存完整帧的空间/时间的技巧是丢弃一部分“旧”帧,以便在任何给定时间您都可能具有撤消状态,例如1、2、4、8、16、32 和 64 次操作前。撤消一个或两个操作将只需要读取一个帧。撤消三个将需要读取一个检查点并重复一个操作。撤消五个将需要读取一个检查点并重复三个操作。撤消 33 将需要读取检查点并重复 31 次操作。

    为了提高应用程序的流畅度,在某些情况下,在撤消操作期间在后台重新计算检查点帧可能会有所帮助。例如,在撤消了 17 次操作之后,一个人可能会在后台开始计算从起点向后退 48、40 和 36 步的状态,因此如果想再往前走,那么他已经完成了一些工作。请注意,可能会丢弃返回 1、2、4、8 或 16 次操作的帧,因为可以通过从当前状态向前重播命令来重新创建它们。

    【讨论】:

    • 在撤消帧之间具有非恒定间隔(f0 = 1、f1 = 2、f2 = 4、f3 = 8 等)的难点在于保持这些间隔。例如,要确保 f2 始终位于新笔画之前的四个笔画,您需要转到 f3,并渲染中间的每个笔画。相同的逻辑适用于 f3、f4 等。是的,您应该从 fmax(它是空画布)开始,但这确实意味着您每次都需要重建整个撤消堆栈。
    • @jlukanta:如果检查点与未来某个帧的距离相同,则没有必要在当前帧之前设置精确距离。如果从编辑开始对所有帧编号为 1,则应尝试将最后一个奇数帧、最后一个 2 的奇数倍数、最后一个 4 的奇数倍数等。这有点难以描述整体方案,但基本上最终使用lgN时间