extends BrushShape
class_name StairsShape
## Stairs, by TrenchBroom's rule (DrawShapeToolExtensions.cpp): tread depth is the run
## divided by the step count, and step i is a solid from the FLOOR up to its own tread —
## not a floating slab. That is what makes a flight watertight underneath and climbable
## rather than a set of hovering shelves.
const HEADROOM_M := 2.0 # the standing capsule is 1.58 m (player_procedural.tscn)
func id() -> String:
return "stairs"
## STEPS 0 = AUTO: one step per grid step of height, which puts every tread on the lattice
## the map was built on (TrenchBroom's ceil(height / stepHeight) with stepHeight = the grid).
func params() -> Array:
return [{"id": "steps", "label": "Steps", "type": "int", "default": 0, "min": 0, "max": 64,
"step": 1, "tip": "How many steps. 0 = one per grid step of height."}]
## The step count the flight will have, auto resolved.
func step_count(lo: Vector3, hi: Vector3, ctx: Dictionary) -> int:
var n := int(param(ctx, "steps"))
if n > 0:
return n
var st: float = float(ctx.get("grid_step", 1.0))
var vert: int = int(ctx["axis"])
return maxi(1, roundi((hi[vert] - lo[vert]) / maxf(st, 1.0e-6)))
func pieces(lo: Vector3, hi: Vector3, ctx: Dictionary) -> Array:
var vert: int = int(ctx["axis"])
var run: int = int(ctx["run_axis"])
var normal: Vector3 = ctx["normal"]
var view: Vector3 = ctx["view"]
var n: int = step_count(lo, hi, ctx)
var step_h: float = (hi[vert] - lo[vert]) / float(n)
var tread: float = (hi[run] - lo[run]) / float(n)
# The flight rises AWAY FROM THE AIMED SURFACE, so stairs put on a floor go up.
# On a ceiling the bounds hang downwards and so does the flight, which is what
# the bounds say and is the same answer TrenchBroom gives.
var up_positive: bool = normal[vert] >= 0.0
# And it ASCENDS AWAY FROM YOU, so you arrive at the bottom step rather than
# facing a wall of the top one.
var away_positive: bool = view[run] > 0.0
var out: Array = []
for i in n:
var slo: Vector3 = lo
var shi: Vector3 = hi
if up_positive:
shi[vert] = lo[vert] + step_h * float(i + 1)
else:
slo[vert] = hi[vert] - step_h * float(i + 1)
if away_positive:
slo[run] = lo[run] + tread * float(i)
shi[run] = lo[run] + tread * float(i + 1)
else:
slo[run] = hi[run] - tread * float(i + 1)
shi[run] = hi[run] - tread * float(i)
out.append(ShapeKit.box_piece(slo, shi))
return out
## THE STAIRS CUT IS ONE BOX — the flight's own footprint, from the floor it
## stands on to the top of the flight plus headroom.
##
## Not the stepped complement of the treads, which was the other candidate. The
## flight is solid from the floor up to each tread (see pieces), so the
## complement is what is left of the bounds above them — a stair-shaped slot that
## hugs the geometry. It looks better and it is wrong for the same reason it
## looks better: it hugs. There is no headroom in it, so the ceiling of the cut
## IS the tread above, and you cannot walk up a flight whose ceiling is the next
## step. The rectangular cutting reads as a cutting because that is what it is.
func clearance(lo: Vector3, hi: Vector3, ctx: Dictionary) -> Array:
var vert: int = int(ctx["axis"])
var normal: Vector3 = ctx["normal"]
var head: float = ShapeKit.lattice(HEADROOM_M, float(ctx["grid_step"]))
var clo: Vector3 = lo
var chi: Vector3 = hi
# AWAY FROM THE AIMED SURFACE, the same direction the flight rises. On a
# ceiling the bounds hang downwards and so does the headroom.
if normal[vert] >= 0.0:
chi[vert] = hi[vert] + head
else:
clo[vert] = lo[vert] - head
return [ShapeKit.box_piece(clo, chi)]
func describe(lo: Vector3, hi: Vector3, ctx: Dictionary, size_text: String) -> String:
var n: int = step_count(lo, hi, ctx)
var vert: int = int(ctx["axis"])
var st: float = maxf(float(ctx["grid_step"]), 1.0e-6)
var rise_u: float = (hi[vert] - lo[vert]) / st * float(ctx["grid_units"]) / float(n)
var fmt: Callable = ctx["fmt"]
return "stairs, %d steps of %s in %s" % [n, String(fmt.call(rise_u)), size_text]
func clearance_name(ctx: Dictionary) -> String:
var st: float = maxf(float(ctx["grid_step"]), 1.0e-6)
var u: float = ShapeKit.lattice(HEADROOM_M, st) / st * float(ctx["grid_units"])
return "%s units of headroom" % String.num(u, 0)