OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_element_rotate.gd

161 lines · gd ref:
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)