extends Node
## GATE — R on a VERTEX / EDGE / FACE turns that element, not its whole brush (#109,
## user 2026-10-13: "rotate with 3d cursor as pivot doesn't work for edge / vertex, it
## rotates the entire brush instead").
##
## A 2 m cube [-1,1]^3. Each case selects one element the way _pick_element leaves it
## (_sel_kind / _sel_keys / _sel_point / _sel_edge_a,b), presses R, types the angle, Enter:
## V vertex (1,1,1), 45° about Y round the 3D cursor at the origin: the corner is at
## R(v - p) + p, and the opposite corner (-1,-1,-1) has NOT moved
## E edge (1,1,-1)-(1,1,1), 30° about Y round the cursor: both endpoints at R(e - p) + p,
## the far edge (-1,-1,±1) has NOT moved
## F face +y, 20° about X round its own centroid (pivot bbox): a plane with normal
## R·(0,1,0) exists and the bottom face's normal is still (0,-1,0)
## Each starts from a fresh cube. Keys through the map's own input path (gate_pivot's).
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_element_rotate.tscn
var fails := 0
var checks := 0
const CUBE := [1, 0, 0, 1, -1, 0, 0, 1, 0, 1, 0, 1, 0, -1, 0, 1, 0, 0, 1, 1, 0, 0, -1, 1]
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")
var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 8:
await get_tree().process_frame
await _run(map)
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 element_rotate: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _key(code: Key) -> InputEventKey:
var e := InputEventKey.new()
e.keycode = code
e.physical_keycode = code
e.pressed = true
return e
func _cube(map: Node, at: Vector3) -> int:
var persist: Node = map.get("_persist")
var pl: Array = CUBE.duplicate()
for f in 6: # shift the cube to `at`: d += n·at
pl[f * 4 + 3] = float(pl[f * 4 + 3]) + Vector3(pl[f * 4], pl[f * 4 + 1], pl[f * 4 + 2]).dot(at)
var dot: String = persist.record("brush.create", {"planes": pl, "proj": [], "material_id": 2,
"level_id": 0, "sky": false, "group": ""})
persist.refold()
return (map.get("level") as Node).brush_by_key(dot)
func _verts(level: Node, b: int) -> PackedVector3Array:
var out := PackedVector3Array()
for p: Vector3 in level.get_brush_edges_of(b):
var seen := false
for q: Vector3 in out:
if q.distance_to(p) < 1e-3:
seen = true
break
if not seen:
out.append(p)
return out
func _has(vs: PackedVector3Array, p: Vector3) -> bool:
for q: Vector3 in vs:
if q.distance_to(p) < 0.01:
return true
return false
## Select an element as _pick_element would: its incident planes by dot, kind, point.
func _select(map: Node, b: int, kind: String, point: Vector3, a := Vector3.ZERO, bb := Vector3.ZERO) -> void:
var level: Node = map.get("level")
map.select_brushes(PackedInt32Array([b]))
var idx: PackedInt32Array = map.call("_planes_through", b, point, 1e-3)
var keys: PackedStringArray = level.get_brush(b).get("plane_keys", PackedStringArray())
var sel := PackedStringArray()
for i in idx:
sel.append(keys[i])
map.set("_sel_mode", kind)
map.set("_sel_kind", kind)
map.set("_sel_keys", sel)
map.set("_sel_point", point)
map.set("_sel_edge_a", a)
map.set("_sel_edge_b", bb)
func _turn(map: Node, axis: String, deg: float, pivot_mode: String) -> bool:
var stack: Node = map.call("_ensure_ops")
map.set_pivot_mode(pivot_mode)
map._unhandled_key_input(_key(KEY_R))
var rot: Object = stack.get("active")
if not (rot is RotateOperator):
return false
rot.set_tool_setting("axis", rot.axis_index(axis))
rot.set_tool_setting("angle", deg)
return bool(stack.call("confirm"))
func _run(map: Node3D) -> void:
var level: Node = map.get("level")
var cur: Node3D = map.get("_cursor3d")
cur.global_position = (level as Node3D).global_transform * Vector3.ZERO
print("-- V: a vertex, 45° about Y round the cursor --")
var b := _cube(map, Vector3(0, 0, 0))
_select(map, b, "vertex", Vector3(1, 1, 1))
_ok("V the turn commits", _turn(map, "y", 45.0, "cursor"))
await get_tree().process_frame
var vs := _verts(level, b)
var want_v: Vector3 = Basis(Vector3.UP, deg_to_rad(45.0)) * Vector3(1, 1, 1)
_ok("V the corner is at R(v - p) + p", _has(vs, want_v), [want_v, vs])
_ok("V the opposite corner did not move (the brush was not turned)", _has(vs, Vector3(-1, -1, -1)), vs)
print("-- E: an edge, 30° about Y round the cursor --")
var b2 := _cube(map, Vector3(20, 0, 0))
cur.global_position = (level as Node3D).global_transform * Vector3(20, 0, 0)
_select(map, b2, "edge", Vector3(21, 1, 0), Vector3(21, 1, -1), Vector3(21, 1, 1))
_ok("E the turn commits", _turn(map, "y", 30.0, "cursor"))
await get_tree().process_frame
var vs2 := _verts(level, b2)
var r30 := Basis(Vector3.UP, deg_to_rad(30.0))
var ea: Vector3 = r30 * Vector3(1, 1, -1) + Vector3(20, 0, 0)
var eb: Vector3 = r30 * Vector3(1, 1, 1) + Vector3(20, 0, 0)
_ok("E both endpoints at R(e - p) + p", _has(vs2, ea) and _has(vs2, eb), [ea, eb])
_ok("E the far edge did not move", _has(vs2, Vector3(19, -1, -1)) and _has(vs2, Vector3(19, -1, 1)), vs2)
print("-- F: the +y face, 20° about X round its own centroid --")
var b3 := _cube(map, Vector3(40, 0, 0))
_select(map, b3, "face", Vector3(40, 1, 0))
_ok("F the turn commits", _turn(map, "x", 20.0, "bbox"))
await get_tree().process_frame
var pl: PackedFloat32Array = level.get_brush(b3).get("planes", PackedFloat32Array())
var want_n: Vector3 = Basis(Vector3.RIGHT, deg_to_rad(20.0)) * Vector3.UP
var turned := false
var bottom := false
for i in pl.size() / 4:
var n := Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2])
turned = turned or n.distance_to(want_n) < 1e-3
bottom = bottom or n.distance_to(Vector3.DOWN) < 1e-3
_ok("F a plane with the turned normal exists", turned, want_n)
_ok("F the bottom face is unchanged (the brush was not turned)", bottom)