OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_outliner_curves.gd

441 lines · gd ref:
extends Node
## GATE — CURVES IN THE OUTLINER (#120 §16.3, §16.7 Q1; step 2: rows + rename).
##   R1  a room with no curves has no Curves row
##   R2  a free curve is a row "∿ <name>" under Curves
##   R3  linked into a collection it is listed there only
##   R4  a curve ONE cable uses is not a row of its own: it is the first row when the
##       cable's row opens ("∿ path · <name>"), under the cable's collection row too
##   R5  a curve two things use (a cable and a curve modifier) is its own row again
##   R6  rename: F2 opens the editor on a curve row; the edit (the row's text, ∿ and all)
##       is one curve.set {name}; the row shows the new name
##   R7  renaming a COLLECTION that holds brushes keeps the name clean: its row reads
##       "Hall (5)" and the edit used to save " (5)" into the name
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_outliner_curves.tscn

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var panel: Node
var cam: Camera3D


## Aim the crosshair at `at` from above (gate_curve_tool's recipe: a headless viewport keeps
## its pointer at (0, 0), so the wrap offset carries it to the centre).
func _aim_at(at: Vector3) -> void:
	cam.global_position = at + Vector3(0.01, 4.0, 0.01)
	cam.look_at(at, Vector3.UP)
	await get_tree().process_frame
	map.set("_wrap_offset", get_viewport().get_visible_rect().size * 0.5 - get_viewport().get_mouse_position())


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)
	cam = Camera3D.new()
	add_child(cam)
	cam.current = true
	for i in 6:
		await get_tree().process_frame
	await _run()
	get_tree().root.get_node("/root/GameMode").set_mode("play")
	if checks < 56:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE outliner_curves: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _fold() -> void:
	persist.refold()
	panel.refresh()
	await get_tree().process_frame


func _rows() -> Dictionary:
	return panel.get("_items")


func _text(id: String) -> String:
	var it: TreeItem = _rows().get(id, null)
	return it.get_text(0) if it != null else ""


func _parent_id(id: String) -> String:
	var it: TreeItem = _rows().get(id, null)
	if it == null or it.get_parent() == null:
		return ""
	var r: Variant = it.get_parent().get_metadata(0)
	return String((r as Dictionary)["id"]) if r is Dictionary else ""


func _select_row(id: String) -> void:
	var tree: Tree = panel.get("_tree")
	tree.deselect_all()
	var it: TreeItem = _rows().get(id, null)
	if it != null:
		it.select(0)


## Open a group row the way a click on its arrow does, and wait for the deferred fill.
func _open(id: String) -> void:
	var it: TreeItem = _rows().get(id, null)
	if it == null:
		return
	it.collapsed = false
	await get_tree().process_frame
	await get_tree().process_frame


## Edit a row's name in place: what _on_item_edited receives (a headless Tree cannot type).
func _edit(id: String, text: String) -> void:
	var it: TreeItem = _rows().get(id, null)
	if it == null:
		return
	it.set_text(0, text)
	panel.call("_commit_name", it)


func _run() -> void:
	persist = map.get("_persist")
	panel = map.get("_outliner")
	_ok("setup: journal and outliner", persist != null and panel != null)
	if persist == null or panel == null:
		return
	var O0 := Vector3(200.137, 1.31, 199.873)

	# ---- R1 ----
	await _fold()
	_ok("R1 no curves: no Curves row", not _rows().has("curves:"))

	# ---- R2 ----
	var f: String = map.create_curve([CurveMath.point(O0), CurveMath.point(O0 + Vector3(3.3, 0, 0))], false, "Rail")
	await _fold()
	_ok("R2 a free curve has a row", _rows().has("curve:" + f), f)
	_ok("R2 ... under Curves", _parent_id("curve:" + f) == "curves:", _parent_id("curve:" + f))
	_ok("R2 ... named with ∿", _text("curve:" + f) == "∿ Rail", _text("curve:" + f))

	# ---- R3 ----
	persist.begin_gesture("A")
	var a: String = persist.record("col.create", {"name": "A", "parent": ""})
	persist.record("col.link", {"target": a, "members": ["c:" + f]})
	persist.end_gesture()
	await _fold()
	_ok("R3 linked into A: its row is under A", _rows().has("col:%s/c:%s" % [a, f]))
	_ok("R3 ... and not under Curves", not _rows().has("curve:" + f))
	_ok("R3 ... Curves is gone with nothing left in it", not _rows().has("curves:"))

	# ---- R4 ----
	var cable: String = map.add_cable([CurveMath.point(O0 + Vector3(0, 0, 6)), CurveMath.point(O0 + Vector3(5.1, 0, 6))],
		false, {"radius": 0.05})
	var u := String(persist.shape_state(cable)["params"]["curve"])
	await _fold()
	_ok("R4 the cable's curve has no row of its own", not _rows().has("curve:" + u))
	await _open("group:" + cable)
	var cid := "group:%s/c:%s" % [cable, u]
	_ok("R4 opening the cable lists its curve", _rows().has(cid), cid)
	_ok("R4 ... first, as its path", _rows().has(cid) and (_rows()["group:" + cable] as TreeItem).get_child(0) == _rows()[cid]
		and _text(cid).begins_with("∿ path"), _text(cid))
	persist.begin_gesture("B")
	var b: String = persist.record("col.create", {"name": "B", "parent": ""})
	persist.record("col.link", {"target": b, "members": ["g:" + cable]})
	persist.end_gesture()
	await _fold()
	await _open("col:%s/g:%s" % [b, cable])
	_ok("R4 ... and under the cable's row in a collection", _rows().has("col:%s/g:%s/c:%s" % [b, cable, u]))

	# ---- R5 ----
	var box: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, O0 + Vector3(0, 0, 12))), "size": [1, 1, 1],
		"material_id": 2, "level_id": 0})
	map.add_modifier("curve", box, {"curve": u})
	await _fold()
	_ok("R5 used by a cable AND a modifier: its own row", _rows().has("curve:" + u))

	# ---- R6 ----
	_select_row("curve:" + u)
	_ok("R6 F2 opens the editor on a curve row", panel.rename_selected())
	var g0: int = (persist.gestures() as Array).size()
	_edit("curve:" + u, "∿ Wire")
	await _fold()
	_ok("R6 the edit is one gesture", (persist.gestures() as Array).size() == g0 + 1)
	_ok("R6 ... naming the curve (no ∿ in the name)", String(map.curve_states()[u]["name"]) == "Wire", map.curve_states()[u]["name"])
	_ok("R6 ... and the row says so", _text("curve:" + u) == "∿ Wire", _text("curve:" + u))

	# ---- S (step 3): selection both ways, delete, drawn selected ----
	# A fresh free curve, and u (cable + modifier: its own row "curve:<u>").
	var s: String = map.create_curve([CurveMath.point(O0 + Vector3(0, 0, -6)), CurveMath.point(O0 + Vector3(2.7, 0, -6))],
		false, "Spare")
	await _fold()
	_select_row("curve:" + s)
	panel.call("_apply_tree_selection")
	await get_tree().process_frame
	_ok("S1 a curve row click selects the curve", map.selected_curve() == s and (map.get("_sel_brushes") as Array).is_empty(),
		map.selected_curve())
	_ok("S7 the selected curve is drawn in its own colour", int(map.curve_view_selected_surfaces()) > 0,
		map.curve_view_selected_surfaces())
	map.select_shape(box)
	await get_tree().process_frame
	await get_tree().process_frame
	_ok("S7 ... and not once nothing curve is selected", int(map.curve_view_selected_surfaces()) == 0)
	map.select_curve(u)
	await get_tree().process_frame
	await get_tree().process_frame
	var lit: TreeItem = _rows().get("curve:" + u, null)
	_ok("S2 a viewport select_curve lights its row", lit != null and lit.is_selected(0))
	var data: Dictionary = panel.drag_data_for_selection()
	_ok("S9 a curve row drags as c:<dot>", (data["members"] as Array) == ["c:" + u], data)
	# The cable's OWN curve (only the cable uses it): its row is under the cable.
	var cable2: String = map.add_cable([CurveMath.point(O0 + Vector3(0, 0, 18)), CurveMath.point(O0 + Vector3(4.4, 0, 18))],
		false, {"radius": 0.05})
	var u2 := String(persist.shape_state(cable2)["params"]["curve"])
	await _fold()
	map.select_curve(u2)
	await get_tree().process_frame
	await get_tree().process_frame
	var lit2: TreeItem = _rows().get("group:%s/c:%s" % [cable2, u2], null)
	_ok("S3 selecting a cable's own curve opens the cable and lights it", lit2 != null and lit2.is_selected(0))
	# X on a USED curve: refused (§16.7 Q2).
	var g1: int = (persist.gestures() as Array).size()
	var n_used: int = map.delete_selection()
	await _fold()
	_ok("S5 X on a curve a cable uses deletes nothing", n_used == 0 and map.curve_states().has(u2)
		and (persist.gestures() as Array).size() == g1)
	_ok("S5 ... and says which cable", String(map.get("_last_refusal")).contains(cable2), map.get("_last_refusal"))
	# X on the free one.
	map.select_curve(s)
	var n_free: int = map.delete_selection()
	await _fold()
	_ok("S4 X deletes a free curve, one gesture", n_free == 1 and not map.curve_states().has(s)
		and (persist.gestures() as Array).size() == g1 + 1)
	_ok("S8 ... and the selection lets go of it", map.selected_curve() == "")
	persist.undo_last()
	await _fold()
	_ok("S4 Ctrl+Z brings it back", map.curve_states().has(s))
	_select_row("curve:" + s)
	_ok("S6 the outliner's Delete on a curve row deletes it", int(panel.delete_selected()) == 1 and not map.curve_states().has(s))
	persist.undo_last()
	await _fold()
	map.select_curve(s)
	map.remove_curve(s)                        # any fold that takes it (here the plain API)
	await _fold()
	_ok("S8 a fold that removes the selected curve drops the selection", map.selected_curve() == "", map.selected_curve())

	# ---- R7 ----
	persist.begin_gesture("C")
	var c: String = persist.record("col.create", {"name": "Hall", "parent": ""})
	persist.record("col.link", {"target": c, "members": ["g:" + box]})
	persist.end_gesture()
	await _fold()
	var shown := _text("col:" + c)
	_ok("R7 setup: the row shows a count", shown.ends_with(")"), shown)
	_edit("col:" + c, shown.replace("Hall", "Lobby"))
	await _fold()
	_ok("R7 renaming keeps the count out of the name", String(persist.collection_tree()["cols"][c]["name"]) == "Lobby",
		persist.collection_tree()["cols"][c]["name"])

	# ---- H (step 4): hide; Edit curve on the selection; Shift+D ----
	# Live now: f (in A), u (cable + modifier: "curve:<u>"), u2 (cable2's own).
	var drawn0: int = map.curve_view_surfaces()
	panel.model.set_row_hidden("curve:" + u, true)
	panel.model.apply()
	await _fold()
	_ok("H1 the eye on a curve row hides its wire", map.is_curve_hidden(u) and int(map.curve_view_surfaces()) < drawn0,
		[map.is_curve_hidden(u), drawn0, map.curve_view_surfaces()])
	panel.model.set_row_hidden("curve:" + u, false)
	panel.model.set_row_hidden("col:" + a, true)
	panel.model.apply()
	await _fold()
	_ok("H2 hiding a collection hides its curves", map.is_curve_hidden(f) and not map.is_curve_hidden(u))
	panel.model.set_row_hidden("col:" + a, false)
	panel.model.set_row_hidden("group:" + cable2, true)
	panel.model.apply()
	await _fold()
	_ok("H3 hiding a cable hides its own curve", map.is_curve_hidden(u2))
	panel.model.set_row_hidden("group:" + cable2, false)
	panel.model.apply()
	await _fold()
	map.select_curve(u)
	map.hide_selection()
	await _fold()
	_ok("H4 H on a selected curve hides it, and deselects", map.is_curve_hidden(u) and map.selected_curve() == ""
		and panel.model.hidden_rows.has("curve:" + u))
	map.unhide_all()
	await _fold()
	_ok("H4 ... Alt+H shows it again", not map.is_curve_hidden(u) and int(map.curve_view_surfaces()) == drawn0)
	panel.model.save(String(panel.get("_save_key")))      # leave the saved view as found

	# E1: aimed right at f's first point, with u selected, Edit curve starts on u.
	var fp: Vector3 = (map.curve_states()[f]["points"] as Array)[0]["p"]
	await _aim_at(fp)
	map.select_curve(u)
	var ops: Node = map.call("_ensure_ops")
	var ce: Node = map.get("_curve_edit")
	var started: bool = ops.activate(ce)
	_ok("E1 Edit curve starts on the SELECTED curve, not the aimed one", started and String(ce.get("_dot")) == u,
		[started, ce.get("_dot")])
	ops.cancel()
	await get_tree().process_frame

	# D1: Shift+D copies the curve into the same collections and selects the copy.
	persist.begin_gesture("link u")
	persist.record("col.link", {"target": a, "members": ["c:" + u]})
	persist.end_gesture()
	await _fold()
	map.select_curve(u)
	var g2: int = (persist.gestures() as Array).size()
	var before: Array = map.curve_states().keys()
	map.duplicate_selection()
	await _fold()
	var made: Array = map.curve_states().keys().filter(func(d: String) -> bool: return not before.has(d))
	_ok("D1 Shift+D makes one curve, one gesture", made.size() == 1 and (persist.gestures() as Array).size() == g2 + 1,
		[made, (persist.gestures() as Array).size() - g2])
	if made.size() == 1:
		var cp: String = made[0]
		_ok("D1 ... the same points", ((map.curve_states()[cp]["points"] as Array)[1]["p"] as Vector3).is_equal_approx(
			(map.curve_states()[u]["points"] as Array)[1]["p"]))
		_ok("D1 ... in the same collection, and selected", (persist.collection_tree()["cols"][a]["members"] as Array).has("c:" + cp)
			and map.selected_curve() == cp)
	# Shift+D then MOVES the copy (Blender's; every duplicate since §17 step 5c): Esc leaves
	# it where it was made.
	var ops_d: Node = map.call("_ensure_ops")
	_ok("D1 ... and Move is up on the copy", ops_d.get("active") == map.get("_move"))
	ops_d.cancel()
	await get_tree().process_frame

	# ---- G (step 4b): G / R on a whole selected curve ----
	var gc: String = map.create_curve([CurveMath.point(O0 + Vector3(0, 0, 30)), CurveMath.point(O0 + Vector3(3.1, 0, 30)),
		CurveMath.point(O0 + Vector3(3.1, 0, 33.7))], false, "Moved")
	await _fold()
	var p0: Array = (map.curve_states()[gc]["points"] as Array).duplicate(true)
	map.select_curve(gc)
	var ops2: Node = map.call("_ensure_ops")
	var mv: Node = map.get("_move")
	var g3: int = (persist.gestures() as Array).size()
	_ok("G1 G starts on a selected curve", ops2.activate(mv))
	mv.set_tool_setting("axis", 0)
	mv.set_tool_setting("distance", 4)
	ops2.confirm()
	await _fold()
	var p1: Array = map.curve_states()[gc]["points"]
	var dd: Vector3 = (p1[0]["p"] as Vector3) - (p0[0]["p"] as Vector3)
	var same := true
	for i in p0.size():
		same = same and ((p1[i]["p"] as Vector3) - (p0[i]["p"] as Vector3)).is_equal_approx(dd)
	_ok("G1 ... every point moves by the same +x", dd.x > 0.0 and is_zero_approx(dd.y) and is_zero_approx(dd.z) and same, dd)
	_ok("G1 ... one gesture, and the curve stays selected", (persist.gestures() as Array).size() == g3 + 1
		and map.selected_curve() == gc)
	# R: 90° about Y through the curve's centre.
	var rt: Node = map.get("_rotate")
	var c0: Vector3 = map.curve_centre(gc)
	var len0 := ((p1[1]["p"] as Vector3) - (p1[0]["p"] as Vector3)).length()
	_ok("G2 R starts on a selected curve", ops2.activate(rt))
	rt.set_tool_setting("angle", 90.0)
	ops2.confirm()
	await _fold()
	var p2: Array = map.curve_states()[gc]["points"]
	var seg: Vector3 = (p2[1]["p"] as Vector3) - (p2[0]["p"] as Vector3)
	_ok("G2 ... turned 90° about Y (the first span now runs along z), same length",
		absf(seg.x) < 1.0e-3 and absf(absf(seg.z) - len0) < 1.0e-3, seg)
	_ok("G2 ... about its centre", map.curve_centre(gc).is_equal_approx(c0), [c0, map.curve_centre(gc)])
	# EVERY handle, exactly: an open end's auto handle is vertical (along its normal), so
	# one end alone cannot tell a turned handle from an unturned one.
	var rot := Basis(Vector3.UP, deg_to_rad(90.0))
	var turned := true
	for i2 in p2.size():
		turned = turned and (p2[i2]["h_out"] as Vector3).is_equal_approx(rot * (p1[i2]["h_out"] as Vector3))
	_ok("G2 ... its handles turned with it", turned and not (p1[1]["h_out"] as Vector3).is_equal_approx(rot * (p1[1]["h_out"] as Vector3)),
		[p1[1]["h_out"], p2[1]["h_out"]])
	# Cancel writes nothing.
	var g4: int = (persist.gestures() as Array).size()
	ops2.activate(mv)
	mv.set_tool_setting("distance", 3)
	ops2.cancel()
	await _fold()
	_ok("G3 Esc writes nothing", (persist.gestures() as Array).size() == g4
		and (map.curve_states()[gc]["points"] as Array)[0]["p"].is_equal_approx(p2[0]["p"]))

	# ---- P (step 5): click a curve's wire to select it (§16.7 Q4: nearer wins) ----
	var was_all: bool = map.overlays_all_on()
	map.set_overlays_all(true)
	map.set_overlay("curves", true)
	map.clear_selection()
	var wire: PackedVector3Array = map.curve_wire(gc)
	var on_wire: Vector3 = wire[wire.size() / 2]
	await _aim_at(on_wire)
	map.call("_pick_element")
	await get_tree().process_frame
	_ok("P1 a click on a curve's wire selects it", map.selected_curve() == gc, map.selected_curve())
	await _aim_at(on_wire + Vector3(6.0, 0, 6.0))       # well off every wire
	map.clear_selection()                               # a click on nothing keeps a selection
	map.call("_pick_element")
	await get_tree().process_frame
	_ok("P2 a click away from every wire picks no curve", map.selected_curve() == "", map.selected_curve())
	panel.model.set_row_hidden("curve:" + gc, true)
	panel.model.apply()
	await _aim_at(on_wire)
	map.clear_selection()
	map.call("_pick_element")
	await get_tree().process_frame
	_ok("P3 a hidden curve cannot be picked", map.selected_curve() == "", map.selected_curve())
	panel.model.set_row_hidden("curve:" + gc, false)
	panel.model.apply()
	panel.model.save(String(panel.get("_save_key")))
	map.set_overlay("curves", false)
	map.clear_selection()
	map.call("_pick_element")
	await get_tree().process_frame
	_ok("P4 ... nor one whose overlay is off", map.selected_curve() == "", map.selected_curve())
	map.set_overlay("curves", true)
	map.set_overlays_all(was_all)

	# P5 / P6: NEARER WINS over the brush hit. The curves are built at the surface the ray
	# ACTUALLY hits (the first brush's top is not the surface everywhere): one resting 0.1 m
	# above it — the floor is hit just behind the wire — is picked; one 2 m UNDER it is not.
	# STRAIGHT (auto = false, zero handles): an open curve's auto end handles leave along the
	# normal, and a 3 m one bows ~1.75 m UP — "under" would cross back above the floor.
	var lvl: Node = map.get("level")
	var plate: int = (lvl.get_live_brush_ids() as PackedInt32Array)[0]
	map.select_brushes(PackedInt32Array([plate]))
	var top: Vector3 = map.selection_centre() + Vector3(0, map.selection_size().y * 0.5, 0)
	map.clear_selection()
	await _aim_at(top + Vector3(0.0, 0.0, 2.17))
	var hit0: Dictionary = map.call("_crosshair_hit")
	var surf: Vector3 = hit0["position"] if not hit0.is_empty() else top
	var on_floor: String = map.create_curve([CurveMath.point(surf + Vector3(-1.53, 0.1, 0)),
		CurveMath.point(surf + Vector3(1.47, 0.1, 0))], false, "On floor", false)
	# 3 m aside in z, so the ray down to it does not pass through "On floor" as well.
	var under: String = map.create_curve([CurveMath.point(surf + Vector3(-1.53, -2.0, 3.0)),
		CurveMath.point(surf + Vector3(1.47, -2.0, 3.0))], false, "Under floor", false)
	await _fold()
	var w5: PackedVector3Array = map.curve_wire(on_floor)
	await _aim_at(w5[w5.size() / 2])
	var hit5: Dictionary = map.call("_crosshair_hit")
	map.clear_selection()
	map.call("_pick_element")
	await get_tree().process_frame
	_ok("P5 a curve ON the floor is picked though the floor is hit behind it", not hit5.is_empty()
		and map.selected_curve() == on_floor, [hit5.is_empty(), map.selected_curve()])
	var w6: PackedVector3Array = map.curve_wire(under)
	await _aim_at(w6[w6.size() / 2] + Vector3(0, 2.0, 0))   # the camera stays above the floor
	var h6: Dictionary = map.call("_crosshair_hit")
	map.clear_selection()
	map.call("_pick_element")
	await get_tree().process_frame
	_ok("P6 a curve UNDER the floor loses to the floor", not h6.is_empty() and map.selected_curve() == "",
		[h6.is_empty(), map.selected_curve()])