I asked ChatGPT to draw spiral stairs with Grasshopper

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]

Probably stole parts of my GH definition from more than 10 years ago…

@Helvetosaur :laughing: could be

But most likely it just read the API docs.

now ask it to create a:

  • winding double-u stair, with
  • minimum 3" inside tread length, with
  • closed stringer, with
  • sliders that adjust the top and bottom tread widths, with
  • custom handrail profile from to-scale referenced curve in rhino, with
  • gooseneck transition on interior handrail

i uhh… need it for a friend… :wink:

@BTH , somethig like this?

import Rhino.Geometry as rg
import Rhino
import math

# Inputs:
# OuterU
# InnerU
# MaxHeight
# RiserHeight
# StringerHeight
# HandrailHeight
# BalusterGap

# Outputs:
# Treads
# Risers
# RiserLines
# InnerStringer
# OuterStringer
# Stringers
# InnerHandrail
# OuterHandrail
# Handrails
# Balusters
# BalusterSpaced
# Stair
# Info

Treads = []
Risers = []
RiserLines = []
InnerStringer = []
OuterStringer = []
Stringers = []
InnerHandrail = None
OuterHandrail = None
Handrails = []
Balusters = []
BalusterSpaced = []
Stair = []
Info = ""

tol = Rhino.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance if Rhino.RhinoDoc.ActiveDoc else 0.001


def pt_and_t_at_factor(crv, f):
    f = max(0.0, min(1.0, f))
    ok, t = crv.LengthParameter(crv.GetLength() * f)
    if not ok:
        t = crv.Domain.ParameterAt(f)
    return crv.PointAt(t), t


def make_planar(points):
    clean = []
    for p in points:
        if len(clean) == 0 or clean[-1].DistanceTo(p) > tol:
            clean.append(p)

    if len(clean) < 3:
        return None

    if clean[0].DistanceTo(clean[-1]) > tol:
        clean.append(clean[0])

    breps = rg.Brep.CreatePlanarBreps(rg.Polyline(clean).ToNurbsCurve(), tol)
    if breps and len(breps) > 0:
        return breps[0]

    return None


def lerp_angle(a, b, t):
    diff = b - a
    while diff > math.pi:
        diff -= 2.0 * math.pi
    while diff < -math.pi:
        diff += 2.0 * math.pi
    return a + diff * t


def intersect_ray_with_curve(origin, direction, crv):
    direction.Unitize()

    bbox = crv.GetBoundingBox(True)
    diag = bbox.Diagonal.Length
    if diag <= 0:
        diag = 10000

    ray = rg.Line(origin, origin + direction * diag * 10.0)
    ray_crv = rg.LineCurve(ray)

    events = rg.Intersect.Intersection.CurveCurve(ray_crv, crv, tol, tol)

    if events is None or events.Count == 0:
        return None, None

    best_pt = None
    best_t = None
    best_dist = None

    for e in events:
        pt = e.PointA
        v = pt - origin

        if v * direction <= 0:
            continue

        d = origin.DistanceTo(pt)

        if best_dist is None or d < best_dist:
            best_dist = d
            best_pt = pt
            best_t = e.ParameterB

    return best_pt, best_t


def curve_segment_points(crv, t0, t1, z, reverse_result=False):
    pts = []

    reverse_param = False
    if t1 < t0:
        t0, t1 = t1, t0
        reverse_param = True

    pts.append(crv.PointAt(t0))

    ok, pl = crv.TryGetPolyline()
    if ok:
        mids = []
        for p in pl:
            ok_cp, t = crv.ClosestPoint(p)
            if ok_cp and t > t0 + tol and t < t1 - tol:
                mids.append((t, p))

        mids.sort(key=lambda x: x[0])

        for t, p in mids:
            pts.append(p)
    else:
        samples = 8
        for k in range(1, samples):
            a = float(k) / float(samples)
            t = t0 + (t1 - t0) * a
            pts.append(crv.PointAt(t))

    pts.append(crv.PointAt(t1))

    if reverse_param:
        pts.reverse()

    if reverse_result:
        pts.reverse()

    return [rg.Point3d(p.X, p.Y, z) for p in pts]


def curve_segment_points_sloped(crv, t0, t1, z0, z1, reverse_result=False):
    raw = []

    reverse_param = False
    if t1 < t0:
        t0, t1 = t1, t0
        z0, z1 = z1, z0
        reverse_param = True

    raw.append((t0, crv.PointAt(t0)))

    ok, pl = crv.TryGetPolyline()
    if ok:
        mids = []
        for p in pl:
            ok_cp, t = crv.ClosestPoint(p)
            if ok_cp and t > t0 + tol and t < t1 - tol:
                mids.append((t, p))

        mids.sort(key=lambda x: x[0])

        for t, p in mids:
            raw.append((t, p))
    else:
        samples = 8
        for k in range(1, samples):
            a = float(k) / float(samples)
            t = t0 + (t1 - t0) * a
            raw.append((t, crv.PointAt(t)))

    raw.append((t1, crv.PointAt(t1)))

    if reverse_param:
        raw.reverse()

    pts = []

    for t, p in raw:
        if abs(t1 - t0) <= tol:
            a = 0.0
        else:
            a = (t - t0) / (t1 - t0)

        z = z0 + (z1 - z0) * a
        pts.append(rg.Point3d(p.X, p.Y, z))

    if reverse_result:
        pts.reverse()

    return pts


