mirror of
https://github.com/XCharts-Team/XCharts.git
synced 2026-05-14 20:00:09 +00:00
修复SaveAsImage被其他组件遮挡时无法正常保存的问题 (#337)
This commit is contained in:
@@ -80,6 +80,7 @@ slug: /changelog
|
||||
|
||||
## master
|
||||
|
||||
* (2026.02.26) 修复`SaveAsImage`被其他组件遮挡时无法正常保存的问题 (#337)
|
||||
* (2026.02.26) 增加`Axis`的`mainAxis`参数设置主轴可控制柱图的朝向 (#331)
|
||||
* (2026.02.03) 修复`UITable`的`viewport`在不同的锚点下可能会绘制异常的问题
|
||||
* (2026.01.15) 修复`Pie`的点击有时候不响应的问题 (#357)
|
||||
|
||||
@@ -285,7 +285,7 @@ namespace XCharts.Editor
|
||||
}
|
||||
if (GUILayout.Button(Styles.btnSaveAsImage))
|
||||
{
|
||||
m_Chart.SaveAsImage();
|
||||
m_Chart.SaveAsImage("png", "", 4f);
|
||||
}
|
||||
if (m_CheckWarning)
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace XCharts.Editor
|
||||
}
|
||||
if (GUILayout.Button(Styles.btnSaveAsImage))
|
||||
{
|
||||
m_UIComponent.SaveAsImage();
|
||||
m_UIComponent.SaveAsImage("png", "", 4f);
|
||||
}
|
||||
OnDebugEndInspectorGUI();
|
||||
}
|
||||
|
||||
@@ -627,6 +627,8 @@ namespace XCharts.Runtime
|
||||
vh.Clear();
|
||||
var maxPainter = settings.maxPainter;
|
||||
var maxSeries = m_Series.Count;
|
||||
if (painter == null || painter.index < 0 || painter.index >= maxPainter)
|
||||
return;
|
||||
var rate = Mathf.CeilToInt(maxSeries * 1.0f / maxPainter);
|
||||
m_PainterUpper.Refresh();
|
||||
m_PainterTop.Refresh();
|
||||
|
||||
@@ -209,15 +209,20 @@ namespace XCharts.Runtime
|
||||
/// </summary>
|
||||
/// <param name="imageType">type of image: png, jpg, exr</param>
|
||||
/// <param name="savePath">save path</param>
|
||||
public void SaveAsImage(string imageType = "png", string savePath = "")
|
||||
/// <param name="exportScale">export resolution scale. 1 means original size</param>
|
||||
/// <param name="useRecursiveBackgroundColor">whether to recursively use lower-level UI background color</param>
|
||||
public void SaveAsImage(string imageType = "png", string savePath = "", float exportScale = 1f,
|
||||
bool useRecursiveBackgroundColor = false)
|
||||
{
|
||||
StartCoroutine(SaveAsImageSync(imageType, savePath));
|
||||
StartCoroutine(SaveAsImageSync(imageType, savePath, exportScale, useRecursiveBackgroundColor));
|
||||
}
|
||||
|
||||
private IEnumerator SaveAsImageSync(string imageType, string path)
|
||||
private IEnumerator SaveAsImageSync(string imageType, string path, float exportScale,
|
||||
bool useRecursiveBackgroundColor)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
ChartHelper.SaveAsImage(rectTransform, canvas, imageType, path);
|
||||
ChartHelper.SaveAsImage(rectTransform, canvas, imageType, path, exportScale,
|
||||
useRecursiveBackgroundColor);
|
||||
}
|
||||
|
||||
public Vector3 GetTitlePosition(Title title)
|
||||
|
||||
@@ -18,7 +18,11 @@ namespace XCharts.Runtime
|
||||
protected bool m_Refresh;
|
||||
protected Action<VertexHelper, Painter> m_OnPopulateMesh;
|
||||
|
||||
public Action<VertexHelper, Painter> onPopulateMesh { set { m_OnPopulateMesh = value; } }
|
||||
public Action<VertexHelper, Painter> onPopulateMesh
|
||||
{
|
||||
get { return m_OnPopulateMesh; }
|
||||
set { m_OnPopulateMesh = value; }
|
||||
}
|
||||
public int index { get { return m_Index; } set { m_Index = value; } }
|
||||
public Type type { get { return m_Type; } set { m_Type = value; } }
|
||||
public void Refresh()
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using XUGL;
|
||||
#if dUI_TextMeshPro
|
||||
using TMPro;
|
||||
#endif
|
||||
@@ -1060,34 +1061,359 @@ namespace XCharts.Runtime
|
||||
private static extern void Download(string base64str, string fileName);
|
||||
#endif
|
||||
|
||||
public static Texture2D SaveAsImage(RectTransform rectTransform, Canvas canvas, string imageType = "png", string path = "")
|
||||
private static void SetLayerRecursively(GameObject obj, int layer)
|
||||
{
|
||||
if (obj == null) return;
|
||||
obj.layer = layer;
|
||||
var trans = obj.transform;
|
||||
for (int i = 0; i < trans.childCount; i++)
|
||||
{
|
||||
SetLayerRecursively(trans.GetChild(i).gameObject, layer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloneChildrenRecursively(Transform source, Transform targetParent, int layer)
|
||||
{
|
||||
if (source == null || targetParent == null) return;
|
||||
for (int i = 0; i < source.childCount; i++)
|
||||
{
|
||||
var child = source.GetChild(i);
|
||||
var childClone = GameObject.Instantiate(child.gameObject, targetParent, false);
|
||||
SetLayerRecursively(childClone, layer);
|
||||
SyncPainterCallbacks(child, childClone.transform);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SyncPainterCallbacks(Transform source, Transform clone)
|
||||
{
|
||||
if (source == null || clone == null) return;
|
||||
var sourcePainter = source.GetComponent<Painter>();
|
||||
var clonePainter = clone.GetComponent<Painter>();
|
||||
if (sourcePainter != null && clonePainter != null)
|
||||
{
|
||||
clonePainter.onPopulateMesh = sourcePainter.onPopulateMesh;
|
||||
clonePainter.index = sourcePainter.index;
|
||||
clonePainter.type = sourcePainter.type;
|
||||
clonePainter.material = sourcePainter.material;
|
||||
clonePainter.Refresh();
|
||||
}
|
||||
var count = Mathf.Min(source.childCount, clone.childCount);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SyncPainterCallbacks(source.GetChild(i), clone.GetChild(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DestroyObject(GameObject obj)
|
||||
{
|
||||
if (obj == null) return;
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
GameObject.DestroyImmediate(obj, true);
|
||||
else
|
||||
GameObject.Destroy(obj);
|
||||
#else
|
||||
GameObject.Destroy(obj);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static byte[] EncodeImage(Texture2D tex, string imageType)
|
||||
{
|
||||
var cam = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
|
||||
var pos = RectTransformUtility.WorldToScreenPoint(cam, rectTransform.position);
|
||||
var width = (int)(rectTransform.rect.width * canvas.scaleFactor);
|
||||
var height = (int)(rectTransform.rect.height * canvas.scaleFactor);
|
||||
var posX = pos.x + rectTransform.rect.xMin * canvas.scaleFactor;
|
||||
var posY = pos.y + rectTransform.rect.yMin * canvas.scaleFactor;
|
||||
var rect = new Rect(posX, posY, width, height);
|
||||
var tex = new Texture2D(width, height, TextureFormat.ARGB32, false);
|
||||
tex.ReadPixels(rect, 0, 0);
|
||||
tex.Apply();
|
||||
byte[] bytes;
|
||||
switch (imageType)
|
||||
{
|
||||
case "png":
|
||||
bytes = tex.EncodeToPNG();
|
||||
break;
|
||||
return tex.EncodeToPNG();
|
||||
case "jpg":
|
||||
bytes = tex.EncodeToJPG();
|
||||
break;
|
||||
return tex.EncodeToJPG();
|
||||
case "exr":
|
||||
bytes = tex.EncodeToEXR();
|
||||
break;
|
||||
return tex.EncodeToEXR();
|
||||
default:
|
||||
Debug.LogError("SaveAsImage ERROR: not support image type:" + imageType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static float[] GetChartCornerRadius(BaseChart chart, float chartWidth, float chartHeight, float scaleFactor)
|
||||
{
|
||||
if (chart == null || chartWidth <= 0 || chartHeight <= 0)
|
||||
return null;
|
||||
|
||||
var background = chart.GetChartComponent<Background>();
|
||||
if (background == null || background.borderStyle == null || !background.borderStyle.roundedCorner)
|
||||
return null;
|
||||
|
||||
var cornerRadius = background.borderStyle.cornerRadius;
|
||||
if (cornerRadius == null || cornerRadius.Length == 0)
|
||||
return null;
|
||||
|
||||
float brLt = 0, brRt = 0, brRb = 0, brLb = 0;
|
||||
bool needRound = false;
|
||||
UGL.InitCornerRadius(cornerRadius, chartWidth, chartHeight, false, false,
|
||||
ref brLt, ref brRt, ref brRb, ref brLb, ref needRound);
|
||||
|
||||
if (!needRound)
|
||||
return null;
|
||||
|
||||
return new float[]
|
||||
{
|
||||
brLt * scaleFactor,
|
||||
brRt * scaleFactor,
|
||||
brRb * scaleFactor,
|
||||
brLb * scaleFactor
|
||||
};
|
||||
}
|
||||
|
||||
private static float GetRoundedRectCoverage(float x, float y, float width, float height,
|
||||
float radiusLt, float radiusRt, float radiusRb, float radiusLb, float aaWidth = 1f)
|
||||
{
|
||||
if (radiusLb > 0 && x < radiusLb && y < radiusLb)
|
||||
{
|
||||
var dx = x - radiusLb;
|
||||
var dy = y - radiusLb;
|
||||
var dist = Mathf.Sqrt(dx * dx + dy * dy);
|
||||
var delta = radiusLb - dist;
|
||||
if (delta >= aaWidth) return 1f;
|
||||
if (delta <= -aaWidth) return 0f;
|
||||
return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
|
||||
}
|
||||
if (radiusLt > 0 && x < radiusLt && y > height - radiusLt)
|
||||
{
|
||||
var dx = x - radiusLt;
|
||||
var dy = y - (height - radiusLt);
|
||||
var dist = Mathf.Sqrt(dx * dx + dy * dy);
|
||||
var delta = radiusLt - dist;
|
||||
if (delta >= aaWidth) return 1f;
|
||||
if (delta <= -aaWidth) return 0f;
|
||||
return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
|
||||
}
|
||||
if (radiusRt > 0 && x > width - radiusRt && y > height - radiusRt)
|
||||
{
|
||||
var dx = x - (width - radiusRt);
|
||||
var dy = y - (height - radiusRt);
|
||||
var dist = Mathf.Sqrt(dx * dx + dy * dy);
|
||||
var delta = radiusRt - dist;
|
||||
if (delta >= aaWidth) return 1f;
|
||||
if (delta <= -aaWidth) return 0f;
|
||||
return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
|
||||
}
|
||||
if (radiusRb > 0 && x > width - radiusRb && y < radiusRb)
|
||||
{
|
||||
var dx = x - (width - radiusRb);
|
||||
var dy = y - radiusRb;
|
||||
var dist = Mathf.Sqrt(dx * dx + dy * dy);
|
||||
var delta = radiusRb - dist;
|
||||
if (delta >= aaWidth) return 1f;
|
||||
if (delta <= -aaWidth) return 0f;
|
||||
return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
|
||||
}
|
||||
return 1f;
|
||||
}
|
||||
|
||||
private static void ApplyRoundedCornerClip(Texture2D tex, float[] cornerRadii)
|
||||
{
|
||||
if (tex == null || cornerRadii == null || cornerRadii.Length < 4)
|
||||
return;
|
||||
|
||||
var width = tex.width;
|
||||
var height = tex.height;
|
||||
if (width <= 0 || height <= 0)
|
||||
return;
|
||||
|
||||
var radiusLt = Mathf.Max(0, cornerRadii[0]);
|
||||
var radiusRt = Mathf.Max(0, cornerRadii[1]);
|
||||
var radiusRb = Mathf.Max(0, cornerRadii[2]);
|
||||
var radiusLb = Mathf.Max(0, cornerRadii[3]);
|
||||
if (radiusLt <= 0 && radiusRt <= 0 && radiusRb <= 0 && radiusLb <= 0)
|
||||
return;
|
||||
|
||||
var colors = tex.GetPixels32();
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
var px = x + 0.5f;
|
||||
var py = y + 0.5f;
|
||||
var coverage = GetRoundedRectCoverage(px, py, width, height,
|
||||
radiusLt, radiusRt, radiusRb, radiusLb, 1f);
|
||||
if (coverage <= 0f)
|
||||
{
|
||||
var index = y * width + x;
|
||||
var color = colors[index];
|
||||
color.a = 0;
|
||||
colors[index] = color;
|
||||
}
|
||||
else if (coverage < 1f)
|
||||
{
|
||||
var index = y * width + x;
|
||||
var color = colors[index];
|
||||
color.a = (byte)Mathf.Clamp(Mathf.RoundToInt(color.a * coverage), 0, 255);
|
||||
colors[index] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
tex.SetPixels32(colors);
|
||||
tex.Apply();
|
||||
}
|
||||
|
||||
private static Color32 GetBackgroundColorRecursive(Transform parent)
|
||||
{
|
||||
if (parent == null) return new Color32(255, 255, 255, 255);
|
||||
|
||||
// Try to find Image components with colors in child nodes
|
||||
for (int i = 0; i < parent.childCount; i++)
|
||||
{
|
||||
var child = parent.GetChild(i);
|
||||
var image = child.GetComponent<Image>();
|
||||
if (image != null && image.enabled && child.gameObject.activeInHierarchy)
|
||||
{
|
||||
var color = image.color;
|
||||
if (color.a > 0)
|
||||
{
|
||||
// Found a visible background image
|
||||
color.a = 1f; // Make it fully opaque for proper blending
|
||||
return color;
|
||||
}
|
||||
}
|
||||
// Recursively search child nodes
|
||||
var foundColor = GetBackgroundColorRecursive(child);
|
||||
if (foundColor.a > 0)
|
||||
return foundColor;
|
||||
}
|
||||
|
||||
return Color.white;
|
||||
}
|
||||
|
||||
public static Texture2D SaveAsImage(RectTransform rectTransform, Canvas canvas, string imageType = "png",
|
||||
string path = "", float exportScale = 1f, bool useRecursiveBackgroundColor = false)
|
||||
{
|
||||
if (rectTransform == null || canvas == null)
|
||||
return null;
|
||||
|
||||
var clampedExportScale = Mathf.Max(1f, exportScale);
|
||||
var scaleFactor = canvas.scaleFactor <= 0 ? 1f : canvas.scaleFactor;
|
||||
var outputScaleFactor = scaleFactor * clampedExportScale;
|
||||
var width = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.width * outputScaleFactor));
|
||||
var height = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.height * outputScaleFactor));
|
||||
var chart = rectTransform.GetComponent<BaseChart>();
|
||||
var cornerRadii = GetChartCornerRadius(chart, rectTransform.rect.width, rectTransform.rect.height, outputScaleFactor);
|
||||
|
||||
Texture2D tex = null;
|
||||
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
|
||||
var antiAliasing = QualitySettings.antiAliasing > 0 ? QualitySettings.antiAliasing : 4;
|
||||
rt.antiAliasing = Mathf.Clamp(antiAliasing, 1, 8);
|
||||
|
||||
var oldActive = RenderTexture.active;
|
||||
var captureLayer = 31;
|
||||
var rootObj = new GameObject("xcharts_save_image_root");
|
||||
var camObj = new GameObject("xcharts_save_image_camera");
|
||||
var canvasObj = new GameObject("xcharts_save_image_canvas");
|
||||
var contentObj = new GameObject("xcharts_save_image_content", typeof(RectTransform));
|
||||
|
||||
try
|
||||
{
|
||||
SetLayerRecursively(rootObj, captureLayer);
|
||||
SetLayerRecursively(camObj, captureLayer);
|
||||
SetLayerRecursively(canvasObj, captureLayer);
|
||||
SetLayerRecursively(contentObj, captureLayer);
|
||||
|
||||
camObj.transform.SetParent(rootObj.transform, false);
|
||||
var camera = camObj.AddComponent<Camera>();
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
|
||||
// Get background color - try multiple sources for better results
|
||||
Color32 bgColor = new Color32(255, 255, 255, 255);
|
||||
var chartParent = rectTransform.parent;
|
||||
|
||||
// First, try to get from chart's Background component
|
||||
if (chart != null)
|
||||
{
|
||||
bgColor = chart.GetChartBackgroundColor();
|
||||
//bgColor.a = 255;
|
||||
}
|
||||
|
||||
// If enabled, find background color recursively from sibling nodes
|
||||
if (useRecursiveBackgroundColor && (bgColor.a < 255 ||
|
||||
(bgColor.r == 255 && bgColor.g == 255 && bgColor.b == 255)))
|
||||
{
|
||||
var siblingBgColor = GetBackgroundColorRecursive(chartParent);
|
||||
if (siblingBgColor.a > 0)
|
||||
bgColor = siblingBgColor;
|
||||
}
|
||||
|
||||
camera.backgroundColor = bgColor;
|
||||
|
||||
camera.cullingMask = 1 << captureLayer;
|
||||
camera.orthographic = true;
|
||||
camera.orthographicSize = height / 2f;
|
||||
camera.nearClipPlane = -100;
|
||||
camera.farClipPlane = 100;
|
||||
camera.allowHDR = false;
|
||||
camera.allowMSAA = rt.antiAliasing > 1;
|
||||
camera.targetTexture = rt;
|
||||
|
||||
canvasObj.transform.SetParent(rootObj.transform, false);
|
||||
var captureCanvas = canvasObj.AddComponent<Canvas>();
|
||||
captureCanvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
captureCanvas.worldCamera = camera;
|
||||
captureCanvas.planeDistance = 1;
|
||||
captureCanvas.pixelPerfect = canvas.pixelPerfect;
|
||||
captureCanvas.sortingOrder = 0;
|
||||
canvasObj.AddComponent<GraphicRaycaster>();
|
||||
|
||||
var canvasRect = canvasObj.GetComponent<RectTransform>();
|
||||
canvasRect.anchorMin = Vector2.zero;
|
||||
canvasRect.anchorMax = Vector2.one;
|
||||
canvasRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
canvasRect.anchoredPosition = Vector2.zero;
|
||||
canvasRect.sizeDelta = new Vector2(width, height);
|
||||
|
||||
contentObj.transform.SetParent(canvasObj.transform, false);
|
||||
var contentRect = contentObj.GetComponent<RectTransform>();
|
||||
contentRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
contentRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
contentRect.pivot = rectTransform.pivot;
|
||||
contentRect.anchoredPosition = Vector2.zero;
|
||||
contentRect.sizeDelta = rectTransform.rect.size;
|
||||
contentRect.localScale = new Vector3(clampedExportScale, clampedExportScale, 1f);
|
||||
|
||||
// Clone sibling nodes (including background layers below chart)
|
||||
var chartSiblingIndex = rectTransform.GetSiblingIndex();
|
||||
if (chartParent != null)
|
||||
{
|
||||
for (int i = 0; i < chartParent.childCount; i++)
|
||||
{
|
||||
var sibling = chartParent.GetChild(i);
|
||||
// Only clone siblings below the chart (smaller sibling index)
|
||||
if (i < chartSiblingIndex)
|
||||
{
|
||||
var siblingClone = GameObject.Instantiate(sibling.gameObject, contentObj.transform, false);
|
||||
SetLayerRecursively(siblingClone, captureLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloneChildrenRecursively(rectTransform, contentObj.transform, captureLayer);
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
camera.Render();
|
||||
|
||||
RenderTexture.active = rt;
|
||||
tex = new Texture2D(width, height, TextureFormat.ARGB32, false);
|
||||
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||
tex.Apply();
|
||||
ApplyRoundedCornerClip(tex, cornerRadii);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = oldActive;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
DestroyObject(rootObj);
|
||||
}
|
||||
|
||||
var bytes = EncodeImage(tex, imageType);
|
||||
if (bytes == null)
|
||||
return null;
|
||||
|
||||
var fileName = rectTransform.name + "." + imageType;
|
||||
#if UNITY_WEBGL
|
||||
string base64str = Convert.ToBase64String(bytes);
|
||||
|
||||
Reference in New Issue
Block a user