2维多边形编辑器

发布时间 2023-10-31 23:48:17作者: yanghui01

效果

 

多边形表示

//#define X_ROTATE_90

using System;
using System.Collections.Generic;
using UnityEngine;

public class MyPolygon : MonoBehaviour
{
    [SerializeField]
    private List<Vector2> m_VertexList = new List<Vector2>();

    public MyPolygon()
    {
        m_VertexList.Add(new Vector2(1, 1));
        m_VertexList.Add(new Vector2(1, -1));
        m_VertexList.Add(new Vector2(-1, -1));
        m_VertexList.Add(new Vector2(-1, 1));
    }

    public Vector2 GetVertex(int index)
    {
        return m_VertexList[index];
    }

    public Vector2 GetEdge(int index)
    {
        var p1 = m_VertexList[index];
        var p2 = m_VertexList[index % m_VertexList.Count];
        return p2 - p1;
    }

    public void InsertVertex(int index, Vector2 vertex)
    {
        m_VertexList.Insert(index, vertex);
    }

    public void SetVertex(int index, Vector2 vertex)
    {
        m_VertexList[index] = vertex;
    }

    public void RemoveVertexAt(int index)
    {
        m_VertexList.RemoveAt(index);
    }

    public int GetVertexCount()
    {
        return m_VertexList.Count;
    }

    public int GetEdgeCount()
    {
        return m_VertexList.Count;
    }

    //离指定点最近的顶点
    public bool GetNearestVertex(Vector2 localPoint, out int vertexIndex, out float distance)
    {
        if (m_VertexList.Count == 0)
        {
            vertexIndex = -1;
            distance = float.MaxValue;
            return false;
        }

        int nearestPointIndex = 0;
        float nearestDistance = float.MaxValue;

        for (int i = 0; i < m_VertexList.Count; ++i)
        {
            float curDistance = Vector2.Distance(localPoint, m_VertexList[i]);
            if (curDistance < nearestDistance)
            {
                nearestPointIndex = i;
                nearestDistance = curDistance;
            }
        }

        vertexIndex = nearestPointIndex;
        distance = nearestDistance;
        return true;
    }

    //离指定点最近的边
    public bool GetNearestEdge(Vector2 localPoint, out int vertexIndex0, out int vertexIndex1, out float distance)
    {
        if (m_VertexList.Count < 2)
        {
            vertexIndex0 = -1;
            vertexIndex1 = -1;
            distance = float.MaxValue;
            return false;
        }

        int nearestVertexIndex0 = 0;
        int nearestVertexIndex1 = 0;
        float nearestDistance = float.MaxValue;

        for (int i = 0; i < m_VertexList.Count; ++i)
        {
            int index0 = i;
            int index1 = i + 1;
            if (index1 == m_VertexList.Count)
            {
                index1 = 0;
            }

            float curDistance = Shape2DHelper.PointToLineSegmentDistance(localPoint, m_VertexList[index0], m_VertexList[index1]);
            if (curDistance < nearestDistance)
            {
                nearestVertexIndex0 = index0;
                nearestVertexIndex1 = index1;
                nearestDistance = curDistance;
            }
        }

        vertexIndex0 = nearestVertexIndex0;
        vertexIndex1 = nearestVertexIndex1;
        distance = nearestDistance;
        return true;
    }



#if UNITY_EDITOR
    [NonSerialized]
    public bool m_InEdit;

#if X_ROTATE_90
    private Matrix4x4 m_ModelMaxtrix = new Matrix4x4(); //模型空间矩阵, localToWorld

    public void ResetModelMatrix()
    {
        m_ModelMaxtrix.SetTRS(this.transform.position, Quaternion.Euler(90, 0, 0), Vector3.one); //人为把polygon绕x轴旋转90度
    }
#else
    public void ResetModelMatrix()
    {
        //do nothing
    }
#endif

