OverviewFilesCommitsIssuesDocsReleasesPeers

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

138 lines · gd ref:
extends RefCounted
class_name BundleCables
## A BUNDLE BETWEEN TWO FACE SETS (#120 L3, docs/node-tree-and-stacks.md §14.6):
## Cablerator's Create a Cable Mass, on the editor's face set. Pure and static: the faces
## come in as map._selected_face_polys() gives them ({pts, n}), the cables' curve points go
## out, and the map writes them.
##
## ISLANDS: the faces' connected components by shared corners (within ON). Exactly two, or
## nothing is made and `islands` says how many there were (Cablerator refuses the same way).
## Island A = the one holding the first face.
##
## ENDS, from one Pcg32 stream (`seed`), cable i from A_i to B_i:
##   random  a face picked by area, a triangle of its fan by area, a uniform point in it;
##   centres the island's face centres, cycled (cable i uses centre i mod faces).
## Each end's normal is its face's. Both handles are FREE: an end leaves along its normal,
## tension x chord / 3 long, its tension x (1 + jitter x u), u in [-1, 1] from the same
## stream (drawn whatever the jitter, so changing it never moves the ends). `slack` hangs
## each cable as the curve tool does (CurveOperator.hung: interior points).

const Pcg32Script := preload("res://scenes/maps/shapes/pcg32.gd")
const CurveOperatorScript := preload("res://scenes/maps/operators/curve_operator.gd")
## Two corners closer than this (metres) are one.
const ON := 1.0e-3


## The faces' islands: [[face index, …], …], each ascending, ordered by their first face.
static func islands(faces: Array) -> Array:
	var parent: Array = []
	for i in faces.size():
		parent.append(i)
	for i in faces.size():
		for j in range(i + 1, faces.size()):
			if _touch(faces[i], faces[j]):
				var ri := _root(parent, i)
				var rj := _root(parent, j)
				if ri != rj:
					parent[maxi(ri, rj)] = mini(ri, rj)
	var by_root := {}
	var order: Array = []
	for i in faces.size():
		var r := _root(parent, i)
		if not by_root.has(r):
			by_root[r] = []
			order.append(r)
		(by_root[r] as Array).append(i)
	var out: Array = []
	for r: int in order:
		out.append(by_root[r])
	return out


static func _root(parent: Array, i: int) -> int:
	while int(parent[i]) != i:
		i = int(parent[i])
	return i


static func _touch(a: Dictionary, b: Dictionary) -> bool:
	for p: Vector3 in (a["pts"] as PackedVector3Array):
		for q: Vector3 in (b["pts"] as PackedVector3Array):
			if p.distance_to(q) < ON:
				return true
	return false


## The bundle: {islands: how many, cables: [[CurveMath points], …]} — no cables unless
## there are exactly two islands. `o` = {count, random, seed, tension, jitter, slack}.
static func bundle(faces: Array, o: Dictionary) -> Dictionary:
	var isl := islands(faces)
	if isl.size() != 2:
		return {"islands": isl.size(), "cables": []}
	var A: Array = (isl[0] as Array).map(func(i: int) -> Dictionary: return faces[i])
	var B: Array = (isl[1] as Array).map(func(i: int) -> Dictionary: return faces[i])
	var ta := _tris(A)
	var tb := _tris(B)
	var rng: Object = Pcg32Script.seeded(maxi(0, int(o.get("seed", 0))), 1)
	var random := bool(o.get("random", true))
	var tension := float(o.get("tension", 1.0))
	var jitter := float(o.get("jitter", 0.0))
	var slack := float(o.get("slack", 0.0))
	var cables: Array = []
	for i in maxi(0, int(o.get("count", 1))):
		var ea := _end(A, ta, i, random, rng)
		var eb := _end(B, tb, i, random, rng)
		var u1: float = rng.range_f(-1.0, 1.0)
		var u2: float = rng.range_f(-1.0, 1.0)
		var chord := (ea["p"] as Vector3).distance_to(eb["p"])
		var pa := CurveMath.point(ea["p"], ea["n"])
		pa["handle"] = "free"
		pa["h_out"] = (ea["n"] as Vector3) * tension * (1.0 + jitter * u1) * chord / 3.0
		pa["h_in"] = -(pa["h_out"] as Vector3)
		var pb := CurveMath.point(eb["p"], eb["n"])
		pb["handle"] = "free"
		pb["h_in"] = (eb["n"] as Vector3) * tension * (1.0 + jitter * u2) * chord / 3.0
		pb["h_out"] = -(pb["h_in"] as Vector3)
		var pts: Array = [pa, pb]
		if slack > 1.0e-6:
			pts = CurveOperatorScript.hung(pts, false, slack)
		cables.append(CurveMath.auto_handles(pts, false))
	return {"islands": 2, "cables": cables}


## An island's fan triangles: {tris: [[a, b, c, n, area]], total}.
static func _tris(island: Array) -> Dictionary:
	var tris: Array = []
	var total := 0.0
	for f: Dictionary in island:
		var pts: PackedVector3Array = f["pts"]
		for k in range(1, pts.size() - 1):
			var area := (pts[k] - pts[0]).cross(pts[k + 1] - pts[0]).length() * 0.5
			if area > 1.0e-12:
				tris.append([pts[0], pts[k], pts[k + 1], f["n"], area])
				total += area
	return {"tris": tris, "total": total}


## End i on an island: {p, n}.
static func _end(island: Array, t: Dictionary, i: int, random: bool, rng: Object) -> Dictionary:
	if not random or float(t["total"]) <= 0.0:
		var f: Dictionary = island[i % island.size()]
		var c := Vector3.ZERO
		for p: Vector3 in (f["pts"] as PackedVector3Array):
			c += p
		return {"p": c / float((f["pts"] as PackedVector3Array).size()), "n": f["n"]}
	var pick: float = rng.next_float() * float(t["total"])
	var tri: Array = t["tris"][(t["tris"] as Array).size() - 1]
	for tr: Array in (t["tris"] as Array):
		if pick < float(tr[4]):
			tri = tr
			break
		pick -= float(tr[4])
	var s := sqrt(float(rng.next_float()))
	var r2: float = rng.next_float()
	var a: Vector3 = tri[0]
	var b: Vector3 = tri[1]
	var c2: Vector3 = tri[2]
	return {"p": a * (1.0 - s) + b * (s * (1.0 - r2)) + c2 * (s * r2), "n": tri[3]}