using System; using System.Linq; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using Unity.Mathematics; using TMPro; namespace Assets.Scripts { public class GraphHandler : MonoBehaviour { #region PROPERTIES public List Values { get { return values; } } public Vector2Int XAxisRange { get { return xAxisRange; } } public Vector2 ActivePointValue { get { return activePointValue; } } public Vector2 BottomLeft { get { return bottomLeft; } } public Vector2 TopRight { get { return topRight; } } public Vector2 Center { get { return center; } } #endregion #region VARIABLES public bool updateGraph = false; private GraphSettings GS; [SerializeField] private Canvas canvas; private RectTransform graph; private RectTransform graphContent; private Vector2 contentScale = Vector2.zero; private List values; private List sortedIndices; private Vector2Int xAxisRange = new Vector2Int(-1, -1); private Vector2Int prevXAxisRange = new Vector2Int(-1, -1); public int activePointIndex = -1; private Vector2 activePointValue = Vector2.zero; private bool pointIsActive = false; private List points; private List pointImages; private List pointRects; private List pointOutlines; private List pointOutlineRects; private List pointOutlineImages; private List lines; private List lineRects; private List lineImages; private List xGridRects; private List xGridImages; private List xAxisTexts; private List xAxisTextRects; private List yGridRects; private List yGridImages; private List yAxisTexts; private List yAxisTextRects; private RectTransform zoomSelectionRectTransform; private Image zoomSelectionImage; private List zoomSelectionOutlines; private List zoomSelectionOutlineImages; private RectTransform pointSelectionRectTransform; private Image pointSelectionImage; private List pointSelectionOutlines; private List pointSelectionOutlineImages; private RectTransform maskObj; private Image backgroundImage; private RectTransform backgroundRect; private GameObject pointParent; private GameObject lineParent; private GameObject gridParent; private GameObject outlineParent; private List outlines; private List outlineImages; private List lockedHoveredPoints; private List lockedPoints; private List fixedHoveredPoints; public int fixedPointIndex = -1; private Vector2 contentOffset = Vector2.zero; private Vector2 bottomLeft, topRight, center; public MouseActionType mouseActionType; public RectangleType rectangleType; public RectangleSelectionType rectangleSelectionType; public RectangleSelectionPhase rectangleSelectionPhase; public PointSelectionType pointSelectionType; private List initialLockedPoints; private List recentlyLockedPoints; private bool mouseInsideBounds = false; private Vector2 mousePos; private Vector2 previousMousePos; private Vector2 initialMousePos = Vector2.zero; private bool initialMouseInsideBounds = false; private Vector2 zoomPoint = Vector2.zero; private Vector2 absoluteZoomPoint = Vector2.zero; public Vector2 targetZoom = new Vector2(1f, 1f); private Vector2 zoom = new Vector2(1f, 1f); private Vector2 moveOffset; public Vector2 targetMoveOffset; private Vector2 initialMoveOffset = Vector2.zero; private float timeToUpdateMouse = 0; private float timeToUpdateTouch = 0; private float timeToUpdateScroll = 0; private bool error; #endregion #region EVENTS #endregion #region ENDPOINTS public void CreatePoint(Vector2 newValue) { CreatePointInternal(newValue); } public void ChangePoint(int indexToChange, Vector2 newValue) { ChangePointInternal(indexToChange, newValue); } public void SetCornerValues(Vector2 newBottomLeft, Vector2 newTopRight) { SetCornerValuesInternal(newBottomLeft, newTopRight); } public void UpdateGraph() { UpdateGraphInternal(UpdateMethod.All); } #endregion #region METHODS private void PrepareGraph() { if(canvas == null) { canvas = new GameObject("GraphCanvas").AddComponent(); } canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.gameObject.AddComponent(); if(GetComponent() == null) this.gameObject.AddComponent(); graph = this.gameObject.GetComponent(); graph.SetParent(canvas.transform); graph.anchoredPosition = Vector2.zero; graph.sizeDelta = GS.GraphSize; maskObj = new GameObject("MaskObj").AddComponent(); maskObj.SetParent(graph); maskObj.anchoredPosition = Vector2.zero; maskObj.gameObject.AddComponent(); Mask mask = maskObj.gameObject.AddComponent(); mask.showMaskGraphic = false; backgroundRect = new GameObject("Background").AddComponent(); backgroundRect.SetParent(maskObj); backgroundRect.anchoredPosition = Vector2.zero; backgroundImage = backgroundRect.gameObject.AddComponent(); graphContent = new GameObject("GraphContent").AddComponent(); graphContent.SetParent(backgroundRect.transform); graphContent.sizeDelta = Vector2.zero; gridParent = CreateParent("GridParent"); lineParent = CreateParent("LineParent"); pointParent = CreateParent("PointParent"); outlineParent = CreateParent("OutlineParent"); CreateOutlines(); outlineParent.transform.SetParent(graph); fixedPointIndex = -1; //SetCornerValues(Vector2.zero, new Vector2(3f, 3f * GS.GraphSize.y / GS.GraphSize.x)); CreateselectionTypes(); UpdateGraphInternal(UpdateMethod.All); } private GameObject CreateParent(string name) { GameObject parent = new GameObject(name); parent.transform.SetParent(name == "OutlineParent" ? graph : graphContent); Image image = parent.AddComponent(); image.color = new Color(0, 0, 0, 0); image.raycastTarget = false; parent.GetComponent().anchoredPosition = Vector2.zero; return parent; } private void CreateOutlines() { for(int i = 0; i < 4; i++) { Image outlineImage = new GameObject("Outline").AddComponent(); RectTransform outline = outlineImage.GetComponent(); outline.SetParent(outlineParent.transform); outlineImage.color = GS.OutlineColor; outlineImage.raycastTarget = false; outlines.Add(outline); outlineImages.Add(outlineImage); } } private void CreatePointInternal(Vector2 value) { int i = points.Count; GameObject outline = CreatePointOutline(i); GameObject point = new GameObject("Point" + i); points.Add(point); values.Add(value); point.transform.SetParent(outline.transform); Image image = point.AddComponent(); image.color = GS.PointColor; pointImages.Add(image); RectTransform pointRectTransform = point.GetComponent(); pointRectTransform.sizeDelta = Vector2.one * GS.PointRadius; pointRects.Add(pointRectTransform); image.sprite = GS.PointSprite; EventTrigger trigger = point.AddComponent(); var eventTypes = new[] { new { Type = EventTriggerType.PointerEnter, Callback = (Action) (() => MouseTrigger(i, true)) }, new { Type = EventTriggerType.PointerExit, Callback = (Action) (() => MouseTrigger(i, false)) }, new { Type = EventTriggerType.PointerClick, Callback = (Action) (() => PointClicked(i)) } }; foreach (var eventType in eventTypes) { EventTrigger.Entry entry = new EventTrigger.Entry { eventID = eventType.Type }; entry.callback.AddListener((data) => { eventType.Callback(); }); trigger.triggers.Add(entry); } if (points.Count > 1) { GameObject line = new GameObject("Line"); line.transform.SetParent(lineParent.transform); lineImages.Add(line.AddComponent()); line.GetComponent().color = GS.LineColor; lineRects.Add(line.GetComponent()); lines.Add(line); if(value.x < bottomLeft.x || value.x > topRight.x) line.SetActive(false); } lockedHoveredPoints.Add(i); SortIndices(); if(value.x < bottomLeft.x || value.x > topRight.x) outline.SetActive(false); } private void ChangePointInternal(int index, Vector2 newValue) { values[index] = newValue; SortIndices(); } private void SortIndices() { sortedIndices = values.Select((vector, index) => new { vector, index }) .OrderBy(item => item.vector.x) .ThenBy(item => item.vector.y) .Select(item => item.index) .ToList(); } private GameObject CreatePointOutline(int i) { GameObject outline = new GameObject("PointOutline" + i); pointOutlines.Add(outline); if (pointParent != null) { outline.transform.SetParent(pointParent.transform); } Image image = outline.AddComponent(); image.color = GS.PointColor; pointOutlineImages.Add(image); RectTransform rectTransform = outline.GetComponent(); rectTransform.sizeDelta = new Vector2(GS.PointRadius, GS.PointRadius); pointOutlineRects.Add(rectTransform); Sprite sprite = GS.PointSprite; image.sprite = sprite; return outline; } private void CreateGridLines(bool createX) { if(createX) { GameObject xGrid = new GameObject("xGrid" + xGridRects.Count); xGrid.transform.SetParent(gridParent.transform); Image xGridImage = xGrid.AddComponent(); xGridImage.raycastTarget = false; xGridRects.Add(xGrid.GetComponent()); xGridImages.Add(xGridImage); if(xGridRects.Count > 1) { TextMeshProUGUI xText = new GameObject("xText" + xGridRects.Count).AddComponent(); RectTransform textRect = xText.gameObject.GetComponent(); textRect.SetParent(xGrid.GetComponent()); xText.font = GS.GridTextFont; xText.fontStyle = FontStyles.Bold; xText.fontStyle = FontStyles.Bold; xText.alignment = TextAlignmentOptions.Center; xText.verticalAlignment = VerticalAlignmentOptions.Middle; xText.color = GS.XAxisTextColor; xText.enableAutoSizing = true; textRect.sizeDelta = Vector2.one * GS.XAxisTextSize; xText.raycastTarget = false; xAxisTexts.Add(xText); xAxisTextRects.Add(textRect); } } else { GameObject yGrid = new GameObject("yGrid" + yGridRects.Count); yGrid.transform.SetParent(gridParent.transform); Image yGridImage = yGrid.AddComponent(); yGridImage.raycastTarget = false; yGridRects.Add(yGrid.GetComponent()); yGridImages.Add(yGridImage); if(yGridRects.Count > 1) { TextMeshProUGUI yText = new GameObject("yText" + yGridRects.Count).AddComponent(); RectTransform textRect = yText.gameObject.GetComponent(); textRect.SetParent(yGrid.GetComponent()); yText.font = GS.GridTextFont; yText.fontStyle = FontStyles.Bold; yText.alignment = TextAlignmentOptions.Center; yText.verticalAlignment = VerticalAlignmentOptions.Middle; yText.color = GS.YAxisTextColor; yText.enableAutoSizing = true; textRect.sizeDelta = Vector2.one * GS.YAxisTextSize; yText.raycastTarget = false; yAxisTexts.Add(yText); yAxisTextRects.Add(textRect); } } } private void CreateselectionTypes() { GameObject selectionParent = new GameObject("SelectionParent"); selectionParent.transform.SetParent(graphContent); selectionParent.AddComponent().anchoredPosition = new Vector2(0f, 0f); zoomSelectionImage = new GameObject("ZoomSelection").AddComponent(); zoomSelectionRectTransform = zoomSelectionImage.GetComponent(); zoomSelectionRectTransform.SetParent(selectionParent.transform); for(int i = 0; i < 4; i++) { Image image = new GameObject("Outline").AddComponent(); RectTransform rect = image.GetComponent(); rect.SetParent(zoomSelectionRectTransform); zoomSelectionOutlineImages.Add(image); zoomSelectionOutlines.Add(rect); } zoomSelectionRectTransform.gameObject.SetActive(false); pointSelectionImage = new GameObject("PointSelection").AddComponent(); pointSelectionRectTransform = pointSelectionImage.GetComponent(); pointSelectionRectTransform.SetParent(selectionParent.transform); for(int i = 0; i < 4; i++) { Image image = new GameObject("Outline").AddComponent(); RectTransform rect = image.GetComponent(); rect.SetParent(pointSelectionRectTransform); pointSelectionOutlineImages.Add(image); pointSelectionOutlines.Add(rect); } pointSelectionRectTransform.gameObject.SetActive(false); } private void CheckIfUpdateGraph() { CalculateMousePosition(); if(mouseInsideBounds) { if(Input.GetMouseButtonDown(0) || Input.GetMouseButton(0) || Input.GetMouseButtonUp(0)) { timeToUpdateMouse = GS.updatePeriod; } if(Input.touchCount > 0) { timeToUpdateTouch = GS.updatePeriod; } if(Input.mouseScrollDelta.y != 0) { timeToUpdateScroll = GS.updatePeriod; } } if(timeToUpdateMouse > 0) UpdateGraphInternal(UpdateMethod.UpdatePositionAndScale | UpdateMethod.UpdatePointVisuals | UpdateMethod.UpdateContent | UpdateMethod.MouseAction | UpdateMethod.UpdateGridLines); if(timeToUpdateTouch > 0) UpdateGraphInternal(UpdateMethod.UpdatePositionAndScale | UpdateMethod.UpdatePointVisuals | UpdateMethod.UpdateContent | UpdateMethod.MouseZoom | UpdateMethod.MouseAction | UpdateMethod.UpdateGridLines); if(timeToUpdateScroll > 0) UpdateGraphInternal(UpdateMethod.UpdatePositionAndScale | UpdateMethod.UpdatePointVisuals | UpdateMethod.UpdateContent | UpdateMethod.MouseZoom | UpdateMethod.UpdateGridLines); timeToUpdateMouse -= Time.deltaTime; timeToUpdateTouch -= Time.deltaTime; timeToUpdateScroll -= Time.deltaTime; } public void UpdateGraphInternal(UpdateMethod methodsToUpdate) { if (methodsToUpdate.HasFlag(UpdateMethod.UpdatePositionAndScale) || methodsToUpdate.HasFlag(UpdateMethod.All)) UpdatePositionAndScale(); CalculateCornerValues(); if (methodsToUpdate.HasFlag(UpdateMethod.UpdateOutlines) || methodsToUpdate.HasFlag(UpdateMethod.All)) UpdateOutlines(); if (methodsToUpdate.HasFlag(UpdateMethod.UpdateContent) || methodsToUpdate.HasFlag(UpdateMethod.All)) HandleActiveObjects(); if (methodsToUpdate.HasFlag(UpdateMethod.UpdatePointVisuals) || methodsToUpdate.HasFlag(UpdateMethod.All)) UpdatePointVisuals(); if (methodsToUpdate.HasFlag(UpdateMethod.UpdateContent) || methodsToUpdate.HasFlag(UpdateMethod.All)) UpdateContent(); CalculateMousePosition(); if (methodsToUpdate.HasFlag(UpdateMethod.MouseZoom) || methodsToUpdate.HasFlag(UpdateMethod.All)) MouseZoom(); if (methodsToUpdate.HasFlag(UpdateMethod.MouseAction) || methodsToUpdate.HasFlag(UpdateMethod.All)) MouseAction(); if (methodsToUpdate.HasFlag(UpdateMethod.UpdateGridLines) || methodsToUpdate.HasFlag(UpdateMethod.All)) UpdateGridLines(); } private void UpdatePositionAndScale() { contentScale = GS.GraphScale * zoom; maskObj.sizeDelta = GS.GraphSize; contentOffset = absoluteZoomPoint - zoomPoint * contentScale - moveOffset; graphContent.anchoredPosition = -GS.GraphSize / 2 + contentOffset; graph.sizeDelta = GS.GraphSize; backgroundRect.sizeDelta = GS.GraphSize; backgroundImage.color = GS.BackgroundColor; } private void UpdateOutlines() { for (int i = 0; i < outlines.Count; i++) { if (i % 2 == 0) // Left and Right outlines { outlines[i].sizeDelta = new Vector2(GS.OutlineWidth, GS.GraphSize.y + GS.OutlineWidth * 2); outlines[i].anchoredPosition = new Vector2((i == 0 ? -1 : 1) * (GS.GraphSize.x + GS.OutlineWidth) / 2, 0); } else // Top and Bottom outlines { outlines[i].sizeDelta = new Vector2(GS.GraphSize.x + GS.OutlineWidth * 2, GS.OutlineWidth); outlines[i].anchoredPosition = new Vector2(0, (i == 1 ? -1 : 1) * (GS.GraphSize.y + GS.OutlineWidth) / 2); } outlineImages[i].color = GS.OutlineColor; } } private void CalculateCornerValues() { topRight = new Vector2(Mathf.Clamp(topRight.x, bottomLeft.x, Mathf.Infinity), Mathf.Clamp(topRight.y, bottomLeft.y, Mathf.Infinity)); bottomLeft = -contentOffset / contentScale; topRight = bottomLeft + GS.GraphSize / contentScale; center = (topRight - bottomLeft) / 2f + bottomLeft; } private void UpdateContent() { if (xAxisRange.x == -1 || xAxisRange.y == -1) return; Vector2 bounds = new Vector2(bottomLeft.y, topRight.y); for (int i = xAxisRange.x - 1; i <= xAxisRange.y + 1; i++) { if (i < 0 || i > sortedIndices.Count - 1) continue; int index = sortedIndices[i]; float currentValue = values[index].y; float prevValue = values[Mathf.Clamp(index - 1, 0, values.Count - 1)].y; float nextValue = values[Mathf.Clamp(index + 1, 0, values.Count - 1)].y; if ((currentValue < bounds.x && prevValue < bounds.x && nextValue < bounds.x) || (currentValue > bounds.y && prevValue > bounds.y && nextValue > bounds.y)) { continue; } UpdateAnchoredPosition(pointOutlineRects[index], CalculatePosition(i)); if (lines.Count > 0 && index < lines.Count) { Vector2 point1 = CalculatePosition(index); Vector2 point2 = CalculatePosition(index + 1); float distance = Vector2.Distance(point1, point2); UpdateAnchoredPosition(lineRects[index], (point2 + point1) / 2f); UpdateSizeDelta(lineRects[index], new Vector2(distance, GS.LineWidth)); Vector2 direction = point2 - point1; float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; lineRects[index].rotation = Quaternion.AngleAxis(angle, Vector3.forward); lineImages[index].color = GS.LineColor; } } } private void HandleActiveObjects() { if(prevXAxisRange.x < xAxisRange.x) { for(int i = prevXAxisRange.x - 1; i < xAxisRange.x - 1; i++) { if(i < 0) continue; pointOutlines[sortedIndices[i]].SetActive(false); if(i < lines.Count) lines[sortedIndices[i]].SetActive(false); } } else if(prevXAxisRange.x > xAxisRange.x && xAxisRange.x >= 0) { for(int i = xAxisRange.x - 1; i < prevXAxisRange.x; i++) { if(i < 0) continue; pointOutlines[sortedIndices[i]].SetActive(true); if(i < lines.Count) lines[sortedIndices[i]].SetActive(true); } } if(prevXAxisRange.y > xAxisRange.y) { for(int i = xAxisRange.y + 2; i <= prevXAxisRange.y + 2; i++) { if(i > pointOutlines.Count - 1 || i < 0) continue; pointOutlines[sortedIndices[i]].SetActive(false); if(i < lines.Count) lines[sortedIndices[i]].SetActive(false); } } else if(xAxisRange.y > prevXAxisRange.y) { for(int i = prevXAxisRange.y + 2; i <= xAxisRange.y + 1; i++) { if(i > pointOutlines.Count - 1 || i < 0) continue; pointOutlines[sortedIndices[i]].SetActive(true); if(i < lines.Count) lines[sortedIndices[i]].SetActive(true); } } prevXAxisRange = xAxisRange; xAxisRange = new Vector2Int(MinMaxBinarySearch(true), MinMaxBinarySearch(false)); } private Vector2 CalculatePosition(int i) { return values[i] * contentScale; } private void MouseTrigger(int pointIndex, bool enter) { fixedHoveredPoints.Add(pointIndex); fixedHoveredPoints = fixedHoveredPoints.Distinct().ToList(); if (enter) { activePointIndex = pointIndex; activePointValue = values[pointIndex]; pointIsActive = enter; if (pointSelectionType == PointSelectionType.Select) { lockedHoveredPoints.Add(pointIndex); lockedHoveredPoints = lockedHoveredPoints.Distinct().ToList(); } } else { // Do not process PointerExit event for locked points if (lockedPoints.Contains(activePointIndex) && pointSelectionType == PointSelectionType.Select || fixedPointIndex == activePointIndex && pointSelectionType == PointSelectionType.FixZoomPoint) { return; } activePointIndex = pointIndex; activePointValue = values[pointIndex]; pointIsActive = enter; } } private void PointClicked(int pointIndex) { if (pointSelectionType == PointSelectionType.FixZoomPoint) { if (fixedPointIndex != -1) { ChangeZoomPoint(values[pointIndex]); fixedHoveredPoints.Add(fixedPointIndex); } fixedHoveredPoints = fixedHoveredPoints.Distinct().ToList(); fixedPointIndex = (fixedPointIndex == pointIndex) ? -1 : pointIndex; ChangeZoomPoint((fixedPointIndex == -1) ? center : values[pointIndex]); } else { if (lockedPoints.Contains(pointIndex)) { lockedPoints.Remove(pointIndex); } else { lockedPoints.Add(pointIndex); } } } private void UpdatePointVisuals() { if(xAxisRange.x == -1 || xAxisRange.y == -1) return; for(int i = xAxisRange.x; i <= xAxisRange.y; i++) { if(activePointIndex == sortedIndices[i]) continue; lockedHoveredPoints.Add(sortedIndices[i]); fixedHoveredPoints.Add(sortedIndices[i]); } } private void UpdatePoints() { for (int i = 0; i < lockedHoveredPoints.Count; i++) { Vector2 targetSize; Color targetColor; float targetSpeed; int numToUpdate = lockedHoveredPoints[i]; bool isActive = activePointIndex == numToUpdate && pointIsActive; if (lockedPoints.Contains(numToUpdate)) { targetSize = Vector2.one * GS.PointLockedRadius; targetColor = GS.PointLockedColor; targetSpeed = GS.PointLockedSpeed; } else if (isActive && pointSelectionType == PointSelectionType.Select) { targetSize = Vector2.one * GS.PointHoverRadius; targetColor = GS.PointHoverColor; targetSpeed = GS.PointHoverSpeed; } else { targetSize = Vector2.one * GS.PointRadius; targetColor = GS.PointColor; targetSpeed = GS.PointHoverSpeed; } pointRects[numToUpdate].sizeDelta = Vector2.Lerp(pointRects[numToUpdate].sizeDelta, targetSize, Time.deltaTime * targetSpeed); pointImages[numToUpdate].color = Color.Lerp(pointImages[numToUpdate].color, targetColor, Time.deltaTime * targetSpeed); if (!isActive && Vector2.Distance(pointRects[numToUpdate].sizeDelta, targetSize) < 0.5f && Vector4.Distance(pointImages[numToUpdate].color, targetColor) < 0.5f) { pointImages[numToUpdate].color = targetColor; pointRects[numToUpdate].sizeDelta = targetSize; lockedHoveredPoints.RemoveAt(i); } else if(!fixedHoveredPoints.Contains(numToUpdate) && numToUpdate != fixedPointIndex) { pointOutlineRects[numToUpdate].sizeDelta = pointRects[numToUpdate].sizeDelta + Vector2.one * GS.UnfixedPointOutlineWidth; } } UpdatePointOutlines(); } private void UpdatePointOutlines() { for (int i = 0; i < fixedHoveredPoints.Count; i++) { Vector2 targetSize; Color targetColor; float targetSpeed; int numToUpdate = fixedHoveredPoints[i]; bool isActive = activePointIndex == numToUpdate && pointIsActive; if (fixedPointIndex == numToUpdate) { targetSize = new Vector2(GS.FixedPointOutlineWidth, GS.FixedPointOutlineWidth); targetColor = GS.FixedPointOutlineColor; targetSpeed = GS.FixedPointOutlineSpeed; } else if (isActive && pointSelectionType == PointSelectionType.FixZoomPoint) { targetSize = new Vector2(GS.UnfixedPointOutlineHoverWidth, GS.UnfixedPointOutlineHoverWidth); targetColor = GS.UnfixedPointOutlineHoverColor; targetSpeed = GS.UnfixedPointOutlineHoverSpeed; } else { targetSize = new Vector2(GS.UnfixedPointOutlineWidth, GS.UnfixedPointOutlineWidth); targetColor = GS.UnfixedPointOutlineColor; targetSpeed = GS.UnfixedPointOutlineHoverSpeed; } targetSize += pointRects[numToUpdate].sizeDelta; RectTransform outlineRectTransform = pointOutlineRects[numToUpdate]; if (pointSelectionType == PointSelectionType.FixZoomPoint) outlineRectTransform.sizeDelta = Vector2.Lerp(outlineRectTransform.sizeDelta, targetSize, Time.deltaTime * targetSpeed); else outlineRectTransform.sizeDelta = targetSize; Image outlineImage = pointOutlineImages[numToUpdate]; targetColor = Color.Lerp(outlineImage.color, targetColor, Time.deltaTime * targetSpeed); outlineImage.color = targetColor; if (!isActive && Vector2.Distance(outlineRectTransform.sizeDelta, targetSize) < 0.5f) fixedHoveredPoints.RemoveAt(i); } } private void UpdateGridLines() { Vector2 GridStartPoint; Vector2 spacing = CalculateGridSpacing(); GridStartPoint = new Vector2(Mathf.Ceil(bottomLeft.x * spacing.x) / spacing.x, Mathf.Ceil(bottomLeft.y * spacing.y) / spacing.y) * contentScale; int2 eventualOverlay = new int2(-1, -1); int requiredYGridlines = Mathf.CeilToInt((topRight.y - bottomLeft.y) * spacing.y) + 1; int requiredXGridlines = Mathf.CeilToInt((topRight.x - bottomLeft.x) * spacing.x) + 1; while(xGridRects.Count <= requiredXGridlines) { CreateGridLines(true); } while(yGridRects.Count <= requiredYGridlines) { CreateGridLines(false); } for (int i = 0; i < requiredXGridlines; i++) { RectTransform rect = xGridRects[i]; Image rectImage = xGridImages[i]; if(!rect.gameObject.activeSelf) rect.gameObject.SetActive(true); if (i == 0) { UpdateSizeDelta(rect, new Vector2(GS.XAxisWidth, GS.GraphSize.y * 2f)); rectImage.color = GS.XAxisColor; UpdateAnchoredPosition(rect, new Vector2(0, center.y * contentScale.y)); } else { UpdateSizeDelta(rect, new Vector2(GS.XGridWidth, GS.GraphSize.y * 2f)); rectImage.color = GS.XGridColor; if (Mathf.Round(GridStartPoint.x + (i + eventualOverlay.x) / spacing.x * contentScale.x) == 0) eventualOverlay.x = 0; UpdateAnchoredPosition(rect, new Vector2(GridStartPoint.x + (i + eventualOverlay.x) / spacing.x * contentScale.x, center.y * contentScale.y)); UpdateSizeDelta(xAxisTextRects[i - 1], new Vector2(1f / spacing.x * contentScale.x, GS.XAxisTextSize)); UpdateAnchoredPosition(xAxisTextRects[i - 1], new Vector2(0, -center.y * contentScale.y + GS.XAxisTextOffset)); xAxisTexts[i - 1].text = Mathf.Floor(1f / spacing.x) > 0 ? Mathf.RoundToInt(GridStartPoint.x / contentScale.x + (i + eventualOverlay.x) / spacing.x).ToString() : (GridStartPoint.x / contentScale.x + (i + eventualOverlay.x) / spacing.x).ToString("R"); } } for (int i = 0; i < requiredYGridlines; i++) { RectTransform rect = yGridRects[i]; Image rectImage = yGridImages[i]; if(!rect.gameObject.activeSelf) rect.gameObject.SetActive(true); if (i == 0) { UpdateSizeDelta(rect, new Vector2(GS.GraphSize.x * 2f, GS.YAxisWidth)); rectImage.color = GS.YAxisColor; UpdateAnchoredPosition(rect, new Vector2(center.x * contentScale.x, 0)); } else { UpdateSizeDelta(rect, new Vector2(GS.GraphSize.x * 2f, GS.YGridWidth)); rectImage.color = GS.YGridColor; if (Mathf.Round(GridStartPoint.y + (i + eventualOverlay.y) / spacing.y * contentScale.y) == 0) eventualOverlay.y = 0; UpdateAnchoredPosition(rect, new Vector2(center.x * contentScale.x, GridStartPoint.y + (i + eventualOverlay.y) / spacing.y * contentScale.y)); UpdateSizeDelta(yAxisTextRects[i - 1], new Vector2(1f / spacing.x * contentScale.x, GS.XAxisTextSize)); UpdateAnchoredPosition(yAxisTextRects[i - 1], new Vector2(-center.x * contentScale.x + GS.YAxisTextOffset, 0)); yAxisTexts[i - 1].text = Mathf.Floor(1f / spacing.y) > 0 ? Mathf.RoundToInt(GridStartPoint.y / contentScale.y + (i + eventualOverlay.y) / spacing.y).ToString() : (GridStartPoint.y / contentScale.y + (i + eventualOverlay.y) / spacing.y).ToString("R"); } } for(int i = requiredXGridlines; i < xGridRects.Count; i++) { if(xGridRects[i].gameObject.activeSelf) xGridRects[i].gameObject.SetActive(false); } for(int i = requiredYGridlines; i < yGridRects.Count; i++) { if(yGridRects[i].gameObject.activeSelf) yGridRects[i].gameObject.SetActive(false); } } private Vector2 CalculateGridSpacing() { int exponentX = Mathf.FloorToInt(Mathf.Log(zoom.x, 2)); int exponentY = Mathf.FloorToInt(Mathf.Log(zoom.y, 2)); float closestX = Mathf.Pow(2, exponentX); float closestY = Mathf.Pow(2, exponentY); return new Vector2(closestX, closestY) * GS.GridSpacing; } private void SetCornerValuesInternal(Vector2 newBottomLeft, Vector2 newTopRight) { Vector2 newCenter = (newTopRight - newBottomLeft) / 2f + newBottomLeft; targetMoveOffset = (newCenter - center) * contentScale + moveOffset; ChangeZoomPoint(newCenter); targetZoom = GS.GraphSize / GS.GraphScale / (newTopRight - newBottomLeft); } private void CalculateMousePosition() { mousePos = (new Vector2(Input.mousePosition.x, Input.mousePosition.y) - new Vector2(graphContent.transform.position.x, graphContent.transform.position.y)) / contentScale; mouseInsideBounds = mousePos.x > bottomLeft.x && mousePos.y > bottomLeft.y && mousePos.x < topRight.x && mousePos.y < topRight.y; mousePos = new Vector2(Mathf.Clamp(mousePos.x, bottomLeft.x, topRight.x), Mathf.Clamp(mousePos.y, bottomLeft.y, topRight.y)); } private void MouseZoom() { if(!mouseInsideBounds) return; if(Input.mouseScrollDelta.y == 0) return; if(fixedPointIndex == -1) ChangeZoomPoint(mousePos); targetZoom = zoom + Input.mouseScrollDelta.y * zoom * GS.ZoomSpeed / 100f; } private void ChangeZoomPoint(Vector2 newZoomPoint) { absoluteZoomPoint = (newZoomPoint -zoomPoint) * contentScale + absoluteZoomPoint; zoomPoint = newZoomPoint; } private void MouseAction() { if(Input.GetMouseButtonDown(0)) { initialMouseInsideBounds = mouseInsideBounds; if(!mouseInsideBounds) return; initialMousePos = Input.mousePosition; if(mouseActionType == MouseActionType.Move) initialMoveOffset = moveOffset; else if(mouseActionType == MouseActionType.SelectPoints) { initialLockedPoints.Clear(); for(int i = 0; i < lockedPoints.Count; i++) initialLockedPoints.Add(lockedPoints[i]); } return; } if(Input.GetMouseButton(0) && initialMouseInsideBounds) { if(Input.GetMouseButtonDown(1)) { initialMouseInsideBounds = false; zoomSelectionRectTransform.gameObject.SetActive(false); pointSelectionRectTransform.gameObject.SetActive(false); } if(previousMousePos != mousePos) { Vector2 currentMousePos = Input.mousePosition; if(mouseActionType == MouseActionType.Move) targetMoveOffset = (initialMousePos - currentMousePos) + initialMoveOffset; else if(mouseActionType == MouseActionType.SelectAreaToZoom) { if(!zoomSelectionRectTransform.gameObject.activeSelf) zoomSelectionRectTransform.gameObject.SetActive(true); SelectAreaToZoom(false); } else if (mouseActionType == MouseActionType.SelectPoints) { if(!pointSelectionRectTransform.gameObject.activeSelf) pointSelectionRectTransform.gameObject.SetActive(true); SelectPoints(false); } } previousMousePos = mousePos; } else if(Input.GetMouseButtonUp(0) && initialMouseInsideBounds) { if(mouseActionType == MouseActionType.SelectAreaToZoom) SelectAreaToZoom(true); else if(mouseActionType == MouseActionType.SelectPoints) SelectPoints(true); recentlyLockedPoints.Clear(); } if (Input.touchCount == 1) { Touch touch = Input.GetTouch(0); if(touch.phase == TouchPhase.Began) { initialMousePos = touch.position; mousePos = (new Vector2(touch.position.x, touch.position.y) - new Vector2(graphContent.transform.position.x, graphContent.transform.position.y)) / contentScale; initialMouseInsideBounds = mousePos.x > bottomLeft.x && mousePos.y > bottomLeft.y && mousePos.x < topRight.x && mousePos.y < topRight.y; if(mouseActionType == MouseActionType.Move) initialMoveOffset = moveOffset; else if(mouseActionType == MouseActionType.SelectPoints) { initialLockedPoints.Clear(); for(int i = 0; i < lockedPoints.Count; i++) initialLockedPoints.Add(lockedPoints[i]); } } else if(touch.phase == TouchPhase.Moved && initialMouseInsideBounds) { if(mouseActionType == MouseActionType.Move) { Vector2 currentTouchPos = touch.position; targetMoveOffset = (initialMousePos - currentTouchPos) + initialMoveOffset; } else if(mouseActionType == MouseActionType.SelectAreaToZoom) { if(!zoomSelectionRectTransform.gameObject.activeSelf) zoomSelectionRectTransform.gameObject.SetActive(true); SelectAreaToZoom(false); } else if(mouseActionType == MouseActionType.SelectPoints) { if(!pointSelectionRectTransform.gameObject.activeSelf) pointSelectionRectTransform.gameObject.SetActive(true); SelectPoints(false); } } else if(touch.phase == TouchPhase.Ended && initialMouseInsideBounds) { if(mouseActionType == MouseActionType.SelectAreaToZoom) SelectAreaToZoom(true); else if(mouseActionType == MouseActionType.SelectPoints) SelectPoints(true); recentlyLockedPoints.Clear(); } } } private void SelectAreaToZoom(bool release) { Vector2 relativeInitialMousePos = (new Vector2(initialMousePos.x, initialMousePos.y) - new Vector2(graphContent.transform.position.x, graphContent.transform.position.y)) / contentScale; Vector2 preserveAspectRatioCorner = new Vector2(mousePos.x, relativeInitialMousePos.y + (mousePos.x - relativeInitialMousePos.x) / GS.GraphSize.x * GS.GraphSize.y * contentScale.x / contentScale.y); Vector2 originalAspectRatioCorner = new Vector2(mousePos.x, relativeInitialMousePos.y + (mousePos.x - relativeInitialMousePos.x)); Vector2 newCorner = rectangleType == RectangleType.Free ? mousePos : (rectangleType == RectangleType.PreserveAspectRatio ? preserveAspectRatioCorner : originalAspectRatioCorner); if(!release) { zoomSelectionRectTransform.anchoredPosition = (relativeInitialMousePos + (newCorner - relativeInitialMousePos) / 2f) * contentScale; Vector2 areaSize = new Vector2(Mathf.Abs(newCorner.x - relativeInitialMousePos.x), Mathf.Abs(newCorner.y - relativeInitialMousePos.y)) * contentScale; zoomSelectionRectTransform.sizeDelta = areaSize; zoomSelectionImage.color = GS.ZoomSelectionColor; for(int i = 0; i < zoomSelectionOutlines.Count; i++) { if (i % 2 == 0) // Left and Right outlines { zoomSelectionOutlines[i].sizeDelta = new Vector2(GS.ZoomSelectionOutlineWidth, areaSize.y + GS.ZoomSelectionOutlineWidth * 2); zoomSelectionOutlines[i].anchoredPosition = new Vector2((i == 0 ? -1 : 1) * (areaSize.x + GS.ZoomSelectionOutlineWidth) / 2, 0); } else // Top and Bottom outlines { zoomSelectionOutlines[i].sizeDelta = new Vector2(areaSize.x + GS.ZoomSelectionOutlineWidth * 2, GS.ZoomSelectionOutlineWidth); zoomSelectionOutlines[i].anchoredPosition = new Vector2(0, (i == 1 ? -1 : 1) * (areaSize.y + GS.ZoomSelectionOutlineWidth) / 2); } zoomSelectionOutlineImages[i].color = GS.ZoomSelectionOutlineColor; } } else { zoomSelectionRectTransform.gameObject.SetActive(false); Vector2 newBottomLeft = new Vector2(Mathf.Min(relativeInitialMousePos.x, newCorner.x), Mathf.Min(relativeInitialMousePos.y, newCorner.y)); Vector2 newTopRight = new Vector2(Mathf.Max(relativeInitialMousePos.x, newCorner.x), Mathf.Max(relativeInitialMousePos.y, newCorner.y)); if((newTopRight - newBottomLeft).magnitude > (topRight - bottomLeft).magnitude / 16f) SetCornerValues(newBottomLeft, newTopRight); } } private void SelectPoints(bool release) { Vector2 relativeInitialMousePos = (new Vector2(initialMousePos.x, initialMousePos.y) - new Vector2(graphContent.transform.position.x, graphContent.transform.position.y)) / contentScale; if (!release) { Vector2 newCorner = mousePos; pointSelectionRectTransform.anchoredPosition = (relativeInitialMousePos + (newCorner - relativeInitialMousePos) / 2f) * contentScale; Vector2 areaSize = new Vector2(Mathf.Abs(newCorner.x - relativeInitialMousePos.x), Mathf.Abs(newCorner.y - relativeInitialMousePos.y)) * contentScale; pointSelectionRectTransform.sizeDelta = areaSize; pointSelectionImage.color = GS.PointSelectionColor; for (int i = 0; i < pointSelectionOutlines.Count; i++) { if (i % 2 == 0) // Left and Right outlines { pointSelectionOutlines[i].sizeDelta = new Vector2(GS.PointSelectionOutlineWidth, areaSize.y + GS.PointSelectionOutlineWidth * 2); pointSelectionOutlines[i].anchoredPosition = new Vector2((i == 0 ? -1 : 1) * (areaSize.x + GS.PointSelectionOutlineWidth) / 2, 0); } else // Top and Bottom outlines { pointSelectionOutlines[i].sizeDelta = new Vector2(areaSize.x + GS.PointSelectionOutlineWidth * 2, GS.PointSelectionOutlineWidth); pointSelectionOutlines[i].anchoredPosition = new Vector2(0, (i == 1 ? -1 : 1) * (areaSize.y + GS.PointSelectionOutlineWidth) / 2); } pointSelectionOutlineImages[i].color = GS.PointSelectionOutlineColor; } if(rectangleSelectionPhase == RectangleSelectionPhase.Moving) { Vector2 newBottomLeft = new Vector2(Mathf.Min(relativeInitialMousePos.x, mousePos.x), Mathf.Min(relativeInitialMousePos.y, mousePos.y)); Vector2 newTopRight = new Vector2(Mathf.Max(relativeInitialMousePos.x, mousePos.x), Mathf.Max(relativeInitialMousePos.y, mousePos.y)); PointSelect(true, newBottomLeft, newTopRight); } } else { pointSelectionRectTransform.gameObject.SetActive(false); Vector2 newBottomLeft = new Vector2(Mathf.Min(relativeInitialMousePos.x, mousePos.x), Mathf.Min(relativeInitialMousePos.y, mousePos.y)); Vector2 newTopRight = new Vector2(Mathf.Max(relativeInitialMousePos.x, mousePos.x), Mathf.Max(relativeInitialMousePos.y, mousePos.y)); if(rectangleSelectionPhase == RectangleSelectionPhase.Release) { PointSelect(false, newBottomLeft, newTopRight); } } } private void PointSelect(bool moving, Vector2 newBottomLeft, Vector2 newTopRight) { for (int index = (int)xAxisRange.x; index <= (int)xAxisRange.y; index++) { Vector2 pointValue = values[index]; if (pointValue.x >= newBottomLeft.x && pointValue.x <= newTopRight.x && pointValue.y >= newBottomLeft.y && pointValue.y <= newTopRight.y) { if(rectangleSelectionType == RectangleSelectionType.SelectUnselect) { if(moving) { if (initialLockedPoints.Contains(index) && lockedPoints.Contains(index)) { lockedPoints.Remove(index); recentlyLockedPoints.Add(index); } else if(!initialLockedPoints.Contains(index) && !lockedPoints.Contains(index)) { lockedPoints.Add(index); recentlyLockedPoints.Add(index); } } else { if(lockedPoints.Contains(index)) lockedPoints.Remove(index); else lockedPoints.Add(index); } } else { if(moving) { if(!initialLockedPoints.Contains(index) && !lockedPoints.Contains(index)) { lockedPoints.Add(index); recentlyLockedPoints.Add(index); } } else { if(!lockedPoints.Contains(index)) lockedPoints.Add(index); } } lockedHoveredPoints.Add(index); lockedHoveredPoints = lockedHoveredPoints.Distinct().ToList(); lockedPoints = lockedPoints.Distinct().ToList(); } } for(int i = 0; i < recentlyLockedPoints.Count; i++) { int index = recentlyLockedPoints[i]; Vector2 pointValue = values[index]; if(!(pointValue.x >= newBottomLeft.x && pointValue.x <= newTopRight.x && pointValue.y >= newBottomLeft.y && pointValue.y <= newTopRight.y)) { if(initialLockedPoints.Contains(index)) { if(!lockedPoints.Contains(index)) lockedPoints.Add(index); } else { if(lockedPoints.Contains(index)) lockedPoints.Remove(index); } } lockedHoveredPoints.Add(index); lockedHoveredPoints = lockedHoveredPoints.Distinct().ToList(); lockedPoints = lockedPoints.Distinct().ToList(); } } private int MinMaxBinarySearch(bool findLeft) //important for large numbers of points { //this function finds the points that are closest to the sides of the graph window float target; target = findLeft ? bottomLeft.x : topRight.x; int min = 0; int max = sortedIndices.Count - 1; float value; while (min <= max) { int middle = min + (max - min) / 2; value = values[sortedIndices[middle]].x; if ((findLeft ? value >= target : value <= target)) { if ((findLeft && (middle == 0 || values[sortedIndices[middle - 1]].x < target)) || (!findLeft && (middle == sortedIndices.Count - 1 || values[sortedIndices[middle + 1]].x > target))) { return middle; } if (findLeft) max = middle - 1; else min = middle + 1; } else { if (findLeft) min = middle + 1; else max = middle - 1; } } return -1; } private void UpdateSizeDelta(RectTransform rect, Vector2 size) { if(Mathf.Abs(rect.sizeDelta.x - size.x) > 0.1f || Mathf.Abs(rect.sizeDelta.y - size.y) > 0.1f) rect.sizeDelta = size; } private void UpdateAnchoredPosition(RectTransform rect, Vector2 position) { if(Mathf.Abs(rect.sizeDelta.x - position.x) > 0.1f || Mathf.Abs(rect.sizeDelta.y - position.y) > 0.1f) rect.anchoredPosition = position; } private bool CheckForErrors() { if(GetComponent() == null) { Debug.LogError("This GameObject has no GraphSettings script attached. Attach GraphSettings and restart"); error = true; return true; } if(GetComponent().GridTextFont == null) { Debug.LogError("No font was found. Assign a font for GraphSettings.GridTextFont and restart"); error = true; } if(GetComponent().PointSprite == null) { Debug.LogError("No point sprite was found. Assign a sprite for GraphSettings.PointSprite and restart"); error = true; } if(error) return true; return false; } #endregion #region LIFECYCLE private void Awake() { values = new List(); sortedIndices = new List(); points = new List(); pointRects = new List(); pointImages = new List(); pointOutlines = new List(); pointOutlineRects = new List(); pointOutlineImages = new List(); lines = new List(); lineRects = new List(); lineImages = new List(); xGridRects = new List(); xGridImages = new List(); yGridRects = new List(); yGridImages = new List(); xAxisTexts = new List(); yAxisTexts = new List(); xAxisTextRects = new List(); yAxisTextRects = new List(); zoomSelectionOutlines = new List(); zoomSelectionOutlineImages = new List(); pointSelectionOutlines = new List(); pointSelectionOutlineImages = new List(); outlines = new List(); outlineImages = new List(); lockedHoveredPoints = new List(); lockedPoints = new List(); initialLockedPoints = new List(); recentlyLockedPoints = new List(); fixedHoveredPoints = new List(); } private void Start() { if (CheckForErrors()) return; GS = GetComponent(); PrepareGraph(); ExampleFunction(); } private void Update() { if (Input.GetKey(KeyCode.LeftShift)) mouseActionType = MouseActionType.SelectAreaToZoom; else if (Input.GetKey(KeyCode.LeftControl)) mouseActionType = MouseActionType.SelectPoints; else mouseActionType = MouseActionType.Move; if (error) return; CheckIfUpdateGraph(); /* for(int i = 0; i <= xAxisRange.y + 1; i += 2) { if(xAxisRange.x != -1 && xAxisRange.y != -1) { int index = sortedIndices[i]; values[index] = new Vector2(values[index].x, Mathf.Sin(Time.time / 2f + values[index].x)); lineImages[index].color = new Color(1f, Mathf.Sin(Time.time * 2f + values[index].x) / 2f + 0.35f, 0, 1f); } } for(int i = 1; i <= xAxisRange.y; i += 2) { if(xAxisRange.x != -1 && xAxisRange.y != -1) { int index = sortedIndices[i]; values[index] = new Vector2(values[index].x, -Mathf.Sin(Time.time / 2f + values[index].x)); lineImages[index].color = new Color(1f, Mathf.Sin(Time.time * 2f + values[index].x) / 2f + 0.35f, 0, 1f); } }*/ if (updateGraph) UpdateGraphInternal(UpdateMethod.All); if (lockedHoveredPoints.Count > 0) UpdatePoints(); if (fixedHoveredPoints.Count > 0) UpdatePointOutlines(); zoom = Vector2.Lerp(zoom, targetZoom, GS.SmoothZoomSpeed * Time.deltaTime); moveOffset = Vector2.Lerp(moveOffset, targetMoveOffset, GS.SmoothMoveSpeed * Time.deltaTime); } private void ExampleFunction() { for (float i = 0; i < 50; i += 0.2f) CreatePoint(new Vector2(i, 0.2f * i + Mathf.Sin(i))); UpdateGraph(); } #endregion } public enum MouseActionType { Move, SelectAreaToZoom, SelectPoints } public enum RectangleType { Free, PreserveAspectRatio, OriginalAspectRatio } public enum RectangleSelectionType { SelectAll, SelectUnselect } public enum RectangleSelectionPhase { Moving, Release } public enum PointSelectionType { Select, FixZoomPoint } [Flags] public enum UpdateMethod { UpdatePositionAndScale = 1 << 0, UpdateOutlines = 1 << 1, UpdatePointVisuals = 1 << 2, UpdateContent = 1 << 3, MouseZoom = 1 << 4, MouseAction = 1 << 5, UpdateGridLines = 1 << 6, All = 1 << 7 } }