Script to Load and Insert Detail Family

Hi, all.
I’m new to pyRevit toolbar creation (just saw my first couple of videos on the topic recently), but am very excited for the opportunities ahead!
Could someone help me write a script for a pushbutton that will insert a detail family (or load one if it’s not already loaded)? The pseudo-code is as follows:

  1. click button to start script (already have this and the button already works - but the script itself does nothing yet).
  2. script will check if a specific detail family, say “symbol.rfa”, is loaded into current project model.
  3. if it is loaded, the script gets it ready to be inserted into the working view (similar to how the “Detail Component” in the Annotate tab already works).
  4. if is not loaded, the script will then load that detail family from a specific folder, say C:\pyRevit\MyTools.
  5. after loading it into the current project model, the script gets it ready to be inserted into the working view.

Any help would be greatly appreciated :slight_smile:

Hi @EBSoares
Check out this: GitHub - jmcouffin/pyRevit-BILT_NA_2022

That will get you there.
In the handout i talk about content bundles or content button.

This is a bundle type that lets you attach 1 or 2 families to a push button.

You can also install this demo toolbar nad figure out how it works and how it is structured clicking on the button + alt.

Hi, @Jean-Marc - thanks for the reply :+1:

It looks like the code there would require the Revit family to be actually located in the folder for the pushbutton itself. And I’m not sure if the program would even check if the family is already loaded in the project model before attempting to load the family.

Is there a way to write a script that will do that? Just like the pseudo-code above? That way we can keep all of our families in their usual folder (so it’s easy to make updates), write down the actual file path in the script, and go from there.

Thanks in advance

Check this: The Building Coder: Family API Add-in Load Family and Place Instances
The scenario 1 is what you are after.
It is c# but the logic is there.

You are right - after reading, it is exactly what I need :grinning:
I forgot to mention on the OP, but I am also new to Python (never wrote a thing in this language) and do not know how to convert C# to it in order to place it in the scipt.py file - could you give me a hand on that by any chance?

Got the code from Jeremy Tammik converted to Python (thanks Jeremy and Jean-Mark!).

Issue now is that it doesn’t seem to compile. I wish I knew enough about Python to fix it, but alas I can’t… Could someone help me figure this one out?

import clr
import os.path
import System
import Autodesk.Revit.DB as DB
import Autodesk.Revit.UI as UI


clr.AddReference("RevitAPI")
clr.AddReference("RevitAPIUI")


class CmdTableLoadPlace(UI.IExternalCommand):
	FamilyName = "family_api_table"
	_family_folder = os.path.dirname(System.Reflection.Assembly.GetExecutingAssembly().Location)
	_family_ext = "rfa"
	_family_path = os.path.join(_family_folder, FamilyName)
	_family_path = os.path.splitext(_family_path)[0] + "." + _family_ext


	_added_element_ids = []


	def Execute(self, commandData):
		uiapp = commandData.Application
		uidoc = uiapp.ActiveUIDocument
		app = uiapp.Application
		doc = uidoc.Document


		# Retrieve the family if it is already present:
		family = DB.FilteredElementCollector(doc).OfClass(DB.Family).FirstOrDefault(lambda f: f.Name == self.FamilyName)


	if not family:
		# It is not present, so check for the file to load it from:
		if not os.path.exists(self._family_path):
			TaskDialog.Show("Error", "Please ensure that the sample table family file '{0}' exists in '{1}'.".format(self.FamilyName, self._family_folder))
			return UI.Result.Failed


			# Load family from file:
			with DB.Transaction(doc, "Load Family") as tx:
				family = doc.LoadFamily(self._family_path)


		# Determine the family symbol
		symbol = None
		for s in family.Symbols:
			symbol = s
			# Our family only contains one symbol, so pick it and leave
			break


		# Place the family symbol:
		# Subscribe to document changed event to retrieve family instance elements added by the PromptForFamilyInstancePlacement operation:
		app.DocumentChanged += self.OnDocumentChanged
		self._added_element_ids.clear()


		# PromptForFamilyInstancePlacement cannot be called inside transaction.
		uidoc.PromptForFamilyInstancePlacement(symbol)


		app.DocumentChanged -= self.OnDocumentChanged


		# Access the newly placed family instances:
		n = len(self._added_element_ids)
		msg = "Placed {0} {1} family instance{2}{3}".format(n, family.Name, self.PluralSuffix(n), self.DotOrColon(n))
		ids = ", ".join(str(id.IntegerValue) for id in self._added_element_ids)
		TaskDialog.Show("Information", "{0}\n{1}".format(msg, ids))


		return UI.Result.Succeeded


	def OnDocumentChanged(self, sender, args):
		self._added_element_ids.extend(args.GetAddedElementIds())


	def PluralSuffix(self, n):
		return "" if n == 1 else "s"


	def DotOrColon(self, n):
		return ":" if n == 0 else "."

