拖一下画布,为什么撤销回不去了?
一个报表画布编辑器允许用户拖动矩形、文本、线段和图片,调整位置与大小。看起来是一次拖动,内部却同时涉及画布节点、文档数据、参考线和历史记录。
节点已经移动,不代表文档已提交新位置。每移动一个像素都记历史,撤销一次可能只退一小步;记录的又是可变对象,旧快照还可能被后续编辑改掉。
为什么不直接把节点状态全部存下来?
节点状态包含交互中的临时信息,文档则应保存重新加载后仍有意义的位置、尺寸和样式。把两者全部混在一起,辅助线、拖动偏移和页面缩放也可能进入历史。
项目把文档几何与临时交互分开。单元素通常在拖动结束时提交位置,多选先保留共同偏移,结束后把同一个 dx、dy 应用到所有元素,再清掉临时偏移。这样保持相对位置,也让一次拖动对应一次文档操作。
线段移动用节点临时 x、y,结束后把偏移加到两个端点,再将节点位置归零,防止下次叠加。端点拖动则有例外:它实时更新几何,第一次记录历史,后续更新不再新增记录。
缩放以后,为什么尺寸会重复变大?
画布节点可能同时保存 width、height 和 scale。如果先把 scale 乘进宽高,之后又继续保留原 scale,下一次变换或加载就可能重复放大。
项目在矩形缩放结束时把 scale 折进尺寸,再还原为 1,计算关系如下:
newWidth = node.width × node.scaleX
newHeight = node.height × node.scaleY
node.scale = (1, 1)
这套处理不直接套给所有元素。文本有排版高度和最小宽度,线段保存的是端点。屏幕像素、文档坐标和纸张毫米也先区分,网格步长按每毫米对应的像素比例换算。
参考线吸住了,松手后为什么又偏一点?
例如网格步长 10,元素拖到 x=98,附近参考边缘在 97。移动时吸到 97,结束时再按网格取整到 100,界面显示过对齐,最终却没有对齐。
解决思路是先约定两种吸附的优先级。项目按横纵轴分别处理:某轴命中参考线就保留该结果,没命中的轴再走网格。容差内选择更近候选,缩放时还看正在拖哪个手柄,避免拖右边缘却把左边一起移动。
因此吸附不只是计算一个最近坐标,还需要把命中结果带到提交阶段,防止后面再修正一次。
撤销历史保存快照,还是保存操作?
可以保存文档快照,撤销时直接恢复;也可以保存命令或差量,撤销时执行逆操作。前者容易实现,后者需要处理逆操作和合并,但能避免每步都保存完整状态。
原型先采用快照,只记录元素、纸张方向和选中项,不复制全部编辑器状态。撤销时把当前快照放进重做栈,再取出上一份;撤销后继续编辑会清空重做栈,恢复选中项时过滤不存在的元素。
已有用例覆盖吸附、选择、图片放置、序列化,以及部分复制粘贴与样式更新;一次手势撤销一次、连续端点拖动、旧快照不受污染等仍需分别补齐,真实指针事件还要到浏览器检查。
快照保存的是数组引用,没有深拷贝,正确性依赖不可变更新。后续代码若原地改端点数组或样式对象,历史也可能一起变。当前最多保留 100 步,限制的是步数,并没有限制每步文档和图片占多少内存。