OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_bevelled_block.gd

145 lines · gd ref:
extends SceneTree
## Gate: the block is the size asked for, is bevelled, and is ONE shared mesh.
##
## "Instanced" is the load-bearing word. A builder bot stacks many of these, so
## the geometry has to be a shared resource rather than a copy per block: a
## hundred blocks must mean a hundred MeshInstance3D nodes and exactly one
## ArrayMesh. Building the mesh in _ready per instance would look identical on
## screen and be a hundred copies in memory, which is the failure this exists to
## catch — and it is invisible without asking.

var ok := true
var _ran := false


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


func _process(_delta: float) -> bool:
	if _ran:
		return true
	_ran = true
	_run()
	print("GATE bevelled_block: %s" % ("PASS" if ok else "FAIL"))
	if not ok:
		quit(1)
	return true


func _mesh_inst(n: Node) -> MeshInstance3D:
	for c in n.get_children():
		if c is MeshInstance3D:
			return c as MeshInstance3D
	return null


func _run() -> void:
	var Catalogue: GDScript = load("res://scenes/game/prefab_catalogue.gd")
	_p(Catalogue != null, "prefab_catalogue.gd loads")
	if Catalogue == null:
		return
	_p(bool(Catalogue.has("block.bevelled")), "the catalogue knows 'block.bevelled'")
	if not Catalogue.has("block.bevelled"):
		return

	var a: Node = Catalogue.instantiate("block.bevelled")
	var b: Node = Catalogue.instantiate("block.bevelled")
	_p(a != null and b != null, "two blocks instantiate")
	if a == null or b == null:
		return
	root.add_child(a)
	root.add_child(b)

	var ma := _mesh_inst(a)
	var mb := _mesh_inst(b)
	_p(ma != null and mb != null, "each block has a MeshInstance3D")
	if ma == null or mb == null:
		return
	_p(ma.mesh != null, "and a mesh")

	# ---- THE POINT: one resource, two nodes ----
	_p(ma.mesh == mb.mesh,
		"both blocks share the SAME ArrayMesh instance (this is what 'instanced' means)")
	_p(ma.material_override == mb.material_override, "and the same material")

	# ---- the size asked for ----
	var aabb: AABB = ma.mesh.get_aabb()
	_p(abs(aabb.size.x - 10.0) < 0.01, "x is 10 m (%.3f)" % aabb.size.x)
	_p(abs(aabb.size.y - 10.0) < 0.01, "y is 10 m (%.3f)" % aabb.size.y)
	_p(abs(aabb.size.z - 20.0) < 0.01, "z is 20 m (%.3f)" % aabb.size.z)
	_p(aabb.get_center().length() < 0.01, "centred on its origin, so a stack is predictable")

	# ---- actually bevelled ----
	# A plain box is 12 triangles. A chamfered one adds 12 edge quads (24) and 8
	# corner triangles, so anything near 12 means the bevel silently did not happen.
	var tris: int = ma.mesh.get_faces().size() / 3
	_p(tris > 40, "the mesh is chamfered, not a plain box (%d triangles)" % tris)

	# A bevel means no vertex sits on a corner of the bounding box.
	var corner := Vector3(5.0, 5.0, 10.0)
	var on_corner := 0
	for v: Vector3 in ma.mesh.get_faces():
		if abs(abs(v.x) - corner.x) < 0.001 and abs(abs(v.y) - corner.y) < 0.001 \
				and abs(abs(v.z) - corner.z) < 0.001:
			on_corner += 1
	_p(on_corner == 0, "no vertex sits on a sharp corner — the edges are cut back (%d)" % on_corner)

	# SELF-CHECK: that test must be able to fail. A plain BoxMesh of the same size
	# has eight such vertices, so the check is discriminating and not vacuous.
	var plain := BoxMesh.new()
	plain.size = Vector3(10.0, 10.0, 20.0)
	var plain_on_corner := 0
	for v: Vector3 in plain.get_faces():
		if abs(abs(v.x) - corner.x) < 0.001 and abs(abs(v.y) - corner.y) < 0.001 \
				and abs(abs(v.z) - corner.z) < 0.001:
			plain_on_corner += 1
	_p(plain_on_corner > 0,
		"self-check: a plain box of the same size DOES hit corners (%d)" % plain_on_corner)

	# ---- THE NORMALS POINT OUT ----
	# ⛔ This is here because the block shipped inside-out. Winding by
	# (b-a) x (c-a) and testing against the centroid is correct arithmetic and the
	# WRONG CONVENTION: Godot's generate_normals() produced the exact negation, so
	# all 132 vertex normals pointed toward the origin, front faces were inward,
	# the outsides were back-face culled and the block rendered as its interior.
	#
	# Checked against the stored normals rather than against the winding, because
	# the winding measured fine the whole time -- 0 of 44 triangles "inward" by its
	# own arithmetic -- which is exactly why this reads as a winding bug and is not
	# one. The block is convex and centred, so a normal is outward iff it points
	# away from the origin.
	var arrays: Array = ma.mesh.surface_get_arrays(0)
	var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
	var norms: PackedVector3Array = arrays[Mesh.ARRAY_NORMAL]
	_p(norms.size() == verts.size() and verts.size() > 0,
		"the mesh carries a normal per vertex (%d/%d)" % [norms.size(), verts.size()])
	var inward := 0
	for i in range(0, verts.size(), 3):
		var centre_t: Vector3 = (verts[i] + verts[i + 1] + verts[i + 2]) / 3.0
		for k in 3:
			if norms[i + k].dot(centre_t) <= 0.0:
				inward += 1
	_p(inward == 0,
		"every normal points AWAY from the centre — the block is not inside-out (%d inward)" % inward)

	# SELF-CHECK: that test must be able to fail. Negated normals on the same mesh
	# must be detected as inward, or the loop above proves only that it ran.
	var faked := 0
	for i in range(0, verts.size(), 3):
		var centre_f: Vector3 = (verts[i] + verts[i + 1] + verts[i + 2]) / 3.0
		for k in 3:
			if (-norms[i + k]).dot(centre_f) <= 0.0:
				faked += 1
	_p(faked == verts.size(),
		"self-check: flipping every normal is detected as fully inward (%d/%d)" % [faked, verts.size()])

	# ---- it can be stood on ----
	var body: Node = a.get_node_or_null(^"Body")
	_p(body != null and body is StaticBody3D, "the block has a StaticBody3D")

	a.free()
	b.free()