In the meantime i tested the new ChatGPT Work mode by creating a spiral stair with intermediary landings. It went very fast, and didn’t produce any errors.
import Rhino
import Rhino.Geometry as rg
import math
import traceback
# ============================================================
# INPUTS
#
# Center : Point
# Radius : Number
# TotalHeight : Number
# RiserHeight : Number
# Turns : Number
# LandingCount : Integer (0 or greater)
# StringerUp : Number
# StringerDown : Number
# PoleRadius : Number
# HandrailHeight : Number
#
# OUTPUTS
#
# Treads
# Risers
# Landings
# Underside
# StringerCurve
# Stringer
# Pole
# Handrail
# Stair
# Info
# ============================================================
Treads = []
Risers = []
Landings = []
Underside = None
StringerCurve = None
Stringer = None
Pole = None
Handrail = None
Stair = []
Info = "Script started."
if Rhino.RhinoDoc.ActiveDoc is not None:
tol = Rhino.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance
else:
tol = 0.001
# ------------------------------------------------------------
# Basic geometry
# ------------------------------------------------------------
def polar_point(center, radius, angle, elevation):
return rg.Point3d(
center.X + math.cos(angle) * radius,
center.Y + math.sin(angle) * radius,
elevation
)
def create_arc(
center,
radius,
start_angle,
sweep_angle,
elevation
):
start_point = polar_point(
center,
radius,
start_angle,
elevation
)
middle_point = polar_point(
center,
radius,
start_angle + sweep_angle * 0.5,
elevation
)
end_point = polar_point(
center,
radius,
start_angle + sweep_angle,
elevation
)
arc = rg.Arc(
start_point,
middle_point,
end_point
)
if not arc.IsValid:
return None
return arc.ToNurbsCurve()
# ------------------------------------------------------------
# Horizontal tread
# ------------------------------------------------------------
def create_tread(
center,
radius,
start_angle,
sweep_angle,
elevation,
tolerance
):
center_point = rg.Point3d(
center.X,
center.Y,
elevation
)
outer_start = polar_point(
center,
radius,
start_angle,
elevation
)
outer_end = polar_point(
center,
radius,
start_angle + sweep_angle,
elevation
)
outer_arc = create_arc(
center,
radius,
start_angle,
sweep_angle,
elevation
)
if outer_arc is None:
return None
start_side = rg.LineCurve(
center_point,
outer_start
)
end_side = rg.LineCurve(
outer_end,
center_point
)
joined = rg.Curve.JoinCurves(
[
start_side,
outer_arc,
end_side
],
tolerance
)
if not joined:
return None
planar_breps = rg.Brep.CreatePlanarBreps(
joined[0],
tolerance
)
if not planar_breps:
return None
return planar_breps[0]
# ------------------------------------------------------------
# Vertical radial riser
# ------------------------------------------------------------
def create_riser(
center,
radius,
angle,
lower_elevation,
upper_elevation,
tolerance
):
center_bottom = rg.Point3d(
center.X,
center.Y,
lower_elevation
)
outer_bottom = polar_point(
center,
radius,
angle,
lower_elevation
)
outer_top = polar_point(
center,
radius,
angle,
upper_elevation
)
center_top = rg.Point3d(
center.X,
center.Y,
upper_elevation
)
return rg.Brep.CreateFromCornerPoints(
center_bottom,
outer_bottom,
outer_top,
center_top,
tolerance
)
# ------------------------------------------------------------
# Riser count
# ------------------------------------------------------------
def calculate_riser_count(
total_height,
requested_riser_height,
landing_count
):
height_sections = (
landing_count + 1
)
approximate_count = max(
height_sections,
int(
round(
total_height /
requested_riser_height
)
)
)
candidates = []
for count in range(
max(height_sections, approximate_count - 50),
approximate_count + 51
):
if count % height_sections != 0:
continue
actual_height = (
total_height /
float(count)
)
difference = abs(
actual_height -
requested_riser_height
)
candidates.append(
(
difference,
count
)
)
if not candidates:
return height_sections
candidates.sort(
key=lambda item: item[0]
)
return candidates[0][1]
# ------------------------------------------------------------
# Smooth helical curve
# ------------------------------------------------------------
def create_helix_curve(
center,
radius,
total_height,
total_rotation,
vertical_offset=0.0
):
number_of_turns = abs(
total_rotation /
(2.0 * math.pi)
)
sample_count = max(
64,
int(
math.ceil(
number_of_turns * 120.0
)
)
)
points = []
for index in range(sample_count + 1):
factor = (
float(index) /
float(sample_count)
)
angle = (
total_rotation *
factor
)
elevation = (
center.Z +
total_height * factor +
vertical_offset
)
points.append(
polar_point(
center,
radius,
angle,
elevation
)
)
return rg.Curve.CreateInterpolatedCurve(
points,
3
)
# ------------------------------------------------------------
# Exterior stringer
# ------------------------------------------------------------
def create_stringer_surface(
reference_curve,
stringer_up,
stringer_down
):
if reference_curve is None:
return None
upper_curve = reference_curve.DuplicateCurve()
lower_curve = reference_curve.DuplicateCurve()
upper_curve.Transform(
rg.Transform.Translation(
0.0,
0.0,
stringer_up
)
)
lower_curve.Transform(
rg.Transform.Translation(
0.0,
0.0,
-stringer_down
)
)
lofts = rg.Brep.CreateFromLoft(
[
upper_curve,
lower_curve
],
rg.Point3d.Unset,
rg.Point3d.Unset,
rg.LoftType.Straight,
False
)
if not lofts:
return None
return lofts[0]
# ------------------------------------------------------------
# Continuous underside
# ------------------------------------------------------------
def create_underside_surface(
center,
inner_radius,
outer_radius,
total_height,
total_rotation,
vertical_offset
):
"""
Creates one smooth helical soffit extending from the
central pole to the exterior stringer.
"""
inner_curve = create_helix_curve(
center,
inner_radius,
total_height,
total_rotation,
vertical_offset
)
outer_curve = create_helix_curve(
center,
outer_radius,
total_height,
total_rotation,
vertical_offset
)
if inner_curve is None or outer_curve is None:
return None
lofts = rg.Brep.CreateFromLoft(
[
inner_curve,
outer_curve
],
rg.Point3d.Unset,
rg.Point3d.Unset,
rg.LoftType.Straight,
False
)
if not lofts:
return None
return lofts[0]
# ------------------------------------------------------------
# Central pole
# ------------------------------------------------------------
def create_pole(
center,
pole_radius,
total_height
):
base_plane = rg.Plane(
rg.Point3d(
center.X,
center.Y,
center.Z
),
rg.Vector3d.ZAxis
)
circle = rg.Circle(
base_plane,
pole_radius
)
cylinder = rg.Cylinder(
circle,
total_height
)
if not cylinder.IsValid:
return None
return cylinder.ToBrep(
True,
True
)
# ============================================================
# MAIN
# ============================================================
try:
# --------------------------------------------------------
# Check inputs
# --------------------------------------------------------
required_inputs = [
("Center", Center),
("Radius", Radius),
("TotalHeight", TotalHeight),
("RiserHeight", RiserHeight),
("Turns", Turns),
("LandingCount", LandingCount),
("StringerUp", StringerUp),
("StringerDown", StringerDown),
("PoleRadius", PoleRadius),
("HandrailHeight", HandrailHeight)
]
for input_name, input_value in required_inputs:
if input_value is None:
raise ValueError(
input_name + " is missing."
)
# --------------------------------------------------------
# Convert inputs
# --------------------------------------------------------
center = rg.Point3d(Center)
radius = float(Radius)
total_height = float(TotalHeight)
requested_riser_height = float(RiserHeight)
turns = float(Turns)
landing_count = int(LandingCount)
stringer_up = float(StringerUp)
stringer_down = float(StringerDown)
pole_radius = float(PoleRadius)
handrail_height = float(HandrailHeight)
# --------------------------------------------------------
# Validate inputs
# --------------------------------------------------------
if radius <= tol:
raise ValueError(
"Radius must be greater than zero."
)
if total_height <= tol:
raise ValueError(
"TotalHeight must be greater than zero."
)
if requested_riser_height <= tol:
raise ValueError(
"RiserHeight must be greater than zero."
)
if turns <= 0:
raise ValueError(
"Turns must be greater than zero."
)
if landing_count < 0:
raise ValueError(
"LandingCount cannot be negative."
)
if stringer_up < 0:
raise ValueError(
"StringerUp cannot be negative."
)
if stringer_down < 0:
raise ValueError(
"StringerDown cannot be negative."
)
if stringer_up + stringer_down <= tol:
raise ValueError(
"StringerUp and StringerDown cannot both be zero."
)
if pole_radius <= tol:
raise ValueError(
"PoleRadius must be greater than zero."
)
if pole_radius >= radius:
raise ValueError(
"PoleRadius must be smaller than Radius so the "
"underside has a valid radial width."
)
if handrail_height <= tol:
raise ValueError(
"HandrailHeight must be greater than zero."
)
# --------------------------------------------------------
# Stair dimensions
# --------------------------------------------------------
total_rotation = (
turns *
2.0 *
math.pi
)
landing_sweep = math.radians(90.0)
total_landing_rotation = (
landing_count *
landing_sweep
)
if total_rotation <= total_landing_rotation:
raise ValueError(
"Total rotation must be greater than the combined "
"rotation of all landings."
)
riser_count = calculate_riser_count(
total_height,
requested_riser_height,
landing_count
)
actual_riser_height = (
total_height /
float(riser_count)
)
height_sections = (
landing_count + 1
)
steps_per_section = (
riser_count //
height_sections
)
landing_indices = []
for landing_number in range(
1,
landing_count + 1
):
landing_indices.append(
(
landing_number *
steps_per_section
) - 1
)
ordinary_tread_count = (
riser_count -
landing_count
)
ordinary_tread_sweep = (
total_rotation -
total_landing_rotation
) / float(ordinary_tread_count)
current_angle = 0.0
stair_parts = []
# --------------------------------------------------------
# Generate treads and risers
# --------------------------------------------------------
for step_index in range(riser_count):
lower_elevation = (
center.Z +
step_index *
actual_riser_height
)
tread_elevation = (
center.Z +
(step_index + 1) *
actual_riser_height
)
is_landing = (
step_index in landing_indices
)
if is_landing:
tread_sweep = landing_sweep
else:
tread_sweep = ordinary_tread_sweep
# Front riser
riser = create_riser(
center,
radius,
current_angle,
lower_elevation,
tread_elevation,
tol
)
if riser is not None:
Risers.append(riser)
stair_parts.append(riser)
# Horizontal tread
tread = create_tread(
center,
radius,
current_angle,
tread_sweep,
tread_elevation,
tol
)
if tread is not None:
Treads.append(tread)
stair_parts.append(tread)
if is_landing:
Landings.append(tread)
current_angle += tread_sweep
# --------------------------------------------------------
# Join staircase
# --------------------------------------------------------
if stair_parts:
joined_stair = rg.Brep.JoinBreps(
stair_parts,
tol
)
if joined_stair:
Stair = list(joined_stair)
else:
Stair = stair_parts
# --------------------------------------------------------
# Smooth stringer
# --------------------------------------------------------
StringerCurve = create_helix_curve(
center,
radius,
total_height,
total_rotation,
0.0
)
if StringerCurve is None:
raise RuntimeError(
"Failed to create StringerCurve."
)
Stringer = create_stringer_surface(
StringerCurve,
stringer_up,
stringer_down
)
if Stringer is None:
raise RuntimeError(
"Failed to create Stringer."
)
# --------------------------------------------------------
# Continuous underside
# --------------------------------------------------------
Underside = create_underside_surface(
center,
pole_radius,
radius,
total_height,
total_rotation,
-stringer_down
)
if Underside is None:
raise RuntimeError(
"Failed to create Underside."
)
# --------------------------------------------------------
# Central pole
# --------------------------------------------------------
Pole = create_pole(
center,
pole_radius,
total_height
)
if Pole is None:
raise RuntimeError(
"Failed to create Pole."
)
# --------------------------------------------------------
# Handrail
# --------------------------------------------------------
Handrail = create_helix_curve(
center,
radius,
total_height,
total_rotation,
handrail_height
)
if Handrail is None:
raise RuntimeError(
"Failed to create Handrail."
)
# --------------------------------------------------------
# Information
# --------------------------------------------------------
landing_heights = []
for landing_number in range(
1,
landing_count + 1
):
landing_height = (
center.Z +
total_height *
float(landing_number) /
float(landing_count + 1)
)
landing_heights.append(
"{0:.3f}".format(
landing_height
)
)
Info = (
"Generation completed.\n"
"Treads: {0}\n"
"Risers: {1}\n"
"Landings: {2}\n"
"Landing elevations: {3}\n"
"Riser count: {4}\n"
"Requested riser height: {5:.3f}\n"
"Actual riser height: {6:.3f}\n"
"Continuous underside: PoleRadius to Radius\n"
"Underside offset: {7:.3f}\n"
"Stringer up: {8:.3f}\n"
"Stringer down: {9:.3f}\n"
"Pole radius: {10:.3f}\n"
"Handrail height: {11:.3f}\n"
"Total rotation: {12:.3f} turns"
).format(
len(Treads),
len(Risers),
len(Landings),
", ".join(landing_heights),
riser_count,
requested_riser_height,
actual_riser_height,
-stringer_down,
stringer_up,
stringer_down,
pole_radius,
handrail_height,
turns
)
except Exception:
Info = (
"ERROR:\n" +
traceback.format_exc()
)