extends SceneTree
## HIDING A BRUSH — the outliner's eye, hyper-games #28.
##
## "$HYPER_TPS_GODOT" --headless --script scenes/test/hidden_brush_gate.gd
##
## Hiding is a VIEW, and this asserts the three things that make it one rather
## than a quiet edit:
##
## 1. IT DOES NOT RE-CLIP. Hiding filters faces at mesh emit, after csg4 has
## run. Dropping the brush from csg4's INPUT instead would re-clip its
## neighbours — faces buried behind a wall become visible the moment you
## hide it — so unhiding would not restore what you had. The discriminating
## control is a TOMBSTONE of the same brush: that really does remove it from
## the input, so if hide and tombstone agree, hide is not a view.
##
## 2. IT DOES NOT REACH THE SHELL. build_shell reads shell_view, off the store,
## and the hidden set lives on the NODE. If hiding ever touched it, hiding a
## boundary wall would un-seal the level and the terrain would flood every
## room — a view toggle silently filling the map with hillside.
##
## 3. IT IS EXACTLY REVERSIBLE. Unhide restores the original triangle and face
## counts to the digit, which is only true if the clip never changed.
##
## Plus the collider, which you asked to drop so you can fly through what you hid.
const WALLS := [
[Vector3(0, 0, 0), Vector3(7, 0.5, 7)],
[Vector3(0, 4.5, 0), Vector3(7, 5, 7)],
[Vector3(0, 0.5, 0), Vector3(0.5, 4.5, 7)],
[Vector3(6.5, 0.5, 0), Vector3(7, 4.5, 7)],
[Vector3(0.5, 0.5, 0), Vector3(6.5, 4.5, 0.5)],
[Vector3(0.5, 0.5, 6.5), Vector3(6.5, 4.5, 7)],
]
## A pillar standing against the -X wall, so it BURIES part of that wall's face.
## That shared surface is the whole point: it is what a re-clip would change and
## an emit filter would not.
const PILLAR := [Vector3(0.5, 0.5, 3.0), Vector3(1.5, 4.5, 4.0)]
var _failures: Array[String] = []
var _frame := 0
func _check(label: String, ok: bool, detail: String) -> void:
if not ok:
_failures.append("%s (%s)" % [label, detail])
print(" %-52s %s %s" % [label, "OK " if ok else "FAIL", detail])
static func _box(lo: Vector3, hi: Vector3) -> PackedFloat32Array:
return PackedFloat32Array([
-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 _new_level() -> Node3D:
var lvl := ClassDB.instantiate("DCLevel") as Node3D
root.add_child(lvl)
for w: Array in WALLS:
lvl.add_brush(_box(w[0], w[1]), 0, PackedInt32Array(), 0, false, "")
lvl.add_brush(_box(PILLAR[0], PILLAR[1]), 0, PackedInt32Array(), 0, false, "")
lvl.rebuild()
return lvl
func _process(_delta: float) -> bool:
_frame += 1
if _frame < 3:
return false # colliders need a world; see dc_level_gate
print("hidden brush gate")
var lvl := _new_level()
var ids: PackedInt32Array = PackedInt32Array()
for i in lvl.get_brush_count():
pass
# The pillar is the last brush added, so its id is the highest live one.
var pillar_id := 0
for b in range(1, 64):
if not (lvl.get_brush(b) as Dictionary).is_empty():
pillar_id = b
_check("the pillar has an id", pillar_id > 0, "#%d" % pillar_id)
var t0: int = lvl.get_triangle_count()
var f0: int = int(lvl.get_stats().get("faces", 0))
var c0: int = lvl.get_collider_count()
var shell0: Dictionary = lvl.build_shell()
print(" visible: %d tris, %d faces, %d colliders, shell %d tris" %
[t0, f0, c0, int(shell0.get("triangles", 0))])
# ---- hide it ---------------------------------------------------------
lvl.set_brush_hidden(pillar_id, true)
lvl.rebuild()
var t1: int = lvl.get_triangle_count()
var c1: int = lvl.get_collider_count()
_check("get_hidden_brush_ids reports it",
(lvl.get_hidden_brush_ids() as PackedInt32Array).has(pillar_id),
"%s" % str(lvl.get_hidden_brush_ids()))
_check("is_brush_hidden agrees", lvl.is_brush_hidden(pillar_id), "true")
_check("stats count it", int(lvl.get_stats().get("hidden_brushes", -1)) == 1,
"%s" % str(lvl.get_stats().get("hidden_brushes", -1)))
_check("hiding removes its triangles", t1 < t0, "%d vs %d" % [t1, t0])
_check("and its collider", c1 == c0 - 1, "%d vs %d" % [c1, c0])
# ---- 2. THE SHELL IS UNTOUCHED ---------------------------------------
# The one that matters. A hidden wall that left the shell would let the
# terrain into the level.
var shell1: Dictionary = lvl.build_shell()
_check("the shell is byte-identical",
int(shell1.get("triangles", 0)) == int(shell0.get("triangles", 0))
and absf(float(shell1.get("volume", 0.0)) - float(shell0.get("volume", 0.0))) < 1e-6,
"%d tris, %.1f m^3" % [int(shell1.get("triangles", 0)), float(shell1.get("volume", 0.0))])
_check("and still sealed", bool(shell1.get("sealed", false)), "yes")
# ---- 3. EXACTLY REVERSIBLE -------------------------------------------
lvl.set_brush_hidden(pillar_id, false)
lvl.rebuild()
_check("unhiding restores the triangles exactly", lvl.get_triangle_count() == t0,
"%d vs %d" % [lvl.get_triangle_count(), t0])
_check("and the faces exactly", int(lvl.get_stats().get("faces", 0)) == f0,
"%d vs %d" % [int(lvl.get_stats().get("faces", 0)), f0])
_check("and the colliders", lvl.get_collider_count() == c0,
"%d vs %d" % [lvl.get_collider_count(), c0])
# ---- 1. THE DISCRIMINATING CONTROL: hide != tombstone -----------------
# A tombstone removes the pillar from csg4's INPUT, so the -X wall face it was
# burying gets re-clipped and comes back whole. Hiding must NOT do that. If
# these two agree, the filter is in the wrong place and unhiding is only
# accidentally exact.
lvl.set_brush_hidden(pillar_id, true)
lvl.rebuild()
var t_hidden: int = lvl.get_triangle_count()
lvl.set_brush_hidden(pillar_id, false)
lvl.remove_brush(pillar_id)
lvl.rebuild()
var t_removed: int = lvl.get_triangle_count()
print(" hidden %d tris vs tombstoned %d tris (visible %d)" % [t_hidden, t_removed, t0])
_check("hiding is NOT the same as removing", t_hidden != t_removed,
"%d vs %d — a re-clip would make them equal" % [t_hidden, t_removed])
_check("and removing really does re-clip", t_removed != t0,
"%d vs %d" % [t_removed, t0])
print("\nRESULT: %s" % ("PASS" if _failures.is_empty() else "FAIL"))
for f: String in _failures:
print(" - %s" % f)
quit(0 if _failures.is_empty() else 1)
return true