Hi @EBSoares ,

the problem with the script is you translated is… that is not a runnable pyrevit script.

The execution of a revit Add-in command and a pyRevit script is very different, even if they use the same Revit API.

The good news is that wirting python and a pyRevit script is easier than writing and compiling an add-in!

What you wrote is a Class and, in order to run, it needs to be instantiated and tell what method to execute.
But this is the Revit Add-in way, revit knows that a class that implements the IExternalCommand has the Execute method and launches it when you click the button.

Within pyrevit, you just need to write the code as it is a sequence of instructions to execute - a script.
Or, if you’re feeling fancy and like a bit of organization like me, you can create functions to group functionality in a logical manner and then call those functions at the end of the file.

Other things to note: you won’t have access to the commandData, but by using import pyrevit (or better, importing its submodules) you can access the current document with fewer lines. And in this specific case, the pyrevit functions that you need already retrieve the doc by themselves.

This is completely untested, but I’d rewrite the code like this:

import os.path

from pyrevit.revit import query
from pyrevit.revit.db import create
from pyrevit.revit.db import transaction 
from pyrevit import forms

def main(family_name, family_path):
    if query.get_family(family_name):
        # the family is already there, we exit early
        return

    if not os.path.exists(family_path):
        forms.alert(
            "Please ensure that the sample table family file '{0}' exists.".format(family_path),
            title="Error",
        )
        return

    with transaction.Transaction(name="Load Family"):
         family = create.load_family(family_path)
         symbol = next(s for s in family.Symbols)
         place_family_symbol(symbol)


def place_family_symbol(symbol):
   added_element_ids=[]

    def update_added_elements_ids(sender, args):
        nonlocal added_element_ids
        added_element_ids = args.GetAddedElementIds()

    app.DocumentChanged += update_added_elements_ids
    uidoc.PromptForFamilyInstancePlacement(symbol)
    app.DocumentChanged -= update_added_elements_ids
    # here you can build the message and show it with form.alert(msg, warn_icon=False)
    # ...


def build_family_path(family_name):
    # not sure this will work, you can print(family_folder) after the following line to see what it contains
    family_folder = os.path.dirname(System.Reflection.Assembly.GetExecutingAssembly().Location)
    return os.path.join(family_folder, family_name) + ".rfa"


# the last thing to do is to set the arguments and call the main function
family_name = "family_api_table"
family_path = build_family_path(family_name)
main(family_name, family_path)

If I may, given you other post, I would suggest you to start with a generic python course, there are many available online;
trying to write code by copying and pasting things found online feels like tossing wheels, nuts and bolts on a floor hoping to build a car :wink:

Umfortunately using pyrevit you have the double duty to know/understand two programming languages, since most of the actionable info around revit API is c# centered…

3 Likes

Hi, Andrea…
This is a fantastic, well-meaning, and in-depth explanation of everything I’ve been after in this forum - with a ready-to-use code (even if untested) to boot :smile:
Like you said, I’ve started reviewing a Python course a couple weeks ago (I’ve seen some videos already, but this webpage is my favorite so far).
A friend of mine who is really good at Python (guy’s a genius) has offered to help me work on this code too, so I’ll pass on what you wrote above to him in case it helps some more in any way.
Once it’s all done and tested I’ll post the final code here in case someone else in the future has the same need.
Thank you for your kindness and God bless you!
Edgar

2 Likes

@sanzoghenzo

I have a question regarding subscribing to the DocumentChanged event. Is it a single event? I am concerned that I keep adding these handlers in the system memory when I use sys.exit

