Inserting a Family Instance Hosted to a Top Face Keeps going to Z=0

Inserting a Family Instance Hosted to a Top Face Keeps going to Z=0

ChristianMolino
Enthusiast Enthusiast
383 Views
4 Replies
Message 1 of 5

Inserting a Family Instance Hosted to a Top Face Keeps going to Z=0

ChristianMolino
Enthusiast
Enthusiast

UPDATE: 4/28/2025 - After some further debugging it appears that if I change the face selection to manual and use the face reference overload it works, which makes it seem that it is something in the automatic face detection that is not working. Anyone have any ideas what is wrong in that block of code?

Hello all,

 

I am trying to insert a face based family onto the top face of an element using NewFamilyInstance Method (Face, XYZ, XYZ, FamilySymbol). From what I can tell, it is getting the correct face and inserting the family, however it is inserting it at a z=0 elevation rather than hosting to directly to to the face of the host. I need it to work exactly like manually hosting to the face so that once I move, rotate, or twist the host, the hosted family will follow with it.

 

I am trying to follow @jeremy_tammik 's post "Insert Face Hosted Sprinkler" but it still continues to do the same thing. I had to make a few modifications to his GetPlanarFace method, as it was failing to get any solid objects from the geo(first "if(null != solid)" statement), but the instance ("else if" statement) seems to be finding the correct plane (seems to have the correct normal and elevation from the debug output).

ChristianMolino_0-1745516247168.png

The constraints are also quite different between the manually placed versus the API placed family instance.

ChristianMolino_1-1745516325564.png

 

ChristianMolino_2-1745516371296.png

 

If I do a snoop selection on each of both the manually placed and the API placed instance, you can see the "Host" element has a completely different ID as well as when you click on the host it shows one as a FamilySymbol and the other as a FamilyInstance

ChristianMolino_3-1745516656440.png

 

ChristianMolino_4-1745516747745.png

 

Does anyone know what I could be doing wrong here that is causing this unexpected behavior? Apologies in advance if this is an easy fix, I am very new to all of this. Thank you all in advance!

 

 

    public Result InsertConnections(FINCast.UI.InsertPrecastElementsDialog.InsertResult insertResult)
    {
        try
        {
            Element hostElement = insertResult.HostElement;
            string selectedType = insertResult.SelectedElementType;
            string selectedRule = insertResult.SelectedRule;

            // Load XML configuration
            ACRule rule = LoadRuleConfig(selectedType, selectedRule);

            // Get the connection family symbol
            FamilySymbol symbol = GetFamilySymbol(rule);

            PlanarFace topFace = GetPlanarFace(hostElement);
            XYZ p = PointOnFace(topFace);

            // Place instances
            using (Transaction tx = new Transaction(doc, "Insert Connections"))
            {
                tx.Start();

                FamilyInstance fi = doc.Create.NewFamilyInstance(topFace, p, XYZ.BasisX, symbol);

                tx.Commit();
            }

            TaskDialog.Show("Success", $"Inserted {placementPoints.Count} Vector Connectors into Double Tee ID: {hostElement.Id.IntegerValue}.");
            return Result.Succeeded;
        }
        catch (Exception ex)
        {
            TaskDialog.Show("Error", $"Failed to insert connections: {ex.Message}");
            return Result.Failed;
        }
    }

    XYZ PointOnFace(PlanarFace face)
    {
        XYZ p = new XYZ(0, 0, 0);
        Mesh mesh = face.Triangulate();

        for (int i = 0; i < mesh.NumTriangles; ++i)
        {
            MeshTriangle triangle = mesh.get_Triangle(i);
            p += triangle.get_Vertex(0);
            p += triangle.get_Vertex(1);
            p += triangle.get_Vertex(2);
            p *= 0.3333333333333333;
            break;
        }
        return p;
    }

    private PlanarFace GetPlanarFace(Element element)
    {
        Options opt = new Options();
        opt.ComputeReferences = true;
        opt.DetailLevel = ViewDetailLevel.Fine; // Align with previous settings for consistency
        GeometryElement geo = element.get_Geometry(opt);

        PlanarFace topFace = null;

        MessageBox.Show("Number of geometry objects: " + geo.Cast<GeometryObject>().Count());

        foreach (GeometryObject obj in geo)
        {
            Solid solid = obj as Solid;

            if (null != solid)
            {
                double volume = solid.Volume; // Volume in cubic feet
                MessageBox.Show($"Solid found with volume: {volume} ft³");

                // Skip solids with zero volume
                if (volume <= 0.0)
                {
                    MessageBox.Show("Skipping solid with zero volume.");
                    continue;
                }

                foreach (Face face in solid.Faces)
                {
                    PlanarFace pf = face as PlanarFace;

                    if (null != pf)
                    {
                        XYZ normal = pf.FaceNormal;
                        XYZ centroid = pf.Evaluate(new UV(0.5, 0.5));
                        MessageBox.Show($"Planar face found: Normal = ({normal.X}, {normal.Y}, {normal.Z}), Centroid Z = {centroid.Z}");

                        if (normal.Z > 0.5)
                        {
                            topFace = pf;
                            break;
                        }
                    }
                }
            }
            else if (obj is GeometryInstance instance)
            {
                GeometryElement instanceGeo = instance.GetInstanceGeometry();
                if (instanceGeo != null)
                {
                    foreach (GeometryObject nestedObj in instanceGeo)
                    {
                        Solid nestedSolid = nestedObj as Solid;

                        if (null != nestedSolid)
                        {
                            double volume = nestedSolid.Volume;
                            MessageBox.Show($"Nested solid found with volume: {volume} ft³");

                            if (volume <= 0.0)
                            {
                                MessageBox.Show("Skipping nested solid with zero volume.");
                                continue;
                            }

                            foreach (Face nestedFace in nestedSolid.Faces)
                            {
                                PlanarFace pf = nestedFace as PlanarFace;

                                if (null != pf)
                                {
                                    XYZ normal = pf.FaceNormal;
                                    XYZ centroid = pf.Evaluate(new UV(0.5, 0.5));
                                    MessageBox.Show($"Nested planar face found: Normal = ({normal.X}, {normal.Y}, {normal.Z}), Centroid Z = {centroid.Z}");

                                    if (normal.Z > 0.5)
                                    {
                                        topFace = pf;
                                        break;
                                    }
                                }
                            }
                        }
                        else
                            MessageBox.Show("No solid found in nested geometry object.");
                    }
                }
                else
                    MessageBox.Show("No nested geometry found in GeometryInstance.");
            }
            else
                MessageBox.Show("No solid found in geometry object.");

            if (topFace != null)
                break;
        }

        if (null != topFace)
        {
            MessageBox.Show($"Selected top face: Normal = ({topFace.FaceNormal.X}, {topFace.FaceNormal.Y}, {topFace.FaceNormal.Z})");
            return topFace;
        }
        else
            throw new InvalidOperationException("No suitable planar face found.");
    }

 

