extends "res://scenes/maps/shapes/param/param_shape.gd"
## TUNNEL. Local: x = along the tunnel, y = up, z = across; size = the OUTER box
## (length, outer height, outer width). The lining is `wall` thick; the bore is the
## box minus the wall at the sides and the top.
##
## Profiles:
## round — a half ellipse (springing at `spring`), straight side walls below it
## pointed — a two-centred (gothic) arch, scaled to the height
## flat — two walls and a roof slab
## circle — a full ring (no floor)
## `facets` = how many voussoirs make the curved part (the full ring for circle).
## The inner and outer curves use the SAME parametrisation, so voussoir j is the quad
## between the same parameter on both: radial, convex, nothing to clip.
const AXIAL_MARGIN_M := 1.0 # the clearance bore runs past each mouth (ArchShape)
func id() -> String:
return "tunnel"
func label() -> String:
return "Tunnel"
func params() -> Array:
return [
{"id": "profile", "label": "Arch profile", "type": "enum", "default": "round",
"options": ["round", "pointed", "flat", "circle"]},
{"id": "facets", "label": "Facets", "type": "int", "default": 8, "min": 2, "max": 64,
"step": 1, "tip": "Voussoirs in the curved part (the whole ring for circle)."},
{"id": "wall", "label": "Wall width", "type": "float", "default": 0.5, "min": 0.05,
"max": 5.0, "step": 0.05, "unit": "m"},
{"id": "spring", "label": "Wall height", "type": "float", "default": 0.0, "min": 0.0,
"max": 50.0, "step": 0.1, "unit": "m", "tip": "Straight walls before the arch starts."},
{"id": "floor", "label": "Floor", "type": "bool", "default": false},
{"id": "clearance", "label": "Cut terrain", "type": "bool", "default": true},
]
## The curved part's points, (z, y), for one of the two curves. `w` = the lining
## thickness to subtract (0 = the outer curve, wall = the inner).
func _curve(size: Vector3, p: Dictionary, w: float) -> Array:
var W := size.z
var H := size.y
var spring: float = minf(float(p["spring"]), H * 0.9)
var facets: int = p["facets"]
var pts: Array = []
match String(p["profile"]):
"round":
var rz := W * 0.5 - w
var ry := H - w - spring
for j in facets + 1:
var t := PI * float(j) / float(facets)
pts.append(Vector2(W * 0.5 + rz * cos(t), spring + ry * sin(t)))
"pointed":
# Two arcs of radius = the span, each centred on the opposite springing
# point; apex height span*sqrt(3)/2, then scaled to the box.
var zl := w
var zr := W - w
var span := zr - zl
var apex := span * sqrt(3.0) * 0.5
var scale := (H - w - spring) / maxf(apex, 1.0e-6)
var half := maxi(1, facets / 2)
# Left arc, centred on the RIGHT springing point: angle pi (the left
# springing point) up to 2pi/3 (the apex).
for j in half + 1:
var th := PI - (PI / 3.0) * float(j) / float(half)
pts.append(Vector2(zr + span * cos(th), spring + span * sin(th) * scale))
# Right arc, centred on the LEFT springing point: angle pi/3 (the apex,
# already added) down to 0 (the right springing point).
for j in range(1, half + 1):
var ph := (PI / 3.0) * (1.0 - float(j) / float(half))
pts.append(Vector2(zl + span * cos(ph), spring + span * sin(ph) * scale))
"circle":
var cz := W * 0.5
var cy := H * 0.5
for j in facets + 1:
var t2 := TAU * float(j) / float(facets)
pts.append(Vector2(cz + (W * 0.5 - w) * cos(t2), cy + (H * 0.5 - w) * sin(t2)))
return pts
func _xyz(p2: Vector2, x: float) -> Vector3:
return Vector3(x, p2.y, p2.x)
func local_pieces(size: Vector3, p: Dictionary) -> Array:
var L := size.x
var H := size.y
var W := size.z
var wall: float = minf(float(p["wall"]), minf(W, H) * 0.45)
var prof := String(p["profile"])
var out: Array = []
if prof == "flat":
out.append(tagged(box(Vector3(0, 0, 0), Vector3(L, H, wall)), "wall"))
out.append(tagged(box(Vector3(0, 0, W - wall), Vector3(L, H, W)), "wall"))
out.append(tagged(box(Vector3(0, H - wall, wall), Vector3(L, H, W - wall)), "roof"))
else:
var outer := _curve(size, p, 0.0)
var inner := _curve(size, p, wall)
for j in outer.size() - 1:
var v := [_xyz(outer[j], 0.0), _xyz(outer[j], L), _xyz(outer[j + 1], 0.0), _xyz(outer[j + 1], L),
_xyz(inner[j], 0.0), _xyz(inner[j], L), _xyz(inner[j + 1], 0.0), _xyz(inner[j + 1], L)]
var piece: Dictionary = ShapeKit.hex_piece(v)
if not piece.is_empty():
out.append(tagged(piece, "voussoir %d" % (j + 1)))
var spring: float = minf(float(p["spring"]), H * 0.9)
if prof != "circle" and spring > 0.0:
out.append(tagged(box(Vector3(0, 0, 0), Vector3(L, spring, wall)), "wall"))
out.append(tagged(box(Vector3(0, 0, W - wall), Vector3(L, spring, W)), "wall"))
if bool(p["floor"]) and prof != "circle":
out.append(tagged(box(Vector3(0, 0, wall), Vector3(L, wall, W - wall)), "floor"))
return out
## The OUTER envelope extended past each mouth (ArchShape's rule: the terrain meets
## the outside of the lining; the axial margin is what makes the bore break through).
func local_clearance(size: Vector3, p: Dictionary) -> Array:
if not bool(p["clearance"]):
return []
var L := size.x
var prof := String(p["profile"])
var m := AXIAL_MARGIN_M
if prof == "flat":
return [box(Vector3(-m, 0, 0), Vector3(L + m, size.y, size.z))]
var profile: Array[Vector2] = []
var spring: float = minf(float(p["spring"]), size.y * 0.9)
var curve := _curve(size, p, 0.0)
if prof == "circle":
for j in curve.size() - 1:
profile.append(curve[j])
else:
if spring > 0.0:
profile.append(Vector2(size.z, 0.0))
for q: Vector2 in curve:
profile.append(q)
if spring > 0.0:
profile.append(Vector2(0.0, 0.0))
# (z, y) profile, extruded along x: span = z (2), vert = y (1), tunnel = x (0).
var pc: Dictionary = ShapeKit.prism_piece(profile, 2, 1, 0, -m, L + m)
return [] if pc.is_empty() else [pc]