OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_axis_local.gd

226 lines · gd ref:
extends Node
## GATE — the axis key twice = the thing's OWN axis, in Move, Rotate and element Move
## (#109, user 2026-10-13: "for knife and in general we need double tap of an axis to
## switch to normal aligned"; Blender's X X). The knife has gate_knife_local.
##
## A plain 2 m cube turned 30° about Y (r):
##   M1  G, X, X: the move axis is r·X and the HUD says local
##   M2  2 steps + Enter: the cube moved by exactly r·X × 2 grid steps
##   R1  R, Z, Z, 90°, Enter: every face normal n became Rot(r·Z, 90°)·n — the turn was
##       about the cube's own Z, not the world's
##   E1  a face selected, element G, X, X: the drag direction is r·X
##   S1  a box SHAPE turned by r, S, X, X, one step, Enter: its size grew on its OWN X
##       only, and its frame is still turned (a world scale of a turned shape is refused)
## Keys through the map's own input path (gate_pivot's).
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_axis_local.tscn

var fails := 0
var checks := 0
## Tilted on TWO axes, so the cube's own X, Y and Z all differ from the world's.
var R := Basis(Vector3.UP, deg_to_rad(30.0)) * Basis(Vector3.RIGHT, deg_to_rad(20.0))
const AXIS_KEYS := [["x", KEY_X, Vector3.RIGHT], ["y", KEY_Y, Vector3.UP], ["z", KEY_Z, Vector3.BACK]]


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 < 19:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE axis_local: %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


## The cube [-1,1]^3 turned by R, centred at c; returns its brush id.
func _cube(map: Node, c: Vector3) -> int:
	var persist: Node = map.get("_persist")
	var pl: Array = []
	for n: Vector3 in [Vector3.RIGHT, Vector3.LEFT, Vector3.UP, Vector3.DOWN, Vector3.BACK, Vector3.FORWARD]:
		var n2: Vector3 = R * n
		pl.append_array([n2.x, n2.y, n2.z, n2.dot(c) + 1.0])
	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 _centre(level: Node, b: int) -> Vector3:
	var s := Vector3.ZERO
	var vs: PackedVector3Array = level.get_brush_edges_of(b)
	for p: Vector3 in vs:
		s += p
	return s / float(maxi(vs.size(), 1))


func _normals(level: Node, b: int) -> Array[Vector3]:
	var out: Array[Vector3] = []
	var pl: PackedFloat32Array = level.get_brush(b).get("planes", PackedFloat32Array())
	for i in pl.size() / 4:
		out.append(Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).normalized())
	return out


func _has_n(ns: Array[Vector3], n: Vector3) -> bool:
	for m: Vector3 in ns:
		if m.distance_to(n) < 1e-3:
			return true
	return false


