> **本文回答**：块定义 `BlockDef` + 块引用 `BlockRefEnt` —— 复用图形、参数化插入、属性 Attribute。

## 一、概念

- **Block Definition**：一组实体的封装（类似"符号"模板），不直接显示
- **Block Reference (INSERT)**：在地图上的一次放置，通过 `blockId` 引用某个 BlockDef；可单独设旋转 / 比例 / 插入点
- 同一 BlockDef 被 1000 次引用，只占用 1 份定义的内存

## 二、定义块

```js
const def = new vjmapext.BlockDef('chair');   // name='chair'；blockId 此刻还是 ""——
                                            // 在下方 blocks.add(def) 注册时才自动生成（如 "shzmgyqi"）

def.addEntity(new vjmapext.LineEnt([0,0], [40,0]));
def.addEntity(new vjmapext.LineEnt([40,0], [40,40]));
def.addEntity(new vjmapext.LineEnt([40,40], [0,40]));
def.addEntity(new vjmapext.LineEnt([0,40], [0,0]));

mapcadLayer.blocks.add(def);                 // 注册进 layer 的块表
```

### 基点 (basePoint)

`basePoint` 默认 (0,0)，是"定义原点"元数据——**渲染时不会自动减基点**：约定是 `def` 里的实体坐标本身就必须是相对基点的本地坐标（内置 `BLOCK` 命令建块时先把选中实体 `move(basePoint → 原点)` 再入 def）。`basePoint` 字段的实际用途：DWG 导出时写成 `DbBlock.origin`、随序列化往返、块管理面板显示。

```js
const def = new vjmapext.BlockDef('chair', [20, 20]);  // ctor 第二参即 basePoint
// 或 def.basePoint = new vjmapext.Point2D(20, 20);
// 想让 (20,20) 成为锚点，def 内实体坐标要事先写成相对它的本地坐标：
def.addEntity(new vjmapext.LineEnt([-20,-20], [20,-20]));  // 而不是 [0,0]→[40,0]
```

## 三、插入（BlockRefEnt）

引用按 **blockId**（不是 name）关联：

```js
const ref1 = new vjmapext.BlockRefEnt(def.blockId, [100, 100]);
mapcadLayer.addEntity(ref1);                  // addEntity 会自动注入 blocks 引用

const ref2 = new vjmapext.BlockRefEnt(def.blockId, [200, 200], 1.5, Math.PI / 4);
mapcadLayer.addEntity(ref2);
```

### 属性

| 属性 | 说明 |
|---|---|
| `blockId` | 引用的 BlockDef.blockId（构造第 1 参）；`blockName` 为只读 getter，反查定义名 |
| `insertionPoint` | 插入点（构造第 2 参，Point2DLike） |
| `scaleFactor` | 统一缩放（构造第 3 参，number） |
| `rotation` | 旋转（构造第 4 参，弧度） |
| `attributes` | 属性标签（见下） |

## 四、属性标签 (Attributes)

属性值要能**显示**，块定义里必须有对应的 ATTDEF——即设了 `tag` 的 `TextEnt`；渲染时属性值按 `tag` 匹配替换该文本（文本位置由 ATTDEF 自己的位置决定）：

```js
const def = new vjmapext.BlockDef('device');
def.addEntity(new vjmapext.CircleEnt([0,0], 20));

// ATTDEF：带 tag 的 TextEnt，位置/字高/旋转由它决定
const attdef = new vjmapext.TextEnt([0, -25], 'NAME', 8);
attdef.tag = 'NAME';
attdef.prompt = '设备名称';
def.addEntity(attdef);
mapcadLayer.blocks.add(def);

const ref = new vjmapext.BlockRefEnt(def.blockId, [100, 100]);
ref.initAttributesFromDef();                        // 按 ATTDEF 初始化属性（INSERT 命令会自动调）
ref.addAttribute({ tag: 'NAME', textString: 'P-001' });  // 按 tag upsert
mapcadLayer.addEntity(ref);
```

没有匹配 ATTDEF 的属性不渲染，但仍会进属性面板、随序列化保存、DWG 导出时写入 `ref.attribute`（`{tag: textString}` 映射）。

`BlockAttribute` 字段：`tag` / `textString` / `position` / `height` / `rotation` / `invisible` / `constant` / `preset` / `prompt` 等（见 d.ts）。相关方法：`addAttribute` / `getAttributes` / `getAttributeByTag(tag)` / `setAttributes(attrs)` / `initAttributesFromDef()`。

