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()