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