ViveNavMeshEditor.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. using UnityEditor;
  4. using UnityEngine.AI;
  5. using System;
  6. using System.Collections.Generic;
  7. /// \brief Custom inspector for ViveNavMesh. This handles the conversion from Unity NavMesh to Mesh and performs some
  8. /// computational geometry to find the borders of the mesh.
  9. [CustomEditor(typeof(ViveNavMesh))]
  10. public class ViveNavMeshEditor : Editor {
  11. private SerializedProperty p_area;
  12. private SerializedProperty p_mesh;
  13. private SerializedProperty p_material;
  14. private SerializedProperty p_alpha;
  15. private SerializedProperty p_layer_mask;
  16. private SerializedProperty p_ignore_layer_mask;
  17. private SerializedProperty p_query_trigger_interaction;
  18. private SerializedProperty p_sample_radius;
  19. private SerializedProperty p_ignore_sloped_surfaces;
  20. private SerializedProperty p_dewarp_method;
  21. void OnEnable()
  22. {
  23. p_area = serializedObject.FindProperty("_NavAreaMask");
  24. p_mesh = serializedObject.FindProperty("_SelectableMesh");
  25. p_material = serializedObject.FindProperty("_GroundMaterialSource");
  26. p_alpha = serializedObject.FindProperty("GroundAlpha");
  27. p_layer_mask = serializedObject.FindProperty("_LayerMask");
  28. p_ignore_layer_mask = serializedObject.FindProperty("_IgnoreLayerMask");
  29. p_query_trigger_interaction = serializedObject.FindProperty("_QueryTriggerInteraction");
  30. p_sample_radius = serializedObject.FindProperty("_SampleRadius");
  31. p_ignore_sloped_surfaces = serializedObject.FindProperty("_IgnoreSlopedSurfaces");
  32. p_dewarp_method = serializedObject.FindProperty("_DewarpingMethod");
  33. }
  34. public override void OnInspectorGUI()
  35. {
  36. GUIStyle bold_wrap = EditorStyles.boldLabel;
  37. bold_wrap.wordWrap = true;
  38. GUILayout.Label("Navmesh Preprocessor for HTC Vive Locomotion", bold_wrap);
  39. GUILayout.Label("Adrian Biagioli 2017", EditorStyles.miniLabel);
  40. GUILayout.Label("Before Using", bold_wrap);
  41. GUIStyle wrap = EditorStyles.label;
  42. wrap.wordWrap = true;
  43. GUILayout.Label(
  44. "Make sure you bake a Navigation Mesh (NavMesh) in Unity before continuing (Window > Navigation). When you "+
  45. "are done, click \"Update Navmesh Data\" below. This will update the graphic of the playable area "+
  46. "that the player will see in-game.\n",
  47. wrap);
  48. ViveNavMesh mesh = (ViveNavMesh)target;
  49. serializedObject.Update();
  50. // Area Mask //
  51. string[] areaNames = GameObjectUtility.GetNavMeshAreaNames();
  52. int[] area_index = new int[areaNames.Length];
  53. int temp_mask = 0;
  54. for (int x = 0; x < areaNames.Length; x++)
  55. {
  56. area_index[x] = GameObjectUtility.GetNavMeshAreaFromName(areaNames[x]);
  57. temp_mask |= ((p_area.intValue >> area_index[x]) & 1) << x;
  58. }
  59. EditorGUI.BeginChangeCheck();
  60. temp_mask = EditorGUILayout.MaskField("Area Mask", temp_mask, areaNames);
  61. if(EditorGUI.EndChangeCheck())
  62. {
  63. p_area.intValue = 0;
  64. for(int x=0; x<areaNames.Length; x++)
  65. p_area.intValue |= (((temp_mask >> x) & 1) == 1 ? 0 : 1) << area_index[x];
  66. p_area.intValue = ~p_area.intValue;
  67. }
  68. serializedObject.ApplyModifiedProperties();
  69. // Sanity check for Null properties //
  70. bool HasMesh = (mesh.SelectableMesh != null && mesh.SelectableMesh.vertexCount != 0) || (mesh.SelectableMeshBorder != null && mesh.SelectableMeshBorder.Length != 0);
  71. // Fixes below error message popping up with prefabs. Kind of hacky but gets the job done
  72. bool isPrefab = EditorUtility.IsPersistent(target);
  73. if (isPrefab && mesh.SelectableMesh == null)
  74. mesh.SelectableMesh = new Mesh();
  75. bool MeshNull = mesh.SelectableMesh == null;
  76. bool BorderNull = mesh.SelectableMeshBorder == null;
  77. if (MeshNull || BorderNull) {
  78. string str = "Internal Error: ";
  79. if (MeshNull)
  80. str += "Selectable Mesh == null. ";
  81. if (BorderNull)
  82. str += "Border point array == null. ";
  83. str += "This may lead to strange behavior or serialization. Try updating the mesh or delete and recreate the Navmesh object. ";
  84. str += "If you are able to consistently get a Vive Nav Mesh object into this state, please submit a bug report.";
  85. EditorGUILayout.HelpBox(str, MessageType.Error);
  86. }
  87. // Update / Clear Navmesh Data //
  88. if (GUILayout.Button("Update Navmesh Data"))
  89. {
  90. Undo.RecordObject(mesh, "Update Navmesh Data");
  91. NavMeshTriangulation tri = NavMesh.CalculateTriangulation();
  92. Vector3[] verts = tri.vertices;
  93. int[] tris = tri.indices;
  94. int[] areas = tri.areas;
  95. int vert_size = verts.Length;
  96. int tri_size = tris.Length;
  97. RemoveMeshDuplicates(verts, tris, out vert_size, 0.01f);
  98. DewarpMesh(verts, mesh.DewarpingMethod, mesh.SampleRadius);
  99. CullNavmeshTriangulation(verts, tris, areas, p_area.intValue, mesh.IgnoreSlopedSurfaces, ref vert_size, ref tri_size);
  100. Mesh m = ConvertNavmeshToMesh(verts, tris, vert_size, tri_size);
  101. // Can't use SerializedProperties here because BorderPointSet doesn't derive from UnityEngine.Object
  102. mesh.SelectableMeshBorder = FindBorderEdges(m);
  103. serializedObject.Update();
  104. p_mesh.objectReferenceValue = m;
  105. serializedObject.ApplyModifiedPropertiesWithoutUndo();
  106. mesh.SelectableMesh = mesh.SelectableMesh; // Make sure that setter is called
  107. }
  108. GUI.enabled = HasMesh;
  109. if(GUILayout.Button("Clear Navmesh Data"))
  110. {
  111. Undo.RecordObject(mesh, "Clear Navmesh Data");
  112. // Note: Unity does not serialize "null" correctly so we set everything to empty objects
  113. Mesh m = new Mesh();
  114. serializedObject.Update();
  115. p_mesh.objectReferenceValue = m;
  116. serializedObject.ApplyModifiedPropertiesWithoutUndo();
  117. mesh.SelectableMesh = mesh.SelectableMesh; // Make sure setter is called
  118. mesh.SelectableMeshBorder = new BorderPointSet[0];
  119. }
  120. GUI.enabled = true;
  121. GUILayout.Label(HasMesh ? "Status: NavMesh Loaded" : "Status: No NavMesh Loaded");
  122. // Render Settings //
  123. EditorGUILayout.LabelField("Render Settings", EditorStyles.boldLabel);
  124. EditorGUI.BeginChangeCheck();
  125. EditorGUILayout.PropertyField(p_material);
  126. if (EditorGUI.EndChangeCheck())
  127. {
  128. Undo.RecordObject(mesh, "Change Ground Material");
  129. serializedObject.ApplyModifiedPropertiesWithoutUndo();
  130. mesh.GroundMaterial = new Material((Material)p_material.objectReferenceValue); // Reload material
  131. }
  132. EditorGUILayout.PropertyField(p_alpha);
  133. serializedObject.ApplyModifiedProperties();
  134. // Raycast Settings //
  135. EditorGUILayout.LabelField("Raycast Settings", EditorStyles.boldLabel);
  136. int temp_layer_mask = p_layer_mask.intValue;
  137. bool temp_ignore_layer_mask = p_ignore_layer_mask.boolValue;
  138. EditorGUI.BeginChangeCheck();
  139. temp_layer_mask = LayerMaskField("Layer Mask", temp_layer_mask);
  140. if(EditorGUI.EndChangeCheck())
  141. {
  142. p_layer_mask.intValue = temp_layer_mask;
  143. }
  144. serializedObject.ApplyModifiedProperties();
  145. EditorGUI.BeginChangeCheck();
  146. temp_ignore_layer_mask = EditorGUILayout.Toggle("Ignore Layer Mask", temp_ignore_layer_mask);
  147. if(EditorGUI.EndChangeCheck())
  148. {
  149. p_ignore_layer_mask.boolValue = temp_ignore_layer_mask;
  150. }
  151. serializedObject.ApplyModifiedProperties();
  152. QueryTriggerInteraction temp_query_trigger_interaction = (QueryTriggerInteraction) p_query_trigger_interaction.intValue;
  153. EditorGUI.BeginChangeCheck();
  154. temp_query_trigger_interaction = (QueryTriggerInteraction) EditorGUILayout.EnumPopup("Query Trigger Interaction", (QueryTriggerInteraction) temp_query_trigger_interaction);
  155. if(EditorGUI.EndChangeCheck())
  156. {
  157. p_query_trigger_interaction.intValue = (int) temp_query_trigger_interaction;
  158. }
  159. serializedObject.ApplyModifiedProperties();
  160. // Navmesh Settings //
  161. EditorGUILayout.LabelField("Navmesh Settings", EditorStyles.boldLabel);
  162. GUILayout.Label(
  163. "Make sure the sample radius below is equal to your Navmesh Voxel Size (see Advanced > Voxel Size " +
  164. "in the navigation window). Increase this if the selection disk is not appearing.",
  165. wrap);
  166. EditorGUILayout.PropertyField(p_sample_radius);
  167. EditorGUILayout.PropertyField(p_ignore_sloped_surfaces);
  168. EditorGUILayout.PropertyField(p_dewarp_method);
  169. serializedObject.ApplyModifiedProperties();
  170. }
  171. private static void DewarpMesh(Vector3[] verts, NavmeshDewarpingMethod dw, float step)
  172. {
  173. if (dw == NavmeshDewarpingMethod.None)
  174. return;
  175. for (int x = 0; x < verts.Length; x++)
  176. {
  177. if (dw == NavmeshDewarpingMethod.RaycastDownward)
  178. {
  179. RaycastHit hit;
  180. // Have the raycast span over the entire navmesh voxel
  181. Vector3 sample = verts[x];
  182. double vy = Math.Round(verts[x].y / step) * step;
  183. sample.y = (float)vy;
  184. if (Physics.Raycast(sample, Vector3.down, out hit, (float)step + 0.01f))
  185. verts[x] = hit.point;
  186. }
  187. else if (dw == NavmeshDewarpingMethod.RoundToVoxelSize)
  188. {
  189. // Clamp the point to the voxel grid in the Y direction
  190. double vy = Math.Round((verts[x].y - 0.05) / step) * step + 0.05;
  191. verts[x].y = (float)vy;
  192. }
  193. }
  194. }
  195. /// \brief Modifies the given NavMesh so that only the Navigation areas are present in the mesh. This is done only
  196. /// by swapping, so that no new memory is allocated.
  197. ///
  198. /// The data stored outside of the returned array sizes should be considered invalid and will contain garbage data.
  199. /// \param vertices vertices of Navmesh
  200. /// \param indices indices of Navmesh triangles
  201. /// \param areas Navmesh areas
  202. /// \param areaMask Area mask to include in returned mesh. Areas outside of this mask are culled.
  203. /// \param vert_size New size of navMesh.vertices
  204. /// \param tri_size New size of navMesh.areas and one third of the size of navMesh.indices
  205. private static void CullNavmeshTriangulation(Vector3[] vertices, int[] indices, int[] areas, int areaMask, bool ignore_sloped_surfaces, ref int vert_size, ref int tri_size)
  206. {
  207. // Step 1: re-order triangles so that valid areas are in front. Then determine tri_size.
  208. tri_size = indices.Length / 3;
  209. for(int i=0; i < tri_size; i++)
  210. {
  211. Vector3 p1 = vertices[indices[i * 3]];
  212. Vector3 p2 = vertices[indices[i * 3 + 1]];
  213. Vector3 p3 = vertices[indices[i * 3 + 2]];
  214. Plane p = new Plane(p1, p2, p3);
  215. bool vertical = Mathf.Abs(Vector3.Dot(p.normal, Vector3.up)) > 0.99f;
  216. // If the current triangle isn't flat (normal is up) or if it doesn't match
  217. // with the provided mask, we should cull it.
  218. if(((1 << areas[i]) & areaMask) == 0 || (ignore_sloped_surfaces && !vertical)) // If true this triangle should be culled.
  219. {
  220. // Swap area indices and triangle indices with the end of the array
  221. int t_ind = tri_size - 1;
  222. int t_area = areas[t_ind];
  223. areas[t_ind] = areas[i];
  224. areas[i] = t_area;
  225. for(int j=0;j<3;j++)
  226. {
  227. int t_v = indices[t_ind * 3 + j];
  228. indices[t_ind * 3 + j] = indices[i * 3 + j];
  229. indices[i * 3 + j] = t_v;
  230. }
  231. // Then reduce the size of the array, effectively cutting off the previous triangle
  232. tri_size--;
  233. // Stay on the same index so that we can check the triangle we just swapped.
  234. i--;
  235. }
  236. }
  237. // Step 2: Cull the vertices that aren't used.
  238. vert_size = 0;
  239. for(int i=0; i < tri_size * 3; i++)
  240. {
  241. int prv = indices[i];
  242. if (prv >= vert_size)
  243. {
  244. int nxt = vert_size;
  245. // Bring the current vertex to the end of the "active" array by swapping it with what's currently there
  246. Vector3 t_v = vertices[prv];
  247. vertices[prv] = vertices[nxt];
  248. vertices[nxt] = t_v;
  249. // Now change around the values in the triangle indices to reflect the swap
  250. for(int j=i; j < tri_size * 3; j++)
  251. {
  252. if (indices[j] == prv)
  253. indices[j] = nxt;
  254. else if (indices[j] == nxt)
  255. indices[j] = prv;
  256. }
  257. // Increase the size of the vertex array to reflect the changes.
  258. vert_size++;
  259. }
  260. }
  261. }
  262. /// \brief Converts a NavMesh (or a NavMesh area) into a standard Unity mesh. This is later used
  263. /// to render the mesh on-screen using Unity's standard rendering tools.
  264. ///
  265. /// \param navMesh Precalculated Nav Mesh Triangulation
  266. /// \param vert_size size of vertex array
  267. /// \param tri_size size of triangle array
  268. private static Mesh ConvertNavmeshToMesh(Vector3[] vraw, int[] iraw, int vert_size, int tri_size)
  269. {
  270. Mesh ret = new Mesh();
  271. if(vert_size >= 65535)
  272. {
  273. Debug.LogError("Playable NavMesh too big (vertex count >= 65535)! Limit the size of the playable area using"+
  274. "Area Masks. For now no preview mesh will render.");
  275. return ret;
  276. }
  277. Vector3[] vertices = new Vector3[vert_size];
  278. for (int x = 0; x < vertices.Length; x++) {
  279. vertices[x].x = vraw[x].x;
  280. vertices[x].y = vraw[x].y;
  281. vertices[x].z = vraw[x].z;
  282. }
  283. int[] triangles = new int[tri_size * 3];
  284. for (int x = 0; x < triangles.Length; x++)
  285. triangles[x] = iraw[x];
  286. ret.name = "Navmesh";
  287. ret.Clear();
  288. ret.vertices = vertices;
  289. ret.triangles = triangles;
  290. ret.RecalculateNormals();
  291. ret.RecalculateBounds();
  292. return ret;
  293. }
  294. /// \brief VERY naive implementation of removing duplicate vertices in a mesh. O(n^2).
  295. ///
  296. /// This is necessary because Unity NavMeshes for some reason have a whole bunch of duplicate vertices (or vertices
  297. /// that are very close together). So some processing needs to be done go get rid of these.
  298. ///
  299. /// If this becomes an actual performance hog, consider changing this to sort the vertices first using a more
  300. /// optimized process O(n lg n) then removing adjacent duplicates.
  301. ///
  302. /// \param verts Vertex array to process
  303. /// \param elts Triangle indices array
  304. /// \param verts_size size of vertex array after processing
  305. /// \param threshold Threshold with which to combine vertices
  306. private static void RemoveMeshDuplicates(Vector3[] verts, int[] elts, out int verts_size, double threshold)
  307. {
  308. int size = verts.Length;
  309. for (int x = 0; x < size; x++)
  310. {
  311. for (int y = x + 1; y < size; y++)
  312. {
  313. Vector3 d = verts[x] - verts[y];
  314. if (x != y && Mathf.Abs(d.x) < threshold && Mathf.Abs(d.y) < threshold && Mathf.Abs(d.z) < threshold)
  315. {
  316. verts[y] = verts[size - 1];
  317. for (int z = 0; z < elts.Length; z++)
  318. {
  319. if (elts[z] == y)
  320. elts[z] = x;
  321. if (elts[z] == size - 1)
  322. elts[z] = y;
  323. }
  324. size--;
  325. y--;
  326. }
  327. }
  328. }
  329. verts_size = size;
  330. }
  331. /// \brief Given some mesh m, calculates a number of polylines that border the mesh. This may return more than
  332. /// one polyline if, for example, the mesh has holes in it or if the mesh is separated in two pieces.
  333. ///
  334. /// \param m input mesh
  335. /// \returns array of cyclic polylines
  336. private static BorderPointSet[] FindBorderEdges(Mesh m)
  337. {
  338. // First, get together all the edges in the mesh and find out
  339. // how many times each edge is used. Edges that are only used
  340. // once are border edges.
  341. // Key: edges (note that because of how the hashcode / equals() is set up, two equivalent edges will effectively
  342. // be equal)
  343. // Value: How many times this edge shows up in the mesh. Any keys with a value of 1 are border edges.
  344. Dictionary<Edge, int> edges = new Dictionary<Edge, int>();
  345. for (int x = 0; x < m.triangles.Length / 3; x++)
  346. {
  347. int p1 = m.triangles[x * 3];
  348. int p2 = m.triangles[x * 3 + 1];
  349. int p3 = m.triangles[x * 3 + 2];
  350. Edge[] e = new Edge[3];
  351. e[0] = new Edge(p1, p2);
  352. e[1] = new Edge(p2, p3);
  353. e[2] = new Edge(p3, p1);
  354. foreach (Edge d in e) {
  355. int curval;
  356. edges.TryGetValue(d, out curval); // 0 if nonexistant
  357. edges[d] = curval + 1;
  358. }
  359. }
  360. // Next, consolidate all of the border edges into one List<Edge>
  361. List<Edge> border = new List<Edge>();
  362. foreach (KeyValuePair<Edge, int> p in edges)
  363. {
  364. if (p.Value == 1) // border edge == edge only used once.
  365. border.Add(p.Key);
  366. }
  367. // Perform the following routine:
  368. // 1. Pick any unvisited edge segment [v_start,v_next] and add these vertices to the polygon loop.
  369. // 2. Find the unvisited edge segment [v_i,v_j] that has either v_i = v_next or v_j = v_next and add the
  370. // other vertex (the one not equal to v_next) to the polygon loop. Reset v_next as this newly added vertex,
  371. // mark the edge as visited and continue from 2.
  372. // 3. Traversal is done when we get back to v_start.
  373. // Source: http://stackoverflow.com/questions/14108553/get-border-edges-of-mesh-in-winding-order
  374. bool[] visited = new bool[border.Count];
  375. bool finished = false;
  376. int cur_index = 0;
  377. List<Vector3[]> ret = new List<Vector3[]>();
  378. while(!finished)
  379. {
  380. int[] raw = FindPolylineFromEdges(cur_index, visited, border);
  381. Vector3[] fmt = new Vector3[raw.Length];
  382. for (int x = 0; x < raw.Length; x++)
  383. fmt[x] = m.vertices[raw[x]];
  384. ret.Add(fmt);
  385. finished = true;
  386. for (int x=0;x<visited.Length;x++) {
  387. if (!visited[x])
  388. {
  389. cur_index = x;
  390. finished = false;
  391. break;
  392. }
  393. }
  394. }
  395. BorderPointSet[] ret_set = new BorderPointSet[ret.Count];
  396. for (int x = 0; x < ret.Count; x++)
  397. {
  398. ret_set[x] = new BorderPointSet(ret[x]);
  399. }
  400. return ret_set;
  401. }
  402. /// Given a list of edges, finds a polyline connected to the edge at index start.
  403. /// Guaranteed to run in O(n) time. Assumes that each edge only has two neighbor edges.
  404. ///
  405. /// \param start starting index of edge
  406. /// \param visited tally of visited edges (perhaps from previous calls)
  407. /// \param edges list of edges
  408. private static int[] FindPolylineFromEdges(int start, bool[] visited, List<Edge> edges)
  409. {
  410. List<int> loop = new List<int>(edges.Count);
  411. loop.Add(edges[start].min);
  412. loop.Add(edges[start].max);
  413. visited[start] = true;
  414. // With each iteration of this while loop, we look for an edge that connects to the previous one
  415. // but hasn't been processed yet (to prevent simply finding the previous edge again, and to prevent
  416. // a hang if faulty data is given).
  417. while (loop[loop.Count - 1] != edges[start].min)
  418. {
  419. int cur = loop[loop.Count - 1];
  420. bool found = false;
  421. for (int x = 0; x < visited.Length; x++)
  422. {
  423. // If we have visited this edge before, or both vertices on this edge
  424. // aren't connected to cur, then skip the edge
  425. if (!visited[x] && (edges[x].min == cur || edges[x].max == cur))
  426. {
  427. // The next vertex in the loop
  428. int next = edges[x].min == cur ? edges[x].max : edges[x].min;
  429. loop.Add(next);
  430. // Mark the edge as visited and continue the outermost loop
  431. visited[x] = true;
  432. found = true;
  433. break;
  434. }
  435. }
  436. if (!found)// acyclic, so break.
  437. break;
  438. }
  439. int[] ret = new int[loop.Count];
  440. loop.CopyTo(ret);
  441. return ret;
  442. }
  443. static List<int> layerNumbers = new List<int>();
  444. // From http://answers.unity3d.com/questions/42996/how-to-create-layermask-field-in-a-custom-editorwi.html
  445. static LayerMask LayerMaskField(string label, LayerMask layerMask)
  446. {
  447. var layers = UnityEditorInternal.InternalEditorUtility.layers;
  448. layerNumbers.Clear();
  449. for (int i = 0; i < layers.Length; i++)
  450. layerNumbers.Add(LayerMask.NameToLayer(layers[i]));
  451. int maskWithoutEmpty = 0;
  452. for (int i = 0; i < layerNumbers.Count; i++)
  453. {
  454. if (((1 << layerNumbers[i]) & layerMask.value) > 0)
  455. maskWithoutEmpty |= (1 << i);
  456. }
  457. maskWithoutEmpty = UnityEditor.EditorGUILayout.MaskField(label, maskWithoutEmpty, layers);
  458. int mask = 0;
  459. for (int i = 0; i < layerNumbers.Count; i++)
  460. {
  461. if ((maskWithoutEmpty & (1 << i)) > 0)
  462. mask |= (1 << layerNumbers[i]);
  463. }
  464. layerMask.value = mask;
  465. return layerMask;
  466. }
  467. private struct Edge
  468. {
  469. public int min
  470. {
  471. get; private set;
  472. }
  473. public int max
  474. {
  475. get; private set;
  476. }
  477. public Edge(int p1, int p2)
  478. {
  479. // This is done so that if p1 and p2 are switched, the edge has the same hash
  480. min = p1 < p2 ? p1 : p2;
  481. max = p1 >= p2 ? p1 : p2;
  482. }
  483. public override bool Equals(object obj)
  484. {
  485. if (!(obj is Edge))
  486. return false;
  487. Edge e = (Edge)obj;
  488. return e.min == min && e.max == max;
  489. }
  490. public override int GetHashCode()
  491. {
  492. // Small note: this breaks when you have more than 65535 vertices.
  493. return (min << 16) + max;
  494. }
  495. }
  496. }
  497. #endif