How to scale a geometry using pyRevit

Hi there,
I am truly challenged by solving a problem that initially seemed simple, but has turned into a huge challenge.
I need to create enlarged solids from my rooms. The rooms are irregular or L-shaped. I tried to extract the room boundaries and the direct shape, but the problem is that I cannot scale up correctly so that it preserves the room’s overall shape and location.

Could you kindly guide me on how to do that?

Best Regards,
Farshid

Hi Farshid,

If I understand correctly that your goal is to create solids from rooms and generate them in Revit as direct shapes, you actually don’t need to work with room boundaries/edges, but can do all operations on solids. I wrote some code that prompts the user to pick one or more rooms, retrieves solids from them, scales each solid in relation to the room’s location point by the desired scale factor and generates direct shapes from scaled solids:

# -*- coding: utf-8 -*-

from pyrevit import revit, script, forms
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *
from Autodesk.Revit.Exceptions import OperationCanceledException
from System.Collections.Generic import List

def main():

   doc = revit.doc

   forms.alert(msg="Please select one or more rooms")

   try:
       
      selection = revit.uidoc.Selection.PickObjects(ObjectType.Element, RoomSelectionFilter(), "Select one or more rooms")

      rooms = list(selection)
      
      t = Transaction(doc, "Create scaled direct shapes from rooms")
      t.Start()

      for room in rooms:

         room_id = room.ElementId
         room_element = doc.GetElement(room_id)

         solid = get_room_solid(room_element)

         # Get the location point of the room and it's coordinates

         location_point = room_element.Location.Point

         # Prepare transformation: 
         
         scale_factor = 0.5   # If scale_factor is 1, output will be the same size as input. If scale_factor is 2, output will be 2 times enlarged, if scale_factor is 0.5 it will 2 times smaller, etc.

         scale_transform = Transform(Transform.Identity)    # Make an empty transformation instance
         scale_transform.BasisX = XYZ(scale_factor, 0, 0)
         scale_transform.BasisY = XYZ(0, scale_factor, 0)
         scale_transform.BasisZ = XYZ(0, 0, scale_factor)

         # Set the origin. Origin is the reference point, in relation to which the geometry will be transformed. If it's just XYZ.Zero, or no value will be provided, it will use Project Base Point.
         # We will use each room's location point as reference point. The tricky part is we can't just use location point coordinates, but have to substract the scaled vector from them.
         scale_transform.Origin = XYZ((location_point.X - (location_point.X * scale_factor)), (location_point.Y - (location_point.Y * scale_factor)), (location_point.Z - (location_point.Z * scale_factor)))

         solid_transformed = SolidUtils.CreateTransformed(solid, scale_transform)

         # Set the category for direct shape. I chose generic model but it could be most of Revit categories. Surprisingly, it can't be Room though :D
         category = ElementId(BuiltInCategory.OST_GenericModel)

         # Create an empty direct shape
         direct_shape = DirectShape.CreateElement(doc, category)

         # Make an IList to store the solid as GeometryObject
         net_list = List[GeometryObject]()
         net_list.Add(solid_transformed)

         # Add the IList content to the direct shape
         direct_shape.SetShape(net_list)

      t.Commit()

   except OperationCanceledException:
   
      script.exit()
       

class RoomSelectionFilter(ISelectionFilter):
    
   def AllowElement(self, element):
      if (element.Category.Name == "Rooms"):
         return True
        
   def AllowReference(self, reference, position):
      return False
   

def get_room_solid(room):

      options = Options()
      options.ComputeReferences = True
      options.IncludeNonVisibleObjects = False
      geometry_element = room.get_Geometry(options)
      
      for geometry_object in geometry_element:
         
         if isinstance(geometry_object, GeometryInstance):
            instance_geometry = geometry_object.GetInstanceGeometry()
            for instance_object in instance_geometry:
                  if isinstance(instance_object, Solid) and instance_object.Volume > 0:
                     return instance_object
         elif isinstance(geometry_object, Solid) and geometry_object.Volume > 0:
            return geometry_object
      return None


if __name__ == "__main__":
    main()

I think the trickiest part is to actually keep the scaled rooms in the desired location, so each room’s location point.

To achieve that you need to make sure that the scaling happens relative to the room’s own location point, not the project base point.

But the Transform class in Revit API works in a slightly unintuitive way: the scaling matrix defined by BasisX, BasisY, and BasisZ scales around the origin of the transformation.
To keep the room in place, you must pre-translate the geometry so that the location point becomes the origin, apply the scale, then move it back, hence the line:

scale_transform.Origin = XYZ((location_point.X - (location_point.X * scale_factor)), (location_point.Y - (location_point.Y * scale_factor)), (location_point.Z - (location_point.Z * scale_factor)))

I used a one line formula, but another, maybe easier to follow way would be to do this step by step. I figured this out thanks to this thread where someone had a similar problem:https://forums.autodesk.com/t5/revit-api-forum/non-conformal-transform-to-solid-scale-along-one-axis/td-p/9769505

So we can create 3 separate transformations and then combine them into one: move the translation origin to the location point, define scale transformation without specifying origin (default value which is world origin will be used) and then move the geometry back to the right location. In our case it would look like this:

# -*- coding: utf-8 -*-

from pyrevit import revit, script, forms
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *
from Autodesk.Revit.Exceptions import OperationCanceledException
from System.Collections.Generic import List

def main():

   doc = revit.doc

   forms.alert(msg="Please select one or more rooms")

   try:
       
      selection = revit.uidoc.Selection.PickObjects(ObjectType.Element, RoomSelectionFilter(), "Select one or more rooms")

      rooms = list(selection)
      
      t = Transaction(doc, "Create scaled direct shapes from rooms")
      t.Start()

      for room in rooms:

         room_id = room.ElementId
         room_element = doc.GetElement(room_id)

         solid = get_room_solid(room_element)

         # Get the location point of the room and it's coordinates

         location_point = room_element.Location.Point

         # Prepare transformation: 
         
         scale_factor = 2  # If scale_factor is 1, output will be the same size as input. If scale_factor is 2, output will be 2 times enlarged, if scale_factor is 0.5 it will 2 times smaller, etc.

         move_origin_to_location_point = Transform.CreateTranslation(location_point)

         scale_transform = Transform(Transform.Identity)    # Make an empty transformation instance
         scale_transform.BasisX = XYZ(scale_factor, 0, 0)
         scale_transform.BasisY = XYZ(0, scale_factor, 0)
         scale_transform.BasisZ = XYZ(0, 0, scale_factor)

         move_back_transform = Transform.CreateTranslation(-location_point)

         combined_transform = move_origin_to_location_point * scale_transform * move_back_transform

         solid_transformed = SolidUtils.CreateTransformed(solid, combined_transform)

         # Set the category for direct shape. I chose generic model but it could be most of Revit categories. Surprisingly, it can't be Room though :D
         category = ElementId(BuiltInCategory.OST_GenericModel)

         # Create an empty direct shape
         direct_shape = DirectShape.CreateElement(doc, category)

         # Make an IList to store the solid as GeometryObject
         net_list = List[GeometryObject]()
         net_list.Add(solid_transformed)

         # Add the IList content to the direct shape
         direct_shape.SetShape(net_list)

      t.Commit()

   except OperationCanceledException:
   
      script.exit()
       

class RoomSelectionFilter(ISelectionFilter):
    
   def AllowElement(self, element):
      if (element.Category.Name == "Rooms"):
         return True
        
   def AllowReference(self, reference, position):
      return False
   

def get_room_solid(room):

      options = Options()
      options.ComputeReferences = True
      options.IncludeNonVisibleObjects = False
      geometry_element = room.get_Geometry(options)
      
      for geometry_object in geometry_element:
         
         if isinstance(geometry_object, GeometryInstance):
            instance_geometry = geometry_object.GetInstanceGeometry()
            for instance_object in instance_geometry:
                  if isinstance(instance_object, Solid) and instance_object.Volume > 0:
                     return instance_object
         elif isinstance(geometry_object, Solid) and geometry_object.Volume > 0:
            return geometry_object
      return None


if __name__ == "__main__":
    main()

If you want to do anything further with the output, like moving it by some specified value, that’s where another fun part begins, especially if you work in any other units than feet. Generally, Revit API code always uses Revit internal units, which is feet, and if you use units like centimeters, you have to scale them in your code. In such cases, I usually have a global variable at the top of my script, like this if I work in cm:

SCALE = 30.48

because 1 foot = 30.48 centimeters.

And then, if you specify any numeric values in your code in cm, you have to divide it by the SCALE value.

Please let me know if anything needs clarification or you have any questions.
All the best,
Magda

2 Likes

Hi Magdalena,

Thank you so much for providing such detailed instructions. They are both helpful and valuable. However, I need to take my time to fully understand the steps, as there are quite a few of them. I truly appreciate your offer for further assistance, and I will certainly reach out if I find that I need help.

Best regards,
Farshid