    //模型空间的3个轴
    public void GetPolygonSpaceAxis(out Vector3 wOrigin, out Vector3 wRight, out Vector3 wUp, out Vector3 wForward)
    {
        wOrigin = this.transform.position;
        wRight = Vector3.right;
        wUp = Vector3.up;
        wForward = Vector3.forward;

#if X_ROTATE_90
        wOrigin.Set(m_ModelMaxtrix.m03, m_ModelMaxtrix.m13, m_ModelMaxtrix.m23);
        wRight.Set(m_ModelMaxtrix.m00, m_ModelMaxtrix.m10, m_ModelMaxtrix.m20);
        wUp.Set(m_ModelMaxtrix.m01, m_ModelMaxtrix.m11, m_ModelMaxtrix.m21);
        wForward.Set(m_ModelMaxtrix.m02, m_ModelMaxtrix.m12, m_ModelMaxtrix.m22);
#endif
    }

    public Vector3 PointLocalToWorld(Vector3 local)
    {
#if X_ROTATE_90
        return m_ModelMaxtrix.MultiplyPoint(local);
#else
        //这边不考虑GameObject的缩放和旋转
        return local + this.transform.position;
#endif
    }

    public Vector3 PointWorldToLocal(Vector3 world)
    {
#if X_ROTATE_90
        return m_ModelMaxtrix.inverse.MultiplyPoint(world);
#else
        //这边不考虑GameObject的缩放和旋转
        return world - this.transform.position;
#endif
    }

    private void OnDrawGizmos()
    {
        if (m_InEdit || m_VertexList.Count < 3)
            return;

        var wCenter = transform.position;

        var wVertex0 = PointLocalToWorld(m_VertexList[0]);
        var lastWVertex = wVertex0;
        for (var i = 1; i < m_VertexList.Count; ++i)
        {
            var curWVertex = PointLocalToWorld(m_VertexList[i]);

            Gizmos.DrawLine(lastWVertex, curWVertex);
            lastWVertex = curWVertex;
        }
        Gizmos.DrawLine(lastWVertex, wVertex0);
    }
#endif

    }

多边形编辑器

1) 在多边形的边上按下拖拽可以添加顶点

2) 按住ctrl,靠近现有顶点时,顶点会变成红色,表示删除模式;在已有顶点上点击可以删除该顶点

#if UNITY_EDITOR

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(MyPolygon))]
public class MyPolygonEditor : Editor
{

    private MyPolygon m_Target;
    private bool m_InEdit;

    private Color m_DrawPolygonColor = new Color(0.6f, 1.0f, 0.6f, 1.0f);
    private Vector3[] m_TempPoints = new Vector3[2];
    private Plane m_TempPlane = new Plane();

    private int m_HighlightVertex = -1;
    private int m_HighlightEdgeVertex0 = -1;
    private int m_HighlightEdgeVertex1 = -1;
    private int m_EditMode = 0; //1:顶点编辑, 2:边编辑

    private void OnEnable()
    {
        m_Target = (MyPolygon)target;
        SceneView.duringSceneGui -= OnSceneGUI_2;
        SceneView.duringSceneGui += OnSceneGUI_2;
    }

    private void OnDisable()
    {
        SceneView.duringSceneGui -= OnSceneGUI_2;
        m_Target.m_InEdit = false;
        m_Target = null;
        if (m_InEdit)
        {
            m_InEdit = false;
            Undo.undoRedoPerformed -= OnUndoRedoCallback;
        }
    }

    private void OnUndoRedoCallback()
    {
        ResetEditContext();
    }

    private void ResetEditContext()
    {
        if (null != m_Target)
            m_Target.ResetModelMatrix();

        m_HighlightVertex = -1;
        m_HighlightEdgeVertex0 = -1;
        m_HighlightEdgeVertex1 = -1;
        m_EditMode = 0;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        EditorGUI.BeginChangeCheck();
        m_InEdit = EditorGUILayout.ToggleLeft("Edit", m_InEdit);
        m_Target.m_InEdit = m_InEdit;

        if (EditorGUI.EndChangeCheck())
        {
            if (m_InEdit)
            {
                ResetEditContext();

                Undo.undoRedoPerformed -= OnUndoRedoCallback;
                Undo.undoRedoPerformed += OnUndoRedoCallback;
            }
        }
    }

    private void OnSceneGUI_2(SceneView sceneView)
    {
        if (!m_InEdit)
            return;

        CheckCtxtMenuReset();
        DrawPolygon();
        DrawPolygonEdit();
    }

