# 保存补丁（Save Drawing）

> 文档：`vjcad/doc/version/versioning.md`
> 示例：`vjcad/example/version/02savepatch.js`

每次保存就是 **生成一个新 patch**。vjcad 计算 `originalJson` ↔ `currentJson` 的差异，把 diff 上传到服务端，附在分支末尾。

## 1. 命令式保存（最简）

```js
await Engine.editor.executerWithOp('SAVESERVER');
```

`SAVESERVER` 弹框让用户输入提交说明，然后执行底层 saveDrawing。

## 2. API 式保存（自定义流程）

```js
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}` 结构：

```ts
// 形如（路径示意）：
[
    { 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`）

```ts
{
    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` 走这一流程）。下面的检查是更严格的业务策略——远端有任何新补丁就拒绝保存：

```js
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` 字段：

```js
// 新建设计图（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`
