撤销标记管理是 SunvStation 地理编辑系统中的一项核心功能，它通过在执行可撤销操作前创建标记，支持用户对编辑操作进行撤销和重做。该机制基于命令模式，将每次可撤销操作封装为独立的命令单元，存储在 Undo/Redo 栈中，确保数据编辑的安全性和可追溯性。

撤销标记管理在批量地理对象编辑、属性修改和几何变换等场景中尤为重要，能够有效防止误操作导致的数据丢失，提升编辑流程的容错能力和用户体验。

## 撤销标记系统架构

撤销标记系统采用双层架构设计：上层通过 `SSProcessManager` 提供统一的 Python 接口，下层由 `ScaleMap` 的 C++ 核心实现命令存储和状态管理。系统维护两个命令栈：Undo 栈存储已执行的操作历史，Redo 栈存储被撤销的操作，两者协同工作实现操作的双向回溯。

```mermaid
flowchart TD
    A[用户操作] --> B[SSProcessManager<br/>pushUndoMark]
    B --> C[ScaleMap<br/>pushUndoMark]
    C --> D{创建命令标记}
    D --> E[命令序列化]
    E --> F[Undo栈<br/>LIFO结构]
    F --> G[操作执行]
    G --> H{用户触发撤销?}
    H -->|是| I[undo方法]
    H -->|否| J{用户触发重做?}
    I --> K[弹出Undo栈顶命令]
    K --> L[执行逆向操作]
    L --> M[压入Redo栈]
    J -->|是| N[redo方法]
    N --> O[弹出Redo栈顶命令]
    O --> P[执行正向操作]
    P --> Q[压入Undo栈]
```

## 核心 API 方法

### 创建撤销标记

`pushUndoMark()` 方法是撤销标记管理的入口，在执行任何可撤销操作前调用，为后续操作建立撤销点。该方法接受可选的操作描述参数，用于标识命令组的用途。

| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `pushUndoMark(action="CommandGroup")` | `action`: 命令组名称（可选） | void | 在 Undo 栈中创建新的标记位置 |

**使用示例**：

```python
# 批量修改前创建撤销标记
SSProcess.pushUndoMark("批量修改管线属性")

for i in range(SSProcess.getSelGeoCount()):
    SSProcess.setSelGeoValue(i, "SSObj_Color", "16777215")
    SSProcess.setSelGeoValue(i, "SSObj_LineWidth", "2")

# 修改完成后，用户可通过 Ctrl+Z 或调用 undo() 方法撤销
```

