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])