瀏覽代碼

Merge branch 'shapes'

* shapes: (26 commits)
  Fix broken tests and error in intersection calculations.
  Fix issue caused by tangent line segment checks Fixing a bad test case that should've returned false...
  Add tests for circle intersects quad. Failing Tests: CircleTest.NotIntersecting
  Test line intersects circle.
  Add tests for circle-intersects circle.
  Test shape constructors - Fix bug in square -> rectangle
  More test coverage
  Test "line intersects quad". Remove redundant test.
  Fixing compilation issues cause by a bad merge.
  Add C++ GitIgnore
  Cleanup
  Fix parallel to point.
  Cleanup, attribution
  Fix line intersects line, line contains point. Add more tests.
  Add function to check for lines are parallel. Add tests for point in square.
  Fix point on line to not give false positives for co-linear data.
  Fix tests for point on line with infinite slope.
  Add more tests and helpers. Failing: contains(line, point) where line.slope = Inf
  Add tests for point exists inside a circle.
  Add tests for orthogonal(line, point)
  ...
Sam Jaffe 6 年之前
父節點
當前提交
185e474fe0

+ 35 - 0
.gitignore

@@ -1 +1,36 @@
+# C++.gitignore
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+# cxxtestgen
 *_tc.cpp

+ 1 - 0
math/include/game/math/angle.hpp

@@ -9,6 +9,7 @@
 
 namespace math {
   struct degree {
+    degree(double v);
     double value;
   };
 

+ 27 - 1
math/include/game/math/common.hpp

@@ -11,5 +11,31 @@
 
 namespace math {
   vec2 rotate(vec2 const & center, vec2 const & point, radian r);
-  quad rotate(vec2 const & center, quad const & q, radian r);
+  dim2::quad rotate(vec2 const & center, dim2::quad const & q, radian r);
+
+  bool contains(dim2::line const & shape, dim2::point const & pt);
+  bool contains(dim2::circle const & shape, dim2::point const & pt);
+  bool contains(dim2::quad const & shape, dim2::point const & pt);
+
+  bool intersects(dim2::line const & lhs, dim2::line const & rhs);
+  bool intersects(dim2::line const & lhs, dim2::circle const & rhs);
+  bool intersects(dim2::line const & lhs, dim2::quad const & rhs);
+  bool intersects(dim2::quad const & lhs, dim2::line const & rhs);
+  bool intersects(dim2::quad const & lhs, dim2::circle const & rhs);
+  bool intersects(dim2::quad const & lhs, dim2::quad const & rhs);
+  bool intersects(dim2::circle const & lhs, dim2::line const & rhs);
+  bool intersects(dim2::circle const & lhs, dim2::quad const & rhs);
+  bool intersects(dim2::circle const & lhs, dim2::circle const & rhs);
+}
+
+namespace math {
+  inline bool intersects(dim2::quad const & lhs, dim2::line const & rhs) {
+    return intersects(rhs, lhs);
+  }
+  inline bool intersects(dim2::circle const & lhs, dim2::line const & rhs) {
+    return intersects(rhs, lhs);
+  }
+  inline bool intersects(dim2::circle const & lhs, dim2::quad const & rhs) {
+    return intersects(rhs, lhs);
+  }
 }

+ 42 - 0
math/include/game/math/compare.hpp

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <algorithm>
+#include <cmath>
+
+namespace math {
+  template <typename T> T safe_div(T num, T denom) {
+    return (num == denom && denom == 0) ? 0 : num / denom;
+  }
+
+  template <typename T> bool between_exclusive(T val, T min, T max) {
+    return val >= min && val < max;
+  }
+
+  template <typename T> bool between(T val, T min, T max) {
+    return val >= min && val <= max;
+  }
+
+  template <typename T> bool approx_equal(T lhs, T rhs, T eps) {
+    T const a = std::abs(lhs);
+    T const b = std::abs(rhs);
+    return std::abs(lhs - rhs) <= (std::max(a, b) * eps);
+  }
+
+  template <typename T> bool essentially_equal(T lhs, T rhs, T eps) {
+    T const a = std::abs(lhs);
+    T const b = std::abs(rhs);
+    return std::abs(lhs - rhs) <= (std::min(a, b) * eps);
+  }
+
+  template <typename T> bool definitely_greater(T lhs, T rhs, T eps) {
+    T const a = std::abs(lhs);
+    T const b = std::abs(rhs);
+    return (lhs - rhs) > (std::max(a, b) * eps);
+  }
+
+  template <typename T> bool definitely_less(T lhs, T rhs, T eps) {
+    T const a = std::abs(lhs);
+    T const b = std::abs(rhs);
+    return (rhs - lhs) > (std::max(a, b) * eps);
+  }
+}

+ 12 - 9
math/include/game/math/math_fwd.hpp

@@ -19,15 +19,6 @@ namespace math {
 
     template <typename T, size_t N> using square_matrix = matrix<T, N, N>;
   }
-
-  struct line;
-  struct circle;
-  struct quad;
-  struct rectangle;
-  struct square;
-
-  struct degree;
-  struct radian;
 }
 
 namespace math {
@@ -37,4 +28,16 @@ namespace math {
   using rgba = vector::vector<uint8_t, 4>;
 
   using matr4 = matrix::matrix<float, 4, 4>;
+
+  namespace dim2 {
+    using point = vec2;
+    struct line;
+    struct circle;
+    struct quad;
+    struct rectangle;
+    struct square;
+  }
+
+  struct degree;
+  struct radian;
 }

+ 21 - 7
math/include/game/math/shape.hpp

@@ -11,29 +11,43 @@
 
 #include "math_fwd.hpp"
 
-namespace math {
+namespace math { namespace dim2 {
+  using point = vec2;
+
   struct line {
-    vec2 first, second;
+    float length() const;
+    float slope() const;
+    point first, second;
   };
 
   struct circle {
-    vec2 center;
+    point center;
     float radius;
   };
 
   struct quad {
-    vec2 ll, lr, ur, ul;
+    point ll, lr, ur, ul;
   };
 
   struct rectangle {
     operator quad() const;
-    vec2 origin, size;
+    point origin, size;
   };
 
   struct square {
     operator rectangle() const;
     operator quad() const;
-    vec2 origin;
+    point origin;
     float size;
   };
-}
+}}
+
+namespace math { namespace shapes {
+  std::vector<dim2::line> segments(dim2::quad const & shape);
+}}
+
+namespace math { namespace lines {
+  bool parallel(dim2::line const & lhs, dim2::line const & rhs);
+  dim2::point intersection(dim2::line const & lhs, dim2::line const & rhs);
+  dim2::line orthogonal(dim2::line const & from, dim2::point const & to);
+}}

+ 22 - 0
math/math-test/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 252 - 2
math/math.xcodeproj/project.pbxproj

@@ -7,15 +7,68 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		CD1FCFD2227E194D00F9BF93 /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD3786181CF9F61100BE89B2 /* libmath.dylib */; };
+		CD1FCFD8227E195B00F9BF93 /* shape_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1FCFC8227E193000F9BF93 /* shape_test.cxx */; };
+		CD1FCFE9227E198100F9BF93 /* GoogleMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD1FCFE1227E197800F9BF93 /* GoogleMock.framework */; };
+		CD1FCFEC227E4C2E00F9BF93 /* common.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3C80BA1D68902300ACC795 /* common.cpp */; };
 		CD3AC71E1D2C0AF8002B4BB0 /* shape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC71C1D2C0AF8002B4BB0 /* shape.cpp */; };
 		CD3C809F1D675AB100ACC795 /* angle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3C809D1D675AB100ACC795 /* angle.cpp */; };
