唯杰地图 logo唯杰地图
VJMAP
EN

保存补丁(Save Drawing)

vjcad/references/version/save-patch.md 8.8 KB168 行
原文

文档:vjcad/doc/version/versioning.md 示例:vjcad/example/version/02savepatch.js

每次保存就是 生成一个新 patch。vjcad 计算 originalJson ↔ currentJson 的差异,把 diff 上传到服务端,附在分支末尾。

1. 命令式保存(最简)

await Engine.editor.executerWithOp('SAVESERVER');

SAVESERVER 弹框让用户输入提交说明,然后执行底层 saveDrawing。

2. API 式保存(自定义流程)

class SavePatchCommand {
    async main() {
        const drawingManager = new DrawingManagerService();
        const ss = Engine.currentDoc.serverSource;
        if (!ss) return message.warn("当前文档不是服务端图纸");

        // 1. 序列化"打开时的状态"和"现在的状态"
        // ⚠️ currentJson 必须是 JSON 字符串(内部 JSON.parse),toDb() 对象要 stringify
        const originalJson = await Engine.currentDoc.getOriginalJson();
        const currentJson  = JSON.stringify(Engine.currentDoc.toDb());   // toDb() -> DbDoc 对象

        // 2. 调 saveDrawing(前端 WASM createWebcadDiff 算出 RFC 6902 JSON Patch,只上传 patch)
        const patch = await drawingManager.saveDrawing({
            type:         ss.type,           // 必填:'imports' | 'designs'
            mapid:        ss.mapid,
            version:      ss.version,
            designPath:   ss.designPath,     // designs 类型用
            branchName:   ss.branchName,
            parentId:     ss.lastPatchId,    // 基于的父补丁
            originalJson,
            currentJson,
            remark:       '修改了若干圆和直线'   // 字段名是 remark,不是 message
        });

        // 3. 先判结果:鉴权拒绝时 status 是数字 403(真值),所以用 === true
        if (patch.status !== true) return message.error(patch.error || '保存失败');
        if (patch.patchId === 'no_change') return message.info('没有变化,未生成补丁');

        // 4. 服务端可能重分配新增实体 ID:先同步本地实体,再重新序列化
        let savedJson = currentJson;
        if (patch.idMapping && Object.keys(patch.idMapping).length > 0) {
            for (const block of Engine.currentDoc.blocks.items) {
                for (const ent of block.items) {
                    const newId = patch.idMapping[String(ent.id)];
                    if (newId !== undefined) ent.id = newId;
                }
            }
            savedJson = JSON.stringify(Engine.currentDoc.toDb());
        }

        // 5. 更新 serverSource,让下一次保存基于新顶点
        ss.lastPatchId = patch.patchId;
        await Engine.currentDoc.setOriginalJson(savedJson);   // 异步,返回 Promise<boolean>

        message.success(`已保存补丁 ${patch.patchId}`);
    }
}
CommandRegistry.regist(new CommandDefinition(
    'SAVEPATCH', '保存补丁', SavePatchCommand, new CommandOptions()
));

完整代码:vjcad/example/version/02savepatch.js

3. saveDrawing 入参

字段 说明
type 'imports' | 'designs'(必填)
mapid / version / branchName 标识图纸位置
parentId 父补丁 ID;通常等于当前 serverSource.lastPatchId
originalJson? 打开时 的图纸 JSON 字符串(baseline);已存在的图纸必须传,不传只用于新建 designs 图纸(见第 6 节)
currentJson 现在 的图纸 JSON 字符串(必填)
remark? 备注(commit message;字段名是 remark 不是 message)
author? / drawingName? 元数据(配置了 userInfo 时服务端用 userName/userId 覆盖 author)
conflictResolution? 保存冲突 后重存用:{ entityId: { choice: 'server' | 'client', entityData? } },配合 parentId = conflict.latestPatchId(合并分支用的是 mergeBranch 自己的 'target' | 'source')
readOnlyEntityIds? / editAreas? / editLayers? / tileAlpha? 编辑域与瓦片相关

ISaveDrawingParams 没有 message / forceFull / mergeMode 字段。

diff 在 前端 计算:saveDrawing 调 WASM createWebcadDiff(originalJson, currentJson, readOnlyEntityIds),得到 RFC 6902 JSON Patch(实体 items 先按 id、图层 / 文字样式按名称转成对象再比较),超过阈值时压缩,然后只把 patch 上传给服务端——不上传整图,也不是 {added/modified/erased} 结构:

// 形如(路径示意):
[
    { op: 'add',     path: '/dbBlocks/<块名>/items/<新实体id>', value: { /* 实体数据 */ } },
    { op: 'replace', path: '/dbBlocks/<块名>/items/<实体id>/<属性>', value: '<新值>' },
    { op: 'remove',  path: '/dbBlocks/<块名>/items/<实体id>' },
]

patch 为空数组时直接返回 { status: true, patchId: 'no_change' },不发请求。DiffUtils.diff 不参与保存,只用于 DIFF 命令的差异面板。

4. saveDrawing 返回(ISaveWebcadPatchResponse)

{
    status:    boolean;    // 是否成功(先判它;鉴权拒绝时实际是数字 403,用 === true 判)
    patchId?:  string;     // 新补丁 ID(形如 'p3');无变化时为 'no_change'
    branch?:   string;     // 字段名是 branch,不是 branchName
    designPath?: string;
    isNew?:    boolean;    // 是否新图纸
    error?:    string;     // 冲突时为 'conflict'
    errorCode?: string;
    idMapping?: Record<string, number>;  // 服务端重映射的新增实体 ID { 旧id: 新id }
    conflict?: { hasConflict: boolean; conflictingPatches: string[]; conflictingEntities: IConflictEntityInfo[];
                 conflictingLayers?: IConflictLayerInfo[]; latestPatchId: string };
}

保存成功后的顺序(同 SaveToServerCommand):先判 patchId === 'no_change'(此时不要改 lastPatchId)→ 有 idMapping 先改本地实体 id 再重新序列化 → serverSource.lastPatchId = patchId → await setOriginalJson(新 JSON)。漏掉最后两步,下次保存会拿旧 baseline 再 diff 一遍,产生 重复变更。

5. (可选)保存前检查最新 patch

服务端本身会做冲突检测:只检查 parentId 之后的补丁是否也改过本次 修改 的实体 / 图层(新增实体不算),无交集就直接追加到分支末尾;有交集才返回 status:false, error:'conflict' 和 conflict 详情,此时用 conflictResolution 并以 parentId = conflict.latestPatchId 重存(SAVESERVER 会自动弹 ConflictResolutionDialog 走这一流程)。下面的检查是更严格的业务策略——远端有任何新补丁就拒绝保存:

const branches = await drawingManager.listBranches({
    type: ss.type, mapid: ss.mapid, version: ss.version   // 对象参数
});
const cur = branches.find(b => b.name === ss.branchName);  // IBranchInfo.name 不是 branchName
const latest = cur.patches[cur.patches.length - 1];

if (latest.id !== ss.lastPatchId) {                        // IPatchInfo.id 不是 patchId
    message.warn('远端已有新补丁,请先合并或重新打开');
    return;
}
// 否则保存

6. 不传 originalJson:只用于新建图纸

有 originalJson 时库自动算 diff(增量保存,走 createWebcadDiff);不传 originalJson 时 saveDrawing 把 currentJson 作为 vcadData 上传、不带 patch。服务端只在图纸 还没有 base.vcad(新建) 时接受 vcadData;对已存在的图纸会直接返回 status:false, error:'patch data is required'——没有"全量覆盖保存"。ISaveDrawingParams 也没有 forceFull 字段:

// 新建设计图(designs,不传 designPath 由服务端生成)
const res = await drawingManager.saveDrawing({
    type: 'designs',
    currentJson,
    drawingName: '新图纸',
    remark: '初始版本'
});
// res: { status: true, designPath, isNew: true }

想"整图替换"已有图纸:把当前文档与最新版本的 JSON 作为 originalJson/currentJson 正常走增量保存,或另建 designs 图纸。

红旗

  • originalJson 一定要是"打开时的"那份;如果保存后没更新 setOriginalJson,下次会重复 diff
  • 保存的 commit message 不要超过 200 字,部分后端会截断
  • 保存失败要保留 currentJson,让用户重试或导出本地
  • parentId 应是该分支真实存在的 patch;填了不存在的 id 不会被拒绝,而是静默跳过冲突检测直接追加,可能覆盖别人的修改
  • 大量小操作建议合并成一次保存(每次保存都是一次网络往返)

参考(官方文档 MCP)

  • 文档:vjcad/doc/version/versioning.md
  • 示例:vjcad/example/version/02savepatch.js