0 Likes
Accepted solutions (1)
384 Views
4 Replies
Replies (4)
Message 2 of 5

jeremy_tammik
Alumni
Alumni

My memory is a sieve; so, I rely on the AIs to scour the entire Internet for me and pick up everything the community ever wrote. Here is what Gemini has to say in answer to your question, and it sounds pretty sensible to me; I obviously have not tested, so please let us know whether it solves the issue, helps you get on the right track, or is complete hallucinatory nonsense -- thank you:

   

Ah, Christian, welcome to the fascinating world of face-based family instance placement via the Revit API! It's a common stumbling block, and your detailed description is excellent. You're on the right track by looking at Jeremy's post, but let's clarify a few key aspects that might be causing your family instances to stubbornly stick to Z=0.

The core issue lies in how the NewFamilyInstance method interprets the input when you're aiming for face-based hosting. The overload you're using:

C#
 
FamilyInstance fi = doc.Create.NewFamilyInstance(Face, XYZ, XYZ, FamilySymbol);

is indeed for face-based families. However, the second XYZ argument isn't just a placement point in global coordinates; it's the reference point on the face where the family instance will be initially placed. Revit then takes care of orienting and attaching the family to that face.

The problem you're encountering, where the instance goes to Z=0, strongly suggests that the XYZ point you're providing (p from your PointOnFace method) isn't correctly aligned with the local coordinate system of the face. While your GetPlanarFace method seems to be identifying the correct face (based on your debug output), the PointOnFace method, which calculates an average point of the first triangle's vertices, might not be the ideal way to define the insertion point on the face for hosting purposes.

Here's a breakdown of why this is happening and how to fix it:

Understanding Face-Based Hosting and the API:

When you manually place a face-based family, you're essentially clicking on a face. Revit internally determines the local coordinate system of that face at the clicked point and aligns the family's host face to it. The API needs similar information.

The NewFamilyInstance(Face, XYZ, XYZ, FamilySymbol) method expects:

  1. Face: The planar face you want to host onto.
  2. XYZ (Placement Point): A point on the selected Face in world coordinates. Revit will use this point as a reference to orient and attach the family.
  3. XYZ (Reference Direction): A vector that defines the initial orientation of the family instance. This is often the face normal or a vector aligned with the desired rotation.
  4. FamilySymbol: The family symbol to be placed.

