using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Textures.TextureWraps; namespace LLib.ImGui; public sealed class MapPolygonEditor { private readonly MapTextureService _mapService; private float _zoom = 0.5f; private Vector2 _offset = Vector2.Zero; private bool _isPanning; private Vector2 _lastMousePos; private Vector2 _panStartPos; private bool _isDrawing; private readonly List _drawingVertices = new List(); private bool _isDraggingVertex; private bool _suppressClickThisFrame; private int _dragPolyIndex = -1; private int _dragVertexIndex = -1; private int _hoveredVertexIndex = -1; private int _hoveredEdgeIndex = -1; private const float VertexHandleRadius = 5f; private const float VertexHitRadius = 12f; private const float EdgeMidpointRadius = 4f; private const float EdgeMidpointHitRadius = 10f; private const float VertexSnapRadius = 10f; private const float ClickThreshold = 4f; private const float ClosePolygonPixelDist = 12f; private const int MinVerticesForPolygon = 3; public bool IsDrawing => _isDrawing; public int SelectedIndex { get; set; } = -1; public MapPolygonEditor(MapTextureService mapService) { _mapService = mapService; } public void StartDrawing() { _isDrawing = true; _drawingVertices.Clear(); } public void CancelDrawing() { _isDrawing = false; _drawingVertices.Clear(); } public void ResetView() { _zoom = 0.5f; _offset = Vector2.Zero; } public void Draw(IEditablePolygonSet polygons, Vector3? playerPosition, Vector2? size = null) { Vector2 contentRegionAvail = Dalamud.Bindings.ImGui.ImGui.GetContentRegionAvail(); Vector2 size2 = size ?? new Vector2(contentRegionAvail.X, MathF.Max(contentRegionAvail.Y, 400f)); if (!Dalamud.Bindings.ImGui.ImGui.BeginChild("##MapPolygonEditor", size2, border: true)) { Dalamud.Bindings.ImGui.ImGui.EndChild(); return; } Vector2 cursorScreenPos = Dalamud.Bindings.ImGui.ImGui.GetCursorScreenPos(); Vector2 contentRegionAvail2 = Dalamud.Bindings.ImGui.ImGui.GetContentRegionAvail(); Dalamud.Bindings.ImGui.ImGui.InvisibleButton("##MapCapture", contentRegionAvail2); bool flag = Dalamud.Bindings.ImGui.ImGui.IsItemHovered(); IDalamudTextureWrap textureWrap = _mapService.GetTextureWrap(); if (textureWrap == null) { Dalamud.Bindings.ImGui.ImGui.SetCursorScreenPos(cursorScreenPos + new Vector2(8f, 8f)); if (_mapService.IsSearching) { Dalamud.Bindings.ImGui.ImGui.TextDisabled("Loading map texture..."); } else { Dalamud.Bindings.ImGui.ImGui.TextDisabled("No map texture available."); } Dalamud.Bindings.ImGui.ImGui.EndChild(); return; } float value = 2048f * _zoom; Vector2 vector = new Vector2(value); Vector2 imagePos = cursorScreenPos + _offset + contentRegionAvail2 * 0.5f - vector * 0.5f; int nearVertexIndex = -1; if (!_isDrawing && SelectedIndex >= 0 && SelectedIndex < polygons.PolygonCount) { IList vertices = polygons.GetVertices(SelectedIndex); nearVertexIndex = FindNearestVertexHandle(vertices, imagePos, vector); } HandleVertexDrag(polygons, imagePos, vector, flag, nearVertexIndex); if (!_isDraggingVertex) { HandlePanZoom(cursorScreenPos, contentRegionAvail2, flag); } imagePos = cursorScreenPos + _offset + contentRegionAvail2 * 0.5f - vector * 0.5f; ImDrawListPtr windowDrawList = Dalamud.Bindings.ImGui.ImGui.GetWindowDrawList(); windowDrawList.AddImage(textureWrap.Handle, imagePos, imagePos + vector); DrawPolygons(windowDrawList, polygons, imagePos, vector); if (playerPosition.HasValue) { Vector3 valueOrDefault = playerPosition.GetValueOrDefault(); Vector2 center = _mapService.WorldToScreen(valueOrDefault.X, valueOrDefault.Z, imagePos, vector); windowDrawList.AddCircleFilled(center, 4f, 4278255360u); windowDrawList.AddCircle(center, 6f, 4278190080u); } if (_isDrawing) { DrawPolygonInProgress(windowDrawList, imagePos, vector); } HandleMouseInput(polygons, imagePos, vector, flag); HandleDrawingInput(polygons, imagePos, vector, flag); Dalamud.Bindings.ImGui.ImGui.EndChild(); } private void DrawPolygons(ImDrawListPtr drawList, IEditablePolygonSet polygons, Vector2 imagePos, Vector2 imageSizeVec) { for (int i = 0; i < polygons.PolygonCount; i++) { IList vertices = polygons.GetVertices(i); if (vertices.Count >= 3) { uint num = polygons.GetColor(i); bool num2 = i == SelectedIndex; if (num2) { num = (num & 0xFFFFFF) | 0x90000000u; } DrawFilledPolygon(drawList, vertices, imagePos, imageSizeVec, num); float thickness = (num2 ? 3f : 1f); uint color = (num2 ? uint.MaxValue : (num | 0xFF000000u)); DrawPolygonBorder(drawList, vertices, imagePos, imageSizeVec, color, thickness); if (num2) { DrawVertexHandles(drawList, vertices, imagePos, imageSizeVec); } string label = polygons.GetLabel(i); if (!string.IsNullOrEmpty(label)) { Vector2 vector = ComputeCentroid(vertices); Vector2 pos = _mapService.WorldToScreen(vector.X, vector.Y, imagePos, imageSizeVec); drawList.AddText(pos, uint.MaxValue, label); } } } } private void DrawFilledPolygon(ImDrawListPtr drawList, IList verts, Vector2 imagePos, Vector2 imageSizeVec, uint color) { Vector2[] array = new Vector2[verts.Count]; for (int i = 0; i < verts.Count; i++) { array[i] = _mapService.WorldToScreen(verts[i].X, verts[i].Y, imagePos, imageSizeVec); } List list = new List(verts.Count); for (int j = 0; j < verts.Count; j++) { list.Add(j); } float polySignedArea = SignedArea(array, list); while (list.Count > 2) { bool flag = false; for (int k = 0; k < list.Count; k++) { int index = (k - 1 + list.Count) % list.Count; int index2 = (k + 1) % list.Count; int num = list[index]; int num2 = list[k]; int num3 = list[index2]; if (IsEar(array, list, num, num2, num3, polySignedArea)) { drawList.AddTriangleFilled(array[num], array[num2], array[num3], color); list.RemoveAt(k); flag = true; break; } } if (!flag) { break; } } } private static float SignedArea(Vector2[] verts, List indices) { float num = 0f; for (int i = 0; i < indices.Count; i++) { Vector2 vector = verts[indices[i]]; Vector2 vector2 = verts[indices[(i + 1) % indices.Count]]; num += (vector2.X - vector.X) * (vector2.Y + vector.Y); } return num; } private static bool IsEar(Vector2[] verts, List indices, int prev, int curr, int next, float polySignedArea) { Vector2 vector = verts[prev]; Vector2 vector2 = verts[curr]; Vector2 vector3 = verts[next]; float num = Cross2D(vector2 - vector, vector3 - vector); if (polySignedArea > 0f && num >= 0f) { return false; } if (polySignedArea <= 0f && num <= 0f) { return false; } foreach (int index in indices) { if (index != prev && index != curr && index != next && PointInTriangle(verts[index], vector, vector2, vector3)) { return false; } } return true; } private static bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c) { float num = Cross2D(p - a, b - a); float num2 = Cross2D(p - b, c - b); float num3 = Cross2D(p - c, a - c); bool flag = num < 0f || num2 < 0f || num3 < 0f; bool flag2 = num > 0f || num2 > 0f || num3 > 0f; return !(flag && flag2); } private static float Cross2D(Vector2 a, Vector2 b) { return a.X * b.Y - a.Y * b.X; } private void DrawPolygonBorder(ImDrawListPtr drawList, IList verts, Vector2 imagePos, Vector2 imageSizeVec, uint color, float thickness) { for (int i = 0; i < verts.Count; i++) { int index = (i + 1) % verts.Count; Vector2 p = _mapService.WorldToScreen(verts[i].X, verts[i].Y, imagePos, imageSizeVec); Vector2 p2 = _mapService.WorldToScreen(verts[index].X, verts[index].Y, imagePos, imageSizeVec); drawList.AddLine(p, p2, color, thickness); } } private void DrawVertexHandles(ImDrawListPtr drawList, IList verts, Vector2 imagePos, Vector2 imageSizeVec) { _hoveredVertexIndex = FindNearestVertexHandle(verts, imagePos, imageSizeVec); _hoveredEdgeIndex = -1; if (_hoveredVertexIndex < 0 && !_isDraggingVertex) { _hoveredEdgeIndex = FindNearestEdgeMidpoint(verts, imagePos, imageSizeVec); } for (int i = 0; i < verts.Count; i++) { int index = (i + 1) % verts.Count; Vector2 vector = (verts[i] + verts[index]) / 2f; Vector2 center = _mapService.WorldToScreen(vector.X, vector.Y, imagePos, imageSizeVec); bool num = i == _hoveredEdgeIndex; uint col = (num ? 4278255615u : 2164260863u); float radius = (num ? 6f : 4f); drawList.AddCircleFilled(center, radius, col); Vector2 vector2 = verts[i]; Vector2 center2 = _mapService.WorldToScreen(vector2.X, vector2.Y, imagePos, imageSizeVec); bool num2 = i == _hoveredVertexIndex; uint col2 = (num2 ? 4278255615u : uint.MaxValue); float radius2 = (num2 ? 7f : 5f); drawList.AddCircleFilled(center2, radius2, col2); drawList.AddCircle(center2, radius2, 4278190080u); } } private void DrawPolygonInProgress(ImDrawListPtr drawList, Vector2 imagePos, Vector2 imageSizeVec) { Vector2 mousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); if (_drawingVertices.Count == 0) { drawList.AddCircle(mousePos, 4f, uint.MaxValue); return; } for (int i = 0; i < _drawingVertices.Count - 1; i++) { Vector2 p = _mapService.WorldToScreen(_drawingVertices[i].X, _drawingVertices[i].Y, imagePos, imageSizeVec); Vector2 p2 = _mapService.WorldToScreen(_drawingVertices[i + 1].X, _drawingVertices[i + 1].Y, imagePos, imageSizeVec); drawList.AddLine(p, p2, uint.MaxValue, 2f); } for (int j = 0; j < _drawingVertices.Count; j++) { Vector2 center = _mapService.WorldToScreen(_drawingVertices[j].X, _drawingVertices[j].Y, imagePos, imageSizeVec); uint col = ((j == 0) ? 4278255360u : uint.MaxValue); drawList.AddCircleFilled(center, 4f, col); } MapTextureService mapService = _mapService; List drawingVertices = _drawingVertices; float x = drawingVertices[drawingVertices.Count - 1].X; List drawingVertices2 = _drawingVertices; Vector2 p3 = mapService.WorldToScreen(x, drawingVertices2[drawingVertices2.Count - 1].Y, imagePos, imageSizeVec); drawList.AddLine(p3, mousePos, 2164260863u, 1.5f); if (_drawingVertices.Count >= 3) { Vector2 vector = _mapService.WorldToScreen(_drawingVertices[0].X, _drawingVertices[0].Y, imagePos, imageSizeVec); if (Vector2.Distance(mousePos, vector) < 12f) { drawList.AddCircle(vector, 12f, 4278255360u, 0, 2f); } } } private void HandlePanZoom(Vector2 canvasPos, Vector2 canvasSize, bool isHovered) { ImGuiIOPtr iO = Dalamud.Bindings.ImGui.ImGui.GetIO(); if (isHovered && MathF.Abs(iO.MouseWheel) > 0.01f) { float zoom = _zoom; _zoom = MathF.Max(0.1f, MathF.Min(3f, _zoom + iO.MouseWheel * 0.05f)); Vector2 vector = Dalamud.Bindings.ImGui.ImGui.GetMousePos() - canvasPos - canvasSize * 0.5f - _offset; _offset -= vector * (_zoom / zoom - 1f); } bool flag = Dalamud.Bindings.ImGui.ImGui.IsMouseDown(ImGuiMouseButton.Left) || Dalamud.Bindings.ImGui.ImGui.IsMouseDown(ImGuiMouseButton.Right) || Dalamud.Bindings.ImGui.ImGui.IsMouseDown(ImGuiMouseButton.Middle); if (isHovered && flag) { if (!_isPanning) { _isPanning = true; _lastMousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); _panStartPos = _lastMousePos; } else { Vector2 mousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); Vector2 vector2 = mousePos - _lastMousePos; _offset += vector2; _lastMousePos = mousePos; } } else { _isPanning = false; } } private void HandleVertexDrag(IEditablePolygonSet polygons, Vector2 imagePos, Vector2 imageSizeVec, bool mapHovered, int nearVertexIndex) { if (!_isDraggingVertex && !_isDrawing && mapHovered && nearVertexIndex >= 0 && Dalamud.Bindings.ImGui.ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { _isDraggingVertex = true; _dragPolyIndex = SelectedIndex; _dragVertexIndex = nearVertexIndex; } if (_isDraggingVertex && Dalamud.Bindings.ImGui.ImGui.IsMouseDown(ImGuiMouseButton.Left) && _dragPolyIndex >= 0 && _dragPolyIndex < polygons.PolygonCount) { IList vertices = polygons.GetVertices(_dragPolyIndex); if (_dragVertexIndex >= 0 && _dragVertexIndex < vertices.Count) { Vector2 mousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); Vector2 vector = _mapService.ScreenToWorld(mousePos, imagePos, imageSizeVec); Vector2? vector2 = FindSnapTarget(polygons, _dragPolyIndex, _dragVertexIndex, mousePos, imagePos, imageSizeVec); vertices[_dragVertexIndex] = vector2 ?? vector; } } if (_isDraggingVertex && Dalamud.Bindings.ImGui.ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { _isDraggingVertex = false; _dragPolyIndex = -1; _dragVertexIndex = -1; _suppressClickThisFrame = true; } if (!_isDraggingVertex && !_isDrawing && mapHovered && nearVertexIndex >= 0 && Dalamud.Bindings.ImGui.ImGui.IsMouseClicked(ImGuiMouseButton.Right) && SelectedIndex >= 0 && SelectedIndex < polygons.PolygonCount) { IList vertices2 = polygons.GetVertices(SelectedIndex); if (vertices2.Count > 3) { vertices2.RemoveAt(nearVertexIndex); } } } private void HandleMouseInput(IEditablePolygonSet polygons, Vector2 imagePos, Vector2 imageSizeVec, bool mapHovered) { if (!Dalamud.Bindings.ImGui.ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { return; } if (_suppressClickThisFrame) { _suppressClickThisFrame = false; } else { if (_isDrawing || _isDraggingVertex || Vector2.Distance(Dalamud.Bindings.ImGui.ImGui.GetMousePos(), _panStartPos) > 4f || !mapHovered) { return; } Vector2 mousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); Vector2 point = _mapService.ScreenToWorld(mousePos, imagePos, imageSizeVec); if (_hoveredEdgeIndex >= 0 && SelectedIndex >= 0 && SelectedIndex < polygons.PolygonCount) { IList vertices = polygons.GetVertices(SelectedIndex); int hoveredEdgeIndex = _hoveredEdgeIndex; if (hoveredEdgeIndex < vertices.Count) { int index = (hoveredEdgeIndex + 1) % vertices.Count; Vector2 item = (vertices[hoveredEdgeIndex] + vertices[index]) / 2f; vertices.Insert(index, item); return; } } int num = -1; for (int i = 0; i < polygons.PolygonCount; i++) { IList vertices2 = polygons.GetVertices(i); if (PointInPolygon(point, vertices2)) { num = i; } } if (num >= 0) { SelectedIndex = num; } } } private void HandleDrawingInput(IEditablePolygonSet polygons, Vector2 imagePos, Vector2 imageSizeVec, bool mapHovered) { if (!_isDrawing) { return; } if (Dalamud.Bindings.ImGui.ImGui.IsKeyPressed(ImGuiKey.Escape)) { _isDrawing = false; _drawingVertices.Clear(); } else if (Dalamud.Bindings.ImGui.ImGui.IsKeyPressed(ImGuiKey.Enter) && _drawingVertices.Count >= 3) { FinishDrawing(polygons); } else { if (!mapHovered) { return; } if (Dalamud.Bindings.ImGui.ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left) && _drawingVertices.Count >= 3) { FinishDrawing(polygons); } else { if (!Dalamud.Bindings.ImGui.ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { return; } Vector2 mousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); Vector2 item = _mapService.ScreenToWorld(mousePos, imagePos, imageSizeVec); if (_drawingVertices.Count >= 3) { Vector2 value = _mapService.WorldToScreen(_drawingVertices[0].X, _drawingVertices[0].Y, imagePos, imageSizeVec); if (Vector2.Distance(mousePos, value) < 12f) { FinishDrawing(polygons); return; } } _drawingVertices.Add(item); } } } private void FinishDrawing(IEditablePolygonSet polygons) { _isDrawing = false; if (_drawingVertices.Count < 3) { _drawingVertices.Clear(); return; } List drawingVertices = _drawingVertices; int count = drawingVertices.Count; List list = new List(count); CollectionsMarshal.SetCount(list, count); Span span = CollectionsMarshal.AsSpan(list); int num = 0; Span span2 = CollectionsMarshal.AsSpan(drawingVertices); span2.CopyTo(span.Slice(num, span2.Length)); num += span2.Length; polygons.AddPolygon(list); SelectedIndex = polygons.PolygonCount - 1; _drawingVertices.Clear(); } private int FindNearestVertexHandle(IList verts, Vector2 imagePos, Vector2 imageSizeVec) { Vector2 mousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); float num = 12f; int result = -1; for (int i = 0; i < verts.Count; i++) { Vector2 value = _mapService.WorldToScreen(verts[i].X, verts[i].Y, imagePos, imageSizeVec); float num2 = Vector2.Distance(mousePos, value); if (num2 < num) { num = num2; result = i; } } return result; } private int FindNearestEdgeMidpoint(IList verts, Vector2 imagePos, Vector2 imageSizeVec) { if (verts.Count < 2) { return -1; } Vector2 mousePos = Dalamud.Bindings.ImGui.ImGui.GetMousePos(); float num = 10f; int result = -1; for (int i = 0; i < verts.Count; i++) { int index = (i + 1) % verts.Count; Vector2 vector = (verts[i] + verts[index]) / 2f; Vector2 value = _mapService.WorldToScreen(vector.X, vector.Y, imagePos, imageSizeVec); float num2 = Vector2.Distance(mousePos, value); if (num2 < num) { num = num2; result = i; } } return result; } private Vector2? FindSnapTarget(IEditablePolygonSet polygons, int currentPolyIndex, int currentVertexIndex, Vector2 mouseScreenPos, Vector2 imagePos, Vector2 imageSizeVec) { float num = 10f; Vector2? result = null; for (int i = 0; i < polygons.PolygonCount; i++) { IList vertices = polygons.GetVertices(i); for (int j = 0; j < vertices.Count; j++) { if (i != currentPolyIndex || j != currentVertexIndex) { Vector2 value = _mapService.WorldToScreen(vertices[j].X, vertices[j].Y, imagePos, imageSizeVec); float num2 = Vector2.Distance(mouseScreenPos, value); if (num2 < num) { num = num2; result = vertices[j]; } } } } return result; } private static bool PointInPolygon(Vector2 point, IList verts) { if (verts.Count < 3) { return false; } bool flag = false; int num = 0; int index = verts.Count - 1; while (num < verts.Count) { float x = verts[num].X; float y = verts[num].Y; float x2 = verts[index].X; float y2 = verts[index].Y; if (y > point.Y != y2 > point.Y && point.X < (x2 - x) * (point.Y - y) / (y2 - y) + x) { flag = !flag; } index = num++; } return flag; } private static Vector2 ComputeCentroid(IList verts) { float num = 0f; float num2 = 0f; for (int i = 0; i < verts.Count; i++) { num += verts[i].X; num2 += verts[i].Y; } return new Vector2(num / (float)verts.Count, num2 / (float)verts.Count); } }