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

原文链接: 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 Box


		bb = 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

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值