Getting the top Face of an Element

Getting the top Face of an Element

ChristianMolino
Enthusiast Enthusiast
426 Views
6 Replies
Message 1 of 7

Getting the top Face of an Element

ChristianMolino
Enthusiast
Enthusiast

Hi all,

 

I am working on some code that will get the top face of a family I have loaded into the model to use as a host for NewFamilyInstance. I am trying to use both the Reference and Face overloads for it but just cannot seem to get it to work the way it should.

 

If I use the basic most simple way of getting the face, I am pulling it from the instance geometry, but then it hosts to the family symbol of the host family rather than to the family instance of the host family

        private Face GetTopFace(Element element)
        {
            var options = new Options()
            {
                ComputeReferences = true,
                IncludeNonVisibleObjects = true,
                DetailLevel = ViewDetailLevel.Fine
            };

            var topFace = element.get_Geometry(options)
                .OfType<GeometryInstance>()
                .SelectMany(g => g.GetInstanceGeometry().OfType<Solid>())
                .Where(s => s.Volume > 0)
                .OrderByDescending(s => s.Volume)
                .FirstOrDefault()
                ?.Faces
                .OfType<PlanarFace>()
                .OrderByDescending(f => f.Area)
                .FirstOrDefault();

            if (topFace != null)
            {
                TaskDialog.Show("Debug", "Found topFace with Id: " + topFace.Id);
                return topFace;
            }
            System.Diagnostics.Debug.WriteLine("Error: No suitable top face found using GetTopFace.");
            return null;
        }

 

If I use a ReferenceIntersector to get the face reference and pass that through, it inserts the family the correct way hosted to the family instance face.

