前言
作为三维软件的使用者,有的时候是很好奇它们是如何做到选中一个物体的。可以想象一下,电脑屏幕是一个二维的,当我们点击鼠标的时候的,它就像是射出一条光线,然后选中的物体就是和这条光线相交的。
Revit 提供了一个类 ReferenceIntersector
,它的功能和上面的描述非常接近,不过它并不局限于从屏幕出发的一条的光线,可以是任意的位置和方向。
ReferenceIntersector
两行代码就可以找到从 center
出发,rayDirection
为方向的射线遇到的第一个构件。实际上,难点其实也不在于这个类怎么用。而是光线的位置如何设定,怎么理解 Revit 的坐标系。
ReferenceIntersector refIntersector = new ReferenceIntersector(view3D);
ReferenceWithContext referenceWithContext = refIntersector.FindNearest(center, rayDirection);
例子
来源:官方文档,有修改,该例子没有 Transaction
,无法运行,需要加上。
这个例子的目的是假设有一个天窗(skyLight),从这个天窗向下找到的第一个楼板。
步骤:
- 通过 UI 选中天窗
- 计算从天窗到楼板的距离,并用一条线表示
- 用 ModelCurve 来表示这条线
效果:
通过 UI 选中天窗
关键逻辑:
- 得到选中的物体,
revit.Application.ActiveUIDocument.Selection.GetElementIds()
; - 代码仅仅处理你只选中一个天窗的情况,不要多选;
- 天窗的类型是
FamilyInstance
,它自身的类型是BuiltInCategory.OST_Windows
,它的 host 必须是BuiltInCategory.OST_Roofs
。
代码:
Document doc = revit.Application.ActiveUIDocument.Document;
ICollection<ElementId> selectedIds = revit.Application.ActiveUIDocument.Selection.GetElementIds();
// If skylight is selected, process it.
FamilyInstance skylight = null;
if (selectedIds.Count == 1)
{
foreach (ElementId id in selectedIds)
{
Element e = doc.GetElement(id);
if (e is FamilyInstance)
{
FamilyInstance instance = e as FamilyInstance;
bool isWindow = (instance.Category.Id.IntegerValue == (int)BuiltInCategory.OST_Windows);
bool isHostedByRoof = (instance.Host.Category.Id.IntegerValue == (int)BuiltInCategory.OST_Roofs);
if (isWindow && isHostedByRoof)
{
skylight = instance;
}
}
}
}
if (skylight == null)
{
message = "Please select one skylight.";
return Result.Cancelled;
}
计算从天窗到楼板的距离,并用一条线表示
这个逻辑被包在函数 CalculateLineAboveFloor
里。
第一步,找到第一个不是模板的三维视图。
// Find a 3D view to use for the ReferenceIntersector constructor
FilteredElementCollector collector = new FilteredElementCollector(doc);
Func<View3D, bool> isNotTemplate = v3 => !(v3.IsTemplate);
View3D view3D = collector.OfClass(typeof(View3D)).Cast<View3D>().First<View3D>(isNotTemplate);
第二步,计算光线的方向和位置。起点是天窗几何位置的中心,方向是垂直向下。
BoundingBoxXYZ box = skylight.get_BoundingBox(view3D);
XYZ center = box.Min.Add(box.Max).Multiply(0.5);
XYZ rayDirection = new XYZ(0, 0, -1);
第三步,设置过滤,只要楼板。
ElementClassFilter filter = new ElementClassFilter(typeof(Floor));
第四步,开始进行光线追踪,找到最近的楼板的面。
ReferenceIntersector refIntersector = new ReferenceIntersector(filter, FindReferenceTarget.Face, view3D);
ReferenceWithContext referenceWithContext = refIntersector.FindNearest(center, rayDirection);
第五步,从光线追踪的结果里拿到相交的位置,和光线的起点做一条线。
Reference reference = referenceWithContext.GetReference();
XYZ intersection = reference.GlobalPoint;
Line result = Line.CreateBound(center, intersection);
用 ModelCurve 来表示这条线
步骤:
第一步,创建 SketchPlane
。
Plane plane = Plane.CreateByNormalAndOrigin(new XYZ(1, 0, 0), line.GetEndPoint(0));
SketchPlane sketchPlane = SketchPlane.Create(doc, plane);
第二步,创建模型线。
ModelCurve curve = doc.Create.NewModelCurve(line, sketchPlane);
整体代码:
public class RayProjection : IExternalCommand
{
public Result Execute(ExternalCommandData revit, ref string message, ElementSet elements)
{
Document doc = revit.Application.ActiveUIDocument.Document;
ICollection<ElementId> selectedIds = revit.Application.ActiveUIDocument.Selection.GetElementIds();
// If skylight is selected, process it.
FamilyInstance skylight = null;
if (selectedIds.Count == 1)
{
foreach (ElementId id in selectedIds)
{
Element e = doc.GetElement(id);
if (e is FamilyInstance)
{
FamilyInstance instance = e as FamilyInstance;
bool isWindow = (instance.Category.Id.IntegerValue == (int)BuiltInCategory.OST_Windows);
bool isHostedByRoof = (instance.Host.Category.Id.IntegerValue == (int)BuiltInCategory.OST_Roofs);
if (isWindow && isHostedByRoof)
{
skylight = instance;
}
}
}
}
if (skylight == null)
{
message = "Please select one skylight.";
return Result.Cancelled;
}
Transaction tran = new Transaction(doc, "Projection");
tran.Start();
// Calculate the height
Line line = CalculateLineAboveFloor(doc, skylight);
// Create a model curve to show the distance
Plane plane = Plane.CreateByNormalAndOrigin(new XYZ(1, 0, 0), line.GetEndPoint(0));
SketchPlane sketchPlane = SketchPlane.Create(doc, plane);
ModelCurve curve = doc.Create.NewModelCurve(line, sketchPlane);
tran.Commit();
// Show a message with the length value
TaskDialog.Show("Distance", "Distance to floor: " + String.Format("{0:f2}", line.Length));
return Result.Succeeded;
}
/// <summary>
/// Determines the line segment that connects the skylight to the nearest floor.
/// </summary>
/// <returns>The line segment.</returns>
private Line CalculateLineAboveFloor(Document doc, FamilyInstance skylight)
{
// Find a 3D view to use for the ReferenceIntersector constructor
FilteredElementCollector collector = new FilteredElementCollector(doc);
Func<View3D, bool> isNotTemplate = v3 => !(v3.IsTemplate);
View3D view3D = collector.OfClass(typeof(View3D)).Cast<View3D>().First<View3D>(isNotTemplate);
// Use the center of the skylight bounding box as the start point.
BoundingBoxXYZ box = skylight.get_BoundingBox(view3D);
XYZ center = box.Min.Add(box.Max).Multiply(0.5);
// Project in the negative Z direction down to the floor.
XYZ rayDirection = new XYZ(0, 0, -1);
ElementClassFilter filter = new ElementClassFilter(typeof(Floor));
ReferenceIntersector refIntersector = new ReferenceIntersector(filter, FindReferenceTarget.Face, view3D);
ReferenceWithContext referenceWithContext = refIntersector.FindNearest(center, rayDirection);
Reference reference = referenceWithContext.GetReference();
XYZ intersection = reference.GlobalPoint;
// Create line segment from the start point and intersection point.
Line result = Line.CreateBound(center, intersection);
return result;
}
}