前言:我们会经常遇到需要计算一个构件的最小外轮廓,一般直接取BoundingBox只有最大和最小值坐标,也是基于x-y坐标系下的。往往不是最小的矩形,所以分享下面的算法来计算最小的外轮廓,条件为法向量是指向Z轴的,暂时没有考虑曲线的情况
背景
需要得到下面构件的最小外接矩形
一般我们使用BoundingBox只能取到最大包络框,而且是基于x-y坐标系下面的。
实际上我们需要的是最小的外接矩形
代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autodesk.Revit.UI;
using Autodesk.Revit.DB;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.UI.Selection;
namespace RevitTest
{
[Transaction(TransactionMode.Manual)]
public class MinimumBoundingRectangleCmd : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
UIDocument uidoc = commandData.Application.ActiveUIDocument;
Document doc = uidoc.Document;
Reference selRef;
try
{
selRef = uidoc.Selection.PickObject(ObjectType.Element, "选择构件");
}
catch
{
return Result.Succeeded;
}
//实体构件
List<CurveLoop> downCLList = GetDownFaceCurveLoop(GetSolids(doc.GetElement(selRef)));
//得到凸包轮廓
List<Line> convexColsure = GetConvexClosure(doc, downCLList.SelectMany(x => x).OfType<Line>().ToList());
//得到最小的轮廓
List<Line> minBoundingRectangle = GetMinimumBoundingRectangle(doc, convexColsure);
Transaction trans = new Transaction(doc, "生成轮廓线");
trans.Start();
minBoundingRectangle.ForEach(x => LineTest(doc, x));
trans.Commit();
return Result.Succeeded;
}
/// <summary>
/// 得到最小的矩形轮廓
/// </summary>
/// <param name="convexColsure"></param>
/// <returns></returns>
public List<Line> GetMinimumBoundingRectangle(Document doc, List<Line> convexColsure)
{
List<Line> result = new List<Line>();
//确定x-y坐标系下最大的外轮廓
IEnumerable<XYZ> pointList = convexColsure.Select(x => x.GetEndPoint(0));
double area = double.MaxValue;
foreach (var l in convexColsure)
{
List<Line> rectabgleLineList = GetMinimumBoundingOnPoints(pointList, l.Direction, XYZ.BasisZ.CrossProduct(l.Direction));
double nwArea = rectabgleLineList[0].Length * rectabgleLineList[1].Length;
if (nwArea < area)
{
area = nwArea;
result = rectabgleLineList;
}
}
return result;
}
/// <summary>
/// 获取点集中的最大点和最小点
/// </summary>
/// <param name="points">点集合</param>
/// <param name="xAxis">x轴</param>
/// <param name="yAxis">y轴</param>
/// <returns></returns>
public List<Line> GetMinimumBoundingOnPoints(IEnumerable<XYZ> points, XYZ xAxis, XYZ yAxis)
{
XYZ minPoint = null;
XYZ maxPoint = null;
if (points.Count() < 2)
{
return null;
}
Transform transform = Transform.Identity;
transform.BasisX = xAxis;
transform.BasisY = yAxis;
//取逆矩阵,转回想要的坐标系
transform = transform.Inverse;
double minX = int.MaxValue;
double maxX = int.MinValue;
double minY = int.MaxValue;
double maxY = int.MinValue;
double minZ = int.MaxValue;
double maxZ = int.MinValue;
foreach (var xyz in points)
{
//转换
XYZ current = transform == null ? xyz : transform.OfPoint(xyz);
minX = Math.Min(current.X, minX);
maxX = Math.Max(current.X, maxX);
minY = Math.Min(current.Y, minY);
maxY = Math.Max(current.Y, maxY);
minZ = Math.Min(current.Z, minZ);
maxZ = Math.Max(current.Z, maxZ);
}
minPoint = new XYZ(minX, minY, minZ);
maxPoint = new XYZ(maxX, maxY, maxZ);
XYZ point1 = new XYZ(minX, maxY, maxZ);
XYZ point2 = new XYZ(maxX, minY, minZ);
List<Line> result = new List<Line>()
{
Line.CreateBound(point1,minPoint).CreateTransformed(transform.Inverse) as Line,
Line.CreateBound(minPoint,point2).CreateTransformed(transform.Inverse) as Line,
Line.CreateBound(point2,maxPoint).CreateTransformed(transform.Inverse) as Line,
Line.CreateBound(maxPoint,point1).CreateTransformed(transform.Inverse) as Line,
};
return result;
}
/// <summary>
/// 得到凸包轮廓
/// </summary>
/// <param name="doc"></param>
/// <param name="lines"></param>
/// <returns></returns>
public List<Line> GetConvexClosure(Document doc, List<Line> lines)
{
List<Line> result = new List<Line>();
List<XYZ> allPointList = new List<XYZ>();
lines.ForEach(x =>
{
XYZ point1 = SetZ(x.GetEndPoint(0), 0);
XYZ point2 = SetZ(x.GetEndPoint(1), 0);
if (allPointList.FirstOrDefault(m => m.DistanceTo(point1) < 1 / 304.8) == null)
allPointList.Add(point1);
if (allPointList.FirstOrDefault(m => m.DistanceTo(point2) < 1 / 304.8) == null)
allPointList.Add(point2);
});
//找出最x轴最小的点,然后Y轴最小点
XYZ originPoint = allPointList.OrderBy(x => x.DotProduct(XYZ.BasisX)).ThenBy(x => x.DotProduct(XYZ.BasisY)).FirstOrDefault();
//LineTest(doc, Line.CreateBound(originPoint, originPoint.Add(XYZ.BasisX * 100)));
XYZ startPoint = originPoint;
XYZ endPoint = null;
while (true)
{
if (allPointList.Count == 0) break;
XYZ startDirection = new XYZ();
if (endPoint == null)
startDirection = -XYZ.BasisY;
else
startDirection = (endPoint - startPoint).Normalize();
IEnumerable<XYZ> directionList = allPointList.OrderBy(x =>
{
if (x.DistanceTo(startPoint) < 1 / 304.8 || (endPoint != null && x.DistanceTo(endPoint) < 1 / 304.8)) return 1000000000;
XYZ direction = (x - startPoint).Normalize();
return startDirection.AngleOnPlaneTo(direction, XYZ.BasisZ);
});
XYZ onePoint = directionList.FirstOrDefault();
result.Add(Line.CreateBound(startPoint, onePoint));
if (onePoint.DistanceTo(originPoint) < 1 / 304.8) break;
endPoint = startPoint;
startPoint = onePoint;
}
return result;
}
/// <summary>
/// 返回坐标的Z值
/// </summary>
/// <param name="point"></param>
/// <param name="zValue"></param>
/// <returns></returns>
public XYZ SetZ(XYZ point, double zValue)
{
return new XYZ(point.X, point.Y, zValue);
}
/// <summary>
/// 得到实体集合中的所有底面轮廓集合
/// </summary>
/// <param name="solids"></param>
/// <returns></returns>
public List<CurveLoop> GetDownFaceCurveLoop(List<Solid> solids)
{
List<CurveLoop> result = new List<CurveLoop>();
List<PlanarFace> topFaceList = new List<PlanarFace>();
foreach (var s in solids)
{
foreach (Face geoFace in s.Faces)
{
PlanarFace temFace = geoFace as PlanarFace;
if (temFace == null) continue;
if (IsParallel(temFace.FaceNormal, XYZ.BasisZ, false))
{
topFaceList.Add(temFace);
//break;
}
}
}
foreach (var topFace in topFaceList)
{
var curveLoopList = topFace.GetEdgesAsCurveLoops().ToList();
result.AddRange(curveLoopList);
}
return result;
}
/// <summary>
/// 计算两点平面上的距离
/// </summary>
/// <param name="onePoint"></param>
/// <param name="twoPoint"></param>
/// <returns></returns>
public double GetHorizontalDistance(XYZ onePoint, XYZ twoPoint)
{
return Math.Pow(Math.Pow(onePoint.X - twoPoint.X, 2) + Math.Pow(onePoint.Y - twoPoint.Y, 2), 0.5);
}
/// <summary>
/// 生成直线
/// </summary>
/// <param name="doc"></param>
/// <param name="l"></param>
/// <returns></returns>
public ModelCurve LineTest(Document doc, Line l)
{
XYZ basicZ = XYZ.BasisZ;
if (l.Direction.AngleTo(XYZ.BasisZ) < 0.0001 || l.Direction.AngleTo(-XYZ.BasisZ) < 0.0001)
basicZ = XYZ.BasisY;
XYZ normal = basicZ.CrossProduct(l.Direction).Normalize();
Plane plane = Plane.CreateByNormalAndOrigin(normal, l.GetEndPoint(0));
Transaction transCreate = null;
if (!doc.IsModifiable)
transCreate = new Transaction(doc, "模型线测试");
transCreate?.Start();
SketchPlane sktpl = SketchPlane.Create(doc, plane);
ModelCurve mc = doc.IsFamilyDocument ? doc.FamilyCreate.NewModelCurve(l, sktpl) : doc.Create.NewModelCurve(l, sktpl);
transCreate?.Commit();
return mc;
}
/// <summary>
/// 拿到对应构件的Solid集合
/// </summary>
/// <param name="elem"></param>
/// <param name="vdtLevel"></param>
/// <returns></returns>
public List<Solid> GetSolids(Element elem, ViewDetailLevel vdtLevel = ViewDetailLevel.Fine)
{
if (elem == null)
{
return new List<Solid>();
}
GeometryElement geometryElement = elem.get_Geometry(new Options
{
ComputeReferences = true,
DetailLevel = vdtLevel,
IncludeNonVisibleObjects = false,
});
return GetSolids(geometryElement);
}
/// <summary>
/// 获取所有的Solid
/// </summary>
/// <param name="geometryElement"></param>
/// <returns></returns>
public List<Solid> GetSolids(GeometryElement geometryElement)
{
List<Solid> result = new List<Solid>();
foreach (GeometryObject geomObj in geometryElement)
{
Solid solid = geomObj as Solid;
if (null != solid)
{
result.Add(solid);
continue;
}
//If this GeometryObject is Instance, call AddCurvesAndSolids
GeometryInstance geomInst = geomObj as GeometryInstance;
if (null != geomInst)
{
GeometryElement transformedGeomElem = geomInst.GetInstanceGeometry(Transform.Identity);
result.AddRange(GetSolids(transformedGeomElem));
}
}
return result;
}
/// <summary>
/// 得到实体指定方向的一开始的面(取该方向上最里面的面)
/// </summary>
/// <param name="targetSolid"></param>
/// <returns></returns>
public List<CurveLoop> GetDirectionOriginCurveLoop(Solid targetSolid, XYZ direction)
{
List<CurveLoop> result = new List<CurveLoop>();
List<PlanarFace> topFaceList = new List<PlanarFace>();
foreach (Face geoFace in targetSolid.Faces)
{
PlanarFace temFace = geoFace as PlanarFace;
if (temFace == null) continue;
if (IsParallel(temFace.FaceNormal, direction))
{
topFaceList.Add(temFace);
}
}
var topFace = topFaceList.OrderBy(x => x.Origin.DotProduct(direction)).FirstOrDefault();
var curveLoopList = topFace.GetEdgesAsCurveLoops().ToList();
result.AddRange(curveLoopList);
return result;
}
/// <summary>
/// 向量是否平行
/// </summary>
/// <param name="vector1"></param>
/// <param name="vector2"></param>
/// <param name="v">true为同向平行,false为反向平行,null为平行</param>
/// <param name="tolerance">允许误差的角度</param>
/// <returns></returns>
public bool IsParallel(XYZ vector1, XYZ vector2, bool? v = null, double tolerance = 0.1)
{
var angle = vector1.AngleTo(vector2) / Math.PI * 180;
if (v == null)
{
return angle >= 180 - tolerance || angle <= tolerance;
}
else if (v == true)
{
return angle <= tolerance;
}
else
{
return angle >= 180 - tolerance;
}
}
/// <summary>
/// 传入集合的集合体,批量操作
/// </summary>
/// <param name="solids"></param>
/// <param name="booleanOperationsType"></param>
/// <returns></returns>
public Solid GetUnionSolid(List<Solid> solids, BooleanOperationsType booleanOperationsType)
{
Solid firstSolid = solids[0];
solids.RemoveAt(0);
//对所有的几何体进行融合
foreach (var oneSoild in solids)
{
try
{
firstSolid = SolidBooleanOperation(firstSolid, oneSoild, booleanOperationsType);
}
catch
{
}
}
return firstSolid;
}
/// <summary>
/// Solid布尔操作
/// </summary>
/// <param name="solidA"></param>
/// <param name="solidB"></param>
/// <param name="booleanOperationsType"></param>
/// <returns></returns>
public Solid SolidBooleanOperation(Solid solidA, Solid solidB, BooleanOperationsType booleanOperationsType)
{
Solid result = null;
try
{
result = BooleanOperationsUtils.ExecuteBooleanOperation(solidA, solidB, booleanOperationsType);
}
catch (Exception ex)
{
result = BooleanOperationsUtils.ExecuteBooleanOperation(solidB, solidA, booleanOperationsType);
}
return result;
}
}
}
思路
一、确定底面轮廓线
通过找到构件的所有底面,然后返回所有的面的轮廓线
二、根据轮廓线计算凸包轮廓
把线的起终点都添加到集合里面,然后去重。由于我们当前方法仅处理法向量为XYZ.BasisZ的平面,需要把所有的点都降到统一的平面上,默认Z轴为0。
凸包算法的思路:
-
找到X轴和Y轴数值最小的点,确定为原点,也为起点
-
默认开始的方向为-Y轴的方向
-
根据点集里面的点,分别构建一个起点出发,指向其他点的向量。
-
如果是原点的时候,就找-Y轴和哪个点构造的向量的逆时针夹角最小。这个点就是我们要的下一个凸点。
-
以找到的凸点为起点,找到的凸点和上一个凸点的向量为初始向量。重复构造点集向量,找最小夹角的过程。
-
在回到原点的时候,寻找结束。
三、根据凸包轮廓生成矩形轮廓
-
以凸包轮廓的每一条线的方向为X轴,通过Z轴和X轴的叉乘得到Y轴
-
以X和Y轴的单位向量,构建一个Transform对象,并把BasisX和BasisY设置组成对应的XY轴方向。(相当于把构件以点旋转了一定角度,把坐标系变成我们要的坐标系)
-
把所有的点都转到以我们需要的XY轴的坐标系下,再计算四个角点,并连成矩形。得到的线再转回到项目的坐标下,就得到这条边对应的外接矩形了。
-
所有的边都计算一个外接矩形,然后得到哪个外接矩形面积最小,就是我们要的那个了。
写在最后
希望这篇文章可以给你带来帮助~~~