extends Node
## GATE — THE EDGE SET (#120 L3, docs/node-tree-and-stacks.md §14.4): edge mode, Shift-click
## builds a cross-brush SET of edges, as face mode does for faces. An edge is held by
## durable names — the brush key and its two plane keys — never by a picked point.
## E1 a plain click on an edge: a set of one, {b, p: the two plane keys, sorted}
## E2 Shift-click another edge of the SAME brush: two, and the brush stays selected
## E3 Shift-click an edge of another brush: three
## E4 Shift-click the first edge again: removed (two)
## E5 selected_edges() resolves each from its planes: the right endpoints and normals
## E6 the set survives a re-fold
## E7 the highlight draws every edge in the set
## E8 a brush gone: its edge drops out of selected_edges()
## E9 a plain click restarts the set
## E10 face mode clears it
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_edge_set.tscn
var fails := 0
var checks := 0
var _cam: Camera3D = null
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-66s %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)
_cam = Camera3D.new()
add_child(_cam)
_cam.current = true
for i in 6:
await get_tree().process_frame
await _run(map)
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 13:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE edge_set: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _box_planes(lo: Vector3, hi: Vector3) -> Array:
return [-1, 0, 0, -lo.x, 1, 0, 0, hi.x, 0, -1, 0, -lo.y, 0, 1, 0, hi.y, 0, 0, -1, -lo.z, 0, 0, 1, hi.z]
func _wall(map: Node, lo: Vector3, hi: Vector3) -> int:
return map._create_brush_journalled({"planes": _box_planes(lo, hi),
"proj": [0, 0, 0, 0, 0, 0], "material_id": 7, "level_id": 0})
## Aim along -n at `at` (a point on a face, near the edge wanted) and click.
func _pick(map: Node, at: Vector3, n: Vector3, add: bool) -> void:
_cam.global_position = at + n * 3.0
_cam.look_at(at, Vector3.UP if absf(n.y) < 0.9 else Vector3.FORWARD)
await get_tree().physics_frame
map._pick_element(add, get_viewport().get_visible_rect().size * 0.5)
## The plane key of `brush` whose outward normal is `n`.
func _key(level: Node, brush: int, n: Vector3) -> String:
var d: Dictionary = level.get_brush(brush)
var pl: PackedFloat32Array = d["planes"]
var keys: PackedStringArray = d["plane_keys"]
for i in pl.size() / 4:
if Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).normalized().distance_to(n) < 1.0e-4:
return keys[i]
return ""
func _pair(level: Node, brush: int, n1: Vector3, n2: Vector3) -> Array:
var p := [_key(level, brush, n1), _key(level, brush, n2)]
p.sort()
return p
func _run(map: Node3D) -> void:
map._ensure_persistence()
var persist: Node = map.get("_persist")
var level: Node = map.get("level")
var y0 := 60.0
var wa := _wall(map, Vector3(200, y0, 0), Vector3(201, y0 + 2, 2))
var wb := _wall(map, Vector3(203, y0, 0), Vector3(204, y0 + 2, 2))
level.rebuild()
for i in 3:
await get_tree().physics_frame
var ka := String(level.get_brush_key(wa))
var kb := String(level.get_brush_key(wb))
var has := map.get("_sel_edges") != null and map.has_method("selected_edges")
_ok("setup: the map has _sel_edges and selected_edges()", has)
if not has:
return
map.set_select_mode("edge")
map.clear_selection()
# ---- E1: the top edge of wa's +X face ----
await _pick(map, Vector3(201, y0 + 1.9, 1), Vector3.RIGHT, false)
var se: Array = map.get("_sel_edges")
var e_top := {"b": ka, "p": _pair(level, wa, Vector3.RIGHT, Vector3.UP)}
_ok("E1 a plain click: one edge, wa's +X / +Y planes", se.size() == 1 and se[0] == e_top, se)
# ---- E2: wa's +X face, its front (+Z) edge ----
await _pick(map, Vector3(201, y0 + 1, 1.9), Vector3.RIGHT, true)
se = map.get("_sel_edges")
var e_front := {"b": ka, "p": _pair(level, wa, Vector3.RIGHT, Vector3.BACK)}
_ok("E2 Shift-click another edge of the same brush: two", se.size() == 2 and se.has(e_front), se)
_ok("E2 ... and the brush stays selected", (map.get("_sel_brushes") as Array).has(wa), map.get("_sel_brushes"))
# ---- E3: wb's -X face, its top edge ----
await _pick(map, Vector3(203, y0 + 1.9, 1), Vector3.LEFT, true)
se = map.get("_sel_edges")
var e_b := {"b": kb, "p": _pair(level, wb, Vector3.LEFT, Vector3.UP)}
_ok("E3 Shift-click an edge of another brush: three", se.size() == 3 and se.has(e_b), se)
# ---- E4 ----
await _pick(map, Vector3(201, y0 + 1.9, 1), Vector3.RIGHT, true)
se = map.get("_sel_edges")
_ok("E4 Shift-click the first edge again: removed", se.size() == 2 and not se.has(e_top), se)
# ---- E5 ----
var res: Array = map.selected_edges()
var front: Dictionary = {}
for r: Dictionary in res:
if String(r["b"]) == ka:
front = r
var ends: Array = [front.get("a", Vector3.INF), front.get("e", Vector3.INF)] if not front.is_empty() else []
var want := [Vector3(201, y0, 2), Vector3(201, y0 + 2, 2)]
var ends_ok := ends.size() == 2 and ((ends[0] as Vector3).distance_to(want[0]) < 1.0e-3 and (ends[1] as Vector3).distance_to(want[1]) < 1.0e-3
or (ends[0] as Vector3).distance_to(want[1]) < 1.0e-3 and (ends[1] as Vector3).distance_to(want[0]) < 1.0e-3)
_ok("E5 the front edge resolves to (201, y0, 2) .. (201, y0+2, 2)", res.size() == 2 and ends_ok, ends)
var ns: Array = front.get("normals", [])
_ok("E5 ... with its two face normals (+X, +Z)", ns.size() == 2 and (ns.has(Vector3.RIGHT) or (ns[0] as Vector3).distance_to(Vector3.RIGHT) < 1.0e-4
or (ns[1] as Vector3).distance_to(Vector3.RIGHT) < 1.0e-4), ns)
# ---- E6 ----
persist.refold()
_ok("E6 a re-fold keeps the set (both still resolve)", (map.get("_sel_edges") as Array).size() == 2
and (map.selected_edges() as Array).size() == 2, map.selected_edges().size())
# ---- E7 ----
var lines: PackedVector3Array = map.edge_set_lines() if map.has_method("edge_set_lines") else PackedVector3Array()
_ok("E7 the highlight's lines: every edge in the set (2 x 2 points)", lines.size() == 4, lines.size())
# ---- E8 ----
persist.begin_gesture("Delete wb")
persist.record("brush.remove", {"brush_key": kb})
persist.end_gesture()
persist.refold()
_ok("E8 wb gone: its edge drops out of selected_edges()", (map.selected_edges() as Array).size() == 1,
(map.selected_edges() as Array).size())
persist.undo_last()
persist.refold()
# ---- E9 ----
await _pick(map, Vector3(201, y0 + 1.9, 1), Vector3.RIGHT, false)
_ok("E9 a plain click restarts the set", (map.get("_sel_edges") as Array).size() == 1, map.get("_sel_edges"))
# ---- E10 ----
map.set_select_mode("face")
_ok("E10 face mode clears the edge set", (map.get("_sel_edges") as Array).is_empty(), map.get("_sel_edges"))