OverviewFilesCommitsIssuesDocsReleasesPeers

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

120 lines · gd ref:
extends RefCounted
## CURVES IN THE FOLD (#120 F5, docs/node-tree-and-stacks.md §6): curve.* journal records
## -> the room's curves. Pure and deterministic, like CollectionFold; a curve is no brush,
## so a curve edit never re-folds the level.
##
##   curve.create  {points: [point], closed, name}       a curve; its dot is its id
##   curve.set     {target, points?}                     the whole point list
##                 {target, index, fields}               one point's fields (p, h_in, h_out,
##                                                       tilt, radius, n): a drag writes one
##                 {target, closed?, name?}              the latest wins, per field
##   curve.remove  {target}                              gone (undo brings it back)
##
## ON THE WIRE a point is {p: [x,y,z], h_in, h_out, n: [x,y,z], tilt, radius} (to_wire /
## from_wire); in a state it is CurveMath's {p: Vector3, …}.

const KINDS := ["curve.create", "curve.set", "curve.remove"]
const VEC_KEYS := ["p", "h_in", "h_out", "n"]


static func handles(kind: String) -> bool:
	return KINDS.has(kind)


static func _before(a: Dictionary, b: Dictionary) -> bool:
	if int(a["seq"]) != int(b["seq"]):
		return int(a["seq"]) < int(b["seq"])
	return String(a["author"]) < String(b["author"])


static func point_to_wire(q: Dictionary) -> Dictionary:
	var out := {}
	for k: String in q:
		var v: Variant = q[k]
		out[k] = [v.x, v.y, v.z] if v is Vector3 else v
	return out


static func point_from_wire(d: Variant) -> Dictionary:
	var q := CurveMath.point(Vector3.ZERO, Vector3.ZERO)
	if not (d is Dictionary):
		return q
	for k: Variant in (d as Dictionary):
		var v: Variant = (d as Dictionary)[k]
		if VEC_KEYS.has(String(k)):
			if v is Vector3:
				q[String(k)] = v
			elif v is Array and (v as Array).size() == 3:
				q[String(k)] = Vector3(float(v[0]), float(v[1]), float(v[2]))
		elif String(k) in ["tilt", "radius"]:
			q[String(k)] = float(v)
		elif String(k) == "handle":            # §6.1: an unknown type reads as auto
			q["handle"] = String(v) if CurveMath.HANDLE_TYPES.has(String(v)) else "auto"
	return q


static func to_wire(pts: Array) -> Array:
	return pts.map(func(q: Dictionary) -> Dictionary: return point_to_wire(q))


static func from_wire(pts: Variant) -> Array:
	var out: Array = []
	if pts is Array:
		for d: Variant in (pts as Array):
			out.append(point_from_wire(d))
	return out


## Every curve's final state: {dot: {dot, seq, ver, author, points, closed, name, removed}}.
## `ver` = the highest seq among its live records (a follow-curve's copies carry it in their
## names, so an edited curve renames them). Undone records are ignored. A point index past the end is skipped (the record that
## added it was undone), never guessed at.
static func resolve_states(records: Array) -> Dictionary:
	var live: Array = []
	for r: Dictionary in records:
		if handles(String(r.get("kind", ""))) and not bool(r.get("undone", false)):
			live.append(r)
	live.sort_custom(_before)
	var states := {}
	for r: Dictionary in live:
		var kind := String(r["kind"])
		if kind == "curve.create":
			var dot := "%s:%d" % [String(r["author"]), int(r["seq"])]
			states[dot] = {"dot": dot, "seq": int(r["seq"]), "ver": int(r["seq"]), "author": String(r["author"]),
				"points": from_wire(r.get("points", [])), "closed": bool(r.get("closed", false)),
				"name": String(r.get("name", "Curve")), "removed": false}
			continue
		var t := String(r.get("target", ""))
		if not states.has(t):
			continue
		var s: Dictionary = states[t]
		s["ver"] = maxi(int(s["ver"]), int(r["seq"]))
		match kind:
			"curve.set":
				if r.has("points"):
					s["points"] = from_wire(r["points"])
				if r.has("index"):
					var i := int(r["index"])
					if i >= 0 and i < (s["points"] as Array).size():
						var q: Dictionary = (s["points"] as Array)[i]
						var f: Dictionary = point_from_wire(r.get("fields", {}))
						for k: Variant in (r.get("fields", {}) as Dictionary):
							q[String(k)] = f[String(k)]
				if r.has("closed"):
					s["closed"] = bool(r["closed"])
				if r.has("name"):
					s["name"] = String(r["name"])
			"curve.remove":
				s["removed"] = true
	return states


## The live curves only.
static func live(records: Array) -> Dictionary:
	var out := {}
	var st := resolve_states(records)
	for d: String in st:
		if not bool(st[d]["removed"]):
			out[d] = st[d]
	return out