extends Node
## GATE — ONE MODAL CONVENTION (#120 F3, ModalControls).
## M1 a tool's key table IS its status line: its rows, the values, confirm / cancel / H
## M2 ... and its help overlay: every row, with the current state inline; H opens and
## closes it
## M3 bound values: ] steps the focused value, Shift+] a tenth of a step, [ back; Tab
## moves the focus to the next value
## M4 MIRROR, migrated, through the map's real key path: H shows the overlay in the HUD;
## Y changes the axis and both the status line and the overlay say so; Enter mirrors
## and a summary card says what happened
## M5 EXTRUDE, migrated: its distance is a bound value — [ ] and Shift+[ ] step whole
## grid steps (finer than the grid means nothing there), the unit follows the grid,
## the aim's state is a row, a confirm shows the summary card
## M6 ROTATE, migrated: the angle is bound — ] the sticky step, Shift+] 1°, and a
## changed step setting is used at once; Z then Z again reads "local Z"
## M7 SCALE, migrated: the size steps name the grid step and show the factor; C toggles the pivot row
## M8 MOVE, migrated: axis mode shows the signed distance and no plane row; Shift+Y locks
## Y (move in the plane) and the plane's offset row appears
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_modal_controls.tscn
var fails := 0
var checks := 0
class TestOp extends LevelOperator:
var axis := "x"
var width := 1.0
var count := 3.0
func op_name() -> String:
return "test"
func setup_modal(m: ModalControls) -> void:
m.confirm_label = "apply"
m.table([["X Y Z", "axis", func() -> String: return axis.to_upper()]])
m.bind("width", "width", func() -> float: return width, func(v: float) -> void: width = v, 0.5, "m")
m.bind("count", "count", func() -> float: return count, func(v: float) -> void: count = v, 1.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 _key(code: int, shift := false) -> InputEventKey:
var e := InputEventKey.new()
e.physical_keycode = code
e.keycode = code
e.shift_pressed = shift
e.pressed = true
return e
func _ready() -> void:
_unit()
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_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
await _mirror(map)
await _extrude(map)
await _rotate(map)
await _scale(map)
await _move(map)
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 34:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE modal_controls: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _unit() -> void:
var op := TestOp.new()
var m := op.modal()
var st := m.status_text()
_ok("M1 the status line has the tool's row with its state", st.contains("X Y Z axis: X"), st)
_ok("M1 ... the focused value with its keys, the other value plain", st.contains("[ ] width: 1m") and st.contains("count: 3")
and not st.contains("next value"), st)
_ok("M1 ... confirm, cancel and help", st.contains("Enter apply") and st.contains("Esc cancel") and st.contains("H this help"), st)
var help := m.help_text("TEST")
var all_rows := true
for r: Array in m.rows():
all_rows = all_rows and help.contains(String(r[0])) and help.contains(String(r[1]))
_ok("M2 the help overlay lists every row", all_rows, help)
_ok("M2 ... with the state inline", help.contains("[color=#7fe3b0]X[/color]") and help.contains("1m"))
_ok("M2 H opens it", m.handle_key(_key(KEY_H)) and m.help_open)
m.handle_key(_key(KEY_H))
_ok("M2 ... and H closes it", not m.help_open)
m.handle_key(_key(KEY_BRACKETRIGHT))
_ok("M3 ] steps the focused value", is_equal_approx(op.width, 1.5), op.width)
m.handle_key(_key(KEY_BRACKETRIGHT, true))
_ok("M3 Shift+] a tenth of a step", is_equal_approx(op.width, 1.55), op.width)
m.handle_key(_key(KEY_BRACKETLEFT))
_ok("M3 [ steps back", is_equal_approx(op.width, 1.05), op.width)
m.handle_key(_key(KEY_TAB))
m.handle_key(_key(KEY_BRACKETRIGHT))
_ok("M3 Tab moves the focus: ] now steps the count", is_equal_approx(op.count, 4.0) and is_equal_approx(op.width, 1.05)
and m.focused() == "count", [op.count, op.width, m.focused()])
op.free()
func _mirror(map: Node3D) -> void:
var level: Node = map.get("level")
var hud: Node = get_tree().root.get_node("/root/BuildHud")
var b: int = (level.get_live_brush_ids() as PackedInt32Array)[0]
map.set("_sel_mode", "brush")
map.select_brushes(PackedInt32Array([b]))
var ops: Node = map.call("_ensure_ops")
ops.activate(map.get("_mirror"))
map._update_op_hud()
_ok("M4 Mirror is up, its status from the table", String(ops.status_line()).contains("X Y Z axis: X"), ops.status_line())
map._unhandled_key_input(_key(KEY_H))
_ok("M4 H through the map shows the help in the HUD", String(hud.tool_help_text()).contains("MIRROR")
and String(hud.tool_help_text()).contains("axis"), hud.tool_help_text())
map._unhandled_key_input(_key(KEY_Y))
_ok("M4 Y: the status line says axis Y", String(ops.status_line()).contains("axis: Y"), ops.status_line())
_ok("M4 ... and so does the overlay", String(hud.tool_help_text()).contains("[color=#7fe3b0]Y[/color]"), hud.tool_help_text())
ops.confirm()
await get_tree().process_frame
_ok("M4 the confirm shows a summary card", String(hud.summary_text()).begins_with("Mirrored 1 brush(es) across Y"), hud.summary_text())
_ok("M4 the help closes with the tool", String(hud.tool_help_text()) == "", hud.tool_help_text())
func _extrude(map: Node3D) -> void:
var level: Node = map.get("level")
var hud: Node = get_tree().root.get_node("/root/BuildHud")
var cam := Camera3D.new()
add_child(cam)
cam.current = true
# Aim straight down at the top of the first brush.
var b: int = (level.get_live_brush_ids() as PackedInt32Array)[0]
map.select_brushes(PackedInt32Array([b]))
var top: Vector3 = map.selection_centre() + Vector3(0, map.selection_size().y * 0.5, 0)
map.clear_selection()
cam.global_position = top + Vector3(0.3, 3.0, 0.3)
cam.look_at(top, Vector3.UP)
await get_tree().process_frame
var ops: Node = map.call("_ensure_ops")
var ex: Node = map.get("_extrude")
_ok("M5 Extrude activates on the aimed face", ops.activate(ex))
var st0 := String(ex.status())
_ok("M5 its status: the distance with its keys and the grid unit", st0.contains("[ ] distance: +0 × "), st0)
map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
map._unhandled_key_input(_key(KEY_BRACKETRIGHT, true))
_ok("M5 ] and Shift+] each step one whole grid step", String(ex.status()).contains("distance: +2 × "), ex.status())
var unit_before := String(ex.status()).get_slice("× ", 1).get_slice(" ", 0)
map.set("_grid_idx", int(map.get("_grid_idx")) + 1)
var unit_after := String(ex.status()).get_slice("× ", 1).get_slice(" ", 0)
_ok("M5 the unit follows the grid", unit_before != unit_after, [unit_before, unit_after])
ops.confirm()
await get_tree().process_frame
_ok("M5 the confirm shows a summary card", String(hud.summary_text()).begins_with("Extruded a face +2"), hud.summary_text())
func _rotate(map: Node3D) -> void:
var level: Node = map.get("level")
var b: int = (level.get_live_brush_ids() as PackedInt32Array)[1]
map.set("_sel_mode", "brush")
map.select_brushes(PackedInt32Array([b]))
var ops: Node = map.call("_ensure_ops")
var ro: Node = map.get("_rotate")
_ok("M6 Rotate activates on the selection", ops.activate(ro))
ro.set_tool_setting("step", 0) # 15° (whatever this machine last used)
_ok("M6 its status: the axis and the angle", String(ro.status()).contains("X Y Z axis: Y")
and String(ro.status()).contains("[ ] angle: +0°"), ro.status())
map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
map._unhandled_key_input(_key(KEY_BRACKETRIGHT, true))
_ok("M6 ] the 15° step, Shift+] 1°", String(ro.status()).contains("angle: +16°"), ro.status())
ro.set_tool_setting("step", 3) # 45°
map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
_ok("M6 a changed step is used at once", String(ro.status()).contains("angle: +61°"), ro.status())
map._unhandled_key_input(_key(KEY_Z))
map._unhandled_key_input(_key(KEY_Z))
_ok("M6 Z, Z again: local Z", String(ro.status()).contains("axis: local Z"), ro.status())
ops.cancel()
func _scale(map: Node3D) -> void:
var level: Node = map.get("level")
map.set("_sel_mode", "brush")
map.select_brushes(PackedInt32Array([(level.get_live_brush_ids() as PackedInt32Array)[1]]))
var ops: Node = map.call("_ensure_ops")
var sc: Node = map.get("_scale")
_ok("M7 Scale activates on the selection", ops.activate(sc))
map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
_ok("M7 ] one size step, the factor shown", String(sc.status()).contains("[ ] size: +1 steps of ") and String(sc.status()).contains("(×"), sc.status())
var before := String(sc.status()).contains("pivot: base")
map._unhandled_key_input(_key(KEY_C))
_ok("M7 C toggles the pivot row", String(sc.status()).contains("pivot: base") != before, sc.status())
ops.cancel()
func _move(map: Node3D) -> void:
var level: Node = map.get("level")
map.set("_sel_mode", "brush")
map.select_brushes(PackedInt32Array([(level.get_live_brush_ids() as PackedInt32Array)[1]]))
var ops: Node = map.call("_ensure_ops")
var mv: Node = map.get("_move")
_ok("M8 Move activates on the selection", ops.activate(mv))
map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
var st := String(mv.status())
_ok("M8 axis mode: the signed distance, no plane row", (st.contains("[ ] distance: +") or st.contains("[ ] distance: -"))
and not st.contains("plane offset"), st)
map._unhandled_key_input(_key(KEY_Y, true))
st = String(mv.status())
_ok("M8 Shift+Y: plane mode, Y locked, its offset row", st.contains("Y locked") and st.contains("plane offset: "), st)
_ok("M8 ... and the axis distance is not shown", not st.contains("distance:"), st)
ops.cancel()