OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_faces_to_brushes.gd

250 lines · gd ref:
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))