Files
stickman/tests/test_phase20a_hit_testing.gd

108 lines
4.2 KiB
GDScript

extends SceneTree
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
var _checks: int = 0
var _failures: int = 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var world := Node2D.new()
root.add_child(world)
var camera := Camera2D.new()
root.add_child(camera)
var selection: StageSelection = STAGE_SELECTION.new(world, camera) as StageSelection
var triangle: PropBlock = _make_prop("Triangle")
world.add_child(triangle)
var triangle_points := PackedVector2Array([Vector2(-50.0, -50.0), Vector2(50.0, -50.0), Vector2(0.0, 50.0)])
triangle.set_custom_shapes([{
"shape_type": "polygon",
"points": triangle_points,
"closed": true,
"fill_color": "#55AAFFFF",
"outline_color": "#111111FF",
"outline_width": 2.0,
}], triangle_points)
await process_frame
_check(selection.hit_test_all(Vector2.ZERO).has(triangle), "visible polygon interior is selectable")
_check(not selection.hit_test_all(Vector2(40.0, 40.0)).has(triangle), "empty point inside polygon AABB is rejected")
var front: PropBlock = _make_prop("Front")
front.position = Vector2.ZERO
front.z_index = 2
world.add_child(front)
front.set_custom_shapes([{
"shape_type": "polygon",
"points": triangle_points,
"closed": true,
"fill_color": "#FFAA55FF",
"outline_color": "#111111FF",
"outline_width": 2.0,
}], triangle_points)
await process_frame
var overlap_hits: Array[Node2D] = selection.hit_test_all(Vector2.ZERO)
_check(overlap_hits.size() == 2 and overlap_hits[0] == front and overlap_hits[1] == triangle, "overlap candidates preserve front-to-back draw order")
_check(not selection.hit_test_all(Vector2(40.0, 40.0)).has(front), "nearby overlapping prop is excluded outside its visible shape")
triangle.position = Vector2(200.0, 0.0)
triangle.rotation = PI * 0.5
triangle.scale = Vector2(2.0, 1.0)
await process_frame
var transformed_center: Vector2 = triangle.to_global(Vector2.ZERO)
var transformed_empty: Vector2 = triangle.to_global(Vector2(40.0, 40.0))
_check(selection.hit_test_all(transformed_center).has(triangle), "polygon hit testing follows object translation, rotation, and scale")
_check(not selection.hit_test_all(transformed_empty).has(triangle), "transformed empty polygon area remains unselectable")
var line_prop: PropBlock = PROP_BLOCK.new() as PropBlock
line_prop.name = "Thin Line"
line_prop.position = Vector2(400.0, 100.0)
world.add_child(line_prop)
var line_points := PackedVector2Array([Vector2(-20.0, 0.0), Vector2(20.0, 0.0)])
var line_shape: Array[Dictionary] = [{
"shape_type": "line",
"points": line_points,
"closed": false,
"fill_color": "#00000000",
"outline_color": "#FFFFFFFF",
"outline_width": 2.0,
}]
line_prop.set_custom_shapes(line_shape, PackedVector2Array())
await process_frame
_check(selection.hit_test_all(Vector2(400.0, 105.0)).has(line_prop), "thin visible line remains selectable within the screen-space touch tolerance")
_check(not selection.hit_test_all(Vector2(400.0, 109.0)).has(line_prop), "thin line rejects points beyond the touch tolerance")
camera.zoom = Vector2(2.0, 2.0)
_check(selection.hit_test_all(Vector2(400.0, 104.0)).has(line_prop), "line touch tolerance scales with camera zoom")
_check(not selection.hit_test_all(Vector2(400.0, 105.0)).has(line_prop), "zoomed line does not retain an oversized world-space tolerance")
camera.zoom = Vector2.ONE
line_prop.modulate.a = 0.0
_check(not selection.hit_test_all(Vector2(400.0, 100.0)).has(line_prop), "fully transparent line is not selectable")
line_prop.modulate.a = 1.0
line_prop.visible = false
_check(not selection.hit_test_all(Vector2(400.0, 100.0)).has(line_prop), "hidden line is not selectable")
print("PHASE 20A HIT TESTING: %d checks, %d failures" % [_checks, _failures])
world.queue_free()
camera.queue_free()
await process_frame
quit(1 if _failures > 0 else 0)
func _make_prop(prop_name: String) -> PropBlock:
var prop := PROP_BLOCK.new() as PropBlock
prop.name = prop_name
return prop
func _check(condition: bool, message: String) -> void:
_checks += 1
if condition:
print("PASS: " + message)
else:
_failures += 1
push_error("FAIL: " + message)