OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_make_parametric.gd

184 lines · gd ref:
extends Node
## GATE — MAKE PARAMETRIC (#120 F2, docs/node-tree-and-stacks.md §3.1): any brush becomes
## a parametric shape that generates it.
##   M1  an axis-aligned box brush -> a `box` shape, the same corners, the brush gone;
##       one gesture, one Ctrl+Z brings the brush back
##   M2  a wedge (a box with a slanted face) -> a `hull` shape with the same corners
##   M3  ... each face keeps its own texture
##   M4  resizing the hull shape stretches it (size is a real knob)
##   M5  a brush in a collection: its shape is in that collection
##   M6  a shape's piece is refused (nothing written)
##   M7  the Q menu offers it for a plain brush, and the menu action (Keymap.run's path for
##       a menu-only action) converts it
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_make_parametric.tscn

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node


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")
	map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
	add_child(map)
	for i in 6:
		await get_tree().process_frame
	await _run()
	get_tree().root.get_node("/root/GameMode").set_mode("play")
	if checks < 15:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE make_parametric: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


## Two corner sets equal within 1 mm (order-free).
func _same_corners(a: PackedVector3Array, b: PackedVector3Array) -> bool:
	if a.size() != b.size():
		return false
	for v: Vector3 in a:
		var hit := false
		for w: Vector3 in b:
			if v.distance_to(w) < 1.0e-3:
				hit = true
				break
		if not hit:
			return false
	return true


func _corners_of_group(g: String) -> PackedVector3Array:
	var out := PackedVector3Array()
	for b: int in (level.get_group_brush_ids(g) as PackedInt32Array):
		out.append_array(FaceKit.corners(level.get_brush(b)["planes"]))
	return out


func _new_brush(planes: Array, face_tex: Array) -> int:
	persist.begin_gesture("Add")
	var dot: String = persist.record("brush.create", {"planes": planes, "proj": [], "material_id": 1,
		"level_id": 0, "sky": false, "face_tex": face_tex})
	persist.end_gesture()
	persist.refold()
	return int(level.brush_by_key(dot))


func _tex(name: String) -> Dictionary:
	return {"tex": name, "mode": 0}