The Issue with PointOnFace:

Your PointOnFace method calculates a centroid of the first triangle of the face's triangulation. While this point lies on the plane of the face, it doesn't guarantee that it aligns correctly with how Revit wants to establish the local coordinate system for hosting. This can lead to the instance being placed at the world origin (or some other unexpected location) and then potentially moved to the face, but without the proper hosting relationship.

The Solution: Leveraging Reference and Transform:

The key to correctly hosting a face-based family using the API is to use the Reference of the face and potentially its Transform. Here's a refined approach:

  1. Get the Reference of the Top Face: Instead of just passing the PlanarFace object directly to NewFamilyInstance, obtain its Reference. The Reference object uniquely identifies a specific geometric entity within an element.

  2. Determine a Suitable Placement Point: You still need an XYZ point on the face for initial placement. A simple approach is to use the Evaluate method of the PlanarFace at a UV parameter (e.g., (0.5, 0.5) for the center).

  3. Use NewFamilyInstance(Reference, XYZ, XYZ, FamilySymbol): This overload is specifically designed for hosting onto a face using its Reference.

Here's the modified code snippet:

C#
 
    public Result InsertConnections(FINCast.UI.InsertPrecastElementsDialog.InsertResult insertResult)
    {
        try
        {
            Document doc = RevitUIApplication.ActiveUIDocument.Document; // Get the document object
            Element hostElement = insertResult.HostElement;
            string selectedType = insertResult.SelectedElementType;
            string selectedRule = insertResult.SelectedRule;

            // Load XML configuration (assuming this part works)
            ACRule rule = LoadRuleConfig(selectedType, selectedRule);

            // Get the connection family symbol (assuming this part works)
            FamilySymbol symbol = GetFamilySymbol(rule);

            PlanarFace topFace = GetPlanarFace(hostElement);
            if (topFace == null)
            {
                TaskDialog.Show("Error", "Could not find the top planar face.");
                return Result.Failed;
            }

            // Get the Reference of the top face
            Reference faceReference = Reference.Create(topFace);

            // Determine a placement point on the face (e.g., the center)
            XYZ placementPoint = topFace.Evaluate(new UV(0.5, 0.5));

            // Define a reference direction (e.g., the face normal)
            XYZ referenceDirection = topFace.FaceNormal;

            // Place the face-based family instance using the Face Reference
            using (Transaction tx = new Transaction(doc, "Insert Connections"))
            {
                tx.Start();

                FamilyInstance fi = doc.Create.NewFamilyInstance(faceReference, placementPoint, referenceDirection, symbol);

                tx.Commit();
            }

            TaskDialog.Show("Success", $"Inserted connection onto Double Tee ID: {hostElement.Id.IntegerValue}.");
            return Result.Succeeded;
        }
        catch (Exception ex)
        {
            TaskDialog.Show("Error", $"Failed to insert connections: {ex.Message}");
            return Result.Failed;
        }
    }

    private PlanarFace GetPlanarFace(Element element)
    {
        Options opt = new Options();
        opt.ComputeReferences = true;
        opt.DetailLevel = ViewDetailLevel.Fine;
        GeometryElement geo = element.get_Geometry(opt);

        foreach (GeometryObject obj in geo)
        {
            if (obj is Solid solid && solid.Volume > 0)
            {
                foreach (Face face in solid.Faces)
                {
                    if (face is PlanarFace pf && pf.FaceNormal.Z > 0.5)
                    {
                        return pf;
                    }
                }
            }
            else if (obj is GeometryInstance instance)
            {
                GeometryElement instanceGeo = instance.GetInstanceGeometry();
                if (instanceGeo != null)
                {
                    foreach (GeometryObject nestedObj in instanceGeo)
                    {
                        if (nestedObj is Solid nestedSolid && nestedSolid.Volume > 0)
                        {
                            foreach (Face nestedFace in nestedSolid.Faces)
                            {
                                if (nestedFace is PlanarFace nestedPf && nestedPf.FaceNormal.Z > 0.5)
                                {
                                    return nestedPf;
                                }
                            }
                        }
                    }
                }
            }
        }
        return null; // Return null if no suitable face is found
    }