private Reference GetFaceReference(Element element)
{
    View3D view3D = GetOrCreateView3D();
    if (view3D == null)
    {
        System.Diagnostics.Debug.WriteLine("Error: Could not obtain a 3D view for ReferenceIntersector.");
        return null;
    }

    // Use ReferenceIntersector with a list of ElementIds
    List<ElementId> targetElementIds = new List<ElementId> { element.Id };
    ReferenceIntersector intersector = new ReferenceIntersector(targetElementIds, FindReferenceTarget.Face, view3D);

    BoundingBoxXYZ bbox = element.get_BoundingBox(null);
    if (bbox == null)
    {
        System.Diagnostics.Debug.WriteLine("Error: Host element lacks a bounding box.");
        return null;
    }

    XYZ startPoint = new XYZ((bbox.Min.X + bbox.Max.X) / 2.0, (bbox.Min.Y + bbox.Max.Y) / 2.0, bbox.Max.Z + 100);
    XYZ direction = -XYZ.BasisZ;
    IList<ReferenceWithContext> refs = intersector.Find(startPoint, direction);

    Reference topFaceRef = null;
    double maxZ = double.MinValue;

    // Fallback: Track the face with the highest Z-coordinate regardless of normal
    Reference highestFaceRef = null;
    double highestZ = double.MinValue;

    foreach (ReferenceWithContext rwc in refs)
    {
        Reference r = rwc.GetReference();
        GeometryObject geomObj = element.GetGeometryObjectFromReference(r);
        XYZ centroid = r.GlobalPoint;
        bool isTopFace = false;
        XYZ normal = null;

        if (geomObj is PlanarFace pf)
        {
            normal = pf.FaceNormal;
            isTopFace = normal.Z > 0.4; // Lowered threshold to account for slight deviations
            System.Diagnostics.Debug.WriteLine($"ReferenceIntersector Face: Type = PlanarFace, Normal = ({normal.X}, {normal.Y}, {normal.Z}), Global Centroid Z = {centroid.Z}");
        }
        else if (geomObj is CylindricalFace cf)
        {
            // For a CylindricalFace, compute the normal at the center
            normal = cf.ComputeNormal(new UV(0.5, 0.5));
            isTopFace = normal.Z > 0.4; // Lowered threshold to account for slight deviations
            System.Diagnostics.Debug.WriteLine($"ReferenceIntersector Face: Type = CylindricalFace, Normal at center = ({normal.X}, {normal.Y}, {normal.Z}), Global Centroid Z = {centroid.Z}");
        }
        else
        {
            System.Diagnostics.Debug.WriteLine($"ReferenceIntersector Face: Type = {geomObj?.GetType().Name ?? "Unknown"}, Global Centroid Z = {centroid.Z} (skipped, unsupported type)");
            continue;
        }

        // Track the face with the highest Z-coordinate as a fallback
        if (centroid.Z > highestZ)
        {
            highestZ = centroid.Z;
            highestFaceRef = r;
        }

        if (isTopFace && centroid.Z > maxZ)
        {
            maxZ = centroid.Z;
            topFaceRef = r;
        }
    }

    // Fallback: If no face meets the normal criteria, use the face with the highest Z-coordinate
    if (topFaceRef == null && highestFaceRef != null)
    {
        topFaceRef = highestFaceRef;
        maxZ = highestZ;
        System.Diagnostics.Debug.WriteLine("Fallback: No face met normal criteria; selected face with highest Z-coordinate.");
    }

    if (topFaceRef != null)
    {
        GeometryObject topFaceObj = element.GetGeometryObjectFromReference(topFaceRef);
        if (topFaceObj is PlanarFace topPlanarFace)
        {
            System.Diagnostics.Debug.WriteLine($"Selected Top Face: Type = PlanarFace, Normal = ({topPlanarFace.FaceNormal.X}, {topPlanarFace.FaceNormal.Y}, {topPlanarFace.FaceNormal.Z}), Global Centroid Z = {maxZ}");
        }
        else if (topFaceObj is CylindricalFace topCylindricalFace)
        {
            XYZ normal = topCylindricalFace.ComputeNormal(new UV(0.5, 0.5));
            System.Diagnostics.Debug.WriteLine($"Selected Top Face: Type = CylindricalFace, Normal at center = ({normal.X}, {normal.Y}, {normal.Z}), Global Centroid Z = {maxZ}");
        }

        // Retrieve the face geometry object from the reference
        GeometryObject topFaceObj1 = doc.GetElement(topFaceRef.ElementId).GetGeometryObjectFromReference(topFaceRef);

        return topFaceRef;
    }

    System.Diagnostics.Debug.WriteLine("Error: No suitable top face found using ReferenceIntersector.");
    return null;
}
FamilyInstance instance = doc.Create.NewFamilyInstance(topFaceRef, point, lengthDirection, symbol);
FamilyInstance instance1 = doc.Create.NewFamilyInstance(topFace, point, lengthDirection, symbol);

I would much rather use the simpler code to easily get the face for the instance, but just cannot figure out why it wants to host to the family symbol rather than the family instance. Any ideas on what I would need to tweak here to get it to host correctly?

 

ChristianMolino_0-1746133445757.png

 

0 Likes
Accepted solutions (2)
427 Views
6 Replies
Replies (6)
Message 2 of 7

TripleM-Dev.net
Advisor
Advisor

Can't you just use the normal of a planarface? (https://revapidocs.com/2020.htm?id=5ddd3db6-5b9a-afda-d96e-6a607c3bcc87.htm )

 

If it points up then it's a upwards pointing face, then get the Z-value of the origin, the Z with the highest value is the topmost face.

Can happen that multiple planarfaces will meet this criteria depending the shape of the family.

 

