This commit is contained in:
SoulliesOfficial
2026-06-09 01:43:55 -04:00
parent 0fb72f5bba
commit 5fc1392747
171 changed files with 30149 additions and 22331 deletions

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ea823e8c44a0ca842a3b0e59390994de
guid: ef107ab630341ed4083145b5a21db705
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni.Editor
{
/// <summary>
/// 类型→绘制器注册表。集中管理 Prefab 引用和 Type→IPropertyDrawer 映射。
/// 替代分散在 IHaveInspection 各 Generate 方法中的 basePrefabs 直接访问。
/// 使用惰性单例,首次访问时自动注册所有内置 Drawer。
/// </summary>
public class DrawerRegistry
{
private static DrawerRegistry _instance;
/// <summary> 全局单例 </summary>
public static DrawerRegistry Instance
{
get
{
if (_instance == null)
{
_instance = new DrawerRegistry();
_instance.RegisterBuiltInDrawers();
}
return _instance;
}
}
// 按属性类型注册的 Drawer (优先级最高)
private readonly Dictionary<Type, IPropertyDrawer> _attributeDrawers = new Dictionary<Type, IPropertyDrawer>();
// 按字段类型注册的 Drawer
private readonly Dictionary<Type, IPropertyDrawer> _typeDrawers = new Dictionary<Type, IPropertyDrawer>();
// 特殊 Drawer (Button, HintText 等不绑定字段类型的)
private readonly Dictionary<string, IPropertyDrawer> _namedDrawers = new Dictionary<string, IPropertyDrawer>();
// Fallback Drawer
private IPropertyDrawer _fallbackDrawer;
/// <summary> Prefab 引用桥接到 BasePrefabsCollection </summary>
private BasePrefabsCollection Prefabs => EditorManager.instance.basePrefabs;
/// <summary> 注册字段类型→Drawer 映射 </summary>
public void RegisterDrawer(Type fieldType, IPropertyDrawer drawer)
{
_typeDrawers[fieldType] = drawer;
}
/// <summary> 注册属性类型→Drawer 映射(属性特化优先于字段类型) </summary>
public void RegisterAttributeDrawer<TAttr>(IPropertyDrawer drawer) where TAttr : Attribute
{
_attributeDrawers[typeof(TAttr)] = drawer;
}
/// <summary> 注册命名 Drawer用于 Button、HintText 等非类型绑定的控件) </summary>
public void RegisterNamedDrawer(string name, IPropertyDrawer drawer)
{
_namedDrawers[name] = drawer;
}
/// <summary> 设置 Fallback Drawer类型未注册时使用 </summary>
public void SetFallbackDrawer(IPropertyDrawer drawer)
{
_fallbackDrawer = drawer;
}
/// <summary>
/// 获取适用的 Drawer。查找优先级:
/// 1. 属性注册(如 [InspectorSlider] → SliderDrawer
/// 2. 类型注册(如 bool → ToggleDrawer
/// 3. Enum 基类型检测
/// 4. Fallback (InputFieldDrawer)
/// </summary>
public IPropertyDrawer GetDrawer(Type fieldType, Attribute[] attributes = null)
{
// 1. 属性特化优先
if (attributes != null)
{
foreach (var attr in attributes)
{
if (_attributeDrawers.TryGetValue(attr.GetType(), out var attrDrawer))
return attrDrawer;
}
}
// 2. 精确类型匹配
if (fieldType != null && _typeDrawers.TryGetValue(fieldType, out var typeDrawer))
return typeDrawer;
// 3. Enum 基类型检测
if (fieldType != null && fieldType.IsEnum && _typeDrawers.TryGetValue(typeof(Enum), out var enumDrawer))
return enumDrawer;
// 4. Fallback
return _fallbackDrawer;
}
/// <summary> 获取命名 Drawer </summary>
public IPropertyDrawer GetNamedDrawer(string name)
{
_namedDrawers.TryGetValue(name, out var drawer);
return drawer;
}
/// <summary> 获取 DynamicUI Element Prefab </summary>
public GameObject GetPrefab(string elementType)
{
return elementType switch
{
"button" => Prefabs.button,
"toggle" => Prefabs.toggle,
"inputField" => Prefabs.inputField,
"slider" => Prefabs.slider,
"vector2InputField" => Prefabs.vector2InputField,
"vector3InputField" => Prefabs.vector3InputField,
"enumDropdown" => Prefabs.enumDropdown,
"stringListDropdown" => Prefabs.stringListDropdown,
"baseColorPicker" => Prefabs.baseColorPicker,
"emissionColorPicker" => Prefabs.emissionColorPicker,
"hintText" => Prefabs.hintText,
"parameterText" => Prefabs.parameterText,
"hsvDrawer" => Prefabs.hsvDrawer,
"container" => Prefabs.dynamicUIContainer,
"subcontainer" => Prefabs.dynamicUISubcontainer,
_ => null,
};
}
/// <summary>
/// 注册所有内置 Drawer覆盖全部基本字段类型和特化属性。
/// </summary>
private void RegisterBuiltInDrawers()
{
// 基本类型 Drawer
var inputFieldDrawer = new InputFieldDrawer();
RegisterDrawer(typeof(float), inputFieldDrawer);
RegisterDrawer(typeof(int), inputFieldDrawer);
RegisterDrawer(typeof(string), inputFieldDrawer);
SetFallbackDrawer(inputFieldDrawer);
RegisterDrawer(typeof(bool), new ToggleDrawer());
RegisterDrawer(typeof(System.Enum), new EnumDropdownDrawer());
RegisterDrawer(typeof(Vector2), new Vector2FieldDrawer());
RegisterDrawer(typeof(Vector3), new Vector3FieldDrawer());
RegisterDrawer(typeof(Color), new BaseColorPickerDrawer());
// 属性特化 Drawer
RegisterAttributeDrawer<InspectorSliderAttribute>(new SliderDrawer());
RegisterAttributeDrawer<InspectorEmissionColorAttribute>(new EmissionColorPickerDrawer());
// 命名 Drawer非类型绑定的控件
RegisterNamedDrawer("button", new ButtonDrawer());
RegisterNamedDrawer("hintText", new HintTextDrawer());
RegisterNamedDrawer("parameterText", new ParameterTextDrawer());
RegisterNamedDrawer("slider", new SliderDrawer());
RegisterNamedDrawer("stringListDropdown", new StringListDropdownDrawer());
RegisterNamedDrawer("emissionColorPicker", new EmissionColorPickerDrawer());
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b796d9d7294bb5248a040f9e7dc8be8b

View File

@@ -0,0 +1,27 @@
namespace Ichni.Editor
{
/// <summary>
/// 延迟引用容器 —— 在 Build() 时由 InspectorBuilder 填充。
/// 用于在 Button 回调中访问其他控件(如 UnboundInputField 的值)。
///
/// 用法:
/// <code>
/// var nameRef = new ElementRef&lt;DynamicUIInputField&gt;();
/// InspectorBuilder.For(this)
/// .Section("Search")
/// .UnboundInputField("Name").WithRef(nameRef)
/// .Button("Find", () => {
/// string name = nameRef.Value?.GetValue&lt;string&gt;();
/// })
/// .Build();
/// </code>
/// </summary>
public class ElementRef<T> where T : DynamicUIElement
{
/// <summary> 构建后填充的控件实例Build() 之前为 null </summary>
public T Value { get; internal set; }
/// <summary> 隐式转换,方便直接当 T 使用 </summary>
public static implicit operator T(ElementRef<T> r) => r?.Value;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2de2ed51f60dceb469874524fb69bd28

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni.Editor
{
/// <summary>
/// 可插拔属性绘制器接口。每种字段类型bool, float, Color 等)
/// 对应一个 Drawer 实现,负责创建和配置对应类型的 DynamicUIElement。
/// </summary>
public interface IPropertyDrawer
{
/// <summary> 默认占据的列数 (1-3)1 = 1/3 行宽 </summary>
int DefaultSpan { get; }
/// <summary> 默认高度 (px) </summary>
float DefaultHeight { get; }
/// <summary> 创建并初始化一个 DynamicUIElement挂载到 parent 下 </summary>
DynamicUIElement Draw(DrawContext ctx);
}
/// <summary>
/// 绘制上下文,传递给 IPropertyDrawer.Draw() 的所有必要信息。
/// 由 InspectorBuilder 在 Build() 时构造。
/// </summary>
public struct DrawContext
{
/// <summary> 绑定的目标元素 </summary>
public IBaseElement Target;
/// <summary> 反射路径ReflectionHelper 使用的字段名) </summary>
public string FieldName;
/// <summary> 显示标签 </summary>
public string Label;
/// <summary> 父容器Row 的 RectTransform </summary>
public Transform Parent;
/// <summary> Drawer 注册表,用于获取 Prefab 引用 </summary>
public DrawerRegistry Registry;
/// <summary> 目标 Inspector 窗口(用于 Mark 和 MarkedElements 注册) </summary>
public IHaveInspection Inspection;
// ──────── 可选配置(由 Builder 链式方法设置)────────
/// <summary> 覆盖默认 Span </summary>
public int? OverrideSpan;
/// <summary> 覆盖默认高度 </summary>
public float? OverrideHeight;
/// <summary> 是否启用自动更新 </summary>
public bool AutoUpdate;
/// <summary> 是否只读 </summary>
public bool ReadOnly;
/// <summary> 值变更时的回调 </summary>
public Action OnChangedCallback;
/// <summary> 控件启用条件(返回 false 时禁用交互) </summary>
public Func<bool> EnabledCondition;
/// <summary> Mark 标记键 </summary>
public string MarkKey;
// ──────── Slider 专用 ────────
public float SliderMin;
public float SliderMax;
public bool SliderWholeNumbers;
// ──────── EmissionColor 专用 ────────
public string EmissionEnabledName;
public string EmissionIntensityName;
// ──────── Button 专用 ────────
public Action ButtonAction;
// ──────── HintText 专用 ────────
public string StaticText;
public Func<string> DynamicText;
// ──────── Dropdown 专用 ────────
public Type EnumType;
public List<string> StringListOptions;
// ──────── Unbound 控件 ────────
/// <summary> 无绑定控件类型 (None = 正常绑定控件) </summary>
public UnboundKind Unbound;
/// <summary> 无绑定 InputField 的默认文本 </summary>
public string DefaultText;
/// <summary> 无绑定 Slider 的默认值 </summary>
public float DefaultFloat;
/// <summary> 无绑定 Vector3Field 的默认值 </summary>
public Vector3 DefaultVector3;
}
/// <summary>
/// 无绑定控件类型枚举。
/// None 表示正常的数据绑定控件;其他值表示不绑定任何字段的独立控件。
/// </summary>
public enum UnboundKind
{
None = 0,
InputField,
Toggle,
Slider,
Vector3Field,
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cefee972a8f49184aaff2f2559b67fcf

View File

@@ -0,0 +1,147 @@
using System;
namespace Ichni.Editor
{
/// <summary>
/// 字段/属性标注 —— 驱动 InspectorBuilder.AutoBuild() 自动生成 UI 控件。
/// 替代旧版 DynamicUIAttribute新增 GroupOrder 和 Order 排序支持。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InspectorFieldAttribute : Attribute
{
/// <summary> 显示名称,为空时使用成员名 </summary>
public string Name { get; set; }
/// <summary> 所属 Section 标题 </summary>
public string Group { get; set; }
/// <summary> Section 排序权重(与 InspectorBuilder.Section 的 sectionOrder 统一排序,升序) </summary>
public int GroupOrder { get; set; }
/// <summary> Section 内元素排序权重(升序,同 order 按声明顺序 stable sort </summary>
public int Order { get; set; }
/// <summary>
/// 排版格子要求。一行最大宽度分为 3 份。
/// 填 1 代表需占 1/3。填 3 代表全宽占满整行。
/// 填 0 由内置工厂根据数据类型自动识别。
/// </summary>
public int Span { get; set; }
/// <summary>
/// 垂直占位高度 (px)。填 0 由系统按类型智能推断。
/// </summary>
public float Height { get; set; }
/// <summary> 是否自动更新(绑定 IHaveAutoUpdate </summary>
public bool AutoUpdate { get; set; }
/// <summary> 只读模式(生成 HintText 替代可编辑控件) </summary>
public bool ReadOnly { get; set; }
public InspectorFieldAttribute(
string name = "",
string group = "Default",
int groupOrder = 0,
int order = 0,
int span = 0,
float height = 0f,
bool autoUpdate = false,
bool readOnly = false)
{
Name = name;
Group = group;
GroupOrder = groupOrder;
Order = order;
Span = span;
Height = height;
AutoUpdate = autoUpdate;
ReadOnly = readOnly;
}
}
/// <summary>
/// Slider 控件标注,适用于 float/int 类型字段。
/// 替代旧版 DynamicUISliderAttribute。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InspectorSliderAttribute : InspectorFieldAttribute
{
public float Min { get; set; }
public float Max { get; set; }
public bool WholeNumbers { get; set; }
public InspectorSliderAttribute(
string name = "",
string group = "Default",
float min = 0f,
float max = 1f,
bool wholeNumbers = false,
int groupOrder = 0,
int order = 0,
int span = 0,
float height = 0f,
bool autoUpdate = false,
bool readOnly = false)
: base(name, group, groupOrder, order, span, height, autoUpdate, readOnly)
{
Min = min;
Max = max;
WholeNumbers = wholeNumbers;
}
}
/// <summary>
/// EmissionColor 拾色器标注,支持 HDR 及独立 Emission 强度/开关通道。
/// 替代旧版 DynamicUIEmissionColorAttribute。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InspectorEmissionColorAttribute : InspectorFieldAttribute
{
/// <summary> Emission 开关字段名,"NULL" 表示强制启用 </summary>
public string EmissionEnabledName { get; set; }
/// <summary> Emission 强度字段名,"NULL" 表示使用颜色 Alpha 通道 </summary>
public string EmissionIntensityName { get; set; }
public InspectorEmissionColorAttribute(
string name,
string enabledName = "NULL",
string intensityName = "NULL",
string group = "Default",
int groupOrder = 0,
int order = 0,
int span = 3,
float height = 0f)
: base(name, group, groupOrder, order, span, height)
{
EmissionEnabledName = enabledName;
EmissionIntensityName = intensityName;
}
}
/// <summary>
/// 按钮标注,挂载在无参 Method 上。
/// 替代旧版 DynamicUIButtonAttribute。
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class InspectorButtonAttribute : Attribute
{
public string Name { get; set; }
public string Group { get; set; }
public int GroupOrder { get; set; }
public int Order { get; set; }
public InspectorButtonAttribute(
string name = "",
string group = "Default",
int groupOrder = 0,
int order = 0)
{
Name = name;
Group = group;
GroupOrder = groupOrder;
Order = order;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: be7c441b9cc629e4287d879fd13a700f

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f497f16c91f01db48ab2e21dc67a0f3b

View File

@@ -0,0 +1,181 @@
using System.Collections.Generic;
using UnityEngine;
namespace Ichni.Editor
{
/// <summary>
/// 自动行打包算法 —— 将元素列表按 Span 和高度层级自动装入行。
/// 替代手动调用 GenerateSubcontainer(N) 手动分配列数。
/// </summary>
public static class LayoutPacker
{
/// <summary> 每行最大 Span 总量 </summary>
public const int MaxSpanPerRow = 3;
/// <summary> 行总宽度 (px),与 DynamicUI 现有 600px 布局一致 </summary>
public const float TotalRowWidth = 600f;
/// <summary> 默认行高 (px) </summary>
public const float DefaultRowHeight = 100f;
/// <summary>
/// 高度层级。同一行内只允许相同层级的元素,
/// 避免 100px 的 Toggle 和 280px 的 ColorPicker 混排导致空白浪费。
/// </summary>
public enum HeightTier
{
/// <summary> 标准高度 (&lt;= 100px): bool, float, int, string, enum, Vector2, Vector3, Button... </summary>
Standard,
/// <summary> 中等高度 (&lt;= 280px): BaseColorPicker </summary>
Tall,
/// <summary> 超高 (&gt; 280px): EmissionColorPicker 等 </summary>
ExtraTall
}
/// <summary> 待打包的元素定义 </summary>
public struct ElementDef
{
/// <summary> 占据的列数 (1-3) </summary>
public int Span;
/// <summary> 元素高度 (px) </summary>
public float Height;
/// <summary> 高度层级(由 Height 自动推断) </summary>
public HeightTier Tier;
/// <summary> 排序权重Section 内排序) </summary>
public int Order;
/// <summary> 稳定排序用的原始插入索引 </summary>
public int InsertionIndex;
/// <summary> 绘制上下文 </summary>
public DrawContext Context;
/// <summary> 对应的绘制器 </summary>
public IPropertyDrawer Drawer;
}
/// <summary> 一行的定义 </summary>
public struct RowDef
{
/// <summary> 行内元素 </summary>
public List<ElementDef> Elements;
/// <summary> 行高 = max(行内所有元素的 Height) </summary>
public float RowHeight;
}
/// <summary>
/// 将元素列表打包为行列表。
/// 约束: 同一行内 Span 总和 &lt;= MaxSpanPerRow且元素必须属于相同 HeightTier。
/// 元素先按 Order 升序 stable sort再按顺序贪心装箱。
/// </summary>
public static List<RowDef> Pack(List<ElementDef> elements)
{
var rows = new List<RowDef>();
if (elements == null || elements.Count == 0)
return rows;
// Stable sort: 先按 Order 升序,同 Order 按 InsertionIndex 升序
elements.Sort((a, b) =>
{
int cmp = a.Order.CompareTo(b.Order);
return cmp != 0 ? cmp : a.InsertionIndex.CompareTo(b.InsertionIndex);
});
var currentRow = new List<ElementDef>();
int currentSpan = 0;
HeightTier currentTier = elements[0].Tier;
float currentMaxHeight = 0f;
int currentRowSpan = -1; // 当前行元素的 Span 值(-1 表示尚未确定)
for (int i = 0; i < elements.Count; i++)
{
var elem = elements[i];
int elemSpan = Mathf.Clamp(elem.Span, 1, MaxSpanPerRow);
bool spanOverflow = currentSpan + elemSpan > MaxSpanPerRow;
bool tierMismatch = currentRow.Count > 0 && elem.Tier != currentTier;
// GridLayoutGroup 强制统一 cellSize同一行内混搭不同 Span 宽度会导致变形
bool spanMismatch = currentRow.Count > 0 && elemSpan != currentRowSpan;
if (spanOverflow || tierMismatch || spanMismatch)
{
// 当前行已满、高度层级不兼容或 Span 不一致,提交当前行
FlushRow(rows, currentRow, currentMaxHeight);
currentRow = new List<ElementDef>();
currentSpan = 0;
currentMaxHeight = 0f;
currentTier = elem.Tier;
currentRowSpan = -1;
}
currentRow.Add(elem);
currentSpan += elemSpan;
currentRowSpan = elemSpan;
if (elem.Height > currentMaxHeight)
currentMaxHeight = elem.Height;
}
// 提交最后一行
if (currentRow.Count > 0)
{
FlushRow(rows, currentRow, currentMaxHeight);
}
return rows;
}
/// <summary> 根据高度值推断 HeightTier </summary>
public static HeightTier InferTier(float height)
{
if (height <= DefaultRowHeight)
return HeightTier.Standard;
if (height <= 280f)
return HeightTier.Tall;
return HeightTier.ExtraTall;
}
/// <summary> 根据类型推断默认 Span </summary>
public static int InferSpan(System.Type fieldType)
{
if (fieldType == null) return 1;
if (fieldType == typeof(bool)) return 1;
if (fieldType == typeof(float) || fieldType == typeof(int) || fieldType == typeof(string)) return 1;
if (fieldType.IsEnum) return 1;
if (fieldType == typeof(Vector2)) return 2;
if (fieldType == typeof(Vector3) || fieldType == typeof(Color)) return 3;
return 1;
}
/// <summary> 根据类型推断默认高度 </summary>
public static float InferHeight(System.Type fieldType, System.Attribute[] attributes = null)
{
if (attributes != null)
{
foreach (var attr in attributes)
{
if (attr is InspectorEmissionColorAttribute)
return 320f;
}
}
if (fieldType == typeof(Color))
return 280f;
return DefaultRowHeight;
}
private static void FlushRow(List<RowDef> rows, List<ElementDef> rowElements, float maxHeight)
{
rows.Add(new RowDef
{
Elements = rowElements,
RowHeight = Mathf.Max(maxHeight, DefaultRowHeight)
});
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4e8a9feff817f194da598f2ef0c357ea

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 25964ec060be79b4f835e09d9383ad7a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 处理 Color 类型的 Drawer。
/// 创建 DynamicUIBaseColorPicker。
/// </summary>
public class BaseColorPickerDrawer : IPropertyDrawer
{
public int DefaultSpan => 3;
public float DefaultHeight => 280f;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("baseColorPicker"), ctx.Parent);
var element = go.GetComponent<DynamicUIBaseColorPicker>();
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
DynamicUIElement.DisableNavigation(
element.inputFieldBaseR,
element.inputFieldBaseG,
element.inputFieldBaseB,
element.inputFieldBaseA);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f148308680f4a4b47a9d1097376ca1aa

View File

@@ -0,0 +1,40 @@
using Lean.Pool;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 按钮 Drawer不绑定数据字段。
/// 创建 DynamicUIButton 并绑定 ButtonAction。
/// </summary>
public class ButtonDrawer : IPropertyDrawer
{
public int DefaultSpan => 1;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("button"), ctx.Parent);
var element = go.GetComponent<DynamicUIButton>();
element.SetText(ctx.Label);
element.Initialize(ctx.Target, ctx.Label, string.Empty);
if (ctx.ButtonAction != null)
{
UnityAction action = () => ctx.ButtonAction();
element.ApplyFunction(action);
}
DynamicUIElement.DisableNavigation(element.button);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 23d17d2a4ce34f84a84f140f213d166c

View File

@@ -0,0 +1,40 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 处理带 [InspectorEmissionColor] 属性的 Color 类型的 Drawer。
/// 创建 DynamicUIEmissionColorPicker支持 Emission 开关和强度控制。
/// </summary>
public class EmissionColorPickerDrawer : IPropertyDrawer
{
public int DefaultSpan => 3;
public float DefaultHeight => 320f;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("emissionColorPicker"), ctx.Parent);
var element = go.GetComponent<DynamicUIEmissionColorPicker>();
element.Initialize(ctx.Target, ctx.Label,
ctx.EmissionEnabledName ?? "NULL",
ctx.FieldName,
ctx.EmissionIntensityName ?? "NULL");
DynamicUIElement.DisableNavigation(
element.inputFieldEmissionR,
element.inputFieldEmissionG,
element.inputFieldEmissionB,
element.inputFieldEmissionI);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 444ef9b226757004f9290d1ea11d2c60

View File

@@ -0,0 +1,49 @@
using System;
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
namespace Ichni.Editor
{
/// <summary>
/// 处理所有 Enum 类型的 Drawer。
/// 通过反射获取字段的枚举类型,创建 DynamicUIEnumDropdown。
/// </summary>
public class EnumDropdownDrawer : IPropertyDrawer
{
public int DefaultSpan => 1;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("enumDropdown"), ctx.Parent);
var element = go.GetComponent<DynamicUIEnumDropdown>();
// 获取枚举类型:优先使用 DrawContext 中的 EnumTypefallback 到反射
Type enumType = ctx.EnumType;
if (enumType == null && ctx.Target != null && !string.IsNullOrEmpty(ctx.FieldName))
{
var field = ctx.Target.GetType().GetField(ctx.FieldName);
if (field != null) enumType = field.FieldType;
else
{
var prop = ctx.Target.GetType().GetProperty(ctx.FieldName);
if (prop != null) enumType = prop.PropertyType;
}
}
if (enumType != null)
{
element.SetUpEnum(enumType);
}
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 13456a684c2325341a1010f04d0dabb5

View File

@@ -0,0 +1,38 @@
using Lean.Pool;
using UnityEngine;
namespace Ichni.Editor
{
/// <summary>
/// 提示文本 Drawer支持静态文本和 Func&lt;string&gt; 动态文本。
/// 创建 DynamicUIHintText。
/// </summary>
public class HintTextDrawer : IPropertyDrawer
{
public int DefaultSpan => 1;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("hintText"), ctx.Parent);
var element = go.GetComponent<DynamicUIHintText>();
element.Initialize(ctx.Target, string.Empty, string.Empty);
if (ctx.DynamicText != null)
{
element.SetUpdatingContent(ctx.DynamicText);
}
else if (ctx.StaticText != null)
{
element.SetContent(ctx.StaticText);
}
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6437a7c7bdd1fe748b37bb0ebda0abd1

View File

@@ -0,0 +1,42 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 处理 float, int, string 类型的 Drawer。
/// 创建 DynamicUIInputField 并绑定反射路径。
/// </summary>
public class InputFieldDrawer : IPropertyDrawer
{
public int DefaultSpan => 1;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("inputField"), ctx.Parent);
var element = go.GetComponent<DynamicUIInputField>();
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
DynamicUIElement.DisableNavigation(element.inputField);
if (ctx.AutoUpdate && element is IHaveAutoUpdate autoUpdate)
{
autoUpdate.SetAutoUpdate(true);
}
if (ctx.ReadOnly)
{
element.inputField.interactable = false;
}
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 00072d6a1502bc74d8b7d116cc648bd1

View File

@@ -0,0 +1,35 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
namespace Ichni.Editor
{
/// <summary>
/// 参数文本 Drawer绑定字段并显示只读文本支持自动更新。
/// 创建 DynamicUIParameterText。
/// </summary>
public class ParameterTextDrawer : IPropertyDrawer
{
public int DefaultSpan => 1;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("parameterText"), ctx.Parent);
var element = go.GetComponent<DynamicUIParameterText>();
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
if (ctx.AutoUpdate)
{
element.SetAutoUpdate(true);
}
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f9545528a874aff49a39469a79e1860c

View File

@@ -0,0 +1,33 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 处理带 [InspectorSlider] 属性的 float/int 类型的 Drawer。
/// 创建 DynamicUISlider 并配置范围参数。
/// </summary>
public class SliderDrawer : IPropertyDrawer
{
public int DefaultSpan => 3;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("slider"), ctx.Parent);
var element = go.GetComponent<DynamicUISlider>();
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName,
ctx.SliderMin, ctx.SliderMax, ctx.SliderWholeNumbers);
DynamicUIElement.DisableNavigation(element.slider);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8740b15baf2fd0147a93e04b4fd41051

View File

@@ -0,0 +1,35 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
namespace Ichni.Editor
{
/// <summary>
/// 处理 List&lt;string&gt; 选择的 Drawer。
/// 创建 DynamicUIStringListDropdown 并配置选项列表。
/// </summary>
public class StringListDropdownDrawer : IPropertyDrawer
{
public int DefaultSpan => 1;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("stringListDropdown"), ctx.Parent);
var element = go.GetComponent<DynamicUIStringListDropdown>();
if (ctx.StringListOptions != null)
{
element.SetUpStringList(ctx.StringListOptions);
}
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c9f1b1ee81941b94cbb465da262713bf

View File

@@ -0,0 +1,32 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 处理 bool 类型的 Drawer。
/// 创建 DynamicUIToggle。
/// </summary>
public class ToggleDrawer : IPropertyDrawer
{
public int DefaultSpan => 1;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("toggle"), ctx.Parent);
var element = go.GetComponent<DynamicUIToggle>();
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
DynamicUIElement.DisableNavigation(element.toggle);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 86988bf7af4e8864094e9fc687e93d29

View File

@@ -0,0 +1,38 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 处理 Vector2 类型的 Drawer。
/// 创建 DynamicUIVector2InputField。
/// </summary>
public class Vector2FieldDrawer : IPropertyDrawer
{
public int DefaultSpan => 2;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("vector2InputField"), ctx.Parent);
var element = go.GetComponent<DynamicUIVector2InputField>();
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
if (ctx.AutoUpdate)
{
element.SetAutoUpdate(true);
}
DynamicUIElement.DisableNavigation(element.inputFieldX, element.inputFieldY);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 77e741bd7c95a8d4690052986898525e

View File

@@ -0,0 +1,38 @@
using Ichni.RhythmGame;
using Lean.Pool;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Editor
{
/// <summary>
/// 处理 Vector3 类型的 Drawer。
/// 创建 DynamicUIVector3InputField。
/// </summary>
public class Vector3FieldDrawer : IPropertyDrawer
{
public int DefaultSpan => 3;
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
public DynamicUIElement Draw(DrawContext ctx)
{
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("vector3InputField"), ctx.Parent);
var element = go.GetComponent<DynamicUIVector3InputField>();
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
if (ctx.AutoUpdate)
{
element.SetAutoUpdate(true);
}
DynamicUIElement.DisableNavigation(element.inputFieldX, element.inputFieldY, element.inputFieldZ);
int span = ctx.OverrideSpan ?? DefaultSpan;
float height = ctx.OverrideHeight ?? DefaultHeight;
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 09da90190e75fd14886f20c784b4bc6f

View File

@@ -74,7 +74,6 @@ namespace Ichni.Editor
float.Parse(inputFieldBaseB.text), float.Parse(inputFieldBaseA.text));
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, newValue));
colorPreview.color = newValue;
connectedBaseElement.Refresh();
// 同步到HSV轮盘
if (hsvDrawer != null)
{

View File

@@ -1,80 +1,123 @@
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Ichni.RhythmGame;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace Ichni.Editor
{
public abstract class DynamicUIElement : MonoBehaviour
{
Inspector Inspector => EditorManager.instance.uiManager.inspector;
public TMP_Text title;
public CanvasGroup canvasGroup;
public IBaseElement connectedBaseElement;
/// <summary>
/// 参数名,通过反射获取饿修改对应变量的值
/// </summary>
public string parameterName;
public virtual void Initialize(IBaseElement baseElement, string title, string parameterName)
{
if (canvasGroup == null) canvasGroup = gameObject.AddComponent<CanvasGroup>();
this.connectedBaseElement = baseElement;
this.parameterName = parameterName;
if (title != string.Empty)
{
this.title.text = title;
}
else
{
this.title.gameObject.SetActive(false);
}
}
public DynamicUIElement Mark(string mark = "Default", IHaveInspection inspection = null)
{
inspection ??= Inspector;
if (mark == "Default")
{
mark = title.text;
}
inspection.MarkedElements.TryAdd(mark, this);
return this;
}
public abstract DynamicUIElement AddListenerFunction(UnityAction action);
}
public interface IHaveAutoUpdate
{
public bool isAutoUpdate { get; set; }
public bool isReceiving { get; set; }
public void SetAutoUpdate(bool enable);
public void UpdateContent()
{
if (isAutoUpdate && isReceiving)
{
ApplyContent();
}
}
public void ApplyContent();
}
public interface IHaveTagLink
{
public IBaseElement connectedBaseElement { get; set; }
// public void GetApply()
// {
// EditorManager.instance.projectInformation.tagManager.SyncTagedElement(connectedBaseElement);
// }
}
}
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Ichni.RhythmGame;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace Ichni.Editor
{
public abstract class DynamicUIElement : MonoBehaviour
{
Inspector Inspector => EditorManager.instance.uiManager.inspector;
public TMP_Text title;
public CanvasGroup canvasGroup;
public IBaseElement connectedBaseElement;
/// <summary>
/// 参数名,通过反射获取饿修改对应变量的值
/// </summary>
public string parameterName;
/// <summary>
/// 缓存的 LayoutElement用于 InspectorBuilder 的 Flexible 布局模式。
/// </summary>
private LayoutElement _layoutElement;
public virtual void Initialize(IBaseElement baseElement, string title, string parameterName)
{
if (canvasGroup == null) canvasGroup = gameObject.AddComponent<CanvasGroup>();
this.connectedBaseElement = baseElement;
this.parameterName = parameterName;
if (title != string.Empty)
{
this.title.text = title;
}
else
{
this.title.gameObject.SetActive(false);
}
}
public DynamicUIElement Mark(string mark = "Default", IHaveInspection inspection = null)
{
inspection ??= Inspector;
if (mark == "Default")
{
mark = title.text;
}
inspection.MarkedElements.TryAdd(mark, this);
return this;
}
/// <summary>
/// 设置此元素在 Flexible 布局中的尺寸。
/// 由 InspectorBuilder 或 Drawer 在元素创建后调用。
/// </summary>
public void SetLayoutSize(float preferredWidth, float preferredHeight)
{
if (_layoutElement == null)
_layoutElement = GetComponent<LayoutElement>();
if (_layoutElement == null)
_layoutElement = gameObject.AddComponent<LayoutElement>();
_layoutElement.preferredWidth = preferredWidth;
_layoutElement.preferredHeight = preferredHeight;
}
/// <summary>
/// 禁用指定 Selectable 组件的导航(统一处理,避免各 Generate 方法重复编写)。
/// </summary>
public static void DisableNavigation(Selectable selectable)
{
if (selectable != null)
selectable.navigation = new Navigation { mode = Navigation.Mode.None };
}
/// <summary>
/// 批量禁用多个 Selectable 的导航。
/// </summary>
public static void DisableNavigation(params Selectable[] selectables)
{
var nav = new Navigation { mode = Navigation.Mode.None };
foreach (var s in selectables)
{
if (s != null)
s.navigation = nav;
}
}
public abstract DynamicUIElement AddListenerFunction(UnityAction action);
}
public interface IHaveAutoUpdate
{
public bool isAutoUpdate { get; set; }
public bool isReceiving { get; set; }
public void SetAutoUpdate(bool enable);
public void UpdateContent()
{
if (isAutoUpdate && isReceiving)
{
ApplyContent();
}
}
public void ApplyContent();
}
public interface IHaveTagLink
{
public IBaseElement connectedBaseElement { get; set; }
// public void GetApply()
// {
// EditorManager.instance.projectInformation.tagManager.SyncTagedElement(connectedBaseElement);
// }
}
}

View File

@@ -97,7 +97,6 @@ namespace Ichni.Editor
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, colorParameterName, emissionColor));
colorPreview.color = emissionColor;
connectedBaseElement.Refresh();
}
public void SliderChange(float value)

View File

@@ -49,7 +49,6 @@ namespace Ichni.Editor
private void ApplyParameters(int value)
{
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
connectedBaseElement.Refresh();
}
public override DynamicUIElement AddListenerFunction(UnityAction action)

View File

@@ -113,7 +113,6 @@ namespace Ichni.Editor
{
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
}
connectedBaseElement.Refresh();
}
public override DynamicUIElement AddListenerFunction(UnityAction action)

View File

@@ -156,7 +156,6 @@ namespace Ichni.Editor
}
Ichni.Editor.Commands.CommandManager.ExecuteCommand(
new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, convertedValue));
connectedBaseElement.Refresh();
}
#endregion

View File

@@ -76,7 +76,6 @@ namespace Ichni.Editor
private void ApplyParameters(string value)
{
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
connectedBaseElement.Refresh();
}
public override DynamicUIElement AddListenerFunction(UnityAction action)

View File

@@ -53,7 +53,6 @@ namespace Ichni.Editor
private void ApplyParameters(bool value)
{
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
connectedBaseElement.Refresh();
}
public override DynamicUIElement AddListenerFunction(UnityAction action)

View File

@@ -116,7 +116,6 @@ namespace Ichni.Editor
{
Vector2 newValue = new Vector2(float.Parse(inputFieldX.text), float.Parse(inputFieldY.text));
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, newValue));
connectedBaseElement.Refresh();
}
public override DynamicUIElement AddListenerFunction(UnityAction action)

View File

@@ -135,7 +135,6 @@ namespace Ichni.Editor
{
Vector3 newValue = new Vector3(float.Parse(inputFieldX.text), float.Parse(inputFieldY.text), float.Parse(inputFieldZ.text));
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, newValue));
connectedBaseElement.Refresh();
}
public override DynamicUIElement AddListenerFunction(UnityAction action)

View File

@@ -1,4 +1,3 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
@@ -9,21 +8,64 @@ namespace Ichni.Editor
{
Inspector Inspector => EditorManager.instance.uiManager.inspector;
public DynamicUIContainer parentContainer;
public GridLayoutGroup gridLayoutGroup;
public RectTransform rect;
public List<DynamicUIElement> dynamicUIElements;
/// <summary> 当前行高 </summary>
[HideInInspector]
public float rowHeight = 100f;
/// <summary>
/// 使用 GridLayoutGroup 实现 N 列自动换行布局。
/// 当添加的元素数量超过 elementCountPerRow 时,会自动换行到下一行。
/// </summary>
public void Initialize(DynamicUIContainer parentContainer, int elementCountPerRow, float height = 100)
{
this.parentContainer = parentContainer;
this.gridLayoutGroup.cellSize = new Vector2(600f / elementCountPerRow, height);
// 支持对象池复用:避免每次 new List 产生 GC直接清空
if (this.dynamicUIElements == null)
this.dynamicUIElements = new List<DynamicUIElement>();
this.rowHeight = height;
int columns = Mathf.Max(1, elementCountPerRow);
// ── 启用 GridLayoutGroup ──
var grid = GetComponent<GridLayoutGroup>();
if (grid == null)
grid = gameObject.AddComponent<GridLayoutGroup>();
grid.enabled = true;
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
grid.constraintCount = columns;
grid.childAlignment = TextAnchor.UpperLeft;
grid.startCorner = GridLayoutGroup.Corner.UpperLeft;
grid.startAxis = GridLayoutGroup.Axis.Horizontal;
grid.padding = new RectOffset(10, 10, 0, 0);
float spacing = 10f;
float totalSpacing = (columns - 1) * spacing;
float cellWidth = (LayoutPacker.TotalRowWidth - totalSpacing) / columns;
grid.cellSize = new Vector2(cellWidth, height);
grid.spacing = new Vector2(spacing, spacing);
// ── LayoutElement: preferredHeight = -1 让 GridLayoutGroup 的 ILayoutElement 接管高度 ──
var layoutElement = GetComponent<LayoutElement>();
if (layoutElement == null)
layoutElement = gameObject.AddComponent<LayoutElement>();
layoutElement.preferredHeight = -1;
layoutElement.flexibleHeight = -1;
// ── ContentSizeFitter: 让子容器高度随网格行数动态增长 ──
var fitter = GetComponent<ContentSizeFitter>();
if (fitter == null)
fitter = gameObject.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
fitter.enabled = true;
if (dynamicUIElements == null)
dynamicUIElements = new List<DynamicUIElement>();
else
this.dynamicUIElements.Clear();
dynamicUIElements.Clear();
}
public DynamicUISubcontainer Mark(string mark, IHaveInspection inspection = null)
{
inspection ??= Inspector;
@@ -31,4 +73,4 @@ namespace Ichni.Editor
return this;
}
}
}
}

View File

@@ -1,98 +0,0 @@
using System;
namespace Ichni.Editor
{
/// <summary>
/// 数值或枚举控制参数标签,挂载在 Field 或 Property 上
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DynamicUIAttribute : Attribute
{
public string Name { get; set; }
/// <summary>
/// 属于哪个 Container (比如 "InGame Settings" 或 "Path Node")
/// </summary>
public string Group { get; set; }
/// <summary>
/// 排版格子要求。一行最大宽度分为 3 份。
/// 填 1 代表需占 1/3。填 3 代表全宽占满整行。
/// 若填 0由内置工厂根据数据类型自动识别例如 Vector3 与 Color 自动为 3bool为1 等)。
/// </summary>
public int Span { get; set; }
/// <summary>
/// 垂直占位高度。填 0 由系统智能推断(如基础件 100ColorPicker 240也可显式填写如 150f 覆盖。
/// </summary>
public float Height { get; set; }
public bool AutoUpdate { get; set; }
public bool ReadOnly { get; set; }
public DynamicUIAttribute(string name = "", string group = "Default", int span = 0, float height = 0f, bool autoUpdate = false, bool readOnly = false)
{
Name = name;
Group = group;
Span = span;
Height = height;
AutoUpdate = autoUpdate;
ReadOnly = readOnly;
}
}
/// <summary>
/// 支持 HDR 以及独立 Emission 强度或开关控制通道的拾色器。
/// 若无独立通道字段可置为 "NULL",将自动转为读取并写入至 Target Color 的 Alpha 通道中。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DynamicUIEmissionColorAttribute : DynamicUIAttribute
{
public string EmissionEnabledName { get; set; }
public string EmissionIntensityName { get; set; }
public DynamicUIEmissionColorAttribute(string name, string enabledName = "NULL", string intensityName = "NULL", string group = "Default", int span = 3, float height = 0f)
: base(name, group, span, height)
{
EmissionEnabledName = enabledName;
EmissionIntensityName = intensityName;
}
}
/// <summary>
/// 滑块控制属性,适用于 float/int 类型字段,自动生成带范围限制的 Slider 替代 InputField。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DynamicUISliderAttribute : DynamicUIAttribute
{
public float Min { get; set; }
public float Max { get; set; }
public bool WholeNumbers { get; set; }
public DynamicUISliderAttribute(string name = "", string group = "Default",
float min = 0f, float max = 1f, bool wholeNumbers = false,
int span = 0, float height = 0f, bool autoUpdate = false, bool readOnly = false)
: base(name, group, span, height, autoUpdate, readOnly)
{
Min = min;
Max = max;
WholeNumbers = wholeNumbers;
}
}
/// <summary>
/// 操作按钮卡片,挂载在没有任何参数的 Method() 方法上
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class DynamicUIButtonAttribute : Attribute
{
public string Name { get; set; }
public string Group { get; set; }
public DynamicUIButtonAttribute(string name = "", string group = "Default")
{
Name = name;
Group = group;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8a540054c95f48544bd6efcea6b452cc

View File

@@ -1,228 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using Ichni.RhythmGame;
using UnityEngine.Events;
namespace Ichni.Editor
{
public static class DynamicUIAutoBuilder
{
// 反射信息快取
private class FieldDef
{
public MemberInfo Member;
public DynamicUIAttribute Attr;
public Type ValueType;
}
private class MethodDef
{
public MethodInfo Method;
public DynamicUIButtonAttribute Attr;
}
/// <summary>
/// 接管任何 IBaseElement 的 UI 生成作业
/// </summary>
public static void AutoBuild(IBaseElement element, IHaveInspection inspector)
{
Type t = element.GetType();
// 扫描所有带 DynamicUI 标记的字段或属性
var dynamicFields = new List<FieldDef>();
foreach (var member in t.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
var uiAttr = member.GetCustomAttribute<DynamicUIAttribute>();
if (uiAttr != null)
{
Type vType = member is FieldInfo f ? f.FieldType : (member as PropertyInfo)?.PropertyType;
if (vType != null) dynamicFields.Add(new FieldDef { Member = member, Attr = uiAttr, ValueType = vType });
}
}
// 扫描所有带 DynamicUIButton 的方法
var dynamicMethods = new List<MethodDef>();
foreach (var method in t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
var btnAttr = method.GetCustomAttribute<DynamicUIButtonAttribute>();
if (btnAttr != null && method.GetParameters().Length == 0)
{
dynamicMethods.Add(new MethodDef { Method = method, Attr = btnAttr });
}
}
// 按照 Group 归类准备流式渲染
var allGroups = dynamicFields.Select(f => f.Attr.Group)
.Concat(dynamicMethods.Select(m => m.Attr.Group))
.Distinct().ToList();
foreach (var groupName in allGroups)
{
var container = inspector.GenerateContainer(groupName);
var fieldsInGroup = dynamicFields.Where(f => f.Attr.Group == groupName).ToList();
var methodsInGroup = dynamicMethods.Where(m => m.Attr.Group == groupName).ToList();
// ======== 智能栅格打包推演 ========
List<FieldDef> currentLineBuffer = new List<FieldDef>();
int currentSpanCount = 0; // 一行容量最高是 3
// 先排变量
foreach (var field in fieldsInGroup)
{
int span = GetEffectiveSpan(field);
span = Mathf.Clamp(span, 1, 3);
// 如果该行的槽被塞满了或者装不下了
if (currentSpanCount > 0 && currentSpanCount + span > 3)
{
FlushLine(element, inspector, container, currentLineBuffer, 3);
currentLineBuffer.Clear();
currentSpanCount = 0;
}
// 特殊兼容:由于 GridLayoutGroup 的强制统一 cellSize, 我们如果在同一行混搭不同 Span 宽度的,反而会让它变形。
// 因此若当前这行的空间要求和前一个需求跨度不一致,也必须强行断点分层!
if (currentLineBuffer.Count > 0)
{
var lastSpan = GetEffectiveSpan(currentLineBuffer[0]);
if (span != lastSpan)
{
FlushLine(element, inspector, container, currentLineBuffer, GetHighestSpanRequirement(currentLineBuffer));
currentLineBuffer.Clear();
currentSpanCount = 0;
}
}
currentLineBuffer.Add(field);
currentSpanCount += span;
}
if (currentLineBuffer.Count > 0) FlushLine(element, inspector, container, currentLineBuffer, GetHighestSpanRequirement(currentLineBuffer));
// ======== 最后把按钮并排排好 ========
if (methodsInGroup.Count > 0)
{
var btnSubcontainer = container.GenerateSubcontainer(3); // 按钮统统放在 1/3 的槽里(你可改规则)
foreach (var method in methodsInGroup)
{
MethodInfo mInfo = method.Method;
// 通过强行抓取生成 UnityAction 给工厂
UnityAction action = () => { mInfo.Invoke(element, null); element.Refresh(); };
string n = string.IsNullOrEmpty(method.Attr.Name) ? mInfo.Name : method.Attr.Name;
inspector.GenerateButton(element, btnSubcontainer, n, action);
}
}
}
}
// 核心工厂发射器
private static void FlushLine(IBaseElement element, IHaveInspection inspector, DynamicUIContainer container, List<FieldDef> fields, int lineSpanCapacity)
{
// 如果一行要放 3 份Span 都是 1 拼成的),那格子数量也是 3600/3=200一个
// 但是如果是独占的Span 都是 3格子数量反而应该是 1600/1=600一个
// 为了防止排版错开变形,我们通过 3 / span 来决定 Subcontainer 内部允许的等分元素数量。
int subcontainerDivision = Mathf.Max(1, Mathf.RoundToInt(3f / lineSpanCapacity));
float rowHeight = GetHighestHeightRequirement(fields);
var sub = container.GenerateSubcontainer(subcontainerDivision, rowHeight);
foreach (var fd in fields)
{
string title = string.IsNullOrEmpty(fd.Attr.Name) ? fd.Member.Name : fd.Attr.Name;
string pName = fd.Member.Name;
if (fd.Attr.ReadOnly)
{
var val = ReflectionHelper.GetDeepValue(element, pName);
inspector.GenerateHintText(element, sub, $"{title}: {val}");
continue;
}
// 智能类型分流
if (fd.Attr is DynamicUIEmissionColorAttribute emissionAttr)
{
// 使用之前为了消除扩展方法调不出来的尴尬而专门暴露的强转/普通调用,我们直接这里也安全使用
IHaveInspection rawInspector = inspector as IHaveInspection;
if(rawInspector != null) rawInspector.GenerateEmissionColorPicker(element, sub, title, emissionAttr.EmissionEnabledName, pName, emissionAttr.EmissionIntensityName);
}
else if (fd.ValueType == typeof(bool))
{
inspector.GenerateToggle(element, sub, title, pName);
}
else if (fd.ValueType == typeof(Vector2))
{
inspector.GenerateVector2InputField(element, sub, title, pName, fd.Attr.AutoUpdate);
}
else if (fd.ValueType == typeof(Vector3))
{
inspector.GenerateVector3InputField(element, sub, title, pName, fd.Attr.AutoUpdate);
}
else if (fd.ValueType == typeof(Color))
{
inspector.GenerateBaseColorPicker(element, sub, title, pName);
}
else if (fd.ValueType.IsEnum)
{
inspector.GenerateDropdown(element, sub, title, fd.ValueType, pName);
}
else if (fd.Attr is DynamicUISliderAttribute sliderAttr)
{
inspector.GenerateSlider(element, sub, title, pName, sliderAttr.Min, sliderAttr.Max, sliderAttr.WholeNumbers);
}
else // 容错给普通的 InputField
{
inspector.GenerateInputField(element, sub, title, pName, fd.Attr.AutoUpdate);
}
}
}
private static int SmartInferSpan(Type t)
{
if (t == typeof(bool)) return 1;
if (t == typeof(float) || t == typeof(int) || t == typeof(string)) return 1;
if (t.IsEnum) return 1;
if (t == typeof(Vector2)) return 2;
if (t == typeof(Vector3) || t == typeof(Color)) return 3; // 巨型组件通篇独占一行
return 1;
}
private static int GetHighestSpanRequirement(List<FieldDef> fields)
{
int highest = 1;
foreach (var f in fields)
{
int span = GetEffectiveSpan(f);
if (span > highest) highest = span;
}
return highest;
}
private static int GetEffectiveSpan(FieldDef fd)
{
return fd.Attr.Span > 0 ? fd.Attr.Span : SmartInferSpan(fd.ValueType);
}
private static float SmartInferHeight(FieldDef fd)
{
// EmissionColorPicker 有 Toggle/RGB滑块/Intensity输入框高度明显大于 BaseColorPicker
if (fd.Attr is DynamicUIEmissionColorAttribute) return 320f;
if (fd.ValueType == typeof(Color)) return 280f;
// 多数基础组件默认为 100
return 100f;
}
private static float GetHighestHeightRequirement(List<FieldDef> fields)
{
float highest = 100f;
foreach (var f in fields)
{
float height = f.Attr.Height > 0 ? f.Attr.Height : SmartInferHeight(f);
if (height > highest) highest = height;
}
return highest;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f0bc252acb576104aaff0846e4c1e136

View File

@@ -13,6 +13,11 @@ using Object = UnityEngine.Object;
namespace Ichni.Editor
{
/// <summary>
/// Inspector 工厂接口 —— 提供底层 UI 控件的实例化方法。
/// 外部代码应通过 <see cref="InspectorBuilder"/> 构建 Inspector
/// 而非直接调用本接口的 Generate 方法。
/// </summary>
public interface IHaveInspection
{
public RectTransform WindowRect { get; set; }
@@ -226,6 +231,7 @@ namespace Ichni.Editor
return hintText;
}
[Obsolete("GenerateParameterText 已无外部调用者,请使用 InspectorBuilder.HintText() 替代。")]
public DynamicUIParameterText GenerateParameterText(IBaseElement baseElement, DynamicUISubcontainer subcontainer,
string title, string parameterName, bool isAutoUpdate = false)
{

View File

@@ -22,6 +22,9 @@ namespace Ichni.Editor
public Dictionary<string, DynamicUISubcontainer> MarkedSubcontainers { get; set; }
public Dictionary<string, DynamicUIElement> MarkedElements { get; set; }
/// <summary> Drawer 注册表单例的便捷访问 </summary>
public DrawerRegistry DrawerRegistry => DrawerRegistry.Instance;
private void Awake()
{
WindowRect = inspectorRect;

View File

@@ -1,59 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni.Editor
{
public static class StandardInspectionElement
{
private static IHaveInspection inspector => EditorManager.instance.uiManager.inspector;
private static Inspector inspectorUI => EditorManager.instance.uiManager.inspector;
public static void GenerateForTransform(GameElement gameElement, DynamicUIContainer generateContainer = null)//关于有Transform
{
if (generateContainer is null)
{
generateContainer = inspector.GenerateContainer("Generate Elements");
}
var animationSubcontainer = generateContainer.GenerateSubcontainer(3);
var displacementButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Displacement", () =>
{
Displacement.GenerateElement("New Displacement", Guid.NewGuid(), new List<string>(), true, gameElement,
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
}); //位移
var swirlButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Swirl", () =>
{
Swirl.GenerateElement("New Swirl", Guid.NewGuid(), new List<string>(), true, gameElement,
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
}); //旋转
var scaleButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Scale", () =>
{
Scale.GenerateElement("New Scale", Guid.NewGuid(), new List<string>(), true, gameElement,
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
}); //缩放
var LookAtButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Look At",
() => LookAt.GenerateElement("New Look At", Guid.NewGuid(),
new List<string>(), true, gameElement, null, new FlexibleBool()));
var displacementTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Displacement Tracker", () =>
{
DisplacementTracker.GenerateElement("New Displacement Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
null, 0f);
});
var swirlTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Swirl Tracker", () =>
{
SwirlTracker.GenerateElement("New Swirl Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
null, 0f);
}); var ScaleTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Scale Tracker", () =>
{
ScaleTracker.GenerateElement("New Scale Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
null, 0f);
});
}
public static void GenerateForLoading()
{
inspectorUI.ClearInspector();
var container = inspector.GenerateContainer("Loading");
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 97779e95563721b4e8d4a28bb0d46cb9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: