保存补丁(Save Drawing)
文档:
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