From b1636d5932e12ce71311189621bd28839cde4221 Mon Sep 17 00:00:00 2001 From: lrc <571244399@qq.com> Date: Sat, 25 Jul 2026 21:38:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=8F=8D=E5=B0=84=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=9D=90?= =?UTF-8?q?=E8=B4=A8=E5=8F=8D=E5=B0=84=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Accessors/MaterialPropertyAccessor.cs | 110 +++++++++ .../Accessors/ReflectionValueAccessor.cs | 77 +++++++ .../XuiavrgFieldDropdownValueAttribute.cs | 39 ++++ .../XuiavrgFieldToggleGroupValueAttribute.cs | 39 ++++ .../Attribute/XuiavrgSplitAttribute.cs | 7 +- .../Scripts/AvrgMemberMetaData.cs | 129 ++++------- .../Scripts/AvrgMemberMetaDataSet.cs | 89 ++++---- .../Scripts/AvrgMvvmBinding.cs | 7 +- .../Scripts/Core/IAvrgValueAccessor.cs | 64 ++++++ .../Scripts/XuiFieldTempEntry.cs | 169 +++----------- .../Scripts/XuiavrgFieldTag.cs | 39 +++- .../Scripts/XuiavrgUtilities.cs | 209 ++++++++++++++---- .../AvrgValues/XUIAvrgMDColorValue.cs | 58 +++++ .../AvrgValues/XUIAvrgMDDropdownValue.cs | 100 +++++++++ .../AvrgValues/XUIAvrgMDInputFieldValue.cs | 67 ++++++ .../AvrgValues/XUIAvrgMDLabelValue.cs | 57 +++++ .../AvrgValues/XUIAvrgMDSliderValue.cs | 86 +++++++ .../AvrgValues/XUIAvrgMDTextureValue.cs | 63 ++++++ .../AvrgValues/XUIAvrgMDToggleGroupValue.cs | 114 ++++++++++ .../AvrgValues/XUIAvrgMDToggleValue.cs | 52 +++++ .../AvrgValues/XUIAvrgMDVectorValue.cs | 137 ++++++++++++ Runtime/OverrideUI/XUIAvrgMDValue.cs | 72 +++++- 22 files changed, 1444 insertions(+), 340 deletions(-) create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/MaterialPropertyAccessor.cs create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/ReflectionValueAccessor.cs create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldDropdownValueAttribute.cs create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldToggleGroupValueAttribute.cs create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/Core/IAvrgValueAccessor.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs create mode 100644 Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/MaterialPropertyAccessor.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/MaterialPropertyAccessor.cs new file mode 100644 index 0000000..3f9aaf4 --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/MaterialPropertyAccessor.cs @@ -0,0 +1,110 @@ +using System; +using UnityEngine; +using UnityEngine.Rendering; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 材质属性访问器 — 通过 Material.Get/Set 系列方法读写 Shader 属性 + /// + public class MaterialPropertyAccessor : IAvrgValueAccessor + { + private readonly Material _material; + private readonly string _propName; + private readonly ShaderPropertyType _shaderType; + + public Type ValueType { get; } + public bool IsReadOnly { get; } + + public MaterialPropertyAccessor( + Material material, + string propName, + ShaderPropertyType shaderType, + Type valueType, + bool isReadOnly = false) + { + _material = material; + _propName = propName; + _shaderType = shaderType; + ValueType = valueType; + IsReadOnly = isReadOnly; + } + + public object GetValue() + { + if (_material == null) return null; + + return _shaderType switch + { + ShaderPropertyType.Float or ShaderPropertyType.Range => _material.GetFloat(_propName), + ShaderPropertyType.Int => _material.GetInt(_propName), + ShaderPropertyType.Color => _material.GetColor(_propName), + ShaderPropertyType.Vector => _material.GetVector(_propName), + ShaderPropertyType.Texture => _material.GetTexture(_propName), + _ => null + }; + } + + public void SetValue(object value) + { + if (IsReadOnly || _material == null) return; + + switch (_shaderType) + { + case ShaderPropertyType.Float: + case ShaderPropertyType.Range: + _material.SetFloat(_propName, Convert.ToSingle(value)); + break; + case ShaderPropertyType.Int: + _material.SetInt(_propName, Convert.ToInt32(value)); + break; + case ShaderPropertyType.Color: + _material.SetColor(_propName, (Color)value); + break; + case ShaderPropertyType.Vector: + _material.SetVector(_propName, (Vector4)value); + break; + case ShaderPropertyType.Texture: + _material.SetTexture(_propName, value as Texture); + break; + } + } + + public string FormatDisplay(object value) + { + if (value == null) return "None"; + if (value is Texture tex && tex != null) return tex.name; + return value.ToString(); + } + } + + /// + /// 材质属性访问器工厂 — 持有 Shader 属性名和类型,创建时绑定 Material 实例 + /// + public class MaterialPropertyAccessorFactory : IAvrgValueAccessorFactory + { + private readonly string _propName; + private readonly ShaderPropertyType _shaderType; + private readonly Type _valueType; + private readonly bool _isReadOnly; + + public MaterialPropertyAccessorFactory( + string propName, + ShaderPropertyType shaderType, + Type valueType, + bool isReadOnly = false) + { + _propName = propName; + _shaderType = shaderType; + _valueType = valueType; + _isReadOnly = isReadOnly; + } + + public IAvrgValueAccessor CreateAccessor(object targetInstance) + { + var material = targetInstance as Material; + if (material == null) return null; + return new MaterialPropertyAccessor(material, _propName, _shaderType, _valueType, _isReadOnly); + } + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/ReflectionValueAccessor.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/ReflectionValueAccessor.cs new file mode 100644 index 0000000..f4bbc76 --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Accessors/ReflectionValueAccessor.cs @@ -0,0 +1,77 @@ +using System; +using System.Reflection; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 反射值访问器 — 通过 FieldInfo / PropertyInfo 读写 C# 对象成员 + /// + public class ReflectionValueAccessor : IAvrgValueAccessor + { + private readonly MemberInfo _memberInfo; + private object _targetInstance; + + public Type ValueType { get; } + public bool IsReadOnly { get; } + + public ReflectionValueAccessor(MemberInfo memberInfo, object targetInstance) + { + _memberInfo = memberInfo; + _targetInstance = targetInstance; + + ValueType = memberInfo switch + { + FieldInfo fi => fi.FieldType, + PropertyInfo pi => pi.PropertyType, + _ => typeof(object) + }; + + IsReadOnly = memberInfo is PropertyInfo pi && !pi.CanWrite; + } + + public object GetValue() + { + if (_targetInstance == null) return null; + return _memberInfo switch + { + FieldInfo fi => fi.GetValue(_targetInstance), + PropertyInfo pi => pi.GetValue(_targetInstance), + _ => null + }; + } + + public void SetValue(object value) + { + if (IsReadOnly || _targetInstance == null) return; + switch (_memberInfo) + { + case FieldInfo fi: + fi.SetValue(_targetInstance, value); + break; + case PropertyInfo pi: + pi.SetValue(_targetInstance, value); + break; + } + } + + public string FormatDisplay(object value) => value?.ToString() ?? ""; + } + + /// + /// 反射值访问器工厂 — 持有 MemberInfo,创建时绑定 TargetInstance + /// + public class ReflectionAccessorFactory : IAvrgValueAccessorFactory + { + private readonly MemberInfo _memberInfo; + + public ReflectionAccessorFactory(MemberInfo memberInfo) + { + _memberInfo = memberInfo; + } + + public IAvrgValueAccessor CreateAccessor(object targetInstance) + { + return new ReflectionValueAccessor(_memberInfo, targetInstance); + } + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldDropdownValueAttribute.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldDropdownValueAttribute.cs new file mode 100644 index 0000000..1df7fce --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldDropdownValueAttribute.cs @@ -0,0 +1,39 @@ +using System; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 为 Dropdown UI 提供预设选项 + /// + /// 使用示例: + /// [XuiavrgFieldDropdownValue("选项A", "选项B", "选项C", MergeMode = PresetOptionsMergeMode.Union)] + /// public string quality = "选项A"; + /// + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class XuiavrgFieldDropdownValueAttribute : Attribute, IXuiavrgAttribute + { + /// 预设选项数组 + public readonly string[] Options; + + /// 与已有选项(如 enum 名)的合并模式 + public PresetOptionsMergeMode MergeMode { get; set; } = PresetOptionsMergeMode.Replace; + + public XuiavrgFieldDropdownValueAttribute(params string[] options) + { + Options = options ?? Array.Empty(); + } + + public void BindMetaData(AvrgMemberMetaData metaData) + { + if (metaData == null) return; + + metaData.PresetOptions = Options; + metaData.OptionsMergeMode = MergeMode; + + // 自动设置 Tag 为 Dropdown + if (metaData.Tag == XuiavrgFieldTag.None) + metaData.Tag = XuiavrgFieldTag.Dropdown; + } + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldToggleGroupValueAttribute.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldToggleGroupValueAttribute.cs new file mode 100644 index 0000000..2affa9e --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldToggleGroupValueAttribute.cs @@ -0,0 +1,39 @@ +using System; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 为 ToggleGroup UI 提供预设选项 + /// + /// 使用示例: + /// [XuiavrgFieldToggleGroupValue("低", "中", "高", MergeMode = PresetOptionsMergeMode.Replace)] + /// public string detailLevel = "中"; + /// + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class XuiavrgFieldToggleGroupValueAttribute : Attribute, IXuiavrgAttribute + { + /// 预设选项数组 + public readonly string[] Options; + + /// 与已有选项(如 enum 名)的合并模式 + public PresetOptionsMergeMode MergeMode { get; set; } = PresetOptionsMergeMode.Replace; + + public XuiavrgFieldToggleGroupValueAttribute(params string[] options) + { + Options = options ?? Array.Empty(); + } + + public void BindMetaData(AvrgMemberMetaData metaData) + { + if (metaData == null) return; + + metaData.PresetOptions = Options; + metaData.OptionsMergeMode = MergeMode; + + // 自动设置 Tag 为 ToggleGroup + if (metaData.Tag == XuiavrgFieldTag.None) + metaData.Tag = XuiavrgFieldTag.ToggleGroup; + } + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgSplitAttribute.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgSplitAttribute.cs index 79e7eb6..9e5bcce 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgSplitAttribute.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgSplitAttribute.cs @@ -8,12 +8,11 @@ namespace XericUI.ReflectionUIGenerator /// 将一个复合类型(如 Vector2)拆分为多个子属性,分别绑定到不同的 UI 元素 /// /// 使用示例: - /// [XuiavrgFieldEntryGroup("speed")] /// [XuiavrgSplit("x", "最小")] /// [XuiavrgSplit("y", "最大")] /// public Vector2 speedRange = new Vector2(0, 100); /// - /// 上述代码创建一个 "speed" 编组的 Entry,内部包含两个子绑定: + /// 上述代码创建一个编组 Entry,内部包含两个子元数据: /// speedRange.x 绑定到 UI 的"最小"输入,speedRange.y 绑定到"最大"输入 /// 子绑定通过 XUIAvrgMDValue.BindingSlots 与预制体上的 UI 组件匹配 /// @@ -39,13 +38,12 @@ namespace XericUI.ReflectionUIGenerator public void BindMetaData(AvrgMemberMetaData metaData) { - // 创建子元数据 + // 创建子元数据 — 纯元数据,不持有实例 var subMetaData = new AvrgMemberMetaData { MemberName = $"{metaData.MemberName}.{PropertyPath}", PropertyPath = PropertyPath, LabelName = DisplayLabel ?? PropertyPath, - TargetInstance = null, // 由运行时闭包解析 Hide = metaData.Hide, Required = metaData.Required, FieldOrder = metaData.FieldOrder, @@ -74,7 +72,6 @@ namespace XericUI.ReflectionUIGenerator if (parentType == null || string.IsNullOrEmpty(propertyPath)) return null; - // 仅支持单层路径 if (propertyPath.Contains('/')) return null; diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs index 1e667fd..6e10eaa 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs @@ -1,57 +1,36 @@ using System; using System.Collections.Generic; -using System.Reflection; namespace XericUI.ReflectionUIGenerator { /// /// 单个成员的元数据数据结构 - /// 记录从反射 + Attribute 标记中提取的所有信息 + /// 纯数据描述 — 不持有反射信息(MemberInfo)和实例引用(TargetInstance) + /// + /// 数据的实际读写通过 IAvrgValueAccessorFactory 在 BuildMDVM 阶段创建的 IAvrgValueAccessor 完成。 + /// 这使得同一个元数据模板可复用于 C# 反射对象、Material Shader 属性等不同数据源。 /// public class AvrgMemberMetaData { - // ============ 反射信息 ============ + // ============ 基本标识 ============ - /// 字段/属性名 + /// 字段/属性名(或 Shader 属性名如 "_MainTex") public string MemberName { get; set; } - /// 反射信息 (FieldInfo 或 PropertyInfo) - public MemberInfo MemberInfo { get; set; } - - /// 值类型 - public Type ValueType { get; set; } - - /// 是否为属性 (false 为字段) - public bool IsProperty { get; set; } - - /// 绑定的目标实例 (可选,Step1 时可空) - public object TargetInstance { get; set; } - - // ============ 拆分属性支持 ============ - - /// - /// 子属性路径(仅拆分属性有效) - /// 用于从父值中访问子字段/属性,如 "x"、"y"、"width" - /// - public string PropertyPath { get; set; } - - /// - /// 拆分子元数据列表 - /// 由 [XuiavrgSplit] 特性填充,每个子元数据对应一个拆分属性 - /// - public List SubMetaDataList { get; set; } - - // ============ 从 IXuiavrgAttribute 融合的元数据 ============ - - /// 显示标签 + /// UI 显示标签 public string LabelName { get; set; } - /// 单位文本 + /// 单位文本(如 "m/s") public string UnitLabelName { get; set; } /// 格式化字符串(如 "{0:F2}") public string FormatText { get; set; } + /// 值类型 + public Type ValueType { get; set; } + + // ============ UI 映射 ============ + /// 功能标记 public XuiavrgFieldTag Tag { get; set; } = XuiavrgFieldTag.None; @@ -67,21 +46,12 @@ namespace XericUI.ReflectionUIGenerator /// 是否在 UI 中隐藏 public bool Hide { get; set; } - /// 值变更时触发的反射调用方法名 - public string OnTriggerCall { get; set; } + // ============ 默认值与验证 ============ - /// - /// 类的初始默认值(由类字段初始化器决定) - /// 例如 public float speed = 10f; → DefaultValue = 10f - /// + /// 默认值(用于 UI 初始化和回退参考) public object DefaultValue { get; set; } - // ============ 属性检查 (由 [PropertyCheck] 填充) ============ - - /// - /// 判断依据标识 - /// 默认使用 ValueType.FullName 与 AvrgValueRangeConfig 中的条目匹配 - /// + /// 属性检查依据标识(默认使用 ValueType.FullName) public string PropertyCheckKey { get; set; } /// 是否必填 @@ -99,47 +69,46 @@ namespace XericUI.ReflectionUIGenerator /// 是否启用无效值状态检查 public bool ValidateInvalid { get; set; } - // ============ 原始 Attribute 引用 ============ + /// Shader Range 属性下限(null 表示非 Range 属性) + public float? RangeMin { get; set; } + + /// Shader Range 属性上限(null 表示非 Range 属性) + public float? RangeMax { get; set; } + + // ============ 拆分属性支持 ============ /// - /// 收集到的所有 IXuiavrgAttribute 列表 - /// 保留原始引用,供扩展和自定义处理使用 + /// 子属性路径(仅拆分属性有效) + /// 用于从父值中访问子字段/属性,如 "x"、"y"、"width" /// + public string PropertyPath { get; set; } + + /// + /// 拆分子元数据列表 + /// 由 [XuiavrgSplit] 特性填充 + /// + public List SubMetaDataList { get; set; } + + // ============ 预设选项 (Dropdown/ToggleGroup) ============ + + /// 预设选项列表 + public IReadOnlyList PresetOptions { get; set; } + + /// 预设选项合并模式 + public PresetOptionsMergeMode OptionsMergeMode { get; set; } + + // ============ 扩展 ============ + + /// 收集到的所有 IXuiavrgAttribute 列表 public List Attributes { get; set; } = new List(); - /// - /// 从绑定目标获取值 - /// - public object GetValue() - { - if (TargetInstance == null) return null; - switch (MemberInfo) - { - case PropertyInfo pi: - return pi.GetValue(TargetInstance); - case FieldInfo fi: - return fi.GetValue(TargetInstance); - default: - return null; - } - } + // ============ ★ 核心变更 — 值访问器工厂 ============ /// - /// 设置值到绑定目标 + /// 值访问器工厂 + /// 在元数据提取阶段设置(持有 MemberInfo 或 Shader 属性信息) + /// 在 BuildMDVM 阶段调用 CreateAccessor(targetInstance) 创建真正的读写通道 /// - public void SetValue(object value) - { - if (TargetInstance == null) return; - switch (MemberInfo) - { - case PropertyInfo pi: - if (pi.CanWrite) - pi.SetValue(TargetInstance, value); - break; - case FieldInfo fi: - fi.SetValue(TargetInstance, value); - break; - } - } + public IAvrgValueAccessorFactory AccessorFactory { get; set; } } } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs index 8e174ae..56be430 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs @@ -8,7 +8,7 @@ namespace XericUI.ReflectionUIGenerator { /// /// 元数据集 - /// 包含从对象反射提取的完整元数据,以及用于生成 MVVM 绑定的 BuildMDVM() 方法 + /// 包含从对象/材质反射提取的完整元数据,以及用于生成 MVVM 绑定的 BuildMDVM() 方法 /// public class AvrgMemberMetaDataSet { @@ -33,7 +33,6 @@ namespace XericUI.ReflectionUIGenerator /// internal void MergeGroupEntries() { - // 提取编组和非编组 var grouped = _metaDataList .Where(m => !string.IsNullOrEmpty(m.GroupPath)) .GroupBy(m => m.GroupPath) @@ -47,9 +46,7 @@ namespace XericUI.ReflectionUIGenerator foreach (var gEntry in grouped) { - // 编组内第一个成员作为主元数据 var first = gEntry.First(); - // 收集组内所有成员的 SubMetaDataList(合并拆分属性) foreach (var member in gEntry) { if (member.SubMetaDataList?.Count > 0) @@ -62,9 +59,7 @@ namespace XericUI.ReflectionUIGenerator merged.Add(first); } - // 非编组直接追加 merged.AddRange(nonGrouped); - _metaDataList = merged; } @@ -82,9 +77,10 @@ namespace XericUI.ReflectionUIGenerator } /// - /// 为元数据生成对应的 MVVM 值绑定模板 + /// 为元数据生成对应的 MVVM 值绑定 + /// 通过 AccessorFactory 创建 IAvrgValueAccessor 完成数据读写 /// - /// 可选的业务目标对象(如果元数据中没有绑定目标,则在此传入) + /// 目标实例(C# 对象、Material 等) /// 可选的冲突解决配置 /// 绑定集合 public AvrgMvvmBindingCollection BuildMDVM( @@ -92,30 +88,46 @@ namespace XericUI.ReflectionUIGenerator AvrgMvvmResolverConfig resolverConfig = null) { var bindingCollection = new AvrgMvvmBindingCollection(); + bindingCollection.SourceMetaDataSet = this; var scheduler = new AvrgMvvmScheduler(resolverConfig); foreach (var metaData in _metaDataList) { - // 确定目标实例 - var target = metaData.TargetInstance ?? businessTarget; - if (target == null) + if (metaData.AccessorFactory == null) continue; + if (businessTarget == null) + { + Debug.LogWarning( + $"[Xuiavrg] 元数据 '{metaData.MemberName}' 缺少 targetInstance,跳过绑定"); continue; + } + + var accessor = metaData.AccessorFactory.CreateAccessor(businessTarget); + if (accessor == null) continue; // 创建主绑定 var binding = new AvrgMvvmBinding { MetaData = metaData, - ReadFunc = () => metaData.GetValue(), + ReadFunc = () => accessor.GetValue(), + Accessor = accessor, }; var capturedBinding = binding; - binding.WriteAction = (val) => - { - metaData.SetValue(val); - scheduler.MarkDataDirty(capturedBinding); - }; - // 如果元数据有拆分子属性,创建子绑定 + if (accessor.IsReadOnly) + { + binding.WriteAction = null; + } + else + { + binding.WriteAction = (val) => + { + accessor.SetValue(val); + scheduler.MarkDataDirty(capturedBinding); + }; + } + + // 拆分属性子绑定 if (metaData.SubMetaDataList?.Count > 0) { binding.SubBindings = new List(); @@ -128,14 +140,12 @@ namespace XericUI.ReflectionUIGenerator var subBinding = new AvrgMvvmBinding { MetaData = subMetaData, - // 从父值中读取子属性 - ReadFunc = () => ReadSubProperty(metaData.GetValue(), subPath), - // 修改子属性并写回父值 + ReadFunc = () => ReadSubProperty(accessor.GetValue(), subPath), WriteAction = (val) => { - var parentVal = metaData.GetValue(); + var parentVal = accessor.GetValue(); var modifiedParent = WriteSubProperty(parentVal, subPath, val); - metaData.SetValue(modifiedParent); + accessor.SetValue(modifiedParent); scheduler.MarkDataDirty(capturedBinding); }, }; @@ -155,16 +165,11 @@ namespace XericUI.ReflectionUIGenerator // ============ 子属性访问辅助方法 ============ - /// - /// 从父值中读取子属性(支持单层路径如 "x"、"y") - /// 例如:ReadSubProperty(Vector2(1,2), "x") → 1f - /// private static object ReadSubProperty(object parentValue, string propertyPath) { if (parentValue == null || string.IsNullOrEmpty(propertyPath)) return null; - // 仅支持单层路径 if (propertyPath.Contains('/')) { Debug.LogWarning($"[Xuiavrg] 暂不支持多层嵌套路径 '{propertyPath}'"); @@ -182,10 +187,6 @@ namespace XericUI.ReflectionUIGenerator return null; } - /// - /// 修改父值中的子属性并返回修改后的父值 - /// 适用于值类型(struct)和引用类型 - /// private static object WriteSubProperty(object parentValue, string propertyPath, object value) { if (parentValue == null || string.IsNullOrEmpty(propertyPath)) @@ -221,8 +222,6 @@ namespace XericUI.ReflectionUIGenerator /// /// 对当前元数据集执行属性检查验证 /// - /// 值范围配置(可选,不传入则不执行范围检查) - /// 验证结果集合 public AvrgValidationResultSet Validate(AvrgValueRangeConfig valueRangeConfig = null) { var resultSet = new AvrgValidationResultSet(); @@ -232,7 +231,6 @@ namespace XericUI.ReflectionUIGenerator if (metaData.Hide) continue; - // 只有标记了 PropertyCheck 的才验证 if (!metaData.Required && !metaData.ValidateReasonable && !metaData.ValidateWarning && !metaData.ValidateInvalid) { @@ -240,13 +238,13 @@ namespace XericUI.ReflectionUIGenerator { MetaData = metaData, Result = AvrgMemberCheckResult.Skipped, - CurrentValueString = metaData.GetValue()?.ToString(), + CurrentValueString = metaData.DefaultValue?.ToString(), Message = "未标记属性检查", }); continue; } - var value = metaData.GetValue(); + var value = metaData.DefaultValue; var checkResult = CheckSingleMember(metaData, value, valueRangeConfig); resultSet.Results.Add(checkResult); } @@ -254,9 +252,6 @@ namespace XericUI.ReflectionUIGenerator return resultSet; } - /// - /// 验证单个成员 - /// private AvrgMemberValidationResult CheckSingleMember( AvrgMemberMetaData metaData, object value, @@ -268,7 +263,6 @@ namespace XericUI.ReflectionUIGenerator CurrentValueString = value?.ToString() ?? "(null)", }; - // 根据 CheckKey 查找值范围规则 AvrgValueRangeEntry rangeEntry = null; string checkKey = !string.IsNullOrEmpty(metaData.PropertyCheckKey) ? metaData.PropertyCheckKey @@ -279,22 +273,19 @@ namespace XericUI.ReflectionUIGenerator rangeEntry = rangeConfig.FindEntry(checkKey); } - // ——— 优先级 1: 空值检查 (Required) ——— + // 优先级 1: 空值检查 (Required) if (metaData.Required) { bool isEmpty = false; - // 引用类型 null if (value == null) { isEmpty = true; } - // 字符串 null/空 else if (value is string str) { isEmpty = string.IsNullOrEmpty(str); } - // 使用 ValueRangeEntry 的 IsEmpty 判断 else if (rangeEntry != null && !rangeEntry.skipEmptyCheck) { isEmpty = rangeEntry.IsEmpty(value); @@ -308,7 +299,6 @@ namespace XericUI.ReflectionUIGenerator } } - // 如果没有范围配置,无法继续检查 if (rangeEntry == null) { result.Result = AvrgMemberCheckResult.Valid; @@ -316,7 +306,7 @@ namespace XericUI.ReflectionUIGenerator return result; } - // ——— 优先级 2: 合理值检查(最高优先级,命中即通过) ——— + // 优先级 2: 合理值检查 if (metaData.ValidateReasonable && rangeEntry.IsReasonable(value)) { result.Result = AvrgMemberCheckResult.Valid; @@ -324,7 +314,7 @@ namespace XericUI.ReflectionUIGenerator return result; } - // ——— 优先级 3: 无效值检查 ——— + // 优先级 3: 无效值检查 if (metaData.ValidateInvalid && rangeEntry.IsInvalid(value)) { result.Result = AvrgMemberCheckResult.Invalid; @@ -332,7 +322,7 @@ namespace XericUI.ReflectionUIGenerator return result; } - // ——— 优先级 4: 警告值检查 ——— + // 优先级 4: 警告值检查 if (metaData.ValidateWarning && rangeEntry.IsWarning(value)) { result.Result = AvrgMemberCheckResult.Warning; @@ -340,7 +330,6 @@ namespace XericUI.ReflectionUIGenerator return result; } - // ——— 最终: 通过 ——— result.Result = AvrgMemberCheckResult.Valid; result.Message = "通过"; return result; diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs index 970b894..c8faa0f 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reflection; namespace XericUI.ReflectionUIGenerator { @@ -20,6 +19,12 @@ namespace XericUI.ReflectionUIGenerator /// 写入值到数据源的委托 public Action WriteAction { get; set; } + /// + /// 值访问器引用(在 BuildMDVM 时创建,供 UI 组件使用) + /// 提供 ValueType / IsReadOnly / FormatDisplay 等扩展能力 + /// + public IAvrgValueAccessor Accessor { get; set; } + /// /// 拆分属性子绑定列表 /// 由 [XuiavrgSplit] 特性展开生成,每个子绑定对应一个拆分后的子属性 diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Core/IAvrgValueAccessor.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Core/IAvrgValueAccessor.cs new file mode 100644 index 0000000..77841e2 --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Core/IAvrgValueAccessor.cs @@ -0,0 +1,64 @@ +using System; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 预设选项合并模式 + /// 用于 Dropdown/ToggleGroup 的预设选项与已有选项(如 enum 名)的合并策略 + /// + public enum PresetOptionsMergeMode + { + /// 完全覆盖现有选项 + Replace, + + /// 保留两者交集 + Intersection, + + /// 保留两者并集(去重合并) + Union, + + /// 从现有选项中移除预设选项(差集) + Difference, + } + + /// + /// 值访问器 — 统一的数据读写通道 + /// 解耦"数据的物理存储方式"和"业务层的读写需求" + /// + /// C# 反射对象和 Material Shader 属性通过各自实现此接口, + /// 向 MVVM 绑定层暴露完全相同的数据交互约定。 + /// + public interface IAvrgValueAccessor + { + /// 值的 C# 类型 + Type ValueType { get; } + + /// 是否只读(如 Shader Keywords 不可运行时修改) + bool IsReadOnly { get; } + + /// 从数据源读取当前值 + object GetValue(); + + /// 将值写入数据源(IsReadOnly 时忽略) + void SetValue(object value); + + /// 将值格式化为 UI 显示字符串 + string FormatDisplay(object value); + } + + /// + /// 值访问器工厂 — 在"元数据模板"阶段创建,延迟到"绑定实例"时生产 Accessor + /// + /// 设计意图: + /// 1. 元数据提取阶段:只描述"如何访问数据",不绑定具体实例 + /// 2. BuildMDVM 阶段:传入实例,通过 Factory 生产 Accessor + /// 3. 同一套元数据模板可复用于不同实例 + /// + public interface IAvrgValueAccessorFactory + { + /// 为给定目标实例创建值访问器 + /// 目标实例(C# 对象、Material 等) + /// 值访问器实例,targetInstance 类型不匹配时返回 null + IAvrgValueAccessor CreateAccessor(object targetInstance); + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs index fdaaae4..e658f02 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs @@ -74,24 +74,33 @@ namespace XericUI.ReflectionUIGenerator /// /// 字段临时 Entry 实现类 /// 作为数据对象和 UI 组件之间的中间缓存层 + /// + /// UI 交互事件的分配通过 XUIAvrgMDValue.BindToAccessor() 的多态机制完成, + /// 各类 XUIAvrgMDValue 子类自行管理其专属 UI 组件的交互订阅。 /// public class XuiFieldTempEntry : IFieldEntry { #region 对象池 - internal static ObjectPool Pool { get; private set; } - - static XuiFieldTempEntry() + private static ObjectPool _pool; + internal static ObjectPool Pool { - Pool = new ObjectPool( - createFunc: () => new XuiFieldTempEntry(), - actionOnGet: entry => entry.OnEntryCreate(), - actionOnRelease: entry => entry.OnEntryRelease(), - actionOnDestroy: entry => entry.OnEntryDestroy(), - collectionCheck: false, - defaultCapacity: 32, - maxSize: 512 - ); + get + { + if (_pool == null) + { + _pool = new ObjectPool( + createFunc: () => new XuiFieldTempEntry(), + actionOnGet: entry => entry.OnEntryCreate(), + actionOnRelease: entry => entry.OnEntryRelease(), + actionOnDestroy: entry => entry.OnEntryDestroy(), + collectionCheck: false, + defaultCapacity: 32, + maxSize: 512 + ); + } + return _pool; + } } /// 从池中获取 Entry 实例 @@ -113,7 +122,7 @@ namespace XericUI.ReflectionUIGenerator private GameObject _uiInstance; private GameObject _boundPrefab; private int _fieldOrder; - private bool _uiBound; // 防止重复绑定 + private bool _uiBound; #endregion @@ -166,9 +175,6 @@ namespace XericUI.ReflectionUIGenerator #region 初始化方法 - /// - /// 设置 Entry 基本属性 - /// public void Initialize(string entryName, XuiavrgFieldTag tag = XuiavrgFieldTag.None, int tagIndex = 0) { _entryName = entryName; @@ -179,18 +185,12 @@ namespace XericUI.ReflectionUIGenerator _uiBound = false; } - /// - /// 设置父 Entry(用于嵌套) - /// public void SetParent(IFieldEntry parent) { _parent = parent; _entryPath = null; } - /// - /// 添加子 Entry - /// public void AddChild(XuiFieldTempEntry child) { if (child == null) return; @@ -198,9 +198,6 @@ namespace XericUI.ReflectionUIGenerator _children.Add(child); } - /// - /// 添加 MVVM 绑定到此 Entry - /// public void AddBinding(AvrgMvvmBinding binding, AvrgMvvmScheduler scheduler) { if (binding == null) return; @@ -253,7 +250,7 @@ namespace XericUI.ReflectionUIGenerator /// /// 将 XUIAvrgMDValue 与 MVVM 绑定连接 - /// 支持主绑定 + 拆分属性子绑定(通过 BindingSlots) + /// 通过多态 BindToAccessor 让 UI 子类自行管理交互事件和显示更新 /// private void BindComponentWithBinding(XUIAvrgMDValue mdValue) { @@ -261,39 +258,27 @@ namespace XericUI.ReflectionUIGenerator var primaryBinding = _bindings[0]; - // ——— 1. 主绑定:数据 → UI ——— - primaryBinding.OnUIDataChanged += value => UpdateUIComponent(mdValue, value); + // ★ 多态调用: UI 子类自行处理交互事件和显示更新 + mdValue.BindToAccessor(primaryBinding.Accessor, primaryBinding, _scheduler); - // ——— 主绑定:UI → 数据 ——— - WireUpUIEvents(mdValue, primaryBinding); - - // ——— 2. 拆分属性子绑定匹配 ——— + // 拆分属性子绑定处理 if (primaryBinding.SubBindings?.Count > 0 && mdValue.AvailableBindingSlots?.Count > 0) { - // 收集所有子节点上的 XUIAvrgMDValue var childMDValues = new List(); mdValue.GetComponentsInChildren(true, childMDValues); foreach (var slot in mdValue.AvailableBindingSlots) { - // 按 propertyName 匹配子绑定 var subBinding = primaryBinding.SubBindings.Find( sb => sb.MetaData?.PropertyPath == slot.propertyName); if (subBinding == null) continue; - // 在子节点中查找匹配 Tag 的 XUIAvrgMDValue foreach (var childMD in childMDValues) { - if (childMD == mdValue) continue; // 跳过自身 + if (childMD == mdValue) continue; if (childMD.EntryTag == slot.tag || slot.tag == XuiavrgFieldTag.None) { - // 子绑定:数据 → UI - subBinding.OnUIDataChanged += value => UpdateUIComponent(childMD, value); - - // 子绑定:UI → 数据(订阅子节点的交互事件) - WireUpUIEvents(childMD, subBinding); - - // 广播子元数据到子节点样式组件 + childMD.BindToAccessor(subBinding.Accessor, subBinding, _scheduler); childMD.InitializeWithMetaData(subBinding.MetaData); break; } @@ -301,104 +286,10 @@ namespace XericUI.ReflectionUIGenerator } } - // 广播主元数据到 XUIAvrgMDValue(触发 IXUIAvrgMDValueStyle 子组件) + // 广播主元数据到样式组件 mdValue.InitializeWithMetaData(primaryBinding.MetaData); } - /// - /// 订阅 UI 组件的交互事件 → 写入绑定 - /// - private void WireUpUIEvents(XUIAvrgMDValue mdValue, AvrgMvvmBinding binding) - { - if (mdValue.TryGetComponent(out UnityEngine.UI.Button uiButton)) - { - uiButton.onClick.AddListener(() => - { - binding.WriteFromUI(true); - _scheduler?.MarkUIDirty(binding); - }); - } - - if (mdValue.TryGetComponent(out UnityEngine.UI.Toggle uiToggle)) - { - uiToggle.onValueChanged.AddListener(val => - { - binding.WriteFromUI(val); - _scheduler?.MarkUIDirty(binding); - }); - } - - if (mdValue.TryGetComponent(out UnityEngine.UI.Slider uiSlider)) - { - uiSlider.onValueChanged.AddListener(val => - { - binding.WriteFromUI(val); - _scheduler?.MarkUIDirty(binding); - }); - } - - if (mdValue.TryGetComponent(out UnityEngine.UI.InputField uiInput)) - { - uiInput.onEndEdit.AddListener(val => - { - binding.WriteFromUI(val); - _scheduler?.MarkUIDirty(binding); - }); - } - - if (mdValue.TryGetComponent(out UnityEngine.UI.Dropdown uiDropdown)) - { - uiDropdown.onValueChanged.AddListener(val => - { - binding.WriteFromUI(val); - _scheduler?.MarkUIDirty(binding); - }); - } - } - - /// - /// 更新 UI 组件的显示值(通过 XUIAvrgMDValue 所在 GameObject 上的具体组件) - /// - private static void UpdateUIComponent(XUIAvrgMDValue mdValue, object value) - { - if (mdValue == null || value == null) return; - - // Text - if (mdValue.TryGetComponent(out UnityEngine.UI.Text uiText)) - { - uiText.text = value.ToString(); - return; - } - - // Toggle - if (value is bool boolVal && mdValue.TryGetComponent(out UnityEngine.UI.Toggle uiToggle)) - { - uiToggle.isOn = boolVal; - return; - } - - // Slider - if (value is float floatVal && mdValue.TryGetComponent(out UnityEngine.UI.Slider uiSlider)) - { - uiSlider.value = floatVal; - return; - } - - // InputField - if (mdValue.TryGetComponent(out UnityEngine.UI.InputField uiInput)) - { - uiInput.text = value.ToString(); - return; - } - - // Dropdown - if (value is int intVal && mdValue.TryGetComponent(out UnityEngine.UI.Dropdown uiDropdown)) - { - uiDropdown.value = intVal; - return; - } - } - #endregion #region 生命周期 @@ -419,7 +310,6 @@ namespace XericUI.ReflectionUIGenerator var value = binding.ReadFromDataSource(); binding.NotifyUIUpdate(value); - // 子绑定也做初始值推送 if (binding.SubBindings?.Count > 0) { foreach (var subBinding in binding.SubBindings) @@ -446,7 +336,6 @@ namespace XericUI.ReflectionUIGenerator foreach (var binding in _bindings) { binding.ClearUIEvent(); - // 子绑定也清理 if (binding.SubBindings?.Count > 0) { foreach (var sub in binding.SubBindings) diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgFieldTag.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgFieldTag.cs index 54281d8..c0f5220 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgFieldTag.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgFieldTag.cs @@ -11,41 +11,56 @@ namespace XericUI.ReflectionUIGenerator public enum XuiavrgFieldTag { None = 0, - + /// 标题文本 / Label Label = 1 << 0, - + /// 内容值 / 数值上下文 Context = 1 << 1, - + /// 单位文本 Unit = 1 << 2, - + /// 布尔开关 / Toggle Toggle = 1 << 3, - + /// 按钮触发器 Button = 1 << 4, - + /// 滑块 / 进度条 Slider = 1 << 5, - + /// 下拉选择 Dropdown = 1 << 6, - + /// 输入值 / InputField Input = 1 << 7, - + /// 单选项 / Radio Select = 1 << 8, - + /// 符号 / 图标 Sign = 1 << 9, - + /// 编辑区域 / 多行文本 Edit = 1 << 10, - + /// 输出区域 / 只读展示 Output = 1 << 11, + + /// 颜色 / Color + Color = 1 << 12, + + /// 纹理预览 / Texture + Texture = 1 << 13, + + /// 向量值组 / Vector (Vector2/3/4) + Vector = 1 << 14, + + /// 开关组 / ToggleGroup + ToggleGroup = 1 << 15, + + /// 只读展示 / ReadOnly + ReadOnly = 1 << 16, } } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgUtilities.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgUtilities.cs index 79bd6aa..5c6b17e 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgUtilities.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgUtilities.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using UnityEngine; +using UnityEngine.Rendering; namespace XericUI.ReflectionUIGenerator { @@ -18,19 +20,20 @@ namespace XericUI.ReflectionUIGenerator yield return property; } + // ============ C# 对象: 元数据提取(纯元数据,不绑定实例)============ + /// - /// 从任意对象提取 AvrgMemberMetaData 元数据集 - /// 反射识别成员、识别 IXuiavrgAttribute 标记、执行标记绑定的完整流程 + /// 从类型提取 AvrgMemberMetaData 元数据集(不含实例绑定) + /// 用于构建元数据模板,可复用于不同实例 /// - /// 待提取的数据对象 - /// 提取完成的元数据集 - public static AvrgMemberMetaDataSet ExtractAvrgMemberMetaData(this object source) + /// 待提取的类型 + /// 纯元数据集(无实例引用,由 AccessorFactory 持有反射信息) + public static AvrgMemberMetaDataSet ExtractAvrgMemberMetaData(this Type type) { - if (source == null) - throw new ArgumentNullException(nameof(source)); + if (type == null) + throw new ArgumentNullException(nameof(type)); var metaDataSet = new AvrgMemberMetaDataSet(); - var type = source.GetType(); foreach (var memberInfo in type.GetRuntimeMemberInfo()) { @@ -39,52 +42,30 @@ namespace XericUI.ReflectionUIGenerator if (hideAttr != null && hideAttr.Hide) continue; - // 创建元数据 + // 识别值类型 + Type valueType = memberInfo switch + { + PropertyInfo pi => pi.PropertyType, + FieldInfo fi => fi.FieldType, + _ => typeof(object) + }; + + // 创建元数据 — 不持有 MemberInfo 和 TargetInstance var metaData = new AvrgMemberMetaData { MemberName = memberInfo.Name, - MemberInfo = memberInfo, - TargetInstance = source, + ValueType = valueType, + AccessorFactory = new ReflectionAccessorFactory(memberInfo), + PropertyCheckKey = valueType?.FullName, }; - // 识别值类型和 IsProperty - switch (memberInfo) - { - case PropertyInfo pi: - metaData.IsProperty = true; - metaData.ValueType = pi.PropertyType; - break; - case FieldInfo fi: - metaData.IsProperty = false; - metaData.ValueType = fi.FieldType; - break; - } - - // 提取字段默认值(类初始值) - // Unity 序列化字段可以通过反射读取实例上的当前值作为"默认"参考 - // 但更准确的在类声明阶段的初始值需要通过运行时观察或 Fallback 判断 - // 此处策略:如果 TargetInstance 非 null,直接 ReadValue 作为 DefaultValue - if (source != null && memberInfo is FieldInfo fi2) - { - metaData.DefaultValue = fi2.GetValue(source); - } - else if (source != null && memberInfo is PropertyInfo pi2 && pi2.CanRead) - { - metaData.DefaultValue = pi2.GetValue(source); - } - - // 设置 PropertyCheckKey 默认值(用类型全称) - if (metaData.ValueType != null) - metaData.PropertyCheckKey = metaData.ValueType.FullName; - - // 收集所有 IXuiavrgAttribute + // 收集并执行所有 IXuiavrgAttribute var attributes = memberInfo.GetCustomAttributes() .OfType() .ToList(); metaData.Attributes = attributes; - // 按序执行 BindMetaData foreach (var attr in attributes) { attr.BindMetaData(metaData); @@ -103,11 +84,147 @@ namespace XericUI.ReflectionUIGenerator } /// - /// 从任意对象提取 AvrgMemberMetaData 元数据集的泛型版本 + /// 从对象实例提取元数据并一步生成绑定(便捷方法) + /// 内部调用 Type.ExtractAvrgMemberMetaData() + BuildMDVM() /// - public static AvrgMemberMetaDataSet ExtractAvrgMemberMetaData(this T source) + public static AvrgMvvmBindingCollection BuildBindingsFromObject( + this object source, + AvrgMvvmResolverConfig resolverConfig = null) { - return ExtractAvrgMemberMetaData((object)source); + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var metaDataSet = source.GetType().ExtractAvrgMemberMetaData(); + return metaDataSet.BuildMDVM(source, resolverConfig); + } + + // ============ Material: 元数据提取 ============ + + /// + /// 从 Material 的 Shader 属性中提取元数据集 + /// + /// 目标 Material + /// 纯元数据集(AccessorFactory 持有 MaterialPropertyAccessorFactory) + public static AvrgMemberMetaDataSet ExtractFromMaterial(this Material material) + { + if (material == null) + throw new ArgumentNullException(nameof(material)); + + var shader = material.shader; + if (shader == null) + { + Debug.LogError("[Xuiavrg] Material 没有关联的 Shader"); + return null; + } + + var metaDataSet = new AvrgMemberMetaDataSet(); + int propCount = shader.GetPropertyCount(); + + for (int i = 0; i < propCount; i++) + { + var propName = shader.GetPropertyName(i); + var shaderType = shader.GetPropertyType(i); + var desc = shader.GetPropertyDescription(i); + var flags = shader.GetPropertyFlags(i); + + // 跳过隐藏属性 + if ((flags & UnityEngine.Rendering.ShaderPropertyFlags.HideInInspector) != 0) + continue; + + // ShaderPropertyType → C# Type + Tag + (Type valueType, XuiavrgFieldTag tag) = MapShaderPropertyType(shaderType, propName); + + var metaData = new AvrgMemberMetaData + { + MemberName = propName, + ValueType = valueType, + LabelName = !string.IsNullOrEmpty(desc) ? desc : FormatPropertyName(propName), + Tag = tag, + AccessorFactory = new MaterialPropertyAccessorFactory(propName, shaderType, valueType), + }; + + // 读取当前值作为 DefaultValue + metaData.DefaultValue = ReadMaterialProperty(material, propName, shaderType); + + // Range 属性:提取范围限制 + if (shaderType == ShaderPropertyType.Range) + { + var limits = shader.GetPropertyRangeLimits(i); + metaData.RangeMin = limits.x; + metaData.RangeMax = limits.y; + } + + metaDataSet.Add(metaData); + } + + // ===== Shader Keywords 提取(只读)===== + try + { + var keywordSpace = shader.keywordSpace; + foreach (var kw in keywordSpace.keywords) + { + bool isEnabled = material.IsKeywordEnabled(kw.name); + var kwMetaData = new AvrgMemberMetaData + { + MemberName = $"kwd_{kw.name}", + ValueType = typeof(bool), + LabelName = $"[Keyword] {kw.name}", + Tag = XuiavrgFieldTag.Toggle, + DefaultValue = isEnabled, + AccessorFactory = new MaterialPropertyAccessorFactory( + kw.name, + ShaderPropertyType.Float, // Keywords 不是标准属性,用 Float 占位 + typeof(bool), + isReadOnly: true), + }; + metaDataSet.Add(kwMetaData); + } + } + catch (Exception e) + { + Debug.LogWarning($"[Xuiavrg] 提取 Shader Keywords 失败: {e.Message}"); + } + + metaDataSet.SortByFieldOrder(); + return metaDataSet; + } + + private static (Type valueType, XuiavrgFieldTag tag) MapShaderPropertyType( + ShaderPropertyType shaderType, string propName) + { + return shaderType switch + { + ShaderPropertyType.Float => (typeof(float), XuiavrgFieldTag.Input), + ShaderPropertyType.Range => (typeof(float), XuiavrgFieldTag.Slider), + ShaderPropertyType.Int => (typeof(int), XuiavrgFieldTag.Input), + ShaderPropertyType.Color => (typeof(Color), XuiavrgFieldTag.Color), + ShaderPropertyType.Vector => (typeof(Vector4), XuiavrgFieldTag.Vector), + ShaderPropertyType.Texture => (typeof(Texture), XuiavrgFieldTag.Texture), + _ => (typeof(object), XuiavrgFieldTag.None), + }; + } + + private static object ReadMaterialProperty(Material material, string propName, + ShaderPropertyType shaderType) + { + return shaderType switch + { + ShaderPropertyType.Float or ShaderPropertyType.Range => material.GetFloat(propName), + ShaderPropertyType.Int => material.GetInt(propName), + ShaderPropertyType.Color => material.GetColor(propName), + ShaderPropertyType.Vector => material.GetVector(propName), + ShaderPropertyType.Texture => material.GetTexture(propName), + _ => null + }; + } + + /// + /// 属性名格式化(_MainTex → MainTex, _ShadowColor → Shadow Color) + /// + private static string FormatPropertyName(string propName) + { + if (string.IsNullOrEmpty(propName)) return propName; + return propName.TrimStart('_').Replace('_', ' '); } } } diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs new file mode 100644 index 0000000..9790e8a --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs @@ -0,0 +1,58 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 颜色值组件 — 处理 Color 类型的显示 + /// 通过 Image.color 展示当前颜色值 + /// 颜色修改可通过 RGB 子节点 InputField 或额外的颜色选择逻辑完成 + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDColorValue")] + public class XUIAvrgMDColorValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("用于展示颜色的 Image 组件(未设置则自动查找)")] + private Image _colorDisplay; + + private void Awake() + { + if (_colorDisplay == null) _colorDisplay = GetComponent(); + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null) return; + + // 数据 → UI + binding.OnUIDataChanged += value => + { + if (value is Color color && _colorDisplay != null) + { + _colorDisplay.color = color; + } + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + if (initialValue is Color initColor && _colorDisplay != null) + { + _colorDisplay.color = initColor; + } + } + + protected override void UpdateDisplayValue(object value) + { + if (value is Color color && _colorDisplay != null) + { + _colorDisplay.color = color; + } + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs new file mode 100644 index 0000000..0db3182 --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// Dropdown 值组件 — 处理 string / int / enum 类型的下拉选择交互 + /// 预设选项来源:元数据的 PresetOptions 或 enum 类型的枚举名 + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDDropdownValue")] + public class XUIAvrgMDDropdownValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("关联的 Dropdown 组件(未设置则自动查找)")] + private Dropdown _dropdown; + + private void Awake() + { + if (_dropdown == null) _dropdown = GetComponent(); + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null || _dropdown == null) return; + + // 构建选项列表 + var options = BuildOptions(accessor.ValueType); + _dropdown.ClearOptions(); + _dropdown.AddOptions(options); + + // UI → 数据 + _dropdown.onValueChanged.AddListener(index => + { + if (index >= 0 && index < options.Count) + { + object convertedVal = ConvertOptionToValue(options[index], accessor.ValueType); + binding.WriteFromUI(convertedVal); + scheduler?.MarkUIDirty(binding); + } + }); + + // 数据 → UI + binding.OnUIDataChanged += value => + { + int idx = FindOptionIndex(value, options); + if (idx >= 0) + _dropdown.SetValueWithoutNotify(idx); + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + int initialIdx = FindOptionIndex(initialValue, options); + if (initialIdx >= 0) + _dropdown.SetValueWithoutNotify(initialIdx); + } + + private List BuildOptions(System.Type valueType) + { + // 1. 从元数据 PresetOptions 获取 + if (BoundMetaData?.PresetOptions != null && BoundMetaData.PresetOptions.Count > 0) + return BoundMetaData.PresetOptions.ToList(); + + // 2. enum 类型:自动提取枚举名 + if (valueType != null && valueType.IsEnum) + return System.Enum.GetNames(valueType).ToList(); + + // 3. 回退 + return new List(); + } + + private int FindOptionIndex(object value, List options) + { + if (value == null || options.Count == 0) return -1; + + string valueStr = value.ToString(); + for (int i = 0; i < options.Count; i++) + { + if (options[i] == valueStr) return i; + } + return -1; + } + + private static object ConvertOptionToValue(string option, System.Type targetType) + { + if (targetType == typeof(string)) return option; + if (targetType == typeof(int) && int.TryParse(option, out int intVal)) return intVal; + if (targetType != null && targetType.IsEnum) + return System.Enum.Parse(targetType, option); + return option; + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs new file mode 100644 index 0000000..a66d113 --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs @@ -0,0 +1,67 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// InputField 值组件 — 处理 string / float / int 类型的文本输入交互 + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDInputFieldValue")] + public class XUIAvrgMDInputFieldValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("关联的 InputField 组件(未设置则自动查找)")] + private InputField _inputField; + + private void Awake() + { + if (_inputField == null) _inputField = GetComponent(); + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null || _inputField == null) return; + + // 设置输入类型 + if (accessor.ValueType == typeof(float) || accessor.ValueType == typeof(int) + || accessor.ValueType == typeof(double)) + { + _inputField.contentType = InputField.ContentType.DecimalNumber; + } + + // UI → 数据 + _inputField.onEndEdit.AddListener(val => + { + object convertedVal = ConvertStringToValue(val, accessor.ValueType); + binding.WriteFromUI(convertedVal); + scheduler?.MarkUIDirty(binding); + }); + + // 数据 → UI + binding.OnUIDataChanged += value => + { + _inputField.SetTextWithoutNotify(accessor.FormatDisplay(value)); + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + _inputField.SetTextWithoutNotify(accessor.FormatDisplay(initialValue)); + } + + private static object ConvertStringToValue(string s, Type targetType) + { + if (targetType == typeof(string)) return s; + if (targetType == typeof(float) && float.TryParse(s, out float fVal)) return fVal; + if (targetType == typeof(int) && int.TryParse(s, out int iVal)) return iVal; + if (targetType == typeof(double) && double.TryParse(s, out double dVal)) return dVal; + return s; + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs new file mode 100644 index 0000000..f2b69c1 --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs @@ -0,0 +1,57 @@ +using UnityEngine; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 标签值组件 — 处理只读文本的纯展示 + /// 适用于 Label / Context / Unit / ReadOnly 等展示型 Tag + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDLabelValue")] + public class XUIAvrgMDLabelValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("关联的 Text 组件(未设置则自动查找)")] + private UnityEngine.UI.Text _uiText; + + [SerializeField] + [Tooltip("关联的 TMP_Text 组件(未设置则自动查找)")] + private TMPro.TMP_Text _tmpText; + + private void Awake() + { + if (_uiText == null) _uiText = GetComponent(); + if (_tmpText == null) _tmpText = GetComponent(); + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null) return; + + // 数据 → UI + binding.OnUIDataChanged += value => + { + SetText(value); + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + SetText(initialValue); + } + + private void SetText(object value) + { + string displayStr = value?.ToString() ?? ""; + + if (_tmpText != null) + _tmpText.text = displayStr; + else if (_uiText != null) + _uiText.text = displayStr; + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs new file mode 100644 index 0000000..a90b31a --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs @@ -0,0 +1,86 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// Slider 值组件 — 处理 float / int / double 类型的滑块交互 + /// 适用于 Shader Range 属性或标记了 [XuiavrgFieldTag(Slider)] 的字段 + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDSliderValue")] + public class XUIAvrgMDSliderValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("关联的 Slider 组件(未设置则自动查找)")] + private Slider _slider; + + [SerializeField] + [Tooltip("值显示标签(可选,未设置则不更新)")] + private TMPro.TMP_Text _valueLabel; + + private void Awake() + { + if (_slider == null) _slider = GetComponent(); + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null || _slider == null) return; + + // 设置 Slider 参数 + _slider.wholeNumbers = accessor.ValueType == typeof(int); + + if (BoundMetaData != null) + { + if (BoundMetaData.RangeMin.HasValue) + _slider.minValue = BoundMetaData.RangeMin.Value; + if (BoundMetaData.RangeMax.HasValue) + _slider.maxValue = BoundMetaData.RangeMax.Value; + } + + // UI → 数据 + _slider.onValueChanged.AddListener(val => + { + object convertedVal = ConvertValueForAccessor(val, accessor.ValueType); + binding.WriteFromUI(convertedVal); + scheduler?.MarkUIDirty(binding); + + if (_valueLabel != null) + _valueLabel.text = accessor.FormatDisplay(convertedVal); + }); + + // 数据 → UI + binding.OnUIDataChanged += value => + { + float sliderVal = Convert.ToSingle(value); + _slider.SetValueWithoutNotify(sliderVal); + + if (_valueLabel != null) + _valueLabel.text = accessor.FormatDisplay(value); + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + if (initialValue != null) + { + _slider.SetValueWithoutNotify(Convert.ToSingle(initialValue)); + if (_valueLabel != null) + _valueLabel.text = accessor.FormatDisplay(initialValue); + } + } + + private static object ConvertValueForAccessor(float val, Type targetType) + { + if (targetType == typeof(int)) return (int)val; + if (targetType == typeof(double)) return (double)val; + return val; + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs new file mode 100644 index 0000000..b05f0d7 --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs @@ -0,0 +1,63 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 纹理值组件 — 处理 Texture 类型的预览显示 + /// 通过 RawImage.texture 展示当前纹理 + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDTextureValue")] + public class XUIAvrgMDTextureValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("用于展示纹理的 RawImage 组件(未设置则自动查找)")] + private RawImage _textureDisplay; + + [SerializeField] + [Tooltip("纹理名称标签(可选,用于显示纹理资源名)")] + private TMPro.TMP_Text _nameLabel; + + private void Awake() + { + if (_textureDisplay == null) _textureDisplay = GetComponent(); + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null) return; + + // 数据 → UI + binding.OnUIDataChanged += value => + { + if (value is Texture tex && _textureDisplay != null) + { + _textureDisplay.texture = tex; + } + + if (_nameLabel != null) + { + _nameLabel.text = value is Texture t && t != null ? t.name : "None"; + } + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + if (initialValue is Texture initTex && _textureDisplay != null) + { + _textureDisplay.texture = initTex; + } + + if (_nameLabel != null) + { + _nameLabel.text = initialValue is Texture t && t != null ? t.name : "None"; + } + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs new file mode 100644 index 0000000..01f00f8 --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// ToggleGroup 值组件 — 处理 string / int / enum 类型的单选组交互 + /// 适用于标记了 [XuiavrgFieldToggleGroupValue] 的字段 + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDToggleGroupValue")] + public class XUIAvrgMDToggleGroupValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("关联的 ToggleGroup 组件(未设置则自动查找)")] + private ToggleGroup _toggleGroup; + + private List _toggles = new List(); + private List _optionLabels = new List(); + + private void Awake() + { + if (_toggleGroup == null) _toggleGroup = GetComponent(); + if (_toggleGroup != null) + { + _toggles = _toggleGroup.GetComponentsInChildren(true).ToList(); + } + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null || _toggleGroup == null) return; + + // 构建选项 + BuildOptions(accessor.ValueType); + + // 为每个 Toggle 注册事件 + for (int i = 0; i < _toggles.Count && i < _optionLabels.Count; i++) + { + int idx = i; + _toggles[i].onValueChanged.AddListener(isOn => + { + if (isOn) + { + object convertedVal = ConvertOptionToValue(_optionLabels[idx], accessor.ValueType); + binding.WriteFromUI(convertedVal); + scheduler?.MarkUIDirty(binding); + } + }); + } + + // 数据 → UI + binding.OnUIDataChanged += value => + { + string valueStr = value?.ToString(); + for (int i = 0; i < _toggles.Count && i < _optionLabels.Count; i++) + { + if (_optionLabels[i] == valueStr) + { + _toggles[i].SetIsOnWithoutNotify(true); + return; + } + } + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + string initStr = initialValue?.ToString(); + for (int i = 0; i < _toggles.Count && i < _optionLabels.Count; i++) + { + if (_optionLabels[i] == initStr) + { + _toggles[i].SetIsOnWithoutNotify(true); + break; + } + } + } + + private void BuildOptions(System.Type valueType) + { + _optionLabels.Clear(); + + // 1. 从元数据 PresetOptions + if (BoundMetaData?.PresetOptions != null && BoundMetaData.PresetOptions.Count > 0) + { + _optionLabels.AddRange(BoundMetaData.PresetOptions); + return; + } + + // 2. enum 类型 + if (valueType != null && valueType.IsEnum) + { + _optionLabels.AddRange(System.Enum.GetNames(valueType)); + return; + } + } + + private static object ConvertOptionToValue(string option, System.Type targetType) + { + if (targetType == typeof(string)) return option; + if (targetType == typeof(int) && int.TryParse(option, out int intVal)) return intVal; + if (targetType != null && targetType.IsEnum) + return System.Enum.Parse(targetType, option); + return option; + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs new file mode 100644 index 0000000..84927d0 --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs @@ -0,0 +1,52 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// Toggle 值组件 — 处理 bool 类型的开关交互 + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDToggleValue")] + public class XUIAvrgMDToggleValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("关联的 Toggle 组件(未设置则自动查找)")] + private Toggle _toggle; + + private void Awake() + { + if (_toggle == null) _toggle = GetComponent(); + } + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null || _toggle == null) return; + + // 只读模式下禁用交互 + _toggle.interactable = !accessor.IsReadOnly; + + // UI → 数据 + _toggle.onValueChanged.AddListener(val => + { + binding.WriteFromUI(val); + scheduler?.MarkUIDirty(binding); + }); + + // 数据 → UI + binding.OnUIDataChanged += value => + { + _toggle.SetIsOnWithoutNotify(value is bool b && b); + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + _toggle.SetIsOnWithoutNotify(initialValue is bool bv && bv); + } + } +} diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs new file mode 100644 index 0000000..c80d324 --- /dev/null +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs @@ -0,0 +1,137 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 向量值组件 — 处理 Vector2 / Vector3 / Vector4 类型的多字段输入 + /// 子节点按 BindingSlots 绑定各分量(如 "x", "y", "z", "w") + /// + [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDVectorValue")] + public class XUIAvrgMDVectorValue : XUIAvrgMDValue + { + [SerializeField] + [Tooltip("X 分量 InputField(可选,也可通过 BindingSlots 的子节点绑定)")] + private InputField _inputX; + + [SerializeField] + [Tooltip("Y 分量 InputField")] + private InputField _inputY; + + [SerializeField] + [Tooltip("Z 分量 InputField(Vector3/Vector4 时使用)")] + private InputField _inputZ; + + [SerializeField] + [Tooltip("W 分量 InputField(Vector4 时使用)")] + private InputField _inputW; + + public override void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null) return; + + // 根据向量类型启用/禁用对应分量 + int componentCount = GetComponentCount(accessor.ValueType); + SetInputActive(_inputX, 0, componentCount); + SetInputActive(_inputY, 1, componentCount); + SetInputActive(_inputZ, 2, componentCount); + SetInputActive(_inputW, 3, componentCount); + + // 为每个输入注册事件 + BindInputField(_inputX, 0, accessor, binding, scheduler); + BindInputField(_inputY, 1, accessor, binding, scheduler); + BindInputField(_inputZ, 2, accessor, binding, scheduler); + BindInputField(_inputW, 3, accessor, binding, scheduler); + + // 数据 → UI + binding.OnUIDataChanged += value => + { + UpdateAllInputs(value); + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + UpdateAllInputs(initialValue); + } + + private void BindInputField(InputField input, int componentIndex, + IAvrgValueAccessor accessor, AvrgMvvmBinding binding, AvrgMvvmScheduler scheduler) + { + if (input == null) return; + + input.onEndEdit.AddListener(val => + { + if (float.TryParse(val, out float num)) + { + var currentVal = accessor.GetValue(); + var modified = SetVectorComponent(currentVal, componentIndex, num); + binding.WriteFromUI(modified); + scheduler?.MarkUIDirty(binding); + } + }); + } + + private void UpdateAllInputs(object value) + { + if (value == null) return; + + if (_inputX != null && _inputX.gameObject.activeSelf) + _inputX.SetTextWithoutNotify(GetVectorComponent(value, 0).ToString("F2")); + + if (_inputY != null && _inputY.gameObject.activeSelf) + _inputY.SetTextWithoutNotify(GetVectorComponent(value, 1).ToString("F2")); + + if (_inputZ != null && _inputZ.gameObject.activeSelf) + _inputZ.SetTextWithoutNotify(GetVectorComponent(value, 2).ToString("F2")); + + if (_inputW != null && _inputW.gameObject.activeSelf) + _inputW.SetTextWithoutNotify(GetVectorComponent(value, 3).ToString("F2")); + } + + private static int GetComponentCount(System.Type type) + { + if (type == typeof(Vector2)) return 2; + if (type == typeof(Vector3)) return 3; + if (type == typeof(Vector4)) return 4; + return 0; + } + + private static float GetVectorComponent(object vec, int index) + { + return vec switch + { + Vector2 v2 => index == 0 ? v2.x : v2.y, + Vector3 v3 => index == 0 ? v3.x : index == 1 ? v3.y : v3.z, + Vector4 v4 => index == 0 ? v4.x : index == 1 ? v4.y : index == 2 ? v4.z : v4.w, + _ => 0f + }; + } + + private static object SetVectorComponent(object vec, int index, float value) + { + return vec switch + { + Vector2 v2 => index == 0 ? new Vector2(value, v2.y) : new Vector2(v2.x, value), + Vector3 v3 => index == 0 ? new Vector3(value, v3.y, v3.z) + : index == 1 ? new Vector3(v3.x, value, v3.z) : new Vector3(v3.x, v3.y, value), + Vector4 v4 => index == 0 ? new Vector4(value, v4.y, v4.z, v4.w) + : index == 1 ? new Vector4(v4.x, value, v4.z, v4.w) + : index == 2 ? new Vector4(v4.x, v4.y, value, v4.w) + : new Vector4(v4.x, v4.y, v4.z, value), + _ => vec + }; + } + + private static void SetInputActive(InputField input, int index, int componentCount) + { + if (input != null) + input.gameObject.SetActive(index < componentCount); + } + } +} diff --git a/Runtime/OverrideUI/XUIAvrgMDValue.cs b/Runtime/OverrideUI/XUIAvrgMDValue.cs index 8567d35..01be1f9 100644 --- a/Runtime/OverrideUI/XUIAvrgMDValue.cs +++ b/Runtime/OverrideUI/XUIAvrgMDValue.cs @@ -4,14 +4,14 @@ using UnityEngine; namespace XericUI.ReflectionUIGenerator { /// - /// MDVM 值组件 — 统一 UI 脚本组件 - /// 替换所有 XericUIrAttributeXXX 作为预制体上的标记组件 + /// MDVM 值组件基类 — 预制体上的标记组件 /// /// 职责: /// 1. 提供 EntryTag/TagIndex 供 XuiFieldTempEntry 匹配 - /// 2. 持有绑定的 AvrgMemberMetaData 引用 - /// 3. 初始化时广播元数据到所有子节点上的 IXUIAvrgMDValueStyle - /// 4. 定义绑定槽(BindingSlots),供拆分属性子绑定匹配 + /// 2. 持有绑定的元数据引用 + /// 3. 通过 virtual BindToAccessor() 让子类自行管理 UI 交互事件和显示更新 + /// 4. 初始化时广播元数据到所有子节点上的 IXUIAvrgMDValueStyle + /// 5. 定义绑定槽(BindingSlots),供拆分属性子绑定匹配 /// [AddComponentMenu("Xeric Library/UI Action Vessel/XUIAvrgMDValue")] public class XUIAvrgMDValue : MonoBehaviour @@ -44,7 +44,13 @@ namespace XericUI.ReflectionUIGenerator private List _bindingSlots = new List(); /// 绑定的元数据引用 - public AvrgMemberMetaData BoundMetaData { get; private set; } + public AvrgMemberMetaData BoundMetaData { get; protected set; } + + /// 当前关联的值访问器 + public IAvrgValueAccessor CurrentAccessor { get; protected set; } + + /// 当前关联的 MVVM 绑定 + public AvrgMvvmBinding CurrentBinding { get; protected set; } public XuiavrgFieldTag EntryTag => _entryTag; public int TagIndex => _tagIndex; @@ -52,6 +58,60 @@ namespace XericUI.ReflectionUIGenerator /// 只读访问绑定槽列表 public IReadOnlyList AvailableBindingSlots => _bindingSlots; + /// + /// 由 XuiFieldTempEntry 调用,将值访问器和 MVVM 绑定连接到此 UI 组件 + /// + /// 基类实现提供一个默认的通用绑定逻辑(适用于 Label/Context/Unit 等简单展示型 Tag)。 + /// 子类(如 SliderValue, DropdownValue)应覆写此方法以处理其专属 UI 交互。 + /// + /// 值访问器 — 提供统一的数据读写 + /// MVVM 绑定 — 提供脏标记和 UI→数据通信通道 + /// MVVM 调度器 — 可空,用于标记脏状态 + public virtual void BindToAccessor( + IAvrgValueAccessor accessor, + AvrgMvvmBinding binding, + AvrgMvvmScheduler scheduler) + { + CurrentAccessor = accessor; + CurrentBinding = binding; + + if (binding == null || accessor == null) return; + + // 默认:数据 → UI 单向同步 + binding.OnUIDataChanged += value => + { + UpdateDisplayValue(value); + }; + + // 首次同步 + var initialValue = accessor.GetValue(); + UpdateDisplayValue(initialValue); + } + + /// + /// 更新 UI 显示值(基类默认仅查找 Text/TMP_Text 设置文本) + /// 子类可覆写以实现专属更新逻辑(如 Slider.value, Image.color 等) + /// + protected virtual void UpdateDisplayValue(object value) + { + if (value == null) return; + + // 尝试 Text + var uiText = GetComponent(); + if (uiText != null) + { + uiText.text = CurrentAccessor?.FormatDisplay(value) ?? value.ToString(); + return; + } + + // 尝试 TMP_Text + var tmpText = GetComponent(); + if (tmpText != null) + { + tmpText.text = CurrentAccessor?.FormatDisplay(value) ?? value.ToString(); + } + } + /// /// 由绑定系统调用,传递元数据并广播到 IXUIAvrgMDValueStyle 子组件 ///