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
selected_line = None
selected_regions = None
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
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
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
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
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):
origin = plane.Origin
normal = plane.Normal
mirrored_normal = XYZ(-normal.X, -normal.Y, -normal.Z)
mirrored_plane = Plane.CreateByNormalAndOrigin(mirrored_normal, origin)
return mirrored_plane
plane = ...
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.
for region in selected_regions:
try:
boundaries = region.GetBoundaries()
shape = GeometryCreationUtilities.CreateExtrusionGeometry(boundaries, XYZ(0,0,1), 10)
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!
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!
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])
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
def find_top_face(solid):
for face in solid.Faces:
if face.FaceNormal.IsAlmostEqualTo(XYZ.BasisZ):
return face
top_face_1 = find_top_face(new_shape_1)
top_face_2 = find_top_face(new_shape_2)
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()