跳到主要内容

在线矢量编辑器

编辑器的真相是 文档模型(对象树、样式、图层),DOM 里的 <svg> 是投影。打开文件、撤销、协作、导出都对这份模型说话。禁止 innerHTML 往回灌用户文件。世界单位 = 文档用户空间;相机 = 根 viewBox。

1. 场景怎么切

Document:nodes / groups / z-order / assets
投影:createElementNS 同步到 SVG(可虚拟化)
g.content 用户对象(必须是节点)
g.chrome 选框、手柄、网格、参考线(不进 serialize)
canvas.preview 复杂滤镜 / 位图笔刷预览(可以光栅,确认后写回对象)
DOM:属性面板、IME 文本

必须是节点:可选中对象、剪贴蒙版的引用目标、文字框(几何)。手柄是 chrome,命中要能区分「点到对象」和「点到手柄」。

可以光栅:滤镜烘焙预览、超大位图对象的视口重采样、历史缩略图。确认滤镜后替换 source asset,不要让实时 filter 挂在每个对象上当文档。

export type EditorNode =
| { id: string; type: "path"; d: string; fill: string }
| { id: string; type: "group"; children: string[] };

export interface EditorDoc {
view: { minX: number; minY: number; width: number; height: number };
nodes: Record<string, EditorNode>;
root: string[];
}

export function serializeDocument(doc: EditorDoc, svg: SVGSVGElement): string {
for (const el of svg.querySelectorAll("[data-chrome]")) {
el.remove();
}
svg.setAttribute(
"viewBox",
`${doc.view.minX} ${doc.view.minY} ${doc.view.width} ${doc.view.height}`,
);
return new XMLSerializer().serializeToString(svg);
}

更好:从 EditorDoc 生成 XML,不读 DOM。DOM 可能含 chrome、hover 描边、错误的 style。打开用户文件:DOMParser消毒 → 收成 EditorDoc,再投影。建树必须 createElementNS,见 命名空间

2. 手柄与相机

变换在世界空间做。getScreenCTM 只用于指针 → 世界,见 坐标变换。旋转/缩放手柄不要写进对象的用户 path,用 chrome 的局部坐标系画,逻辑仍改文档的 transform 矩阵。框选与多选见 手柄

文本:短标签可用 &lt;text>;段落用 HTML overlay,基线位置跟世界走。不要用 foreignObject 吃用户 HTML,见 foreignObject

布尔运算、偏移、简化在世界单位做,结果写回 d。屏幕空间运算会随相机变。撤销是文档命令,见 撤销

适用:对象要成组、布尔、导出 SVG、对齐吸附。流程图只是子集,见 流程图;本篇多了任意 path 与资源。

3. 不适用

  • 用 Canvas 当唯一文档还做对齐/吸附/布尔运算:像素没有稳定几何。需要像素工具就 hybrid,确认后变成 path / image 对象。
  • 把库的 toJSON() 当 schema:业务字段、协作 clock、权限过不了。库最多当投影。
  • Snap.svg 新项目,见 09

4. 失败形态

症状原因
导出含选框和网格serialize 了 chrome
打开用户文件 XSSinnerHTML 或消毒漏 foreignObject
缩放后手柄不跟对象手柄用了 CSS 像素布局
布尔运算每次差 0.5px在屏幕空间做几何,没在世界空间
撤销打乱图层命令改了 DOM 没改文档,或反过来

权威资料

核对日期:2026-08-26