Search code examples
c#unity-game-engineunity3d-editorunity3d-gui

Custom window mimicking SceneView


I am building a custom window and I am trying to reuse Unity's Scene view to be able to draw directly from this specific window.

I manage to reproduce the correct window by extend UnityEditor.SceneView and here's what I have:

enter image description here

And here's the code:

[EditorWindowTitle(title = "Shape Editor", useTypeNameAsIconName = false)]
public class StrokeEditor : SceneView
{
    [MenuItem("Recognizer/Shape Editor")]
    public static void Init()
    {
        var w = GetWindow<StrokeEditor>();
        w.in2DMode = true;

        EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
    }

    protected override void OnGUI()
    {
        using (new GUILayout.HorizontalScope())
        {
            GUILayout.Button("Add Stroke");
            GUILayout.Button("Edit Stroke");
            GUILayout.Button("Delete Stroke");
        }

        base.OnGUI();
    }
}

With this, I might be almost done.

Is this the right way to procede ? I feel that something is wrong because whenever I use EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);, it creates also a new scene to the main scene view. (I want the main scene view to stay unchanged) I should also be able to see the tools from the scene view like:

enter image description here

Is there any better way to achieve what I want ?

EDIT 1:

The final usage of all of this is to be able to draw 2D shapes within the window by clicking and dragging the mouse like gestures with mobile phones. From that, I'll be able to get some of the position to feed one of my algorithm...


