> **本文回答**：`mapcadLayer.toJSON / fromJSON`、底层 `serializeStore / deserializeStore`、`DrawingDefaults` —— 保存 / 还原绘图的完整结构。

## 一、SerializedDocument 结构

```ts
interface SerializedDocument {
  version: string;                 // "1.0"，是字符串不是数字
  entities: SerializedEntity[];    // 全部实体
  blocks?: SerializedBlockDef[];   // 数组（不是 Record 映射）
}
// SerializedBlockDef = { blockId, name, basePoint: [x,y], items: SerializedEntity[] }
```

**注意：`drawingDefaults` 不在文档里。** `toJSON()` 只有上面三个键——绘图默认值走 `DrawingDefaults.save()` 独立持久化到 localStorage（见第四节），不随 `toJSON` 走。

`SerializedEntity` 顶层字段：`type / id / color / layerId / lineType / lineWidth? / editable? / selectableInReadonly? / minDisplayLevel? / maxDisplayLevel? / effectColor? / effectWidth? / effectOpacity? / effectBlur? / customProperties?`，几何数据在 `data` 里（LINE → `{start:[x,y], end:[x,y]}`，块引用 → `type:"INSERT"`、`data:{blockId, insertionPoint, scaleFactor, rotation}`）。

> ⚠️ **往返不完整（1.0.1）**：
> - 只处理 POINT/LINE/PLINE/CIRCLE/ARC/HATCH/TEXT/MTEXT/SPLINE/ELLIPSE/INSERT。**标注（DIMLINEAR/DIMALIGNED/DIMANGLE/DIMRADIUS）与 MLEADER** 序列化成 `data: {}`，反序列化无对应分支返回 `null` 被**静默丢弃**——`fromJSON` 后标注/引线全没了（自定义实体同理）。要保留只能自存构造参数（如 `customProperties`）读回后重建。
> - 顶层字段之外的 `visible`、`ltScale`、`ltOffset`、`dynamic` 不写入（读回恢复默认 `true/1/0/false`）；`TextEnt.widthFactor` 也不写。

## 二、toJSON / fromJSON（图层级）

```js
// 保存
const doc = mapcadLayer.toJSON();
const str = JSON.stringify(doc);
localStorage.setItem('drawing', str);

// 还原
const back = JSON.parse(localStorage.getItem('drawing'));
mapcadLayer.fromJSON(back);   // 内部：先清空 store 再逐个 add
mapcadLayer.zoomExtents();    // fromJSON 不会自动 fit，需要手动
```

## 三、底层 API：serializeStore / deserializeStore

文档级 `toJSON/fromJSON` 内部就是这两个导出函数，也可单独用（比如自定义存储后端）：

```js
const { serializeStore, deserializeStore } = vjmapext;

// serializeStore(store, blocks?) → SerializedDocument
const doc = serializeStore(mapcadLayer.store, mapcadLayer.blocks);

// deserializeStore(doc, store, blocks?) → void
// 注意三个行为差异：
//  1. 只调 store.add(entity)，【不清空】已有实体——重复调用会累加
//  2. blocks 容器里已存在同名块时跳过该块定义（不覆盖）
//  3. 传入 blocks 参后，还原的 BlockRefEnt 自动 setBlocksRef，
//     blockName / getNestEntities() 直接可用
deserializeStore(doc, mapcadLayer.store, mapcadLayer.blocks);
```

`fromJSON` = 清 store + `deserializeStore`；直接调 `deserializeStore` 要自己决定是否先清。

## 四、DrawingDefaults（绘图默认值）

新实体的初始样式来源。**字段**（`toJSON()` 输出的键）：

| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `color` | number | `0x7fd3ff` | 0xRRGGBB |
| `fillColor` | number | `0x3bb2d0` | 填充色 |
| `lineType` | string | `'CONTINUOUS'` | 线型名 |
| `ltScale` | number | `1` | 线型比例 |
| `lineWidth` | number | `0` | 像素 |
| `fillOpacity` | number | `0.4` | |
| `borderOpacity` | number | `1` | |
| `layerId` | string | `'0'` | 图层名（默认是字符串 `'0'`） |

**没有 `layer`、没有 `textHeight`、没有 `revcloudRadius`**——字段名是 `layerId`；文字高是 `TextEnt` 构造参数（默认 10），云线弧长是 `RevcloudCommand.arcLength`（静态，默认 50）。

### 读 / 写

```js
mapcadLayer.getDrawingDefaults();   // DrawingDefaultsData 快照

mapcadLayer.setDrawingDefaults({
  color: 0xff0000,
  layerId: '标注',        // 字段是 layerId
});
// 内部 = drawingDefaults.fromJSON(partial) 部分合并
//        + drawingDefaults.save() 写 localStorage
//        + 同步预览色
```

`setDrawingDefaults` 会自动 `save()` 持久化——刷新页面后默认值仍在（`DrawingDefaults` 构造时 `loadFromStorage()` 读回）。这与文档序列化是**两条独立通道**。