    //脚本菜单点击Reset菜单的情况
    private void CheckCtxtMenuReset()
    {
        if (m_HighlightVertex < 0 || m_HighlightVertex >= m_Target.GetVertexCount())
            m_HighlightVertex = -1;
        if (m_HighlightEdgeVertex0 >= m_Target.GetVertexCount() || m_HighlightEdgeVertex1 >= m_Target.GetVertexCount())
        {
            m_HighlightEdgeVertex0 = -1;
            m_HighlightEdgeVertex1 = -1;
        }
    }

    private void DrawPolygon()
    {
        var oldColor = Handles.color;
        Handles.color = m_DrawPolygonColor;

        var lastPoint = m_Target.GetVertex(0);
        for (int i = 1; i < m_Target.GetVertexCount(); ++i)
        {
            var curPoint = m_Target.GetVertex(i);
            m_TempPoints[0] = m_Target.PointLocalToWorld(lastPoint);
            m_TempPoints[1] = m_Target.PointLocalToWorld(curPoint);
            Handles.DrawAAPolyLine(2, m_TempPoints);
            lastPoint = curPoint;
        }
        m_TempPoints[0] = m_Target.PointLocalToWorld(lastPoint);
        m_TempPoints[1] = m_Target.PointLocalToWorld(m_Target.GetVertex(0));
        Handles.DrawAAPolyLine(2, m_TempPoints);

        Handles.color = oldColor;
    }

