2维多边形编辑器
效果
多边形表示
//#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+1) % 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 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 } #if UNITY_EDITOR [NonSerialized] public bool m_InEdit; 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)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端