Modifying python script to work with list input

I made a script using a slightly modified version of the Steamnode GetPointsFromBoxSelection that averages out the Z value of point cloud points that fall within a box family. It works well, but only for a single object input. I’m trying to update it to work with a list of objects, and this python script looks to need changed to work with a list input.

I think this stackoverflow thread has the information needed, but I’m not where I’d need to be with programming to understand how to implement it. https://stackoverflow.com/questions/35515996/revit-api-dynamo-handling-variable-input-types

Script in question posted below. I’m a new user so unable to upload attachments if any other info would help let me know.

IN[1] would be the input that has the list. Revit 2025.3

#python nodes in dynamo 1.2
####BETA VERSION#####
#proposed by Julien Benoit @jbenoit44 
#http://aecuandme.wordpress.com/
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
# Import ToDSType(bool) extension method
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
# Import geometry conversion extension methods
clr.ImportExtensions(Revit.GeometryConversion)
# Import DocumentManager and TransactionManager
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
from System.Collections.Generic import *
# Import RevitAPI
clr.AddReference("RevitAPI")
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.PointClouds import *


doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
app = uiapp.Application
uidoc=DocumentManager.Instance.CurrentUIApplication.ActiveUIDocument


planes=[]
for i in IN[1]:
	planes.append(UnwrapElement(i).ToPlane())
Iplanes=List[Autodesk.Revit.DB.Plane](planes)


pcf=PointCloudFilterFactory.CreateMultiPlaneFilter(planes)

averageDistance = IN[2]
numberOfPoints= IN[3]

points = UnwrapElement(IN[0]).GetPoints(pcf, averageDistance, numberOfPoints)
elements=[]
for p in points:
	X=p.X
	Y=p.Y
	Z=p.Z
	elements.append(Point.Create(XYZ(X,Y,Z)).ToProtoType(False))


OUT = elements

I may be misunderstanding, but it looks like the issue is with getting the points. You are not iterating through the IN list like you are for getting the planes.

1 Like

Hi @matt.wna and welcome !

I’ve never had the chance to work with point clouds before, but my curiosity is too strong…

Here a Python solution using directly a list of SectionViews at input

import clr
import sys
import System
from System.Collections.Generic import List
clr.AddReference('ProtoGeometry')
import Autodesk.DesignScript.Geometry as DS

#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.PointClouds import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

def gen_PairPts(*args):
    """generator for point pair loop
    """
    for idx, pt in enumerate(args):
        try: yield pt, args[idx + 1]
        except : yield pt, args[0]

def solid_fromBBX(bbox):
    """Create a solid from a BoundingBox
    Args:
        bbox (BoundingBox): BoundingBox object
    Returns:
        Solid: Solid generated from the BoundingBox
    """
    pt0 = XYZ( bbox.Min.X, bbox.Min.Y, bbox.Min.Z )
    pt1 = XYZ( bbox.Max.X, bbox.Min.Y, bbox.Min.Z )
    pt2 = XYZ( bbox.Max.X, bbox.Max.Y, bbox.Min.Z )
    pt3 = XYZ( bbox.Min.X, bbox.Max.Y, bbox.Min.Z )
    #create generator
    pairPts = gen_PairPts(pt0, pt1, pt2, pt3) 
    #Create loop, still in BBox coords
    edges = List[Curve]()
    for pt1, pt2 in pairPts:
        edges.Add(Line.CreateBound( pt1, pt2 ))

    height = bbox.Max.Z - bbox.Min.Z
    baseLoop = CurveLoop.Create( edges )
    loopList = List[CurveLoop]()
    loopList.Add( baseLoop )
    preTransformBox = GeometryCreationUtilities.CreateExtrusionGeometry( loopList, XYZ.BasisZ, height )
    transformBox = SolidUtils.CreateTransformed( preTransformBox, bbox.Transform )
    return transformBox
    
