Distributing lines along a polycurve is useful in road design — for example, to visualise curvature variations by drawing perpendicular lines of proportional length at regular intervals. The script accepts a curve (or list of curves), a list of parameter values (0–1), and one or more target lengths. The lengths are interpolated across the parameter range so each line's length grows or shrinks smoothly from start to end.
Utility Class
A small helper to detect whether a value is iterable (including .NET System.Array objects from Dynamo) and to flatten nested lists:
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import re
# Utility class — checks if a unit is iterable and flattens nested lists
class utils:
ul = []
def __init__(self):
self.ul = []
def isIterable(self, unit):
t1 = t2 = False
if isinstance(unit, (tuple, set, list, dict)):
t1 = True
if hasattr(unit, "__iter__"):
iterstr = str(unit.__iter__())
if re.search("System.Array", iterstr):
t2 = True
return t1 or t2
def UnwrapList(self, lst):
self.ul = []
if self.isIterable(lst):
self.UnwrapL(lst)
return self.ul
else:
return lst
def UnwrapL(self, unit):
if self.isIterable(unit):
for item in unit:
self.UnwrapL(item)
else:
self.ul.append(unit)
ut = utils()Core Functions
getPerpLine builds each perpendicular line using the curve tangent. composeLNF distributes lengths — given e.g. 3 target lengths and 100 parameters, it interpolates a smooth length value for every parameter:
# Returns a vector perpendicular to the curve tangent at the given parameter
def getPerpVector(curve, parameter):
TP = curve.TangentAtParameter(parameter)
VT = Vector.Rotate(TP, Vector.ZAxis(), 90)
return VT
# Creates a perpendicular line at a point on the curve with the given length
def getPerpLine(curve, parameter, length):
perpVector = getPerpVector(curve, parameter)
pointAtParam = curve.PointAtParameter(parameter)
L = Line.ByStartPointDirectionLength(pointAtParam, perpVector, length)
return L
# Interpolates line lengths across the parameter range.
# Arc_Length: list of target lengths; Arc_Parameters: list of t values (0..1).
# Returns one length per parameter, distributed proportionally.
def composeLNF(Arc_Length, Arc_Parameters):
ALU = Arc_Length
APU = Arc_Parameters
N = []
for index1 in range(len(ALU) - 1):
N.append([])
LLeft = ALU[index1]
LRight = ALU[index1 + 1]
for index2 in range(len(APU) - 1):
steper = float(index2) / (len(APU) - 1)
Dist = float(LLeft) + (float(LRight) - LLeft) * steper
N[index1].append(Dist)
N.append(ALU[len(ALU) - 1])
P = ut.UnwrapList(N)
P_L = len(P)
segment = float(P_L) / len(APU)
LE = []
for index3 in range(len(APU)):
newIndex = int(float(P_L - 1) / (len(APU) - 1) * index3)
LE.append(P[newIndex])
return LERecursive Iteration
The input can be jagged (a list of curves, each with a different parameter set). The recursive function handles variable nesting depth:
# Handles jagged (variable-depth) inputs recursively.
# Arc_Curves can be a single curve or a nested list of curves.
def returnElements(Arc_Curves, Arc_Parameters, Lengths):
if isinstance(Arc_Curves, (list, tuple)):
retList = []
for Arc_Curve in Arc_Curves:
retList.append(returnElements(Arc_Curve, Arc_Parameters, Lengths))
return retList
elif isinstance(Arc_Parameters, (list, tuple)):
retList = []
for index, Arc_Parameter in enumerate(Arc_Parameters):
try:
retList.append(getPerpLine(Arc_Curves, Arc_Parameter, Lengths[index]))
except:
retList.append(False)
return retList
else:
return getPerpLine(Arc_Curves, Arc_Parameters, Lengths)Input and Normalisation
# Read Dynamo inputs
Arc_Curves = IN[0] # curve or list of curves
Arc_Parameters = IN[1] # parameter values (0..1) at which to create lines
Arc_Length = IN[2] # one or more target lengths
# Normalise inputs
if ut.isIterable(Arc_Parameters):
Arc_Parameters = ut.UnwrapList(Arc_Parameters)
if ut.isIterable(Arc_Length):
Arc_Length = ut.UnwrapList(Arc_Length)
Lengths = composeLNF(Arc_Length, Arc_Parameters)
else:
Arc_Length = [Arc_Length]
Lengths = composeLNF(Arc_Length, Arc_Parameters)
else:
if ut.isIterable(Arc_Length):
Lengths = False
else:
Lengths = Arc_LengthOutput
# Execute and expose both the lines and the computed lengths
Result = returnElements(Arc_Curves, Arc_Parameters, Lengths)
OUT = Result, Lengths