-		CD3C80BC1D68902300ACC795 /* common.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3C80BA1D68902300ACC795 /* common.cpp */; };
 		CDA34D9522517967008036A7 /* matrix_helpers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CDA34D8C22517680008036A7 /* matrix_helpers.hpp */; };
 		CDA34D9622517969008036A7 /* matrix.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CDA34D8D22517680008036A7 /* matrix.hpp */; };
 		CDA34D972251796B008036A7 /* vector.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CDA34D9022517689008036A7 /* vector.hpp */; };
+		CDEDC5B9227F2D38003A2E45 /* common_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEDC5B8227F2D38003A2E45 /* common_test.cxx */; };
 /* End PBXBuildFile section */
 
+/* Begin PBXContainerItemProxy section */
+		CD1FCFD3227E194D00F9BF93 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD3786101CF9F61100BE89B2 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = CD3786171CF9F61100BE89B2;
+			remoteInfo = math;
+		};
+		CD1FCFE0227E197800F9BF93 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05818F861A685AEA0072A469;
+			remoteInfo = GoogleMock;
+		};
+		CD1FCFE2227E197800F9BF93 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05E96ABD1A68600C00204102;
+			remoteInfo = gmock;
+		};
+		CD1FCFE4227E197800F9BF93 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05E96B1F1A68634900204102;
+			remoteInfo = gtest;
+		};
+		CD1FCFE6227E197800F9BF93 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05818F901A685AEA0072A469;
+			remoteInfo = GoogleMockTests;
+		};
+		CD1FCFEA227E198400F9BF93 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */;
+			proxyType = 1;
+			remoteGlobalIDString = 05818F851A685AEA0072A469;
+			remoteInfo = GoogleMock;
+		};
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXFileReference section */
+		CD1FCFC8227E193000F9BF93 /* shape_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = shape_test.cxx; sourceTree = "<group>"; };
+		CD1FCFCD227E194D00F9BF93 /* math-test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "math-test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD1FCFD1227E194D00F9BF93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
 		CD3786181CF9F61100BE89B2 /* libmath.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libmath.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD3AC71C1D2C0AF8002B4BB0 /* shape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = shape.cpp; sourceTree = "<group>"; };
 		CD3C809D1D675AB100ACC795 /* angle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = angle.cpp; sourceTree = "<group>"; };
@@ -24,9 +77,20 @@
 		CDA34D8C22517680008036A7 /* matrix_helpers.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = matrix_helpers.hpp; path = matrix/matrix_helpers.hpp; sourceTree = SOURCE_ROOT; };
 		CDA34D8D22517680008036A7 /* matrix.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = matrix.hpp; path = matrix/matrix.hpp; sourceTree = SOURCE_ROOT; };
 		CDA34D9022517689008036A7 /* vector.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = vector.hpp; path = vector/vector.hpp; sourceTree = SOURCE_ROOT; };
+		CDEDC5B8227F2D38003A2E45 /* common_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = common_test.cxx; sourceTree = "<group>"; };
+		CDEDC5BD227F2DB2003A2E45 /* test_printers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = test_printers.h; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
+		CD1FCFCA227E194D00F9BF93 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD1FCFE9227E198100F9BF93 /* GoogleMock.framework in Frameworks */,
+				CD1FCFD2227E194D00F9BF93 /* libmath.dylib in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		CD3786151CF9F61100BE89B2 /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
@@ -37,14 +101,43 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		CD1FCFCE227E194D00F9BF93 /* math-test */ = {
+			isa = PBXGroup;
+			children = (
+				CD1FCFD1227E194D00F9BF93 /* Info.plist */,
+			);
+			path = "math-test";
+			sourceTree = "<group>";
+		};
+		CD1FCFDA227E197800F9BF93 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				CD1FCFE1227E197800F9BF93 /* GoogleMock.framework */,
+				CD1FCFE3227E197800F9BF93 /* gmock.framework */,
+				CD1FCFE5227E197800F9BF93 /* gtest.framework */,
+				CD1FCFE7227E197800F9BF93 /* GoogleMockTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		CD1FCFE8227E198100F9BF93 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
 		CD37860F1CF9F61100BE89B2 = {
 			isa = PBXGroup;
 			children = (
+				CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */,
 				CDA34D89225175CB008036A7 /* game */,
 				CDA34D8A22517670008036A7 /* include */,
