Files
BABA_YAGA/Packages/app.rive.rive-unity/Editor/AssetEditor.cs
2026-05-19 17:39:03 +07:00

633 lines
25 KiB
C#

using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.Linq;
using Rive.EditorTools;
using System;
namespace Rive
{
[CustomEditor(typeof(Asset))]
public class AssetEditor : Editor
{
File m_file;
private Artboard m_artboard;
private StateMachine m_stateMachine;
private double m_lastTime = 0.0;
public override bool HasPreviewGUI() => true;
public override bool RequiresConstantRepaint()
{
return true;
}
private enum AssetReferenceType
{
Embedded = 0,
Referenced = 1
}
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
var riveAsset = (Asset)target;
// File Assets Section
var embeddedFoldout = new Foldout { text = "File Assets", value = false };
root.Add(embeddedFoldout);
foreach (var embeddedAsset in riveAsset.EmbeddedAssets)
{
var assetContainer = new VisualElement();
assetContainer.style.paddingBottom = 30;
embeddedFoldout.Add(assetContainer);
// Asset Type
var enumField = new EnumField("Type:", embeddedAsset.AssetType);
enumField.SetEnabled(false);
assetContainer.Add(enumField);
// Asset Name
var nameField = new TextField("Name:") { value = embeddedAsset.Name };
// For text fields, make them readonly instead of using SetEnabled(false) to allow for copying the text
StyleAsReadonly(nameField);
nameField.isReadOnly = true;
assetContainer.Add(nameField);
// Asset ID
var idField = new TextField("ID:") { value = embeddedAsset.Id.ToString() };
StyleAsReadonly(idField);
idField.isReadOnly = true;
assetContainer.Add(idField);
// Asset Reference Type
var referenceType = embeddedAsset.InBandBytesSize > 0 ? AssetReferenceType.Embedded : AssetReferenceType.Referenced;
var referenceTypeField = new EnumField("Reference Type:", referenceType);
referenceTypeField.SetEnabled(false);
assetContainer.Add(referenceTypeField);
// Asset Data
if (referenceType == AssetReferenceType.Embedded)
{
var embeddedField = new TextField("Embedded Size:")
{
value = FormatBytes(embeddedAsset.InBandBytesSize),
tooltip = "The size of the asset data embedded in the Rive file."
};
StyleAsReadonly(embeddedField);
embeddedField.isReadOnly = true;
assetContainer.Add(embeddedField);
}
else
{
var assetField = new ObjectField("Referenced Asset")
{
objectType = GetAssetType(embeddedAsset.AssetType),
value = embeddedAsset.OutOfBandAsset,
};
// Allow referenced assets to be updated in the editor
assetField.RegisterValueChangedCallback(evt =>
{
var newValue = evt.newValue as OutOfBandAsset;
Asset asset = target as Asset;
if (asset == null)
{
return;
}
Undo.RecordObject(this, "Updated Referenced Asset");
AssetImporter.SetOobAssetReference((Asset)target, embeddedAsset.Id, newValue);
});
assetContainer.Add(assetField);
}
}
// Artboard Metadata
if (riveAsset.EditorOnlyMetadata != null && riveAsset.EditorOnlyMetadata.Artboards.Count > 0)
{
var contentsFoldout = new Foldout { text = "Artboard Metadata", value = false };
root.Add(contentsFoldout);
for (int i = 0; i < riveAsset.EditorOnlyMetadata.Artboards.Count; i++)
{
bool isDefaultArtboard = i == 0;
var artboard = riveAsset.EditorOnlyMetadata.Artboards[i];
// Create a foldout for each artboard
string artboardLabel = artboard.Name + (isDefaultArtboard ? " (Default)" : "");
var artboardFoldout = new Foldout { text = artboardLabel, value = false };
artboardFoldout.style.paddingLeft = 8;
artboardFoldout.style.paddingRight = 8;
contentsFoldout.Add(artboardFoldout);
var artboardContainer = new VisualElement();
artboardFoldout.Add(artboardContainer);
AddCopyToClipboardMenu(artboardFoldout, artboard.Name, "Copy Artboard Name");
// Artboard Size
var sizeContainer = new VisualElement();
sizeContainer.style.flexDirection = FlexDirection.Row;
sizeContainer.style.marginLeft = 15;
sizeContainer.style.marginTop = 5;
artboardContainer.Add(sizeContainer);
var sizeLabel = new Label("Size:");
sizeLabel.style.marginRight = 8;
sizeContainer.Add(sizeLabel);
var sizeValueLabel = new Label($"{artboard.Width} x {artboard.Height}");
sizeContainer.Add(sizeValueLabel);
// State Machines Container
var stateMachinesContainer = new VisualElement();
stateMachinesContainer.style.marginLeft = 15;
stateMachinesContainer.style.marginTop = 10;
artboardContainer.Add(stateMachinesContainer);
foreach (var stateMachine in artboard.StateMachines)
{
var smContainer = new VisualElement();
smContainer.style.marginBottom = 10;
// State Machine Header
var smHeader = new VisualElement();
smHeader.style.flexDirection = FlexDirection.Row;
smHeader.style.alignItems = Align.Center;
smContainer.Add(smHeader);
var smLabel = new Label("State Machine:");
smLabel.style.marginRight = 8;
smHeader.Add(smLabel);
var smNameField = new TextField();
smNameField.value = stateMachine.Name;
StyleAsReadonly(smNameField);
smNameField.isReadOnly = true;
smNameField.SetEnabled(true);
smNameField.style.flexGrow = 1;
smHeader.Add(smNameField);
// Inputs
if (stateMachine.Inputs.Count > 0)
{
var inputsContainer = new VisualElement();
inputsContainer.style.marginLeft = 15;
inputsContainer.style.marginTop = 5;
smContainer.Add(inputsContainer);
var inputsLabel = new Label("Inputs:");
inputsLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
inputsLabel.style.marginBottom = 5;
inputsContainer.Add(inputsLabel);
foreach (var input in stateMachine.Inputs)
{
var inputContainer = new VisualElement();
inputContainer.style.flexDirection = FlexDirection.Row;
inputContainer.style.alignItems = Align.Center;
inputContainer.style.marginBottom = 2;
var typeLabel = new Label(input.Type);
typeLabel.style.marginRight = 8;
typeLabel.style.width = 60;
var nameField = new TextField();
nameField.value = input.Name;
StyleAsReadonly(nameField);
nameField.isReadOnly = true;
nameField.SetEnabled(true);
nameField.style.flexGrow = 1;
inputContainer.Add(typeLabel);
inputContainer.Add(nameField);
inputsContainer.Add(inputContainer);
}
}
stateMachinesContainer.Add(smContainer);
}
if (artboard.DefaultViewModel != null && !String.IsNullOrEmpty(artboard.DefaultViewModel.Name))
{
var defaultVMContainer = new VisualElement();
defaultVMContainer.style.flexDirection = FlexDirection.Row;
defaultVMContainer.style.alignItems = Align.Center;
defaultVMContainer.style.marginLeft = 15;
defaultVMContainer.style.marginBottom = 5;
artboardContainer.Add(defaultVMContainer);
var defaultVMLabel = new Label("Default View Model:");
defaultVMLabel.style.marginRight = 8;
defaultVMContainer.Add(defaultVMLabel);
var defaultVMNameField = new TextField();
defaultVMNameField.value = artboard.DefaultViewModel.Name;
StyleAsReadonly(defaultVMNameField);
defaultVMNameField.isReadOnly = true;
defaultVMNameField.SetEnabled(true);
defaultVMNameField.style.flexGrow = 1;
defaultVMContainer.Add(defaultVMNameField);
}
}
}
// View Models Section
if (riveAsset.EditorOnlyMetadata != null && riveAsset.EditorOnlyMetadata.ViewModels.Count > 0)
{
var viewModelsFoldout = new Foldout { text = "View Models", value = false };
root.Add(viewModelsFoldout);
foreach (var viewModel in riveAsset.EditorOnlyMetadata.ViewModels)
{
var viewModelFoldout = new Foldout { text = viewModel.Name, value = false };
viewModelFoldout.style.paddingLeft = 8;
viewModelFoldout.style.paddingRight = 8;
viewModelsFoldout.Add(viewModelFoldout);
AddCopyToClipboardMenu(viewModelFoldout, viewModel.Name, "Copy View Model Name");
// Properties
if (viewModel.Properties.Count > 0)
{
var propertiesContainer = new VisualElement();
propertiesContainer.style.marginLeft = 15;
propertiesContainer.style.marginTop = 5;
viewModelFoldout.Add(propertiesContainer);
var propertiesLabel = new Label("Properties:");
propertiesLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
propertiesLabel.style.marginBottom = 5;
propertiesContainer.Add(propertiesLabel);
foreach (var property in viewModel.Properties)
{
var propertyContainer = new VisualElement();
propertyContainer.style.flexDirection = FlexDirection.Row;
propertyContainer.style.alignItems = Align.Center;
propertyContainer.style.marginBottom = 2;
var typeLabel = new Label(GetViewModelPropertyTypeLabel(property));
typeLabel.style.marginRight = 8;
typeLabel.style.minWidth = 60;
var nameField = new TextField();
nameField.value = property.Name;
StyleAsReadonly(nameField);
nameField.isReadOnly = true;
nameField.SetEnabled(true);
nameField.style.flexGrow = 1;
propertyContainer.Add(typeLabel);
propertyContainer.Add(nameField);
propertiesContainer.Add(propertyContainer);
}
}
// Instance Names
if (viewModel.InstanceNames.Count > 0)
{
var instancesContainer = new VisualElement();
instancesContainer.style.marginLeft = 15;
instancesContainer.style.marginTop = 10;
instancesContainer.style.marginBottom = 10;
viewModelFoldout.Add(instancesContainer);
var instancesLabel = new Label("Instances:");
instancesLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
instancesLabel.style.marginBottom = 5;
instancesContainer.Add(instancesLabel);
foreach (var instanceName in viewModel.InstanceNames)
{
var instanceField = new TextField();
instanceField.value = instanceName;
StyleAsReadonly(instanceField);
instanceField.isReadOnly = true;
instanceField.SetEnabled(true);
instancesContainer.Add(instanceField);
}
}
}
}
// Enums Section
if (riveAsset.EditorOnlyMetadata.Enums.Count > 0)
{
var enumsFoldout = new Foldout { text = "Enums", value = false };
root.Add(enumsFoldout);
foreach (var enumData in riveAsset.EditorOnlyMetadata.Enums)
{
// Create a foldout for each enum type
var enumFoldout = new Foldout { text = enumData.Name, value = false };
enumFoldout.style.paddingLeft = 8;
enumFoldout.style.paddingRight = 8;
enumsFoldout.Add(enumFoldout);
AddCopyToClipboardMenu(enumFoldout, enumData.Name, "Copy Enum Name");
// Values
var valuesContainer = new VisualElement();
valuesContainer.style.marginLeft = 15;
valuesContainer.style.marginTop = 5;
valuesContainer.style.marginBottom = 10;
enumFoldout.Add(valuesContainer);
var valuesLabel = new Label("Values:");
valuesLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
valuesLabel.style.marginBottom = 5;
valuesContainer.Add(valuesLabel);
foreach (var value in enumData.Values)
{
var valueField = new TextField();
valueField.value = value;
StyleAsReadonly(valueField);
valueField.isReadOnly = true;
valueField.SetEnabled(true);
valuesContainer.Add(valueField);
}
}
}
return root;
}
private void AddCopyToClipboardMenu(Foldout foldout, string textToCopy, string itemLabel = null)
{
if (string.IsNullOrEmpty(textToCopy))
{
return;
}
itemLabel = itemLabel ?? $"Copy \"{foldout.text}\"";
foldout.AddManipulator(new ContextualMenuManipulator((ContextualMenuPopulateEvent evt) =>
{
evt.menu.AppendAction(itemLabel, (action) =>
{
GUIUtility.systemCopyBuffer = textToCopy;
});
}));
}
private string GetViewModelPropertyTypeLabel(FileMetadata.ViewModelPropertyMetadata property)
{
// We want to display the type of the property, and if it's a ViewModel type, we also want to display the nested ViewModel name.
if (property.Type == ViewModelDataType.ViewModel)
{
return $"{property.Type.ToString()} ({property.NestedViewModelName})";
}
else if (property.Type == ViewModelDataType.Enum && !string.IsNullOrEmpty(property.EnumTypeName))
{
return $"{property.Type.ToString()} ({property.EnumTypeName})";
}
return $"{property.Type.ToString()}";
}
private void StyleAsReadonly(VisualElement element)
{
element.style.opacity = 0.5f;
}
private System.Type GetAssetType(EmbeddedAssetType assetType)
{
switch (assetType)
{
case EmbeddedAssetType.Font:
return typeof(FontOutOfBandAsset);
case EmbeddedAssetType.Image:
return typeof(ImageOutOfBandAsset);
case EmbeddedAssetType.Audio:
return typeof(AudioOutOfBandAsset);
default:
return typeof(System.Object);
}
}
public override Texture2D RenderStaticPreview(
string assetPath,
UnityEngine.Object[] subAssets,
int width,
int height
)
{
RenderTexture prev = RenderTexture.active;
var rect = new Rect(0, 0, width, height);
RenderTexture rt = Render(rect, true);
if (rt != null)
{
RenderTexture sourceForRead = rt;
RenderTexture temp = null;
// Static preview: use the runtime decode material (Rive/UI/Default) in Linear color space.
// This decodes gamma to linear, which works correctly with ReadPixels→Texture2D path.
// We DON'T use the pass-through shader here because the blit+ReadPixels seems to cause issues, and leads to nothing rendering for the static preview.
// TODO: Remove this once we have a proper way to display the texture in Linear color space.
if (Rive.TextureHelper.ProjectNeedsColorSpaceFix)
{
var mat = Rive.TextureHelper.GammaToLinearUIMaterial;
if (mat != null)
{
temp = RenderTexture.GetTemporary(rt.width, rt.height, 0, RenderTextureFormat.ARGB32);
Graphics.Blit(rt, temp, mat);
sourceForRead = temp;
}
}
RenderTexture.active = sourceForRead;
Texture2D tex = new Texture2D(width, height);
tex.ReadPixels(rect, 0, 0);
tex.Apply(true);
RenderTexture.active = prev;
if (temp != null)
{
RenderTexture.ReleaseTemporary(temp);
}
RenderTexture.ReleaseTemporary(rt);
return tex;
}
return null;
}
RenderTexture Render(Rect rect, bool isStatic = false)
{
int width = (int)rect.width;
int height = (int)rect.height;
var descriptor = Rive.TextureHelper.Descriptor(width, height);
RenderTexture rt = RenderTexture.GetTemporary(descriptor);
var cmb = new CommandBuffer();
cmb.SetRenderTarget(rt);
if (m_file == null)
{
var riveAsset = (Rive.Asset)target;
m_file = Rive.File.Load(riveAsset);
m_artboard = m_file?.Artboard(0);
m_stateMachine = m_artboard?.StateMachine();
}
if (m_artboard != null)
{
var rq = new RenderQueue(
UnityEngine.SystemInfo.graphicsDeviceType == GraphicsDeviceType.Metal
? null
: rt
);
var renderer = rq.Renderer();
renderer.Align(Fit.Contain, Alignment.Center, m_artboard);
renderer.Draw(m_artboard);
renderer.AddToCommandBuffer(cmb);
if (!isStatic)
{
var now = EditorApplication.timeSinceStartup;
double time = now - m_lastTime;
m_stateMachine?.Advance((float)(now - m_lastTime));
m_lastTime = now;
}
else
{
m_stateMachine?.Advance(0.0f);
}
}
var prev = RenderTexture.active;
Graphics.ExecuteCommandBuffer(cmb);
GL.InvalidateState();
cmb.Clear();
if (isStatic && FlipY())
{
RenderTexture temp = RenderTexture.GetTemporary(
width,
height,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Default
);
temp.Create();
Graphics.Blit(rt, temp, new Vector2(1, -1), new Vector2(0, 1));
RenderTexture.ReleaseTemporary(rt);
rt = temp;
}
// Caller releases the temporary RT
return rt;
}
public override void OnPreviewGUI(Rect rect, GUIStyle background)
{
if (Event.current.type == EventType.Repaint)
{
RenderTexture rt = Render(rect);
var drawRect = FlipY()
? new Rect(rect.x, rect.y + rect.height, rect.width, -rect.height)
: rect;
// Live preview: use a simple pass-through shader in Linear color space.
// Rive outputs gamma values, and it looks like EditorGUI.DrawPreviewTexture expects sRGB input.
// We DON'T use the decode material here because it would decode to linear, causing burnt/dark colors.
// The pass-through shader (Hidden/Rive/Editor/SRGBEncodePreview) just returns the texture unchanged.
// TODO: Remove this once we have a proper way to display the texture in Linear color space.
var mat = (Rive.TextureHelper.ProjectNeedsColorSpaceFix ? GetEncodePreviewMaterial() : null);
UnityEditor.EditorGUI.DrawPreviewTexture(drawRect, rt, mat);
RenderTexture.ReleaseTemporary(rt);
}
}
private static Material s_encodePreviewMaterial;
private static Material GetEncodePreviewMaterial()
{
if (s_encodePreviewMaterial != null) return s_encodePreviewMaterial;
var shader = Shader.Find("Hidden/Rive/Editor/SRGBEncodePreview");
if (shader == null) return null;
s_encodePreviewMaterial = new Material(shader)
{
name = "Rive_Editor_SRGBEncodePreview",
hideFlags = HideFlags.HideAndDontSave
};
return s_encodePreviewMaterial;
}
private void UnloadPreview()
{
m_stateMachine = null;
m_artboard = null;
if (m_file != null)
{
m_file.Dispose();
m_file = null;
}
}
public void OnDisable()
{
var riveAsset = (Rive.Asset)target;
UnloadPreview();
}
private static bool FlipY()
{
switch (UnityEngine.SystemInfo.graphicsDeviceType)
{
case GraphicsDeviceType.Metal:
case GraphicsDeviceType.Vulkan:
case GraphicsDeviceType.Direct3D11:
return true;
default:
return false;
}
}
static string FormatBytes(uint byteCount)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
int order = 0;
while (byteCount >= 1024 && order < sizes.Length - 1)
{
order++;
byteCount /= 1024;
}
// Adjust the format string to your preferences. For example "{0:0.#}{1}" would
// show a single decimal place, and no space.
return string.Format("{0:0.##} {1}", byteCount, sizes[order]);
}
}
}