OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_face_primitives.gd

147 lines · gd ref:
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])