This was my solution in trying to get my ESC operational when I want to stop placing the components.

def place_family_symbol(symbol):
    added_element_ids = []
    app.DocumentChanged += update_added_elements_ids
    try:
        uidoc.PromptForFamilyInstancePlacement(symbol)
    except Exception as e:
        if str(e) == "The user aborted the pick operation.":
            sys.exit  
    app.DocumentChanged -= update_added_elements_ids
    return added_element_ids


def update_added_elements_ids(args):
    global added_element_ids
    added_element_ids = args.GetAddedElementIds()

Hi @tayOthman,
the += and -= mean “add the event handler” and “remove the event handler”, respectively;
you should definitely take care of removing the handler once you’ve done with it.

in your code, just place the handler de-registration in a finally block, so that it gets called in any case.

2 Likes

Thank you @sanzoghenzo

It is working for me now, However, to end my command I have to hit escape twice. I have a feeling that ending the command will trigger DocumentChanged Event again. do you know if this is the case here?

import os.path
from pyrevit import DB, HOST_APP, forms
from pyrevit.revit import query
from pyrevit.revit.db import transaction

import sys

class FamilyLoader:
    def __init__(self):
        from pyrevit import revit
        self.doc = revit.doc
        self.uidoc = revit.uidoc
        self.app = HOST_APP.app
        self.added_element_ids = []
        self.escape = False
    def main(self, family_name):
        """Main function to load and place a family symbol.
        args:
            family_name (str): Name of the family to load and place.
        returns:
            None
        """
        family_path = self.build_family_path(family_name)
        if not os.path.exists(family_path):
            forms.alert(
                "Please ensure that the family '{0}' exists."\
                    .format(family_path),
                title="Error",
            )
            return
        with transaction.Transaction(name="Load Family"):
            if query.get_family(family_name):
                pass
            else:
                self.doc.LoadFamily(family_path)
            # Get loaded family symbol
            family = query.get_family(family_name)
            family_symbol = family[0]
        self.place_family_symbol(family_symbol)
        return self.place_family_symbol(family_symbol)
    def place_family_symbol(self, symbol):
        """Place family symbol in the Revit model.
        args:
            symbol (DB.FamilySymbol): Family symbol to place.
        returns:
                None"""
        # subscribe to DocumentChanged event to get added element ids
        self.app.DocumentChanged += self.update_added_elements_ids
        try:
            if self.escape:
                self.app.DocumentChanged -= self.update_added_elements_ids
                return self.added_element_ids      
            else: 
                self.uidoc.PromptForFamilyInstancePlacement(symbol)
                # Get added element ids
                added_element_ids = self.added_element_ids
                return added_element_ids
        except Exception as e:
                self.app.DocumentChanged -= self.update_added_elements_ids
                self.app.DocumentChanged -= self.update_added_elements_ids
                self.escape = True
                return self.added_element_ids
        finally:
            self.app.DocumentChanged -= self.update_added_elements_ids

    def update_added_elements_ids(self, sender, args):
        self.added_element_ids.append(args.GetAddedElementIds())
        return self.added_element_ids

    @staticmethod
    def build_family_path(family_name):
            """Build the path to the family file."
            args:
                family_name (str): Name of the family file.
            returns:
                str: Path to the family file.
            """
            family_folder = os.path.join(
                os.path.dirname(
                    os.path.dirname(os.path.dirname(__file__))
                ),
                "Sutter.extension",
                "Source Files",
                "Rooms",
            )
            return os.path.join(family_folder, family_name) + ".rfa"

You’re calling the place_family_symbol twice :wink:

random advice:

  • return self.added_element_ids appears in all paths, you can just put it in the finally block
  • in the same way, I believe it is enough to de-register the self.update_added_elements_ids in the finally block
  • staticmethods are more or less useless, just create a function
  • for path manipulation, pathlib.Path is a handier choice than os.path
2 Likes

I got sore eyes coding late at night lol

If I am returning outside the class would Finally still work? If I keep the de-registration inside finally block, my transaction gets rolled back immediately once I hit escape (although I place these families outside a transaction)

Thank you for the notes. it is really helpful @sanzoghenzo .