OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_mirror.gd

201 lines · gd ref:
extends SceneTree
## GATE — MIRROR (Ctrl+M, Blender transform.mirror; #109 build tools, design on #109
## 2026-10-13). Ctrl+M starts it on the selection, X / Y / Z picks the axis (world; the
## same key again = the brush's own axis), Enter mirrors about the editor pivot.
##   M1  Ctrl+M on a selected brush starts Mirror
##   M2  X, Enter: an asymmetric brush (a corner cut by a slanted plane) is reflected
##       about the pivot — its bounds mirror, the slanted plane's normal flips in x
##   M3  ONE gesture; a refold reproduces it; one undo puts it back
##   M4  Y mirrors along y instead (the slant flips in y)
##   M5  pivot = 3D cursor: the reflection is about the cursor
##   M6  Individual Origins: two brushes each mirror in place
##   M7  a parametric shape asks "Make Real, then go on" and writes nothing
##   M8  an object mirrors its PLACEMENT: position about the pivot, facing mirrored
##   M9  nothing selected or aimed: Ctrl+M refuses, no stray tool
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_mirror.gd

const S := 0.7071068

var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _level: Node
var _persist: Node
var _stack: Node


func _p(c: bool, m: String, got: Variant = "") -> void:
	checks += 1
	print(("ok   " if c else "FAIL: ") + m + ("" if str(got) == "" else "  (" + str(got) + ")"))
	if not c:
		ok = false


func _process(_d: float) -> bool:
	_frame += 1
	if _frame == 1:
		_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
		root.add_child(_map)
		return false
	if _frame < 30:
		return false
	root.get_node("/root/GameMode").set_mode("build")
	_run()
	root.get_node("/root/GameMode").set_mode("play")
	if checks < 20:          # a gate that died early must not read as a pass
		ok = false
		print("FAIL: only %d checks ran -- the gate died before its assertions" % checks)
	print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
	quit(0 if ok else 1)
	return true


func _key(code: int, ctrl := false, shift := false) -> InputEventKey:
	var e := InputEventKey.new()
	e.physical_keycode = code
	e.keycode = code
	e.ctrl_pressed = ctrl
	e.shift_pressed = shift
	e.pressed = true
	return e


## A box [lo, hi] with its (+x, +y) edge cut off by the plane x + y = cut (normal (1,1,0)/√2).
func _wedge(lo: Vector3, hi: Vector3, cut: float) -> int:
	var pl := [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z,
		S, S, 0, cut * S]
	var dot: String = _persist.record("brush.create", {"planes": pl, "proj": [], "material_id": 2,
		"level_id": 0, "sky": false, "group": ""})
	_persist.refold()
	return _level.brush_by_key(dot)


func _bounds(b: int) -> AABB:
	var d: Dictionary = _level.get_brush(b)
	return AABB(d["aabb_min"], (d["aabb_max"] as Vector3) - (d["aabb_min"] as Vector3))


func _has_normal(b: int, n: Vector3) -> bool:
	var pl: PackedFloat32Array = _level.get_brush(b).get("planes", PackedFloat32Array())
	for i in pl.size() / 4:
		if Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).distance_to(n) < 1e-3:
			return true
	return false


func _mirror(axis_key: int) -> bool:
	_map._unhandled_key_input(_key(KEY_M, true))
	var op: Object = _stack.get("active")
	if op == null or op.get_script() == null or String((op.get_script() as Script).get_global_name()) != "MirrorOperator":
		return false
	_map._unhandled_key_input(_key(axis_key))
	return bool(_stack.call("confirm"))