Sources: [ssprocess_mixins/project_mixin.py](ssprocess_mixins/project_mixin.py#L50-L61)

### 执行撤销操作

`undo()` 方法从 Undo 栈中弹出最近执行的命令并执行逆向操作，恢复数据到操作前的状态。支持指定步数或使用标记进行批量撤销。

| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `undo(step=1)` | `step`: 执行步数，-1 时使用 undo 标签 | void | 撤销指定步数的操作 |

**使用示例**：

```python
# 撤销最近一次操作
SSProcess.undo()

# 撤销最近 5 次操作
SSProcess.undo(5)

# 撤销到上一个标记位置
SSProcess.undo(-1)
```

Sources: [PySSMap.py](PySSMap.py#L1633-L1641)

### 执行重做操作

`redo()` 方法从 Redo 栈中弹出被撤销的命令并重新执行，恢复之前撤销的操作。与 `undo()` 方法形成对称操作。

| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `redo(step=1)` | `step`: 执行步数，-1 时使用 undo 标签 | void | 重做指定步数的操作 |

**使用示例**：

```python
# 重做最近一次撤销的操作
SSProcess.redo()

# 重做最近 3 次撤销的操作
SSProcess.redo(3)
```

Sources: [PySSMap.py](PySSMap.py#L1643-L1648)

### 栈状态查询与控制

系统提供多个方法用于查询 Undo/Redo 栈的状态，以及重置栈内容，便于用户了解可撤销/重做的操作范围。

| 方法 | 返回值 | 说明 |
|------|--------|------|
| `getUndoStackSize()` | int | 获取 Undo 栈中的命令数量 |
| `getRedoStackSize()` | int | 获取 Redo 栈中的命令数量 |
| `getUndoStack()` | StringArray | 获取 Undo 栈中的命令描述列表 |
| `getRedoStack()` | StringArray | 获取 Redo 栈中的命令描述列表 |
| `resetUndoRedoStack()` | void | 清空 Undo/Redo 栈，删除所有历史记录 |

**使用示例**：

```python
# 查询可撤销的操作数量
undo_count = SSProcess.getUndoStackSize()
print(f"可撤销操作数: {undo_count}")

# 查询可重做的操作数量
redo_count = SSProcess.getRedoStackSize()
print(f"可重做操作数: {redo_count}")

# 获取 Undo 栈中的命令描述
undo_stack = SSProcess.getUndoStack()
for i in range(undo_stack.size()):
    print(f"  {i+1}. {undo_stack[i]}")

# 清空所有撤销/重做历史（谨慎使用）
# SSProcess.resetUndoRedoStack()
```

Sources: [PySSMap.py](PySSMap.py#L1650-L1675)

### 手动压栈操作

`pushUndoAddobj()` 方法提供手动压栈功能，用于将指定地物对象列表显式地加入 Undo 栈，通常用于自定义编辑场景。

| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `pushUndoAddobj(geoList)` | `geoList`: GeoBaseList 对象列表 | void | 手动将对象列表压入 Undo 栈 |

**使用示例**：

```python
# 创建新对象后手动压栈
obj, geo = SSProcess.createNewObjByCode(3103013)
SSProcess.addNewObjPoint(x, y, z, 0, "Point1")

# 手动压栈以便后续撤销
geo_list = GeoBaseList()
geo_list.append(geo)
SSProcess.pushUndoAddobj(geo_list)
```

Sources: [PySSMap.py](PySSMap.py#L1686-L1692)

## 操作流程规范

### 标准编辑流程

遵循标准的撤销标记管理流程可确保操作的可追溯性和数据安全性。下图展示了完整的编辑操作生命周期：

```mermaid
sequenceDiagram
    participant U as 用户
    participant S as SSProcess
    participant M as ScaleMap
    participant UStack as Undo栈
    participant RStack as Redo栈

    U->>S: 1. pushUndoMark("操作描述")
    S->>M: 2. pushUndoMark()
    M->>UStack: 3. 创建标记并压栈
    M->>RStack: 4. 清空 Redo 栈

    U->>S: 5. 执行编辑操作
    S->>M: 6. 修改数据
    
    opt 用户请求撤销
        U->>S: 7. undo()
        S->>M: 8. undo()
        M->>UStack: 9. 弹出命令
        M->>M: 10. 执行逆向操作
        M->>RStack: 11. 压入 Redo 栈
    end
    
    opt 用户请求重做
        U->>S: 12. redo()
        S->>M: 13. redo()
        M->>RStack: 14. 弹出命令
        M->>M: 15. 执行正向操作
        M->>UStack: 16. 压入 Undo 栈
    end
```

### 标记创建时机

撤销标记应在以下关键节点创建：

1. **批量操作前**：批量修改属性、移动对象、删除对象等操作前
2. **复合操作起始点**：由多个子操作组成的复杂编辑流程开始时
3. **重要编辑前**：不可逆或影响范围较大的编辑操作前
4. **事务边界**：定义明确的编辑事务单元，便于整体撤销

## 典型应用场景

### 批量属性修改

批量修改选择集中地物的属性值时，应在循环前创建撤销标记，确保所有修改可作为一个整体撤销。

```python
# 场景：批量修改管线的颜色和线宽
SSProcess.pushUndoMark("批量修改管线样式")

for i in range(SSProcess.getSelGeoCount()):
    SSProcess.setSelGeoValue(i, "SSObj_Color", "16711680")  # 红色
    SSProcess.setSelGeoValue(i, "SSObj_LineWidth", "3")

# 如需撤销所有修改，调用：
# SSProcess.undo(-1)
```

Sources: [ssprocess_mixins/geo_edit_mixin.py](ssprocess_mixins/geo_edit_mixin.py#L321-L400)

### 对象创建与删除

创建或删除对象时，通过撤销标记管理可快速回退到操作前的状态。

```python
# 场景：创建新注记对象
SSProcess.pushUndoMark("创建注记对象")

obj, geo = SSProcess.createNewObjByClass("说明注记")
note = castToMarkNote(geo)
note.setNote("新建管线节点")
SSProcess.addNewObjPoint(x, y, 0, 0, "节点1")
SSProcess.addNewObjToSaveObjList()
SSProcess.saveBufferObjToDatabase()

# 如需撤销创建操作：
# SSProcess.undo()
```

Sources: [ssprocess_mixins/geo_edit_mixin.py](ssprocess_mixins/geo_edit_mixin.py#L159-L201)

### 几何变换操作

执行对象的平移、旋转、缩放等几何变换时，撤销标记管理提供快速恢复机制。

```python
# 场景：批量旋转选择集对象
from sunvpy.PySSMath import Point3D

SSProcess.pushUndoMark("旋转选择集对象")

# 获取旋转中心（假设第一个对象为参考点）
center_geo = SSProcess.selGeoList[0]
points = center_geo.getPoints()
center_pt = points[0]

# 执行旋转
angle = 1.5708  # 90度，弧度制
SSProcess.map.rotate(SSProcess.selGeoList, center_pt, angle, False)

# 请求视图更新
SSProcess.updateRequest()
```

Sources: [PySSMap.py](PySSMap.py#L1558-L1564)

## 最佳实践建议

### 标记命名规范

为撤销标记提供清晰的描述性名称，便于用户识别和调试。命名应简洁明了，反映操作的实质内容。

| 推荐命名格式 | 示例 | 说明 |
|--------------|------|------|
| "动作+对象类型" | "修改管线属性" | 动词+名词结构 |
| "批量+具体操作" | "批量删除注记" | 明确操作类型 |
| "事务名称" | "节点编辑事务" | 用于复合操作 |
| "功能模块+操作" | "数据导入操作" | 模块化命名 |

### 性能优化建议

1. **避免频繁创建标记**：在循环外创建标记，而非每次迭代都创建
2. **合理使用标记边界**：根据业务逻辑划分事务单元，而非滥用标记
3. **定期清空栈**：在长时间编辑会话中，适时调用 `resetUndoRedoStack()` 释放内存
4. **栈容量监控**：通过 `getUndoStackSize()` 监控栈深度，避免过度占用资源

### 错误处理机制

在创建撤销标记时，应检查地图实例的有效性，并在操作失败时进行适当的错误处理。

```python
def safe_edit_operation():
    """安全的编辑操作封装"""
    if SSProcess.map is None:
        print("错误：无法获取当前地图实例")
        return False
    
    try:
        SSProcess.pushUndoMark("安全编辑操作")
        
        # 执行编辑操作
        # ...
        
        SSProcess.updateRequest()
        return True
    except Exception as e:
        print(f"编辑操作失败：{str(e)}")
        # 可选择在此处调用 undo() 回滚部分修改
        return False
```

Sources: [ssprocess_mixins/project_mixin.py](ssprocess_mixins/project_mixin.py#L50-L61)

## 与其他模块的集成

撤销标记管理与[对象缓存机制](23-dui-xiang-huan-cun-ji-zhi)紧密协同，在批量保存到数据库前提供撤销能力。同时，它与[地物属性读写](15-huo-qu-di-wu-shu-xing-zhi)和[地理对象编辑](19-chuang-jian-mo-ren-di-wu-dui-xiang)模块集成，确保所有编辑操作的可追溯性。

在实现自定义编辑功能时，建议在操作前后分别调用 `pushUndoMark()` 和 `updateRequest()`，形成完整的编辑闭环，保持与系统标准编辑流程的一致性。