extends Node
## GATE — an edit that changes no BRUSH does not replay and re-mesh every brush
## (2026-10-05: "delete is extremely slow" — 11 s per delete). refold() always ran
## DCLevel.apply_journal + level.rebuild(), the rebuild alone 705 ms at 2000 brushes,
## even for deleting a teleporter. Now the brush half is SKIPPED when the brush records
## and the store are both unchanged since the last fold.
##
## 1. an object edit (create / delete / undo) -> brush_fold "skipped"
## 2. a brush edit -> "applied"
## 3. the store changed BEHIND the journal's back (a direct remove_brush) -> "applied",
## and the fold puts the journal's brush back (the skip never hides drift)
## 4. with 1000 brushes, an object delete costs < 1/5 of a brush delete
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_refold_skip.tscn
var fails := 0
var checks := 0
func _ok(what: String, cond: bool, note: String = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", note])
func _box(x: float, z: float) -> Array:
return [1, 0, 0, x + 2, -1, 0, 0, -x, 0, 1, 0, 2, 0, -1, 0, 0, 0, 0, 1, z + 2, 0, 0, -1, -z]
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"
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
_run(map)
if checks < 7:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE refold_skip: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _run(map: Node3D) -> void:
var level: Node = map.get("level")
var persist: Node = map.get("_persist")
for i in 1000:
persist.record("brush.create", {"planes": _box(60.0 + float(i % 40) * 3.0, float(i / 40) * 3.0),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.refold()
_ok("setup: the brush fold ran", String(persist.last_fold().get("brush_fold", "")) == "applied",
str(persist.last_fold().get("brush_fold", "<none>")))
# 1. object-only edits
var dot: String = map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(0, 1, 0)))
persist.refold()
_ok("1. creating an object skips the brush fold", String(persist.last_fold().get("brush_fold", "")) == "skipped")
map.delete_objects([dot])
persist.refold()
_ok("1. deleting it skips the brush fold", String(persist.last_fold().get("brush_fold", "")) == "skipped")
# 2. a brush edit
var bdot: String = persist.record("brush.create", {"planes": _box(-80.0, -80.0), "proj": [],
"material_id": 0, "level_id": 0, "sky": false})
persist.refold()
# "tail" since the tail fold (only new records: applied on top, no rewind) — still a fold.
_ok("2. a brush edit applies the brush fold", String(persist.last_fold().get("brush_fold", "")) in ["applied", "tail"],
persist.last_fold().get("brush_fold", ""))
# 3. drift behind the journal's back
var id: int = level.brush_by_key(bdot)
level.remove_brush(id)
persist.refold()
var b: Dictionary = level.get_brush(level.brush_by_key(bdot))
_ok("3. a store changed behind the journal -> the brush fold runs", String(persist.last_fold().get("brush_fold", "")) == "applied")
_ok("3. and puts the journal's brush back", not b.is_empty() and not bool(b.get("tombstoned", true)))
# 4. cost
var t0 := Time.get_ticks_usec()
var d2: String = map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(0, 1, 0)))
map.delete_objects([d2])
var obj_us := float(Time.get_ticks_usec() - t0)
# Against a FULL brush fold (the rewind + re-mesh this check is about), not the tail
# fold a plain delete now takes.
persist.set("_ix_tail_ok", false)
t0 = Time.get_ticks_usec()
map.delete_brushes([level.brush_by_key(bdot)])
var br_us := float(Time.get_ticks_usec() - t0)
# Before the skip an object create + delete (two folds) cost ~2x ONE brush delete
# (430 vs 221 ms here): each fold re-meshed every brush. Measured after: ~92 vs ~240.
# What is left per fold is linear (records() scan, outliner refresh), not the re-mesh.
_ok("4. an object create+delete (2 folds) costs less than ONE brush delete", obj_us < br_us,
"%.1f ms vs %.1f ms" % [obj_us / 1000.0, br_us / 1000.0])