extends "res://scenes/maps/shapes/param/param_shape.gd"
## SPIRAL STAIRS. Local: the axis is vertical through the centre of the box's x/z
## footprint; size = (x extent, rise, z extent). Each tread is a wedge between two
## angles; DC builds convex pieces, so a tread wider than 45° is split into equal
## sub-wedges at the same height (a wedge's chord faces stay convex well past 45°,
## but the split keeps the outer chord close to the circle).
const MAX_WEDGE_DEG := 45.0
const CLEARANCE_FACETS := 16
func id() -> String:
return "spiral_stairs"
func label() -> String:
return "Spiral stairs"
func params() -> Array:
return [
{"id": "steps", "label": "Steps", "type": "int", "default": 16, "min": 2, "max": 128, "step": 1},
{"id": "sweep_deg", "label": "Sweep", "type": "float", "default": 360.0, "min": 30.0,
"max": 1440.0, "step": 15.0, "unit": "°", "tip": "How far round the flight turns."},
{"id": "inner_radius", "label": "Inner radius", "type": "float", "default": 0.3,
"min": 0.05, "max": 50.0, "step": 0.05, "unit": "m"},
{"id": "outer_radius", "label": "Outer radius", "type": "float", "default": 0.0,
"min": 0.0, "max": 50.0, "step": 0.05, "unit": "m", "tip": "0 = fit the box."},
{"id": "tread_thickness", "label": "Tread thickness", "type": "float", "default": 0.0,
"min": 0.0, "max": 2.0, "step": 0.05, "unit": "m", "tip": "0 = solid to the floor."},
{"id": "clockwise", "label": "Clockwise", "type": "bool", "default": false},
{"id": "center_column", "label": "Centre column", "type": "bool", "default": false},
{"id": "column_facets", "label": "Column sides", "type": "int", "default": 12, "min": 3,
"max": 64, "step": 1},
{"id": "headroom", "label": "Headroom", "type": "float", "default": 2.0,
"min": 0.0, "max": 6.0, "step": 0.1, "unit": "m"},
]
func _radii(size: Vector3, p: Dictionary) -> Vector2:
var fit := minf(size.x, size.z) * 0.5
var ro: float = float(p["outer_radius"]) if float(p["outer_radius"]) > 0.0 else fit
var ri: float = minf(float(p["inner_radius"]), ro * 0.9)
return Vector2(ri, ro)
func _pt(c: Vector2, r: float, a: float, y: float) -> Vector3:
return Vector3(c.x + r * cos(a), y, c.y + r * sin(a))
func local_pieces(size: Vector3, p: Dictionary) -> Array:
var c := Vector2(size.x * 0.5, size.z * 0.5)
var rr := _radii(size, p)
var n: int = p["steps"]
var sweep := deg_to_rad(float(p["sweep_deg"])) * (-1.0 if bool(p["clockwise"]) else 1.0)
var step_a := sweep / float(n)
var sub := maxi(1, ceili(absf(rad_to_deg(step_a)) / MAX_WEDGE_DEG - 1.0e-6))
var tt: float = p["tread_thickness"]
var out: Array = []
for k in n:
var top := size.y * float(k + 1) / float(n)
var bot := maxf(top - tt, 0.0) if tt > 0.0 else 0.0
for s in sub:
var a0 := step_a * (float(k) + float(s) / float(sub))
var a1 := step_a * (float(k) + float(s + 1) / float(sub))
var v := [
_pt(c, rr.y, a0, bot), _pt(c, rr.y, a0, top), _pt(c, rr.y, a1, bot), _pt(c, rr.y, a1, top),
_pt(c, rr.x, a0, bot), _pt(c, rr.x, a0, top), _pt(c, rr.x, a1, bot), _pt(c, rr.x, a1, top),
]
var piece: Dictionary = ShapeKit.hex_piece(v)
if not piece.is_empty():
out.append(tagged(piece, "step %d" % (k + 1))) # a wide step's wedges share its name
if bool(p["center_column"]):
var col := _circle(c, rr.x, int(p["column_facets"]))
var cp: Dictionary = ShapeKit.prism_piece(col, 0, 2, 1, 0.0, size.y)
if not cp.is_empty():
out.append(tagged(cp, "column"))
return out
## A polygon in (x, z) for prism_piece (span = x, vert = z, tunnel = y).
func _circle(c: Vector2, r: float, facets: int) -> Array[Vector2]:
var out: Array[Vector2] = []
for i in facets:
var a := TAU * float(i) / float(facets)
out.append(Vector2(c.x + r * cos(a), c.y + r * sin(a)))
return out
func local_clearance(size: Vector3, p: Dictionary) -> Array:
var head: float = p["headroom"]
if head <= 0.0:
return []
var c := Vector2(size.x * 0.5, size.z * 0.5)
var pc: Dictionary = ShapeKit.prism_piece(_circle(c, _radii(size, p).y, CLEARANCE_FACETS),
0, 2, 1, 0.0, size.y + head)
return [] if pc.is_empty() else [pc]