Solution

  • You can use the new GraphView. This gives you some of the things you are looking for for "free", mainly to zoom and pan the view. Since ShaderGraph uses this, it should be easier to construct nodes, select them and move them around, if that is something you want to.

    Here is a toy example of a custom editor window that allows you to edit list of points in a scriptable object:

    enter image description here


    Shape.cs
    - simple scriptable object with a list of points.

    [CreateAssetMenu(menuName = "Test/ShapeObject")]
    public class Shape : ScriptableObject
    {
        public List<Vector2> PointList = new List<Vector2>();
    }
    

    ShapeEditorWindow.cs
    - editor window with a toolbar and a graphview that opens scriptable objects of type Shape.

    using UnityEngine;
    using UnityEditor;
    using UnityEditor.UIElements;
    
    public class ShapeEditorWindow : EditorWindow
    {
        private ShapeEditorGraphView _shapeEditorGraphView;
        private Shape _shape;
    
        [UnityEditor.Callbacks.OnOpenAsset(1)]
        private static bool Callback(int instanceID, int line)
        {
            var shape = EditorUtility.InstanceIDToObject(instanceID) as Shape;
            if (shape != null)
            {
                OpenWindow(shape);
                return true;
            }
            return false; // we did not handle the open
        }
    
        private static void OpenWindow(Shape shape)
        {
            var window = GetWindow<ShapeEditorWindow>();
            window.titleContent = new GUIContent("Shape Editor");
            window._shape = shape;
            window.rootVisualElement.Clear();
            window.CreateGraphView();
            window.CreateToolbar();
        }
        
        private void CreateToolbar()
        {
            var toolbar = new Toolbar();        
            var clearBtn = new ToolbarButton(()=>_shape.PointList.Clear()); ;
            clearBtn.text = "Clear";  
            var undoBtn = new ToolbarButton(() =>_shape.PointList.RemoveAt(_shape.PointList.Count-1)); 
            undoBtn.text = "Undo";
            toolbar.Add(clearBtn);
            toolbar.Add(new ToolbarSpacer());
            toolbar.Add(undoBtn);
            rootVisualElement.Add(toolbar);
        }
    
        private void CreateGraphView()
        {       
            _shapeEditorGraphView = new ShapeEditorGraphView(_shape);
            _shapeEditorGraphView.name = "Shape Editor Graph";
            rootVisualElement.Add(_shapeEditorGraphView);
        }
    }
    

    ShapeEditorGraphView.cs
    - graphview with zoom, grid, pan (with ContentDragger) and shape editor.

    using UnityEditor.Experimental.GraphView;
    using UnityEngine;
    using UnityEngine.UIElements;
    
    public class ShapeEditorGraphView : GraphView
    {
        const float _pixelsPerUnit = 100f;
        const bool _invertYPosition = true;
        public ShapeEditorGraphView(Shape shape){        
            styleSheets.Add(Resources.Load<StyleSheet>("ShapeEditorGraph"));
            this.StretchToParentSize();
            
            SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);        
            Add(new GridBackground());
    
            //pan with Alt-LeftMouseButton drag/ MidleMouseButton drag
            this.AddManipulator(new ContentDragger());
    
            //other things that might interest you
            //this.AddManipulator(new SelectionDragger());
            //this.AddManipulator(new RectangleSelector());
            //this.AddManipulator(new ClickSelector());
            
            this.AddManipulator(new ShapeManipulator(shape));
            
            contentViewContainer.BringToFront();
            contentViewContainer.Add(new Label { name = "origin", text = "(0,0)" });
    
            //set the origin to the center of the window
            this.schedule.Execute(() =>
            {
                contentViewContainer.transform.position = parent.worldBound.size / 2f;
            });
        }    
        
        public Vector2 WorldtoScreenSpace(Vector2 pos)
        {
            var position = pos * _pixelsPerUnit - contentViewContainer.layout.position;
            if (_invertYPosition) position.y = -position.y; 
            return contentViewContainer.transform.matrix.MultiplyPoint3x4(position);        
        }
    
        public Vector2 ScreenToWorldSpace(Vector2 pos)
        {             
            Vector2 position = contentViewContainer.transform.matrix.inverse.MultiplyPoint3x4(pos);
            if (_invertYPosition) position.y = -position.y;        
            return (position + contentViewContainer.layout.position) / _pixelsPerUnit;
        }
    }
    

    Unfortunately the grid background and the grid lines are the same color, so in order to see the grid lines we have to write a style sheet and set the GridBackground properties. This file has to be in Editor/Resources, and gets loaded with styleSheets.Add(Resources.Load<StyleSheet>("ShapeEditorGraph"));

    Editor/Resources/ShapeEditorGraph.uss

    GridBackground {
        --grid-background-color: rgba(32,32,32,1);
        --line-color: rgba(255,255,255,.1);
        --thick-line-color: rgba(255,255,255,.3);    
        --spacing: 100;
    }
    

    ShapeManipulator.cs
    - draws and edits the shape. This is similar to RectangleSelector.

    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.UIElements;
    
    public class ShapeManipulator : MouseManipulator
    { 
        private Shape _shape;
        private ShapeDraw _shapeDraw;
        
        public ShapeManipulator(Shape shape)
        {
            activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
            _shape = shape;        
            _shapeDraw = new ShapeDraw { points = shape.PointList };
        }
        protected override void RegisterCallbacksOnTarget()
        {
            target.Add(_shapeDraw);
            target.Add(new Label { name = "mousePosition", text = "(0,0)" });
            target.RegisterCallback<MouseDownEvent>(MouseDown);
            target.RegisterCallback<MouseMoveEvent>(MouseMove);
            target.RegisterCallback<MouseCaptureOutEvent>(MouseOut);
            target.RegisterCallback<MouseUpEvent>(MouseUp);
        }
    
        protected override void UnregisterCallbacksFromTarget()
        {
            target.UnregisterCallback<MouseDownEvent>(MouseDown);
            target.UnregisterCallback<MouseUpEvent>(MouseUp);
            target.UnregisterCallback<MouseMoveEvent>(MouseMove);
            target.UnregisterCallback<MouseCaptureOutEvent>(MouseOut);
        }
    
        private void MouseOut(MouseCaptureOutEvent evt) => _shapeDraw.drawSegment = false;
    
        private void MouseMove(MouseMoveEvent evt)
        {
            var t = target as ShapeEditorGraphView;
            var mouseLabel = target.Q("mousePosition") as Label;
            mouseLabel.transform.position = evt.localMousePosition + Vector2.up * 20;
            mouseLabel.text = t.ScreenToWorldSpace(evt.localMousePosition).ToString();
    
            //if left mouse is pressed 
            if ((evt.pressedButtons & 1) != 1) return;
            _shapeDraw.end = t.ScreenToWorldSpace(evt.localMousePosition);
            _shapeDraw.MarkDirtyRepaint();
        }
    
        private void MouseUp(MouseUpEvent evt)
        {
            if (!CanStopManipulation(evt)) return;        
            target.ReleaseMouse();         
            if (!_shapeDraw.drawSegment) return;   
            
            if (_shape.PointList.Count == 0) _shape.PointList.Add(_shapeDraw.start);
    
            var t = target as ShapeEditorGraphView;
            _shape.PointList.Add(t.ScreenToWorldSpace(evt.localMousePosition));
            _shapeDraw.drawSegment = false;
           
            _shapeDraw.MarkDirtyRepaint();
        }
    
        private void MouseDown(MouseDownEvent evt)
        {
            if (!CanStartManipulation(evt)) return;       
            target.CaptureMouse();   
            
            _shapeDraw.drawSegment = true;
            var t = target as ShapeEditorGraphView;
    
            if (_shape.PointList.Count != 0) _shapeDraw.start = _shape.PointList.Last();
            else _shapeDraw.start = t.ScreenToWorldSpace(evt.localMousePosition);
    
            _shapeDraw.end = t.ScreenToWorldSpace(evt.localMousePosition);
            _shapeDraw.MarkDirtyRepaint();
        }
        private class ShapeDraw : ImmediateModeElement
        {
            public List<Vector2> points { get; set; } = new List<Vector2>();
            public Vector2 start { get; set; }
            public Vector2 end { get; set; }
            public bool drawSegment { get; set; }
            protected override void ImmediateRepaint()
            {
                var lineColor = new Color(1.0f, 0.6f, 0.0f, 1.0f);
                var t = parent as ShapeEditorGraphView;            
                //Draw shape        
                for (int i = 0; i < points.Count - 1; i++)
                {
                    var p1 = t.WorldtoScreenSpace(points[i]);
                    var p2 = t.WorldtoScreenSpace(points[i + 1]);
                    GL.Begin(GL.LINES);
                    GL.Color(lineColor);
                    GL.Vertex(p1);
                    GL.Vertex(p2);
                    GL.End();
                }
    
                if (!drawSegment) return;
    
                //Draw current segment
                GL.Begin(GL.LINES);
                GL.Color(lineColor);
                GL.Vertex(t.WorldtoScreenSpace(start));
                GL.Vertex(t.WorldtoScreenSpace(end));
                GL.End();
            }
        }
    }
    

    The is just example code. The goal was to have something working and drawing to the screen.