Building Coder(Revit 二次开发)- 设置匹配范围框的视图剖视框

来源:互联网 发布:淘宝退款优惠券退吗 编辑:程序博客网 时间:2024/05/17 09:35
原文链接:Set View Section Box to Match Scope Box

今天我们讨论一个我很感兴趣的问题:
1. 如何精确地获取空间范围框(Scope Box)的几何位置、尺寸和方向?
2. 如何精确地设置三维视图剖视框(Section Box)的几何位置、尺寸和方向?

换句话说就是如何使用手动调整的范围框来定义视图剖视框,即模型是如何在三维视图中被剪切的。


实际上我已经在博文 create a section view parallel to a wall (译者注:我的翻译版本在这里 创建与墙体平行的剖视图) 中说明了如何设置一个视图剖视框。关键在于正确地设置视图的 SectionBox 属性。该属性是一个 BoundingBoxXYZ 类型的值,即一个转换(Transform)加上最大坐标值和最小坐标值。该属性描述了范围框的位置、方向和尺寸。在那篇博文里,这个属性值被传入 ViewSection.CreateSection() 方法用于创建剖视图。

如果需要修改(而不是创建)一个存在的剖视图,我们只要将更新后的 BoundingBoxXYZ 赋予该剖视图的 SectionBox 属性即可。

问题

下图是一个模型的三维视图,其中有一个虚线表示的范围框。三维视图的 SectionBox 属性被选中,所以视图的剖视框(实线表示)也显示出来了。虚线范围框和实线剖视框都被选中。

Source scope box and target view section box
我们的目标是使用程序重新定位并且旋转剖视框,使其与范围框的位置、方向和尺寸都相同。

Jeremy

你需要实现如下的操作步骤:

1. 从范围框获取所需几何数据

范围框没有提供直接的 Location 属性,所以只能从它的几何定义中计算得到。通过 RevitLookup 我们可以发现,范围框包含12条线段(即范围框的12条边)。所以你需要通过这12条线段来范围框的计算尺寸和方向,进而计算视图剖视框。

2. 创建需要的剖视框对象(转换、最大坐标值、最小坐标值……)

参见 create a section view parallel to a wall 

3. 将剖视框对象设置到视图的 SectionBox 属性

view.SectionBox = newSectionBox

为了验证我的方案,我实现了 GetScopeBoxBoundingBox() 方法用于从范围框的12条边中抽取数据创建 BoundingBox 对象,并基于它 创建了 SetSectionBox 命令。

GetScopeBoxBoundingBox() 方法的算法如下:
1. 选取一条边所在线为X轴,一个端点作为原点;
2. 找到其它两条经过原点的线,分别作为Y轴和Z轴;
3. 可以通过Y轴和Z轴的方向选取,确认新的坐标系是右手螺旋方向;


确认坐标系为右手螺旋方向

当且仅当坐标系确定的平行六面体的有符号体积为正值时,该坐标系为右手螺旋方向。有符号体积的计算公式为:前两个坐标轴向量的叉积与第三个坐标轴的点积。

/// <summary>/// 由向量 a,b,c 围成的平行六面体的有符号体积。德语称之为 Spatprodukt。/// </summary>static double SignedParallelipedVolume( XYZ a, XYZ b, XYZ c ){  return a.CrossProduct( b ).DotProduct( c );} /// <summary>/// 如果三个向量 a,b,c 组成右手螺旋方向的坐标系,则返回 true。/// 即由这三个向量围成的平行六面体的有符号体积为正值。/// </summary>bool IsRightHanded( XYZ a, XYZ b, XYZ c ){          return 0 < SignedParallelipedVolume( a, b, c );}

获取范围框的 Bounding Box

以上准备工作就绪之后,是可以使用 GetScopeBoxBoundingBox() 方法获取 Bounding Box 了。

