diff --git a/h2d/col/Segments.hx b/h2d/col/Segments.hx index 64327223de..167a107d4c 100644 --- a/h2d/col/Segments.hx +++ b/h2d/col/Segments.hx @@ -40,7 +40,20 @@ abstract Segments(Array) from Array to Array { if( s.side(p) < 0 ) return false; } else { - throw "TODO"; + // Ray-casting (winding number) algorithm for arbitrary polygons. + // Counts intersections of a horizontal ray starting from point p and going right. + var w = 0; + for( s in segments ) { + var startY = s.y; + var endY = s.y + s.dy; + if( endY <= p.y ) { + if( startY > p.y && s.side(p) < 0 ) + w++; + } else if( startY <= p.y && s.side(p) > 0 ) { + w--; + } + } + return w != 0; } return true; } diff --git a/test.hxml b/test.hxml new file mode 100644 index 0000000000..286dd68db7 --- /dev/null +++ b/test.hxml @@ -0,0 +1,4 @@ +-cp test +-main test.TestRunner +-lib heaps +--interp diff --git a/test/Assert.hx b/test/Assert.hx new file mode 100644 index 0000000000..78b2f7d6be --- /dev/null +++ b/test/Assert.hx @@ -0,0 +1,44 @@ +package test; + +class Assert { + public static var failures:Int = 0; + public static var successes:Int = 0; + + public static function isTrue(value:Bool, ?pos:haxe.PosInfos) { + if (!value) { + failures++; + trace("FAIL: Expected true, got false at " + pos.fileName + ":" + pos.lineNumber); + } else { + successes++; + } + } + + public static function isFalse(value:Bool, ?pos:haxe.PosInfos) { + if (value) { + failures++; + trace("FAIL: Expected false, got true at " + pos.fileName + ":" + pos.lineNumber); + } else { + successes++; + } + } + + public static function equals(expected:T, actual:T, ?pos:haxe.PosInfos) { + if (expected != actual) { + failures++; + trace("FAIL: Expected " + expected + ", got " + actual + " at " + pos.fileName + ":" + pos.lineNumber); + } else { + successes++; + } + } + + public static function floatEquals(expected:Float, actual:Float, epsilon:Float = 1e-6, ?pos:haxe.PosInfos) { + var diff = expected - actual; + if (diff < 0) diff = -diff; + if (diff > epsilon) { + failures++; + trace("FAIL: Expected " + expected + ", got " + actual + " (diff " + diff + " > epsilon " + epsilon + ") at " + pos.fileName + ":" + pos.lineNumber); + } else { + successes++; + } + } +} diff --git a/test/TestMath.hx b/test/TestMath.hx new file mode 100644 index 0000000000..02040057f8 --- /dev/null +++ b/test/TestMath.hx @@ -0,0 +1,64 @@ +package test; + +class TestMath { + public function new() {} + + public function run() { + testClamp(); + testIsPOT(); + testNextPOT(); + testLerp(); + testInverseLerp(); + testFmt(); + } + + function testClamp() { + Assert.equals(5.0, hxd.Math.clamp(5.0, 0.0, 10.0)); + Assert.equals(0.0, hxd.Math.clamp(-1.0, 0.0, 10.0)); + Assert.equals(10.0, hxd.Math.clamp(15.0, 0.0, 10.0)); + Assert.equals(0.5, hxd.Math.clamp(0.5)); // Default range: 0 to 1 + Assert.equals(0.0, hxd.Math.clamp(-0.2)); + Assert.equals(1.0, hxd.Math.clamp(1.5)); + } + + function testIsPOT() { + Assert.isTrue(hxd.Math.isPOT(1)); + Assert.isTrue(hxd.Math.isPOT(2)); + Assert.isTrue(hxd.Math.isPOT(4)); + Assert.isTrue(hxd.Math.isPOT(1024)); + Assert.isFalse(hxd.Math.isPOT(3)); + Assert.isFalse(hxd.Math.isPOT(5)); + Assert.isFalse(hxd.Math.isPOT(1000)); + } + + function testNextPOT() { + Assert.equals(0, hxd.Math.nextPOT(0)); + Assert.equals(1, hxd.Math.nextPOT(1)); + Assert.equals(2, hxd.Math.nextPOT(2)); + Assert.equals(4, hxd.Math.nextPOT(3)); + Assert.equals(4, hxd.Math.nextPOT(4)); + Assert.equals(8, hxd.Math.nextPOT(5)); + Assert.equals(1024, hxd.Math.nextPOT(1000)); + } + + function testLerp() { + Assert.floatEquals(5.0, hxd.Math.lerp(0.0, 10.0, 0.5)); + Assert.floatEquals(0.0, hxd.Math.lerp(0.0, 10.0, 0.0)); + Assert.floatEquals(10.0, hxd.Math.lerp(0.0, 10.0, 1.0)); + Assert.floatEquals(2.5, hxd.Math.lerp(0.0, 10.0, 0.25)); + } + + function testInverseLerp() { + Assert.floatEquals(0.5, hxd.Math.inverseLerp(0.0, 10.0, 5.0)); + Assert.floatEquals(0.0, hxd.Math.inverseLerp(0.0, 10.0, 0.0)); + Assert.floatEquals(1.0, hxd.Math.inverseLerp(0.0, 10.0, 10.0)); + Assert.floatEquals(0.25, hxd.Math.inverseLerp(0.0, 10.0, 2.5)); + } + + function testFmt() { + // fmt rounds to 4 significant digits (or standard forms) + Assert.floatEquals(1.234, hxd.Math.fmt(1.234)); + Assert.floatEquals(1.234, hxd.Math.fmt(1.2343)); + Assert.floatEquals(0.0, hxd.Math.fmt(1e-12)); + } +} diff --git a/test/TestRunner.hx b/test/TestRunner.hx new file mode 100644 index 0000000000..f75bb1e307 --- /dev/null +++ b/test/TestRunner.hx @@ -0,0 +1,25 @@ +package test; + +class TestRunner { + public static function main() { + Sys.println("Starting test execution..."); + + var mathTests = new TestMath(); + mathTests.run(); + + var segmentsTests = new TestSegments(); + segmentsTests.run(); + + var shaderCacheTests = new TestShaderCache(); + shaderCacheTests.run(); + + Sys.println("Successes: " + Assert.successes); + if (Assert.failures > 0) { + Sys.println("Failures: " + Assert.failures); + Sys.exit(1); + } else { + Sys.println("All tests passed!"); + Sys.exit(0); + } + } +} diff --git a/test/TestSegments.hx b/test/TestSegments.hx new file mode 100644 index 0000000000..c0e9ab6d35 --- /dev/null +++ b/test/TestSegments.hx @@ -0,0 +1,91 @@ +package test; + +import h2d.col.Point; +import h2d.col.Segment; +import h2d.col.Segments; + +class TestSegments { + public function new() {} + + public function run() { + testConvexContains(); + testConcaveContains(); + testDistanceAndProject(); + } + + function testConvexContains() { + var p1 = new Point(0, 0); + var p2 = new Point(0, 10); + var p3 = new Point(10, 10); + var p4 = new Point(10, 0); + + // For containsPoint(p, true) to succeed, the segments must be counter-clockwise + // so that the point is on the inside side of all segment axes (s.side(p) >= 0). + var ccw_s1 = new Segment(p1, p4); + var ccw_s2 = new Segment(p4, p3); + var ccw_s3 = new Segment(p3, p2); + var ccw_s4 = new Segment(p2, p1); + + var ccw_segments:Segments = [ccw_s1, ccw_s2, ccw_s3, ccw_s4]; + + Assert.isTrue(ccw_segments.containsPoint(new Point(5, 5), true)); + Assert.isFalse(ccw_segments.containsPoint(new Point(15, 5), true)); + Assert.isFalse(ccw_segments.containsPoint(new Point(-5, 5), true)); + } + + function testConcaveContains() { + // Create an L-shaped concave polygon + // Vertices: (0,0) -> (10,0) -> (10,5) -> (5,5) -> (5,10) -> (0,10) -> (0,0) + var pts = [ + new Point(0, 0), + new Point(10, 0), + new Point(10, 5), + new Point(5, 5), + new Point(5, 10), + new Point(0, 10) + ]; + + var segmentsList = []; + var prev = pts[pts.length - 1]; + for (p in pts) { + segmentsList.push(new Segment(prev, p)); + prev = p; + } + var segments:Segments = segmentsList; + + // Try containsPoint with isConvex = false. + // This should run our implementation without throwing "TODO". + try { + var inside1 = segments.containsPoint(new Point(2, 2), false); // inside + var inside2 = segments.containsPoint(new Point(8, 2), false); // inside + var inside3 = segments.containsPoint(new Point(8, 8), false); // outside (in the hollow of the L) + var inside4 = segments.containsPoint(new Point(2, 8), false); // inside + var outside = segments.containsPoint(new Point(12, 12), false); // outside + + Assert.isTrue(inside1); + Assert.isTrue(inside2); + Assert.isFalse(inside3); + Assert.isTrue(inside4); + Assert.isFalse(outside); + } catch (e:Dynamic) { + Assert.isTrue(false); + trace("Exception in containsPoint(..., false): " + e); + } + } + + function testDistanceAndProject() { + var p1 = new Point(0, 0); + var p2 = new Point(10, 0); + var s = new Segment(p1, p2); + var segments:Segments = [s]; + + // test distance + Assert.floatEquals(5.0, segments.distance(new Point(5, 5))); + Assert.floatEquals(25.0, segments.distanceSq(new Point(5, 5))); + + // test project + var proj = segments.project(new Point(5, 5)); + Assert.floatEquals(5.0, proj.x); + Assert.floatEquals(0.0, proj.y); + } +} diff --git a/test/TestShaderCache.hx b/test/TestShaderCache.hx new file mode 100644 index 0000000000..a6d1c3c308 --- /dev/null +++ b/test/TestShaderCache.hx @@ -0,0 +1,41 @@ +package test; + +import h3d.impl.ShaderCache; +import haxe.io.Bytes; + +class TestShaderCache { + public function new() {} + + public function run() { + testShaderCacheInMemoryWithAllowSaveFalse(); + } + + function testShaderCacheInMemoryWithAllowSaveFalse() { + // Create a mock/temporary cache file path. + var cacheFile = "temp_shader_cache.data"; + var cache = new ShaderCache(cacheFile); + + // Turn off disk persistence. + cache.allowSave = false; + cache.keepSource = true; + + var shaderSource = " + @input float4 position; + void main() { + output.position = position; + } + "; + var compiledBinary = Bytes.ofString("MOCK_COMPILED_SHADER_BINARY"); + + // Save the compiled shader. It should cache in-memory. + cache.saveCompiledShader(shaderSource, compiledBinary); + + // Try to resolve the shader binary from the cache. + var resolved = cache.resolveShaderBinary(shaderSource); + + Assert.isTrue(resolved != null); + if (resolved != null) { + Assert.equals(compiledBinary.toString(), resolved.toString()); + } + } +}