func _run(map: Node3D) -> void:
	var level: Node = map.get("level")
	var stack: Node = map.call("_ensure_ops")
	var lx: Vector3 = R * Vector3.RIGHT

	print("-- M: G X X --")
	var b := _cube(map, Vector3(0, 0, 0))
	map.select_brushes(PackedInt32Array([b]))
	map.set("_sel_point", Vector3(0, 0, 0))
	var c0 := _centre(level, b)
	map._unhandled_key_input(_key(KEY_G))
	var mv: Object = stack.get("active")
	if not (mv is MoveOperator):
		_ok("M G starts Move", false, mv)
		return
	map._unhandled_key_input(_key(KEY_X))
	map._unhandled_key_input(_key(KEY_X))
	_ok("M1 the axis is the cube's own X", (mv.call("_ax", "x") as Vector3).distance_to(lx) < 1e-3, mv.call("_ax", "x"))
	_ok("M1 the HUD says local", String(mv.status()).contains("local"), mv.status())
	for ak: Array in AXIS_KEYS.slice(1):
		map._unhandled_key_input(_key(ak[1]))
		map._unhandled_key_input(_key(ak[1]))
		var want: Vector3 = R * (ak[2] as Vector3)
		_ok("M1 %s %s: the cube's own %s" % [String(ak[0]).to_upper(), String(ak[0]).to_upper(), String(ak[0]).to_upper()],
			(mv.call("_ax", ak[0]) as Vector3).distance_to(want) < 1e-3 and String(mv.get("_axis")) == ak[0],
			[mv.call("_ax", ak[0]), want])
	map._unhandled_key_input(_key(KEY_X))      # X (world), then X again = local X for M2
	map._unhandled_key_input(_key(KEY_X))
	mv.set_tool_setting("distance", 2)
	var step: float = float(mv.call("grid_step"))
	_ok("M2 Enter commits", bool(stack.call("confirm")))
	await get_tree().process_frame
	var moved := _centre(level, level.brush_by_key(level.get_brush_key(b))) - c0
	_ok("M2 it moved by r·X × 2 steps", moved.distance_to(lx * 2.0 * step) < 1e-3, [moved, lx * 2.0 * step])

	print("-- R: R Z Z 90° --")
	var b2 := _cube(map, Vector3(30, 0, 0))
	map.select_brushes(PackedInt32Array([b2]))
	var before := _normals(level, b2)
	map.set_pivot_mode("bbox")
	map._unhandled_key_input(_key(KEY_R))
	var rot: Object = stack.get("active")
	if not (rot is RotateOperator):
		_ok("R R starts Rotate", false, rot)
		return
	for ak: Array in AXIS_KEYS:
		map._unhandled_key_input(_key(ak[1]))
		map._unhandled_key_input(_key(ak[1]))
		var want_r: Vector3 = R * (ak[2] as Vector3)
		_ok("R0 %s %s: the turn axis is the cube's own %s" % [String(ak[0]).to_upper(), String(ak[0]).to_upper(), String(ak[0]).to_upper()],
			(rot.call("_ax") as Vector3).distance_to(want_r) < 1e-3, [rot.call("_ax"), want_r])
	# ends on Z Z (local Z) — R1 commits about it
	rot.set_tool_setting("angle", 90.0)
	_ok("R1 the turn commits", bool(stack.call("confirm")))
	await get_tree().process_frame
	var turn := Basis((R * Vector3.BACK).normalized(), deg_to_rad(90.0))
	var after := _normals(level, b2)
	var all_ok := true
	for n: Vector3 in before:
		all_ok = all_ok and _has_n(after, turn * n)
	_ok("R1 every face normal turned about the cube's own Z", all_ok, after)

	print("-- E: element G X X --")
	var b3 := _cube(map, Vector3(60, 0, 0))
	map.select_brushes(PackedInt32Array([b3]))
	var top: Vector3 = Vector3(60, 0, 0) + R * Vector3.UP     # the cube's own top face centre
	var idx: PackedInt32Array = map.call("_planes_through", b3, top, 1e-3)
	var keys: PackedStringArray = level.get_brush(b3).get("plane_keys", PackedStringArray())
	var sel := PackedStringArray()
	for i in idx:
		sel.append(keys[i])
	map.set("_sel_mode", "face")
	map.set("_sel_kind", "face")
	map.set("_sel_keys", sel)
	map.set("_sel_point", top)
	var el: Object = map.get("_grab_op")
	if el == null or not stack.call("activate", el):
		_ok("E element grab starts", false, el)
		return
	el.handle_key(_key(KEY_X))
	el.handle_key(_key(KEY_X))
	_ok("E1 the drag is along the cube's own X", (el.get("_dir") as Vector3).distance_to(lx) < 1e-3, el.get("_dir"))
	for ak: Array in AXIS_KEYS.slice(1):
		el.handle_key(_key(ak[1]))
		el.handle_key(_key(ak[1]))
		var want_e: Vector3 = R * (ak[2] as Vector3)
		_ok("E1 %s %s: the drag is along the cube's own %s" % [String(ak[0]).to_upper(), String(ak[0]).to_upper(), String(ak[0]).to_upper()],
			(el.get("_dir") as Vector3).distance_to(want_e) < 1e-3, [el.get("_dir"), want_e])
	stack.call("cancel")

	print("-- S: S X X on a turned shape --")
	var persist: Node = map.get("_persist")
	var sdot: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(R, Vector3(90, 0, 0))),
		"size": [2, 2, 2], "material_id": 2, "level_id": 0})
	persist.refold()
	await get_tree().process_frame
	map.select_shape(sdot)
	map._unhandled_key_input(_key(KEY_S))
	var sc: Object = stack.get("active")
	if not (sc is ScaleOperator):
		_ok("S S starts Scale", false, sc)
		return
	map._unhandled_key_input(_key(KEY_X))
	map._unhandled_key_input(_key(KEY_X))
	_ok("S1 X X on a shape = local", String(sc.get("_space")) == "local", sc.status())
	for ak: Array in AXIS_KEYS.slice(1):
		map._unhandled_key_input(_key(ak[1]))
		map._unhandled_key_input(_key(ak[1]))
		_ok("S1 %s %s on a shape = local %s" % [String(ak[0]).to_upper(), String(ak[0]).to_upper(), String(ak[0]).to_upper()],
			String(sc.get("_space")) == "local" and String(sc.get("_axis")) == ak[0], sc.status())
	map._unhandled_key_input(_key(KEY_X))      # back to local X for the commit
	map._unhandled_key_input(_key(KEY_X))
	# About the centre (the prefs file is shared by every gate: say it here). The grid
	# fit (2026-10-18) runs in the shape's OWN frame: both X faces move one step.
	sc.set_tool_setting("pivot", 1)
	var g: float = float(sc.call("grid_step"))
	var st0: Dictionary = persist.shape_state(sdot)
	var sc0: Vector3 = (st0["frame"] as Transform3D) * ((st0["size"] as Vector3) * 0.5)
	map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
	_ok("S1 Enter commits", bool(stack.call("confirm")))
	await get_tree().process_frame
	var st: Dictionary = persist.shape_state(sdot)
	var size: Vector3 = st.get("size", Vector3.ZERO)
	var fr: Transform3D = st.get("frame", Transform3D.IDENTITY)
	_ok("S1 the size grew on its own X only, a step at each face", absf(size.x - (2.0 + 2.0 * g)) < 1e-3
		and is_equal_approx(size.y, 2.0) and is_equal_approx(size.z, 2.0), [size, 2.0 + 2.0 * g])
	var c1: Vector3 = fr * (size * 0.5)
	_ok("S1 about its centre: the centre stays", c1.distance_to(sc0) < 1e-3, [sc0, c1])
	_ok("S1 the frame is still turned", fr.basis.orthonormalized().is_equal_approx(R), fr.basis)