extends Node
## GATE — FACE PRIMITIVES (#120 F6, docs/node-tree-and-stacks.md §7): mesh-style edits
## as convex brushes.
## F1 face_polygon of a box face: 4 corners, on the plane, counter-clockwise about the
## outward normal, starting at the smallest (x, y, z); all six faces
## F2 face_polygon of a SLANTED face (a box with a corner cut): its 5 corners, convex
## F3 prism outward over a face: the right solid, and its inner cap is the source
## face's plane exactly, reversed (an extrude shares the face: watertight)
## F4 prism inward (a recess's cutter): inside the source solid
## F5 hull_sections of two squares: a frustum, 6 planes; two segments that share a
## (tilted) middle section meet on one plane, opposite normals: watertight
## F6 split_rects: a block is one rectangle; an L is two; every cell covered once
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_face_primitives.tscn
var fails := 0
var checks := 0
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 _plane(pl: PackedFloat32Array, i: int) -> Plane:
return Plane(Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]), pl[i * 4 + 3])
## Counter-clockwise about n, and convex: every turn is a left turn.
func _ccw_convex(poly: PackedVector3Array, n: Vector3) -> bool:
for k in poly.size():
var a := poly[k]
var b := poly[(k + 1) % poly.size()]
var c := poly[(k + 2) % poly.size()]
if (b - a).cross(c - b).dot(n) <= 1.0e-6:
return false
return true
## Every point inside every plane (n · x <= d).
func _inside(pts: PackedVector3Array, pl: PackedFloat32Array) -> bool:
for v: Vector3 in pts:
for i in pl.size() / 4:
if _plane(pl, i).distance_to(v) > 1.0e-3:
return false
return true
## The plane of `pl` that matches plane `p` reversed (opposite normal, d negated).
func _has_reverse(pl: PackedFloat32Array, p: Plane) -> bool:
for i in pl.size() / 4:
var q := _plane(pl, i)
if (q.normal + p.normal).length() < 1.0e-4 and absf(q.d + p.d) < 1.0e-4:
return true
return false
func _ready() -> void:
var box: PackedFloat32Array = ShapeKit.box_piece(Vector3(0, 0, 0), Vector3(2, 1, 3))["planes"]
# ---- F1 ----
var top := -1
for i in box.size() / 4:
if _plane(box, i).normal.is_equal_approx(Vector3.UP):
top = i
var tp := FaceKit.face_polygon(box, top)
_ok("F1 the top face has 4 corners at y = 1", tp.size() == 4 and Array(tp).all(func(v: Vector3) -> bool: return is_equal_approx(v.y, 1.0)), tp)
_ok("F1 counter-clockwise about +y", _ccw_convex(tp, Vector3.UP), tp)
_ok("F1 starts at the smallest corner", tp.size() > 0 and tp[0].is_equal_approx(Vector3(0, 1, 0)), tp[0] if tp.size() > 0 else null)
var all_ok := true
for i in box.size() / 4:
var fp := FaceKit.face_polygon(box, i)
if fp.size() != 4 or not _ccw_convex(fp, _plane(box, i).normal):
all_ok = false
_ok("F1 all six faces: 4 corners, counter-clockwise", all_ok)
# ---- F2 ----
var cut := PackedFloat32Array(box)
var sn := Vector3(1, 1, 0).normalized()
cut.append_array(PackedFloat32Array([sn.x, sn.y, sn.z, sn.dot(Vector3(1.5, 1, 0))]))
var sp := FaceKit.face_polygon(cut, cut.size() / 4 - 1)
_ok("F2 the slanted face has its 4 corners, convex", sp.size() == 4 and _ccw_convex(sp, sn), sp)
var front := FaceKit.face_polygon(cut, 4) # box_piece's -z face, now 5 corners
_ok("F2 the cut side face has 5 corners, convex", front.size() == 5 and _ccw_convex(front, _plane(cut, 4).normal),
"%d %s" % [front.size(), _plane(cut, 4).normal])
# ---- F3 / F4 ----
var up := FaceKit.prism(tp, Vector3.UP, 0.5)
var upc := FaceKit.corners(up)
_ok("F3 an extrude is the slab y 1 .. 1.5 over the face", upc.size() == 8
and Array(upc).all(func(v: Vector3) -> bool: return v.y > 0.999 and v.y < 1.501), upc)
_ok("F3 its inner cap is the face's plane, reversed (watertight)", _has_reverse(up, _plane(box, top)))
var down := FaceKit.prism(tp, Vector3.UP, -0.25)
_ok("F4 a recess cutter sits inside the solid, y 0.75 .. 1", _inside(FaceKit.corners(down), box)
and Array(FaceKit.corners(down)).all(func(v: Vector3) -> bool: return v.y > 0.749), FaceKit.corners(down))
# ---- F5 ----
_ok("setup: the static DCLevel.hull_of", ClassDB.class_has_method("DCLevel", "hull_of"))
if ClassDB.class_has_method("DCLevel", "hull_of"):
var a := _square(Vector3(0, 0, 0), Vector3.RIGHT, 1.0)
var b := _square(Vector3(2, 0, 0), Vector3.RIGHT, 0.5)
var fr := FaceKit.hull_sections(a, b)
_ok("F5 two squares hull to a frustum (6 planes)", fr.size() / 4 == 6, fr.size() / 4)
# Three sections; the middle one tilted, as a bend's frame would be.
var s := _square(Vector3(4, 0.3, 0.2), Vector3(1, 0.4, 0.1).normalized(), 0.7)
var c := _square(Vector3(6, 1, 0), Vector3(0.6, 1, 0).normalized(), 0.6)
var h1 := FaceKit.hull_sections(b, s)
var h2 := FaceKit.hull_sections(s, c)
var shared := false
for i in h1.size() / 4:
var p := _plane(h1, i)
if Array(s).all(func(v: Vector3) -> bool: return absf(p.distance_to(v)) < 1.0e-3) and _has_reverse(h2, p):
shared = true
_ok("F5 two segments share the middle section's plane, reversed", shared)
# ---- F6 ----
var block: Array = []
for y in 2:
for x in 3:
block.append(Vector2i(x, y))
var r1 := FaceKit.split_rects(block)
_ok("F6 a 3 x 2 block is one rectangle", r1 == [Rect2i(0, 0, 3, 2)], r1)
var ell: Array = [Vector2i(0, 0), Vector2i(1, 0), Vector2i(2, 0), Vector2i(0, 1), Vector2i(0, 2)]
var r2 := FaceKit.split_rects(ell)
var count := {}
for r: Rect2i in r2:
for y in r.size.y:
for x in r.size.x:
var k := r.position + Vector2i(x, y)
count[k] = int(count.get(k, 0)) + 1
_ok("F6 an L is two rectangles", r2.size() == 2, r2)
_ok("F6 every cell covered exactly once, nothing else", count.size() == ell.size()
and ell.all(func(k: Vector2i) -> bool: return int(count.get(k, 0)) == 1), count)
print("\nGATE face_primitives: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
## A square cross-section centred at `c`, facing `n`, side `w`.
func _square(c: Vector3, n: Vector3, w: float) -> PackedVector3Array:
var u := n.cross(Vector3.UP if absf(n.y) < 0.9 else Vector3.FORWARD).normalized()
var v := n.cross(u).normalized()
var h := w * 0.5
return PackedVector3Array([c - u * h - v * h, c + u * h - v * h, c + u * h + v * h, c - u * h + v * h])