def get_Points_by_SectionBox(point_cloud, viewSection):
    """Get points by a given SectionBox in a point cloud
    Args:
        point_cloud (PointCloud): PointCloud object
        viewSection (ViewSection): ViewSection object
    Returns:
        tuple: List of transformed points, List of curves from the SectionBox
    """
    global averageDistance
    global numberOfPoints
    tf = point_cloud.GetTransform()
    bbxView = viewSection.CropBox 
    solidbbx = solid_fromBBX(bbxView)
    #
    lst_planes=List[DB.Plane]()
    for face in solidbbx.Faces:
        plane = DB.Plane.CreateByNormalAndOrigin(face.FaceNormal.Negate(), face.Origin)
        lst_planes.Add(plane)
    #
    pcf = PointCloudFilterFactory.CreateMultiPlaneFilter(lst_planes)
    points = point_cloud.GetPoints(pcf, averageDistance, numberOfPoints)
    ds_solidbbx = solidbbx.ToProtoType()
    curves_box = [e.CurveGeometry for face in ds_solidbbx.Faces for e in face.Edges]
    solidbbx.Dispose()
    return [tf.OfPoint(XYZ(p.X, p.Y, p.Z)).ToPoint() for p in points], curves_box
    
toList = lambda x : x if hasattr(x, "__iter__") and not isinstance(x, (str, System.String)) else [x]

point_cloud = UnwrapElement(IN[0])
lst_viewSection = toList(UnwrapElement(IN[1]))
averageDistance = IN[2]
numberOfPoints= IN[3]

OUT  = [get_Points_by_SectionBox(point_cloud, viewSection) for viewSection in lst_viewSection]

Note :
Take time to read the forum rules (How to get help on the Dynamo forums ) and the community guidelines (https://forum.dynamobim.com/faq )

2 Likes

Thanks for quick responses and info.

@c.poupin That’s awesome to see that working. It will take me a bit to get through for sure.

I think that @staylor is right though regarding the iteration. I have hopefully posted a picture of the graph below. (what are dynamo comments called? Notes don’t seem to be size adjustable and I really wanted to mark up this graph. Most of my visual scripting exp is from Unreal)

Let me explain a bit. The goal here is to create a smooth toposolid in revit using dirty point cloud data. I’m currently bringing in a point cloud that is mostly of grade, but it has too many points as well as stray points in it that need filtered. I use a box mesh family and the dynamo graph above to gather up the points that are contained within the boundaries of the box mesh, average out the Z values of the point cloud points, then place another family instance at the calculated height and XY center of the box mesh. I place anywhere from 100-1000 of these boxes of various sizes around the area I need to filter. Once completed. I run another script that gathers up the placed families and generates a toposolid using the origin points of those families. The result is a clean and accurate toposolid from a scan with pretty granular control of detail.

The only issue is that the above script only works with a single element, so each gathering box needs to be clicked on and have the script ran individually.

Ideally, I’d like to just ditch the python aspect out of it and do it natively in dynamo, but I don’t believe the point cloud factory is accessible in dynamo at the moment.

Fairly sure the script @c.poupin posted has the answer in it though. Specifically…

toList = lambda x : x if hasattr(x, "__iter__") and not isinstance(x, (str, System.String)) else [x]

In my attempts to solve this, I’ve continually ran into the same beginner issue of not accessing the right object. I’m currently trying to re-create the python node in the SteamNode package that handles the point cloud filter in an attempt to isolate whats going on, but I’m just not understanding something correctly.

I’m passing in a list of 6 planes into the python script below, and its returning an error on line 32 saying the list object has no attribute “ToPlane”. I’m apparently trying to grab data from the list object and not the actual plane, but I can’t understand how to access the actual object. Can anyone advise on what I’m missing? I can make simple examples work, but this isn’t clicking for me.

# Load the Python Standard and DesignScript Libraries
import sys
import clr
import Revit
import RevitServices
import Autodesk

clr.AddReference('ProtoGeometry')
clr.AddReference('RevitNodes')
clr.AddReference("RevitServices")
clr.AddReference("RevitAPI")
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)

from Autodesk.DesignScript.Geometry import *
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
from System.Collections.Generic import *
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.PointClouds import *

PointCloudElement = IN[0]
PlanesOfBox = IN[1]
AverageDistance = IN[2]
NumberOfPoints = IN[3]

# creates a list variable called 'planes'
Planes = []

# gets the ToPlane variable of each plane element and adds them to Planes list
for EachPlane in PlanesOfBox:
    Planes.append(UnwrapElement(EachPlane).ToPlane())

# not sure what this is doing, what exactly is Iplanes?
Iplanes = List[Autodesk.Revit.DB.Plane](Planes)

# creates the point cloud filter
PointCloudFilter =  PointCloudFilterFactory.CreateMultiPlaneFilter(planes)

