extends Node
## GATE — A SHAPE'S PARTS: selectable, read-only until Make Real (#109, user 2026-10-13:
## "select, prompt on edit"). Every new box is a parametric shape, and vertex / edge /
## face mode used to select the WHOLE shape — element selection looked broken.
## P1 a face-mode click on a shape's piece selects that FACE (not the shape)
## P2 G on it asks "Make Real, then go on / Cancel" — nothing edited, shape still live
## P3 Cancel writes nothing; the shape stays
## P4 Make Real, then go on: the shape is plain brushes, the SAME face (same plane) is
## selected on its new brush, and the grab is running on it
## P5 R on a shape's VERTEX asks too
## P6 the [ ] nudge on a shape's face asks too
## P7 the knife aimed at a shape's piece asks too
## P8 brush mode still selects the whole shape
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_shape_parts.tscn
var fails := 0
var checks := 0
var _cam: Camera3D = null
var _persist: Node
var _level: Node
var _map: Node3D
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")
_map = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
add_child(_map)
_cam = Camera3D.new()
add_child(_cam)
_cam.current = true
for i in 6:
await get_tree().process_frame
await _run()
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 14:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE shape_parts: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
## The action's own key, as Keymap binds it.
func _press(action: String) -> void:
var b: Dictionary = Keymap.binding_of(action)
var e := InputEventKey.new()
e.keycode = int(b.get("key", 0))
e.physical_keycode = e.keycode
e.pressed = true
e.ctrl_pressed = bool(b.get("ctrl", false))
e.shift_pressed = bool(b.get("shift", false))
e.alt_pressed = bool(b.get("alt", false))
_map._unhandled_key_input(e)
## Answer the open prompt the way a click does: the popup's own pick (it closes the
## menu, then runs the action two frames later — wait past that).
func _pick(action: String) -> void:
var q: Node = _map.get("_q_menu")
_ok("(the prompt is open with '%s')" % action, q != null and q.is_open() and q.has_action(action))
if q != null:
q.pick(action)
for i in 5:
await get_tree().process_frame
## Aim straight down at `at` and click there (gate_shape_editor's way).
func _aim(at: Vector3) -> void:
_cam.global_position = at + Vector3(0, 6, 0.01)
_cam.look_at(at, Vector3.UP)
func _click(at: Vector3) -> void:
_aim(at)
_map._pick_element(false, get_viewport().get_visible_rect().size * 0.5)
## A fresh 2 x 1 x 2 box shape with its top at y = 1, centred on (x, 0.5, 0).
func _box(x: float) -> String:
var dot: String = _persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(x - 1.0, 0, -1.0))),
"size": [2, 1, 2], "material_id": 2, "level_id": 0})
_persist.refold()
return dot
func _live(dot: String) -> bool:
return (_persist.live_shapes() as Dictionary).has(dot)
func _top_plane() -> Vector4:
var b: Dictionary = _level.get_brush(int(_map.get("_sel_brush")))
var keys: PackedStringArray = b.get("plane_keys", PackedStringArray())
var sk: PackedStringArray = _map.get("_sel_keys")
var i := keys.find(sk[0]) if not sk.is_empty() else -1
var pl: PackedFloat32Array = b.get("planes", PackedFloat32Array())
return Vector4(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2], pl[i * 4 + 3]) if i >= 0 else Vector4.ZERO
func _run() -> void:
_persist = _map.get("_persist")
_level = _map.get("level")
var stack: Node = _map.call("_ensure_ops")
await get_tree().process_frame
print("-- P1: a face-mode click selects the face --")
var dot := _box(0.0)
await get_tree().process_frame
_map.set_select_mode("face")
_click(Vector3(0, 1, 0))
_ok("P1 the FACE is selected", String(_map.get("_sel_kind")) == "face", _map.get("_sel_kind"))
_ok("P1 not the whole shape", String(_map.selected_shape()) == "", _map.selected_shape())
_ok("P1 on one of the shape's pieces", String(_map.shape_of_brush(int(_map.get("_sel_brush")))) == dot)
var plane0 := _top_plane()
print("-- P2 / P3: G asks; Cancel writes nothing --")
var n0: int = (_persist.records() as Array).size()
_press("builder.move")
_ok("P2 G on a shape's face asks (Make Real, then go on)", String(_map.get("_pending_part_shape")) == dot,
_map.get("_pending_part_shape"))
_ok("P2 and runs nothing yet", stack.get("active") == null, stack.get("active"))
await _pick("builder.prompt_cancel")
_ok("P3 Cancel: the shape is still live, nothing written",
_live(dot) and (_persist.records() as Array).size() == n0 and String(_map.get("_pending_part_shape")) == "")
print("-- P4: Make Real, then go on --")
_press("builder.move")
await _pick("builder.make_real_then")
await get_tree().process_frame
_ok("P4 the shape is plain brushes now", not _live(dot))
_ok("P4 the face is still selected", String(_map.get("_sel_kind")) == "face", _map.get("_sel_kind"))
_ok("P4 on a plain brush", String(_map.shape_of_brush(int(_map.get("_sel_brush")))) == "")
_ok("P4 the SAME face (same plane)", _top_plane().is_equal_approx(plane0), [plane0, _top_plane()])
_ok("P4 and the grab is running on it", stack.get("active") is ElementOperator, stack.get("active"))
stack.call("cancel")
print("-- P5: R on a shape's vertex asks --")
var dot5 := _box(6.0)
await get_tree().process_frame
_map.set_select_mode("vertex")
_click(Vector3(6.9, 1, 0.9))
_ok("P5 (setup) a vertex of the shape is selected", String(_map.get("_sel_kind")) == "vertex"
and String(_map.shape_of_brush(int(_map.get("_sel_brush")))) == dot5, _map.get("_sel_kind"))
_press("builder.rotate")
_ok("P5 R asks", String(_map.get("_pending_part_shape")) == dot5 and stack.get("active") == null,
[_map.get("_pending_part_shape"), stack.get("active")])
await _pick("builder.prompt_cancel")
print("-- P6: the nudge asks --")
_map.set_select_mode("face")
_click(Vector3(6, 1, 0))
_press("builder.move_out")
_ok("P6 the nudge on a shape's face asks", String(_map.get("_pending_part_shape")) == dot5,
_map.get("_pending_part_shape"))
await _pick("builder.prompt_cancel")
_ok("P6 and Cancel leaves the shape live", _live(dot5))
print("-- P7: the knife asks --")
# The knife aims by the map's crosshair, which in build mode follows the POINTER: a
# box on that ray, wherever the headless pointer is (gate_extrude_shape_prompt's way).
_aim(Vector3(6, 1, 0))
var ray: Dictionary = _map.call("_crosshair_ray")
var target: Vector3 = (ray["from"] as Vector3) + (ray["dir"] as Vector3) * 12.0
var dot7: String = _persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, target - Vector3(2, 2, 2))),
"size": [4, 4, 4], "material_id": 2, "level_id": 0})
_persist.refold()
for i in 4:
await get_tree().process_frame
var hit: Dictionary = _map.call("_crosshair_hit")
var hb: int = int(_map.call("_brush_from_hit", hit)) if not hit.is_empty() else -1
_ok("P7 (setup) the aim lands on the new shape", hb >= 0 and String(_map.shape_of_brush(hb)) == dot7, [hb, dot7])
_press("builder.knife")
_ok("P7 the knife aimed at a shape's piece asks", String(_map.get("_pending_part_shape")) == dot7
and not (stack.get("active") is KnifeOperator), [_map.get("_pending_part_shape"), stack.get("active")])
await _pick("builder.prompt_cancel")
print("-- P8: brush mode selects the whole shape --")
_map.set_select_mode("brush")
_click(Vector3(6, 1, 0))
_ok("P8 brush mode still selects the shape", String(_map.selected_shape()) == dot5, _map.selected_shape())