> ⚠️ 默认值只在**交互命令路径**生效：各命令内部 `ctx.drawingDefaults.applyTo(ent)` 后才 `store.add`。程序化 `new LineEnt(...) + addEntity()` **不套用** DrawingDefaults（新实体 color 是构造默认 0xffffff，不是当前设置的默认值）——要套用就显式 `mapcadLayer.drawingDefaults.applyTo(ent)`。

### 手动应用到实体

```js
const dd = new vjmapext.DrawingDefaults({ color: 0xff0000 });
dd.applyTo(entity);   // 把 color/layerId 等刷到实体上
```

## 五、只导出实体（不含块定义）

`EntityBase` **没有 `toJSON()`**（只有 `toGeoJSON()` 导几何）。要拿 SerializedEntity 只能走 `serializeStore`：

```js
// 只要实体数组：调 serializeStore 后丢弃 blocks
const { entities } = serializeStore(mapcadLayer.store);   // 不传 blocks
```

`toGeoJSON()` 适合给外部 GIS 用，但**不能**喂回 `deserializeStore`（结构不同）。

## 六、增量保存 / 差分

没有内置 diff。注意一个坑：

- **实体 id 在 deserialize 时重新生成**，不会保留序列化里的 `id`——不能用实体 id 做跨会话 diff 键；要做差分请用 `setCustomProperty` 存自己的稳定业务键
- `customProperties` 序列化形态是 `{key: {type, value}}`；`getCustomProperty(k)` 返回的也是这个包装对象（不是裸值）

## 七、fromJSON 时的顺序

```
mapcadLayer.fromJSON(doc):
1. store.getAllIds() → 逐个 delete（清空现有实体）
2. deserializeStore: doc.blocks → 注册块定义（同名跳过）
3. doc.entities → 逐个反序列化为真实实体类实例后 store.add
   - Point2D 恢复为 Point2D 实例（不是 plain object）
   - INSERT 实体自动注入 blocks 引用（仅顶层；块定义内部的 INSERT 不注入）
   - 实体 id 重新生成
   - 标注 / MLEADER / 自定义实体无反序列化分支 → 静默跳过（见第一节）
   - visible / ltScale / ltOffset / dynamic 未入档 → 恢复为默认值
4. 不自动 fit——手动 mapcadLayer.zoomExtents()
```

## 八、导出 DWG

如果需要导出为 DWG 文件（不是 JSON），用 `mapcadLayer.exportDwg(opts)`（见 [backend/export-dwg](../../../vjmap/references/backend/export-dwg.md)）。

| 方案 | 用途 |
|---|---|
| `toJSON` | 前端保存、后续在 vjmapext 里还原 |
| `exportDwg` | 下载真正的 DWG 文件，给 CAD 软件用 |

## 九、典型场景

### 1 保存到服务器

```js
await fetch('/api/drawing/save', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id, doc: mapcadLayer.toJSON() })
});
```

### 2 自动保存

```js
let saveTimer;
// ⚠️ 'store:changed' 在 1.0.1 枚举有定义但从未 emit——改用 entity:* 三个真实事件
const markDirty = () => {
  clearTimeout(saveTimer);
  saveTimer = setTimeout(() => {
    localStorage.setItem('drawing', JSON.stringify(mapcadLayer.toJSON()));
  }, 2000);
};
for (const ev of ['entity:added', 'entity:modified', 'entity:deleted'])
  mapcadLayer.eventBus.on(ev, markDirty);
```

### 3 工具栏联动默认值

```js
colorPicker.onChange = v => mapcadLayer.setDrawingDefaults({ color: v });
layerSelect.onChange = v => mapcadLayer.setDrawingDefaults({ layerId: v });

// 即将画的实体都会用新颜色 / 图层
mapcadLayer.executeCommand('LINE');
```

## 十、常见坑

1. **fromJSON 后位置看不到**：没 `zoomExtents`；手动 fit。
2. **先 fromJSON 再 setDrawingDefaults**：不冲突——两者是独立通道（defaults 不随文档走），但默认值的 localStorage 持久化意味着它会跨文档存活，可能不是你想要的。
3. **块定义丢失**：`serializeStore(store)` 不传 `blocks` 参数就只导实体；BlockRefEnt 还原后拿不到 `blockName`/`getNestEntities()`（除非另行注入 `_blocksRef`）。
4. **deserializeStore 重复调用累加**：它只 add 不清——要么用 `mapcadLayer.fromJSON`（自带清空），要么自己先清。
5. **大图 toJSON 1 MB+**：正常；建议 gzip 后上传。
6. **version 是字符串 `"1.0"`**：`=== 1` 数字比较永远不成立。

## 相关

- 批量加载 → [batch/entity-bulk-add](../batch/entity-bulk-add.md)
- 后端 DWG 导出 → [backend/export-dwg](../../../vjmap/references/backend/export-dwg.md)
- 实体属性 → [entity/style-props](../entity/style-props.md)
