extends Node
## GATE — LIVE CUTS IN THE EDITOR (docs/cutting-and-operations.md §9a, user 2026-10-12
## "live by default").
##
## 1. LIVE SUBTRACT: active = a piece of a box shape (the wall), the other selected =
## a box shape (the cutter); _csg("subtract") writes ONE op.create and no
## brush.subtract; the door is air, the cutter is wire (a cutter brush), and the
## cutters come back as the selection
## 2. PICK: the crosshair on the door finds the CUTTER (dc_pick; it has no collider)
## 3. FALLBACK: two plain ungrouped brushes still cut destructively (brush.subtract)
## 4. MAKE REAL: refused on the cutter; on the wall it bakes the door into plain
## brushes, applies the op and removes the cutter, in ONE gesture; Ctrl+Z restores
## the live cut
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_op_editor.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(" %-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)
_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 < 14:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE op_editor: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
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
## Solid at (x, y) through the wall (z 0..1 at W): a level ray along -z.
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 _run(map: Node3D) -> void:
var persist: Node = map.get("_persist")
var level: Node = map.get("level")
if level == null or not level.has_method("get_cutter_brush_ids"):
_ok("this DCLevel build knows cutters (dc-terrain f0efefd+)", false)
return
const W := Vector3(60, 0, 60) # clear of map_test's own geometry
var wall: String = _box(persist, W, Vector3(8, 4, 1))
var cutter: String = _box(persist, W + Vector3(3, -1, -1), Vector3(2, 4, 3))
persist.refold()
var wall_ids: PackedInt32Array = level.get_group_brush_ids(wall)
var cut_ids: PackedInt32Array = level.get_group_brush_ids(cutter)
_ok("setup: a wall shape and a cutter shape", wall_ids.size() == 1 and cut_ids.size() == 1,
"%d / %d" % [wall_ids.size(), cut_ids.size()])
if wall_ids.is_empty() or cut_ids.is_empty():
return
# ---- 1. live subtract ----
var sel := PackedInt32Array(cut_ids)
sel.append(wall_ids[0]) # the ACTIVE brush is the last selected
map.select_brushes(sel)
var seq0: int = persist._max_seq_seen()
map._csg("subtract")
var made: Array = _kinds(persist, seq0)
_ok("1. ONE record: an op.create", made == ["op.create"], made)
_ok("1. the door is air", not _solid(level, W + Vector3(4, 1, 0)))
_ok("1. the wall beside it is solid", _solid(level, W + Vector3(1, 1, 0)) and _solid(level, W + Vector3(7, 1, 0)))
var cutters: PackedInt32Array = level.get_cutter_brush_ids()
_ok("1. the cutter is a cutter brush", cutters.size() == 1, cutters)
var sel_now: Array = map.get("_sel_brushes")
_ok("1. the cutter is the selection now", sel_now.size() == 1 and cutters.has(int(sel_now[0])), sel_now)
var g0: Dictionary = (persist.gestures() as Array)[0]
_ok("1. one gesture 'Live subtract'", String(g0["label"]) == "Live subtract", g0)
# ---- 2. pick ----
var door := W + Vector3(4, 1, 0.5)
_cam.global_position = door + Vector3(0, 0, 6)
_cam.look_at(door, Vector3.UP)
await get_tree().process_frame
# AT THE SCREEN CENTRE: in build mode the pointer is free, and with no argument the
# ray leaves from the mouse — (0, 0) headless, which aims at the sky.
var hit: Dictionary = map._crosshair_hit(get_viewport().get_visible_rect().size * 0.5)
_ok("2. the crosshair on the door finds the cutter", cutters.has(int(hit.get("brush", -1))), hit)
# ---- 3. destructive fallback ----
var a: String = persist.record("brush.create", {"planes": _planes(W + Vector3(0, 0, 10), Vector3(4, 4, 1)),
"proj": [], "material_id": 2, "level_id": 0, "sky": false, "group": ""})
var b: String = persist.record("brush.create", {"planes": _planes(W + Vector3(1, -1, 9), Vector3(1, 3, 3)),
"proj": [], "material_id": 2, "level_id": 0, "sky": false, "group": ""})
persist.refold()
var ia: int = level.brush_by_key(a)
var ib: int = level.brush_by_key(b)
map.select_brushes(PackedInt32Array([ib, ia]))
var seq1: int = persist._max_seq_seen()
map._csg("subtract")
var made2: Array = _kinds(persist, seq1)
_ok("3. plain brushes still cut destructively (brush.subtract, no op)",
made2.has("brush.subtract") and not made2.has("op.create"), made2)
# ---- 4. make real ----
_ok("4. Make Real on the cutter is refused", int(map.make_real(cutter)) == 0)
var seq2: int = persist._max_seq_seen()
var n: int = map.make_real(wall)
var made3: Array = _kinds(persist, seq2)
_ok("4. Make Real on the wall bakes its pieces", n >= 3, n)
_ok("4. ... and applies the op and removes the cutter", made3.has("op.apply") and made3.has("shape.remove")
and made3.has("shape.detach"), made3)
_ok("4. the door is still air, now in plain brushes", not _solid(level, W + Vector3(4, 1, 0)))
_ok("4. no live cutter remains", (level.get_cutter_brush_ids() as PackedInt32Array).is_empty())
_ok("4. the cutter shape is gone", not (persist.live_shapes() as Dictionary).has(cutter))
persist.undo_last()
persist.refold()
_ok("4. ONE Ctrl+Z: the live cut is back", (level.get_cutter_brush_ids() as PackedInt32Array).size() == 1
and (persist.live_shapes() as Dictionary).has(wall))
_ok("4. ... and the door still air", not _solid(level, W + Vector3(4, 1, 0)))
## Box planes (n·x <= d), flat floats — a brush.create payload.
func _planes(o: Vector3, s: Vector3) -> Array:
var hi := o + s
return [-1, 0, 0, -o.x, 1, 0, 0, hi.x, 0, -1, 0, -o.y, 0, 1, 0, hi.y, 0, 0, -1, -o.z, 0, 0, 1, hi.z]