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()
)主要有以下几个核心目的:
XCAFDoc_Document
)的修改操作,要么全部成功,要么全部失败回滚。这可以防止在复杂操作的中间步骤失败时,文档处于一个不一致的“半成品”状态。NewCommand
和 CommitCommand
之间的所有操作被打包成一个单一的“命令”。这个命令会被压入命令历史堆栈中,从而允许用户执行撤销(Undo)和重做(Redo)操作。我们可以将整个 CreateRevol
函数的执行流程分解为几个阶段,来审视事务的边界。
if (m_doc.IsNull()) { ... }
if (m_modelLable.IsNull()) { ... }
if (m_context.IsNull()) { ... }
try {
// 检查输入是否有效
if (shape.IsNull() || (endAngle - startAngle) <= 0) {
return "";
}
NewCommand
),一旦检查失败,就需要立即中止(AbortCommand
),这会产生不必要的开销,并且污染了“命令”历史。结论: 函数开头的守卫代码(Guard Clauses)必须在事务之外。当前代码实现正确。
// ... 计算包围盒、旋转轴等
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_MakeRevol
或 BRepBuilderAPI_MakeSolid
可能会因为输入的轮廓自相交、扭曲或其它拓扑问题而失败,返回一个 IsNull
的 Shape。revolShape
)。它们还没有被添加到文档的数据结构中。因此,即使计算失败,文档本身也未被“污染”,根本不需要回滚。结论: 先完成所有主要的、可能失败的、且不直接修改文档的计算。只有当获得了最终的、有效的几何结果后,才准备开始修改文档。这种模式可以称之为 “计算-然后-提交” (Calculate-Then-Commit)。当前代码实现正确。
// 开始命令事务
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
):
AddShape
: 将几何数据 revolShape
添加到文档的某种存储中(这通常是第一步)。TDF_TagSource::NewChild
: 在 OCAF 的数据树中创建新的 TDF_Label
。TDataStd_...::Set
: 向 Label 中附加各种属性(名称、类型、参数等)。TNaming_Builder
: 将几何形状与 Label 关联起来。TPrsStd_AISPresentation::Set
: 创建与 Label 关联的可视化对象。CommitCommand
/ AbortCommand
):
CommitCommand
: 当所有上述步骤都成功完成后,提交事务,将这些更改固化,并使其成为一个可撤销的单元。AbortCommand
: 在这个区间的任何一步失败(例如 AddShape
失败),或者 try-catch
块捕获到异常时,调用 AbortCommand
。这将撤销自 NewCommand
以来的所有更改,确保文档回到事务开始前的状态。例如,如果 AddShape
成功了,但后续的 TDataStd_Name::Set
失败了,AbortCommand
会确保 AddShape
的结果也被回滚,避免了“僵尸数据”的存在。结论: 将所有直接与文档数据模型(TDF)、命名(TNaming)、可视化(TPrsStd)相关的操作都包裹在 NewCommand
和 CommitCommand
之间,这是事务的核心应用场景。当前代码实现堪称典范。
// 保存为最后创建的形状
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
, CommitCommand
和 AbortCommand
的使用位置,完美地遵循了CAD软件开发的健壮性设计原则。
我们可以把它扩展成一套更完整的、便于记忆的原则性口诀或心法。
这套心法可以概括为 “一慢二包三清” 原则,帮助你在处理 OCAF 事务时做出正确的判断。
核心原则: 在确认一切准备就绪、即将对文档进行不可分割的修改之前,不要轻易开启事务。
口诀:
“校验计算门外汉,
临门一脚再开团。”
解读:
“校验计算门外汉”: 函数的入口参数校验、复杂的几何计算(如布尔运算、倒角、旋转成型等)都应该在事务(NewCommand
)之外完成。这些是“门外汉”,不属于事务管理的核心成员。因为它们:
“临门一脚再开团”: 只有当你已经成功生成了最终的几何体(TopoDS_Shape
),并且确定要将它以及它的所有相关信息(属性、名称、可视化)添加到文档中时,才执行 NewCommand()
。这好比足球射门的“临门一脚”,是真正要改变“比分”(文档状态)的时刻。
核心原则: 将所有需要作为一个原子操作、需要被统一撤销/重做的文档修改,全部包裹在 NewCommand
和 CommitCommand
之间。
口诀:
“数据命名可视化,
一家人要进一家。”
解读:
“数据、命名、可视化”: 这三者是 OCAF 中定义一个“模型对象”最核心的要素:
TDF_Label
的创建、TNaming_Builder
对形状的记录、TDataStd_*
对各种参数(如长宽高、旋转角度、布尔值等)的附加。TDataStd_Name
设置用户可见的名称。TPrsStd_AISPresentation
的创建和设置,决定了对象在三维视图中的颜色、材质和显示状态。“一家人要进一家”: 这些操作紧密耦合,共同描述了一个完整的用户操作。它们必须在同一个事务“家庭”中,要么一起成功(CommitCommand
),要么在任何一个环节出错时一起被抛弃(AbortCommand
)。这样才能保证 Undo/Redo 的完整性,避免出现“只有几何体没有名字”或者“有名字有属性但看不见”的尴尬情况。
核心原则: 将与文档数据模型无关的、不应被撤销的收尾工作,放在事务成功提交之后执行。
口诀:
“通知刷新门外报,
临时垃圾随手倒。”
解读:
“通知刷新门外报”:
CommitCommand
成功后进行。理由是:如果事务被中止,但 UI 已经刷新,就会导致界面与实际数据不一致。“临时垃圾随手倒”:
AIS_Shape
,这些是过程产物,不属于最终模型的一部分。在主操作成功提交后,就应该将它们从交互上下文(AIS_InteractiveContext
)中移除。m_lastCreatedShape
这样的类成员变量,它记录的是程序运行时的状态,而不是文档的可持久化数据。它的更新也应该在事务之外。这些清理和状态更新操作,你不希望在用户点击“撤销”时它们也被“撤销”(例如,撤销创建旋转体时,不应该把已经删掉的预览对象又加回来)。最后,还有一个必须遵守的铁律,它贯穿于整个过程中:
口诀:
“Try-Catch是金钟罩,
回滚(Abort)才是王道。”
解读:
整个“二包”的过程(核心修改)必须被一个 try...catch
块紧紧包裹。任何在 NewCommand
之后发生的异常,都必须在 catch
块中调用 m_doc->AbortCommand()
。这是保证事务原子性的最后一道防线,防止程序异常导致文档数据损坏。
通过遵循 “一慢、二包、三清” 和 异常处理铁律,你就能构建出非常健壮、稳定且用户体验良好的 CAD 功能。