Hello, we have a pipe model containing 3D Solids - and are looking to return the center line for each Solid (preferably while translating the layer from the Solids to the Polylines).
As we lack the data used to construct the solids, I was hoping Dynamo could help out.
I have tested a few methods to generate the lines (including a script from a similar thread a year ago) - with no luck.
The closest I’ve come to a solution is to deconstruct the objects to arcs, extracting the center points for the arcs - then attempting to combine them into lines.
However, in some cases it works on most objects, though each line is doubled.
In other cases I get single lines, but then it does not work on all objects.
Does anyone have a script that could solve this issue?
I have attached the script I’ve been working on (after some tiding up, hopefully it makes sense when you look at it).
Thank you for any help!
centerline_from_solids.dwg (4.6 MB)
UDK_GetLines.dyn (55.1 KB)
Bit of a sicky situation here - ideally you’d go about this in a different way… sadly the solids are kind of all over the place - some “pipes” actually consist of a half dozen runs, some of the rules for the layout of such don’t make sense to me (slope up, then down, then up again?), but it could be a lack of context causing confusion. What is the source of the data?
Looking at this I can see a few easy efficiency gains which will reduce the complexity and the errors you’re facing.
You start by doing some conversion from Solids to PolySurfaces to Surfaces then pull the perimeter curves and convert to basic curves with an ApproximateWithArcAndLineSegments method. The approximation is a good move, but as you’re pulling the perimeter curves of the surfaces you’re getting twice the number of shapes as you need. You’re also pulling curves which form the ‘seam’ on a cylinder, which is happening any time the solid was created as an extrusion. These will produce lines or center points which are WAY off the beaten path.
Instead I recommend looking into pulling the Edges - they are a topology property of the solid so they generate quickly, and they don’t duplicate. Next get the faces which the edge defines (again fast due to the property nature) and count the number of adjacent faces per face. Test this count for equality to 2 and filter out any edges which don’t have two faces - the excluded edges are either non-manifold (not sure ACAD permits such geometry or not) or they are the seam of a cylinder, either way they’re not something you want here.
Next grab the curve geometry from the edges, and approximate that with arcs and line segements. Then remove any lines (List.RemoveIfNot) and filter out any arcs which are gerater than the most common radius length. Next pull the center point from the remaining arcs and prune any duplicates with the desired resolution. Sort the remaining points by their parameter along the best fit line through each group, and draw a polycurve though the resulting points.
This covers your current process fairly well, though there is still going to be outlier issues as bad data in always results in bad data out.
2 Likes
Thank you for the detailed reply!
I realize the data looks weird - as I have removed any manholes in the same model (most of the pipes makes sense if you see the data with the manholes.).
However, some pipes are just offset along negative Z from the terrain, so this model should primarily just illustrate where the objects are located.
I’ll test your method during the week - sounds like it will reduce the duplicate problem, as well as to make the script snappier.
Again, I really appreciate the detailed answer - thank you!
1 Like
Played a round for a bit, and sadly I think there are going to be too many outlier data points in how the geometry is constructed to make anything ‘foolproof’. Your best bet will be to go back to the source data (any idea where it comes from?) and extract relevant info from there.
Yeah I bet - the data is all over there (the data stems from an old project that I’m looking to renew).
The dwg only contains a fracture of all the data we need to process - and unfortunately this data has been hard-converted from GIS to CAD (the original GIS data lacked Z-values as well, so that’s not an option either, unfortunately).
Then pipes were manually created using polylines and sweeps whenever their respective locations were surveyed. The remaining altitudes have been calculated based on terrain offset.
As such the attached dwg illustrates the somewhat messy data I have to work with - however, as long as I can automate the majority of the work I don’t mind doing the remaining part manually.
Actually it’s still quite helpful. You can pull the XY value and project it up onto the solid, and then generate the PolyCurve (or better still the pipe) accordingly. My gut says that in this case that method will be both faster and more reliable than trying to put Humpty Dumpty back together.
This might also help:
Animation was sped up for forum fit purposes, but it takes me about 15 seconds to process the full DWG you provide. Could be reduced further by extracting the Dynamo solid in the Python node, but keeping it platform agnostic means it has more use in other toolsets (i.e. Revit).
The Python Code
########################################
############## Properties ##############
########################################
__author__ = 'Jacob Small'
__version__ = '0.1.0'
__description__ = "Attempts to generate a polycurve centered on a pipe solid."
__DynamoBuilds__ = "2.15"
__ReleaseNotes__ = """ No error handling for U shaped pipes, which will have incorrect sort order for the parameterization. Utilizing a circle might be a better fit from a logical order, but that pesents issues with identifying the 'first and last' point in the sequence. More thought is required here."""
__Dependancies__ = "None"
__Copyright__ = "2024, Autodesk Inc."
__license__ = "Apache 3.0"
########################################
### Configure the Python environment ###
########################################
### standard imports ###
import sys #add the sys class to the Python environment so we can work with the sys objects
import clr #add the CLR (common language runtime) class to the Python environment so we can work with .net libraries
### basic Dynamo imports ###
clr.AddReference('ProtoGeometry') #add the Dynamo geometry library to the CLR
from Autodesk.DesignScript import Geometry as DG #add the Dynamo geometry class using the alias DG, to ensure no overlap between class calls in Revit or Dynamo
#########################################
###### Global variables and inputs ######
#########################################
solids = IN[0] #data from the IN[0] port of the Dynamo environment
if solids.__class__!= list: solids = [solids] #ensure the solids are a list
resolution = IN[1] #The resolution for point pruning
pipeOffset = IN[2] #the maximum offset distance a point can be from the solid
results = [] #a list to hold the results
#########################################
### Generate Polycurve by Pipe Solid ####
#########################################
for solid in solids: #for each solid
edges = solid.Edges #get the edges of the solid
edges = [e for e in edges if len(e.AdjacentFaces)==2] #remove any seam edges (edges which bound only one face)
crvs = [e.CurveGeometry for e in edges] #get the curves from the remaining edges
crvs = [c for c in crvs if c.Length > 0.05] #remove any curves less than 0.05 units in length
pnts = [] #new list to hold the points
#get the centerpoints where we can, and approximate them where we can't
for crv in crvs: #for each curve
if "CenterPoint" in dir(crv): #if the curve has a centerpoint property
pnts.append(crv.CenterPoint) #append the centerpoint to the points list
else: #otherwise
crvSet = DG.Curve.ApproximateWithArcAndLineSegments(crv) #convert the curve to a list of arcs and lines
[crvs.append(c) for c in crvSet if c.__class__ == DG.Arc] #append the arcs to the curves list
pnts = [p for p in pnts if p.DistanceTo(solid)<pipeOffset] #remove any points which are further than the offset distance from the solid
pnts = [i for i in DG.Point.PruneDuplicates(pnts,resolution)] #prune any duplicate points to uncommon ones added by the arc conversion
line = DG.Line.ByBestFitThroughPoints(pnts) #generate a line best fit though the points
params = [line.ParameterAtPoint(p) for p in pnts] #get the parameter on the line for each point
sortedParams = [i for i in params] #duplicate the list of parameters
sortedParams.sort() #sort the duplicated list
indx = [params.index(i) for i in sortedParams] #get the index of the parameter in the original list for each sorted point
pnts = [pnts[i] for i in indx] #get the point at the index for each of the sorted indexes
#pnts = DG.Point.PruneDuplicates(pnts,resolution) #prune
results.append(pc) #append the polycurve to the results list
#########################################
##### Return the results to Dynamo ######
#########################################
OUT = results #return the results to Dynamo
2 Likes
This looks very promising - thank you for still helping me out!
I am very new to Python in Dynamo - could you share the script so I can see how you fit the code into the Dynamo script?
Another note on the original lines (which you previously suggested I could use in combination with the solids); Most of them are wrong - as these cover municipal pipes, some dating back all the way to the 1800s. As such, the majority of the lines I have available are based on drawings and such.
So if the Dynamo script with the Python node can generate all the lines then that would be perfect - as I could convert the lines back into a GIS-format afterwards!
Now’s a good time to learn as I am not at my PC.
Expand the preview and you’ll see a little ‘copy’ button in the top right corner of the code preview. Hit that and switch to Dynamo. In Dynamo place a Python node, hit the plus button a few times to get all three input ports, then double click on the node to open the editor. Paste in the text from your clipboard, hit the save button in the editor and then close it. Wire in the inputs (the solids, resolution number, and pipe distance number) and you should be good to go.
1 Like
Awesome, I’ll test it when I have some spare time.
Thank you so much for explaining it on the go - doesn’t sound to difficult to put the python script to the test! Much appreciated!
1 Like
Alright so I finally had some spare time - and I realize I have more newbie questions.
So I managed to generate a list from the solids, and to create and connect the Python node (thank you so much for all the detailed comments, by the way!).
- What does resolution for object prompting mean?
- I still get an error when attempting to run the code
I’ve attached a few images, hopefully you can point me in the correct direction.
Thank you again, the help is very much appreciated!
The resolution is what I’m using to define the ‘distance’ of the Point.PruneDuplicates node. This is a requirement as the approximation can result in a list of arcs, and therefore a list of points, the center of which will vary. So by setting a ‘resolution’ we remove all poitns within a given distance but one, which becomes the ‘primary’ direction.
That error is likely due to the way you’re grabbing content by layer. If I recall I used All Objects of Type, or something similar, which meant I was providing a flat list of solids. Put a List.Flatten in between your AllObjectsOnLayer and the Python node and I think that will resolve that, but it also looks like you’re passing the Dynamo reference to the autocad object instead of the solids, so put an Object.Geometry and a List.RemoveIfNot node in after that to filter out anything which isn’t a Solid.
Ah of course, completely forgot about copying the geometry into Dynamo.
I changed the script to All Objects of Type (Solid), and it’s running well now.
However, now the python node returns the following error;
"NameError: name ‘pc’ is not defined [’ File “”, line 67, in \n’].
Did your run of the script actually return lines into the output of the Python node?
It did… but I appear to have a copy/paste error above.
Try this:
########################################
############## Properties ##############
########################################
__author__ = 'Jacob Small'
__version__ = '0.1.0'
__description__ = "Attempts to generate a polycurve centered on a pipe solid."
__DynamoBuilds__ = "2.15"
__ReleaseNotes__ = """ No error handling for U shaped pipes, which will have incorrect sort order for the parameterization. Utilizing a circle might be a better fit from a logical order, but that pesents issues with identifying the 'first and last' point in the sequence. More thought is required here."""
__Dependancies__ = "None"
__Copyright__ = "2024, Autodesk Inc."
__license__ = "Apache 3.0"
########################################
### Configure the Python environment ###
########################################
### standard imports ###
import sys #add the sys class to the Python environment so we can work with the sys objects
import clr #add the CLR (common language runtime) class to the Python environment so we can work with .net libraries
### basic Dynamo imports ###
clr.AddReference('ProtoGeometry') #add the Dynamo geometry library to the CLR
from Autodesk.DesignScript import Geometry as DG #add the Dynamo geometry class using the alias DG, to ensure no overlap between class calls in Revit or Dynamo
#########################################
###### Global variables and inputs ######
#########################################
solids = IN[0] #data from the IN[0] port of the Dynamo environment
if solids.__class__!= list: solids = [solids] #ensure the solids are a list
resolution = IN[1] #The resolution for point pruning
pipeOffset = IN[2] #the maximum offset distance a point can be from the solid
results = [] #a list to hold the results
#########################################
### Generate Polycurve by Pipe Solid ####
#########################################
for solid in solids: #for each solid
edges = solid.Edges #get the edges of the solid
edges = [e for e in edges if len(e.AdjacentFaces)==2] #remove any seam edges (edges which bound only one face)
crvs = [e.CurveGeometry for e in edges] #get the curves from the remaining edges
crvs = [c for c in crvs if c.Length > 0.05] #remove any curves less than 0.05 units in length
pnts = [] #new list to hold the points
#get the centerpoints where we can, and approximate them where we can't
for crv in crvs: #for each curve
if "CenterPoint" in dir(crv): #if the curve has a centerpoint property
pnts.append(crv.CenterPoint) #append the centerpoint to the points list
else: #otherwise
crvSet = DG.Curve.ApproximateWithArcAndLineSegments(crv) #convert the curve to a list of arcs and lines
[crvs.append(c) for c in crvSet if c.__class__ == DG.Arc] #append the arcs to the curves list
pnts = [p for p in pnts if p.DistanceTo(solid)<pipeOffset] #remove any points which are further than the offset distance from the solid
pnts = [i for i in DG.Point.PruneDuplicates(pnts,resolution)] #prune any duplicate points to uncommon ones added by the arc conversion
line = DG.Line.ByBestFitThroughPoints(pnts) #generate a line best fit though the points
params = [line.ParameterAtPoint(p) for p in pnts] #get the parameter on the line for each point
sortedParams = [i for i in params] #duplicate the list of parameters
sortedParams.sort() #sort the duplicated list
indx = [params.index(i) for i in sortedParams] #get the index of the parameter in the original list for each sorted point
pnts = [pnts[i] for i in indx] #get the point at the index for each of the sorted indexes
pnts = DG.Point.PruneDuplicates(pnts,resolution) #prune any points too close to
pc = DG.PolyCurve.ByPoints(pnts) #generate a polycurve via the remaining points
results.append(pc) #append the polycurve to the results list
#########################################
##### Return the results to Dynamo ######
#########################################
OUT = results #return the results to Dynamo
I think that contains the missing copy/paste stuff but I cannot test. If not let me know and I’llt ry to rebuild later.
It seems to attempt to work now - however it’s unable to generate points.
The new error message states that it needs two points to generate the lines.
I’ll try to fine-tune the values (IN1 and 2), to see what happens - thanks for taking your time to help me out!
1 Like