OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_shape_handles.gd

163 lines · gd ref:
extends Node
## GATE — A SELECTED SHAPE'S FACE HANDLES (#120 §17.7 Q3a, §17.8 step 3; TrenchBroom's).
##   K1  a selected shape shows six face handles (map.shape_handles), none with nothing
##       selected
##   K2  pressing on the +X dot starts the handle drag (no tool was up)
##   K3  dragging +X grows the size along x; the -X face does not move
##   K4  release is ONE gesture: shape.size + shape.xform
##   K5  the -X handle grows it the other way: the +X face does not move
##   K6  Esc puts it back and writes nothing
##   K7  Ctrl+Z after a drag restores the size
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_shape_handles.tscn

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


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")
	# A REAL window (gate_click_through): headless is 64x64, and the editor's areas cover
	# all of it, so no click would reach the 3D view (BuildDock.covers).
	get_tree().root.size = Vector2i(1920, 1080)
	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 < 10:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE shape_handles: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


## The pointer at screen position `p` (the virtual pointer = mouse + wrap offset).
func _point(p: Vector2) -> void:
	map.set("_wrap_offset", p - get_viewport().get_mouse_position())
	await get_tree().process_frame


## LMB through the REAL paths: with no tool up a press is the map's click-to-select
## (_unhandled_input), and while a tool is up the map's _input owns the mouse.
func _button(pressed: bool) -> void:
	var mb := InputEventMouseButton.new()
	mb.button_index = MOUSE_BUTTON_LEFT
	mb.pressed = pressed
	mb.position = get_viewport().get_mouse_position() + (map.get("_wrap_offset") as Vector2)
	var ops: Node = map.call("_ensure_ops")
	if ops.get("active") == null:
		map._unhandled_input(mb)
	else:
		map._input(mb)
	await get_tree().process_frame


func _state(d: String) -> Dictionary:
	return persist.shape_state(d)


## World x of the shape's -X and +X faces (an axis-aligned frame).
func _xs(d: String) -> Vector2:
	var st := _state(d)
	var o: Vector3 = (st["frame"] as Transform3D).origin
	return Vector2(o.x, o.x + (st["size"] as Vector3).x)


func _handle(id: String) -> Dictionary:
	for h: Dictionary in map.shape_handles():
		if String(h["id"]) == id:
			return h
	return {}


func _run() -> void:
	persist = map.get("_persist")
	var O0 := Vector3(80.0, 2.0, 80.0)
	persist.begin_gesture("box")
	var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, O0)), "size": [2, 2, 2],
		"material_id": 2, "level_id": 0})
	persist.end_gesture()
	persist.refold()
	cam.global_position = O0 + Vector3(1.0, 9.0, 1.0)
	cam.look_at(O0 + Vector3(1.0, 0.0, 1.0), Vector3.FORWARD)
	await get_tree().process_frame

	# ---- K1 ----
	map.clear_selection()
	_ok("K1 nothing selected: no handles", map.has_method("shape_handles") and (map.shape_handles() as Array).is_empty())
	if not map.has_method("shape_handles"):
		return
	map.select_shape(d)
	var hs: Array = map.shape_handles()
	_ok("K1 a selected shape: six face handles", hs.size() == 6, hs.size())

	# ---- K2..K4 ----
	var x0 := _xs(d)
	var hp: Dictionary = _handle("x+")
	var sp: Vector2 = cam.unproject_position(hp["pos"])
	await _point(sp)
	var g0: int = (persist.gestures() as Array).size()
	await _button(true)
	var ops: Node = map.call("_ensure_ops")
	_ok("K2 pressing on the +X dot starts the handle drag", ops.get("active") != null
		and String(ops.get("active").op_name()) == "face handle", ops.get("active"))
	# 1.5 m further along +x on screen.
	var sp2: Vector2 = cam.unproject_position((hp["pos"] as Vector3) + Vector3(1.5, 0, 0))
	await _point(sp2)
	await _button(false)
	persist.refold()
	var x1 := _xs(d)
	_ok("K3 +X grew (by ~1.5 m, snapped to the grid)", x1.y > x0.y + 1.0, [x0, x1])
	_ok("K3 ... the -X face stayed", is_equal_approx(x1.x, x0.x), [x0.x, x1.x])
	_ok("K4 release: one gesture", (persist.gestures() as Array).size() == g0 + 1 and ops.get("active") == null,
		(persist.gestures() as Array).size() - g0)

	# ---- K5 ----
	map.select_shape(d)
	var hm: Dictionary = _handle("x-")
	await _point(cam.unproject_position(hm["pos"]))
	await _button(true)
	await _point(cam.unproject_position((hm["pos"] as Vector3) - Vector3(1.0, 0, 0)))
	await _button(false)
	persist.refold()
	var x2 := _xs(d)
	_ok("K5 -X grew the other way", x2.x < x1.x - 0.5, [x1, x2])
	_ok("K5 ... the +X face stayed", absf(x2.y - x1.y) < 1.0e-3, [x1.y, x2.y])

	# ---- K6 ----
	map.select_shape(d)
	var g1: int = (persist.gestures() as Array).size()
	var hz: Dictionary = _handle("z+")
	await _point(cam.unproject_position(hz["pos"]))
	await _button(true)
	await _point(cam.unproject_position((hz["pos"] as Vector3) + Vector3(0, 0, 1.0)))
	ops.cancel()
	persist.refold()
	_ok("K6 Esc writes nothing", (persist.gestures() as Array).size() == g1, (persist.gestures() as Array).size() - g1)

	# ---- K7 ----
	persist.undo_last()
	persist.refold()
	_ok("K7 Ctrl+Z restores the size before the last drag", absf(_xs(d).x - x1.x) < 1.0e-3, [_xs(d), x1])