extends Node
## GATE — new brushes FROM SELECTED FACES, and DUPLICATE (user 2026-10-05: "select
## multiple faces of close brushes to create new brushes that we can use for csg",
## "do both hull and extrude", "a method like in blender to duplicate").
##
## 1. FACE SET: face mode, Shift-click adds faces of DIFFERENT brushes; Shift-click
## again removes one; a second face of an already-selected brush keeps the brush
## 2. HULL (Alt+J): two facing walls' inner faces -> ONE brush filling the gap; one
## brush.create, one gesture "Brush from faces"; sources untouched; the new brush
## is selected in BRUSH mode; one Ctrl+Z removes it
## 3. HULL of one flat face is REFUSED (no record)
## 4. EXTRUDE (Alt+E): one brush per face, one grid step deep, OUTWARD
## 5. ...and the extruded slab is a working CSG cutter (subtract from a third wall)
## 6. DUPLICATE (Shift+G) a brush: an identical plane set under a new dot, loose (no
## group), Move active; Esc leaves the copy; one Ctrl+Z takes it back
## 7. DUPLICATE a parametric shape: a second live shape, same type
## 8. DUPLICATE an object: same type and props
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_faces_to_brushes.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 < 25:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE faces_to_brushes: %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]
## A journalled box (a real dot, so faces have durable names).
func _wall(map: Node, lo: Vector3, hi: Vector3) -> int:
var id: int = map._create_brush_journalled({"planes": _box_planes(lo, hi),
"proj": [0, 0, 0, 0, 0, 0], "material_id": 7, "level_id": 0})
return id
func _solid_at(level: Node, p: Vector3, skip: Array = []) -> bool:
for b: int in (level.get_live_brush_ids() as PackedInt32Array):
if skip.has(b):
continue
var pl: PackedFloat32Array = level.get_brush(b).get("planes", PackedFloat32Array())
var inside := true
for f in range(pl.size() / 4):
if Vector3(pl[f * 4], pl[f * 4 + 1], pl[f * 4 + 2]).dot(p) > pl[f * 4 + 3] + 1e-4:
inside = false
break
if inside and not pl.is_empty():
return true
return false
## Aim at `at` from `dist` along `n` (the face's outward normal) and pick.
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)
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"]))
return out
func _run(map: Node3D) -> void:
var persist: Node = map.get("_persist")
var level: Node = map.get("level")
map._ensure_persistence()
persist = map.get("_persist")
_ok("setup: the level has hull_planes (dc-terrain with the binding)", level.has_method("hull_planes"))
if not level.has_method("hull_planes"):
return
# Two walls facing each other across a 2 m gap, high in the air, out of the way.
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
_ok("setup: two journalled walls", wa > 0 and wb > 0, "%d %d" % [wa, wb])
# ---- 1. the face set ----
map.set("_sel_mode", "face")
map.clear_selection()
await _pick(map, Vector3(201, y0 + 1, 1), Vector3.RIGHT, false)
await _pick(map, Vector3(203, y0 + 1, 1), Vector3.LEFT, true)
var sf: Array = map.get("_sel_faces")
_ok("1. Shift-click adds a face of ANOTHER brush", sf.size() == 2, sf)
_ok("1. the selection is a face", String(map.get("_sel_kind")) == "face", map.get("_sel_kind"))
await _pick(map, Vector3(203, y0 + 1, 1), Vector3.LEFT, true)
_ok("1. Shift-click the same face again removes it", (map.get("_sel_faces") as Array).size() == 1)
await _pick(map, Vector3(203, y0 + 1, 1), Vector3.LEFT, true)
await _pick(map, Vector3(203.5, y0 + 2, 1), Vector3.UP, true)
_ok("1. a second face of a selected brush is added", (map.get("_sel_faces") as Array).size() == 3)
_ok("1. ... and its brush STAYS selected", (map.get("_sel_brushes") as Array).has(wb), map.get("_sel_brushes"))
await _pick(map, Vector3(203.5, y0 + 2, 1), Vector3.UP, true) # back to the two facing faces
# ---- 2. hull ----
var seq0: int = persist._max_seq_seen()
var live0: int = (level.get_live_brush_ids() as PackedInt32Array).size()
var hid: int = map.faces_to_hull()
persist.refold()
_ok("2. Alt+J makes one brush", hid > 0 and (level.get_live_brush_ids() as PackedInt32Array).size() == live0 + 1,
"%d" % hid)
_ok("2. ONE record: a brush.create", _kinds(persist, seq0) == ["brush.create"], _kinds(persist, seq0))
_ok("2. one gesture 'Brush from faces'", String((persist.gestures() as Array)[0]["label"]) == "Brush from faces")
_ok("2. it fills the gap between the walls", _solid_at(level, Vector3(202, y0 + 1, 1)))
_ok("2. and no further than the walls' faces", not _solid_at(level, Vector3(202, y0 + 2.5, 1))
and not _solid_at(level, Vector3(202, y0 + 1, 2.5)))
_ok("2. the walls are untouched", _solid_at(level, Vector3(200.5, y0 + 1, 1))
and _solid_at(level, Vector3(203.5, y0 + 1, 1)))
_ok("2. the new brush is selected, in BRUSH mode", String(map.get("_sel_mode")) == "brush"
and (map.get("_sel_brushes") as Array) == [hid], "%s %s" % [map.get("_sel_mode"), map.get("_sel_brushes")])
persist.undo_last()
persist.refold()
_ok("2. one Ctrl+Z removes it", not _solid_at(level, Vector3(202, y0 + 1, 1)))
# ---- 3. one flat face ----
map.set("_sel_mode", "face")
map.clear_selection()
await _pick(map, Vector3(201, y0 + 1, 1), Vector3.RIGHT, false)
seq0 = persist._max_seq_seen()
_ok("3. the hull of one flat face is refused", map.faces_to_hull() == -1)
_ok("3. ... and writes nothing", _kinds(persist, seq0).is_empty(), _kinds(persist, seq0))
# ---- 4. extrude ----
await _pick(map, Vector3(203, y0 + 1, 1), Vector3.LEFT, true)
var step: float = map._grid_step()
seq0 = persist._max_seq_seen()
var ex: PackedInt32Array = map.faces_extrude()
persist.refold()
_ok("4. Alt+E makes one brush per face", ex.size() == 2, "%d" % ex.size())
_ok("4. two brush.create records", _kinds(persist, seq0) == ["brush.create", "brush.create"], _kinds(persist, seq0))
_ok("4. wall A's slab sits OUTSIDE its +x face, one grid step deep",
_solid_at(level, Vector3(201 + step * 0.5, y0 + 1, 1)) and not _solid_at(level, Vector3(201 + step * 1.5, y0 + 1, 1)),
"step %.3f" % step)
_ok("4. wall B's slab sits outside its -x face",
_solid_at(level, Vector3(203 - step * 0.5, y0 + 1, 1)) and not _solid_at(level, Vector3(203 - step * 1.5, y0 + 1, 1)))
_ok("4. both are selected in brush mode", String(map.get("_sel_mode")) == "brush"
and (map.get("_sel_brushes") as Array).size() == 2)
# ---- 5. a working cutter ----
var cutter: int = ex[0]
var wc := _wall(map, Vector3(201 - 0.5, y0 + 0.5, 0.5), Vector3(201 + step + 0.5, y0 + 1.5, 1.5))
level.rebuild()
map.clear_selection()
map._note_selected(cutter, false)
map._note_selected(wc, true)
map.csg_discard = true
seq0 = persist._max_seq_seen()
map._csg("subtract")
persist.refold()
_ok("5. the extruded slab cuts a third wall", _kinds(persist, seq0).has("brush.subtract")
and not _solid_at(level, Vector3(201 + step * 0.5, y0 + 1, 1)), _kinds(persist, seq0))
# ---- 6. duplicate a brush ----
map.set("_sel_mode", "brush")
map.clear_selection()
map._note_selected(wa, false)
map.set("_sel_kind", "brush")
var planes_a: PackedFloat32Array = level.get_brush(wa)["planes"]
seq0 = persist._max_seq_seen()
var stack: Node = map.call("_ensure_ops")
_ok("6. Shift+G duplicates", map.duplicate_selection())
var copy: int = (map.get("_sel_brushes") as Array)[0] if not (map.get("_sel_brushes") as Array).is_empty() else -1
_ok("6. one brush.create", _kinds(persist, seq0) == ["brush.create"], _kinds(persist, seq0))
_ok("6. the copy is a NEW brush with the same planes", copy > 0 and copy != wa
and level.get_brush(copy)["planes"] == planes_a)
_ok("6. ... loose (no group)", String(level.get_brush(copy).get("group", "x")) == "")
_ok("6. Move is up on it", stack.get("active") == map.get("_move"))
stack.cancel()
_ok("6. Esc leaves the copy in place", level.get_brush(copy)["planes"] == planes_a
and not bool(level.get_brush(copy).get("tombstoned", false)))
persist.undo_last()
persist.refold()
_ok("6. one Ctrl+Z takes it back", level.brush_by_key(String(level.get_brush_key(copy))) <= 0
or bool(level.get_brush(copy).get("tombstoned", false)))
# ---- 7. duplicate a shape ----
var bx: CreateBoxOperator = map.get("_box")
var si: int = (bx.shape_ids() as Array).find("param:stairs")
bx.set_tool_setting("shape", si)
bx.set_tool_setting("size", 8)
bx.set_tool_setting("height", 4)
bx.set("_origin", Vector3(220, y0, 0))
bx.set("_normal", Vector3.UP)
bx.set("_axis", 1)
bx.set("_view", Vector3(1, 0, 0.01).normalized())
bx.set("_valid", true)
stack._keymap_tool(true)
stack.active = bx
stack.confirm()
persist.refold()
var n_shapes: int = (persist.live_shapes() as Dictionary).size()
var src_shape: String = map.selected_shape()
_ok("7. setup: a stairs shape is selected", src_shape != "", src_shape)
_ok("7. Shift+G duplicates it", map.duplicate_selection())
stack.cancel()
persist.refold()
var dup_shape: String = map.selected_shape()
_ok("7. a second live shape", (persist.live_shapes() as Dictionary).size() == n_shapes + 1
and dup_shape != "" and dup_shape != src_shape, "%s -> %s" % [src_shape, dup_shape])
_ok("7. ... of the same type", String(persist.shape_state(dup_shape).get("type", ""))
== String(persist.shape_state(src_shape).get("type", "-")))
# ---- 8. duplicate an object ----
var od: String = map.create_object("block.bevelled", Transform3D(Basis(), Vector3(230, y0, 0)))
map.select_object(od)
_ok("8. Shift+G duplicates an object", map.duplicate_selection())
stack.cancel()
var odup: String = String((map.selected_objects() as Array)[-1]) if not (map.selected_objects() as Array).is_empty() else ""
var on: Node = map.object_node(odup)
_ok("8. a new object of the same type", odup != "" and odup != od and on != null
and String(on.get_meta("object_type", "")) == "block.bevelled", odup)
_ok("8. ... where the original is", on != null
and (on as Node3D).global_position.is_equal_approx((map.object_node(od) as Node3D).global_position))