3

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...

MadJlzz
  • 767
  • 2
  • 13
  • 35
  • This question sounds a bit broad ..maybe a new SceneView is not what you want .. if you only want something to draw some shapes on what do you need all the tools from the scene view for? I think you would rather want to create a new custom `EditorWindow` with a raster background where you can maybe move around and draw your shapes .. I don't see the need to create a new scene here... – derHugo Aug 28 '20 at 10:07
  • That's because I like the feature of the SceneView being able to zoom in/out and move around. Since it was already implemented, I thought I could be reusing it. – MadJlzz Aug 28 '20 at 10:13

2 Answers2

3

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.

Pluto
  • 3,911
  • 13
  • 21
  • This is exactly what I am looking for. Can you just explain how you did the transformation between World and Screen space ? I didn't get why you're using matrix. Rest of the answer is perfect. – MadJlzz Sep 01 '20 at 15:30
  • 1
    `contentViewContainer.transform.matrix` is the transform matrix of, well.., the content, `_pixelsPerUnit` is the `--spacing:` property of the background, 100 is an arbitrary value just so we dont have 1unit = 1pixel, `_invertYPosition` is to have y values grow from bottom to top, and `contentViewContainer.layout.position` is the offset of the content view (i'm not 100% sure on this but its what RectangleSelector does). – Pluto Sep 01 '20 at 15:45
0

I have tackled something similar to this in the past. When I wanted to extend the SceneView, I've used Gizmos and drawing callbacks to add my own controls to the scene view, but I suspect you might want more freedom than just this.

The other thing I did was to create an "editor preview scene", add a camera to it and made the camera render into my custom EditorWindow. It's a lot of work, but once I did it I was completely free to customize the editor experience.

enter image description here

It's probably quite dangerous inheriting from Unity's SceneView, as I expect that it will change so often that you may struggle to get your stuff working on multiple versions. You might also find yourself breaking stuff when Unity's code doesn't expect anyone to be inheriting from SceneView.

Fydar
  • 374
  • 4
  • 8
  • In my case, `Gizmos` aren't sufficient since they are tied with a `Monobehaviour`. Actually what I am seing with your gif is the kind of thing I want to do. Being able to move around, drawing lines by left clicking and draging with my mouse. Saving line's position points into a `ScriptableObject`... So I have to do everything from scratch then? – MadJlzz Aug 31 '20 at 20:37