Key Changes and Explanations:

  • Reference.Create(topFace): We now create a Reference object from the identified PlanarFace. This Reference is what the NewFamilyInstance method needs for proper face-based hosting.
  • topFace.Evaluate(new UV(0.5, 0.5)): This provides a more consistent way to get a point on the face (the approximate center in this case). You can adjust the UV parameters (ranging from 0.0 to 1.0 in the U and V directions of the face) to place the instance at a different location.
  • topFace.FaceNormal: Using the face normal as the referenceDirection is a common and often appropriate choice for face-based families.
  • doc = RevitUIApplication.ActiveUIDocument.Document;: Ensure you are getting the active document within your command execution context.

Why This Works:

By using the Reference of the face, you're telling Revit exactly which geometric entity the family instance should be hosted onto. The placement point then acts as the initial "touchdown" point on that face, and Revit handles the necessary alignment and constraint creation to ensure the family moves with its host.

Regarding the "Host" Element Difference:

The difference you observed in the "Host" element ID and type between manually placed and API-placed instances is likely due to how Revit internally represents these relationships. When you manually place a face-based family, you're directly interacting with the face of a FamilyInstance. When you use the API with a Reference to a face, you're establishing a similar hosting relationship, but the internal representation might differ slightly in how it's reported by the Snoop tool. The crucial thing is that the behavior – the family moving with the host – should be the same.

Further Considerations:

  • Placement Logic: You might need more sophisticated logic to determine the exact UV point for placement based on your ACRule and the geometry of the host element.
  • Orientation: If your family needs a specific initial rotation, you might need to calculate a more appropriate referenceDirection based on the host element's geometry or the requirements of your ACRule.
  • Error Handling: Ensure robust error handling, especially if the expected top face isn't always found.

Give this modified code a try, Christian. I'm confident it will resolve the Z=0 issue and properly host your family instances to the top face. Let me know how it goes or if you have any more questions!

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 3 of 5

ChristianMolino
Enthusiast
Enthusiast

It needed a little massaging to get it error free, but once I did get it there it still is coming in at z=0 rather than on the actual face. I was trying a couple various overloads yesterday, but all of them are just inserting it on the ground or failing entirely so I am completely stumped on it.

 

Oddly, I tried changing the precast double tee (host family) from structural framing to a generic model and it brought it in on the face, but not in a sense that it actually is hosted to the face, more so just at the right XYZ coordinates to look like it was on the face. If I run the command on a basic floor element it works no problem hosting correctly with the "Face" overload where I can move and rotate the floor and the hosted elements follow.

I know the families are capable of it, as I was testing another add-in that did something similar and it worked no problem, it just had some other shortcomings that led me down the road of self developing to meet my needs. I just have no idea what exact overloads that developer used to get it to work.

 

Here was what Gemini modified for me, but AI seems to be consistently failing this task as I have been battling with Grok for 2 days trying to figure it out lol.

