OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_outliner_tree.gd

149 lines · gd ref:
extends Node
## GATE — THE ORGANIZER AS ONE TREE (hyper-games #124 B4; docs/node-tree-and-stacks.md
## §2.5, Blender's View Layer; user 2026-10-18: "we also need a collapse all next to show
## all for collection viewing … the way we are laying out collections is confusing").
##   O1  collections come FIRST under the map's row (a new one is not at the bottom)
##   O2  an object in a collection is listed there ONLY, not also under SceneObjects
##   O3  a brush object in a collection is listed there only, not also under DCLevel
##   O4  things in no collection stay where they were (SceneObjects / DCLevel)
##   O5  the viewport selecting an object that lives in a collection lights ITS row
##   O6  H on that object hides it (the hidden set names the row it really has)
##   O7  Collapse all folds every row but the map's; Expand all opens them again
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_outliner_tree.tscn

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
var panel: Node


func _ok(what: String, cond: bool, note: Variant = "") -> void:
	checks += 1
	if not cond:
		fails += 1
	print("  %-66s %s  %s" % [what, "OK" if cond else "**FAIL**", str(note)])


func _ready() -> void:
	get_tree().root.size = Vector2i(1920, 1080)
	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 < 9:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE outliner_tree: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _frames(n: int) -> void:
	for i in n:
		await get_tree().process_frame


func _ids() -> Array:
	return (panel.model.rows() as Array).map(func(r: Dictionary) -> String: return String(r["id"]))


func _row(id: String) -> Dictionary:
	for r: Dictionary in (panel.model.rows() as Array):
		if String(r["id"]) == id:
			return r
	return {}


func _run() -> void:
	persist = map.get("_persist")
	level = map.get("level")
	panel = map.get("_outliner")
	# Two objects: A goes into a collection, B stays out. Two loose brushes: one into it.
	var a: String = map.create_object("marker", Transform3D(Basis(), Vector3(5, 1, 5)))
	var b: String = map.create_object("marker", Transform3D(Basis(), Vector3(7, 1, 5)))
	var loose: Array = []
	for x: int in (level.get_live_brush_ids() as PackedInt32Array):
		if String(level.get_brush_group(x)) == "":
			loose.append(x)
	_ok("setup: two objects and two loose brushes", a != "" and b != "" and loose.size() >= 2)
	if loose.size() < 2:
		return
	map.select_objects([a], a)
	var col := CollectionOps.create_from_selection(map, "Things")
	map.select_brushes(PackedInt32Array([int(loose[0])]))
	CollectionOps.link_selection(map, col)
	persist.refold()
	await _frames(2)
	var g0 := String(level.get_brush_group(int(loose[0])))
	var ids := _ids()
	# ---- O1 ----
	var ci := ids.find("col:" + col)
	var first_child := -1
	for i in ids.size():
		if String(_row(String(ids[i])).get("parent", "")) == "node:.":
			first_child = i
			break
	_ok("O1 the collection is the map's FIRST child row", ci >= 0 and ci == first_child, [ci, first_child, ids.slice(0, 4)])
	# ---- O2 / O3 / O4 ----
	_ok("O2 object A (in the collection) is not also under SceneObjects", not ids.has("obj:" + a)
		and ids.has("col:%s/obj:%s" % [col, a]), ids.filter(func(s: String) -> bool: return s.contains(a)))
	_ok("O3 brush object g0 (in the collection) is not also under DCLevel", not ids.has("group:" + g0)
		and ids.has("col:%s/g:%s" % [col, g0]), g0)
	_ok("O4 object B (no collection) stays under SceneObjects", ids.has("obj:" + b)
		and String(_row("obj:" + b).get("parent", "")) == "node:SceneObjects")
	# ---- O5 ----
	panel.refresh()
	map.select_object(a)
	await _frames(3)
	var sel: Dictionary = panel.call("_selected_row")
	_ok("O5 selecting A in the viewport lights its collection row", String(sel.get("id", "")) == "col:%s/obj:%s" % [col, a], sel.get("id"))
	# ---- O6 ----
	panel.hide_objects([a])
	await _frames(2)
	_ok("O6 H on A hides it", not (map.object_node(a) as Node3D).visible)
	panel.unhide_all()
	await _frames(2)
	_ok("O6 ... Alt+H shows it again", (map.object_node(a) as Node3D).visible)
	# ---- O7 ----
	panel.refresh()
	panel.call("collapse_all")
	var tree: Tree = panel.get("_tree")
	var root_it: TreeItem = tree.get_root()
	var all_folded := not root_it.collapsed
	for c: TreeItem in root_it.get_children():
		if c.get_child_count() > 0 and not c.collapsed:
			all_folded = false
	_ok("O7 Collapse all folds every row but the map's", all_folded)
	panel.call("expand_all")
	var all_open := true
	for c2: TreeItem in root_it.get_children():
		if c2.get_child_count() > 0 and c2.collapsed:
			all_open = false
	_ok("O7 Expand all opens them again", all_open)
	# ---- O8 (spacing audit item 7): the Tree FILLS the organizer area; the System filter
	# rides in the button flow instead of a row of its own ----
	var dock: Node = get_tree().root.get_node("/root/BuildDock")
	await _frames(3)
	var org: Rect2 = (dock._sides["organizer"]["frame"] as Control).get_global_rect()
	var tr8: Rect2 = tree.get_global_rect()
	var pr8: Rect2 = (panel as Control).get_global_rect()
	# The panel (its Tree, then the transform fields below it) ends at the area's bottom —
	# no overflow, no dead space — and the TREE is what took the spare height.
	_ok("O8 the outliner ends at the organizer area's bottom (fills it, no overflow)", absf(pr8.end.y - org.end.y) <= 16.0,
		[pr8.end.y, org.end.y])
	_ok("O8 ... and the Tree took the spare height (above its 120 px floor)", tr8.size.y > 124.0, tr8.size.y)
	var sb: Control = panel.get("_system_box")
	_ok("O8 the System filter sits in the button flow, not a row of its own", sb != null and sb.get_parent() is HFlowContainer,
		sb.get_parent() if sb != null else null)
	panel.model.save(String(panel.get("_save_key")))