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]}