diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs new file mode 100644 index 0000000..52880dc --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs @@ -0,0 +1,516 @@ +using UnityEngine; +using UnityEditor; +using Pathfinding.Graphs.Navmesh; +using UnityEditorInternal; + +namespace Pathfinding { + /// <summary>Editor for the RecastGraph.</summary> + [CustomGraphEditor(typeof(RecastGraph), "Recast Graph")] + public class RecastGraphEditor : GraphEditor { + public static bool tagMaskFoldout; + public static bool meshesUnreadableAtRuntimeFoldout; + ReorderableList tagMaskList; + ReorderableList perLayerModificationsList; + + public enum UseTiles { + UseTiles = 0, + DontUseTiles = 1 + } + + static readonly GUIContent[] DimensionModeLabels = new [] { + new GUIContent("2D"), + new GUIContent("3D"), + }; + + static Rect SliceColumn (ref Rect rect, float width, float spacing = 0) { + return GUIUtilityx.SliceColumn(ref rect, width, spacing); + } + + static void DrawIndentedList (ReorderableList list) { + GUILayout.BeginHorizontal(); + GUILayout.Space(EditorGUI.IndentedRect(default).xMin); + list.DoLayoutList(); + GUILayout.Space(3); + GUILayout.EndHorizontal(); + } + + static void DrawColliderDetail (RecastGraph.CollectionSettings settings) { + const float LowestApproximationError = 0.5f; + settings.colliderRasterizeDetail = EditorGUILayout.Slider(new GUIContent("Round Collider Detail", "Controls the detail of the generated sphere and capsule meshes. "+ + "Higher values may increase navmesh quality slightly, and lower values improve graph scanning performance."), Mathf.Round(10*settings.colliderRasterizeDetail)*0.1f, 0, 1.0f / LowestApproximationError); + } + + void DrawCollectionSettings (RecastGraph.CollectionSettings settings, RecastGraph.DimensionMode dimensionMode) { + settings.collectionMode = (RecastGraph.CollectionSettings.FilterMode)EditorGUILayout.EnumPopup("Filter objects by", settings.collectionMode); + + if (settings.collectionMode == RecastGraph.CollectionSettings.FilterMode.Layers) { + settings.layerMask = EditorGUILayoutx.LayerMaskField("Layer Mask", settings.layerMask); + } else { + DrawIndentedList(tagMaskList); + } + + if (dimensionMode == RecastGraph.DimensionMode.Dimension3D) { + settings.rasterizeTerrain = EditorGUILayout.Toggle(new GUIContent("Rasterize Terrains", "Should a rasterized terrain be included"), settings.rasterizeTerrain); + if (settings.rasterizeTerrain) { + EditorGUI.indentLevel++; + settings.rasterizeTrees = EditorGUILayout.Toggle(new GUIContent("Rasterize Trees", "Rasterize tree colliders on terrains. " + + "If the tree prefab has a collider, that collider will be rasterized. " + + "Otherwise a simple box collider will be used and the script will " + + "try to adjust it to the tree's scale, it might not do a very good job though so " + + "an attached collider is preferable."), settings.rasterizeTrees); + settings.terrainHeightmapDownsamplingFactor = EditorGUILayout.IntField(new GUIContent("Heightmap Downsampling", "How much to downsample the terrain's heightmap. A lower value is better, but slower to scan"), settings.terrainHeightmapDownsamplingFactor); + settings.terrainHeightmapDownsamplingFactor = Mathf.Max(1, settings.terrainHeightmapDownsamplingFactor); + EditorGUI.indentLevel--; + } + + settings.rasterizeMeshes = EditorGUILayout.Toggle(new GUIContent("Rasterize Meshes", "Should meshes be rasterized and used for building the navmesh"), settings.rasterizeMeshes); + settings.rasterizeColliders = EditorGUILayout.Toggle(new GUIContent("Rasterize Colliders", "Should colliders be rasterized and used for building the navmesh"), settings.rasterizeColliders); + } else { + // Colliders are always rasterized in 2D mode + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.Toggle(new GUIContent("Rasterize Colliders", "Should colliders be rasterized and used for building the navmesh. In 2D mode, this is always enabled."), true); + EditorGUI.EndDisabledGroup(); + } + + if (settings.rasterizeMeshes && settings.rasterizeColliders && dimensionMode == RecastGraph.DimensionMode.Dimension3D) { + EditorGUILayout.HelpBox("You are rasterizing both meshes and colliders. This is likely just duplicate work if the colliders and meshes are similar in shape. You can use the RecastMeshObj component" + + " to always include some specific objects regardless of what the above settings are set to.", MessageType.Info); + } + } + + public override void OnEnable () { + base.OnEnable(); + var graph = target as RecastGraph; + tagMaskList = new ReorderableList(graph.collectionSettings.tagMask, typeof(string), true, true, true, true) { + drawElementCallback = (Rect rect, int index, bool active, bool isFocused) => { + graph.collectionSettings.tagMask[index] = EditorGUI.TagField(rect, graph.collectionSettings.tagMask[index]); + }, + drawHeaderCallback = (Rect rect) => { + GUI.Label(rect, "Tag mask"); + }, + elementHeight = EditorGUIUtility.singleLineHeight, + onAddCallback = (ReorderableList list) => { + graph.collectionSettings.tagMask.Add("Untagged"); + } + }; + + perLayerModificationsList = new ReorderableList(graph.perLayerModifications, typeof(RecastGraph.PerLayerModification), true, true, true, true) { + drawElementCallback = (Rect rect, int index, bool active, bool isFocused) => { + var element = graph.perLayerModifications[index]; + var w = rect.width; + var spacing = EditorGUIUtility.standardVerticalSpacing; + element.layer = EditorGUI.LayerField(SliceColumn(ref rect, w * 0.3f, spacing), element.layer); + + if (element.mode == RecastMeshObj.Mode.WalkableSurfaceWithTag) { + element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(SliceColumn(ref rect, w * 0.4f, spacing), element.mode); + element.surfaceID = Util.EditorGUILayoutHelper.TagField(rect, GUIContent.none, element.surfaceID, AstarPathEditor.EditTags); + element.surfaceID = Mathf.Clamp(element.surfaceID, 0, GraphNode.MaxTagIndex); + } else if (element.mode == RecastMeshObj.Mode.WalkableSurfaceWithSeam) { + element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(SliceColumn(ref rect, w * 0.4f, spacing), element.mode); + string helpTooltip = "All surfaces on this mesh will be walkable and a " + + "seam will be created between the surfaces on this mesh and the surfaces on other meshes (with a different surface id)"; + GUI.Label(SliceColumn(ref rect, 70, spacing), new GUIContent("Surface ID", helpTooltip)); + element.surfaceID = Mathf.Max(0, EditorGUI.IntField(rect, new GUIContent("", helpTooltip), element.surfaceID)); + } else { + element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(rect, element.mode); + } + + graph.perLayerModifications[index] = element; + }, + drawHeaderCallback = (Rect rect) => { + GUI.Label(rect, "Per Layer Modifications"); + }, + elementHeight = EditorGUIUtility.singleLineHeight, + onAddCallback = (ReorderableList list) => { + // Find the first layer that is not already modified + var availableLayers = graph.collectionSettings.layerMask; + foreach (var mod in graph.perLayerModifications) { + availableLayers &= ~(1 << mod.layer); + } + var newMod = RecastGraph.PerLayerModification.Default; + for (int i = 0; i < 32; i++) { + if ((availableLayers & (1 << i)) != 0) { + newMod.layer = i; + break; + } + } + graph.perLayerModifications.Add(newMod); + } + }; + } + + public override void OnInspectorGUI (NavGraph target) { + var graph = target as RecastGraph; + + Header("Shape"); + + graph.dimensionMode = (RecastGraph.DimensionMode)EditorGUILayout.Popup(new GUIContent("Dimensions", "Should the graph be for a 2D or 3D world?"), (int)graph.dimensionMode, DimensionModeLabels); + if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D && Mathf.Abs(Vector3.Dot(Quaternion.Euler(graph.rotation) * Vector3.up, Vector3.forward)) < 0.99999f) { + EditorGUI.indentLevel++; + EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); + GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon"), GUILayout.ExpandWidth(false)); + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + GUILayout.BeginHorizontal(); + GUILayout.Label("Your graph is not in the XY plane"); + if (GUILayout.Button("Align")) { + graph.rotation = new Vector3(-90, 0, 0); + graph.forcedBoundsCenter = new Vector3(graph.forcedBoundsCenter.x, graph.forcedBoundsCenter.y, -graph.forcedBoundsSize.y * 0.5f); + } + GUILayout.EndHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + EditorGUI.indentLevel--; + } + + // In 3D mode, we use the graph's center as the pivot point, but in 2D mode, we use the center of the base plane of the graph as the pivot point. + // This makes sense because in 2D mode, you typically want to set the base plane's center to Z=0, and you don't care much about the height of the graph. + var pivot = graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D ? new Vector3(0.0f, -0.5f, 0.0f) : Vector3.zero; + var centerOffset = Quaternion.Euler(graph.rotation) * Vector3.Scale(graph.forcedBoundsSize, pivot); + var newCenter = EditorGUILayout.Vector3Field("Center", graph.forcedBoundsCenter + centerOffset); + var newSize = EditorGUILayout.Vector3Field("Size", graph.forcedBoundsSize); + + // Make sure the bounding box is not infinitely thin along any axis + newSize = Vector3.Max(newSize, Vector3.one * 0.001f); + + // Recalculate the center offset with the new size, and then adjust the center so that the pivot point stays the same if the size changes + centerOffset = Quaternion.Euler(graph.rotation) * Vector3.Scale(newSize, pivot); + graph.forcedBoundsCenter = RoundVector3(newCenter) - centerOffset; + graph.forcedBoundsSize = RoundVector3(newSize); + + graph.rotation = RoundVector3(EditorGUILayout.Vector3Field("Rotation", graph.rotation)); + + long estWidth = Mathf.RoundToInt(Mathf.Ceil(graph.forcedBoundsSize.x / graph.cellSize)); + long estDepth = Mathf.RoundToInt(Mathf.Ceil(graph.forcedBoundsSize.z / graph.cellSize)); + + EditorGUI.BeginDisabledGroup(true); + var estTilesX = (estWidth + graph.editorTileSize - 1) / graph.editorTileSize; + var estTilesZ = (estDepth + graph.editorTileSize - 1) / graph.editorTileSize; + var label = estWidth.ToString() + " x " + estDepth.ToString(); + if (graph.useTiles) { + label += " voxels, divided into " + (estTilesX*estTilesZ) + " tiles"; + } + EditorGUILayout.LabelField(new GUIContent("Size", "Based on the voxel size and the bounding box"), new GUIContent(label)); + EditorGUI.EndDisabledGroup(); + + // Show a warning if the number of voxels is too large + if (estWidth*estDepth >= 3000*3000) { + GUIStyle helpBox = GUI.skin.FindStyle("HelpBox") ?? GUI.skin.FindStyle("Box"); + + Color preColor = GUI.color; + if (estWidth*estDepth >= 8192*8192) { + GUI.color = Color.red; + } else { + GUI.color = Color.yellow; + } + + GUILayout.Label("Warning: Might take some time to calculate", helpBox); + GUI.color = preColor; + } + + if (!editor.isPrefab) { + if (GUILayout.Button(new GUIContent("Snap bounds to scene", "Will snap the bounds of the graph to exactly contain all meshes in the scene that matches the masks."))) { + graph.SnapForceBoundsToScene(); + GUI.changed = true; + } + } + + Separator(); + Header("Input Filtering"); + + DrawCollectionSettings(graph.collectionSettings, graph.dimensionMode); + + Separator(); + Header("Rasterization"); + + graph.cellSize = EditorGUILayout.FloatField(new GUIContent("Voxel Size", "Size of one voxel in world units"), graph.cellSize); + if (graph.cellSize < 0.001F) graph.cellSize = 0.001F; + + graph.useTiles = (UseTiles)EditorGUILayout.EnumPopup("Use Tiles", graph.useTiles ? UseTiles.UseTiles : UseTiles.DontUseTiles) == UseTiles.UseTiles; + + if (graph.useTiles) { + EditorGUI.indentLevel++; + graph.editorTileSize = EditorGUILayout.IntField(new GUIContent("Tile Size (voxels)", "Size in voxels of a single tile.\n" + + "This is the width of the tile.\n" + + "\n" + + "A large tile size can be faster to initially scan (but beware of out of memory issues if you try with a too large tile size in a large world)\n" + + "smaller tile sizes are (much) faster to update.\n" + + "\n" + + "Different tile sizes can affect the quality of paths. It is often good to split up huge open areas into several tiles for\n" + + "better quality paths, but too small tiles can lead to effects looking like invisible obstacles.\n\n" + + "Typical values are between 64 and 256"), graph.editorTileSize); + graph.editorTileSize = Mathf.Max(10, graph.editorTileSize); + EditorGUI.indentLevel--; + } + + if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D) { + graph.walkableHeight = EditorGUILayout.DelayedFloatField(new GUIContent("Character Height", "Minimum distance to the roof for an area to be walkable"), graph.walkableHeight); + graph.walkableHeight = Mathf.Max(graph.walkableHeight, 0); + + graph.characterRadius = EditorGUILayout.FloatField(new GUIContent("Character Radius", "Radius of the character. It's good to add some margin.\nIn world units."), graph.characterRadius); + graph.characterRadius = Mathf.Max(graph.characterRadius, 0); + + if (graph.characterRadius < graph.cellSize * 2) { + EditorGUILayout.HelpBox("For best navmesh quality, it is recommended to keep the character radius at least 2 times as large as the voxel size. Smaller voxels will give you higher quality navmeshes, but it will take more time to scan the graph.", MessageType.Warning); + } + + graph.walkableClimb = EditorGUILayout.FloatField(new GUIContent("Max Step Height", "How high can the character step"), graph.walkableClimb); + + // A walkableClimb higher than this can cause issues when generating the navmesh since then it can in some cases + // Both be valid for a character to walk under an obstacle and climb up on top of it (and that cannot be handled with a navmesh without links) + if (graph.walkableClimb >= graph.walkableHeight) { + graph.walkableClimb = graph.walkableHeight; + EditorGUILayout.HelpBox("Max Step Height should be less than Character Height. Clamping to " + graph.walkableHeight+".", MessageType.Warning); + } else if (graph.walkableClimb < 0) { + graph.walkableClimb = 0; + } + } + + if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D) { + graph.maxSlope = EditorGUILayout.Slider(new GUIContent("Max Slope", "Approximate maximum slope"), graph.maxSlope, 0F, 90F); + } + + graph.maxEdgeLength = EditorGUILayout.FloatField(new GUIContent("Max Border Edge Length", "Maximum length of one border edge in the completed navmesh before it is split. A lower value can often yield better quality graphs, but don't use so low values so that you get a lot of thin triangles."), graph.maxEdgeLength); + graph.maxEdgeLength = graph.maxEdgeLength < graph.cellSize ? graph.cellSize : graph.maxEdgeLength; + + // This is actually a float, but to make things easier for the user, we only allow picking integers. Small changes don't matter that much anyway. + graph.contourMaxError = EditorGUILayout.IntSlider(new GUIContent("Edge Simplification", "Simplifies the edges of the navmesh such that it is no more than this number of voxels away from the true value.\nIn voxels."), Mathf.RoundToInt(graph.contourMaxError), 0, 5); + graph.minRegionSize = EditorGUILayout.FloatField(new GUIContent("Min Region Size", "Small regions will be removed. In voxels"), graph.minRegionSize); + + if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D) { + graph.backgroundTraversability = (RecastGraph.BackgroundTraversability)EditorGUILayout.EnumPopup("Background traversability", graph.backgroundTraversability); + } + + if (graph.collectionSettings.rasterizeColliders || (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D && graph.collectionSettings.rasterizeTerrain && graph.collectionSettings.rasterizeTrees)) { + DrawColliderDetail(graph.collectionSettings); + } + + DrawIndentedList(perLayerModificationsList); + + int seenLayers = 0; + for (int i = 0; i < graph.perLayerModifications.Count; i++) { + if ((seenLayers & 1 << graph.perLayerModifications[i].layer) != 0) { + EditorGUILayout.HelpBox("Duplicate layers. Each layer can only be modified by a single rule.", MessageType.Error); + break; + } + seenLayers |= 1 << graph.perLayerModifications[i].layer; + } + + var countStillUnreadable = 0; + for (int i = 0; graph.meshesUnreadableAtRuntime != null && i < graph.meshesUnreadableAtRuntime.Count; i++) { + countStillUnreadable += graph.meshesUnreadableAtRuntime[i].Item2.isReadable ? 0 : 1; + } + if (countStillUnreadable > 0) { + GUILayout.BeginHorizontal(); + GUILayout.Space(EditorGUI.IndentedRect(new Rect(0, 0, 0, 0)).xMin); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + meshesUnreadableAtRuntimeFoldout = GUILayout.Toggle(meshesUnreadableAtRuntimeFoldout, "", EditorStyles.foldout, GUILayout.Width(10)); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + + GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon"), GUILayout.ExpandWidth(false)); + GUILayout.Label(graph.meshesUnreadableAtRuntime.Count + " " + (graph.meshesUnreadableAtRuntime.Count > 1 ? "meshes" : "mesh") + " will be ignored if scanned in a standalone build, because they are marked as not readable." + + "If you plan to scan the graph in a standalone build, all included meshes must be marked as read/write in their import settings.", EditorStyles.wordWrappedMiniLabel); + // EditorGUI.DrawTextureTransparent() EditorGUIUtility.IconContent("console.warnicon") + GUILayout.EndHorizontal(); + + if (meshesUnreadableAtRuntimeFoldout) { + EditorGUILayout.Separator(); + for (int i = 0; i < graph.meshesUnreadableAtRuntime.Count; i++) { + var(source, mesh) = graph.meshesUnreadableAtRuntime[i]; + if (!mesh.isReadable) { + GUILayout.BeginHorizontal(); + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.ObjectField(source, typeof(Mesh), true); + EditorGUILayout.ObjectField(mesh, typeof(Mesh), false); + EditorGUI.EndDisabledGroup(); + if (GUILayout.Button("Make readable")) { + var importer = ModelImporter.GetAtPath(AssetDatabase.GetAssetPath(mesh)) as ModelImporter; + if (importer != null) { + importer.isReadable = true; + importer.SaveAndReimport(); + } + } + GUILayout.EndHorizontal(); + } + } + } + EditorGUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + Separator(); + Header("Runtime Settings"); + + graph.enableNavmeshCutting = EditorGUILayout.Toggle(new GUIContent("Affected by Navmesh Cuts", "Makes this graph affected by NavmeshCut and NavmeshAdd components. See the documentation for more info."), graph.enableNavmeshCutting); + + Separator(); + Header("Debug"); + GUILayout.BeginHorizontal(); + GUILayout.Space(18); + graph.showMeshSurface = GUILayout.Toggle(graph.showMeshSurface, new GUIContent("Show surface", "Toggles gizmos for drawing the surface of the mesh"), EditorStyles.miniButtonLeft); + graph.showMeshOutline = GUILayout.Toggle(graph.showMeshOutline, new GUIContent("Show outline", "Toggles gizmos for drawing an outline of the nodes"), EditorStyles.miniButtonMid); + graph.showNodeConnections = GUILayout.Toggle(graph.showNodeConnections, new GUIContent("Show connections", "Toggles gizmos for drawing node connections"), EditorStyles.miniButtonRight); + GUILayout.EndHorizontal(); + + + Separator(); + Header("Advanced"); + + graph.relevantGraphSurfaceMode = (RecastGraph.RelevantGraphSurfaceMode)EditorGUILayout.EnumPopup(new GUIContent("Relevant Graph Surface Mode", + "Require every region to have a RelevantGraphSurface component inside it.\n" + + "A RelevantGraphSurface component placed in the scene specifies that\n" + + "the navmesh region it is inside should be included in the navmesh.\n\n" + + "If this is set to OnlyForCompletelyInsideTile\n" + + "a navmesh region is included in the navmesh if it\n" + + "has a RelevantGraphSurface inside it, or if it\n" + + "is adjacent to a tile border. This can leave some small regions\n" + + "which you didn't want to have included because they are adjacent\n" + + "to tile borders, but it removes the need to place a component\n" + + "in every single tile, which can be tedious (see below).\n\n" + + "If this is set to RequireForAll\n" + + "a navmesh region is included only if it has a RelevantGraphSurface\n" + + "inside it. Note that even though the navmesh\n" + + "looks continous between tiles, the tiles are computed individually\n" + + "and therefore you need a RelevantGraphSurface component for each\n" + + "region and for each tile."), + graph.relevantGraphSurfaceMode); + + #pragma warning disable 618 + if (graph.nearestSearchOnlyXZ) { + graph.nearestSearchOnlyXZ = EditorGUILayout.Toggle(new GUIContent("Nearest node queries in XZ space", + "Recomended for single-layered environments.\nFaster but can be inacurate esp. in multilayered contexts."), graph.nearestSearchOnlyXZ); + + EditorGUILayout.HelpBox("The global toggle for node queries in XZ space has been deprecated. Use the NNConstraint settings instead.", MessageType.Warning); + } + #pragma warning restore 618 + + if (GUILayout.Button("Export to .obj file")) { + editor.RunTask(() => ExportToFile(graph)); + } + } + + static readonly Vector3[] handlePoints = new [] { new Vector3(-1, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 0, -1), new Vector3(0, 0, 1), new Vector3(0, 1, 0), new Vector3(0, -1, 0) }; + + public override void OnSceneGUI (NavGraph target) { + var graph = target as RecastGraph; + + Handles.matrix = Matrix4x4.identity; + Handles.color = AstarColor.BoundsHandles; + Handles.CapFunction cap = Handles.CylinderHandleCap; + + var center = graph.forcedBoundsCenter; + Matrix4x4 matrix = Matrix4x4.TRS(center, Quaternion.Euler(graph.rotation), graph.forcedBoundsSize * 0.5f); + + if (Tools.current == Tool.Scale) { + const float HandleScale = 0.1f; + + Vector3 mn = Vector3.zero; + Vector3 mx = Vector3.zero; + EditorGUI.BeginChangeCheck(); + for (int i = 0; i < handlePoints.Length; i++) { + var ps = matrix.MultiplyPoint3x4(handlePoints[i]); + Vector3 p = matrix.inverse.MultiplyPoint3x4(Handles.Slider(ps, ps - center, HandleScale*HandleUtility.GetHandleSize(ps), cap, 0)); + + if (i == 0) { + mn = mx = p; + } else { + mn = Vector3.Min(mn, p); + mx = Vector3.Max(mx, p); + } + } + + if (EditorGUI.EndChangeCheck()) { + graph.forcedBoundsCenter = matrix.MultiplyPoint3x4((mn + mx) * 0.5f); + graph.forcedBoundsSize = Vector3.Scale(graph.forcedBoundsSize, (mx - mn) * 0.5f); + } + } else if (Tools.current == Tool.Move) { + EditorGUI.BeginChangeCheck(); + center = Handles.PositionHandle(center, Tools.pivotRotation == PivotRotation.Global ? Quaternion.identity : Quaternion.Euler(graph.rotation)); + + if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) { + graph.forcedBoundsCenter = center; + } + } else if (Tools.current == Tool.Rotate) { + EditorGUI.BeginChangeCheck(); + var rot = Handles.RotationHandle(Quaternion.Euler(graph.rotation), graph.forcedBoundsCenter); + + if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) { + graph.rotation = rot.eulerAngles; + } + } + } + + /// <summary>Exports the INavmesh graph to a .obj file</summary> + public static void ExportToFile (NavmeshBase target) { + if (target == null) return; + + NavmeshTile[] tiles = target.GetTiles(); + + if (tiles == null) { + if (EditorUtility.DisplayDialog("Scan graph before exporting?", "The graph does not contain any mesh data. Do you want to scan it?", "Ok", "Cancel")) { + AstarPathEditor.MenuScan(); + tiles = target.GetTiles(); + if (tiles == null) return; + } else { + return; + } + } + + string path = EditorUtility.SaveFilePanel("Export .obj", "", "navmesh.obj", "obj"); + if (path == "") return; + + //Generate .obj + var sb = new System.Text.StringBuilder(); + + string name = System.IO.Path.GetFileNameWithoutExtension(path); + + sb.Append("g ").Append(name).AppendLine(); + + //Vertices start from 1 + int vCount = 1; + + //Define single texture coordinate to zero + sb.Append("vt 0 0\n"); + + for (int t = 0; t < tiles.Length; t++) { + NavmeshTile tile = tiles[t]; + + if (tile == null) continue; + + var vertices = tile.verts; + + //Write vertices + for (int i = 0; i < vertices.Length; i++) { + var v = (Vector3)vertices[i]; + sb.Append(string.Format("v {0} {1} {2}\n", -v.x, v.y, v.z)); + } + + //Write triangles + TriangleMeshNode[] nodes = tile.nodes; + for (int i = 0; i < nodes.Length; i++) { + TriangleMeshNode node = nodes[i]; + if (node == null) { + Debug.LogError("Node was null or no TriangleMeshNode. Critical error. Graph type " + target.GetType().Name); + return; + } + if (node.GetVertexArrayIndex(0) < 0 || node.GetVertexArrayIndex(0) >= vertices.Length) throw new System.Exception("ERR"); + + sb.Append(string.Format("f {0}/1 {1}/1 {2}/1\n", (node.GetVertexArrayIndex(0) + vCount), (node.GetVertexArrayIndex(1) + vCount), (node.GetVertexArrayIndex(2) + vCount))); + } + + vCount += vertices.Length; + } + + string obj = sb.ToString(); + + using (var sw = new System.IO.StreamWriter(path)) { + sw.Write(obj); + } + } + } +} |