-				CD3C80791D66440200ACC795 /* test */,
 				CD3786321CFA304800BE89B2 /* src */,
+				CD3C80791D66440200ACC795 /* test */,
+				CD1FCFCE227E194D00F9BF93 /* math-test */,
 				CD3786191CF9F61100BE89B2 /* Products */,
+				CD1FCFE8227E198100F9BF93 /* Frameworks */,
 			);
 			sourceTree = "<group>";
 		};
@@ -52,6 +145,7 @@
 			isa = PBXGroup;
 			children = (
 				CD3786181CF9F61100BE89B2 /* libmath.dylib */,
+				CD1FCFCD227E194D00F9BF93 /* math-test.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -69,6 +163,9 @@
 		CD3C80791D66440200ACC795 /* test */ = {
 			isa = PBXGroup;
 			children = (
+				CD1FCFC8227E193000F9BF93 /* shape_test.cxx */,
+				CDEDC5B8227F2D38003A2E45 /* common_test.cxx */,
+				CDEDC5BD227F2DB2003A2E45 /* test_printers.h */,
 			);
 			path = test;
 			sourceTree = "<group>";
@@ -99,6 +196,25 @@
 /* End PBXHeadersBuildPhase section */
 
 /* Begin PBXNativeTarget section */
+		CD1FCFCC227E194D00F9BF93 /* math-test */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CD1FCFD5227E194D00F9BF93 /* Build configuration list for PBXNativeTarget "math-test" */;
+			buildPhases = (
+				CD1FCFC9227E194D00F9BF93 /* Sources */,
+				CD1FCFCA227E194D00F9BF93 /* Frameworks */,
+				CD1FCFCB227E194D00F9BF93 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				CD1FCFEB227E198400F9BF93 /* PBXTargetDependency */,
+				CD1FCFD4227E194D00F9BF93 /* PBXTargetDependency */,
+			);
+			name = "math-test";
+			productName = "math-test";
+			productReference = CD1FCFCD227E194D00F9BF93 /* math-test.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 		CD3786171CF9F61100BE89B2 /* math */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = CD3786231CF9F61100BE89B2 /* Build configuration list for PBXNativeTarget "math" */;
@@ -123,9 +239,14 @@
 		CD3786101CF9F61100BE89B2 /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
+				LastSwiftUpdateCheck = 1010;
 				LastUpgradeCheck = 1010;
 				ORGANIZATIONNAME = "Sam Jaffe";
 				TargetAttributes = {
+					CD1FCFCC227E194D00F9BF93 = {
+						CreatedOnToolsVersion = 10.1;
+						ProvisioningStyle = Automatic;
+					};
 					CD3786171CF9F61100BE89B2 = {
 						CreatedOnToolsVersion = 7.2.1;
 					};
@@ -141,13 +262,61 @@
 			mainGroup = CD37860F1CF9F61100BE89B2;
 			productRefGroup = CD3786191CF9F61100BE89B2 /* Products */;
 			projectDirPath = "";
+			projectReferences = (
+				{
+					ProductGroup = CD1FCFDA227E197800F9BF93 /* Products */;
+					ProjectRef = CD1FCFD9227E197800F9BF93 /* GoogleMock.xcodeproj */;
+				},
+			);
 			projectRoot = "";
 			targets = (
 				CD3786171CF9F61100BE89B2 /* math */,
+				CD1FCFCC227E194D00F9BF93 /* math-test */,
 			);
 		};
 /* End PBXProject section */
 
+/* Begin PBXReferenceProxy section */
+		CD1FCFE1227E197800F9BF93 /* GoogleMock.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = GoogleMock.framework;
+			remoteRef = CD1FCFE0227E197800F9BF93 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CD1FCFE3227E197800F9BF93 /* gmock.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = gmock.framework;
+			remoteRef = CD1FCFE2227E197800F9BF93 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CD1FCFE5227E197800F9BF93 /* gtest.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = gtest.framework;
+			remoteRef = CD1FCFE4227E197800F9BF93 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CD1FCFE7227E197800F9BF93 /* GoogleMockTests.xctest */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.cfbundle;
+			path = GoogleMockTests.xctest;
+			remoteRef = CD1FCFE6227E197800F9BF93 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+		CD1FCFCB227E194D00F9BF93 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
 /* Begin PBXShellScriptBuildPhase section */
 		CD6F73E822517BDA0081ED74 /* ShellScript */ = {
 			isa = PBXShellScriptBuildPhase;
@@ -169,6 +338,15 @@
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
+		CD1FCFC9227E194D00F9BF93 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CDEDC5B9227F2D38003A2E45 /* common_test.cxx in Sources */,
+				CD1FCFD8227E195B00F9BF93 /* shape_test.cxx in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		CD3786141CF9F61100BE89B2 /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -181,7 +359,70 @@
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		CD1FCFD4227E194D00F9BF93 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = CD3786171CF9F61100BE89B2 /* math */;
+			targetProxy = CD1FCFD3227E194D00F9BF93 /* PBXContainerItemProxy */;
+		};
+		CD1FCFEB227E198400F9BF93 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = GoogleMock;
+			targetProxy = CD1FCFEA227E198400F9BF93 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
+		CD1FCFD6227E194D00F9BF93 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = "math-test/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.13;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.math-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 4.2;
+			};
+			name = Debug;
+		};
+		CD1FCFD7227E194D00F9BF93 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = "math-test/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.13;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.math-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 4.2;
+			};
+			name = Release;
+		};
 		CD3786211CF9F61100BE89B2 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -310,6 +551,15 @@
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
+		CD1FCFD5227E194D00F9BF93 /* Build configuration list for PBXNativeTarget "math-test" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CD1FCFD6227E194D00F9BF93 /* Debug */,
+				CD1FCFD7227E194D00F9BF93 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		CD3786131CF9F61100BE89B2 /* Build configuration list for PBXProject "math" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (

+ 67 - 0
math/math.xcodeproj/xcshareddata/xcschemes/math-test.xcscheme

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1010"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      codeCoverageEnabled = "YES"
+      onlyGenerateCoverageForSpecifiedTargets = "YES"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <CodeCoverageTargets>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "CD3786171CF9F61100BE89B2"
+            BuildableName = "libmath.dylib"
+            BlueprintName = "math"
+            ReferencedContainer = "container:math.xcodeproj">
+         </BuildableReference>
+      </CodeCoverageTargets>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CD1FCFCC227E194D00F9BF93"
+               BuildableName = "math-test.xctest"
+               BlueprintName = "math-test"
+               ReferencedContainer = "container:math.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 2 - 0
math/src/angle.cpp

@@ -10,6 +10,8 @@
 #include <cmath>
 
 namespace math {
+  degree::degree(double v) : value(v) {}
+
   radian::radian(double v) : value(v) {}
   radian::radian(degree d) : value(d.value / M_2_PI) {}
   radian::operator degree() const { return {value * M_2_PI}; }

+ 94 - 1
math/src/common.cpp

@@ -7,7 +7,10 @@
 
 #include "game/math/common.hpp"
 
+#include <vector>
+
 #include "game/math/angle.hpp"
+#include "game/math/compare.hpp"
 #include "game/math/shape.hpp"
 
 namespace math {
@@ -18,8 +21,98 @@ namespace math {
     return {{vcos[0] - vsin[1] + c[0], vsin[0] - vcos[1] + c[1]}};
   }
 
-  quad rotate(vec2 const & c, quad const & q, radian r) {
+  dim2::quad rotate(vec2 const & c, dim2::quad const & q, radian r) {
     return {rotate(c, q.ll, r), rotate(c, q.lr, r), rotate(c, q.ur, r),
             rotate(c, q.ul, r)};
   }
+
+  enum orientation { colinear = 0, clockwise = 1, anticlockwise = 2 };
+
+  orientation orient(dim2::line const & ln, dim2::point const & pt) {
+    auto val = (ln.second[1] - ln.first[1]) * (pt[0] - ln.second[0]) -
+               (ln.second[0] - ln.first[0]) * (pt[1] - ln.second[1]);
+
+    if (val == 0) return colinear;
+    return (val > 0) ? clockwise : anticlockwise;
+  }
+
+  bool contains(dim2::line const & ln, dim2::point const & pt) {
+    auto xs = std::minmax(ln.first[0], ln.second[0]);
+    auto ys = std::minmax(ln.first[1], ln.second[1]);
+    return orient(ln, pt) == colinear && between(pt[0], xs.first, xs.second) &&
+           between(pt[1], ys.first, ys.second);
+  }
+
+  bool contains(dim2::circle const & shape, dim2::point const & pt) {
+    vec2 const delta = pt - shape.center;
+    return delta.dot(delta) <= std::pow(shape.radius, 2);
+  }
+
+  static dim2::line ray_x(dim2::point const & pt, dim2::line const & l) {
+    auto x_inf = std::max({l.first[0], l.second[0], pt[0]}) + 1;
+    return {pt, {{x_inf, pt[1]}}};
+  }
+
+  static bool contains(std::vector<dim2::line> const & segments,
+                       dim2::point const & pt) {
+    int hits = 0;
+    for (auto l : segments) {
+      if (!intersects(l, ray_x(pt, l))) continue;
+      if (orient(l, pt) == colinear) return contains(l, pt);
+      ++hits;
+    }
+    return (hits & 1) == 1;
+  }
+
+  bool contains(dim2::quad const & shape, dim2::point const & pt) {
+    return contains(shapes::segments(shape), pt);
+  }
+
+  bool intersects(dim2::line const & lhs, dim2::line const & rhs) {
+    // https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
+    // Find the four orientations needed for general and special cases
+    orientation o1 = orient(lhs, rhs.first);
+    orientation o2 = orient(lhs, rhs.second);
+    orientation o3 = orient(rhs, lhs.first);
+    orientation o4 = orient(rhs, lhs.second);
+
+    // General Case: Lines cross through each other
+    if (o1 != o2 && o3 != o4) return true;
+
+    // Special Cases: one of the points exists on the other line
+    return contains(lhs, rhs.first) || contains(lhs, rhs.second) ||
+           contains(rhs, lhs.first) || contains(rhs, lhs.second);
+  }
+
+  bool intersects(dim2::line const & lhs, dim2::circle const & rhs) {
+    if (contains(rhs, lhs.first) || contains(rhs, lhs.second)) { return true; }
+    dim2::line const orth = lines::orthogonal(lhs, rhs.center);
+    vec2 const delta = orth.second - orth.first;
+    return delta.dot(delta) <= std::pow(rhs.radius, 2) && intersects(lhs, orth);
+  }
+
+  bool intersects(dim2::line const & lhs, dim2::quad const & rhs) {
+    std::vector<dim2::line> segments = shapes::segments(rhs);
+    auto lhs_intersects = [&lhs](dim2::line const & ln) {
+      return intersects(lhs, ln);
+    };
+    return std::any_of(segments.begin(), segments.end(), lhs_intersects) ||
+           contains(segments, lhs.first);
+  }
+
+  bool intersects(dim2::quad const & lhs, dim2::circle const & rhs) {
+    std::vector<dim2::line> segments = shapes::segments(lhs);
+    auto rhs_intersects = [&rhs](dim2::line const & ln) {
+      return intersects(ln, rhs);
+    };
+    return std::any_of(segments.begin(), segments.end(), rhs_intersects) ||
+           contains(lhs, rhs.center);
+  }
+
+  bool intersects(dim2::quad const & lhs, dim2::quad const & rhs);
+
+  bool intersects(dim2::circle const & lhs, dim2::circle const & rhs) {
+    vec2 const delta = rhs.center - lhs.center;
+    return delta.dot(delta) <= std::pow(lhs.radius + rhs.radius, 2);
+  }
 }

+ 50 - 5
math/src/shape.cpp

@@ -7,19 +7,64 @@
 
 #include "game/math/shape.hpp"
 
-namespace math {
+#include <vector>
+
+#include "game/math/compare.hpp"
+
+namespace math { namespace dim2 {
+  float line::length() const { return (second - first).magnitude(); }
+
+  float line::slope() const {
+    return safe_div(second[1] - first[1], second[0] - first[0]);
+  }
 
   rectangle::operator quad() const {
     return {origin, origin + vec2{{size.x(), 0.0}}, origin + size,
             origin + vec2{{0.0, size.y()}}};
   }
 
-  square::operator rectangle() const {
-    return {origin, origin + vec2{{size, size}}};
-  }
+  square::operator rectangle() const { return {origin, vec2{{size, size}}}; }
 
   square::operator quad() const {
     return {origin, origin + vec2{{size, 0.0}}, origin + vec2{{size, size}},
             origin + vec2{{0.0, size}}};
   }
-}
+}}
+
+namespace math { namespace shapes {
+  std::vector<dim2::line> segments(dim2::quad const & shape) {
+    return {{shape.ll, shape.lr},
+            {shape.lr, shape.ur},
+            {shape.ur, shape.ul},
+            {shape.ul, shape.ll}};
+  }
+}}
+
+namespace math { namespace lines {
+  bool parallel(dim2::line const & lhs, dim2::line const & rhs) {
+    return lhs.slope() == rhs.slope();
+  }
+
+  inline dim2::point intersection(float b1, float s1, float b2, float s2) {
+    float const x = safe_div(b1 - b2, s2 - s1);
+    return {{x, b1 + x * s1}};
+  }
+
+  inline dim2::point intersection(dim2::point const & p1, float s1,
+                                  dim2::point const & p2, float s2) {
+    // Solve for Inf * NaN
+    float y1 = p1[0] == 0 ? 0 : s1 * p1[0];
+    float y2 = p2[0] == 0 ? 0 : s2 * p2[0];
+    return intersection(p1[1] - y1, s1, p2[1] - y2, s2);
+  }
+
+  dim2::point intersection(dim2::line const & lhs, dim2::line const & rhs) {
+    return intersection(lhs.first, lhs.slope(), rhs.first, rhs.slope());
+  }
+
+  dim2::line orthogonal(dim2::line const & from, dim2::point const & to) {
+    float const slope = from.slope();
+    if (slope == 0 || isinf(slope)) { return {to, {{to[0], from.first[1]}}}; }
+    return {to, intersection(from.first, slope, to, -1 / slope)};
+  }
+}}

+ 244 - 0
math/test/common_test.cxx

@@ -0,0 +1,244 @@
+//
+//  common_test.cxx
+//  math-test
+//
+//  Created by Sam Jaffe on 5/5/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <cmath>
+#include <gmock/gmock.h>
+
+#include "game/math/angle.hpp"
+#include "game/math/common.hpp"
+#include "game/math/shape.hpp"
+
+#include "test_printers.h"
+
+using namespace math::dim2;
+using namespace testing;
+
+template <typename T, typename G>
+decltype(auto) generate(T min, T max, T step, G && generator) {
+  std::vector<decltype(generator(min))> out;
+  for (T val = min; val < max; val += step) {
+    out.emplace_back(generator(val));
+  }
+  return out;
+}
+
+template <typename T> decltype(auto) generate(T min, T max, T step) {
+  auto identity = [](T const & val) { return val; };
+  return generate(min, max, step, identity);
+}
+
+struct LineTest : TestWithParam<std::tuple<point, int>> {};
+
+TEST_P(LineTest, ExistsOnOriginLine) {
+  point const end = std::get<0>(GetParam());
+  line const ln{{{0, 0}}, end};
+  point const pt = end * std::get<1>(GetParam()) / 100.f;
+  EXPECT_TRUE(math::contains(ln, pt));
+}
+
+TEST_P(LineTest, ColinearOutsideDoesNotContain) {
+  point const end = std::get<0>(GetParam());
+  line const ln{end, end * 2};
+  point const pt = end * std::get<1>(GetParam()) / 100.f;
+  EXPECT_FALSE(math::contains(ln, pt));
+}
+
+std::vector<point> const line_ends{{{1, 1}},   {{0, 1}},  {{1, 0}},
+                                   {{-1, -1}}, {{0, -1}}, {{-1, 0}}};
+
+INSTANTIATE_TEST_CASE_P(Contains, LineTest,
+                        Combine(ValuesIn(line_ends),
+                                ValuesIn(generate(0, 100, 10))));
+
+struct UnitCircleTest : TestWithParam<point> {};
+
+TEST_P(UnitCircleTest, ExistsInCircle) {
+  circle unit{{{0, 0}}, 1};
+  EXPECT_TRUE(math::contains(unit, GetParam()));
+}
+
+TEST_P(UnitCircleTest, OutsideSmallerCircle) {
+  circle subunit{{{0, 0}}, 0.9999};
+  EXPECT_FALSE(math::contains(subunit, GetParam()));
+}
+
+point unit_circle_angle(math::degree degs) {
+  return point(make_vector(math::sin(degs), math::cos(degs)));
+}
+
+INSTANTIATE_TEST_CASE_P(Contains, UnitCircleTest,
+                        ValuesIn(generate(0.0, 360.0, 5.0, unit_circle_angle)));
+
+struct UnitSquareTest : TestWithParam<std::tuple<float, float>> {};
+
+TEST_P(UnitSquareTest, PointInSquare) {
+  square unit{{{0, 0}}, 1};
+  point pt{{std::get<0>(GetParam()), std::get<1>(GetParam())}};
+  EXPECT_TRUE(math::contains(unit, pt));
+}
+
+TEST_F(UnitSquareTest, PointOutsideSquare) {
+  square unit{{{0, 0}}, 1};
+  EXPECT_FALSE(math::contains(unit, {{0.f, 1.1f}}));
+  EXPECT_FALSE(math::contains(unit, {{0.f, -0.1f}}));
+  EXPECT_FALSE(math::contains(unit, {{-0.1f, 0.0f}}));
+  EXPECT_FALSE(math::contains(unit, {{1.1f, 0.0f}}));
+}
+
+INSTANTIATE_TEST_CASE_P(Contains, UnitSquareTest,
+                        Combine(ValuesIn(generate(0.f, 1.f, 0.25f)),
+                                ValuesIn(generate(0.f, 1.f, 0.25f))));
+
+struct LineQuadTest : TestWithParam<point> {};
+
+TEST_P(LineQuadTest, OriginLineIntersectsUnitSquare) {
+  square const unit{{{0, 0}}, 1};
+  line const ln{{{0, 0}}, GetParam()};
+  EXPECT_TRUE(math::intersects(ln, unit));
+  EXPECT_TRUE(math::intersects(unit, ln));
+}
+
+TEST_P(LineQuadTest, CrossingLineIntersectsUnitSquare) {
+  square const unit{{{0, 0}}, 1};
+  line const ln{{{0.5, 0.5}}, GetParam()};
+  EXPECT_TRUE(math::intersects(ln, unit));
+  EXPECT_TRUE(math::intersects(unit, ln));
+}
+
+TEST_P(LineQuadTest, CrossingLineIntersectsSquare) {
+  square const unit{{{0, 0}}, 0.9};
+  line const ln{{{0.5, 0.5}}, GetParam()};
+  EXPECT_TRUE(math::intersects(ln, unit));
+  EXPECT_TRUE(math::intersects(unit, ln));
+}
+
+TEST_F(LineQuadTest, JustPointIntersection) {
+  square const unit{{{0, 0}}, 1};
+  line const ln{{{1, 1}}, {{2, 1}}};
+  EXPECT_TRUE(math::intersects(ln, unit));
+  EXPECT_TRUE(math::intersects(unit, ln));
+}
+
+TEST_F(LineQuadTest, EntirelyEncasedIntersection) {
+  square const unit{{{0, 0}}, 1};
+  line const ln{{{0.5, 0.5}}, {{0.75, 0.75}}};
+  EXPECT_TRUE(math::intersects(ln, unit));
+  EXPECT_TRUE(math::intersects(unit, ln));
+}
+
+TEST_F(LineQuadTest, OutsideByAnInch) {
+  square const unit{{{0, 0}}, 1};
+  line const ln{{{1.001, 1}}, {{2, 1}}};
+  math::intersects(ln, unit);
+  EXPECT_FALSE(math::intersects(ln, unit));
+  EXPECT_FALSE(math::intersects(unit, ln));
+}
+
+INSTANTIATE_TEST_CASE_P(Intersects, LineQuadTest, ValuesIn(line_ends));
+
+TEST(CircleTest, CircleIntersectsSelf) {
+  circle const c1{{{0, 0}}, 1};
+  circle const c2{{{0, 0}}, 1};
+  EXPECT_TRUE(math::intersects(c1, c2));
+}
+
+TEST(CircleTest, CircleIntersectsAtOnePoint) {
+  circle const c1{{{0, 0}}, 1};
+  circle const c2{{{0, 2}}, 1};
+  EXPECT_TRUE(math::intersects(c1, c2));
+}
+
+TEST(CircleTest, CircleIntersectsWithChord) {
+  circle const c1{{{0, 0}}, 0.5};
+  circle const c2{{{0, 1}}, 0.5};
+  EXPECT_TRUE(math::intersects(c1, c2));
+}
+
+TEST(CircleTest, CircleContainedWithin) {
+  circle const c1{{{0, 0}}, 2};
+  circle const c2{{{0, 1}}, 0.5};
+  EXPECT_TRUE(math::intersects(c1, c2));
+}
+
+TEST(CircleTest, CircleOutsideDoesNotIntersect) {
+  circle const c1{{{0, 0}}, 1};
+  circle const c2{{{1.5, 1.5}}, 1};
+  EXPECT_FALSE(math::intersects(c1, c2));
+}
+
+TEST(CircleTest, LineIntersectsFromWithin) {
+  circle const c1{{{0, 0}}, 1};
+  line const ln{{{0.5, 0.5}}, {{1, 1}}};
+  EXPECT_TRUE(math::intersects(c1, ln));
+}
+
+TEST(CircleTest, LineIntersectsOnEdge) {
+  circle const c1{{{0, 0}}, 1};
+  line const ln{{{-1, 1}}, {{1, 1}}};
+  EXPECT_TRUE(math::intersects(c1, ln));
+}
+
+TEST(CircleTest, LineIntersectsWhenContained) {
+  circle const c1{{{0, 0}}, 1};
+  line const ln{{{0.5, 0.5}}, {{-0.5, -0.5}}};
+  EXPECT_TRUE(math::intersects(c1, ln));
+}
+
+TEST(CircleTest, ChordLineIntersects) {
+  circle const c1{{{0, 0}}, 1};
+  line const ln{{{1.5, -0.5}}, {{-0.5, 1.5}}};
+  EXPECT_TRUE(math::intersects(c1, ln));
+}
+
+TEST(CircleTest, OutsideLineDoesntIntersect) {
+  circle const c1{{{0, 0}}, 1};
+  line const ln{{{1, 1}}, {{0, 1.5}}};
+  EXPECT_FALSE(math::intersects(c1, ln));
+}
+
+TEST(CircleTest, OverlappingQuad) {
+  circle const c1{{{0, 0}}, 1};
+  square const sq{{{0.5, 0.5}}, 0.5};
+  EXPECT_TRUE(math::intersects(c1, sq));
+}
+
+TEST(CircleTest, IntersectsAtEdge) {
+  circle const c1{{{0, 0}}, 1};
+  square const sq{{{-1, 1}}, 2};
+  EXPECT_TRUE(math::intersects(c1, sq));
+}
+
+TEST(CircleTest, IntersectsAtCorner) {
+  circle const c1{{{0, 0}}, 1};
+  square const sq{{{0, 1}}, 1};
+  EXPECT_TRUE(math::intersects(c1, sq));
+}
+
+TEST(CircleTest, ContainingQuad) {
+  circle const c1{{{0, 0}}, 1};
+  square const sq{{{0, 0}}, 0.5};
+  EXPECT_TRUE(math::intersects(c1, sq));
+}
+
+TEST(CircleTest, ContainedInQuad) {
+  circle const c1{{{0, 0}}, 0.5};
+  square const sq{{{-1, -1}}, 2};
+  EXPECT_TRUE(math::intersects(c1, sq));
+}
+
+TEST(CircleTest, NonIntersectingTangent) {
+  circle const c1{{{0, 0}}, 1};
+  square const sq{{{1, 1}}, 1};
+  EXPECT_FALSE(math::intersects(c1, sq));
+}
+
+TEST(CircleTest, NonIntersecting) {
+  circle const c1{{{0, 0}}, 1};
+  square const sq{{{1.5, 0.5}}, 0.5};
+  EXPECT_FALSE(math::intersects(c1, sq));
+}

+ 122 - 0
math/test/shape_test.cxx

@@ -0,0 +1,122 @@
+//
+//  shape_test.cxx
+//  math
+//
+//  Created by Sam Jaffe on 5/4/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <gmock/gmock.h>
+
+#include "game/math/shape.hpp"
+
+#include "test_printers.h"
+
+using namespace math::dim2;
+using namespace testing;
+
+struct FromOriginTest : TestWithParam<line> {};
+
+TEST_P(FromOriginTest, IntersectsAtOrigin) {
+  line l1 = {GetParam().first, {{0, 0}}};
+  line l2 = {{{0, 0}}, GetParam().second};
+
+  EXPECT_THAT(math::lines::intersection(l1, l2), Eq(point{{0, 0}}));
+}
+
+std::vector<line> const point_pairs{
+    {{{1, 1}}, {{0, 0}}},   // 0 length
+    {{{1, 1}}, {{1, 0}}},   // -45deg
+    {{{1, 1}}, {{0, 1}}},   // +45deg
+    {{{1, 1}}, {{1, 1}}},   // +0deg
+    {{{1, 1}}, {{2, 1}}},   // -18deg (approx)
+    {{{1, 1}}, {{1, 2}}},   // +18deg (approx)
+    {{{2, 3}}, {{1, 2}}},   //
+    {{{1, 1}}, {{-1, 0}}},  // +135deg
+    {{{1, 1}}, {{0, -1}}},  // -135deg
+    {{{1, 1}}, {{-1, -1}}}, // +180deg
+    {{{1, 1}}, {{1, -1}}},  // +90deg
+    {{{1, 1}}, {{-1, 1}}},  // -90deg
+};
+
+INSTANTIATE_TEST_CASE_P(LineIntersection, FromOriginTest,
+                        ValuesIn(point_pairs));
+
+struct UnitLineTest : TestWithParam<point> {};
+
+TEST_P(UnitLineTest, OrthoOnIntersection) {
+  line const ln{{{0, 0}}, {{1, 0}}};
+  point const pt = GetParam();
+  line const expected{pt, {{pt[0], 0}}};
+  EXPECT_THAT(math::lines::orthogonal(ln, pt), Eq(expected));
+}
+
+TEST_P(UnitLineTest, OrthoOnIntersectionY) {
+  line const ln{{{0, 0}}, {{0, 1}}};
+  point const pt = GetParam();
+  line const expected{pt, {{pt[0], 0}}};
+  EXPECT_THAT(math::lines::orthogonal(ln, pt), Eq(expected));
+}
+
+std::vector<point> x_orthos{
+    {{0, 1}}, {{1, 1}}, {{1, 0}},  {{-1, 0}}, {{0, -1}}, {{-1, -1}},
+    {{0, 2}}, {{2, 2}}, {{2, 0}},  {{-2, 0}}, {{0, -2}}, {{-2, -2}},
+    {{2, 1}}, {{1, 2}}, {{-2, 1}}, {{-1, 2}}, {{1, -2}}, {{2, -1}}};
+
+INSTANTIATE_TEST_CASE_P(LineOrthogonal, UnitLineTest, ValuesIn(x_orthos));
+
+struct DiagonalTest : TestWithParam<std::pair<point, float>> {};
+
+TEST_P(DiagonalTest, OrthoOnIntersection) {
+  line const ln{{{0, 0}}, {{1, 1}}};
+  point const pt = GetParam().first;
+  line const expected{pt, {{GetParam().second, GetParam().second}}};
+  EXPECT_THAT(math::lines::orthogonal(ln, pt), Eq(expected));
+}
+
+std::vector<std::pair<point, float>> diag_orthos{
+    {{{0, 1}}, 0.5f},   {{{1, 1}}, 1.f},    {{{1, 0}}, 0.5f},
+    {{{-1, 0}}, -0.5f}, {{{0, -1}}, -0.5f}, {{{-1, -1}}, -1.f},
+    {{{0, 2}}, 1.f},    {{{2, 2}}, 2.f},    {{{2, 0}}, 1.f},
+    {{{-2, 0}}, -1.f},  {{{0, -2}}, -1.f},  {{{-2, -2}}, -2.f},
+    {{{2, 1}}, 1.5f},   {{{1, 2}}, 1.5f},   {{{-2, 1}}, -0.5f},
+    {{{-1, 2}}, 0.5f},  {{{1, -2}}, -0.5f}, {{{2, -1}}, 0.5f}};
+
+INSTANTIATE_TEST_CASE_P(LineOrthogonal, DiagonalTest, ValuesIn(diag_orthos));
+
+struct QuadTest : TestWithParam<std::tuple<float, float>> {};
+
+TEST_P(QuadTest, SquareProducesQuadWithCornersAtX) {
+  float x = std::get<0>(GetParam());
+  square const square{{{0, 0}}, x};
+  quad const expected{{{0, 0}}, {{x, 0}}, {{x, x}}, {{0, x}}};
+  EXPECT_THAT(quad(square), Eq(expected));
+}
+
+TEST_P(QuadTest, SquareProducesRectangleWithXX) {
+  float side = std::get<0>(GetParam());
+  square const square{{{0, 0}}, side};
+  EXPECT_THAT(rectangle(square).origin, Eq(square.origin));
+  EXPECT_THAT(rectangle(square).size[0], Eq(side));
+  EXPECT_THAT(rectangle(square).size[0], Eq(side));
+}
+
+TEST_P(QuadTest, OffsetSquareProducesRectangleWithXX) {
+  float side = std::get<0>(GetParam());
+  square const square{{{1, 1}}, side};
+  EXPECT_THAT(rectangle(square).origin, Eq(square.origin));
+  EXPECT_THAT(rectangle(square).size[0], Eq(side));
+  EXPECT_THAT(rectangle(square).size[0], Eq(side));
+}
+
+TEST_P(QuadTest, RectProducesQuadWithCornersAtXY) {
+  float x = std::get<0>(GetParam());
+  float y = std::get<1>(GetParam());
+  rectangle const square{{{0, 0}}, {{x, y}}};
+  quad const expected{{{0, 0}}, {{x, 0}}, {{x, y}}, {{0, y}}};
+  EXPECT_THAT(quad(square), Eq(expected));
+}
+
+INSTANTIATE_TEST_CASE_P(Upcast, QuadTest,
+                        Combine(Values(0.5, 1.0, 1.5, 2.0),
+                                Values(0.5, 1.0, 1.5, 2.0)));

+ 33 - 0
math/test/test_printers.h

@@ -0,0 +1,33 @@
+//
+//  test_printers.h
+//  math-test
+//
+//  Created by Sam Jaffe on 5/5/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#ifndef test_printers_h
+#define test_printers_h
+#pragma once
+
+#include "game/math/shape.hpp"
+
+namespace math { namespace vector {
+  inline std::ostream & operator<<(std::ostream & os, math::vec2 const & p) {
+    return os << '[' << p[0] << ',' << p[1] << ']';
+  }
+}}
+namespace math { namespace dim2 {
+  inline bool operator==(line const & lhs, line const & rhs) {
+    return lhs.first == rhs.first && lhs.second == rhs.second;
+  }
+  inline bool operator==(quad const & lhs, quad const & rhs) {
+    return lhs.ll == rhs.ll && lhs.lr == rhs.lr && lhs.ul == rhs.ul &&
+           lhs.ur == rhs.ur;
+  }
+  inline std::ostream & operator<<(std::ostream & os, line const & l) {
+    return os << '[' << l.first << ',' << l.second << ']';
+  }
+}}
+
+#endif /* test_printers_h */