OverviewFilesCommitsIssuesDocsReleasesPeers

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

141 lines · gd ref:
extends RefCounted
class_name EdgeCables
## CABLES FROM EDGES (#120 L3, docs/node-tree-and-stacks.md §14.5): Cablerator's Create a
## Cable from Edges, on the editor's edge set. Pure and static: the edges come in as
## map.selected_edges() gives them ({a, e, normals: [n1, n2]}), the cable's curve points
## go out, and the map writes them.
##
## CHAINS: edges whose ends coincide (within ON) join. A vertex where 1 or 3+ set edges
## meet ENDS a chain (one cable per path, as Cablerator makes one spline per path); a chain
## that comes back to its start is CLOSED.
##
## POINTS: at the chain's vertices, or at its edge CENTRES (which rounds the corners off;
## an open chain keeps its two end vertices so the cable still spans it). Each point moves
## `offset` along its OUTWARD normal — at a vertex the mean of every incident set edge's
## two face normals, at a centre that edge's two — so the cable sits on the edge rather
## than half inside it; that normal is stored as the point's n. Corners: round = auto
## handles, sharp = vector. An open chain's two ends are always vector, so the cable runs
## straight off the end instead of turning out along its normal (auto's rule for a cable
## placed on a wall).

## Two ends closer than this (metres) are one vertex.
const ON := 1.0e-3


## The chains of `edges`: [{verts: [Vector3], vnormals: [Vector3] (per vertex),
## enormals: [Vector3] (per edge: edge i joins vertex i and i + 1, the last of a closed
## chain back to 0), closed: bool}]. Open chains first (from each end vertex in order of
## first appearance), then loops (from their first edge in input order).
static func chains(edges: Array) -> Array:
	var nodes: Array = []                 # Vector3 per vertex
	var inc: Array = []                   # per vertex: incident edge ids
	var en: Array = []                    # per edge: [vertex, vertex], or [] (degenerate)
	var nsum: Array = []                  # per vertex: the sum of incident edges' face normals
	for k in edges.size():
		var e: Dictionary = edges[k]
		var ia := _node(nodes, inc, nsum, e["a"])
		var ie := _node(nodes, inc, nsum, e["e"])
		if ia == ie:
			en.append([])
			continue
		en.append([ia, ie])
		var ns: Array = e.get("normals", [])
		var fsum := Vector3.ZERO
		for n: Vector3 in ns:
			fsum += n
		for v: int in [ia, ie]:
			(inc[v] as Array).append(k)
			nsum[v] = (nsum[v] as Vector3) + fsum
	var used := {}
	var out: Array = []
	for s in nodes.size():
		if (inc[s] as Array).size() == 2:
			continue
		for k2: int in (inc[s] as Array):
			if not used.has(k2):
				out.append(_walk(s, k2, nodes, inc, en, nsum, edges, used))
	for k3 in en.size():
		if not used.has(k3) and not (en[k3] as Array).is_empty():
			out.append(_walk(int(en[k3][0]), k3, nodes, inc, en, nsum, edges, used))
	return out


static func _node(nodes: Array, inc: Array, nsum: Array, p: Vector3) -> int:
	for i in nodes.size():
		if (nodes[i] as Vector3).distance_to(p) < ON:
			return i
	nodes.append(p)
	inc.append([])
	nsum.append(Vector3.ZERO)
	return nodes.size() - 1


static func _walk(start: int, first: int, nodes: Array, inc: Array, en: Array, nsum: Array,
		edges: Array, used: Dictionary) -> Dictionary:
	var vids: Array = [start]
	var eids: Array = []
	var cur := start
	var e := first
	var closed := false
	while true:
		used[e] = true
		eids.append(e)
		var nxt: int = en[e][1] if int(en[e][0]) == cur else en[e][0]
		if nxt == start:
			closed = true
			break
		vids.append(nxt)
		if (inc[nxt] as Array).size() != 2:
			break
		var other: int = inc[nxt][0] if int(inc[nxt][0]) != e else inc[nxt][1]
		if used.has(other):
			break
		cur = nxt
		e = other
	var verts: Array = []
	var vn: Array = []
	for v: int in vids:
		verts.append(nodes[v])
		var s: Vector3 = nsum[v]
		vn.append(s.normalized() if s.length() > 1.0e-9 else Vector3.ZERO)
	var enr: Array = []
	for k: int in eids:
		var fs := Vector3.ZERO
		for n: Vector3 in ((edges[k] as Dictionary).get("normals", []) as Array):
			fs += n
		enr.append(fs.normalized() if fs.length() > 1.0e-9 else Vector3.ZERO)
	return {"verts": verts, "vnormals": vn, "enormals": enr, "closed": closed}


## A chain's curve points (CurveMath's, handles derived): at its vertices or its edge
## centres, each `offset` along its outward normal; `sharp` corners are vector handles.
static func curve_points(chain: Dictionary, centres: bool, offset: float, sharp: bool) -> Array:
	var verts: Array = chain["verts"]
	var vn: Array = chain["vnormals"]
	var enr: Array = chain["enormals"]
	var closed := bool(chain["closed"])
	var corner := "vector" if sharp else "auto"
	var pts: Array = []
	if not centres:
		for i in verts.size():
			pts.append(_pt((verts[i] as Vector3) + (vn[i] as Vector3) * offset, vn[i], corner))
	else:
		var n := verts.size()
		if not closed:
			pts.append(_pt((verts[0] as Vector3) + (vn[0] as Vector3) * offset, vn[0], corner))
		for i in enr.size():
			var mid: Vector3 = ((verts[i] as Vector3) + (verts[(i + 1) % n] as Vector3)) * 0.5
			pts.append(_pt(mid + (enr[i] as Vector3) * offset, enr[i], corner))
		if not closed:
			pts.append(_pt((verts[n - 1] as Vector3) + (vn[n - 1] as Vector3) * offset, vn[n - 1], corner))
	if not closed and pts.size() >= 2:
		pts[0]["handle"] = "vector"
		pts[pts.size() - 1]["handle"] = "vector"
	return CurveMath.auto_handles(pts, closed)


static func _pt(p: Vector3, n: Vector3, handle: String) -> Dictionary:
	var q := CurveMath.point(p, n)
	q["handle"] = handle
	return q