extends RefCounted
class_name CurveDrop
## DROP A CURVE TO THE GROUND (#120 L3, docs/node-tree-and-stacks.md §14.3): Cablerator's
## Drop the Cable. Pure and static — the map hands in the RAY (`ray.call(from)` ->
## {position, normal} or {} on a miss), so a gate can drop onto a fake floor and the map
## onto real physics with the cable's own pieces skipped.
##
## STEP FIRST: spans longer than `step` metres get interior points (even arc length within
## the span, `auto` handles), so a cable follows the ground instead of bridging over it
## (Cablerator's Add Points). Then every point but an OPEN curve's two ends (they stay on
## their walls) falls straight down: it rests `radius` x its own radius above the hit, along
## the hit normal, which becomes its `n`. A miss leaves the point. `aligned` / `free`
## handles are laid onto the hit plane at their length; `auto` / `vector` re-derive from the
## new neighbours (auto_handles).
## Polyline steps per span when measuring and splitting it.
const SPAN_STEPS := 64
static func dropped(pts: Array, closed: bool, step: float, radius: float, ray: Callable) -> Array:
var src := subdivide(pts, closed, step) if step > 1.0e-6 else pts.duplicate(true)
var n := src.size()
for i in n:
if not closed and (i == 0 or i == n - 1):
continue
var q: Dictionary = src[i]
var hit: Dictionary = ray.call(q["p"])
if hit.is_empty():
continue
var nn: Vector3 = (hit["normal"] as Vector3).normalized()
q["p"] = (hit["position"] as Vector3) + nn * radius * float(q.get("radius", 1.0))
q["n"] = nn
var kind := CurveMath.handle_type(q)
if kind == "aligned" or kind == "free":
for key: String in ["h_in", "h_out"]:
var h: Vector3 = q.get(key, Vector3.ZERO)
var flat := h - nn * h.dot(nn)
if flat.length() > 1.0e-9:
q[key] = flat.normalized() * h.length()
return CurveMath.auto_handles(src, closed)
## `pts` with interior points added so no span is longer than `step` (arc length). New
## points sit ON the curve, with `auto` handles, tilt / radius interpolated, no normal.
static func subdivide(pts: Array, closed: bool, step: float) -> Array:
var out: Array = []
var n := pts.size()
for i in n:
out.append((pts[i] as Dictionary).duplicate(true))
if not closed and i == n - 1:
break
if n < 2:
break
var c := CurveMath.controls(pts, closed, i)
# The span as a polyline: cumulative length at each of SPAN_STEPS + 1 parameters.
var acc := PackedFloat64Array([0.0])
var prev: Vector3 = c[0]
for k in range(1, SPAN_STEPS + 1):
var p := CurveMath.bezier(c, float(k) / SPAN_STEPS)
acc.append(acc[acc.size() - 1] + prev.distance_to(p))
prev = p
var span := acc[SPAN_STEPS]
var extra := ceili(span / step - 1.0e-9) - 1
if extra < 1:
continue
var a: Dictionary = pts[i]
var b: Dictionary = pts[(i + 1) % n]
var k2 := 0
for j in range(1, extra + 1):
var want := span * float(j) / float(extra + 1)
while k2 < SPAN_STEPS - 1 and acc[k2 + 1] < want:
k2 += 1
var f := (want - acc[k2]) / maxf(acc[k2 + 1] - acc[k2], 1.0e-12)
var t := (float(k2) + f) / SPAN_STEPS
var q := CurveMath.point(CurveMath.bezier(c, t), Vector3.ZERO)
q["tilt"] = lerpf(float(a.get("tilt", 0.0)), float(b.get("tilt", 0.0)), t)
q["radius"] = lerpf(float(a.get("radius", 1.0)), float(b.get("radius", 1.0)), t)
out.append(q)
return out