using MTE.Undo; using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace MTE { internal class MeshToolbox : IEditor { public int Id { get; } = 9; public bool Enabled { get; set; } = true; public string Name { get; } = "MeshToolbox"; public Texture Icon { get; } = EditorGUIUtility.IconContent("Mesh Icon").image; public bool WantMouseMove { get; } = false; public bool WillEditMesh { get; } = true; private enum SelectMode { Vertex, Quad, } private enum EditorState { Selecting, Action, } /// /// AABB that can contain only one point /// private class AABB { float xMin = float.NaN, yMin, zMin; float xMax, yMax, zMax; public Vector3 Min { get { return new Vector3(xMin, yMin, zMin); } } public Vector3 Max { get { return new Vector3(xMax, yMax, zMax); } } public Vector3 Center { get { return 0.5f*(Min + Max); } } public bool IsEmpty { get { return float.IsNaN(xMin); } } /// Reset to initial state (empty) public void Reset() { xMin = float.NaN; } public void AddPoint(Vector3 point) { if (IsEmpty) { this.xMin = this.xMax = point.x; this.yMin = this.yMax = point.y; this.zMin = this.zMax = point.z; } if (point.x > xMax) { this.xMax = point.x; } else if(point.x < xMin) { this.xMin = point.x; } if (point.y > yMax) { this.yMax = point.y; } else if (point.y < yMin) { this.yMin = point.y; } if (point.z > zMax) { this.zMax = point.z; } else if (point.z < zMin) { this.zMin = point.z; } } } private class VertexModifyGroup { private readonly GameObject gameObject; private List vertexIndexList; public GameObject Target { get { return gameObject; } } public List VertexIndexList { get { return vertexIndexList;} } public VertexModifyGroup(GameObject gameObject) { this.gameObject = gameObject; } public void AppendVertices(IList vertexIndexList) { if (this.vertexIndexList == null) { this.vertexIndexList = new List(); } this.vertexIndexList.AddRange(vertexIndexList); this.vertexIndexList = this.vertexIndexList.Distinct().ToList(); } public void RemoveVertices(IList remove_vertexIndexList) { if (this.vertexIndexList == null) { return; } for (var i = this.VertexIndexList.Count - 1; i >= 0; i--) { var vertexIndex = this.VertexIndexList[i]; var foundIndex = remove_vertexIndexList.IndexOf(vertexIndex); if (foundIndex >= 0) { this.VertexIndexList.RemoveAt(i); remove_vertexIndexList.RemoveAt(foundIndex); } } } public void Clear() { this.VertexIndexList.Clear(); } } #region modfication private readonly List vertexModifyGroups = new List(4); private readonly AABB aabb = new AABB(); private void AddVerticesToModifyGroup(GameObject gameObject, IList vertexIndexList) { if (gameObject == null) { throw new ArgumentNullException("gameObject"); } if (vertexIndexList == null) { throw new ArgumentNullException("vertexIndexList"); } if (vertexIndexList.Count == 0) { return; } { var modifyGroup = vertexModifyGroups.Find(group => group.Target == gameObject); if (modifyGroup == null) { modifyGroup = new VertexModifyGroup(gameObject); vertexModifyGroups.Add(modifyGroup); } modifyGroup.AppendVertices(vertexIndexList); } } private void RemoveVerticesFromModifyGroup(GameObject gameObject, IList vertexIndexList) { if (gameObject == null) { throw new ArgumentNullException("gameObject"); } if (vertexIndexList == null) { throw new ArgumentNullException("vertexIndexList"); } if (vertexIndexList.Count == 0) { return; } { var modifyGroup = vertexModifyGroups.Find(group => group.Target == gameObject); if (modifyGroup == null) { modifyGroup = new VertexModifyGroup(gameObject); vertexModifyGroups.Add(modifyGroup); } modifyGroup.RemoveVertices(vertexIndexList); } } private void RefreshAABB() { aabb.Reset(); for (var i = 0; i < vertexModifyGroups.Count; i++) { var modifyGroup = vertexModifyGroups[i]; var gameObject = modifyGroup.Target; if (!gameObject) { continue; } var transform = gameObject.transform; var meshFilter = gameObject.GetComponent(); if (!meshFilter) { continue; } var mesh = meshFilter.sharedMesh; if (!mesh) { continue; } var meshVertices = mesh.vertices; for (int j = 0; j < modifyGroup.VertexIndexList.Count; j++) { var vertexIndex = modifyGroup.VertexIndexList[j]; if (vertexIndex >= meshVertices.Length) { MTEDebug.LogError( $"Mesh {mesh.name}'s vertices on GameObject has been changed."); continue; } var vertexPosition = meshVertices[vertexIndex]; var pWorld = transform.TransformPoint(vertexPosition); aabb.AddPoint(pWorld); } } } #endregion #region Parameters #region Constant // default const float DefaultBrushSize = 4; // min/max const float MinBrushSize = 0.1f; const float MaxBrushSize = 10f; #endregion private float brushSize; /// /// Brush size (unit: meter) /// public float BrushSize { get { return brushSize; } set { value = Mathf.Clamp(value, MinBrushSize, MaxBrushSize); if (!MathEx.AmostEqual(value, brushSize)) { brushSize = value; EditorPrefs.SetFloat("MTE_MeshToolbox.brushSize", value); } } } private float BrushSizeInU3D => BrushSize * Settings.BrushUnit; #endregion public MeshToolbox() { MTEContext.EnableEvent += (sender, args) => { if (MTEContext.editor == this) { LoadSavedParamter(); } }; MTEContext.EditTypeChangedEvent += (sender, args) => { if (MTEContext.editor == this) { LoadSavedParamter(); } }; brushSize = DefaultBrushSize; } public HashSet DefineHotkeys() { return new HashSet { new Hotkey(this, KeyCode.LeftBracket, () => { BrushSize -= 1; MTEEditorWindow.Instance.Repaint(); }), new Hotkey(this, KeyCode.RightBracket, () => { BrushSize += 1; MTEEditorWindow.Instance.Repaint(); }) }; } private void LoadSavedParamter() { // Load parameters from the EditorPrefs brushSize = EditorPrefs.GetFloat("MTE_MeshToolbox.brushSize", DefaultBrushSize); } public string Header{ get { return StringTable.Get(C.MeshMisc_Header); } } public string Description { get { return StringTable.Get(C.MeshMisc_Description); } } private static class Styles { private static bool unloaded= true; public static void Init() { if (!unloaded) return; //nothing for now unloaded = false; } } private bool pressedHotkeyOnEditor; public void DoArgsGUI() { Styles.Init(); if (!Settings.CompactGUI) { GUILayout.Label(StringTable.Get(C.Settings), MTEStyles.SubHeader); } BrushSize = EditorGUILayoutEx.Slider(StringTable.Get(C.Size), "-", "+", BrushSize, MinBrushSize, MaxBrushSize); if (!Settings.CompactGUI) { EditorGUILayout.Space(); GUILayout.Label(StringTable.Get(C.Tools), MTEStyles.SubHeader); } mode = (SelectMode)GUILayout.Toolbar((int)mode, MTEStyles.MeshSelectModeContents, Settings.CompactGUI ? GUILayout.Height(32) : GUILayout.Height(48)); if (mode == SelectMode.Vertex) { EditorGUILayout.BeginHorizontal(); { GUI.enabled = CanDelete(); if (GUILayout.Button(StringTable.Get(C.Delete), GUILayout.Width(100), GUILayout.Height(40))) { Delete(); } GUI.enabled = true; GUILayout.Space(20); EditorGUILayout.LabelField(StringTable.Get(C.Info_ToolDescription_DeleteVertices), MTEStyles.labelFieldWordwrap); } EditorGUILayout.EndHorizontal(); } else if (mode == SelectMode.Quad) { EditorGUILayout.HelpBox(StringTable.Get(C.Info_SwapQuadSplitDirection), MessageType.Info); if (IsPressedHotkeyForQuadMode(Event.current)) { pressedHotkeyOnEditor = true; } } } private EditorState state = EditorState.Selecting; private SelectMode mode = SelectMode.Vertex; //debug only data private static Vector3 pTest; private static Vector3 pMaxEdgePoint0; private static Vector3 pMaxEdgePoint1; //debug only data public void OnSceneGUI() { var e = Event.current; if (e.commandName == "UndoRedoPerformed") { SceneView.RepaintAll(); return; } if (mode == SelectMode.Vertex) { OnSceneGUIForVertex(); } else if(mode == SelectMode.Quad) { OnSceneGUIForQuad(); if (Settings.DebugMode) { var oldHandlesColor = Handles.color; Handles.color = Color.red; var size = Utility.GetHandleSize(pTest); Handles.DotHandleCap(0, pTest, Quaternion.identity, size * Settings.PointSize, EventType.Repaint); Handles.DrawLine(pMaxEdgePoint0, pMaxEdgePoint1); if (e.type == EventType.MouseDown && e.button == 1) { pMaxEdgePoint1 = pMaxEdgePoint0 = pTest = new Vector3(-9999, -9999, -9999); } Handles.color = oldHandlesColor; } } } private void OnSceneGUIForQuad() { var e = Event.current; if (!(EditorWindow.mouseOverWindow is SceneView)) { return; } if (e.control || e.alt) return; HandleUtility.AddDefaultControl(0); RaycastHit raycastHit; Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); if (Physics.Raycast(ray, out raycastHit, Mathf.Infinity, 1 << MTEContext.TargetLayer //only hit target layer )) { //check tag if (!raycastHit.transform.CompareTag(MTEContext.TargetTag)) { return; } var transform = raycastHit.transform; var target = transform.gameObject; var meshFilter = target.GetComponent(); if (meshFilter == null) { return; } var mesh = meshFilter.sharedMesh; if (mesh == null) { return; } var triangleIndex0 = raycastHit.triangleIndex * 3 + 0; var triangleIndex1 = raycastHit.triangleIndex * 3 + 1; var triangleIndex2 = raycastHit.triangleIndex * 3 + 2; // highlight hovered quad if (QuadMap.FindQuad(target, triangleIndex0, triangleIndex1, triangleIndex2, out Quad quad)) { Handles.color = Settings.FlashAffectedVertex ? Utility.GetFlashingColor(0.583f) : Color.blue; var meshVertices = mesh.vertices; int index0 = quad.index[0]; int index1 = quad.index[1]; int index2 = quad.index[2]; var p0 = transform.TransformPoint(meshVertices[index0]); var p1 = transform.TransformPoint(meshVertices[index1]); var p2 = transform.TransformPoint(meshVertices[index2]); int index3 = quad.index[3]; int index4 = quad.index[4]; int index5 = quad.index[5]; var p3 = transform.TransformPoint(meshVertices[index3]); var p4 = transform.TransformPoint(meshVertices[index4]); var p5 = transform.TransformPoint(meshVertices[index5]); var oldHandlesColor = Handles.color; if (Settings.DebugMode) { Handles.color = Color.green; Handles.DrawPolyLine(p0, p1, p2, p0); Handles.color = Color.blue; Handles.DrawPolyLine(p3, p4, p5, p3); } else { Handles.color = Utility.GetFlashingColor(); //Handles.DrawAAPolyLine(3, 4, p0, p1, p2, p0); //Handles.DrawAAPolyLine(3, 4, p3, p4, p5, p3); Handles.DrawPolyLine(p0, p1, p2, p0); Handles.DrawPolyLine(p3, p4, p5, p3); } Handles.color = oldHandlesColor; } bool pressed = IsPressedHotkeyForQuadMode(e) || pressedHotkeyOnEditor; if (pressed) { pressedHotkeyOnEditor = false; ChangeQuadSplitDirection(target, triangleIndex0, triangleIndex1, triangleIndex2); var meshCollider = target.GetComponent(); MTEEditorWindow.Instance.UpdateMeshColliderImmediately(meshCollider); MTEEditorWindow.Instance.HandleMeshSave(); } } SceneView.RepaintAll(); } private bool IsPressedHotkeyForQuadMode(Event e) { return e.isKey && e.type == EventType.KeyUp && e.keyCode == KeyCode.S; } private void ChangeQuadSplitDirection(GameObject target, int index0, int index1, int index2) { if (!QuadMap.FindQuad(target, index0, index1, index2, out Quad quad)) { return; } Quad newQuad = quad; newQuad.ChangeSplitDirection(); int firstIndex = -1; if (index0 % 6 == 0) { firstIndex = index0; } else if (index0 % 6 == 3) { firstIndex = index0 - 3; } else { throw new MTEEditException("Argument index0 is not a valid first index of triangle"); } Utility.RecordMeshIndices("Swap Quad Split Direction", target, () => { QuadMap.Rebuild(target); }); QuadMap.ModifyQuad(target, firstIndex, newQuad); } private void OnSceneGUIForVertex() { var e = Event.current; // show modifying points if (vertexModifyGroups.Count != 0) { Handles.color = Settings.FlashAffectedVertex ? Utility.GetFlashingColor(0.583f) : Color.blue; for (var i = 0; i < vertexModifyGroups.Count; i++) { var modifyGroup = vertexModifyGroups[i]; if (modifyGroup.VertexIndexList == null || modifyGroup.VertexIndexList.Count == 0) continue; if (!modifyGroup.Target) continue; var meshFilter = modifyGroup.Target.GetComponent(); if (meshFilter == null) continue; var mesh = meshFilter.sharedMesh; if (mesh == null) continue; var meshVertices = mesh.vertices; var transform = modifyGroup.Target.transform; for (int j = 0; j < modifyGroup.VertexIndexList.Count; j++) { var vertexIndex = modifyGroup.VertexIndexList[j]; var vertexPosition = meshVertices[vertexIndex]; var p = transform.TransformPoint(vertexPosition); var size = Utility.GetHandleSize(p); Handles.DotHandleCap(0, p, Quaternion.identity, size * Settings.PointSize, EventType.Repaint); } } } if (!(EditorWindow.mouseOverWindow is SceneView)) { return; } if (e.button != 0 || e.control || e.alt) return; if (state == EditorState.Selecting) { HandleUtility.AddDefaultControl(0); RaycastHit raycastHit; Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); //Debug.Log(string.Format("mouse at ({0}, {1})", e.mousePosition.x, e.mousePosition.y)); if (Physics.Raycast(ray, out raycastHit, Mathf.Infinity, 1 << MTEContext.TargetLayer //only hit target layer )) { //check tag if (!raycastHit.transform.CompareTag(MTEContext.TargetTag)) { return; } if (Settings.ShowBrushRect) { Utility.ShowBrushRect(raycastHit.point, BrushSizeInU3D); } if (e.isKey && e.keyCode == KeyCode.Return && vertexModifyGroups.Count != 0) { RefreshAABB(); state = EditorState.Action; return; } // collect modify group foreach (var gameObject in MTEContext.Targets) { if (!gameObject) { continue; } var meshFilter = gameObject.GetComponent(); var mesh = meshFilter.sharedMesh; var hitPointLocal = gameObject.transform.InverseTransformPoint(raycastHit.point); List vIndex; List vDistance; VertexMap.GetAffectedVertex(gameObject, hitPointLocal, this.BrushSizeInU3D, out vIndex, out vDistance); if (Settings.ShowAffectedVertex) { Utility.ShowAffectedVertices(gameObject, mesh, vIndex); } if (e.type == EventType.MouseDown || e.type == EventType.MouseDrag) { if (vIndex.Count != 0) { if (!e.shift) { AddVerticesToModifyGroup(gameObject, vIndex); } else { RemoveVerticesFromModifyGroup(gameObject, vIndex); } } } } } } if (state == EditorState.Action) { if (e.isKey && e.keyCode == KeyCode.Escape) { state = EditorState.Selecting; return; } //record undo operation for targets that to be modified if (e.type == EventType.MouseDown) { Utility.Record("Mesh Toolbox: move", Vector3.zero, this.BrushSizeInU3D, () => { RefreshAABB(); for (var i = 0; i < vertexModifyGroups.Count; i++) { var modifyGroup = vertexModifyGroups[i]; if (!modifyGroup.Target) { continue; } VertexMap.Rebuild(modifyGroup.Target); } }); } if (e.type == EventType.MouseUp) { MTEEditorWindow.Instance.UpdateDirtyMeshCollidersImmediately(); MTEEditorWindow.Instance.HandleMeshSave(); } // execute the modification if (vertexModifyGroups.Count != 0) { var newPostion = Handles.DoPositionHandle(this.aabb.Center, Quaternion.identity); var offset = newPostion - this.aabb.Center; TranslateVertices(offset); } } SceneView.RepaintAll(); } private void TranslateVertices(Vector3 offset) { // check if offset is big enough if (!(Mathf.Abs(offset.x) > 0.001f) && !(Mathf.Abs(offset.y) > 0.001f) && !(Mathf.Abs(offset.z) > 0.001f)) return; for (var i = 0; i < vertexModifyGroups.Count; i++) { var modifyGroup = vertexModifyGroups[i]; if (modifyGroup.VertexIndexList == null || modifyGroup.VertexIndexList.Count == 0 || !modifyGroup.Target) { continue; } var gameObject = modifyGroup.Target; var transform = gameObject.transform; var offsetLocal = transform.InverseTransformDirection(offset); var mesh = gameObject.GetComponent().sharedMesh; var meshCollider = modifyGroup.Target.GetComponent(); var vertices = mesh.vertices;//TODO performance improvement: mesh.vertices will copy vertices for (int j = 0; j < modifyGroup.VertexIndexList.Count; j++) { var index = modifyGroup.VertexIndexList[j]; vertices[index] += offsetLocal; } mesh.vertices = vertices; MTEEditorWindow.Instance.SetMeshDirty(gameObject); MTEEditorWindow.Instance.SetMeshColliderDirty(meshCollider, mesh.vertexCount); // Rebuild vertex map for this GameObject if its vertices have been translated in xOz. if (Math.Abs(offset.x) > 0.001 || Math.Abs(offset.z) > 0.001) { VertexMap.Rebuild(gameObject); } } RefreshAABB(); } bool CanDelete() { if (vertexModifyGroups.Count == 0) { return false; } for (var i = 0; i < vertexModifyGroups.Count; i++) { var modifyGroup = vertexModifyGroups[i]; if (modifyGroup.VertexIndexList.Count != 0) { return true; } } return false; } void Delete() { using (new Undo.UndoTransaction("Mesh Toolbox: delete")) { for (var i = 0; i < vertexModifyGroups.Count; i++) { var modifyGroup = vertexModifyGroups[i]; var gameObject = modifyGroup.Target; if (!gameObject) { continue; } if (gameObject.GetComponent() != null)//ignore vertex colored GameObjects { continue; } var mesh = gameObject.GetComponent().sharedMesh; var meshCollider = modifyGroup.Target.GetComponent(); var meshTriangles = mesh.triangles; var indexList = modifyGroup.VertexIndexList; // create new indexes List newMeshTriangles = new List(meshTriangles); // remove triangles that contains the removed vertices for (var j = newMeshTriangles.Count - 1; j >= 0; j -= 3) { var index0 = newMeshTriangles[j]; var index1 = newMeshTriangles[j - 1]; var index2 = newMeshTriangles[j - 2]; if (indexList.Contains(index0) || indexList.Contains(index1) || indexList.Contains(index2)) { newMeshTriangles.RemoveAt(j); newMeshTriangles.RemoveAt(j - 1); newMeshTriangles.RemoveAt(j - 2); } } var newTriangles = newMeshTriangles.ToArray(); // record undo operation for targets that to be modified var newIndices = mesh.triangles; UndoRedoManager.Instance().Push(a => { mesh.ModifyIndices("Mesh Toolbox: delete", meshCollider, a, () => { VertexMap.Rebuild(gameObject); }); meshCollider.enabled = false; meshCollider.enabled = true; }, newIndices, "Mesh Toolbox: delete"); // assign mesh.triangles = newTriangles; MTEEditorWindow.Instance.SetMeshDirty(gameObject); // rebuild vertex map VertexMap.Rebuild(gameObject); modifyGroup.Clear(); } } } } }