Jan 12, 2024

How To Cut FilledRegions with a Detail Line

Learn how we can use Revit API to cut selected Filled Regions with a Detailed Line. It's not as complicated as you might expect! You can also check how this works in the YouTube video among other cool tools I made for EF-Tools (Free Extension for Autodesk Revit)

Recently I was making FIlledRegion tools, and I really wanted to make a Saw Cut with a line through regions.

As simple as it sounds, It actually took some time to come up with the workflow that works, and I want to share it with you. 

It can be applied to other elements like Floors and Ceilings as well, so I think it's quite useful for you to know 

💡 Let's learn how to cut elements with line.

Workflow

Firstly, let's brainstorm the workflow so you understand the steps and then we will go into coding part.

Here are the steps we will go through:

- Select FilledRegion and DetialLine
- Create infinite plane from Line
- Create temp Geometry from boundaries
- Cut Geometry with plane in both directions
- Get Outine of new geometries
- Recreate FilledRegions

These are the steps that I will explain to cut an element with a line.

Select Elements

There are different methods to select our elements. To make it the best user experience it's worth using ISelectionFilter, as it will limit elements that can be selected and avoid mistakes by selecting wrong elements.

Also I use forms.WarningBar from pyRevit to make it clearer for the user what needs to be selected while having an orange bar on the top.

Here are my custom ISelectionFilter classes:

class ISelectionFilter_Regions(ISelectionFilter):
    def AllowElement(self, element):
        if type(element) == FilledRegion:
            return True


class ISelectionFilter_DetailLine(ISelectionFilter):
    def AllowElement(self, element):
        if type(element) == DetailLine:
            return True

Now we can combine it all together to prompt user to select multiple Regions and a single Detail Line with selection.PickObject selection.PickObjects.

I also added Try/Except statements to avoid crashing, and if statement to check if we get any elements.

selection = uidoc.Selection #type: Selection

#📦 Define Placeholders
selected_line    = None
selected_regions = None

#1️⃣ Get FilledRegions
with forms.WarningBar(title='Pick Filled Region:'):
    try:
        ref_picked_objects = selection.PickObjects(ObjectType.Element, ISelectionFilter_Regions())
        selected_regions   = [doc.GetElement(ref) for ref in ref_picked_objects]
    except:
        pass


#2️⃣ Get DetailLine
with forms.WarningBar(title='Pick Detail Line:'):
    try:
        ref_picked_object   = selection.PickObject(ObjectType.Element, ISelectionFilter_DetailLine())
        selected_line       = doc.GetElement(ref_picked_object)
    except:
        pass

# Check if elements were selected
if not selected_regions:
    forms.alert("FileldRegions weren't selected. Please Try Again.",exitscript=True)

if not selected_line:
    forms.alert("Detail Line wasn't selected. Please Try Again.", exitscript=True)

Create Infinite Plane from Line

Now we can convert a line into an infinite Plane that will be used to cut any geometry.

We can get Start and End point by using Curve.GetEndPoint and to make a plane we need 3 points to use Plane.CreateByThreePoints. 

So I will get middle point of the line and change Z coordinate to be a bit higher. This will be enough for creating a plane.

Let's make it a function and create our plane

# Function to Create a Plane from DetailLine
def create_plane_from_line(line):
    '''Create Infinite Plane from Line'''
    crv = line.Location.Curve
    pt_start = crv.GetEndPoint(0)
    pt_end   = crv.GetEndPoint(1)
    pt_mid   = (pt_start + pt_end) / 2
    pt_mid   = XYZ(pt_mid.X, pt_mid.Y, pt_mid.Z + 10)
    plane    = Plane.CreateByThreePoints(pt_start, pt_end, pt_mid)
    return plane

#✅ Create Plane from Line (By 3 Points)
plane = create_plane_from_line(selected_line)

Also when we will start cutting our geoemtry, we need to get results from both sides of the line, so we can create 2 new Filled Regions on each side of the line.

For that we will need to mirror our plane, so we can get results from both sides later when we start cutting.

Here is a function to mirror a plane direction. 

def mirror_plane(plane):
    # Get the origin and normal of the original plane
    origin = plane.Origin
    normal = plane.Normal

    # Mirror the normal by reversing its direction
    mirrored_normal = XYZ(-normal.X, -normal.Y, -normal.Z)

    # Create a new mirrored plane with the same origin and mirrored normal
    mirrored_plane = Plane.CreateByNormalAndOrigin(mirrored_normal, origin)
    return mirrored_plane

plane          = ... # Defined Earlier
mirrored_plane = mirror_plane(plane)

Create Geometry from Regions

New we can iterate through all FilledRegions and create geometry from them.

We will use FilledRegion.GetBoundaries to get outline. And then we can use GeometryCreationUtilities.CreateExtrusionGeometry to make it a 3D Shape by extrucing this outline up.

# Create 3D Geometry from FilledRegion's Boundaries
for region in selected_regions:
    try:
        #📦 Create Shape from Boundaries (random height)
        boundaries = region.GetBoundaries()
        shape      = GeometryCreationUtilities.CreateExtrusionGeometry(boundaries, XYZ(0,0,1), 10) #10 - height
    except:
        pass

Cut Geometry with Plane

✂️ Once we have Geometry and a Plane we can start cutting.

We will use BooleanOperationsUtils.CutWithHalfSpace method for that. We can provide Geometry and a Plane and it will return us resulting geometry.

💡 This is also where we will need to flip our Plane to get second part!

#✂ Split Solid with Plane (In both directions)
new_shape_1 = BooleanOperationsUtils.CutWithHalfSpace(shape, plane)
new_shape_2 = BooleanOperationsUtils.CutWithHalfSpace(shape, mirror_plane(plane))

Test Geometry

So now we should have 2 new Geometries. Before going any further we might want to test if we get correct results.

We can create a DirectShape from these geometries to create 3D shapes in Revit UI. This is a step that is useful during development but should be commented out in production.

💡 P.S. Don't forget Transaction!

# Create DirectShape to visualize results
cat_id = ElementId(BuiltInCategory.OST_GenericModel)

# Shape 1
ds1     = DirectShape.CreateElement(doc, cat_id)
ds1.SetShape([new_shape_1])

# Shape 2
ds2     = DirectShape.CreateElement(doc, cat_id)
ds2.SetShape([new_shape_2])

Get New Geo Outline

Once you know that your new geometries are correct, we can start working backwards to create FIlledRegions again.

First of all let's get Geometry Outlines. I will be looking for the Top face of the geometry and then get its Outline using Face.GetEdgesAsCurveLoops

# Function - Find Top Face of Geo
def find_top_face(solid):
    # Iterate through the faces of the solid
    for face in solid.Faces:
        if face.FaceNormal.IsAlmostEqualTo(XYZ.BasisZ):
            return face

#🔝 Get Top Faces of new Geometries
top_face_1 = find_top_face(new_shape_1)
top_face_2 = find_top_face(new_shape_2)

#🔲 Get Top Face Outlines
outline_1 = top_face_1.GetEdgesAsCurveLoops()
outline_2 = top_face_2.GetEdgesAsCurveLoops()

Create new FilledRegions

Now we have everything we need to Create new FilledRegions after the Cut, and delete the initial Region.

We will use FilledRegion.Create method and Document.Delete
We will also make sure that we use the same Region type by getting it with .GetTypeId

Here is the final Step

#🔵 Get Filled Region Type
fr_id = region.GetTypeId()

#✅ Create new FilledRegions
filled_region1 = FilledRegion.Create(doc, fr_id, doc.ActiveView.Id, outline_1)
filled_region2 = FilledRegion.Create(doc, fr_id, doc.ActiveView.Id, outline_2)

#🔥 Delete Old Filled Region
if filled_region1 and filled_region2:
    doc.Delete(region.Id)

Final Code

So let's put it all together in a single snippet to avoid confusion and misunderstanding!

# -*- coding: utf-8 -*-
__title__   = "Split Regions with Line"
__doc__ = """Version = 1.0
Date    = 17.12.2023
_____________________________________________________________________
Description:
Split Selected FilledRegions with a Detail Line.

💡 Detail Line will be used to create an infinite plane!
_____________________________________________________________________
How-to:

-> Click on the button
-> Select Filled Regions
-> Select DetailLine
_____________________________________________________________________
Last update:
- [17.12.2023] - 1.0 RELEASE
_____________________________________________________________________
To-Do?:
- Keep Parameters?
_____________________________________________________________________
Author: Erik Frits"""

# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╚═╝ IMPORTS
#==================================================
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI.Selection import *
from pyrevit import forms


# ╦╔═╗╦═╗╦╔═╗╔╗ ╔═╗╔═╗
# ╚╗╔╝╠═╣╠╦╝║╠═╣╠╩╗║  ║╣ ╚═╗
#  ╚╝ ╩╩╚═╩╩ ╩╚═╝╩═╝╚═╝╚═╝ VARIABLES
#==================================================
uidoc = __revit__.ActiveUIDocument
doc   = __revit__.ActiveUIDocument.Document #type: Document
app   = __revit__.Application

selection = uidoc.Selection #type: Selection


# ╔═╗╦ ╦╔═╗╔╗╔╔╦╗╦╔═╗╔╗╔╔═╗
# ╠╣ ║║  ║║║ ║║ ║║║║╚═╗
# ╚═╝╚═╝╝╚╝ ╩╚═╝╝╚╝╚═╝
#==================================================
def mirror_plane(plane):
    # Get the origin and normal of the original plane
    origin = plane.Origin
    normal = plane.Normal

    # Mirror the normal by reversing its direction
    mirrored_normal = XYZ(-normal.X, -normal.Y, -normal.Z)

    # Create a new mirrored plane with the same origin and mirrored normal
    mirrored_plane = Plane.CreateByNormalAndOrigin(mirrored_normal, origin)
    return mirrored_plane


def find_top_face(solid):
    # Iterate through the faces of the solid
    for face in solid.Faces:
        if face.FaceNormal.IsAlmostEqualTo(XYZ.BasisZ):
            return face


def create_plane_from_line(line):
    '''Create Infinite Plane from Line'''
    crv = line.Location.Curve
    pt_start = crv.GetEndPoint(0)
    pt_end   = crv.GetEndPoint(1)
    pt_mid   = (pt_start + pt_end) / 2
    pt_mid   = XYZ(pt_mid.X, pt_mid.Y, pt_mid.Z + 10)
    plane    = Plane.CreateByThreePoints(pt_start, pt_end, pt_mid)
    return plane

# ╔═╗╦  ╔═╗╔═╗╔═╗╔═╗╔═╗
# ╠═╣╚═╗╚═╗║╣ ╚═╗
# ╚═╝╩═╝╩ ╩╚═╝╚═╝╚═╝╚═╝
#==================================================
class ISelectionFilter_Regions(ISelectionFilter):
    def AllowElement(self, element):
        if type(element) == FilledRegion:
            return True


class ISelectionFilter_DetailLine(ISelectionFilter):
    def AllowElement(self, element):
        if type(element) == DetailLine:
            return True


# ╔╦╗╔═╗╦╔╗╔
# ║║║╠═╣║║║║
# ╩╩ ╩╩╝╚╝
#==================================================
#📦 Define Placeholders
selected_line, selected_regions = None, None

selected_line    = None
selected_regions = None

#1️⃣ Get FilledRegions
with forms.WarningBar(title='Pick Filled Region:'):
    try:
        ref_picked_objects = selection.PickObjects(ObjectType.Element, ISelectionFilter_Regions())
        selected_regions   = [doc.GetElement(ref) for ref in ref_picked_objects]
    except:
        pass


#2️⃣ Get DetailLine
with forms.WarningBar(title='Pick Detail Line:'):
    try:
        ref_picked_object   = selection.PickObject(ObjectType.Element, ISelectionFilter_DetailLine())
        selected_line       = doc.GetElement(ref_picked_object)
    except:
        pass

# Check if elements were selected
if not selected_regions:
    forms.alert("FileldRegions weren't selected. Please Try Again.",exitscript=True)

if not selected_line:
    forms.alert("Detail Line wasn't selected. Please Try Again.", exitscript=True)



#3️⃣ Create Plane from Line (By 3 Points)
plane = create_plane_from_line(selected_line)


#✅ Ensure Elements
if not selected_line or not selected_regions:
    forms.alert('Select Region and Line.', exitscript=True)


#🎯 Modify Shape
t = Transaction(doc, 'EF_Split Regions with Line')
t.Start()

for region in selected_regions:
    try:
        #📦 Create Shape from Boundaries (random height)
        boundaries = region.GetBoundaries()
        shape      = GeometryCreationUtilities.CreateExtrusionGeometry(boundaries, XYZ(0,0,1), 10) #10 - height

        #✂ Split Solid with Plane (In both directions)
        new_shape_1 = BooleanOperationsUtils.CutWithHalfSpace(shape, plane)
        new_shape_2 = BooleanOperationsUtils.CutWithHalfSpace(shape, mirror_plane(plane))

        #🔝 Get Top Faces of new Geometries
        top_face_1 = find_top_face(new_shape_1)
        top_face_2 = find_top_face(new_shape_2)

        #🔲 Get Top Face Outlines
        outline_1 = top_face_1.GetEdgesAsCurveLoops()
        outline_2 = top_face_2.GetEdgesAsCurveLoops()

        #🔵 Get Filled Region Type
        fr_id = region.GetTypeId()

        #✅ Create new FilledRegions
        filled_region1 = FilledRegion.Create(doc, fr_id, doc.ActiveView.Id, outline_1)
        filled_region2 = FilledRegion.Create(doc, fr_id, doc.ActiveView.Id, outline_2)

        #🔥 Delete Old Filled Region
        if filled_region1 and filled_region2:
            doc.Delete(region.Id)

        # # Create DirectShape to visualize results
        # cat_id = ElementId(BuiltInCategory.OST_GenericModel)
        # ds1     = DirectShape.CreateElement(doc, cat_id)
        # ds1.SetShape([new_shape_1])
        #
        # ds2     = DirectShape.CreateElement(doc, cat_id)
        # ds2.SetShape([new_shape_2])
    except:
        pass
t.Commit()

Join Newsletter

📩 You will be added to Revit API Newsletter

Join Us!

which is already read by 7400+ people!

Ready to become Revit Hero for your office? Learn Revit API!

Join this comprehensive course that will guide you step by step on how to create your dream tools for Revit that save time.