今天我们讨论一个我很感兴趣的问题:
1. 如何精确地获取空间范围框(Scope Box)的几何位置、尺寸和方向?
2. 如何精确地设置三维视图剖视框(Section Box)的几何位置、尺寸和方向?
换句话说就是如何使用手动调整的范围框来定义视图剖视框,即模型是如何在三维视图中被剪切的。
实际上我已经在博文 create a section view parallel to a wall (译者注:我的翻译版本在这里 创建与墙体平行的剖视图) 中说明了如何设置一个视图剖视框。关键在于正确地设置视图的 SectionBox 属性。该属性是一个 BoundingBoxXYZ 类型的值,即一个转换(Transform)加上最大坐标值和最小坐标值。该属性描述了范围框的位置、方向和尺寸。在那篇博文里,这个属性值被传入 ViewSection.CreateSection() 方法用于创建剖视图。
如果需要修改(而不是创建)一个存在的剖视图,我们只要将更新后的 BoundingBoxXYZ 赋予该剖视图的 SectionBox 属性即可。
问题
下图是一个模型的三维视图,其中有一个虚线表示的范围框。三维视图的 SectionBox 属性被选中,所以视图的剖视框(实线表示)也显示出来了。虚线范围框和实线剖视框都被选中。
我们的目标是使用程序重新定位并且旋转剖视框,使其与范围框的位置、方向和尺寸都相同。
Jeremy
你需要实现如下的操作步骤:
1. 从范围框获取所需几何数据
范围框没有提供直接的 Location 属性,所以只能从它的几何定义中计算得到。通过 RevitLookup 我们可以发现,范围框包含12条线段(即范围框的12条边)。所以你需要通过这12条线段来范围框的计算尺寸和方向,进而计算视图剖视框。
2. 创建需要的剖视框对象(转换、最大坐标值、最小坐标值……)
参见 create a section view parallel to a wall3. 将剖视框对象设置到视图的 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;
}
我们还差一点儿就要成功了。
根据范围框计算合适的视图剖视框
现在我需要确认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;
结果如下图所示:
表示范围框的虚线被剖视框的实线完全覆盖了。
完整的代码可以在这里下载: SetSectionBox.zip