# gets all filtered points
FilteredPoints = UnwrapElement(PointCloudElement).GetPoints(PointCloudFilter, AverageDistance, NumberOfPoints)

# creates a list variable called 'PointElements'
PointElements = []

# puts points in correct format? and adds them to PointElements list
for EachPoint in FilteredPoints:
    X=p.X
    Y=p.Y
    Z=p.Z
    PointElements.append(Point.Create(XYZ(X, Y, Z)).ToPrototype(False))

OUT = PointElements

I understand this is a very beginner question, but syntax has always posed a problem for me.

@matt.wna
a part of it should look like this

input_lst_PlanesOfBox = IN[1]
for PlanesOfBox in input_lst_PlanesOfBox:
    # creates a list variable called 'planes'
    Iplanes = List[Autodesk.Revit.DB.Plane]()
    for EachPlane in PlanesOfBox:
        # convert dynamo plane to Revit Plane
        rvt_plane = Plane.CreateByNormalAndOrigin(EachPlane.Normal.ToXyz(), EachPlane.Origin.ToXyz())
        Iplanes.Add(rvt_plane)
    #
    # rest of your code
    #

@c.poupin Thanks, I’m not ready for nested loop part of this yet I don’t think, but thank you though for the future hint.

I’m trying to get it working for a single object currently from scratch. The example you posted makes me think I don’t understand some basic concepts still. For example, i thought unwrapping was required. Unless manually building the planes with plane.create accomplishes the same goal. Given that I’m already feeding in the planes with the correct normal orientation makes me wonder why they need to be rebuilt programmatically. I thought that’s what unwrap did…

The Iplanes line as well i also don’t understand. Docs say that the point cloud filter factory only accepts an Ilist, but in the working version of this script, only the regular plane list is passed in. Unless the Iplanes line actually modifies the Planes list? Iplanes is never called again.

Either way thanks for the examples.

You can find explanations here

if you’re working in a .Net environment, some objects need to be converted (python_object to .NetObject ) so that they can be used with methods from an API (.Net)

I’m still unable to get any data from this list. List object doesn’t contain attribute normal. I’m apparently feeding in a class instead of an instance. Do i need to cast or something?

here is an example with a list of Dynamo Solid (Prototype) at input

import clr
import sys
import System
from System.Collections.Generic import List
clr.AddReference('ProtoGeometry')
import Autodesk.DesignScript.Geometry as DS

#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.PointClouds import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

def toList(x):
    if isinstance(x, (list, dict)) or \
            (hasattr(x, "GetType") and x.GetType().GetInterface("ICollection") is not None):
        return x
    else : return [x]
    
point_cloud = UnwrapElement(IN[0])
tf = point_cloud.GetTransform()

lst_ds_solid = toList(IN[1])
averageDistance = IN[2]
numberOfPoints= IN[3]

outPoints = []

for ds_solid in lst_ds_solid:
    Iplanes=List[DB.Plane]()
    for ds_face in ds_solid.Faces:
        ds_surface = ds_face.SurfaceGeometry()
        ds_normal_reversed = ds_surface.NormalAtParameter(0.5, 0.5).Reverse()
        ds_origin = ds_surface.PointAtParameter(0.5, 0.5)
        plane = DB.Plane.CreateByNormalAndOrigin(ds_normal_reversed.ToXyz(), ds_origin.ToXyz())
        Iplanes.Add(plane)
        
    pcf = PointCloudFilterFactory.CreateMultiPlaneFilter(Iplanes)
    points = point_cloud.GetPoints(pcf, averageDistance, numberOfPoints)
    
    outPoints.append([tf.OfPoint(XYZ(p.X, p.Y, p.Z)).ToPoint() for p in points])

OUT = outPoints

@c.poupin You rock, that did it. Sorry for the response delay. Can’t thank you enough for walking me through this sort of thing. You probably saved me easily 6 months off and on of frustration as well as countless hours of clicking on boxes. Your method is much more streamlined than the steamnode as it didn’t require any type of coordinate conversion.

Full graph below, the python node is exactly as @c.poupin posted above. I plan to try to port the normal flip part to native dynamo eventually. I’ll report back with that as hopefully it’ll help me narrow down some of the basics that I still struggle with.

Again, thanks very much for your help. I hope you know the huge impact you have on others trying to learn.

1 Like