It did a pretty good job considering all the parameters. The script went trough a few iterations which took around 1 hour of work.
It’s impressive how well it works for architectural elements such as this one.
import Rhino.Geometry as rg
import math
# Inputs:
# Center : Point3d
# Radius : float
# PoleRadius : float
# TotalHeight : float
# Turns : float
# RiserHeight : float
# StringerHeight : float
# HandrailHeight : float
# HandrailRadius : float
# PostRadius : float
# Outputs:
# Steps
# Pole
# Stringer
# Handrail
# Posts
# Debug
Steps = []
Posts = []
Pole = None
Stringer = None
Handrail = None
Debug = []
tol = 0.01
clearance = Radius * 0.001
if Center is None:
Center = rg.Point3d.Origin
if Radius is None or Radius <= 0:
Debug.append("Radius must be larger than 0.")
elif PoleRadius is None or PoleRadius <= 0:
Debug.append("PoleRadius must be larger than 0.")
elif TotalHeight is None or TotalHeight <= 0:
Debug.append("TotalHeight must be larger than 0.")
elif Turns is None or Turns <= 0:
Debug.append("Turns must be larger than 0.")
elif RiserHeight is None or RiserHeight <= 0:
Debug.append("RiserHeight must be larger than 0.")
elif StringerHeight is None or StringerHeight <= 0:
Debug.append("StringerHeight must be larger than 0.")
elif HandrailHeight is None or HandrailHeight <= 0:
Debug.append("HandrailHeight must be larger than 0.")
elif HandrailRadius is None or HandrailRadius <= 0:
Debug.append("HandrailRadius must be larger than 0.")
elif PostRadius is None or PostRadius <= 0:
Debug.append("PostRadius must be larger than 0.")
elif PoleRadius + clearance >= Radius:
Debug.append("PoleRadius plus clearance must be smaller than Radius.")
else:
step_count = int(math.ceil(TotalHeight / RiserHeight))
actual_riser = TotalHeight / step_count
total_angle = Turns * 2.0 * math.pi
angle_step = total_angle / step_count
outer_radius = Radius
inner_radius = PoleRadius + clearance
step_thickness = actual_riser * 0.15
handrail_path_radius = outer_radius
Debug.append("Step count: {}".format(step_count))
Debug.append("Actual riser height: {:.2f}".format(actual_riser))
Debug.append("Inner radius: {:.2f}".format(inner_radius))
Debug.append("Outer radius: {:.2f}".format(outer_radius))
# ------------------------------------------------------------
# STEPS + POSTS
# ------------------------------------------------------------
for i in range(step_count):
z = Center.Z + i * actual_riser
a0 = i * angle_step
a1 = (i + 1) * angle_step
amid = (a0 + a1) * 0.5
p0 = rg.Point3d(Center.X + inner_radius * math.cos(a0), Center.Y + inner_radius * math.sin(a0), z)
p1 = rg.Point3d(Center.X + outer_radius * math.cos(a0), Center.Y + outer_radius * math.sin(a0), z)
p2 = rg.Point3d(Center.X + outer_radius * math.cos(a1), Center.Y + outer_radius * math.sin(a1), z)
p3 = rg.Point3d(Center.X + inner_radius * math.cos(a1), Center.Y + inner_radius * math.sin(a1), z)
crv = rg.Polyline([p0, p1, p2, p3, p0]).ToNurbsCurve()
breps = rg.Brep.CreatePlanarBreps(crv)
if breps and len(breps) > 0:
tread = breps[0]
path = rg.LineCurve(p0, p0 + rg.Vector3d(0, 0, -step_thickness))
step = tread.Faces[0].CreateExtrusion(path, True)
if step:
Steps.append(step)
px = Center.X + handrail_path_radius * math.cos(amid)
py = Center.Y + handrail_path_radius * math.sin(amid)
base_pt = rg.Point3d(px, py, z)
post_height = HandrailHeight + (actual_riser * 0.5) + HandrailRadius
post_circle = rg.Circle(rg.Plane(base_pt, rg.Vector3d.ZAxis), PostRadius)
post_surface = rg.Surface.CreateExtrusion(
post_circle.ToNurbsCurve(),
rg.Vector3d(0, 0, post_height)
)
if post_surface:
post = post_surface.ToBrep()
post = post.CapPlanarHoles(tol)
Posts.append(post)
# ------------------------------------------------------------
# CENTRAL POLE
# ------------------------------------------------------------
pole_circle = rg.Circle(
rg.Plane(rg.Point3d(Center.X, Center.Y, Center.Z), rg.Vector3d.ZAxis),
PoleRadius
)
pole_surface = rg.Surface.CreateExtrusion(
pole_circle.ToNurbsCurve(),
rg.Vector3d(0, 0, TotalHeight)
)
if pole_surface:
Pole = pole_surface.ToBrep()
Pole = Pole.CapPlanarHoles(tol)
# ------------------------------------------------------------
# EXTERIOR STRINGER
# ------------------------------------------------------------
top_pts = []
bottom_pts = []
samples = max(step_count * 8, 64)
for j in range(samples + 1):
t = float(j) / float(samples)
angle = t * total_angle
z_top = Center.Z + t * TotalHeight
z_bottom = z_top - StringerHeight
x = Center.X + outer_radius * math.cos(angle)
y = Center.Y + outer_radius * math.sin(angle)
top_pts.append(rg.Point3d(x, y, z_top))
bottom_pts.append(rg.Point3d(x, y, z_bottom))
top_crv = rg.Curve.CreateInterpolatedCurve(top_pts, 3)
bottom_crv = rg.Curve.CreateInterpolatedCurve(bottom_pts, 3)
if top_crv and bottom_crv:
lofts = rg.Brep.CreateFromLoft(
[top_crv, bottom_crv],
rg.Point3d.Unset,
rg.Point3d.Unset,
rg.LoftType.Normal,
False
)
if lofts and len(lofts) > 0:
Stringer = lofts[0]
# ------------------------------------------------------------
# HANDRAIL
# ------------------------------------------------------------
rail_pts = []
for j in range(samples + 1):
t = float(j) / float(samples)
angle = t * total_angle
z = Center.Z + t * TotalHeight + HandrailHeight
x = Center.X + handrail_path_radius * math.cos(angle)
y = Center.Y + handrail_path_radius * math.sin(angle)
rail_pts.append(rg.Point3d(x, y, z))
rail_crv = rg.Curve.CreateInterpolatedCurve(rail_pts, 3)
if rail_crv:
pipe = rg.Brep.CreatePipe(
rail_crv,
HandrailRadius,
False,
rg.PipeCapMode.Round,
True,
tol,
tol
)
if pipe and len(pipe) > 0:
Handrail = pipe[0]