def make_stringer_straight_cut(crv, params, height, actual_riser):
    panels = []

    if height <= 0:
        return panels

    if params is None or len(params) < 2:
        return panels

    for i in range(len(params) - 1):
        z0 = actual_riser * i
        z1 = actual_riser * (i + 1)

        top_pts = curve_segment_points_sloped(crv, params[i], params[i + 1], z0, z1)

        for j in range(len(top_pts) - 1):
            a = top_pts[j]
            b = top_pts[j + 1]

            a_low = rg.Point3d(a.X, a.Y, a.Z - height)
            b_low = rg.Point3d(b.X, b.Y, b.Z - height)

            panel = make_planar([a, b, b_low, a_low])
            if panel:
                panels.append(panel)

    return panels


def make_handrail_polyline_and_balusters(crv, params, actual_riser, handrail_height):
    rail_pts = []
    baluster_lines = []

    if handrail_height <= 0:
        return None, baluster_lines

    if params is None or len(params) < 2:
        return None, baluster_lines

    for i in range(len(params) - 1):
        z0 = actual_riser * i
        z1 = actual_riser * (i + 1)

        base_pts = curve_segment_points_sloped(
            crv,
            params[i],
            params[i + 1],
            z0,
            z1,
            reverse_result=False
        )

        for p in base_pts:
            if len(rail_pts) == 0 or rail_pts[-1].DistanceTo(p) > tol:
                rail_pts.append(
                    rg.Point3d(
                        p.X,
                        p.Y,
                        p.Z + handrail_height
                    )
                )

    for i in range(len(params)):
        p = crv.PointAt(params[i])
        z = actual_riser * i

        base = rg.Point3d(p.X, p.Y, z)
        rail_pt = rg.Point3d(p.X, p.Y, z + handrail_height)

        baluster_lines.append(rg.LineCurve(base, rail_pt))

    handrail = None
    if len(rail_pts) > 1:
        handrail = rg.PolylineCurve(rail_pts)

    return handrail, baluster_lines


def make_spaced_balusters(crv, params, actual_riser, handrail_height, gap):
    lines = []

    if handrail_height <= 0:
        return lines

    if gap is None or gap <= 0:
        return lines

    if params is None or len(params) < 2:
        return lines

    path_pts = []

    for i in range(len(params) - 1):
        z0 = actual_riser * i
        z1 = actual_riser * (i + 1)

        seg_pts = curve_segment_points_sloped(
            crv,
            params[i],
            params[i + 1],
            z0,
            z1,
            reverse_result=False
        )

        for p in seg_pts:
            if len(path_pts) == 0 or path_pts[-1].DistanceTo(p) > tol:
                path_pts.append(p)

    if len(path_pts) < 2:
        return lines

    path = rg.PolylineCurve(path_pts)
    length = path.GetLength()

    if length <= tol:
        return lines

    count = int(math.floor(length / gap))

    distances = [0.0]

    for i in range(1, count + 1):
        d = gap * i
        if d < length - tol:
            distances.append(d)

    distances.append(length)

    for d in distances:
        ok, t = path.LengthParameter(d)
        if not ok:
            continue

        base = path.PointAt(t)
        top = rg.Point3d(base.X, base.Y, base.Z + handrail_height)

        lines.append(rg.LineCurve(base, top))

    return lines


if OuterU is None or InnerU is None:
    Info = "OuterU and InnerU are required."

elif MaxHeight <= 0 or RiserHeight <= 0:
    Info = "MaxHeight and RiserHeight must be positive."

