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