OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_rotate_cursor.gd

134 lines · gd ref:
extends Node
## GATE — rotating about the 3D CURSOR keeps each thing's offset from it (#109, user
## 2026-10-13: "it rotates and then sets the cube's position as that of the 3d cursor
## ... making an archway of cubes, rotate them all around the 3d cursor").
##
## gate_pivot only checks that R PICKS the cursor as its pivot, then cancels: no rotation
## was ever committed about it, and never on a parametric shape. Here, with the cursor at
## the origin and a cube centred 10 m away along +x, R 90° about Y is committed and:
##   B1/S1  the cube is still 10 m from the cursor (it orbited, it did not collapse onto it)
##   B2/S2  its centre is where x' = R(x - p) + p puts it: (0, y, -10)
## for B = a plain brush, S = a parametric box shape (the create tool's output).
## Keys go through the real input path (R) where gate_pivot already does.
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_rotate_cursor.tscn

var fails := 0
var checks := 0


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 < 6:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE rotate_cursor: %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 _centre(level: Node, ids: PackedInt32Array) -> Vector3:
	var lo := Vector3.INF
	var hi := -Vector3.INF
	for id: int in ids:
		var d: Dictionary = level.get_brush(id)
		if bool(d.get("clearance", false)) or not d.has("aabb_min"):
			continue
		lo = lo.min(d["aabb_min"] as Vector3)
		hi = hi.max(d["aabb_max"] as Vector3)
	return (lo + hi) * 0.5


## R, 90° about Y, Enter — the user's gesture, with the pivot on the cursor.
func _rotate_90(map: Node, stack: Node) -> bool:
	map.set_pivot_mode("cursor")
	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("y"))
	rot.set_tool_setting("angle", 90.0)
	return bool(stack.call("confirm"))


func _run(map: Node3D) -> void:
	var persist: Node = map.get("_persist")
	var level: Node = map.get("level")
	var stack: Node = map.call("_ensure_ops")
	var cur: Node3D = map.get("_cursor3d")
	cur.global_position = (level as Node3D).global_transform * Vector3.ZERO     # cursor at the level origin
	var want := Vector3(0, 0.5, -10)          # (10, 0.5, 0) turned +90° about Y through the origin

	print("-- B: a plain brush --")
	var bdot: String = persist.record("brush.create", {"planes": [1, 0, 0, 10.5, -1, 0, 0, -9.5,
		0, 1, 0, 1, 0, -1, 0, 0, 0, 0, 1, 0.5, 0, 0, -1, 0.5], "proj": [], "material_id": 2,
		"level_id": 0, "sky": false, "group": ""})
	persist.refold()
	var bid: int = level.brush_by_key(bdot)
	map.select_brushes(PackedInt32Array([bid]))
	_ok("B0 the brush starts 10 m from the cursor", is_equal_approx(_centre(level, PackedInt32Array([bid])).distance_to(Vector3(0, 0.5, 0)), 10.0),
		_centre(level, PackedInt32Array([bid])))
	_ok("B rotate 90 about the cursor commits", _rotate_90(map, stack))
	await get_tree().process_frame
	var bc := _centre(level, PackedInt32Array([level.brush_by_key(bdot)]))
	_ok("B1 still 10 m from the cursor", absf(bc.distance_to(Vector3(0, 0.5, 0)) - 10.0) < 0.05, bc)
	_ok("B2 at R(x - p) + p", bc.distance_to(want) < 0.05, bc)

	print("-- S: a parametric box shape --")
	var sdot: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(9.5, 0, 0))),
		"size": [1, 1, 1], "material_id": 2, "level_id": 0})
	persist.refold()
	await get_tree().process_frame
	var sids: PackedInt32Array = level.get_group_brush_ids(sdot)
	var sc0 := _centre(level, sids)
	map.select_shape(sdot)
	_ok("S0 the shape exists, %d piece(s), centre %s" % [sids.size(), sc0], sids.size() > 0)
	var off0 := sc0 - Vector3(0, sc0.y, 0)
	_ok("S rotate 90 about the cursor commits", _rotate_90(map, stack))
	await get_tree().process_frame
	await get_tree().process_frame
	var sc := _centre(level, level.get_group_brush_ids(sdot))
	var off := sc - Vector3(0, sc.y, 0)
	_ok("S1 still the same distance from the cursor", absf(off.length() - off0.length()) < 0.05,
		[off0.length(), off.length(), sc])
	var want_s := Vector3(off0.z, sc0.y, -off0.x)      # +90° about Y: (x, z) -> (z, -x)
	_ok("S2 at R(x - p) + p", sc.distance_to(want_s) < 0.05, [sc, want_s])

	# S3: AGAIN, without reselecting — the arch is built by repeated turns, and every
	# shape edit regenerates its pieces under NEW ids (_reselect_shape re-takes them).
	_ok("S3 a second rotate commits", _rotate_90(map, stack))
	await get_tree().process_frame
	await get_tree().process_frame
	var sc2 := _centre(level, level.get_group_brush_ids(sdot))
	var want_s2 := Vector3(want_s.z, sc0.y, -want_s.x)
	_ok("S3 still the same distance after the second turn",
		absf(Vector2(sc2.x, sc2.z).length() - off0.length()) < 0.05, sc2)
	_ok("S3 at R(R(x - p)) + p", sc2.distance_to(want_s2) < 0.05, [sc2, want_s2])
	print("   selection after the second turn: shape=%s brushes=%s" % [map.get("_sel_shape"), map.get("_sel_brushes")])