public Result InsertConnections(FINCast.UI.InsertPrecastElementsDialog.InsertResult insertResult)
    {
        try
        {
            Document doc = RevitUIApplication.ActiveUIDocument.Document;
            Element hostElement = insertResult.HostElement;
            string selectedType = insertResult.SelectedElementType;
            string selectedRule = insertResult.SelectedRule;

            // Load XML configuration
            ACRule rule = LoadRuleConfig(selectedType, selectedRule);

            // Get the connection family symbol
            FamilySymbol symbol = GetFamilySymbol(rule);

            PlanarFace planarTopFace = GetPlanarFace(hostElement);
            if (planarTopFace == null)
            {
                TaskDialog.Show("Error", "Could not find the top planar face.");
                return Result.Failed;
            }

            // Get the Reference of the top face
            Reference faceReference = Reference.Create(planarTopFace);

            // Determine a placement point on the face (e.g., the center)
            XYZ placementPoint = planarTopFace.Evaluate(new UV(0.5, 0.5));

            // Define a reference direction that is tangential to the face
            XYZ faceNormal = planarTopFace.FaceNormal;
            XYZ referenceDirection;

            // Ensure the reference direction is not parallel to the face normal
            if (Math.Abs(faceNormal.Z) < 0.99) // If the face is not almost perfectly vertical
            {
                referenceDirection = XYZ.BasisZ; // Use the global Z-axis as a tangential reference
            }
            else
            {
                referenceDirection = XYZ.BasisX; // If the face is vertical, use the global X-axis
            }

            // Place the face-based family instance using the Face Reference
            using (Transaction tx = new Transaction(doc, "Insert Connections"))
            {
                tx.Start();

                FamilyInstance fi = doc.Create.NewFamilyInstance(faceReference, placementPoint, referenceDirection, symbol);

                tx.Commit();
            }

            TaskDialog.Show("Success", $"Inserted connection onto Double Tee ID: {hostElement.Id.IntegerValue}.");
            return Result.Succeeded;
        }
        catch (Exception ex)
        {
            TaskDialog.Show("Error", $"Failed to insert connections: {ex.Message}");
            return Result.Failed;
        }
    }

    private PlanarFace GetPlanarFace(Element element)
    {
        Options opt = new Options();
        opt.ComputeReferences = true;
        opt.DetailLevel = ViewDetailLevel.Fine;
        GeometryElement geo = element.get_Geometry(opt);

        foreach (GeometryObject obj in geo)
        {
            if (obj is Solid solid && solid.Volume > 0)
            {
                foreach (Face face in solid.Faces)
                {
                    if (face is PlanarFace pf && pf.FaceNormal.Z > 0.5)
                    {
                        return pf;
                    }
                }
            }
            else if (obj is GeometryInstance instance)
            {
                GeometryElement instanceGeo = instance.GetInstanceGeometry();
                if (instanceGeo != null)
                {
                    foreach (GeometryObject nestedObj in instanceGeo)
                    {
                        if (nestedObj is Solid nestedSolid && nestedSolid.Volume > 0)
                        {
                            foreach (Face nestedFace in nestedSolid.Faces)
                            {
                                if (nestedFace is PlanarFace nestedPf && nestedPf.FaceNormal.Z > 0.5)
                                {
                                    return nestedPf;
                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

 

0 Likes
Message 4 of 5

ChristianMolino
Enthusiast
Enthusiast

After some further debugging it appears that if I change the face selection to manual and use the face reference overload it works, which makes it seem that it is something in the automatic face detection that is not working. Anyone have any ideas what is wrong in that block of code?

0 Likes
Message 5 of 5

ChristianMolino
Enthusiast
Enthusiast
Accepted solution

The issue indeed laid within the method for getting the face. The previous method was linking up the host relationship to a family symbol rather than a family instance. Below is the change in code that I now have working:

private Reference GetFaceReference(Element element)
{
    // Ensure we have a View3D for ReferenceIntersector
    View3D view3D = null;
    // Try to find an existing 3D view
    FilteredElementCollector viewCollector = new FilteredElementCollector(doc).OfClass(typeof(View3D));
    foreach (View3D v in viewCollector.Cast<View3D>())
    {
        if (!v.IsTemplate) // Exclude view templates
        {
            view3D = v;
            break;
        }
    }

    // If no suitable 3D view exists, create a temporary one
    if (view3D == null)
    {
        using (Transaction tempTx = new Transaction(doc, "Create Temporary 3D View"))
        {
            tempTx.Start();
            ViewFamilyType viewFamilyType = new FilteredElementCollector(doc)
                .OfClass(typeof(ViewFamilyType))
                .Cast<ViewFamilyType>()
                .FirstOrDefault(vft => vft.ViewFamily == ViewFamily.ThreeDimensional);

            if (viewFamilyType == null)
            {
                System.Diagnostics.Debug.WriteLine("Error: No 3D ViewFamilyType found.");
                return null;
            }

            view3D = View3D.CreateIsometric(doc, viewFamilyType.Id);
            tempTx.Commit();
        }
    }

    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; // Project downward to find the top face
    IList<ReferenceWithContext> refs = intersector.Find(startPoint, direction);

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

    foreach (ReferenceWithContext rwc in refs)
    {
        Reference r = rwc.GetReference();
        GeometryObject geomObj = element.GetGeometryObjectFromReference(r);
        if (geomObj is PlanarFace pf && pf.FaceNormal.Z > 0.5)
        {
            XYZ centroid = r.GlobalPoint; // Use global point from ReferenceIntersector
            System.Diagnostics.Debug.WriteLine($"ReferenceIntersector Face: Normal = ({pf.FaceNormal.X}, {pf.FaceNormal.Y}, {pf.FaceNormal.Z}), Global Centroid Z = {centroid.Z}");
            if (centroid.Z > maxZ)
            {
                maxZ = centroid.Z;
                topFaceRef = r;
            }
        }
    }

    if (topFaceRef != null)
    {
        GeometryObject topFaceObj = element.GetGeometryObjectFromReference(topFaceRef);
        if (topFaceObj is PlanarFace topFace)
        {
            System.Diagnostics.Debug.WriteLine($"Selected Top Face: Normal = ({topFace.FaceNormal.X}, {topFace.FaceNormal.Y}, {topFace.FaceNormal.Z}), Global Centroid Z = {maxZ}");
        }
        return topFaceRef;
    }

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