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)