func _run() -> void:
	_persist = _map.get("_persist")
	_level = _map.get("level")
	_stack = _map.call("_ensure_ops")

	print("-- M1-M3: X about the bounding box --")
	_map.set_pivot_mode("bbox")
	var b := _wedge(Vector3(0, 0, 0), Vector3(2, 1, 1), 2.5)
	var key: String = _level.get_brush_key(b)
	var bb0 := _bounds(b)
	_p(_has_normal(b, Vector3(S, S, 0)), "(setup) the wedge's slant faces +x +y")
	_map.select_brushes(PackedInt32Array([b]))
	var seq0: int = _persist._max_seq_seen()
	_p(_mirror(KEY_X), "M1 Ctrl+M starts Mirror, X, Enter mirrors")
	b = _level.brush_by_key(key)
	var bb1 := _bounds(b)
	_p(bb1.is_equal_approx(bb0), "M2 about its own bounding box: the bounds are where they were", [bb0, bb1])
	_p(_has_normal(b, Vector3(-S, S, 0)) and not _has_normal(b, Vector3(S, S, 0)), "M2 the slant now faces -x +y")
	var gestures := 0
	for r: Dictionary in (_persist.records() as Array):
		if int(r["seq"]) > seq0:
			gestures += 1
	_p(gestures >= 1, "M3 it was journalled", gestures)
	_persist.refold()
	b = _level.brush_by_key(key)
	_p(_has_normal(b, Vector3(-S, S, 0)), "M3 a refold reproduces it")
	_persist.undo_gesture(_persist.newest_live_gesture())
	_persist.refold()
	b = _level.brush_by_key(key)
	_p(_has_normal(b, Vector3(S, S, 0)), "M3 one undo puts it back")

	print("-- M4: Y --")
	_map.select_brushes(PackedInt32Array([b]))
	_p(_mirror(KEY_Y), "M4 Ctrl+M, Y, Enter")
	b = _level.brush_by_key(key)
	_p(_has_normal(b, Vector3(S, -S, 0)), "M4 the slant now faces +x -y")
	_persist.undo_gesture(_persist.newest_live_gesture())
	_persist.refold()
	b = _level.brush_by_key(key)

	print("-- M5: about the 3D cursor --")
	var cur: Node3D = _map.get("_cursor3d")
	cur.global_position = (_level as Node3D).global_transform * Vector3(-3, 0, 0)
	_map.set_pivot_mode("cursor")
	_map.select_brushes(PackedInt32Array([b]))
	_p(_mirror(KEY_X), "M5 Ctrl+M, X, Enter about the cursor")
	b = _level.brush_by_key(key)
	var bb5 := _bounds(b)
	_p(absf(bb5.position.x - (-8.0)) < 1e-3 and absf(bb5.end.x - (-6.0)) < 1e-3,
		"M5 x in [-8, -6] (the [0, 2] wedge reflected about x = -3)", bb5)
	_persist.undo_gesture(_persist.newest_live_gesture())
	_persist.refold()
	b = _level.brush_by_key(key)

	print("-- M6: Individual Origins --")
	var b2 := _wedge(Vector3(10, 0, 0), Vector3(12, 1, 1), 12.5)
	var key2: String = _level.get_brush_key(b2)
	b = _level.brush_by_key(key)
	_map.set_pivot_mode("individual")
	_map.select_brushes(PackedInt32Array([b, b2]))
	_p(_mirror(KEY_X), "M6 Ctrl+M, X, Enter with Individual Origins")
	b = _level.brush_by_key(key)
	b2 = _level.brush_by_key(key2)
	_p(_bounds(b).is_equal_approx(bb0) and _has_normal(b, Vector3(-S, S, 0)), "M6 the first mirrored in place")
	_p(absf(_bounds(b2).position.x - 10.0) < 1e-3 and _has_normal(b2, Vector3(-S, S, 0)), "M6 the second mirrored in place",
		_bounds(b2))
	_map.set_pivot_mode("bbox")

	print("-- M7: a shape asks --")
	var sdot: String = _persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(20, 0, 0))),
		"size": [2, 1, 1], "material_id": 2, "level_id": 0})
	_persist.refold()
	_map.select_shape(sdot)
	var seq7: int = _persist._max_seq_seen()
	_map._unhandled_key_input(_key(KEY_M, true))
	_p(String(_map.get("_pending_part_shape")) == sdot and _stack.get("active") == null,
		"M7 Mirror on a shape asks Make Real first", [_map.get("_pending_part_shape"), _stack.get("active")])
	_p(_persist._max_seq_seen() == seq7, "M7 and writes nothing")
	# Cancel as a click does: the popup's own pick closes it (its `menu` key context
	# goes with it); calling the handler alone left the menu open, eating Ctrl+M below.
	var q: Node = _map.get("_q_menu")
	if q != null and q.is_open():
		q.pick("builder.prompt_cancel")
	_map._q_prompt_cancel()

	print("-- M8: an object mirrors its placement --")
	cur.global_position = (_level as Node3D).global_transform * Vector3.ZERO
	_map.set_pivot_mode("cursor")
	var md: String = _map.create_object("marker", Transform3D(Basis(Vector3.UP, deg_to_rad(30.0)), Vector3(3, 0, 3)))
	_map.select_object(md)
	_p(_mirror(KEY_X), "M8 Ctrl+M, X, Enter on a marker")
	_persist.refold()
	var t: Transform3D = (_map.object_node(md) as Node3D).transform
	_p(t.origin.distance_to(Vector3(-3, 0, 3)) < 1e-3, "M8 its position mirrored about the cursor", t.origin)
	var fwd := -t.basis.z
	var want := Basis(Vector3.UP, deg_to_rad(-30.0)) * Vector3.FORWARD
	_p(fwd.distance_to(want) < 1e-3, "M8 its facing mirrored (yaw 30° -> -30°)", [fwd, want])
	_p(t.basis.determinant() > 0.0, "M8 and it is not turned inside out (no negative scale)", t.basis.determinant())

	print("-- M9: nothing to mirror --")
	_map.clear_selection()
	_map._unhandled_key_input(_key(KEY_M, true))
	_p(_stack.get("active") == null, "M9 nothing selected or aimed: no stray tool", _stack.get("active"))