> **本文回答**：服务端打开 / 保存的核心是 `DrawingManagerService`：`drawingManager.openDrawing(params)` → `Engine.view.openDbDoc(file, data)` + 设 `currentDoc.serverSource` + `setOriginalJson(json)`；保存 = 把 `currentDoc.toDb()` 与 `originalJson` 一起交给 `drawingManager.saveDrawing(...)`——diff 在**前端用 WASM 计算**后只上传 patch，依赖 `parentId` 检测协作冲突。手写保存要处理 `no_change` / `idMapping` / `conflict` 三种结果，推荐直接用 `SAVESERVER`。

## 一、最小代码：从服务端打开

```js
import { DrawingManagerService, Engine } from 'vjcad';

const drawingManager = new DrawingManagerService();

const result = await drawingManager.openDrawing({
  type: 'imports',           // 'imports' | 'designs'
  mapid: 'example_map',
  version: 'v1',
  branch: 'main',
  patchId: 'base',           // 可省略 = 最新
  readOnly: false
});

if (result.success) {
  const file = new File(
    [result.webcadJson],
    'drawing.webcad',
    { type: 'application/json' }
  );
  await Engine.view.openDbDoc(file, result.webcadData);

  // 关键：保存来源信息（后续保存依赖）
  Engine.currentDoc.serverSource = {
    type: 'imports',
    mapid: 'example_map',
    version: 'v1',
    branchName: 'main',
    lastPatchId: result.latestPatchId || 'base'
  };
  await Engine.currentDoc.setOriginalJson(result.webcadJson);
}
```

MCP 参考：`vjcad/doc/file/file-operations.md`、`vjcad/example/file/01openfromserver.js`。

## 二、`IOpenDrawingParams` 表

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | `'imports'`\|`'designs'` | 是 | 图纸类型 |
| `mapid` | string | imports必填 | 图纸 ID |
| `version` | string | imports必填 | 版本号 |
| `designPath` | string | designs必填 | 设计图路径 |
| `branch` | string | 否 | 默认 `main` |
| `patchId` | string | 否 | 默认最新 |
| `readOnly` | boolean | 否 | 是否只读 |
| `clipbounds` | `[minX, minY, maxX, maxY]` | 否 | 裁剪范围 |
| `editAreas` | `Array<{minX, minY, maxX, maxY}>` | 否 | 多瓦片编辑区 |
| `editLayers` | `string[]` | 否 | 编辑图层名 |

## 三、`IOpenDrawingResult` 表

| 属性 | 类型 | 说明 |
|---|---|---|
| `success` | boolean | 是否成功 |
| `error` | string | 错误信息 |
| `webcadJson` | string | 解压的 webcad JSON |
| `webcadData` | any | 解析后对象 |
| `latestPatchId` | string | 最新 Patch ID |
| `isReadOnly` | boolean | 是否只读 |

MCP 参考：`vjcad/doc/file/file-operations.md`。

## 四、保存到服务端（增量）

```js
const currentJson  = JSON.stringify(Engine.currentDoc.toDb());
const originalJson = await Engine.currentDoc.getOriginalJson();
const src          = Engine.currentDoc.serverSource;

const r = await drawingManager.saveDrawing({
  type: src.type,
  mapid: src.mapid,
  version: src.version,
  branchName: src.branchName,
  originalJson,                 // 用于 diff 计算
  currentJson,
  parentId: src.lastPatchId,    // 冲突检测
  drawingName: '我的图纸',
  author: '作者',
  remark: '修改说明'
});

if (r.status && r.patchId === 'no_change') {
  // 无变化：什么都不更新（别把 'no_change' 当成下一次的 parentId）
} else if (r.status) {
  // 内置 SAVESERVER 的做法：先清实体 _modified 标记（GROUP 子实体需递归）再重新序列化作为新基准
  for (const block of Engine.currentDoc.blocks.items) block.items.forEach(e => e.clearModifiedForDiff?.());
  let cleanJson = JSON.stringify(Engine.currentDoc.toDb());
  if (r.idMapping && Object.keys(r.idMapping).length) {
    // 服务端给新增实体重分配了 id（{ oldId: newId }）：须同步更新本地实体 id 与 cleanJson 后再存基准
  }
  await Engine.currentDoc.setOriginalJson(cleanJson);
  Engine.currentDoc.serverSource.lastPatchId = r.patchId;
} else if (r.conflict?.hasConflict) {
  // 并发冲突：按 r.conflict.conflictingEntities / conflictingLayers 处理后，带 conflictResolution 重新 saveDrawing
} else {
  console.error(r.error, r.errorCode);
}
```