BoundingBoxXYZ GetScopeBoxBoundingBox( Element scopeBox ){Document doc = scopeBox.Document;Application app = doc.Application;Options opt = app.Create.NewGeometryOptions();GeometryElement geo = scopeBox.get_Geometry( opt );int n = geo.Count<GeometryObject>(); if( 12 != n ){throw new ArgumentException( "Expected exactly 12 lines in scope box geometry" );} XYZ origin = null;XYZ vx = null;XYZ vy = null;XYZ vz = null; // 从平行六面体的12条边中获取X/Y/Z轴 foreach( GeometryObject obj in geo ){Debug.Assert( obj is Line, "expected only lines in scope box geometry" ); Line line = obj as Line; XYZ p = line.get_EndPoint( 0 );XYZ q = line.get_EndPoint( 1 );XYZ v = q - p; if( null == origin ){origin = p;vx = v;}else if( p.IsAlmostEqualTo( origin ) || q.IsAlmostEqualTo( origin ) ){if( q.IsAlmostEqualTo( origin ) ){v = v.Negate();} if( null == vy ){Debug.Assert( IsPerpendicular( vx, v ), "expected orthogonal lines in scope box geometry" );vy = v;}else{Debug.Assert( null == vz, "expected exactly three orthogonal lines to originate in one point" );Debug.Assert( IsPerpendicular( vx, v ), "expected orthogonal lines in scope box geometry" );Debug.Assert( IsPerpendicular( vy, v ), "expected orthogonal lines in scope box geometry" ); vz = v; if( !( IsRightHanded( vx, vy, vz ) ) ){XYZ tmp = vz;vz = vy;vy = tmp;}break;}}} // 创建转换(Transform) Transform t = Transform.Identity;t.Origin = origin;t.BasisX = vx.Normalize();t.BasisY = vy.Normalize();t.BasisZ = vz.Normalize(); Debug.Assert( t.IsConformal, "expected resulting transform to be conformal" ); // 创建 Bounding Box BoundingBoxXYZ bb = new BoundingBoxXYZ();bb.Transform = t;bb.Min = XYZ.Zero;bb.Max = vx + vy + vz; return bb;}

我们还差一点儿就要成功了。

Source scope box partially defining target view section box

根据范围框计算合适的视图剖视框

现在我需要确认Z轴确实是垂直向上的。在考虑视图方向的前提下,使用最靠近观察者的范围框边界作为剖视框的Z轴。


因此我创建了另外一个方法 GetSectionBoundingBoxFromScopeBox()。它根据范围框的位置、视图方向计算出一个合适的剖视图 Bounding Box:
1. 找到最接近观察者的垂直边界;
2. 使用该边界的底部端点作为原点;
3. 找到另外两条源于原点的边界;
4. 使用这三条边界定义 Bounding Box

使用视图方向和范围框 Bounding Box 的最大尺寸来共同确定视点。我们将身处视点来观测范围框。我将会遍历两次范围框的边界集合。在第一次遍历中,我确定原点和Z轴。在第二次遍历中,我确定Y轴和Z轴。

BoundingBoxXYZ GetSectionBoundingBoxFromScopeBox(Element scopeBox,XYZ viewdirTowardViewer ){Document doc = scopeBox.Document;Application app = doc.Application; // 从观察者的角度在范围框的外部找到一个可能的视点BoundingBoxXYZ bb = scopeBox.get_BoundingBox( null ); XYZ v = bb.Max - bb.Min; double size = v.GetLength(); XYZ viewPoint = bb.Min + 10 * size * viewdirTowardViewer; // 获取范围框几何数据(即它的12条边界) Options opt = app.Create.NewGeometryOptions();GeometryElement geo = scopeBox.get_Geometry( opt );int n = geo.Count<GeometryObject>(); if( 12 != n ){throw new ArgumentException( "Expected exactly 12 lines in scope box geometry" );} // 将最接近观察者的那条边界的底部端点作为原点,从原点出发垂直向上的向量作为Z轴。// 如果和观察者距离最近的边界多于一条,则选择最左边的那条(假设给定的视图方向中Z轴是垂直向上的) double dist = double.MaxValue;XYZ origin = null;XYZ vx = null;XYZ vy = null;XYZ vz = null;XYZ p, q; foreach( GeometryObject obj in geo ){Debug.Assert( obj is Line, "expected only lines in scope box geometry" ); Line line = obj as Line; p = line.get_EndPoint( 0 );q = line.get_EndPoint( 1 );v = q - p; if( IsVertical( v ) ){if( q.Z < p.Z ){p = q;v = v.Negate();} if( p.DistanceTo( viewPoint ) < dist ){origin = p;dist = origin.DistanceTo( viewPoint );vz = v;}}} // 找到另外两条以原点为端点的边界作为X轴和Y轴,并确认X/Y/Z组成符合右手螺旋方向的坐标系 foreach( GeometryObject obj in geo ){Line line = obj as Line; p = line.get_EndPoint( 0 );q = line.get_EndPoint( 1 );v = q - p; if( IsVertical( v ) ) // 已经在上面的遍历中处理过了{continue;} if( p.IsAlmostEqualTo( origin ) || q.IsAlmostEqualTo( origin ) ){if( q.IsAlmostEqualTo( origin ) ){v = v.Negate();}if( null == vx ){Debug.Assert( IsPerpendicular( vz, v ), "expected orthogonal lines in scope box geometry" );vx = v;}else{Debug.Assert( null == vy, "expected exactly three orthogonal lines to originate in one point" );Debug.Assert( IsPerpendicular( vz, v ), "expected orthogonal lines in scope box geometry" );Debug.Assert( IsPerpendicular( vx, v ), "expected orthogonal lines in scope box geometry" ); vy = v; if( !( IsRightHanded( vx, vy, vz ) ) ){XYZ tmp = vx;vx = vy;vy = tmp;}break;}}} // 创建转换(Transform) Transform t = Transform.Identity;t.Origin = origin;t.BasisX = vx.Normalize();t.BasisY = vy.Normalize();t.BasisZ = vz.Normalize(); Debug.Assert( t.IsConformal, "expected resulting transform to be conformal" ); // 创建 Bounding Boxbb = new BoundingBoxXYZ();bb.Transform = t;bb.Min = XYZ.Zero;bb.Max = vx + vy + vz; return bb;}

集成测试

创建一个外部命令,在当前的三维视图中首先找到第一个范围框元素,然后执行如下操作:

1. 访问当前视图并确认是否为三维视图;
2. 选中范围框元素;
3. 使用 GetSectionBoundingBoxFromScopeBox() 方法根据范围框得到剖视框;
4. 将剖视框的 Bounding Box 设置到当前视图的 SectionBox 属性

  UIApplication uiapp = commandData.Application;  UIDocument uidoc = uiapp.ActiveUIDocument;  Application app = uiapp.Application;  Document doc = uidoc.Document;   View3D view = doc.ActiveView as View3D;   if( null == view )  {    message = "Please run this command in a 3D view.";    return Result.Failed;  }   Element scopeBox = new FilteredElementCollector( doc, view.Id )      .OfCategory( BuiltInCategory.OST_VolumeOfInterest )      .WhereElementIsNotElementType()      .FirstElement();BoundingBoxXYZ viewSectionBox = GetSectionBoundingBoxFromScopeBox( scopeBox, view.ViewDirection );   using( Transaction tx = new Transaction( doc ) )  {    tx.Start( "Move And Resize Section Box" );     view.SectionBox = viewSectionBox;     tx.Commit();  }  return Result.Succeeded;

结果如下图所示:
Source scope box data transferred to target view section box
表示范围框的虚线被剖视框的实线完全覆盖了。


完整的代码可以在这里下载:SetSectionBox.zip

原创粉丝点击