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:

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.