OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / curves / curve_drop.gd

80 lines · gd ref:
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