Update to combination operations (#194) - allowing targeted, partial regions on source or target
This commit is contained in:
parent
e5d0eb4847
commit
2b12d1fc70
7 changed files with 2484 additions and 238 deletions
362
tests/unit/heightmap_direct_sampling_test.py
Normal file
362
tests/unit/heightmap_direct_sampling_test.py
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
"""Unit tests for HeightMap direct source sampling (Issue #209)
|
||||
|
||||
Tests:
|
||||
- add_noise() - sample noise and add to heightmap
|
||||
- multiply_noise() - sample noise and multiply with heightmap
|
||||
- add_bsp() - add BSP regions to heightmap
|
||||
- multiply_bsp() - multiply by BSP regions (masking)
|
||||
- Equivalence with intermediate HeightMap approach
|
||||
- Error handling
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_add_noise_basic():
|
||||
"""Test basic add_noise() operation"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
hmap = mcrfpy.HeightMap((50, 50), fill=0.0)
|
||||
|
||||
result = hmap.add_noise(noise)
|
||||
|
||||
# Should return self for chaining
|
||||
assert result is hmap, "add_noise() should return self"
|
||||
|
||||
# Check that values have been modified
|
||||
min_val, max_val = hmap.min_max()
|
||||
assert max_val > 0 or min_val < 0, "Noise should add non-zero values"
|
||||
|
||||
print(" PASS: add_noise() basic")
|
||||
|
||||
def test_add_noise_modes():
|
||||
"""Test add_noise() with different modes"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
|
||||
for mode in ["flat", "fbm", "turbulence"]:
|
||||
hmap = mcrfpy.HeightMap((30, 30), fill=0.0)
|
||||
hmap.add_noise(noise, mode=mode)
|
||||
|
||||
min_val, max_val = hmap.min_max()
|
||||
assert min_val >= -2.0 and max_val <= 2.0, f"Mode '{mode}': values out of expected range"
|
||||
|
||||
print(" PASS: add_noise() modes")
|
||||
|
||||
def test_add_noise_scale():
|
||||
"""Test add_noise() with scale parameter"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
|
||||
# Scale 0.5
|
||||
hmap1 = mcrfpy.HeightMap((30, 30), fill=0.0)
|
||||
hmap1.add_noise(noise, scale=0.5)
|
||||
min1, max1 = hmap1.min_max()
|
||||
|
||||
# Scale 1.0
|
||||
hmap2 = mcrfpy.HeightMap((30, 30), fill=0.0)
|
||||
hmap2.add_noise(noise, scale=1.0)
|
||||
min2, max2 = hmap2.min_max()
|
||||
|
||||
# Scale 0.5 should have smaller range
|
||||
range1 = max1 - min1
|
||||
range2 = max2 - min2
|
||||
assert range1 < range2 or abs(range1 - range2 * 0.5) < 0.1, "Scale should affect value range"
|
||||
|
||||
print(" PASS: add_noise() scale")
|
||||
|
||||
def test_add_noise_equivalence():
|
||||
"""Test that add_noise() produces same result as sample() + add()"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
|
||||
# Method 1: Using sample() then add()
|
||||
hmap1 = mcrfpy.HeightMap((40, 40), fill=5.0)
|
||||
sampled = noise.sample(size=(40, 40), mode="fbm", octaves=4)
|
||||
hmap1.add(sampled)
|
||||
|
||||
# Method 2: Using add_noise() directly
|
||||
hmap2 = mcrfpy.HeightMap((40, 40), fill=5.0)
|
||||
hmap2.add_noise(noise, mode="fbm", octaves=4)
|
||||
|
||||
# Compare values - should be identical
|
||||
differences = 0
|
||||
for y in range(40):
|
||||
for x in range(40):
|
||||
v1 = hmap1.get((x, y))
|
||||
v2 = hmap2.get((x, y))
|
||||
if abs(v1 - v2) > 0.0001:
|
||||
differences += 1
|
||||
|
||||
assert differences == 0, f"add_noise() should produce same result as sample()+add(), got {differences} differences"
|
||||
|
||||
print(" PASS: add_noise() equivalence")
|
||||
|
||||
def test_multiply_noise_basic():
|
||||
"""Test basic multiply_noise() operation"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
hmap = mcrfpy.HeightMap((50, 50), fill=1.0)
|
||||
|
||||
result = hmap.multiply_noise(noise)
|
||||
|
||||
assert result is hmap, "multiply_noise() should return self"
|
||||
|
||||
# Values should now be in a range around 0 (noise * 1.0)
|
||||
min_val, max_val = hmap.min_max()
|
||||
# FBM noise ranges from ~-1 to ~1, so multiplied values should too
|
||||
assert min_val < 0.5, "multiply_noise() should produce values less than 0.5"
|
||||
|
||||
print(" PASS: multiply_noise() basic")
|
||||
|
||||
def test_multiply_noise_scale():
|
||||
"""Test multiply_noise() with scale parameter"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
|
||||
# Start with values of 10
|
||||
hmap = mcrfpy.HeightMap((30, 30), fill=10.0)
|
||||
# Multiply by noise scaled to 0.5
|
||||
# Result should be 10 * (noise_value * 0.5)
|
||||
hmap.multiply_noise(noise, scale=0.5)
|
||||
|
||||
min_val, max_val = hmap.min_max()
|
||||
# With scale 0.5, max possible is 10 * 0.5 = 5, min is 10 * -0.5 = -5
|
||||
assert max_val <= 6.0, f"Expected max <= 6.0, got {max_val}"
|
||||
|
||||
print(" PASS: multiply_noise() scale")
|
||||
|
||||
def test_add_bsp_basic():
|
||||
"""Test basic add_bsp() operation"""
|
||||
# Create BSP
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(50, 50))
|
||||
bsp.split_recursive(depth=3, min_size=(8, 8))
|
||||
|
||||
# Create heightmap and add BSP
|
||||
hmap = mcrfpy.HeightMap((50, 50), fill=0.0)
|
||||
result = hmap.add_bsp(bsp)
|
||||
|
||||
assert result is hmap, "add_bsp() should return self"
|
||||
|
||||
# Check that some values are non-zero (inside rooms)
|
||||
min_val, max_val = hmap.min_max()
|
||||
assert max_val > 0, "add_bsp() should add non-zero values inside BSP regions"
|
||||
|
||||
print(" PASS: add_bsp() basic")
|
||||
|
||||
def test_add_bsp_select_modes():
|
||||
"""Test add_bsp() with different select modes"""
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(60, 60))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 10))
|
||||
|
||||
# Test leaves
|
||||
hmap_leaves = mcrfpy.HeightMap((60, 60), fill=0.0)
|
||||
hmap_leaves.add_bsp(bsp, select="leaves")
|
||||
|
||||
# Test all
|
||||
hmap_all = mcrfpy.HeightMap((60, 60), fill=0.0)
|
||||
hmap_all.add_bsp(bsp, select="all")
|
||||
|
||||
# Test internal
|
||||
hmap_internal = mcrfpy.HeightMap((60, 60), fill=0.0)
|
||||
hmap_internal.add_bsp(bsp, select="internal")
|
||||
|
||||
# All modes should produce some non-zero values
|
||||
for name, hmap in [("leaves", hmap_leaves), ("all", hmap_all), ("internal", hmap_internal)]:
|
||||
min_val, max_val = hmap.min_max()
|
||||
assert max_val > 0, f"select='{name}' should produce non-zero values"
|
||||
|
||||
# "all" should cover more area than just leaves or internal
|
||||
count_leaves = hmap_leaves.count_in_range((0.5, 2.0))
|
||||
count_all = hmap_all.count_in_range((0.5, 10.0)) # 'all' may overlap, so higher max
|
||||
count_internal = hmap_internal.count_in_range((0.5, 2.0))
|
||||
|
||||
assert count_all >= count_leaves, "'all' should cover at least as much as 'leaves'"
|
||||
|
||||
print(" PASS: add_bsp() select modes")
|
||||
|
||||
def test_add_bsp_shrink():
|
||||
"""Test add_bsp() with shrink parameter"""
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 80))
|
||||
bsp.split_recursive(depth=2, min_size=(20, 20))
|
||||
|
||||
# Without shrink
|
||||
hmap1 = mcrfpy.HeightMap((80, 80), fill=0.0)
|
||||
hmap1.add_bsp(bsp, shrink=0)
|
||||
count1 = hmap1.count_in_range((0.5, 2.0))
|
||||
|
||||
# With shrink
|
||||
hmap2 = mcrfpy.HeightMap((80, 80), fill=0.0)
|
||||
hmap2.add_bsp(bsp, shrink=2)
|
||||
count2 = hmap2.count_in_range((0.5, 2.0))
|
||||
|
||||
# Shrunk version should have fewer cells
|
||||
assert count2 < count1, f"Shrink should reduce covered cells: {count2} vs {count1}"
|
||||
|
||||
print(" PASS: add_bsp() shrink")
|
||||
|
||||
def test_add_bsp_value():
|
||||
"""Test add_bsp() with custom value"""
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(40, 40))
|
||||
bsp.split_recursive(depth=2, min_size=(10, 10))
|
||||
|
||||
hmap = mcrfpy.HeightMap((40, 40), fill=0.0)
|
||||
hmap.add_bsp(bsp, value=5.0)
|
||||
|
||||
min_val, max_val = hmap.min_max()
|
||||
assert max_val == 5.0, f"Value parameter should set cell values to 5.0, got {max_val}"
|
||||
|
||||
print(" PASS: add_bsp() value")
|
||||
|
||||
def test_multiply_bsp_basic():
|
||||
"""Test basic multiply_bsp() operation (masking)"""
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(50, 50))
|
||||
bsp.split_recursive(depth=3, min_size=(8, 8))
|
||||
|
||||
# Create heightmap with uniform value
|
||||
hmap = mcrfpy.HeightMap((50, 50), fill=10.0)
|
||||
result = hmap.multiply_bsp(bsp)
|
||||
|
||||
assert result is hmap, "multiply_bsp() should return self"
|
||||
|
||||
# Note: BSP leaves partition the ENTIRE space, so all cells are inside some leaf
|
||||
# To get "walls" between rooms, you need to use shrink > 0
|
||||
# Without shrink, all cells should be preserved
|
||||
min_val, max_val = hmap.min_max()
|
||||
assert max_val == 10.0, f"Areas inside BSP should be 10.0, got max={max_val}"
|
||||
|
||||
# Test with shrink to get actual masking (walls between rooms)
|
||||
hmap2 = mcrfpy.HeightMap((50, 50), fill=10.0)
|
||||
hmap2.multiply_bsp(bsp, shrink=2) # Leave 2-pixel walls
|
||||
|
||||
min_val2, max_val2 = hmap2.min_max()
|
||||
assert min_val2 == 0.0, f"Areas between shrunken rooms should be 0, got min={min_val2}"
|
||||
assert max_val2 == 10.0, f"Areas inside shrunken rooms should be 10.0, got max={max_val2}"
|
||||
|
||||
print(" PASS: multiply_bsp() basic (masking)")
|
||||
|
||||
def test_multiply_bsp_with_noise():
|
||||
"""Test multiply_bsp() to mask noisy terrain"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 80))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 10))
|
||||
|
||||
# Generate noisy terrain
|
||||
hmap = mcrfpy.HeightMap((80, 80), fill=0.0)
|
||||
hmap.add_noise(noise, mode="fbm", octaves=6, scale=1.0)
|
||||
hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Mask to BSP regions
|
||||
hmap.multiply_bsp(bsp, select="leaves", shrink=1)
|
||||
|
||||
# Check that some values are 0 (outside rooms) and some are positive (inside)
|
||||
count_zero = hmap.count_in_range((-0.001, 0.001))
|
||||
count_positive = hmap.count_in_range((0.1, 1.5))
|
||||
|
||||
assert count_zero > 0, "Should have zero values outside BSP"
|
||||
assert count_positive > 0, "Should have positive values inside BSP"
|
||||
|
||||
print(" PASS: multiply_bsp() with noise (terrain masking)")
|
||||
|
||||
def test_add_noise_requires_2d():
|
||||
"""Test that add_noise() requires 2D NoiseSource"""
|
||||
for dim in [1, 3, 4]:
|
||||
noise = mcrfpy.NoiseSource(dimensions=dim, seed=42)
|
||||
hmap = mcrfpy.HeightMap((20, 20), fill=0.0)
|
||||
|
||||
try:
|
||||
hmap.add_noise(noise)
|
||||
print(f" FAIL: add_noise() should raise ValueError for {dim}D noise")
|
||||
sys.exit(1)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
print(" PASS: add_noise() requires 2D noise")
|
||||
|
||||
def test_type_errors():
|
||||
"""Test type error handling"""
|
||||
hmap = mcrfpy.HeightMap((20, 20), fill=0.0)
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
|
||||
# add_noise with non-NoiseSource
|
||||
try:
|
||||
hmap.add_noise("not a noise source")
|
||||
print(" FAIL: add_noise() should raise TypeError")
|
||||
sys.exit(1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# add_bsp with non-BSP
|
||||
try:
|
||||
hmap.add_bsp(noise) # passing NoiseSource instead of BSP
|
||||
print(" FAIL: add_bsp() should raise TypeError")
|
||||
sys.exit(1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
print(" PASS: Type error handling")
|
||||
|
||||
def test_invalid_select_mode():
|
||||
"""Test invalid select mode error"""
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(30, 30))
|
||||
hmap = mcrfpy.HeightMap((30, 30), fill=0.0)
|
||||
|
||||
try:
|
||||
hmap.add_bsp(bsp, select="invalid")
|
||||
print(" FAIL: add_bsp() should raise ValueError for invalid select")
|
||||
sys.exit(1)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
print(" PASS: Invalid select mode error")
|
||||
|
||||
def test_method_chaining():
|
||||
"""Test method chaining with direct sampling"""
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
bsp = mcrfpy.BSP(pos=(0, 0), size=(60, 60))
|
||||
bsp.split_recursive(depth=2, min_size=(15, 15))
|
||||
|
||||
hmap = mcrfpy.HeightMap((60, 60), fill=0.0)
|
||||
|
||||
# Chain operations
|
||||
result = (hmap
|
||||
.add_noise(noise, mode="fbm", octaves=4)
|
||||
.normalize(0.0, 1.0)
|
||||
.multiply_bsp(bsp, select="leaves", shrink=1)
|
||||
.scale(10.0))
|
||||
|
||||
assert result is hmap, "Chained operations should return self"
|
||||
|
||||
# Verify the result makes sense
|
||||
min_val, max_val = hmap.min_max()
|
||||
assert min_val == 0.0, "Masked areas should be 0"
|
||||
assert max_val > 0.0, "Unmasked areas should have positive values"
|
||||
|
||||
print(" PASS: Method chaining")
|
||||
|
||||
def run_tests():
|
||||
"""Run all HeightMap direct sampling tests"""
|
||||
print("Testing HeightMap direct sampling (Issue #209)...")
|
||||
|
||||
test_add_noise_basic()
|
||||
test_add_noise_modes()
|
||||
test_add_noise_scale()
|
||||
test_add_noise_equivalence()
|
||||
test_multiply_noise_basic()
|
||||
test_multiply_noise_scale()
|
||||
test_add_bsp_basic()
|
||||
test_add_bsp_select_modes()
|
||||
test_add_bsp_shrink()
|
||||
test_add_bsp_value()
|
||||
test_multiply_bsp_basic()
|
||||
test_multiply_bsp_with_noise()
|
||||
test_add_noise_requires_2d()
|
||||
test_type_errors()
|
||||
test_invalid_select_mode()
|
||||
test_method_chaining()
|
||||
|
||||
print("All HeightMap direct sampling 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue