extends SceneTree
## GATE — editor redesign E5 (docs/editor-redesign.md §8b), on a real map_flat.
## E5a GESTURES: a parametric stairs is ONE gesture of one shape.create with no Adjust
## Last (§13 of parametric-shapes-design — its params live in the Shape panel); a
## 3-brush scale (three brush.scale) is ONE gesture — one Ctrl+Z puts all three
## back, one redo scales them again;
## E5b ADJUST LAST: after the scale, changing Steps 1 -> 3 is one undo of that gesture +
## one new gesture, and the brushes grew further; the panel shows; another edit ends it;
## E5c a two-brush rotate through the real keys is one gesture — one Ctrl+Z restores both;
## E5d a record written outside any gesture is a gesture of one (the legacy shape);
## E5e HISTORY lists gestures newest first with labels and counts, undone ones marked;
## seek to an older row undoes the newer ones, seek to the newest redoes them.
##
## Run: HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_editor_e5.gd
const PINNED := ["tool.box.shape", "tool.box.size", "tool.box.height", "tool.box.p.stairs.steps",
"tool.box.p.arch.segments", "tool.rotate.step", "snap", "grid"]
var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _level: Node
var _persist: Node
var _dock: Node
var _stack: Node
var _hist: Node
var _saved: Dictionary = {}
var _a := -1
var _b := -1
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _key(code: int) -> InputEventKey:
var e := InputEventKey.new()
e.physical_keycode = code
e.keycode = code
e.pressed = true
return e
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
func _make(lo: Vector3, hi: Vector3) -> String:
var ft: Array = []
for i in 6:
ft.append({"tex": "kenney-prototype/dark_01", "mode": 1})
var dot: String = _persist.record("brush.create", {"planes": _box(lo, hi), "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "face_tex": ft})
_persist.refold()
return dot
func _bounds(b: int) -> AABB:
var d: Dictionary = _level.get_brush(b)
return AABB(d["aabb_min"], (d["aabb_max"] as Vector3) - (d["aabb_min"] as Vector3))
func _count() -> int:
return int(_level.get_brush_count())
func _process(_d: float) -> bool:
_frame += 1
if _frame == 1:
_dock = root.get_node("/root/BuildDock")
for k: String in PINNED:
_saved[k] = _dock._prefs.get(k, null)
_dock._prefs.erase(k)
_dock.set_pref("snap", true)
_dock.set_pref("grid", 5)
_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
root.add_child(_map)
return false
if _frame < 30:
return false
_run()
return _finish()
func _run() -> void:
_level = _map.get_node("DCLevel")
_persist = _map.get("_persist")
var keet: Node = root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
var gs: Node = root.get_node_or_null("/root/GameState")
if gs != null:
gs.route("world:level:*", 1, "peer")
root.get_node("/root/GameMode").set_mode("build")
_stack = _map.call("_ensure_ops")
_hist = _map.get("_history")
_p(_hist != null and _hist.list() != null, "the map registered the History panel")
# §13 (2026-10-12): the create tool makes PARAMETRIC shapes — one shape.create, its
# params editable forever in the Shape panel, so no Adjust Last. That is checked first;
# the multi-record gesture and Adjust Last are then exercised on a 3-brush SCALE (one
# brush.scale per brush; Steps is adjustable), which is what the stairs used to stand in for.
print("-- E5a: a parametric stairs is one gesture of one record, no Adjust Last --")
var bx: CreateBoxOperator = _map.get("_box")
bx.set_tool_setting("shape", CreateBoxOperator.Shape.STAIRS)
bx.set_tool_setting("size", 4)
_place(bx)
# Headless has no aimed surface, so the box is placed by hand and made the active tool
# directly; the commit then goes through the REAL OperatorStack.confirm (the gesture).
_stack._keymap_tool(true)
_stack.active = bx
_p(_stack.confirm(), "the stairs commit through the stack")
_persist.refold()
var g: Array = _persist.gestures()
_p(g.size() > 0 and String(g[0]["label"]) == "Add Stairs" and int(g[0]["count"]) == 1,
"ONE gesture 'Add Stairs' of 1 record (shape.create)", g[0] if g.size() > 0 else "none")
_p(_stack.adjustable() == null, "no Adjust Last — its params live in the Shape panel", _stack.adjustable())
print("-- E5a: a 3-brush scale is one gesture --")
var trio: Array = []
for i in 3:
trio.append(_level.brush_by_key(_make(Vector3(50 + i * 3, 0, 0), Vector3(51 + i * 3, 1, 1))))
var b0: Array = trio.map(_bounds)
_map.select_brushes(PackedInt32Array(trio))
_map._unhandled_key_input(_key(KEY_S)) # S since keymap step 3 (was O)
var sc: Object = _stack.active
_p(sc is ScaleOperator, "S starts the scale tool on the three", sc)
if not (sc is ScaleOperator):
return
sc.set_tool_setting("pivot", 1)
sc.set_tool_setting("steps", 1)
_p(_stack.confirm(), "the scale commits through the stack")
_persist.refold()
var b1: Array = trio.map(_bounds)
g = _persist.gestures()
_p(g.size() > 0 and int(g[0]["count"]) == 3 and String(g[0]["label"]) == String(sc.gesture_label()),
"ONE gesture '%s' of 3 records" % sc.gesture_label(), g[0] if g.size() > 0 else "none")
print("-- E5b: adjust last --")
_p(_stack.adjustable() == sc and _stack.adjust_valid(), "the scale is adjustable")
_p(_hist.adjust_bar() != null and _dock.has_panel("adjust_last"), "the Adjust Last panel is up")
var old_g := String(_stack.adjust_gesture())
_p(_stack.adjust("steps", 3), "Steps 1 -> 3 re-runs")
_persist.refold()
var b3: Array = trio.map(_bounds)
_p((b3[0] as AABB).size.x > (b1[0] as AABB).size.x + 0.01, "the brushes grew further",
"%s -> %s" % [(b1[0] as AABB).size, (b3[0] as AABB).size])
g = _persist.gestures()
_p(String(g[0]["g"]) != old_g and not bool(g[0]["undone"]) and int(g[0]["count"]) == 3,
"a NEW live gesture of 3", g[0])
var old_e: Dictionary = {}
for e: Dictionary in _persist.gestures(true):
if String(e["g"]) == old_g:
old_e = e
_p(not old_e.is_empty() and bool(old_e["undone"]), "the original stairs gesture is undone (kept in the journal)")
var offered := false
for e: Dictionary in g:
offered = offered or String(e["g"]) == old_g
_p(not offered, "and DISCARDED from history: the adjusted one replaced it")
_p(_stack.adjust_valid(), "still adjustable after adjusting")
print("-- E5a: one Ctrl+Z / one redo --")
var same := func(a: Array, b: Array) -> bool:
for i in a.size():
if not (a[i] as AABB).is_equal_approx(b[i] as AABB):
return false
return true
_map._undo_redo(false)
_p(same.call(trio.map(_bounds), b0), "ONE undo puts all three back", trio.map(_bounds))
_p(not _dock.has_panel("adjust_last"), "the undo ended the Adjust Last offer")
_map._undo_redo(true)
_p(same.call(trio.map(_bounds), b3), "ONE redo scales all three again (3 steps)")
_map._undo_redo(true)
_p(same.call(trio.map(_bounds), b3), "a second redo does NOT resurrect the replaced 1-step scale")
print("-- E5c: a two-brush rotate is one gesture --")
_a = _level.brush_by_key(_make(Vector3(30, 0, 0), Vector3(32, 1, 1)))
_b = _level.brush_by_key(_make(Vector3(34, 0, 0), Vector3(36, 1, 1)))
var ba := _bounds(_a)
var bb := _bounds(_b)
_map.select_brushes(PackedInt32Array([_a, _b]))
_map._unhandled_key_input(_key(KEY_R))
for i in 6:
_map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
_map._unhandled_key_input(_key(KEY_ENTER))
_p(_stack.active == null and not _bounds(_a).is_equal_approx(ba), "Enter rotated the pair")
g = _persist.gestures()
_p(String(g[0]["label"]) == "Rotate" and int(g[0]["count"]) == 2, "one gesture 'Rotate' of 2", g[0])
_map._undo_redo(false)
_p(_bounds(_a).is_equal_approx(ba) and _bounds(_b).is_equal_approx(bb), "ONE undo restores both brushes")
print("-- E5d: a record outside any gesture --")
var dot := _make(Vector3(40, 0, 0), Vector3(41, 1, 1))
g = _persist.gestures()
_p(String(g[0]["g"]) == dot and int(g[0]["count"]) == 1 and String(g[0]["label"]) == "Create",
"is a gesture of one, named by its kind", g[0])
print("-- E5e: history --")
_hist.refresh()
var rows: Array = _hist.rows()
var labels: Array = rows.map(_label_of)
_p(not labels.has("Rotate · 2"), "the rotate undone BEFORE that create is discarded (a new edit ends the redo branch)",
str(labels))
_p(labels.slice(0, 4) == ["Create", "Create", "Create", "Scale · 3"], "newest first, with counts", str(labels))
_map._undo_redo(false) # undo the newest create: it stays on the redo branch
_hist.refresh()
rows = _hist.rows()
_p(String(_hist.row_text(0)) == "Create" and bool(rows[0]["undone"]), "an undone gesture is listed and marked")
_map._undo_redo(true)
_hist.refresh()
rows = _hist.rows()
var stairs_row := -1
for i in rows.size():
if String(rows[i]["label"]) == "Scale" and not bool(rows[i]["undone"]):
stairs_row = i
_p(stairs_row > 1 and String(_hist.row_text(stairs_row)) == "Scale · 3", "the scale row", _hist.row_text(stairs_row))
# Counted, not looked up: brush_by_key still answers for a brush whose create is undone.
var c_all := _count()
_p(_hist.seek_row(stairs_row) == 3, "seek to the scale changes the 3 newer gestures")
_p(_count() == c_all - 3, "the three newer creates are gone, the scale stays", c_all - _count())
_p(bool((_persist.gestures() as Array)[0]["undone"]), "and the history marks them undone")
_hist.seek_row(0)
_p(_count() == c_all, "seek to the newest row redoes them", _count())
_p(_bounds(_a).is_equal_approx(ba), "but not the discarded rotate")
print("-- flatten from the History panel --")
var fm: Button = _hist.get("_flatten_map_btn")
_p(fm != null and _hist.get("_flatten_room_btn") != null, "the panel has Flatten map / Flatten room")
keet.current_role = "peer"
_hist.refresh()
_p(fm != null and fm.disabled, "a peer cannot flatten (it erases every author's records)")
_hist.ask_flatten("map")
_p(String(_hist.get("_confirm_action")) == "", "and asking does nothing")
keet.current_role = "admin"
_hist.refresh()
_p(fm != null and not fm.disabled, "the admin can")
var c_before := _count()
_hist.ask_flatten("map")
_p(String(_hist.get("_confirm_action")) == "map" and String((_hist.get("_confirm") as ConfirmationDialog).dialog_text).contains("records"),
"asking opens the confirmation with the record count")
_p(not (_persist.gestures() as Array).is_empty(), "nothing flattened before confirming")
_hist.confirm_flatten()
_p((_persist.gestures() as Array).is_empty() and _hist.rows().is_empty(), "confirmed: History is empty", _hist.rows().size())
_p(_count() == c_before, "and the level is unchanged", _count() - c_before)
root.get_node("/root/GameMode").set_mode("play")
func _label_of(e: Dictionary) -> String:
var n := int(e["count"])
return String(e["label"]) + ((" · %d" % n) if n > 1 else "")
func _place(bx: CreateBoxOperator) -> void:
bx.set("_origin", Vector3(20, 0, 20))
bx.set("_normal", Vector3.UP)
bx.set("_axis", 1)
bx.set("_view", Vector3(0, 0, 1))
bx.set("_valid", true)
func _finish() -> bool:
for k: String in PINNED:
if _saved.get(k) == null:
_dock._prefs.erase(k)
else:
_dock._prefs[k] = _saved[k]
_dock._save_layout()
if checks < 24:
_p(false, "only %d checks ran" % checks)
print("GATE editor_e5: %s (%d checks)" % ["PASS" if ok else "FAIL", checks])
quit(0 if ok else 1)
return true