    private void DrawPolygonEdit()
    {
        var curEvt = Event.current;

        var hotControl1 = GUIUtility.hotControl;
        var evtType1 = curEvt.type;

        m_Target.GetPolygonSpaceAxis(out var wOrigin, out var wRight, out var wUp, out var wForward);

        var mouseWRay = HandleUtility.GUIPointToWorldRay(curEvt.mousePosition);
        m_TempPlane.SetNormalAndPosition(-wForward, wOrigin);
        bool isHit = m_TempPlane.Raycast(mouseWRay, out var raycastDist); //向平面投射射线, 射线发射出多少距离遇到平面
        if (!isHit || raycastDist >= 999) return;
        
        var mouseWPoint = mouseWRay.GetPoint(raycastDist);
        var mouseLPoint = m_Target.PointWorldToLocal(mouseWPoint);
        
        if (curEvt.type == EventType.MouseMove) //鼠标没点在控件上时, 指针移动过程中, 高亮的顶点和边才可能变化
        {
            int vertexIndex0 = 0;
            int vertexIndex1 = 0;
            float dist = 0;
            if (m_Target.GetNearestVertex(mouseLPoint, out vertexIndex0, out dist))
            {
                m_HighlightVertex = vertexIndex0;
            }
            if (m_Target.GetNearestEdge(mouseLPoint, out vertexIndex0, out vertexIndex1, out dist))
            {
                m_HighlightEdgeVertex0 = vertexIndex0;
                m_HighlightEdgeVertex1 = vertexIndex1;
            }

            curEvt.Use();

            var wVertex = m_Target.PointLocalToWorld((Vector3)m_Target.GetVertex(m_HighlightVertex));
            if ((HandleUtility.WorldToGUIPoint(wVertex) - curEvt.mousePosition).sqrMagnitude <= 20*20) //如果鼠标指针离顶点很近, 就显示顶点操作ui
            {
                m_EditMode = 1;
            }
            else
            {
                m_EditMode = 2;
            }
        }

        if (-1 != m_HighlightEdgeVertex0 && -1 != m_HighlightEdgeVertex1)
        {
            Handles.color = Color.yellow;
            var lVertex0 = m_Target.GetVertex(m_HighlightEdgeVertex0);
            var lVertex1 = m_Target.GetVertex(m_HighlightEdgeVertex1);
            m_TempPoints[0] = m_Target.PointLocalToWorld(lVertex0);
            m_TempPoints[1] = m_Target.PointLocalToWorld(lVertex1);
            Handles.DrawAAPolyLine(4.0f, m_TempPoints);
            Handles.color = Color.white;

            if (2 == m_EditMode)
            {
                Handles.Label(m_TempPoints[0], $"A-{m_HighlightEdgeVertex0}");
                Handles.Label(m_TempPoints[1], $"B-{m_HighlightEdgeVertex1}");

                var wPoint = Shape2DHelper.GetNearestPointOnEdge(mouseWPoint, m_TempPoints[0], m_TempPoints[1]);

                float handleSize = HandleUtility.GetHandleSize(wPoint) * 0.06f;
                Handles.color = Color.green;

                EditorGUI.BeginChangeCheck();
                wPoint = Handles.Slider2D(
                        wPoint,
                        wForward,
                        wRight,
                        wUp,
                        handleSize, Handles.CircleHandleCap, 0);
                if (EditorGUI.EndChangeCheck())
                {
                    var point = m_Target.PointWorldToLocal(wPoint);
                    Undo.RecordObject(m_Target, "Polygon InsertPoint");
                    m_Target.InsertVertex(m_HighlightEdgeVertex1, point);
                    if (m_HighlightEdgeVertex0 > m_HighlightEdgeVertex1) //最后1个顶点和第1个顶点的情况
                        m_HighlightEdgeVertex0 += 1;

                    //添加该顶点后, 立即变成该点编辑模式
                    m_EditMode = 1;
                    m_HighlightVertex = m_HighlightEdgeVertex1;
                }

                Handles.color = Color.white;
            }
        }

        if (-1 != m_HighlightVertex)
        {
            if (1 == m_EditMode)
            {
                var lVertex = m_Target.GetVertex(m_HighlightVertex);
                var wVertex = m_Target.PointLocalToWorld(lVertex);
                Handles.Label(wVertex, $"{m_HighlightVertex}, {m_HighlightEdgeVertex0}, {m_HighlightEdgeVertex1}");

                bool isDel = curEvt.control || curEvt.command;
                if (isDel && curEvt.type == EventType.MouseDown)
                {
                    if (m_Target.GetVertexCount() > 3)
                    {
                        Undo.RecordObject(m_Target, "Polygon RemovePoint");
                        m_Target.RemoveVertexAt(m_HighlightVertex);

                        m_EditMode = -1;
                        m_HighlightVertex = -1;
                        m_HighlightEdgeVertex0 = -1;
                        m_HighlightEdgeVertex1 = -1;
                        curEvt.Use();
                    }
                }

                float handleSize = HandleUtility.GetHandleSize(wVertex) * 0.06f;
                Handles.color = isDel ? Color.red : Color.green;

                EditorGUI.BeginChangeCheck();
                wVertex = Handles.Slider2D(
                        wVertex,
                        wForward,
                        wRight,
                        wUp,
                        handleSize, Handles.DotHandleCap, 0);

                if (EditorGUI.EndChangeCheck() && -1 != m_EditMode)
                {
                    var point = m_Target.PointWorldToLocal(wVertex);
                    Undo.RecordObject(m_Target, "Polygon SetPoint");
                    m_Target.SetVertex(m_HighlightVertex, point);
                }
                Handles.color = Color.white;
            }
        }

        var hotControl2 = GUIUtility.hotControl;
        var evtType2 = curEvt.type;
        //Debug.Log($"hotControl:{hotControl1}, {hotControl2}, evtType:{evtType1}, {evtType2}");
    }

}

#endif

 

工具函数

Shape2DHelper.GetNearestPointOnEdge

//该条边上离point最近的一个点
public static Vector3 GetNearestPointOnEdge(Vector3 point, Vector3 start, Vector3 end)
{
    var startToPoint = point - start;
    var startToEndDir = (end - start).normalized;
    float dot = Vector3.Dot(startToEndDir, startToPoint); //startToPoint在startToEnd上的投影长度

    if (dot <= 0) //[90, 270]
        return start;

    if (dot >= Vector3.Distance(start, end))
        return end;

    //边的中间点
    var offsetToPoint = startToEndDir * dot;
    return start + offsetToPoint;
}

Shape2DHelper.IsLineSegmentIntersect:线段是否相交 - yanghui01 - 博客园 (cnblogs.com)

Shape2DHelper.PointToLineSegmentDistance:点到线段的距离 - yanghui01 - 博客园 (cnblogs.com)