[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"i-ri:search-line":3,"i-ri:moon-line":8,"i-ri:sun-line":10,"i-ri:menu-3-line":12,"i-ri:close-line":14,"notes-module-items-frontend":16,"notes-page-frontend-02-canvas\u002F01-geometry-history":42,"i-ri:arrow-left-s-line":187,"i-ri:menu-2-line":189,"i-ri:arrow-right-s-line":191,"i-ep:copy-document":193},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm6 7h12v2H9zm-6 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z\"\u002F>",[17,22,26,30,34,38],{"path":18,"title":19,"meta":20},"\u002Fnotes\u002Ffrontend\u002F01-forms\u002F01-clear-semantics","表单清空后，旧数据为什么又回来了？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},"2026-11-05T09:47:15.000Z",{"path":23,"title":24,"meta":25},"\u002Fnotes\u002Ffrontend\u002F01-forms\u002F02-async-persistence","保存请求回来时，文档已经变了",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":27,"title":28,"meta":29},"\u002Fnotes\u002Ffrontend\u002F02-canvas\u002F01-geometry-history","拖一下画布，为什么撤销回不去了？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":31,"title":32,"meta":33},"\u002Fnotes\u002Ffrontend\u002F03-printing\u002F01-pagination-fonts","预览正常，PDF 为何多一页或缺字？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":35,"title":36,"meta":37},"\u002Fnotes\u002Ffrontend\u002F04-web-delivery\u002F01-cache-diagnosis","API 更新了，博客页面为什么还是旧的？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":39,"title":40,"meta":41},"\u002Fnotes\u002Ffrontend","前端与 Web 工程",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"id":43,"type":44,"sourcePath":45,"routePath":27,"path":27,"title":28,"description":46,"updatedAt":21,"meta":47,"body":56,"toc":180},"bm90ZTpub3Rlcy9mcm9udGVuZC8wMi1jYW52YXMvMDEtZ2VvbWV0cnktaGlzdG9yeS5tZA","note","notes\u002Ffrontend\u002F02-canvas\u002F01-geometry-history.md","一个报表画布编辑器允许用户拖动矩形、文本、线段和图片，调整位置与大小。看起来是一次拖动，内部却同时涉及画布节点、文档数据、参考线和历史记录。",{"id":43,"type":44,"sourcePath":45,"slug":48,"title":28,"updatedAt":21,"description":46,"tags":49,"date":53,"comment":54,"summary":55,"lastUpdated":21},"frontend\u002F02-canvas\u002F01-geometry-history",[50,51,52],"frontend","Canvas","编辑器","2026-04-22",true,"画布节点、文档数据和撤销历史更新时间不同。先约定坐标与提交时机，再处理缩放、吸附和一次手势的历史记录。",{"type":57,"children":58},"root",[59,67,72,77,84,89,94,99,105,110,115,128,133,139,144,149,154,160,165,170,175],{"type":60,"tag":61,"props":62,"children":64},"element","h1",{"id":63},"拖一下画布为什么撤销回不去了",[65],{"type":66,"value":28},"text",{"type":60,"tag":68,"props":69,"children":70},"p",{},[71],{"type":66,"value":46},{"type":60,"tag":68,"props":73,"children":74},{},[75],{"type":66,"value":76},"节点已经移动，不代表文档已提交新位置。每移动一个像素都记历史，撤销一次可能只退一小步；记录的又是可变对象，旧快照还可能被后续编辑改掉。",{"type":60,"tag":78,"props":79,"children":81},"h2",{"id":80},"为什么不直接把节点状态全部存下来",[82],{"type":66,"value":83},"为什么不直接把节点状态全部存下来？",{"type":60,"tag":68,"props":85,"children":86},{},[87],{"type":66,"value":88},"节点状态包含交互中的临时信息，文档则应保存重新加载后仍有意义的位置、尺寸和样式。把两者全部混在一起，辅助线、拖动偏移和页面缩放也可能进入历史。",{"type":60,"tag":68,"props":90,"children":91},{},[92],{"type":66,"value":93},"项目把文档几何与临时交互分开。单元素通常在拖动结束时提交位置，多选先保留共同偏移，结束后把同一个 dx、dy 应用到所有元素，再清掉临时偏移。这样保持相对位置，也让一次拖动对应一次文档操作。",{"type":60,"tag":68,"props":95,"children":96},{},[97],{"type":66,"value":98},"线段移动用节点临时 x、y，结束后把偏移加到两个端点，再将节点位置归零，防止下次叠加。端点拖动则有例外：它实时更新几何，第一次记录历史，后续更新不再新增记录。",{"type":60,"tag":78,"props":100,"children":102},{"id":101},"缩放以后为什么尺寸会重复变大",[103],{"type":66,"value":104},"缩放以后，为什么尺寸会重复变大？",{"type":60,"tag":68,"props":106,"children":107},{},[108],{"type":66,"value":109},"画布节点可能同时保存 width、height 和 scale。如果先把 scale 乘进宽高，之后又继续保留原 scale，下一次变换或加载就可能重复放大。",{"type":60,"tag":68,"props":111,"children":112},{},[113],{"type":66,"value":114},"项目在矩形缩放结束时把 scale 折进尺寸，再还原为 1，计算关系如下：",{"type":60,"tag":116,"props":117,"children":122},"pre",{"className":118,"code":120,"language":66,"meta":121},[119],"language-text","newWidth  = node.width  × node.scaleX\nnewHeight = node.height × node.scaleY\nnode.scale = (1, 1)\n","",[123],{"type":60,"tag":124,"props":125,"children":126},"code",{"__ignoreMap":121},[127],{"type":66,"value":120},{"type":60,"tag":68,"props":129,"children":130},{},[131],{"type":66,"value":132},"这套处理不直接套给所有元素。文本有排版高度和最小宽度，线段保存的是端点。屏幕像素、文档坐标和纸张毫米也先区分，网格步长按每毫米对应的像素比例换算。",{"type":60,"tag":78,"props":134,"children":136},{"id":135},"参考线吸住了松手后为什么又偏一点",[137],{"type":66,"value":138},"参考线吸住了，松手后为什么又偏一点？",{"type":60,"tag":68,"props":140,"children":141},{},[142],{"type":66,"value":143},"例如网格步长 10，元素拖到 x=98，附近参考边缘在 97。移动时吸到 97，结束时再按网格取整到 100，界面显示过对齐，最终却没有对齐。",{"type":60,"tag":68,"props":145,"children":146},{},[147],{"type":66,"value":148},"解决思路是先约定两种吸附的优先级。项目按横纵轴分别处理：某轴命中参考线就保留该结果，没命中的轴再走网格。容差内选择更近候选，缩放时还看正在拖哪个手柄，避免拖右边缘却把左边一起移动。",{"type":60,"tag":68,"props":150,"children":151},{},[152],{"type":66,"value":153},"因此吸附不只是计算一个最近坐标，还需要把命中结果带到提交阶段，防止后面再修正一次。",{"type":60,"tag":78,"props":155,"children":157},{"id":156},"撤销历史保存快照还是保存操作",[158],{"type":66,"value":159},"撤销历史保存快照，还是保存操作？",{"type":60,"tag":68,"props":161,"children":162},{},[163],{"type":66,"value":164},"可以保存文档快照，撤销时直接恢复；也可以保存命令或差量，撤销时执行逆操作。前者容易实现，后者需要处理逆操作和合并，但能避免每步都保存完整状态。",{"type":60,"tag":68,"props":166,"children":167},{},[168],{"type":66,"value":169},"原型先采用快照，只记录元素、纸张方向和选中项，不复制全部编辑器状态。撤销时把当前快照放进重做栈，再取出上一份；撤销后继续编辑会清空重做栈，恢复选中项时过滤不存在的元素。",{"type":60,"tag":68,"props":171,"children":172},{},[173],{"type":66,"value":174},"已有用例覆盖吸附、选择、图片放置、序列化，以及部分复制粘贴与样式更新；一次手势撤销一次、连续端点拖动、旧快照不受污染等仍需分别补齐，真实指针事件还要到浏览器检查。",{"type":60,"tag":68,"props":176,"children":177},{},[178],{"type":66,"value":179},"快照保存的是数组引用，没有深拷贝，正确性依赖不可变更新。后续代码若原地改端点数组或样式对象，历史也可能一起变。当前最多保留 100 步，限制的是步数，并没有限制每步文档和图片占多少内存。",{"title":121,"searchDepth":181,"depth":181,"links":182},2,[183,184,185,186],{"id":80,"depth":181,"text":83},{"id":101,"depth":181,"text":104},{"id":135,"depth":181,"text":138},{"id":156,"depth":181,"text":159},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":188},"\u003Cpath fill=\"currentColor\" d=\"m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":190},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":192},"\u003Cpath fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"\u002F>",{"left":4,"top":4,"width":194,"height":194,"rotate":4,"vFlip":6,"hFlip":6,"body":195},1024,"\u003Cpath fill=\"currentColor\" d=\"M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64z\"\u002F>\u003Cpath fill=\"currentColor\" d=\"M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64\"\u002F>"]