extends Node
## GATE — A CURVE IS A COLLECTION MEMBER (#120 §16.2, step 1): "c:<curve dot>", like an
## object or a brush object.
## M1 select_curve holds the curve and drops every other selection; selecting a shape
## drops the curve
## M2 Ctrl+G with a curve selected: the new collection holds "c:<dot>", one gesture
## M3 Shift+M links it into a second collection, M moves it, Ctrl+Alt+G takes it out
## M4 Delete Hierarchy deletes a FREE curve only in the subtree; a curve a cable outside
## still uses survives (§16.7 Q2); one Ctrl+Z brings the free one back
## M5 ... a cable and its curve both in the subtree both go (no curve left behind)
## M7 Save as asset: a member curve is in the blob (flagged a member), a placement folds
## it moved by the frame, Make Real links the new curve into the new collection
## M8 ... a curve in a SUB-collection keeps its place there through Make Real
## M6 a flatten keeps the link: the member names a live curve with the same name
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_curve_member.tscn
const CollectionFold := preload("res://scenes/maps/collection_fold.gd")
const AssetDoc := preload("res://scenes/maps/asset_doc.gd")
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
get_tree().root.get_node("/root/GameMode").set_mode("build")
map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
await _run()
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 29:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE curve_member: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _tree() -> Dictionary:
return persist.collection_tree()["cols"]
func _members(col: String) -> Array:
var t := _tree()
return t[col]["members"] if t.has(col) else []
func _curves() -> Dictionary:
return map.curve_states()
func _curve(at: Vector3, name: String) -> String:
return map.create_curve([CurveMath.point(at), CurveMath.point(at + Vector3(4.37, 0, 0)),
CurveMath.point(at + Vector3(4.37, 0, 3.11))], false, name)
func _box(at: Vector3) -> String:
persist.begin_gesture("box")
var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)), "size": [1, 1, 1],
"material_id": 2, "level_id": 0})
persist.end_gesture()
persist.refold()
return d
func _run() -> void:
persist = map.get("_persist")
_ok("setup: journal", persist != null and map.has_method("create_curve"))
if persist == null:
return
var O0 := Vector3(300.137, 1.31, 299.873)
var f := _curve(O0, "Free")
var shape := _box(O0 + Vector3(0, 0, 10))
_ok("setup: a free curve", _curves().has(f), f)
# ---- M1 ----
map.select_shape(shape)
_ok("M1 the map can select a curve", map.has_method("select_curve"))
if not map.has_method("select_curve"):
return
map.select_curve(f)
_ok("M1 select_curve holds it", map.selected_curve() == f, map.selected_curve())
_ok("M1 ... and drops the shape and its brushes", map.selected_shape() == ""
and (map.get("_sel_brushes") as Array).is_empty() and (map.selected_objects() as Array).is_empty())
map.select_shape(shape)
_ok("M1 selecting a shape drops the curve", map.selected_curve() == "" and map.selected_shape() == shape)
# ---- M2 ----
map.select_curve(f)
var g0: int = (persist.gestures() as Array).size()
var a: String = CollectionOps.create_from_selection(map, "A")
persist.refold()
_ok("M2 Ctrl+G: the collection holds the curve", _members(a) == ["c:" + f], _members(a))
_ok("M2 ... as one gesture", (persist.gestures() as Array).size() == g0 + 1)
# ---- M3 ----
var b: String = CollectionOps.create_empty(map, "B")
map.select_curve(f)
CollectionOps.link_selection(map, b)
persist.refold()
_ok("M3 Shift+M: in A and in B", _members(a).has("c:" + f) and _members(b).has("c:" + f))
var c: String = CollectionOps.create_empty(map, "C")
map.select_curve(f)
CollectionOps.move_selection(map, c)
persist.refold()
_ok("M3 M: only in C", _members(c) == ["c:" + f] and not _members(a).has("c:" + f)
and not _members(b).has("c:" + f), [_members(a), _members(b), _members(c)])
map.select_curve(f)
CollectionOps.unlink_selection(map, c)
persist.refold()
_ok("M3 Ctrl+Alt+G: in none", CollectionFold.collections_of(persist.collection_tree(), "c:" + f).is_empty())
# ---- M4 ----
var cable: String = map.add_cable([CurveMath.point(O0 + Vector3(0, 0, -8)), CurveMath.point(O0 + Vector3(6.21, 0, -8))],
false, {"radius": 0.05})
var u := String(persist.shape_state(cable)["params"]["curve"])
persist.begin_gesture("D")
var d: String = persist.record("col.create", {"name": "D", "parent": ""})
persist.record("col.link", {"target": d, "members": ["c:" + f, "c:" + u]})
persist.end_gesture()
persist.refold()
CollectionOps.remove(map, d, true)
persist.refold()
_ok("M4 Delete Hierarchy: the free curve is gone", not _curves().has(f))
_ok("M4 ... the cable's curve (cable outside) survives", _curves().has(u) and (persist.live_shapes() as Dictionary).has(cable))
persist.undo_last()
persist.refold()
_ok("M4 one Ctrl+Z: the free curve and D are back", _curves().has(f) and _tree().has(d))
# ---- M5 ----
persist.begin_gesture("E")
var e: String = persist.record("col.create", {"name": "E", "parent": ""})
persist.record("col.link", {"target": e, "members": ["g:" + cable, "c:" + u]})
persist.end_gesture()
persist.refold()
CollectionOps.remove(map, e, true)
persist.refold()
_ok("M5 the cable and its curve both go", not _curves().has(u) and not (persist.live_shapes() as Dictionary).has(cable))
persist.undo_last()
persist.refold()
_ok("M5 ... and come back on Ctrl+Z", _curves().has(u) and (persist.live_shapes() as Dictionary).has(cable))
# ---- M7 / M8 ----
var f3 := _curve(O0 + Vector3(0, 3, 0), "Sub curve")
var kbox := _box(O0 + Vector3(2, 0, 2))
persist.begin_gesture("Kit")
var kit: String = persist.record("col.create", {"name": "Kit", "parent": ""})
var sub: String = persist.record("col.create", {"name": "Sub", "parent": kit})
persist.record("col.link", {"target": kit, "members": ["c:" + f, "g:" + kbox]})
persist.record("col.link", {"target": sub, "members": ["c:" + f3]})
persist.end_gesture()
persist.refold()
var w: Dictionary = AssetDoc.write(persist, kit)
var doc: Dictionary = w["doc"]
var crecs: Array = (doc.get("records", []) as Array).filter(func(r: Dictionary) -> bool: return String(r["kind"]) == "curve.create")
var members_flag: Array = crecs.filter(func(r: Dictionary) -> bool: return bool(r.get("member", false)))
_ok("M7 the blob carries both member curves", crecs.size() == 2, crecs.size())
_ok("M7 ... the root's one flagged a member, the sub's not", members_flag.size() == 1
and String(members_flag[0]["name"]) == "Free", members_flag.map(func(r: Dictionary) -> String: return String(r["name"])))
# The free curve reaches x + 4.37 and z + 3.11; the only box sits in [2, 3] — so only a
# bounds that counts member curves reaches past x + 3.
var bhi: Vector3 = (doc.get("bounds", [Vector3.ZERO, Vector3.ZERO]) as Array)[1]
_ok("M7 ... the bounds take in the member curves", bhi.x >= O0.x + 4.37 - 1.0e-3, bhi - O0)
var subrec: Array = (doc.get("records", []) as Array).filter(func(r: Dictionary) -> bool:
return String(r["kind"]) == "col.create" and String(r["name"]) == "Sub")
_ok("M8 ... the sub-collection lists its curve as c:<local>", subrec.size() == 1
and (subrec[0]["members"] as Array).any(func(m: String) -> bool: return m.begins_with("c:L:")),
subrec[0]["members"] if subrec.size() == 1 else subrec)
var lib: Node = map.asset_library()
lib.save_collection(kit, "CurveKit")
var off := Vector3(50, 0, 0)
var pivot: Vector3 = doc["pivot"]
var pl: String = lib.place("CurveKit", Transform3D(Basis.IDENTITY, pivot + off))
persist.refold()
# A placement's curves are FOLD-INTERNAL (refold hands them to ShapeFold, never to
# curve_states()), so its synthetic records are read from AssetFold.payloads — the one
# builder the fold and Make Real share.
var pst: Dictionary = (persist.placements() as Dictionary).get(pl, {})
var pc: Array = []
if not pst.is_empty():
pc = (LevelPersistence.AssetFold.payloads(pst, doc, func(l: String) -> String: return "X" + l) as Array) \
.filter(func(e: Dictionary) -> bool: return String(e["payload"]["kind"]) == "curve.create")
var free_p: Array = pc.filter(func(e: Dictionary) -> bool: return String(e["payload"].get("name", "")) == "Free")
_ok("M7 a placement folds both curves", pc.size() == 2, pc.size())
_ok("M7 ... moved by the frame", free_p.size() == 1
and Vector3(free_p[0]["payload"]["points"][0]["p"][0], free_p[0]["payload"]["points"][0]["p"][1],
free_p[0]["payload"]["points"][0]["p"][2]).is_equal_approx(O0 + off))
_ok("M7 ... with the blob's member flag left out of the record", pc.all(func(e: Dictionary) -> bool:
return not (e["payload"] as Dictionary).has("member")))
map.make_real_asset(pl)
persist.refold()
var real_col := ""
for cd2: String in _tree():
if String(_tree()[cd2]["name"]) == "CurveKit":
real_col = cd2
var real_c: Array = _members(real_col).filter(func(m: String) -> bool: return m.begins_with("c:"))
_ok("M7 Make Real: the new collection holds one curve", real_c.size() == 1, _members(real_col))
_ok("M7 ... a live curve named Free", real_c.size() == 1 and _curves().has(String(real_c[0]).substr(2))
and String(_curves()[String(real_c[0]).substr(2)]["name"]) == "Free")
var real_sub := ""
for cd3: String in _tree():
if String(_tree()[cd3]["name"]) == "Sub" and String(_tree()[cd3]["parent"]) == real_col and real_col != "":
real_sub = cd3
var sub_c: Array = _members(real_sub).filter(func(m: String) -> bool: return m.begins_with("c:"))
_ok("M8 Make Real: the copied Sub holds the sub curve", sub_c.size() == 1 and _curves().has(String(sub_c[0]).substr(2))
and String(_curves()[String(sub_c[0]).substr(2)]["name"]) == "Sub curve", _members(real_sub))
# ---- M6 (last: a flatten re-mints every dot) ----
persist.begin_gesture("F")
var fc: String = persist.record("col.create", {"name": "Flat", "parent": ""})
persist.record("col.link", {"target": fc, "members": ["c:" + f]})
persist.end_gesture()
persist.refold()
var res: Dictionary = persist.flatten_map()
persist.refold()
_ok("M6 flatten ran", not res.has("error"), res.get("error", ""))
var flat := ""
for cd4: String in _tree():
if String(_tree()[cd4]["name"]) == "Flat":
flat = cd4
var fm: Array = _members(flat).filter(func(m: String) -> bool: return m.begins_with("c:"))
_ok("M6 the flattened collection names a LIVE curve", fm.size() == 1 and _curves().has(String(fm[0]).substr(2)), _members(flat))
_ok("M6 ... the same one (its name)", fm.size() == 1 and _curves().has(String(fm[0]).substr(2))
and String(_curves()[String(fm[0]).substr(2)]["name"]) == "Free")