Something similar can be done with a Face (that isn't a planarface) but would need a more defined situation on when it should be considered "topmost face"

 

- Michel

0 Likes
Message 3 of 7

ChristianMolino
Enthusiast
Enthusiast

Yeah I have tried using top most face using z-coordinates as well as using the normal vector to determine the top face. Both of those solutions work as well and return the same face currently being returned, however the issue arises when I actually pass that face to the NewFamilyInstance(). It just keeps referencing/hosting to a face from the family symbol (perfectly square at the global origin) rather than the family instance. 

 

If I do a debug to insert them both at the same time they both return the same face ID so I know its technically getting the right face both times. I found the Face ID is the same for the host families "GetInstanceGeometry" and "GetSymbolGeometry" if I snoop into it so I think its getting one from the Symbol Geometry, but it doesn't make sense since the code is specifically calling out the "GetInstanceGeometry"

.SelectMany(g => g.GetInstanceGeometry().OfType<Solid>())

 

ChristianMolino_0-1746189066746.png

0 Likes
Message 4 of 7

TripleM-Dev.net
Advisor
Advisor
Accepted solution

Ok, have you looked into building the string for the stablereference manually? (see https://thebuildingcoder.typepad.com/blog/2016/04/stable-reference-string-magic-voodoo.html )

 

Place the family on the host in the UI, then use Revit Lookup to see what the reference is for the placed family (the HostFace parameter)

It will look something like this (depends on type of family, has to be hostable on a face)

 

4ffdfe0c-4598-4a49-848d-bd076295171a-000555d1:0:INSTANCE:4ffdfe0c-4598-4a49-848d-bd076295171a-000555b5:22:SURFACE

 

Parts of the string + meaning:

4ffdfe0c-4598-4a49-848d-bd076295171a-000555d1 = Unique Id of the host instance

0 = we assume this is a future thing, is always 0 (if I remember correctly)

INSTANCE = also fixed, it;s a instance reference

4ffdfe0c-4598-4a49-848d-bd076295171a-000555b5 = Unique Id of the Host SYMBOL!

22 = host face id, which you already found. (Id number of face)

SURFACE = fixed, as you are referencing a Face.

 

So with these variables the string can be built and then use: ParseFromStableRepresentation  to create the Reference and use this in the "NewFamilyInstance"

and see if it helps.

This helped me solve dimensioning almost all elements in Revit correctly about a decade ago.

If you need help with the stablereference, search this forum there's lots of post about them, but most can be resolved with the build of the string yourself.

 

Note: assumed the host face is located in the same document.!

 

0 Likes
Message 5 of 7

ChristianMolino
Enthusiast
Enthusiast

Wow you are an absolute legend! That was the missing piece to my puzzle! It's hosting exactly as it should now. Thank you for solving my headache for the last 2 weeks!

To archive the solution (Note you will likely want to change the conditions for finding the top face to use the normal as stated above, the logic here just works for the specific family I am using):

        /// <summary>
        /// Automatically selects the top face of the given element using geometry options.
        /// </summary>
        /// <param name="element">The host e;ement to find the face on.</param>
        /// <returns></returns>
        private Reference GetTopFaceReference(Element element)
        {
            var options = new Options()
            {
                ComputeReferences = true,
                IncludeNonVisibleObjects = true,
                DetailLevel = ViewDetailLevel.Fine
            };

            var topFace = element.get_Geometry(options)
                .OfType<GeometryInstance>()
                .SelectMany(g => g.GetInstanceGeometry().OfType<Solid>())
                .Where(s => s.Volume > 0)
                .OrderByDescending(s => s.Volume)
                .FirstOrDefault()
                ?.Faces
                .OfType<PlanarFace>()
                .OrderByDescending(f => f.Area)
                .FirstOrDefault();

            if (topFace != null)
            {
                // Get the host instance's UniqueId
                string hostInstanceUniqueId = element.UniqueId;

                // Get the symbol's UniqueId
                FamilyInstance familyInstance = element as FamilyInstance;
                if (familyInstance == null)
                {
                    TaskDialog.Show("Error", "Element is not a FamilyInstance.");
                    return null;
                }
                string symbolUniqueId = familyInstance.Symbol.UniqueId;

                // Get the stable representation of the face's reference
                // We need to find the face's index in the solid's face collection
                Reference faceReference = topFace.Reference;

                if (faceReference == null)
                {
                    TaskDialog.Show("Error", "No reference found for the top face.");
                    return null;
                }

                string faceId = topFace.Id.ToString();

                // Build the stable reference string
                string stableRefString = $"{hostInstanceUniqueId}:0:INSTANCE:{symbolUniqueId}:{faceId}:SURFACE";

                // Parse the stable reference string back to a Reference
                Reference stableReference = Reference.ParseFromStableRepresentation(doc, stableRefString);

                TaskDialog.Show("Debug", $"Stable Reference: {stableRefString}");

                return stableReference;
            }
            System.Diagnostics.Debug.WriteLine("Error: No suitable top face found using GetTopFace.");
            return null;
        }

 

0 Likes
Message 6 of 7

rhanzlick
Advocate
Advocate
Accepted solution

Happy you got this to work, but explicitly constructing the stableRep string definitely does NOT feel like the best approach to me... You mentioned that you compared the "GetInstanceGeom" vs "GetSymbolGeom". I am replying specifically to address this - you've stumbled upon a very tricky "gotchya" (one of many) in Revit's API. It had me stumped for a while in a different application. Reading the docs (specifically the last sentence in the REMARKS) in [0], and the explanations in [1] and [2] should help your understanding.


To summarize, (to the best of my understanding) the GetInstanceGeometry actually calls GetSymbolGeometry, makes a copy (which is now independent of the original object), and returns the results with the FamilyInstance.Transform applied. Therefore, the object ACTUALLY referenced by the resulting GeometryObjects is the FamilySymbol - and subsequently are not meaningful for creating new objects.

However, for your application, you SHOULD use "GetSymbolGeometry" because it is a reference to the "real GeometryObjects", and not independent/abstracted copies. Switching to this should work as you intend (and prevent you from having to explicitly construct stableReps - lol):

- You can verify this by:
[A]: setting ComputeReferences=true in your Options (as you already have)
[B]: querying the results from both "GetInstanceGeom" and "GetSymbolGeom", converting them to Solids, and concatenating all of the faces (as you already have)
[C]: from the results of faces in both collections, select face.Reference.CreateStableRepresentation(element.Document)


The (2) resulting List<string> created in [C] will show you that from GetInstanceGeometry, the referenced UniqueId actually belongs to the FamilySymbol, while the results from GetSymbolGeometry include both the UniqueId from FamilySymbol, but also an extra component that shows the UniqueId from the FamilyInstance (along with the word INSTANCE and some other info).


Overall, the intent of the API is clever - a sort of lazy-loading in the vein of the DRY principle... only the naming of the methods is IMHO backwards and therefore unintuitive.

Try out simply using the face obtained by GetSymbolGeometry and let us know how it works!

good luck,
Ryan


[0]: https://www.revitapidocs.com/2024/22d4a5d4-dfc2-7227-2cae-b989729696ec.htm

[1]: https://help.autodesk.com/view/RVT/2024/ENU/?guid=Revit_API_Revit_API_Developers_Guide_Revit_Geometr...

[2]: https://help.autodesk.com/view/RVT/2024/ENU/?guid=Revit_API_Revit_API_Developers_Guide_Revit_Geometr...

0 Likes
Message 7 of 7

ChristianMolino
Enthusiast
Enthusiast

Thanks Ryan, you are spot on with this. I agree this naming convention is totally backwards from what you would expect so I didn't even think of trying that initially. That simplified the method quite a bit! Even so, I think the info provided by @TripleM-Dev.net will prove valuable to some other use cases in this project I am working on as it appears I can use that to access the special references 0-9.

 

        private (Face topFace, Reference topFaceReference) GetTopFace(Element element)
        {
            var options = new Options()
            {
                ComputeReferences = true,
                IncludeNonVisibleObjects = true,
                DetailLevel = ViewDetailLevel.Fine
            };

            var topFace = element.get_Geometry(options)
                .OfType<GeometryInstance>()
                .SelectMany(g => g.GetSymbolGeometry().OfType<Solid>())
                .Where(s => s.Volume > 0)
                .OrderByDescending(s => s.Volume)
                .FirstOrDefault()
                ?.Faces
                .OfType<PlanarFace>()
                .OrderByDescending(f => f.Area)
                .FirstOrDefault();

            if (topFace != null)
            {
                Reference topFaceReference = topFace.Reference;

                return (topFace, topFaceReference);
            }
            System.Diagnostics.Debug.WriteLine("Error: No suitable top face found using GetTopFace.");
            return (null, null);
        }
0 Likes