diff --git a/Editor/Pattern/UISplineCurveRendererEditor.cs b/Editor/Pattern/UISplineCurveRendererEditor.cs
new file mode 100644
index 0000000..8b4f6de
--- /dev/null
+++ b/Editor/Pattern/UISplineCurveRendererEditor.cs
@@ -0,0 +1,81 @@
+using UnityEditor;
+using UnityEngine;
+
+using XericLibrary.Runtime.UIGraph;
+
+namespace XericLibraryEditor.XericComponent
+{
+ ///
+ /// UISplineCurveRenderer 的 Inspector:在默认字段上面加一条烘焙工具栏。
+ /// 烘焙系数(采样数、触发时机)本来就是公开字段,这里只补"手动烘一次"和状态读数。
+ ///
+ [CustomEditor(typeof(UISplineCurveRenderer))]
+ public class UISplineCurveRendererEditor : Editor
+ {
+ private UISplineCurveRenderer script;
+
+ private void OnEnable()
+ {
+ script = (UISplineCurveRenderer)target;
+ }
+
+ public override void OnInspectorGUI()
+ {
+ DrawBakeBar();
+
+ serializedObject.Update();
+ DrawDefaultInspector();
+ serializedObject.ApplyModifiedProperties();
+ }
+
+ ///
+ /// 烘焙状态条:读数 + 手动烘焙按钮。
+ /// 烘焙结果落在 XericRectSpline 的序列化字段里,所以按完要标脏,
+ /// 否则存不进场景,下次打开还得重烘。
+ ///
+ private void DrawBakeBar()
+ {
+ if (script == null)
+ return;
+
+ var path = script.splinePath;
+ var spline = path != null ? path.xericSpline : null;
+
+ EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+
+ EditorGUILayout.LabelField("弧长烘焙", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField("采样点",
+ spline != null ? spline.BakeCount.ToString() : "未绑定样条");
+ EditorGUILayout.LabelField("曲线长度",
+ spline != null ? spline.SplineLength.ToString("0.###") : "-");
+ EditorGUILayout.LabelField("已烘焙次数", script.BakeRunCount.ToString());
+ EditorGUILayout.LabelField("校正状态",
+ script.progressMapping == SplineProgressMapping.BakedDistance
+ ? (spline != null && spline.BakeCount >= script.bakeSubdivide
+ ? "按烘焙距离校正"
+ : "烘焙数据不够,暂时退回参数映射")
+ : "未启用(直接按参数)");
+
+ using (new EditorGUI.DisabledScope(spline == null))
+ {
+ if (GUILayout.Button("烘焙一次", GUILayout.Height(24)))
+ {
+ // XericRectSpline 是普通 [Serializable] 类,不是 Object,
+ // 能登记撤销/标脏的只有承载它的那个路径组件
+ Undo.RecordObject(path, "烘焙样条");
+ script.BakeNow();
+ EditorUtility.SetDirty(path);
+ EditorUtility.SetDirty(script);
+ SceneView.RepaintAll();
+ }
+ }
+
+ if (script.bakeTrigger == SplineBakeTrigger.Manual)
+ EditorGUILayout.HelpBox(
+ "触发时机是 Manual:几何怎么改都不会自动烘,进度贴不贴得准就看上面这个按钮按没按。",
+ MessageType.Info);
+
+ EditorGUILayout.EndVertical();
+ }
+ }
+}
diff --git a/Editor/Pattern/UISplineCurveRendererEditor.cs.meta b/Editor/Pattern/UISplineCurveRendererEditor.cs.meta
new file mode 100644
index 0000000..2f35c29
--- /dev/null
+++ b/Editor/Pattern/UISplineCurveRendererEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4cb828489da70e091cbdc53afe9881ec
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Spline.meta b/Editor/Spline.meta
new file mode 100644
index 0000000..6486cd5
--- /dev/null
+++ b/Editor/Spline.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3dd3579dda7928297735f22b46528170
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Spline/SplinePathCreateMenuItems.cs b/Editor/Spline/SplinePathCreateMenuItems.cs
new file mode 100644
index 0000000..0160386
--- /dev/null
+++ b/Editor/Spline/SplinePathCreateMenuItems.cs
@@ -0,0 +1,212 @@
+using UnityEditor;
+using UnityEngine;
+
+using XericLibrary.Runtime;
+using XericLibrary.Runtime.UIGraph;
+using XericLibrary.Runtime.XericComponent;
+
+namespace XericLibraryEditor.Spline
+{
+ ///
+ /// 样条曲线路径的 Hierarchy 右键创建菜单。
+ /// 此文件直接放在 Release/Editor/ 下由 Unity 编译,
+ /// 不经过外部 DLL 管线,确保菜单立即生效。
+ ///
+ public static class SplinePathCreateMenuItems
+ {
+ private const string MenuRoot = "GameObject/Xeric Library/Spline/";
+
+ ///
+ /// 创建带 XericRectSplinePath 的 UI 节点(二维样条)
+ ///
+ [MenuItem(MenuRoot + "2D Spline", false, 20)]
+ private static void CreateRectSplinePath(MenuCommand menuCommand)
+ {
+ var go = CreateUiGameObject("2D Spline", new Vector2(300f, 150f));
+ var path = go.AddComponent();
+
+ // 给两个默认控制点,创建后场景里立刻有点可选中
+ path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.25f, 0.5f)));
+ path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.75f, 0.5f)));
+
+ GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
+ Finish(go, "Create 2D Spline");
+ }
+
+ ///
+ /// 创建带 XericSplinePath 的场景节点(三维样条)
+ ///
+ [MenuItem(MenuRoot + "3D Spline", false, 21)]
+ private static void CreateSplinePath(MenuCommand menuCommand)
+ {
+ var go = new GameObject("3D Spline");
+ var path = go.AddComponent();
+
+ path.xericSpline.Add(new XericSpline.SplinePoint(Vector3.left));
+ path.xericSpline.Add(new XericSpline.SplinePoint(Vector3.right));
+
+ GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
+ Finish(go, "Create 3D Spline");
+ }
+
+ ///
+ /// 创建由二维样条驱动的 UI 曲线渲染节点,并自动绑定同物体上的样条路径
+ ///
+ [MenuItem(MenuRoot + "Spline Curve Renderer", false, 22)]
+ private static void CreateSplineCurveRenderer(MenuCommand menuCommand)
+ {
+ var go = CreateUiGameObject("Spline Curve Renderer", new Vector2(300f, 150f));
+ var path = go.AddComponent();
+
+ path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.1f, 0.5f)));
+ path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.9f, 0.5f)));
+
+ var renderer = go.AddComponent();
+ renderer.splinePath = path;
+
+ GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
+ Finish(go, "Create Spline Curve Renderer");
+ }
+
+ ///
+ /// 建一条能直接跑起来的曲线进度条:底轨铺满 + 填充到 60%,两条面片共用同一条样条
+ ///
+ [MenuItem(MenuRoot + "Progress Bar Demo", false, 23)]
+ private static void CreateProgressBarDemo(MenuCommand menuCommand)
+ {
+ var go = CreateUiGameObject("Spline Progress Bar", new Vector2(520f, 220f));
+ var path = go.AddComponent();
+
+ // S 形:弧长校正在这条形状上才看得出差别(匀速直线段上校正前后一模一样)
+ AddDemoPoint(path, new Vector2(0.08f, 0.3f), new Vector2(0.28f, 0f));
+ AddDemoPoint(path, new Vector2(0.5f, 0.78f), new Vector2(0.3f, 0f));
+ AddDemoPoint(path, new Vector2(0.92f, 0.22f), new Vector2(0.26f, 0f));
+
+ var renderer = go.AddComponent();
+ renderer.splinePath = path;
+ // 面片0=底轨铺满并压暗,面片1=填充到 60% 保持全亮
+ renderer.headTailPairs = new[] { 1f, 0f, 0.6f, 0f };
+ renderer.ribbonTints = new[]
+ {
+ new Color32(90, 90, 90, 255),
+ new Color32(255, 255, 255, 255),
+ };
+ renderer.curveWidth = 14f;
+ renderer.fillRibbonIndex = 1;
+ go.AddComponent();
+ ApplyProgressMaterial(renderer);
+
+ PlaceUnderCanvas(go, menuCommand);
+ Finish(go, "Create Spline Progress Bar");
+ }
+
+ ///
+ /// 换上真正消费 uv0.y 的材质,并配一张沿路径方向的手风琴条纹,这样一进来就能看见流动。
+ /// 纹理是运行时生成的(不落盘、不进版本库),要长期用请自己把图拖到 progressTexture 上。
+ ///
+ private static void ApplyProgressMaterial(UISplineCurveRenderer renderer)
+ {
+ var shader = Shader.Find("XericLibrary/UIGraph/XericUICurveProgress");
+ if (shader == null || !shader.isSupported)
+ {
+ // 必须写全:本文件在 XericLibraryEditor.Spline 里,裸 Debug 会被
+ // 外层命名空间 XericLibraryEditor.Debug 抢走
+ UnityEngine.Debug.LogWarning("[Spline Progress] 找不到 XericUICurveProgress 着色器,演示沿用默认材质(默认材质不读 uv0.y,也没有采贴图)");
+ return;
+ }
+
+ var texture = new Texture2D(4, 128, TextureFormat.RGBA32, false);
+ texture.wrapMode = TextureWrapMode.Repeat;
+ for (int y = 0; y < texture.height; y++)
+ {
+ // 沿 uv.y(也就是路径进度)方向的亮带 + 暗谷
+ float wave = 0.5f + 0.5f * Mathf.Sin(y * Mathf.PI * 2f / texture.height * 3f);
+ var color = new Color(1f, 1f, 1f, Mathf.Lerp(0.25f, 1f, wave));
+ for (int x = 0; x < texture.width; x++)
+ texture.SetPixel(x, y, color);
+ }
+ texture.Apply();
+
+ var material = new Material(shader);
+ material.SetFloat("_Tile", 5f);
+ material.SetFloat("_Scroll", 0.45f);
+ material.SetFloat("_EdgeExp", 0.85f);
+ material.SetFloat("_EdgeStepRange", 0.12f);
+
+ renderer.progressTexture = texture;
+ renderer.material = material;
+ }
+
+ // 工具方法
+
+ /// 默认手柄偏移(锚点空间):手柄全零时贝塞尔段退化成直线,看不出这是一条曲线
+ private const float DefaultHandleOffset = 0.1f;
+
+ ///
+ /// 造一个带默认手柄的端点。与 2D 编辑器里插入端点用的是同一套默认值,
+ /// 两边各自实现(编辑器在 DLL 里,这里要当场编译,不能等它重新构建)。
+ ///
+ private static XericRectSpline.RectSplinePoint MakeDefaultPoint(Vector2 anchor)
+ {
+ var point = new XericRectSpline.RectSplinePoint(anchor);
+ // Local 是相对锚点的偏移;StartHandle/EndHandle 是叠在锚点上的绝对值
+ point.StartHandleLocal = new Vector2(-DefaultHandleOffset, 0f);
+ point.EndHandleLocal = new Vector2(DefaultHandleOffset, 0f);
+ return point;
+ }
+
+ private static void AddDemoPoint(XericRectSplinePath path, Vector2 anchor, Vector2 handleOffset)
+ {
+ var point = new XericRectSpline.RectSplinePoint(anchor);
+ // Start/EndHandle 是"叠在锚点上的"绝对锚点值,不是偏移量
+ point.EndHandle = anchor + handleOffset;
+ point.StartHandle = anchor - handleOffset;
+ path.xericSpline.Add(point);
+ }
+
+ ///
+ /// Graphic 不在 Canvas 下就画不出来,演示物必须落到某个 Canvas 里;
+ /// 场景里没有就现建一个(与 BlueprintMenuItems 同一套路)
+ ///
+ private static void PlaceUnderCanvas(GameObject go, MenuCommand menuCommand)
+ {
+ var parent = menuCommand.context as GameObject;
+ if (parent != null && parent.GetComponentInParent