func _run() -> void:
	persist = map.get("_persist")
	level = map.get("level")
	persist.refold()

	# ---- M1 ----
	var box: Array = Array(ShapeKit.box_piece(Vector3(40, 0, 40), Vector3(43, 2, 44))["planes"])
	var b1 := _new_brush(box, [])
	var c1 := FaceKit.corners(level.get_brush(b1)["planes"])
	var k1 := String(level.get_brush_key(b1))
	map.select_brushes(PackedInt32Array([b1]))
	var g0: int = (persist.gestures() as Array).size()
	var made: Array = map.make_parametric()
	_ok("M1 one shape made", made.size() == 1, made)
	if made.is_empty():
		return
	var s1 := String(made[0])
	_ok("M1 ... of type box", String(persist.shape_state(s1).get("type", "")) == "box", persist.shape_state(s1).get("type"))
	_ok("M1 ... generating the same corners", _same_corners(_corners_of_group(s1), c1), _corners_of_group(s1))
	_ok("M1 the brush is gone", int(level.brush_by_key(k1)) <= 0 or bool(level.get_brush(int(level.brush_by_key(k1))).get("tombstoned", false))
		or not (level.get_live_brush_ids() as PackedInt32Array).has(int(level.brush_by_key(k1))))
	_ok("M1 one gesture", (persist.gestures() as Array).size() == g0 + 1)
	persist.undo_last()
	persist.refold()
	_ok("M1 one Ctrl+Z: the brush is back, the shape gone", (level.get_live_brush_ids() as PackedInt32Array).has(int(level.brush_by_key(k1)))
		and not (persist.live_shapes() as Dictionary).has(s1))

	# ---- M2 / M3 ----
	var wedge: Array = Array(ShapeKit.box_piece(Vector3(50, 0, 50), Vector3(54, 3, 52))["planes"])
	var sn := Vector3(1, 1, 0).normalized()
	wedge.append_array([sn.x, sn.y, sn.z, sn.dot(Vector3(53, 3, 50))])
	var texs: Array = []
	for i in wedge.size() / 4:
		texs.append(_tex("t%d" % i))
	var b2 := _new_brush(wedge, texs)
	var c2 := FaceKit.corners(level.get_brush(b2)["planes"])
	var pl2: PackedFloat32Array = level.get_brush(b2)["planes"]
	map.select_brushes(PackedInt32Array([b2]))
	var s2 := String(map.make_parametric()[0])
	_ok("M2 a wedge becomes a hull", String(persist.shape_state(s2).get("type", "")) == "hull")
	_ok("M2 ... with the same corners", _same_corners(_corners_of_group(s2), c2), [c2.size(), _corners_of_group(s2).size()])
	var piece: int = (level.get_group_brush_ids(s2) as PackedInt32Array)[0]
	var pp: PackedFloat32Array = level.get_brush(piece)["planes"]
	var pft: Array = level.get_brush(piece)["face_tex"]
	var tex_ok := pp.size() == pl2.size()
	for i in pl2.size() / 4:
		var n := Vector3(pl2[i * 4], pl2[i * 4 + 1], pl2[i * 4 + 2])
		for j in pp.size() / 4:
			if Vector3(pp[j * 4], pp[j * 4 + 1], pp[j * 4 + 2]).dot(n) > 0.9999:
				tex_ok = tex_ok and String((pft[j] as Dictionary).get("tex", "")) == "t%d" % i
	_ok("M3 each face keeps its own texture", tex_ok, pft.map(func(t: Dictionary) -> String: return String(t.get("tex", ""))))

	# ---- M4 ----
	var sz: Vector3 = persist.shape_state(s2)["size"]
	persist.begin_gesture("Size")
	persist.record("shape.size", {"target": s2, "size": [sz.x * 2.0, sz.y, sz.z]})
	persist.end_gesture()
	persist.refold()
	var c4 := _corners_of_group(s2)
	var span := 0.0
	var lo := INF
	var hi := -INF
	for v: Vector3 in c4:
		lo = minf(lo, v.x)
		hi = maxf(hi, v.x)
	span = hi - lo
	_ok("M4 doubling the size doubles the hull along x", absf(span - 8.0) < 1.0e-3, span)

	# ---- M5 ----
	var b5 := _new_brush(Array(ShapeKit.box_piece(Vector3(60, 0, 60), Vector3(61, 1, 61))["planes"]), [])
	map.select_brushes(PackedInt32Array([b5]))
	var col := CollectionOps.create_from_selection(map, "Kept")
	persist.refold()
	map.select_brushes(PackedInt32Array([int(level.brush_by_key(String(level.get_brush_key(b5))))]))
	var s5 := String(map.make_parametric()[0])
	var members: Array = persist.collection_tree()["cols"][col]["members"]
	_ok("M5 the shape REPLACES the brush in its collection", members == ["g:" + s5], members)

	# ---- M6 ----
	map.select_brushes(PackedInt32Array([piece]))
	var g6: int = (persist.gestures() as Array).size()
	_ok("M6 a shape's piece is refused", (map.make_parametric() as Array).is_empty())
	_ok("M6 ... nothing written", (persist.gestures() as Array).size() == g6 or persist.gestures()[0]["count"] == 0)

	# ---- M7 ----
	var b7 := _new_brush(Array(ShapeKit.box_piece(Vector3(70, 0, 70), Vector3(71, 1, 71))["planes"]), [])
	map.select_brushes(PackedInt32Array([b7]))
	var rows7: Array = []
	for sec: Array in map.q_sections():
		for r: Array in sec[1]:
			rows7.append(String(r[0]))
	_ok("M7 the Q menu offers Make parametric", rows7.has("builder.make_parametric"), rows7)
	var n7: int = (persist.live_shapes() as Dictionary).size()
	var ev := InputEventAction.new()
	ev.action = "builder.make_parametric"
	ev.pressed = true
	map._unhandled_input(ev)
	persist.refold()
	_ok("M7 the menu action converts it", (persist.live_shapes() as Dictionary).size() == n7 + 1)