503 lines
16 KiB
Python
503 lines
16 KiB
Python
"""Unit tests for HeightMap region-based operations
|
|
|
|
Tests:
|
|
- Scalar operations with pos/size parameters (fill, add_constant, scale, clamp, normalize)
|
|
- Combination operations with pos/source_pos/size parameters
|
|
- BSP operations with pos parameter for coordinate translation
|
|
- Region parameter validation and error handling
|
|
"""
|
|
import mcrfpy
|
|
import sys
|
|
|
|
# ============================================================================
|
|
# Scalar operations with region parameters
|
|
# ============================================================================
|
|
|
|
def test_fill_region():
|
|
"""Test fill() with region parameters"""
|
|
hmap = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
|
|
# Fill a 5x5 region starting at (5, 5)
|
|
result = hmap.fill(10.0, pos=(5, 5), size=(5, 5))
|
|
|
|
# Should return self
|
|
assert result is hmap, "fill() should return self"
|
|
|
|
# Check values
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 10.0 if (5 <= x < 10 and 5 <= y < 10) else 0.0
|
|
actual = hmap.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: fill() with region")
|
|
|
|
|
|
def test_fill_region_inferred_size():
|
|
"""Test fill() with position but no size (infers remaining)"""
|
|
hmap = mcrfpy.HeightMap((15, 15), fill=0.0)
|
|
|
|
# Fill from (10, 10) with no size - should fill to end
|
|
hmap.fill(5.0, pos=(10, 10))
|
|
|
|
for x in range(15):
|
|
for y in range(15):
|
|
expected = 5.0 if (x >= 10 and y >= 10) else 0.0
|
|
actual = hmap.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: fill() with inferred size")
|
|
|
|
|
|
def test_add_constant_region():
|
|
"""Test add_constant() with region parameters"""
|
|
hmap = mcrfpy.HeightMap((20, 20), fill=1.0)
|
|
|
|
# Add 5.0 to a 10x10 region at origin
|
|
hmap.add_constant(5.0, pos=(0, 0), size=(10, 10))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 6.0 if (x < 10 and y < 10) else 1.0
|
|
actual = hmap.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: add_constant() with region")
|
|
|
|
|
|
def test_scale_region():
|
|
"""Test scale() with region parameters"""
|
|
hmap = mcrfpy.HeightMap((20, 20), fill=2.0)
|
|
|
|
# Scale a 5x5 region by 3.0
|
|
hmap.scale(3.0, pos=(5, 5), size=(5, 5))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 6.0 if (5 <= x < 10 and 5 <= y < 10) else 2.0
|
|
actual = hmap.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: scale() with region")
|
|
|
|
|
|
def test_clamp_region():
|
|
"""Test clamp() with region parameters"""
|
|
hmap = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
|
|
# Set up varying values
|
|
for x in range(20):
|
|
for y in range(20):
|
|
# Values from 0 to 39
|
|
val = float(x + y)
|
|
hmap.fill(val, pos=(x, y), size=(1, 1))
|
|
|
|
# Clamp only the center region
|
|
hmap.clamp(5.0, 15.0, pos=(5, 5), size=(10, 10))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
original = float(x + y)
|
|
if 5 <= x < 15 and 5 <= y < 15:
|
|
# Clamped region
|
|
expected = max(5.0, min(15.0, original))
|
|
else:
|
|
# Unaffected
|
|
expected = original
|
|
actual = hmap.get((x, y))
|
|
assert abs(actual - expected) < 0.001, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: clamp() with region")
|
|
|
|
|
|
def test_normalize_region():
|
|
"""Test normalize() with region parameters"""
|
|
hmap = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
|
|
# Set up the center region with known min/max
|
|
for x in range(5, 15):
|
|
for y in range(5, 15):
|
|
val = float((x - 5) + (y - 5)) # 0 to 18
|
|
hmap.fill(val, pos=(x, y), size=(1, 1))
|
|
|
|
# Normalize the center region to 0-100
|
|
hmap.normalize(0.0, 100.0, pos=(5, 5), size=(10, 10))
|
|
|
|
# Check the center region is normalized
|
|
center_min, center_max = float('inf'), float('-inf')
|
|
for x in range(5, 15):
|
|
for y in range(5, 15):
|
|
val = hmap.get((x, y))
|
|
center_min = min(center_min, val)
|
|
center_max = max(center_max, val)
|
|
|
|
assert abs(center_min - 0.0) < 0.001, f"Normalized min should be 0.0, got {center_min}"
|
|
assert abs(center_max - 100.0) < 0.001, f"Normalized max should be 100.0, got {center_max}"
|
|
|
|
# Check outside region unchanged (should still be 0.0)
|
|
assert hmap.get((0, 0)) == 0.0, "Outside region should be unchanged"
|
|
|
|
print(" PASS: normalize() with region")
|
|
|
|
|
|
# ============================================================================
|
|
# Combination operations with region parameters
|
|
# ============================================================================
|
|
|
|
def test_add_region():
|
|
"""Test add() with region parameters"""
|
|
h1 = mcrfpy.HeightMap((20, 20), fill=1.0)
|
|
h2 = mcrfpy.HeightMap((20, 20), fill=5.0)
|
|
|
|
# Add h2 to h1 in a specific region
|
|
h1.add(h2, pos=(5, 5), source_pos=(0, 0), size=(10, 10))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 6.0 if (5 <= x < 15 and 5 <= y < 15) else 1.0
|
|
actual = h1.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: add() with region")
|
|
|
|
|
|
def test_add_source_pos():
|
|
"""Test add() with different source_pos"""
|
|
h1 = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
h2 = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
|
|
# Set up h2 with non-zero values in a specific area
|
|
h2.fill(10.0, pos=(10, 10), size=(5, 5))
|
|
|
|
# Copy from h2's non-zero region to h1's origin
|
|
h1.add(h2, pos=(0, 0), source_pos=(10, 10), size=(5, 5))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 10.0 if (x < 5 and y < 5) else 0.0
|
|
actual = h1.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: add() with source_pos")
|
|
|
|
|
|
def test_copy_from_region():
|
|
"""Test copy_from() with region parameters"""
|
|
h1 = mcrfpy.HeightMap((30, 30), fill=0.0)
|
|
h2 = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
|
|
# Fill h2 with a pattern
|
|
for x in range(20):
|
|
for y in range(20):
|
|
h2.fill(float(x * 20 + y), pos=(x, y), size=(1, 1))
|
|
|
|
# Copy a 10x10 region from h2 to h1 at offset
|
|
h1.copy_from(h2, pos=(5, 5), source_pos=(3, 3), size=(10, 10))
|
|
|
|
# Verify copied region
|
|
for x in range(10):
|
|
for y in range(10):
|
|
src_val = float((3 + x) * 20 + (3 + y))
|
|
dest_val = h1.get((5 + x, 5 + y))
|
|
assert dest_val == src_val, f"At dest ({5+x},{5+y}): expected {src_val}, got {dest_val}"
|
|
|
|
# Verify outside copied region is still 0
|
|
assert h1.get((0, 0)) == 0.0, "Outside region should be 0"
|
|
assert h1.get((4, 4)) == 0.0, "Just outside region should be 0"
|
|
|
|
print(" PASS: copy_from() with region")
|
|
|
|
|
|
def test_multiply_region():
|
|
"""Test multiply() with region parameters (masking)"""
|
|
h1 = mcrfpy.HeightMap((20, 20), fill=10.0)
|
|
h2 = mcrfpy.HeightMap((20, 20), fill=0.5)
|
|
|
|
# Multiply a 5x5 region
|
|
h1.multiply(h2, pos=(5, 5), size=(5, 5))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 5.0 if (5 <= x < 10 and 5 <= y < 10) else 10.0
|
|
actual = h1.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: multiply() with region")
|
|
|
|
|
|
def test_lerp_region():
|
|
"""Test lerp() with region parameters"""
|
|
h1 = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
h2 = mcrfpy.HeightMap((20, 20), fill=100.0)
|
|
|
|
# Lerp a region with t=0.3
|
|
h1.lerp(h2, 0.3, pos=(5, 5), size=(10, 10))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 30.0 if (5 <= x < 15 and 5 <= y < 15) else 0.0
|
|
actual = h1.get((x, y))
|
|
assert abs(actual - expected) < 0.001, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: lerp() with region")
|
|
|
|
|
|
def test_max_region():
|
|
"""Test max() with region parameters"""
|
|
h1 = mcrfpy.HeightMap((20, 20), fill=5.0)
|
|
h2 = mcrfpy.HeightMap((20, 20), fill=10.0)
|
|
|
|
# Max in a specific region
|
|
h1.max(h2, pos=(5, 5), size=(5, 5))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 10.0 if (5 <= x < 10 and 5 <= y < 10) else 5.0
|
|
actual = h1.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: max() with region")
|
|
|
|
|
|
def test_min_region():
|
|
"""Test min() with region parameters"""
|
|
h1 = mcrfpy.HeightMap((20, 20), fill=10.0)
|
|
h2 = mcrfpy.HeightMap((20, 20), fill=3.0)
|
|
|
|
# Min in a specific region
|
|
h1.min(h2, pos=(5, 5), size=(5, 5))
|
|
|
|
for x in range(20):
|
|
for y in range(20):
|
|
expected = 3.0 if (5 <= x < 10 and 5 <= y < 10) else 10.0
|
|
actual = h1.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: min() with region")
|
|
|
|
|
|
# ============================================================================
|
|
# BSP operations with pos parameter
|
|
# ============================================================================
|
|
|
|
def test_add_bsp_pos_default():
|
|
"""Test add_bsp() with default pos (origin-relative, like to_heightmap)"""
|
|
# Create BSP at non-origin position
|
|
bsp = mcrfpy.BSP(pos=(10, 10), size=(30, 30))
|
|
bsp.split_recursive(depth=2, min_size=(10, 10))
|
|
|
|
# Create heightmap larger than BSP
|
|
hmap = mcrfpy.HeightMap((50, 50), fill=0.0)
|
|
|
|
# Default pos=None should translate to origin-relative (BSP at 0,0)
|
|
hmap.add_bsp(bsp)
|
|
|
|
# Verify: the BSP region should be mapped starting from (0, 0)
|
|
# Check that at least some of the 30x30 region has non-zero values
|
|
count = 0
|
|
for x in range(30):
|
|
for y in range(30):
|
|
if hmap.get((x, y)) > 0:
|
|
count += 1
|
|
|
|
assert count > 0, "add_bsp() with default pos should map BSP to origin"
|
|
|
|
# Check that outside BSP's relative bounds is zero
|
|
assert hmap.get((35, 35)) == 0.0, "Outside BSP bounds should be 0"
|
|
|
|
print(" PASS: add_bsp() with default pos (origin-relative)")
|
|
|
|
|
|
def test_add_bsp_pos_custom():
|
|
"""Test add_bsp() with custom pos parameter"""
|
|
# Create BSP at (0, 0)
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(20, 20))
|
|
bsp.split_recursive(depth=2, min_size=(5, 5))
|
|
|
|
# Create heightmap
|
|
hmap = mcrfpy.HeightMap((50, 50), fill=0.0)
|
|
|
|
# Map BSP to position (15, 15) in heightmap
|
|
hmap.add_bsp(bsp, pos=(15, 15))
|
|
|
|
# Verify: there should be values in the 15-35 region
|
|
count_in_region = 0
|
|
for x in range(15, 35):
|
|
for y in range(15, 35):
|
|
if hmap.get((x, y)) > 0:
|
|
count_in_region += 1
|
|
|
|
assert count_in_region > 0, "add_bsp() with pos=(15,15) should place BSP there"
|
|
|
|
# Verify: the 0-15 region should be empty
|
|
count_outside = 0
|
|
for x in range(15):
|
|
for y in range(15):
|
|
if hmap.get((x, y)) > 0:
|
|
count_outside += 1
|
|
|
|
assert count_outside == 0, "Before pos offset should be empty"
|
|
|
|
print(" PASS: add_bsp() with custom pos")
|
|
|
|
|
|
def test_multiply_bsp_pos():
|
|
"""Test multiply_bsp() with pos parameter for masking"""
|
|
# Create BSP
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(30, 30))
|
|
bsp.split_recursive(depth=2, min_size=(10, 10))
|
|
|
|
# Create heightmap with uniform value
|
|
hmap = mcrfpy.HeightMap((50, 50), fill=10.0)
|
|
|
|
# Multiply/mask at position (10, 10) with shrink
|
|
hmap.multiply_bsp(bsp, pos=(10, 10), shrink=2)
|
|
|
|
# Check that areas outside the BSP+pos region are zeroed
|
|
# At (5, 5) should be zeroed (before pos offset)
|
|
assert hmap.get((5, 5)) == 0.0, "Before BSP region should be zeroed"
|
|
|
|
# At (45, 45) should be zeroed (after BSP region)
|
|
assert hmap.get((45, 45)) == 0.0, "After BSP region should be zeroed"
|
|
|
|
# Inside the BSP region (accounting for pos and shrink), some should be preserved
|
|
preserved_count = 0
|
|
for x in range(10, 40):
|
|
for y in range(10, 40):
|
|
if hmap.get((x, y)) > 0:
|
|
preserved_count += 1
|
|
|
|
assert preserved_count > 0, "Inside BSP region should have some preserved values"
|
|
|
|
print(" PASS: multiply_bsp() with pos")
|
|
|
|
|
|
# ============================================================================
|
|
# Error handling
|
|
# ============================================================================
|
|
|
|
def test_region_out_of_bounds():
|
|
"""Test that out-of-bounds positions raise ValueError"""
|
|
hmap = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
|
|
# Position beyond bounds
|
|
try:
|
|
hmap.fill(1.0, pos=(25, 25))
|
|
print(" FAIL: Should raise ValueError for out-of-bounds pos")
|
|
sys.exit(1)
|
|
except ValueError:
|
|
pass
|
|
|
|
# Negative position
|
|
try:
|
|
hmap.fill(1.0, pos=(-1, 0))
|
|
print(" FAIL: Should raise ValueError for negative pos")
|
|
sys.exit(1)
|
|
except ValueError:
|
|
pass
|
|
|
|
print(" PASS: Out-of-bounds position error handling")
|
|
|
|
|
|
def test_region_size_exceeds_bounds():
|
|
"""Test that explicit size exceeding bounds raises ValueError"""
|
|
hmap = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
|
|
# Size that exceeds remaining space
|
|
try:
|
|
hmap.fill(1.0, pos=(15, 15), size=(10, 10))
|
|
print(" FAIL: Should raise ValueError when size exceeds bounds")
|
|
sys.exit(1)
|
|
except ValueError:
|
|
pass
|
|
|
|
print(" PASS: Size exceeds bounds error handling")
|
|
|
|
|
|
def test_source_region_out_of_bounds():
|
|
"""Test that out-of-bounds source_pos raises ValueError"""
|
|
h1 = mcrfpy.HeightMap((20, 20), fill=0.0)
|
|
h2 = mcrfpy.HeightMap((10, 10), fill=5.0)
|
|
|
|
# source_pos beyond source bounds
|
|
try:
|
|
h1.add(h2, source_pos=(15, 15))
|
|
print(" FAIL: Should raise ValueError for out-of-bounds source_pos")
|
|
sys.exit(1)
|
|
except ValueError:
|
|
pass
|
|
|
|
print(" PASS: Out-of-bounds source_pos error handling")
|
|
|
|
|
|
def test_method_chaining_with_regions():
|
|
"""Test that region operations support method chaining"""
|
|
hmap = mcrfpy.HeightMap((30, 30), fill=0.0)
|
|
|
|
# Chain multiple regional operations
|
|
result = (hmap
|
|
.fill(10.0, pos=(5, 5), size=(10, 10))
|
|
.scale(2.0, pos=(5, 5), size=(10, 10))
|
|
.add_constant(-5.0, pos=(5, 5), size=(10, 10)))
|
|
|
|
assert result is hmap, "Chained operations should return self"
|
|
|
|
# Verify: region should be 10*2-5 = 15, outside should be 0
|
|
for x in range(30):
|
|
for y in range(30):
|
|
expected = 15.0 if (5 <= x < 15 and 5 <= y < 15) else 0.0
|
|
actual = hmap.get((x, y))
|
|
assert actual == expected, f"At ({x},{y}): expected {expected}, got {actual}"
|
|
|
|
print(" PASS: Method chaining with regions")
|
|
|
|
|
|
# ============================================================================
|
|
# Run all tests
|
|
# ============================================================================
|
|
|
|
def run_tests():
|
|
"""Run all HeightMap region tests"""
|
|
print("Testing HeightMap region operations...")
|
|
|
|
# Scalar operations
|
|
test_fill_region()
|
|
test_fill_region_inferred_size()
|
|
test_add_constant_region()
|
|
test_scale_region()
|
|
test_clamp_region()
|
|
test_normalize_region()
|
|
|
|
# Combination operations
|
|
test_add_region()
|
|
test_add_source_pos()
|
|
test_copy_from_region()
|
|
test_multiply_region()
|
|
test_lerp_region()
|
|
test_max_region()
|
|
test_min_region()
|
|
|
|
# BSP operations
|
|
test_add_bsp_pos_default()
|
|
test_add_bsp_pos_custom()
|
|
test_multiply_bsp_pos()
|
|
|
|
# Error handling
|
|
test_region_out_of_bounds()
|
|
test_region_size_exceeds_bounds()
|
|
test_source_region_out_of_bounds()
|
|
test_method_chaining_with_regions()
|
|
|
|
print("All HeightMap region tests PASSED!")
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
success = run_tests()
|
|
sys.exit(0 if success else 1)
|
|
except Exception as e:
|
|
print(f"FAIL: Unexpected exception: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|