extends Node
## GATE — THE STACK PANEL (docs/cutting-and-operations.md §9b).
##
## 1. ROWS: a wall with two live cuts -> the panel shows the generator + 2 op rows, in
## stack order
## 2. EYE: switching op 1 off makes its door solid again, and its cutter STAYS a
## cutter (wire) — one op.set, one Ctrl+Z brings the cut back
## 3. KIND: op 2 subtract -> intersect (op.set)
## (no ORDER section: subtract / intersect / slice commute as sets — §9b)
## 5. REMOVE: ✕ deletes an op (op.remove) — one row fewer; Ctrl+Z restores it
## 6. CUTTER: selecting a cutter shows its "cuts" row
## 8. MODIFIERS (#120 F2), through the widgets: Add modifier ▸ Mirror adds a row BEFORE
## the cuts; its axis dropdown sets y; a chamfer row comes AFTER the cuts (the fixed
## stage order); the eye switches a modifier off; ✕ removes it
## 9. GENERATOR row: the dropdown assigns stairs; a plain brush shows Make parametric,
## and pressing it makes a shape
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_stack_panel.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 6:
await get_tree().process_frame
await _run(map)
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 24:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE stack_panel: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _solid(level: Node, p: Vector3) -> bool:
return not (level.raycast(p + Vector3(0, 0, 6), p + Vector3(0, 0, -6)) as Dictionary).is_empty()
func _box(persist: Node, origin: Vector3, size: Vector3) -> String:
return persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, origin)),
"size": [size.x, size.y, size.z], "material_id": 2, "level_id": 0})
func _kinds(persist: Node, since: int) -> Array:
var out: Array = []
for r: Dictionary in (persist.records() as Array):
if int(r["seq"]) > since:
out.append(String(r["kind"]))
out.sort()
return out
## The op rows the panel is showing: [{dot, kind, on}] in order.
func _rows(panel: Node) -> Array:
panel.call("refresh")
return panel.call("op_rows")
func _run(map: Node3D) -> void:
var persist: Node = map.get("_persist")
var level: Node = map.get("level")
var panel: Node = map.get("_stack_panel")
_ok("the Stack panel exists", panel != null)
if panel == null or not map.has_method("op_stack"):
_ok("the map has the stack API (op_stack, set_op, move_op, remove_op)", false)
return
const W := Vector3(60, 0, 60)
var wall: String = _box(persist, W, Vector3(8, 4, 1))
var c1: String = _box(persist, W + Vector3(1, -1, -1), Vector3(2, 4, 3)) # door at x 1..3
var c2: String = _box(persist, W + Vector3(5, -1, -1), Vector3(2, 4, 3)) # door at x 5..7
persist.record("op.create", {"op": "subtract", "cutter": c1, "targets": [wall]})
persist.record("op.create", {"op": "subtract", "cutter": c2, "targets": [wall]})
persist.refold()
map.select_shape(wall)
# ---- 1. rows ----
var rows: Array = _rows(panel)
_ok("1. the subject is the wall", String(panel.call("subject")) == wall, panel.call("subject"))
_ok("1. two op rows", rows.size() == 2, rows)
_ok("1. ... in stack order (c1's op first)", rows.size() == 2 and String(rows[0]["cutter"]) == c1, rows)
_ok("1. both doors are air", not _solid(level, W + Vector3(2, 1, 0)) and not _solid(level, W + Vector3(6, 1, 0)))
if rows.size() != 2:
return
var op1 := String(rows[0]["dot"])
var op2 := String(rows[1]["dot"])
# ---- 2. eye ----
var s0: int = persist._max_seq_seen()
panel.call("press", op1, "eye")
_ok("2. eye off: ONE op.set", _kinds(persist, s0) == ["op.set"], _kinds(persist, s0))
_ok("2. door 1 is solid again", _solid(level, W + Vector3(2, 1, 0)))
_ok("2. door 2 is still air", not _solid(level, W + Vector3(6, 1, 0)))
_ok("2. c1 is STILL a cutter (wire, not solid)", (level.get_cutter_brush_ids() as PackedInt32Array).size() == 2)
_ok("2. the row reads off", not bool(_rows(panel)[0]["on"]))
persist.undo_last()
persist.refold()
_ok("2. Ctrl+Z: door 1 is air again", not _solid(level, W + Vector3(2, 1, 0)))
# ---- 3. kind ----
panel.call("press", op2, "kind", "intersect")
_ok("3. op 2 reads intersect", String(_rows(panel)[1]["kind"]) == "intersect", _rows(panel))
# subtract c1 then intersect c2: only the overlap with c2 remains (x 5..7), minus nothing.
_ok("3. intersect after subtract keeps only c2's overlap", _solid(level, W + Vector3(6, 1, 0))
and not _solid(level, W + Vector3(4, 1, 0)))
# ---- 5. remove ----
var s5: int = persist._max_seq_seen()
panel.call("press", op2, "remove")
_ok("5. ✕: ONE op.remove", _kinds(persist, s5) == ["op.remove"], _kinds(persist, s5))
_ok("5. one row left", _rows(panel).size() == 1, _rows(panel))
persist.undo_last()
persist.refold()
_ok("5. Ctrl+Z: two rows again", _rows(panel).size() == 2, _rows(panel))
# ---- 7. the WIDGETS, not just press(): the eye CheckBox and the kind OptionButton ----
panel.call("refresh")
var row0: Node = (panel.get("_rows") as Node).get_child(0)
var eye: CheckBox = row0.get_child(0)
eye.button_pressed = false # what a click does: toggled(false)
await get_tree().process_frame
_ok("7. clicking the eye switches op 1 off", not bool(_rows(panel)[0]["on"]), _rows(panel))
var row0b: Node = (panel.get("_rows") as Node).get_child(0)
var kind: OptionButton = row0b.get_child(1)
kind.item_selected.emit(2) # pick "slice"
await get_tree().process_frame
_ok("7. the kind dropdown sets op 1 to slice", String(_rows(panel)[0]["kind"]) == "slice", _rows(panel))
# ---- 6. cutter ----
map.select_shape(c2)
panel.call("refresh")
var cuts: Array = panel.call("cut_rows")
_ok("6. the cutter shows what it cuts", cuts.size() == 1 and String(cuts[0]["target"]) == wall, cuts)
# ---- 8. modifiers ----
map.select_shape(wall)
panel.call("refresh")
var add: OptionButton = panel.get("_add")
_ok("8. Add modifier is offered for a shape", add.visible and add.item_count == 1 + (panel.get("MOD_KINDS") as Array).size())
add.item_selected.emit(1) # Mirror
await get_tree().process_frame
var mods: Array = panel.call("mod_rows")
_ok("8. a mirror modifier", mods.size() == 1 and String(mods[0]["kind"]) == "mirror", mods)
var rows_box: Node = panel.get("_rows")
_ok("8. its row comes first, before the two cuts", rows_box.get_child_count() == 3
and String((rows_box.get_child(0).get_child(1) as Label).text).contains("Mirror"))
(rows_box.get_child(0).get_child(2) as OptionButton).item_selected.emit(1) # axis Y
await get_tree().process_frame
_ok("8. the axis dropdown sets y", String((panel.call("mod_rows")[0]["params"] as Dictionary).get("axis", "")) == "y",
panel.call("mod_rows"))
add.item_selected.emit(1 + (panel.get("MOD_KINDS") as Array).find("chamfer"))
await get_tree().process_frame
rows_box = panel.get("_rows")
_ok("8. a chamfer row comes after the cuts", rows_box.get_child_count() == 4
and String((rows_box.get_child(3).get_child(1) as Label).text).contains("Chamfer"))
(rows_box.get_child(0).get_child(0) as CheckBox).button_pressed = false # mirror's eye
await get_tree().process_frame
_ok("8. the eye switches the mirror off", not bool(panel.call("mod_rows")[0]["on"]))
rows_box = panel.get("_rows")
var x: Button = rows_box.get_child(3).get_child(rows_box.get_child(3).get_child_count() - 1)
x.pressed.emit() # chamfer's ✕
await get_tree().process_frame
_ok("8. ✕ removes the chamfer", (panel.call("mod_rows") as Array).size() == 1)
# ---- 9. generator ----
var gen: HBoxContainer = panel.get("_gen")
var ob: OptionButton = gen.get_child(1)
var stairs_i := -1
for i in ob.item_count:
if ob.get_item_text(i) == "Stairs":
stairs_i = i
ob.item_selected.emit(stairs_i)
await get_tree().process_frame
_ok("9. the generator dropdown assigns stairs", String(persist.shape_state(wall).get("type", "")) == "stairs")
persist.begin_gesture("Add")
var bdot: String = persist.record("brush.create", {"planes": Array(ShapeKit.box_piece(Vector3(90, 0, 90), Vector3(91, 1, 91))["planes"]),
"proj": [], "material_id": 1, "level_id": 0, "sky": false})
persist.end_gesture()
persist.refold()
map.select_brushes(PackedInt32Array([int(level.brush_by_key(bdot))]))
panel.call("refresh")
gen = panel.get("_gen")
var mp: Node = gen.get_child(1)
_ok("9. a plain brush shows Make parametric", mp is Button and (mp as Button).text == "Make parametric")
var n9: int = (persist.live_shapes() as Dictionary).size()
(mp as Button).pressed.emit()
await get_tree().process_frame
_ok("9. ... which makes it a shape", (persist.live_shapes() as Dictionary).size() == n9 + 1)