Files
JJBB/Assets/Plugins/NavMeshExtension/Scripts/Editor/NavMeshObjectEditor.cs
2024-08-23 15:49:34 +08:00

624 lines
23 KiB
C#

/* This file is part of the "NavMesh Extension" project by Rebound Games.
* You are only allowed to use these resources if you've bought them directly or indirectly
* from Rebound Games. You shall not license, sublicense, sell, resell, transfer, assign,
* distribute or otherwise make available to any third party the Service or the Content.
*/
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.IO;
namespace NavMeshExtension
{
/// <summary>
/// Custom Editor for editing vertices and exporting the mesh.
/// </summary>
[CustomEditor(typeof(NavMeshObject))]
public class NavMeshObjectEditor : Editor
{
//navmesh object reference
private NavMeshObject script;
//export path
private const string assetPath = "Assets/NavMeshExtension/Prefabs/";
//scene view help texts
private const string editOnText = "Click: Place Vertices\n" +
"Ctrl+Click: Submesh\n" +
"Always exit Edit Mode!";
private const string editOffText = "Click vertices: Select\n" +
"Right-click: Deselect\n" +
"BS: Delete Selection";
//converted array of vertex positions
private Vector3[] allPoints;
//indices of selected vertices
private List<int> selected = new List<int>();
//whether or not placement mode is active
private static bool placing = false;
//undo/redo flag for eventually disabling placing
private static bool undoRedo = false;
//clicked mouse position in the scene
private Vector3 mousePosition;
//vertex index near mouse position
private int dragIndex;
void OnEnable()
{
script = (NavMeshObject)target;
}
void OnDisable()
{
//if undo or redo was performed,
//don't toggle placing - and vice versa
if (undoRedo)
PerformUndoRedo();
else
placing = false;
}
/// <summary>
/// Custom inspector override for buttons.
/// </summary>
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space();
//display current count of placed vertices in the active submesh
EditorGUILayout.LabelField("Current Vertices: " + script.current.Count);
//enter placement mode
if (!placing && GUILayout.Button("Edit Mode: Off"))
{
Undo.RegisterCompleteObjectUndo(script, "Edit On");
//if possible, create new submesh
if (CheckSubMesh())
{
GameObject newObj = script.CreateSubMesh();
Undo.RegisterCreatedObjectUndo(newObj, "Edit On");
}
//clear handle selections
selected.Clear();
placing = true;
}
GUI.color = Color.yellow;
//leave placement mode and try to combine submeshes
if (placing && GUILayout.Button("Edit Mode: On"))
{
Undo.RegisterCompleteObjectUndo(script, "Edit Off");
//if possible, combine submeshes
if (CheckCombine())
{
//get all mesh filters
MeshFilter[] meshFilters = script.GetComponentsInChildren<MeshFilter>();
Undo.RecordObject(meshFilters[0], "Edit Off");
//let the script combine them
script.Combine();
for (int i = 1; i < meshFilters.Length; i++)
{
Undo.DestroyObjectImmediate(meshFilters[i].gameObject);
}
}
placing = false;
}
GUI.color = Color.white;
//export mesh to asset and prefab
if (GUILayout.Button("Save as Prefab"))
{
//get gameobject and its mesh filter
GameObject gObj = script.gameObject;
Mesh mesh = gObj.GetComponent<MeshFilter>().sharedMesh;
if (!mesh)
{
Debug.LogWarning("Could not save as prefab, asset does not have a Mesh.");
return;
}
if (AssetDatabase.Contains(mesh))
{
Debug.Log("Mesh asset already exists. Cancelling.");
return;
}
//get the current unix timestamp for a unique naming scheme
var epochStart = new System.DateTime(1970, 1, 1, 0, 0, 0, System.DateTimeKind.Utc);
string timestamp = (System.DateTime.UtcNow - epochStart).TotalSeconds.ToString("F0");
string assetName = "NavMesh_";
//check that the folder does exist
string dir = Path.GetDirectoryName(assetPath);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
AssetDatabase.ImportAsset(dir);
}
//create the mesh asset at the path specified, with unique name
AssetDatabase.CreateAsset(mesh, assetPath + assetName + timestamp + ".asset");
AssetDatabase.SaveAssets();
//create the prefab of this gameobject at the path specified, with the same name
PrefabUtility.CreatePrefab(assetPath + assetName + timestamp + ".prefab", gObj,
ReplacePrefabOptions.ConnectToPrefab);
//rename instance to prefab name
gObj.name = assetName + timestamp;
}
if (GUILayout.Button("Create Mesh"))
{
script.CreateMeshFromPoints();
}
}
/// <summary>
/// Draw Scene GUI handles, circles and outlines for submesh vertices.
/// <summary>
public void OnSceneGUI()
{
//get world positions of vertices
allPoints = ConvertAllPoints();
//create a ray to get where we clicked in the scene view and pass in mouse position
Ray worldRay = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
RaycastHit hitInfo;
Event e = Event.current;
//this prevents selecting other objects in the scene
int controlID = GUIUtility.GetControlID(FocusType.Passive);
HandleUtility.AddDefaultControl(controlID);
//find index of closest vertex, if any
dragIndex = FindClosest();
if (!placing && e.type == EventType.KeyDown && e.keyCode == KeyCode.Backspace)
{
e.Use();
Undo.RegisterCompleteObjectUndo(script, "Delete Vertex");
Undo.RecordObject(script.GetComponent<MeshFilter>().sharedMesh, "Delete Vertex");
DeleteSelected();
return;
}
//in the edit mode, ray hit something
if (placing)
{
Tools.current = Tool.None;
Handles.BeginGUI();
GUILayout.Window(2, new Rect(Screen.width - 157, Screen.height - 100, 100, 50), (id) =>
{
GUILayout.Label(editOnText);
}, "Control Info Box");
Handles.EndGUI();
if (Physics.Raycast(worldRay, out hitInfo))
{
//the actual hit position
mousePosition = hitInfo.point;
//place new point if the left mouse button was clicked
if (e.type == EventType.MouseUp && e.button == 0 && !e.alt)
{
Undo.RegisterCompleteObjectUndo(script, "Add Vertex");
//get current submesh vertex count
int currentCount = script.current.Count;
//create a new submesh, if control was hold in addition to the mouse click
//or if autoSplit is true and the current count exceeds splitAt
if ((e.control && currentCount >= 3) ||
(script.autoSplit && currentCount >= script.splitAt))
{
GameObject newObj = script.CreateSubMesh();
Undo.RegisterCreatedObjectUndo(newObj, "Add Vertex");
}
//call this method when you've used an event.
//the event's type will be set to EventType.Used,
//causing other GUI elements to ignore it
e.Use();
//add point to existing vertex, if near any
//but don't add it to the same submesh (closed mesh)
//otherwise just add a new vertex position
if (script.current.Contains(dragIndex))
script.AddPoint(script.transform.TransformPoint(script.list[dragIndex]));
else if (dragIndex >= 0)
script.AddPoint(dragIndex);
else
script.AddPoint(mousePosition);
//invoke new mesh calculation
Undo.RegisterCompleteObjectUndo(script.subMesh, "Add Vertex");
script.CreateMesh();
return;
}
}
}
//not in edit mode
if (!placing)
{
Handles.BeginGUI();
GUILayout.Window(2, new Rect(Screen.width - 150, Screen.height - 100, 100, 50), (id) =>
{
GUILayout.Label(editOffText);
}, "Control Info Box");
Handles.EndGUI();
//clicking near vertices will select them and show handles
if (e.type == EventType.MouseUp && e.button == 0 && !e.alt)
{
//select/unselect vertex point
if (dragIndex >= 0)
{
if (!selected.Contains(dragIndex))
selected.Add(dragIndex);
else
selected.Remove(dragIndex);
SceneView.RepaintAll();
}
else if(selected.Count == 0)
Selection.activeObject = null;
}
//unselect all vertices
else if (e.type == EventType.MouseUp && e.button == 1 && !e.alt)
{
selected.Clear();
}
}
//draw scene gizmos
DrawSelectedHandles();
DrawPolygonOutline();
//handle undo of vertex modifications (redraw mesh)
if (e.type == EventType.ValidateCommand && e.commandName == "UndoRedoPerformed")
undoRedo = true;
HandleUtility.AddDefaultControl(-1);
HandleUtility.Repaint();
}
private void PerformUndoRedo()
{
undoRedo = false;
if (script.current.Count > 1 && script.subMesh)
{
script.subMesh.triangles = script.RecalculateTriangles(null);
OptimizeMesh(script.subMesh);
return;
}
Mesh sharedMesh = script.GetComponent<MeshFilter>().sharedMesh;
if (!sharedMesh) return;
List<int> triangles = new List<int>();
for (int i = 0; i < script.subPoints.Count; i++)
triangles.AddRange(script.RecalculateTriangles(script.subPoints[i].list));
sharedMesh.triangles = new int[sharedMesh.vertexCount * 3];
script.UpdateMesh(ConvertAllPoints());
sharedMesh.triangles = triangles.ToArray();
if (!placing) script.Combine();
}
//check if a combine of submeshes is possible
private bool CheckCombine()
{
NavMeshManagerEditor.GetSceneView().Focus();
//get count of all submeshes
int subPointsCount = script.subPoints.Count;
if (subPointsCount > 0)
{
//remove submesh without references
if (script.subPoints[subPointsCount - 1].list.Count == 0)
script.subPoints.RemoveAt(subPointsCount - 1);
else if (script.subPoints[subPointsCount - 1].list.Count <= 2)
{
NavMeshManagerEditor.ShowNotification("Can't combine submeshes.\nYou haven't placed enough points.");
return false;
}
}
return true;
}
//check if creating a new submesh is possible
private bool CheckSubMesh()
{
NavMeshManagerEditor.GetSceneView().Focus();
//get count of all submeshes
int subPointsCount = script.subPoints.Count;
if (subPointsCount > 0 && script.subPoints[subPointsCount - 1].list.Count <= 2)
{
NavMeshManagerEditor.ShowNotification("Resuming current submesh.");
return false;
}
return true;
}
//find closest vertex to the mouse position
private int FindClosest()
{
//initialize variables
List<int> closest = new List<int>();
Vector2 mousePos = Event.current.mousePosition;
int near = -1;
//loop over vertices to find the nearest ones
for (int i = 0; i < allPoints.Length; i++)
{
Vector2 screenPoint = HandleUtility.WorldToGUIPoint(allPoints[i]);
if (Vector2.Distance(screenPoint, mousePos) < 10)
closest.Add(i);
}
//don't do further calculation in some cases
if (closest.Count == 0)
return near;
else if (closest.Count == 1)
return closest[0];
else
{
//there are more than a few vertices near the mouse position,
//here only the closest vertex to the camera should matter
Vector3 camPos = Camera.current.transform.position;
float nearDist = float.MaxValue;
//loop over all vertices and get the one near to the camera
for (int i = 0; i < closest.Count; i++)
{
float dist = Vector3.Distance(allPoints[closest[i]], camPos);
if (dist < nearDist)
{
nearDist = dist;
near = closest[i];
}
}
}
closest.Clear();
return near;
}
//convert relative vertex positions to world positions
private Vector3[] ConvertAllPoints()
{
int count = script.list.Count;
List<Vector3> all = new List<Vector3>();
for (int i = 0; i < count; i++)
all.Add(script.transform.TransformPoint(script.list[i]));
return all.ToArray();
}
//delete previously selected vertices and rebuild mesh
private void DeleteSelected()
{
//get mesh references
MeshFilter filter = script.GetComponent<MeshFilter>();
List<Vector3> vertices = new List<Vector3>(filter.sharedMesh.vertices);
//filter selected list for unique entries
selected = selected.Distinct().ToList();
selected = selected.OrderByDescending(x => x).ToList();
//loop over selected vertex indices
for (int i = 0; i < selected.Count; i++)
{
//remove index from mesh vertices
int index = selected[i];
vertices.RemoveAt(index);
script.list.RemoveAt(index);
//loop over submeshes and remove it there too
for (int j = 0; j < script.subPoints.Count; j++)
{
script.subPoints[j].list.Remove(index);
//decrease higher entries, as the array is smaller now
for (int k = 0; k < script.subPoints[j].list.Count; k++)
{
if (script.subPoints[j].list[k] >= index)
script.subPoints[j].list[k] -= 1;
}
}
}
//clear selection
selected.Clear();
//loop over submeshes to remove obsolete indices,
//e.g. if a submesh has only 2 vertices after removal
for (int i = script.subPoints.Count - 1; i >= 0; i--)
{
//check for vertex count
if (script.subPoints[i].list.Count <= 2)
{
//construct a combined list with all indices
List<int> allIndices = new List<int>();
for (int j = 0; j < script.subPoints.Count; j++)
allIndices.AddRange(script.subPoints[j].list);
//check whether an index occurs more than once
List<int> duplicates = allIndices.GroupBy(x => x)
.Where(x => x.Count() > 1)
.Select(x => x.Key)
.ToList();
//if an index in this submesh is not being used in other
//submeshes anymore, this means that we can remove it too
for (int j = 0; j < script.subPoints[i].list.Count; j++)
if (!duplicates.Contains(script.subPoints[i].list[j]))
selected.Add(script.subPoints[i].list[j]);
//delete this submesh entry
script.subPoints.RemoveAt(i);
}
}
//recalculate triangles for complete mesh
List<int> triangles = new List<int>();
for (int i = 0; i < script.subPoints.Count; i++)
triangles.AddRange(script.RecalculateTriangles(script.subPoints[i].list));
//assign triangles and update vertices
filter.sharedMesh.triangles = triangles.ToArray();
script.list = vertices;
script.UpdateMesh(ConvertAllPoints());
//recursively delete the remaining obsolete indices
//which were found by looking through all submeshes
if (selected.Count > 0)
{
DeleteSelected();
return;
}
//deletion done - optimize mesh
OptimizeMesh(filter.sharedMesh);
}
//rebuild mesh properties
private void OptimizeMesh(Mesh mesh)
{
mesh.RecalculateNormals();
mesh.RecalculateBounds();
;
}
//draw selection and move handles
private void DrawSelectedHandles()
{
//don't draw anything without mesh
if (!script.GetComponent<MeshFilter>().sharedMesh)
return;
Handles.color = new Color(1, 0, 0, 0.2f);
//get current vertex count of the active submesh
int currentCount = script.current.Count;
//draw a solid disc at the lastest position we clicked in edit mode
if (placing && currentCount > 0 && (!script.autoSplit
|| (script.autoSplit && currentCount != script.splitAt)))
{
Vector3 pos = allPoints[script.current[currentCount - 1]];
Handles.DrawSolidDisc(pos, Vector3.up,
HandleUtility.GetHandleSize(pos) * 0.1f);
}
//don't continue without selected vertices
if (selected.Count == 0)
return;
//draw a solid disc for each vertex we selected
Vector3[] selectedHandles = new Vector3[selected.Count];
for (int i = 0; i < selected.Count; i++)
{
Vector3 pos = allPoints[selected[i]];
selectedHandles[i] = pos;
Handles.DrawSolidDisc(pos, Vector3.up,
HandleUtility.GetHandleSize(pos) * 0.1f);
}
//get the center position of selected vertices and draw a handle there
Vector3 center = GetCenterOfVector3(selectedHandles);
Vector3 handle = Handles.PositionHandle(center, Quaternion.identity);
Vector3 diff = handle - center;
//if the handle moved
if (diff != Vector3.zero)
{
Undo.RegisterCompleteObjectUndo(script, "Handle Moved");
Mesh myMesh = script.gameObject.GetComponent<MeshFilter>().sharedMesh;
if (myMesh) Undo.RecordObject(myMesh, "Handle Moved");
//adjust corresponding mesh vertices
for (int i = 0; i < selected.Count; i++)
script.list[selected[i]] += diff;
//update mesh with new vertex positions
script.UpdateMesh(ConvertAllPoints());
}
}
//draws submesh outlines
private void DrawPolygonOutline()
{
List<NavMeshObject.SubPoints> sub = script.subPoints;
Handles.color = Color.yellow;
//for each submesh
for (int i = 0; i < sub.Count; i++)
{
//get actual vertex positions from indices
Vector3[] points = new Vector3[sub[i].list.Count];
for (int j = 0; j < points.Length; j++)
points[j] = allPoints[sub[i].list[j]];
//draw polyline with these points
Handles.DrawPolyLine(points);
//connect the first and last point to a loop
if (points.Length >= 2)
{
Vector3[] p = { points[0], points[points.Length - 1] };
Handles.DrawPolyLine((p));
}
}
Handles.color = Color.red;
//draw a circle at each vertex position
for (int i = 0; i < allPoints.Length; i++)
{
Handles.CircleCap(0, allPoints[i],
Quaternion.LookRotation(Vector3.up, Vector3.forward),
HandleUtility.GetHandleSize(allPoints[i]) * 0.1f);
}
}
//returns the center point of an array of Vector3s
private Vector3 GetCenterOfVector3(Vector3[] points)
{
Vector3 sum = Vector3.zero;
if (points.Length == 0) return sum;
for (int i = 0; i < points.Length; i++)
sum += points[i];
return sum / points.Length;
}
}
}