3D / voxel unit tests
This commit is contained in:
parent
e12e80e511
commit
71cd2b9b41
22 changed files with 4705 additions and 0 deletions
247
tests/unit/voxel_navigation_test.py
Normal file
247
tests/unit/voxel_navigation_test.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Unit tests for Milestone 12: VoxelGrid Navigation Projection
|
||||
|
||||
Tests VoxelGrid.project_column() and Viewport3D voxel-to-nav projection methods.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test counters
|
||||
tests_passed = 0
|
||||
tests_failed = 0
|
||||
|
||||
def test(name, condition):
|
||||
"""Simple test helper"""
|
||||
global tests_passed, tests_failed
|
||||
if condition:
|
||||
tests_passed += 1
|
||||
print(f" PASS: {name}")
|
||||
else:
|
||||
tests_failed += 1
|
||||
print(f" FAIL: {name}")
|
||||
|
||||
def approx_eq(a, b, epsilon=0.001):
|
||||
"""Approximate floating-point equality"""
|
||||
return abs(a - b) < epsilon
|
||||
|
||||
# =============================================================================
|
||||
# Test projectColumn() on VoxelGrid
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== Testing VoxelGrid.project_column() ===")
|
||||
|
||||
# Test 1: Empty grid - all air
|
||||
vg = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
nav = vg.project_column(5, 5)
|
||||
test("Empty grid - height is 0", approx_eq(nav['height'], 0.0))
|
||||
test("Empty grid - not walkable (no floor)", nav['walkable'] == False)
|
||||
test("Empty grid - transparent", nav['transparent'] == True)
|
||||
test("Empty grid - default path cost", approx_eq(nav['path_cost'], 1.0))
|
||||
|
||||
# Test 2: Simple floor
|
||||
vg2 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
stone = vg2.add_material("stone", (128, 128, 128))
|
||||
vg2.fill_box((0, 0, 0), (9, 0, 9), stone) # Floor at y=0
|
||||
nav2 = vg2.project_column(5, 5)
|
||||
test("Floor at y=0 - height is 1.0 (top of floor voxel)", approx_eq(nav2['height'], 1.0))
|
||||
test("Floor at y=0 - walkable", nav2['walkable'] == True)
|
||||
test("Floor at y=0 - not transparent (has solid voxel)", nav2['transparent'] == False)
|
||||
|
||||
# Test 3: Solid column extending to top - no headroom at boundary
|
||||
vg3 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
stone3 = vg3.add_material("stone", (128, 128, 128))
|
||||
vg3.fill_box((0, 0, 0), (9, 0, 9), stone3) # Floor at y=0
|
||||
vg3.fill_box((0, 2, 0), (9, 9, 9), stone3) # Solid block from y=2 to y=9
|
||||
nav3 = vg3.project_column(5, 5, headroom=2)
|
||||
# Scan finds y=9 as topmost floor (boundary has "air above" but no actual headroom)
|
||||
# Height = 10.0 (top of y=9 voxel), no air above means airCount=0, so not walkable
|
||||
test("Top boundary floor - height at top", approx_eq(nav3['height'], 10.0))
|
||||
test("Top boundary floor - not walkable (no headroom)", nav3['walkable'] == False)
|
||||
|
||||
# Test 4: Single floor slab with plenty of headroom
|
||||
vg4 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
stone4 = vg4.add_material("stone", (128, 128, 128))
|
||||
vg4.fill_box((0, 2, 0), (9, 2, 9), stone4) # Floor slab at y=2 (air below, 7 voxels air above)
|
||||
nav4 = vg4.project_column(5, 5, headroom=2)
|
||||
test("Floor slab at y=2 - height is 3.0", approx_eq(nav4['height'], 3.0))
|
||||
test("Floor slab - walkable (7 voxels headroom)", nav4['walkable'] == True)
|
||||
|
||||
# Test 5: Custom headroom thresholds
|
||||
nav4_h1 = vg4.project_column(5, 5, headroom=1)
|
||||
test("Headroom=1 - walkable", nav4_h1['walkable'] == True)
|
||||
nav4_h7 = vg4.project_column(5, 5, headroom=7)
|
||||
test("Headroom=7 - walkable (exactly 7 air voxels)", nav4_h7['walkable'] == True)
|
||||
nav4_h8 = vg4.project_column(5, 5, headroom=8)
|
||||
test("Headroom=8 - not walkable (only 7 air)", nav4_h8['walkable'] == False)
|
||||
|
||||
# Test 6: Multi-level floor (finds topmost walkable)
|
||||
vg5 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
stone5 = vg5.add_material("stone", (128, 128, 128))
|
||||
vg5.fill_box((0, 0, 0), (9, 0, 9), stone5) # Bottom floor at y=0
|
||||
vg5.fill_box((0, 5, 0), (9, 5, 9), stone5) # Upper floor at y=5
|
||||
nav5 = vg5.project_column(5, 5)
|
||||
test("Multi-level - finds top floor", approx_eq(nav5['height'], 6.0))
|
||||
test("Multi-level - walkable", nav5['walkable'] == True)
|
||||
|
||||
# Test 7: Transparent material
|
||||
vg6 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
glass = vg6.add_material("glass", (200, 200, 255), transparent=True)
|
||||
vg6.set(5, 5, 5, glass)
|
||||
nav6 = vg6.project_column(5, 5)
|
||||
test("Transparent voxel - column is transparent", nav6['transparent'] == True)
|
||||
|
||||
# Test 8: Non-transparent material
|
||||
vg7 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
wall = vg7.add_material("wall", (100, 100, 100), transparent=False)
|
||||
vg7.set(5, 5, 5, wall)
|
||||
nav7 = vg7.project_column(5, 5)
|
||||
test("Opaque voxel - column not transparent", nav7['transparent'] == False)
|
||||
|
||||
# Test 9: Path cost from material
|
||||
vg8 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=1.0)
|
||||
mud = vg8.add_material("mud", (139, 90, 43), path_cost=2.0)
|
||||
vg8.fill_box((0, 0, 0), (9, 0, 9), mud) # Floor of mud
|
||||
nav8 = vg8.project_column(5, 5)
|
||||
test("Mud floor - path cost is 2.0", approx_eq(nav8['path_cost'], 2.0))
|
||||
|
||||
# Test 10: Cell size affects height
|
||||
vg9 = mcrfpy.VoxelGrid((10, 10, 10), cell_size=2.0)
|
||||
stone9 = vg9.add_material("stone", (128, 128, 128))
|
||||
vg9.fill_box((0, 0, 0), (9, 0, 9), stone9) # Floor at y=0
|
||||
nav9 = vg9.project_column(5, 5)
|
||||
test("Cell size 2.0 - height is 2.0", approx_eq(nav9['height'], 2.0))
|
||||
|
||||
# Test 11: Out of bounds returns default
|
||||
nav_oob = vg.project_column(-1, 5)
|
||||
test("Out of bounds - not walkable", nav_oob['walkable'] == False)
|
||||
test("Out of bounds - height 0", approx_eq(nav_oob['height'], 0.0))
|
||||
|
||||
# =============================================================================
|
||||
# Test Viewport3D voxel-to-nav projection
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== Testing Viewport3D voxel-to-nav projection ===")
|
||||
|
||||
# Create viewport with navigation grid
|
||||
vp = mcrfpy.Viewport3D(pos=(0, 0), size=(640, 480))
|
||||
vp.set_grid_size(20, 20)
|
||||
vp.cell_size = 1.0
|
||||
|
||||
# Test 12: Initial nav grid state
|
||||
cell = vp.at(10, 10)
|
||||
test("Initial nav cell - walkable", cell.walkable == True)
|
||||
test("Initial nav cell - transparent", cell.transparent == True)
|
||||
test("Initial nav cell - height 0", approx_eq(cell.height, 0.0))
|
||||
test("Initial nav cell - cost 1", approx_eq(cell.cost, 1.0))
|
||||
|
||||
# Test 13: Project simple voxel grid
|
||||
vg_nav = mcrfpy.VoxelGrid((10, 5, 10), cell_size=1.0)
|
||||
stone_nav = vg_nav.add_material("stone", (128, 128, 128))
|
||||
vg_nav.fill_box((0, 0, 0), (9, 0, 9), stone_nav) # Floor
|
||||
vg_nav.offset = (5, 0, 5) # Position grid at (5, 0, 5) in world
|
||||
|
||||
vp.add_voxel_layer(vg_nav)
|
||||
vp.project_voxel_to_nav(vg_nav, headroom=2)
|
||||
|
||||
# Check cell within grid footprint
|
||||
cell_in = vp.at(10, 10) # World (10, 10) = voxel grid local (5, 5)
|
||||
test("Projected cell - walkable (floor present)", cell_in.walkable == True)
|
||||
test("Projected cell - height is 1.0", approx_eq(cell_in.height, 1.0))
|
||||
test("Projected cell - not transparent", cell_in.transparent == False)
|
||||
|
||||
# Check cell outside grid footprint (unchanged)
|
||||
cell_out = vp.at(0, 0) # Outside voxel grid area
|
||||
test("Outside cell - still walkable (unchanged)", cell_out.walkable == True)
|
||||
test("Outside cell - height still 0", approx_eq(cell_out.height, 0.0))
|
||||
|
||||
# Test 14: Clear voxel nav region
|
||||
vp.clear_voxel_nav_region(vg_nav)
|
||||
cell_cleared = vp.at(10, 10)
|
||||
test("Cleared cell - walkable reset to true", cell_cleared.walkable == True)
|
||||
test("Cleared cell - height reset to 0", approx_eq(cell_cleared.height, 0.0))
|
||||
test("Cleared cell - transparent reset to true", cell_cleared.transparent == True)
|
||||
|
||||
# Test 15: Project with walls (blocking)
|
||||
vg_wall = mcrfpy.VoxelGrid((10, 5, 10), cell_size=1.0)
|
||||
stone_wall = vg_wall.add_material("stone", (128, 128, 128))
|
||||
vg_wall.fill_box((0, 0, 0), (9, 4, 9), stone_wall) # Solid block (no air above floor)
|
||||
vg_wall.offset = (0, 0, 0)
|
||||
|
||||
vp2 = mcrfpy.Viewport3D(pos=(0, 0), size=(640, 480))
|
||||
vp2.set_grid_size(20, 20)
|
||||
vp2.add_voxel_layer(vg_wall)
|
||||
vp2.project_voxel_to_nav(vg_wall)
|
||||
|
||||
cell_wall = vp2.at(5, 5)
|
||||
test("Solid block - height at top", approx_eq(cell_wall.height, 5.0))
|
||||
test("Solid block - not transparent", cell_wall.transparent == False)
|
||||
|
||||
# Test 16: project_all_voxels_to_nav with multiple layers
|
||||
vp3 = mcrfpy.Viewport3D(pos=(0, 0), size=(640, 480))
|
||||
vp3.set_grid_size(20, 20)
|
||||
|
||||
# First layer - lower priority
|
||||
vg_layer1 = mcrfpy.VoxelGrid((20, 5, 20), cell_size=1.0)
|
||||
dirt = vg_layer1.add_material("dirt", (139, 90, 43))
|
||||
vg_layer1.fill_box((0, 0, 0), (19, 0, 19), dirt) # Floor everywhere
|
||||
|
||||
# Second layer - higher priority, partial coverage
|
||||
vg_layer2 = mcrfpy.VoxelGrid((5, 5, 5), cell_size=1.0)
|
||||
stone_l2 = vg_layer2.add_material("stone", (128, 128, 128))
|
||||
vg_layer2.fill_box((0, 0, 0), (4, 2, 4), stone_l2) # Higher floor
|
||||
vg_layer2.offset = (5, 0, 5)
|
||||
|
||||
vp3.add_voxel_layer(vg_layer1, z_index=0)
|
||||
vp3.add_voxel_layer(vg_layer2, z_index=1)
|
||||
vp3.project_all_voxels_to_nav()
|
||||
|
||||
cell_dirt = vp3.at(0, 0) # Only dirt layer
|
||||
cell_stone = vp3.at(7, 7) # Stone layer overlaps (higher z_index)
|
||||
test("Multi-layer - dirt area height is 1", approx_eq(cell_dirt.height, 1.0))
|
||||
test("Multi-layer - stone area height is 3 (higher layer)", approx_eq(cell_stone.height, 3.0))
|
||||
|
||||
# Test 17: Viewport projection with different headroom values
|
||||
vg_low = mcrfpy.VoxelGrid((10, 5, 10), cell_size=1.0)
|
||||
stone_low = vg_low.add_material("stone", (128, 128, 128))
|
||||
vg_low.fill_box((0, 0, 0), (9, 0, 9), stone_low) # Floor at y=0
|
||||
# Grid has height=5, so floor at y=0 has 4 air voxels above (y=1,2,3,4)
|
||||
|
||||
vp4 = mcrfpy.Viewport3D(pos=(0, 0), size=(640, 480))
|
||||
vp4.set_grid_size(20, 20)
|
||||
vp4.add_voxel_layer(vg_low)
|
||||
|
||||
vp4.project_voxel_to_nav(vg_low, headroom=1)
|
||||
test("Headroom 1 - walkable (4 air voxels)", vp4.at(5, 5).walkable == True)
|
||||
|
||||
vp4.project_voxel_to_nav(vg_low, headroom=4)
|
||||
test("Headroom 4 - walkable (exactly 4 air)", vp4.at(5, 5).walkable == True)
|
||||
|
||||
vp4.project_voxel_to_nav(vg_low, headroom=5)
|
||||
test("Headroom 5 - not walkable (only 4 air)", vp4.at(5, 5).walkable == False)
|
||||
|
||||
# Test 18: Grid offset in world space
|
||||
vg_offset = mcrfpy.VoxelGrid((5, 5, 5), cell_size=1.0)
|
||||
stone_off = vg_offset.add_material("stone", (128, 128, 128))
|
||||
vg_offset.fill_box((0, 0, 0), (4, 0, 4), stone_off)
|
||||
vg_offset.offset = (10, 5, 10) # Y offset = 5
|
||||
|
||||
vp5 = mcrfpy.Viewport3D(pos=(0, 0), size=(640, 480))
|
||||
vp5.set_grid_size(20, 20)
|
||||
vp5.add_voxel_layer(vg_offset)
|
||||
vp5.project_voxel_to_nav(vg_offset)
|
||||
|
||||
cell_off = vp5.at(12, 12)
|
||||
test("Y-offset grid - height includes offset", approx_eq(cell_off.height, 6.0)) # floor 1 + offset 5
|
||||
|
||||
# =============================================================================
|
||||
# Summary
|
||||
# =============================================================================
|
||||
|
||||
print(f"\n=== Results: {tests_passed} passed, {tests_failed} failed ===")
|
||||
|
||||
if tests_failed > 0:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("All tests passed!")
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue