OCCT 中 OCAF 事务开始结束的最佳时机

std::string GeometrySamples::CreateRevol(const TopoDS_Shape& shape, const gp_Dir& axis,
	double startAngle, double endAngle, bool isSolid)
{
	if (m_doc.IsNull()) {
		qWarning() << "文档对象无效";
		return "";
	}
	if (m_modelLable.IsNull()) {
		qWarning() << "模型标签无效";
		return "";
	}
	if (m_context.IsNull()) {
		qWarning() << "交互上下文无效";
		return "";
	}

	try {
		// 检查输入是否有效
		if (shape.IsNull() || (endAngle - startAngle) <= 0) {
			return "";
		}

		TopoDS_Shape revolShape;

		// 计算旋转体
		double angleRad = (endAngle - startAngle) * M_PI / 180.0;
		gp_Pnt origin(0, 0, 0);
		Bnd_Box boundingBox;
		BRepBndLib::Add(shape, boundingBox);
		if (!boundingBox.IsVoid()) {
			Standard_Real xmin, ymin, zmin, xmax, ymax, zmax;
			boundingBox.Get(xmin, ymin, zmin, xmax, ymax, zmax);
			gp_Pnt center((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, (zmin + zmax) / 2.0);
			gp_Lin axisLine(origin, axis);
			origin = ElCLib::Value(ElCLib::Parameter(axisLine, center), axisLine);
		}

		gp_Ax1 revolAxis(origin, axis);
		TopoDS_Shape startShape = shape;
		if (startAngle != 0.0) {
			gp_Trsf startTrsf;
			startTrsf.SetRotation(revolAxis, startAngle * M_PI / 180.0);
			BRepBuilderAPI_Transform startTransform(shape, startTrsf);
			startShape = startTransform.Shape();
		}
		BRepPrimAPI_MakeRevol revolMaker(startShape, revolAxis, angleRad);
		revolShape = revolMaker.Shape();

		if (revolShape.IsNull()) {
			return "";
		}

		// 开始命令事务
		m_doc->NewCommand();

		// 最终检查并转换为实体
		if (isSolid && !revolShape.IsNull() && revolShape.ShapeType() != TopAbs_SOLID) {
			if (revolShape.ShapeType() == TopAbs_SHELL) {
				BRepBuilderAPI_MakeSolid solidMaker;
				solidMaker.Add(TopoDS::Shell(revolShape));
				if (solidMaker.IsDone()) {
					ShapeFix_Solid fixSolid(solidMaker.Solid());
					fixSolid.Perform();
					revolShape = fixSolid.Solid();
				}
			}
		}

		// 添加形状
		std::string shapeName = AddShape(revolShape);
		if (shapeName.empty()) {
			m_doc->AbortCommand();
			return "";
		}

		// 创建新标签
		TDF_Label child1 = TDF_TagSource::NewChild(m_modelLable);
		// 设置名称
		TDataStd_Name::Set(child1, TCollection_ExtendedString(L"回旋体"));
		// 设置类型
		TDataStd_Integer::Set(child1, DocModelType_REVOLVED_BODY);
		// 保存形状
		TNaming_Builder namingBuilder(child1);
		namingBuilder.Generated(revolShape);

		// 在 child1 下添加详细信息标签
		TDF_Label detailedLabel = TDF_TagSource::NewChild(child1);
		TDataStd_Name::Set(detailedLabel, "DetailedInfo");
		// 保存参数到 RealArray (轴方向、起始角度、结束角度)
		Handle(TDataStd_RealArray) detailedInfoArray = TDataStd_RealArray::Set(detailedLabel, 1, 5);
		detailedInfoArray->SetValue(1, axis.X());
		detailedInfoArray->SetValue(2, axis.Y());
		detailedInfoArray->SetValue(3, axis.Z());
		detailedInfoArray->SetValue(4, startAngle);
		detailedInfoArray->SetValue(5, endAngle);
		// 保存是否为实体参数
		TDataStd_BooleanArray::Set(detailedLabel, 1, 1)->SetValue(1, isSolid);

		// 创建展示对象
		Handle(TPrsStd_AISPresentation) anAisPresentation = TPrsStd_AISPresentation::Set(child1, TNaming_NamedShape::GetID());

		// 设置显示属性
		anAisPresentation->SetColor(Quantity_NOC_GREEN);
		anAisPresentation->SetMaterial(Graphic3d_NOM_PLASTIC);

		// 显示对象
		anAisPresentation->Display(Standard_True);
		anAisPresentation->Update();

		// 提交命令
		m_doc->CommitCommand();

		// 保存为最后创建的形状
		m_lastCreatedShape = revolShape;

		// 关联到 rootTreeNode
		m_rootTreeNode->Append(TDataStd_TreeNode::Set(child1));

		// 更新模型树
		if (m_eventAdmin) {
			ctkDictionary eventData;
			eventData["command"] = "update";
			m_eventAdmin->postEvent(ctkEvent("com/tomainapp/modeltree", eventData));
		}

		// 清除预览对象
		if (!m_RevolPreviewAIS.IsNull()) {
			m_context->Remove(m_RevolPreviewAIS, Standard_True);
		}
		this->EraseAllTempObjects();

		qDebug() << "创建旋转" << (isSolid ? "体" : "面") << "成功,名称: " << QString::fromStdString(shapeName);
		return shapeName;
	}
	catch (const Standard_Failure& ex) {
		m_doc->AbortCommand();
		qWarning() << "旋转体创建失败:" << ex.GetMessageString();
		return "";
	}
	catch (const std::exception& e) {
		m_doc->AbortCommand();
		qWarning() << "旋转体创建失败:" << e.what();
		return "";
	}
	catch (...) {
		m_doc->AbortCommand();
		qWarning() << "旋转体创建失败: 未知异常";
		return "";
	}
}

事务的核心目的

在像 OpenCASCADE 的 OCAF (Open CASCADE Application Framework) 这样的框架中,事务(在代码中体现为 m_doc->NewCommand()m_doc->CommitCommand()/AbortCommand())主要有以下几个核心目的:

  1. 原子性(Atomicity): 一系列对文档(XCAFDoc_Document)的修改操作,要么全部成功,要么全部失败回滚。这可以防止在复杂操作的中间步骤失败时,文档处于一个不一致的“半成品”状态。
  2. 撤销/重做(Undo/Redo): NewCommandCommitCommand 之间的所有操作被打包成一个单一的“命令”。这个命令会被压入命令历史堆栈中,从而允许用户执行撤销(Undo)和重做(Redo)操作。
  3. 数据完整性: 保护文档数据在多步操作过程中的一致性和有效性。

最佳时机分析

我们可以将整个 CreateRevol 函数的执行流程分解为几个阶段,来审视事务的边界。

1.阶段一:前置检查与参数验证 (Pre-flight Checks)
if (m_doc.IsNull()) { ... }
if (m_modelLable.IsNull()) { ... }
if (m_context.IsNull()) { ... }

try {
    // 检查输入是否有效
    if (shape.IsNull() || (endAngle - startAngle) <= 0) {
        return "";
    }
  • 时机分析: 绝对不应该在这里开始事务。
  • 原因: 这些检查不涉及任何对文档数据的修改。它们只是验证函数能否继续执行。如果在这里就启动事务(NewCommand),一旦检查失败,就需要立即中止(AbortCommand),这会产生不必要的开销,并且污染了“命令”历史。

结论: 函数开头的守卫代码(Guard Clauses)必须在事务之外。当前代码实现正确。


2. 阶段二:核心几何计算 (Core Geometry Computation)
// ... 计算包围盒、旋转轴等
gp_Ax1 revolAxis(origin, axis);
// ... 处理起始角度
BRepPrimAPI_MakeRevol revolMaker(startShape, revolAxis, angleRad);
revolShape = revolMaker.Shape();

if (revolShape.IsNull()) {
    return "";
}

// ... 尝试将 Shell 转换为 Solid
if (isSolid && !revolShape.IsNull() && revolShape.ShapeType() != TopAbs_SOLID) {
    // ...
}
  • 时机分析: 强烈建议不要在这里开始事务。
  • 原因:
    • 高失败率: 几何计算(尤其是布尔运算、倒角、抽壳、实体化等)是整个流程中最容易失败的部分。BRepPrimAPI_MakeRevolBRepBuilderAPI_MakeSolid 可能会因为输入的轮廓自相交、扭曲或其它拓扑问题而失败,返回一个 IsNull 的 Shape。
    • 性能开销: 几何计算是CPU密集型操作,可能非常耗时。如果在计算开始前就启动事务,意味着在整个漫长的计算过程中,文档都处于一个“事务中”的状态。这在逻辑上是不必要的。
    • 无文档修改: 到目前为止,所有操作都是在内存中创建临时的几何对象 (revolShape)。它们还没有被添加到文档的数据结构中。因此,即使计算失败,文档本身也未被“污染”,根本不需要回滚。

结论: 先完成所有主要的、可能失败的、且不直接修改文档的计算。只有当获得了最终的、有效的几何结果后,才准备开始修改文档。这种模式可以称之为 “计算-然后-提交” (Calculate-Then-Commit)当前代码实现正确。


3. 阶段三:文档数据修改 (Document Data Modification)
// 开始命令事务
m_doc->NewCommand();

// 添加形状
std::string shapeName = AddShape(revolShape);
if (shapeName.empty()) {
    m_doc->AbortCommand();
    return "";
}

// 创建新标签
TDF_Label child1 = TDF_TagSource::NewChild(m_modelLable);
// ... 设置名称、类型、保存形状、保存参数
TDataStd_Name::Set(...);
TDataStd_Integer::Set(...);
TNaming_Builder namingBuilder(...);
TDataStd_RealArray::Set(...);
TDataStd_BooleanArray::Set(...);

// 创建展示对象
Handle(TPrsStd_AISPresentation) anAisPresentation = TPrsStd_AISPresentation::Set(...);
// ... 设置显示属性
anAisPresentation->SetColor(...);
anAisPresentation->SetMaterial(...);

// 显示对象
anAisPresentation->Display(Standard_True);
anAisPresentation->Update();

// 提交命令
m_doc->CommitCommand();
  • 时机分析: 这是开始和结束事务的完美区间。
  • 原因:
    • 事务开始 (NewCommand):
      • 放在所有文档修改操作之前。这里的“文档修改”包括:
        1. AddShape: 将几何数据 revolShape 添加到文档的某种存储中(这通常是第一步)。
        2. TDF_TagSource::NewChild: 在 OCAF 的数据树中创建新的 TDF_Label
        3. TDataStd_...::Set: 向 Label 中附加各种属性(名称、类型、参数等)。
        4. TNaming_Builder: 将几何形状与 Label 关联起来。
        5. TPrsStd_AISPresentation::Set: 创建与 Label 关联的可视化对象。
      • 这些操作共同定义了一个完整的、新的“回旋体”对象。它们必须作为一个整体成功或失败。
    • 事务结束 (CommitCommand / AbortCommand):
      • CommitCommand: 当所有上述步骤都成功完成后,提交事务,将这些更改固化,并使其成为一个可撤销的单元。
      • AbortCommand: 在这个区间的任何一步失败(例如 AddShape 失败),或者 try-catch 块捕获到异常时,调用 AbortCommand。这将撤销自 NewCommand 以来的所有更改,确保文档回到事务开始前的状态。例如,如果 AddShape 成功了,但后续的 TDataStd_Name::Set 失败了,AbortCommand 会确保 AddShape 的结果也被回滚,避免了“僵尸数据”的存在。

结论: 将所有直接与文档数据模型(TDF)、命名(TNaming)、可视化(TPrsStd)相关的操作都包裹在 NewCommandCommitCommand 之间,这是事务的核心应用场景。当前代码实现堪称典范。


4. 阶段四:后处理与UI更新 (Post-processing and UI Updates)
// 保存为最后创建的形状
m_lastCreatedShape = revolShape;

// 关联到 rootTreeNode
m_rootTreeNode->Append(TDataStd_TreeNode::Set(child1));

// 更新模型树
if (m_eventAdmin) {
    // ...
    m_eventAdmin->postEvent(...);
}

// 清除预览对象
if (!m_RevolPreviewAIS.IsNull()) {
    m_context->Remove(m_RevolPreviewAIS, Standard_True);
}
this->EraseAllTempObjects();
  • 时机分析: 这些操作应在事务成功提交之后执行。
  • 原因:
    • m_lastCreatedShape: 这是类的内部状态,不属于文档的可撤销历史。
    • m_eventAdmin->postEvent: 这是向其它模块(如UI)发送通知。通知应该在数据已经确认无误地提交到文档后才发出。如果在事务内发送通知,而事务随后被中止,UI 可能已经刷新,但底层数据却被回滚了,导致UI与数据不一致。
    • m_context->Remove(...) / EraseAllTempObjects(): 这是清理临时对象/预览对象。这个操作不应该被撤销。你肯定不希望用户点击“撤销”创建旋转体时,被删除的临时预览对象又被恢复回来。

结论: 将非文档数据、非持久化的状态更新和UI通知放在事务之外,在 CommitCommand 之后执行。当前代码实现正确。

总结

当前代码中事务的最佳时机分析如下:

操作阶段 描述 是否在事务内 理由
阶段一 输入参数验证 避免为无效操作启动事务。
阶段二 核心几何计算 计算耗时且易失败,不应在事务中进行。先确保结果有效。
阶段三 文档修改 这是事务的核心。所有对文档数据、结构和可视化表现的修改都应在此,保证操作的原子性可撤销性
阶段四 UI更新与后处理 保证在数据成功提交后才通知UI,并执行不可撤销的清理操作。

因此,函数 GeometrySamples::CreateRevol 中关于 NewCommand, CommitCommandAbortCommand 的使用位置,完美地遵循了CAD软件开发的健壮性设计原则。


我们可以把它扩展成一套更完整的、便于记忆的原则性口诀或心法。

这套心法可以概括为 “一慢二包三清” 原则,帮助你在处理 OCAF 事务时做出正确的判断。

OCAF 事务处理心法:一慢、二包、三清

一慢:事务开启要“慢”半拍

核心原则: 在确认一切准备就绪、即将对文档进行不可分割的修改之前,不要轻易开启事务。

口诀:

“校验计算门外汉,
临门一脚再开团。”

解读:

  • “校验计算门外汉”: 函数的入口参数校验、复杂的几何计算(如布尔运算、倒角、旋转成型等)都应该在事务(NewCommand之外完成。这些是“门外汉”,不属于事务管理的核心成员。因为它们:

    • 高失败率: 几何计算容易失败,没必要为一次注定失败的计算开启并立即中止一个事务。
    • 高性能消耗: 计算过程耗时,不应长时间占用事务状态。
    • 不修改文档: 它们只是在内存中生成临时结果,尚未触及文档的持久化数据。
  • “临门一脚再开团”: 只有当你已经成功生成了最终的几何体(TopoDS_Shape),并且确定要将它以及它的所有相关信息(属性、名称、可视化)添加到文档中时,才执行 NewCommand()。这好比足球射门的“临门一脚”,是真正要改变“比分”(文档状态)的时刻。


二包:核心修改要“全包”

核心原则: 将所有需要作为一个原子操作、需要被统一撤销/重做的文档修改,全部包裹在 NewCommandCommitCommand 之间。

口诀:

“数据命名可视化,
一家人要进一家。”

解读:

  • “数据、命名、可视化”: 这三者是 OCAF 中定义一个“模型对象”最核心的要素:

    1. 数据 (Data): TDF_Label 的创建、TNaming_Builder 对形状的记录、TDataStd_* 对各种参数(如长宽高、旋转角度、布尔值等)的附加。
    2. 命名 (Naming): TDataStd_Name 设置用户可见的名称。
    3. 可视化 (Presentation): TPrsStd_AISPresentation 的创建和设置,决定了对象在三维视图中的颜色、材质和显示状态。
  • “一家人要进一家”: 这些操作紧密耦合,共同描述了一个完整的用户操作。它们必须在同一个事务“家庭”中,要么一起成功(CommitCommand),要么在任何一个环节出错时一起被抛弃(AbortCommand)。这样才能保证 Undo/Redo 的完整性,避免出现“只有几何体没有名字”或者“有名字有属性但看不见”的尴尬情况。


三清:善后工作要“分清”

核心原则: 将与文档数据模型无关的、不应被撤销的收尾工作,放在事务成功提交之后执行。

口诀:

“通知刷新门外报,
临时垃圾随手倒。”

解读:

  • “通知刷新门外报”:

    • UI 更新: 比如刷新模型树、更新属性面板等,应该在 CommitCommand 成功后进行。理由是:如果事务被中止,但 UI 已经刷新,就会导致界面与实际数据不一致。
    • 事件/消息发送: 同理,给系统其他模块发送“操作已完成”的通知,也必须在数据真正落盘之后。
  • “临时垃圾随手倒”:

    • 清理预览/临时对象: 比如你在操作过程中创建的用于预览的 AIS_Shape,这些是过程产物,不属于最终模型的一部分。在主操作成功提交后,就应该将它们从交互上下文(AIS_InteractiveContext)中移除。
    • 更新非文档状态: 比如 m_lastCreatedShape 这样的类成员变量,它记录的是程序运行时的状态,而不是文档的可持久化数据。它的更新也应该在事务之外。这些清理和状态更新操作,你不希望在用户点击“撤销”时它们也被“撤销”(例如,撤销创建旋转体时,不应该把已经删掉的预览对象又加回来)。

异常处理的铁律

最后,还有一个必须遵守的铁律,它贯穿于整个过程中:

口诀:

“Try-Catch是金钟罩,
回滚(Abort)才是王道。”

解读:
整个“二包”的过程(核心修改)必须被一个 try...catch 块紧紧包裹。任何在 NewCommand 之后发生的异常,都必须在 catch 块中调用 m_doc->AbortCommand()。这是保证事务原子性的最后一道防线,防止程序异常导致文档数据损坏。

通过遵循 “一慢、二包、三清”异常处理铁律,你就能构建出非常健壮、稳定且用户体验良好的 CAD 功能。

你可能感兴趣的:(OCCT 中 OCAF 事务开始结束的最佳时机)