else:
    if StringerHeight is None:
        StringerHeight = 0.0

    if HandrailHeight is None:
        HandrailHeight = 0.0

    if BalusterGap is None:
        BalusterGap = 100.0

    step_count = int(math.ceil(float(MaxHeight) / float(RiserHeight)))
    actual_riser = float(MaxHeight) / step_count

    straight_count = int(round(step_count * 0.25))
    wind_start = straight_count
    wind_end = step_count - straight_count

    start_inner, start_inner_t = pt_and_t_at_factor(InnerU, 0.0)
    start_outer, start_outer_t = pt_and_t_at_factor(OuterU, 0.0)

    end_inner, end_inner_t = pt_and_t_at_factor(InnerU, 1.0)
    end_outer, end_outer_t = pt_and_t_at_factor(OuterU, 1.0)

    start_vec = start_outer - start_inner
    end_vec = end_outer - end_inner

    start_angle = math.atan2(start_vec.Y, start_vec.X)
    end_angle = math.atan2(end_vec.Y, end_vec.X)

    InnerPts = []
    OuterPts = []
    InnerParams = []
    OuterParams = []

    for i in range(step_count + 1):
        f = float(i) / float(step_count)
        inner_pt, inner_t = pt_and_t_at_factor(InnerU, f)

        if i <= wind_start:
            angle = start_angle
        elif i >= wind_end:
            angle = end_angle
        else:
            local = float(i - wind_start) / float(wind_end - wind_start)
            smooth = 0.5 - 0.5 * math.cos(local * math.pi)
            angle = lerp_angle(start_angle, end_angle, smooth)

        dir_vec = rg.Vector3d(math.cos(angle), math.sin(angle), 0)
        dir_vec.Unitize()

        outer_pt, outer_t = intersect_ray_with_curve(inner_pt, dir_vec, OuterU)

        if outer_pt is None:
            outer_pt, outer_t = pt_and_t_at_factor(OuterU, f)

        InnerPts.append(inner_pt)
        OuterPts.append(outer_pt)
        InnerParams.append(inner_t)
        OuterParams.append(outer_t)

        RiserLines.append(
            rg.LineCurve(
                rg.Point3d(inner_pt.X, inner_pt.Y, 0),
                rg.Point3d(outer_pt.X, outer_pt.Y, 0)
            )
        )

    for i in range(1, step_count + 1):
        inner_pt = InnerPts[i]
        outer_pt = OuterPts[i]

        z0 = actual_riser * (i - 1)
        z1 = actual_riser * i

        riser = make_planar([
            rg.Point3d(inner_pt.X, inner_pt.Y, z0),
            rg.Point3d(outer_pt.X, outer_pt.Y, z0),
            rg.Point3d(outer_pt.X, outer_pt.Y, z1),
            rg.Point3d(inner_pt.X, inner_pt.Y, z1)
        ])

        if riser:
            Risers.append(riser)

    for i in range(step_count):
        z = actual_riser * i

        inner_seg = curve_segment_points(
            InnerU,
            InnerParams[i],
            InnerParams[i + 1],
            z,
            reverse_result=True
        )

        outer_seg = curve_segment_points(
            OuterU,
            OuterParams[i],
            OuterParams[i + 1],
            z,
            reverse_result=False
        )

        boundary = []

        boundary.append(rg.Point3d(InnerPts[i].X, InnerPts[i].Y, z))
        boundary.append(rg.Point3d(OuterPts[i].X, OuterPts[i].Y, z))

        for p in outer_seg:
            boundary.append(p)

        boundary.append(rg.Point3d(InnerPts[i + 1].X, InnerPts[i + 1].Y, z))

        for p in inner_seg:
            boundary.append(p)

        tread = make_planar(boundary)
        if tread:
            Treads.append(tread)

    OuterStringer = make_stringer_straight_cut(
        OuterU,
        OuterParams,
        StringerHeight,
        actual_riser
    )

    InnerStringer = make_stringer_straight_cut(
        InnerU,
        InnerParams,
        StringerHeight,
        actual_riser
    )

    OuterHandrail, outer_balusters = make_handrail_polyline_and_balusters(
        OuterU,
        OuterParams,
        actual_riser,
        HandrailHeight
    )

    InnerHandrail, inner_balusters = make_handrail_polyline_and_balusters(
        InnerU,
        InnerParams,
        actual_riser,
        HandrailHeight
    )

    if InnerHandrail:
        Handrails.append(InnerHandrail)

    if OuterHandrail:
        Handrails.append(OuterHandrail)

    inner_balusters_spaced = make_spaced_balusters(
        InnerU,
        InnerParams,
        actual_riser,
        HandrailHeight,
        BalusterGap
    )

    outer_balusters_spaced = make_spaced_balusters(
        OuterU,
        OuterParams,
        actual_riser,
        HandrailHeight,
        BalusterGap
    )

    Balusters = inner_balusters + outer_balusters
    BalusterSpaced = inner_balusters_spaced + outer_balusters_spaced

    Stringers = InnerStringer + OuterStringer
    Stair = Treads + Risers + Stringers + Handrails + Balusters + BalusterSpaced

    Info = "Steps: {0}, actual riser: {1:.2f}, stringer height: {2:.2f}, handrail height: {3:.2f}, baluster gap: {4:.2f}".format(
        step_count,
        actual_riser,
        StringerHeight,
        HandrailHeight,
        BalusterGap
    )
  • winding double-u stair, with :white_check_mark: (meant to first write “U-shaped”, not “double-U shaped”)
  • minimum 3" inside tread length, with :cross_mark: (0:15 shows a tread that looks less than 3" deep)
  • closed stringer, with :white_check_mark:
  • sliders that adjust the top and bottom tread widths, with :white_check_mark: / :cross_mark: (there are no sliders for the widths, but i guess the outline curves could be used)
  • custom handrail profile from to-scale referenced curve in rhino, with :cross_mark: (doesn’t look to be part of the component
  • gooseneck transition on interior handrail :cross_mark: (was kinda expecting chatgpt to find a way to create a transition like the photo below)

Forgive my ignorance on the subject, but I have a question:
How did you run the script inside GH? And did the code create the tool shown in the video?

Thanks!

Kind of defeats the point of studying design, no? What does the office of the future look like - begging AI “not to make mistakes”? I’m curious, because you say it’s impressive - and maybe it is, but isn’t it kind of meaningless? You get results, but are you enjoying the process - do you find it stimulating and engaging watching an “AI” solve problems?

@leandro.arquivos3d you would get an answer faster by using google or the Search on this very website

@leandro.arquivos3d , to answer your question:

  1. Place a Python grasshopper component on the canvas.
  2. Copy-paste the script from ChatGPT inside the component.
  3. Add the to the component the inputs and the outputs with proper Type Hints as ChatGPT writes them at the beginning.

@Artstep , i totally enjoy the process. It simplifies grasshopper work. I can build libraries of architectural elements and i can focus more on the spaces, volumes and their aesthetical qualities and functionality rather than doing grasshopper definitions.
The more i work with AI the faster the process becomes. I become better with prompts and with code structure.

@BTH , I am still thinking about that gooseneck (not a detail i regularly use, but interesting) I think this is a sub-element of the stair and it might need another script with proper inputs such as the position of the rails in 3D. I would not expect ChatGPT to integrate it in the main stair component because it is already too complicated. Usually i split complex problems into parts.

Fair enough - seems to me you’re using AI to ameliorate the absence of a team and time - the weavers could work “faster” after automation as well, shame so many of them ended up starving for it though - reading what you said definitely made me wonder whether faster is what you want, if you slowed down, what would you see?

Nah, actually you are very wrong to think so.

I automate some tasks in order to get other tasks. I have more free time because what took one day, now takes few hours. With that free time i can study more or expand into other kinds of work so i get more variety.

So much free time and yet you don’t have enough to learn scripting yourself :wink:

But I know scripting. I use AI to learn more!

I don’t think this is “learning” - if anything, you’re teaching OpenAI how to do your job better and faster than you:

If you were learning, then you would write that staircase script yourself in 20 minutes, or find the one that was already posted here to learn from - really, step out of it for a second - you are impressed that ChatGPT managed to do a spiral and a linear array in 1 hour after multiple failed attempts? I wish you could see how amusing that is.

Nobody asked, and it’s completely meta, but - if you enjoy design why would you use AI to avoid doing it? You seem to imply that by NOT learning the tools you need, you’re somehow “saving time”? No, you’re just picking up debt. This is a script that I’m sure is entirely within your ability to write - it’s a great script for studying in fact, would highly recommend to beginners - but you’re saying you need help writing it? You don’t think that has anything to do with the fact that you’re not writing them yourself? Essentially you’re saying that you don’t have time to live the life that you have chosen for yourself, what use is the free time then? (And that’s ignoring of what happens when you need to adjust it by yourself or if someone needs you to explain what the code that you gave them does…)

It just seems so bizarre that you would come to a forum and say “Look what AI can do”… a few simple solids? You know what, I’d like to see what YOU can do even if it is a few simple solids, why should we care about ChatGPT struggling with simple operations? (the code is pretty inefficient even if it does have verbose error-checking at the start that would be a pain to type out) Especially since apparently you have all this free time coming out of your ears…

Whether this was brought in by a colleague, an employee, a student, a friend… like what can you say about this? “Wow, that’s such a cool prompt dude - so rad how ChatGPT made that for you” - how exhilarating, the future of design looks riveting… christ, just give us the rope now… don’t drag it out

Just reminds me of the Voronoi warning all over again:

fair enough, i thought it was a common-enough term that chatgpt would pick up on it. a lot of components in architecture (maybe more so in construction) have unique names even if the component visually looks similar or the same.

for example (this will be using north-american terms), look at all the unique names each part of a window has:

in the window example, the “stool” and “apron” are quite visually similar, to my eye at least. maybe the apron is really the same piece of wood as the stool with the same cross-section and finish but the “apron” is vertical and the “stool” is horizontal (plus an inch longer or so)

so i was wondering if chatgpt would pick up on the unique names and auto-magically know what to do

Hi @BTH ,
In what i see it’s a combination of both: it picking up the terms and understanding the geometry.
Investigating a term first, to see if it understands it and then describe and/or adjust its geometry.

For the gooseneck, i asked it if it understands what i mean and it described it in architectural terms with examples. Then i had to refine the geometry. There were several tryouts but here’s the result.

import Rhino
import Rhino.Geometry as rg
import traceback


# ============================================================
# INPUTS
#
# Inclined      : Curve, Item Access
# Horizontal    : Curve, Item Access
# Radius        : Number, Item Access
# NeckHeight    : Number, Item Access
# NeckPosition  : Number, Item Access
# LowerBulge    : Number, Item Access
# UpperBulge    : Number, Item Access
# Tolerance     : Number, Item Access
#
# OUTPUTS
#
# Gooseneck
# Parts
# Info
# ============================================================


Gooseneck = None
Parts = []
Info = ""


# ============================================================
# HELPERS
# ============================================================

def get_curve(value):
    if value is None:
        return None

    if isinstance(value, rg.Line):
        return rg.LineCurve(value)

    if isinstance(value, rg.Curve):
        return value.DuplicateCurve()

    return None


def unitized(vector):
    result = rg.Vector3d(vector)

    if not result.Unitize():
        return None

    return result


def dot(vector_a, vector_b):
    return rg.Vector3d.Multiply(
        vector_a,
        vector_b
    )


def horizontal_projection(vector):
    result = rg.Vector3d(
        vector.X,
        vector.Y,
        0.0
    )

    return unitized(result)


def add_part(curve, name):
    if curve is None:
        raise Exception(
            "{} is null.".format(name)
        )

    if not curve.IsValid:
        raise Exception(
            "{} is invalid.".format(name)
        )

    if curve.GetLength() <= Tolerance:
        raise Exception(
            "{} is too short.".format(name)
        )

    Parts.append(curve)


def create_bezier_transition(
    start_point,
    start_tangent,
    end_point,
    end_tangent,
    start_handle,
    end_handle,
    name
):
    """
    Creates a cubic Bezier represented as a degree-3 NURBS curve.

    Control points:

        P0 = transition start
        P1 = P0 + start tangent * start handle
        P2 = P3 - end tangent * end handle
        P3 = transition end

    This provides tangent continuity at both ends.
    """

    tangent_a = unitized(start_tangent)
    tangent_b = unitized(end_tangent)

    if tangent_a is None:
        raise Exception(
            "{}: invalid start tangent.".format(name)
        )

    if tangent_b is None:
        raise Exception(
            "{}: invalid end tangent.".format(name)
        )

    if start_point.DistanceTo(end_point) <= Tolerance:
        raise Exception(
            "{}: endpoints are too close.".format(name)
        )

    start_handle = max(
        float(start_handle),
        Tolerance * 10.0
    )

    end_handle = max(
        float(end_handle),
        Tolerance * 10.0
    )

    point_0 = rg.Point3d(
        start_point
    )

    point_1 = (
        point_0
        + tangent_a * start_handle
    )

    point_3 = rg.Point3d(
        end_point
    )

    point_2 = (
        point_3
        - tangent_b * end_handle
    )

    curve = rg.NurbsCurve.Create(
        False,
        3,
        [
            point_0,
            point_1,
            point_2,
            point_3
        ]
    )

    if curve is None:
        raise Exception(
            "{} could not be created.".format(name)
        )

    if not curve.IsValid:
        raise Exception(
            "{} is invalid.".format(name)
        )

    if curve.GetLength() <= Tolerance:
        raise Exception(
            "{} is too short.".format(name)
        )

    return curve


def make_horizontal_semicircle(
    start_point,
    end_point,
    desired_end_direction,
    radius
):
    """
    Creates a fixed horizontal semicircular U-turn.

    The two possible semicircles are tested. The one whose
    end tangent best matches the Horizontal input direction
    is selected.
    """

    if abs(start_point.Z - end_point.Z) > Tolerance:
        return None

    diameter = end_point - start_point

    if diameter.Length <= Tolerance:
        return None

    actual_radius = diameter.Length * 0.5

    radius_error = abs(
        actual_radius - radius
    )

    allowed_error = max(
        Tolerance,
        radius * 0.001
    )

    if radius_error > allowed_error:
        return None

    direction = unitized(
        desired_end_direction
    )

    if direction is None:
        return None

    center = rg.Point3d(
        (start_point.X + end_point.X) * 0.5,
        (start_point.Y + end_point.Y) * 0.5,
        (start_point.Z + end_point.Z) * 0.5
    )

    middle_a = (
        center
        + direction * radius
    )

    middle_b = (
        center
        - direction * radius
    )

    candidates = []

    arc_a = rg.Arc(
        start_point,
        middle_a,
        end_point
    )

    if arc_a.IsValid:
        candidates.append(
            arc_a.ToNurbsCurve()
        )

    arc_b = rg.Arc(
        start_point,
        middle_b,
        end_point
    )

    if arc_b.IsValid:
        candidates.append(
            arc_b.ToNurbsCurve()
        )

    if not candidates:
        return None

    best_curve = None
    best_score = -1.0e300

    for candidate in candidates:
        tangent = candidate.TangentAtEnd

        if tangent.Unitize():
            score = dot(
                tangent,
                direction
            )

            if score > best_score:
                best_score = score
                best_curve = candidate

    return best_curve


# ============================================================
# MAIN
# ============================================================

try:

    # --------------------------------------------------------
    # Defaults
    # --------------------------------------------------------

    document = Rhino.RhinoDoc.ActiveDoc

    if document is not None:
        document_tolerance = (
            document.ModelAbsoluteTolerance
        )
    else:
        document_tolerance = 0.001

    if Tolerance is None or Tolerance <= 0.0:
        Tolerance = document_tolerance

    if Radius is None or Radius <= Tolerance:
        Radius = 100.0

    if NeckHeight is None or NeckHeight <= Tolerance:
        NeckHeight = Radius * 2.0

    if NeckPosition is None:
        NeckPosition = 0.0

    if LowerBulge is None or LowerBulge <= 0.0:
        LowerBulge = 1.0

    if UpperBulge is None or UpperBulge <= 0.0:
        UpperBulge = 1.0


    # --------------------------------------------------------
    # Convert inputs
    # --------------------------------------------------------

    inclined_curve = get_curve(
        Inclined
    )

    horizontal_curve = get_curve(
        Horizontal
    )

    if inclined_curve is None:
        raise Exception(
            "Inclined is null or invalid."
        )

    if horizontal_curve is None:
        raise Exception(
            "Horizontal is null or invalid."
        )

    if inclined_curve.GetLength() <= Tolerance:
        raise Exception(
            "Inclined is too short."
        )

    if horizontal_curve.GetLength() <= Tolerance:
        raise Exception(
            "Horizontal is too short."
        )


    # --------------------------------------------------------
    # Direction convention
    #
    # Inclined:
    #   Start = lower/free end
    #   End   = gooseneck connection
    #
    # Horizontal:
    #   Start = U-turn connection
    #   End   = free end
    # --------------------------------------------------------

    inclined_end = rg.Point3d(
        inclined_curve.PointAtEnd
    )

    horizontal_start = rg.Point3d(
        horizontal_curve.PointAtStart
    )


    # --------------------------------------------------------
    # Inclined direction
    # --------------------------------------------------------

    inclined_tangent = rg.Vector3d(
        inclined_curve.TangentAtEnd
    )

    if not inclined_tangent.Unitize():
        raise Exception(
            "Could not calculate the Inclined end tangent."
        )


    # NeckPosition uses the inclined direction in plan.
    # This moves the neck forward or backward without changing
    # its elevation.

    inclined_plan_direction = horizontal_projection(
        inclined_tangent
    )

    if inclined_plan_direction is None:
        raise Exception(
            "Inclined must have a horizontal directional component."
        )


    # --------------------------------------------------------
    # Horizontal direction
    # --------------------------------------------------------

    horizontal_tangent = rg.Vector3d(
        horizontal_curve.TangentAtStart
    )

    horizontal_direction = horizontal_projection(
        horizontal_tangent
    )

    if horizontal_direction is None:
        raise Exception(
            "Horizontal must have a non-vertical direction."
        )


    # --------------------------------------------------------
    # Vertical direction
    # --------------------------------------------------------

    vertical_direction = rg.Vector3d.ZAxis

    if horizontal_start.Z < inclined_end.Z:
        vertical_direction.Reverse()


    # --------------------------------------------------------
    # Determine which side of Horizontal contains Inclined
    # --------------------------------------------------------

    side_axis = rg.Vector3d.CrossProduct(
        rg.Vector3d.ZAxis,
        horizontal_direction
    )

    if not side_axis.Unitize():
        raise Exception(
            "Could not determine the U-turn side direction."
        )

    vector_to_inclined = (
        inclined_end
        - horizontal_start
    )

    if dot(
        vector_to_inclined,
        side_axis
    ) >= 0.0:

        side_direction = rg.Vector3d(
            side_axis
        )

    else:

        side_direction = -side_axis


    # --------------------------------------------------------
    # Fixed horizontal U-turn
    #
    # These points never move when NeckPosition changes.
    # --------------------------------------------------------

    uturn_end = rg.Point3d(
        horizontal_start
    )

    uturn_start = (
        uturn_end
        + side_direction * (2.0 * Radius)
    )

    uturn_start.Z = uturn_end.Z


    horizontal_uturn = make_horizontal_semicircle(
        uturn_start,
        uturn_end,
        horizontal_direction,
        Radius
    )

    if horizontal_uturn is None:
        raise Exception(
            "Failed to construct the fixed horizontal U-turn."
        )


    # The upper Bezier must match this exact tangent.

    uturn_start_tangent = rg.Vector3d(
        horizontal_uturn.TangentAtStart
    )

    if not uturn_start_tangent.Unitize():
        raise Exception(
            "Could not calculate the U-turn start tangent."
        )


    # --------------------------------------------------------
    # Base neck position
    #
    # This defines the neck position when NeckPosition = 0.
    # The upper transition has space to rise and turn before
    # reaching the fixed U-turn.
    # --------------------------------------------------------

    upper_transition_height = max(
        Radius * 1.5,
        Tolerance * 20.0
    )

    base_neck_top = (
        uturn_start
        - vertical_direction * upper_transition_height
    )

    base_neck_bottom = (
        base_neck_top
        - vertical_direction * NeckHeight
    )


    # --------------------------------------------------------
    # Move only the vertical neck
    #
    # Positive NeckPosition:
    #   moves along the inclined curve direction in plan
    #
    # Negative NeckPosition:
    #   moves in the opposite direction
    #
    # The U-turn remains fixed.
    # --------------------------------------------------------

    neck_translation = (
        inclined_plan_direction
        * NeckPosition
    )

    neck_bottom = (
        base_neck_bottom
        + neck_translation
    )

    neck_top = (
        base_neck_top
        + neck_translation
    )


    # --------------------------------------------------------
    # Vertical neck
    # --------------------------------------------------------

    vertical_neck = rg.LineCurve(
        neck_bottom,
        neck_top
    )

    if not vertical_neck.IsValid:
        raise Exception(
            "Failed to construct the vertical neck."
        )

    if vertical_neck.GetLength() <= Tolerance:
        raise Exception(
            "Vertical neck is too short."
        )


    # --------------------------------------------------------
    # Lower Bezier transition
    #
    # Inclined end -> vertical neck bottom
    #
    # Start tangent follows Inclined.
    # End tangent is vertical.
    # LowerBulge multiplies both Bezier handle lengths.
    # --------------------------------------------------------

    lower_distance = inclined_end.DistanceTo(
        neck_bottom
    )

    if lower_distance <= Tolerance:
        raise Exception(
            "Inclined endpoint is too close to the neck bottom."
        )


    lower_start_handle = max(
        Radius * 0.75,
        lower_distance * 0.30
    )

    lower_end_handle = max(
        Radius * 0.60,
        lower_distance * 0.25
    )


    lower_start_handle *= LowerBulge
    lower_end_handle *= LowerBulge


    lower_transition = create_bezier_transition(
        inclined_end,
        inclined_tangent,
        neck_bottom,
        vertical_direction,
        lower_start_handle,
        lower_end_handle,
        "Inclined-to-neck transition"
    )


    # --------------------------------------------------------
    # Upper Bezier transition
    #
    # Moved vertical neck top -> fixed horizontal U-turn
    #
    # Start tangent is vertical.
    # End tangent exactly matches the fixed U-turn.
    # UpperBulge multiplies both Bezier handle lengths.
    # --------------------------------------------------------

    upper_distance = neck_top.DistanceTo(
        uturn_start
    )

    if upper_distance <= Tolerance:
        raise Exception(
            "Neck top is too close to the U-turn start."
        )


    upper_start_handle = max(
        Radius * 0.75,
        upper_distance * 0.35
    )

    upper_end_handle = max(
        Radius * 0.75,
        upper_distance * 0.35
    )


    upper_start_handle *= UpperBulge
    upper_end_handle *= UpperBulge


    upper_transition = create_bezier_transition(
        neck_top,
        vertical_direction,
        uturn_start,
        uturn_start_tangent,
        upper_start_handle,
        upper_end_handle,
        "Neck-to-U-turn transition"
    )


    # --------------------------------------------------------
    # Verify endpoint gaps
    # --------------------------------------------------------

    lower_gap_1 = inclined_end.DistanceTo(
        lower_transition.PointAtStart
    )

    lower_gap_2 = lower_transition.PointAtEnd.DistanceTo(
        vertical_neck.PointAtStart
    )

    upper_gap_1 = vertical_neck.PointAtEnd.DistanceTo(
        upper_transition.PointAtStart
    )

    upper_gap_2 = upper_transition.PointAtEnd.DistanceTo(
        horizontal_uturn.PointAtStart
    )

    uturn_gap = horizontal_uturn.PointAtEnd.DistanceTo(
        horizontal_curve.PointAtStart
    )

    maximum_gap = max(
        lower_gap_1,
        lower_gap_2,
        upper_gap_1,
        upper_gap_2,
        uturn_gap
    )

    if maximum_gap > Tolerance * 10.0:
        raise Exception(
            "Connection gap is too large: {:.6f}".format(
                maximum_gap
            )
        )


    # --------------------------------------------------------
    # Assemble parts in original curve direction
    # --------------------------------------------------------

    add_part(
        inclined_curve.DuplicateCurve(),
        "Inclined rail"
    )

    add_part(
        lower_transition,
        "Inclined-to-neck transition"
    )

    add_part(
        vertical_neck,
        "Vertical neck"
    )

    add_part(
        upper_transition,
        "Neck-to-U-turn transition"
    )

    add_part(
        horizontal_uturn,
        "Horizontal U-turn"
    )

    add_part(
        horizontal_curve.DuplicateCurve(),
        "Horizontal rail"
    )


    # --------------------------------------------------------
    # Join
    # --------------------------------------------------------

    joined = rg.Curve.JoinCurves(
        Parts,
        Tolerance,
        True
    )

    if joined is None or len(joined) == 0:
        raise Exception(
            "All parts were created, but Rhino could not join them."
        )


    if len(joined) == 1:

        Gooseneck = joined[0]

        Info = (
            "Gooseneck created successfully.\n"
            "Transitions: cubic Bezier/NURBS curves.\n"
            "U-turn: fixed horizontal semicircle.\n"
            "NeckPosition moves only the vertical neck.\n"
            "Neck movement is horizontal and follows Inclined.\n"
            "Radius: {:.3f}\n"
            "Neck height: {:.3f}\n"
            "Neck position: {:.3f}\n"
            "Lower bulge: {:.3f}\n"
            "Upper bulge: {:.3f}\n"
            "Lower start handle: {:.3f}\n"
            "Lower end handle: {:.3f}\n"
            "Upper start handle: {:.3f}\n"
            "Upper end handle: {:.3f}\n"
            "Lower transition length: {:.3f}\n"
            "Upper transition length: {:.3f}\n"
            "Maximum connection gap: {:.6f}\n"
            "Parts: {}"
        ).format(
            Radius,
            NeckHeight,
            NeckPosition,
            LowerBulge,
            UpperBulge,
            lower_start_handle,
            lower_end_handle,
            upper_start_handle,
            upper_end_handle,
            lower_transition.GetLength(),
            upper_transition.GetLength(),
            maximum_gap,
            len(Parts)
        )

    else:

        Gooseneck = list(joined)

        Info = (
            "Geometry was created, but Rhino returned "
            "{} separate joined curves.\n"
            "Maximum connection gap: {:.6f}\n"
            "Inspect Parts or slightly increase Tolerance."
        ).format(
            len(joined),
            maximum_gap
        )


except Exception as error:

    Gooseneck = None

    Info = (
        "ERROR:\n{}\n\n"
        "Detailed traceback:\n{}"
    ).format(
        str(error),
        traceback.format_exc()
    )

Bold of you to assume someone isn’t learning simply because of the tool they’re using. Unless you’re omniscient, omnipresent, and omnipotent, in other words, God, you have no way of knowing whether this person is actually learning from this exercise or not. It doesn’t take much to entertain the possibility that they are. Beyond simply copy-pasting code, they could be reading it, dissecting it, modifying it, experimenting with it, or even spending their own time trying to understand it independently. Also, if you ask AI to explain why it chose a data structure, why a loop is written a certain way, or why a particular algorithm is more efficient, that is learning.

In fact, I can personally confirm that simply interacting with AI and seeing how it approaches problems teaches you a lot implicitly. You pick up syntax, coding patterns, project structure, debugging strategies, and problem-solving techniques almost by osmosis. And OP explicitly said as much:

You simply chose not to believe him.


What’s actually amusing is seeing your comments all over this forum, constantly complaining about AI.

I also find it funny that you feel the need to lecture complete strangers on how they’re allowed to learn.

Watching AI solve a problem is an incredibly effective learning tool, especially for beginners. Instead of spending hours trying to discover the correct approach by digging through endless documentation, Stack Overflow posts, YouTube videos, forum threads, and outdated blog articles, they can ask a question and watch the solution being built step by step, almost like having a personal tutor available at any time.

That’s one of the oldest learning methods there is: first observe how something is done, then understand it, and finally apply it yourself. AI simply compresses the time it takes to reach that first stage.

You honestly sound like a grumpy old man insisting people should ignore Google and go back to searching through library books because “that’s how we used to do it.” :old_man: :rofl:

Besides, I don’t think OP was even asking for advice on how to learn programming. He was simply showcasing what the tool is currently capable of.

Why is it bizarre to you?

Whenever a new technology appears, people naturally experiment with it, test its limits, share discoveries, and discuss how it fits into their workflow. That’s exactly what’s happening with AI today. The internet is full of people sharing interesting things they’ve built with it. Some of it is low effort, sure, but a lot of it is genuinely impressive. That’s not bizarre, it’s how people have always reacted to new technology.

Quite a lot, actually.

I’ve had countless conversations with colleagues and friends about how rapidly AI has advanced and how it’s solving problems that seemed impossible just a few years ago. A chatbot generating code is honestly one of the least impressive things modern AI can do.

Personally, I’ve used AI not only for programming but also for mathematics, geometry, logic, brainstorming, and solving architectural problems at work.

It’s also enabled me to build a whole suite of custom Grasshopper tools that save me countless hours. I’ve created scripts that detect third-party plugins in definitions so I can replace them with native components, built my own nesting algorithm instead of relying on OpenNest, automated group coloring based on naming conventions, created profiling tools that identify the most computationally expensive components, and much more. Along the way, I’ve become far more comfortable with both Python and C# than I ever would have otherwise.

Of course, the next step is to study those examples in depth and eventually write similar code from scratch. That’s how learning works: observe, understand, and then apply. AI doesn’t replace those later stages, it simply makes the first one dramatically more accessible.

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()
    )

It’s incredible how backwards this is - but you’re right in one thing it is almost like having a personal tutor, but even you realise that it’s not.

The rest… well… I don’t care - maybe work through your thoughts with ChatGPT and come back when you’ve got something worth reading.

Thank you for comparing me to God though, that was very amusing.

when you say it went very fast, you mean that chatgpt generated an accurate script on the first prompt or something else?

sorry i haven’t had time to digest your script, but it looks enticing since stairs can be challenging for new designers in our office, and these look like neat and compact packages that we could share with them