Hello,
I try to determine for all types of surfaces what is the outer Edgeloop.
Is it always the first one on the face.EdgeLoops?
If not how to find it?
Philippe.
Solved! Go to Solution.
Solved by jeremytammik. Go to Solution.
Solved by jeremytammik. Go to Solution.
The area approach mentioned in the above link is probably more reliable but I had an alternate idea based on getting all co-ordinates linked to the edge loops they are from and then finding the minimums in a certain direction. Below I've taken min.x but but same would apply for min.y, max.x and max.y
I could not think of a situation where if you convert the 3D co-ordinates of each edge curve to a co-ordinate system of the plane you would end up with a min.x for the outer loop greater than the min.x for the inner loop. Similarly the max.x of the outer loop should be greater than the max.x of the inner loop. This appeared to be true for most cases but then there was the issue of circular curves where the two end points could be further back than a point projecting into the centre of the curve. So I decided to treat cyclic curves differently by checking for min.x along the curve.
I came up with the two below functions one for planner faces and the other for faces in general. They both seem to give reliable results on the cases in the image below (based on expected edge counts for the outer loops). However there may be a case that fail. The parts related to co-ordinate transformations may need looking at (there may be better approaches). I checked the z of each transformed point to see that it was 0 or virtually 0, so seems to be as expected.
For the more generalised method I'm using API functionality which projects a point onto a face, I was expecting some possible intersection errors since I'm checking points on the boundary of the face. I've not checked such things in detail.
In summary the area method mentioned in link is likely better and more reliable, not sure which is faster.
Public Shared Function Run(ByVal commandData As Autodesk.Revit.UI.ExternalCommandData) As Autodesk.Revit.UI.Result Dim IntApp As UIApplication = commandData.Application Dim IntUIDoc As UIDocument = IntApp.ActiveUIDocument If IntUIDoc Is Nothing Then Return Result.Failed Else Dim IntDoc As Document = IntUIDoc.Document Dim R As Reference = Nothing Try R = IntUIDoc.Selection.PickObject(Selection.ObjectType.Face) Catch ex As Exception End Try If R Is Nothing Then Return Result.Cancelled Else Dim F_El As Element = IntDoc.GetElement(R.ElementId) If F_El Is Nothing Then Return Result.Failed Else Dim F As Face = TryCast(F_El.GetGeometryObjectFromReference(R), Face) If F Is Nothing Then Return Result.Failed Else Dim EA1 As EdgeArray = PlannerFaceOuterLoop(F) Dim EA2 As EdgeArray = OuterLoop(F) Return Result.Succeeded End Function Public Shared Function OuterLoop(F As Face) As EdgeArray Dim MinU = Function(C As Curve) As Double If C.IsCyclic Then Dim Min As Double = Double.NaN For I = 0 To 20 Dim Param As Double = I / 20 Dim CuvPt As XYZ = C.Evaluate(Param, True) Dim IR As IntersectionResult = F.Project(CuvPt) If IR Is Nothing = False Then If Min = Double.NaN Then Min = IR.UVPoint.U Else If IR.UVPoint.U < Min Then Min = IR.UVPoint.U End If End If End If Next Return Min Else Dim Pt1 As XYZ = C.GetEndPoint(0) Dim Pt2 As XYZ = C.GetEndPoint(1) Dim IR1 As IntersectionResult = F.Project(Pt1) Dim IR2 As IntersectionResult = F.Project(Pt2) If IR1 Is Nothing OrElse IR2 Is Nothing Then Return Double.NaN If IR1.UVPoint.U < IR2.UVPoint.U Then Return IR1.UVPoint.U Else Return IR2.UVPoint.U End If End If End Function Dim Loops As IEnumerable(Of EdgeArray) Loops = From L As EdgeArray In F.EdgeLoops Select L Dim Cv As IEnumerable(Of Tuple(Of EdgeArray, Double)) Cv = From L2 As EdgeArray In Loops From L3 As Edge In L2 Let Mu As Double = MinU(L3.AsCurve) Where Mu <> Double.NaN Select New Tuple(Of EdgeArray, Double)(L2, Mu) Dim Out As Tuple(Of EdgeArray, Double) = Cv.ToList.Find(Function(Jx) Jx.Item2 = Cv.Min(Function(Jv) Jv.Item2)) If Out Is Nothing Then Return Nothing Else Return Out.Item1 End If End Function Public Shared Function PlannerFaceOuterLoop(F As Face) As EdgeArray Dim PF As PlanarFace = TryCast(F, PlanarFace) If PF Is Nothing Then Return Nothing Else Dim FN As XYZ = PF.FaceNormal Dim T As Transform = Transform.Identity T.BasisZ = FN T.BasisX = PF.XVector T.BasisY = PF.YVector T.Origin = PF.Origin 'Dim Zeds As New List(Of Double) Dim MinU = Function(C As Curve) As Double If C.IsCyclic Then Dim Min As Double = Nothing For I = 0 To 20 Dim Param As Double = I / 20 Dim CuvPt As XYZ = C.Evaluate(Param, True) Dim XYZt As XYZ = T.Inverse.OfPoint(CuvPt) If I = 0 Then Min = XYZt.X Else If XYZt.X < Min Then Min = XYZt.X End If End If Next Return Min Else Dim Pt1 As XYZ = C.GetEndPoint(0) Dim Pt2 As XYZ = C.GetEndPoint(1) Dim XYZp As XYZ() = New XYZ(1) {} XYZp(0) = T.Inverse.OfPoint(Pt1) XYZp(1) = T.Inverse.OfPoint(Pt2) ' Zeds.Add(XYZp(0).Z) ' Zeds.Add(XYZp(1).Z) If XYZp(0).X < XYZp(1).X Then Return XYZp(0).X Else Return XYZp(1).X End If End If End Function Dim Loops As IEnumerable(Of EdgeArray) Loops = From L As EdgeArray In F.EdgeLoops Select L Dim Cv As IEnumerable(Of Tuple(Of EdgeArray, Double)) Cv = From L2 As EdgeArray In Loops From L3 As Edge In L2 Let Mu As Double = MinU(L3.AsCurve) Select New Tuple(Of EdgeArray, Double)(L2, Mu) Dim Out As Tuple(Of EdgeArray, Double) = Cv.ToList.Find(Function(Jx) Jx.Item2 = Cv.Min(Function(Jv) Jv.Item2)) If Out Is Nothing Then Return Nothing Else Return Out.Item1 End If End Function
Dear Rpthomas108,
Thank you very much for sharing this nice idea and implementation!
It makes sense to me. Serious testing will be in order 🙂
I hope Philippe found it useful. How sad that he does not say.
I have a couple of suggestions:
Cheers,
Jeremy
I found your code really hard to understand, so I refactored it and rewrote some parts like this:
<Transaction(TransactionMode.ReadOnly)> Public Class AdskCommand Implements IExternalCommand Shared Function MinU1(C As Curve, F As Face) As Double If C.IsCyclic Then Dim Min As Double = Double.NaN For I = 0 To 20 Dim Param As Double = I / 20 Dim CuvPt As XYZ = C.Evaluate(Param, True) Dim IR As IntersectionResult = F.Project(CuvPt) If IR Is Nothing = False Then If Min = Double.NaN Then Min = IR.UVPoint.U Else If IR.UVPoint.U < Min Then Min = IR.UVPoint.U End If End If End If Next Return Min Else Dim Pt1 As XYZ = C.GetEndPoint(0) Dim Pt2 As XYZ = C.GetEndPoint(1) Dim IR1 As IntersectionResult = F.Project(Pt1) Dim IR2 As IntersectionResult = F.Project(Pt2) If IR1 Is Nothing OrElse IR2 Is Nothing Then Return Double.NaN If IR1.UVPoint.U < IR2.UVPoint.U Then Return IR1.UVPoint.U Else Return IR2.UVPoint.U End If End If End Function Public Shared Function OuterLoop(F As Face) As EdgeArray Dim Loops As EdgeArrayArray = F.EdgeLoops Dim Cv As List(Of Tuple(Of EdgeArray, Double)) = New List(Of Tuple(Of EdgeArray, Double))(Loops.Size) For Each L2 As EdgeArray In Loops For Each L3 As Edge In L2 Dim Mu As Double = MinU1(L3.AsCurve, F) Cv.Add(New Tuple(Of EdgeArray, Double)(L2, Mu)) Next Next Dim Out As Tuple(Of EdgeArray, Double) = Cv.Find(Function(Jx) Jx.Item2 = Cv.Min(Function(Jv) Jv.Item2)) If Out Is Nothing Then Return Nothing Else Return Out.Item1 End If End Function Shared Function MinU2(C As Curve, T As Transform) As Double If C.IsCyclic Then Dim Min As Double = Nothing For I = 0 To 20 Dim Param As Double = I / 20 Dim CuvPt As XYZ = C.Evaluate(Param, True) Dim XYZt As XYZ = T.Inverse.OfPoint(CuvPt) If I = 0 Then Min = XYZt.X Else If XYZt.X < Min Then Min = XYZt.X End If End If Next Return Min Else Dim Pt1 As XYZ = C.GetEndPoint(0) Dim Pt2 As XYZ = C.GetEndPoint(1) Dim XYZp As XYZ() = New XYZ(1) {} XYZp(0) = T.Inverse.OfPoint(Pt1) XYZp(1) = T.Inverse.OfPoint(Pt2) ' Zeds.Add(XYZp(0).Z) ' Zeds.Add(XYZp(1).Z) If XYZp(0).X < XYZp(1).X Then Return XYZp(0).X Else Return XYZp(1).X End If End If End Function Public Shared Function PlanarFaceOuterLoop(F As Face) As EdgeArray Dim PF As PlanarFace = TryCast(F, PlanarFace) If PF Is Nothing Then Return Nothing Else Dim FN As XYZ = PF.FaceNormal Dim T As Transform = Transform.Identity T.BasisZ = FN T.BasisX = PF.XVector T.BasisY = PF.YVector T.Origin = PF.Origin 'Dim Zeds As New List(Of Double) Dim Loops As EdgeArrayArray = F.EdgeLoops Dim Cv As List(Of Tuple(Of EdgeArray, Double)) = New List(Of Tuple(Of EdgeArray, Double))(Loops.Size) For Each L2 As EdgeArray In Loops For Each L3 As Edge In L2 Dim Mu As Double = MinU2(L3.AsCurve, T) Cv.Add(New Tuple(Of EdgeArray, Double)(L2, Mu)) Next Next Dim Out As Tuple(Of EdgeArray, Double) = Cv.ToList.Find(Function(Jx) Jx.Item2 = Cv.Min(Function(Jv) Jv.Item2)) If Out Is Nothing Then Return Nothing Else Return Out.Item1 End If End Function Public Function Execute( ByVal commandData As ExternalCommandData, ByRef message As String, ByVal elements As ElementSet) _ As Result Implements IExternalCommand.Execute Dim uiapp As UIApplication = commandData.Application Dim uidoc As UIDocument = uiapp.ActiveUIDocument Dim app As Application = uiapp.Application Dim doc As Document = uidoc.Document Dim sel As Selection = uidoc.Selection Dim R As Reference = Nothing Try R = sel.PickObject(ObjectType.Face) Catch ex As Exception End Try If R Is Nothing Then Return Result.Cancelled Else Dim F_El As Element = doc.GetElement(R.ElementId) If F_El Is Nothing Then Return Result.Failed Else Dim F As Face = TryCast(F_El.GetGeometryObjectFromReference(R), Face) If F Is Nothing Then Return Result.Failed Else Dim EA1 As EdgeArray = PlanarFaceOuterLoop(F) Dim EA2 As EdgeArray = OuterLoop(F) Return Result.Succeeded End Function End Class
Here are your methods simplified and rewritten in C#:
#region Rpthomas108 searches for minimum point // In Revit API discussion forum thread // https://forums.autodesk.com/t5/revit-api-forum/is-the-first-edgeloop-still-the-outer-loop/m-p/7225379 public static double MinU( Curve C, Face F ) { return C.Tessellate() .Select<XYZ, IntersectionResult>( p => F.Project( p ) ) .Min<IntersectionResult>( ir => ir.UVPoint.U ); } public static double MinX( Curve C, Transform Tinv ) { return C.Tessellate() .Select<XYZ, XYZ>( p => Tinv.OfPoint( p ) ) .Min<XYZ>( p => p.X ); } public static EdgeArray OuterLoop( Face F ) { EdgeArray eaMin = null; EdgeArrayArray loops = F.EdgeLoops; double uMin = double.MaxValue; foreach( EdgeArray a in loops ) { double uMin2 = double.MaxValue; foreach( Edge e in a ) { double min = MinU( e.AsCurve(), F ); if( min < uMin2 ) { uMin2 = min; } } if( uMin2 < uMin ) { uMin = uMin2; eaMin = a; } } return eaMin; } public static EdgeArray PlanarFaceOuterLoop( Face F ) { PlanarFace face = F as PlanarFace; if( face == null ) { return null; } Transform T = Transform.Identity; T.BasisZ = face.FaceNormal; T.BasisX = face.XVector; T.BasisY = face.YVector; T.Origin = face.Origin; Transform Tinv = T.Inverse; EdgeArray eaMin = null; EdgeArrayArray loops = F.EdgeLoops; double uMin = double.MaxValue; foreach( EdgeArray a in loops ) { double uMin2 = double.MaxValue; foreach( Edge e in a ) { double min = MinX( e.AsCurve(), Tinv ); if( min < uMin2 ) { uMin2 = min; } } if( uMin2 < uMin ) { uMin = uMin2; eaMin = a; } } return eaMin; } #endregion // Rpthomas108 searches for minimum point
Note how short and sweet MinU and MinX became?
I added them to The Building Coder samples release:
https://github.com/jeremytammik/the_building_coder_samples/releases/tag/2018.0.134.3
Cheers,
Jeremy
Hello Jeremy
Thanks for the reply, sorry for the delay in response.
In truth I was detecting straight line curves by looking at the cyclic property as it was convenient for the example. A more generalised solution should take into account the various types of curve (perhaps with casting attempts or gettype comparisons). The important aspect is that the straight curves give certainty that one of two points is the minimum whilst other types of curve in theory have to be evaluated/tessellated along them otherwise the min may be missed. So the first step for performance is to establish if you need to evaluate the curve at all.
I didn't consider the Tessellate function but that does seem the better option. I picked 20 because it seemed a good resolution for the example cases. Probably I'm being a bit pessimistic about how long it takes a modern computer to iterate these things whilst ignoring possible errors in long winding curves.
Yes planar. The other type of non-planar function was looking at the face uv co-ordinates in a similar arbitrary way.
I'm glad you see the basis of the approach as useful. They tell me lambda expressions are great but I don't know where to draw the line sometimes.
Regards
Richard
Dear Richard,
Yes, I like your approach a lot.
Kiss! -- https://en.wikipedia.org/wiki/KISS_principle
Yes, Tessellate is very handy indeed.
I love lambda expressions too. I am just not fluent in VB, and never saw them there before 🙂
Cheers,
Jeremy
Hi I would like to toss this in for discussion,
It should be able to seperate all curve loops of a faceinto interior / exterior curves...
It needs some adaptation for non planar faces...
The idea is quite simple:
The face has an area which can be computed, The area of each curve can be computed (https://en.wikipedia.org/wiki/Shoelace_formula)...
So the only issues is to find a combination of + / - so that the resulting sum equals the area of the initial face...
This will work just fine if the face has a limited number since it uses a branch and bound approch for itterating through the possibilities...
public static CurveLoop[] GetOuterBoundaries(Autodesk.Revit.DB.Face source) { List<CurveLoop> allBounds = new List<CurveLoop>(); int totalArea = (int)(source.Area * 100); allBounds.AddRange(GeometryHelper.GetBoundaries(source)); List<int> boundAreas = new List<int>(); for (int i = 0; i < allBounds.Count; i++) { boundAreas.Add( (int)(GeometryHelper.Area(allBounds[i], out XYZ currentCenter) * 100) ); } if (boundAreas.Sum() > totalArea) { List<int[]> results = SumUpRecursivly(boundAreas, totalArea, new List<int>()); for (int i = 0; i < results.Count; i++) { if (results[i].Length == 0) continue; int[] r = results[i]; for (int ii = 0; ii < r.Length; ii++) { int index = boundAreas.IndexOf(r[ii]); boundAreas.RemoveAt(index); allBounds.RemoveAt(index); } return allBounds.ToArray(); } } return allBounds.ToArray(); } /// adaptiation of: https://stackoverflow.com/questions/4632322/finding-all-possible-combinations-of-numbers-to-reach-a-... private static List<int[]> SumUpRecursivly(List<int> numbers, int target, List<int> partial) { int s = 0; foreach (int x in numbers) s += x; foreach (int x in partial) s -= x; int diff = Math.Abs(s - target); if (Math.Abs(s - target) < 10 ) { return new List<int[]> { partial.ToArray() }; } if (numbers.Count == 0 || s < target) { return new List<int[]> { new int[0] }; } List<int[]> results = new List<int[]>(); for (int i = 0; i < numbers.Count; i++) { List<int> remaining = new List<int>(); int n = numbers[i]; for (int j = 0; j < numbers.Count; j++) { if (j != i) remaining.Add(numbers[j]); } List<int> partial_rec = new List<int>(partial) { n }; results.AddRange(SumUpRecursivly(remaining, target, partial_rec)); } return results; }
Thank you for the nice idea.
However, I am not immediately convinced...
Imagine a situation with many rooms, many of which have holes.
Imagine that many of the holes are the exact same size as many of the other rooms.
How do you tell them apart, if all you consider is their area?
Okay, since I am still exploring revit Api, I just found this:
https://thebuildingcoder.typepad.com/blog/2015/01/exporterifcutils-curve-loop-sort-and-validate.html
That should do the trick , shouldn't it.
However I can't find the functions in outocomplete, which assemblies do I need?
Search the Revit API DLLs for something named IFC. I think the namespace is
using Autodesk.Revit.DB.IFC;
Yes, got them. And it does the job quite nicly, for reference once again:
https://thebuildingcoder.typepad.com/blog/2015/01/wall-elevation-profiles-in-the-building-coder-samp...
On friday I was a bit made that this functionality is hidden away in a class that is called
Autodesk.Revit.DB.IFC.ExporterIFCUtils.SortCurveLoops
I mean it is obvious that you have to look into the ExporterIFCUtils namespace in order to make a distinction between inner and outer boundary loops....
But today I feel like a little kid looking for easter eggs, I wounder what more is hidden within the Revit API.
Maybe there is a line saying:
"This apt has super easter bunny powers"
Dear Richard,
Congratulations on finding this egg, and best of luck discovering all the others!
Yes, another developer who is a big fan of the hidden wonders of the Rev it API utility classes is Rudolf Honke:
https://thebuildingcoder.typepad.com/blog/2013/04/handy-utility-classes.html
Cheers,
Jeremy
I tried using SortCurveLoops on the the face shown below, expecting to get one list with the outer loop and one list with the 3 inner loops, but I got one list containing one list containing two loops.
This article also mentions co-planar loops in the description of ValidateCurveLoops. Perhaps the problem I had is caused by the fact that also SortCurveLoops only works with co-planar loops?
Here is the code I use to find the outer loop and all the inner loops. It finds the outer loop by cycling through all the tessellated points of all the loop edges and finding the one with the lowest U.
I did a few tests and it seems to work well. I am surprised to see that my short function does the same job as other long functions shown in the previous posts. I am learning LINQ, so I spent some time to get this to work with LINQ, but my previous version using 3 nested foreach was very simple to do.
Am I doing something wrong?
Am I doing something different from what this post describes?
public Face Face;
private CurveLoop _outerLoop;
private List<CurveLoop> _innerLoops;
private void GetInnerAndOuterLoops()
{
var allLoops = Face.GetEdgesAsCurveLoops();
_outerLoop = allLoops
.SelectMany(loop => loop
.SelectMany(curve => curve.Tessellate()
.Select(point => (Face.Project(point).UVPoint.V, Loop: loop))))
.Aggregate((lowestLoop, nextLoop) => lowestLoop.V < nextLoop.V ? lowestLoop : nextLoop).Loop;
_innerLoops = allLoops.Where(loop => loop != _outerLoop).ToList();
}
If it works it works.
One thing I considered at the time was resolution of the points you get from tessellate i.e. if you have a curve does one of the points on that curve (from tessellate) describe the actual minimum location of that curve (for some curves that is unlikely to be the case). Since there is the parametric curve and the actual points are obtained from that. Below is an exaggerated example of what I mean by that. Depends also on rotation of UV axis on face in comparison to those points.
For the most part I don't think such a thing would cause issues unless you set out to prove it didn't work i.e. in a real world scenario there is no arrangement you would likely have that would be affected by such things. So it's a question of comfort level through testing really.
I think it has also since been noted that there are patterns in how the faces are constructed that gives away the actual outer loop of a face.
Was also at the time dealing with PlanarFaces only so also should note that for those you can use Face.IsInside with solid creation utils, this makes things far more straightforward than my original above code and perhaps more reliable i.e. extrude each loop and check points from each within faces of one another to find other loop.
The documentation of Curve.Tessellate says both the tolerance is slightly larger than 1/16” and is defined internally by Revit to be adequate for display purposes.
Tessellation in computer graphics is often adjusted to the zoom level or to the desired rendering quality. In other words, if "you can see" that one curve is below the other curve, then the tessellation can see it too. But I don't know if Curve.Tessellation is the same tessellation used for the graphics card. We could also talk about the definition of "you can see", but let's not add speculation to the speculation 🙂
You say you got one list with one list with two loops...
If you are unsure which loop is which, or, in any case, I would highly recommend implementing some little debugging utility functions to display those loops graphically, or it will be very hard to understand what you are getting.
I implemented such stuff in several blog posts using the Creator class:
Thank you for the list of articles, it will be very helpful in the near future!
This article seems to mention the same problem that I found in SortCurveLoops.
I did another test with SortCurveLoops, and indeed it seems to be working reliably only with planar faces. With curved faces it usually does nothing. Only in one curved face I was able to get 2 out of the 4 loops, but it usually gets none.
In this snapshot you can see two masses. The first one has only planar faces, the second one is a copy of the first one with a void that creates a curved face.
The texts show the first line of each loop with the loop indexes as returned by SortCurveLoops. I like to create texts at 1/3 of each line, so it visually gives an idea of the direction of the loop. Just looking at the texts you immediately understand which loops are clockwise and which ones are counterclockwise.
I have the feeling that SortCurveLoops projects the curves to a plane, then crunches the numbers on the projected curves. If this is the case, then it will never be reliable on curved faces. The correct approach would be to work on the UV coordinates.
Here is the code I used:
var loops = face.GetEdgesAsCurveLoops();
var sortedLoops = ExporterIFCUtils.SortCurveLoops(loops);
for (var i = 0; i < sortedLoops.Count; i++)
{
for (var j = 0; j < sortedLoops[i].Count; j++)
{
CreateTextNote($"[{i}][{j}]", sortedLoops[i][j].First().Evaluate(0.33, true), doc);
}
}
TextNote CreateTextNote(string text, XYZ origin, Document doc)
{
var options = new TextNoteOptions
{
HorizontalAlignment = HorizontalTextAlignment.Center,
VerticalAlignment = VerticalTextAlignment.Middle,
TypeId = doc.GetDefaultElementTypeId(ElementTypeGroup.TextNoteType)
};
return TextNote.Create(doc, doc.ActiveView.Id, origin, text, options);
}