## 五、展开（getNestEntities）

取引用展开后的实体数组（不修改原引用）：

```js
const nested = ref.getNestEntities();   // EntityBase[]，已应用插入点/缩放/旋转
```

交互式炸开用内置 `EXPLODE` 命令（`mapcadLayer.executeCommand('EXPLODE')`，支持 BlockRefEnt 和闭合 PolylineEnt）。程序化：把展开结果加进 store 并删原引用：

```js
ref.getNestEntities().forEach(e => mapcadLayer.addEntity(e.clone()));
mapcadLayer.deleteEntity(ref.id);
```

炸开后失去 block 关联，无法再集中修改。

## 六、动态块修改

修改 BlockDef 里的实体后，需要把相关引用标脏才会重绘：

```js
const def = mapcadLayer.blocks.itemByName('chair');
def.getEntities()[0].color = 0xff0000;
mapcadLayer.getEntities()
  .filter(e => e.type === 'INSERT' && e.blockName === 'chair')
  .forEach(e => e.changed());
```

## 七、嵌套

Block 可以包含 Block 引用（多层嵌套支持），但**内层引用要手动注入块表**——`BlockDef.addEntity` 不注入，只有 `mapcadLayer.addEntity`/内置命令才 `setBlocksRef`；内层没有 `_blocksRef` 时展开为空，外层引用里那把椅子就不显示：

```js
const outer = new vjmapext.BlockDef('room');
outer.addEntity(new vjmapext.PolylineEnt([[0,0],[500,0],[500,300],[0,300]], true));
const inner = new vjmapext.BlockRefEnt(chairDef.blockId, [100, 100]);
inner.setBlocksRef(mapcadLayer.blocks);       // ← 必须，否则内层展开为 []
outer.addEntity(inner);
mapcadLayer.blocks.add(outer);
```

⚠️ `fromJSON`/`deserializeStore` 还原块定义时同样**不给块内的 INSERT 注入**（只给顶层实体注入）。含嵌套块的文档读回后要补一遍，再让顶层引用重建缓存：

```js
const blocks = mapcadLayer.blocks;
blocks.getAllItems().forEach(def => def.getEntities()
  .forEach(e => { if (e.type === 'INSERT') e.setBlocksRef(blocks); }));
mapcadLayer.getEntities().filter(e => e.type === 'INSERT').forEach(e => e.changed());
```

## 八、性能对比

| 方式 | 1000 个相同图形 |
|---|---|
| 直接 add 1000 个 PolylineEnt | 内存 × 1000 |
| BlockRef 引用同一 BlockDef | 内存 × 1（定义）+ 1000 个小引用 |

## 九、典型场景

- 机械设备 / 元器件
- 建筑家具（椅子、桌子）
- 道路标志
- 图面图例

## 十、常见坑

1. **BlockDef 未注册就 ref**：`blockId` 找不到定义 → 展开为空。先 `mapcadLayer.blocks.add(def)`。**注意顺序**：`new BlockDef()` 后 `blockId` 还是 `""`，`blocks.add()` 才生成——所以必须 add 完再 `new BlockRefEnt(def.blockId, ...)`。
2. **`ref.blockName`/`ref.getNestEntities()` 依赖注入**：二者要经 `mapcadLayer.addEntity(ref)` 注入 blocks 引用（内部字段 `_blocksRef`）才有效——注入前 `blockName` 返回 `''`、`getNestEntities()` 返回 `[]`。**注意缓存中毒**：`getNestEntities()` 结果缓存在 `_cachedNestEnts`，而 `setBlocksRef` 注入时**不清缓存**——若在注入前查过一次，注入后拿到的仍是 `[]`，须调 `ref.changed()` 才重建（`changed()` 会清缓存）。`blockName` 是活 getter 不受影响，会出现"`blockName` 正常但 `nest` 恒空"的假象。
3. **引用用错键**：`new BlockRefEnt` 第 1 参是 `def.blockId`（`blocks.add` 时生成的 id），不是 `'chair'` 这个名字；按名反查用 `blocks.itemByName('chair').blockId`。
4. **动态修改 BlockDef 不刷新**：改完定义要把引用 `changed()` 标脏（见第六节）。
5. **`position` 字段不生效**：`BlockAttribute.position` 只被序列化保留——渲染时属性文字显示在 ATTDEF 的位置，DWG 导出只写 `{tag: textString}`。要控制文字位置，改定义里那个 ATTDEF `TextEnt` 的 `insertionPoint`（本地坐标）。