> 返回值 `ISaveWebcadPatchResponse`：`{status, patchId?, branch?, designPath?, isNew?, error?, errorCode?, idMapping?, conflict?}`。无变化时返回 `{status: true, patchId: 'no_change'}`；`idMapping` 与冲突处理细节较多，**推荐直接用 `SAVESERVER` 命令**（内置处理了上述全部分支并同步本地缓存）。

MCP 参考：`vjcad/doc/file/file-operations.md`、`vjcad/example/file/03saveserver.js`。

## 五、`ISaveDrawingParams` 表（保存）

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | `'imports'`\|`'designs'` | 是 | 图纸类型 |
| `mapid` | string | imports必填 | — |
| `version` | string | imports必填 | — |
| `designPath` | string | designs可选 | 空 = 新建 |
| `branchName` | string | 否 | 默认 `main` |
| `originalJson` | string | 否 | 用于 diff |
| `currentJson` | string | 是 | 当前 webcad JSON |
| `parentId` | string | 否 | 父 Patch（冲突检测） |
| `drawingName` | string | 否 | 图纸名 |
| `author` | string | 否 | 作者 |
| `remark` | string | 否 | 备注 |

## 六、增量保存特点

- **只保存修改部分**：diff 在**浏览器端**用 WASM `createWebcadDiff(originalJson, currentJson)` 计算，只上传 patch（超阈值再压缩），节省带宽 / 存储。
- **缺少 `originalJson` 时**走整图分支：把 `currentJson` 作为 `vcadData` 整图上传，**不带 `parentId`**（失去冲突检测），等同新建保存。
- **支持版本历史**：每次保存生成新 patch。
- **支持冲突检测**：通过 `parentId` 检测并发修改。

MCP 参考：`vjcad/doc/file/file-operations.md`。

## 七、命令版

```js
await Engine.editor.executerWithOp('OPENFROMSERVER');   // 弹出图纸浏览器
await Engine.editor.executerWithOp('SAVESERVER');       // 增量保存
```

## 八、图纸列表查询

```js
const all = await drawingManager.listAllDrawings();
console.log('后台 DWG:', all.serverMaps);
console.log('已导入:', all.imports);
console.log('设计图:', all.designs);

const drafts = await drawingManager.listWebcadDraws({
  mapid: 'example_map',
  version: 'v1'
});
```

MCP 参考：`vjcad/doc/file/file-operations.md`。

> ⚠️ **分支族方法名的双宿主分工**：`DrawingManagerService` 上是**友好名** `createBranch`/`deleteBranch`/`mergeBranch`/`listBranches`/`deleteDrawing`；底层 `Service` 类（`new vjcad.Service(url, token)`，负责原始 HTTP 调用）上才是 **`createWebcadBranch`/`deleteWebcadBranch`/`mergeWebcadBranch`/`deleteWebcadDraw`**。`listWebcadDraws` 两个宿主都有。在 `drawingManager` 上找 `createWebcadBranch` 会得到 `undefined`（反之亦然），跨文档抄代码时注意名字前缀。

## 反例

- 打开后没有设 `currentDoc.serverSource`：`SAVESERVER` 时找不到归属，保存失败。
- 没 `setOriginalJson`：`getOriginalJson()` 为空，`saveDrawing` 走整图 `vcadData` 分支——不做增量 diff、不带 `parentId`，失去冲突检测。
- 保存成功后无条件 `lastPatchId = r.patchId`：无变化时 `patchId` 是 `'no_change'`，下次会拿它当 `parentId`。
- 不传 `parentId`：多人协作时数据相互覆盖。

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/file/file-operations.md`
- 示例：`vjcad/example/file/01openfromserver.js`、`03saveserver.js`
