extends Node
## GATE — DELETE and INSPECT from the Outliner (2026-10-05: "we should be able to
## delete nodes and currently cannot" · "there is one clearance node that has 3511
## something but i dont know what").
##
## Delete used to refuse everything but an authored collection. Now it acts on any row
## whose contents are SAVED IN THE ROOM, as ONE gesture (one Ctrl+Z):
## 1. an object row (a teleporter) -> object.remove; Ctrl+Z brings it back
## 2. several selected object rows -> all of them, one gesture
## 3. a brush GROUP row (Unsorted) -> every brush in it, one gesture; Ctrl+Z restores
## 4. a plain scene node (WallRunRange) -> adopted, so deletable + undoable; the level node is refused
## 5. a collection still deletes only the collection (view-local), never its brushes
## And a group row can be OPENED to see what is in it:
## 6. expanding a group row lists its brushes (key + size), capped, with a "more" row
## 7. clicking a brush row selects exactly that brush
## 8. the row stays open across a refresh (every fold rebuilds the tree)
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_outliner_delete.tscn
var fails := 0
var checks := 0
func _ok(what: String, cond: bool, note: String = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", 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"
root_mode("build")
var map: Node3D = (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(map)
if checks < 26:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE outliner_delete: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func root_mode(m: String) -> void:
var gm: Node = get_tree().root.get_node_or_null("/root/GameMode")
if gm != null:
gm.set_mode(m)
## Select exactly these rows in the panel's Tree, the active one last.
func _select_rows(panel: Node, ids: Array) -> void:
var tree: Tree = panel.get("_tree")
tree.deselect_all()
for id: Variant in ids:
var it: TreeItem = (panel.get("_items") as Dictionary).get(String(id), null)
if it != null:
it.select(0)
func _gesture0(persist: Node) -> Dictionary:
var g: Array = persist.gestures()
return g[0] if not g.is_empty() else {}
func _run(map: Node3D) -> void:
var panel: Node = map.get("_outliner")
var persist: Node = map.get("_persist")
var level: Node = map.get("level")
_ok("map_test has an Outliner and a journal", panel != null and persist != null)
if panel == null or persist == null:
return
panel.refresh()
var items: Dictionary = panel.get("_items")
# ---- 1. one object row ----
var tele := "obj:scene:TeleporterToFlat"
_ok("setup: the teleporter has a row", items.has(tele))
_select_rows(panel, [tele])
panel.delete_selected()
await get_tree().process_frame
_ok("1. Delete on an object row removes the object", map.object_node("scene:TeleporterToFlat") == null)
_ok("1. as one gesture", int(_gesture0(persist).get("count", 0)) == 1, str(_gesture0(persist)))
persist.undo_last()
persist.refold()
_ok("1. Ctrl+Z brings it back", map.object_node("scene:TeleporterToFlat") != null)
# ---- 2. several object rows ----
panel.refresh()
items = panel.get("_items")
_select_rows(panel, ["obj:scene:SpawnPoint1", "obj:scene:SpawnPoint2"])
panel.delete_selected()
await get_tree().process_frame
_ok("2. two selected object rows are both deleted",
map.object_node("scene:SpawnPoint1") == null and map.object_node("scene:SpawnPoint2") == null)
_ok("2. as ONE gesture of 2", int(_gesture0(persist).get("count", 0)) == 2, str(_gesture0(persist)))
persist.undo_last()
persist.refold()
_ok("2. one Ctrl+Z brings both back",
map.object_node("scene:SpawnPoint1") != null and map.object_node("scene:SpawnPoint2") != null)
# ---- 3. a brush group row ----
panel.refresh()
items = panel.get("_items")
var rest_row: Dictionary = {}
for r: Dictionary in panel.model.rows():
if String(r["id"]) == "derived:rest":
rest_row = r
var n_rest := int(rest_row.get("count", 0))
var live0: int = (level.get_live_brush_ids() as PackedInt32Array).size()
_ok("setup: an Unsorted group with brushes", n_rest > 0, "%d" % n_rest)
_select_rows(panel, ["derived:rest"])
panel.delete_selected()
await get_tree().process_frame
var live1: int = (level.get_live_brush_ids() as PackedInt32Array).size()
_ok("3. Delete on a group row removes every brush in it", live1 == live0 - n_rest, "%d -> %d" % [live0, live1])
_ok("3. as one gesture", String(_gesture0(persist).get("label", "")) == "Delete"
and int(_gesture0(persist).get("count", 0)) == n_rest, str(_gesture0(persist)))
persist.undo_last()
persist.refold()
_ok("3. one Ctrl+Z restores them", (level.get_live_brush_ids() as PackedInt32Array).size() == live0)
# ---- 4. a plain scene node IS deletable ----
# User 2026-10-05: "wallrunrange is undeletable ? everything should be able to be
# deleted". A content node of the map scene is adopted as a BASE object, so Delete
# journals an object.remove (detached, children and all) and Ctrl+Z brings it back.
panel.refresh()
items = panel.get("_items")
var plain := "obj:scene:WallRunRange"
_ok("setup: the wall-run range is an object row", items.has(plain), str(items.keys().filter(
func(k: Variant) -> bool: return String(k).contains("WallRun"))))
var wr: Node = map.object_node("scene:WallRunRange")
_ok("setup: ... with its walls under it", wr != null and wr.get_node_or_null("Straight") != null)
_select_rows(panel, [plain])
panel.delete_selected()
await get_tree().process_frame
_ok("4. Delete takes the range out of the world", wr != null and not wr.is_inside_tree())
_ok("4. as one object.remove gesture", (_gesture0(persist).get("kinds", {}) as Dictionary).has("object.remove")
and not bool(_gesture0(persist).get("undone", true)), str(_gesture0(persist)))
persist.undo_last()
persist.refold()
_ok("4. one Ctrl+Z puts it back, walls and all", wr != null and wr.is_inside_tree()
and wr.get_node_or_null("Straight") != null)
# ---- 4b. the level itself is not a scene node to delete ----
panel.refresh()
items = panel.get("_items")
var sysrow := "node:DCLevel"
_ok("setup: the level's own row", items.has(sysrow))
var g_before: int = (persist.gestures() as Array).size()
_select_rows(panel, [sysrow])
panel.delete_selected()
await get_tree().process_frame
_ok("4b. the level node is refused", map.get_node_or_null("DCLevel") != null
and (persist.gestures() as Array).size() == g_before)
# ---- 5. a collection (room state, #120 F1) holding an operator group ----
var gk: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(20, 0, 20))),
"size": [2, 1, 2], "material_id": 2, "level_id": 0}) # a brush object: the shape
persist.refold()
var cdot: String = CollectionOps.create_empty(map, "C")
persist.begin_gesture("Link")
persist.record("col.link", {"target": cdot, "members": ["g:" + gk]})
persist.end_gesture()
persist.refold()
panel.refresh()
live0 = (level.get_live_brush_ids() as PackedInt32Array).size()
_select_rows(panel, ["col:" + cdot])
panel.delete_selected()
persist.refold()
await get_tree().process_frame
_ok("5. a collection is deleted", not (persist.collection_tree()["cols"] as Dictionary).has(cdot))
_ok("5. but not its brushes", (level.get_live_brush_ids() as PackedInt32Array).size() == live0)
# ---- 6-8. inspect a group ----
panel.refresh()
items = panel.get("_items")
var it: TreeItem = items.get("derived:rest", null)
_ok("6. a group row starts closed", it != null and it.collapsed)
if it == null:
return
it.collapsed = false # what clicking the arrow does
await get_tree().process_frame
items = panel.get("_items")
var kids: Array = []
var c := it.get_first_child()
while c != null:
kids.append(c)
c = c.get_next()
var first: Variant = kids[0].get_metadata(0) if not kids.is_empty() else null
_ok("6. opening it lists its brushes",
kids.size() == mini(n_rest, panel.MEMBER_ROWS) + (1 if n_rest > panel.MEMBER_ROWS else 0), "%d rows for %d" % [kids.size(), n_rest])
_ok("6. each says what it is (key + size)", first is Dictionary
and String((first as Dictionary).get("brush_key", "")) != ""
and String(kids[0].get_text(0)).contains("×"), kids[0].get_text(0) if not kids.is_empty() else "")
if first is Dictionary:
# A CLICK, not just select(): in SELECT_MULTI a programmatic select() emits nothing
# (scene/gui/tree.cpp Tree::item_selected); the mouse path emits multi_selected.
var tree: Tree = panel.get("_tree")
tree.deselect_all()
kids[0].select(0)
tree.multi_selected.emit(kids[0], 0, true)
await get_tree().process_frame
await get_tree().process_frame
var sel: Array = map.get("_sel_brushes")
_ok("7. clicking a brush row selects exactly that brush",
sel.size() == 1 and String(level.get_brush_key(int(sel[0]))) == String(first["brush_key"]), str(sel))
panel.refresh()
var it2: TreeItem = (panel.get("_items") as Dictionary).get("derived:rest", null)
_ok("8. the row stays open across a refresh", it2 != null and not it2.collapsed and it2.get_first_child() != null)